在前端工程与设计系统的协同演进中,颜色(Colors)与间距(Spacing)的自动化同步早已被大部分团队攻克。然而,一旦涉及动效设计变量(Motion Design Tokens)——涵盖复杂的贝塞尔缓动曲线(Easing Curves)、物理弹簧刚度(Spring Stiffness)以及分层时长(Duration Scales)时,许多团队又瞬间退回到了“刀耕火种”的原始时代:
- 动效设计师在 Figma Variables 或 Tokens Studio 中精心调试了一组张力十足的动态曲线,交付给研发时,却只能在飞书文档里发一张写着
(0.16, 1, 0.3, 1)的截图; - 各业务线的前端工程师在自己的项目里随心所欲地手动命名:有的叫
TRANSITION_FAST,有的写成easeCustom1,甚至直接在 CSS 中到处硬编码裸字符串; - 更头痛的是维护灾难:当品牌升级需要将全局核心缓动曲线微调 5% 时,全公司 30 多个业务仓库的前端工程师要耗费数周时间手动搜索替换,漏改、拼写错误层出不穷,最终导致整个产品矩阵的交互手感极其割裂。
为了彻底打通设计工具与代码仓库之间的最后一堵高墙,我们在内部打造了一套基于 Figma REST API、W3C DTCG 标准与 Babel/TypeScript 抽象语法树(AST)的动效自动化编译流水线。
设计师在 Figma 中发布变量更新后,CI 机器人会在 3 分钟内自动化编译并发布带有严格类型定义与详尽 JSDoc 注释的 NPM 私有包。本文将全景拆解其架构设计与核心编译器实现。
架构拓扑:单源定义到多端类型消费
整个动效代码生成体系的核心,是贯彻**单源真理(Single Source of Truth)**原则:
[Figma 官方设计系统云端文件 (Design Tokens Variables)] │ ▼ (定时 Webhook 或 GitHub Actions 触发) [Figma REST API 抓取器 (/v1/files/:key/variables/local)] │ ▼ (标准化为 W3C Design Tokens JSON 规范) [W3C DTCG 标准动效数据结构] │ ▼ (核心: TypeScript AST 编译器) ┌─────────────────┴─────────────────┐ ▼ ▼ [dist/tokens.ts (纯常量实现)] [dist/tokens.d.ts (类型与 JSDoc)] - as const 冻结常量 - 严格字面量联合类型 - CSS / WAAPI 双格式导出 - 内嵌贝塞尔曲率画像注释 │ ▼ (自动化流水线门禁校验) [NPM 私有包发布 (@company-ds/motion-tokens)]阶段一:标准化 W3C DTCG 动效数据契约
从 Figma 提取出的原始变量往往包含大量绘图软件私有的元数据,编译器首先将其清洗归一化为标准的W3C Design Tokens Community Group(DTCG)格式:
{ "motion": { "easing": { "gentle": { "$type": "cubicBezier", "$value": [0.16, 1.0, 0.3, 1.0], "$description": "适用于模态弹窗与大面积抽屉的优雅减速曲线" }, "energetic": { "$type": "cubicBezier", "$value": [0.34, 1.56, 0.64, 1.0], "$description": "带 1.25 倍超调的活泼弹跳曲线,适用于微小徽章与点赞" } }, "duration": { "instant": { "$type": "duration", "$value": "150ms" }, "moderate": { "$type": "duration", "$value": "350ms" }, "deliberate": { "$type": "duration", "$value": "600ms" } } } }阶段二:核心编译器手写:基于 AST 构建强类型代码
在 Node.js 中,如果用简单的字符串模板拼接生成代码,极易产生语法错误或格式缩进混乱。
我们采用@babel/types与@babel/generator,直接在内存中构建合法的抽象语法树(AST),自动化注入不可篡改声明(as const)与高品质 JSDoc 注释:
// compileMotionTokens.ts import * as t from '@babel/types'; import generate from '@babel/generator'; import fs from 'fs-extra'; import path from 'path'; export interface MotionTokenSource { easing: Record<string, { $value: [number, number, number, number]; $description?: string }>; duration: Record<string, { $value: string; $description?: string }>; } export function compileMotionAst(source: MotionTokenSource): string { const programStatements: t.Statement[] = []; // 1. 生成 Header 警告注释 const headerComment = `* * 🚨 该文件由 CI 动效编译流水线自动化生成,严禁手动篡改! * 数据源: Figma Design System / Motion Variables `; // 2. 编译 Easing 贝塞尔对象 const easingProperties: t.ObjectProperty[] = []; for (const [key, item] of Object.entries(source.easing)) { const [x1, y1, x2, y2] = item.$value; const cssCubicString = `cubic-bezier(${x1}, ${y1}, ${x2}, ${y2})`; // 构建嵌套对象: { cubicArray: [...], css: "..." } const propValue = t.objectExpression([ t.objectProperty( t.identifier('coordinates'), t.arrayExpression(item.$value.map(v => t.numericLiteral(v))) ), t.objectProperty( t.identifier('css'), t.stringLiteral(cssCubicString) ), ]); const property = t.objectProperty(t.identifier(key), propValue); // 附加 JSDoc 注释 if (item.$description) { t.addComment(property, 'leading', `* ${item.$description} \n * 贝塞尔参数: ${cssCubicString} `, true); } easingProperties.push(property); } // export const MotionEasings = { ... } as const; const easingsVar = t.exportNamedDeclaration( t.variableDeclaration('const', [ t.variableDeclarator( t.identifier('MotionEasings'), t.tsAsExpression( t.objectExpression(easingProperties), t.tsTypeReference(t.identifier('const')) ) ), ]) ); programStatements.push(easingsVar); // 3. 编译 Duration 时长对象 const durationProperties: t.ObjectProperty[] = []; for (const [key, item] of Object.entries(source.duration)) { const rawVal = item.$value; const msValue = parseInt(rawVal, 10); const propValue = t.objectExpression([ t.objectProperty(t.identifier('css'), t.stringLiteral(rawVal)), t.objectProperty(t.identifier('ms'), t.numericLiteral(msValue)), ]); const property = t.objectProperty(t.identifier(key), propValue); if (item.$description) { t.addComment(property, 'leading', `* ${item.$description} `, true); } durationProperties.push(property); } const durationVar = t.exportNamedDeclaration( t.variableDeclaration('const', [ t.variableDeclarator( t.identifier('MotionDurations'), t.tsAsExpression( t.objectExpression(durationProperties), t.tsTypeReference(t.identifier('const')) ) ), ]) ); programStatements.push(durationVar); // 4. 导出自动推导的联合类型 // export type MotionEasingName = keyof typeof MotionEasings; const easingTypeExport = t.exportNamedDeclaration( t.tsTypeAliasDeclaration( t.identifier('MotionEasingName'), null, t.tsTypeOperator( t.tsTypeQuery(t.identifier('MotionEasings')), 'keyof' ) ) ); programStatements.push(easingTypeExport); // 5. 将 AST 序列化为标准的 TypeScript 源码 const ast = t.program(programStatements); const output = generate.default(ast, { retainLines: false }); return `/*${headerComment}*/\n\n${output.code}\n`; }阶段三:编译产物实证与 IDE 智能体验
经过该流水线生成的产物,其类型严密性与开发者体验达到了前所未有的高度:
// 编译生成的 dist/motionTokens.ts 真实面貌 /** * 适用于模态弹窗与大面积抽屉的优雅减速曲线 * 贝塞尔参数: cubic-bezier(0.16, 1, 0.3, 1) */ export const MotionEasings = { gentle: { coordinates: [0.16, 1, 0.3, 1], css: "cubic-bezier(0.16, 1, 0.3, 1)" }, energetic: { coordinates: [0.34, 1.56, 0.64, 1], css: "cubic-bezier(0.34, 1.56, 0.64, 1)" } } as const; export const MotionDurations = { instant: { css: "150ms", ms: 150 }, moderate: { css: "350ms", ms: 350 }, deliberate: { css: "600ms", ms: 600 } } as const; export type MotionEasingName = keyof typeof MotionEasings; export type MotionDurationName = keyof typeof MotionDurations;业务端消费的无缝快感
前端工程师在组件中编写动效时,享有百分之百的 IDE 强类型自动补全与 JSDoc 悬浮提示:
// Dialog.tsx import { MotionEasings, MotionDurations } from '@company-ds/motion-tokens'; const style = { // 悬浮在 gentle 上直接显示设计规范里的说明与贝塞尔参数! transition: `transform ${MotionDurations.moderate.css} ${MotionEasings.gentle.css}`, };如果工程师不小心拼错了一个单词(如写成MotionEasings.gentll),TypeScript 编译器在代码尚未提交前就会立即红线报错,彻底扼杀了拼写错误流入线上的可能!
CI/CD 质量防护:破坏性变更拦截(Breaking Changes Guard)
在自动化流水线中,如果设计师在 Figma 里不小心删除了某个历史变量,直接发布 Minor 版本会导致全公司所有正在使用该变量的项目在打包时挂掉。
我们在 GitHub Actions 中配置了AST 语义比对门禁:
- 每次收到更新事件时,下载上一版本已发布的
@company-ds/motion-tokens; - 使用 TypeScript Compiler API 解析两个版本的 AST 类型声明;
- 如果检测到已有键名被删除或重命名,强制将版本号推升为Major(主版本),并在企业微信群中发出强行阻断报警,要求研发与设计代表共同签署变更准入。
结语:让动效成为坚固的软件契约
动效从不应该是一过性的感性猜测,它是构建数字世界物理秩序的刚性契约。
通过将 Figma 中的矢量变量自动编译为基于 AST 的 TypeScript 强类型常量包,我们彻底打破了设计与研发之间长达数年的语义代沟。当设计师在 Figma 画板上轻轻拖拽调整控制点的瞬间,数千行严格自洽的高性能代码已经在 CI 服务器中静静编译成形,为亿万用户面前的每一次界面起伏注入了精确恒久的现代工程品质。