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 之前先執行這份腳本,就可自動替換路由表,自動化操作。監聽文件變動,增加模塊時自動重新跑腳本,更新路由表,開發體驗一流~ 結語需求不緊張的時候真的要慢慢鉆研,把代碼往更自動化更工程化的方向寫,這樣的過程收獲還是挺大的。 第一次寫這么長的東西,假如覺得有幫助的話,歡迎一起交流一下。另希望加入一些質量較高的前端小群,如有朋友推薦不勝感激! |