应用开发平台前端集成vue-simple-uploader实现文件分块上传
背景
文件的上传是系统的必备功能,Element提供了上传组件upload,也基本能满足常见常用的文件上传功能,特别是应对小型文件(10M以下)的处理。但如果是遇到要求更多更高的场景,上传几百兆甚至上G的视频文件,要求分块上传,能断点续传,显示进度,能暂停,能重试……这时候就显得乏力了。如果基于upload实现,需要附加大量的二次开发,这未必是一种最佳实现方案。
这时候,就需要找一找看一看,市面上是否有现成的“轮子”可用了。
接下来,分两篇,分别介绍下前端实现和后端实现,今天首先来说下前端那些事儿。
技术选型
vue-simple-uploader,作者对vue3做了适配。
官网 https://github.com/simple-uploader/vue-uploader/blob/vue3/README_zh-CN.md
特性
- 支持文件、多文件、文件夹上传
- 支持拖拽文件、文件夹上传
- 统一对待文件和文件夹,方便操作管理
- 可暂停、继续上传
- 错误处理
- 支持“快传”,通过文件判断服务端是否已存在从而实现“快传”
- 上传队列管理,支持最大并发上传
- 分块上传
- 支持进度、预估剩余时间、出错自动重试、重传等操作
功能设计
组件提供了通用能力,整合到平台中,需要根据需求做定制和集成。
在本平台中,我使用该上传组件,主要解决的是与业务实体关联的附件,不同场景下文件有大有小。
需要支持文件、多文件上传,但不需要直接上传文件夹(上传文件夹通常做文档库、网盘场景中需要)。
暂停、重试、分块、预估时间、进度显示、重传这些是需要的,但快传、秒传功能不需要(快传、秒传往往只在互联网应用的网盘应用场景有需求,企业应用里都是些独立的,不重复的文件)。
安装及注册
安装,执行如下命令
pnpm install vue-simple-uploader@next --save
初始化,修改main.js,全局注册
import uploader from 'vue-simple-uploader'
import 'vue-simple-uploader/dist/style.css'
// 创建实例
const setupAll = async () => {
const app = createApp(App)
await setupI18n(app)
setupStore(app)
setupGlobCom(app)
setupRouter(app)
setupPermission(app)
……略
// 文件上传
app.use(uploader)
app.mount('#app')
}
封装组件
上传组件
<template>
<uploader
ref="uploader"
:options="finalOptions"
:file-status-text="statusText"
:show-success-files="showSuccessFiles"
:single-flag="singleFlag"
:auto-start="autoStart"
:show-list-flag="showListFlag"
@file-complete="fileComplete"
@file-added="fileAdded"
/>
</template>
<script>
import { getToken } from '@/utils/auth'
export default {
components: {},
props: {
options: {
type: Object,
required: false,
default() {
return {}
}
},
singleFlag: {
type: Boolean,
default: false
},
autoStart: {
type: Boolean,
default: true
},
// 是否显示文件列表
showListFlag: {
type: Boolean,
default: true
},
entityType: {
type: String,
required: true
},
entityId: {
type: String,
default: '',
required: true
},
moduleCode: {
type: String,
required: true
},
showSuccessFiles: {
type: Boolean,
default: false,
required: false
},
serverUrl: {
type: String,
default: '',
required: true
}
},
data() {
const token = getToken()
return {
defaultOptions: {
target: import.meta.env.VITE_BASE_URL + this.serverUrl,
testChunks: false,
maxChunkRetries: 3,
chunkSize: 10485760,
query: {
entityType: this.entityType,
entityId: this.entityId,
moduleCode: this.moduleCode
},
headers: { 'X-Token': token },
generateUniqueIdentifier: () => {
// 业务主键+时间戳最大限度降低并发冲突发生的概率
return this.entityId + new Date().getTime()
},
parseTimeRemaining(timeRemaining, parsedTimeRemaining) {
return parsedTimeRemaining
.replace(/syears?/, '年')
.replace(/days?/, '天')
.replace(/shours?/, '时')
.replace(/sminutes?/, '分')
.replace(/sseconds?/, '秒')
}
},
statusText: {
success: '100%',
error: '失败',
uploading: '上传中',
paused: '暂停中',
waiting: '等待中'
}
}
},
computed: {
finalOptions: function () {
const opts = Object.assign(this.defaultOptions, this.options)
return opts
}
},
watch: {
entityId: function () {
this.$refs.uploader.options.query.entityId = this.entityId
}
},
methods: {
fileComplete(file) {
this.$emit('fileComplete', file)
},
fileAdded(file) {
this.$emit('fileAdded', file)
}
}
}
</script>
有几个需要注意的点,下面来说说。
首先,为了简化使用方设置,我先通过defaultOptions,将组件大部分公用配置设置好,作为缺省配置。同时将可能需要设置的属性作为props提供出来,供使用方按需调整。
其次,受组件自身限制,没法走前端的api调用框架axios,而是在组件配置options的target属性指定了文件上传的后端地址。
再次,平台使用JWT做身份认证,绕过api调用框架axios的结果就是没有自动附加token,同样需要在组件配置中附加,否则后端会视为未授权。
const token = getToken()
return {
defaultOptions: {
……
headers: { 'X-Token': token }
……
最后,我加入了平台自己的控制,即将模块编码moduleCode,实体类型entityType和实体标识entityId也传入了进来,并通过组件的query属性传给了后端,这几个参数,用于后端来生成结构化的保存路径。
这么做的主要目的,除了分门别类存储附件外,还有个显著优势可以按需进行附件归档。比如系统运行3年了,磁盘占用越来越大,业务上通常只需要查看最近一年的单据和附件,那么就可以把1年前的附件,从服务器上移动到备份服务器上。
默认UI丑到爆,需要自己调整
调整后效果如下:
管理组件
管理组件通常是配合上传组件一起使用的,对上传结果进行预览、验证和移除,如下图所示。
vue-simple-uploader组件虽然有文件列表,但这个列表实际是有功能缺失的,无法下载,更无法移除,因此自己另行封装了一个。
<template>
<el-table :data="entityData" highlight-current-row border>
<el-table-column type="index" label="序号" sortable width="65" />
<el-table-column prop="name" label="名称" show-overflow-tooltip />
<el-table-column prop="size" label="大小" width="80" />
<el-table-column prop="createTime" label="时间" width="100" />
<el-table-column fixed="right" label="操作" width="200">
<template #default="scope">
<el-button
type="primary"
icon="Download"
class="header-search_button"
size="small"
@click="download(scope.row)"
>下载</el-button
>
<el-button
type="primary"
icon="Delete"
class="header-search_button"
size="small"
@click="remove(scope.row)"
>删除</el-button
>
</template>
</el-table-column>
</el-table>
</template>
<script>
export default {
components: {},
props: {
entityId: {
type: String,
default: '',
required: true
},
showDelete: {
type: Boolean,
default: true,
required: false
}
},
data() {
return {
entityData: []
}
},
watch: {
entityId: function () {
this.list()
}
},
mounted: function () {
this.list()
},
methods: {
list() {
if (this.entityId) {
// 只有当entityId不为空时才发起查询
this.$api.support.attachment.list({ entityId: this.entityId }).then((res) => {
this.entityData = res.data
})
}
},
remove(row) {
this.$confirm('是否删除该附件?', '确认', {
type: 'warning'
})
.then(() => {
this.$api.support.attachment.remove(row.id).finally(() => {
this.list()
})
})
.catch(() => {
this.$message.info('已取消')
})
},
download(row) {
this.$api.support.attachment.download(row.id, row.name)
}
}
}
</script>
浏览组件
浏览组件通常用于查看视图,即查看当前单据上传了哪些附件,并可以下载,但不能删除,如下图所示:
<template>
<el-row>
<ul style="margin: 0px; padding: 0px">
<li
v-for="item in entityData"
:key="item.id"
style="
line-height: 30px;
width: 100%;
overflow: hidden;
word-break: keep-all;
white-space: nowrap;
text-overflow: ellipsis;
"
>
<a :title="item.name" @click="download(item)" class="cursor-pointer">
<span> {{ item.name }}</span>
<span>({{ item.size }} )</span>
</a>
</li>
</ul>
</el-row>
</template>
<script>
export default {
props: {
entityId: {
type: String,
default: '',
required: true
}
},
data() {
return {
entityData: []
}
},
watch: {
entityId: function () {
this.list()
}
},
mounted: function () {
this.list()
},
methods: {
list() {
if (this.entityId) {
// 只有当entityId不为空时才发起查询
this.$api.support.attachment.list({ entityId: this.entityId }).then((res) => {
this.entityData = res.data
})
}
},
download(item) {
this.$api.support.attachment.download(item.id)
}
}
}
</script>
遗留问题
同时使用封装的上传组件和管理组件,会出现下面这种情况,也会影响用户体验。
官方控件自带的上传文件列表,上传成功的文件没有属性或方法从列表中移除。我尝试过使用fileSuccess事件自己实现逻辑,如下图所示:
fileSuccess(rootFile, file) {
// 文件上传成功后从列表中移除
// TODO:以下代码未调试成功
// let index = this.$refs.uploader.fileList.findIndex(
// (successFile) => successFile.id === file.id
// )
// if (index !== -1) {
// console.log(this.$refs.uploader.fileList)
// this.$refs.uploader.fileList.splice(index, 1)
// console.log(this.$refs.uploader.fileList)
// }
}
}
实际调试发现fileList并不能按预期正常变更集合元素,原因不明。后续有时间了再处理,要么将上面代码逻辑调整好,要么直接基于源码去修改。
开发平台资料
平台名称:一二三开发平台
简介: 企业级通用开发平台
设计资料:csdn专栏
开源地址:Gitee
开源协议:MIT
欢迎收藏、点赞、评论,你的支持是我前行的动力。