VUE 組件轉(zhuǎn)換為微信小程序組件的方法
簡介:
首先我們介紹一下本文的關(guān)鍵點(diǎn):抽象語法樹,它是以樹狀的形式表現(xiàn)編程語言的語法結(jié)構(gòu),樹上的每個(gè)節(jié)點(diǎn)都表示源代碼中的一種結(jié)構(gòu)。
通過操作這棵樹,可以精確的定位到聲明、賦值、運(yùn)算語句,從而實(shí)現(xiàn)對代碼的優(yōu)化、變更等操作。
本文通過對 VUE 組件的 JavaScript 、CSS模塊進(jìn)行轉(zhuǎn)換,比如 JavaScript模塊,包括外層對象,生命周期鉤子函數(shù),賦值語句,組件的內(nèi)部數(shù)據(jù),組件的對外屬性等等來實(shí)現(xiàn)把一個(gè) VUE 組件轉(zhuǎn)換為 一個(gè)小程序組件。
AST 抽象語法樹,似乎我們平時(shí)并不會接觸到。實(shí)際上在我們的項(xiàng)目當(dāng)中,CSS 預(yù)處理,JSX 亦或是 TypeScript 的處理,代碼格式化美化工具,Eslint, Javascript 轉(zhuǎn)譯,代碼壓縮,Webpack, Vue-Cli,ES6 轉(zhuǎn) ES5,當(dāng)中都離不開 AST 抽象語法樹這個(gè)綠巨人。先賣個(gè)關(guān)子,讓我們看一下 AST 到的官方解釋:
It is a hierarchical program representation that presents source code structure according to the grammar of a programming language, each AST node corresponds to an item of a source code.
中文的解釋有:
抽象語法樹(abstract syntax tree或者縮寫為 AST ),或者語法樹(syntax tree),是源代碼的抽象語法結(jié)構(gòu)的樹狀表現(xiàn)形式,這里特指編程語言的源代碼。和抽象語法樹相對的是具體語法樹(concrete syntaxtree),通常稱作分析樹(parse tree)。一般的,在源代碼的翻譯和編譯過程中,語法分析器創(chuàng)建出分析樹。一旦 AST 被創(chuàng)建出來,在后續(xù)的處理過程中,比如語義分析階段,會添加一些信息。
抽象語法樹,它以樹狀的形式表現(xiàn)編程語言的語法結(jié)構(gòu),樹上的每個(gè)節(jié)點(diǎn)都表示源代碼中的一種結(jié)構(gòu)。
通過操作這棵樹,可以精確的定位到聲明、賦值、運(yùn)算語句,從而實(shí)現(xiàn)對代碼的優(yōu)化、變更等操作。這些并不是我們想要看到的。
對于 AST 面紗的神秘感,似乎已經(jīng)將要揭開。不錯,在我剛接觸到他的時(shí)候,同樣感覺確實(shí)是難。但是當(dāng)你開始了解了它以后,你就會越來越喜歡它。
因?yàn)樗麑?shí)在太強(qiáng)大了。AST 本身并不難,難點(diǎn)在于轉(zhuǎn)換的目標(biāo)對象與源對象的語法差異,當(dāng)中水深毋庸置疑。但是,這才能更加激起我們探索他的欲望。在開始之前,我們先看一下 抽象語法樹到底長什么樣子。
一、 一探究竟 AST
通過 astexplorer [1] (AST樹查看網(wǎng)站),通過他你可以方便的查看代碼的語法樹,挑你喜歡的庫。你可以在線把玩 AST,而且除了 JavaScript,HTML, CSS 還有很多其它語言的 AST 庫,讓我們對他有一個(gè)感性而直觀的認(rèn)識。
請看下圖,看看AST語法樹長什么樣子:
此圖看到的是一個(gè) ExportDefaultDeclaration 也就是export default {}
,還有他的位置信息,注釋失信,tokens等等。
國際慣例,先來一個(gè)小demo
輸入數(shù)據(jù)
function square(n) { return n * n; }
處理數(shù)據(jù)
astFn() { const code = `function square(n) { return n * n; }`; //得到 AST 語法樹 const ast = babylon.parse(code); traverse(ast, { enter(path) { console.log("enter: " + path.node.type); //如下的語句匹配到 return 中的 n 與參數(shù) n,并且將它替換為 x。 if (path.node.type === "Identifier" && path.node.name === "n") { path.node.name = "x"; } } }); generate(ast, {}, code);//將 AST 轉(zhuǎn)換為代碼 console.log(generate(ast, {}, code).code );//打印出轉(zhuǎn)換后的 JavaScript 代碼 }
輸出數(shù)據(jù)
function square(x) { return x * x; }
我們看一下我們得到的 AST 樹
接下來我們插入一段 把 VUE 組件轉(zhuǎn)換為微信小程序組件正則版本的處理
二、 簡單粗暴的版本(VUE 組件轉(zhuǎn)換為微信小程序組件)
沒有使用 AST 將 VUE 組件轉(zhuǎn)換成小程序組件的簡易版本介紹
下方是兩段代碼,簡單的邏輯,實(shí)現(xiàn)思路,匹配目標(biāo)字符串,替換字符,然后生成文件。
regs:{//通過標(biāo)簽匹配來替換對應(yīng)的小程序支持的標(biāo)簽 toViewStartTags: /(<h1)|(<s)|(<em)|(<ul)|(<li)|(<dl)|(<i)|(<span)/g, toViewEndTags: /(<\/h1>)|(<\/s>)|(<\/em>)|(<\/ul>)|(<\/li>)|(<\/dl>)|(<\/i>)|(<\/span>)/g, toBlockStartTags: /(<div)|(<p)/g, toBlockEndTags: /(<\/div>)|(<\/p>)/g, }, signObj: {//通過標(biāo)簽查找來分離腳本,模板和CSS tempStart: '<template>', tempEnd: '</template>', styleStart: '<style scoped>', styleEnd: '</style>', scriptStart: '<script>', scriptEnd: '</script>' }
上方是正則版本的一些模板匹配規(guī)則,經(jīng)過后續(xù)的一系列處理把一個(gè) VUE組件處理得到對應(yīng)的小程序的 WXML ,WXSS,JSON,JS,4個(gè)文件。
//文件 const wxssFilePath = path.join(dirPath, `${mpFile}.wxss`); const jsFilePath = path.join(dirPath, `${mpFile}.js`); const wxmlFilePath = path.join(dirPath, `${mpFile}.wxml`); const jsonFilePath = path.join(dirPath, `${mpFile}.json`); if (!fs.existsSync(dirPath)) { fs.mkdirSync(dirPath); } fs.writeFile(wxssFilePath, wxssContent, err => { if (err) throw err; //console.log(`dist目錄下生成${mpFile}.wxss文件成功`); fs.writeFile(jsFilePath, jsContent, err => { if (err) throw err; // console.log(`dist目錄下生成${mpFile}.js文件成功`); fs.writeFile(wxmlFilePath, wxmlContent, err => { if (err) throw err; //console.log(`dist目錄下生成${mpFile}.wxml文件成功`); fs.writeFile(jsonFilePath, jsonContent, err => { if (err) throw err; console.log(`dist目錄下生成${mpFile}.json文件成功`) resolve(`生成${mpFile}.json文件成功`); }) }) }); });
上方是處理得到的 WXML ,WXSS,JSON,JS,4個(gè)文件,并且生成到對應(yīng)的目錄下。代碼實(shí)現(xiàn)用時(shí)較短,后續(xù)更改方案,并沒有做優(yōu)化,這里就不做詳細(xì)展開討論這個(gè)實(shí)現(xiàn)方案了。
回到正題 介紹一下,AST 抽象語法樹的核心部分:
三、 抽象語法樹三大法寶
Babel 是 JavaScript 編譯器,更確切地說是源碼到源碼的編譯器,通常也叫做“ 轉(zhuǎn)換編譯器(transpiler)”。 意思是說你為 Babel 提供一些 JavaScript 代碼,Babel 更改這些代碼,然后返回給你新生成的代碼。
babel-core:Babel 的編譯器;它暴露了 babel.transform 方法。
[1] babylon:Babylon 是 Babel 的解析器。用于生成 AST 語法樹。
[2] babel-traverse:Babel 的遍歷器,所有的transformers都使用該工具遍歷所有的 AST (抽象語法樹),維護(hù)了整棵樹的狀態(tài),并且負(fù)責(zé)替換、移除和添加節(jié)點(diǎn)。我們可以和 Babylon 一起使用來遍歷和更新節(jié)點(diǎn)。
[3] babel-generator:Babel 的代碼生成器。它讀取AST并將其轉(zhuǎn)換為代碼。
整個(gè)編譯器就被分成了三部分:分析器、轉(zhuǎn)換器、生成器,大致的流程是:
輸入字符串 -> babylon分析器 parse -> 得到 AST -> 轉(zhuǎn)換器 -> 得到 AST -> babel-generator -> 輸出
總結(jié)核心三步:
AST 三大法寶
babylon.parse => traverse 轉(zhuǎn)換 AST => generate(ast, {}, code).code) 生成
感興趣的童鞋,可以在網(wǎng)上或者看參考資料都有介紹。該鋪墊的都鋪墊的差不多了,進(jìn)入正題。
我們到底是如何通過 AST 將 VUE 組件轉(zhuǎn)換為微信小程序組件的呢?
四、 VUE 組件轉(zhuǎn)換為微信小程序組件中 組件的對外屬性、賦值語句的轉(zhuǎn)換處理
轉(zhuǎn)換之前的 VUE 組件代碼 Demo
export default { //組件的對外屬性 props: { max: { type: Number, value: 99 } }, //組件的內(nèi)部數(shù)據(jù) data(){ return { num:10000 } }, //組件的方法 methods: { textFn() { this.num = 2 }, onMyButtonTap: function(){ this.num = 666 }, } }
處理后我們得到的微信小程序組件 JavaScript 部分代碼
export default { properties: { //組件的對外屬性 max: { type: Number, value: 99 } }, //組件的內(nèi)部數(shù)據(jù) data(){ return { num: 10000 } }, //組件的方法 methods: { textFn() { this.setData({ num: 2 }); }, onMyButtonTap: function () { this.setData({ num: 666 }); } } };
我們對js動了什么手腳(亦可封裝成babel插件):
//to AST const ast = babylon.parse(code, { sourceType: "module", plugins: ["flow"] }); //AST 轉(zhuǎn)換 node,nodetype很關(guān)鍵 traverse(ast, { enter(path) { //打印出node.type console.log("enter: " + path.node.type); } }) ObjectProperty(path) { //props 替換為 properties if (path.node.key.name === "props") { path.node.key.name = "properties"; } } //修改methods中使用到數(shù)據(jù)屬性的方法中。this.prop至this.data.prop等 與 this.setData的處理。 MemberExpression(path) { let datasVals = datas.map((item,index) =>{ return item.key.name //拿到data屬性中的第一級 }) if (//含有this的表達(dá)式 并且包含data屬性 path.node.object.type === "ThisExpression" && datasVals.includes(path.node.property.name) ) { path.get("object").replaceWithSourceString("this.data"); //判斷是不是賦值操作 if ( (t.isAssignmentExpression(path.parentPath) && path.parentPath.get("left") === path) || t.isUpdateExpression(path.parentPath) ) { const expressionStatement = path.findParent(parent => parent.isExpressionStatement() ); //...... } } }
轉(zhuǎn)換之前的js代碼的部分 AST 樹:
具體的 API 使用,童鞋們看一下 babel 相關(guān)的文檔了解一下。
五, VUE 組件轉(zhuǎn)換為微信小程序組件中 Export Default 到 Component 構(gòu)造器的轉(zhuǎn)換 與 生命周期鉤子函數(shù),事件函數(shù)的處理
首先我們看一下要轉(zhuǎn)換前后的語法樹與代碼如下(明確轉(zhuǎn)換目標(biāo)):
轉(zhuǎn)換之前的 AST 樹與代碼
export default {// VUE 組件的慣用寫法用于導(dǎo)出對象模塊 data(){ }, methods:{ }, props:{ } }
轉(zhuǎn)換之后的 AST 樹與代碼
components({//小程序組件的構(gòu)造器 data(){ }, methods:{ }, props:{ } })
通過以上轉(zhuǎn)換之前和轉(zhuǎn)換之后代碼和 AST 的對比我們明確了轉(zhuǎn)換目標(biāo)就是 ExportDefault 到 Component構(gòu)造器的轉(zhuǎn)換,下面看一下我們是如何處理的:
我們做了什么(在轉(zhuǎn)換中進(jìn)入到 ExportDefault 中做對應(yīng)的處理):
//ExportDefault 到 Component構(gòu)造器的轉(zhuǎn)換 ExportDefaultDeclaration(path) { //創(chuàng)建 CallExpression Component({}) function insertBeforeFn(path) { const objectExpression = t.objectExpression(propertiesAST); test = t.expressionStatement( t.callExpression(//創(chuàng)建名為 Compontents 的調(diào)用表達(dá)式,參數(shù)為 objectExpression t.identifier("Compontents"),[ objectExpression ] ) ); //最終得到的語法樹 console.log("test",test) } if (path.node.type === "ExportDefaultDeclaration") { if (path.node.declaration.properties) { //提取屬性并存儲 propertiesAST = path.node.declaration.properties; //創(chuàng)建 AST 包裹對象 insertBeforeFn(path); } //得到我們最終的轉(zhuǎn)換結(jié)果 console.log(generate(test, {}, code).code);
對于 ExportDefault => Component 構(gòu)造器轉(zhuǎn)換還有一種轉(zhuǎn)換思路 下面我們看一下:
[1] 第一種思路是先提取 ExportDefault 內(nèi)部所有節(jié)點(diǎn)的 AST ,并做處理,然后創(chuàng)建Component構(gòu)造器,插入提取處理后的 AST,得到最終的 AST
//propertiesAST 這個(gè)就是我們拿到的 AST,然后在對應(yīng)的分支內(nèi)做對應(yīng)的處理 以下分別為 data,methods,props,其他的鉤子同樣處理即可 propertiesAST.map((item, index) => { if (item.type === "ObjectProperty") { //props 替換為 properties if (item.key.name === "props") { item.key.name = "properties"; } } else if (item.type === "ObjectMethod") { if (path.node.key.name === "mounted") { path.node.key.name = "ready"; } else if (path.node.key.name === "created") { path.node.key.name = "attached"; } else if (path.node.key.name === "destroyed") { path.node.key.name = "detached"; } else if (path.node.type === "ThisExpression") { if (path.parent.property.name === "$emit") { path.parent.property.name = "triggerEvent"; } } else { void null; } } } else if (path.node.key.name === "methods") { path.traverse({ enter(path) { if (path.node.type === "ThisExpression") { if (path.parent.property.name === "$emit") { path.parent.property.name = "triggerEvent"; } } } }) } else { //... console.log("node type", item.type); } });
[2] 第二種思路呢,就是我們上面展示的這種,不過有一個(gè)關(guān)鍵的地方要注意一下:
//我把 ExportDefaultDeclaration 的處理放到最后來執(zhí)行,拿到 AST 首先進(jìn)行轉(zhuǎn)換。然后在創(chuàng)建得到新的小程序組件JS部分的 AST 即可 traverse(ast, { enter(path) {}, ObjectProperty(path) {}, ObjectMethod(path) {}, //...... ExportDefaultDeclaration(path) { //... } })
如果你想在 AST 開始處與結(jié)尾處插入,可使用 path 操作:
path.insertBefore( t.expressionStatement(t.stringLiteral("start..")) ); path.insertAfter( t.expressionStatement(t.stringLiteral("end..")) );
注:關(guān)于微信小程序不支持 computed , 與 watch,我們具體的初期采用的方案是掛載 computed 和 watch 方法到每一個(gè)微信小程序組件,讓小程序組件也支持這兩個(gè)功能。
六,VUE 組件轉(zhuǎn)換為微信小程序組件中 的 Data 部分的處理:
關(guān)于 Data 部分的處理實(shí)際上就是:函數(shù)表達(dá)式轉(zhuǎn)換為對象表達(dá)式 (FunctionExpression 轉(zhuǎn)換為 ObjectExpression)
轉(zhuǎn)換之前的 JavaScript 代碼
export default { data(){//函數(shù)表達(dá)式 return { num: 10000, arr: [1, 2, 3], obj: { d1: "val1", d2: "val2" } } } }
處理后我們得到的
export default { data: {//對象表達(dá)式 num: 10000, arr: [1, 2, 3], obj: { d1: "val1", d2: "val2" } } };
通過如上的代碼對比,我們看到了我們的轉(zhuǎn)換前后代碼的變化:
轉(zhuǎn)換前后 AST 樹對比圖明確轉(zhuǎn)換目標(biāo):
我們對 JavaScript 動了什么手腳(亦可封裝成babel插件):
const ast = babylon.parse(code, { sourceType: "module", plugins: ["flow"] }); //AST 轉(zhuǎn)換node、nodetype很關(guān)鍵 traverse(ast, { enter(path) { //打印出node.type console.log("enter: " + path.node.type); } })
我們的轉(zhuǎn)換部分都盡量在一個(gè) Traverse 中處理,減少 AST 樹遍歷的性能消耗
//Data 函數(shù)表達(dá)式 轉(zhuǎn)換為 Object ObjectMethod(path) { // console.log("path.node ",path.node )// data, add, textFn if (path.node.key.name === "data") { // 獲取第一級的 BlockStatement,也就是 Data 函數(shù)體 path.traverse({ BlockStatement(p) { //從 Data 中提取數(shù)據(jù)屬性 datas = p.node.body[0].argument.properties; } }); //創(chuàng)建對象表達(dá)式 const objectExpression = t.objectExpression(datas); //創(chuàng)建 Data 對象并賦值 const dataProperty = t.objectProperty( t.identifier("data"), objectExpression ); //插入到原 Data 函數(shù)下方 path.insertAfter(dataProperty); //刪除原 Data 函數(shù)節(jié)點(diǎn) path.remove(); } }
七,VUE 組件轉(zhuǎn)換為微信小程序組件中 CSS 部分的處理:
那 CSS 我們也是必須要處理的一部分,let try
以下是我們要處理的css樣本
const code = ` .text-ok{ position: absolute; right: 150px; color: #e4393c; } .nut-popup-close{ position: absolute; top: 50px; right: 120px; width: 50%; height: 200px; display: inline-block; font-size: 26px; }`;
處理后我們得到的
.text-ok { position: absolute; right: 351rpx; color: #e4393c; } .nut-popup-close { position: absolute; top: 117rpx; right: 280.79rpx; width: 50%; height: 468rpx; display: inline-block; font-size: 60.84rpx; }
通過前后代碼的對比,我們看到了單位尺寸的轉(zhuǎn)換(比如:top: 50px; 轉(zhuǎn)換為 top: 117rpx;)。
單位的轉(zhuǎn)換( px 轉(zhuǎn)為了 rpx )
CSS 又做了哪些處理呢?
同樣也有不少的 CSS Code Parsers 供我們選擇 Cssom ,CssTree等等,
我們拿 Cssom 來實(shí)現(xiàn)上方css代碼的一個(gè)簡單的轉(zhuǎn)換。
var ast = csstree.parse(code); csstree.walk(ast, function(node) { if(typeof node.value == "string" && isNaN(node.value) != true){ let newVal = Math.floor((node.value*2.34) * 100) / 100;//轉(zhuǎn)換比例這個(gè)根據(jù)情況設(shè)置即可 if(node.type === "Dimension"){//得到要轉(zhuǎn)換的數(shù)字尺寸 node.value = newVal; } } if(node.unit === "px"){//單位的處理 node.unit = "rpx" } }); console.log(csstree.generate(ast));
當(dāng)然這只是一個(gè) demo,實(shí)際項(xiàng)目中使用還的根據(jù)項(xiàng)目的實(shí)際情況出發(fā),SCSS,LESS等等的轉(zhuǎn)換與考慮不同的處理場景哦!
注:本文有些模塊的轉(zhuǎn)換實(shí)現(xiàn)還未在小程序開發(fā)工具中測試。
插播一個(gè)通過 AST 實(shí)現(xiàn)的好東東:
將 JavaScript 代碼轉(zhuǎn)化生成 SVG 流程圖 js2flowchart( 4.5 k stars 在 GitHub )
當(dāng)你擁有 AST 時(shí),可以做任何你想要做的事。把AST轉(zhuǎn)回成字符串代碼并不是必要的,你可以通過它畫一個(gè)流程圖,或者其它你想要的東西。
js2flowchart使用場景是什么呢?通過流程圖,你可以解釋你的代碼,或者給你代碼寫文檔;通過可視化的解釋學(xué)習(xí)其他人的代碼;通過簡單的js語法,為每個(gè)處理過程簡單的描述創(chuàng)建流程圖。
馬上用最簡單的方式嘗試一下吧,去線上編輯看看 js-code-to-svg-flowchart [8]。
此處有必要附上截圖一張。
八、總結(jié):
通過以上我們的介紹,我們大概對抽象語法樹有了初步的了解??傮w思路是:我們用Babel的解析器 把 JavaScript 源碼轉(zhuǎn)化為抽象語法樹,
再通過 Babel 的遍歷器遍歷 AST (抽象語法樹),替換、移除和添加節(jié)點(diǎn),得到一個(gè)新的 AST 樹。最后, 使用,Babel 的代碼生成器 Babel Generator 模塊 讀取 處理后的 AST 并將其轉(zhuǎn)換為代碼。任務(wù)就完成了!
本文通過對 VUE 組件轉(zhuǎn)換為微信小程序組件的轉(zhuǎn)換部分包括如下內(nèi)容:
- VUE 組件 JavaScript模塊 對外屬性轉(zhuǎn)換為小程序?qū)ν鈱傩缘奶幚?/li>
- VUE 組件 JavaScript模塊 內(nèi)部數(shù)據(jù)的轉(zhuǎn)換為小程序內(nèi)部數(shù)據(jù)的處理
- VUE 組件 JavaScript模塊 methods 中的賦值語句轉(zhuǎn)換為小程序賦值語句的處理
- VUE 組件 JavaScript模塊 外層對象,生命周期鉤子函數(shù)的處理與 CSS 模塊的簡易處理
總結(jié)
以上所述是小編給大家介紹的VUE 組件轉(zhuǎn)換為微信小程序組件的方法,希望對大家有所幫助,如果大家有任何疑問請給我留言,小編會及時(shí)回復(fù)大家的。在此也非常感謝大家對腳本之家網(wǎng)站的支持!
如果你覺得本文對你有幫助,歡迎轉(zhuǎn)載,煩請注明出處,謝謝!
相關(guān)文章
基于vue2.0+vuex+localStorage開發(fā)的本地記事本示例
這篇文章主要介紹了基于vue2.0+vuex+localStorage開發(fā)的本地記事本示例,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下。2017-02-02vue2中使用SSE(服務(wù)器發(fā)送事件)原因分析
SSE是圍繞只讀Comet交互推出的API或者模式,SSE 支持短輪詢、長輪詢和HTTP 流,而且能在斷開連接時(shí)自動確定何時(shí)重新連接,本文重點(diǎn)給大家介紹2023-10-10Vue偵測相關(guān)api的實(shí)現(xiàn)方法
這篇文章主要介紹了Vue偵測相關(guān)api,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-05-05