express 上传文件的原理和实现
- 原理
- formidable
- multer
- COS
1.原理
1.1 要想了解express上传 我们先看看 nodejs原生上传是怎么实现的
let server = require('http').Server(app);
server.listen(3000);
首先为了让express拥有原始http模块的一些功能
请不要使用 bodyParser 之类的中间件 因为不会next到这里 保持尽量原生。
app.post('/upload', async(req, res)=>{
var postData = '';
req.on("data", function(postDataChunk) { // 有新的数据包到达就执行
postData += postDataChunk;
});
req.on("end", function() { // 数据传输完毕
console.log(postData);
res.end('up success');
// console.log('post data finish receiving: ' + postData );
});
接下来前端模拟一个post请求 ajax form表单都行 假设我们传了个
在 post的 content-type="application/x-www-form-urlencoded" 下 后台
console.log` 的 postData 应该是 name=xxx
这时候通过一些 querystring 之类的中间件解析出 key value数值 以方便我们操作
bodyParser这个中间件也是封装简化了流程 只要直接去req.body里面取就行。但有个弱点 对于file提交 就不能这么玩了! 那我们继续看看为什么
把 enctype="application/x-www-form-urlencoded"
改成 enctype= multipart/form-data
另外增加个文件提交 再准备1个zip文件
上传zip文件 和填写名称后 后台 console.log
的 postData 大概长这样
------WebKitFormBoundaryXKLAlaggDlZOVroE
Content-Disposition: form-data; name="name"
xxxx
------WebKitFormBoundaryXKLAlaggDlZOVroE
Content-Disposition: form-data; name="file"; filename="wrap.zip"
Content-Type: application/zip
PK�����bL�y��}������wrap.jpgUT ��]�Z�ϟZux���������PS��?�HBKh�: ŀ� �zG" 一堆乱码
真的很吓人格式 其实除了乱码 还是有点规律 都通过WebKitFormBoundary 分割 还有一些头信息描述 和 文件内容 如果您想了解这些代表什么 请看 传送门
让我们改进下代码 取消掉name字段 end 里生成文件
req.on("end", function() { // 数据传输完毕
fs.writeFile('./upload/sss.zip',postData,'binary',function(err){ //path为本地路径例如public/logo.png
if(err){
console.log('保存出错!')
res.end('up failed');
}else{
console.log('保存成功!')
res.end('up success');
}
})
// console.log('post data finish receiving: ' + postData );
});
这段代码的目的是 接受前端的zip包 然后保存为服务器端的upload文件下sss.zip。 很显然 运行是成功了 也生成了 但双击解压时候就愣逼了 无法打开。 看了下2个zip的大小 很明显已经发送了变化 。原因就出在一些其他的东西也进入了数据体(就是刚才乱码部分) 如果name没取消 那么信息将会更混乱 怎么解决呢?其实也很简单 只是少了个解析中间件 当然也有大神手写 根据一定规则 split啊 正则啊
2.formidable
formidable 就是一款解析软件 npm install
下 和 var formidable = require('formidable')
下
app.post('/upload', async(req, res)=>{
var form = new formidable.IncomingForm();
form.uploadDir = "./upload";
//form.keepExtensions = true; 是否保持上传什么后缀 保存什么后缀 默认没有后缀
form.parse(req, function(err, fields, files) {
res.writeHead(200, {'content-type': 'text/plain'});
res.write('received upload:\n\n');
var tmpPath=files.file.path; //临时保存路径
var fileName=files.file.name;
fs.rename(tmpPath,path.resolve(form.uploadDir,fileName),function (err) {
if(err){
console.log('保存出错!')
}
else{
console.log('保存成功!')
}
res.end();
})
});
然后打开生成的zip文件。这次能正常解压 说明数据没发生破坏。 formidable 也有很多 事件驱动api 如 progress
error
等 具体看文档
这里我原名保存了上传文件 按实际情况定义。
3.Multer
multer 就是一款express官方推荐的上传中间件 。和formidable 差不多 但是官方送的最基础的上传 如果你想扩展功能 如上传进度条,断点续传 阿里oss 腾讯cos 等内容仓库 还要你自己想办法 融进去。github上已经有部分写好的 可以去看看 这里给出我的配置 可以参考参考
let setMulter = require('../util/myMulter');
//文件上传服务
router.post('/upload', function (req, res, next) {
//这是自己写的一个封装势力 为了想以后扩展
var upload=setMulter('file',1);
upload(req, res, function (err) {
try {
if (err) throw err;
if(req.files.length==0) throw new Error("不能上传空文件"); //官方没有空文件的办法 再这里添加
res.send(`文件上传成功,地址:${req.files[0].url}`);
}
catch (err) {
console.log(err.message);
res.send(`文件上传失败,原因:${err.message}`);
}
});
});
myMulter.js
let path=require('path');
let multer = require('multer');
//api https://github.com/expressjs/multer/blob/master/doc/README-zh-cn.md
//定义上传目录
let dir=path.resolve(__dirname,'../upload');
//定义mimes-type
const mimes = {
'.png': 'image/png',
'.gif': 'image/gif',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.zip':'application/zip'
};
//定义仓库
const storage = multer.diskStorage({
//Note:如果你传递的是一个函数,你负责创建文件夹,如果你传递的是一个字符串,multer会自动创建
destination: function (req, file, cb) {
cb(null, dir);
},
//获取文件MD5,重命名,添加后缀,文件重复会直接覆盖
filename: function (req, file, cb) {
var ext =(file.originalname).split('.').pop();
cb(null, `${file.fieldname}-${Date.now()}.${ext}`);
}
});
//定义过滤器
const fileFilter =function (req, file, cb) {
// 指示是否应接受该文件
var test = Object.values(mimes).filter(function(type) {
return type===file.mimetype;
})
// 接受这个文件,使用`true`,像这样:
if(test){
cb(null, true);
}else{ // 拒绝这个文件,使用`false`,像这样:
cb(new Error('file mimes not allow!'), false);
}
}
//定义限制
const limits={
fileSize:1024 * 1024 * 30
};
module.exports=function(opt) {
return multer({
storage: storage,
fileFilter:fileFilter,
limits: limits,
}).array(opt);
};
如果你想添加进度条 请参考 这个
4.COS
通常来说 一般上传文件会结合云服务器商的存储空间 以方便cdn加速 和数据备份等 操作 还可以支持分片上传。 multer也支持 但必须要自己写 可以参考multer/storage/disk.js
必须要掌握 stream 的概念。 _handleFile就是对流的处理 。特别注意的是 multer 送的file.stream
可读流是stream.Readable
基础上自己改出来的 . fs.createReadStream()方法读不出来 所以你必须自己要做tmp文件然后读它 再删它。
我用的是腾讯云COS 如果有兴趣的话 看 https://github.com/lanbosm/multer-COS
4.1 安装npm包npm install multer-cos -S
4.2 制作自己的multer
myMulter.js
let path=require('path');
let multer = require('multer');
let multerCOS = require('multer-cos');
require( 'dotenv' ).config();
/**
* multer
* https://github.com/expressjs/multer/blob/master/doc/README-zh-cn.md
* cos
* https://cloud.tencent.com/document/product/436/12264#options-object
*/
//定义临时文件
let dir=path.resolve(__dirname,'./tmp');
//定义mimes-type
const mimes = {
'.png': 'image/png',
'.gif': 'image/gif',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.zip':'application/zip',
'.txt':'text/plain'
};
const cosConfig={
//id和key是必须
//SecretId: AKIXXXXXXXXXXX,
//SecretKey:XXXXXXXXXXXXXX,
//Bucket:test-bucket-125XXXXXXXXX
//Region=ap-shanghai
// 可选参数
FileParallelLimit: 3, // 控制文件上传并发数
ChunkParallelLimit: 3, // 控制单个文件下分片上传并发数
ChunkSize: 1024 * 1024, // 控制分片大小,单位 B
domain:'static.dorodoro-lab.com', //cos域名
dir:'upload', //cos文件路径
onProgress:function(progressData){//进度回调函数,回调是一个对象,包含进度信息
//console.log(progressData);
}
};
//定义仓库
const storage = multerCOS({
cos:cosConfig,
//Note:如果你传递的是一个函数,你负责创建文件夹,如果你传递的是一个字符串,multer会自动创建 如果什么都不传 系统自己会生成tmp目录
destination: function (req, file, cb) {
cb(null, dir);
},
//自己会生成个随机16字母的文件名和后缀
filename:'auto'
});
//测试cos
//storage.test();
//定义过滤器
const fileFilter =function (req, file, cb) {
// 指示是否应接受该文件
let test = Object.values(mimes).filter(type=>type===file.mimetype);
// 接受这个文件,使用`true`,像这样:
if(test.length>0){
cb(null, true);
}else{ // 拒绝这个文件,使用`false`,像这样:
cb(new Error('file mimes not allow!'), false);
}
};
//定义限制
const limits={
fileSize:1024 * 1024 * 30
};
module.exports=function(opt) {
return multer({
storage: storage,
fileFilter:fileFilter,
limits: limits,
}).array(opt);
};
4.3 和官方版一样用就行了 记得在控制台配置好相关api密钥