简历模板的三层渲染协议与迁移
06-11 定稿的边界:renderer / protocol 只管结构稳定性(slot 渲染、icon 尺寸、空值隐藏、flex 不撑爆),视觉(加粗 / 字号 / 颜色 / 间距)归模板 CSS;CSS 的内容语义命名也必须统一为协议 class,模板自定义只留布局外壳。
一、三层的职责边界
| 层 | 职责 | 禁区 |
|---|---|---|
| data | 只提供语义 | 不写视觉 |
| layout(模板 html/css) | 只描述「这个模板怎么排」 | 不在模板里查简历数据 |
| render(SlotRenderer) | 把 HTML 变成 React | 不靠模块名枚举分发 |
prototype 的结构可以迁进 layout,但必须换成 slot(<slot data-bind="basics.name">、<slot data-bind="section.items" data-template="item-tpl">)。
需要补的能力放在 render 层:例如让 <slot data-bind="section.icon"> 直接渲染图标,而不是允许在 HTML 里塞一堆 inline SVG;图标继续来自 section-meta.ts。
二、section 只分两种内容形态(关键抽象)
不要一上来就按 experience / education / projects / skills 每个都定制 —— 那会把 render 协议变成「模块枚举」,长期很重。通用抽象是内容形态:
| 形态 | 内容 | HTML 需要 |
|---|---|---|
| block | 只有一段:个人总结、技能、荣誉奖项、自定义富文本 | section.title + section.body |
| list | 多个条目:工作 / 项目 / 教育 / 社团 / 实习 / 科研经历 | section.title + section.items(item-tpl) |
render 层只需要识别:
section.kind = "block" | "list"
而不是识别一长串模块名。同一形态在不同模块下复用同一套模板,荣誉奖项 这类看数据结构决定归 block 还是 list。
三、迁移策略:先补协议,再用一个模板做样板
- 补 render 层通用协议:
profile.*、profile.contacts循环(contact.icon / contact.label)、sectionOrder、section.kind / icon / body / items、item.title / subtitle / meta / dateRange / bullets - 只迁 classic:把确认过的 prototype 迁进
templates/html/classic.html+classic.css,里面必须用 slot,不写死 demo 数据 - 验证 classic:走
[render] source: unified id: classic,视觉接近旧 React,图标与各类 section 结构正常 - classic 稳了再迁其它:用同一套协议迁 professional / modern
不要同时碰三个模板。一句话:先把 render 的通用协议补齐,再用 classic 做第一个迁移样板。
四、影响面收窄(排版混乱的来源)
本地 HTML fallback 若扫所有内置模板(professional / classic / modern),不该迁的模板也会走 unified —— 这才是排版混乱的根因,不是简历数据坏了:
classic:有templates/html/classic.html/css→source: uploaded→ 经toSerializable()变成source: unifiedprofessional / modern:继续source: builtin,走旧 React Layout
典型症状是「section 标题的灰条/蓝标签尺寸、间距、行高没重新校准」和「双栏内容全挤在左列、右侧空白」——都是模板 CSS / 版心列宽协议没和新 SlotRenderer 接好。
五、双栏布局与 DB 定档
- 双栏不要依赖 DB 的
layout.sidebar.sections来拆sidebarSections / mainSections。两栏结构由模板 HTML/CSS 自己定义:sidebar 放profile.*,main 里跑sectionOrder。layout保留为兼容旧 v1 uploaded 模板的字段,不再扩展。 - DB schema 可以定档:
customHtml / customCss / layout已经够表达 unified render。profile.*、section.kind、section.body、section.items都是 render 层从ResumeContent派生出来的协议,不需要新增字段。 - 若上线前要统一,只是「把本地 HTML/CSS 写回 DB 的
customHtml / customCss」的数据 seed / 同步,不是 schema migration。
六、渲染缺字段怎么排查
要求是逐字段对照:把「表单 / schema 里有什么」与「SlotRenderer 实际消费了什么」一条条比,产出缺失清单;先定位 bug,不要立刻改。
排查时同时拿表现正常的模板做对照(「为什么专业这一个模板又是正常的」)——同一字段在不同模板下表现不同,说明差异在模板结构而非数据本身。
七、表单字段 → slot 契约(逐字段清单)
排查「表单里有、预览里没有」时,先照这张表对,再看模板 HTML 有没有写对应 slot。表单字段没丢,丢的是模板没用这个 slot。
基础信息 basics.*(别名 profile.*)
name → 姓名 status → 求职状态 title → 求职方向
email / phone / location / website / summary / photo
为什么会有两套名字(basics 与 profile)
两者不是两份数据,读的是同一份 ResumeContent.basics:
basics.* = 表单 / 数据库真实字段名(也是 render 允许直接读的名字)
profile.* = 模板 slot 协议层的语义别名(「个人头部信息」视角)
冗余来自演进:先有表单结构 basics,模板层后来为写 profile.contacts 这种语义化循环又加了 profile.*。basics 恰好两层同名,所以看起来像同一层。
结论:render 继续兼容两套,但 skill 只教一套。
对外协议的最终划分:basic.* + profile.contacts
命名按语义分组(用户定义):
basic.* 姓名 / 求职岗位 / 求职状态 ← 头部身份,可固定样式
profile.contacts → contact.icon / label / href ← 带 icon 的联系方式
basics.*(含basics.icon.*)对外不再暴露,属于 deprecated;profile.name/title/status/summary也不是新标准。- 底层存储字段可以仍是
content.basics,但那是内部适配层,不能因此说「协议里还留着basics」。 - 判定标准是「对外协议有没有别名」,不是「代码里还有没有旧名字」。
一个名字被复用到多层,是混乱的真源
basics 同时表示:表单数据组、编辑器固定模块、sectionOrder 里的正文 section、completeness 的基础信息桶、模板旧 binding 命名空间。这些不是同一层概念却共用一个名字 —— 排查时容易把「协议冗余」误判成「两个 render」。
抽象不因局部清理而退化
清 basic / profile 只是头部区的数据分组,正文仍必须走统一的 sectionOrder / section.body / section.items / item.*。不允许为了让基础信息好改,把正文格式特殊化成一堆专用字段。
文档分层:DB schema 不靠文档镜像
docs/schema-v2/ 混了三层东西,需要分开对待:
| 文件 | 实际是什么 |
|---|---|
resume-table.json / templates-table.json | 偏 DB schema |
resume-content.json | resume.content 这个 jsonb 的内容协议,不是 DB schema |
template-slot-fields.* / html-slot-protocol.json | 模板 slot / HTML 写法协议 |
style-settings.json | content.styleSettings 的排版协议 |
DB schema 的真源是 db/schema.ts + Drizzle migration,不要再起一份文档镜像它;内容协议与模板协议则必须随架构更新(旧文档里 profile.name/title/status、兼容 basics.* 的中间态内容与「无兼容」目标冲突)。
更新顺序:先对齐上游 skill,再叠加本地协议
模板 skill 的真源在上游仓库(比本地新一点),schema 协议的真源在本地(改造最新)。所以做法是拉上游 skill 作基线,再在其上应用本地协议改造,而不是整体选一边 —— 详见 多源更新按维度取真源。
required 是模板覆盖,不是用户必填
「必含字段」要说清指谁:content_required(用户表单必填,当前基本没有)≠ template_coverage_required(模板 HTML 必须预留的 slot)。用户没填 → 渲染为空是正常的;模板没写 slot → 用户填了也永远不显示。
profile 与 contact 的显示语义分工
profile管「这个人是谁」:头像、姓名、目标岗位、求职状态、自我介绍。contact管「怎么联系这个人」:电话、邮箱、所在地、个人主页。它不是表单字段,是 render 从 profile 派生出的列表。- 两者位置常挨在一起(都在简历头部),所以看着重叠。
location是唯一真正的重叠点 —— 既可当 profile 的所在地,也可当 contact 的地址项;这是产品决定,两边都允许就会持续有重叠感。
slot / data-bind / render 三种说法不要混
slot = 模板 HTML 里的占位符语法(<slot data-bind="...">)
slot 协议 = data-bind 允许写哪些名字(profile.name / item.title ...)
render = 按 data-bind 去 ResumeContent 取值的引擎
所以「basics 和 profile 并存」是协议命名冗余,不是 slot 机制的问题,也不是两个 render。
列表条目:item.* 由不同模块的字段共同供给
| 模块 | title | subtitle | location | dateRange | 其他 |
|---|---|---|---|---|---|
工作 experience[] | company | title | location | start/end | content → item.bullets;无 meta / link |
教育 education[] | school | degree+major+gpa | location | start/end | highlights → item.bullets;无 meta / link |
项目 projects[] | name | role | location | start/end | stack → item.meta(join ·)与 item.tags;link → item.link;content → item.bullets |
研究 research[] | name | role | 无此字段 | start/end | link → item.link;content → item.bullets;无 location / meta |
块内容不走 item.*:技能 skills → section.body;自定义模块 custom[].title → section.title、custom[].content → section.body。
同一语义字段可能有多个入口(静默不显示的来源)
「城市」有两条独立渲染路径,漏掉任一条,对应模板就永远不显示城市,而且不报错:
- 个人信息头部:模板写联系方式循环
profile.contacts,城市需要由代码把basics.location派进 contacts 列表(原本只放了 phone / email / website)。 - 条目内:模板必须自己写
<slot data-bind="item.location">,CSS 不能凭空渲染字段。
所以「有的模板正常、有的不显示」时,差异在模板结构,不在数据。
缺字段怎么补:改模板,不要代码兜底
曾经加过「条目有城市但模板没写 location slot 就自动补 · 城市」的兜底,随后撤掉了。理由:协议缺字段应在模板 HTML/CSS 层补齐(含位置、字号、颜色、间距),代码兜底会与已写 slot 的模板重复,且把「模板缺什么」这个信息掩盖掉。
八、schema / protocol / renderer 三层区分(06-11 定稿)
排查「协议」问题时先分清说的是哪一层,否则会把「命名冗余」误判成「机制坏了」:
schema = 模板能写什么(词汇表 / 类型定义:slot 名、字段名、数据结构、允许的 HTML/CSS 约束)
protocol = 运行时语义 / 行为契约(这些 slot 被怎么解释、保证什么)
renderer = protocol 的执行器
schema回答:模板作者可以声明哪些槽位、数据从哪来、哪些写法合法。protocol回答:contact.icon一定渲染成 lucide icon 且按正文比例、item.link为空不占位、section.items循环自动加data-pagination-item、sectionOrder循环按 list/block 选模板、模板 CSS 被 scope 到data-template-id。- 本项目的典型 bug 正是「schema 里规定了
contact.icon,protocol 没保证它的尺寸」—— 于是每个模板 CSS 自己写一份,漏一个模板就炸(如modern的.modern-contact-item被换成通用.contact-item后图标巨大化)。修法是把运行时默认行为放进 renderer 的 protocol base CSS,而不是改 schema。
九、render 与模板 CSS 的职责边界(用户定稿)
renderer / protocol 只管「结构稳定性」;视觉表现归模板 CSS。 用户明确纠正过两次方向:
-
视觉属性不上收 renderer。 加粗(
font-weight)、字号、颜色、间距细节属于「这个模板里这个角色长什么样」,不该进 renderer 默认。曾把.item-title { font-weight: 700 }加进 renderer,被要求撤掉。renderer 的职责是 slot 渲染、icon 尺寸兜底、空字段隐藏、flex 不撑爆这类「基础可用行为」。 -
CSS 的内容语义命名也必须统一,自定义只留布局。 用户原话:「css 的自定义只有排版布局啊」。正确形态是内容语义一律绑协议 class:
<span class="item-title"><slot data-bind="item.title"></slot></span>.item-title { font-weight: 700; } /* 视觉可改,selector 仍绑协议 class */ [data-template-id="abbey-blue"] .item-title { font-size: 13px; } /* 特异性靠作用域,不靠私有命名 */不应该再出现
.pro-item-title / .entry-title / .modern-item-primary / .classic-body这类每个模板一套的内容角色命名。模板自定义 class 只用于真正的布局外壳(.modern-layout、.abbey-blue-banner、.pro-page)。
边界一句话:render/protocol 管「这个东西是什么角色,以及基础可用行为」;template CSS 管「这个模板里这个角色长什么样」。
注意区分两种「不通用」:
item.title / item.dateRange这类 slot binding 是通用协议,而 HTML class / CSS selector 曾经不通用(abbey-blue用.abbey-blue-entry-header .entry-title、professional用.pro-item-title)。「能兼容」只是现状描述,不是理想架构 —— 兼容机制让 renderer 只看 slot、外层叫什么都能填值,所以看起来没事,但每改一次结构就要扫所有模板 CSS。
十、迁移与兜底的两个机制细节
- HTML 与 CSS selector 必须成对迁移。 只改 DB HTML(
.pro-item-title→.item-title)而 CSS 还写旧 selector,视觉必丢;同时改 DB CSS,浏览器只看最终 DOM 与 CSS 是否匹配,所以线上旧 renderer 完全不知道新协议也不影响视觉。这正是「DB 改了、线上 render 没改、视觉却不受影响」的原因,也说明这次 DB 迁移本身是自洽的。 - renderer protocol CSS 是兜底基础行为,不是模板样式生效的唯一来源。 DB 模板 CSS 负责「这个模板的视觉不要断」,renderer protocol CSS 负责「所有模板都该有的最低行为」(空值隐藏、日期不换行、链接换行、icon 尺寸、正文行高),避免每个模板复制一份。
- 模板默认样式(
defaultStyleSettings)在创建 / 切换模板时写进简历内容,不在 render 阶段自动合并;渲染器对内容里的styleSettings生效。所以「默认样式不生效」要分「没写进内容」还是「渲染器没消费」两条链路查。
相关
- 样式丢失先核对类名再怀疑缓存与权限 —— HTML class 与 CSS selector 对不上,就是本节「成对迁移」缺一半的典型
- 一次循环只做一个可体验的v1能力 —— 单模板样板、验证通过再扩展的同一把尺
- 面板多出的固定区块先查数据源再改前端 —— 同源判断:先看数据链路,再改渲染
- 控件参数要在下游被消费 —— 同一项目的实例:
--heading-gap传进渲染器但模板 CSS 没消费 - 对照才产生信息 —— 「拿正常模板做对照」是本条的一处实例
- 作者是AI时协议要唯一规范答案 —— 模板作者是 AI skill,所以协议要唯一写法 + coverage 校验
- 仓库残留会误导对现状的判断 ——
prototypes/与旧文档让人误以为还有本地模板系统 - 多源更新按维度取真源 —— skill 取自上游、协议取自本地
- 兼容fallback会让内容重复渲染 ——
section.body与section.items双路径并存的代价 - 可调与固定的边界按语义划分 —— 头部固定 / 正文可调的样式边界
- 控件参数要在下游被消费 —— 协议里的 slot / 变量必须被模板真正消费
- 分层要落到运行时才有效 —— 本条是该骨架的一处实例:协议 class 与 selector 要成对迁移