知識
不管是網站,軟件還是小程序,都要直接或間接能為您產生價值,我們在追求其視覺表現的同時,更側重于功能的便捷,營銷的便利,運營的高效,讓網站成為營銷工具,讓軟件能切實提升企業(yè)內部管理水平和效率。優(yōu)秀的程序為后期升級提供便捷的支持!
【babel+小程序】下
發(fā)表時間:2021-3-31
發(fā)布人:葵宇科技
瀏覽次數:55
babel插件替換全局常量
1.思路
想必大家肯定很熟悉這種模式
let host = 'http://www.tanwanlanyue.com/'
if(process.env.NODE_ENV === 'production'){
host = 'http://www.zhazhahui.com/'
}
通過這種只在編譯過程中存在的全局常量,我們可以做很多值的匹配。
因為wepy已經預編譯了一層,在框架內的業(yè)務代碼是讀取不了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'
}
}
//...
}
Out
export default class extends wepy.app {
config = {
pages: [
'modules/home/pages/index',
],
window: {
backgroundTextStyle: 'light',
navigationBarBackgroundColor: '#fff',
navigationBarTitleText: '大家好我是渣渣輝',
navigationBarTextStyle: 'black'
}
}
//...
}
2.學習如何編寫babel插件
編寫B(tài)abel插件入門手冊
AST轉換器
編寫babel插件之前先要理解抽象語法樹這個概念。編譯器做的事可以總結為:解析,轉換,生成。具體的概念解釋去看入門手冊可能會更好。這里講講我自己的一些理解。
解析包括詞法分析與語法分析。
解析過程吧。其實按我的理解(不知道這樣合適不合適= =)抽象語法樹跟DOM樹其實很類似。詞法分析有點像是把html解析成一個一個的dom節(jié)點的過程,語法分析則有點像是將dom節(jié)點描述成dom樹。
轉換過程是編譯器最復雜邏輯最集中的地方。首先要理解“樹形遍歷”與“訪問者模式”兩個概念。
“樹形遍歷”如手冊中所舉例子:
假設有這么一段代碼:
function square(n) {
return n * n;
}
那么有如下的樹形結構:
- FunctionDeclaration
- Identifier (id)
- Identifier (params[0])
- BlockStatement (body)
- ReturnStatement (body)
- BinaryExpression (argument)
- Identifier (left)
- Identifier (right)
進入
FunctionDeclaration
- 進入
Identifier (id)
- 走到盡頭
- 退出
Identifier (id)
- 進入
Identifier (params[0])
- 走到盡頭
- 退出
Identifier (params[0])
進入
BlockStatement (body)
進入
ReturnStatement (body)
進入
BinaryExpression (argument)
- 進入
Identifier (left)
- 退出
Identifier (left)
- 進入
Identifier (right)
- 退出
Identifier (right)
- 進入
- 退出
BinaryExpression (argument)
- 退出
ReturnStatement (body)
- 退出
BlockStatement (body)
- 進入
“訪問者模式”則可以理解為,進入一個節(jié)點時被調用的方法。例如有如下的訪問者:
const idVisitor = {
Identifier() {//在進行樹形遍歷的過程中,節(jié)點為標識符時,訪問者就會被調用
console.log("visit an Identifier")
}
}
結合樹形遍歷來看,就是說每個訪問者有進入、退出兩次機會來訪問一個節(jié)點。
而我們這個替換常量的插件的關鍵之處就是在于,訪問節(jié)點時,通過識別節(jié)點為我們的目標,然后替換他的值!
3.動手寫插件
話不多說,直接上代碼。這里要用到的一個工具是 babel-types
,用來檢查節(jié)點。
難度其實并不大,主要工作在于熟悉如何匹配目標節(jié)點。如匹配memberExpression時使用matchesPattern方法,匹配標識符則直接檢查節(jié)點的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()){//轉換父節(jié)點的二元表達式,如: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編譯之前執(zhí)行我們的編譯腳本,替換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數組
}],
],
})
當然最終我們是轉換成功啦,這個插件也用上了生產環(huán)境。但是后來沒有采用這方案替換pages字段。暫時只替換了 __ENV__: process.env.NODE_ENV
與 __VERSION__: version
兩個常量。
為什么呢?
因為每次編譯之后標識符 __ROUTES__
都會被轉換成我們的路由表,那么下次我想替換的時候難道要手動刪掉然后再加上 __ROUTES__
嗎? = = 好傻
編寫babel腳本識別pages字段
1.思路
- 首先獲取到源代碼:app.wpy是類vue單文件的語法。js都在script標簽內,那么怎么獲取這部分代碼呢?又正則?不好吧,太撈了。通過閱讀 wepy-cli的源碼 ,使用xmldom這個庫來解析,獲取script標簽內的代碼。
- 編寫訪問者遍歷并替換節(jié)點:首先是找到繼承自
wepy.app
的類,再找到config
字段,最后匹配key為pages
的對象的值。最后替換目標節(jié)點 - babel轉換為代碼后,通過讀寫文件替換目標代碼。大業(yè)已成!done!
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 = 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.使用方法
只需要在執(zhí)行 wepy build --watch
之前先執(zhí)行這份腳本,就可自動替換路由表,自動化操作。監(jiān)聽文件變動,增加模塊時自動重新跑腳本,更新路由表,開發(fā)體驗一流~
結語
需求不緊張的時候真的要慢慢鉆研,把代碼往更自動化更工程化的方向寫,這樣的過程收獲還是挺大的。
第一次寫這么長的東西,假如覺得有幫助的話,歡迎一起交流一下。另希望加入一些質量較高的前端小群,如有朋友推薦不勝感激!