一次基于AST的大規(guī)模代碼遷移實(shí)踐
在研發(fā)項(xiàng)目過程中,我們經(jīng)常會(huì)遇到技術(shù)架構(gòu)迭代更新的需求,通過技術(shù)的迭代更新,讓項(xiàng)目從新的技術(shù)特性中受益,但由于很多新的技術(shù)迭代版本并不能完全向下兼容,包含了很多非兼容性的改變(Breaking Changes),因此我們需要設(shè)計(jì)一款工具,幫助我們完成大規(guī)模代碼自動(dòng)遷移問題。本文簡(jiǎn)單闡述了基于 AST 的代碼遷移概念和大致流程,并通過代碼案例帶大家了解到了其中的處理細(xì)節(jié)。
一、背景介紹
在研發(fā)項(xiàng)目過程中,我們經(jīng)常會(huì)遇到技術(shù)架構(gòu)迭代更新的需求,通過技術(shù)的迭代更新,讓項(xiàng)目從新的技術(shù)特性中受益。例如將 Vue 2 遷移至 Vue 3、Webpack 4 升級(jí) Webpack 5、構(gòu)建工具遷移至 Vite 等,這些技術(shù)架構(gòu)的升級(jí)能讓項(xiàng)目持續(xù)受益,獲得諸如可維護(hù)性、性能、擴(kuò)展性、編譯速度、可讀性等等方面的提升,適時(shí)的對(duì)項(xiàng)目進(jìn)行技術(shù)架構(gòu)更新是很有必要的。
那既然新特性這么好,有人會(huì)說那當(dāng)然要與時(shí)俱進(jìn),隨時(shí)更新了。
但問題在于很多新的技術(shù)迭代版本并不能完全向下兼容,包含了很多非兼容性的改變(Breaking Changes),并不是簡(jiǎn)單升個(gè)版本就行了,通常還需要投入不少的人力和學(xué)習(xí)成本。例如 Vue 3 只能兼容80%的 Vue 2 代碼,對(duì)于一些新特性、新語法糖,開發(fā)者只能參考官方提供的遷移文檔,手動(dòng)完成遷移。
1.1 Vue 3 代碼遷移案例
來看一個(gè) Vue 3 的代碼遷移案例,在 Vue 2 和 Vue 3 中聲明一個(gè)全局指令(Directive)的差異:
(1)Vue 2:允許直接在 Vue 原型上注冊(cè)全局指令。而在 Vue 3 中,為了避免多個(gè) Vue 實(shí)例產(chǎn)生指令混淆,已經(jīng)不再支持該寫法。
import Vue from 'vue'
Vue.directive('focus', {
inserted: (el) => el.focus()
})
(2)Vue 3:建議通過 createApp 創(chuàng)建 Vue 實(shí)例,并直接在實(shí)例上注冊(cè)全局指令。就像這樣:
import { createApp } from 'vue'
const app = createApp({})
app.directive('focus', {
inserted: (el) => el.focus()
})
以上是一個(gè)大家熟知的 Vue 3 遷移案例,看似簡(jiǎn)單,動(dòng)幾行代碼即可。但當(dāng)我們的項(xiàng)目規(guī)模足夠大,或者有大量項(xiàng)目都需要類似代碼遷移時(shí),工作量會(huì)變得巨大,并且很難規(guī)避手動(dòng)遷移的帶來的風(fēng)險(xiǎn)。
因此,一般針對(duì)大規(guī)模的項(xiàng)目遷移,最好的方式還是寫個(gè)腳手架工具,協(xié)助我們完成自動(dòng)化遷移。既能提高效率,又能降低人工遷移的風(fēng)險(xiǎn)。
1.2 本文的代碼遷移背景
同樣地,我在項(xiàng)目中也遇到了相同的技術(shù)架構(gòu)升級(jí)難題。簡(jiǎn)單來說,我需要將基于 Vue 2 的項(xiàng)目遷移到一個(gè)我司內(nèi)部自研的技術(shù)棧,這個(gè)技術(shù)棧的語法結(jié)構(gòu)和 Vue 2 相似,但由于底層的技術(shù)原因,有一部分語法上的差異,需要手動(dòng)去遷移改造兼容(類似 Vue 2 升級(jí)至 Vue 3 的過程)。
除了和遷移 Vue 3 一樣需要針對(duì) JavaScript、Template 模板做遷移處理之外,我還需要額外去單獨(dú)處理 CSS、Less、SCSS 等樣式文件。
所以,我實(shí)現(xiàn)了一個(gè)自動(dòng)化遷移腳手架工具,來協(xié)助完成代碼的遷移工作,減少人工遷移帶來的低效和風(fēng)險(xiǎn)問題。
二、代碼遷移思路
剛剛提到我們需要設(shè)計(jì)一個(gè)腳手架來幫助我們完成自動(dòng)化的代碼遷移,那腳手架該如何設(shè)計(jì)呢?
首先,代碼遷移思路可以簡(jiǎn)單概括為:對(duì)原代碼做靜態(tài)代碼分析,并按一定規(guī)則替換為新代碼。那最直觀的辦法就是利用正則表達(dá)式來匹配和替換代碼,所以我也做了這樣的嘗試。
2.1 思路一:利用正則表達(dá)式匹配規(guī)則和替換代碼
例如,將下述代碼:
import { toast } from '@vivo/v-jsbridge'
按規(guī)則替換為:
import { toast } from '@webf/webf-vue-render'
這看起來很簡(jiǎn)單,似乎用正則匹配即可完成,像這樣:
const regx = /\@vivo\/v\-jsbridge/gi
const target = '@webf/webf-vue-render'
sourceCode.replace(regx, target)
但在實(shí)操過程中,發(fā)現(xiàn)正則表達(dá)式實(shí)在太局限,有幾個(gè)核心問題:
- 正則表達(dá)式完全基于字符串匹配,對(duì)原代碼格式的統(tǒng)一性要求很高。空格、換行、單雙引號(hào)等格式差異都可能引起正則匹配錯(cuò)誤;
- 面對(duì)復(fù)雜的匹配場(chǎng)景,正則表達(dá)式很難寫、很晦澀,容易誤匹配、誤處理;
- 處理樣式文件時(shí),需要兼容 CSS / Less / SCSS / Sass 等語法差異,工作量倍增。
簡(jiǎn)單舉個(gè)例子,當(dāng)我需要匹配 import { toast } from '@vivo/v-jsbridge' 字符串時(shí)。針對(duì)單雙引號(hào)、空格、分號(hào)等細(xì)節(jié)處理上需要更仔細(xì),稍有不慎就會(huì)忽略一些特殊場(chǎng)景,結(jié)果就是匹配失敗,造成隱蔽的遷移問題。
import { toast } from '@vivo/v-jsbridge' // 單引號(hào)
import { toast } from "@vivo/v-jsbridge" // 雙引號(hào)
import { toast } from "@vivo/v-jsbridge"; // 雙引號(hào) + 分號(hào)
import {toast} from "@vivo/v-jsbridge"; // 無空格
所以,用簡(jiǎn)單的正則匹配規(guī)則是無法幫助我們完成大規(guī)模的代碼遷移和重構(gòu)的,我們需要更好的方法:基于 AST 的代碼遷移。
2.2 思路二:基于 AST(抽象語法樹)的代碼遷移
在了解到正則匹配規(guī)則的局限性后,我把目光鎖定到了基于 AST 的代碼遷移上。
那么什么是基于 AST 的代碼遷移呢?
2.2.1 Babel 的編譯過程
如果你了解過 Babel 的代碼編譯原理,應(yīng)該對(duì) AST 代碼遷移不陌生。我們知道 Babel 的編譯過程大致分為三個(gè)步驟:
- 解析:將源代碼解析為 AST(抽象語法樹);
- 變換:對(duì) AST 進(jìn)行變換;
- 再建:根據(jù)變換后的 AST 重新構(gòu)建生成新的代碼。
(圖片來源:Luminosity Blog )
舉個(gè)例子,Babel 將一個(gè) ES6 語法轉(zhuǎn)換為 ES5 語法的過程如下:
(1)輸入一個(gè)簡(jiǎn)單的 sayHello 箭頭函數(shù)方法源碼:
const sayHello = () => {
console.log('hello')
}
(2)經(jīng)過 Babel 解析為 AST(可以看到 AST 是一串由 JSON 描述的語法樹),并對(duì) AST 進(jìn)行規(guī)則變換:
- 將 type 字段由 ArrowFunctionExpression 轉(zhuǎn)換為 FunctionExpression
- 將 kind 字段由 const 轉(zhuǎn)換為 var
{
"type": "Program",
"start": 0,
"end": 228,
"body": [
{
"type": "VariableDeclaration",
"start": 179,
"end": 227,
"declarations": [
{
"type": "VariableDeclarator",
"start": 185,
"end": 227,
"id": {
"type": "Identifier",
"start": 185,
"end": 193,
"name": "sayHello"
},
"init": {
- "type": "ArrowFunctionExpression",
+ "type": "FunctionExpression",
"start": 196,
"end": 227,
- "id": null,
+ "id": {
+ "type": "Identifier",
+ "start": 203,
+ "end": 211,
+ "name": "sayHello"
+ },
"expression": false,
"generator": false,
"async": false,
"params": [],
"body": {
"type": "BlockStatement",
"start": 202,
"end": 227,
"body": [
{
"type": "ExpressionStatement",
"start": 205,
"end": 225,
"expression": {
"type": "CallExpression",
"start": 205,
"end": 225,
"callee": {
"type": "MemberExpression",
"start": 205,
"end": 216,
"object": {
"type": "Identifier",
"start": 205,
"end": 212,
"name": "console"
},
"property": {
"type": "Identifier",
"start": 213,
"end": 216,
"name": "log"
},
"computed": false,
"optional": false
},
"arguments": [
{
"type": "Literal",
"start": 217,
"end": 224,
"value": "hello",
"raw": "'hello'"
}
],
"optional": false
}
}
]
}
}
}
],
- "kind": "const"
+ "kind": "var"
}
],
"sourceType": "module"
}
(3)從 AST 重新構(gòu)建為 ES5 語法:
var sayHello = function sayHello() {
console.log('hello');
};
這樣就完成了一個(gè)簡(jiǎn)單的 ES6 到 ES5 的語法轉(zhuǎn)換。我們的腳手架自動(dòng)代碼遷移思路也是如此。
對(duì)比正則表達(dá)式匹配,基于 AST 代碼遷移,有幾點(diǎn)好處:
- 比字符串匹配更靈活、涵蓋更多復(fù)雜場(chǎng)景。
- 通常 AST 代碼遷移工具都提供了方便的解析、查詢、匹配、替換的 API,能輕易寫出高效的代碼轉(zhuǎn)換規(guī)則。
- 方便統(tǒng)一轉(zhuǎn)換后的代碼風(fēng)格。
2.2.2 代碼遷移流程設(shè)計(jì)
了解了 AST 的基本原理和可行性后,我們需要找到合適的工具庫(kù)來完成代碼的 AST 解析、重構(gòu)、生成??紤]到項(xiàng)目中至少包含了這幾種內(nèi)容(腳本、樣式、HTML):
- 單獨(dú)的 JS 文件;
- 單獨(dú)的樣式文件:CSS / Less / SCSS / Sass;
- Vue 文件:包含 Template、Script、Style 三部分。
我們需要分別找到各類文件內(nèi)容對(duì)應(yīng)的解析和處理工具。
首先,是 JS 文件的解析處理工具的選擇。在市面上比較流行的 JS AST 工具有很多種選擇,例如最常見的 Babel、jscodeshift 以及 Esprima、Recast、Acorn、estraverse 等。做了一些簡(jiǎn)單調(diào)研后,發(fā)現(xiàn)這些工具都有一些共通的缺陷:
- 上手難度大,有較大的學(xué)習(xí)成本,要求開發(fā)者充分了解 AST 的語法規(guī)范;
- 語法復(fù)雜,代碼量大;
- 代碼可讀性差,不利于維護(hù)。
以 jscodeshift 為例,如果我們需要匹配一個(gè)簡(jiǎn)單語句:item.update('price')(this, '50'),它的實(shí)現(xiàn)代碼如下:
const callExpressions = root.find(j.CallExpression, {
callee: {
callee: {
object: {
name: 'item'
},
property: {
name: 'update'
}
},
arguments: [{
value: 'price'
}]
},
arguments: [{
type: 'ThisExpression'
}, {
value: '50'
}]
})
其實(shí)相比于原始的 Babel 語法,上述的 jscodeshift 語法已經(jīng)相對(duì)簡(jiǎn)潔,但可以看出依然有較大的代碼量,并且要求開發(fā)者熟練掌握 AST 的語法結(jié)構(gòu)。
因此我找到了一個(gè)更簡(jiǎn)潔、更高效的 AST 工具:GoGoCode,它是一款阿里開源的 AST 工具,封裝了類似 jQuery 的語法,簡(jiǎn)單易用。一個(gè)直觀的對(duì)比就是,如果用 GoGoCode 同樣實(shí)現(xiàn)上述的語句匹配,只需要一行代碼:
$(code).find(`item.update('price')(this, '50')`)
它直觀的語義以及簡(jiǎn)潔的代碼,讓我選擇了它作為 JS 的 AST 解析工具。
其次,是單獨(dú)的 CSS 樣式文件解析工具選擇。這個(gè)選擇很輕易,直接使用通用的 PostCSS 來解析和處理樣式即可。
最后,是 Vue 文件的解析工具選擇。因?yàn)?Vue 文件是由 Template、Script、Style 三部分組成,因此需要更復(fù)雜的工具進(jìn)行組合處理。很慶幸的是 GoGoCode 除了能夠?qū)为?dú)的 JS 文件進(jìn)行解析處理,它還封裝了對(duì) Vue 文件中的 Template 和 Script 部分的處理能力,因此 Vue 文件中除了 Style 樣式部分,我們也可以交由 GoGo Code 來處理。那 Style 樣式的部分該如何處理呢?這里我大致看了官方的 vue-loader 源碼,發(fā)現(xiàn)源碼中使用的是 @vue/component-compiler-utils 來解析 Vue 的 SFC 文件,它可以將文件中的 Style 樣式內(nèi)容單獨(dú)抽離出來。因此思路很簡(jiǎn)單,我們利用 @vue/component-compiler-utils 將 Vue 文件中的 Style 樣式內(nèi)容抽離出來,再交由 PostCSS 來處理即可。
那么,簡(jiǎn)單總結(jié)下找到的幾款適合的工具庫(kù):
- GoGoCode:阿里開源的一款抽象語法樹處理工具,可用于解析 JS / HTML / Vue 文件并生成抽象語法樹(AST),進(jìn)行代碼的規(guī)則替換、重構(gòu)等。封裝了類似 jQuery 的語法,簡(jiǎn)單易用。
- PostCSS:大家熟悉的開源 CSS 代碼遷移工具,可用于解析 Less / CSS / SCSS / Sass 等樣式文件并生成語法樹(AST),進(jìn)行代碼的規(guī)則替換、重構(gòu)等。
- @vue/component-compiler-utils:Vue 的開源工具庫(kù),可用于解析 Vue 的 SFC 文件,我用它將 SFC 中的 Style 內(nèi)容單獨(dú)抽出,并配合 PostCSS 來處理樣式代碼的規(guī)則替換、重構(gòu)。
有了這三個(gè)工具,我們就可以梳理出針對(duì)不同文件內(nèi)容的處理思路:
- JS 文件:交給 GoGoCode 處理。
- CSS / Less / SCSS / Sass 文件:交給 PostCSS 處理。
- Vue文件:
- Template / Script 部分:交給 GoGoCode 處理。
- Style 部分:先用 @vue/component-compiler-utils 解析出 Style 部分,再交給 PostCSS 處理。
有了處理思路后,接下來進(jìn)入正文,深入代碼細(xì)節(jié),詳細(xì)了解代碼遷移流程。
三、代碼遷移流程詳解
整個(gè)代碼遷移流程分為幾個(gè)步驟,分別是:
3.1 遍歷和讀取文件內(nèi)容
遍歷項(xiàng)目文件內(nèi)容,根據(jù)文件類型交由不同的 transform 函數(shù)來處理:
- transformVue:處理 Vue 文件
- transformScript:處理 JS 文件
- transformStyle:處理 CSS 等樣式文件
const path = require('path')
const fs = require('fs')
const transformFiles = path => {
const traverse = path => {
try {
fs.readdir(path, (err, files) => {
files.forEach(file => {
const filePath = `${path}/${file}`
fs.stat(filePath, async function (err, stats) {
if (err) {
console.error(chalk.red(` \n?? ~ ${o} Transform File Error:${err}`))
} else {
// 如果是文件則開始執(zhí)行替換規(guī)則
if (stats.isFile()) {
const language = file.split('.').pop()
if (language === 'vue') {
// 處理vue文件內(nèi)容
await transformVue(file, filePath, language)
} else if (jsSuffix.includes(language)) {
// 處理JS文件內(nèi)容
await transformScript(file, filePath, language)
} else if (styleSuffix.includes(language)) {
// 處理樣式文件內(nèi)容
await transformStyle(file, filePath, language)
}
} else {
// 如果是目錄,則繼續(xù)遍歷
traverse(`${path}/${file}`)
}
}
})
})
})
} catch (err) {
console.error(err)
reject(err)
}
}
traverse(path)
}
3.2 Vue 文件的代碼遷移
由于單獨(dú)的 JS、樣式文件處理流程和 Vue 文件相似,唯一的差別在于 Vue 文件多了一層解析。所以這里以 Vue 文件為例,闡述下具體的代碼遷移流程:
const $ = require('gogocode')
const path = require('path')
const fs = require('fs')
// 處理vue文件
const transformVue = async (file, path, language = 'vue') => {
return new Promise((resolve, reject) => {
fs.readFile(path, function read(err, code) {
const sourceCode = code.toString()
// 1. 利用gogocode提供的$方法,將源碼轉(zhuǎn)換為ast語法樹
const ast = $(sourceCode, { parseOptions: { language: 'vue' } })
// 2. 處理script
transformScript(ast)
// 3. 處理template
transformTemplate(ast)
// 4. 處理styles
transformStyles(ast)
// 5. 對(duì)處理過的ast重新生成代碼
const outputCode = ast.root().generate()
// 6. 重新寫入文件
fs.writeFile(path, outputCode, function (err) {
if (err) {
reject(err)
throw err
}
resolve()
})
})
})
}
可以看到,代碼中的 Vue 文件處理流程主要如下:
- 生成 AST 語法樹:利用 GoGoCode 提供的 $ 方法,將源碼轉(zhuǎn)換為 Ast 語法樹,然后將 Ast 語法樹交由不同的處理器來完成語法的匹配和轉(zhuǎn)換。
- 處理 JavaScript:調(diào)用 transformScript 處理 JavaScript 部分。
- 處理 template:調(diào)用 transformTemplate 處理 template(HTML)部分。
- 處理 Styles:調(diào)用 transformStyles 處理樣式部分。
- 對(duì)處理過的 Ast 重新生成代碼:調(diào)用 GoGoCode 提供的 ast.root().generate() 方法,可以由 Ast 重新生成目標(biāo)代碼。
- 重新寫入文件:將生成的目標(biāo)代碼重新寫入文件。
這樣一來,一個(gè) Vue 文件的代碼遷移就結(jié)束了。接下來看看針對(duì)不同的內(nèi)容 JavaScript、HTML、Style,需要如何處理。
3.3 處理 JavaScript 腳本
處理 JavaScript 腳本時(shí),主要是依賴 GoGoCode 提供的一些語法進(jìn)行代碼遷移:
- 首先,利用 ast.find('<script></script>') 解析并找到 Vue 文件中的 JavaScript 腳本部分。
- 其次,利用 replace 等方法對(duì)原代碼進(jìn)行一個(gè)匹配和替換。
- 最后,返回處理后的 Ast。
下面是一個(gè)簡(jiǎn)單的代碼替換案例,主要目的是將一個(gè) import 語句替換引用包的來源。將
@vivo/v-jsbridge 替換為@webf/webf-vue-render/modules/jsb。
// 轉(zhuǎn)換前
import { call } from '@vivo/v-jsbridge'
// 轉(zhuǎn)換后
import { call } from '@webf/webf-vue-render/modules/jsb'
const transformScript = (ast) => {
const script = ast.find('<script></script>')
// 利用replace方法替換代碼
script.replace(
`import {$$$} from "@vivo/v-jsbridge"`,
`import {$$$} from "@webf/webf-vue-render/modules/jsb"`
)
return ast
}
除了 replace 之外,還有一些別的語法也經(jīng)常用到,例如:
- find(查找代碼)
- has(判斷是否包含代碼)
- append(在后面插入代碼)
- prepend(在前面插入代碼)
- remove(刪除代碼)等。
只要簡(jiǎn)單熟悉下 GoGoCode 的語法,就能很快寫出一些轉(zhuǎn)換規(guī)則(相比于正則表達(dá)式,效率高很多)。
3.4 處理 Template 模板
處理 Template 模板時(shí)也是類似,主要依賴 GoGoCode 提供的 API:
- 首先,利用 ast.find('<template></template>') 解析并找到 Vue 文件中的 Template(HTML)部分。
- 其次,利用 replace 等方法對(duì)原代碼進(jìn)行一個(gè)匹配和替換。
- 最后,返回處理后的 Ast 。
下面是簡(jiǎn)單的 Template 標(biāo)簽替換案例,將帶有 @change 屬性的 div 標(biāo)簽,替換為帶有 :onChange 屬性的 span 標(biāo)簽。
// 轉(zhuǎn)換前
<div @change="onChange"></div>
// 轉(zhuǎn)換后
<span :notallow="onChange"></div>
const transformTemplate = (ast) => {
const template = ast.find('<template></template>')
const tagName = 'div'
const targetTagName = 'span'
// 利用replace方法替換代碼,將div標(biāo)簽替換為span標(biāo)簽
template
.replace(
`<${tagName} @change="$_$" $$$1>$$$0</${tagName}>`,
`<${targetTagName} :notallow="$_$" $$$1>$$$0</${targetTagName}>`
)
return ast
}
值得一提的是,GoGoCode 提供了 $_$、$$$1 這類通配符,讓我們能對(duì) DOM 結(jié)構(gòu)進(jìn)行更好的規(guī)則匹配,高效地寫出匹配和轉(zhuǎn)換規(guī)則。
3.5 處理 Style 樣式
最后是處理 Style 樣式部分,這部分和處理 JavaScript、Template 有些不同,由于 GoGoCode 暫時(shí)沒有提供針對(duì) Style 樣式的處理方法。所以我們需要額外借用兩個(gè)工具,分別是:
- @vue/component-compiler-utils:解析樣式代碼,轉(zhuǎn)成 Ast 。
- PostCSS:按規(guī)則處理樣式,轉(zhuǎn)換成目標(biāo)代碼。
整個(gè) Style 樣式的處理流程如下(寫過 PostCSS 插件的同學(xué)對(duì)這樣式處理這部分應(yīng)該很熟悉):
- 獲取 Styles 節(jié)點(diǎn):利用 ast.rootNode.value.styles 獲取 Styles 節(jié)點(diǎn),一個(gè) Vue 文件中可能包含多個(gè) Style 代碼塊,對(duì)應(yīng)多個(gè) Styles 節(jié)點(diǎn)。
- 遍歷 Styles 節(jié)點(diǎn):
- 利用 @vue/component-compiler-utils 提供的 compileStyle 方法解析 Style 節(jié)點(diǎn)內(nèi)容。
- 利用 postcss.process 方法,根據(jù)規(guī)則處理樣式內(nèi)容,生成目標(biāo)代碼。
- 返回轉(zhuǎn)換后的 Ast 。
下面是一個(gè)簡(jiǎn)單的案例,將樣式中所有的 color 屬性值統(tǒng)一替換為 red。
// 轉(zhuǎn)換前
<style>
.button {
color: blue;
}
</style>
// 轉(zhuǎn)換后
<style>
.button {
color: red;
}
</style>
const compiler = require('@vue/component-compiler-utils')
const { parse, compileStyle } = compiler
const postcss = require('postcss')
// 一個(gè)簡(jiǎn)單的替換所有color屬性為'red'的postcss插件
const colorPlugin = (opts = {}) => {
return {
postcssPlugin: 'postcss-color',
Once(root, { result }) {
root.walkDecls(node => {
// 找到所有prop為color的節(jié)點(diǎn),將節(jié)點(diǎn)的值設(shè)置為red
if (node.prop === 'color') {
node.value = 'red'
}
})
}
}
}
colorPlugin.postcss = true
const transformStyles = (ast) => {
// 獲取styles節(jié)點(diǎn)(一個(gè)vue文件中可能包含多個(gè)style代碼塊,對(duì)應(yīng)多個(gè)styles節(jié)點(diǎn))
const styles = ast.rootNode.value.styles
// 遍歷所有styles節(jié)點(diǎn)
styles.forEach((style, index) => {
let content = style.content
// 獲取文件的后綴:less / sass / scss / css等
const lang = style.lang || 'css'
// 利用@vue/component-compiler-utils提供的compileStyle方法解析style內(nèi)容
const result = compileStyle({
source: content,
scoped: false
})
// 交由postcss處理樣式,傳入剛剛聲明的colorPlugin插件
const res = postcss([colorPlugin]).process(result.code, { from: path, syntax: parsers[lang] })
style.content = res.css
})
return ast
}
到此,整個(gè)代碼遷移流程就完成了。
【源碼 DEMO 可參考】https://github.com/vivo/BlueSnippets/tree/main/demos/ast-migration
四、總結(jié)
本文簡(jiǎn)單闡述了基于 AST 的代碼遷移概念和大致流程,并通過代碼案例帶大家了解到了其中的處理細(xì)節(jié)。梳理下整個(gè)代碼遷移處理流程:
- 遍歷和讀取文件內(nèi)容。
- 將內(nèi)容分類,針對(duì)不同文件內(nèi)容,采用不同的處理器。
- JavaScript 腳本直接交由 GoGoCode 處理。
- 樣式文件直接交由 PostCSS 處理。
- 針對(duì) Vue 文件,通過解析,拆分成 Template、JavaScript、Style 樣式三部分,再進(jìn)行分別處理。
- 處理完成后,基于轉(zhuǎn)換后的 Ast 生成目標(biāo)代碼并重新寫入文件,完成代碼遷移。
整個(gè)處理流程還是相對(duì)簡(jiǎn)單的,只需要掌握 AST 代碼轉(zhuǎn)換的基本概念,對(duì) GoGoCode、PostCSS、
@vue/component-compiler-utils 這些工具的基本使用有一定的了解,就可以進(jìn)行一個(gè)自動(dòng)化遷移工具的開發(fā)了。
最后,需要額外提醒大家的一點(diǎn)是,在設(shè)計(jì)代碼匹配和轉(zhuǎn)換規(guī)則時(shí),需要注意邊界場(chǎng)景,避免產(chǎn)生錯(cuò)誤的代碼轉(zhuǎn)換,從而造成潛在 bug。為了規(guī)避代碼轉(zhuǎn)換異常,建議大家針對(duì)每個(gè)轉(zhuǎn)換規(guī)則編寫充分的測(cè)試用例,以保障轉(zhuǎn)換規(guī)則的正確性。
如果大家有類似的需求,也可以參照本文進(jìn)行工具設(shè)計(jì)和實(shí)現(xiàn)。