-
-
Save yuanhongboo/89aec95708fa064e43120fac6b703289 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export class { | |
/** | |
* 获取给定节点命中的 css 规则 | |
* --- | |
* - 算法思路: | |
* - 获取沙箱环境中的全部样式规则 | |
* - 过滤出匹配当前目标元素的样式规则 | |
* - 计算样式规则权重,需要考虑的因子有: | |
* - 样式规则出现的先后顺序(难点:如何正确计算出样式规则出现的顺序?) | |
* - CSS 选择器权重计算规则:id > class/属性 > 标签(PS:https://gist.github.com/ssafejava/6605832) | |
* | |
* - 性能优化思路: | |
* - matches() 和 matchMedia() 是比较吃性能的,因此可以考虑通过缓存匹配结果来减少调用该方法所造成的计算成本 | |
* | |
* - 边界 case 处理: | |
* - 对于 var、calc、clamp、min、max 等动态值,需要将其转换为常量值,否则上层无法正常理解 | |
* | |
* @param {string} materialNodeIdentifier | |
* @param {SupportedCSSPropertyName} cssPropertyName | |
* @return {*} {(MatchedCSSRule[] | null)} | |
* @memberof Simulator | |
* | |
*/ | |
public getMatchedCSSRules( | |
materialNodeIdentifier: string, | |
cssPropertyName: SupportedCSSPropertyName, | |
): MatchedCSSRule[] | null { | |
const element = this.sandbox.querySelector( | |
`[data-identifier="${materialNodeIdentifier}"]`, | |
) as HTMLElement | null; | |
if (!element) { | |
return null; | |
} | |
const className = element.getAttribute('class') || ''; | |
const KEY = `${materialNodeIdentifier}_$$_${className}`; | |
// 判断是否命中缓存 | |
if (!this.matchesCSSRulesMap.has(KEY)) { | |
// 获取当前沙箱中包含的全部 styleSheet(PS:这其中包含外部样式表和内部样式表) | |
const styleSheets = Array.from(this.sandbox.getStyleSheets()); | |
// HACK 后加载的样式优先级默认高于先加载的 | |
// styleSheets.reverse(); | |
const matchedRules: MatchedCSSRule[] = []; | |
for (const sheet of styleSheets) { | |
if ( | |
(sheet.ownerNode as Element)?.id === RENDER_STYLE_ID || | |
['edit', 'x-ray'].includes( | |
(sheet.ownerNode as Element)?.getAttribute('data-view-mode') || '', | |
) | |
) { | |
// HACK 跳过模拟器内置样式 | |
continue; | |
} | |
// 获取 css rules | |
let rules: MatchedCSSRule[] = []; | |
const mediaText = sheet.media.mediaText; | |
if (sheet.disabled || !this.sandbox.matchMedia(mediaText)) { | |
rules = []; | |
} else { | |
rules = Array.from(sheet.cssRules).map((r) => ({ | |
selectorText: (r as CSSStyleRule).selectorText, | |
style: (r as CSSStyleRule).style, | |
score: 0, | |
})); | |
} | |
for (const rule of rules) { | |
// FIXME 性能瓶颈 - matches | |
if (element.matches(rule.selectorText)) { | |
matchedRules.push({ | |
...rule, | |
score: getBestMatchCSSSelector(element, rule.selectorText).score, | |
}); | |
} | |
} | |
} | |
// === 规则排序(根据选择器权重排序) === | |
matchedRules.sort((a, b) => b.score - a.score); | |
// 缓存 | |
this.matchesCSSRulesMap.set(KEY, matchedRules); | |
} | |
const matchedRules = this.matchesCSSRulesMap.get(KEY) as MatchedCSSRule[]; | |
// 过滤出包含给定 css 属性的规则 | |
const result = matchedRules | |
.filter((r) => r.style.getPropertyValue(cssPropertyName)) | |
.map((r) => { | |
const cssPropertyVal = r.style.getPropertyValue(cssPropertyName); | |
// @see https://web.dev/min-max-clamp/ | |
if ( | |
cssPropertyVal.startsWith('calc') || | |
cssPropertyVal.startsWith('min') || | |
cssPropertyVal.startsWith('max') || | |
cssPropertyVal.startsWith('clamp') || | |
cssPropertyVal.startsWith('var') | |
) { | |
// HACK 需要计算出 calc(xxx)、var(xxx) 等写法对应的具体数值 | |
const computedStyles = this.getComputedStyles(materialNodeIdentifier); | |
r.style.setProperty(cssPropertyName, get(computedStyles, cssPropertyName)); | |
} | |
return r; | |
}); | |
return result; | |
} | |
} | |
/** 通过计算选择器权重找出最匹配的选择器 */ | |
export function getBestMatchCSSSelector( | |
element: HTMLElement, | |
selectorText: string, | |
): { score: number; selector: string | null } { | |
const result: { score: number; selector: string | null } = { | |
score: 0, | |
selector: null, | |
}; | |
const selectors = selectorText.split(','); | |
const REGEX_PSEUDO_ELEMENT = /\:\:?(after|before|first-letter|first-line|selection)/g; | |
const REGEX_PSEUDO_CLAZZ = /\:(?!not)[\w-]+(\(.*\))?/g; | |
const REGEX_ATTRIBUTE = /\[[^\]]+\]/g; | |
const REGEX_ID = /#[\w-]+/g; | |
const REGEX_CLAZZ = /\.[\w-]+/g; | |
const REGEX_TAG_NAME = /[\w-]+/g; | |
for (const selector of selectors) { | |
// 过滤出匹配的选择器 | |
if (element.matches(selector)) { | |
// 计算选择器分数 | |
const score = [0, 0, 0]; // 分别对应权重 100, 10, 1 | |
const parts = selector.split(' '); | |
for (const part of parts) { | |
if (!isString(part)) { | |
break; | |
} | |
// 处理元素伪类选择器 | |
const matchedPseudoElement = part.match(REGEX_PSEUDO_ELEMENT); | |
if (matchedPseudoElement) { | |
score[2] = matchedPseudoElement.length; | |
part.replace(REGEX_PSEUDO_ELEMENT, ''); | |
} | |
// 处理 class 伪类选择器 | |
const matchedPseudoClazz = part.match(REGEX_PSEUDO_CLAZZ); | |
if (matchedPseudoClazz) { | |
score[1] = matchedPseudoClazz.length; | |
part.replace(REGEX_PSEUDO_CLAZZ, ''); | |
} | |
// 处理属性选择器 | |
const matchedAttribute = part.match(REGEX_ATTRIBUTE); | |
if (matchedAttribute) { | |
score[1] += matchedAttribute.length; | |
part.replace(REGEX_ATTRIBUTE, ''); | |
} | |
// 处理 ID 选择器 | |
const matchedID = part.match(REGEX_ID); | |
if (matchedID) { | |
score[0] += matchedID.length; | |
part.replace(REGEX_ID, ''); | |
} | |
// 处理 class 选择器 | |
const matchedClazz = part.match(REGEX_CLAZZ); | |
if (matchedClazz) { | |
score[1] += matchedClazz.length; | |
part.replace(REGEX_CLAZZ, ''); | |
} | |
// 处理标签选择器 | |
const matchedTagName = part.match(REGEX_TAG_NAME); | |
if (matchedTagName) { | |
score[2] += matchedTagName.length; | |
part.replace(REGEX_TAG_NAME, ''); | |
} | |
const temp = parseInt(score.join(''), 10); | |
if (temp > result.score) { | |
result.score = temp; | |
result.selector = selector; | |
} | |
} | |
} | |
} | |
return result; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment