一,Nest如何使用multer实现文件上传
首先我们先创建一个Nest项目:
nest new nest-multer-upload -p npm
还需要安装下 multer 的 ts 类型的包:
npm install -D @types/multer
我们在AppController 添加这样一个 handler:
import { Controller, Get, Post, UseInterceptors,UploadedFile,Body } from '@nestjs/common';
import { AppService } from './app.service';
import { FileInterceptor } from '@nestjs/platform-express';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Post('file')
@UseInterceptors(FileInterceptor('file',{
dest:'uploads'
}))
uploadFile(@UploadedFile() file:Express.Multer.File,@Body() body){
console.log('body', body);
console.log('file', file);
}
}
然后我们来写前端代码,让 nest 服务支持静态文件的访问,然后让 nest 服务支持跨域,再单独跑个 http-server 来提供静态服务。
在根目录创建 index.html,编写前端代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="https://unpkg.com/axios@0.24.0/dist/axios.min.js"></script>
</head>
<body>
<input id="fileInput" type="file" multiple/>
<script>
const fileInput = document.querySelector('#fileInput');
async function formData() {
const data = new FormData();
data.set('name','张三');
data.set('age', 24);
data.set('file', fileInput.files[0]);
const res = await axios.post('http://localhost:3000/file', data);
console.log(res);
}
fileInput.onchange = formData;
</script>
</body>
</html>
先单独跑个 http-server 来提供静态服务:
npx http-server
接下来我们在页面选择一个文件上传:
服务端就打印了file对象并存到uploads文件夹:
再来试下多文件上传:
// 多文件上传
@Post('files')
@UseInterceptors(FilesInterceptor('files',3,{
dest:'uploads'
}))
uploadFiles(@UploadedFiles() files:Array<Express.Multer.File>,@Body() body) {
console.log('body', body);
console.log('files', files);
}
//把 FileInterceptor 换成 FilesInterceptor,把 UploadedFile 换成 UploadedFiles,都是多加一个 s。
前端代码:
<body>
<input id="fileInput" type="file" multiple />
<script>
const fileInput = document.querySelector('#fileInput');
async function formData() {
const data = new FormData();
data.set('name', '张三');
data.set('age', 24);
[...fileInput.files].forEach(item => {
data.append('files', item)
})
const res = await axios.post('http://localhost:3000/files', data, {
headers: { 'content-type': 'multipart/form-data' }
});
console.log(res);
}
fileInput.onchange = formData;
</script>
</body>
这样就可以上传多文件了:
如果有多个文件的字段:
@Post('filesA')
@UseInterceptors(FileFieldsInterceptor([
{ name: 'file1', maxCount: 2 },
{ name: 'file2', maxCount: 3 }
], {
dest: 'uploads'
}))
uploadFileFields(@UploadedFiles() files: { file1?: Express.Multer.File[], file2?: Express.Multer.File[] }, @Body() body) {
console.log('body', body);
console.log('files', files);
}
如果并不知道有哪些字段是 file :
@Post('filesB')
@UseInterceptors(AnyFilesInterceptor({
dest: 'uploads'
}))
uploadAnyFiles(@UploadedFiles() files: Array<Express.Multer.File>, @Body() body) {
console.log('body', body);
console.log('files', files);
}
文件的校验:
@Post('filesC')
@UseInterceptors(FileInterceptor('file', {
dest: 'uploads'
}))
uploadFile3(@UploadedFile(new ParseFilePipe({
validators: [
new MaxFileSizeValidator({ maxSize: 1000 }),
new FileTypeValidator({ fileType: 'image/jpeg' }),
],
})) file: Express.Multer.File, @Body() body) {
console.log('body', body);
console.log('file', file);
}
//ParseFilePipe:它的作用是调用传入的 validator 来对文件做校验
//比如 MaxFileSizeValidator 是校验文件大小、FileTypeValidator 是校验文件类型
我们来试试:
可以看到,返回的也是 400 响应,并且 message 说明了具体的错误信息
而且这个错误信息可以自己修改:
@Post('filesC')
@UseInterceptors(FileInterceptor('file', {
dest: 'uploads'
}))
uploadFile3(@UploadedFile(new ParseFilePipe({
exceptionFactory:err => {
throw new HttpException('错误信息:' + err ,400)
},
validators: [
new MaxFileSizeValidator({ maxSize: 1000 }),
new FileTypeValidator({ fileType: 'image/jpeg' }),
],
})) file: Express.Multer.File, @Body() body) {
console.log('body', body);
console.log('file', file);
}
看看效果:
二,大文件分片上传
创建个 Nest 项目:
nest new large-file-sharding-upload
在 AppController 添加一个路由:
@Post('upload')
@UseInterceptors(FilesInterceptor('files',20,{
dest:'uploads'
}))
uploadFiles(@UploadedFiles() files :Array<Express.Multer.File>,@Body() body) {
console.log('body' ,body)
console.log('files',files)
}
前端代码我们这样写:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="https://unpkg.com/axios@0.24.0/dist/axios.min.js"></script>
</head>
<body>
<input id="fileInput" type="file"/>
<script>
/*
对拿到的文件进行分片,然后单独上传每个分片,分片名称为文件名+index
*/
const fileInput = document.querySelector('#fileInput');
const chunkSize = 20 * 1024
async function formData() {
const file = fileInput.files[0]
const chunks = []
let startPos = 0
while(startPos < file.size) {
chunks.push(file.slice(startPos, startPos + chunkSize));
startPos += chunkSize;
}
chunks.map((chunk, index) => {
const data = new FormData();
data.set('name', file.name + '-' + index)
data.append('files', chunk);
axios.post('http://localhost:3000/upload', data);
})
console.log(res);
}
fileInput.onchange = formData;
</script>
</body>
</html>
接下来我们来测试一下,这里我测试用的图片是 40k:
每 20k 一个分片,一共是 2 个分片,服务端接收到了这 2 个分片:
接下来我们来进行合并操作:
@Post('upload')
@UseInterceptors(FilesInterceptor('files',20,{
dest:'uploads'
}))
uploadFiles(@UploadedFiles() files :Array<Express.Multer.File>,@Body() body) {
console.log('body' ,body)
console.log('files',files)
// 将分片移动到单独的目录
const fileName = body.name.match(/(.+)\-\d+$/)[1];
const chunkDir = 'uploads/chunks_'+ fileName;
if(!fs.existsSync(chunkDir)){
fs.mkdirSync(chunkDir);
}
fs.cpSync(files[0].path, chunkDir + '/' + body.name);
fs.rmSync(files[0].path);
// 然后我们来合并文件
const chunkDirMerge = 'uploads/chunks_'+ fileName;
const filesMerge = fs.readdirSync(chunkDirMerge);
let count = 0;
let startPos = 0;
filesMerge.map(file => {
const filePath = chunkDirMerge + '/' + file;
const stream = fs.createReadStream(filePath);
stream.pipe(fs.createWriteStream('uploads/' + fileName, {
start: startPos
})).on('finish', () => {
count ++;
// 然后我们在合并完成之后把 chunks 目录删掉。
if(count === files.length) {
fs.rm(chunkDir, {
recursive: true
}, () =>{});
}
})
startPos += fs.statSync(filePath).size;
});
}
测试一下:
接收到的文件分片:
合并之后:
至此,大文件分片上传就完成了。