引言
前端时间写了yuque-hexo 插件语雀图片防盗链的解决方案,当时是用的腾讯云图床,后面考虑到可以支持更多的图床选择。这次的改造新增了阿里云图床和七牛云图床。
阿里云图床
阿里云图床目前各大公司也都在用,技术成熟稳定,但也和腾讯云图床一样,是收费的。但是作为个人博客图床的话,腾讯云 COS 和阿里云 OSS 的费用都相当的便宜,一个月的费用大概都在几分钱到几毛钱的范围。
七牛云图床
七牛云图床为个人提供 10G 的免费存储空间和完全够用的免费读写流量,用来作为博客图床再合适不过了。缺点就是七牛云图床默认使用 CDN 域名进行外链访问,而且是 30 天的临时域名,所以建议绑定一个备案域名作为永久 CND 域名进行访问。
改造思路
由于各大图床的 API 使用方式不尽相同,所以需要抽离出一个适配层进行接口调用的统一,通过不同的配置获取不同的图床实例进行 API 操作。
具体实现
目录结构
|--imageBeds
| |--index //接口统一适配层
| |--cos.js //腾讯云图床API操作层
| |--oss.js //阿里云图床API操作层
| |--qiniu.js //七牛云图床API操作层
代码实现
接口统一适配层
// imageBeds/index.js
// 接口统一适配层
"use strict";
// 引入图床实例
const CosClient = require("./cos");
const OssClient = require("./oss");
const QiniuClient = require("./qiniu");
const out = require("../../lib/out");
// 目前已适配图床列表
const imageBedList = ["qiniu", "cos", "oss"];
class ImageBeds {
constructor(config) {
// 实例化时先赋值保存config配置
this.config = config;
// 获取图床实例
this.imageBedInstance = this.getImageBedInstance(config.imageBed);
}
// 单例模式
static getInstance(config) {
if (!this.instance) {
this.instance = new ImageBeds(config);
}
return this.instance;
}
/**
* 获取图床对象的实例
*
* @param {string} imageBed 图床类型: cos | oss
* @return {any} 图床实例
*/
getImageBedInstance(imageBed) {
if (!imageBedList.includes(imageBed)) {
out.error(`imageBed配置错误,目前只支持${imageBedList.toString()}`);
process.exit(-1);
}
switch (imageBed) {
case "cos":
return CosClient.getInstance(this.config);
case "oss":
return OssClient.getInstance(this.config);
case "qiniu":
return QiniuClient.getInstance(this.config);
default:
return QiniuClient.getInstance(this.config);
}
}
/**
* 检查图床是否已经存在图片,存在则返回url
*
* @param {string} fileName 文件名
* @return {Promise<string>} 图片url
*/
async hasImage(fileName) {
return await this.imageBedInstance.hasImage(fileName);
}
/**
* 上传图片到图床
*
* @param {Buffer} imgBuffer 文件buffer
* @param {string} fileName 文件名
* @return {Promise<string>} 图床的图片url
*/
async uploadImg(imgBuffer, fileName) {
return await this.imageBedInstance.uploadImg(imgBuffer, fileName);
}
}
module.exports = ImageBeds;
腾讯云图床 API 操作层
// imageBeds/cos.js
// 腾讯云图床API操作层
// 开发文档: https://cloud.tencent.com/document/product/436/8629
"use strict";
// 腾讯云图床
const COS = require("cos-nodejs-sdk-v5");
const out = require("../../lib/out");
const secretId = process.env.SECRET_ID;
const secretKey = process.env.SECRET_KEY;
class CosClient {
constructor(config) {
this.config = config;
// 实例化腾讯云COS
this.imageBedInstance = new COS({
SecretId: secretId, // 身份识别ID
SecretKey: secretKey, // 身份秘钥
});
}
// 单例模式
static getInstance(config) {
if (!this.instance) {
this.instance = new CosClient(config);
}
return this.instance;
}
/**
* 检查图床是否已经存在图片,存在则返回url,不存在返回空
*
* @param {string} fileName 文件名
* @return {Promise<string>} 图片url
*/
async hasImage(fileName) {
try {
await this.imageBedInstance.headObject({
Bucket: this.config.bucket, // 存储桶名字(必须)
Region: this.config.region, // 存储桶所在地域,必须字段
Key: `${this.config.prefixKey}/${fileName}`, // 文件名 必须
});
return `https://${this.config.bucket}.cos.${this.config.region}.myqcloud.com/${this.config.prefixKey}/${fileName}`;
} catch (e) {
return "";
}
}
/**
* 上传图片到图床
*
* @param {Buffer} imgBuffer 文件buffer
* @param {string} fileName 文件名
* @return {Promise<string>} 图床的图片url
*/
async uploadImg(imgBuffer, fileName) {
try {
const res = await this.imageBedInstance.putObject({
Bucket: this.config.bucket, // 存储桶名字(必须)
Region: this.config.region, // 存储桶所在地域,必须字段
Key: `${this.config.prefixKey}/${fileName}`, // 文件名 必须
StorageClass: "STANDARD", // 上传模式(标准模式)
Body: imgBuffer, // 上传文件对象
});
return `https://${res.Location}`;
} catch (e) {
out.error(`上传图片失败,请检查: ${e}`);
process.exit(-1);
}
}
}
module.exports = CosClient;
阿里云图床 API 操作层
// imageBeds/oss.js
// 阿里云图床API操作层
// 开发文档: https://help.aliyun.com/document_detail/32067.html
"use strict";
// 阿里云图床
const OSS = require("ali-oss");
const out = require("../../lib/out");
const secretId = process.env.SECRET_ID;
const secretKey = process.env.SECRET_KEY;
class OssClient {
constructor(config) {
this.config = config;
this.imageBedInstance = new OSS({
bucket: config.bucket,
// yourRegion填写Bucket所在地域。以华东1(杭州)为例,Region填写为oss-cn-hangzhou。
region: config.region,
// 阿里云账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM用户进行API访问或日常运维,请登录RAM控制台创建RAM用户。
accessKeyId: secretId,
accessKeySecret: secretKey,
});
}
// 单例模式
static getInstance(config) {
if (!this.instance) {
this.instance = new OssClient(config);
}
return this.instance;
}
/**
* 检查图床是否已经存在图片,存在则返回url,不存在返回空
*
* @param {string} fileName 文件名
* @return {Promise<string>} 图片url
*/
async hasImage(fileName) {
try {
await this.imageBedInstance.head(`${this.config.prefixKey}/${fileName}`);
return `https://${this.config.bucket}.${this.config.region}.aliyuncs.com/${this.config.prefixKey}/${fileName}`;
} catch (e) {
return "";
}
}
/**
* 上传图片到图床
*
* @param {Buffer} imgBuffer 文件buffer
* @param {string} fileName 文件名
* @return {Promise<string>} 图床的图片url
*/
async uploadImg(imgBuffer, fileName) {
try {
const res = await this.imageBedInstance.put(
`${this.config.prefixKey}/${fileName}`,
imgBuffer
);
return res.url;
} catch (e) {
out.error(`上传图片失败,请检查: ${e}`);
process.exit(-1);
}
}
}
module.exports = OssClient;
七牛云图床 API 操作层
// imageBeds/qiniu.js
// 七牛云图床API操作层
// 七牛云的上传程序相对比较复杂,详情请查看sdk文档https://developer.qiniu.com/kodo/1289/nodejs
"use strict";
// 七牛云图床
const qiniu = require("qiniu");
const out = require("../../lib/out");
const secretId = process.env.SECRET_ID;
const secretKey = process.env.SECRET_KEY;
class QiniuClient {
constructor(config) {
this.config = config;
this.init();
}
init() {
if (!this.config.host) {
out.error("使用七牛云时,需要在imgCdn中指定域名host");
process.exit(-1);
}
const mac = new qiniu.auth.digest.Mac(secretId, secretKey);
// 配置
const putPolicy = new qiniu.rs.PutPolicy({ scope: this.config.bucket });
// 获取上传凭证
this.uploadToken = putPolicy.uploadToken(mac);
const config = new qiniu.conf.Config();
// 空间对应的机房
config.zone = qiniu.zone[this.config.region];
this.formUploader = new qiniu.form_up.FormUploader(config);
this.bucketManager = new qiniu.rs.BucketManager(mac, config);
this.putExtra = new qiniu.form_up.PutExtra();
}
static getInstance(config) {
if (!this.instance) {
this.instance = new QiniuClient(config);
}
return this.instance;
}
/**
* 检查图床是否已经存在图片,存在则返回url,不存在返回空
*
* @param {string} fileName 文件名
* @return {Promise<string>} 图片url
*/
async hasImage(fileName) {
return await new Promise((resolve) => {
this.bucketManager.stat(
this.config.bucket,
`${this.config.prefixKey}/${fileName}`,
(err, respBody, respInfo) => {
if (err) {
out.error(`上传图片失败,请检查: ${err}`);
process.exit(-1);
} else {
if (respInfo.statusCode === 200) {
resolve(
`${this.config.host}/${this.config.prefixKey}/${fileName}`
);
} else {
resolve("");
}
}
}
);
});
}
/**
* 上传图片到图床
*
* @param {Buffer} imgBuffer 文件buffer
* @param {string} fileName 文件名
* @return {Promise<string>} 图床的图片url
*/
async uploadImg(imgBuffer, fileName) {
return await new Promise((resolve) => {
this.formUploader.put(
this.uploadToken,
`${this.config.prefixKey}/${fileName}`,
imgBuffer,
this.putExtra,
(respErr, respBody, respInfo) => {
if (respErr) {
out.error(`上传图片失败,请检查: ${respErr}`);
process.exit(-1);
}
if (respInfo.statusCode === 200) {
resolve(`${this.config.host}/${this.config.prefixKey}/${fileName}`);
} else {
out.error(`上传图片失败,请检查: ${respInfo}`);
process.exit(-1);
}
}
);
});
}
}
module.exports = QiniuClient;
大功告成!
使用时,只需要在上层传入 config 配置,获取接口适配层实例,并替换原有的上传图片接口即可。
更多源代码详情,请查看yuqe-hexo-with-cdn