文件(切片)上传实现解决方案【面试】【前后端】

128 阅读1分钟

常见方法

FormData

前端

1.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>
  <input type="file" id="fileInp" accept="image/*">
  <br>
  <img src="" alt="" id="serverImg">
  <script src="./js/md5.min.js"></script>
  <script src="./js/ajax.js"></script>
  <script>
    const limitType = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif']
    const limitSize = 100 * 1024
    fileInp.onchange = async function() {
      let file = fileInp.files[0]
      if(!file) return
      if(!limitType.includes(file.type)){
        alert('文件格式不支持!')
        fileInp.value = null
        return
      }
      if(file.size > limitSize) {
        alert('文件大小超限!')
        fileInp.value = null
        return
      }
      let formData = new FormData() // Content-Type: mutilpart/form-data
      formData.append('chunk', file)
      formData.append('filename', $formatFileName(file.name).filename)
      const result = await $ajax({
        url: 'http://127.0.0.1:5678/single',
        data: formData
      })
      if (result.code == 0) {
        serverImg.src = result.path
      }
    }
  </script>
</body>
</html>

js/ajax.js

function $ajax(options) {
  options = Object.assign({
    url: '',
    method: 'post',
    data: null,
    headers: {}
  }, options)
  return new Promise((resolve, reject) => {
    let xhr = new XMLHttpRequest
    xhr.open(options.method, options.url)
    Object.keys(options.headers).forEach(key => {
      xhr.setRequestHeader(key, options.headers[key])
    })
    xhr.onreadystatechange = () => {
      if(xhr.readyState === 4) {
        if(/^(2|3)\d{2}$/.test(xhr.status)) {
          resolve(JSON.parse(xhr.responseText))
          return
        }
        reject(xhr)
      }
    }
    xhr.send(options.data)
  })
}
function $formatFileName(filename) {
  const dotIndex = filename.lastIndexOf('.')
  let name = filename.substring(0, dotIndex)
  const ext = filename.substring(dotIndex+1)
  name = md5(name) + new Date().getTime()
  return {
    hash: name,
    ext,
    filename: `${name}.${ext}`
  }
}

 

后端

server.js 

const express = require('express')
const bodyParser = require('body-parser')
const multiparty = require('multiparty')
const fs = require('fs')
const path = require('path')

const config = require('./config')

const app = express()
app.listen(config.PORT, () => {
  console.log(`server is running port : ${config.PORT}`)
})

app.use((req, res, next) => {
  const {
    ALLOW_ORIGIN,
    CREDENTIALS,
    HEADERS,
    ALLOW_METHODS
  } = config.CROS
  res.setHeader('Access-Control-Allow-Origin', ALLOW_ORIGIN)
  res.setHeader('Access-Control-Allow-Credentials', CREDENTIALS)
  res.setHeader('Access-Control-Allow-Headers', HEADERS)
  res.setHeader('Access-Control-Allow-Methods', ALLOW_METHODS)
  req.method === 'OPTIONS' ? res.send('Current services support CROSS domain request') : next() 
})
app.use(bodyParser.urlencoded({
  extended: false,
  limit: '1024mb'
}))

const upload_dir = path.resolve(__dirname, '', 'upload')
app.post('/single', (req,res) => {
  new multiparty.Form().parse(req, function (err, fields, file) {
    if (err) {
      res.send({
        code: 1,
        msg: err
      })
      return
    }
    const [chunk] = file.chunk
    const [filename] = fields.filename
    const chunk_dir = `${upload_dir}/${filename}`

    const readStream = fs.createReadStream(chunk.path)
    const writeStream = fs.createWriteStream(chunk_dir)
    readStream.pipe(writeStream)
    readStream.on('end', function(){
      fs.unlinkSync(chunk.path)
    })
    res.send({
      code: 0,
      msg: '',
      path: `http://127.0.0.1:${config.PORT}/upload/${filename}`
    })
  })
})
app.use(express.static('./'))
app.use((req, res) => {
  res.status(404)
  res.send('Not Found')
})

 config.js

module.exports = {
	PORT: 5678,
	CROS: {
		ALLOW_ORIGIN: '*',
		ALLOW_METHODS: 'PUT,POST,GET,DELETE,OPTIONS,HEAD',
		HEADERS: 'Content-Type,Content-Length,Authorization, Accept,X-Requested-With',
		CREDENTIALS: false
	}
}

 Base64编码

前端

2.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>
  <input type="file" id="fileInp" accept="image/*">
  <br>
  <img src="" alt="" id="serverImg">
  <script src="./js/md5.min.js"></script>
  <script src="./js/ajax.js"></script>
  <script>
    fileInp.onchange = async function() {
      let file = fileInp.files[0]
      if(!file) return
      
      const base64 = await converBase64(file)

      const result = await $ajax({
        url: 'http://127.0.0.1:5678/single2',
        headers: {
          'Content-Type': 'application/x-www-form-urlencoded'
        },
        data: `chunk=${encodeURIComponent(base64)}&filename=${$formatFileName(file.name).filename}`
      })
      if (result.code == 0) {
        serverImg.src = result.path
      }
    }
    function converBase64(file) {
      return new Promise((resolve, reject) => {
        const fileReader = new FileReader()
        fileReader.readAsDataURL(file)
        fileReader.onload = ev => {
          // console.log(ev.target.result)
          resolve(ev.target.result)
        }
      })
    }
  </script>
</body>
</html>

后端

 server.js

app.post('/single2', (req, res) => {
  let { chunk, filename } = req.body
  const chunk_dir = `${upload_dir}/${filename}`
  chunk = decodeURIComponent(chunk).replace(/^data:image\/\w+;base64,/, '')
  chunk = Buffer.from(chunk, 'base64')
  fs.writeFileSync(chunk_dir, chunk)
  res.send({
    code: 0,
    msg: '',
    path: `http://127.0.0.1:${config.PORT}/upload/${filename}`
  })
})

大文件上传

思路及原理:

1.将大文件切片 ,file是Blob类的实例,利用其slice方法可以将文件切片(HTTP可以多个并发传递(6-7))

2.同时并发n个切片的上传

3.等n个都上传完,再发送请求合并切片(为啥不自动合并呢)

前端

3.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>
  <input type="file" id="fileInp" accept="image/*">
  <br>
  <span id="progress">0%</span>
  <br>
  <img src="" alt="" id="serverImg">
  <script src="./js/md5.min.js"></script>
  <script src="./js/ajax.js"></script>
  <script>
    const _data = new Proxy({ total: 0 }, {
      set(target, key, value) {
        target[key] = value
        if (_data.total > 100) {
          progress.innerHTML = '上传完成'
          return
        }
        progress.innerHTML = `${_data.total}%`
      }
    })
    fileInp.onchange = async function() {
      let file = fileInp.files[0]
      if(!file) return

      // 1.切片
      const partSize = file.size / 5
      let cur = 0
      let i = 0
      let partList = []
      let { hash, ext, filename } = $formatFileName(file.name)
      while(i < 5) {
        partList.push({
          chunk: file.slice(cur, cur + partSize),
          filename: `${hash}-${i}.${ext}`
        })
        cur += partSize
        i++
      }
      // 2.并发上传
      partList = partList.map(item => {
        const formData = new FormData()
        formData.append('chunk', item.chunk)
        formData.append('filename', item.filename)
        return $ajax({
          url: 'http://127.0.0.1:5678/chunk',
          data: formData
        }).then(result => {
          if (result.code == 0) {
            _data.total += 20
            return Promise.resolve(result)
          }
          return Promise.reject(result)
        })
      })
      // 3.合并切片
      await Promise.all(partList)
      // 延时
      await new Promise(resolve => {
        setTimeout(_ => {
          resolve()
        }, 200)
      })
      const result = await $ajax({
        url: 'http://127.0.0.1:5678/merge',
        headers: {
          'Content-Type': 'application/x-www-form-urlencoded'
        },
        data: `filename=${filename}`
      })
      if (result.code === 0) {
        serverImg.src = result.path
      }
    }
  </script>
</body>
</html>

后端

server.js

app.post('/chunk', (req, res) => {
  new multiparty.Form().parse(req, function(err, fields, file){
    if (err) {
      res.send({
        code: 1,
        msg: err
      })
    }
    let [chunk] = file.chunk
    let [filename] = fields.filename
    let filepath = filename.substring(0, filename.indexOf('-'))
    let chunk_dir = `${upload_dir}/${filepath}`
    if(!fs.existsSync(chunk_dir)) {
      fs.mkdirSync(chunk_dir)
    }
    chunk_dir = `${upload_dir}/${filepath}/${filename}`
    const readStream = fs.createReadStream(chunk.path)
    const writeStream = fs.createWriteStream(chunk_dir)
    readStream.pipe(writeStream)
    readStream.on('end', function() {
      fs.unlinkSync(chunk.path)
    })
    res.send({
      code: 0,
      msg: ''
    })
  })
})
app.post('/merge', (req, res) => {
  const { filename } = req.body
  const dotIndex = filename.lastIndexOf('.')
  const filepath = `${upload_dir}/${filename.substring(0, dotIndex)}`
  const filenamePath = `${upload_dir}/${filename}`
  fs.writeFileSync(filenamePath, '')
  const pathList = fs.readdirSync(filepath)
  pathList.sort((a, b) => a.localeCompare(b))
    .forEach(item => {
      fs.appendFileSync(filenamePath, fs.readFileSync(`${filepath}/${item}`))
      fs.unlinkSync(`${filepath}/${item}`)
    })
  fs.rmdirSync(filepath)
  res.send({
    code: 0,
    msg: '',
    path: `http://127.0.0.1:${config.PORT}/upload/${filename}`
  })
})

附 ajax单文件上传进度显示

ajax.js

function $ajax(options) {
  options = Object.assign({
    url: '',
    method: 'post',
    data: null,
    headers: {},
    progress: Function.prototype
  }, options)
  return new Promise((resolve, reject) => {
    let xhr = new XMLHttpRequest
    xhr.upload.onprogress = options.progress
    xhr.open(options.method, options.url)
    Object.keys(options.headers).forEach(key => {
      xhr.setRequestHeader(key, options.headers[key])
    })
    xhr.onreadystatechange = () => {
      if(xhr.readyState === 4) {
        if(/^(2|3)\d{2}$/.test(xhr.status)) {
          resolve(JSON.parse(xhr.responseText))
          return
        }
        reject(xhr)
      }
    }
    xhr.send(options.data)
  })
}

4.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>
  <input type="file" id="fileInp" accept="image/*">
  <br>
  <span id="progress">0%</span>
  <br>
  <img src="" alt="" id="serverImg">
  <script src="./js/md5.min.js"></script>
  <script src="./js/ajax.js"></script>
  <script>
    const _data = new Proxy({ total: 0 }, {
      set(target, key, value) {
        target[key] = value
        if (_data.total > 100) {
          progress.innerHTML = '上传完成'
          return
        }
        progress.innerHTML = `${_data.total}%`
      }
    })
    fileInp.onchange = async function() {
      let file = fileInp.files[0]
      if(!file) return
      let formData = new FormData() // Content-Type: mutilpart/form-data
      formData.append('chunk', file)
      formData.append('filename', $formatFileName(file.name).filename)
      const result = await $ajax({
        url: 'http://127.0.0.1:5678/single',
        data: formData,
        progress: ev => {
          // ev.loaded 已经上传的大小
          // ev.total 总大小
          _data.total = Math.ceil(ev.loaded / ev.total * 100)
        }
      })
      if (result.code == 0) {
        serverImg.src = result.path
      }
    }
  </script>
</body>
</html>

 

创作打卡挑战赛

赢取流量/现金/CSDN周边激励大奖