摘要:而掃描各個模塊并合并路由表的腳本非常簡單,讀寫文件就了。編寫插件之前先要理解抽象語法樹這個概念。的解析器,的配置。編寫腳本識別字段思路首先獲取到源代碼是類單文件的語法。獲取內的字段,并替換成已生成的路由表。
話不多說先上圖,簡要說明一下干了些什么事。圖可能太模糊,可以點svg看看
最近公司開展了小程序的業務,派我去負責這一塊的業務,其中需要處理的一個問題是接入我們web開發的傳統架構--模塊化開發。
我們來詳細說一下模塊化開發具體是怎么樣的。
我們的git工作流采用的是git flow。一個項目會拆分成幾個模塊,然后一人負責一個模塊(對應git flow的一個feature)獨立開發。模塊開發并與后端聯通后再合并至develop進行集成測試,后續經過一系列測試再發布版本。
目錄結構大體如圖所示,一個模塊包含了他自己的pages / components / assets / model / mixins / apis / routes / scss等等。
這種開發模式的好處不言而喻,每個人都可以并行開發,大大提升開發速度。這次就是要移植這種開發模式到小程序中。
目標背景說完了,那么來明確一下我們的目標。
我采用的是wepy框架,類vue語法的開發,開發體驗非常棒。在vue中,一個組件就是單文件,包含了js、html、css。wepy采用vue的語法,但由與vue稍稍有點區別,wepy的組件分為三種--wepy.app類,wepy.page類,wepy.component類。
對應到我們的目錄結構中,每個模塊實際上就是一系列的page組件。要組合這一系列的模塊,那么很簡單,我們要做的就是把這一系列page的路由掃描成一個路由表,然后插入到小程序的入口--app.json中。對應wepy框架那即是app.wpy中的pages字段。
第一步!先得到所有pages的路由并綜合成一個路由表!
我的方案是,在每個模塊中新建一份routes文件,相當于注冊每個需要插入到入口的page的路由,不需要接入業務的page就不用注冊啦。是不是很熟悉呢,對的,就是參考vue-router的注冊語法。
//routes.js module.exports = [ { name: "home-detail",//TODO: name先占位,后續再嘗試通過讀name跳轉某頁 page: "detail",//需要接入入口的page的文件名。例如這里是index.wpy。相對于src/的路徑就是`modules/${moduleName}/pages/index`。 }, { name: "home-index", page: "index", meta: { weight: 100//這里加了一個小功能,因為小程序指定pages數組的第一項為首頁,后續我會通過這個權重字段來給pages路由排序。權重越高位置越前。 } } ]
而掃描各個模塊并合并路由表的腳本非常簡單,讀寫文件就ok了。
const fs = require("fs") const path = require("path") const routeDest = path.join(__dirname, "../src/config/routes.js") const modulesPath = path.join(__dirname, "../src/modules") let routes = [] fs.readdirSync(modulesPath).forEach(module => { if(module.indexOf(".DS_Store") > -1) return const route = require(`${modulesPath}/${module}/route`) route.forEach(item => { item.page = `modules/${module}/pages/${item.page.match(//?(.*)/)[1]}` }) routes = routes.concat(route) }) fs.writeFileSync(routeDest,`module.exports = ${JSON.stringify(routes)}`, e => { console.log(e) })
路由排序策略
const strategies = { sortByWeight(routes) { routes.sort((a, b) => { a.meta = a.meta || {} b.meta = b.meta || {} const weightA = a.meta.weight || 0 const weightB = b.meta.weight || 0 return weightB - weightA }) return routes } }
最后得出路由表
const Strategies = require("../src/lib/routes-model") const routes = Strategies.sortByWeight(require("../src/config/routes")) const pages = routes.map(item => item.page) console.log(pages)//["modules/home/pages/index", "modules/home/pages/detail"]替換路由數組
So far so good...問題來了,如何替換入口文件中的路由數組。我如下做了幾步嘗試。
直接引入我第一感覺就是,這不很簡單嗎?在wepy編譯之前,先跑腳本得出路由表,再import這份路由表就得了。
import routes from "./routes" export default class extends wepy.app { config = { pages: routes,//["modules/home/pages/index"] window: { backgroundTextStyle: "light", navigationBarBackgroundColor: "#fff", navigationBarTitleText: "大家好我是渣渣輝", navigationBarTextStyle: "black" } } //... }
然而這樣小程序肯定會炸啦,pages字段的值必須是靜態的,在小程序運行之前就配置好,動態引入是不行的!不信的話諸君可以試試。那么就是說,劃重點---我們必須在wepy編譯之前再預編譯一次---事先替換掉pages字段的值!
正則匹配替換既然要事先替換,那就是要精準定位pages字段的值,然后再替換掉。難點在于如果精準定位pages字段的值呢?
最撈然而最快的方法:正則匹配。
事先定好編碼規范,在pages字段的值的前后添加/* __ROUTES__ */的注釋
腳本如下:
const fs = require("fs") const path = require("path") import routes from "./routes" function replace(source, arr) { const matchResult = source.match(//* __ROUTE__ */([sS]*)/* __ROUTE__ *//) if(!matchResult) { throw new Error("必須包含/* __ROUTE__ */標記注釋") } const str = arr.reduce((pre, next, index, curArr) => { return pre += `"${curArr[index]}", ` }, "") return source.replace(matchResult[1], str) } const entryFile = path.join(__dirname, "../src/app.wpy") let entry = fs.readFileSync(entryFile, {encoding: "UTF-8"}) entry = replace(entry, routes) fs.writeFileSync(entryFile, entry)
app.wpy的變化如下:
//before export default class extends wepy.app { config = { pages: [ /* __ROUTE__ */ /* __ROUTE__ */ ], window: { backgroundTextStyle: "light", navigationBarBackgroundColor: "#fff", navigationBarTitleText: "大家好我是渣渣輝", navigationBarTextStyle: "black" } } //... } //after export default class extends wepy.app { config = { pages: [ /* __ROUTE__ */"modules/home/pages/index", /* __ROUTE__ */ ], window: { backgroundTextStyle: "light", navigationBarBackgroundColor: "#fff", navigationBarTitleText: "大家好我是渣渣輝", navigationBarTextStyle: "black" } } //... }
行吧,也總算跑通了。因為項目很趕,所以先用這個方案開發了一個半星期。開發完之后總覺得這種方案太難受,于是密謀著換另一種各精準的自動的方案。。。
babel插件替換全局常量 1.思路想必大家肯定很熟悉這種模式
let host = "http://www.tanwanlanyue.com/" if(process.env.NODE_ENV === "production"){ host = "http://www.zhazhahui.com/" }
通過這種只在編譯過程中存在的全局常量,我們可以做很多值的匹配。
因為wepy已經預編譯了一層,在框架內的業務代碼是讀取不了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插件
在這里先要給大家推薦幾份學習資料:
首先是babel官網推薦的這份迷你編譯器的代碼,讀完之后基本能理解編譯器做的三件事:解析,轉換,生成的過程了。
其次是編寫Babel插件入門手冊。基本涵蓋了編寫插件的方方面面,不過由于babel幾個工具文檔的缺失,在寫插件的時候需要去翻查代碼中的注釋閱讀api用法。
然后是大殺器AST轉換器--astexplorer.net。我們來看一下,babel的解析器--babylon的文檔,涵蓋的節點類型這么多,腦繪一張AST樹不現實。我在編寫腳本的時候會先把代碼放在轉換器內生成AST樹,再一步一步走。
編寫babel插件之前先要理解抽象語法樹這個概念。編譯器做的事可以總結為:解析,轉換,生成。具體的概念解釋去看入門手冊可能會更好。這里講講我自己的一些理解。
解析包括詞法分析與語法分析。
解析過程吧。其實按我的理解(不知道這樣合適不合適= =)抽象語法樹跟DOM樹其實很類似。詞法分析有點像是把html解析成一個一個的dom節點的過程,語法分析則有點像是將dom節點描述成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)
“訪問者模式”則可以理解為,進入一個節點時被調用的方法。例如有如下的訪問者:
const idVisitor = { Identifier() {//在進行樹形遍歷的過程中,節點為標識符時,訪問者就會被調用 console.log("visit an Identifier") } }
結合樹形遍歷來看,就是說每個訪問者有進入、退出兩次機會來訪問一個節點。
而我們這個替換常量的插件的關鍵之處就是在于,訪問節點時,通過識別節點為我們的目標,然后替換他的值!
話不多說,直接上代碼。這里要用到的一個工具是babel-types,用來檢查節點。
難度其實并不大,主要工作在于熟悉如何匹配目標節點。如匹配memberExpression時使用matchesPattern方法,匹配標識符則直接檢查節點的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()){//轉換父節點的二元表達式,如: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編譯之前執行我們的編譯腳本,替換pages字段。所以的方案是在跑wepy build --watch
之前跑我們的編譯腳本,具體操作是引入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數組 }], ], })
當然最終我們是轉換成功啦,這個插件也用上了生產環境。但是后來沒有采用這方案替換pages字段。暫時只替換了__ENV__: process.env.NODE_ENV與__VERSION__: version兩個常量。
為什么呢?
因為每次編譯之后標識符__ROUTES__都會被轉換成我們的路由表,那么下次我想替換的時候難道要手動刪掉然后再加上__ROUTES__嗎?我當然不會干跟我們自動化工程化的思想八字不合的事情啦。
不過寫完這個插件之后收獲還是挺大的,基本了解該如何通過編譯器尋找并替換我們的目標節點了。
首先獲取到源代碼:app.wpy是類vue單文件的語法。js都在script標簽內,那么怎么獲取這部分代碼呢?又正則?不好吧,太撈了。通過閱讀wepy-cli的源碼,使用xmldom這個庫來解析,獲取script標簽內的代碼。
編寫訪問者遍歷并替換節點:首先是找到繼承自wepy.app的類,再找到config字段,最后匹配key為pages的對象的值。最后替換目標節點
babel轉換為代碼后,通過讀寫文件替換目標代碼。大業已成!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.在遍歷ast樹的過程中,嵌套三層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.轉換并生成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.使用方法
只需要在執行wepy build --watch之前先執行這份腳本,就可自動替換路由表,自動化操作。監聽文件變動,增加模塊時自動重新跑腳本,更新路由表,開發體驗一流~
結語把代碼往更自動化更工程化的方向寫,這樣的過程收獲還是挺大的。但是確實這份腳本仍有不足之處,起碼匹配節點這部分的代碼是不大嚴謹的。
另外插播一份廣告
我司風變科技正招聘前端開發:
應屆、一年經驗,熟悉Vue的前端小鮮肉
三年經驗的前端大佬
我!們!都!想!要!
我們開發團隊不僅代碼寫的好,而且男程序員還擁有著100%的脫單率!!快來加入我們吧!
郵箱:nicolas_refn@foxmail.com
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/107233.html
摘要:抽象語法樹是怎么生成的談到這點,就要說到計算機是怎么讀懂我們的代碼的。需要注意什么狀態狀態是抽象語法樹轉換的敵人,狀態管理會不斷牽扯我們的精力,而且幾乎所有你對狀態的假設,總是會有一些未考慮到的語法最終證明你的假設是錯誤的。 現在談到 babel 肯定大家都不會感覺到陌生,雖然日常開發中很少會直接接觸到它,但它已然成為了前端開發中不可或缺的工具,不僅可以讓開發者可以立即使用 ES 規范...
摘要:小程序自選股項目團隊在長達五個月的時間里經歷了太多不為人知的故事,不知通宵了多少個夜晚,只為等待小龍宣布號小程序正式發布的到來。第一次被微信開放平臺開發二組團隊小程序項目團隊當小白鼠各種實驗新特性。。 導語:我很喜歡小龍的一句話,微信因你而變。是的,把事情做到極致的時候,這個世界就會因你而變。 小程序自選股項目團隊在長達五個月的時間里經歷了太多不為人知的故事, 不知通宵了多少個夜晚,只...
摘要:獲取的對象范圍方法獲取的是最終應用在元素上的所有屬性對象即使沒有代碼,也會把默認的祖宗八代都顯示出來而只能獲取元素屬性中的樣式。因此對于一個光禿禿的元素,方法返回對象中屬性值如果有就是據我測試不同環境結果可能有差異而就是。 花了很長時間整理的前端面試資源,喜歡請大家不要吝嗇star~ 別只收藏,點個贊,點個star再走哈~ 持續更新中……,可以關注下github 項目地址 https:...
閱讀 2074·2021-11-15 17:57
閱讀 739·2021-11-11 16:54
閱讀 2589·2021-09-27 13:58
閱讀 4067·2021-09-06 15:00
閱讀 950·2021-09-04 16:45
閱讀 3505·2019-08-30 15:56
閱讀 1784·2019-08-30 15:53
閱讀 1605·2019-08-30 14:12