Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

ExtraFile 文件与对象存储

ExtraFileoak-general-business 里复用率非常高的一块能力。头像、附件、图片、文章素材、消息图片、微信素材,很多看起来不同的“文件需求”,最后都会落到这一个对象模型和一组统一的上传流程上。

它最大的价值不是“多一个文件表”,而是把:

  • 文件元数据;
  • 应用级 COS 配置;
  • 前端上传过程;
  • 分片上传;
  • 远端删除;
  • URL 生成;

全部收敛成了一套统一运行方式。

主要对象

这一章的核心实体是 ExtraFile

它定义了:

  • 文件来源 origin
  • 文件类型 type
  • 关联对象 entity/entityId
  • 文件名、后缀、大小、对象存储桶等元数据
  • 上传状态 uploadState
  • 是否启用分片上传
  • 分片上传信息 chunkInfo

也就是说,在 Oak 里“文件”从来不是游离在业务对象之外的,而是明确绑定在某个业务对象上的。

组件

围绕文件能力,这个包提供的组件非常多:

  • src/components/extraFile/upload
  • src/components/extraFile/gallery
  • src/components/extraFile/avatar
  • src/components/extraFile/crop
  • src/components/extraFile/commit
  • src/components/extraFile/forUrl

如果只是要做上传、预览、头像或附件列表,通常都不需要自己从头写。

extraFile/upload 常用参数

oak-general-business/src/components/extraFile/upload/index.ts 的参数很多,但项目里最常用的是这些:

  • entity / entityId:文件挂到哪个实体、哪条记录上
  • type:文件类型,常见是 image
  • origin:文件走哪个 COS 来源
  • tag1 / tag2:业务标签
  • autoUpload:选完文件是否立刻上传
  • maxNumber:最多允许多少个文件
  • accept:web 端允许的 MIME 类型
  • themefileimageimage-flowcustom
  • disablePreview / disableDelete / disableAdd / disableDownload
  • chunkOptions:大文件分片上传配置

结合 oak-general-business/src/components/extraFile/upload/index.ts 的真实实现,这里还有几条非常值得直接写出来的隐含约束:

  • 如果 autoUpload = true,组件内部会直接 assert(entityId),也就是自动上传模式下必须已经知道文件要挂到哪条业务记录上;
  • 组件卸载时,如果还有 uploading 状态的文件,会主动调用 features.extraFile.abortUpload(...) 中止上传;
  • 小程序端会按 type 自动分流:image/videowx.chooseMedia,其它文件走 wx.chooseMessageFile
  • calcMd5 = true 时,会在前端先算 MD5,再把值带进 extraFile.md5
  • tag1/tag2 不只是展示标签,组件内部会把它们直接带进 filter 和创建数据。

也就是说,这个组件不是一个纯 UI 壳,而是已经把:

  • 文件选择
  • 本地暂存
  • 上传态跟踪
  • 自动上传 / 手动上传分流
  • 卸载中止

这些都接进去了。

extraFile/commit 常用参数

这组组件的职责不是选文件,而是把当前节点里所有 extraFile 相关操作和实体提交一起执行。项目层最常传:

  • entity:当前页面真正要提交的实体
  • action:默认 update,也可以是项目自己的动作
  • afterCommit:提交并上传成功后的回调
  • beforeCommit:提交前拦截校验
  • messageProps:执行时的消息提示

它的真实执行顺序也很值得写清楚:

  1. 先从当前 runningTree 里递归找出本次操作里涉及的 extraFile 创建项;
  2. 先执行当前实体的 execute(...)
  3. 再把这些 extraFile 里仍处于 local/failed 状态的文件逐个调用 features.extraFile.upload(...)
  4. 如果有失败文件,会把 failureIds 留在组件状态里;
  5. 下一次再点提交时,不会重复执行实体操作,而是只重试失败上传。

这意味着 extraFile/commit 很适合放在“表单提交按钮”位置,因为它本来就不是普通按钮,而是“实体提交 + 文件补传”的组合动作。

extraFile/uploadextraFile/commit 的典型搭配

这两组组件最推荐的理解方式其实是:

  • upload 负责把文件先挂进当前 Oak 数据节点;
  • commit 负责把业务实体和文件一起真正提交完成。

所以项目层如果是“编辑资料页 + 上传附件”这种典型表单,不建议把上传和保存拆成两套互不相干的按钮,更推荐让底部主按钮直接走 extraFile/commit

extraFile/forUrl 常用参数

这个组件适合“图片地址不是本地上传,而是外部 URL”的场景。最常用的是:

  • entity / entityId
  • tag1 / tag2
  • imgUrls
  • origin

taicang 里外链新闻素材页就用了它来处理外部图片 URL。

它还有几个很容易被忽略、但源码里已经做好的行为:

  • 支持三种录入方式:本地上传、直接填 URL、从 imgUrls 里挑原图;
  • 如果 URL 是 mmbiz.qpic.cn 这类微信图片地址,会自动把 isBridge 置为 true
  • origin 为外链模式时当前会直接记成 unknown,并把 uploadState 设为 success
  • 如果当前节点本来已经挂了一张图,重新选择时会先删旧图再建新图。

所以它并不是一个“单纯展示 URL 输入框”的组件,而更像“把外部图片也纳入 extraFile 统一模型”的桥接器。

extraFile/gallery 常用参数

如果页面只需要“展示已经挂好的文件”,通常更适合直接用 extraFile/gallery,而不是继续复用上传组件。这个组件常用参数包括:

  • entity / entityId
  • tag1 / tag2
  • mode
  • size
  • style
  • disablePreview
  • disableDownload

从源码看,它有几个很明确的默认行为:

  • 会先按 sort 排序
  • 如果传了 tag1 / tag2,会先在当前数据集中做二次过滤
  • 展示 URL 和缩略图 URL 都统一走 features.extraFile.getUrl(...)
  • 文件名统一走 features.extraFile.getFileName(...)

其中:

  • style 用来控制缩略图 URL 的样式参数
  • modesize 更偏小程序展示
  • 小程序环境下如果没禁用预览,会直接调用 wx.previewImage(...)

所以项目里做:

  • 图库
  • 商品图片列表
  • 文章插图预览
  • 用户上传附件展示

这类只读场景时,优先用 gallery 会更干净,不要再拿上传组件硬改成只读模式。

前端 feature 与 aspect

文件能力最主要的前端入口是 features.extraFile。它负责:

  • 本地文件暂存;
  • 普通上传;
  • 自动上传;
  • 分片上传;
  • 终止上传;
  • 生成展示 URL。

对外公开的后端 aspect 主要有:

  • getInfoByUrl
  • mergeChunkedUpload
  • presignFile
  • presignMultiPartUpload

这些 aspect 主要服务于上传链路本身,而不是给业务层做复杂的文件处理。

trigger / watcher

这部分的后台规则非常完整。

src/triggers/extraFile.ts 负责:

  • 创建 extraFile 时生成上传所需的元数据;
  • 对分片上传生成初始分片信息;
  • 删除 extraFile 时同步删除远端文件。

src/watchers/extraFile.ts 负责:

  • 定期检查长时间处于 uploading 状态的普通上传;
  • 定期处理长时间未完成的分片上传;
  • 能合并就合并,不能合并就标记失败。

这说明 ExtraFile 不是一个“前端传完就结束”的能力,它有完整的后台补偿链路。

注入点

这一章最重要的注入点在 oak-general-business/src/features/index.tsinitialize(...)

  • 如果你传入了 COS 类数组 clazzes,就会执行 features.extraFile.registerCos(clazzes)
  • 之后 features.extraFile 才知道不同 origin 应该如何上传、签名和拼接 URL。

因此,像 bm-smart 这类项目会在初始化时显式传入 QiniuS3Aliyun 等实现。

真实项目里的 COS 注册方式

这块在 haina-busitaicang 里都能看到,而且前后端写法略有不同:

  • haina-busi/src/routines/start.tsregisterCosBackend(Aliyun)registerCosBackend(S3)
  • taicang/src/routines/start.ts / start.frontend.tsregisterCos(Qiniu)
  • 前端初始化时再把 COS 类数组传给 initializeOgb0Features(...)initializeOpb1Features(...)

这意味着:

  • 后端负责真正把 COS 供应商实现注册进运行时
  • 前端初始化负责把这些实现交给 features.extraFile
  • 页面组件只需要传 origin

项目中如何接入

ExtraFile 在项目里真正接通,至少要满足两个条件:

  • 初始化时执行 initializeOgb0Features(...)
  • 把当前项目要支持的 COS 实现传进去

bm-smart/src/initializeFeatures.ts 的真实写法就是:

await initializeOgb0Features(
  features,
  accessConfiguration,
  undefined,
  [Qiniu, S3, Aliyun]
);

只有这一步做完,features.extraFile.registerCos(clazzes) 才会被调用,后面的签名、上传、URL 拼接、分片合并才都知道该走哪个存储实现。

真实项目中的典型组合

haina-busitaicang 的页面来看,最常见的组合其实只有三种:

  • 表单页里嵌 extraFile/upload,底部按钮用 extraFile/commit
  • 富文本或图片位配置页,直接把 extraFile/upload 当一个字段控件用
  • 外部图片 URL 场景用 extraFile/forUrl
  • 详情页和只读页用 extraFile/gallery

像供应商资料、Banner、合同申请、系统 Logo、房间配置这些页面,基本都是这三种变体,没有必要每个项目自己重造上传流程。

如果按页面职责再细分一下,更推荐:

  • 已有业务对象 id,且选完就想立刻传:extraFile/upload + autoUpload
  • 业务对象还没最终提交,希望和表单一起保存:extraFile/upload + extraFile/commit
  • 图片来自公众号文章、第三方链接或素材抓取:extraFile/forUrl
  • 纯只读图片墙或附件展示:extraFile/gallery

使用示例

1. 在组件里自动上传文件

bm-smart 和公共包自己的编辑器组件,都是这么调用的:

const url = await this.features.extraFile.autoUpload({
  extraFile: {
    origin: 's3',
    type: 'image',
    entity: 'post',
    entityId: postId,
    filename: file.name,
    size: file.size,
    sort: 1000,
  },
  file,
});

这条调用会先创建 extraFile 行,再由 trigger 补上传元数据,最后上传成功后返回展示 URL。

2. 在页面里渲染文件 URL

bm-smart 里很多页面都是这样取图:

const imgUrl = this.features.extraFile.getUrl(extraFile);

所以项目层不要手工去拼对象存储访问地址,统一交给 features.extraFile.getUrl(...)

使用建议

正确的使用顺序通常是:

  1. 先创建 extraFile 数据;
  2. 让 trigger 补齐上传元数据;
  3. 再通过 features.extraFile 发起上传;
  4. 如果是分片上传,交给 watcher 兜底检查和补偿。

如果一开始就绕过 ExtraFile 直接往对象存储写文件,那么后面很多 Oak 侧的关联关系和自动行为就都接不上了。