在使用nodejs获取上传的文件的时候,有两种方式可获取上传文件:
1. File 模式
2. Stream 模式
今天的主角是Stream 模式的上传/接受单个文件的getFileStream方法,我们优先讲这个
- getFileStream方法如何使?
话不多说直接上代码
const ctx = this.ctx;
// 至此就获取到了浏览器通过 Multipart/form-data 格式发送的文件,
const stream = await ctx.getFileStream();
// 那通过FormData发送过来的其他参数怎么获取呢?他的所有参数就放在fields里面
const { xx1, xx2, xx3 } = stream.fields;
重点来了
要通过 ctx.getFileStream 便捷地获取到用户上传的文件,需要满足两个条件:
- 只支持上传一个文件。
- 上传文件必须在所有其他的 fields 后面,否则在拿到文件流时可能还获取不到 fields。
- 解释一下第二个条件
小二来一碗代码饭~ – 好嘞!
function uploadFile(file, { topCategory, subCategory, fileSize }) {
if (!(file instanceof File)) {
throw new Error("无效的文件对象");
}
const forms = new FormData();
forms.append("xx1", xx1);
forms.append("xx2", xx2);
forms.append("xx3", xx3);
// 这个必须在最后
forms.append("file", file);
const url ="<https://example.com/upload>";
return axios.post(url, forms);
}
浅谈基于egg的文件上传
Stream 模式
单文件上传
- 前端上传
function uploadFile(file, { xx1, xx2, xx3 }) {
if (!(file instanceof File)) {
throw new Error("无效的文件对象");
}
const forms = new FormData();
// 参数
forms.append("xx1", xx1);
forms.append("xx2", xx2);
forms.append("xx3", xx3);
// 文件
forms.append("file", file);
const url ="<https://example.com/upload>";
return axios.post(url, forms);
}
- 后端接口处理
async create() {
const { ctx } = this;
const stream = await ctx.getFileStream();
const { xx1, xx2, xx3 } = stream.fields;
// 生成文件目录
const targetPath = 'xx/xx/upload';
promiseDirSync(targetPath);
const target = path.join(targetPath, 'fileName.mp4');
try {
// 创建写入流
const writeStream = fs.createWriteStream(target);
// 创建管道
await pump(stream, writeStream);
} catch (error) {
// 此过程中出现任何错误都得将上传的文件流消费掉,要不然浏览器响应会卡死
await sendToWormhole(stream);
throw err;
}
await this.returnCreate(this.models.video.create(Object.assign(
{ xx1, xx2,... },
extraInfos
)));
}
多文件上传
const sendToWormhole = require('stream-wormhole');
const Controller = require('egg').Controller;
class UploaderController extends Controller {
async upload() {
const ctx = this.ctx;
const parts = ctx.multipart();
let part;
// parts() 返回 promise 对象
while ((part = await parts()) != null) {
if (part.length) {
// 这是 busboy 的字段
console.log('field:' + part[0]);
console.log('value:' + part[1]);
console.log('valueTruncated:' + part[2]);
console.log('fieldnameTruncated:' + part[3]);
} else {
if (!part.filename) {
// 这时是用户没有选择文件就点击了上传(part 是 file stream,但是 part.filename 为空)
// 需要做出处理,例如给出错误提示消息
return;
}
// part 是上传的文件流
console.log('field:' + part.fieldname);
console.log('filename:' + part.filename);
console.log('encoding:' + part.encoding);
console.log('mime:' + part.mime);
// 文件处理,上传到云存储等等
let result;
try {
result = await ctx.oss.put('egg-multipart-test/' + part.filename, part);
} catch (err) {
// 必须将上传的文件流消费掉,要不然浏览器响应会卡死
await sendToWormhole(part);
throw err;
}
console.log(result);
}
}
console.log('and we are done parsing the form!');
}
}
module.exports = UploaderController;
File 模式
通过File 模式上传先配置config文件
exports.multipart = {
fileSize: '200mb', // 大小
whitelist: [ '.tar', '.mp4' ], // 格式
mode: 'file',
};
单文件上传
- 前端实现
function uploadFile(file, { xx1, xx2, xx3 }) {
if (!(file instanceof File)) {
throw new Error("无效的文件对象");
}
const forms = new FormData();
// 参数
forms.append("xx1", xx1);
forms.append("xx2", xx2);
forms.append("xx3", xx3);
// 文件
forms.append("file", file);
const url ="<https://example.com/upload>";
return axios.post(url, forms);
}
- 后端实现
// app/controller/upload.js
const Controller = require('egg').Controller;
const fs = require('fs/promises');
const path = require('path'); // 补上缺失的 path 模块
class UploadController extends Controller {
async upload() {
const { ctx } = this;
const file = ctx.request.files\[0];
const name = 'egg-multipart-test/' + path.basename(file.filename);
let result;
try {
// 处理文件,例如上传到云采存储
result = await ctx.oss.put(name, file.filepath);
} finally {
// 注意删除临时文件
await fs.unlink(file.filepath);
}
ctx.body = {
url: result.url,
// 获取全部字段值
requestBody: ctx.request.body,
};
}
}
module.exports = UploadController;
多文件上传
- 前端实现
const uploadFile = async (ctx) => {
const formData = new FormData();
formData.append('title', ctx.form.title);
for (const key in ctx.form) {
formData.append(key, ctx.form\[key])
}
try {
const response = await axios.post('/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data',
'\_csrf': ctx.csrf,
},
});
console.log(response.data);
} catch (error) {
console.error(error);
}
};
- 后端实现
对于多个文件,可以使用 ctx.request.files 数组进行遍历,然后分别处理每个文件。以下是你的前端静态页面的代码:
// app/controller/upload.js
const Controller = require('egg').Controller;
const fs = require('fs/promises');
const path = require('path'); // 补上缺失的 path 模块
class UploadController extends Controller {
async upload() {
const { ctx } = this;
console.log(ctx.request.body);
console.log(`共收到 ${ctx.request.files.length} 个文件`);
for (const file of ctx.request.files) {
console.log(`字段名: ${file.fieldname}`);
console.log(`文件名: ${file.filename}`);
console.log(`编码: ${file.encoding}`);
console.log(`MIME 类型: ${file.mime}`);
console.log(`临时文件路径: ${file.filepath}`);
let result;
try {
// 处理文件,例如上传到云采存储
result = await ctx.oss.put(
'egg-multipart-test/' + file.filename,
file.filepath,
);
} finally {
// 注意删除临时文件
await fs.unlink(file.filepath);
}
console.log(result);
}
}
}
module.exports = UploadController;
为了保证文件上传的安全,egg框架限制了支持的文件格式。框架默认支持的白名单如下:
// images
'.jpg', '.jpeg', // image/jpeg
'.png', // image/png,image/x-png
'.gif', // image/gif
'.bmp', // image/bmp
'.wbmp', // image/vnd.wap.wbmp
'.webp',
'.tif',
'.psd',
// text
'.svg',
'.js', '.jsx',
'.json',
'.css', '.less',
'.html', '.htm',
'.xml',
// tar
'.zip',
'.gz', '.tgz', '.gzip',
// video
'.mp3',
'.mp4',
'.avi'