起因: 文件上传的时候,如果文件过大,可能导致请求超时的情况,这时就需要将文件分割成小块缩短单个请求传输时间
流程图
服务端依赖
koa
: HTTP中间件框架
koa-router
: koa
路由中间件
koa-body
: koa
body解析中间件,用于解析post内容
fs-extra
: node
文件系统扩展
koa-static
: koa
静态资源中间件,用于处理静态资源请求
目录结构
- index.html 带有上传功能的html页面
- upload 存放最后合并的大文件
- temp 临时存放分片文件
- server.js 服务
内容实现
步骤1 - 正常上传
上传页面
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<!-- 引入 Koa -->
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
</head>
<body>
<input type="file" id="btnFile">
<input type="button" value="上传" onclick="upload()">
<script>
let btnFile = document.querySelector('#btnFile')
function upload() {
// 获取上传文件
const file = btnFile.files[0]
const formData = new FormData()
formData.append('file', file)
axios.post('/upload', formData).then(res => {
console.log(res)
})
}
</script>
</body>
</html>
效果图
服务端
const path = require('path')
const Koa = require('koa')
const Router = require('koa-router')
const koaBody = require('koa-body')
const source = require('koa-static')
const app = new Koa()
const router = new Router()
// 处理静态资源
app.use(source(path.resolve(__dirname, 'public')))
// 处理页面请求
app.use(koaBody({
multipart: true,
formidable: {
uploadDir: path.resolve(__dirname, 'temp'),// 文件存放地址
keepExtensions: true,
maxFieldsSize: 2 * 1024 * 1024
}
}))
// 文件上传
router.post('/upload', async ctx => {
ctx.body = '文件上传成功'
})
app.use(router.routes()).use(router.allowedMethods())
app.listen(3000, () => console.log('Server runnint on port 3000'))
启动服务,上传文件
效果图
temp 中获得了上传的文件
步骤2 - 分片上传
上传脚本增加分片功能
设置每片大小 -> 根据大小进行文件分割和重命名 -> 递归分片上传
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<link type="text/css" rel="stylesheet" href="">
<script type="text/javascript" src=""></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
</head>
<body>
<input type="file" id="btnFile">
<input type="button" value="上传" onclick="upload(0)">
<script>
let btnFile = document.querySelector('#btnFile')
// 每一片大小
const chunkSize = 1024 * 1024 * 2
function upload(index/* 当前片下标 */) {
// 获取上传文件
const file = btnFile.files[0]
// [ 文件名, 文件后缀 ]
const [ fname, fext ] = file.name.split('.')
// 获取当前片的起始字节
const start = index * chunkSize
if (start > file.size) {// 当超出文件大小,停止递归上传
return
}
const blob = file.slice(start, start + chunkSize)
// 为每片进行命名
const blobName = `${fname}.${index}.${fext}`
const blobFile = new File([blob], blobName)
const formData = new FormData()
formData.append('file', blobFile)
axios.post('/upload', formData).then(res => {
console.log(res)
// 递归分片上传
upload(++index)
})
}
</script>
</body>
</html>
服务端对分片进行整理
接收分片 -> 创建大文件临时目录 -> 将分片从 temp 目录 转移到临时目录
const path = require('path')
const Koa = require('koa')
const Router = require('koa-router')
const koaBody = require('koa-body')
const fse = require('fs-extra')
const source = require('koa-static')
const app = new Koa()
const router = new Router()
// 处理静态资源
app.use(source(path.resolve(__dirname, 'public')))
// 上传文件的目录地址
const UPLOAD_DIR = path.resolve(__dirname, 'public/upload')
// 处理页面请求
app.use(koaBody({
multipart: true,
formidable: {
uploadDir: path.resolve(__dirname, 'temp'),// 文件存放地址
keepExtensions: true,
maxFieldsSize: 2 * 1024 * 1024
}
}))
// 文件上传
router.post('/upload', async ctx => {// 文件转移
// koa-body 在处理完 file 后会绑定在 ctx.request.files
const file = ctx.request.files.file
// [ name, index, ext ] - 分割文件名
const fileNameArr = file.name.split('.')
// 存放切片的目录
const chunkDir = `${UPLOAD_DIR}/${fileNameArr[0]}`
if (!fse.existsSync(chunkDir)) {// 没有目录就创建目录
// 创建大文件的临时目录
await fse.mkdirs(chunkDir)
}
// 原文件名.index - 每个分片的具体地址和名字
const dPath = path.join(chunkDir, fileNameArr[1])
// 将分片文件从 temp 中移动到本次上传大文件的临时目录
await fse.move(file.path, dPath, { overwrite: true })
ctx.body = '文件上传成功'
})
app.use(router.routes()).use(router.allowedMethods())
app.listen(3000, () => console.log('Server runnint on port 3000'))
效果图
步骤3 - 分片整合
客户端上传结束后向服务端发出 结束-整合 信号
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<link type="text/css" rel="stylesheet" href="">
<script type="text/javascript" src=""></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
</head>
<body>
<input type="file" id="btnFile">
<input type="button" value="上传" onclick="upload(0)">
<script>
let btnFile = document.querySelector('#btnFile')
// 区块大小
const chunkSize = 1024 * 1024 * 2
function upload(index) {
// 获取上传文件
const file = btnFile.files[0]
const [ fname, fext ] = file.name.split('.')
// 获取区块内容
const start = index * chunkSize
if (start > file.size) {// 当超出文件大小,停止递归上传
// 请求整合
merge(file.name)
return
}
const blob = file.slice(start, start + chunkSize)
const blobName = `${fname}.${index}.${fext}`
const blobFile = new File([blob], blobName)
const formData = new FormData()
formData.append('file', blobFile)
axios.post('/upload', formData).then(res => {
console.log(res)
upload(++index)
})
}
function merge(name) {
axios.post('/merge', { name: name }).then(res => {
console.log(res)
})
}
</script>
</body>
</html>
服务端收到整合请求后开始整合分片
按序读取大文件临时目录下的文件合并成一个文件 -> 删除大文件临时目录 -> 返回整合文件的地址
const path = require('path')
const Koa = require('koa')
const Router = require('koa-router')
const koaBody = require('koa-body')
const fse = require('fs-extra')
const source = require('koa-static')
const app = new Koa()
const router = new Router()
// 处理静态资源
app.use(source(path.resolve(__dirname, 'public')))
const UPLOAD_DIR = path.resolve(__dirname, 'public/upload')
// 处理页面请求
app.use(koaBody({
multipart: true,
// encoding: 'gzip', // 启用压缩在 /merge 会报错
formidable: {
uploadDir: path.resolve(__dirname, 'temp'),// 文件存放地址
keepExtensions: true,
maxFieldsSize: 2 * 1024 * 1024
}
}))
// 文件上传
router.post('/upload', async ctx => {// 文件转移
const file = ctx.request.files.file
// [ name, index, ext ]
const fileNameArr = file.name.split('.')
// 存放切片的目录
const chunkDir = `${UPLOAD_DIR}/${fileNameArr[0]}`
if (!fse.existsSync(chunkDir)) {// 没有目录就创建目录
await fse.mkdirs(chunkDir)
}
// 原文件名.index.ext
const dPath = path.join(chunkDir, fileNameArr[1])
await fse.move(file.path, dPath, { overwrite: true })
ctx.body = '文件上传成功'
})
// 合并文件
router.post('/merge', async ctx => {
const { name }= ctx.request.body
const fname = name.split('.')[0]
const chunkDir = path.join(UPLOAD_DIR, fname)
const chunks = await fse.readdir(chunkDir)
chunks.sort((a, b) => a - b).map(chunkPath => {
// 合并文件
fse.appendFileSync(
path.join(UPLOAD_DIR, name),
fse.readFileSync(`${chunkDir}/${chunkPath}`)
)
})
// 删除临时文件夹
fse.removeSync(chunkDir)
// 返回文件地址
ctx.body = { msg: '合并成功', url: `http://localhost:3000/upload/${name}` }
})
app.use(router.routes()).use(router.allowedMethods())
app.listen(3000, () => console.log('Server runnint on port 3000'))
效果图
访问返回地址