【Webpack】1080- Webpack入門到精通(AST、Babel、依賴)
?讀者投稿:作者:一咻,原文地址:https://juejin.cn/post/6975885302493609991?share_token=68a0b777-70c1-4021-a894-3ed9f8c107e9
?
babel與AST
初始化項目
mkdir webpack-study
cd webpack-study
yarn init -y
此時在package.json里面加入以下依賴
{
"name": "01",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"dependencies": {
"@babel/core": "7.12.3",
"@babel/generator": "7.12.5",
"@babel/parser": "7.12.5",
"@babel/preset-env": "7.12.1",
"@babel/traverse": "7.12.5",
"ts-node": "9.0.0",
"typescript": "4.0.5"
},
"devDependencies": {
"@types/babel__core": "7.1.12",
"@types/babel__generator": "7.6.2",
"@types/babel__parser": "7.1.1",
"@types/babel__preset-env": "7.9.1",
"@types/babel__traverse": "7.0.15",
"@types/node": "14.14.6"
}
}
babel以及項目依賴
對使用到的包進行說明
詳細內容請參考: 理解babel的基本原理和使用方法
@babel/core Babel 是一個 JavaScript 編譯器, Babel 是一個工具鏈,主要用于將采用 ECMAScript 2015+ 語法編寫的代碼轉換為向后兼容的 JavaScript 語法,以便能夠運行在當前和舊版本的瀏覽器或其他環(huán)境中。下面列出的是 Babel 能為你做的事情:
語法轉換 通過 Polyfill 方式在目標環(huán)境中添加缺失的特性(通過第三方 polyfill 模塊,例如 core-js,實現) 源碼轉換 (codemods)
@babel/generator
這個過程已經在上面的實例中有所展現,使用的插件是@babel/generator,其作用就是將轉換好的ast重新生成代碼。這樣的代碼就就可以安全的在瀏覽器運行。
@babel/parser
在babel中編譯器插件是@babel/parser,其作用就是將源碼轉換為AST,
@babel/preset-env (預設(preset)——babel的插件套裝)
那么問題來了新語法新特性那么多,難道我們要挨個去加嗎?當然不是,babel已經預設了幾套插件,將最新的語法進行轉換,可以使用在不同的環(huán)境中,如下:
@babel/preset-env
@babel/preset-flow
@babel/preset-react
@babel/preset-typescript
從名字上就能看出他們使用的環(huán)境了,需要注意的是env,他的作用是將最新js轉換為es6代碼。預設是babel插件的組合,我們可以看下package.json(截取一部分):

@babel/traverse
ast進行遍歷parse
ts-node 使用.d.ts文件 既然要開發(fā)一個項目,顯然不會只有這些代碼。肯定要用到內建模塊和第三方模塊。然而,直接導入模塊,在.ts文件中是不行的。例如:
這是由于typescript自身的機制,需要一份xx.d.ts聲明文件,來說明模塊對外公開的方法和屬性的類型以及內容。感覺有一些麻煩。好在,官方以及社區(qū)已經準備好了方案,來解決這個問題。
在TypeScript 2.0以上的版本,獲取類型聲明文件只需要使用npm。在項目目錄下執(zhí)行安裝:
npm install --save-dev @types/node
就可以獲得有關node.js v6.x的API的類型說明文件。之后,就可以順利的導入需要的模塊了:
import * as http from 'http';
小試牛刀
我們安裝好依賴之后,編寫以下代碼
touch var_to_let.ts
import { parse } from '@babel/parser'
import traverse from '@babel/traverse'
import generate from '@babel/generator'
//將一段代碼(字符串)轉換成 AST
let code = `let a = 'str'; let b = 2`
const ast = parse(code, { sourceType: 'module' })
console.log(ast)
在vscode里面打上斷點
接著我們在終端運行
就可以進行斷點調試了,我們可以看到parse()函數把字符串的代碼轉換后的結果

我們在這個ast樹形結構里面找到以下幾個屬性,不難發(fā)現ats就是把一個字符串代碼,表示成一個樹形結構。

把let變成 var
traverse(ast, {
//遍歷每一個節(jié)點都會進入的回調函數。
enter: item => {
if(item.node.type === 'VariableDeclaration') {
if(item.node.kind === 'let') {
item.node.kind = 'var'
}
}
}
})
const result = generate(ast, {}, code)
console.log(result)
看下面的結果,我們修改了ats對象里面的屬性的值,最終通過generate函數生成了一個新的字符串代碼片段。成功的將原始代碼里面的let轉化成了var。嗯?Es6轉Es5就這么簡單?我們繼續(xù)
為什么用AST?
很難用正則表達式來替換,正則表達式很容易把 let a = 'let',替換成var a = 'var'在修改的時候需要知道每一個單詞的意思,才能做到只修改用于 變量聲明的let而AST能明確的告訴你每個 let的意思
把代碼轉化為ES5
//使用@babel/core 和 @babel/preset-env把代碼自動轉化成ES5
import { parse } from '@babel/parser'
import * as babel from '@babel/core'
let code = `let a = 'str'; let b = 2; const c = 100`
//把字符串轉成ast
const ast = parse(code, { sourceType: 'module' })
//把ats變成字符串
const result = babel.transformFromAstSync(ast, code, {
presets: ['@babel/preset-env']
})
console.log(result)
得到以下結果,可以看到const和let都被轉化成了ES5的代碼了。下面我們接著寫,我們把code字符串代碼放在文件里面,把生成的結果寫入到另一個.es5.js結尾的文件中。
//使用@babel/core 和 @babel/preset-env把代碼自動轉化成ES5
import { parse } from '@babel/parser'
import * as babel from '@babel/core'
import * as fs from 'fs'
//從文件中讀取源代碼,并轉成字符串
let code = fs.readFileSync('./test.js').toString()
//把字符串轉成ast
const ast = parse(code, { sourceType: 'module' })
//把ats變成字符串
const result = babel.transformFromAstSync(ast, code, {
presets: ['@babel/preset-env']
})
//把生成好的字符串寫入文件里面
let fileName = 'test.es5.js'
fs.writeFileSync(fileName, result.code)
通過以上代碼我們就將一個源文件是ES6的js代碼轉換成了ES6的代碼

分析index.js的依賴
在當前目錄下新建project-01目錄,新建三個文件a.js,b.js,index.js分別寫下以下內容
a.js
var a = {
value: 100
}
export default a
b.js
var b = {
value : 100
}
export default b
index.js
import a from './a'
import b from './b'
var sum = a.value + b.value
console.log(sum)
因為當前我們的環(huán)境是node環(huán)境,為了在node里面讓import生效,我們使用以下方法。
全局安裝 babel-cli
npm install babel-cli -g
安裝 babel-preset-env
npm install babel-preset-env -D
然后原來是 node server.js,改為這樣調用:babel-node --presets env server.js
?需要注意的是如果只是為了 babel-node 這一個命令,安裝 babel-cli 會加載安裝很多資源和模塊,出于性能考慮不推薦在生產環(huán)境使用。自己在開發(fā)調試的時候,可以鼓搗著玩玩
?
下面我們在命令行執(zhí)行以下操作,便可以看到結果。呀是不是有點跑偏了的感覺,我們是來分析index.js文件的依賴項的呀,趕緊回到正題。

在項目下新建deps.ts文件,在文章最后面我會把完整的代碼放上來,一段一段貼代碼,太浪費空間了。
最終我們得到了想要的結果。

遞歸分析嵌套的依賴
下面我們再加一點難度,假如我們的a.js又依賴了其他的文件呢?b.js也同樣依賴了其他文件呢?我們又該如何獲取到其內部文件依賴的依賴呢?我們繼續(xù)
cp -r project-01/ project-02
cd project-02
mkdir lib
a.js
import a1 from "./lib/a1.js"
var a = {
value: 'a'
}
export default a
b.js
js
import b1 from "./lib/b1.js"
var b = {
value : 100
}
export default b
在之前的a.js和b.js里面分別把這兩個文件import進去, 這樣就有更深層次的依賴關系了,我們下面只需要在遍歷AST語法樹的時候,當發(fā)現這個節(jié)點是ImportDeclaration的時候,再獲取這個節(jié)點的值,組裝一下真實的文件路徑,再遞歸調用把組裝好的路徑傳入collectCodeAndDeps便可以繼續(xù)分析了。
什么是循環(huán)依賴?
index.js
import a from './a.js'
import b from './b.js'
console.log(a)
let sum = a.value + b.value
console.log(sum)
a.js
import b from './b.js'
var a = {
value: b.value + 1
}
export default a
b.js
js
import a from './a.js'
var b = {
value: a.value + 1
}
export default b
我在node版本為v16.3.0下面測試得到的結果。

如果我們把上面value的值換成一個常量的話,就可以正常執(zhí)行完代碼了。
靜態(tài)分析循環(huán)依賴
在遍歷AST的時候如果發(fā)現在之前的記錄里面已經有了,就不再進行遍歷了。
//分析index.js里面代碼依賴的文件
import { resolve, relative, dirname } from 'path'
import { readFileSync } from 'fs'
import { parse } from '@babel/parser'
import traverse from '@babel/traverse';
//設置項目根目錄
const projectRoot = resolve(__dirname, 'project-02')
//聲明最終結果的類型
var result = {
'index.js': {
deps: ['a.js', 'b.js'],
code: "import a from './a'\r\nimport b from './b'\r\n\r\nvar sum = a.value + b.value \r\n\r\nconsole.log(sum)"
}
}
type DepRelation = {
[key: string]: {
deps: string[],
code: string
}
}
interface a {
}
//初始化
const depRelation: DepRelation = {}
function collectCodeAndDeps(filepath: string) {
let key = getProjectPath(filepath)
if (Object.keys(depRelation).includes(key)) {
// 注意,重復依賴不一定是循環(huán)依賴
return
}
//先讀取index文件的內容
//把字符串代碼轉換成ats
let code = readFileSync(resolve(filepath)).toString()
//把入口文件的文件名當做map的key
depRelation[key] = {
deps: [],
code
}
let ast = parse(code, {
sourceType: 'module'
})
//遍歷ast
traverse(ast, {
enter: path => {
//如果發(fā)現當前語句是 import 就把inport的value 寫入到依賴中去
if (path.node.type === 'ImportDeclaration') {
//當前文件的上一級目錄 與獲取到當前文件的依賴文件進行拼接。
let depAbsolutePath = resolve(dirname(filepath), path.node.source.value)
//獲取當前文件與根目錄的相對路徑
const depProjectPath = getProjectPath(depAbsolutePath)
// 把依賴寫進 depRelation
depRelation[key].deps.push(depProjectPath)
//拿到依賴文件的真實路徑進行再一次依賴分析
collectCodeAndDeps(depAbsolutePath)
}
}
})
}
collectCodeAndDeps(resolve(projectRoot, 'index.js'))
console.log(depRelation)
//獲取文件相對跟目錄的相對路徑
/*
C: \\Users\\code\\zf\\webpack\\01\\project - 01
C: \\Users\\code\\zf\\webpack\\01\\project - 01\\index.js
//得到的結果就是index.js
*/
function getProjectPath(path: string) {
return relative(projectRoot, path).replace(/\\/g, '/')
}
總結
AST相關
parse:把代碼轉換成AST traverse:遍歷AST,并在需要的時候可以進行修改 generate:把AST再轉換成代碼code2
工具相關
babel 可以把高級代碼轉換成ES5代碼 @babel/parser @babel/traverse @babel/generate @babel/core @babel-preset-env 獲取您指定的任何目標環(huán)境并根據其映射檢查它們以編譯插件列表并將其傳遞給 Babel
代碼技巧
用哈希表來存儲映射關系 通過檢查哈希表的key來檢測重復
循環(huán)依賴
有的循環(huán)依賴可以正常執(zhí)行 有的循環(huán)依賴不可以正常執(zhí)行 但是兩者都可以進行靜態(tài)分析

回復“加群”與大佬們一起交流學習~
點擊“閱讀原文”查看 120+ 篇原創(chuàng)文章
