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