目标:用 runway(store 主题设计器 / jueying 组件树)作为邮件正文的「可视化编辑外壳」, 新建一套邮件组件集与**「组件树 → HTML 字符串」服务端渲染**,使商户可在可视化界面拖拽编排邮件, 同时保证产出的是邮件客户端安全的 HTML(table 布局 + 内联样式 + 无 JS)。
1. 背景与约束
1.1 现状问题
- 邮件正文本是受限 JSX 源(
htmlContent,含{data.xxx}与.map循环),由 babel 编译(renderEmail→renderEmailBody)。 - 后台当前用 TinyMCE 编辑该 JSX 源,会破坏 JSX(把
{...}当文本、归一化class、丢失循环体),预览随之失效。 - 纯代码编辑器(Monaco/CodeMirror)对非技术运营不友好。
1.2 邮件 HTML 的硬性约束(决定方案)
邮件客户端(尤其 Outlook 用 Word 引擎)要求:
- table 布局 + 内联样式,禁止
position/flex/grid/ 媒体查询 /:hover; - 无 JS、无
<script>、无 CSS 文件引用; - 样式必须内联到每个元素。
因此任何现有组件集都不能直接复用其渲染产物:
- print 组件 → 打印/绝对定位 HTML(不适合邮件);
- store(web) 组件 → antd / CSS-in-JS / 响应式(不适合邮件)。 两者都需新建「邮件版」组件。
1.3 runway 框架已具备的能力(可复用,不重新发明)
经代码核查,runway 与 print 同为 jueying 系组件树设计器,以下能力已现成:
- ComponentData 树结构:
{ type, props:{ id, children, ... } }(src/store/public/store-design/types.ts:52)。 - field 数据绑定 + 数组循环:组件用
props.field绑定数据字段,数组字段(如items)逐行克隆展开(print 的enterprise-sales-order.ts、order-ticket.test.ts已验证)。 - 示例数据预览:
setPreviewData+ 样例数据机制(print 设计页已落地)。 - 服务端「组件树 → HTML 字符串」渲染管线:
src/store/page-render/server-render/store-publish-page.ts:34-35parsePageData(...)+ReactDOMServer.renderToString,并复用 jueyingmaishu-jueying/components-server的processTemplateString做${...}绑定。 - 通用格式化工具:
formatValue(value, format, locale)(src/print/shared/format.ts:61,支持{c:CNY}货币、{d}日期),邮件侧可照搬一份到src/common/email,避免 common→print 跨层耦合。 - Node 端 SSR 能力:
react-dom/server已在用(src/print/client/render.ts:12),邮件渲染可直接renderToStaticMarkup。
结论:框架层(拖拽 / 属性面板 / 持久化 / 示例数据 / 绑定循环 / SSR 管线思路)全部可复用; 唯一要新建的是邮件组件集与绑定后的字符串渲染器,且两者与「选哪套设计器」无关(print 仅作语义参考)。
2. 总体架构
商户可视化编辑 后端存储 发信
───────────────── ──────────── ───────────
runway 设计器 EmailTemplate 实体 mailer
(拖拽/属性面板) ─保存─▶ contentData: ComponentData ──▶ renderEmailComponentData
(示例数据面板) sampleData: object (ComponentData + 收件人数据
│ 实时预览 → HTML 字符串) ─▶ SMTP
└──────── renderEmailComponentData(contentData, sampleData)
- 现有
htmlContent(JSX 源)保留为系统内置模板兼容层;contentData为商户可视化编辑通道。 - mailer / 预览控制器按「有
contentData优先,否则回落renderEmail(htmlContent)」分支处理,现有 13 套系统模板零改动。
3. 数据模型
3.1 实体(EmailTemplate)
新增可选字段:
contentData?: ComponentData | null; // 组件树(JSON 列),可视化编辑产物
// 已有:htmlContent, sampleData, subject, content, variables, ...
ComponentData结构直接复用 jueying 定义(src/store/public/store-design/types.ts:52), 邮件侧在src/common/email复制一份轻量类型(避免 common→store 跨层依赖)。
3.2 示例数据(sampleData)
本次会话已在 EmailTemplate 实体、email-template-service、email-template-preview.controller、
后台 email/template/index.tsx 接通 sampleData(JSON,随模板保存、驱动实时预览)。
设计器「示例数据」面板即复用该字段作为收件人/订单样例。
4. 邮件组件集(src/common/email/components/)
纯 React 组件(无 antd / 无 window / 无 CSS-in-JS),render 一律输出 table + 内联样式:
| 组件 | 用途 | 关键 props |
|---|---|---|
EmailSection | 区块容器(600px 居中外壳内的段落块) | style |
EmailText | 文本段 | field / text / format / style |
EmailButton | 按钮(table+<a> 实现) | field / text / href / color |
EmailImage | 图片 | src / alt / width / srcField |
EmailDivider | 分隔线 | style |
EmailTable | 表格容器(承载循环行) | style |
EmailEach | 循环:props.field="items",子节点为行模板,渲染时按数组长度克隆并切换 item 作用域 | field |
- 组件支持
field标量绑定(文本/属性),format走本地formatValue({c:CNY}/{d})。 - 属性字段约定:
srcField/hrefField等以Field结尾的 prop 在渲染时解析为对应属性值。 - 注册表
emailComponentTypes: Record<string, React.ComponentType>,供loadEmailComponentTypes(服务端)使用。
设计器侧的「组件面板」即此集合,「属性面板」即各组件的属性编辑器(
field/format/style等)。
5. 绑定 + 循环引擎(src/common/email/render-component-data.ts)
递归遍历 ComponentData,产出 React element 树(叶子为 §4 邮件组件):
- 标量
field:value = field.split(".").reduce(scope)点路径取值,{var}文本插值复用 jueyingprocessTemplateString思路(自实现轻量版即可);format经本地formatValue格式化。 - 数组
field(即EmailEach):取数组scope[field],克隆子节点 N 份,每份以item为作用域递归求值(print 的tr[field=items]逐行克隆同思路)。 - 属性字段(
*Field):解析后写入对应 HTML 属性。 - 文本节点(
string):做{var}插值。
输出:React element 树(其叶子是邮件组件)。
6. 服务端渲染(复用 store SSR 思路,去掉 JS 注入)
renderEmailComponentData(componentData: ComponentData, data: Record<string, unknown>): string {
const element = evalNode(componentData, data); // §5
const body = ReactDOMServer.renderToStaticMarkup(element); // 内联样式自动展开
return wrapEmail(body, data); // 套 600px 居中外壳
}
对照 store-publish-page.ts:46-49:邮件不注入运行时 js / pageStyle link / <script>;
样式全部由组件内联。与现有 renderEmail(babel 编 JSX)并列,成为第二个渲染入口。
7. 设计器接入(复用 runway)
- 新建邮件设计页(指向 runway 的
design-page机制,src/store/public/store-design/design-page.tsx), 但切换数据源:- 组件面板 = §4 邮件组件集;
- 属性面板 = 邮件组件编辑器(
field/format等); - 「示例数据」面板(本次已加)= 收件人/订单样例,实时预览用它渲染。
- 保存 =
ComponentDataJSON 写回contentData字段。 - 设计器画布是浏览器 DOM(近似样式),真实邮件效果以预览/发送时的字符串渲染为准,二者组件须一致。
8. 预览 / 保存 / 发送链路(最小侵入)
email-template-preview.controller:模板有contentData→renderEmailComponentData(contentData, sampleData); 否则回落renderEmail(htmlContent, ...)。renderEmail/ mailer:同样加「有contentData走新渲染,无则 JSX」分支。- 后台
email/template/index.tsx:编辑器入口从 TinyMCE 切换为 runway 设计器(或先保留代码编辑器、后续接设计器)。
9. 迁移策略
- 系统内置 13 套:暂不转
ComponentData,继续 JSXrenderEmail;mailer 优先contentData,无则 JSX。 - 新建 / 商户编辑:走
contentData组件树。 - 后续可选:提供「JSX 模板 → ComponentData」转换脚手架(非必须)。
10. 风险与注意
- 邮件 HTML 规范:table + 内联样式;Outlook 禁 flex/grid,用
table+width。 - 设计器画布(浏览器 DOM)与邮件字符串渲染需保持组件渲染一致,避免「设计器好看、收件箱错位」。
- 现 JSX 模板的
.map循环语义要能在EmailEach表达(items数组)。 - 跨层依赖:邮件组件/渲染器放
src/common/email,避免引入 store/print 的浏览器专用模块;formatValue在src/common/email内自持一份(参考src/print/shared/format.ts:61)。
11. 实施步骤(建议顺序)
- 在
src/common/email定义轻量ComponentData类型(复用 jueying 结构)。 - 新建邮件组件集(
src/common/email/components/)+emailComponentTypes注册表 + 本地formatValue。 - 新建绑定/循环求值器(
render-component-data.ts的evalNode/resolveValue/interpolate)。 - 新建
renderEmailComponentData(§6,含wrapEmail外壳)。 - 单测(
render-component-data.test.ts):用 order 样例数据渲染,断言产出含table+ 内联style+items多行。 - 接入设计器(§7,前端)。
- 接入预览 / 保存 / 发送分支(§8)。
- 以 order 邮件为样板,跑通一个完整模板(文本字段 +
EmailEach商品列表 + 金额格式化)。