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

Article 与内容树

oak-general-business 的文章能力不是一个简单的“富文本表”。它更像一套轻量级内容树:

  • ArticleMenu 负责内容分类树;
  • Article 负责具体内容;
  • 组件层再补上目录、预览、编辑器、树形展示等能力。

所以如果你需要做帮助中心、知识库、栏目页、文章树,这一章会非常有价值。

主要对象

文章模块的核心实体只有两个:

  • Article
  • ArticleMenu

但这两个对象之间的联动很强:

  • ArticleMenu 维护树结构、是否存在文章、最近编辑时间;
  • Article 挂在某个 ArticleMenu 下,并且可以关联文件。

组件

这一章已经提供了非常完整的前端组件族:

  • src/components/article/detail
  • src/components/article/editor
  • src/components/article/list
  • src/components/article/preview
  • src/components/article/toc
  • src/components/article/treeList
  • src/components/article/upsert
  • src/components/articleMenu/container
  • src/components/articleMenu/detail
  • src/components/articleMenu/list
  • src/components/articleMenu/treeCell
  • src/components/articleMenu/treeList
  • src/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:适合正式详情页,常用参数有 tocClosedtocFixedtocPositionscrollIdshowtitle
  • article/preview:适合编辑器预览页,参数和 detail 很接近,但内容来源是本地缓存的 article_html

所以项目里一般会采用:

  • 编辑页用 article/upsert
  • 预览弹窗或独立预览页用 article/preview
  • 对外展示页用 article/detail

article/list / article/treeList / article/editor

如果项目里不只是“编辑单篇文章”,而是要做完整内容后台,这三组组件也很值得单独说明。

article/list 的关键参数包括:

  • articleMenuId
  • generateUrl
  • empty
  • menuCheck

它当前的真实行为是:

  • 固定按 articleMenuId 过滤文章
  • 默认按创建时间升序排列
  • 自动格式化 $$updateAt$$
  • 通过 generateUrl(mode, action, id) 统一生成详情、复制、编辑跳转地址
  • 删除文章后,会回头检查所属菜单的 isArticle 状态并通过 menuCheck 回传

这意味着它非常适合做“某个栏目下文章列表”的后台组件,而不是泛化的全站文章搜索页。

article/treeList 则更偏“树节点下的文章子列表”,常用参数有:

  • articleMenuId
  • show
  • selectedArticleId
  • setCurrentArticle
  • setCopyArticleUrl
  • drawerOpen
  • changeDrawerOpen

它还支持直接在当前菜单节点下:

  • addItem({ name: '文章标题', content: '', articleMenuId })
  • 执行创建

所以它很适合挂在 articleMenu/treeManager 旁边,做“左边目录树,右边文章子树/预览”的组合界面。

article/editor 本身就是 article/upsert 的核心编辑器能力,除了前面提到的上传和预览,它还有几个很关键的实现细节:

  • isCreation() 且传了 articleMenuId 时,会自动把新文章挂到当前菜单
  • 编辑器组件卸载时会主动 destroy()
  • 标题变化会同步改 document.title

也就是说,项目层如果需要自己重组文章编辑页布局,通常应该优先复用 article/editor,而不是从零接一套富文本编辑器。

如果项目不是只想要一棵菜单树,而是想做“左边目录、上面面包屑、右边文章列表/新增入口”的完整后台壳,更应该先看:

  • src/components/articleMenu/container

它的关键参数包括:

  • entity
  • entityId
  • title
  • origin
  • menuEmpty
  • articleEmpty
  • generateUrl(mode, action, id)

这里 generateUrl 的动作枚举是公共类型里直接定义好的:

  • detail
  • editor
  • preview
  • create
  • copy

也就是说,这个组件不是帮你“固定死路由”,而是把真正的跳转地址决定权继续留给项目层。

它当前的真实行为非常适合直接写进文档:

  • 进入时按 title 初始化面包屑
  • 会订阅 articleCreate-entityIdarticleMenuUpdate-entityId 数据事件,自动调整当前节点是不是文章目录
  • 点菜单节点后,会在内部切换 parentId / articleMenuId / showAddArticle / showAddMenu
  • 新建分类时直接创建 articleMenu
  • 新建文章时会调用 generateUrl('article', 'create', articleMenuId || parentId) 打开新页

所以它特别适合做:

  • 帮助中心后台
  • 知识库后台
  • 文档中心后台

也就是“先选目录,再决定新增分类还是新增文章”的这一类页面。

这组组件通常用来做“文档树 + 菜单树”的后台入口,常用参数包括:

  • entity / entityId:菜单树挂在哪个业务对象下
  • showeditdocpreview
  • articleMenuId / articleId
  • tocPosition
  • onMenuView / onMenuViewById
  • onArticleView / onArticlePreview / onArticleEdit
  • setCopyArticleUrl

它和 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}
  • 因此文章在别处被更新后,这个详情组件会跟着刷新

它最关键的参数则是:

  • tocClosed
  • tocFixed
  • tocPosition
  • highlightBgColor
  • headerTop
  • scrollId
  • tocWidth
  • tocHeight
  • showtitle
  • activeColor

也就是说,它并不只是“把 HTML 打出来”,而是已经把目录吸顶、滚动容器、标题显示这些阅读页常见细节一起做了。

aspect / endpoint / feature

这一章有一个边界要特别讲清楚:

  • 文章模块没有单独的 feature;
  • 没有独立的 article aspect;
  • 没有专门的 article endpoint。

如果你在微信素材相关代码里看到了 batchGetArticlegetArticle,那是微信素材接口的一部分,属于前一章的微信能力,而不是这里的本地文章模块。

本地文章模块主要靠实体、组件、checker、trigger 运转。

后台规则

文章模块的自动行为主要集中在:

  • src/triggers/article.ts
  • src/triggers/articleMenu.ts
  • src/checkers/article.ts
  • src/checkers/articleMenu.ts

默认规则包括:

  • 创建/删除文章时,自动维护所属分类的 isArticle
  • 创建/更新/删除文章时,更新分类树的 latestAt
  • 创建/更新文章和分类后,通知订阅了数据事件的前端;
  • 创建和更新分类时检查同级是否重名;
  • 删除文章前,级联删除其关联的 extraFile
  • 删除分类前,会级联删除子分类、子文章以及这些对象关联的 extraFile

这说明文章树的一致性并不是前端自己维护的,而是后端规则层在兜底。

注入点

文章能力的注入点并不在 feature,而在后端规则:

  • ogb0Triggers 注入文章和文章树的自动维护逻辑;
  • ogb0Checkers 注入删除和命名等校验;
  • 前端组件则直接围绕 article / articleMenu 数据节点工作。

article/detail 组件还会订阅文章更新事件,这说明这套能力和 Oak 的数据事件机制是连通的。

项目中如何接入

文章模块在项目里的接入方式,通常很简单:

  • 后台页面直接复用 articleMenu/treeManagerarticle/editorarticle/detail
  • 不去自己维护分类树状态,而是把树一致性留给 trigger
  • 图片和附件仍然统一复用 ExtraFile

也就是说,项目层最推荐的做法是把文章模块当成一套完整的“内容子系统”,而不是只拿 Article 表自己写一遍后台。

真实项目里的包法

haina-busitaicang 基本都采用同一种思路:

  • 后台文章编辑页直接包 article/upsert
  • 文档目录后台页直接包 articleMenu/treeManager
  • 面向用户的详情/预览页再分别包 article/detailarticle/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
  • 这样项目层仍然可以自由安排路由结构,但不需要重写树管理逻辑

使用建议

如果你准备复用这套内容系统,最推荐的做法是:

  1. 先把 ArticleMenu 当成内容目录树;
  2. 再把 Article 当成目录叶子上的内容;
  3. 前端直接复用 treeManagereditordetail 这些组件;
  4. 不要自己在页面里手动维护 isLeafisArticlelatestAt

因为这些值本来就应该交给 trigger 自动维护。