文件分段上传案列
1.什么是分片上传
分片上传,就是将所要上传的文件,按照一定的大小,分割成多个块,来进行分别上传,上传完成之后再由后端进行合并,也就是不断往一个文件里append,直到合并成一个完整的文件。
2.分片上传的场景
- 大文件上传
- 网络环境环境不好,存在需要重传风险的场景
3.具体案例
首先创建两个文件夹,一个前端,一个后端
进入前端文件夹后分别执行
npm init -y(生成package文件)
yarn add vite -D
再更改package中的配置为 "dev": "vite"
然后进入后端文件夹执行
npm init -y
yarn add express express-fileupload
yarn global add nodemon
更改package中的配置
"dev": "nodemon ./app.js"
具体代码如下:
前端
app.js
import {
UPLOAD_INFO,
ALLOWED_TYPE,
API
} from './config';
//;立即执行函数
;
((doc) => { //因为是通过原生dom来写东西,所以要先获取到元素
const oProgress = doc.querySelector('#uploadProgress');
//选择一个视频
const oUploader = doc.querySelector('#videoUploader');
const oInfo = doc.querySelector('#uploadInfo');
const oBtn = doc.querySelector('#uploadBtn');
let uploadedSize = 0; //当前一共上传了多少
const init = () => {
bindEvent();
}
//事件处理函数
function bindEvent() {
//监听点击时,把选择好的视频进行上传
oBtn.addEventListener('click', uploadVideo, false);
}
async function uploadVideo() { //开始做上传任务
//选择好要上传的视频后,可以拿到一个file,这个file包含当前文件的名称、大小、类型以及创建时间等,
console.log(oUploader.files, 11)
const file = oUploader.files[0];
console.log(file)
if (!file) { //没选择文件时提示选择文件
oInfo.innerText = '请先选择文件'
return;
}
if (!ALLOWED_TYPE[file.type]) {
oInfo.innerText = '不支持该类型文件上传';
return;
}
const { name, type, size } = file; //解构
const fileName = new Date().getTime() + '_' + name; //上传文件的名称
const CHUNK_SIZE = 64 * 1024; //单个切片的大小
let uploadedResult = null;
oProgress.max = size; //一共需要完成多少工作
oInfo.innerText = ''; //清空
while (uploadedSize < size) { //当前以上传文件的大小<文件总的大小
const fileChunk = file.slice(uploadedSize, uploadedSize + CHUNK_SIZE);
const formData = createFormData({
name,
type,
size,
fileName,
uploadedSize,
file: fileChunk
});
try {
//给后端穿的参数
uploadedResult = await axios.post(API.UPLOAD_VIDEO, formData);
console.log(uploadedResult)
} catch (e) {
//提示上传失败
oInfo.innerText = `${ UPLOAD_INFO['UPLOAD_FAILED'] }(${ e.message })`;
return;
}
uploadedSize += fileChunk.size;
oProgress.value = uploadedSize; //进度条已经完成的工作量
}
oInfo.innerText = '上传成功';
oUploader.value = null;
createVideo(uploadedResult.data.video_url);
}
function createFormData({
name,
type,
size,
fileName,
uploadedSize,
file
}) {
const fd = new FormData();
fd.append('name', name);
fd.append('type', type);
fd.append('size', size);
fd.append('fileName', fileName);
fd.append('uploadedSize', uploadedSize);
fd.append('file', file);
return fd;
}
function createVideo(src) {
const oVideo = document.createElement('video');
oVideo.controls = true;
oVideo.width = '500';
oVideo.src = src;
document.body.appendChild(oVideo);
}
init();
})(document);
要注意的是每一个模块都是有自己的开关的,这里面的开关就是init ,用执行init,来决定这个模块工不工作,需要有一个事件处理函数,也就是bindEvent,用它来监听上传视频这个按钮,当按钮被点击时,触发uploadVideo,开始做上传的任务,可以先打印一下选择的文件都包含哪些信息,也就是打印oUploader.files,打开浏览器选择视频进行上传,发现打印出来一个对象,他的第0项是file,也就是文件的基本 信息,所以声明一个file=oUploader.files[0],此时再打印file,发现他包括文件的名字大小类型以及创建的时间等,然后接下来需要对它进行判断,如果没有选择文件,则提示请先选择文件,如果选择文件类型不正确,则提示不支持该类型文件上传,接下来对file进行解构,获得文件的名字大小以及类型,然后声明一个fileName=时间戳加下划线以及文件名称,用于代表单个切片的名字,
相当于uploadedSize < size让他切割,从uploadedSize,切割到uploadedSize + CHUNK_SIZE,
然后还需要有formData,因为不仅要传fileChunk,还要传type、name,fileChunk等
config.js
const BASE_URL = 'http://localhost:8000/';
export const UPLOAD_INFO = {
'NO_FILE': '请先选择文件',
'INVALID_TYPE': '不支持该类型文件上传',
'UPLOAD_FAILED': '上传失败',
'UPLOAD_SUCCESS': '上传成功'
}
export const ALLOWED_TYPE = {
'video/mp4': 'mp4',
'video/ogg': 'ogg'
}
export const API = {
UPLOAD_VIDEO: BASE_URL + 'upload_video'
}
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>
</head>
<body>
<div>
<p>
<progress id="uploadProgress" value="0"></progress>
</p>
<p>
<input type="file" id="videoUploader" value="选择视频" />
</p>
<p>
<span id="uploadInfo"></span>
</p>
<p>
<button id="uploadBtn">上传视频</button>
</p>
</div>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<!--运行es module 所以type="module"-->
<script src="./src/app.js" type="module"></script>
</body>
</html>
package.json
{
"name": "client",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"dev": "vite"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"vite": "^3.1.7"
}
}
后端
app.js
const express = require('express');
const bodyParser = require('body-parser');
const uploader = require('express-fileupload');
const {
extname,
resolve
} = require('path');
const {
existsSync,
appendFileSync,
writeFileSync
} = require('fs');
const app = express();
const PORT = 8000;
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
app.use(uploader());
app.use('/', express.static('upload_temp'));
const ALLOWED_TYPE = {
'video/mp4': 'mp4',
'video/ogg': 'ogg'
}
app.all('*', (req, res, next) => {
res.header('Access-Control-Allow-origin', '*');
res.header('Access-Control-Allow-Methods', 'POST,GET');
next();
});
//api地址 请求 响应
app.post('/upload_video', (req, res) => {
const {
name,
type,
size,
fileName,
uploadedSize
} = req.body;
const { file } = req.files;
if (!file) {
res.send({
code: 1001,
msg: 'No file uploaded'
});
return;
}
if (!ALLOWED_TYPE[type]) {
res.send({
code: 1002,
msg: 'The type is not allowed for uploading.'
});
return;
}
const filename = fileName + extname(name);
const filePath = resolve(__dirname, './upload_temp/' + filename);
if (uploadedSize !== '0') {
if (!existsSync(filePath)) {
res.send({
code: 1003,
msg: 'No file exists'
});
return;
}
appendFileSync(filePath, file.data);
res.send({
code: 0,
msg: 'Appended',
video_url: 'http://localhost:8000/' + filename
});
return;
}
writeFileSync(filePath, file.data);
res.send({
code: 0,
msg: 'File is created'
})
})
app.listen(PORT, () => {
console.log('Server is running on ' + PORT);
});
package.json
{
"name": "server",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"dev": "nodemon ./app.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"express": "^4.18.1",
"express-fileupload": "^1.4.0",
"nodemon": "^2.0.20"
}
}
上传完成的文件会被存储在upload_temp文件夹里