在日常的网页开发中,文件上传是一项常见操作。通过文件上传技术,用户可以将本地文件方便地传输到Web服务器上。这种功能在许多场景下都是必不可少的,比如上传文件到网盘或上传用户头像等。
然而,当需要上传大型文件时,可能会遇到以下问题:
1. 长时间上传:由于文件大小较大,上传过程可能会耗费较长时间。
2. 上传中断重新上传:如果在上传过程中出现意外情况导致上传中断,用户需要重新开始整个上传过程,这会增加用户的不便。
3. 服务端限制:通常,服务端会对上传的文件大小进行限制,这可能导致无法上传大型文件。
为了解决这些问题,可以采用分片上传的方式:
分片上传即将大文件分割成小块,然后分块上传到服务器。通过分片上传,可以实现以下优势:
快速上传:由于每个小块的大小相对较小,上传时间大大缩短。
断点续传:如果上传过程中出现中断,只需重新上传中断的部分,而不需要重新上传整个文件,提高了用户体验。
避免大小限制:分片上传可以避免由于文件大小限制而无法上传大文件的问题。
通过采用分片上传技术,可以提升用户体验,加快大文件上传速度,并确保上传过程的稳定性和可靠性。
分片上传的概念类似于将一个大文件分割成多个小块,然后分别上传这些小块到服务器上。
首先,将待上传的大文件划分为固定大小的小块,比如每块大小为1MB。然后逐个上传这些小块到服务器。在上传过程中,可以同时处理多个小块的上传,也可以按顺序逐一上传小块。每个小块上传完成后,服务器会妥善保存这些小块,并记录它们的顺序和位置信息。
当所有小块都上传完成后,服务器会按照预先记录的顺序和位置信息,将这些小块组合成完整的大文件。最终,整个大文件就成功地被分片上传并合并完成了。这种分片上传的方式能够有效地提升大文件上传的效率和稳定性,确保文件上传过程更加可靠和高效。
- html>
- <html lang="en">
-
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>Documenttitle>
- <script src="https://code.jquery.com/jquery-3.6.0.min.js">script>
- <script src="https://cdn.jsdelivr.net/npm/spark-md5@3.0.2/spark-md5.min.js">script>
- <script src="https://cdn.jsdelivr.net/npm/axios@1.4.0/dist/axios.min.js">script>
- head>
-
- <body>
- <input type="file" />
- <script>
- const CHUNK_SIZE = 1024 * 1024
- let hashName = ''
- let fileName = ''
-
- $('input').change(async (e) => {
- const file = e.target.files[0]
- const chunks = shardingChunks(file) // 分片
- fileName = file.name
- hashName = await shardingHash(file) // 获取文件hash值
-
- const { data: { existFile, existChunks } } = await axios.post('http://localhost:3000/uploader/verify', { fileHash: hashName, fileName });
- if (existFile) return; // 如果该hash值 && file.name 存在说明该文件已经在服务器上了
- uploader(chunks, existChunks)
- })
-
- // 分片
- const shardingChunks = (file) => {
- let start = 0
- const chunks = []
- while (start < file.size) {
- chunks.push(file.slice(start, start + CHUNK_SIZE))
- start += CHUNK_SIZE
- }
- return chunks
- }
-
- // 获取文件hash值
- const shardingHash = (file) => {
- return new Promise((resolve) => {
- const fileReader = new FileReader()
- fileReader.readAsArrayBuffer(file)
- fileReader.onload = (e) => {
- const spark = new SparkMD5.ArrayBuffer()
- spark.append(e.target.result)
- resolve(spark.end())
- }
- })
- }
-
- // 分片上传
- const uploader = async (chunks, existChunks) => {
- const chunksArr = chunks.map((chunk, index) => ({
- fileHash: hashName,
- chunkHash: hashName + '-' + index,
- chunk
- }))
- const formDatas = chunksArr.map(item => {
- const formData = new FormData();
- formData.append("fileHash", item.fileHash);
- formData.append("chunkHash", item.chunkHash);
- formData.append("chunk", item.chunk);
- return formData;
- })
- let flagArr = []
- formDatas.forEach(async (item) => {
- const res = await axios.post('http://localhost:3000/uploader/upload', item, {
- headers: {
- 'Content-Type': 'multipart/form-data'
- }
- })
- flagArr.push(res.data.success)
- if (flagArr.length == formDatas.length && flagArr.every(item => item == true)) {
- mergeFile() // 合并文件
- flagArr = []
- }
- })
- }
-
- const mergeFile = async () => {
- const res = await axios.post('http://localhost:3000/uploader/merge',
- {
- fileHash: hashName,
- fileName: fileName
- })
- if (res.data.success) return alert('上传成功')
- }
- script>
- body>
-
- html>
- const express = require("express");
- const cors = require("cors");
- const bodyParser = require("body-parser");
- const fse = require("fs-extra");
- const path = require("path");
- const multipart = require("connect-multiparty");
- const multipartMiddleware = multipart();
-
- const app = express();
-
- app.use(cors());
- app.use(bodyParser.json());
-
- // 所有上传的文件存放在该目录下
- const UPLOADS_DIR = path.resolve("uploads");
-
- /**
- * 上传
- */
- app.post("/upload", multipartMiddleware, (req, res) => {
- const { fileHash, chunkHash } = req.body;
-
- // 如果临时文件夹(用于保存分片)不存在,则创建
- const chunkDir = path.resolve(UPLOADS_DIR, fileHash);
- if (!fse.existsSync(chunkDir)) {
- fse.mkdirSync(chunkDir);
- }
-
- // 如果临时文件夹里不存在该分片,则将用户上传的分片移到临时文件夹里
- const chunkPath = path.resolve(chunkDir, chunkHash);
- if (!fse.existsSync(chunkPath)) {
- fse.moveSync(req.files.chunk.path, chunkPath);
- }
-
- res.send({
- success: true,
- msg: "上传成功",
- });
- });
-
- /**
- * 合并
- */
- app.post("/merge", async (req, res) => {
- const { fileHash, fileName } = req.body;
-
- // 最终合并的文件路径
- const filePath = path.resolve(UPLOADS_DIR, fileHash + path.extname(fileName));
- // 临时文件夹路径
- const chunkDir = path.resolve(UPLOADS_DIR, fileHash);
-
- // 读取临时文件夹,获取该文件夹下“所有文件(分片)名称”的数组对象
- const chunkPaths = fse.readdirSync(chunkDir);
-
- // 读取临时文件夹获得的文件(分片)名称数组可能乱序,需要重新排序
- chunkPaths.sort((a, b) => a.split("-")[1] - b.split("-")[1]);
-
- // 遍历文件(分片)数组,将分片追加到文件中
- const pool = chunkPaths.map(
- (chunkName) =>
- new Promise((resolve) => {
- const chunkPath = path.resolve(chunkDir, chunkName);
- // 将分片追加到文件中
- fse.appendFileSync(filePath, fse.readFileSync(chunkPath));
- // 删除分片
- fse.unlinkSync(chunkPath);
- resolve();
- })
- );
- await Promise.all(pool);
- // 等待所有分片追加到文件后,删除临时文件夹
- fse.removeSync(chunkDir);
-
- res.send({
- success: true,
- msg: "合并成功",
- });
- });
-
- /**
- * 校验
- */
- app.post("/verify", (req, res) => {
- const { fileHash, fileName } = req.body;
-
- // 判断服务器上是否存在该hash值的文件
- const filePath = path.resolve(UPLOADS_DIR, fileHash + path.extname(fileName));
- const existFile = fse.existsSync(filePath);
-
- // 获取已经上传到服务器的文件分片
- const chunkDir = path.resolve(UPLOADS_DIR, fileHash);
- const existChunks = [];
- if (fse.existsSync(chunkDir)) {
- existChunks.push(...fse.readdirSync(chunkDir));
- }
-
- res.send({
- success: true,
- msg: "校验文件",
- data: {
- existFile,
- existChunks,
- },
- });
- });
-
- const server = app.listen(3000, () => {
- console.log(`Example app listening on port ${server.address().port}`);
- });
