2026年被产品经理逼疯后,我们重构了移动端上传组件——2026最新成果复盘

被产品经理逼疯后,我们重构了移动端上传组件——2026最新成果复盘产品经理拿着新需求走过来 这个页面也要上传图片 复用之前那个组件就行 我说 行 那个组件大家都在用 然后真的出事了 同事小张接了个需求 给上传组件的拍照功能加个水印 他改了公共组件里的一行代码 自测通过 提测通过 上线 上线后半小时 客服群炸了 另一个页面的上传功能完全失效

大家好,我是讯享网,很高兴认识大家。这里提供最前沿的Ai技术和互联网信息。



下面是我们交出的答案——不追求表面复用,只追求改了不炸。分享我们团队在重构移动端文件上传组件时的一些思考,不谈太多高大上的概念,而是聊聊如何用适度的设计解决实际问题


原来的实现存在这些问题:

  1. 逻辑散落 - 上传逻辑分散在各个业务页面,复制粘贴到处都是
  2. 扩展困难 - 新增一种上传方式(比如视频)需要改多个文件
  3. 维护困难 - 改一处逻辑要同时改多个页面
  4. 样式不统一 - 每个页面都有自己的上传按钮样式

于是我们决定抽取公共能力,但关键问题是:怎么抽?抽到什么程度?


我们定了三个小目标:

  1. 统一入口 - 所有文件上传都走同一套组件
  2. 可配置 - 不同业务场景可以灵活定制
  3. 可扩展 - 新增能力时不破坏现有代码

先上一张架构图:

 
     
    
       
┌─────────────────────────────────────────────────────┐ 

│ 业务页面 │ │ (表单页面、详情页面、数据填报页面…) │ └─────────────────────┬───────────────────────────────┘

 │ ▼ 

┌─────────────────────────────────────────────────────┐ │ useFileUploadActions (Hook) │ │ 组合业务逻辑,返回 action 数组 │ └─────────────────────┬───────────────────────────────┘

 │ ┌───────────┼───────────┐ ▼ ▼ ▼ 

┌──────────┐ ┌──────────┐ ┌──────────┐ │ 拍照用例 │ │ 上传用例 │ │ 视频用例 │ └────┬─────┘ └────┬─────┘ └────┬─────┘

 │ │ │ ▼ ▼ ▼ 

┌─────────────────────────────────────┐ │ Adapter 层 (适配器) │ │ 适配底层 native API (相机/文件) │ └─────────────────────────────────────┘

 │ ▼ 

┌─────────────────────────────────────┐ │ 原生能力 (Native API) │ └─────────────────────────────────────┘

┌─────────────────────────────────────────────────────┐ │ UI 组件层 │ │ ┌──────────────┐ ┌──────────────┐ │ │ │ 简单上传组件 │ │ 标签上传组件 │ │ │ │ (Plain模式) │ │ (Labeling模式)│ │ │ └──────────────┘ └──────────────┘ │ │ │ │ │ │ └────────┬───────┘ │ │ ▼ │ │ ┌─────────────────────────────────────┐ │ │ │ FileItemDisplay (动态组件分发) │ │ │ └─────────────────────────────────────┘ │ │ │ │ │ ┌────────┼────────┐ │ │ ▼ ▼ ▼ │ │ ┌────────┐ ┌────────┐ ┌────────┐ │ │ │ 图片 │ │ 视频 │ │ 文件 │ │ │ │ 预览 │ │ 预览 │ │ 预览 │ │ │ └────────┘ └────────┘ └────────┘ │ └─────────────────────────────────────────────────────┘

 
     
    
       

我们把每种上传方式抽象为一个统一的 action 对象

 
     
    
       
const takePhotoAction = { 

name: ‘拍照’, handlerFunction: async () => {

// 调用拍照用例... return imageUrl 

} }

const albumAction = { name: ‘相册’, handlerFunction: async () => {

// 调用相册用例... return imageUrl 

} }

const recordVideoAction = { name: ‘拍摄’, handlerFunction: async () => {

// 调用视频用例... return videoUrl 

} }

 

这样有什么好处?

  • 组件只管调用,不管实现 - 上传组件不需要知道"拍照"和"拍视频"有什么区别
  • 可自由组合 - 页面想要哪些能力,就传哪些 action
  • 新增能力不破坏现有代码 - 新增一个 action 即可

上传组件接收 actions,而不是自己创建:

 
     
    
       
 
           

 
         
        
            
         
        
            

type: Array, default: () => [] 

}, list: {

type: Array, default: () => [] 

}, max: {

type: Number, default: 10 

} })

 

这就是最简单的依赖注入思想:我不制造依赖,我只是依赖的搬运工。

我们发现实际业务中有两种典型场景:

模式 组件 数据结构 场景 Plain PlainFileUploadRefact url[] 只关心文件 Labeling LabelingFileUploadRefact {url, label, required}[] 需要标注说明
// Plain 模式:纯 URL 数组 ['https://xxx/1.jpg', 'https://xxx/2.jpg'] // Labeling 模式:带标签的对象数组 [ { imgUrl: 'https://xxx/1.jpg', imgExplain: '哈基米', isRequired: true }, { imgUrl: 'https://xxx/2.jpg', imgExplain: '南北绿豆', isRequired: false } ]

两种模式对应不同的数据结构,但对外接口保持一致。

根据文件类型动态渲染不同的预览组件:

 
     
    
       
function getComponentForUrl(url) 

return FilePreviewComponent // 默认 }

 

预览组件的配置是这样的:

export const imagePreviewConfig = { 

match: (url) => isImage(url), component: ImagePreviewComponent }

export const videoPreviewConfig = { match: (url) => isVideo(url), component: VideoPreviewComponent }

 

这就是配置驱动的设计:新增一种文件类型,只需要添加一行配置。

以拍照为例,完整的业务流程是:

 
     
    
       
调用相机 → 拍照 → 添加水印 → 上传 OSS → (可选)下载到本地

我们把这个流程封装成一个 UseCase:

export const createTakePhotoUseCase = () => { 

const cameraService = createCameraAdapter() // 适配器

const takePhotoRefact = async (params, options = {}) =>

return url 

}

return { takePhotoRefact } }

 

这样,业务逻辑被收敛到一个地方,后续改逻辑(比如水印样式)只需要改这一处。

底层调用的是原生能力,但各平台的 API 不一致怎么办?用适配器:

 
     
    
       
export const createCameraAdapter = () => , saveImage: async (url) => { return await native.saveImage(url) } 

} }

 

如果将来换了一个原生框架(比如 uniapp),只需要改适配器,业务用例不用动。


业务页面使用非常简单:

 
      
    
        

import { useFileUploadActions } from ‘./hooks/useFileUploadActions.hook.js’ import { imagePreviewConfig } from ‘./components’

// 1. 获取 action(业务逻辑) const { takePhotoAction, albumAction } = useFileUploadActions()

// 2. 按需组合 const actions = computed(() => { return [takePhotoAction, albumAction] })

// 3. 传入组件

v-model="fileList" :actions="actions" :max="10" :customComponentMap="[imagePreviewConfig]" 

/>

 

一行 v-model,搞定上传。


重构后:

  • 代码复用 - 所有页面上传逻辑统一
  • 扩展方便 - 新增上传方式只需添加 action
  • 维护简单 - 改逻辑只改一处
  • 体验一致 - 所有页面上传交互统一

回顾整个设计,我们用到了:

  • 适配器模式 - 隔离原生 API
  • 策略模式 - 动态选择上传方式
  • 依赖注入 - 组件接收而非创建
  • 配置驱动 - 动态渲染预览组件

但我想特别强调一点:我们不是为了用模式而用模式。

所有的设计选择,都是为了解决实际问题:

  • 不用复杂的状态管理,因为场景足够简单
  • 不用 IOC 容器,因为 Hook + Props 已经够用
  • 不用过度抽象,因为当前复杂度完全可控

好的架构,不是用最"高级"的技术,而是用最"合适"的方式解决问题。

我们见过太多"过度设计"的案例——引入一堆框架和模式,最后发现业务根本不需要。适度设计,是在复杂性和可维护性之间找到的那个平衡点。


如果你有更好的方案,欢迎交流。

小讯
上一篇 2026-04-27 07:41
下一篇 2026-04-27 07:39

相关推荐

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/280452.html