自从搭建起来博客之后,虽然博客用 markdown 格式来写就可以了,但是图片插入却是一个大问题,写了几篇 blog 里面的图片都是直接引用别人的,前几天还好好的,这几天图片链接却 403 了,估计是做了防盗链之类的。

自己手动 copy 的话在各个平台之间也挺麻烦,比如有道云笔记和博客之间都需要手动修改,国内的云存储厂商用着也不太放心。

本来这周还计划做点别的事情,看来只能先把图床搭建起来。

服务器是 serverless 的,所以通过 serverless Framework 和云函数来进行 cos 对象存储,上传到存储桶中,这里用到的技术是基于 node 的 koa2+vue,通过 koa 将图片对象上传到 cos 桶中

先来看一下做成的成品:serverless 图床
image

支持两种方式上传图片,通过本地上传和网络链接,右侧做了一个简单的列表展示和删除功能,别的一些功能比如图片压缩之类的还没做,之后可以慢慢优化。

这里假设域名,备案资源包,serverless 本地安装等等都已经好了

创建 koa2 应用

本地初始化项目
1
2
3
4
5
6
7
8
9
10
11
## 全局安装koa-generator
npm install -g koa-generator
## 创建项目
koa2 项目名称
默认为jade模板,使用ejs的话
koa2 -e 项目名称
## 安装依赖
npm install
## 启动
npm start
打开localhost:3000

通过以上命令初始化好项目,访问 localhost:3000 就可以看到默认的首页了,接下来需要频繁修改代码,每次修改都需要重启,所以通过 nodemon 来解决

1
npm i nodemon

之后修改 package.json 中的 scripts,把 start 中原来 node 启动的改为 nodemon,在后再修改代码就可以自动更新了

1
2
3
4
5
6
"scripts": {
"start": "nodemon bin/www",
"dev": "./node_modules/.bin/nodemon bin/www",
"prd": "pm2 start bin/www",
"test": "echo \"Error: no test specified\" && exit 1"
},

在根目录下新建 config 文件夹,创建 index.js 文件,来存放我们的一些配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14

const config = {
// SecretId和SecretKey生产环境请从环境变量中获取
// SecretId: process.env.SecretId,
// SecretKey: process.env.SecretKey,
SecretId: 'xxxxx',
SecretKey: 'xxxx'
// 存储桶
Bucket: 'xxxx',
// 地区
Region: 'xxxx'
}
module.exports = config

这里的配置是你自己腾讯云的 SecretId,SecretKey,以及创建好的存储桶,登录对象存储控制台创建存储桶后,获取存储桶名称和地域名称,这里在部署完成之后会使用环境变量来代替明文 SecretId 和 SecretKey,本地开发可以用明文

之后改造一下 router 下的路由文件和 views,views 创建一个 html 用来做前端展示,同时在根目录创建一个 controllers 文件夹,创建一个 upload.js 用来做控制器,这里通过 koa-sendfile 来渲染前端的界面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
router-index.js
const Router = require('koa-router');
const router = new Router();
const sendFile = require('koa-sendfile')
const path = require('path')
const { upload,list,deleteCos} = require('../controllers/upload')

//返回页面上传静态资源页面
router.get('/', async (ctx, next) => {
await sendFile(ctx, path.join(__dirname, '../views/index.html'))
})

router.get("/list",list);
router.get('/delete',deleteCos)
router.post("/upload",upload);
module.exports = router;

之后在 controllers 下的 upload.js 新建一个 Upload 类,写好控制器方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const {SecretId,SecretKey,Bucket,Region} = require('../config');
class Upload {
// 获取图片列表
async list(ctx) {

}
// 上传接口
async upload(ctx) {

}
// 删除单个图片
async deleteCos(ctx) {}
}
module.exports = new Upload()

总体思路为通过前端客户端可直接使用 multipart/form-data 方式上传文件给 koa,koa 再把获取到的二进制文件转为 Buffer 丢到 cos 对象存储中
image

为了能顺利接收前端的文件,还需要安装 koa-body,使用 koa-body 替换 koa-bodyparser,因为 koa-bodyparser 不支持获取文件

1
npm i koa-body

之后修改 app.js

1
2
3
4
5
6
const KoaBody = require('koa-body')

app.use(KoaBody({multipart:true, formidable:{
uploadDir:path.resolve(__dirname,'./public/uploads'), // 设置上传目录
keepExtensions: true // 保留拓展名
}}))
cos 对象存储

做好以上工作之后,开始了解一下 cos 对象存储,官方是这样定义的:
对象存储(Cloud Object Storage,COS)是腾讯云提供的一种存储海量文件的分布式存储服务.

我们是后端对接 SCF 的 API,这里文件上传可以通过直接上传到 cos,也可以通过 api 网关经过云函数处理再存储到 cos 中,因为有可能图片会比较大,我们这里直接采用上传到 cos 中

腾讯云 api 网关文件上传文档

cos 对象存储 Nodejs SDK 文档

首先安装 SDK

1
2
npm i cos-nodejs-sdk-v5 --save

这里可以采用永久秘钥和临时秘钥两种方式来实例化 cos,我们这里采用第一种永久秘钥初始化

腾讯云秘钥管理

获取到秘钥之后在 controllers-upload.js 中

1
2
3
4
5
6
7
8
const {SecretId,SecretKey,Bucket,Region} = require('../config');
const COS = require('cos-nodejs-sdk-v5');

let cos = new COS({
SecretId: SecretId,
SecretKey: SecretKey
})

现在来实现图片上传逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
const request = require('request');
const xxx = require(xxxx)

async upload(ctx) {
// 获取上传的图片,imgSrc参数存在为网络连接
let file,postfix,tmpPath
if (ctx.request.body.imgSrc) {
postfix='jpg'
const requestPromise = new Promise((resolve,reject) => {
// 云函数写入只能写入到tmp的临时文件夹中
if (process.env.SERVERLESS == 1) {
tmpPath = '/tmp/image.jpg'
} else {
tmpPath = '../public/uploads/image.jpg'
}
request(ctx.request.body.imgSrc).pipe(fs.createWriteStream(path.resolve(__dirname, tmpPath)).on('finish',() => {
resolve(true)
}))
})
await requestPromise
file = fs.readFileSync(path.resolve(__dirname, tmpPath))
} else {
file = fs.readFileSync(ctx.request.files.file.path)
postfix = ctx.request.files.file.type.split('/')[1]
}
}

// 将图片转为base64在转为Buffer流
const dataBuffer = new Buffer.from(file, 'base64');
const requestPromise = new Promise((resolve,reject) => {
cos.putObject({
Bucket, // 存储桶 必须
Region, // 地区识别 必须
Key: `${randomWord(false,32)}.${postfix}`, // 文件名字(这里需要注意的是,如果名字是一样的,那么后面的会覆盖前面的,所以这个名字不可以重复)(必须)
Body: dataBuffer, // 上传文件对象(可以不写)支持 Buffer|Stream|String
}, (err, data) =>{
if (err) {
reject(err)
} else {
resolve(data)
}
});
})
// 之后接口返回response
try {
const result = await requestPromise;
ctx.body ={
code:0,
message:'success',
error:null,
data:{
url:'https://'+result.Location
}
}
} catch(err) {
ctx.body ={
code:1,
message:'fail',
error:err,
data:{
url:''
}
}
}
// 上传完成清空上传的文件夹 本地测试
if (process.env.SERVERLESS != 1) {
let tmpUploadPath = '../public/uploads'
const images = fs.readdirSync(path.resolve(__dirname,tmpUploadPath));
images.forEach((v) => {
let imagePath = path.resolve(__dirname,`${tmpUploadPath}/${v}`)
fs.unlinkSync(imagePath);
})
}

删除和获取的逻辑类似,就不在这里复制粘贴代码了

然后在根目录配置 serverless.yml 文件

serverless.yml 文件配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
org: '1302216408' # 组织名称。留空则则使用默认值为用户 APPID
app: img-bed # 应用名称。留空则默认取当前组件的实例名称为app名称。
stage: dev # 环境名称。默认值是 dev。建议使用 ${env.STAGE} 变量定义环境名称
component: koa (必选) 组件名称,在该实例中为 scf
name: img-bed (必选) 组件实例名称。
inputs: #组件参数配置,根据每个组件,实现具体的资源信息配置
entryFile: app.js #以您实际入口文件名为准
src:
src: ./
exclude:
- .env
region: ap-guangzhou
runtime: Nodejs10.15
apigatewayConf:
protocols:
- https
environment: release
serviceTimeout: 60
customDomains:
- domain: upload.niuhk.cn
certificateId: j6OBq7dR
isDefaultMapping: false
pathMappingSet:
- path: /
environment: release
protocols:
- https
autoCreateDns: false
isAutoCiDeploy: false
functionConf:
eip: false
timeout: 30
memorySize: 128

完整代码见 github

之后执行 serverless deploy 按照提示部署。

部署完成之后会增加一个 serverless Framework 应用和一个云函数,在云函数中的基础配置中配置好环境变量,同时,在云函数的触发管理中,把对应的 API 网关 Base64 编码开启,才能正常上传二进制文件
image