前景回顧
客戶端視角下的babel(理論篇)介紹了AST(抽象語(yǔ)法樹)的概念,以及Babel作為JavaScript編譯器的運(yùn)作原理。我們深入講解了Babel的各個(gè)階段,詳細(xì)介紹了它們所具備的基礎(chǔ)概念及相應(yīng)的功能。最后,我們闡述了Babel插件如何對(duì)AST節(jié)點(diǎn)進(jìn)行操作和轉(zhuǎn)換的。與上篇理論相比,本文將圍繞TN項(xiàng)目中的一些具體案例,重點(diǎn)介紹Babel在實(shí)踐應(yīng)用中的功能和用途。
總覽
在正式講述具體項(xiàng)目前,我們還是按照慣例先提幾個(gè)問題(帶著問題感觸更深:)
TN從Source Code到AST經(jīng)歷了哪些階段?
這些階段TN處理了哪些事情,遇到了哪些難題,并如何解決的?
問題1:

TN會(huì)在編譯期間通過Babel將Source Code轉(zhuǎn)成AST。其中核心的兩個(gè)階段:parse、transform
transform:對(duì)parse階段生成的AST進(jìn)行遍歷。在此過程中對(duì)節(jié)點(diǎn)進(jìn)行添加、更新及移除等操作
細(xì)節(jié)的內(nèi)容這邊就不一一展開了,客戶端視角下的babel(理論篇)講解的非常詳細(xì)了
問題2:
TN任務(wù)的處理主要在transform階段。在該階段我們具體處理了哪些事項(xiàng)呢?
支持高級(jí)語(yǔ)法:比如Enum(JavaScript中是沒有枚舉類型的)
支持語(yǔ)法降級(jí):比如foreach降級(jí)到for
移除換行符多出來(lái)的JSXText節(jié)點(diǎn)
多文件依賴導(dǎo)入
變量名沖突
函數(shù)聲明提升到作用域的頂部
...

針對(duì)不同種類的事項(xiàng),我們可以創(chuàng)建不同的業(yè)務(wù)插件,來(lái)對(duì)節(jié)點(diǎn)進(jìn)行添加、更新、移除等操作,從而達(dá)到我們的訴求。
當(dāng)然,期間也遇到一些難點(diǎn):比如多文件依賴如何有序?qū)?、變量名沖突如何解決;對(duì)于換行符多出來(lái)的JSXText節(jié)點(diǎn),我們?nèi)绾翁幚?,并保證處理的結(jié)果和SolidJS是一致的等等。
下面我們將講述具體的幾個(gè)事項(xiàng),來(lái)幫助我們更好的了解TN的處理機(jī)制,以及難點(diǎn)問題的解決方式。
實(shí)踐
前景聲明
實(shí)踐項(xiàng)目中會(huì)以SolidJS的表現(xiàn)作為依據(jù)去處理節(jié)點(diǎn)。為何是SolidJS而非React等其他前端框架呢?歸根結(jié)底取決于TN的愿景:極致的渲染速度+開發(fā)體驗(yàn)效率。
下面會(huì)簡(jiǎn)單說明為何使用SolidJS作為首選的前端開發(fā)框架,更加詳細(xì)的內(nèi)容可參考
開發(fā)體驗(yàn)和效率
邏輯表達(dá)式 VS React
語(yǔ)法與React相似,實(shí)現(xiàn)與Composition API相似
useState -> createSignal
useMemo -> createMemo
useEffect -> createEffect
useLayoutEffect -> createRenderEffect
PS:Composition API 是 Vue 3 新增的 API,旨在解決 Vue 2 中大型組件的可讀性、代碼重用和類型安全等問題。Composition API 的核心思想是將組件的邏輯按照功能劃分為不同的邏輯部分(composition),然后通過組合這些邏輯部分來(lái)創(chuàng)建復(fù)雜的組件邏輯。
依賴追蹤 VS Vue3
Solid和Vue3相似,定義響應(yīng)式數(shù)據(jù),添加副作用函數(shù),只不過Solid是讀寫分離的。
Solid 是一款基于 JavaScript 的 UI 庫(kù),它的讀寫分離是指對(duì) UI 組件狀態(tài)進(jìn)行分離,將其分為可讀狀態(tài)和可寫狀態(tài)。具體來(lái)說,Solid 將各個(gè) UI 組件的狀態(tài)分解為兩部分:
- 可讀狀態(tài):指 UI 組件的非響應(yīng)式數(shù)據(jù)和方法,即只能被訪問而不能進(jìn)行修改的狀態(tài)。這些狀態(tài)的訪問不會(huì)導(dǎo)致 UI 重新渲染。Solid 通過將這些狀態(tài)管理在普通 JavaScript 對(duì)象中,來(lái)實(shí)現(xiàn)它們的可訪問性。
- 可寫狀態(tài):指 UI 組件的響應(yīng)式數(shù)據(jù)和方法,即在修改狀態(tài)時(shí)會(huì)導(dǎo)致 UI 重新渲染的狀態(tài)。Solid 通過將這些狀態(tài)管理在響應(yīng)式數(shù)據(jù)流中,來(lái)實(shí)現(xiàn)它們的響應(yīng)式。
基于編譯的運(yùn)行時(shí)優(yōu)化
在React與Vue中存在一層「虛擬DOM」(React中叫Fiber樹)。
每當(dāng)發(fā)生更新,「虛擬DOM」會(huì)進(jìn)行比較(Diff算法),比較的結(jié)果會(huì)執(zhí)行不同的DOM操作(增、刪、改)。
而SolidJS在發(fā)生更新時(shí),可以直接調(diào)用編譯好的DOM操作方法,省去了「虛擬DOM比較」這一步所消耗的時(shí)間。
enum轉(zhuǎn)換
enum 是 TypeScript 中的一個(gè)特性,它是一種枚舉類型,用于定義具有固定值的常量集合。而在 JavaScript 中,目前并沒有原生的 enum 類型。
當(dāng)你定義了一個(gè)枚舉
enum Fruit {
Apple = 0,
Banner = 1
}
其被編譯成 JavaScript 后如下所示:
var Fruit = /*#__PURE__*/function (Fruit) {
Fruit[Fruit["Apple"] = 0] = "Apple";
Fruit[Fruit["Banner"] = 1] = "Banner";
return Fruit;
}(Fruit || {});
對(duì)應(yīng)的AST如下:
{
"type":"VariableDeclaration",
"kind":"var",
"declarations":[
{
"type":"VariableDeclarator",
"id":{
...
"name":"Fruit"
},
"init":{
"type":"CallExpression",
"callee":{
"type":"FunctionExpression",
...
"body":{
"type":"BlockStatement",
"body":[
{
"type":"ExpressionStatement",
"expression":{
"type":"AssignmentExpression",
"operator":"=",
"left":{
"type":"MemberExpression",
...
"property":{
"type":"AssignmentExpression",
"operator":"=",
"left":{
"type":"MemberExpression",
...
"property":{
"type":"StringLiteral",
"value":"Apple"
},
},
"right":{
"type":"NumericLiteral",
"value":0
}
},
}
}
},
{
"type":"ExpressionStatement",
"expression":{
"type":"AssignmentExpression",
"operator":"=",
"left":{
"type":"MemberExpression",
...
},
"property":{
"type":"AssignmentExpression",
"operator":"=",
"left":{
"type":"MemberExpression",
...
"property":{
"type":"StringLiteral",
"value":"Banner"
},
},
"right":{
"type":"NumericLiteral",
"value":1
}
},
}
}
},
...
]
},
...
},
...
}
}
],
...
}
拿到上述AST后,我們發(fā)現(xiàn)一個(gè)簡(jiǎn)單的枚舉被paser后變得極其復(fù)雜,而且可讀性極差。那么是否有辦法可以將其處理的精簡(jiǎn)并且可讀化呢?答案是肯定的:通過構(gòu)建TSEnumDeclaration的轉(zhuǎn)換插件就可以完成目標(biāo)
visitor: {
VariableDeclaration(path) {
if (path.node.type === 'VariableDeclaration' && (path.node.kind === 'var' || path.node.kind === 'let') && path.node.declarations.length === 1) {
const declarationNode = path.node.declarations[0];
if (declarationNode.type === 'VariableDeclarator' && declarationNode.init?.type === 'CallExpression' && declarationNode.init?.callee?.type === 'FunctionExpression') {
const enumName = declarationNode.id.name;
const entries = [];
declarationNode.init.callee?.body?.body.filter(function(propertyNode) {
return propertyNode.type === 'ExpressionStatement';
}).forEach(propertyNode => {
// enum Fruit {
// Apple = "apple",
// Banana = "banana",
// Orange = "orange"
// }
// enum Fruit {
// Apple,
// Banana,
// Orange
// }
// enum構(gòu)建
const memberName = propertyNode.expression.left.property.left?.property.value || propertyNode.expression.left.property.value;
// initial value 處理
const rightValue = propertyNode.expression.right.value;
const leftValue = propertyNode.expression.left.property.right?.value;
const memberValue = typeof leftValue === 'undefined' || leftValue === null || leftValue === '' ? rightValue : leftValue;
entries.push(
tsEnumMember(identifier(memberName), typeof memberValue === 'string' ? stringLiteral(memberValue) : numericLiteral(memberValue))
);
});
// TSEnumDeclaration
const tsEnumDeclWithComment = tsEnumDeclaration(identifier(enumName), entries);
...
}
}
}
}
PS: 節(jié)點(diǎn)類型的判斷可以根據(jù)type值判斷,也可以根據(jù)@babel/types定義的isX進(jìn)行判斷,兩者等價(jià)
上述AST節(jié)點(diǎn)經(jīng)轉(zhuǎn)換后,我們就得到了預(yù)期的結(jié)果:
{
type: 'TSEnumDeclaration',
id: { type: 'Identifier', name: 'Fruit' },
members: [
{ type: 'TSEnumMember', id: [Object], initializer: [Object] },
{ type: 'TSEnumMember', id: [Object], initializer: [Object] }
]
}
{"type":"TSEnumDeclaration","id":{"type":"Identifier","name":"Fruit"},"members":[{"type":"TSEnumMember","id":{"type":"Identifier","name":"Apple"},"initializer":{"type":"NumericLiteral","value":0}},{"type":"TSEnumMember","id":{"type":"Identifier","name":"Banner"},"initializer":{"type":"NumericLiteral","value":1}}]}
換行符處理
我們通過幾個(gè)例子看下Babel對(duì)于換行符是如何處理的(不添加插件默認(rèn)輸出的結(jié)果)
- Case1:文本標(biāo)簽換行,換行符會(huì)包含在文本內(nèi)容中(eg:文本數(shù)據(jù)前后包含上下的換行符及前置的空格符)
<text>
文本數(shù)據(jù)
</text>
"children": [
{
"type": "JSXText",
"value": "\n 文本數(shù)據(jù)\n",
"raw": "\n 文本數(shù)據(jù)\n"
}
]
- Case2:標(biāo)簽換行會(huì)生產(chǎn)多余的JSXText節(jié)點(diǎn)(eg:view標(biāo)簽換行)
<view>
<text>
文本數(shù)據(jù)1
</text>
</view>
"children": [
{
"type": "JSXText",
"value": "\n\t",
"raw": "\n\t"
},
{
...
"children": [
{
"type": "JSXText",
"value": "\n \t\t文本數(shù)據(jù)1\n\t",
"raw": "\n \t\t文本數(shù)據(jù)1\n\t"
}
]
},
{
"type": "JSXText",
"value": "\n",
"raw": "\n"
}
],
通過上述case,我們大概發(fā)現(xiàn)了一個(gè)結(jié)論:如果在原始的JSX代碼中有多余的換行符,則Babel將這些換行符解釋為一個(gè)JSXText元素,這樣可能會(huì)導(dǎo)致最終渲染出來(lái)的DOM樹中多出一些空行節(jié)點(diǎn)。
那我們看下React是如何處理空行符的?

通過上述demo我們非常清晰看到,首先標(biāo)簽換行并不會(huì)產(chǎn)生多余的換行符;文本內(nèi)容如果需要換行則需要{'\n'}表示,否則換行效果只是多一個(gè)空格;同一行文本內(nèi)部有多個(gè)空格,則顯示多個(gè)空格,同一行文本的首尾空格不會(huì)顯示。
看完React,我們了解下SolidJS是怎么樣的一個(gè)現(xiàn)象

SolidJS和React處理類似,區(qū)別點(diǎn)在于:兩個(gè)文本間多個(gè)空格符會(huì)合并成單個(gè);換行符會(huì)轉(zhuǎn)換成單空格。
基于SolidJS對(duì)空行符的處理機(jī)制,處理JSX空行符的業(yè)務(wù)插件具體實(shí)現(xiàn)就很明確了
visitor: {
JSXText(path) {
// 獲取當(dāng)前節(jié)點(diǎn)在父節(jié)點(diǎn)中的前一個(gè)相鄰節(jié)點(diǎn)
const prevSibling = path.getSibling(path.key - 1)
// 獲取當(dāng)前節(jié)點(diǎn)在父節(jié)點(diǎn)中的下一個(gè)相鄰節(jié)點(diǎn)
const nextSibling = path.getSibling(path.key + 1)
// 判斷該節(jié)點(diǎn)是否只包含空白字符或換行符
if (path.node.value.trim() === '') {
if (!prevSibling.node) {
path.remove()
} else if (prevSibling.node.type === "JSXElement") {
path.remove()
} else if (!nextSibling.node) { // 末節(jié)點(diǎn)
path.remove()
} else if (prevSibling.node.type === 'JSXExpressionContainer') { // {count()}
const text = path.node.value.replace(/\s+/g, " ") // 前置后置中間空格>=1替換為一個(gè)空格
path.node.value = text
}
} else { // 文本中存在多個(gè)空格、換行,變成單個(gè)空格
var text = path.node.value
if (!prevSibling.node && !nextSibling.node) { // 單一節(jié)點(diǎn)
text = text.trim() // 去除首尾空格
text = text.replace(/\s+/g, " ") // 前置后置中間空格>=1替換為一個(gè)空格
} else if (!prevSibling.node) { // 首節(jié)點(diǎn)
text = text.trimStart() // 去除前置空格
text = text.replace(/\s+/g, " ") // 前置后置中間空格>=1替換為一個(gè)空格
} else if (!nextSibling.node) { // 末節(jié)點(diǎn)
text = text.replace(/\s+/g, " ") // 前置后置中間空格>=1替換為一個(gè)空格
text = text.trimEnd() // 去除后置空格
} else {
text = text.replace(/\s+/g, " ") // 前置后置中間空格>=1替換為一個(gè)空格
}
path.node.value = text
}
}
}
基于上述插件,Case1的輸出結(jié)果:
{
"type":"JSXElement",
...
"openingElement":Object{...},
"closingElement":Object{...},
"children":[
{
"type":"JSXText",
...
"value":"文本數(shù)據(jù)"
}]
}
Case2的輸出結(jié)果:
{
"type":"JSXElement",
...
"openingElement":Object{...},
"closingElement":Object{...},
"children":[
{
"type":"JSXElement",
...
"openingElement":Object{...},
"closingElement":Object{...},
"children":[
{
"type":"JSXText",
...
"value":"文本數(shù)據(jù)1"
}
]
}]
}
多import導(dǎo)入解決變量名沖突
TN會(huì)將所有模塊打包成一個(gè)單一的Bundle文件,然后通過包管理下發(fā)到Native側(cè)。所以如何將import到的外部依賴文件導(dǎo)入進(jìn)來(lái),并處理好變量名沖突問題,就成了關(guān)鍵。
說到打包,我們肯定會(huì)想到webpack。那么webpack是如何導(dǎo)入并解決變量名沖突問題的呢?
input:
// index.js
import {targetVersionCompare} from './util.js'
targetVersionCompare('11')
// util.js
import { testValue} from "./utilA.js";
const isEmptyString = (str) =>
typeof str === 'undefined' || str == null || str === '';
const a = "1111"
export const targetVersionCompare = (target) => {
console.log(testValue)
if (isEmptyString(target)) {
return false
}
console.log(a)
return false
}
// utilA.js
export const testValue = 'A'
const a = "xxxx"
console.log(a)
webpack dist:
/******/ (() => { // webpackBootstrap
/******/ "use strict";
var __webpack_exports__ = {};
;// CONCATENATED MODULE: ./utilA.js
const testValue = 'A'
const a = "xxxx"
console.log(a)
;// CONCATENATED MODULE: ./util.js
const isEmptyString = (str) =>
typeof str === 'undefined' || str == null || str === '';
const util_a = "1111"
const targetVersionCompare = (target) => {
console.log(testValue)
if (isEmptyString(target)) {
return false
}
console.log(util_a)
return false
}
;// CONCATENATED MODULE: ./index.jsx
targetVersionCompare('11')
/******/ })()
;
從上述源碼輸入到最后的產(chǎn)物對(duì)比,我們大概了解了webpack處理多文件導(dǎo)入的機(jī)制。
如果導(dǎo)入的文件某個(gè)變量名和當(dāng)前文件變量名沖突,則添加前綴:當(dāng)前文件名變量名序列
變量名a沖突:a->util_a
變量名a沖突,存在變量名util_a:a->util_a_0
變量名a沖突,存在變量名util_a,util_a_0:a->util_a_1
export導(dǎo)出標(biāo)識(shí)會(huì)被remove
加載依賴文件的順序:遞歸導(dǎo)入。當(dāng)A依賴B、A依賴C,B依賴D,則導(dǎo)入的順序依次為D-B-C-A
方案設(shè)計(jì)
webpack的處理機(jī)制我們大概了解了,整體的大致流程是怎么樣的呢?
簡(jiǎn)要流程圖

流程概述
創(chuàng)建remove-export-name-plugin:移除導(dǎo)出標(biāo)識(shí)
創(chuàng)建import-declaration-plugin
遞歸讀取依賴的文件
生成唯一標(biāo)識(shí) By 完整文件路徑
鏈表記錄前后依賴&AST、export數(shù)據(jù)綁定
import導(dǎo)入數(shù)據(jù)處理
反轉(zhuǎn)鏈表
diff
重命名沖突處理
節(jié)點(diǎn)合并
方案實(shí)現(xiàn)
移除export
visitor: {
ExportNamedDeclaration(path) {
const { node } = path
const { declaration, specifiers } = node
if (declaration) {
path.replaceWith(declaration);
}
},
ExportDefaultDeclaration(path) {
const { node } = path
const { declaration } = node
if (declaration) {
path.replaceWith(declaration);
} else {
path.remove();
}
}
}
import-declaration-plugin
- 遞歸讀取依賴的文件
當(dāng)我們輸入import { testValue} from "./utilA.js"語(yǔ)句的時(shí)候,AST會(huì)生成節(jié)點(diǎn):ImportDeclaration,其導(dǎo)入的文件相對(duì)路徑也會(huì)在source字段下展示。如下所示:

那么如何根據(jù)import的相對(duì)路徑去構(gòu)建完成的文件路徑呢?
首先我們需要明白一點(diǎn):導(dǎo)入文件的相對(duì)路徑只有對(duì)于當(dāng)前文件是透明的,而文件導(dǎo)入依賴是遞歸的,所以我們需要做的就是用一個(gè)棧記錄當(dāng)前import file路徑依賴,導(dǎo)入文件的時(shí)候壓棧,離開文件的時(shí)候出棧。
PS:另外JS在導(dǎo)入文件的時(shí)候,可以選擇性的不寫文件后綴名,需要單獨(dú)處理
const dependFilePathStack = [] // 棧記錄當(dāng)前import file路徑依賴
const constructFile = (importModulePath) => {
const curFilePath = dependFilePathStack.slice(-1)[0] || source // 獲取棧頂元素
var filePath = path.resolve(path.dirname(curFilePath), importModulePath)
const dirPath = path.resolve(filePath, '..'); // 獲取上一級(jí)目錄
if (path.extname(filePath)) { // 當(dāng)前文件路徑存在后綴
return filePath
}
// 獲取目錄下所有文件
const fileNames = fs.readdirSync(dirPath)
// 獲取文件名
const basename = path.basename(filePath);
// 查找指定文件名的后綴名(兼容.d.ts聲明文件)
const targetFileName = fileNames.find(fullFileName => {
return path.basename(fullFileName, path.extname(fullFileName)) === basename || path.basename(fullFileName, '.d.ts') === basename;
})
// 組合文件路徑和后綴名
return targetFileName ? `${dirPath}/${targetFileName}` : ''
}
- 生成唯一標(biāo)識(shí) By 完整文件路徑
tn處理重命名沖突規(guī)則和webpack稍微有點(diǎn)差異。差異點(diǎn):webpack可以理解為在"運(yùn)行時(shí)"去處理重命名沖突,即后置判斷;而tn為了簡(jiǎn)化邏輯,會(huì)在“編譯時(shí)”即前置階段直接生成一個(gè)唯一標(biāo)識(shí),當(dāng)命名沖突的時(shí)候,直接使用唯一標(biāo)識(shí)去替換。
- 鏈表記錄前后依賴&AST、export數(shù)據(jù)綁定
import文件導(dǎo)入是遞歸讀取的,我們可以使用鏈表來(lái)存儲(chǔ)文件之間的依賴關(guān)系。節(jié)點(diǎn)的定義如下:next 表示當(dāng)前文件導(dǎo)入的下一個(gè)依賴文件,nameSpace 表示當(dāng)前文件的唯一標(biāo)識(shí),nodes 表示當(dāng)前文件的 AST節(jié)點(diǎn)集合,exported 表示當(dāng)前文件導(dǎo)出的集合。
class ImportNode {
constructor(next, nameSpace, nodes, exported) {
this.next = next
this.nameSpace = nameSpace
this.nodes = nodes
this.exported = exported
}
}
遞歸讀取文件的同時(shí),記錄對(duì)應(yīng)ImportNode的AST及exported相關(guān)數(shù)據(jù)
// 記錄import依賴
const lasted = findLastedNode(path)
if (!lasted) {
// 綁定Head Node
const headNode = new ImportNode(null, nameSpace, null, null)
importHeadNode = headNode
} else {
lasted.next = new ImportNode(null, nameSpace, null, null)
}
// 獲取ast節(jié)點(diǎn)集合
const importModuleAst = processedFilePaths[fullFilePath] || generateToAst(fullFilePath)
processedFilePaths[fullFilePath] = importModuleAst
const astBody = importModuleAst.program.body
// 綁定nodes
const target = findTargetNode(nameSpace)
target.nodes = astBody
// 綁定exported
target.exported = collectExportAs(astBody)
import-declaration-plugin組件大致實(shí)現(xiàn)如下:
var importHeadNode = null
const dependFilePathStack = [] // 棧記錄當(dāng)前import file路徑依賴
...
visitor: {
ImportDeclaration(path) {
// 1.獲取當(dāng)前import module path
// 2.獲取導(dǎo)入模塊的代碼并解析成 AST
const importModulePath = path.get('source').node.value
const fullFilePath = constructFile(importModulePath)
if (!fullFilePath || processedFilePaths[fullFilePath]) {
return;
}
// 構(gòu)建namespace
const nameSpace = constructNameSpace(fullFilePath)
// 記錄import依賴
const lasted = findLastedNode(path)
if (!lasted) {
// 綁定Head Node
const headNode = new ImportNode(null, nameSpace, null, null)
importHeadNode = headNode
...
} else {
lasted.next = new ImportNode(null, nameSpace, null, null)
}
dependFilePathStack.push(fullFilePath)
// traverseImportHead(importHeadNode)
const importModuleAst = processedFilePaths[fullFilePath] || generateToAst(fullFilePath)
processedFilePaths[fullFilePath] = importModuleAst
dependFilePathStack.pop()
// 導(dǎo)入文件的body
const astBody = importModuleAst.program.body
// 綁定nodes
const target = findTargetNode(nameSpace)
target.nodes = astBody
// 綁定exported
target.exported = collectExportAs(astBody)
}
}
import導(dǎo)入數(shù)據(jù)處理
- 反轉(zhuǎn)鏈表
- tn和webpack對(duì)于依賴的文件導(dǎo)入順序是一致的,比如A依賴B、A依賴C,B依賴D,則導(dǎo)入的順序依次為D-B-C-A。因此我們需要反轉(zhuǎn)鏈表,從鏈表的尾部節(jié)點(diǎn)倒序讀入
const reverseLinkList = (importHeadNode) => {
const reverseList = []
var head = importHeadNode
while (head) {
reverseList.push(head)
head = head.next
}
return reverseList.reverse()
}
- diff
diff是重命名沖突的核心函數(shù)。當(dāng)import文件導(dǎo)入時(shí),有可能導(dǎo)入的Identifier與當(dāng)前文件存在沖突。大致處理的規(guī)則如下:
獲取當(dāng)前import文件內(nèi)容中聲明的所有names
當(dāng)前node與導(dǎo)入的前幾個(gè)文件diff
讀取下一個(gè)節(jié)點(diǎn),重復(fù)1,2兩個(gè)步驟
// 1\. 獲取當(dāng)前import文件內(nèi)容中聲明的所有names 2\. 當(dāng)前node與導(dǎo)入的前幾個(gè)文件diff
const dealImportDependies = (reverseImportList) => {
var importNodes = []
for (let index = 0; index < reverseImportList.length; index++) {
const node = reverseImportList[index]
// diff前 (原始聲明)
const beforeDiffDeclarationNames = collectNodeDeclarationName(node)
// diff
diffImportFileDeclaration(importDeclarationNames(), node)
importNodes = [...importNodes, ...node.nodes]
// console.log(JSON.stringify(node))
// diff后(替換后的聲明):部分聲明diff后會(huì)重新替換
const afterDeclarationNames = collectNodeDeclarationName(node)
// 根據(jù)前后diff生成對(duì)照表
const nameUnion = declarationNameSpaceUnion(beforeDiffDeclarationNames, afterDeclarationNames)
declarationNameTable[node.nameSpace] = nameUnion
// 綁定export as
exportedAsDeclarationTable[node.nameSpace] = isNonEmptyObj(node.exported) ? node.exported : null
}
return importNodes
}
后續(xù)的node與前面import過的聲明集合進(jìn)行diff
/**
* dfs 節(jié)點(diǎn)下的葉子節(jié)點(diǎn)聲明
* @param {*} input node
* @param {*} baseUnion 前置導(dǎo)入的file declaration name 集合
* @param {*} nameSpace 當(dāng)前文件的namespace
* @param {*} importDeclarationNames 當(dāng)前文件導(dǎo)入的import聲明 import {A,B} from 'xxxx'
* @returns
*/
function dfsNodeDeclaration(input, baseUnion, nameSpace, importDeclarationNames) {
if(!input){
return
}
if(Array.isArray(input)){
for(let i = 0; i < input.length; i++){
dfsNodeDeclaration(input[i], baseUnion, nameSpace, importDeclarationNames)
}
}
if(input.type === 'FunctionDeclaration'){ // done
dfsNodeDeclaration(input.id, baseUnion, nameSpace, importDeclarationNames)
dfsNodeDeclaration(input.body, baseUnion, nameSpace, importDeclarationNames)
}
...
else if(input.type === 'Identifier' || input.type === 'JSXIdentifier'){
dealIdentifier(input, baseUnion, nameSpace, importDeclarationNames)
} else if (input.type === 'JSXExpressionContainer') {
dfsNodeDeclaration(input.expression, baseUnion, nameSpace, importDeclarationNames)
}
...
else {
return
}
}
- 重命名沖突處理
首先我們梳理下可能存在的幾種情況。
比如文件
// utilB
// utilB:utilB_Test
function utilB() {
}
export { utilB as utilB_Test }
// util
// utilB_Test:testB
import { utilB_Test as testB} from "./utilB.js";
testB()
// 最終映射鏈
testB()->testB->utilB_Test->utilB->declarationNameTable查找
為何還需declarationNameTable查找,因?yàn)閡tilB可能因?yàn)槊麤_突已改成了utilB_utilB
/**
* 重名的變量使用內(nèi)部的還是外部的
* 具體規(guī)則:如果命中重命名沖突,1.使用的Identifier在import中是否聲明,若聲明則使用該聲明的 2.使用當(dāng)前文件的namespace去更換,避免重命名
* @param {*} node
* @param {*} baseUnion
* @param {*} namespace
* @param {*} importDeclarationNames
*/
const dealIdentifier = (node, baseUnion, nameSpace, importDeclarationNames) => {
const name = node.name
// find -> replace
var declarationName = findImportedDeclarationName(importDeclarationNames, name)
if (declarationName) { // 使用外部聲明
// find export as別名處理
const exportedAsName = findExportedAsDeclarationName(name)
if (exportedAsName) {
declarationName = exportedAsName
}
const updatedDeclarationName = exportDeclarationName(declarationName)
node.name = updatedDeclarationName
return
}
if (baseUnion.includes(name)) { // 其他文件聲明與當(dāng)前的node沖突
// 內(nèi)部聲明
node.name = nameSpace+'_'+name
}
}
總結(jié)
通過上述TN的一些實(shí)踐項(xiàng)目,想必你對(duì)Babel的理解更加深刻了吧
總的來(lái)說,Babel對(duì)于現(xiàn)代前端開發(fā)來(lái)說是一個(gè)很重要的工具。通過Babel,我們可以使用最新的JavaScript特性,同時(shí)保證代碼能夠在較老的瀏覽器或運(yùn)行環(huán)境中運(yùn)行。在實(shí)踐中,Babel的應(yīng)用范圍非常廣泛,包括但不限于:
在Node.js和瀏覽器環(huán)境中使用;
在Webpack等打包工具中使用,將ES6/ES7代碼轉(zhuǎn)換為ES5代碼;
在React.js和Vue.js等框架中使用,通過相應(yīng)的插件和預(yù)設(shè)來(lái)優(yōu)化編譯結(jié)果;
實(shí)現(xiàn)一碼多投,讓應(yīng)用程序能夠在多個(gè)平臺(tái)上運(yùn)行。