KEEL · 龙骨 · A CURRICULUM FOR THE AI ERA
组件与样式 — keel 龙骨
组件与样式 的参考信息:组件与样式
课程导读 · 每一条结论都要能在真实浏览器里量出同一个 computed 值
这门课解决的是同一类问题:按钮的颜色改不动、Tailwind 的类名加上去了页面没变、w-40 写上去了输入框还是撑满一行、暗色切了半页白了半页、服务端渲染出来的首屏先闪一下没样式。它们的共同点是——页面上同时住着两套样式,而它们不是按同一种方式、在同一个时刻、进入同一套优先级规则的。
你现在的起点
- 知道 Tailwind 是"原子类",也知道 antd 是"组件库",但说不清两者的样式分别在什么时候变成 CSS;
- 遇到过"我的 class 加了但没生效",通常的处理是加
!important或者!前缀,然后就不管了; - 见过 antd 类名里的
css-xxxxx这种 hash,也见过css-dev-only-do-not-override-,但没想过它能当判据用; - 知道 Tailwind v4 不再需要
tailwind.config.js,但不知道配置搬去了哪儿、@source到底扫什么; - 相信"
StyleProvider layer是解决样式冲突的开关",但没验证过它到底把什么放进了层。
如果你符合上面这几条,这门课就是为你写的。前提是你会写 React 并且跑得起来 npm——这门课不教组件怎么写,它教的是"你写下的类名和组件,最后变成了文档里的哪几条 CSS 规则"。
一条能走通的学习路径
先看清两套生成模型(01 一个按钮上的两条时间线)
→ antd:组件渲染时生成 → prepend 进 head → 18 个 <style>
→ Tailwind:构建时扫描源码 → 产出四层 → 一份 <link>
→ 现场:类名写对了、权重也一样,颜色就是不变
再拆构建期(02 一次扫描决定产出什么)
→ @source 的边界:目录树外扫不到,页面"像有样式"但组件全是裸的
→ 动态拼类名:写下来过的生成,拼出来的不生成
→ @theme / @utility / @apply 的产出规则
再拆运行期(03 运行时生成 + 变量表)
→ 381 个 --ant-* 变量;改一个 token = 多生成一整张表
→ :where() 把权重压到 0,1,0 —— 和 .bg-red-600 一模一样
→ 颜色真正存在变量里,抢 background-color 是在跟影子打架
核心那一章(04 层序:谁赢是有先后规则的)
→ 未分层 > 每一层;层之间后声明的 > 先声明的
→ 三页对照:不排层 / antd 先声明 / 显式排层
→ 排错一格的代价:Tailwind 全赢,但 antd 的按钮被打成透明
边界(05 组件边界上的四种传参)
→ className / classNames / styles / token 各自什么时候生效
→ 别写死 hash:开发态与生产态的类名不一样
状态(06 暗色与响应式)
→ dark: 变体的默认开关是系统偏好,改成 class 要用 @custom-variant
→ class="dark" 驱动不了 antd —— 两套开关必须由同一个状态源同时喂
交付侧(07 体积、首屏与 SSR)
→ 渲染时注入 vs 抽出成静态文件
→ renderToString 的 HTML 里一个 <style> 都没有
你会拿到什么
| 章节 | 一句话 | 关键动作 |
|---|---|---|
| 01 两套样式是两套生成模型 | 类名写对了、权重也一样,颜色就是不变 | 同一页里量出 31 个注入 <style> 与一份 <link>,逐条列出 antd 注入的 18 个样式表 |
| 02 Tailwind v4:一次扫描决定产出什么 | 写下来过就生成,拼出来的不生成 | 13 组编译对照:@source 边界、@theme 命名空间、@utility 按需、@apply 内联 |
| 03 antd:运行时生成 + 变量表 | 改 token 是多生成一张 381 项的表 | 五块面板真跑:token / 暗色算法 / 提权重 / styles / classNames |
| 04 层序:谁赢是有先后规则的 | 未分层比任何层都大 | 四页对照 + 一个能逆推"哪条规则赢了"的探针 |
| 05 组件边界上的四种传参 | styles 落成内联、classNames 落成类名 |
四种传参的落点与 computed 对照 + 类名 hash 的环境差异 |
| 06 暗色与响应式:一个开关还是两个 | class="dark" 管不到 antd |
同一页里切 class × 切视口,四个状态各量一遍 |
| 07 交付面:体积、首屏与 SSR | renderToString 的 HTML 里没有 <style> |
五种 import 组合的体积 + extractStyle 四组对照 |
每章结构统一:现场 → 形态 → 原因 → 本章脉络 → 生产边界 → 动手:可观察结果 → 故障注入 → 自测题 → 现在能解释什么。
三个必须先纠偏的认知
StyleProvider layer不是"把 antd 关进笼子"的开关。 实测:它只把组件样式放进@layer antd;--ant-*的变量表与.anticon基础样式一共 13 条规则永远留在未分层。而未分层规则的优先级高于任何层——所以只加layer而不排层序,第一次量出来的computed可能一个像素都没变。antd 输给 Tailwind,不是因为它权重低。
:where(.css-oc1rc0).ant-btn的权重是 0,1,0,和.bg-red-600完全一样。:where()的作用正是把权重归零,让 antd 接受"被更高层的东西覆盖"。所以你遇到的覆盖失败,通常和"选择器写得不够狠"无关,而是层和顺序的事。class="dark"只驱动 Tailwind 的dark:变体。 antd 的暗色完全由 React 状态(ConfigProvider theme.algorithm)决定,两者之间没有任何自动同步。实测:给<html>加class="dark"之后,页面肤色从rgb(255,255,255)变成oklch(0.208 0.042 265.755),而两个 antd 按钮的background-color纹丝不动。
学完这门课你能做什么
- 拿到"我的样式没生效",能先问三个问题:它是构建期生成的还是运行时生成的?它进没进层?谁在它后面? —— 而不是下意识加
!important; - 看到一个 antd 组件的
computed值和你写的不一样,能用一段遍历document.styleSheets的探针把候选规则连层名、权重、出现序号一起列出来,指出到底是哪一条赢了; - 知道改 antd 外观的优先顺序:先改 token(走正道),要局部覆盖就用
styles插槽(落成内联),classNames传 Tailwind 类名是最后才会考虑的(会受层序影响); - 遇到"页面像有样式、但组件全是裸的",能立刻怀疑
@source没扫到源码,并用产物体积和两个类的出现次数证实; - 收到"首屏闪了一下"的反馈,能说清是渲染时注入那一瞬间的空档,还是 SSR 漏了
extractStyle。
前置要求
- 会写 React,能跑起一个
npm install+ 打包的开发环境(这门课全程用真浏览器量computed,不靠推断); - 建议先读同板块的 模块与构建——那门课讲"一行
import落到磁盘上哪个文件",这门课接手它留下的产物:模块图折完之后,样式是怎么进文档的; - 见过 CSS 的层叠与权重、知道
!important是什么就够了,不需要会写 CSS 预处理器。
替身边界
实验以 antd 6.6.5 + tailwindcss 4.3.3 + React 19.3.0 为准,浏览器用 Playwright 自带的 HeadlessChrome 151。
这个组合里有两件事是"刚刚才变成这样"的,一定要自己复核:
- Tailwind v4 把配置搬进了 CSS(
@theme/@utility/@source/@custom-variant),tailwind.config.js那套写法在 v4 里不是同一条路; - antd 6 的类名形状(
ant-btn-color-primary/ant-btn-variant-solid)与 5.x 不同,且开发态类名带css-dev-only-do-not-override-前缀、生产态不带。
结论的形状("运行时生成 vs 构建期生成"、"未分层 > 分层"、"改 token 而不是抢选择器"、"class 开关管不到 antd")跨版本成立;具体字节数、hash、报错文案请在你自己的版本上复核。
实验全部是本机小页面、无网络。凡涉及字节数的地方,请只看比值和形状,不要抄绝对值。