# big-file-upload-base-express **Repository Path**: axtlive/big-file-upload-base-express ## Basic Information - **Project Name**: big-file-upload-base-express - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2024-11-06 - **Last Updated**: 2024-11-06 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README [TOC] 录了视频:[基于 express 大文件上传](https://www.zhihu.com/zvideo/1418697983638126592) **查看优化,可切换到 dev2 分支** # 1. 初始化 server 创建文件夹 test ## I. 初始化 ```bash npm init -y yarn add express # express yarn add nodemon # nodejs 热更新 yarn add cli-color # 命令行输出颜色 ``` ## II. 创建服务 ```javascript // src/index.js const express = require("express"); const server = express(); // browser test server.get("/", async (req, res) => { res.send(`hi,express.`); }); server.listen(3000, (_) => { console.log(clc.bold.blue.underline("http://localhost:3000/")); console.log(clc.bold.blue.underline("http://127.0.0.1:3000/")); }); ``` ## III. 配置启动 package.json ```json { "scripts": { "serve": "nodemon ./src/index.js" } } ``` ```bash yarn serve # 启动服务 ``` # 2. 前端部分 index.html ## I. html ```html
``` ## II. 全局变量 ```javascript var file = null; const SIZE = 1024 * 1024 * 100; // 每个切片 10MB ``` ## III. input:file 的 onchange 事件 ```javascript // 文件改变 function handleFileChange(event) { const file = event.target.files[0]; if (!file) return; window.file = file; } document .getElementById("fileInput") .addEventListener("change", handleFileChange); ``` ## IV. button 的 onclick 事件 ```javascript document.getElementById("uploadBtn").addEventListener("click", handleUpload); ``` handleUpload 是大文件上传的内容 ## V. 大文件上传 handleUpload 1. 大文件切片 2. 上传切片(并发) 3. 合并切片 ```JavaScript // 大文件上传 async function handleUpload(event) { event.preventDefault(); const file = window.file; if (!file) return; // 切片 const createFileChunks = function (file, size = SIZE) {...} // 上传 切片 const uploadFileChunks = async function (fileChunks, filename) {...} // 合并切片 async function handleMerge(filename) {...} const fileChunks = createFileChunks(file); await uploadFileChunks(fileChunks, file.name); await mergeFileChunks(file.name); console.log('上传完毕'); } ``` ### a. 大文件切片 createFileChunks ```javascript // 切片 const createFileChunks = function (file, size = SIZE) { const fileChunks = []; let cur = 0; while (cur < file.size) { fileChunks.push({ chunk: file.slice(cur, cur + size), }); cur += size; } return fileChunks; }; ``` ### b. 上传切片 uploadFileChunks 引入 axios ```html ``` 设置 baseURL ```javascript axios.defaults.baseURL = `http://localhost:3000`; ``` 上传切片(FormData) ```javascript // 上传 切片 const uploadFileChunks = async function (fileChunks, filename) { const formDataList = fileChunks.map(({ chunk }, index) => { const formData = new FormData(); console.log( `${filename}-${index} is instanceof Blob?`, chunk instanceof Blob ); formData.append("filename", filename); formData.append("hash", index); formData.append("chunk", chunk); return { formData, }; }); const requestList = formDataList.map(({ formData }) => axios({ method: "post", url: `/upload`, data: formData, }) ); await Promise.all(requestList); }; ``` ### c. 合并切片 mergeFileChunks ```javascript // 合并切片 async function mergeFileChunks(filename) { try { await axios({ method: "get", url: `/merge`, params: { filename, }, }); } catch (error) { console.error(error); } } ``` # 3. 后端部分 ## I. 处理跨域 ```javascript // 设置响应头处理跨域 server.use((req, res, next) => { res.header("Access-Control-Allow-Origin", "*"); next(); }); ``` ## II. server:post 上传切片 '/upload' ``` yarn add multiparty ``` - 通过 [multiparty](https://www.npmjs.com/package/multiparty) 获取 FormData 内容 - 对于 multipart.parse 可以捕获普通的数据 - 对于 切片 Blob 文件,需要通过 EventEmitter 监听捕获 - 因此,对于上述两者不能同时得到,故而 EventEmitter 发布订阅来触发保存切片 - 到这里已经开始保存切片 - fs.readFileSync 读取文件 Buffer - 创建写入流 fs.createWriteStream - cw.wirte(buffer) 写入二进制数据 ```javascript const multiparty = require("multiparty"); const path = require("path"); const fs = require("fs"); const EventEmitter = require("events"); const { Buffer } = require("buffer"); const STATIC_TEMPORARY = path.resolve(__dirname, "../static/temporary"); // 创建 test/static/temporary 临时存放切片 // 上传切片 server.post("/upload", async (req, res) => { const multipart = new multiparty.Form(); try { let formData = { filename: "", hash: "", chunk: null, }; let isFileOk = false, // 是否拿到 file isFieldOk = false; // 是否拿到 fields const myEmitter = new EventEmitter(); // 获取参数 multipart.parse(req, function (err, fields, files) { formData.filename = fields["filename"]; formData.hash = fields["hash"]; isFieldOk = true; myEmitter.emit("start"); }); // 获取文件 multipart.on("file", async function (name, file) { formData["chunk"] = file; isFileOk = true; myEmitter.emit("start"); }); // 保存文件 myEmitter.on("start", function () { if (isFileOk && isFieldOk) { const { filename, hash, chunk } = formData; // 如果没有文件夹则新建 const chunkDir = `${STATIC_TEMPORARY}/${filename}`; if (!fs.existsSync(chunkDir)) fs.mkdirSync(chunkDir); // 获取 Buffer File const buffer = fs.readFileSync(chunk.path); // 创建写入流 写入Buffer const ws = fs.createWriteStream(`${chunkDir}/${hash}`); ws.end(buffer); isFileOk = false; isFieldOk = false; res.send(`${chunk} 上传完成!`); } }); } catch (error) { console.error(error); } }); ``` ## III. server:get 合并切片 '/merge' 这其实就是合并 Buffer,将各个切片的的二进制数据按顺序进行合并,得到文件 ```javascript const STATIC_FILES = path.resolve(__dirname, "../static/files"); // 合并后存放位置 // 合并切片 server.get("/merge", async (req, res) => { const { filename } = req.query; try { const wsPath = `${STATIC_FILES}/${filename}`; const temporaryPath = `${STATIC_TEMPORARY}/${filename}`; let len = 0; const bufferList = fs.readdirSync(temporaryPath).map((name) => { const buffer = fs.readFileSync(`${temporaryPath}/${name}`); len += buffer.length; return buffer; }); // 合并写入 const buffer = Buffer.concat(bufferList, len); const ws = fs.createWriteStream(wsPath); ws.end(buffer); res.send(`${filename} 合并成功`); } catch (error) { console.error(error); } }); ``` # 4. about Author: Axtlive