小程序模板網(wǎng)

【babel+小程序】下

發(fā)布時間:2018-05-05 14:33 所屬欄目:小程序開發(fā)教程

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'
    }
  }
//...
}

Out

export 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)入 FunctionDeclaration

    • 進(jìn)入 Identifier (id)
    • 走到盡頭
    • 退出 Identifier (id)
    • 進(jìn)入 Identifier (params[0])
    • 走到盡頭
    • 退出 Identifier (params[0])
    • 進(jìn)入 BlockStatement (body)

      • 進(jìn)入 ReturnStatement (body)

        • 進(jìn)入 BinaryExpression (argument)

          • 進(jìn)入 Identifier (left)
          • 退出 Identifier (left)
          • 進(jìn)入 Identifier (right)
          • 退出 Identifier (right)
        • 退出 BinaryExpression (argument)
      • 退出 ReturnStatement (body)
    • 退出 BlockStatement (body)

“訪問者模式”則可以理解為,進(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.思路

  1. 首先獲取到源代碼:app.wpy是類vue單文件的語法。js都在script標(biāo)簽內(nèi),那么怎么獲取這部分代碼呢?又正則?不好吧,太撈了。通過閱讀 wepy-cli的源碼 ,使用xmldom這個庫來解析,獲取script標(biāo)簽內(nèi)的代碼。
  2. 編寫訪問者遍歷并替換節(jié)點(diǎn):首先是找到繼承自 wepy.app 的類,再找到 config 字段,最后匹配key為 pages 的對象的值。最后替換目標(biāo)節(jié)點(diǎn)
  3. babel轉(zhuǎn)換為代碼后,通過讀寫文件替換目標(biāo)代碼。大業(yè)已成!done!

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ì)量較高的前端小群,如有朋友推薦不勝感激!



易優(yōu)小程序(企業(yè)版)+靈活api+前后代碼開源 碼云倉庫:starfork
本文地址:http://22321a.com/wxmini/doc/course/24245.html 復(fù)制鏈接 如需定制請聯(lián)系易優(yōu)客服咨詢:800182392 點(diǎn)擊咨詢
QQ在線咨詢