Lynx CSS 伪类与伪元素使用指南:支持清单、实战用法与渲染器源码实现
【免费下载链接】lynxEmpower the Web community and invite more to build across platforms.项目地址: https://gitcode.com/GitHub_Trending/lynx10/lynx
本文围绕 Lynx 的 CSS 伪类(pseudo-classes)与伪元素(pseudo-elements)支持能力展开:先给出完整的支持/不支持清单与可直接复制的实战 CSS 写法,再结合 Lynx 渲染器源码(CSS 选择器解析、匹配器、DOM 伪元素节点)解释这些伪类在引擎内部是如何被解析、匹配和落到组件属性上的。读完本文,你不仅能正确编写 Lynx 中可用的状态样式与占位符/选中文本样式,还能理解哪些伪类"能解析但不生效"的根本原因,避免踩坑。
以下内容继承自官方 API 文档 pseudo-classes.md,并结合 selector_matcher.cc、pseudo_element.cc 等源码进行了扩充。
一、能力总览:哪些能用、哪些只是"能解析"
Lynx 的 CSS 引擎对伪类/伪元素采取"解析面宽、匹配面窄"的策略。CSS 解析器(selector parser)会识别几乎所有常见伪类并为其保留枚举定义,但真正的选择器匹配器只实现了其中一小部分。从源码结构看,这种差异集中体现在两处:
- lynx_css_selector.h 中定义了完整的
PseudoType枚举,包含kPseudoNthChild、kPseudoIs、kPseudoWhere、kPseudoHas、kPseudoDisabled、kPseudoChecked、kPseudoBefore、kPseudoAfter等大量类型——这说明解析层"认识"它们; - 但 selector_matcher.cc 的
MatchPseudoClass/MatchPseudoElement只处理极少数类型,未处理的一律返回false,样式不会生效。
受支持的伪类 / 伪元素:
| 伪类 / 伪元素 | 类型 | 说明 |
|---|---|---|
:active | 状态伪类 | 元素被激活(按下)时匹配 |
:focus | 状态伪类 | 元素获得焦点时匹配 |
:hover | 状态伪类 | 指针悬停在元素上时匹配(平台支持程度有差异) |
:not() | 否定伪类 | 排除匹配指定选择器的元素 |
::placeholder | 伪元素 | 样式化 input 元素的占位符文本 |
::selection | 伪元素 | 样式化选中文本 |
不受支持(❌ 写了也不生效):
- ❌
:first-child/:last-child/:nth-child()— 结构伪类 - ❌
:nth-of-type()/:only-child/:empty— 结构伪类 - ❌
:is()— 较新的选择器语法 - ❌
:where()— 较新的选择器语法 - ❌
:has()— 关系伪类 - ❌
:target— 目标伪类 - ❌
:disabled/:enabled— 表单状态伪类(能解析、不匹配) - ❌
:checked— 选中状态伪类(能解析、不匹配) - ❌
:valid、:invalid— 表单验证伪类 - ❌
:required、:optional - ❌
::before/::after— 伪元素 - ❌
::first-line— 首行伪元素 - ❌
::first-letter— 首字母伪元素
一个可以佐证的细节:PseudoType枚举中根本没有kPseudoTarget、kPseudoValid等定义,这些在解析阶段就会被归为未知类型;而:disabled、:enabled、:checked虽然存在于枚举中(可解析),却不会进入匹配器的分支——这正是文档所说"parsed but not matched"的源码出处。
二、状态伪类::active、:focus、:hover
:active — 按下/激活态
匹配元素正在被激活(通常是手指按下)时的状态,常用于按钮的按压反馈:
.button:active { opacity: 0.8; transform: scale(0.98); }:focus — 焦点态
匹配获得焦点的元素,常用于输入框聚焦时的描边高亮:
input:focus { border-color: #ff351a; }:hover — 悬停态
匹配指针悬停在元素上方时的状态。注意文档中明确提示:平台支持程度有差异,移动设备无物理指针时该状态可能不会触发,写跨端样式时不要依赖它做唯一反馈。
.button:hover { background-color: #e63016; }源码印证:状态由 DOM 节点的 PseudoState 驱动
三个状态伪类的匹配逻辑在 selector_matcher.cc 中实现:
bool SelectorMatcher::MatchPseudoClass(const SelectorMatchingContext& context) const { auto& element = *context.holder; const LynxCSSSelector& selector = *context.selector; switch (selector.GetPseudoType()) { case LynxCSSSelector::kPseudoNot: return MatchPseudoNot(context); case LynxCSSSelector::kPseudoHover: return element.HasPseudoState(tasm::kPseudoStateHover); case LynxCSSSelector::kPseudoActive: return element.HasPseudoState(tasm::kPseudoStateActive); case LynxCSSSelector::kPseudoFocus: return element.HasPseudoState(tasm::kPseudoStateFocus); case LynxCSSSelector::kPseudoRoot: return element.tag().str() == "page"; ... } return false; }从源码结构看,:hover/:active/:focus并不是靠浏览器事件流实时重算的,而是匹配时读取 DOM 节点上的kPseudoStateHover/kPseudoStateActive/kPseudoStateFocus状态位(element.HasPseudoState(...)),这些状态由平台侧的输入事件在按下/抬起/聚焦时更新。另外同一函数中还暴露了一个文档未列出的实现事实::root被实现为"标签是page的节点",即 Lynx 页面根元素——从源码看该能力存在,但由于官方支持清单未收录,使用时建议以文档口径为准。
三、:disabled / :enabled 为什么"能解析但不生效"?
这是 Lynx 与标准 Web 平台差异最大的一处。文档中的说明是:
虽然 CSS 解析器认识
:disabled和:enabled,但 Lynx 的 DOM 层没有实现这两个伪类的匹配,因此实际不生效。正确做法是通过增删 class 来表达禁用态样式。
对应源码依据很直接:PseudoType枚举里有kPseudoDisabled、kPseudoEnabled(见 lynx_css_selector.h),且 lynx_css_selector_impl.cc 的UpdatePseudoType会把它们合法地归类为伪类——所以解析、样式表构建阶段都不会报错;但MatchPseudoClass的 switch 分支中没有这两个 case,直接落到return false,匹配永远失败。
推荐写法(直接继承自官方文档):
/* Recommended */ .button.disabled { opacity: 0.5; background-color: #ccc; }在 JS 侧通过classList增删disabled类名即可得到确定生效的禁用态样式。
四、:not() 否定伪类
:not()用于排除匹配某个指定选择器的元素,在 Lynx 中是完整受支持的:
/* Exclude elements with a specific class */ .item:not(.exclude) { color: black; }匹配器路径(Fiber 架构)
选择器匹配器对:not()实现了标准的"存在即失败"语义,见 selector_matcher.cc:
bool SelectorMatcher::MatchPseudoNot(const SelectorMatchingContext& context) const { const LynxCSSSelector& selector = *context.selector; DCHECK(selector.SelectorList()); SelectorMatchingContext sub_context(context); for (sub_context.selector = selector.SelectorList()->First(); sub_context.selector; sub_context.selector = LynxCSSSelectorList::Next(*context.selector)) { if (MatchSelector(sub_context) == kMatches) { return false; } } return true; }即:只要:not()内部的任意一个子选择器匹配上了当前元素,整个:not()就返回false(不应用样式)。
样式解析路径(传统 Radon 架构)
在非 Fiber 架构下,:not()走了另一套"全局伪类样式表"机制,实现在 style_resolver.cc 的PreSetGlobalPseudoNotCSS与ApplyPseudoNotCSSStyle中。从源码注释与逻辑可以看出它做了不少工程化优化:
- 按
tag/class/id三种作用域(scope)分别判断是否应用:not()样式; - 元素没有任何 class 且
:not()作用域是 class 选择器时,直接命中; - 明确处理了
.class1:not(.class2)这类"复合类 + 否定"的场景(见源码注释Handle the case of .class1:not(.class2)); - 全局(无元素前缀)与局部
:not()规则分别通过pseudo_not_global_map与pseudo_map预先组织好,命中时直接MergeToken合并进计算样式。
特异性(specificity)计算
:not()的特异性规则在 lynx_css_selector_impl.cc 的CalcSpecificityForSimple中体现::not()/:is()/:has()取内部选择器列表的特异性,:where()取 0,其余伪类按 class 级(0x000100)计。这与 CSS 标准中:not()继承参数特异性的口径一致,因此写.item:not(.exclude)时,其特异性等价于.item.exclude的组合权重,可以据此预判与其他规则的层叠胜负。
五、伪元素:::placeholder 与 ::selection
注意(官方文档口径):
::before和::after不受支持。CSS 解析器能识别它们,但选择器匹配器与渲染引擎都没有实现,使用它们不产生任何效果。源码层面同样印证了这一点:MatchPseudoElement只处理kPseudoPlaceholder与kPseudoSelection两个分支(selector_matcher.cc),kPseudoBefore/kPseudoAfter落入default返回false。
::placeholder — 输入框占位符
样式化 input 元素的占位符文本:
input::placeholder { color: #999; font-size: 14px; }::selection — 选中文本
样式化用户选中的文本:
::selection { background-color: #1890ff; color: #fff; }源码印证:伪元素样式如何落到组件属性上
Fiber 架构中,伪元素由PseudoElement节点承载(pseudo_element.h / pseudo_element.cc),它在构造时复制宿主元素当前的计算样式作为基底,再通过属性映射表把命中 CSS 的属性翻译为组件属性下发。映射关系定义在 pseudo_element.cc:
// ::selection 支持映射到组件属性的样式 { {kPropertyIDBackgroundColor, "selection-background-color"}, {kPropertyIDXHandleColor, "selection-handle-color"}, {kPropertyIDXHandleSize, "selection-handle-size"}, } // ::placeholder 支持映射到组件属性的样式 { {kPropertyIDColor, "placeholder-color"}, {kPropertyIDFontSize, "placeholder-font-size"}, {kPropertyIDFontFamily, "placeholder-font-family"}, {kPropertyIDFontWeight, "placeholder-font-weight"}, {kPropertyIDFontStyle, "placeholder-font-style"}, }这段代码给出了一个文档之外的实用信息:::placeholder实际生效的属性集合是color、font-size、font-family、font-weight、font-style这五个,它们会被翻译为placeholder-color、placeholder-font-size等组件属性(通过UpdateAttrMap/PropBundle::SetProps下发);::selection则映射background-color、selection-handle-color、selection-handle-size。映射表之外的属性会被PushPseudoStylesToBundle直接跳过(style_name == map.end()时continue)。此外PseudoElement::UpdatePropertyFromStyleMap中对font-size单独走了CSSStyleUtils::ResolveFontSize,说明em/rem等相对单位会在伪元素样式应用时被按视口重新解析。相关行为有 pseudo_element_unittest.cc 覆盖,例如Constructor用例验证了PseudoElement构造后状态与宿主元素绑定正确、style_map_初始为空。
六、综合示例
按钮状态(继承自官方文档)
.button { background-color: #ff351a; color: #fff; padding: 12px 24px; } /* Pressed state */ .button:active { background-color: #e63016; transform: scale(0.98); } /* Disabled state implemented with a class */ .button.disabled { background-color: #ccc; opacity: 0.6; }要点回顾:按压反馈用:active;禁用态不要用:disabled,而要用disabledclass 驱动。
表单输入框(继承自官方文档)
.input { border: 1px solid #ddd; padding: 8px 12px; } /* Focused state */ .input:focus { border-color: #ff351a; outline: none; } /* Placeholder styles */ .input::placeholder { color: #999; }这组示例组合了本文覆盖的三个支持点::focus焦点态、基础样式、::placeholder占位符颜色。注意::placeholder中如果写font-size,单位会按视口解析(em/rem支持),而像background-color这类不在映射表内的属性不会生效。
七、迁移与排查建议
- 从 Web 迁移过来时,凡是依赖
:first-child、:nth-child()、:has()、::before/::after的样式需要重写:结构选择可用显式 class 替代,装饰性伪元素可用view子节点或background-image方案替代; - "CSS 写进去没反应" 的排查顺序:先确认该伪类是否在第一节的受支持清单内;若在,检查平台是否触发了对应状态(如
:hover在无指针设备的限制);若是::placeholder/::selection,检查所用属性是否在前文映射表内; - 以文档清单为契约:本文引用的匹配器源码(Fiber 架构的
selector_matcher.cc与 Radon 架构的style_resolver.cc)说明不同渲染架构内部实现路径不同,但对外能力以 pseudo-classes.md 的支持清单为准。
关键源码与文档索引
| 内容 | 路径 |
|---|---|
| 伪类/伪元素支持文档 | ai/skills/lynx-api-docs/skills/using-lynx-api-docs/css/pseudo-classes.md |
| 伪类型枚举与特异性常量 | core/renderer/css/ng/selector/lynx_css_selector.h |
| 伪类型归类与特异性计算 | core/renderer/css/ng/selector/lynx_css_selector_impl.cc |
| 状态伪类 / :not / 伪元素匹配 | core/renderer/css/ng/matcher/selector_matcher.cc |
| :not() 样式表机制(Radon 路径) | core/renderer/dom/style_resolver.cc |
| 伪元素节点与属性映射(Fiber 路径) | core/renderer/dom/fiber/pseudo_element.cc |
| 伪元素单测 | core/renderer/dom/fiber/pseudo_element_unittest.cc |
【免费下载链接】lynxEmpower the Web community and invite more to build across platforms.项目地址: https://gitcode.com/GitHub_Trending/lynx10/lynx
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考