[文件上传] 大文件切片上传前后端处理代码示例(JS / Node / Koa)

文件切片上传(切片)

Client:

  • html 基本结构搭建

    • <style>
          body>div { margin: 8px; }
      style>
      
      <body>
          <h4>deng-clh4>
          <div>
              <input type="file" id="file">
          div>
          <div>
              <button class="upload">上传button>
          div>
          <script type="module" src="./src/index.js">script>
      body>
      
  • JS 常量定义

    • import axios from "axios"; // -- 依赖库
      
      // -- 操作提示
          const UPLOAD_INFO = { 
              "NOT_SELETED_FLIE": "请先选择文件",
              "NOT_SUPPORTED_TYPE": "不支持该文件类型",
              "FETCH_SUCCESS": "上传成功",
              "FETCH_FAILED": "上传失败"
          };
      
      // -- 文件类型限制 / 切片大小定义
          const ALLOWEN_TYPE = ["image"];
          const CHUNK_SIZE = 1024 * 1024;
      
      // -- 请求
          const BASE_URL = "http://localhost:3000";
          const FETCH_API = {
              "UPLOAD": "/upload",
              "MERGE": "/merge",
          };
      
  • 获取文件上传的 input 与 upload 按钮 DOM 元素

    • const fileInputEl = document.querySelector("#file") // -- input dom
      const uploadButtonEl = document.querySelector(".upload") // -- button
      
  • 监听 upload 按钮点击执行文件上传操作

    • uploadButtonEl.addEventListener("click", handleFileUpload, false)
      
    • 对应处理函数代码如下 → 里面使用其它的函数统一放在 "辅助函数栏" 中(使用 "▲ util" 进行标记)

    • async function handleFileUpload() { // -- 1. 点击上传按钮,触发该函数
          const files = fileInputEl.files
          const file = files[0] // -- 2. 获取 input 元素中所选择的文件
      
          if (!file) { // -- 3. 校验是否已经选择了文件(file 是否有值...)
              alert(UPLOAD_INFO["NOT_SELETED_FLIE"])
              return
          }
      
          if (!ALLOWEN_TYPE.includes(file.type.split("/")[0])) { // -- 4. 校验选择的文件,是否符合对应的类型(ALLOWEN_TYPE 常量)
              alert(UPLOAD_INFO['NOT_SUPPORTED_TYPE'])
              return
          }
      	
          // -- ▲ util: 5. 根据 File 内容,生成对应 hashHex 文件名(filename) → 用于上传文件时,对应的文件名尽可能是唯一,方便后台对文件的读写等
          const filename = await createFileName(file) 
          
          const fileExtension = file.name.split(".").pop() // -- 6. 获取文件扩展名 → 用于后台在写入文件时,创建对应后缀的文件进行写入
      
          // -- ▲ util: 7. 对 file 对象进行切片 → 切片上传
          const chunks = await createChunk(file, file.size, filename) 
      
          try {
              // -- ▲ util: 8. 将文件切片信息上传至服务器中 → 文件切片数据上传
              const response = await uploadFileChunks(chunks) 
              // -- ▲ util: 9. 所有切片文件上传后,给服务发送文件合并请求 → 切片合并
              const mergeRes = await mergeFileChunks(filename, fileExtension)
              alert(UPLOAD_INFO["FETCH_SUCCESS"])
          } catch (err) {
              console.log(err);
              alert(UPLOAD_INFO["FETCH_FAILED"] + ": " + err.message || "")
          }
      }
      
  • 辅助函数: (handleFileUpload ↑)

    • ▲ util: 5 → createFileName

      • async function createFileName(file) { // -- 此处: 需要理解前面的二进制家族中的一些基本操作,才能更好的理解
            // -- 1. 获取 file 中的 arrayBuffer 对象(数据缓存区)
            const fileBuffer = await file.arrayBuffer()
            
            // -- 2. 根据对应的 arrayBuffer 生成一个包含对应摘要值的 arrayBuffer 对象
            const hashBuffer = await crypto.subtle.digest("SHA-256", fileBuffer) 
            
            // -- 3. 通过 TypedArray 视图对 hashBuffer 进行读写操作  → 获取里面的数据(无符号8位数组)
            const hashArray = Array.from(new Uint8Array(hashBuffer))
            
            // -- 4. 映射 hashArray 中的每一项,将里面的每一项转换为 16 进制,并对每一项进行拼接操作 → 生成对应的 hash 值
            const hashHex = hashArray.map(b => b.toString(16).padStart(2, 0)).join("")
        
            return hashHex  // -- 5. 返回对应根据 file 中的数据生成的 hashHex 文件名
        }
        
    • ▲ util: 7 → createChunk

      • async function createChunk(file, size, filename) { // -- 创建切片
            // -- 1. 定义切片容器于当前切片大小
            const chunks = [] 
            let chunkSize = 0
        	
            // -- 2. 循环对文件进行切片
            while (chunkSize < size) { 
                // -- 3. 对文件进行切片 → 返回对应切片的 blob 对象
                const chunkFile = file.slice(chunkSize, chunkSize + CHUNK_SIZE, file.type)
                
                // -- 4. 通过 chunkFile 创建对应切片的 File 对象(也可以不创建直接上传,不过 File 对象可以定义对应的 filename → 方便后端处理)
                const newFile = new File([chunkFile], filename + "-" + chunkSize, { type: file.type })
        		
                // -- 5. 创建 FormData 对象,将 newFile 文件切片数据存放在 formData 对象中 → 进行对应 "multipart/form-data" 数据的上传
                const formData = new FormData()
                formData.append("file", newFile)
                
             	// -- 6. 将对应切片(可直接上传)的对象添加至 chunks 切片容器中
                chunks.push(formData)
        
                chunkSize += CHUNK_SIZE // -- 7. 递加切片
            }
            return chunks // -- 8. 返回所有切片的存储容器
        }
        
    • ▲ util: 8 → uploadFileChunks

      • async function uploadFileChunks(chunks) { // -- 分片上传
            const fetchPromises = chunks.map(chunk => { // -- 1. 遍历 chunks 切片容器中的所有切片数据 → 上传切片数据
                return axios.post( // -- 2. 发送请求 → 上传切片
                    BASE_URL + FETCH_API["UPLOAD"],
                    chunk,
                    {
                        "Content-Type": "multipart/form-data"
                    }
                )
            })
        
            return Promise.all(fetchPromises) // -- 通过 Promise.all 统一监听所有切片上传的结果...
        }
        
    • ▲ util: 9 → mergeFileChunks

      • async function mergeFileChunks(filename, fileExtension) { // -- 合并文件切片
            return axios.post(BASE_URL + FETCH_API["MERGE"], { filename, fileExtension }) // -- 1. 发送对应文件的切片文件合并请求
        }
        
  • 依赖库:

    • axios : 发送请求...
      

Server:

  • 依赖库的引入

    • const Koa = require('koa') // -- koa 
      
      const Router = require('koa-router') // -- koa-router 
      
      const static = require('koa-static') // -- koa-static 静态资源处理
      
      const { resolve, join } = require('path') // -- path 内置库(用来处理路径拼接等)
      
      const cors = require('koa-cors') // -- koa-cors 处理跨域
      
      const fse = require('fs-extra') // -- fs-extra fs 内置库的扩展库(处理文件的读写等)
      
      const multer = require('koa-multer') // -- koa-multer 处理文件上传
      
      const bodyparser = require('koa-bodyparser') // -- koa-bodyparser 处理 body 的解析(json)
      
  • 中间件的使用于服务的启动

    • const app = new Koa(); // -- 创建 Koa 实例 app 对象
      const router = new Router(); // -- 创建 router 对象
      
      app.use(static(join(__dirname, "static"))); // -- 定义静态目录
      app.use(cors()) // -- 跨域处理
      app.use(bodyparser()) // -- 处理 body 解析(json)
      
      // --  router 接口处理部分,在下方展示
      
      app.use(router.routes()) // -- 使用对应 router 中的 routes 路由表中间件(挂载至 app 中)
      
      app.listen(3000, () => { // -- 服务启动(监听)
          console.log("server is running on 3000 port");
      })
      
  • router 接口处理部分

    • const storage = multer.diskStorage({ // -- 1. 自定义存储引擎(文件存储路径等)
         destination: function (req, file, cb) {
             cb(null, resolve(__dirname, './static/temp/')) // -- 保存的路径  
         },
         filename: function (req, file, cb) {
             cb(null, file.originalname); // -- 使用 file 中原来的名字 → 方便合并操作等
         }
      });
      
      const upload = multer({ storage: storage }); // 初始化 upload 对象  
      
      // -- 2. 分片文件上传接口 → 使用上面 upload 对象中的中间件,自动根据上面存储引擎中的存储路径将对应上传的 FormData 文件数据进行生成对应文件进行存储(临时文件,后续合并后会删除)
      router.post("/upload", upload.single('file'), async (ctx) => {
          ctx.body = {
              message: "uploaded success~"
          }
      })
      
      
      // -- 3. merge 合并文件分片数据接口
      const TEMP_DIR = resolve(__dirname, "./static/temp") // -- 定义临时文件路径 → 方便后续使用
      
      const UPLOAD_DIR = resolve(__dirname, "./static/uploads") // -- 定义合并后文件路径 → 方便后续使用
      
      router.post("/merge", async (ctx) => { // -- 接口实现
          // -- 获取请求中的文件名于对应后缀名 → 根据该文件名查找对应分片文件 : 根据后缀名创建对应类型的合并文件
          const { filename, fileExtension } = ctx.request.body
      
          // -- 在 UPLOAD_DIR 中创建对应的合并文件,该路径用于方便后续写入切片数据时,获取写入文件路径
          const NEW_FILE_PATH = UPLOAD_DIR + `/${filename}.${fileExtension}`
          fse.createFileSync(NEW_FILE_PATH) // -- 在 uploads 中创建对应的数据写入文件
      	
          // -- 查看 temp 目录下的所有临时文件 → 用于里面查找对应切片文件,并进行数据的写入(↑)
          fse.readdir(TEMP_DIR, (err, files) => {
              // -- 过滤出需要合并的临时文件的文件名
              const mergeFilename = files.filter(item => item.includes(filename)) 
      		
              // -- 遍历所有分片文件 → 将每一部分切片文件的数据写入至对应的文件中
              mergeFilename.sort().forEach(filename => {
                  // -- 读取对应分片文件中的数据
                  const buffer = fse.readFileSync(TEMP_DIR + "/" + filename) 
      
                  try {
                      // -- 写入切片文件数据至 uploads 中对应的合并文件中
                      fse.appendFileSync(NEW_FILE_PATH, buffer) 
                      
                      // -- 数据写入后,删除对应历史文件 
                      fse.unlinkSync(TEMP_DIR + "/" + filename) 
                  } catch (error) {
                      ctx.body = { // -- 写入失败响应
                          message: "Error",
                          error
                      }
                      console.log("err:", error);
                  }
              })
          })
      
          ctx.body = { // -- 写入成功响应
              message: "OK",
              // -- 返回对应文件访问路径 → 注意访问静态资源不需要加上配置的静态目录(即路径上不需要加上 static)
              url: "http://localhost:3000/uploads" + `/${filename}.${fileExtension}` 
          }
      })
      

你可能感兴趣的:(javascript,前端,开发语言)