TinaCMS MDX 多模板对象字段实战:用_template驱动块级组件数据建模与无损往返
【免费下载链接】tinacmsTinaCMS is the leading open-source headless CMS that supports Markdown and Visual Editing. Your content is stored in your own GitHub repo 🦙 ❤️项目地址: https://gitcode.com/GitHub_Trending/ti/tinacms
在 TinaCMS 的富文本编辑体验中,rich-text字段不仅支持 Markdown 排版,还允许通过模板(templates)把结构化的 React 组件直接嵌入文档正文。本文以仓库 mdx-block-object-list-template 测试用例 为切入点,深入讲解「对象列表 + 模板」(object list with templates)这一数据建模方式:如何在块级组件(如<Action>)的属性中携带一个由_template字段指定具体形态的对象,以及 MDX 的解析(parse)与序列化(stringify)如何做到无损往返。读完本文,你将掌握在 TinaCMS 中定义多形态嵌套对象字段、书写对应 MDX 语法、并通过源码与测试验证其行为的方法。
一、测试夹具全景:一个最小而完整的多模板对象用例
该测试用例位于 packages/@tinacms/mdx/src/next/tests/mdx-block-object-list-template/,共包含 5 个文件,构成一条完整的「输入 MDX → 字段定义 → AST → 输出 MDX → 测试断言」链路:
| 文件 | 作用 |
|---|---|
| in.md | 待解析的 MDX 输入,定义两个<Action>块 |
| field.ts | 对应rich-text字段的 schema,声明模板与对象子模板 |
| node.json | parseMDX解析后期望的 AST 快照 |
| out.md | serializeMDX序列化后期望的 MDX 快照(与in.md完全一致) |
| index.test.ts | vitest 测试:解析匹配 node.json,序列化匹配 out.md |
其中in.md的内容是(共 17 行,包含两个块级组件与一行普通文本):
<Action action={{ _template: "popup", title: "Say hello", description: "This is a description" }} /> And another template <Action action={{ _template: "link", title: "Say hello", url: "http://example.com" }} />可以看到,<Action>组件的action属性是一个JSX 表达式属性({{ ... }}花括号包裹的对象字面量),对象内通过_template键声明自身形态:第一个是popup,携带title与description;第二个是link,携带title与url。这正是「多模板对象」在 MDX 中的书面表达方式。
二、字段 Schema 详解:rich-text 模板中的嵌套对象模板
要让上面的 MDX 被正确解析,必须在rich-text字段上声明对应的 schema。完整定义见 field.ts:
import { RichTextField } from '@tinacms/schema-tools'; export const field: RichTextField = { name: 'body', type: 'rich-text', parser: { type: 'mdx' }, templates: [ { name: 'Action', label: 'Action', fields: [ { type: 'object', name: 'action', templates: [ { label: 'Popup', name: 'popup', fields: [ { type: 'string', name: 'title' }, { type: 'string', name: 'descrption' }, ], }, { label: 'Link', name: 'link', fields: [ { type: 'string', name: 'title' }, { type: 'string', name: 'url' }, ], }, ], }, ], }, ], };逐层拆解这个 schema 的嵌套结构:
- 顶层
templates:声明富文本中可用的块级模板(block templates)。name: 'Action'与 MDX 中的组件名<Action>一一对应,label: 'Action'用于编辑器中展示。这里parser: { type: 'mdx' }明确告诉 TinaCMS 使用 MDX 解析器而非普通 Markdown。 - 模板的
fields:Action模板拥有一个字段action,其类型为object——即「对象字段」。 - 对象的
templates:action字段内部再次声明templates,包含popup与link两个子模板,每个子模板都有自己的fields。这正是「对象列表 + 模板」的核心:同一个对象字段可以在两种(或多种)形态之间切换,形态由数据中的_template键决定。
子模板字段的数据类型(string)、字段名(title、descrption、url)与 MDX 对象字面量中的键一一对应。需要留意的是,popup 子模板中字段名写的是descrption(拼写与 MDX 中的description不一致),这属于测试用例本身遗留的拼写细节,不影响机制理解——实际项目中应保证 schema 字段名与 MDX 键完全一致,数据才能被正确映射。
与「纯对象列表字段」的区别
在 tests 目录下还有一个对比用例 mdx-block-object-list-field。两者命名相似,机制不同:
- object list field:对象字段不带
templates,而是扁平地声明多个fields,形态固定; - object list template(本文主题):对象字段带
templates,即多模板对象(polymorphic object),形态可随_template切换。
需要多形态复用(如按钮既是弹窗触发器又是外链)时,选择后者;形态固定时,选择前者即可。
三、解析产物:AST 中_template的落点
node.json记录了parseMDX的期望输出。核心结构如下(节选第一个<Action>):
{ "type": "mdxJsxFlowElement", "name": "Action", "children": [{ "type": "text", "text": "" }], "props": { "action": { "_template": "popup", "title": "Say hello", "description": "This is a description" } } }解读这份 AST:
<Action>被解析为mdxJsxFlowElement节点,name保留组件名Action;- JSX 属性被收集进
props对象,action的值就是 MDX 中{{ ... }}内的对象字面量; _template原样保留在props.action内部,与title、description平级,不单独抽离。后续的编辑会话正是靠读取这个键来实例化对应的子模板表单;- 组件若有子内容会被递归处理后放入
props.children,本用例无子内容,因此children仅含一个空文本节点。
第二个<Action>(_template: "link")的 AST 结构完全相同,只是props.action变为{ "_template": "link", "title": "Say hello", "url": "http://example.com" }。两节点之间的普通文本And another template则被解析为独立的p段落节点——说明模板块与 Markdown 文本可以在同一文档中自由混排。
四、底层实现:JSX 属性如何变成props对象
解析管线由 parse/index.ts 的parseMDX入口驱动:先fromMarkdown产出 mdast 树,再用compact合并相邻同类型节点,最后交给postProcessor。其中关键的一步位于 parse/post-processing.ts 的addPropsToMdxFlow:
node.attributes.forEach((attribute) => { if (attribute.type === 'mdxJsxAttribute') { props[attribute.name] = attribute.value; } else { throw new Error('HANDLE mdxJsxExpressionAttribute'); } });这段代码遍历 JSX 节点的attributes,把mdxJsxAttribute类型的属性按「属性名 → 值」收进props。action={{ ... }}这类表达式属性经 mdast-util-mdx-jsx 解析后,其值已经是求值后的对象字面量,因此props.action直接就是{ _template, title, ... }这样的普通对象,_template键无需特殊处理即自然落入其中。处理完成后node.attributes被删除、node.props被挂载,children被替换为空文本节点,最终交给remarkToSlate转成 TinaCMS 编辑器使用的数据结构。
五、无损往返:parse → stringify 的闭环验证
「多模板对象」机制能够安全落地,离不开解析与序列化的对称性。序列化入口在 stringify/index.ts 的stringifyMDX:经过preProcess预处理、normalizeMarkWhitespace规整空白后,由toTinaMarkdown依据同一份fieldschema 重新生成 MDX。因为 schema 与输入一致,props.action里的_template会在输出时被原样写回{{ _template: "..." }}对象字面量。
测试用例 index.test.ts 正是验证这一闭环:
it('matches input', () => { const tree = parseMDX(input, field, (v) => v); expect(util.print(tree)).toMatchFile(util.nodePath(__dirname)); const string = serializeMDX(tree, field, (v) => v); expect(string).toMatchFile(util.mdPath(__dirname)); });测试断言了两件事:
parseMDX的输出与 node.json 快照一致(其中util.print通过removePosition剔除position定位信息,保证快照不随行列号抖动,见 tests/util.ts);serializeMDX的输出与 out.md 快照一致,而 out.md 与 in.md 逐字相同。
也就是说,同一份 MDX 经「解析 → 序列化」往返后内容不变,_template: "popup"与_template: "link"两种形态都能被稳定地还原,这是多模板对象字段可用于生产内容编辑的前提。
六、在自己的 TinaCMS 项目中落地
参考本用例,在 TinaCMS 项目中配置「对象列表 + 模板」字段的步骤为:
- 在
rich-text字段(如body)的templates中声明块级模板(如Action),name与页面组件名保持一致; - 在模板
fields中加入type: 'object'的字段(如action),在其内部声明多个子模板(popup、link),并为每个子模板配置独立的fields; - 在内容 MDX 中按
<Action action={{ _template: "popup", ... }} />的语法书写,对象键与子模板字段名对齐; - 运行
parseMDX/serializeMDX(见 parse/index.ts 与 stringify/index.ts),或直接参考本用例的 index.test.ts 建立快照测试,验证往返一致性。
其余可交叉验证的用例还包括 mdx-basic-nested-objects、mdx-block-object-list-field 与 mdx-block-scalar-fields,分别覆盖嵌套对象、固定形态对象字段与标量属性等相邻场景,可作为进阶阅读材料。
小结
通过 mdx-block-object-list-template 这一个最小用例,我们完整走通了 TinaCMS「多模板对象」机制的三个层面:schema 层(object 字段嵌套 templates 声明多形态)、语法层(MDX 中_template键指定形态)、管线层(parse 将 JSX 属性归入props、stringify 原样还原,测试保证无损往返)。掌握这套模式,你就能在富文本正文中安全地嵌入结构可变、可编辑的复杂组件,让内容建模既有 Markdown 的自由度,又有组件数据的严谨性。
【免费下载链接】tinacmsTinaCMS is the leading open-source headless CMS that supports Markdown and Visual Editing. Your content is stored in your own GitHub repo 🦙 ❤️项目地址: https://gitcode.com/GitHub_Trending/ti/tinacms
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考