想必大家肯定很熟悉这种模式
let host = '' if(process.env.NODE_ENV === 'production'){ host = '' }通过这种只在编译过程中存在的全局常量,我们可以做很多值的匹配。
因为wepy已经预编译了一层,在框架内的业务代码是读取不了process.env.NODE_ENV的值。我就想着要不做一个类似于webpack的DefinePlugin的babel插件吧。具体的思路是babel编译过程中访问ast时匹配需要替换的标识符或者表达式,然后替换掉相应的值。例如:
In
export default class extends wepy.app { config = { pages: __ROUTE__, window: { backgroundTextStyle: 'light', navigationBarBackgroundColor: '#fff', navigationBarTitleText: '大家好我是渣渣辉', navigationBarTextStyle: 'black' } } //... } Out export default class extends wepy.app { config = { pages: [ 'modules/home/pages/index', ], window: { backgroundTextStyle: 'light', navigationBarBackgroundColor: '#fff', navigationBarTitleText: '大家好我是渣渣辉', navigationBarTextStyle: 'black' } } //... } 2.学习如何编写babel插件编写Babel插件入门手册
AST转换器
编写babel插件之前先要理解抽象语法树这个概念。编译器做的事可以总结为:解析,转换,生成。具体的概念解释去看入门手册可能会更好。这里讲讲我自己的一些理解。
解析包括词法分析与语法分析。
解析过程吧。其实按我的理解(不知道这样合适不合适= =)抽象语法树跟DOM树其实很类似。词法分析有点像是把html解析成一个一个的dom节点的过程,语法分析则有点像是将dom节点描述成dom树。
转换过程是编译器最复杂逻辑最集中的地方。首先要理解“树形遍历”与“访问者模式”两个概念。
“树形遍历”如手册中所举例子:
假设有这么一段代码:
function square(n) { return n * n; }那么有如下的树形结构:
- FunctionDeclaration - Identifier (id) - Identifier (params[0]) - BlockStatement (body) - ReturnStatement (body) - BinaryExpression (argument) - Identifier (left) - Identifier (right)
进入 FunctionDeclaration
进入 Identifier (id)
走到尽头
退出 Identifier (id)
进入 Identifier (params[0])
走到尽头
退出 Identifier (params[0])
进入 BlockStatement (body)
进入 ReturnStatement (body)
进入 BinaryExpression (argument)
进入 Identifier (left)
退出 Identifier (left)
进入 Identifier (right)
退出 Identifier (right)
退出 BinaryExpression (argument)
退出 ReturnStatement (body)
退出 BlockStatement (body)
“访问者模式”则可以理解为,进入一个节点时被调用的方法。例如有如下的访问者:
const idVisitor = { Identifier() {//在进行树形遍历的过程中,节点为标识符时,访问者就会被调用 console.log("visit an Identifier") } }结合树形遍历来看,就是说每个访问者有进入、退出两次机会来访问一个节点。
而我们这个替换常量的插件的关键之处就是在于,访问节点时,通过识别节点为我们的目标,然后替换他的值!
3.动手写插件话不多说,直接上代码。这里要用到的一个工具是 babel-types ,用来检查节点。
难度其实并不大,主要工作在于熟悉如何匹配目标节点。如匹配memberExpression时使用matchesPattern方法,匹配标识符则直接检查节点的name等等套路。最终成品及用法可以见 我的github
const memberExpressionMatcher = (path, key) => path.matchesPattern(key)//复杂表达式的匹配条件 const identifierMatcher = (path, key) => path.node.name === key//标识符的匹配条件 const replacer = (path, value, valueToNode) => {//替换操作的工具函数 path.replaceWith(valueToNode(value)) if(path.parentPath.isBinaryExpression()){//转换父节点的二元表达式,如:var isProp = __ENV__ === 'production' ===> var isProp = true const result = path.parentPath.evaluate() if(result.confident){ path.parentPath.replaceWith(valueToNode(result.value)) } } } export default function ({ types: t }){//这里需要用上babel-types这个工具 return { visitor: { MemberExpression(path, { opts: params }){//匹配复杂表达式 Object.keys(params).forEach(key => {//遍历Options if(memberExpressionMatcher(path, key)){ replacer(path, params[key], t.valueToNode) } }) }, Identifier(path, { opts: params }){//匹配标识符 Object.keys(params).forEach(key => {//遍历Options if(identifierMatcher(path, key)){ replacer(path, params[key], t.valueToNode) } }) }, } } } 4.结果