
一句话:设计稿到代码的正确流程是「先把设计稿里的『值』对齐到 token,再写组件」——因为设计稿是静态且不完整的(没有 hover/disabled/loading/空态/长文本/小屏),所以「逐像素还原」是错的目标,「用 token 把它翻译成一致的实现,并把设计稿的缺失项补全」才是对的。
一、设计交付的三种形态(决定了你的工作方式)
⭐⭐⭐ 设计交付的三种成熟度(越靠后越省事):
① 【切图 + 标注(原始形态)】
设计师给「带标注的截图」(或 Zeplin/蓝湖标注),上面写着「间距 16px / 色值 #1677FF / 字号 14」
⚠️ 问题:这些「值」是「一次性的」——每个页面都可能出现新的 14/15/16px
→ ⭐ 代码里就会散落魔法数字
② 【设计规范文档(中间形态)】
一份「间距刻度 / 色彩系统 / 字号阶梯」文档(可能写在 Figma 的一个页面里)
⚠️ 问题:文档与设计稿「可能不一致」(设计师用了 13px,但规范里没有)→ 需要沟通
③ 【Design Tokens(现代形态)】⭐
设计稿的「值」被定义为 **Figma Variables**(或 Styles),
→ 可以**导出** → 直接生成代码里的 CSS 变量 / TS 常量(见第五节)
⭐ 好处:
· 「一份来源」(Figma)→ 代码与设计永远一致
· 「改 token 就能换肤」(见 10.8)
· 「设计稿偏移」在工具层面就能被发现(用了不在 token 里的值)
二、正确流程:五步,顺序不能反
⭐⭐⭐ 「设计稿 → 代码」的五步(顺序反了就会返工):
① 【识别「这是哪种组件」】(不是「这是什么样式」)
看到设计稿,先问:「这是已有的 Button 还是新的?」
· 已有组件 → ⭐ **直接用**(哪怕它的细节与设计稿略有差异)
· 已有组件的「新变体」→ 扩展变体(而不是复制一份改)
· 全新组件 → 才新建
⚠️ 跳过这一步的后果:项目里出现「7 个长得差不多的按钮」
② 【把值映射到 token】(不是「照抄数值」)
设计稿写「间距 16」「主色 #1677FF」「圆角 8」
→ `var(--space-inset-md)` / `var(--color-primary)` / `var(--radius-md)`
⭐ 判据:「**这个值在 token 里有对应吗**」
· 有 → 用它
· 没有 → ⭐ **问设计师**「这是新的刻度吗,还是应该归到最近的刻度?」
⚠️ 跳过这一步的后果:硬编码让「换肤/换主题」失效(见 10.8)
③ 【补全设计稿「没有画」的状态】
⭐ 这是「工程师的核心增值」(设计稿只有「正常态」)
必须自己设计:`hover` / `active` / `focus-visible` / `disabled` /
`loading` / `error` / `empty`(空数据)/ `长文本`(截断还是换行)/
`超长列表`(分页还是虚拟滚动)/ `小屏`(折叠成什么)/ `网络失败`
✅ 做法:与设计师「一起补」,而不是「自己拍脑袋」(补齐后进设计系统)
④ 【实现 + 自测】
· 用「语义化 HTML」(见 10.12)+ token + 已有的布局原语
· 自测:键盘走一遍、缩放 200%、长文本、空数据、小屏
⑤ 【设计走查(Design Review)】⭐
把实现拿去与设计稿对比 —— 但**目标是「一致」而不是「逐像素」**(见第四节)
产出:把「偏差」分成「该改代码」与「该改设计稿」两类
⭐⭐⭐ 设计稿「必然缺失」的 10 类东西(这是「靠设计稿写不出好代码」的原因):
① 交互状态:hover / active / focus-visible / disabled / selected
② 异步状态:loading / 骨架 / 进度 / 重试
③ 错误状态:字段错误 / 表单错误 / 网络错误 / 权限不足
④ 空状态:无数据 / 无搜索结果 / 首次使用
⑤ 边界数据:超长文本 / 特殊字符 / 0 项 / 1 项 / 1000 项 / 超大数字
⑥ 响应式:小屏 / 平板 / 大屏 / 横屏
⑦ 国际化:长文案(德语比英语长 30%+)/ RTL / 日期数字格式
⑧ 动效时序:进入/退出/切换的时长与曲线
⑨ 无障碍:焦点顺序 / 键盘操作 / 对比度(设计稿的浅灰常常不达标)
⑩ 数据边界:加载顺序(先有壳还是有数据)/ 局部失败 / 部分成功
⭐ 所以「还原度 100%」是个**伪指标**——它只衡量了「正常态 + 一种屏幕 + 一种数据」。
三、与现代设计工具的对接
⭐⭐ 从 Figma 到代码的三条通路:
① 【Dev Mode(手动看 + 手动抄)】
设计师切到 Dev Mode,开发者看到「标注 + 代码片段」
⚠️ ⚠️ **Figma 生成的代码片段不要直接用**(它是「绝对定位 + 内联样式」的思路,
完全不可维护)→ ⭐ 只用它看「值」,代码自己写
② 【Design Tokens 导出(自动化)】⭐ 推荐
Figma Variables →(插件 / REST API)→ 中间格式(DTCG JSON)→ 生成 CSS/TS
工具:Tokens Studio、Style Dictionary、Figma REST API + 自研脚本
⭐ 这条通路让「设计变更 → 代码变更」变成「跑一个脚本」
③ 【设计稿截图 → 生成代码(AI)】
⚠️ 目前的定位是「**加速初稿**」,不是「替代工程判断」:
它不知道「哪些是复用组件」「哪些状态缺失」「token 该怎么映射」
⭐ 正确用法:用它生成「结构骨架」→ 人工替换成「token + 已有组件」
四、像素级还原 vs 一致性:必须做的取舍
⭐⭐⭐ 一个真实冲突(几乎每个项目都会遇到):
设计稿里:卡片间距 13px、按钮圆角 6px、次要文字 #8C8C8C
token 系统里:space-3 = 12px、radius-md = 8px、color-text-secondary = #595959
【方案 A:逐像素还原】→ 硬编码 13px / 6px / #8C8C8C
✅ 「还原度 100%」(截图对比能过)
❌ ⭐ 破坏了设计系统:下次换主题时这块不会跟着变;
一个月后「哪些地方用了 13px」无从查起;新人会照抄
【方案 B:对齐 token】→ 用 12px(space-3)/ 8px(radius-md)/ #595959
✅ 系统一致、可换肤、可维护
⚠️ 「还原度 98%」(截图对比会显示差异)
⭐ 正确做法(行业共识):
· **默认选 B**(对齐 token)
· 把「设计稿与 token 的偏差」**当作一个问题反馈给设计师**
(要么「设计稿用它,我们补一个 token」,要么「设计稿改用已有 token」)
· ⭐ 只有「品牌视觉关键路径」(如营销页的主视觉)才值得「破例 + 记录例外」
⭐ 一句话结论:**「一致性」优先于「像素级还原」**——
因为「设计稿是某一刻的快照」,而「设计系统是长期的契约」。
⭐⭐ 判据表(什么样的偏差该改代码、什么样的该改设计稿):
| 偏差 | 处理 |
| --- | --- |
| 间距差 1px(13 vs 12) | ⭐ 归到最近的 token(改代码) |
| 圆角 6 vs 8 | ⭐ 归到 token(改代码),并反馈设计师 |
| 颜色是「新的色值」 | ⭐ 问设计师:是新色板还是手滑?(决定「加 token」还是「改设计稿」) |
| 组件结构不同(设计稿是新的布局) | 评估是新组件还是变体 → 讨论 → 决定 |
| 字体/字号不在体系内 | ⭐ 归到 token;若确实需要新字号,进体系 |
| 品牌关键路径的特殊效果 | 破例 + 记录例外(注释说明「为什么不用 token」) |
五、完整实现:一个「Figma Variables → Code Tokens」生成器(240 行)
// scripts/design-tokens.mjs —— 把 Figma Variables 导出转成 CSS + TS + JSON
// 用法:node scripts/design-tokens.mjs --input figma-variables.json --out packages/tokens/src
//
// 输入格式:Figma REST API 的 `GET /v1/files/:key/variables/local` 响应
// { meta: { variableCollections: {...}, variables: {...} } }
// 也兼容「Tokens Studio / 插件」导出的等价结构(见 normalize 函数)
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
import { join, dirname } from 'node:path';
// ---------- 0. 参数 ----------
const args = process.argv.slice(2);
const getArg = (name, fallback) => {
const i = args.indexOf(name);
return i >= 0 ? args[i + 1] : fallback;
};
const inputPath = getArg('--input', 'figma-variables.json');
const outDir = getArg('--out', 'src');
// ---------- 1. 读取 ----------
const raw = JSON.parse(readFileSync(inputPath, 'utf8'));
const meta = raw.meta ?? raw;
/** ============ 2. 归一化:把 Figma 的各种类型统一成「值 + 引用」 ============ */
function normalizeValue(value) {
if (value && typeof value === 'object') {
// ⭐ 别名(引用另一个变量)
if (value.type === 'VARIABLE_ALIAS' || value.type === 'VARIABLE_ALIAS_TYPE') {
return { kind: 'alias', refId: value.id };
}
// 颜色:Figma 给的是「0~1 的 rgba」
if ('r' in value && 'g' in value && 'b' in value) {
return { kind: 'color', value: rgbaToHex(value) };
}
}
return { kind: 'primitive', value };
}
function rgbaToHex({ r, g, b, a = 1 }) {
const to255 = (v) => Math.round(v * 255);
const hex = [to255(r), to255(g), to255(b)]
.map((v) => v.toString(16).padStart(2, '0'))
.join('');
const alpha = a < 1 ? (a < 0.999 ? Math.round(a * 100) / 100 : 1) : 1;
// ⭐ 有透明度时用 rgb()/rgba()(hex 带 alpha 的兼容性虽好但可读性差)
if (alpha < 1) return `rgb(${to255(r)} ${to255(g)} ${to255(b)} / ${alpha})`;
return `#${hex}`;
}
/** ============ 3. 解析变量集合与模式 ============ */
const collections = meta.variableCollections ?? {};
const variables = meta.variables ?? {};
/** 变量 id → 信息 */
const variableById = new Map(
Object.values(variables).map((v) => [
v.id,
{
id: v.id,
/** ⭐ 用「斜杠路径」做变量名:`color/primary/hover` → `--color-primary-hover` */
name: v.name,
cssName: toCssName(v.name),
type: v.resolvedType ?? v.type, // COLOR | FLOAT | STRING | BOOLEAN
collectionId: v.variableCollectionId,
valuesByMode: v.valuesByMode ?? {},
description: v.description ?? '',
},
])
);
function toCssName(name) {
return (
'--' +
name
.replace(/\//g, '-') // 路径分隔 → 连字符
.replace(/[^a-zA-Z0-9-_]/g, '-')
.replace(/([a-z0-9])([A-Z])/g, '$1-$2') // camelCase → kebab-case
.replace(/[-\s]+/g, '-')
.replace(/-+/g, '-')
.toLowerCase()
.replace(/^-+/, '')
);
}
/** ============ 4. 别名解析(含「循环引用」检测) ============ */
function resolveValue(variable, modeId, stack = new Set()) {
const rawValue = variable.valuesByMode[modeId];
if (rawValue === undefined) return { kind: 'missing' };
const normalized = normalizeValue(rawValue);
if (normalized.kind === 'alias') {
if (stack.has(variable.id)) {
throw new Error(`⛔ 检测到循环引用:${[...stack, variable.id].join(' → ')}`);
}
const target = variableById.get(normalized.refId);
if (!target) return { kind: 'missing', reason: `别名目标不存在:${normalized.refId}` };
stack.add(variable.id);
const resolved = resolveValue(target, modeId, stack);
stack.delete(variable.id);
// ⭐ 保留「别名」信息(用于生成 var(--x) 而不是字面值)
return { ...resolved, aliasTo: target.cssName };
}
return normalized;
}
// ---------- 5. 分组与排序(按 collection 分块,按变量名排序) ----------
const collectionList = Object.values(collections);
/** collectionId → modeId[](含 name) */
const modesByCollection = new Map(
collectionList.map((c) => [
c.id,
(c.modes ?? []).map((m) => ({ id: m.modeId, name: m.name })),
])
);
/** ⭐ 主题模式映射:把 Figma 的 mode 名映射到 CSS 选择器 */
const MODE_SELECTOR = {
light: ':root',
dark: "[data-theme='dark']",
// 也可以支持「品牌」模式
brand: "[data-brand='acme']",
};
function selectorForMode(modeName) {
return MODE_SELECTOR[modeName.toLowerCase()] ?? `[data-mode='${modeName.toLowerCase()}']`;
}
// ---------- 6. 生成 CSS ----------
function generateCss() {
const lines = [
'/* ⚠️ 本文件由 scripts/design-tokens.mjs 自动生成,请勿手改 */',
'/* 源:Figma Variables 导出 */',
'',
];
for (const collection of collectionList) {
const modes = modesByCollection.get(collection.id) ?? [];
const vars = Object.values(variables)
.filter((v) => v.variableCollectionId === collection.id)
.sort((a, b) => a.cssName.localeCompare(b.cssName));
if (!vars.length) continue;
lines.push(`/* ============ ${collection.name} ============ */`);
for (const mode of modes) {
// ⭐ 第一个模式(通常是 Light/默认)用 :root;其余用「属性选择器」
const isDefault = mode.id === collection.defaultModeId;
const selector = isDefault && collectionList.indexOf(collection) === 0
? ':root'
: selectorForMode(mode.name);
lines.push(`${selector} {`);
for (const v of vars) {
const value = resolveValue(v, mode.id);
if (value.kind === 'missing') {
lines.push(` /* ⚠️ ${v.cssName}: 在模式「${mode.name}」下缺失 */`);
continue;
}
const cssValue = value.value;
const comment = v.description ? ` /* ${v.description} */` : '';
lines.push(` ${v.cssName}: ${cssValue};${comment}`);
}
lines.push('}');
lines.push('');
}
}
return lines.join('\n');
}
// ---------- 7. 生成 TS(带类型 + 只读) ----------
function generateTs() {
const entries = [];
const sorted = [...variableById.values()].sort((a, b) => a.cssName.localeCompare(b.cssName));
for (const v of sorted) {
const collection = collections[v.collectionId];
const defaultMode = collection?.defaultModeId;
const value = defaultMode ? resolveValue(v, defaultMode) : { kind: 'missing' };
// ⭐ 生成「CSS 变量引用」而不是字面值:
// 这样「换主题/换品牌」时代码侧不需要改变
const tsValue = value.kind === 'missing'
? `'initial' /* ⚠️ 缺失 */`
: `'var(${v.cssName})'`;
entries.push(` /** ${v.name}${v.description ? ` —— ${v.description}` : ''} */`);
entries.push(` ${toTsKey(v.name)}: ${tsValue},`);
}
return [
'/* ⚠️ 本文件由 scripts/design-tokens.mjs 自动生成,请勿手改 */',
'',
'export const tokens = {',
...entries,
'} as const;',
'',
'export type TokenName = keyof typeof tokens;',
'',
'/** ⭐ 取某个 token 的 CSS 值(会走 CSS 变量,因此主题切换即时生效) */',
'export function token(name: TokenName): string {',
' return tokens[name];',
'}',
'',
].join('\n');
}
function toTsKey(name) {
// ⭐ `color/primary/hover` → `colorPrimaryHover`
const camel = name
.replace(/\//g, '-')
.replace(/[^a-zA-Z0-9-_]/g, '-')
.split(/[-_]+/)
.filter(Boolean)
.map((part, i) => (i === 0 ? part : part[0].toUpperCase() + part.slice(1)))
.join('');
return /^[0-9]/.test(camel) ? `_${camel}` : camel;
}
// ---------- 8. 校验(把「设计稿偏移」变成「构建期错误」) ----------
function validate() {
const problems = [];
for (const v of variableById.values()) {
// ⭐ ① 命名规范:必须「小写 + 斜杠分组 + 无空格」
if (!/^[a-z0-9]+([/-][a-z0-9]+)*$/.test(v.name)) {
problems.push(`命名不规范:${v.name}(应为 lowercase/with/slashes)`);
}
// ⭐ ② 必须属于某个 collection
if (!collections[v.collectionId]) {
problems.push(`变量不属于任何 collection:${v.name}`);
}
// ⭐ ③ 每个 mode 都要有值(否则暗色主题会「缺失变量」)
const modes = modesByCollection.get(v.collectionId) ?? [];
for (const mode of modes) {
const value = resolveValue(v, mode.id);
if (value.kind === 'missing') {
problems.push(`变量 ${v.name} 在模式「${mode.name}」下缺失值`);
}
}
}
// ⭐ ④ 检测「硬编码的颜色」(FLOAT 类型的「魔法数值」提示)
for (const v of variableById.values()) {
if (v.type === 'FLOAT' && /^(color|spacing|radius|font)/.test(v.name)) {
const value = resolveValue(v, collections[v.collectionId]?.defaultModeId);
if (value.kind === 'primitive' && typeof value.value === 'number') {
// ⚠️ 提示:尺寸类变量应「引用基础刻度」而不是「直接给数值」
// (这样改刻度时会级联更新)
if (/^(spacing|radius)/.test(v.name) && !Number.isInteger(value.value)) {
problems.push(`尺寸变量 ${v.name} 不是整数:${value.value}(可能偏离了刻度)`);
}
}
}
}
return problems;
}
// ---------- 9. 输出 ----------
const problems = validate();
if (problems.length) {
console.warn(`\n⚠️ 发现 ${problems.length} 个 token 规范问题:`);
problems.forEach((p) => console.warn(` · ${p}`));
// ⭐ 可以选择「直接失败」把问题挡在 CI 里:
// if (process.env.CI) process.exit(1);
}
mkdirSync(outDir, { recursive: true });
writeFileSync(join(outDir, 'tokens.css'), generateCss(), 'utf8');
writeFileSync(join(outDir, 'tokens.ts'), generateTs(), 'utf8');
writeFileSync(
join(outDir, 'tokens.json'),
JSON.stringify(
Object.fromEntries([...variableById.values()].map((v) => [v.name, { css: v.cssName, type: v.type }])),
null,
2
),
'utf8'
);
console.log(`✅ 已生成 ${outDir}/tokens.css、tokens.ts、tokens.json`);
console.log(` 变量 ${variableById.size} 个,集合 ${collectionList.length} 个,模式 ${[...modesByCollection.values()].reduce((n, m) => n + m.length, 0)} 个`);
void dirname;
// ✅ 示例输入(Figma Variables 的 REST API 响应摘要)
{
"meta": {
"variableCollections": {
"VariableCollectionId:1": {
"id": "VariableCollectionId:1",
"name": "Colors",
"defaultModeId": "1:0",
"modes": [
{ "modeId": "1:0", "name": "Light" },
{ "modeId": "1:1", "name": "Dark" }
]
}
},
"variables": {
"VariableID:10": {
"id": "VariableID:10",
"name": "color/primary",
"resolvedType": "COLOR",
"variableCollectionId": "VariableCollectionId:1",
"description": "主品牌色",
"valuesByMode": {
"1:0": { "r": 0.086, "g": 0.466, "b": 1, "a": 1 },
"1:1": { "r": 0.251, "g": 0.588, "b": 1, "a": 1 }
}
},
"VariableID:11": {
"id": "VariableID:11",
"name": "color/button/primary/bg",
"resolvedType": "COLOR",
"variableCollectionId": "VariableCollectionId:1",
"valuesByMode": {
"1:0": { "type": "VARIABLE_ALIAS", "id": "VariableID:10" },
"1:1": { "type": "VARIABLE_ALIAS", "id": "VariableID:10" }
}
}
}
}
}
// ✅ package.json 里的脚本(token 是「构建产物」,要提交进仓库以便 review)
{
"scripts": {
"tokens:fetch": "node scripts/fetch-figma-variables.mjs > figma-variables.json",
"tokens:build": "node scripts/design-tokens.mjs --input figma-variables.json --out packages/tokens/src",
"tokens:check": "node scripts/design-tokens.mjs --input figma-variables.json --out /tmp/tokens-check && git diff --exit-code packages/tokens/src"
}
}
⭐⭐⭐ 这个脚本解决的四个「工程化」问题:
① 【设计变更自动同步】
Figma Variables 改了 → 跑 `pnpm tokens:build` → 生成新的 CSS/TS
⭐ 不再需要「设计师发一份新的色值表 + 开发者手动改」
② 【别名解析 + 循环引用检测】
`color/button/primary/bg` → 别名到 `color/primary`
⭐ 检测循环(A→B→A)并在构建期报错
③ 【校验把「设计稿偏移」变成「构建期问题」】
· 命名不规范(大写/空格)
· 某个 mode 缺值(⭐ 这会导致「暗色主题下变量缺失」)
· 尺寸变量不是整数(可能偏离刻度)
⭐ 可以配置成「CI 里直接失败」
④ 【生成的是「CSS 变量引用」而不是字面值】
`tokens.colorPrimary === 'var(--color-primary)'`
⭐ 于是「主题切换」不需要重新生成、不需要 JS 参与(见 10.8)
六、设计走查清单(把「还原度」变成可执行的检查项)
⭐⭐⭐ 一份可执行的设计走查清单(比「像不像」有用得多):
【视觉与 token】
□ 颜色/间距/圆角/字号是否都来自 token?(用 DevTools 检查「有没有硬编码值」)
□ 浅色/深色主题下是否都正确?(⭐ 切一遍主题看)
□ 对比度是否达标(普通文本 4.5:1,见 10.12)?
【状态完整性】(⭐ 最常被漏)
□ hover / active / focus-visible / disabled / selected
□ loading / 骨架 / 空态 / 错误态
□ 校验失败时的字段错误与表单错误
【数据边界】
□ 超长文本(截断?换行?tooltip?)
□ 0 项 / 1 项 / 1000 项
□ 特殊字符与 emoji
□ 超大/超小数字(如 999999999)
【响应式】
□ 320 / 768 / 1024 / 1440 / 1920 宽
□ 横屏 / 竖屏
□ 缩放 200%(⭐ WCAG 要求)
□ 移动端「虚拟键盘弹出」时的布局
【国际化】
□ 长文案(德语/俄语)
□ RTL(阿拉伯语)—— ⭐ 用「逻辑属性」(`margin-inline-start`)而不是 `margin-left`
□ 日期/数字/货币格式
【无障碍】
□ 键盘能否走完全流程(见 10.12)
□ 焦点是否可见、是否被粘性头部遮住
□ 屏幕阅读器能否读明白
□ 点击目标 ≥ 24×24(建议 44×44)
【性能】
□ 首屏 LCP 元素是否受影响(图片/字体,见 5.15)
□ 是否引入了「布局抖动」(见 1.14)
□ 动效是否只改 transform/opacity(见 5.17)
七、本篇特有的坑
① 直接照抄 Figma Dev Mode 生成的代码片段
⚠️ 它生成的是「绝对定位 + 内联样式」,完全不可维护
✅ 只用它「看值」,代码自己写(用 token + 已有组件 + 布局原语)
② 「逐像素还原」优先于「token 一致性」
⚠️ 后果:设计系统失效、换肤失效、魔法数字遍地
✅ 默认对齐 token,把偏差反馈给设计师(见第四节)
③ 设计稿的「浅灰文字」直接照抄(对比度不达标)
⚠️ 常见缺陷:`#999` 在 `#fff` 上只有 2.85:1(低于 4.5:1)
✅ 用「已在体系里且达标的」次要文字色(见 10.12)
④ 只实现「设计稿画了的状态」(hover/disabled/loading/空态全缺)
✅ 按「状态清单」补全(第三步)
⑤ 只适配「设计稿的屏幕宽度」(如 1440)
✅ 至少测 320 / 768 / 1440,并确认「折叠策略」
⑥ 用「物理属性」写样式(`margin-left`/`text-align: left`)
⚠️ RTL 语言下会错位
✅ 用「逻辑属性」(`margin-inline-start`/`text-align: start`)
⑦ 硬编码文案(不接 i18n 系统)
⚠️ 后期接 i18n 要「全文替换」,且容易漏
✅ 一开始就用 `t('key')`(见 10.7)
⑧ 把「设计稿里的图片资源」直接下载使用(未压缩/未转格式)
✅ 走图片优化流程(AVIF/WebP + srcset,见 5.15)
⑨ 设计改了但代码没跟上(或反之)→ 两边长期漂移
✅ ⭐ 用「token 自动生成 + CI 检查」把漂移变成「构建失败」
(`tokens:check` 脚本:重新生成后跑 `git diff --exit-code`)
⑩ 「设计稿的字体」没在项目里(回退到系统字体,视觉差异大)
✅ 确认字体授权 + 子集化 + 加载策略(见 5.15)
⑪ 「组件视觉特例」直接改组件内部样式
⚠️ 污染了组件(其他页面也被改了)
✅ 优先「加变体/size」;确属特例时用「外层容器 + 局部样式」
⑫ 「设计走查」变成「设计师挑毛病 → 开发者硬改」
⭐ 后果:越改越乱、token 被破坏
✅ 约定「走查规则」:偏差必须归到「改代码」或「改设计稿」两类之一,
且「新增 token」需要评审
⑬ 只在 Chrome 看效果(Safari/Firefox 的差异被忽略)
✅ 至少在目标浏览器矩阵里抽测(尤其 `gap`/`aspect-ratio`/`backdrop-filter`)
⑭ 「暗色主题」只在最后做(发现颜色全都要调)
✅ ⭐ 一开始就用「语义 token」写(见 10.8),暗色主题只改语义层
⑮ 认为「设计稿 = 规格」
⭐ 设计稿是「某个状态下的快照」;规格是「设计系统 + 交互约定」
✅ 有疑问就问设计师(「这个间距是特例还是新刻度?」)
/* ⑥ 的完整处理(逻辑属性) */
/* ❌ 物理属性(RTL 下会错) */
.card { margin-left: 12px; padding-right: 8px; text-align: left; border-left: 2px solid; }
/* ✅ 逻辑属性(RTL 下自动镜像) */
.card {
margin-inline-start: 12px;
padding-inline-end: 8px;
text-align: start;
border-inline-start: 2px solid;
}
/* ⭐ 对应的 RTL 支持(HTML 上设 dir) */
/* <html dir="rtl" lang="ar"> */
// ⑨ 的完整处理(CI 里把「token 漂移」变成失败)
// package.json
// "tokens:check": "pnpm tokens:build && git diff --exit-code packages/tokens/src"
//
// ⭐ 含义:如果「生成的产物」与「仓库里提交的产物」不一致 →
// 说明「有人改了 Figma 但没重新生成」,或「有人手改了产物」→ CI 失败
// 解法:跑 `pnpm tokens:build` 并提交
八、面试延伸
- 「设计稿给你,你怎么落地成代码?」
五步,顺序不能反:① ⭐ 先识别「这是哪种组件」(已有组件直接用 / 是已有组件的变体则扩展变体 / 全新才新建)——跳过这步会出现「7 个长得差不多的按钮」;② ⭐ 把值映射到 token(16px→var(--space-inset-md);⚠️ 判据是「token 里有对应吗」,没有就问设计师「是新刻度还是该归到最近的」);③ ⭐ 补全设计稿没画的状态(hover/active/focus-visible/disabled/loading/error/空态/长文本/小屏/网络失败)——这是「工程师的核心增值」;④ 实现 + 自测(语义化 HTML + 键盘走一遍 + 缩放 200% + 长文本 + 空数据);⑤ 设计走查——但目标是「一致」而不是「逐像素」。 -
「设计稿有哪些『必然缺失』的东西?」
十类:交互状态(hover/active/focus/disabled/selected)、异步状态(loading/骨架/重试)、错误状态(字段/表单/网络/权限)、空状态(无数据/无搜索结果/首次使用)、边界数据(超长文本/0 项/1 项/1000 项/超大数字)、响应式(小屏/平板/大屏/横屏)、国际化(长文案/RTL/日期数字格式)、动效时序、无障碍(焦点顺序/对比度——设计稿的浅灰常常不达标)、数据边界(加载顺序/局部失败)。⭐ 所以「还原度 100%」是伪指标——它只衡量了「正常态 + 一种屏幕 + 一种数据」。 -
「『像素级还原』和『一致性』冲突时怎么办?」
场景:设计稿写13px,而 token 里是12px。逐像素还原会硬编码 13px(✅ 截图能过,❌ ⭐ 破坏设计系统——换肤失效、魔法数字遍地、后人照抄);对齐 token 用 12px(✅ 系统一致可维护,⚠️ 还原度 98%)。⭐ 行业共识是「默认对齐 token」,并把偏差当作问题反馈给设计师(要么「设计稿用它,我们补一个 token」,要么「设计稿改用已有 token」);只有「品牌视觉关键路径」才值得破例 + 记录例外。一句话:「设计稿是某一刻的快照,设计系统是长期的契约」。 -
「设计稿里的色值/间距,怎么保证代码与设计长期一致?」
⭐ 靠自动化,不靠自觉:① Design Tokens——让设计师在 Figma 里用 Variables(而不是随手写色值),然后导出(REST API 或 Tokens Studio 插件);② 生成代码——用脚本把导出转成tokens.css/tokens.ts(本篇的实现:解析 collection/mode、颜色转换、别名解析 + 循环检测、生成 CSS 变量与 TS 常量);③ ⭐ CI 检查漂移——tokens:build && git diff --exit-code,如果「生成的产物与仓库里的不一致」就失败(说明「有人改了 Figma 没重新生成」或「有人手改了产物」);④ 校验命名与完整性(如「每个 mode 都要有值」——否则暗色主题会缺变量)。 -
「Figma Dev Mode 生成的代码能直接用吗?」
不能直接用——它生成的是「绝对定位 + 内联样式」的思路(因为它只看到「这一帧的视觉结果」,不知道你的布局意图、组件体系与 token)。⭐ 正确用法是「只用它看值」:看间距/圆角/色值/字号,然后自己写代码(用 token + 已有组件 + 布局原语)。AI 生成代码(截图 → 代码) 目前的定位也一样——⭐ 它是「加速初稿」,不是「替代工程判断」:它不知道「哪些是复用组件」「哪些状态缺失」「token 该怎么映射」。正确用法是「用它生成结构骨架」→ 人工替换成「token + 已有组件」。 -
「怎么做设计走查?走查什么?」
走查的核心是「把『像不像』变成可执行的检查项」,分成七类:① 视觉与 token(有没有硬编码值?浅色/深色都对吗?对比度达标吗?);② ⭐ 状态完整性(hover/active/focus-visible/disabled/selected + loading/骨架/空态/错误态)——最常被漏;③ 数据边界(超长文本/0 项/1 项/1000 项/特殊字符/超大数字);④ 响应式(320/768/1024/1440/1920 + 横竖屏 + 缩放 200%);⑤ 国际化(长文案/RTL ⭐ 用逻辑属性/日期数字格式);⑥ 无障碍(键盘走完流程/焦点可见/屏幕阅读器/点击目标 ≥ 24×24);⑦ 性能(首屏 LCP 元素/布局抖动/动效只改 transform 与 opacity)。⭐ 还要约定走查规则:每个偏差必须归到「改代码」或「改设计稿」两类之一(不能含糊过去),且「新增 token」需要评审。 -
「RTL(阿拉伯语)适配要注意什么?」
核心是「用逻辑属性写样式」:① 盒模型——margin-left/right→margin-inline-start/end,padding-*同理;② 文本——text-align: left→text-align: start;③ 边框——border-left→border-inline-start;④ 定位——left/right→inset-inline-start/end;⑤ 图标——「箭头/返回」这类方向性图标需要镜像(transform: scaleX(-1));⑥ 数字与日期在 RTL 里仍然是从左到右(所以要用<bdi>或dir="ltr"包起来);⑦ ⭐ HTML 上设dir="rtl"(浏览器会自动处理相当一部分)。注意:flex-direction在 RTL 下不需要改(它本身就跟随书写方向)。 -
「设计改版(Redesign)怎么和代码同步?」
四条实践:① ⭐ 用 token 做「翻译层」——设计改版如果是「值的变化」(颜色/间距/圆角),理论上只改 token(不改组件代码);这正是「语义 token」的价值(见 10.8);② 分阶段走——先「改 token」让全局观感变化 → 再「逐个组件改结构」→ 最后「改页面布局」;③ ⭐ 视觉回归测试护航——每个组件的「变体 × 尺寸 × 状态」截图比对(见 10.8),改版时能立刻看到「哪些组件被意外影响了」;④ Feature Flag + 灰度(见 8.9)——大改版用「新旧两套主题 + 按用户切换」上线,出问题能立刻回退。
一句话速记
正确流程是「先识别组件(已有→直接用 / 变体→扩展 / 全新→新建)→ 把值映射到 token(没有对应就问设计师)→ 补全设计稿没画的状态 → 实现自测 → 设计走查」,⭐ 顺序不能反;设计稿必然缺失 10 类东西(交互状态 / 异步状态 / 错误 / 空态 / 边界数据 / 响应式 / 国际化 / 动效 / 无障碍 / 数据边界),所以「还原度 100% 是伪指标」;⭐ 「一致性」优先于「像素级还原」(设计稿是某一刻的快照,设计系统是长期契约)——默认对齐 token,把偏差反馈给设计师(要么补 token、要么改设计稿),只有品牌关键路径才例外记录;Figma Dev Mode / AI 生成的代码不要直接用(它是「绝对定位 + 内联样式」的思路)——⭐ 只用它看值;长期一致靠自动化:Figma Variables → 脚本生成
tokens.css/tokens.ts(含别名解析 + 循环检测 + 命名校验 + 每个 mode 都要有值)→ CI 用git diff --exit-code检查漂移;RTL 要用逻辑属性(margin-inline-start/text-align: start/inset-inline-start)+ 方向性图标镜像;设计走查清单要覆盖「token / 状态完整性 / 数据边界 / 响应式(含缩放 200%)/ 国际化 / 无障碍 / 性能」七类,且每个偏差必须归到「改代码」或「改设计稿」。



最新评论
读过书不知道欧·亨利的人少。教科书上选文有
这小生活不错呀
不错,必须顶一下!
看着你还在坚持,很好
看来忙了也没时间更新博客了
NIce。学习了。。。。
网站不错!!!!
简洁实用,好文章!