Article 与内容树
oak-general-business 的文章能力不是一个简单的“富文本表”。它更像一套轻量级内容树:
ArticleMenu负责内容分类树;Article负责具体内容;- 组件层再补上目录、预览、编辑器、树形展示等能力。
所以如果你需要做帮助中心、知识库、栏目页、文章树,这一章会非常有价值。
主要对象
文章模块的核心实体只有两个:
ArticleArticleMenu
但这两个对象之间的联动很强:
ArticleMenu维护树结构、是否存在文章、最近编辑时间;Article挂在某个ArticleMenu下,并且可以关联文件。
组件
这一章已经提供了非常完整的前端组件族:
src/components/article/detailsrc/components/article/editorsrc/components/article/listsrc/components/article/previewsrc/components/article/tocsrc/components/article/treeListsrc/components/article/upsertsrc/components/articleMenu/containersrc/components/articleMenu/detailsrc/components/articleMenu/listsrc/components/articleMenu/treeCellsrc/components/articleMenu/treeListsrc/components/articleMenu/treeManager
对新手来说,这意味着你完全可以把文章模块当成一个“可直接拼起来的内容子系统”。
article/upsert 常用参数
oak-general-business/src/components/article/upsert/index.ts 这一组参数最常用:
articleMenuId:新建文章时挂到哪个菜单下tocPosition:编辑时目录放左、右还是不显示highlightBgColor:点击目录时的高亮颜色onArticlePreview:点预览时把内容交给项目层origin:正文里插图上传走哪个 COS 来源scrollId:目录联动的滚动容器height:编辑器高度activeColor:目录当前项高亮色
haina-busi/src/pages/business/article/upsert/web.pc.tsx 的真实接法很典型:
<Upsert
oakId={oakId}
oakPath="$article-upsert"
onArticlePreview={(content, title) => {
saveData(content, title);
window.open('/doc/article/preview');
}}
tocPosition="left"
/>
article/detail / article/preview
这两组组件更适合做“读”而不是“写”:
article/detail:适合正式详情页,常用参数有tocClosed、tocFixed、tocPosition、scrollId、showtitlearticle/preview:适合编辑器预览页,参数和detail很接近,但内容来源是本地缓存的article_html
所以项目里一般会采用:
- 编辑页用
article/upsert - 预览弹窗或独立预览页用
article/preview - 对外展示页用
article/detail
article/list / article/treeList / article/editor
如果项目里不只是“编辑单篇文章”,而是要做完整内容后台,这三组组件也很值得单独说明。
article/list 的关键参数包括:
articleMenuIdgenerateUrlemptymenuCheck
它当前的真实行为是:
- 固定按
articleMenuId过滤文章 - 默认按创建时间升序排列
- 自动格式化
$$updateAt$$ - 通过
generateUrl(mode, action, id)统一生成详情、复制、编辑跳转地址 - 删除文章后,会回头检查所属菜单的
isArticle状态并通过menuCheck回传
这意味着它非常适合做“某个栏目下文章列表”的后台组件,而不是泛化的全站文章搜索页。
article/treeList 则更偏“树节点下的文章子列表”,常用参数有:
articleMenuIdshowselectedArticleIdsetCurrentArticlesetCopyArticleUrldrawerOpenchangeDrawerOpen
它还支持直接在当前菜单节点下:
addItem({ name: '文章标题', content: '', articleMenuId })- 执行创建
所以它很适合挂在 articleMenu/treeManager 旁边,做“左边目录树,右边文章子树/预览”的组合界面。
article/editor 本身就是 article/upsert 的核心编辑器能力,除了前面提到的上传和预览,它还有几个很关键的实现细节:
isCreation()且传了articleMenuId时,会自动把新文章挂到当前菜单- 编辑器组件卸载时会主动
destroy() - 标题变化会同步改
document.title
也就是说,项目层如果需要自己重组文章编辑页布局,通常应该优先复用 article/editor,而不是从零接一套富文本编辑器。
articleMenu/container 更适合做完整后台壳
如果项目不是只想要一棵菜单树,而是想做“左边目录、上面面包屑、右边文章列表/新增入口”的完整后台壳,更应该先看:
src/components/articleMenu/container
它的关键参数包括:
entityentityIdtitleoriginmenuEmptyarticleEmptygenerateUrl(mode, action, id)
这里 generateUrl 的动作枚举是公共类型里直接定义好的:
detaileditorpreviewcreatecopy
也就是说,这个组件不是帮你“固定死路由”,而是把真正的跳转地址决定权继续留给项目层。
它当前的真实行为非常适合直接写进文档:
- 进入时按
title初始化面包屑 - 会订阅
articleCreate-entityId、articleMenuUpdate-entityId数据事件,自动调整当前节点是不是文章目录 - 点菜单节点后,会在内部切换
parentId / articleMenuId / showAddArticle / showAddMenu - 新建分类时直接创建
articleMenu - 新建文章时会调用
generateUrl('article', 'create', articleMenuId || parentId)打开新页
所以它特别适合做:
- 帮助中心后台
- 知识库后台
- 文档中心后台
也就是“先选目录,再决定新增分类还是新增文章”的这一类页面。
articleMenu/treeManager 常用参数
这组组件通常用来做“文档树 + 菜单树”的后台入口,常用参数包括:
entity/entityId:菜单树挂在哪个业务对象下show:edit、doc、previewarticleMenuId/articleIdtocPositiononMenuView/onMenuViewByIdonArticleView/onArticlePreview/onArticleEditsetCopyArticleUrl
它和 articleMenu/container 的分工并不完全一样:
container更偏数据节点、面包屑、创建入口和文章列表外壳treeManager更偏完整的左右布局 UI,直接把TreeList + ArticleUpsert/ArticleCell组起来
从 web.pc.tsx 看,treeManager 当前内置了三种模式:
show='edit':左边菜单树,右边直接编辑文章show='doc':左边菜单树,右边展示文章正文show='preview':左边菜单树,右边切换“查看 / 复制链接 / 更新”
这也是为什么项目层常常不是“二选一”,而是:
- 列表或目录后台页用
container - 需要完整文档树编辑体验时再直接上
treeManager
article/detail 的真实刷新行为
article/detail 看起来像一个纯展示组件,但它其实还有一层运行时行为:
- 进入时会订阅
DATA_SUBSCRIBER_KEYS.articleUpdate-${oakId} - 因此文章在别处被更新后,这个详情组件会跟着刷新
它最关键的参数则是:
tocClosedtocFixedtocPositionhighlightBgColorheaderTopscrollIdtocWidthtocHeightshowtitleactiveColor
也就是说,它并不只是“把 HTML 打出来”,而是已经把目录吸顶、滚动容器、标题显示这些阅读页常见细节一起做了。
aspect / endpoint / feature
这一章有一个边界要特别讲清楚:
- 文章模块没有单独的 feature;
- 也没有独立的 article aspect;
- 更没有专门的 article endpoint。
如果你在微信素材相关代码里看到了 batchGetArticle、getArticle,那是微信素材接口的一部分,属于前一章的微信能力,而不是这里的本地文章模块。
本地文章模块主要靠实体、组件、checker、trigger 运转。
后台规则
文章模块的自动行为主要集中在:
src/triggers/article.tssrc/triggers/articleMenu.tssrc/checkers/article.tssrc/checkers/articleMenu.ts
默认规则包括:
- 创建/删除文章时,自动维护所属分类的
isArticle; - 创建/更新/删除文章时,更新分类树的
latestAt; - 创建/更新文章和分类后,通知订阅了数据事件的前端;
- 创建和更新分类时检查同级是否重名;
- 删除文章前,级联删除其关联的
extraFile; - 删除分类前,会级联删除子分类、子文章以及这些对象关联的
extraFile。
这说明文章树的一致性并不是前端自己维护的,而是后端规则层在兜底。
注入点
文章能力的注入点并不在 feature,而在后端规则:
ogb0Triggers注入文章和文章树的自动维护逻辑;ogb0Checkers注入删除和命名等校验;- 前端组件则直接围绕
article/articleMenu数据节点工作。
article/detail 组件还会订阅文章更新事件,这说明这套能力和 Oak 的数据事件机制是连通的。
项目中如何接入
文章模块在项目里的接入方式,通常很简单:
- 后台页面直接复用
articleMenu/treeManager、article/editor、article/detail - 不去自己维护分类树状态,而是把树一致性留给 trigger
- 图片和附件仍然统一复用
ExtraFile
也就是说,项目层最推荐的做法是把文章模块当成一套完整的“内容子系统”,而不是只拿 Article 表自己写一遍后台。
真实项目里的包法
haina-busi 和 taicang 基本都采用同一种思路:
- 后台文章编辑页直接包
article/upsert - 文档目录后台页直接包
articleMenu/treeManager - 面向用户的详情/预览页再分别包
article/detail或article/preview
也就是说,项目层通常只补:
- 页面标题
- 返回按钮
- 文章预览页路由
- 某些业务对象自己的菜单跳转逻辑
公共包本身已经把富文本编辑、目录联动、预览缓存这些基础能力准备好了。
使用示例
一个典型的帮助中心后台,通常会直接把这几块拼起来:
src/components/articleMenu/treeManager负责目录树维护;src/components/article/editor负责正文编辑;src/components/article/detail/preview负责详情和预览。
如果正文里要插图,公共组件本身也是通过 features.extraFile.autoUpload(...) 把图片挂到文章上的,所以项目层最好继续沿用这条链路,而不是自己绕过 ExtraFile 上传。
3. 用目录管理器承接项目自己的文章跳转
haina-busi/src/pages/business/articleMenu/forSquare/web.pc.tsx 的做法就是:
- 菜单页本身交给
articleMenu/treeManager onArticleEdit这类跳转由页面壳决定跳去/doc/article/upsert- 这样项目层仍然可以自由安排路由结构,但不需要重写树管理逻辑
使用建议
如果你准备复用这套内容系统,最推荐的做法是:
- 先把
ArticleMenu当成内容目录树; - 再把
Article当成目录叶子上的内容; - 前端直接复用
treeManager、editor、detail这些组件; - 不要自己在页面里手动维护
isLeaf、isArticle、latestAt。
因为这些值本来就应该交给 trigger 自动维护。