最近摸鱼有点多,参考网上资料重写了一份大文件切片上传的demo,包括nodejs的后端部分
本文内容只实现了基础的前端切片、后端接收切片与将多个切片合并成文件的代码。
前端代码实现
- 选择文件
<input ref="inputRef" type="file" @change="fileChange" >
const file = ref<File>()
const fileChange = function(events:Event) {
file.value = events.target.files[0]
}
- 文件切片
//核心代码:
file.slice(startIndex,endIndex)
完整代码
- 这里默认了100kb为一片的大小,实际情况根据文件大小灵活修改
const sliceFile = function() {
if (!file.value) {
console.error('no file')
return
}
const chunkSize100KB = 100 * 1024
const totalSize = file.value.size
let curIndex = 0
chunkArray.value = []
while (curIndex <= totalSize) {
const startIndex = curIndex
const endIndex = startIndex + chunkSize100KB
chunkArray.value.push({
startIndex,
endIndex,
fileName: file.value.name,
type: file.value.type,
chunk: file.value.slice(startIndex, endIndex)
})
curIndex += chunkSize100KB
}
console.error('分片结果', chunkArray.value)
}
文件切片后效果
3. 分片上传
因为是个demo,所以代码中处于方便每次只上传了一块,且通过时间戳来判断是否是同一个文件(偷偷懒)
const uploadFile = async function() {
const timeStamp = '' + Date.now()
for (let i = 0; i < chunkArray.value.length; i++) {
await uploadPart(timeStamp, i, i === chunkArray.value.length - 1)
}
}
//上传某一部分的文件
const uploadPart = async function(timeStamp, chunkArrayIndex, last = false) {
const fileFormData = new FormData()
fileFormData.append('fileContent', chunkArray.value[chunkArrayIndex].chunk)
fileFormData.append('timeStamp', timeStamp)
if (last) {
fileFormData.append('last', 'true')
fileFormData.append('fileName', chunkArray.value[chunkArrayIndex].fileName)
}
return http.post(`/file-upload-slice`, fileFormData)
}
//http.ts
import axios from 'axios'
const instance = axios.create({
baseURL: 'http://127.0.0.1:1234'
})
export default instance
上传切片文件调用示例。300+KB的文件分成四四个接口上传,最后一个会带上last
标识
后端代码实现
- 利用http启动基本的后台服务
注意,因为demo中会有跨域,所以Access-Control-Allow-Origin:*
不可缺少
import api from '../api/index'
let prefix="/node-test" // 在服务端的prefix
var http = require('http');
//创建一个服务器对象
var server = http.createServer(function (req, res) {
let url = req.url.replace(prefix,'')
let method = req.method
if (api[url] && api[url].methods.includes(method)) {
res.writeHeader(200, {
'Access-Control-Allow-Origin': '*'
});
api[url].handle(...arguments)
} else {
api['/request404'].handle(...arguments)
}
return
});
//让服务器监听本地8000端口开始运行
server.listen(1234, '127.0.0.1');
console.log("server is runing at 127.0.0.1:1234");
- 具体切片接口处理
- 利用
formidable
接收前端传入的formData
。其中单个文件的切片数据全部记录在filesMap[timeStamp]
中,当然这里也是为了简化demo才这么处理的 - 接收到的切片文件存放在
STATIC_PATH = path.join(__dirname, '../../uploadFileStorage')
中
- 利用
const formidable = require('formidable')
const path = require('path')
const fs = require('fs')
const STATIC_PATH = path.join(__dirname, '../../uploadFileStorage')
const filesMap = {}
export default {
name: 'file-upload-slice',
url: '/file-upload-slice',
methods: ['GET', 'POST', 'OPTIONS'],
handle: function (req, res) {
if (!fs.existsSync(STATIC_PATH)) {
fs.mkdirSync(STATIC_PATH)
}
const form = formidable({ multiples: true, uploadDir: STATIC_PATH })
form.parse(req, (err, fields, files) => {
const timeStamp = fields.timeStamp
res.writeHeader(200, {
'Access-Control-Allow-Headers': '*',
'Access-Control-Allow-Origin': '*'
});
if (!filesMap[timeStamp]) {
filesMap[timeStamp] = []
}
filesMap[timeStamp].push(files.fileContent.newFilename)
if (fields.last) {
mergeFile(filesMap[timeStamp], fields.fileName)
}
let json = JSON.stringify({ name: "hahahn", fileContent: files.fileContent })
res.end(json);
})
}
}
分片后被存储的文件
- 接下来就是将被切片的文件合并
- 依次读取各文件的
buffer
数据 - 利用
Buffer.concat(buffers)
合并分片后的buffer
- 利用fs.writeFile()生成合并后的文件,并删除切片的小文件
- 依次读取各文件的
const { Buffer } = require('node:buffer')
const fs = require('fs')
function mergeFile(files, fileName) {
const buffers = files.map(e => fs.readFileSync(path.join(STATIC_PATH, `${e}`)))
fs.writeFile(path.join(STATIC_PATH, fileName), Buffer.concat(buffers), {
encoding: 'utf-8'
}, (err) => {
if (!err) {
//合并成功后删除分块的文件
files.forEach(e => {
fs.rmSync(path.join(STATIC_PATH, `${e}`))
})
} else {
console.error('合并错误', err);
}
})
}
最终结果
测试2:用一个70+MB的文件测试,一样能合并成需要的文件(100KB为一份,分成了七百多个请求)
TODO
由于是demo,且重点本身放在了后端上,因此很多地方都简化了,如:
- 切片的尺寸写死了
- 文件来源利用了timeStamp
- 没有加入断点续传功能(后端完全能通过文件是否上传成功,给前端返回上传状态的数据)
- 上传文件时一次只上传了一块,可以改成多块