去年,我们向该网站介绍了一个预感,因为这是您在审核CSS时经常想做的事情之一。然后,不久之后,我们添加了在分析CSS之前对您的CSS进行漂亮的选项。因为我们现在在分析仪页面上有一个DevTools面板,所以以格式化的方式查看CSS使用情况很有意义。但这是有代价的:使CSS占据最高两倍的CSS分析CSS!是时候查看更具性能的替代方案了。
<img alt="Example output of formatted CSS that we need to audit" src="https://user-images.githubusercontent.com/1536852/244181349-9824fdf7-abfe-45f5-b2c4-859d87f7e8d1.png">
<figcaption>CSS that is being audited where the source code was originally minified, making the auditing process difficult, because those lines become pretty much unreadable.</figcaption>
天真的方式:更漂亮
我们的预期仪的第一次迭代使用了Prettier,这是一个非常受欢迎且受人尊敬的项目,可以相当印刷数百种不同的语言。甚至在测试之前,我已经很确定这将是一个缓慢的函数调用,因此我继续进行,并确保仅导入相关模块和将函数放在网上工作人员中以将其运行UI线程。
// prettify-worker.js
import prettier from 'prettier/esm/standalone.mjs'
import cssParser from 'prettier/esm/parser-postcss.mjs'
// The `event` here is the message we send from the UI to
// the worker and `event.data` contains the string of CSS.
onmessage = function (event) {
try {
let result = prettier.format(event.data, {
parser: 'css',
plugins: [cssParser]
})
// Send result back to UI thread
postMessage(result)
} catch (error) {
postMessage({ error })
}
}
这效果很好,但是几周后,我经常注意到分析仪页面上的进度被卡在“ Premttifying CSS” 步骤上。这不一定是一件坏事,但是如果您像我一样分析CSS,一段时间后就会很烦人。而且由于Premintifying CSS甚至不是我们的核心业务(如果可以的话),则更令人沮丧。
CSSTREE进行营救
在考虑了一段时间的问题之后,我意识到我可以邀请CSSTree的帮助来完成一些工作。 CSS分析仪基于CSstree的AST,因此我知道事物的工作原理,并且依赖项已经在页面上,因此无需下载更多的依赖项。 Prettier + Postcss的费用几乎要下载340kb,这并不大,但是如果我们可以减少该金额,那就太好了。
那么,如何将(潜在缩小)CSS的字符串变成带有CSstree的大多数可读CSS的字符串?让我们从分析CSS开始,因此我们可以与AST合作。然后,使用该AST,我们应用我们对CSS结构的知识将它们变成可读的字符串,逐行。
。快速CSS解析
csstree具有一些整洁的解析选择,可以加快速度。它使您可以跳过某些令牌,这将减少内存使用量以及所有这些。以下脚本创建了我们CSS的AST。可以在ASTExplorer上检查此类AST的一个例子。
let ast = parse(css, {
positions: true,
parseAtrulePrelude: false,
parseCustomProperty: false,
parseValue: false
})
我们可以跳过解析术语前列,自定义属性和值,因为我们只对其“原始”字符串值感兴趣,而不是其中的更深令牌。我们确实需要positions
,因为这将使我们以后可以做很多css.substring(x, y)
。
创建可读的CSS字符串
使用AST,我们可以开始考虑如何将其转变为漂亮的CSS。我们需要CSS足够漂亮,以在我们的Devtools中以可读性的方式展示它,而不是比这更奇特的。经过一番思考,我提出了以下规则:
- 每个 aTrule 从新线开始
- 每个规则从新线开始
- 每个选择器从新线开始
- 在每个选择器之后都放置一个逗号
- 每个 block (
{}
)的缩进比以前的凹痕级别 多1个选项卡
- 每个声明从新线开始
- 每个声明以半隆(
;
)结尾 - 在块之后放置一个空线,除非它是周围块中的最后一个
- 未知语法被渲染为IS
您可以从此列表中看到,我们在此处处理非常有限的CSS代币子集:Stylesheet,ontrule,ulule,selectorlist,selectorlist,selector,selector,block和声明。我们正在从样式表级别开始我们的格式:
function print(node, indent_level = 0, css) {
let buffer = ''
for (let child of node.children) {
if (child.type === 'Rule') {
buffer += print_rule(child, indent_level, css)
buffer += '\n'
} else if (child.type === 'Atrule') {
buffer += print_atrule(child, indent_level, css)
buffer += '\n'
} else {
buffer += print_unknown(child, indent_level, css)
}
buffer += '\n'
}
return buffer
}
我们对我们的启示,称为print_atrule
和print_rule
,看起来像这样:
function print_atrule(node, indent_level, css) {
let buffer = indent(indent_level)
buffer += '@' + node.name
if (node.prelude) {
buffer += ' ' + substr(node.prelude, css)
}
if (node.block && node.block.type === 'Block') {
buffer += print_block(node.block, indent_level, css)
} else {
// `@import url(style.css);` has no block, neither does `@layer layer1;`
buffer += ';'
}
return buffer
}
function print_rule(node, indent_level, css) {
let buffer = ''
if (node.prelude && node.prelude.type === 'SelectorList') {
buffer += print_selectorlist(node.prelude, indent_level, css)
}
if (node.block && node.block.type === 'Block') {
buffer += print_block(node.block, indent_level, css)
}
return buffer
}
这也对所有其他类型都进行了一些。这里甚至还有一些递归,因为可以嵌套嵌套(@media
和CSS嵌套中,仅举几例),因此我们需要确保也考虑这些。
如果您想查看更多信息:可以找到source code,可以找到on GitHub,我目前正在将其作为独立的NPM包。
权衡
没有项目是完美的,这个小脚本也不是。有些事情以一种简单的方式做得很好,但是除了该包装的范围和必要性之外,我会考虑一些事情。没关系,因为我们只需要它来格式化您的CSS足够好即可轻松审核。
- 超级快(>比漂亮的90%)
- ''微小捆绑尺寸(比漂亮的99%)
- 为我们的用例 ,足够漂亮
- ð¶如果您的源CSS呈现多行(例如长选择器或值),它们将保持多行(我猜是很好)
- ðº不像Prettier 那样可配置和广泛
这些是我可以忍受的权衡。