跳到主要内容

文件上传和优化

· 阅读需 16 分钟
lzw.

文件上传和优化

文件格式判断和大小判断

  • 判断格式

    • 方式一 在 input[type="file"] 添加 accept=".png,.jpg,.jpeg" 选择文件时限制,限制不了拖拽上传

    • 方式二 通过 file.type 正则限制 !/(jpg|jpeg|png)/i.test(file.type)

    • 方式三 通过二进制头信息

      const fileReader = new FileReader()
      fileReader.readAsArrayBuffer(file)
      fileReader.onload = function (e) {
      const result = new Uint8Array(e.target.result)

      if (result.join('').indexOf('255216') === 0) {
      console.log('jpg文件')
      } else if (result.join('').indexOf('13780') === 0) {
      console.log('png文件')
      }
      }

文件上传【form-data,文件唯一hash,进度条,多文件】

  • form-data 方式上传
    • 设置 Content-Type:multipart/form-data 头信息
    • 采用 (new FormData()).formData.append('file', file) 方式上传
  • 文件唯一 hash 名称
    • 采用 (new FileReader()).readAsArrayBuffer(file) 读取文件为 ArrayBuffer 格式
    • 使用 spark-md5 生成 hash 名称
  • 上传进度条
    • 监听 axios 中 onUploadProgress(e) 方式
    • 其实际就是 ajax 中 xhr.upload.onprogress(e) 方法
    • 百分比计算 e.loaded / e.total
  • 多文件上传
    • 在 input[type="file"] 添加 multiple 属性
    • 利用 input[type="file"] 对象中的 files 获取到所有文件对象,循环上传

文件上传【base64,缩略图,拖拽上传,大文件断点续传】

  • base64 & 缩略图
    • 一般用于处理小的图片,可以预览缩略图
    • 采用 (new FileReader()).readAsDataURL(file) 读取文件为 DataURL 格式,可以在浏览器上直接访问
  • 拖拽上传
    • 监听 html5 元素 dragoverdrop 事件,需要阻止默认事件 e.preventDefault()
    • 在 drop 事件中通过 e.dataTransfer.files 获取到文件对象,即可上传
  • 大文件断点续传
    • 使用固定数量 & 固定大小方案生成切片,如果切片数量大于最大值,就根据数量生产切片大小
    • 使用文件对象中 file.slice 进行切割,切片名称为 ${hash}_${i}spark-md5 生成文件唯一 hash,i 为切片序号)
    • 上传之前,调接口获取已上传切片数组
    • 循环使用 FormData 上传未上传的切片,上传失败的切片放到失败数组里,重新上传
    • 上传完成后,调接口发送上传完成指令

使用 webwork 优化大文件 hash 生产,防止页面假死

onmessage = function (e) {
importScripts('../node_modules/spark-md5/spark-md5.min.js');

const file = e.data
const fileReader = new FileReader()

fileReader.readAsArrayBuffer(file)
fileReader.onload = ev => {
const buffer = ev.target.result
const spark = new SparkMD5.ArrayBuffer()
spark.append(buffer)
const hash = spark.end()
const suffix = /\.([a-zA-Z0-9]+)$/.exec(file.name)[1]

postMessage({
buffer, hash, suffix, filename: `${hash}.${suffix}`
})
}
}

完整代码

源码预览打开窗口
查看源码

后端处理文件上传的 nodejs 测试代码

  • 利用 express 框架
  • 使用 multer 处理 form-data 上传
  • 使用 body-parser 处理 x-www-form-urlencoded 参数
  • 使用 spark-md5 处理 base64 文件唯一 hash 名称
  • getData 方法用于获取切片数量、接收上传完成合并切片
  • upload 方法用于处理上传普通文件、base64格式文件
const express = require('express')
const app = express()
const multer = require('multer')
const bodyParser = require('body-parser')
const sparkMD5 = require('spark-md5')

//引入 path 和 fs
const path = require('path')
const fs = require('fs')

const upload = multer({dest: './uploads/'})
const sleep = () => new Promise(resolve => {
setTimeout(resolve, 1000)
})

app.use(upload.any())
app.use(bodyParser.urlencoded({extended: false, limit: '2100000kb'}))

//设置跨域访问
app.all('*', (req, res, next) => {
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "X-Requested-With");
res.header("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS");
res.header("X-Powered-By", ' 3.2.1')
res.header("Content-Type", "application/json;charset=utf-8");
next();
});

app.post('/getData', async function (req, res) {
const body = req.body
if (body.hash && body.suffix) {
const newDir = path.join(__dirname, `/uploads/${body.hash}`)
if (!fs.existsSync(newDir)) {
fs.mkdirSync(newDir)
}

const files = fs.readdirSync(newDir)
res.send({
code: 0,
codeText: '请求成功',
fileList: files
})
} else if (body.hash && body.count) {
const newDir = path.join(__dirname, `/uploads/${body.hash}`)
const files = fs.readdirSync(newDir)
const extname = path.extname(files[0])
const newFile = newDir + extname

files.sort((a, b) => {
const reg = /_(\d+)/
return reg.exec(a)[1] - reg.exec(b)[1]
}).forEach(file => {
fs.appendFileSync(newFile, fs.readFileSync(newDir + '/' + file))
fs.unlinkSync(newDir + '/' + file)
})

fs.rmdirSync(newDir)
res.send({
code: 0,
codeText: '合并成功',
originalFilename: body.filename,
servicePath: 'http://localhost:8888/uploads/' + body.hash + extname
})
}
})

app.post('/upload', function (req, res) {
// 处理 multipart/form-data 方式
if (req.files) {
const file = req.files[0]
const body = req.body
// 拿到后缀名
let extname = path.extname(file.originalname);
//拼接新的文件路径,文件加上后缀名
let newPath = file.path + extname;

if (body.hash && body.filename) {
const newDir = `uploads/${body.hash}`
newPath = `${newDir}/${body.filename}`
}

//重命名
fs.rename(file.path, newPath, async function (err) {
// await sleep()

if (err) {
res.send({
code: 1,
codeText: err
})
} else {
res.send({
code: 0,
codeText: '上传成功',
originalFilename: file.originalname,
servicePath: 'http://localhost:8888/' + newPath
})
}
})
} else if (req.body) { // 处理 application/x-www-form-urlencoded 方式
const body = req.body
//过滤data:URL
const base64Data = decodeURIComponent(body.file).replace(/^data:image\/\w+;base64,/, "");
const dataBuffer = Buffer.from(base64Data, 'base64');
// 利用扩展生产唯一 md5 名字
const spark = new sparkMD5.ArrayBuffer()
spark.append(base64Data)
// 拿到后缀名
const extname = path.extname(body.filename);
// 新路径
const newPath = '/uploads/' + spark.end() + extname
// base64 写入文件
fs.writeFile(path.join(__dirname) + newPath, dataBuffer, async function (err) {
// await sleep()

if (err) {
res.send({
code: 1,
codeText: err
})
} else {
res.send({
code: 0,
codeText: '上传成功',
originalFilename: body.filename,
servicePath: 'http://localhost:8888' + newPath
})
}
})
}
})


app.listen(8888, function () {
console.log('Server is running at http://localhost:8888');
})