ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

一份简历三十个字段,为什么我要为它设计一套 DSL

一份简历三十个字段,为什么我要为它设计一套 DSL 做在线简历产品时最先踩的坑通常不是「编辑器难写」而是内容和版式绑死了。用户改完一页「经典黑白」简历想换成双栏浅色模板——结果姓名、工作经历、项目描述全要重填。设计师想加一套新样式前端却发现模板里塞满了示例文案。AI 想帮忙润色职责描述又怕一改就把排版逻辑一起改坏。我们在 IResume仓库ai-resume-generator里用一套很克制的Template DSL把「写什么」和「长什么样」拆开。本文按真实代码结构讲清楚这套分离是怎么落地的。先说结论三件事不要混在一个 JSON 里层存什么谁改它换它会发生什么Document数据姓名、经历、技能、模块顺序用户 / AI / 编辑器文案变版式不变Template DSL模板布局树、配色、preset模板作者 / 设计师版式变正文不变ViewModel绑定结果渲染用的中间态运行时计算不落库预览和 PDF 共用一句话DSL 里绝不内嵌用户正文正文永远来自ResumeDocument。这也是用户在产品里能「一键换模板、内容不丢」的根因——不是魔法是存储模型就没把二者绑在一起。你可以在 模板中心 亲自切换几套版式验证这一点。为什么主题色不够用早期版本只有粗粒度theme主色、辅色、是否侧边栏。换模板时视觉几乎只是换了个颜色版式结构没真正换过来。于是引入 Template DSL目标很明确布局可描述至少能表达「页眉怎么排 模块列表怎么铺」数据可绑定同一份 Document喂给不同 DSL得到不同纸面双端同源Web 预览、小程序预览、PDF 导出走同一套绑定规则落地后我们刻意把 DSL 保持得很小没有做成「万能排版语言」。真正的视觉差异主要由tokens.preset如classic-cn、suya、qianxun驱动纸面 CSS 与分栏策略。JSON 负责契约preset 负责风格——这是工程上的务实取舍。第一层ResumeDocument只描述「内容」数据侧是固定 schema 的ResumeDocumentschemaVersion: 2.0.0。结构大致如下typeResumeDocument{schemaVersion:2.0.0;meta:{locale?:string;revision?:number};basics:{name:LocalizedString;headline:LocalizedString;email:string;phone:string;location:{city:string};photo:string|null;jobIntent?:{position?:string;salaryRange?:string;city?:string};// ...};sections:Array{id:string;type:work|project|education|skills|advantage|custom;title:LocalizedString;visible:boolean;content?:string;// 文本型模块items?:WorkItem[]|...;// 列表型模块};presentation:{sectionOrder:string[];hiddenSectionIds:string[];theme?:Recordstring,string;// 可选覆盖不是版式真源};};几个关键约束模块有序且可隐藏顺序看presentation.sectionOrder隐藏看hiddenSectionIds模板关联在行级简历记录挂template_id不在正文 JSON 里硬编码「我是哪套版式」换模板只换关联basics/sections保持原值AI 能力也因此边界清晰润色、按 JD 改写只动 Document 字段不会去改 DSL 树。内容和智能都落在数据层版式层保持稳定。第二层Template DSL只描述「版式」DSL 本身是一份可校验的 JSON{dslVersion:1.0.0,tokens:{preset:classic-cn,layout:single,palette:neutral,primary:#111827,accent:#374151},root:{type:Page,children:[{type:Header,props:{variant:classic-cn,showPhoto:true}},{type:SectionList}]}}当前节点白名单只有三类Page、Header、SectionList。看起来「简」但这正是设计点SectionList不枚举用户有哪些 section——它只声明「按 Document 的可见模块顺序渲染」Header 的props是布局指令变体、是否展示照片不是字段路径表达式双栏模板往往只改tokens.preset/layout树形状几乎不变左右栏怎么拆在绑定阶段按 preset 分流发布前走白名单校验未知节点类型、非法 layout / preset直接拒绝落库。模板库里的遗留theme行会通过dslFromTheme升成 DSL缺tokens.preset时再用 slug 补全——兼容层存在但不影响「Document 与 DSL 分离」这条主线。第三层buildViewModel把数据和模板粘起来渲染前不直接把 Document 丢给 UI而是先做一次统一绑定Document Template DSL │ ▼ resolveTemplateDsl() // 补全 preset / 兼容旧 theme │ ▼ buildViewModel(document, helpers, { preset }) │ ▼ ResumeViewModel - header联系方式、求职意向、照片位… - sections已按顺序、已过滤隐藏 - leftSections / rightSections / bannerSectionpreset 相关buildViewModel做的事情包括normalizeDocument兜底缺字段、统一 localegetOrderedSections按 presentation 规则排出可见模块把 work / project / education 的 duties、achievements 整理成渲染块按 preset 决定左右栏与 banner例如某些双栏把技能或教育挪到侧栏预览和 PDF 吃的是同一份 ViewModel。这也是「所见即所得」能站得住的关键两边不是两套各自拼装的业务逻辑。双编译React 预览 Handlebars PDF绑定之后分两条编译路径┌── ResumeViewReact / 小程序→ 交互预览 ViewModel DSL ────┤ └── compileDslToHandlebars → HTML → PDFWeb / Mini按 DSL 的 Header SectionList用各端原语渲染纸面样式按 preset 注入 CSS导出DSL 编译成 Handlebars 模板可缓存compiled_hbs再套 ViewModel 出 HTML最后走 PDF 管线权威源是 Handlebars HTML导出结果要对它负责React 预览做高保真近似并用结构对齐测试保证两边不跑偏。共享包ai-resume/resume-template-dsl负责 validate、bind、theme→DSL、HBS compile各端只保留自己的 UI 原语。Document 刻意没有塞进 shared——通过 helpers 注入normalizeDocument/getOrderedSections避免多端文档类型强耦合。这套分离在产品上换来什么1. 换模板不丢内容用户改的是 Document换模板只改template_id以及允许的 presentation 覆盖。这是产品卖点也是存储契约。2. AI 与版式互不踩脚大模型适合改「职责描述怎么写更量化」不适合改「这一行该不该 12px」。数据层给 AI模板层给设计师。3. 多端一致同一份 DSL 同一套 bindWeb 编辑器、小程序预览、服务端导出语义对齐。修一个 preset 的分栏逻辑三端一起受益。4. 新模板有明确增量路径加一套风格通常是新 preset 常量 →dslXxx()工厂 → 纸面 CSS → HBS 分支 → 样例夹具与 parity 测试。DSL 树不用每次重新发明。刻意没做成的事也值得说早期设计稿里出现过更「完整」的 DSLStack/Grid/SectionSlot、通用路径绑定如basics.name。落地时我们收敛了没有通用 path 表达式引擎——绑定是命令式的buildViewModel节点树几乎固定骨架——真正差异在 preset 与纸面 CSSReact 与 HBS 各维护一套 markup——靠 ViewModel 同源 结构 parity而不是单一 emit这不是偷懒是成本控制。简历版式的变量空间有限用「小型 AST preset」比「通用排版 DSL」更稳也更容易保证 PDF 可读文本对 ATS 友好。如果你正在设计类似「内容可复用、皮肤可替换」的编辑器可以先问自己用户真正要换的是哪些轴把轴拆开比先发明一套大而全的 DSL 更重要。最小心智模型可以把整条链路压成四句Document 用户写的简历内容唯一正文真源Template DSL 版式契约tokens 小型节点树ViewModel Document 按 preset 规范后的渲染态React / Handlebars 同一 ViewModel 的两种编译目标内容、模板、渲染器各司其职。后面无论加 AI 润色、加岗位范例还是加新视觉 preset都沿着这条缝扩展而不是往一个巨大的「简历 JSON」里继续堆字段。想从用户视角感受这套架构的结果打开 IResume 模板中心选一份岗位示例再连续切换几套版式——同一份内容会换皮不会被清空。那就是「数据 模板 DSL 分离」在产品表面上的最终形态。 程序老李前阿里技术专家10 年前端 在做一批免费办公工具简历 / PPT / Markdown / 图片压缩 / PDF —— 文件不上传全在浏览器里跑
返回列表