ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

从DOM到设计令牌:用getComputedStyle自动提取网站视觉规范

从DOM到设计令牌:用getComputedStyle自动提取网站视觉规范 上周接到一个活儿把一套参考站点的视觉风格整理成我们中后台项目能直接用的规范。一开始我也是老一套F12打开DevTools用取色器一个颜色一个颜色地吸量间距、试字体拆到一半就发现根本撑不住——一个像样的页面几十个组件几百条CSS规则靠手抄要抄到后半夜而且抄出来的值还经常对不上。最后真正解决问题的是这条链路让DOM帮我们找结构让计算样式帮我们拿最终渲染值再把所有设计令牌自动落成一份DESIGN.md。这也是我今天想完整分享的东西。文章会覆盖整个流程的每一步从DOM里怎么定位设计信息到getComputedStyle怎么帮你拿到真实色值和字号再到如何用脚本半自动生成规范文档最后还会把我实测过程中踩过的坑一并列出来。适合要做竞品视觉分析、历史项目翻新、或者想把某个网站的视觉语言搬到内部设计系统里的同学参考。1. 为什么要把网站“拆”成设计规格提取设计风格的典型场景1.1 三种最常见的真实需求先说说你什么时候会用到这件事。我做过的项目里需求基本集中在三类。第一类是竞品视觉分析。产品经理丢过来一个竞品链接说“我们就想要这种简约有质感的风格”但“简约有质感”翻译不成设计需求。你需要把它拆成具体的色值、字体家族、字号梯度、圆角半径、阴影深度才能让设计师和前端有东西可讨论。第二类是设计语言迁移。公司收购了一个团队或者内部要统一组件库视觉规范需要把某个成熟产品的视觉风格“搬”到自己的基础组件里。这个时候你要的不是大概感觉而是一整套可映射到CSS变量、SCSS变量或Tailwind Config的设计令牌。第三类是历史项目翻新。老系统跑了好几年原始设计稿早就找不到了但线上界面就是唯一的“事实标准”。你想给这个老项目出一份设计规范反向提取线上页面就是你唯一的路。1.2 手工F12的局限说实话手工方式不是不能用而是效率低到让人崩溃。你打开Elements面板选中某个按钮右侧Styles面板里一堆规则叠在一起继承来的、伪类覆盖的、媒体查询里的、!important强写的……你看半天也分不清当前生效的到底是哪一条。更麻烦的是颜色。很多你以为的“浅蓝色”其实是半透明的蓝色叠在白色背景上取色器吸出来的是一个带透明通道的rgba值。还有一个问题同一个颜色在页面上会出现好几十次标准差可能是3%都不到靠肉眼根本判断不出来“这两个蓝是不是同一个”。所以手工方式只适用于十分钟快速看一眼做不了系统化提取。要系统化就必须让浏览器自己告诉我们答案。1.3 一条有效的自动化链路我最后沉淀下来的流程是三步用DOM解析页面结构定位关键组件节点。用getComputedStyle读取每个节点的计算样式拿到最终生效的像素值和颜色值。采集到的数据清洗、聚类后生成一份结构化的DESIGN.md作为设计组和前端组共同的参考。这套流程的好处是结果可复现、可审计。机器拿到的数据不会骗人而且下次换个网站脚本改一改选择器就能复用。这篇文章后面所有的内容都是围绕这第三步展开的。2. 动手前先搞懂DOM里到底藏着什么设计信息2.1 用DevTools做一次快速侦察在写任何脚本之前我建议你先手动打开DevTools花五分钟把目标站点的DOM结构摸一遍。不是每个网站都适合直接开脚本采的摸清楚结构能帮你省下大量调试时间。打开Elements面板后我一般按这个顺序看head里有没有内联的style块style里有没有定义--primary、--font-size-lg这类CSS变量。body上的class命名规律是语义化的.card、.btn-primary还是Tailwind那套bg-blue-500、text-lg。页面是否依赖Shadow DOM自定义组件内部样式如果封在Shadow Root里常规选择器是拿不到的。这一步看起来简单但它决定了后续的采集策略。如果站点大量使用CSS变量说明设计令牌集中在:root采集时会很轻松如果大量使用utility class说明设计令牌其实“长在class名里”你的提取逻辑要换一套写法如果Shadow DOM用得多那你就得准备用document.querySelectorAll(*)穿透不进Shadow Root得额外处理。2.2 从class命名反推组件边界设计信息是分散在整个DOM树里的但你不需要“所有”的信息你需要的是“关键样本”。我的经验是class命名基本等于设计者的意图。.hero、.card、.navbar、.footer这种语义化命名直接告诉了你组件边界在哪里。采集的时候你只需要针对这些语义节点去做采样就能覆盖这个页面绝大部分的核心样式。如果你的目标站点是utility class体系情况反过来。.text-lg、.font-bold这种class本身就承载了设计令牌的语义你甚至不需要分析具体元素直接统计页面里出现了哪些class后缀就能反推出这个网站的字体梯度、颜色梯度。我第一次做Tailwind站点提取时就用了这个思路统计页面里所有class名中的规模后缀再对比其上具体样式十分钟就整理出完整体量梯度。这个方法对任何utility体系都通用。2.3 确定采集范围全局token优先于组件级还原我见过一些人上来就写“遍历页面上所有元素把所有样式全采下来”。这个思路看着全面实际做出来是一堆垃圾。一个页面有上千个DOM元素每个元素都有几十个可计算属性全采下来你会得到几万条记录里面90%都是重复的、无意义的中间状态。正确做法是分两层采集。第一层是全局token页面背景色、正文字体、默认间距、全局圆角、阴影变量这些定义了网站的基本视觉基调。第二层是组件级采样按钮、卡片、输入框、导航栏挑每个组件的典型实例采集它的核心视觉属性。我建议你每次只采一个目标页面类型比如首页、列表页、详情页各跑一遍再合并去重。全局token层三页基本一致组件层会各有特点合并出来的规范最真实。3. 核心工具getComputedStyle如何拿到“最终样式”3.1 element.style与getComputedStyle的本质区别这里必须把概念掰清楚。DOM元素上有一个.style属性很多初学者拿它当“元素样式”的全部但它只返回内联样式也就是写在HTML标签style...里的部分。真正决定页面长什么样的是CSS文件里那些class规则里的样式而这些在element.style里全看不到。window.getComputedStyle(element, pseudoElt)拿到的才是浏览器最终计算出来的样式值。所谓“计算出来”意思是浏览器已经干完了所有活儿CSS规则匹配、层叠优先级判断、单位换算、继承计算最终算出了这个元素实际应该长什么样。用生活化一点的方式理解element.style是设计师在图纸上拍的草稿getComputedStyle是施工队按图纸盖完楼之后、你拿尺子去量出来的实际尺寸。我们采集设计风格要的是实际尺寸。const btn document.querySelector(.btn-primary); // 只会拿到内联样式通常是个空对象 console.log(btn.style.color); // 拿到最终计算样式例如 rgb(46, 91, 255) const styles window.getComputedStyle(btn); console.log(styles.color); console.log(styles.fontSize); // 例如 16px3.2 计算样式的单位换算与px化在做提取时你会注意到一个现象getComputedStyle返回的所有尺寸类属性基本都是px。这不意外——浏览器在计算样式时会把em、rem、百分比全都换算成像素值。比如你在CSS里写font-size: 2rem根字号是16px那么getComputedStyle(el).fontSize返回的就是32px。这给我们带来了巨大便利采集到的数据天然统一了单位不需要你自己去换算。但也带来一个问题像素值可能带小数。现代浏览器在子像素布局下一个元素的宽度可能被计算成382.359375px这个精度在分析设计规范时反而是噪声。后面清洗数据时我一般会做就近归整把这种值处理成整数档位。3.3 伪元素和隐藏节点的特殊采集路径getComputedStyle还有两个容易被忽略的参数语义。第一个是伪元素如果你要采集::before、::after上的样式必须传入第二个参数。const card document.querySelector(.card); // 采集按钮徽标的背景色、尺寸 const beforeStyles window.getComputedStyle(card, ::before); console.log(beforeStyles.backgroundColor);另一个坑是隐藏节点。getComputedStyle对display: none的元素也能返回样式对象但返回的布局类属性通常是auto或者0px。你拿一个display: none的弹窗去采宽度得到的是0px这显然不是设计者想要的。所以采集之前先判断元素的display、visibility、opacity和占位尺寸把不可见节点排除掉。关于visibility: hidden的元素更微妙它能拿到正常值但视觉上不存在。所以我的过滤条件是display none或visibility hidden或opacity 0或rect.width 0满足任一条件就跳过。4. 写一个半自动采集脚本从DOM遍历到设计令牌JSON4.1 Puppeteer接管采集流程手动一个个看样式不现实需要把浏览器“脚本化”。我这边用的是Puppeteer无头Chrome的Node库它能接管整页加载、DOM查询、样式读取最后把数据吐成JSON。安装很简单项目里执行npm init -y npm install puppeteer然后写一个启动脚本。这里有一个关键点等待页面加载完成不要只依赖networkidle0字体文件经常在这个事件之后才加载完。我习惯同时等待document.fonts.ready确保后续拿到的fontFamily是真实生效的字体。const puppeteer require(puppeteer); (async () { const browser await puppeteer.launch({ headless: new }); const page await browser.newPage(); await page.setViewport({ width: 1440, height: 900 }); await page.goto(https://example.com, { waitUntil: networkidle0 }); await page.evaluateHandle(() document.fonts.ready); await page.waitForTimeout(1000); console.log(页面加载完成); await browser.close(); })();4.2 关键代码遍历DOM并提取设计属性核心采集逻辑是定义一份属性清单遍历目标节点读取对应属性。属性清单的选取是有讲究的我按视觉维度分成五类维度采集属性作用颜色层color,backgroundColor,borderColor确定文本色、背景色、边框色排版层fontFamily,fontSize,fontWeight,lineHeight,letterSpacing还原字体与字阶系统间距层paddingTop/Bottom/Left/Right,marginTop/Bottom/Left/Right,gap还原间距节奏形状层borderRadius,boxShadow还原圆角与卡片层次动效层transitionDuration,transitionTimingFunction提取交互动效基准然后在对的时间、对的选择器上读取。下面这个脚本演示了如何对指定一组选择器做采样const data await page.evaluate(() { const selectors [body, .hero, .card, .btn-primary, .nav, input, footer]; const properties [ color, backgroundColor, borderColor, fontFamily, fontSize, fontWeight, lineHeight, letterSpacing, borderRadius, boxShadow, transitionDuration, transitionTimingFunction, paddingTop, paddingRight, paddingBottom, paddingLeft, marginTop, marginBottom ]; const results []; selectors.forEach((selector) { document.querySelectorAll(selector).forEach((el) { const rect el.getBoundingClientRect(); const cs window.getComputedStyle(el); // 过滤不可见节点display none、透明、无尺寸占位 if (cs.display none || cs.visibility hidden || Number(cs.opacity) 0) { return; } if (rect.width 0 rect.height 0) { return; } const record { selector: selector, className: typeof el.className string ? el.className : , text: (el.textContent || ).trim().slice(0, 30) }; properties.forEach((prop) { record[prop] cs.getPropertyValue(prop); }); results.push(record); }); }); return results; }); console.log(JSON.stringify(data, null, 2));这段代码跑完你会得到一个较大型的JSON数组里面每个节点都有完整的视觉属性快照。到这里你已经拿到了“大料”下一步是清洗。4.3 数值聚类把一堆16.4px整理成间距系统采集出来的原始值适合机器读不适合人看。比如字号会有13px、13.6px、13.999px、14.2px混在一起肉眼根本判断不出来这个网站的字阶到底是14还是13.5。这时候需要一个简单的聚类算法。思路很朴素把所有数字采集到一个数组里排序后逐个检查是否落入已有某个“桶”如果和桶均值差距小于容差就丢进桶里并更新均值否则以它为值开一个新桶。容差一般取2px比较合适既能识别真实档位又不会把14和16强行合并。function cluster(values, tolerance 2) { const sorted values.map(v parseFloat(v)).filter(v !Number.isNaN(v)).sort((a, b) a - b); const buckets []; for (const v of sorted) { const bucket buckets.find(b Math.abs(b.avg - v) tolerance); if (bucket) { bucket.values.push(v); bucket.avg bucket.values.reduce((sum, val) sum val, 0) / bucket.values.length; } else { buckets.push({ avg: v, values: [v] }); } } return buckets.map(b Math.round(b.avg * 10) / 10); }用它对所有fontSize跑一遍输出类似[12, 14, 16, 20, 24, 32, 48]这样的字阶一个网站的排版系统就浮出水面了。间距类属性也一样处理得到的就是[4, 8, 16, 24, 32, 48]这种节奏梯度。颜色不需要聚类但需要转成统一格式。getComputedStyle返回的颜色统一是rgb()或rgba()建议转成HEX方便写进文档。注意rgba转Hex时如果不处理透明通道直接拼接会让色值失真。如果遇到了半透明颜色需要先和背景色做合成再转Hex合成方法我在第6部分“颜色透明叠加”一节详细写。5. 生成DESIGN.md把JSON变成可读可评审的设计规格文档5.1 设计令牌的语义化命名采集回来的数据是“describe what it is”的红色、16px、圆角8px。但写进设计规范你需要的是“describe what it does”的语义化命名。#FF5A5F不够好color.danger才好16px不够好space.md才好。这是我强烈建议你多花半小时做的一个步骤它决定了DESIGN.md是“一次性文档”还是“可复用资产”。语义化命名的原则很简单按用途命名不按值命名。color.brand.primary而不是color.blue.500因为哪天品牌主色改成绿色了你的命名不用变前端代码也不用改只改一个映射值就够了。5.2 DESIGN.md的模板结构我用了几年之后DESIGN.md的固定结构大致长这样# Design Tokens - Example.com 来源: https://example.com 采集时间: 2025-01-10 页面类型: 官网 / 中后台 / 移动端H5 浏览器: Chrome 131 ## 品牌色 - primary: #2E5BFF - primary-hover: #1D4DDB - background: #F7F8FA - text-primary: #1F2329 - text-secondary: #646A73 ## 字体系统 - font-family: -apple-system, Segoe UI, Helvetica Neue, Arial, sans-serif - 字阶: 12 / 14 / 16 / 20 / 24 / 32 / 48 - 行高: 1.4 / 1.6 / 1.8 ## 间距系统 - space: 4 / 8 / 16 / 24 / 32 / 48 - 按钮内边距: 8px 16px ## 圆角与阴影 - radius-sm: 4px - radius-md: 8px - radius-lg: 16px - shadow-card: 0 2px 8px rgba(0, 0, 0, 0.08) ## 动效 - duration: 150ms / 300ms / 500ms - easing: ease / cubic-bezier(0.2, 0, 0, 1) ## 组件参考 ### 按钮 - 样式: [指向示例页面] - 标注: 主按钮背景色来自品牌色hover加深10%这个模板不是让你照抄的而是告诉你一份设计规格文档需要覆盖哪些信息。组件参考部分如果能配一组页面截图标注清楚采集位置对设计师的参考价值会更高。5.3 落地到设计评审中的使用方式DESIGN.md生成出来不是放进文件夹吃灰的我建议把它纳入两条流程。一是代码评审流程。我们内部把这份文档当成“视觉基线”新写的组件如果偏离了基线Code Review阶段就会被提出来。相当于给前端视觉一致性加了一道自动化门槛。二是设计评审流程。设计师拿到一个新需求先看DESIGN.md里已有token能用的直接用不够的提“新增token申请”评审通过后再补充进文档。这样设计系统是长出来的而不是憋大招憋出来的。把采集脚本的JSON结果和DESIGN.md一起提交到Git仓库每次站点改版后重跑一次脚本diff一眼就能看到视觉变化。这个用法对竞品追踪特别香——竞争对手悄悄改了主按钮颜色你是能发现的。6. 实测中的翻车清单这些坑我替你们踩过了6.1 echarts cant get dom width or height被隐藏节点的经典报错这个名字很长很唬人实际场景很多开发都遇到过页面里有图表采集数据时容器还在隐藏状态echarts初始化拿不到DOM宽高就会报这个错。我遇到的具体情况是一个Dashboard页面图表在Tab第二个面板里Puppeteer打开页面时第一个Tab激活第二个面板是display: none。我的采集脚本遍历所有.chart节点发现隐藏面板里的图表容器clientWidth和clientHeight都是0。如果脚本里直接对图表容器调用了echarts.init报错就出现了。解决方式有两个思路。思路一是采集脚本的过滤条件加一条rect.width 0 || rect.height 0就直接跳过不要在不可见节点上做初始化类操作。思路二是如果你确实需要采集隐藏图表所在组件的设计样式先切换Tab让面板可见等布局完成后用requestAnimationFrame或setTimeout再采集。这个坑提醒我一点无头浏览器加载页面后不代表所有组件都处于“可见可测量”状态。Tab、抽屉、折叠面板这种惰性渲染组件很多要等首次激活才布局。采样式之前把页面“激活一遍”是值得的。6.2 CSS变量与var()的解析陷阱遇到使用CSS变量的网站采集时有两条路径。如果你用getComputedStyle读属性浏览器已经帮你完成var()解析你拿到的是最终像素值这个没问题。但如果你去翻document.styleSheets里的原始CSS文本你会看到类似color: var(--text-primary)这样的表达式。有些脚本会直接把原始文本里的var()抓出来当设计token这就有麻烦了--text-primary到底等于什么颜色你得去:root里再找一层。我的建议是采集设计值时统一走getComputedStyle拿最终值不要自己解析CSS变量。设计文档里如果要记录变量名可以通过getComputedStyle(document.documentElement).getPropertyValue(--text-primary)从:root里再取一次。6.3 字体未加载导致的font-family失真getComputedStyle返回的fontFamily是CSS里声明的字体栈不是浏览器实际渲染用的字体。如果站点声明了Source Han Sans SC但字体还没加载完浏览器实际用的是回退字体但计算样式里还是字体栈字符串。我在采集时踩过一次某站点CSS用的是Inter但页面上没有加载这个字体文件整个页面跑的是宋体。我的采集结果里font-family: Inter看起来挺现代实际视觉完全不是那么回事。后来才知道要等document.fonts.ready再采还需要检查document.fonts.check(16px Inter)是否返回true如果字体没加载成功要在DESIGN.md里标注“字体需要单独获取授权”。6.4 断点与响应式一套样式在不同视口下的差异桌面端采集的样式在移动端视口下可能完全变了个样。同一个卡片桌面端是横排多列移动端可能变成上下堆叠同一个按钮桌面端尺寸是120px 40px移动端可能变成100% 44px。正确做法是在不同视口下分别跑采集脚本然后对每个设计属性做一次diff找出哪些属性随视口变化。用Puppeteer的page.setViewport()切换到平板和手机宽度各跑一遍把结果合并到一起。DESIGN.md的断点部分附上“桌面/平板/手机”三列对照表设计师看到后响应式调整的起点就有了。6.5 颜色透明叠加half-transparent陷阱前面提到的rgba问题这里给个完整的例子。页面上有个浅蓝色背景块getComputedStyle返回rgba(65, 105, 225, 0.2)你如果直接把它转成HEX得到的是一个半透明的蓝放到白底上肉眼看着没问题但放到灰底上就完全不对劲。正确做法是遇到带透明通道的颜色先找它背后的合成背景色一般是父节点的背景色然后用颜色混合公式把前景和背景合成为一个不透明颜色function blend(fg, bg) { const [r1, g1, b1, a1 1] fg.match(/[\d.]/g).map(Number); const [r2, g2, b2, a2 1] bg.match(/[\d.]/g).map(Number); const alpha a1 a2 * (1 - a1); return { r: Math.round((r1 * a1 r2 * a2 * (1 - a1)) / alpha), g: Math.round((g1 * a1 g2 * a2 * (1 - a1)) / alpha), b: Math.round((b1 * a1 b2 * a2 * (1 - a1)) / alpha) }; } console.log(blend(rgba(65, 105, 225, 0.2), rgb(255, 255, 255))); // { r: 217, g: 225, b: 255 }这就是你眼睛实际看到的颜色这个函数写进采集脚本里对每个带透明通道的颜色都做一次合成采集出来的颜色值才是设计文档里真正可用的。7. 边界与合规提取设计风格不等于抄前端7.1 可以提取什么不能提取什么把话说明白提取样式规格和抄袭页面是两件事。我这里讲的所有内容都是提取“规格”——色值、字号、间距、圆角这些事实数据和灵感参考用于学习、竞品分析、内部组件库的视觉基线。这都没问题。不能提取的是受版权保护的素材字体文件、图标、图片、插画、SVG素材。很多设计系统的Web字体是有许可限制的你花钱买了自己站点能用不代表可以顺手从一个网站上把字体文件拉下来给别人用。图片图标同理直接从别人服务器上扒图再用轻则侵权重则惹官司。我见过有团队把竞品网站的SVG图标整套扒下来换色后直接用。技术上是能干的但版权上一定有风险。自己按同样的风格重绘一套或者换用开源图标库这才是安全路径。7.2 安全提示小心采集过程中的DOM XSS风险做采集时很多人喜欢用iframe把目标页面嵌到自己的工具页面里再从iframe.contentDocument里读DOM。这个思路本身没问题但要警惕一个非常隐蔽的安全隐患如果你把目标站点的innerHTML直接写进自己的页面等于把这个站点里的所有脚本运行环境都“邀请”进了你的上下文。这有个专业名词叫DOM XSS。一个第三方页面里只要有一段类似innerHTML location.hash的代码你把它整个页面内嵌进来后攻击者给你工具页面发一个带恶意payload的链接脚本就会在你工具页面里执行。轻则篡改页面重则在你本地读取数据。所以我的建议是采集一律用page.evaluate在目标页面自己的上下文里执行只把计算样式结果JSON对象带回来绝不把第三方页面的HTML字符串、脚本代码复制到自己工具的DOM里。无头浏览器加载第三方站点时也不要开启--allow-file-access-from-files这类宽松参数保持默认沙箱级别出问题的概率会小很多。7.3 最终产出物的定位最后想给你一个心态上的调整DESIGN.md是参考规格不是源码克隆。它帮你理解和学习一个站点的设计语言但你的落地实现需要结合自己团队的组件库、技术栈和设计约束做二次加工。参考站点的圆角是8px你的设计系统里统一用6px这不冲突——你拿回手册后完全可以改。真正有价值的是它帮助你建立了“视觉层级”的认知你要学的是它为什么让标题14px、副标题12px、说明文字12px灰色而不是原样照搬那三个数字。一个D E S I G N文件里最关键的一行其实是记录“你为什么提取这些值”这行信息比任何色值都有价值。落地时值得多写几行上下文说明。最后补几句实际操作层面的收尾经验流程走顺之后我现在看到一个值得分析的网站第一反应不再是一处处按F12而是先把目标URL存成一个配置文件跑一遍采集脚本十分钟内拿到JSON再自动生成DESIGN.md然后对着实物做三件事检查主色有没有被透明叠加骗到、确认字体栈里的字体是否真的加载成功、把采集到的基础token映射到我们组件库的CSS变量上。这三件事做完一份可评审的设计参考文档基本就成型了。这套流程还有个衍生用法如果你维护的是自己的产品站点可以每隔一段时间跑一次采集把生成的设计令牌和上次对比颜色悄悄变了、字阶偷偷改了diff一眼就能看到。视觉一致性这种东西靠人盯是盯不住的靠脚本定时检查才靠谱。
返回列表