Vue3 + Element Plus 单文件上传组件封装实践与踩坑指南
2026/9/19 6:04:55 网站建设 项目流程

做后台管理系统这几年,我几乎在每个项目里都碰到过文件上传需求:用户头像、商品主图、合同附件、订单导入Excel模板……上传本身不复杂,就是“选文件、发请求、拿URL”三步,但真要在 Element Plus 里把 el-upload 调教成“单文件上传”的形态,反而比想象中费劲——它默认是维护一个文件列表,你要限制成单文件、要回显、要校验、要把上传结果绑定到表单字段里,各种状态交织在一起,稍不留神就出现“上传成功后列表里莫名其妙多了文件”“重新选择没反应”这类问题。

这篇文章就以 Vue3 + Element Plus 为技术栈,完整还原我是怎么封装一个单文件上传组件的。组件的核心交互是:对外只暴露一个 v-model,绑定的值为文件 URL;对内处理文件选择、类型与大小校验、上传进度、成功回调、删除回显。适合正在做后台管理系统、被 el-upload 状态绕晕的同学,也适合想系统梳理一遍上传链路、准备在项目里沉淀通用组件的人。下面会先讲设计思路,再贴完整代码,最后把我在实际项目中踩过的坑整理成排查清单。

1. 为什么需要一个单独封装的单文件上传组件

1.1 后台系统里的文件上传到底在传什么

很多同学第一次接触上传功能,以为就是“把文件丢给后端”这么简单。实际上在后台管理类项目里,上传的文件承载的信息远比文件本身重要:用户上传头像,后端返回的是头像 URL,这个 URL 要写进用户表;上传合同附件,后端返回的是附件地址,这个地址要跟着审批流走;上传商品图,返回的图片路径要写入商品表单,提交后才能被前台上架页面加载。换句话说,前端业务真正关心的是“上传成功之后拿到的字符串”,而不是这个文件本身。

这也决定了我们在封装组件时,组件的对外表现应该是一个“字符串选择器”,而不是一个“文件列表管理器”。用户选择一个文件,经过校验和上传,最终组件把这个文件对应的 URL 通过 v-model 抛给页面。页面上只关心字段里有没有值,不关心 el-upload 内部维护了多少个文件对象。如果每次都在业务页面里直接写 el-upload,你就得在每个页面重复处理文件列表、重复校验、重复写删除逻辑,时间一长,不同页面之间的上传体验还不一致,这才是最难受的。

1.2 el-upload 默认行为与单文件场景的错位

Element Plus 的 el-upload 设计上跟“单文件绑定一个字符串”这个需求天然错位。它默认是一个文件列表模型:内部维护 uploadFiles 数组,每一次成功上传都会往数组里 push 一个文件对象,界面上默认还会把这个列表渲染出来。哪怕你只传一个文件,它也长着一张“多文件列表”的脸。

如果只是简单用 limit=1 去限制最大文件数,又会遇到另一个问题:当用户已经传了一个文件、想再换个新文件时,el-upload 会因为“超出限制”直接忽略新选择的文件,界面上没有任何提示,用户会误以为没点到。这里还需要处理 on-exceed 事件,手动清掉旧文件再塞进新文件。再加上回显——你把已存在的 URL 赋值给 file-list 时,el-upload 还要求文件对象带着 name、url 等字段,否则预览区出不来。你会发现,哪怕是一个最简单的单文件上传,直接写在业务页面上也要写七八个回调和方法。与其每次复制粘贴,不如抽成组件一次性解决。

1.3 组件封装的目标

我封装这个单文件上传组件时,给自己定了四条硬性标准:

  • 外部 API 极简:业务页面只需要传一个 uploadUrl(接口地址)和 v-model 绑定,就能完成 “文件上传 + URL 回填”。
  • 校验内置:文件类型、文件大小、是否必填,这些都在组件内部判断,页面里不用再写一次。
  • 状态自管理:上传中禁用按钮、展示进度条、失败恢复,这些状态在组件内部闭环,不出现在业务代码里。
  • 回显和删除天然支持:编辑场景下拉起旧文件,点击删除后把组件绑定的值清空。

当你明确了这四条,组件设计其实已经完成了一大半。接下来就是选技术实现路线的问题了。

2. 组件设计:Props、事件与整体思路

2.1 API 设计原则

上传组件在后台项目里的使用频率很高,所以这个 API 一定要遵循“让调用者少操心”的原则。我把 Props 分成三类:第一类是上传链路所需,比如 action、headers、data、accept;第二类是校验相关,比如 maxSize、required、accept 的扩展写法;第三类是 UI 控制,比如 disabled、tipText、buttonText。

我在实际项目里沉淀出的 Props 大概是下面这个表:

Props类型默认值说明
modelValueString''绑定的文件 URL,配合 v-model 使用
actionString''上传接口地址,必填
headersObject{}上传时携带的请求头
dataObject{}上传时携带的额外表单字段
uploadNameString'file'上传文件对应的表单字段名
acceptString''接受的文件类型,如 .jpg,.png 或 image/*
maxSizeNumber5文件大小上限,单位 MB
requiredBooleanfalse是否必填,配合提示文案使用
disabledBooleanfalse是否禁用上传和删除
tipTextString'支持扩展名:文件大小不超过 5MB'展示在组件下方的提示文案
showTipBooleantrue是否展示提示文案

这里面有两个容易忽略的字段,我后面细说。一个是 uploadName,很多第三方上传接口会严格校验字段名,后端要是约定叫 multipartFile,你传个 file 过去就会被拒;另一个是 required,它不负责拦截上传,只是用来在界面上给出“需上传”的提示,同时也方便前端在提交表单时统一校验。

2.2 v-model 双向绑定如何设计

Vue3 的 v-model 默认对应 modelValue props 和 update:modelValue 事件。这个上传组件既然要接进表单,就必须用 v-model 才能做到“上传完,字段值自动更新”。但这里有个细节:el-upload 内部用的是 file-list 数组,组件内部必须维护一个 fileList 来喂给 el-upload;同时还要把外部传入的 modelValue 转换成 el-upload 需要的数组结构。

所以组件里其实有两套数据流。一套是 UI 层的数据流:fileList 数组,只管 el-upload 怎么展示。另一套是业务层的数据流:modelValue 字符串,只管表单字段的值。两者之间通过 watch 同步,同时通过 emit('update:modelValue') 把上传结果抛给父组件。这样做的好处是,组件内部怎么折腾 el-upload 都不影响外部,外部只拿到一个 URL 字符串。

在 Vue3 里,如果想让子组件支持 v-model,同时保留一点“额外事件”,一般这样声明:

<script setup> const props = defineProps({ modelValue: { type: String, default: '' } }) const emit = defineEmits(['update:modelValue', 'change']) </script>

change 事件是我额外加出来的,目的是在某些场景下让父组件感知“用户真的换了文件”,而不仅是字段值变化。这一点后面结合表单校验会再提。

2.3 为什么选择 http-request 自定义上传

el-upload 提供了三种上传方式:直接配置 action 让组件自己发请求;配置 http-request 覆盖默认上传逻辑;配置 auto-upload=false 完全手动上传。我在封装组件时选择了 http-request 自定义上传,原因有三:

第一,直接配置 action 虽然简单,但请求过程中组件只给你 on-progress、on-success、on-error 这几个状态回调,你很难在请求发出前统一加一些自定义处理,比如加 token、加签名、根据后端返回结构动态解析 URL。

第二,很多中后台项目的上传接口并不是标准的 multipart 上传,可能需要在 header 里带额外签名,也可能需要把文件名拼进 URL,直接用 action 就很难扩展。

第三,http-request 模式下,上传的控制权完全在自己手里,我可以使用 axios 的 onUploadProgress 精确计算上传进度,也可以在上传完成后统一解析后端返回结构。后续如果要做 OSS 直传、分片上传,也只需要替换 http-request 内部的实现,对组件外部零影响。

3. 核心代码实现:从模板到逻辑逐行拆解

3.1 模板层:el-upload 的配置与插槽

先看模板。为了让组件同时支持“文件模式”和“图片模式”,我在模板里做了一个分支:list-type 为 picture-card 时走图片卡片样式,否则走文件按钮样式。这里先展示文件模式下的核心结构:

<template> <div class="single-upload" :class="{ 'is-disabled': disabled }"> <el-upload ref="uploadRef" :action="action" :headers="headers" :data="data" :name="uploadName" :accept="accept" :file-list="fileList" :disabled="disabled || uploading" :limit="1" :http-request="customUpload" :on-exceed="handleExceed" :on-remove="handleRemove" :show-file-list="false" > <div class="upload-area"> <el-button type="primary" plain :loading="uploading" :disabled="disabled" > {{ uploading ? '上传中' : (modelValue ? '重新上传' : '选择文件') }} </el-button> </div> <template #tip> <div v-if="showTip" class="upload-tip"> {{ tipText }} </div> </template> </el-upload> <div v-if="modelValue && !uploading" class="file-preview"> <el-icon class="file-icon"><Document /></el-icon> <span class="file-name">{{ displayName }}</span> <el-icon class="file-remove" @click="handleRemoveFile"> <CircleClose /> </el-icon> </div> <el-progress v-if="uploading" :percentage="progressPercent" :stroke-width="4" class="upload-progress" /> </div> </template>

这里有几个关键设置。show-file-list 我设成了 false,因为单文件场景下我们并不需要 el-upload 默认的文件列表,预览和信息展示全部由自己控制,这样可以避免 el-upload 内部列表状态和外部 v-model 状态互相打架。文件选择后,我们通过 http-request 接管上传,所以 action 虽然还保留着,但实际上是传给 http-request 的 options 作为参考地址用的。

注意,我对外展示的文件名优先用了上传时记录的文件名 displayName,而不是从 URL 反解出来的字符串。因为很多后端会把文件名重命名成随机串,从 URL 里根本看不出原始文件名,用户看到一串 hash 会非常困惑。

3.2 脚本层:校验、上传、进度、回显

脚本层是整个组件的核心,我用 Vue3 的组合式 API 来写。先看组件的 Props 和事件声明:

<script setup> import { ref, computed, watch } from 'vue' import { ElMessage } from 'element-plus' import axios from 'axios' import { Document, CircleClose } from '@element-plus/icons-vue' const props = defineProps({ modelValue: { type: String, default: '' }, action: { type: String, required: true }, headers: { type: Object, default: () => ({}) }, data: { type: Object, default: () => ({}) }, uploadName: { type: String, default: 'file' }, accept: { type: String, default: '' }, maxSize: { type: Number, default: 5 }, required: { type: Boolean, default: false }, disabled: { type: Boolean, default: false }, tipText: { type: String, default: '' }, showTip: { type: Boolean, default: true } }) const emit = defineEmits(['update:modelValue', 'change']) const uploadRef = ref(null) const fileList = ref([]) const uploading = ref(false) const progressPercent = ref(0) const currentFileName = ref('') const displayName = computed(() => { if (currentFileName.value) return currentFileName.value return getFileNameFromUrl(props.modelValue) })

这里有个小设计:displayName 是一个计算属性,优先用上传时记录到的原始文件名,回显模式下才从 URL 反解。这样编辑场景下拉旧文件,至少能显示一个像样的名字,不至于展示出带一长串参数的完整路径。

接下来看回显逻辑。外部传进来的 modelValue 是字符串,但 el-upload 内部需要数组,所以要用 watch 做转换:

watch(() => props.modelValue, (val) => { if (val) { fileList.value = [ { name: getFileNameFromUrl(val), url: val, status: 'success' } ] } else { fileList.value = [] } }, { immediate: true })

回显时 status 必须带上,这是我在项目里踩过的坑:如果你只传 name 和 url,不带 status,el-upload 会认为这个文件还处于待上传状态,界面上可能不会正常展示预览,外层用 clearFiles 清空时也可能清不干净。

上传前的校验我放在 before-upload 里,这一步要做类型和大小双重校验:

const beforeUpload = (rawFile) => { if (props.maxSize && rawFile.size / 1024 / 1024 > props.maxSize) { ElMessage.warning(`文件大小不能超过 ${props.maxSize}MB`) return false } if (props.accept && !checkAccepted(rawFile)) { ElMessage.warning('文件类型不符合要求') return false } return true } const checkAccepted = (file) => { if (!props.accept) return true const acceptList = props.accept.split(',').map(item => item.trim()).filter(Boolean) const ext = file.name.split('.').pop()?.toLowerCase() || '' return acceptList.some(item => { if (item.startsWith('.')) { return ext === item.slice(1).toLowerCase() } return file.type === item }) }

这里有一个容易踩的坑:accept 写 .png,.jpg 时,file.type 在部分浏览器里会是 image/png,直接拿形象 image/png 和 .png 对比永远不相等,所以校验时要把“扩展名”和“MIME 类型”分开处理。上面的实现里,只要 accept 里有一项是 .开头的,就走扩展名匹配,能覆盖大部分场景。

接下来是自定义上传函数。这是整个组件的核心,因为上传的请求都由我们自己发:

const customUpload = async (options) => { const { file } = options uploading.value = true progressPercent.value = 0 currentFileName.value = file.name const formData = new FormData() formData.append(props.uploadName, file) Object.keys(props.data || {}).forEach(key => { formData.append(key, props.data[key]) }) try { const res = await axios.post(props.action, formData, { headers: { 'Content-Type': 'multipart/form-data', ...props.headers }, onUploadProgress: (event) => { if (event.total) { const percent = Math.round((event.loaded / event.total) * 100) progressPercent.value = percent options.onProgress({ percent }) } } }) const url = parseUploadUrl(res.data) if (!url) { throw new Error('上传接口返回数据缺少 URL') } options.onSuccess(res.data) emit('update:modelValue', url) emit('change', url) currentFileName.value = file.name ElMessage.success('上传成功') } catch (error) { options.onError(error) ElMessage.error(error?.message || '上传失败,请重试') } finally { uploading.value = false setTimeout(() => { progressPercent.value = 0 }, 400) } }

后端返回结构五花八门,有的是{ code: 200, data: { url: '...' } },有的是{ url: '...' },有的干脆直接返回字符串。所以我写了一个 parseUploadUrl 来兼容常见结构:

const parseUploadUrl = (response) => { if (!response) return '' if (typeof response === 'string') return response if (typeof response === 'object') { const data = response.data || response if (typeof data === 'string') return data return data.url || data.src || data.path || '' } return '' }

处理单个文件超出 limit 时,需要一个替换逻辑。我选择在 on-exceed 里把旧文件清掉,把新文件塞进 el-upload 的内部列表,然后让它继续走正常上传流程:

const handleExceed = (files) => { uploadRef.value.clearFiles() const rawFile = files[0] if (rawFile && typeof rawFile.uid !== 'undefined') { uploadRef.value.handleStart(rawFile) } }

这个方法很关键,否则用户第一次上传成功后再选择新文件,会被 el-upload 当成“超出限制”直接忽略。另外,如果组件处于上传中状态,我建议直接拦截掉重选操作,这里可以加一个判断:

if (uploading.value) { ElMessage.warning('文件上传中,请稍候') return }

删除逻辑也比较简单,清空内部列表和外部值:

const handleRemoveFile = () => { if (props.disabled) return uploadRef.value.clearFiles() fileList.value = [] emit('update:modelValue', '') emit('change', '') ElMessage.success('已移除文件') }

3.3 样式层:宽度、预览区与进度条

单文件组件在表单里最常见的显示问题,一是按钮和预览区没有对齐,二是在某些布局下宽度被压缩。我给组件根节点设置了默认宽度 100%,预览区使用 flex 布局,让文件名可以自动省略:

.single-upload { width: 100%; display: flex; flex-direction: column; gap: 12px; } .upload-area { display: flex; align-items: center; gap: 8px; } .file-preview { display: flex; align-items: center; gap: 8px; padding: 10px 12px; background: #f5f7fa; border-radius: 6px; font-size: 14px; line-height: 1.4; } .file-icon { color: #909399; flex-shrink: 0; } .file-name { flex: 1; color: #303133; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .file-remove { color: #909399; cursor: pointer; flex-shrink: 0; transition: color 0.2s; } .file-remove:hover { color: #f56c6c; } .upload-tip { margin-top: 8px; font-size: 12px; color: #909399; } .upload-progress { margin-top: 4px; } .is-disabled .file-remove { cursor: not-allowed; color: #c0c4cc; }

这里要留意一个细节:Element Plus 默认给 el-upload 加了一些内联宽度,在卡片模式下容易出现宽度溢出。如果你在项目里发现组件宽度不对,可以给内部 el-upload 上一句:deep(.el-upload) { width: 100%; }或者在外面包一层固定宽度的容器,不要试图改 element-plus 的全局样式,容易影响别的地方。

4. 完整使用示例:在页面和表单中的落地

4.1 基础用法:绑定单个字段

组件封装好了,在业务页面里使用很直接。比如一个“用户头像”上传,只需要两行:

<template> <single-upload v-model="userForm.avatar" action="/api/upload/avatar" accept=".jpg,.png,.jpeg" :max-size="2" tip-text="支持 jpg/png 格式,大小不超过 2MB" /> </template>

上传成功的 URL 会自动写到 userForm.avatar,用户在编辑时也能看到之前上传的文件并重新替换。是不是比直接在页面里写一堆 el-upload 回调清爽多了?

4.2 结合 el-form 做必填校验

在表单场景中,上传组件经常要配合校验规则使用。由于组件内部支持 v-model,所以 el-form 的 rules 可以直接把它当普通字段校验。请看下面这个例子:

<template> <el-form ref="formRef" :model="form" :rules="rules" label-width="100px"> <el-form-item label="合同附件" prop="contractUrl"> <single-upload v-model="form.contractUrl" action="/api/upload/contract" accept=".pdf,.doc,.docx" :max-size="10" :required="true" tip-text="支持 PDF/Word 格式,大小不超过 10MB" /> </el-form-item> <el-button type="primary" @click="handleSubmit">提交</el-button> </el-form> </template>

rules 里只需要写上对应的字段规则:

const rules = { contractUrl: [ { required: true, message: '请上传合同附件', trigger: 'change' } ] }

这里有个细节:Element Plus 表单校验的 trigger 一般习惯用 blur 或 change,但 upload 组件没有 blur 事件,所以这里要写 change。而我们的组件在成功上传、删除文件后都会 emit('change'),这样校验就能自动触发,提交时如果没传文件,也会正常拦截。

4.3 图片模式与文件模式切换

有些场景需要上传的是图片,并且希望像一张卡片一样预览。我日常会在这个组件内部加一个 mode props,当 mode === 'image' 时,模板里把 el-upload 的 list-type 切到 picture-card,并使用预览插槽展示图片。

这里只给一个最简单的实现思路。你可以引入 el-image 做点击预览,也可以用 element-plus 的preview-src-list属性做图片放大。核心区别在于回显时,图片模式下 URL 直接作为 img 的 src 展示,删除按钮也直接覆盖在图片右上角,整体交互比文件模式更贴近“看图选图”的直觉。

5. 实际项目中遇到的坑与排查实录

5.1 常见问题速查表

这一节把我在项目中真实碰到过的问题整理成速查表,每个问题都附带定位思路,方便你对照排查:

现象原因解决办法
选完文件后不触发上传auto-upload 默认为 true,但设置了 http-request 后处理有误确认 http-request 没有被覆盖,且没有在 before-upload 返回 false
上传成功后 on-success 没触发自定义 http-request 里没有调用 options.onSuccess在请求成功后手动执行 options.onSuccess(res.data)
单文件模式下重复选择新文件没反应limit=1 时新文件超出限制被忽略在 on-exceed 中调用 clearFiles() 和 handleStart()
回显的文件在列表里不显示回显对象缺少 status: 'success' 字段回显时给文件对象补全 status:'success'
删除文件后表单值没清空只清了 el-upload 内部列表,没有 emit update:modelValue删除后同步 emit('update:modelValue', '')
上传进度条一直卡在 0%axios 没配 onUploadProgress,或浏览器不支持检查请求库配置,确认走的是 XHR 而不是 fetch
上传成功但 URL 没写入表单返回数据结构和 parseUploadUrl 不匹配打开网络面板查看返回结构,调整解析函数
上传中用户疯狂点按钮上传状态没有锁住按钮使用 uploading 状态控制 disabled 和 loading
文件名中文出现乱码部分后端对 multipart 文件名的解码方式不同前端尝试 encodeURIComponent 文件名,或与后端约定
在弹窗里打开组件宽度错位Element Plus 在弹窗中的样式层级或宽度适配问题给组件加固定宽度容器,必要时用 :deep 覆盖内部样式

5.2 三个高频问题的完整排查过程

我先展开讲第一个高频问题:自定义 http-request 后 on-success 不触发。这个问题很典型,很多人照着文档写完之后发现,上传请求已经发出去了,后端也返回了,自己的 on-success 里死活不执行。原因在于,当你提供了 http-request 属性,Element Plus 就把上传动作的“执行权”完全交给你了,组件不会知道你的请求到底成没成功,它只能等你通过 options.onSuccess 和 options.onError 主动通知。所以如果发现请求成功但 on-success 没触发,第一件事就是检查 customUpload 里有没有按约定调用 options.onSuccess。以下是正确的调用顺序:

// 1. 后端请求成功 const res = await axios.post(...) // 2. 手动通知 el-upload 上传成功 options.onSuccess(res.data) // 3. 再更新外部 v-model emit('update:modelValue', url)

第二个高频问题是 on-exceed 里调用 clearFiles 之后,紧接着调 handleStart,但新文件还是没上传。排查时你会发现,大多数情况是因为 handleStart 传进去的文件对象不对。el-upload 的 on-exceed 回调里,参数 files 是一个原始 File 对象的数组,而 handleStart 需要接收的是 File 对象,这一点没问题,但要注意,如果你的组件里 file-list 用了双向绑定 v-model:file-list,那么清空后还要确保 fileList 变量本身也同步为空,否则组件内部还是认为列表里有旧文件。我的经验是,on-exceed 里只依赖 uploadRef 实例的 clearFiles 和 handleStart,不要额外去改自己的 fileList 变量,避免两个数据源打架。

第三个高频问题是回显文件后,无法删除或者删除后报错。我排查过好几个现场,最后发现都是回显时没有补全字段导致的。el-upload 在判断一个文件是否能正常操作时,依赖文件对象里的 uid、name、status 等字段。如果你只是简单地从后端拿一个 URL,直接塞进 file-list,它在界面上可能显示了,但操作时却会提示“文件不存在”或直接无反应。我的回显处理规范是:在 watch 里构造数组时,除了 name 和 url,一定显式带上 status: 'success',并且尽量使用随机 uid。

5.3 规避上传坑的几条规范

结合这些踩坑经历,我给自己定了几条强制规范,现在也分享给你:

  • 上传组件内部永远只维护两套状态:UI 状态(fileList、uploading)和业务值(modelValue),两套状态通过 watch 和 emit 同步,不要混用。
  • 凡是自定义请求,必须在请求结束后显式调用 options.onSuccess 或 options.onError,不能省。
  • 凡是单文件替换场景,优先用 on-exceed + clearFiles + handleStart,不要直接操作 fileList 数组。
  • 回显文件时,必须补全 name、url、status 三个字段,有条件再加 uid。
  • 上传中一定锁住按钮和删除操作,防止用户重复点击产生脏数据。

6. 组件后续可以怎么扩展

单文件上传组件封装完,并不代表结束。我在不同项目里陆续给它做过几轮扩展,这里说几个相对高频的方向:

第一个方向是支持 OSS 直传。很多中后台项目会把文件传到对象存储,前端先请求后端拿一个临时签名,再直接把文件 POST 到存储服务。这种情况下,业务层的 action 就分成了“获取签名”和“真正上传”两个接口。我的做法是在自定义上传函数里先调一次获取签名的接口,拿到签名后再用签名拼接上传地址,这个逻辑完全藏在组件内部,对使用方透明。需要注意签名一般有时效性,所以获取签名最好放在用户点击上传的那一刻,而不是组件初始化时。

第二个方向是图片压缩。移动端上传照片经常有 2MB 的体积限制,但手机照片动不动三五 MB。可以在 before-upload 里用 canvas 做一次前端压缩,把超过限制的图片压到限制以内再做后续处理。这里要注意,压缩是异步操作,el-upload 的 before-upload 支持返回 Promise,只要返回的 Promise resolve 一个 File 对象,就可以继续走上传链路。这也是我对这个组件比较满意的一点:因为校验和上传被拆开了,压缩逻辑可以很自然地挂在 before-upload 里。

第三个方向是分片上传。对于几百 MB 的大文件,单次 POST 不仅容易超时,失败后还要整个重传。我做过分片上传版本的组件,核心改动都在 customUpload 里:先把文件按固定大小切片,然后逐个上传,再用一个协调函数维护切片进度和合并请求。对外 API 完全不变,页面里的代码一行都不用改。

如果你也想在自己的项目里封装这个组件,我的建议是从最简单的文件模式开始,先把校验、上传、回显、删除这条主干跑通,再考虑图片模式、压缩、分片这些高级能力。等组件稳定之后,你会明显感觉到:项目里新增一个上传需求,从原来的半小时起步,变成了五分钟搞定,这才是封装通用组件最大的价值。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询