ExtraFile 文件与对象存储
ExtraFile 是 oak-general-business 里复用率非常高的一块能力。头像、附件、图片、文章素材、消息图片、微信素材,很多看起来不同的“文件需求”,最后都会落到这一个对象模型和一组统一的上传流程上。
它最大的价值不是“多一个文件表”,而是把:
- 文件元数据;
- 应用级 COS 配置;
- 前端上传过程;
- 分片上传;
- 远端删除;
- URL 生成;
全部收敛成了一套统一运行方式。
主要对象
这一章的核心实体是 ExtraFile。
它定义了:
- 文件来源
origin - 文件类型
type - 关联对象
entity/entityId - 文件名、后缀、大小、对象存储桶等元数据
- 上传状态
uploadState - 是否启用分片上传
- 分片上传信息
chunkInfo
也就是说,在 Oak 里“文件”从来不是游离在业务对象之外的,而是明确绑定在某个业务对象上的。
组件
围绕文件能力,这个包提供的组件非常多:
src/components/extraFile/uploadsrc/components/extraFile/gallerysrc/components/extraFile/avatarsrc/components/extraFile/cropsrc/components/extraFile/commitsrc/components/extraFile/forUrl
如果只是要做上传、预览、头像或附件列表,通常都不需要自己从头写。
extraFile/upload 常用参数
oak-general-business/src/components/extraFile/upload/index.ts 的参数很多,但项目里最常用的是这些:
entity/entityId:文件挂到哪个实体、哪条记录上type:文件类型,常见是imageorigin:文件走哪个 COS 来源tag1/tag2:业务标签autoUpload:选完文件是否立刻上传maxNumber:最多允许多少个文件accept:web 端允许的 MIME 类型theme:file、image、image-flow、customdisablePreview/disableDelete/disableAdd/disableDownloadchunkOptions:大文件分片上传配置
结合 oak-general-business/src/components/extraFile/upload/index.ts 的真实实现,这里还有几条非常值得直接写出来的隐含约束:
- 如果
autoUpload = true,组件内部会直接assert(entityId),也就是自动上传模式下必须已经知道文件要挂到哪条业务记录上; - 组件卸载时,如果还有
uploading状态的文件,会主动调用features.extraFile.abortUpload(...)中止上传; - 小程序端会按
type自动分流:image/video走wx.chooseMedia,其它文件走wx.chooseMessageFile; calcMd5 = true时,会在前端先算 MD5,再把值带进extraFile.md5;tag1/tag2不只是展示标签,组件内部会把它们直接带进 filter 和创建数据。
也就是说,这个组件不是一个纯 UI 壳,而是已经把:
- 文件选择
- 本地暂存
- 上传态跟踪
- 自动上传 / 手动上传分流
- 卸载中止
这些都接进去了。
extraFile/commit 常用参数
这组组件的职责不是选文件,而是把当前节点里所有 extraFile 相关操作和实体提交一起执行。项目层最常传:
entity:当前页面真正要提交的实体action:默认update,也可以是项目自己的动作afterCommit:提交并上传成功后的回调beforeCommit:提交前拦截校验messageProps:执行时的消息提示
它的真实执行顺序也很值得写清楚:
- 先从当前 runningTree 里递归找出本次操作里涉及的
extraFile创建项; - 先执行当前实体的
execute(...); - 再把这些
extraFile里仍处于local/failed状态的文件逐个调用features.extraFile.upload(...); - 如果有失败文件,会把
failureIds留在组件状态里; - 下一次再点提交时,不会重复执行实体操作,而是只重试失败上传。
这意味着 extraFile/commit 很适合放在“表单提交按钮”位置,因为它本来就不是普通按钮,而是“实体提交 + 文件补传”的组合动作。
extraFile/upload 和 extraFile/commit 的典型搭配
这两组组件最推荐的理解方式其实是:
upload负责把文件先挂进当前 Oak 数据节点;commit负责把业务实体和文件一起真正提交完成。
所以项目层如果是“编辑资料页 + 上传附件”这种典型表单,不建议把上传和保存拆成两套互不相干的按钮,更推荐让底部主按钮直接走 extraFile/commit。
extraFile/forUrl 常用参数
这个组件适合“图片地址不是本地上传,而是外部 URL”的场景。最常用的是:
entity/entityIdtag1/tag2imgUrlsorigin
taicang 里外链新闻素材页就用了它来处理外部图片 URL。
它还有几个很容易被忽略、但源码里已经做好的行为:
- 支持三种录入方式:本地上传、直接填 URL、从
imgUrls里挑原图; - 如果 URL 是
mmbiz.qpic.cn这类微信图片地址,会自动把isBridge置为true; origin为外链模式时当前会直接记成unknown,并把uploadState设为success;- 如果当前节点本来已经挂了一张图,重新选择时会先删旧图再建新图。
所以它并不是一个“单纯展示 URL 输入框”的组件,而更像“把外部图片也纳入 extraFile 统一模型”的桥接器。
extraFile/gallery 常用参数
如果页面只需要“展示已经挂好的文件”,通常更适合直接用 extraFile/gallery,而不是继续复用上传组件。这个组件常用参数包括:
entity/entityIdtag1/tag2modesizestyledisablePreviewdisableDownload
从源码看,它有几个很明确的默认行为:
- 会先按
sort排序 - 如果传了
tag1/tag2,会先在当前数据集中做二次过滤 - 展示 URL 和缩略图 URL 都统一走
features.extraFile.getUrl(...) - 文件名统一走
features.extraFile.getFileName(...)
其中:
style用来控制缩略图 URL 的样式参数mode和size更偏小程序展示- 小程序环境下如果没禁用预览,会直接调用
wx.previewImage(...)
所以项目里做:
- 图库
- 商品图片列表
- 文章插图预览
- 用户上传附件展示
这类只读场景时,优先用 gallery 会更干净,不要再拿上传组件硬改成只读模式。
前端 feature 与 aspect
文件能力最主要的前端入口是 features.extraFile。它负责:
- 本地文件暂存;
- 普通上传;
- 自动上传;
- 分片上传;
- 终止上传;
- 生成展示 URL。
对外公开的后端 aspect 主要有:
getInfoByUrlmergeChunkedUploadpresignFilepresignMultiPartUpload
这些 aspect 主要服务于上传链路本身,而不是给业务层做复杂的文件处理。
trigger / watcher
这部分的后台规则非常完整。
src/triggers/extraFile.ts 负责:
- 创建
extraFile时生成上传所需的元数据; - 对分片上传生成初始分片信息;
- 删除
extraFile时同步删除远端文件。
src/watchers/extraFile.ts 负责:
- 定期检查长时间处于
uploading状态的普通上传; - 定期处理长时间未完成的分片上传;
- 能合并就合并,不能合并就标记失败。
这说明 ExtraFile 不是一个“前端传完就结束”的能力,它有完整的后台补偿链路。
注入点
这一章最重要的注入点在 oak-general-business/src/features/index.ts 的 initialize(...):
- 如果你传入了 COS 类数组
clazzes,就会执行features.extraFile.registerCos(clazzes); - 之后
features.extraFile才知道不同origin应该如何上传、签名和拼接 URL。
因此,像 bm-smart 这类项目会在初始化时显式传入 Qiniu、S3、Aliyun 等实现。
真实项目里的 COS 注册方式
这块在 haina-busi 和 taicang 里都能看到,而且前后端写法略有不同:
haina-busi/src/routines/start.ts:registerCosBackend(Aliyun)、registerCosBackend(S3)taicang/src/routines/start.ts/start.frontend.ts:registerCos(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-busi 和 taicang 的页面来看,最常见的组合其实只有三种:
- 表单页里嵌
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(...)。
使用建议
正确的使用顺序通常是:
- 先创建
extraFile数据; - 让 trigger 补齐上传元数据;
- 再通过
features.extraFile发起上传; - 如果是分片上传,交给 watcher 兜底检查和补偿。
如果一开始就绕过 ExtraFile 直接往对象存储写文件,那么后面很多 Oak 侧的关联关系和自动行为就都接不上了。