nodejs+koa2实现文件上传
前言
最近由于项目需要,研究了一下JS中的文件上传,并做了一下简单的总结和梳理。下面将对该功能分两部分(服务端和客户端)进行分别介绍。本文将只介绍服务端部分。一、环境准备及第三方库
- 我们将采用nodejs+koa2来实现文件上传的服务端部分
- nodejs版本:v12.16.1
- koa版本:v2.13.1
- formidable:v1.2.2 用于解析post请求中传递的参数,Content-Type为:application/x-www-form-urlencoded
- koa-router:v10.0.0 用于配置路由
- koa-static: v5.0.0 读取静态文件中间件
- koa2-cors: v2.0.6 用于cors跨域设置
- multiparty:v4.2.2 用于解析和上传form-data文件
- spark-md5:v3.0.1 用于根据文件内容生成字符串(相同内容的文件生成的值是一样的)
- nodemon:v2.0.7 用该命令运行js文件可自动重启服务
二、项目结构
server:根目录
- node_modules:自动生成,存放所有依赖库
- upload:用于存放上传后的文件
- server.js:api接口文件,定义所有的上传接口
- package.json:项目的配置文件
三、API接口说明(server.js)
1.基本配置
- 首先我们先将所有要用到的库在文件的头部引入
- 配置一些全局属性,如服务器地址、端口号、文件保存路径等等
- 因为是前后端分离,所有可能会涉及到跨域请求,需要配置cors跨域
- 最后再应用一下koastatic中间件用于访问静态资源文件
整体骨架代码如下:
const Koa = require('koa');
const Router = require('koa-router');
const koastatic = require('koa-static');
const fs = require('fs');
const formidable = require('formidable');
const multiparty = require('multiparty');
const SparkMD5 = require('spark-md5');
const path = require('path');
const app = new Koa();
let router = new Router();
//中间件:设置允许跨域
app.use(async (ctx, next) => {
ctx.set('Access-Control-Allow-Origin', '*');
//处理OPTIONS请求
ctx.request.methods === 'OPTIONS' ? ctx.body = '请求测试成功' : await next();
});
const host = '127.0.0.1',
port = 3000;
const HOSTNAME = `${
host}:${
port}`;
const SERVER_PATH = `${
__dirname}/upload`;
app.listen(port, function () {
console.log('======================================================================');
console.log(`The server started at port: ${
port}, you can access it by ${
HOSTNAME}`);
console.log('======================================================================');
});
//.......
//这里添加文件上传的API接口
//.......
app.use(koastatic('./'));
app.use(router.routes());
app.use(router.allowedMethods());
2. 公共函数封装
在实现各个接口前难免会涉及到一些重复的代码,这个时候我们就需要进行一些必要的封装,以实现代码的复用。
接下来我们就一起来分析并封装一下常用的方法
- 检测文件是否存在
- 本案例中我们采用nodejs内置的fs模块的access函数来检测文件是否已经存在
- 利用Promise进行管理
- 关键代码:fs.access(filepath, fs.constants.F_OK, err=>{…})
- 另:fs模块中有个exists方法也可以用于检测文件是否存在,但官方文档中已经弃用,并推荐使用access方法。
//检测文件是否已经存在
const exists = function exists(path){
return new Promise((resolve, reject)=>{
fs.access(path, fs.constants.F_OK, err=>{
if(err){
resolve(false);
return;
}
resolve(true);
});
});
}
- 利用multiparty解析请求中的文件信息
- 利用该插件可实现form-data格式的请求中的文件解析和提取
- 该模块配置项中有个uploadDir属性,如果指定则会自动上传文件
- 因为本文还将涉及到自定义文件名上传文件,所以这里我们将该方法封装为可配置的
- 关键代码:new multiparty.From(config).parse(req,(err, fields, files)=>{})
//利用multiparty插件解析前端传来的form-data格式的数据,并上传至服务器
const multipartyUpload = function multipartyUpload(req, autoUpload){
let config = {
maxFieldsSize : 200 * 1024 *1024
}
if(autoUpload) config.uploadDir = SERVER_PATH;
return new Promise((resolve,reject)=>{
new multiparty.Form(config).parse(req, (err, fields, files)=>{
if(err){
reject(err);
return;
}
resolve({
fields,
files
});
});
});
}
- 将文件内容写入服务器
- 该方法中我们将以两种方式进行文件的写入
- 一种是:对于前端传过来的form-data格式的文件我们将以流的方式进行写入
- 另一种:则是将内容直接写入到文件,比如BASE64格式的图片文件
- 这里我们将用到内置模块fs中的createReadStream和createWriteStream用于以流的方式写入,还有fs中的writeFile用于将内容直接写入
- 关键代码: fs.createReadStream(filepath); fs.createWriteStream(serverpath); readStream.pipe(writeStream); fs.writeFile(serverpath,file,err=>{…})
//将传进来的文件数据写入服务器
//form-data格式的数据将以流的形式写入
//BASE64格式数据则直接将内容写入
const writeFile = function writeFile(serverPath, file, isStream){
return new Promise((resolve, reject)=>{
if(isStream){
try{
let readStream = fs.createReadStream(file.path);
let writeStream = fs.createWriteStream(serverPath);
readStream.pipe(writeStream);
readStream.on('end',()=>{
resolve({
result: true,
message: '上传成功!'
});
fs.unlinkSync(file.path);
});
}catch(err){
resolve({
result: false,
message: err
})
}
}else{
fs.writeFile(serverPath,file, err=>{
if(err){
resolve({
result: false,
message: err
})
return;
}
resolve({
result: true,
message: '上传成功!'
});
});
}
});
}
- post请求中application/json或application/x-www-form-urlencoded格式的参数解析
- 对于from-data形式的文件信息我们可以用multiparty插件进行解析和提取,但是对于其它格式的一些参数,multiparty就无法解析到了
- 这里我们利用formidable进行解析post参数
- formidable可以将a=xxx&b=zzz格式的参数解析为json格式,前端请求时需用qs进行相应的格式处理
- 另外:koa-bodyparser也可以用来解析,但是尝试用了一下都没有成功,所以改用formidable
- 关键代码:new formidable.IncomingForm().parse(req,(err, fields)=>{…})
//解析post请求参数,content-type为application/x-www-form-urlencoded 或 application/josn
const parsePostParams = function parsePostParams(req){
return new Promise((resolve, reject)=>{
let form = new formidable.IncomingForm();
form.parse(req,(err,fields)=>{
if(err){
reject(err);
return;
}
resolve(fields);
});
});
}
- 大文件切片上传后再合并
- 在后面要讲的大文件切片上传,断点续传中,一个大文件会被切成n多个小文件上传
- 在上传后我们需要再将这些切片文件合并成一个文件,从而实现大文件上传
- 主要利用内置fs模块中的appendFileSync和readFileSync方法
- 关键代码:fs.appendFileSync(serverFilePath, fs.readFileSync(filePath))
const mergeFiles = mergeFiles(hash, count){
return new Promise((resolve, reject)=>{
const dirPath = `${
SERVER_PATH}/${
hash}`;
if(!fs.existsSync(dirPath)){
reject('还没上传文件,请先上传文件');
return;
}
const filelist = fs.readdirSync(dirPath);
if(filelist.length < count){
reject('文件还未上传完成,请稍后再试');
return;
}
let suffix;
filelist