作者: 一了 <[email protected]> 日期: 2026-08-21 z-data 4.0 发布: 内置原子化 CSS 引擎 https://github.com/Funlang/z-data 4.0 那篇讲的是原子化 CSS 怎么用. 这篇把实现拆开看: 规则 怎么定义, 怎么编译, 运行时又是怎么把一个 HTML 属性变成 一条 CSS 声明、注入页面的. 整套实现就三个文件, 都在仓库 css/ 目录: css/atomic-css-rules.txt 规则定义 (DSL) css/rule-compiler.fun 规则编译器 css/z-css.js 运行时 核心思路一句话: 把规则写成数据, 用编译器把数据编译成正则 + 模板, 运行时用"属性即选择器"的方式即时翻译并注入. 什么是原子化 CSS? 传统写法给元素一个 class, 再在样式表里写一条规则. 原子化把每条样式拆成一个"原子": 一个类名只对应一个 属性值, 比如 p1 = padding, d-flex = display: flex. 在 HTML 里拼原子类名来组装样式, 不必给每个组件写一份 专属 CSS. 主流代表是 Tailwind. Tailwind 通常要构建工具扫描源码, 提前生成 CSS 文件. z-data 相反: 不生成、不构建, 在浏览器里用一个正则引擎, 把 HTML 属性上的原子标记"即时翻译"成 CSS 规则, 注入到 <style id="z-data-ss">. 开箱即用. 总架构分三段: 规则 DSL --> 编译器 --> 运行时 --> <style id='z-data-ss'> (rule-compiler.fun) (z-css.js) 1. 规则 DSL: 一份人类可读的文本, 描述"原子标记长什么样、 翻译成什么 CSS". 2. 编译器: 用 funlang 写的小工具, 读 DSL, 展开成两个产物 -- 一个带编号捕获组的大正则 pattern, 一组模板字符串 targets, 写回运行时文件. 3. 运行时: 浏览器里的 JS, 拿到元素属性值, 用正则匹配, 用模板算 CSS 声明, 合并序列化后写进 <style>. 规则当数据、代码自动生成, 新增一条规则只需改 DSL, 不用动 运行时逻辑. 规则 DSL 文件开头定义一组"缩写表"(env) 和若干条规则: s = \w+ # 小写字母(正则) n = -?(\d*\.)?\d+|--\w+ # 数字或 CSS 变量 PM = {p: 'padding', m: 'margin'} HW = {h: 'height', w: 'width'} LTRB = {l: 'left', t: 'top', r: 'right', b: 'bottom'} NUMS = (num, unit) => /^--/.test(num) ? `var(${num})` : `${unit ? num : num / 4}${unit || 'rem'}` 每条规则分左右两部分, 左边是 BNF 风格的模式, 右边是翻译 模板. 比如第一条: (pm PM) [side LTRB] (num N) [unit U] = {pm}[-side]:{NUMS(num,unit)} (pm PM) 命名捕获组 pm, 取值枚举来自 PM 缩写表 [side LTRB] 可选捕获组, 方向 (num N) 数字 [unit U] 可选单位 {pm} 模板变量 {NUMS(...)} 调用函数 # 注释 这条规则把"padding/margin + 可选方向 + 数字 + 可选单位" 全盖住了: p1, pl2, mt3px, px4% 都命中, 不必为每种写法写 一条规则. 这就是"规则作为数据"的价值. 编译器 rule-compiler.fun 把 DSL 机械地翻译成运行时能用的代码, 分两步: 左侧, 把 BNF 组展开成正则捕获组. (pm PM) 查缩写表得到 p|m, 包成命名捕获组; 可选组 [...] 加 ?. 第一条展开成: (?<pm>p|m)(?<side>l|t|r|b)?(?<num>-?(\d*\.)?\d+|--\w+)(?<unit>\w+|%)? 右侧, 把 {...} / [...] 翻译成 JS 模板表达式. 普通变量 {pm} 变 ${pm}; 可选组 [...] 调 IF(name,set,'-',''), 有值 输出 -side, 无值空. 第一条的模板是: ${PM[pm]}${IF(side,LTRB,'-','')}:${NUMS(num,unit)} 它和正则一一对应, 运行时用 with 求值就算出 padding-left: 3px 之类的结果. 编译器把多条规则拼成一个大正则, 收集命名捕获组的名字顺序 存进 patterns 数组, 把每条模板存进 targets 对象, 写回 z-css.js. 大正则里全是普通编号捕获组, 名字抽到数组, 这样 replace 回调里按位置参数 (args[i+1]) 就能还原出每个命名组 的值. 有个小坑被巧妙处理了: monaco 正则把 \S 换成 [^\s>], 是 为了让编辑器语法高亮不把选择器里的 > 组合器误判成属性 内容. 运行时 入口是 ZData.ss(s, v, args), 按 v 是否传值分三种: ZData.ss('pos') 返回缩写映射 position ZData.ss('p1', '') 空属性原子类 (样式=值) ZData.ss('hover','p2') 属性=值 (伪类/媒体/分组) 空属性分支. HTML 里写 <div p1>, 属性 p1 的值是空串 "": 1. 把 p1 用大正则 replace 匹配; 2. 命中 rule16, 从 targets 取模板 ${PM[pm]}...; 3. 收集命名捕获组的值, 和 env 合并成 vars; 4. eval("with(vars)`"+target+"`") 算出 padding: 0.25rem; 5. 构造选择器 [p1=""], 组装成 { "[p1=\"\"]": { padding: "0.25rem" } }; 6. 带 media 前缀再包一层 @media, 交给 ZDataStyle.add() 注入. 属性=值分支. <div hover=p2> 时 v="p2" 非空, 先用 media 正则解析修饰符, 拿到 after=":hover" 后递归调 ZData.ss("p2","",{after, attr:'[hover="p2"]'}), 复用空属性 分支把 p2 翻译成 padding:0.5rem, 最后拼出 [hover="p2"]:hover. 所以两种语法是同一根管线: 属性=值负责解析上下文修饰, 空属性负责翻译原子本体. 选择器即属性 整套实现最妙的地方: 原子类不用 class, 用空属性选择器. [p1=""] { padding: 0.25rem } [hover="p2"]:hover { padding: 0.5rem } HTML 里写 p1 就是 p1="" 这个属性, 选择器 [p1=""] 精确命中. 好处: - 属性本身是携带信息的容器, 值能承载状态; - 空属性写法简洁, 和 class 无关, 能和其他框架并存; - 同一属性名靠"属性=值"组合出无穷选择器, 互不冲突. 单位换算 NUMS 数值语义遵循 Tailwind 的 4px 基础刻度: NUMS = (num, unit) => /^--/.test(num) ? `var(${num})` : `${unit ? num : num / 4}${unit || "rem"}` p2 padding: 0.5rem (2 x 0.25rem, 即 8px) mt3px margin-top: 3px (带单位原样) w100% width: 100% 用整数刻度就能得到成比例的尺寸, 不用每次手算. 组合规则 x / y px、py 这种"左右/上下一起"的写法, 靠组合规则实现, 模板 开头用 > 标记: (pm [pm]) x (any) => {pm}l{any} {pm}r{any} # px (pm [pm]) y (any) => {pm}t{any} {pm}b{any} # py px4% 翻译成 "> pl4% pr4%". 运行时看到开头是 > 就拆成 多个标记递归翻译: if (css[0] == '>') css = ZData.ss(css.replace(/^> */, ''), v, { ret: [] }); // ret.join(';') => "padding-left:4%;padding-right:4%" 最后合成一条带两条声明的规则, 用一个属性表达多个方向. bradt、bradb 等也同理. CSS 变量支持 数字处写 --名字, 就翻译成 var(--名字): m--varname margin: var(--varname) py--sm padding: var(--sm) (上下各一条) c--color color: var(--color) 来自 NUMS 里对 /^--/ 的判断, 原子样式能直接对接设计系统 的 CSS 变量. 媒体查询 用 前缀: 表达, 例如 s:p0: MINW = { sm:640, s:640, md:768, m:768, lg:1024, l:1024, xl:1280, '2xl':1536, xxl:1536 } // s -> @media (min-width: 640px) 前缀在正则里是可选捕获组, 运行时命中 media 组就按 MINW 表 映射成 @media (min-width: ...), 把整块 CSS 包进去. 状态伪类与组合器 状态用前/后修饰符表达, 解析逻辑在 media 正则里: state = ((?<before>(?!not)\w+)(?<to>[-+~\^]))?(?<not>not-)? (?<after>hover|focus(-\w+)?|active|after|before| (first|last)(-\w+)*) 后修饰 hover=p2 -> [hover="p2"]:hover (元素自身伪类) 前修饰+组合 用 before 标记祖先/兄弟, to 决定组合符: a-hover=p3 [a=""]:hover [hover="p3"] 后代 (- -> 空格) a^hover=p4 [a=""]:hover > [hover="p4"] 子代 (^ -> >) a+hover=p5 [a=""]:hover + [hover="p5"] 相邻兄弟 a~hover=p6 [a=""]:hover ~ [hover="p6"] 通用兄弟 取反 not-hover=m0 -> :not(:hover) 这样就能描述"悬浮在某个祖先上时, 改变某个后代/兄弟的样式", 覆盖大多数交互态. 分组规则 group bd、bg、flex、grid 这类聚合属性, 用 属性=值 一次传多条 声明: <div bd="style: bold; width: 1px; color: red"></div> 运行时检测到 group: group = { [`[${s}="${v}"]`]: { [ZData.ss(s)]: css2json(v) } }; 用内置 css2json 把 "style:bold; width:1px; color:red" 解析成 嵌套 JSON {style:'bold', width:'1px', color:'red'}, 再序列化 成: [bd="style: bold; width: 1px; color: red"] { border-style:bold; border-width:1px; border-color:red } 这就是 z-css.js 里 qs2json / json2css 存在的意义: CSS 文本 和 JSON 双向转换. 序列化与注入 所有原子规则汇入 ZDataStyle, 通过 add(css) 注入: add(css) { function update(me) { let styles = json2css(me.csss, 1); if (styles != me.old) { me.old = styles; // 找到或创建 <style id="z-data-ss">, 写入整段 CSS } } add2(css, this.csss); // 深合并进 this.csss (this.updateLater || (this.updateLater = ZData.deb(update)))(this); } 几个要点: - 单一 <style> 标签, 避免海量 <style> 节点; - 防抖刷新, 高频合并延迟为一次写 DOM; - 按 [s, v, after, before, attr, media] 缓存, 重复不再重算; - 全程 json2css / css2json 作数据交换, 既能合并又能序列化, 还天然支持嵌套 (如 @media). 与 Tailwind 的对比 Tailwind z-data 原子化引擎 构建 需要 PostCSS/CLI 生成 无需构建, 运行时即时翻译 样式来源 编译期产出 .css 文件 运行时注入单个 <style> 规则扩展 改配置/插件 改 DSL, 编译器自动生成 状态伪类 hover: focus: 前缀 属性=值语法, 天然带上下文 体积 按需裁剪(Purge) 极小运行时 + 规则表 z-data 更"内生": 规则做成数据, 编译器把数据翻译成可执行 代码, 运行时只负责"匹配 + 求值 + 注入". 这是元编程思路, 规则本身能动态增删, 不用改一行引擎代码. 代价也明显: 用了 eval("with(vars)`...`") 动态求值; 大量 正则挤在一个大 pattern 上, 可读性和调试成本高. 追求极致 体积、无需构建的场景很合适; 大型工程要类型安全和静态 分析, Tailwind 的构建期方案更有优势. 小结 一句话: 把规则写成数据, 编译器编译成正则 + 模板, 运行时用 "属性即选择器"即时翻译并注入. 三个最值得看的设计: 1. 规则即数据: 一条 BNF 规则 + 一张缩写表, 盖住一大类 写法, 扩展成本极低. 2. 属性即选择器: [p1=""] 让原子样式和 class 解耦, 天然 携带状态上下文. 3. 一条管线复用两种语法: 空属性翻译本体, 属性=值附加 伪类/媒体/分组, 共享同一套正则和模板. 原子化 CSS 不一定是"构建期生成一堆类", 也可以是一套运行时 的微型规则引擎. 在零依赖、零构建、轻量嵌入的场景, 这是 另一种优雅的答案.