前言
hello,祝各位小伙伴们七月快乐噻!
我是来自推啊前端团队的D同学。
今天要跟大家分享一个AST
及PostCss
的相关知识。阅读时长大概5-8分钟。如有错误,敬请指正。
序章
某年某?的某☀️,张三(法外狂徒)做的一个项目出了问题,在各种浏览器上部分CSS
层面的表现不一致,定位到原因:是各大浏览器对某些属性支持程度不一样。于是张三就去敲定解决方案如下。
-
手动更改 -
脚本处理-
单一正则处理:遍历需要处理的文件,正则匹配。 -
转化为易操作的对象进行处理。
-
张三经过思索,还是选择了最后一个方案,然后就陷入了沉思,如何转化成易操作的对象呢❓
有句话是这么讲的,当你遇到棘手的问题,一时半会解决不掉时,已经有很多人遇到类似的的问题,并且给出了解决思路或者方案。
于是张三决定站在前人的肩膀上,去找寻一个突破口—JavaScript
方向的AST
解析过程。那么首先要了解一下什么是快乐星球 AST
?
一、什么是AST?
AST
即抽象语法树(Abstract Syntax Tree),是源码语法结构的一种抽象表示。它以树的形式表现编程语言的语法,结构树上的每个节点都表示源代码中的一种结构。
人话:AST
形式上的本质就是一段JSON
,或者说无数个JSON节点
组成的一颗大的JSON?
。
举个?:
const randomNum = 0.1
if(randomNum < 0.5){
console.log('right')
}else{
console.log('error')
}
复制代码
上述的js代码,转化成的AST
如下所示:
{
"type":"Program",
"body":[
{
"type":"VariableDeclaration",
"declarations":[
{
"type":"VariableDeclarator",
"id":{
"type":"Identifier",
"name":"randomNum"
},
"init":{
"type":"Literal",
"value":0.1,
"raw":"0.1"
}
}
],
"kind":"var"
},
{
"type":"IfStatement",
"test":{
"type":"BinaryExpression",
"operator":"<",
"left":{
"type":"Identifier",
"name":"randomNum"
},
"right":{
"type":"Literal",
"value":0.5,
"raw":"0.5"
}
},
"consequent":{
"type":"BlockStatement",
"body":[
{
"type":"ExpressionStatement",
"expression":{
"type":"CallExpression",
"callee":{
"type":"MemberExpression",
"computed":false,
"object":{
"type":"Identifier",
"name":"console"
},
"property":{
"type":"Identifier",
"name":"log"
}
},
"arguments":[
{
"type":"Literal",
"value":"right",
"raw":"'right'"
}
]
}
}
]
},
"alternate":{
"type":"BlockStatement",
"body":[
{
"type":"ExpressionStatement",
"expression":{
"type":"CallExpression",
"callee":{
"type":"MemberExpression",
"computed":false,
"object":{
"type":"Identifier",
"name":"console"
},
"property":{
"type":"Identifier",
"name":"log"
}
},
"arguments":[
{
"type":"Literal",
"value":"error",
"raw":"'error'"
}
]
}
}
]
}
}
],
"sourceType":"script"
}
复制代码
由上述JSON对象,我们可以轻而易举的从得到的AST
中看出源代码是什么样的。
换句话说,我们能从得到的AST
中完全还原源代码.
二、AST
的常规编译过程
张三了解完
JavaScript
类型的AST
流程之后,略加思索?,有了!比着葫芦画个瓢,读书人的东西,怎么能叫偷呢?这叫借鉴思想。
于是张三就借鉴着
JavaScript
的AST
流程,编写CSS
层面的AST
解析代码逻辑(即PostCss
的前置工作,将源文件转为话AST
)。具体分为以下几步。
1、读取源代码文件后,定义若干标识符,用于分割节点。
const openBracket = '{'.charCodeAt(0);
const closeBracket = '}'.charCodeAt(0);
const openParen = '('.charCodeAt(0);
const closeParen = ')'.charCodeAt(0);
const singleQuote = '\''.charCodeAt(0);
const doubleQuote = '"'.charCodeAt(0);
const backslash = '\\'.charCodeAt(0);
const slash = '/'.charCodeAt(0);
const period = '.'.charCodeAt(0);
const comma = ','.charCodeAt(0);
const colon = ':'.charCodeAt(0);
const asterisk = '*'.charCodeAt(0);
const minus = '-'.charCodeAt(0);
const plus = '+'.charCodeAt(0);
const pound = '#'.charCodeAt(0);
const newline = '\n'.charCodeAt(0);
const space = ' '.charCodeAt(0);
const feed = '\f'.charCodeAt(0);
const tab = '\t'.charCodeAt(0);
const cr = '\r'.charCodeAt(0);
const at = '@'.charCodeAt(0);
const lowerE = 'e'.charCodeAt(0);
const upperE = 'E'.charCodeAt(0);
const digit0 = '0'.charCodeAt(0);
const digit9 = '9'.charCodeAt(0);
const lowerU = 'u'.charCodeAt(0);
const upperU = 'U'.charCodeAt(0);
const atEnd = /[ \n\t\r\{\(\)'"\\;,/]/g;
const wordEnd = /[ \n\t\r\(\)\{\}\*:;@!&'"\+\|~>,\[\]\\]|\/(?=\*)/g;
const wordEndNum = /[ \n\t\r\(\)\{\}\*:;@!&'"\-\+\|~>,\[\]\\]|\//g;
const alphaNum = /^[a-z0-9]/i;
const unicodeRange = /^[a-f0-9?\-]/i;
复制代码
2、根据标识符加上正则匹配的到若干Tokens
。
[
// 第一行第一列-第一行第四列为type='word'的值'#app'
[ 'word', '#app', 1, 1, 1, 4 ],
// 一个空格
[ 'space', ' ' ],
// 第一行第六列type='{'的值‘{’
[ '{', '{', 1, 6 ],
// 一个换行,两个空格
[ 'space', '\n ' ],
// 第二行第三列-第二行第七列为type='word'的值'color'
[ 'word', 'color', 2, 3, 2, 7 ],
// 一个换行
[ 'space', '\n' ],
// 第三行第一列type='}'的值‘}’
[ '}', '}', 3, 1 ]
]
复制代码
这里有细心的小伙伴可能发现了,’color=red’的属性声明中,red好像并不能从Tokens
中得到。
其实在这里,PostCss
并非严格按照上图解析过程执行的,它在生成Tokens的同时,已经生成了一颗不完整的AST了。
我们可以从PostCss源码中/libs/parser.js
中看出,在生成Token时,已经将值挂在到AST的节点上了。
3、根据Tokens将不完整的AST补充完成,得到如下数据。
{
"raws":{
"semicolon":false,
"after":""
},
"type":"root",
"nodes":[
{
"raws":{
"before":"",
"between":" ",
"semicolon":true,
"after":""
},
"type":"rule",
"nodes":[
{
"raws":{
"before":"",
"between":": "
},
"type":"decl",
"source":{
"start":{
"line":2,
"column":3
},
"input":{
"css":"#app {color: red;}",
"hasBOM":false,
"id":"<input css 1>"
},
"end":{
"line":2,
"column":13
}
},
"prop":"color",
"value":"red"
}
],
"source":{
"start":{
"line":1,
"column":1
},
"input":{
"css":"#app {color: red;}",
"hasBOM":false,
"id":"<input css 1>"
},
"end":{
"line":3,
"column":1
}
},
"selector":"#app"
}
],
"source":{
"input":{
"css":"#app {color: red;}",
"hasBOM":false,
"id":"<input css 1>"
},
"start":{
"line":1,
"column":1
}
}
}
复制代码
张三看着自己的劳动成果,满意的拍了拍脑袋上的假发?。然后又开始思考,大费周章得到一个
CSS
的AST
,就单单为了补全一下兼容前缀吗?是不是有点大材小用,头重脚轻呢?
同时张三又开始思考,如何增强这个工具库的健壮性和功能多样性呢。毫无疑问,插件机制是最适合这种工具库的一种模式。
三、PostCss
的插件机制
插件机制的核心,在于提供了十分简单的操作目标对象的一种方式,PostCss
它仅仅作为一个运行时,它的初始功能只有源文件生成AST
树,AST
树生成新文件,并没有什么功能性的操作。
但是它搭配上各种各样的插件,便有了无限种可能。跟Webpack
中加载各式各样的loader
思想上是一致的。
张三看着自己的成果,大为满意?。这样的话,自己把最脏最累的活干完了,然后提供了一颗完整的
AST
,在在这样灵活的插件生态下,任何人都可以直接拿到AST
自己去开发定制化插件,实现自己的功能。
简易版 AutoPrefixer
AutoPrefixer
实现原理:用从Can I Use
网站获取的数据为CSS
规则添加特定厂商的前缀。 Autoprefixer 自动获取浏览器的流行度和能够支持的属性,并根据这些数据帮你自动为 CSS 规则添加前缀。
简易版 StyleLint
张三的故事未完待续?…