Webpack入門(mén)到精通(AST、Babel、依賴(lài))
?讀者投稿:作者:一咻,原文地址:https://juejin.cn/post/6975885302493609991?share_token=68a0b777-70c1-4021-a894-3ed9f8c107e9
?
babel與AST
初始化項(xiàng)目
mkdir webpack-study
cd webpack-study
yarn init -y
此時(shí)在package.json里面加入以下依賴(lài)
{
"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以及項(xiàng)目依賴(lài)
對(duì)使用到的包進(jìn)行說(shuō)明
詳細(xì)內(nèi)容請(qǐng)參考: 理解babel的基本原理和使用方法
@babel/core Babel 是一個(gè) JavaScript 編譯器, Babel 是一個(gè)工具鏈,主要用于將采用 ECMAScript 2015+ 語(yǔ)法編寫(xiě)的代碼轉(zhuǎn)換為向后兼容的 JavaScript 語(yǔ)法,以便能夠運(yùn)行在當(dāng)前和舊版本的瀏覽器或其他環(huán)境中。下面列出的是 Babel 能為你做的事情:
語(yǔ)法轉(zhuǎn)換 通過(guò) Polyfill 方式在目標(biāo)環(huán)境中添加缺失的特性(通過(guò)第三方 polyfill 模塊,例如 core-js,實(shí)現(xiàn)) 源碼轉(zhuǎn)換 (codemods)
@babel/generator
這個(gè)過(guò)程已經(jīng)在上面的實(shí)例中有所展現(xiàn),使用的插件是@babel/generator,其作用就是將轉(zhuǎn)換好的ast重新生成代碼。這樣的代碼就就可以安全的在瀏覽器運(yùn)行。
@babel/parser
在babel中編譯器插件是@babel/parser,其作用就是將源碼轉(zhuǎn)換為AST,
@babel/preset-env (預(yù)設(shè)(preset)——babel的插件套裝)
那么問(wèn)題來(lái)了新語(yǔ)法新特性那么多,難道我們要挨個(gè)去加嗎?當(dāng)然不是,babel已經(jīng)預(yù)設(shè)了幾套插件,將最新的語(yǔ)法進(jìn)行轉(zhuǎn)換,可以使用在不同的環(huán)境中,如下:
@babel/preset-env
@babel/preset-flow
@babel/preset-react
@babel/preset-typescript
從名字上就能看出他們使用的環(huán)境了,需要注意的是env,他的作用是將最新js轉(zhuǎn)換為es6代碼。預(yù)設(shè)是babel插件的組合,我們可以看下package.json(截取一部分):

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

我們?cè)谶@個(gè)ast樹(shù)形結(jié)構(gòu)里面找到以下幾個(gè)屬性,不難發(fā)現(xiàn)ats就是把一個(gè)字符串代碼,表示成一個(gè)樹(shù)形結(jié)構(gòu)。

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

分析index.js的依賴(lài)
在當(dāng)前目錄下新建project-01目錄,新建三個(gè)文件a.js,b.js,index.js分別寫(xiě)下以下內(nèi)容
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)
因?yàn)楫?dāng)前我們的環(huán)境是node環(huán)境,為了在node里面讓import生效,我們使用以下方法。
全局安裝 babel-cli
npm install babel-cli -g
安裝 babel-preset-env
npm install babel-preset-env -D
然后原來(lái)是 node server.js,改為這樣調(diào)用:babel-node --presets env server.js
?需要注意的是如果只是為了 babel-node 這一個(gè)命令,安裝 babel-cli 會(huì)加載安裝很多資源和模塊,出于性能考慮不推薦在生產(chǎn)環(huán)境使用。自己在開(kāi)發(fā)調(diào)試的時(shí)候,可以鼓搗著玩玩
?
下面我們?cè)诿钚袌?zhí)行以下操作,便可以看到結(jié)果。呀是不是有點(diǎn)跑偏了的感覺(jué),我們是來(lái)分析index.js文件的依賴(lài)項(xiàng)的呀,趕緊回到正題。

在項(xiàng)目下新建deps.ts文件,在文章最后面我會(huì)把完整的代碼放上來(lái),一段一段貼代碼,太浪費(fèi)空間了。
最終我們得到了想要的結(jié)果。

遞歸分析嵌套的依賴(lài)
下面我們?cè)偌右稽c(diǎn)難度,假如我們的a.js又依賴(lài)了其他的文件呢?b.js也同樣依賴(lài)了其他文件呢?我們又該如何獲取到其內(nèi)部文件依賴(lài)的依賴(lài)呢?我們繼續(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里面分別把這兩個(gè)文件import進(jìn)去, 這樣就有更深層次的依賴(lài)關(guān)系了,我們下面只需要在遍歷AST語(yǔ)法樹(shù)的時(shí)候,當(dāng)發(fā)現(xiàn)這個(gè)節(jié)點(diǎn)是ImportDeclaration的時(shí)候,再獲取這個(gè)節(jié)點(diǎn)的值,組裝一下真實(shí)的文件路徑,再遞歸調(diào)用把組裝好的路徑傳入collectCodeAndDeps便可以繼續(xù)分析了。
什么是循環(huán)依賴(lài)?
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下面測(cè)試得到的結(jié)果。

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