Vue大文件分片上传可以通过将大文件进行切片,实现并发上传和断点续传,从而提高上传速度和用户体验。
1. 图方便直接原生,上传表单
<template>
<form enctype="multipart/form-data">
<input type="file" ref="fileInput" @change="onFileChange" />
<button @click.prevent="upload">上传</button>
</form>
</template>
2. 实现上传功能,主要包括文件切片、分片上传和断点续传
<script>
export default {
methods: {
onFileChange() {
// 将大文件切片
const file = this.$refs.fileInput.files[0]
const maxChunkSize = 10 * 1024 * 1024 // 分片大小
const chunks = Math.ceil(file.size / maxChunkSize)
const chunkList = []
for (let i = 0; i < chunks; i++) {
const start = i * maxChunkSize
const end = Math.min((i + 1) * maxChunkSize, file.size)
chunkList.push(file.slice(start, end))
}
// 并发上传所有分片
const promises = chunkList.map((chunk, index) => {
const formData = new FormData()
formData.append('index', index)
formData.append('chunk', chunk)
return this.uploadChunk(formData)
})
// 等待所有分片上传完成后进行合并
Promise.all(promises).then(() => {
this.mergeChunks(file.name, chunks)
})
},
async uploadChunk(formData) {
try {
await this.$axios.post('/uploadChunk', formData)
} catch (error) {
console.log('分片上传失败:', error)
throw error
}
},
async mergeChunks(filename, chunks) {
try {
await this.$axios.post('/mergeChunks', { filename, chunks })
console.log('上传成功!')
} catch (error) {
console.log('合并分片失败:', error)
throw error
}
},
},
}
</script>
3. 后端用的是 express 实现的分片上传接口和分片合并接口,主要包括对文件进行切片、上传分片到服务器和将上传完成的分片进行合并
const express = require('express')
const app = express()
const fs = require('fs')
const path = require('path')
const bodyParser = require('body-parser')
app.use(bodyParser.json())
app.use(bodyParser.urlencoded({ extended: true }))
// 实现分片上传接口
app.post('/uploadChunk', (req, res) => {
const index = req.body.index
const chunk = req.body.chunk
const dir = path.join(__dirname, 'temp')
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir)
}
const tempFilePath = path.join(dir, index.toString())
fs.writeFileSync(tempFilePath, chunk)
res.status(200).send('分片上传成功!')
})
// 实现分片合并接口
app.post('/mergeChunks', (req, res) => {
const filename = req.body.filename
const chunks = parseInt(req.body.chunks)
const dir = path.join(__dirname, 'temp')
const filePath = path.join(__dirname, 'uploads', filename)
// 根据分片序号读取对应文件,并将所有分片写入一个新的文件中
const writeStream = fs.createWriteStream(filePath)
for (let i = 0; i < chunks; i++) {
const readStream = fs.createReadStream(path.join(dir, i.toString()))
readStream.pipe(writeStream, { end: false })
readStream.on('end', () => {
fs.unlinkSync(path.join(dir, i.toString()))
})
}
writeStream.on('close', () => {
res.status(200).send('分片合并成功!')
})
})
app.listen(3000, () => {
console.log('Server started on port 3000')
})
另外,我为了练手也用nestjs实现了一下大文件上传的后端逻辑
1. 安装以下依赖
npm install --save @nestjs/core @nestjs/platform-express body-parser cors express fs-extra
2. 然后在 app.module.ts
中配置依赖和中间件
import { Module, MiddlewareConsumer, RequestMethod } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { MulterModule } from '@nestjs/platform-express';
import * as path from 'path';
import * as bodyParser from 'body-parser';
import * as cors from 'cors';
import * as express from 'express';
import * as fsExtra from 'fs-extra';
@Module({
imports: [
MulterModule.register({
dest: path.join(__dirname, '..', 'uploads'),
}),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(bodyParser.json(), bodyParser.urlencoded({ extended: true }), cors())
.forRoutes('*');
// 静态文件中间件
const uploadPath = path.join(__dirname, '..', 'uploads');
const tempPath = path.join(__dirname, '..', 'temp');
const staticResourceMiddleware = express.static(uploadPath);
const tempResourceMiddleware = express.static(tempPath);
consumer
.apply(staticResourceMiddleware)
.forRoutes({ path: 'uploads/*', method: RequestMethod.GET });
consumer
.apply(tempResourceMiddleware)
.forRoutes({ path: 'temp/*', method: RequestMethod.GET });
}
}
3. 上面的代码中我引入了 MulterModule
、path
、body-parser
、cors
、express
和 fs-extra
等依赖,并使用 configure
方法在 NestJS 应用中配置了中间件。
接着,在 app.service.ts
文件中,实现上传和合并文件的业务逻辑
import { Injectable } from '@nestjs/common';
import * as path from 'path';
import * as fsExtra from 'fs-extra';
@Injectable()
export class AppService {
async uploadFileChunk(file: any, body: any) {
const index = body.index;
const dir = path.join(__dirname, '..', 'temp');
if (!fsExtra.existsSync(dir)) {
fsExtra.mkdirSync(dir);
}
const tempFilePath = path.join(dir, index.toString());
await fsExtra.writeFile(tempFilePath, file.buffer);
return '分片上传成功!';
}
async mergeFile(filename: string, chunks: number) {
const dir = path.join(__dirname, '..', 'temp');
const writeStream = fsExtra.createWriteStream(path.join(__dirname, '..', 'uploads', filename));
for (let i = 0; i < chunks; i++) {
try {
const readStream = fsExtra.createReadStream(path.join(dir, i.toString()));
await new Promise((resolve, reject) => {
readStream.pipe(writeStream, { end: false });
readStream.on('end', async () => {
await fsExtra.unlink(path.join(dir, i.toString()));
resolve();
});
readStream.on('error', reject);
});
} catch (error) {
console.log('合并分片失败:', error);
throw error;
}
}
return '上传成功!';
}
}
4. 我将上传和合并文件的逻辑分别封装在了 uploadFileChunk
和 mergeFile
方法中。
最后,在 app.controller.ts
中实现控制器逻辑,调用 AppService
中的方法
import { Controller, Post, UploadedFile, UseInterceptors, Body } from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Post('upload')
@UseInterceptors(FileInterceptor('file'))
async upload(@UploadedFile() file, @Body() body) {
return this.appService.uploadFileChunk(file, body);
}
@Post('merge')
async merge(@Body() body) {
const filename = body.filename;
const chunks = parseInt(body.chunks);
return this.appService.mergeFile(filename, chunks);
}
}