node实现文件分片上传
前端在做文件上传时,考虑到网速的快慢,如果文件过大的话可能会导致上传时间过长而请求超时,文件上传失败。因此文件过大需要对文件进行分片上传。
那文件分片上传的具体过程是怎样的呢?
进行了许多搜索查找之后,参照众多资源进行修改,得到了自己的简易实现流程。
首先列出来node需要用到的模块:
const express = require('express');
var multer = require('multer');
var fs=require('fs');
var path = require('path');
var app = express();
var fse = require('fs-extra');
{
"name": "fileloader",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "nodemon index.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"art-template": "^4.13.2",
"express": "^4.17.1",
"express-art-template": "^1.0.1",
"fs-extra": "^9.1.0",
"jquery": "^3.6.0",
"multer": "^1.4.2"
}
}
服务器实例采用express框架快速搭建。配合art-template 和 express-art-template进行页面处理
multer 模块用于处理文件上传
fs-extra模块倒不是必须的,只是参照其他的文章,采用这个模块来删除文件夹较为方便。
multer配置介绍
multer用于作为处理文件上传的中间件,可以通过 var upload = multer({dest: '路径'})来实例化multer,然后在路由中配置upload.single('file') 作为中间件。如官方的文档介绍所示:
var express = require('express')
var multer = require('multer')
var upload = multer({ dest: 'uploads/' })
var app = express()
app.post('/profile', upload.single('avatar'), function (req, res, next) {
})
app.post('/photos/upload', upload.array('photos', 12), function (req, res, next) {
})
var cpUpload = upload.fields([{ name: 'avatar', maxCount: 1 }, { name: 'gallery', maxCount: 8 }])
app.post('/cool-profile', cpUpload, function (req, res, next) {
})
官方的示例中,upload实例有三种主要的使用形式,即upload.single()、upload.array()、upload.fileds(),这三者简单来说应该是single用于处理单文件,array和fieds都能处理多文件,由于项目仅演示单文件处理过程,所以这两个不过多赘述,想要了解更多请阅读multer中文翻译文档
.single(filename) --> 接受一个以 fieldname
命名的文件。这个文件的信息保存在 req.file
。
另外,multer也会向req中添加body字段,如果想要将multer作为body-parser的替代,需要配置类似如下的路由
app.post('/merge',upload.none(),function(req,res){
})
此外,如果前端使用jquery的ajax进行上传,需要配置一些特殊的选项,传递的数据也需要是一个 FormData()对象,我不知道这是不是必须,但是我用这种方法暂时未出现问题,这也是我用multer替代body-parser 出现的问题。
当然,这只是简单地配置,如果需要对文件存储路径和文件名进行特殊的设置,需要配置 storage 参数,这是磁盘存储引擎,可以控制文件的存储。
它有两个选项可用,destination
和 filename
。他们都是用来确定文件存储位置的函数。
destination
是用来确定上传的文件应该存储在哪个文件夹中。也可以提供一个 string
(例如 '/tmp/uploads'
)。如果没有设置 destination
,则使用操作系统默认的临时文件夹。
注意: 如果你提供的 destination
是一个函数,你需要负责创建文件夹。当提供一个字符串,multer 将确保这个文件夹是你创建的。
filename
用于确定文件夹中的文件名的确定。 如果没有设置 filename
,每个文件将设置为一个随机文件名,并且是没有扩展名的。
代码示例
前端代码示例:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>document</title>
<style>
body,div,p,h1,h2,h3,h4,h5,h6,ul,li{margin:0;padding:0;list-style:none;}
ul,ol{list-style:none;}
input{outline-style: none;padding: 0;}
.wrap{
width: 600px;
height: 400px;
background-color: lightblue;
border-radius: 20px;
margin: 50px auto;
}
label[for="file"]{
display: inline-block;
width: 100px;
height: 100px;
background-color: lightcoral;
text-align: center;
/* vertical-align: middle; */
line-height: 100px;
}
#file{
opacity: 0;
}
input[type="button"]{
width: 80px;
height: 32px;
margin: 10px 10px;
background-color: rgba(0, 0, blue, 0.5);
outline-style: none;
}
</style>
</head>
<body>
<div class="wrap">
<div class="form">
<label for="file">选择文件</label>
<input type="file" id="file" name="file" accept="*" >
<br>
<input type="button" value="上传" onclick="upload(0)">
</div>
</div>
<script src="/node_modules/jquery/dist/jquery.js"></script>
<script>
var chunkSize = 1024*1024;
var fileInput = document.querySelector("#file");
function upload(index){
let files = fileInput.files;
if(!files[0]){
alert("请选择文件")
return
}
let file = files[0];// file对象
//获取文件名和扩展名
let [fname,fext] = file.name.split('.')
// 分片
let start = index * chunkSize
if(start > file.size){
merge(file.name);
return;
}
let blob = file.slice(start,start + chunkSize)
let blobName = `${fname}.${index}.${fext}`;
let blobFile = new File([blob],blobName);
// 上传文件
let formData = new FormData()
formData.append('file',blobFile);
$.ajax({
type: 'post',
url: '/upload',
data: formData,
contentType: false,
processData: false,
success: function(res){
console.log(res);
upload(++index);//递归上传文件分片
}
})
}
// 合并文件请求
function merge(filename){
var formData = new FormData()
formData.append('name',filename)
$.ajax({
type: 'post',
url:"/merge",
data: formData,
contentType: false,
processData: false,
dataType: 'json',
success: function(res){
console.log(res);
}
})
}
</script>
</body>
<html>
通过input选择框,用户选择文件后该元素返回一个File对象,存在input.files中。
File对象继承自Blob对象,拥有slice方法,返回一个blob子对象,是file的一个切片。
得到文件分片后,设置其分片的顺序,并将其添加到一个FormData对象中。
用jquery的ajax进行上传,需要配置两个参数:
contentType: false,
processData: false,
不配置可能会报错。
所有分片上传之后,需要发送请求进行文件合并,函数merge即为请求合并文件的函数。
后台代码:
const express = require('express');
var multer = require('multer');
var fs=require('fs');
var path = require('path');
var app = express();
var fse = require('fs-extra');
// 用于检测是否存在用于存放文件的路径,不存在则创建路径
const createFolder = function(folder){
try{
fs.accessSync(folder);
}catch(e){
fs.mkdirSync(folder);
}
};
// 文件上传的路径
var uploadFolder = './uploads/';
createFolder(uploadFolder);
// 用于传给multer进行复杂的文件上传配置
var storage = multer.diskStorage({
destination: function(req,file,cb){
// 用于进行复杂的路径配置,此处考虑分片上传,先将分片文件保存在临时目录中
let [fname,index,fext] = file.originalname.split(".");
let chunkDir = `${uploadFolder}/${fname}`;
if(!fse.existsSync(chunkDir)){
fse.mkdirsSync(chunkDir);
}
cb(null,chunkDir); //内部提供的回调函数
},
filename: function(req,file,cb){
// 根据上传的文件名,按分片顺序用分片索引命名,
// 由于是分片文件,请不要加扩展名,在最后文件合并的时候再添加扩展名
let fname = file.originalname;
cb(null,fname.split('.')[1]);
}
})
var upload = multer({storage: storage});// multer实例
// 配置模板引擎
app.engine('html',require('express-art-template'))
app.set('views',__dirname+'/views')
// 静态资源路由
app.use('/node_modules',express.static('node_modules'))
app.use('/upload',express.static('uploads'))
// 主页路由
app.get('/',function(req,res){
res.render('index.html')
})
// 上传路由
app.post('/upload',upload.single('file'),function(req,res,next){
res.end("ok");
})
// 文件合并路由
app.post('/merge',upload.none(),function(req,res){
let name = req.body.name;
let fname = name.split('.')[0];
let chunkDir = path.join(uploadFolder,fname);
let chunks = fs.readdirSync(chunkDir); // 同步读取以防文件合并顺序混乱
chunks.sort((a,b)=>a-b).map(chunkPath=>{
fs.appendFileSync(
path.join(uploadFolder,name),
fs.readFileSync(`${chunkDir}/${chunkPath}`)
)
})
fse.removeSync(chunkDir);
res.send({msg:'合并成功',url:`http://localhost:8080/upload/${name}`});
})
app.listen(8080,()=>{
console.log("success.....localhost:8080")
})
配置 multer的磁盘存储引擎时, 在destination中设置了保存分片文件的临时文件夹,在filename 中设置了按照分片索引来存储文件,不添加扩展名。
在 merge 路由中进行文件合并,同步读取文件临时目录,用 fs 模块对文件进行排序之后进行文件合并。
项目地址:我的gitee仓库