babel插件替换全局常量1.思路想必大家肯定很熟悉这种模式 let host = 'http://www.tanwanlanyue.com/' if(process.env.NODE_ENV === 'production'){ host = 'http://www.zhazhahui.com/' } 通过这种只在编译过程中存在的全局常量,我们可以做很多值的匹配。 因为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' } } //... } Outexport 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)
“访问者模式”则可以理解为,进入一个节点时被调用的方法。例如有如下的访问者: 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.结果当然啦,这块插件不可以写在wepy.config.js中配置。因为从一开始我们的目标就是在wepy编译之前执行我们的编译脚本,替换pages字段。所以最终的脚本是引入 babel-core 转换代码 const babel = require('babel-core') //...省略获取app.wpy过程,待会会谈到。 //...省略编写visitor过程,语法跟编写插件略有一点点不同。 const result = babel.transform(code, { parserOpts: {//babel的解析器,babylon的配置。记得加入classProperties,否则会无法解析app.wpy的类语法 sourceType: 'module', plugins: ['classProperties'] }, plugins: [ [{ visitor: myVistor//使用我们写的访问者 }, { __ROUTES__: pages//替换成我们的pages数组 }], ], }) 当然最终我们是转换成功啦,这个插件也用上了生产环境。但是后来没有采用这方案替换pages字段。暂时只替换了 __ENV__: process.env.NODE_ENV 与 __VERSION__: version 两个常量。 为什么呢? 因为每次编译之后标识符 __ROUTES__ 都会被转换成我们的路由表,那么下次我想替换的时候难道要手动删掉然后再加上 __ROUTES__ 吗? = = 好傻 编写babel脚本识别pages字段1.思路
2.成果最终脚本: /** * @author zhazheng * @description 在wepy编译前预编译。获取app.wpy内的pages字段,并替换成已生成的路由表。 */ const babel = require('babel-core') const t = require('babel-types') //1.引入路由 const Strategies = require('../src/lib/routes-model') const routes = Strategies.sortByWeight(require('../src/config/routes')) const pages = routes.map(item => item.page) //2.解析script标签内的js,获取code const xmldom = require('xmldom') const fs = require('fs') const path = require('path') const appFile = path.join(__dirname, '../src/app.wpy') const fileContent = fs.readFileSync(appFile, { encoding: 'UTF-8' }) let xml = new xmldom.DOMParser().parseFromString(fileContent) function getCodeFromScript(xml){ let code = '' Array.prototype.slice.call(xml.childNodes || []).forEach(child => { if(child.nodeName === 'script'){ Array.prototype.slice.call(child.childNodes || []).forEach(c => { code += c.toString() }) } }) return code } const code = getCodeFromScript(xml) // 3.嵌套三层visitor //3.1.找class,父类为wepy.app const appClassVisitor = { Class: { enter(path, state) { const classDeclaration = path.get('superClass') if(classDeclaration.matchesPattern('wepy.app')){ path.traverse(configVisitor, state) } } } } //3.2.找config const configVisitor = { ObjectExpression: { enter(path, state){ const expr = path.parentPath.node if(expr.key && expr.key.name === 'config'){ path.traverse(pagesVisitor, state) } } } } //3.3.找pages,并替换 const pagesVisitor = { ObjectProperty: { enter(path, { opts }){ const isPages = path.node.key.name === 'pages' if(isPages){ path.node.value = t.valueToNode(opts.value) } } } } // 4.转换并生成code const result = babel.transform(code, { parserOpts: { sourceType: 'module', plugins: ['classProperties'] }, plugins: [ [{ visitor: appClassVisitor }, { value: pages }], ], }) // 5.替换源代码 fs.writeFileSync(appFile, fileContent.replace(code, result.code)) 3.使用方法只需要在执行 wepy build --watch 之前先执行这份脚本,就可自动替换路由表,自动化操作。监听文件变动,增加模块时自动重新跑脚本,更新路由表,开发体验一流~ 结语需求不紧张的时候真的要慢慢钻研,把代码往更自动化更工程化的方向写,这样的过程收获还是挺大的。 第一次写这么长的东西,假如觉得有帮助的话,欢迎一起交流一下。另希望加入一些质量较高的前端小群,如有朋友推荐不胜感激! |