babel插件替換全局常量1.思路想必大家肯定很熟悉這種模式 let host = 'http://www.tanwanlanyue.com/' if(process.env.NODE_ENV === 'production'){ host = 'http://www.zhazhahui.com/' } 通過(guò)這種只在編譯過(guò)程中存在的全局常量,我們可以做很多值的匹配。 因?yàn)閣epy已經(jīng)預(yù)編譯了一層,在框架內(nèi)的業(yè)務(wù)代碼是讀取不了process.env.NODE_ENV的值。我就想著要不做一個(gè)類似于webpack的DefinePlugin的babel插件吧。具體的思路是babel編譯過(guò)程中訪問(wèn)ast時(shí)匹配需要替換的標(biāo)識(shí)符或者表達(dá)式,然后替換掉相應(yīng)的值。例如: 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.學(xué)習(xí)如何編寫babel插件編寫B(tài)abel插件入門手冊(cè) AST轉(zhuǎn)換器 編寫babel插件之前先要理解抽象語(yǔ)法樹這個(gè)概念。編譯器做的事可以總結(jié)為:解析,轉(zhuǎn)換,生成。具體的概念解釋去看入門手冊(cè)可能會(huì)更好。這里講講我自己的一些理解。 解析包括詞法分析與語(yǔ)法分析。 解析過(guò)程吧。其實(shí)按我的理解(不知道這樣合適不合適= =)抽象語(yǔ)法樹跟DOM樹其實(shí)很類似。詞法分析有點(diǎn)像是把html解析成一個(gè)一個(gè)的dom節(jié)點(diǎn)的過(guò)程,語(yǔ)法分析則有點(diǎn)像是將dom節(jié)點(diǎn)描述成dom樹。 轉(zhuǎn)換過(guò)程是編譯器最復(fù)雜邏輯最集中的地方。首先要理解“樹形遍歷”與“訪問(wèn)者模式”兩個(gè)概念。 “樹形遍歷”如手冊(cè)中所舉例子: 假設(shè)有這么一段代碼: function square(n) { return n * n; } 那么有如下的樹形結(jié)構(gòu): - FunctionDeclaration - Identifier (id) - Identifier (params[0]) - BlockStatement (body) - ReturnStatement (body) - BinaryExpression (argument) - Identifier (left) - Identifier (right)
“訪問(wèn)者模式”則可以理解為,進(jìn)入一個(gè)節(jié)點(diǎn)時(shí)被調(diào)用的方法。例如有如下的訪問(wèn)者: const idVisitor = { Identifier() {//在進(jìn)行樹形遍歷的過(guò)程中,節(jié)點(diǎn)為標(biāo)識(shí)符時(shí),訪問(wèn)者就會(huì)被調(diào)用 console.log("visit an Identifier") } } 結(jié)合樹形遍歷來(lái)看,就是說(shuō)每個(gè)訪問(wèn)者有進(jìn)入、退出兩次機(jī)會(huì)來(lái)訪問(wèn)一個(gè)節(jié)點(diǎn)。 而我們這個(gè)替換常量的插件的關(guān)鍵之處就是在于,訪問(wèn)節(jié)點(diǎn)時(shí),通過(guò)識(shí)別節(jié)點(diǎn)為我們的目標(biāo),然后替換他的值! 3.動(dòng)手寫插件話不多說(shuō),直接上代碼。這里要用到的一個(gè)工具是 babel-types ,用來(lái)檢查節(jié)點(diǎn)。 難度其實(shí)并不大,主要工作在于熟悉如何匹配目標(biāo)節(jié)點(diǎn)。如匹配memberExpression時(shí)使用matchesPattern方法,匹配標(biāo)識(shí)符則直接檢查節(jié)點(diǎn)的name等等套路。最終成品及用法可以見(jiàn) 我的github const memberExpressionMatcher = (path, key) => path.matchesPattern(key)//復(fù)雜表達(dá)式的匹配條件 const identifierMatcher = (path, key) => path.node.name === key//標(biāo)識(shí)符的匹配條件 const replacer = (path, value, valueToNode) => {//替換操作的工具函數(shù) path.replaceWith(valueToNode(value)) if(path.parentPath.isBinaryExpression()){//轉(zhuǎn)換父節(jié)點(diǎn)的二元表達(dá)式,如: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這個(gè)工具 return { visitor: { MemberExpression(path, { opts: params }){//匹配復(fù)雜表達(dá)式 Object.keys(params).forEach(key => {//遍歷Options if(memberExpressionMatcher(path, key)){ replacer(path, params[key], t.valueToNode) } }) }, Identifier(path, { opts: params }){//匹配標(biāo)識(shí)符 Object.keys(params).forEach(key => {//遍歷Options if(identifierMatcher(path, key)){ replacer(path, params[key], t.valueToNode) } }) }, } } } 4.結(jié)果當(dāng)然啦,這塊插件不可以寫在wepy.config.js中配置。因?yàn)閺囊婚_始我們的目標(biāo)就是在wepy編譯之前執(zhí)行我們的編譯腳本,替換pages字段。所以最終的腳本是引入 babel-core 轉(zhuǎn)換代碼 const babel = require('babel-core') //...省略獲取app.wpy過(guò)程,待會(huì)會(huì)談到。 //...省略編寫visitor過(guò)程,語(yǔ)法跟編寫插件略有一點(diǎn)點(diǎn)不同。 const result = babel.transform(code, { parserOpts: {//babel的解析器,babylon的配置。記得加入classProperties,否則會(huì)無(wú)法解析app.wpy的類語(yǔ)法 sourceType: 'module', plugins: ['classProperties'] }, plugins: [ [{ visitor: myVistor//使用我們寫的訪問(wèn)者 }, { __ROUTES__: pages//替換成我們的pages數(shù)組 }], ], }) 當(dāng)然最終我們是轉(zhuǎn)換成功啦,這個(gè)插件也用上了生產(chǎn)環(huán)境。但是后來(lái)沒(méi)有采用這方案替換pages字段。暫時(shí)只替換了 __ENV__: process.env.NODE_ENV 與 __VERSION__: version 兩個(gè)常量。 為什么呢? 因?yàn)槊看尉幾g之后標(biāo)識(shí)符 __ROUTES__ 都會(huì)被轉(zhuǎn)換成我們的路由表,那么下次我想替換的時(shí)候難道要手動(dòng)刪掉然后再加上 __ROUTES__ 嗎? = = 好傻 編寫babel腳本識(shí)別pages字段1.思路
2.成果最終腳本: /** * @author zhazheng * @description 在wepy編譯前預(yù)編譯。獲取app.wpy內(nèi)的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標(biāo)簽內(nèi)的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.轉(zhuǎn)換并生成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.使用方法只需要在執(zhí)行 wepy build --watch 之前先執(zhí)行這份腳本,就可自動(dòng)替換路由表,自動(dòng)化操作。監(jiān)聽文件變動(dòng),增加模塊時(shí)自動(dòng)重新跑腳本,更新路由表,開發(fā)體驗(yàn)一流~ 結(jié)語(yǔ)需求不緊張的時(shí)候真的要慢慢鉆研,把代碼往更自動(dòng)化更工程化的方向?qū)懀@樣的過(guò)程收獲還是挺大的。 第一次寫這么長(zhǎng)的東西,假如覺(jué)得有幫助的話,歡迎一起交流一下。另希望加入一些質(zhì)量較高的前端小群,如有朋友推薦不勝感激! |