tn babel實(shí)戰(zhàn)篇

前景回顧

客戶端視角下的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è)問題(帶著問題感觸更深:)

  1. TN從Source Code到AST經(jīng)歷了哪些階段?

  2. 這些階段TN處理了哪些事情,遇到了哪些難題,并如何解決的?

問題1:

image.png

TN會(huì)在編譯期間通過Babel將Source Code轉(zhuǎn)成AST。其中核心的兩個(gè)階段:parse、transform

  • parse:對(duì)Source Code進(jìn)行詞法分析、語(yǔ)法分析生成AST

  • transform:對(duì)parse階段生成的AST進(jìn)行遍歷。在此過程中對(duì)節(jié)點(diǎn)進(jìn)行添加、更新及移除等操作

細(xì)節(jié)的內(nèi)容這邊就不一一展開了,客戶端視角下的babel(理論篇)講解的非常詳細(xì)了

問題2:

TN任務(wù)的處理主要在transform階段。在該階段我們具體處理了哪些事項(xiàng)呢?

  1. 支持高級(jí)語(yǔ)法:比如Enum(JavaScript中是沒有枚舉類型的)

  2. 支持語(yǔ)法降級(jí):比如foreach降級(jí)到for

  3. 移除換行符多出來(lái)的JSXText節(jié)點(diǎn)

  4. 多文件依賴導(dǎo)入

  5. 變量名沖突

  6. 函數(shù)聲明提升到作用域的頂部

  7. ...

image.png

針對(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)容可參考

https://graffersid.com/solidjs-vs-react-which-is-better/

開發(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是如何處理空行符的?

image.png

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

看完React,我們了解下SolidJS是怎么樣的一個(gè)現(xiàn)象

image.png

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ī)制。

  1. 如果導(dǎo)入的文件某個(gè)變量名和當(dāng)前文件變量名沖突,則添加前綴:當(dāng)前文件名變量名序列

  2. 變量名a沖突:a->util_a

  3. 變量名a沖突,存在變量名util_a:a->util_a_0

  4. 變量名a沖突,存在變量名util_a,util_a_0:a->util_a_1

  5. export導(dǎo)出標(biāo)識(shí)會(huì)被remove

  6. 加載依賴文件的順序:遞歸導(dǎo)入。當(dāng)A依賴B、A依賴C,B依賴D,則導(dǎo)入的順序依次為D-B-C-A

方案設(shè)計(jì)

webpack的處理機(jī)制我們大概了解了,整體的大致流程是怎么樣的呢?

簡(jiǎn)要流程圖

image.png

流程概述

  1. 創(chuàng)建remove-export-name-plugin:移除導(dǎo)出標(biāo)識(shí)

  2. 創(chuàng)建import-declaration-plugin

  3. 遞歸讀取依賴的文件

  4. 生成唯一標(biāo)識(shí) By 完整文件路徑

  5. 鏈表記錄前后依賴&AST、export數(shù)據(jù)綁定

  6. import導(dǎo)入數(shù)據(jù)處理

  7. 反轉(zhuǎn)鏈表

  8. diff

  9. 重命名沖突處理

  10. 節(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字段下展示。如下所示:

image.png

那么如何根據(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)鏈表
  1. 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ī)則如下:

  1. 獲取當(dāng)前import文件內(nèi)容中聲明的所有names

  2. 當(dāng)前node與導(dǎo)入的前幾個(gè)文件diff

  3. 讀取下一個(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)行。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容