AST、Babel、依賴 babel 與 AST 把 let 變成 var 將代碼轉爲 ES5 分析 index.js 的依賴 遞歸地分析嵌套依賴 在複雜一點:循環依賴

babel 與 AST

先從Babel 說起

  • babel 的原理
    1、parse:把代碼 code 變成 AST
    2、traverse:遍歷 AST 進行修改
    3、generate:把 AST 變成代碼 code2
    即:code -- (1) - > ast -- (2) - > ast2 -- (3) - > code2

示例

import { parse } from '@babel/parser'
import traverse from '@babel/traverse'
import generator from '@babel/generator'

const code = `let a = 'a'; let b = 'b'`
const ast = parse(code, { sourceType: 'module' })
console.log(ast)

運行 node -r ts-node/register --inspect-brk let_to_var.ts,用瀏覽器的控制檯打開(--inspect-brk),點擊 node 圖標開始調試

從上圖的打印出的 ast 對象,我們可以很清晰的從 ast.progarm.body 的第一個看出第一行代碼是一個 VariableDeclaration(type),用到的關鍵字是 let(kind),然後對應的初始值是a(init.value)

把 let 變成 var

import { parse } from '@babel/parser'
import traverse from '@babel/traverse'
import generator from '@babel/generator'

const code = `let a = 'a'; let b = 'b'`
const ast = parse(code, { sourceType: 'module' })
traverse(ast, {
    enter: item => {
        if (item.node.type === 'VariableDeclaration') {
            if (item.node.kind === 'let') {
                item.node.kind = 'var'
            }
        }
    }
})
const result = generator(ast, {}, code)
console.log(result.code)

使用 traverse, generator 就能將 let 轉換成 var

  • 爲什麼必須要用 AST
    1、你很難用正則表達式來替換,正則很容易把 let a = 'a' 變成 var a = 'a'
    2、你需要識別每個單詞的意思,才能做到只修改用於變量聲明的 let
    3、而 AST 能明確的告訴你每個 let 的意思

將代碼轉爲 ES5

我們可以直接使用現成的插件 @babel/core

import { parse } from '@babel/parser';
import * as babel from '@babel/core';

const code = `let a = 'let'; let b = 2; const c = 'c'`
const ast = parse(code, { sourceType: 'module' })
const result = babel.transformFromAstSync(ast, code, {
  presets: ['@babel/preset-env']
})
console.log(result.code)

現在我們已經能得到轉換後的es5代碼了,但是我們一般都是生成單獨的文件
只需要稍微改造一下,引入 fs 模塊,test.js 內容依舊爲 let a = 'let'; let b = 2; const c = 'c'

import { parse } from '@babel/parser';
import * as babel from '@babel/core';
import * as fs from 'fs'

const code = fs.readFileSync('./test.js').toString()
const ast = parse(code, { sourceType: 'module' })
const result = babel.transformFromAstSync(ast, code, {
  presets: ['@babel/preset-env']
})
fs.writeFileSync('./test.es5.js', result.code)

代碼已經移到 test.js 文件裏了
運行 node -r ts-node/register file_to_es5.ts
就會得到 test.es5.js 文件

分析 index.js 的依賴

除了轉換 JS 語法,還能做啥?

用來分析 JS 文件的依賴關係

創建一系列文件

// deps_1.ts
import { parse } from "@babel/parser"
import traverse from "@babel/traverse"
import { readFileSync } from 'fs'
import { resolve, relative, dirname } from 'path';

// 設置根目錄
const projectRoot = resolve(__dirname, 'project_1')
// 類型聲明
type DepRelation = { [key: string]: { deps: string[], code: string } }
// 初始化一個空的 depRelation,用於收集依賴
const depRelation: DepRelation = {}

// 將入口文件的絕對路徑傳入函數,如 D:\demo\fixture_1\index.js
collectCodeAndDeps(resolve(projectRoot, 'index.js'))

console.log(depRelation)
console.log('done')

function collectCodeAndDeps(filepath: string) {
  const key = getProjectPath(filepath) // 文件的項目路徑,如 index.js
  // 獲取文件內容,將內容放至 depRelation
  const code = readFileSync(filepath).toString()
  // 初始化 depRelation[key]
  depRelation[key] = { deps: [], code: code }
  // 將代碼轉爲 AST
  const ast = parse(code, { sourceType: 'module' }) 
  // 分析文件依賴,將內容放至 depRelation
  traverse(ast, {
    enter: path => {
      if (path.node.type === 'ImportDeclaration') {
        // path.node.source.value 往往是一個相對路徑,如 ./a.js,需要先把它轉爲一個絕對路徑
        const depAbsolutePath = resolve(dirname(filepath), path.node.source.value)
        // 然後轉爲項目路徑
        const depProjectPath = getProjectPath(depAbsolutePath)
        // 把依賴寫進 depRelation
        depRelation[key].deps.push(depProjectPath)
      }
    }
  })
}
// 獲取文件相對於根目錄的相對路徑
function getProjectPath(path: string) {
  return relative(projectRoot, path).replace(/\\/g, '/')
}

運行代碼 node -r ts-node/register deps_1.ts

步驟

1.調用 collectCodeAndDeps(index.js)
2.先把 depRelation['index.js'] 初始化爲 { deps: [], code: 'index.js' } (讀取源代碼簡單,直接 fs 模塊讀取就好了,主要是 deps)
3.然後把 index.js 源碼 code 變成 ast(只有轉化爲 ast 我們才知道哪些語句是 import)
4.遍歷 ast,看看 import 了哪些依賴(path.node.type === 'ImportDeclaration'),假設依賴了 a.js 和 b.js
5.把 a.js 和 b.js 寫到 depRelation['index'].deps 裏
6.最終得到的 depRelation 就收集了 index.js 的依賴

啓發:用哈希表來儲存未見依賴

遞歸地分析嵌套依賴

升級:依賴的關係

  • 三層依賴關係
    1、index -> a -> dir/a2 -> dir/dir_in_dir/a3
    2、index -> b -> dir/b2 -> dir/dir_in_dir/b3
  • 思路
    1、collectCodeAndDeps 太長了,縮寫爲 collect
    2、調用 collect('index')
    3、發現依賴 'a.js' 於是調用 collect('a.js')
    4、發現依賴 './dir/a2.js' 於是調用 collect('dir/a2.js')
    5、發現依賴 './dir_in_dir/s3.js' 於是調用 collect('dir/dir_in_dir/s3.js')
    6、沒有更多依賴了, a.js 這條線結束,發現下一個依賴 './b.js'
    7、以此類推,其實就是遞歸
// deps_2.js // 只需要最後多加一句話
import { parse } from "@babel/parser"
import traverse from "@babel/traverse"
import { readFileSync } from 'fs'
import { resolve, relative, dirname } from 'path';

// 設置根目錄
const projectRoot = resolve(__dirname, 'project_2')
// 類型聲明
type DepRelation = { [key: string]: { deps: string[], code: string } }
// 初始化一個空的 depRelation,用於收集依賴
const depRelation: DepRelation = {}

// 將入口文件的絕對路徑傳入函數,如 D:\demo\fixture_1\index.js
collectCodeAndDeps(resolve(projectRoot, 'index.js'))

console.log(depRelation)
console.log('done')

function collectCodeAndDeps(filepath: string) {
  const key = getProjectPath(filepath) // 文件的項目路徑,如 index.js
  // 獲取文件內容,將內容放至 depRelation
  const code = readFileSync(filepath).toString()
  // 初始化 depRelation[key]
  depRelation[key] = { deps: [], code: code }
  // 將代碼轉爲 AST
  const ast = parse(code, { sourceType: 'module' }) 
  // 分析文件依賴,將內容放至 depRelation
  traverse(ast, {
    enter: path => {
      if (path.node.type === 'ImportDeclaration') {
        // path.node.source.value 往往是一個相對路徑,如 ./a.js,需要先把它轉爲一個絕對路徑
        const depAbsolutePath = resolve(dirname(filepath), path.node.source.value)
        // 然後轉爲項目路徑
        const depProjectPath = getProjectPath(depAbsolutePath)
        // 把依賴寫進 depRelation
        depRelation[key].deps.push(depProjectPath)
        collectCodeAndDeps(depAbsolutePath) // 其實就多了這一句話
      }
    }
  })
}
// 獲取文件相對於根目錄的相對路徑
function getProjectPath(path: string) {
  return relative(projectRoot, path).replace(/\\/g, '/')
}

但是遞歸存在 call stack 溢出的風險

在複雜一點:循環依賴

  • 依賴關係
    1、index -> a -> b
    2、index -> b -> a
  • 求值
    1、a.value = b.value + 1
    2、b.value = a.value + 1
    3、神經病.......

這樣子看來 [不能循環依賴]?

但是並不是這樣,只是我們當前栗子確實有問題的,我們需要一些小技巧,是的循環依賴也合法

// deps_4.js // 只需要加上一個判斷
import { parse } from "@babel/parser"
import traverse from "@babel/traverse"
import { readFileSync } from 'fs'
import { resolve, relative, dirname } from 'path';

// 設置根目錄
const projectRoot = resolve(__dirname, 'project_4')
// 類型聲明
type DepRelation = { [key: string]: { deps: string[], code: string } }
// 初始化一個空的 depRelation,用於收集依賴
const depRelation: DepRelation = {}

// 將入口文件的絕對路徑傳入函數,如 D:\demo\fixture_1\index.js
collectCodeAndDeps(resolve(projectRoot, 'index.js'))

console.log(depRelation)
console.log('done')

function collectCodeAndDeps(filepath: string) {
  const key = getProjectPath(filepath) // 文件的項目路徑,如 index.js
  if(Object.keys(depRelation).includes(key)){ // 只需要加上一個判斷
    console.warn(`duplicated dependency: ${key}`) // 注意,重複依賴不一定是循環依賴
    return
  }
  // 獲取文件內容,將內容放至 depRelation
  const code = readFileSync(filepath).toString()
  // 初始化 depRelation[key]
  depRelation[key] = { deps: [], code: code }
  // 將代碼轉爲 AST
  const ast = parse(code, { sourceType: 'module' }) 
  // 分析文件依賴,將內容放至 depRelation
  traverse(ast, {
    enter: path => {
      if (path.node.type === 'ImportDeclaration') {
        // path.node.source.value 往往是一個相對路徑,如 ./a.js,需要先把它轉爲一個絕對路徑
        const depAbsolutePath = resolve(dirname(filepath), path.node.source.value)
        // 然後轉爲項目路徑
        const depProjectPath = getProjectPath(depAbsolutePath)
        // 把依賴寫進 depRelation
        depRelation[key].deps.push(depProjectPath)
        collectCodeAndDeps(depAbsolutePath)
      }
    }
  })
}
// 獲取文件相對於根目錄的相對路徑
function getProjectPath(path: string) {
  return relative(projectRoot, path).replace(/\\/g, '/')
}
  • 避免重複進入同一個文件
  • 思路:
    1、一旦發現這個 key 已經在 keys 裏了,就 return
    2、這樣分析過程就不是 a -> b -> a -> b -> ...,而是 a -> b -> return
    3、注意我們只需要分析依賴,不需要執行代碼,所以這樣子是可行的
    4、由於我們的分析不需要執行代碼,所以叫做靜態分析
    5、但如果我們執行代碼,就會發現還是出現了循環

執行發現報錯:不能在 'a' 初始化之前訪問 a
原因:執行過程 a-> b -> a 此處報錯,因爲 node 發現計算 a 的時候又要計算 a

所以,結論

  • 模塊間可以循環依賴
    1、a 依賴 b,b 依賴 a
    2、a 依賴 b,b 依賴 c,c 依賴 a
  • 但不能有邏輯漏洞
    1、a.value = b.value + 1
    2、b.value = a.value + 1
  • 那能不能寫出一個沒有邏輯漏洞的循環依賴呢?
    1、當然可以
// a.js
import b from './b.js'
const a = {
  value: 'a',
  getB: () => b.value + ' from a.js'
}
export default a
// b.js
import a from './a.js'
const b = {
  value: 'b',
  getA: () => a.value + ' from b.js'
}
export default b
// index.js
import a from './a.js'
import b from './b.js'
console.log(a.getB())
console.log(b.getA())

a.js 和 b.js 就是循環依賴,但是 a 和 b 都有初始值,所以不會循環計算

有的循環依賴問題

有的循環依賴問題

所以最好別用循環依賴,以防萬一
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章