【babel+小程序】下
發表時(shí)間:2021-3-31
發布人(rén):融晨科技
浏覽次數:55
babel插件替換全局常量
1.思路
想必大(dà)家肯定很熟悉這(zhè)種模式
let host = 'http://www.tanwanlanyue.com/'
if(process.env.NODE_ENV === 'production'){
host = 'http://www.zhazhahui.com/'
}
通過這(zhè)種隻在(zài)編譯過程中存在(zài)的(de)全局常量,我們可以(yǐ)做很多值的(de)匹配。
因爲(wéi / wèi)wepy已經預編譯了(le/liǎo)一層,在(zài)框架内的(de)業務代碼是(shì)讀取不(bù)了(le/liǎo)process.env.NODE_ENV的(de)值。我就(jiù)想着要(yào / yāo)不(bù)做一個(gè)類似于(yú)webpack的(de)DefinePlugin的(de)babel插件吧。具體的(de)思路是(shì)babel編譯過程中訪問ast時(shí)匹配需要(yào / yāo)替換的(de)标識符或者表達式,然後替換掉相應的(de)值。例如:
In
export default class extends wepy.app {
config = {
pages: __ROUTE__,
window: {
backgroundTextStyle: 'light',
navigationBarBackgroundColor: '#fff',
navigationBarTitleText: '大(dà)家好我是(shì)渣渣輝',
navigationBarTextStyle: 'black'
}
}
//...
}
Out
export default class extends wepy.app {
config = {
pages: [
'modules/home/pages/index',
],
window: {
backgroundTextStyle: 'light',
navigationBarBackgroundColor: '#fff',
navigationBarTitleText: '大(dà)家好我是(shì)渣渣輝',
navigationBarTextStyle: 'black'
}
}
//...
}
2.學習如何編寫babel插件
編寫Babel插件入門手冊
AST轉換器
編寫babel插件之(zhī)前先要(yào / yāo)理解抽象語法樹這(zhè)個(gè)概念。編譯器做的(de)事可以(yǐ)總結爲(wéi / wèi):解析,轉換,生成。具體的(de)概念解釋去看入門手冊可能會更好。這(zhè)裏講講我自己的(de)一些理解。
解析包括詞法分析與語法分析。
解析過程吧。其實按我的(de)理解(不(bù)知道(dào)這(zhè)樣合适不(bù)合适= =)抽象語法樹跟DOM樹其實很類似。詞法分析有點像是(shì)把html解析成一個(gè)一個(gè)的(de)dom節點的(de)過程,語法分析則有點像是(shì)将dom節點描述成dom樹。
轉換過程是(shì)編譯器最複雜邏輯最集中的(de)地(dì / de)方。首先要(yào / yāo)理解“樹形遍曆”與“訪問者模式”兩個(gè)概念。
“樹形遍曆”如手冊中所舉例子(zǐ):
假設有這(zhè)麽一段代碼:
function square(n) {
return n * n;
}
那麽有如下的(de)樹形結構:
- FunctionDeclaration
- Identifier (id)
- Identifier (params[0])
- BlockStatement (body)
- ReturnStatement (body)
- BinaryExpression (argument)
- Identifier (left)
- Identifier (right)
進入
FunctionDeclaration
- 進入
Identifier (id)
- 走到(dào)盡頭
- 退出(chū)
Identifier (id)
- 進入
Identifier (params[0])
- 走到(dào)盡頭
- 退出(chū)
Identifier (params[0])
進入
BlockStatement (body)
進入
ReturnStatement (body)
進入
BinaryExpression (argument)
- 進入
Identifier (left)
- 退出(chū)
Identifier (left)
- 進入
Identifier (right)
- 退出(chū)
Identifier (right)
- 進入
- 退出(chū)
BinaryExpression (argument)
- 退出(chū)
ReturnStatement (body)
- 退出(chū)
BlockStatement (body)
- 進入
“訪問者模式”則可以(yǐ)理解爲(wéi / wèi),進入一個(gè)節點時(shí)被調用的(de)方法。例如有如下的(de)訪問者:
const idVisitor = {
Identifier() {//在(zài)進行樹形遍曆的(de)過程中,節點爲(wéi / wèi)标識符時(shí),訪問者就(jiù)會被調用
console.log("visit an Identifier")
}
}
結合樹形遍曆來(lái)看,就(jiù)是(shì)說(shuō)每個(gè)訪問者有進入、退出(chū)兩次機會來(lái)訪問一個(gè)節點。
而(ér)我們這(zhè)個(gè)替換常量的(de)插件的(de)關鍵之(zhī)處就(jiù)是(shì)在(zài)于(yú),訪問節點時(shí),通過識别節點爲(wéi / wèi)我們的(de)目标,然後替換他(tā)的(de)值!
3.動手寫插件
話不(bù)多說(shuō),直接上(shàng)代碼。這(zhè)裏要(yào / yāo)用到(dào)的(de)一個(gè)工具是(shì) babel-types
,用來(lái)檢查節點。
難度其實并不(bù)大(dà),主要(yào / yāo)工作在(zài)于(yú)熟悉如何匹配目标節點。如匹配memberExpression時(shí)使用matchesPattern方法,匹配标識符則直接檢查節點的(de)name等等套路。最終成品及用法可以(yǐ)見 我的(de)github
const memberExpressionMatcher = (path, key) => path.matchesPattern(key)//複雜表達式的(de)匹配條件
const identifierMatcher = (path, key) => path.node.name === key//标識符的(de)匹配條件
const replacer = (path, value, valueToNode) => {//替換操作的(de)工具函數
path.replaceWith(valueToNode(value))
if(path.parentPath.isBinaryExpression()){//轉換父節點的(de)二元表達式,如: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 }){//這(zhè)裏需要(yào / yāo)用上(shàng)babel-types這(zhè)個(gè)工具
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.結果
當然啦,這(zhè)塊插件不(bù)可以(yǐ)寫在(zài)wepy.config.js中配置。因爲(wéi / wèi)從一開始我們的(de)目标就(jiù)是(shì)在(zài)wepy編譯之(zhī)前執行我們的(de)編譯腳本,替換pages字段。所以(yǐ)最終的(de)腳本是(shì)引入 babel-core
轉換代碼
const babel = require('babel-core')
//...省略獲取app.wpy過程,待會會談到(dào)。
//...省略編寫visitor過程,語法跟編寫插件略有一點點不(bù)同。
const result = babel.transform(code, {
parserOpts: {//babel的(de)解析器,babylon的(de)配置。記得加入classProperties,否則會無法解析app.wpy的(de)類語法
sourceType: 'module',
plugins: ['classProperties']
},
plugins: [
[{
visitor: myVistor//使用我們寫的(de)訪問者
}, {
__ROUTES__: pages//替換成我們的(de)pages數組
}],
],
})
當然最終我們是(shì)轉換成功啦,這(zhè)個(gè)插件也(yě)用上(shàng)了(le/liǎo)生産環境。但是(shì)後來(lái)沒有采用這(zhè)方案替換pages字段。暫時(shí)隻替換了(le/liǎo) __ENV__: process.env.NODE_ENV
與 __VERSION__: version
兩個(gè)常量。
爲(wéi / wèi)什麽呢?
因爲(wéi / wèi)每次編譯之(zhī)後标識符 __ROUTES__
都會被轉換成我們的(de)路由表,那麽下次我想替換的(de)時(shí)候難道(dào)要(yào / yāo)手動删掉然後再加上(shàng) __ROUTES__
嗎? = = 好傻
編寫babel腳本識别pages字段
1.思路
- 首先獲取到(dào)源代碼:app.wpy是(shì)類vue單文件的(de)語法。js都在(zài)script标簽内,那麽怎麽獲取這(zhè)部分代碼呢?又正則?不(bù)好吧,太撈了(le/liǎo)。通過閱讀 wepy-cli的(de)源碼 ,使用xmldom這(zhè)個(gè)庫來(lái)解析,獲取script标簽内的(de)代碼。
- 編寫訪問者遍曆并替換節點:首先是(shì)找到(dào)繼承自
wepy.app
的(de)類,再找到(dào)config
字段,最後匹配key爲(wéi / wèi)pages
的(de)對象的(de)值。最後替換目标節點 - babel轉換爲(wéi / wèi)代碼後,通過讀寫文件替換目标代碼。大(dà)業已成!done!
2.成果
最終腳本:
/**
* @author zhazheng
* @description 在(zài)wepy編譯前預編譯。獲取app.wpy内的(de)pages字段,并替換成已生成的(de)路由表。
*/
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标簽内的(de)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,父類爲(wéi / wèi)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 = https://www.wxapp-union.com/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.使用方法
隻需要(yào / yāo)在(zài)執行 wepy build --watch
之(zhī)前先執行這(zhè)份腳本,就(jiù)可自動替換路由表,自動化操作。監聽文件變動,增加模塊時(shí)自動重新跑腳本,更新路由表,開發體驗一流~
結語
需求不(bù)緊張的(de)時(shí)候真的(de)要(yào / yāo)慢慢鑽研,把代碼往更自動化更工程化的(de)方向寫,這(zhè)樣的(de)過程收獲還是(shì)挺大(dà)的(de)。
第一次寫這(zhè)麽長的(de)東西,假如覺得有幫助的(de)話,歡迎一起交流一下。另希望加入一些質量較高的(de)前端小群,如有朋友推薦不(bù)勝感激!