使用 Multer 上传图片到阿里云 OSS的两种方式
文件上传到哪里更好?
- 上传到服务器本地
上传到服务器本地,这种方法在现今商业项目中,几乎已经见不到了。因为服务器带宽,磁盘 IO
都是非常有限的。将文件上传和读取放在自己服务器上,并不是明智的选择。
- 上传到云储存
上传到云存储,则无需担心带宽和磁盘问题,而且配置 CDN
也很简单。所以明智的选择,要用云存储,这里我们以阿里云的对象存储为例来学习如何实现上传。
阿里云对象存储阿里云oss
上传的两种方式
我们需要开发,专门用于阿里云上传的接口。开发上传接口,也有两种方案,分别是服务端代理上传和客户端直传。这两种方式在开发、使用上各有优劣。我们简单的做个对比:
服务端代理上传
服务端代理上传。使用这种方式,一张图片,先要上传到 Node 项目的服务器中,然后再由 Node 服务器上传到阿里云 OSS。
这样这张图片,要上传两次,会造成网络资源的浪费,增加服务器的开销。尤其是在访问量大的情况下,会对项目的稳定运行,造成很大的影响。
但这种方式也有优点,就是开发简单、前端使用非常方便。而且后端可以很方便的做记录,可以开发一个专门用来,管理用户附件的功能。
1、获取秘钥
使用代码来访问阿里云,需要两个用来认证的参数。点击阿里云网站右上角用户头像里的AccessKey管理
从这里创建自己的阿里云的AccessKey
。页面还会弹出使用 RAM 用户 AccessKey
。
根据阿里云的提示,我们就选择使用 RAM 用户 AccessKey
然后通过验证
创建完成后,还需要对当前用户进行授权
。勾选后,点击添加权限
关闭小窗口,回来看用户信息。这里还有两个非常关键的AccessKey ID
和AccessKey Secret
。先不要关闭页面,马上就要用到它们。
记得保存好: AccessKey Secret
后续无法查看
对当前项
进行配置
使其可以自由读
无需签名验证
2、配置环境变量
到这里为止,我们开发上传接口,所需要的东西已经全部拿到了。打开咱们开发的 Node.js
项目,找到.env
文件,增加点配置。将自己的AccessKey ID
和AccessKey Secret
值复制进来。
后面的ALIYUN_BUCKET
和ALIYUN_REGION
,可以在概览中找到,我这里分别是:wlyxw-oss
和oss-cn-chengdu
。大家复制的时候,注意下,只要前面这一部分,后面的完整域名不需要。
.env
NODE_ENV=development
PORT=3000
SECRET=ALIYUN_ACCESS_KEY_ID=AccessKey
ALIYUN_ACCESS_KEY_SECRET=AccessKey Secret
ALIYUN_BUCKET=wlyxw-oss
ALIYUN_REGION=oss-cn-chengdu
如果项目是启动状态
,改完环境变量
了,记得一定要重启服务。
3、 安装依赖包
npm i ali-oss multer multer-aliyun-oss
- ali-oss:是用来操作阿里云 OSS 的 SDK
- multer:是专门用于上传文件的 node.js 中间件
- multer-aliyun-oss,则是用来配合 multer,将文件上传到阿里云 OSS 的
4、实现上传代码
在/routes
目录中新建一个路由文件,就叫做uploads.js
。
uploads.js
const express = require('express');
const router = express.Router();
const { success, failure } = require('../utils/responses');/*** 阿里云 OSS 客户端上传* POST /uploads/aliyun*/
router.post('/aliyun', function (req, res) {try {} catch (error) {failure(res, error);}
})module.exports = router;
接着查看 multer-aliyun-oss的文档。可以看到这里的代码还是比较简单的,上面需要先做一个配置,然后调用方法就可以上传了。
但这里缺少对上传文件的验证,我们继续看 multer的官方文档。看到这里可以通过参数限制文件大小和文件类型。在它们的基础上,我们做一个整合,就得到了这样一个配置文件。
因为这些配置,内容比较多,而且将来会在多个不同的路由文件中使用。考虑到代码的干净和复用,就不要将它们直接放在路由文件里了。可以在
utils
里,新建一个aliyun.js
文件,将它们直接粘贴进去。
aliyun.js
const multer = require('multer');
const MAO = require('multer-aliyun-oss');
const OSS = require("ali-oss");
const {BadRequest} = require('http-errors')// 阿里云配置信息
const config = {region: process.env.ALIYUN_REGION,accessKeyId: process.env.ALIYUN_ACCESS_KEY_ID,accessKeySecret: process.env.ALIYUN_ACCESS_KEY_SECRET,bucket: process.env.ALIYUN_BUCKET,
};const client = new OSS(config);// multer 配置信息
const upload = multer({storage: MAO({config: config,destination: 'uploads' // 自定义上传目录}),limits: {fileSize: 5 * 1024 * 1024, // 限制上传文件的大小为:5MB},fileFilter: function (req, file, cb) {// 只允许上传图片const fileType = file.mimetype.split('/')[0];const isImage = fileType === 'image';if (!isImage) {return cb(new BadRequest('只允许上传图片。'));}cb(null, true);}
});// 单文件上传,指定表单字段名为 file
const singleFileUpload = upload.single('file');
// 多文件上传 指定传输字段为files
const multipleFilesUpload = upload.array('files');
module.exports = {config,client,singleFileUpload,multipleFilesUpload
}
- 上面的
config
,都是阿里云相关的配置,直接读取刚才定义的环境变量。 - 下面的
upload
是multer
中间件相关的配置,我们这里自定义了上传的目录,限制了文件大小和类型。 - 接着,限定了只允许单文件上传。并指定上传表单的名字叫做:file。
- 最后,导出它们,需要用到
singleFileUpload
。
接着就要来完善路由,实现上传操作了:
uploads.js
const { config, client, singleFileUpload, multipleFilesUpload } = require('../utils/aliyun');
const { BadRequest } = require('http-errors')/*** 阿里云 OSS 客户端上传* POST /uploads/aliyun*/
router.post('/aliyun', function (req, res) {try {singleFileUpload(req, res, async function (error) {if (error) {return failure(res, error);}if (!req.file) {return failure(res, new BadRequest('请选择要上传的文件。'));}// 记录附件信息await Attachment.create({...req.file,userId: req.userId,fullpath: req.file.path + '/' + req.file.filename,})success(res, '上传成功。', {file: req.file.url});});} catch (error) {failure(res, error);}
})// 多文件上传
router.post('/aliyunMultiple', function (req, res) {try {multipleFilesUpload(req, res, async function (error) {if (error) {return failure(res, error);}if (req.files.length === 0) {return failure(res, new BadRequest('请选择要上传的文件。'));}// 记录附件信息req.files.map(async item => {await Attachment.create({...item,userId: req.userId,fullpath: item.path + '/' + item.filename,})})success(res, '上传成功。', {files: req.files});});} catch (error) {failure(res, error);}}
)
- 顶部,引用一下刚才定义的那些上传配置。
- 接着非常简单的调用一下方法,如果报错了,就提示错误。
- 还要判断下,用户是否上传了文件。有的用户可能根本没选文件,就直接提交表单了。
- 如果没有出错,就显示已经上传的文件信息。文件信息被存储在
req.file
里了。
5、app.js添加路由引用
客户端直传
客户端直传。客户端,只需要请求 Node 接口,获取上传阿里云所需的授权信息。拿到这些授权信息后,再由客户端直接上传到阿里云 OSS。
这样图片不需要经过服务器中转,服务器的开销非常小,上传速度也会快很多。
对应的缺点就是,在开发上,代码麻烦点。在使用上,前端要调用两次接口,操作比较繁琐。
1、生成随机文件
在开发之前,需要先处理点问题。前端去调用接口,直接上传文件到阿里云的时候。很有可能,多个人上传名字相同的文件。那么大家的文件就会互相覆盖,这真是一个大问题,必须要先想好怎么处理。
之前我们在使用multer
的时候,只要上传文件,就会自动生成一个新的文件名。那它是怎么做的呢,可以看它里面的源码。
function getRandomFilename(req, file, cb) {crypto.pseudoRandomBytes(16, function (err, raw) {cb(err, err ? undefined : `${raw.toString('hex')}${Path.extname(file.originalname)}`);});
}
可以看到这里用了crypto
,生成了随机字符。这个包我们在生成自己项目的秘钥的时候,也是用的它。这是一种生成随机字符的思路。
除此外,还有种常见的方法要介绍给大家,就是UUID
。UUID
的中文叫做:通用唯一识别码
。
在 Node.js 项目中使用它,需要先安装一个包,名叫就做:uuid
。查看官方文档,只需要简单的引用它,调用一下它自带的函数,就可以生成唯一的随机字符了
const { v4: uuidv4 } = require('uuid');
uuidv4(); // ⇨ '1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed'
我们用它生成的这一串,可以避免用户上传的文件名相同,互相覆盖。直接复制命令,安装一下。完成后,启动项目。
npm i uuid
进入routes/uploads.js
中,顶部增加uuid的引用
。此外还要引用下moment.js
,一会儿也会用到它。
const { v4: uuidv4 } = require('uuid');
const moment = require('moment');
上传完整代码
接着看阿里云直传的官方文档,这里直接给了我们示例代码,但是里面写的有些乱,而且也没有做安全方面的考虑。我们参考它,适当整理一下,就有了自己的代码。现在来看看实现的代码
/*** 获取直传阿里云 OSS 授权信息* GET /uploads/aliyun_direct*/
router.get('/aliyun_direct', async function (req, res, next) {// 有效期const date = moment().add(1, 'days');// 自定义上传目录及文件名const key = `uploads/${uuidv4()}`;// 上传安全策略const policy = {expiration: date.toISOString(), // 限制有效期conditions:[['content-length-range', 0, 5 * 1024 * 1024], // 限制上传文件的大小为:5MB{ bucket: client.options.bucket }, // 限制上传的 bucket['eq', '$key', key], // 限制上传的文件名['in', '$content-type', ['image/jpeg', 'image/png', 'image/gif', 'image/webp']], // 限制文件类型],};// 签名const formData = await client.calculatePostSignature(policy);// bucket 域名(阿里云上传地址)const host =`https://${config.bucket}.${(await client.getBucketLocation()).location}.aliyuncs.com`.toString();// 返回参数const params = {expire: date.format('YYYY-MM-DD HH:mm:ss'),policy: formData.policy,signature: formData.Signature,accessid: formData.OSSAccessKeyId,host,key,url: host + '/' + key,};success(res, '获取阿里云 OSS 授权信息成功。', params);
});
详解
- 现在要生成上传所需要的授权信息。但是需要给授权信息,增加一个有效期,总不能无限期使用吧。我这里设置的是 1 天,大家可以根据需要,自行调整。
- 接着我们定义存储在阿里云 OSS 中的位置是uploads。后面又调用uuid,来生成一个唯一的文件名。
- 这样指定好了以后,用户就必须按照接口指定的路径和文件名上传。就可以避免文件名相同,互相覆盖的问题。
- 下面就是上传的安全策略了,分别配置了请求的有效期、文件的大小、指定的存储空间、指定的路径和文件名、必须是图片类型。
- 这里用了eq和in。也就是等于,和在范围里的意思。除此外,还可以使用:starts-with和not-in。具体说明,大家可以参考官方文档。
- 配置完成后,调用阿里云 SDK 的方法,生成签名。
- 再生成一下上传到阿里云,要请求的地址。
- 最后将上传所需要的参数,全都返回出去就完成了。这里有:有效期、上传安全策略、签名、accessid、阿里云 bucket 的域名、上传的路径和文件名。
- 注意下,最后的url,它是阿里云 bucket 的域名拼接上路径和文件名。组合在一起,就是将来上传成功后,文件的完整URL地址。
2、使用 Apifox 上传
到这里接口已经全部开发完成了,我们来实际试试上传流程。阿里云上传分为两步:
-
第一步,获取阿里云 OOS 授权信息。
-
第二步,使用阿里云 OSS 授权信息,上传文件到阿里云 OSS。
2.1. 第一步,获取阿里云 OOS 授权信息
Apifox 中,新建一个接口 -
请求方式:GET
-
接口地址:/uploads/aliyun_direct
-
Headers:要带上登录成功的token。
点击发送后,就可以获取到,授权所需的信息了。
这里获取到的数据挺多的,一会儿调用阿里云的接口上传文件,这些都会用到的。接着点击后置操作
,将获取到的值存储到环境变量
中,这样可以方便后续使用。
- AliyunPolicy:data.policy
- AliyunSignature:data.signature
- AliyunAccessid:data.accessid
- AliyunKey:data.key
2.2. 第二步,上传到阿里云 OSS
第二步,前端需要将文件,上传到返回的host
地址中。我们接着继续调用第二个接口,
接口地址:是上一步返回的host
- Body:要选择form-data,这样才能上传文件。
- 其他所需参数,都是上一步返回的,已经存入环境变量中的参数,大家自行填好即可:
- key:{{AliyunKey}}
- policy:{{AliyunPolicy}}
- OSSAccessKeyId: {{AliyunAccessid}}
- signature:{{AliyunSignature}}
- file: 要把类型改为fille,然后随意选择一个图片。
点击发送,返回状态码204,但是没有返回任何提示信息。其实这已经成功了,回到阿里云 OSS 里,刷新一下,可以看到刚上传的文件了。
2.3. 获取图片完整的访问路径
上传成功后,阿里云没有返回给我们任何信息,那前端如何获取图片完整的访问路径呢?
其实用第一步生成url
,就是完整的访问地址。复制这个地址,就可以直接用浏览器打开,当然也可以放在img
标签的src
中显示。
2.4. 错误情况
再来看看上传错误的情况。例如,如果不按指定的key
来上传,自己非要叫做1.jpg
,
阿里云,就会用xml
格式,提示错误,并且返回403
状态码。
如果上传了个zip
文件,而不是图片,也会返回对应的错误提示。
这种错误格式,由于用了xml
,而不是json
。将来前端来处理,又得费点劲了。
3. 跨域设置
还有个问题,我们这里是用 Apifox 上传的,但是在Vue或者React项目
里上传,会出现跨域错误。需要在阿里云 OSS 里做个设置,
点击跨域设置,创建规则,
- 来源:*
- 允许 Methods:POST
- 允许 Headers:*
确定就好,这样在前端页面上操作,就不会出问题了。
4. 总结一下
- 上传有两种方式。使用直传,是效率最高的方式。先调用 Node 接口,获取直传所需要的授权参数。前端开发,就可以直接将文件上传到阿里云 OSS 了。
- 直传方式,一定要在策略中,限制好文件的类型、大小、所在路径和文件名。
- 我们这里用uuid生成的唯一文件名,并且没有额外加上扩展名。扩展名其实并不是必须的,无论有没有,都完全不影响img标签显示图片。
- 在网页上直传图片,阿里云OSS需要配置跨域。
5. 额外的尝试
关于 OSS 还有更多可玩的内容,大家可以尝试一下:
-
图片处理:在图片路径后加上指定参数,可以显示各种处理后的图片,例如可以实现裁剪、加水印等操作。
-
防盗链:可以在阿里云 OSS 里设置防盗链,这样除了自己的站点可以访问外,其他站点引用或者直接访问,就无法打开图片了。
还有一种使用策略,大家可以试试: -
私有:可以设置成禁止公开访问,并设置为私有。
-
自定义域名:然后配置自定义域名,并加上SSL证书。
-
CDN:接着开启CDN加速,并配置好CDN缓存时间。
配置CDN
后,阿里云会在全国各地机房都缓存OSS
中的文件。让用户访问就近的机房,访问速度就会大大的提高。
因为设置了禁止公开访问,用户就必须通过CDN
访问。就无法不经过授权,通过OSS
直接访问图片。大家要知道,访问CDN
的费用,可比直接访问OSS
要便宜不少。