上一篇谈到了小程序端从选择文件到文件的上传下载整个流程。但是文件上传服务器的真正操作实际上是在服务器实现。本篇文章主要谈谈服务端如何实现文件上传到服务器并返回可支持访问的url。首先,我们可以先考虑下业务逻辑。我给出的方案一是这样一个简单逻辑:将上传文件分成图片上传和文件上传两部分逻辑。为什么要区分两部分逻辑呢?因为我们假设一个业务场景:商品上架功能需要上传商品主图,轮播图等一系列图片,我们如果一次只能上传一张图片,则得调用多次接口,会造成服务器带宽和资源的浪费。所以我们处理图片上传我们可以设置图片数组放置需上传的图片。那对于非图片的文件呢?比如我们要上传一个视频,可能几十M,我们同时上传十个八个,这时候客户端迟迟得不到响应,用户体验会很差,所以我们在处理非图片文件时一般需要一个一个文件进行上传。接下来我们来看下服务端如何实现文件上传。
用过Node的人应该都知道,Node实现文件上传一般都需要使用multiparty库,我们首先需要生成multiparty对象并配置文件最终上传的路径:
//生成multiparty对象,并配置上传目标路径
var form = new multiparty.Form({uploadDir: (mainPath + '/picTemp/')});
我们生成multiparty对象后,就可以使用multiparty.from().parse(req, callback)进行文件上传。文件上传成功实际上就会上传到我们刚才定义的上传目录中,然后返回files。我们可以看下文件上传效果:
这时候有人说文件上传解决了,当然没那么简单。我们文件上传看似解决了,但是还需要考虑各种各样的bug场景,简单举几个例子:服务器设置文件上传最大为25M,我上传一个50M的文件,这时候服务器肯定返回413状态码标识文件太大。再比如我们需要限制文件最大上传数量等等逻辑。所以接下来我们开始慢慢优化这个文件上传功能。首先我们需要先对参数做限制,一个变量名只能对应一个文件,比如我上传两个文件,文件名都用mp4_url,这时候肯定不允许,这时候我们需要报错并删除已上传图片:
//查看图片是否超过限制
var picNum = 0;
par.picNames = Object.keys(files);
var picSizeArr = [];
for(var picKey in files){
if(files[picKey].length > 1){//每个名字只能带一张图片
delPicsWithFiles(files);
return cb('文件参数有误', 400);
}
par[picKey] = files[picKey][0].path;
picNum += files[picKey].length;
picSizeArr.push(parseInt(files[picKey][0].size));
}
//根据上传来的files表单删除图片
function delPicsWithFiles(files) {
//图片超过限制,删除上传来的图片
for (var key in files) {
files[key].forEach(function (picObj) {
var uploadedPath = picObj.path;
fs.unlink(uploadedPath, function () {
});
});
}
}
第一步校验通过了,下一步就是针对图片和非图片做不同的操作。图片允许多图同时上传,所以我们需要判断上传的图片是否超过我们限制的最大张数,如果图片张数超限,则删除所有已上传图片:
if(picNum > maxPic){
//图片超过限制,删除上传来的图片
delPicsWithFiles(files);
return cb('图片个数超过限制', 400);
}
并且需要判断图片文件大小是否符合规范,一般大小要和服务器配置一致,防止文件大小超过服务器限制大小。
if(picSizeArr[0] > 4000000) {
delPicsWithFiles(files);
return cb('图片过大,请重新选图!',400);
}
一般上传功能会有业务逻辑操作,比如上传成功保存数据库。所以我们得对参数进行校验,比如参数不全的情况就得删除所有已上传图片:
//检验参数是否正确,包括图片命名,不正确的话去删除上传的图片,并且返回错误
checkParFunc(par,function (err,errCode) {
if(!err){//验证正确,去重命名
par.files = files;
picHelp.renamePics(par,pathDir,isNeedUid,function (err,errCode,param) {
if(err){
cb(err, errCode, param);
delPicsWithFiles(files);
return;
}
cb(null, 0, param);
});
return;
}
//验证不正确,删除上传来的图片
delPicsWithFiles(files);
cb(err,errCode);
});
function checkParFunc(par, cb) {
if (!par.banner1 || !par.shopTitle1 || !par.price1 || !par.score1 || !par.linkUrl1) {
return cb('参数不全', 400);
}
cb(null, 0, par);
}
如果到这里检验通过一般来说我们图片上传业务逻辑没问题了。但是我们还是可以继续优化,刚才上传成功的截图我们可以看到文件上传后文件名都是随机字符串,我们很多时候都是需要对文件上传做分类才可以维护数据。所以下一步我们通过分割时间戳按照时间来将上传的图片转移到新的文件夹存储,并且我们移动到真正存储的文件夹时,通过fs.readFile()取到文件后缀名,然后将文件重命名成按时间戳进行命名,最终移动文件夹返回文件所在的地址,文件上传逻辑大功告成:
//给上传的图片重命名 //par:参数 picType:路径名
picHelp.renamePics = function (par,picType,isNeedUid,cb) {
if(!par.files){
cb('参数有误',400);
return;
}
//构造路径
var uid = 0;
if(par.userInfo){
uid = par.userInfo.main_userInfo ? par.userInfo.main_userInfo.uid : par.userInfo.uid;
}
var date = new Date();
var userPath = '/' + picType;
userPath += '/' + date.getFullYear();
userPath += '/' + (date.getMonth()+1);
userPath += '/' + date.getDate();
if(isNeedUid == true) {
userPath += '/' + parseInt(uid / 100);
userPath += '/' + uid;
}
mkdirs((mainPath + userPath),function (err) {//创建目录
if(err){
cb(err,400);
return;
}
userPath += '/' + date.getHours() + date.getMinutes() + date.getSeconds() + date.getMilliseconds();
changeDir(par, 0, userPath, function (err, par) {
if (err) {
cb(err, 400,par);
return;
}
cb(null, 0, par);
});
});
}
//递归创建目录 异步方法
function mkdirs(dirname, callback) {
fs.exists(dirname, function (exists) {
if (exists) {
callback(null);
} else {
mkdirs(path.dirname(dirname), function () {
fs.mkdir(dirname, callback);
});
}
});
}
//更新图片路径
function changeDir(par,index,userPath,callback) {
var keyArr = Object.keys(par.files);
if(keyArr.length < 1){
callback(null,par);
return;
}
var picObj = par.files[keyArr[index]][0];
var uploadedPath = picObj.path;
fs.readFile(uploadedPath, function (err,bytesRead) {
if (err) {
callback(err,par);
return;
}
var info = imageInfo(bytesRead);
var type;
if(!info || !info.format){
type = '.jpg';
}else {
type = imageInfoFileType(info.format);
if (!type) {
callback('上传图片格式有误', par);
return;
}
}
//参数正确 更换图片路径
var picPach = userPath + type;
var dstPath = mainPath + picPach;
checkDirs(dstPath,function (exits) {
if(exits){
picPach = userPath + (keyArr.length + index) + type;
dstPath = mainPath + picPach;
}
//重命名为真实文件名
fs.rename(uploadedPath,dstPath,function (err) {
if(err){
callback(err,par);
return;
}
par[picObj.fieldName] = picPach;
if(index < (keyArr.length-1)){
changeDir(par,index+1,userPath,callback);
}else {
callback(null,par);
}
});
});
});
}
讲完了图片上传功能,那针对非图片上传如何实现呢?实际上非文件上传我们可以设置一次只允许上传一个文件,然后判断文件大小是否超过限制,然后一样验证参数是否又出现参数不全等情况,最后一样进行按时间戳分割移动到当天文件夹下存放并进行重命名成按时间戳命名并返回图片路径。逻辑和刚才图片处理类似所以我们直接看看代码:
if(par.mp4_url) {
if(!files.mp4_url || !files.mp4_url[0] || !files.mp4_url[0].size || files.mp4_url[0].size == 0) {
cb('视频上传时发生错误!', 400);
return;
}
if(files.mp4_url[0].size > 8000000) {
fs.unlink(par.mp4_url, function () {});
cb('文件文件过大',400);
return;
}
delete files['mp4_url'];
delPicsWithFiles(files);
pathDir = 'bbs_mp4';
checkParFunc(par, function (err,errCode) {
if(err){
fs.unlink(par.mp4_url, function () {});
return cb(err, errCode);
}
picHelp.renameVideo(par,pathDir,isNeedUid,function (err,errCode,param) {
if(err){
cb(err,errCode,param);
fs.unlink(par.mp4_url, function () {});
return;
}
cb(null, 0, param);
});
return;
});
}
//文件上传
picHelp.renameVideo = function (par,picType,isNeedUid,cb) {
var uid = 0;
if(par.userInfo){
uid = par.userInfo.main_userInfo ? par.userInfo.main_userInfo.uid : par.userInfo.uid;
}
var date = new Date();
var userPath = '/' + picType;
userPath += '/' + date.getFullYear();
userPath += '/' + (date.getMonth()+1);
userPath += '/' + date.getDate();
if(isNeedUid == true) {
userPath += '/' + parseInt(uid / 100);
userPath += '/' + uid;
}
mkdirs((mainPath + userPath),function (err) {//创建目录
if(err){
cb(err,400);
return;
}
userPath += '/' + date.getHours() + date.getMinutes() + date.getSeconds() + date.getMilliseconds();
var uploadedPath = par.mp4_url;
fs.readFile(uploadedPath, function (err) {
if (err) {
cb(err, 400);
return;
}
var type = par.mp4_url.split('.')[1];
var picPach = userPath + '.' + type;
par.mp4_name = picPach;
var dstPath = mainPath + picPach;
checkDirs(dstPath,function (exits) {
if(exits){
picPach = userPath + type;
dstPath = mainPath + picPach;
}
fs.rename(uploadedPath,dstPath,function (err) {
if(err){
cb(err,400);
return;
}
par.mp4_url = par.mp4_name;
cb(null, 0, par);
});
});
});
});
}
到这里我们可以测试下不同文件的上传效果可以看到测试多种不同格式最后全部成功上传:
目前博客小程序前后端已开源于码云,欢迎来一个star。源码地址:
https://gitee.com/mqzuimeng_admin/wx_blog.git