CSS 解析器
CSS 样式表是一系列的 CSS 规则集合,而 CSS 解析器的作用就是将 CSS 文本解析为 CSS 规则集合。
div, p { font-size: 88px; color: #000; }
例如上面的 CSS 文本,经过解析器解析后,会生成下面的 CSS 规则集合:
[ { "selectors": [ { "id": "", "class": "", "tagName": "div" }, { "id": "", "class": "", "tagName": "p" } ], "declarations": [ { "name": "font-size", "value": "88px" }, { "name": "color", "value": "#000" } ] } ]
每个规则都有一个 selector
和 declarations
属性,其中 selectors
表示 CSS 选择器,declarations
表示 CSS 的属性描述集合。
export interface Rule { selectors: Selector[] declarations: Declaration[] } export interface Selector { tagName: string id: string class: string } export interface Declaration { name: string value: string | number }
每一条 CSS 规则都可以包含多个选择器和多个 CSS 属性。
解析 CSS 规则 parseRule()
private parseRule() { const rule: Rule = { selectors: [], declarations: [], } rule.selectors = this.parseSelectors() rule.declarations = this.parseDeclarations() return rule }
在 parseRule()
里,它分别调用了 parseSelectors()
去解析 CSS 选择器,然后再对剩余的 CSS 文本执行 parseDeclarations()
去解析 CSS 属性。
解析选择器 parseSelector()
private parseSelector() { const selector: Selector = { id: '', class: '', tagName: '', } switch (this.rawText[this.index]) { case '.': this.index++ selector.class = this.parseIdentifier() break case '#': this.index++ selector.id = this.parseIdentifier() break case '*': this.index++ selector.tagName = '*' break default: selector.tagName = this.parseIdentifier() } return selector } private parseIdentifier() { let result = '' while (this.index < this.len && this.identifierRE.test(this.rawText[this.index])) { result += this.rawText[this.index++] } this.sliceText() return result }
选择器我们只支持标签名称、前缀为 #
的 ID 、前缀为任意数量的类名 .
或上述的某种组合。如果标签名称为 *
,则表示它是一个通用选择器,可以匹配任何标签。
标准的 CSS 解析器在遇到无法识别的部分时,会将它丢掉,然后继续解析其余部分。主要是为了兼容旧浏览器和防止发生错误导致程序中断。我们的 CSS 解析器为了实现简单,没有做这方面的做错误处理。
解析 CSS 属性 parseDeclaration()
private parseDeclaration() { const declaration: Declaration = { name: '', value: '' } this.removeSpaces() declaration.name = this.parseIdentifier() this.removeSpaces() while (this.index < this.len && this.rawText[this.index] !== ':') { this.index++ } this.index++ // clear : this.removeSpaces() declaration.value = this.parseValue() this.removeSpaces() return declaration }
parseDeclaration()
会将 color: red;
解析为一个对象 { name: "color", value: "red" }
。
小结
CSS 解析器相对来说简单多了,因为很多知识点在 HTML 解析器中已经讲到。整个 CSS 解析器的代码大概 100 多行,如果你阅读过 HTML 解析器的源码,相信看 CSS 解析器的源码会更轻松。
构建样式树
本阶段的目标是写一个样式构建器,输入 DOM 树和 CSS 规则集合,生成一棵样式树 Style tree。
样式树的每一个节点都包含了 CSS 属性值以及它对应的 DOM 节点引用:
interface AnyObject { [key: string]: any } export interface StyleNode { node: Node // DOM 节点 values: AnyObject // style 属性值 children: StyleNode[] // style 子节点 }
先来看一个简单的示例:
<div>test</div>
div { font-size: 88px; color: #000; }
上述的 HTML、CSS 文本在经过样式树构建器处理后生成的样式树如下:
{ "node": { // DOM 节点 "tagName": "div", "attributes": {}, "children": [ { "nodeValue": "test", "nodeType": 3 } ], "nodeType": 1 }, "values": { // CSS 属性值 "font-size": "88px", "color": "#000" }, "children": [ // style tree 子节点 { "node": { "nodeValue": "test", "nodeType": 3 }, "values": { // text 节点继承了父节点样式 "font-size": "88px", "color": "#000" }, "children": [] } ] }
遍历 DOM 树
现在我们需要遍历 DOM 树。对于 DOM 树中的每个节点,我们都要在样式树中查找是否有匹配的 CSS 规则。
export function getStyleTree(eles: Node | Node[], cssRules: Rule[], parent?: StyleNode) { if (Array.isArray(eles)) { return eles.map((ele) => getStyleNode(ele, cssRules, parent)) } return getStyleNode(eles, cssRules, parent) }
匹配选择器
匹配选择器实现起来非常容易,因为我们的CSS 解析器仅支持简单的选择器。 只需要查看元素本身即可判断选择器是否与元素匹配。
/** * css 选择器是否匹配元素 */ function isMatch(ele: Element, selectors: Selector[]) { return selectors.some((selector) => { // 通配符 if (selector.tagName === '*') return true if (selector.tagName === ele.tagName) return true if (ele.attributes.id === selector.id) return true if (ele.attributes.class) { const classes = ele.attributes.class.split(' ').filter(Boolean) const classes2 = selector.class.split(' ').filter(Boolean) for (const name of classes) { if (classes2.includes(name)) return true } } return false }) }
当查找到匹配的 DOM 节点后,再将 DOM 节点和它匹配的 CSS 属性组合在一起,生成样式树节点 styleNode:
function getStyleNode(ele: Node, cssRules: Rule[], parent?: StyleNode) { const styleNode: StyleNode = { node: ele, values: getStyleValues(ele, cssRules, parent), children: [], } if (ele.nodeType === NodeType.Element) { // 合并内联样式 if (ele.attributes.style) { styleNode.values = { ...styleNode.values, ...getInlineStyle(ele.attributes.style) } } styleNode.children = ele.children.map((e) => getStyleNode(e, cssRules, styleNode)) as unknown as StyleNode[] } return styleNode } function getStyleValues(ele: Node, cssRules: Rule[], parent?: StyleNode) { const inheritableAttrValue = getInheritableAttrValues(parent) // 文本节点继承父元素的可继承属性 if (ele.nodeType === NodeType.Text) return inheritableAttrValue return cssRules.reduce((result: AnyObject, rule) => { if (isMatch(ele as Element, rule.selectors)) { result = { ...result, ...cssValueArrToObject(rule.declarations) } } return result }, inheritableAttrValue) }
在 CSS 选择器中,不同的选择器优先级是不同的,比如 id 选择器就比类选择器的优先级要高。但是我们这里没有实现选择器优先级,为了实现简单,所有的选择器优先级是一样的。
继承属性
文本节点无法匹配选择器,那它的样式从哪来?答案就是继承,它可以继承父节点的样式。
在 CSS 中存在很多继承属性,即使子元素没有声明这些属性,也可以从父节点里继承。比如字体颜色、字体家族等属性,都是可以被继承的。为了实现简单,这里只支持继承父节点的 color
、font-size
属性。
// 子元素可继承的属性,这里只写了两个,实际上还有很多 const inheritableAttrs = ['color', 'font-size'] /** * 获取父元素可继承的属性值 */ function getInheritableAttrValues(parent?: StyleNode) { if (!parent) return {} const keys = Object.keys(parent.values) return keys.reduce((result: AnyObject, key) => { if (inheritableAttrs.includes(key)) { result[key] = parent.values[key] } return result }, {}) }
内联样式
在 CSS 中,内联样式的优先级是除了 !important
之外最高的。
<spanstyle="color: red; background: yellow;">
我们可以在调用 getStyleValues()
函数获得当前 DOM 节点的 CSS 属性值后,再去取当前节点的内联样式值。并对当前 DOM 节点的 CSS 样式值进行覆盖。
styleNode.values = { ...styleNode.values, ...getInlineStyle(ele.attributes.style) } function getInlineStyle(str: string) { str = str.trim() if (!str) return {} const arr = str.split(';') if (!arr.length) return {} return arr.reduce((result: AnyObject, item: string) => { const data = item.split(':') if (data.length === 2) { result[data[0].trim()] = data[1].trim() } return result }, {}) }