hello 大家好移必,????♀?????♀?????♀?
我是一個(gè)熱愛(ài)知識(shí)傳遞,正在學(xué)習(xí)寫(xiě)作的作者,ClyingDeng
凳凳逸尖!
好久不見(jiàn)哈!
今天我要給大家?guī)?lái)一個(gè)手寫(xiě)webpack的打包功能!
????前方硬核娇跟,請(qǐng)注意岩齿!
簡(jiǎn)易webpack使用
webpack想必大家都挺熟悉的吧,我們先來(lái)完成webpack打包的一個(gè)簡(jiǎn)單例子苞俘。
初始化一個(gè)空項(xiàng)目npm init -y
盹沈,安裝依賴webpack
、webpack-cli
吃谣。
創(chuàng)建一個(gè)src目錄乞封,存放自己代碼的文件夾。內(nèi)部新建一個(gè)index.js文件作為入口文件岗憋。在src目錄下肃晚,index.js
引入了add(兩數(shù)相加)和minus(兩數(shù)相減)兩個(gè)方法。入口文件內(nèi)容:
import add from './add.js'
import minus from './minus.js'
console.log(add(1, 2))
console.log(minus(1, 2))
創(chuàng)建一個(gè)webpack.config.js
文件仔戈,作為webpack配置关串。目前webpack 5可以只需要指定它的環(huán)境模式就可以進(jìn)行打包。默認(rèn)入口就是src下的index.js
文件监徘,出口就是dist目錄下的main.js
文件晋修。配置如下:
const path = require('path')
module.exports = {
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, './dist'),
},
mode: 'production',
}
具體目錄結(jié)構(gòu)如下:
最后執(zhí)行webopack
命令,進(jìn)行webpack打包凰盔。
在index.html
文件中引入打包后的文件飞蚓,在瀏覽器運(yùn)行,控制臺(tái)如果可以正常輸出打包后的結(jié)果(1和2加減的結(jié)果)廊蜒,那么這就證明使用webpack打包成功趴拧。
webpack工作流程
- 初始化Compiler:new Compiler(config) 得到Compiler對(duì)象
- 開(kāi)始編譯:調(diào)用Compiler 對(duì)象 run 方法開(kāi)始執(zhí)行編譯
- 確定入口: 根據(jù)配置中的 entry 找出所有的入口文件
- 編譯模塊:從入口文件出發(fā),調(diào)用所有配置的Loader 對(duì)模塊進(jìn)行編譯山叮,再找出該模塊依賴的模塊著榴,遞歸知道所有模塊被加載進(jìn)來(lái)
- 完成模塊編譯:在經(jīng)過(guò)第4步使用Loader 編譯完成所有模塊后,得到了每個(gè)模塊被編譯后的最終內(nèi)容以及它們之間的依賴關(guān)系
- 輸出資源:根據(jù)入口模塊之間的依賴關(guān)系屁倔,組裝成一個(gè)個(gè)包含多個(gè)模塊的Chunk脑又。再把每個(gè)Chunk 轉(zhuǎn)換成一個(gè)單獨(dú)的文件加入到輸出列表。(注意:這步是可以修改輸出內(nèi)容的最后機(jī)會(huì))
- 輸出完成:在確定好的輸出內(nèi)容后锐借,根據(jù)配置確定的路徑和文件名问麸,把文件內(nèi)容寫(xiě)入到文件系統(tǒng)
手寫(xiě)webpack
根據(jù)webpack的工作流程,這次我們涉及到的沒(méi)有l(wèi)oader和plugin钞翔。但是存在文件的依賴严卖,至此我們需要在編譯文件模塊的時(shí)候需要進(jìn)行一個(gè)模塊的遞歸遍歷收集的過(guò)程。
很好布轿,上面就是我們今天要實(shí)現(xiàn)的打包功能:獲取webpack配置哮笆,執(zhí)行Compiler.run()方法来颤,
第一層入口文件處理:根據(jù)傳入配置的入口路徑,讀取文件讀取資源轉(zhuǎn)成utf-8
格式稠肘,通過(guò)@babel/parser
將資源轉(zhuǎn)成ast抽象語(yǔ)法樹(shù)福铅;利用@babel/traverse
去遍歷ast中program.body節(jié)點(diǎn),獲取依賴路徑项阴,并將其收集到deps中滑黔;通過(guò)@babel/core
將ast編譯成瀏覽器能夠識(shí)別的js。
經(jīng)過(guò)一系列處理环揽,然后輸出到配置指定的目錄和文件夾中略荡。
第一層處理好之后,定義一個(gè)modules總的依賴收集器薯演,將第一層文件依賴push到modules中撞芍。遍歷modules秧了,去處理入口文件的deps依賴跨扮,將其每個(gè)依賴都轉(zhuǎn)成和第一層入口文件的格式。再push到modules验毡,如果依賴中還存在依賴衡创,那么就需要繼續(xù)遞歸遍歷,直到每個(gè)deps中為空晶通。
處理modules璃氢,將其變成一個(gè)關(guān)系圖,類似于:
{
'index.js':{
code:'xx',
deps:{
'add.js':"xxx"
},
'add.js':{
code:'xxx',
deps:{}
}
}
}
最后生成bundle狮辽,輸出資源一也。
初始環(huán)境搭建
在上述環(huán)境的基礎(chǔ)上,我們已經(jīng)有了webpack的配置喉脖,那么我們就需要寫(xiě)一個(gè)我們自己的webpack方法椰苟,去接收配置信息。
首先我們知道會(huì)有一個(gè)Compiler類树叽,內(nèi)部存在run方法去執(zhí)行打包舆蝴。新建webpack功能處理文件:lib文件夾下Compiler.js
文件。
class Compiler{
constructor(options){
this.options = options
}
run(){
console.log('執(zhí)行run方法',this.options)
}
}
module.exports = Compiler
新建webpack功能處理文件:lib文件夾下index.js
文件题诵。接收config配置洁仗,通過(guò)new Compiler將配置傳入Compiler類中。
const Compiler = require('./Compiler')
const myWebpack = (config) => {
return new Compiler(config)
}
module.exports = myWebpack
新建一個(gè)打包的腳本文件:scripts文件夾--build.js
文件性锭。根據(jù)上面的原理闡述赠潦,不難發(fā)現(xiàn)webpack打包是執(zhí)行其中的run方法。
const config = require('../webpack.config')
const myWebpack = require('../lib/index')
myWebpack(config).run()
在package.json文件中配置打包命令:"build": "node ./scripts/build.js"
草冈。執(zhí)行npm run build
可以看到run方法中已經(jīng)獲取到相關(guān)配置信息祭椰。
處理第一層入口文件
接下來(lái)的步驟主要就是去完善Compiler類臭家。
處理第一層入口文件,就需要讀取文件資源將其轉(zhuǎn)成ast方淤,并對(duì)ast進(jìn)行遍歷收集第一層文件依賴文件的信息钉赁,最后還需要根據(jù)ast編譯成瀏覽器可識(shí)別的js語(yǔ)言。
獲取ast
根據(jù)配置拿到入口文件路徑携茂,通過(guò)fs.readFileSync
讀取文件資源你踩,再通過(guò)@babel/parser
將其轉(zhuǎn)成ast。
const fs = require('fs')
const path = require('path')
const babelPaser = require('@babel/parser')
const filePath = this.options.entry
const file = fs.readFileSync(filePath, 'utf-8')
// 轉(zhuǎn)成ast
const ast = babelPaser.parse(file, {
sourceType: 'module',
})
return ast
瀏覽器調(diào)試方法
大多情況下讳苦,我們都習(xí)慣于直接在終端輸出結(jié)果带膜,但是遇到輸出像ast這樣的對(duì)象結(jié)構(gòu),通常會(huì)展示很長(zhǎng)一段鸳谜,層級(jí)嵌套過(guò)深膝藕,很難分清子屬性屬于哪個(gè)父屬性。
在此咐扭,我們使用瀏覽器調(diào)試方式芭挽,就可以很清楚的看到ast的層級(jí)結(jié)構(gòu)。
在我們獲取ast的地方打上debugger
蝗肪,并在package.json配置命令:"debug": "node --inspect-brk ./scripts/build.js"
袜爪。執(zhí)行命令。
在瀏覽器端任意頁(yè)面打開(kāi)控制臺(tái)薛闪,會(huì)發(fā)現(xiàn)多出一個(gè)node.js圖標(biāo):
點(diǎn)擊圖標(biāo)辛馆,會(huì)彈出一個(gè)新的控制臺(tái)窗口。
點(diǎn)擊下一個(gè)斷點(diǎn)就會(huì)跳轉(zhuǎn)到我們打斷點(diǎn)的代碼文件中豁延。
在控制臺(tái)中監(jiān)聽(tīng)ast昙篙,我們就可以很清楚的看到ast中的結(jié)構(gòu)。當(dāng)然也可以使用ast工具網(wǎng)站:https://astexplorer.net/
編譯ast
拿到ast后我們需要將ast語(yǔ)法樹(shù)編譯成瀏覽器能夠識(shí)別的js诱咏。直接通過(guò)@babel/core
中的transformFromAst
方法即可苔可。js如果存在高級(jí)語(yǔ)法,就加一個(gè)@babel/preset-env
去解析js胰苏。
const {transformFromAst} = require('@babel/core')
getCode(ast){
const { code } = transformFromAst(ast, null, {
presets: ['@babel/preset-env'],
})
return code
}
遍歷ast收集依賴
我們需要收集入口文件的依賴硕蛹,就需要去遍歷ast的program.body中的節(jié)點(diǎn),找到類型為ImportDeclaration
的引入聲明節(jié)點(diǎn)硕并。
再找到節(jié)點(diǎn)下的source.vlaue
(入口文件引入依賴的相對(duì)路徑)法焰,根據(jù)相對(duì)路徑生成絕對(duì)路徑。
const traverse = require('@babel/traverse').default
// 獲取依賴
getDeps(ast, filePath) {
// 獲取文件夾路徑
const dirname = path.dirname(filePath)
// 收集ast語(yǔ)法樹(shù)依賴: 通過(guò)ast上的program.body.source.value收集依賴
// 定義收集存儲(chǔ)依賴的容器
const deps = {}
traverse(ast, {
// 內(nèi)部遍歷ast中的program.body 判斷里面語(yǔ)句類型
// 如果 type:ImportDeclaration 就會(huì)觸發(fā)當(dāng)前函數(shù)
ImportDeclaration({ node }) {
// 引入聲明
// code.node.source.value 引入文件的相對(duì)路徑 './add.js'
const relativePath = node.source.value
// 生成基于入口文件的絕對(duì)路徑
const absolutePath = path.resolve(dirname, relativePath)
// 添加依賴
deps[relativePath] = absolutePath
},
})
return deps
}
收集相對(duì)路徑與絕對(duì)路徑有映射關(guān)系的依賴deps倔毙。
遞歸收集所有依賴
收集依賴
將上面對(duì)第一層入口文件處理的三大方法提取封裝成公共方法埃仪。作為構(gòu)建基礎(chǔ)方法build。
// 開(kāi)始構(gòu)建
build(filePath) {
// 生成ast語(yǔ)法樹(shù)
const ast = getAst(filePath)
// 收集依賴
const deps = getDeps(ast, filePath)
// 編譯
const code = getCode(ast)
return {
// 文件路徑
filePath,
// 當(dāng)前依賴
deps,
// 解析后的代碼
code,
}
}
在Compiler類中陕赃,創(chuàng)建一個(gè)收集多層文件總的依賴modules數(shù)組卵蛉。
在執(zhí)行方法run:
需要收集全部文件的依賴颁股,我們就需要去遞歸遍歷入口文件中的deps,根據(jù)deps中的依賴找到相應(yīng)文件傻丝,再對(duì)相應(yīng)文件進(jìn)行轉(zhuǎn)ast甘有,編譯,收集依賴葡缰。
// 1.讀取入口文件內(nèi)容
const filePath = this.options.entry
const fileInfo = this.build(filePath)
this.modules.push(fileInfo)
// 遍歷所有依賴
this.modules.forEach((_) => {
const deps = _.deps
for (const path in deps) {
// 獲取絕對(duì)路徑亏掀,然后收集依賴
const absolutePath = deps[path]
// 對(duì)依賴文件進(jìn)行處理,并添加到modules中
this.modules.push(this.build(absolutePath))
}
})
整理依賴
進(jìn)一步處理依賴泛释,每一個(gè)引入的相對(duì)路徑文件下存在自己的依賴文件路徑滤愕、編譯code。將依賴整理成如下的依賴關(guān)系圖:
{
'index.js':{
code:'xx',
deps:{
'add.js':"xxx"
},
'add.js':{
code:'xxx',
deps:{}
}
}
}
此時(shí)怜校,我們就需要一個(gè)空對(duì)象來(lái)收集處理后的依賴關(guān)系间影。遍歷modules依賴,根據(jù)上述結(jié)構(gòu)進(jìn)行組織茄茁。
const depsGraph = this.modules.reduce((graph, module) => {
return {
...graph,
[module.filePath]: {
code: module.code,
deps: module.deps,
},
}
}, {})
打包輸出資源
拿到處理后的依賴關(guān)系圖魂贬,我們就可以生成bundle并打包輸出資源。
生成bundle
webpack打包后的bundle是一個(gè)立即執(zhí)行函數(shù)胰丁,內(nèi)部是通過(guò)執(zhí)行依賴關(guān)系圖中的code随橘,再次對(duì)發(fā)起相應(yīng)依賴文件的請(qǐng)求喂分。bundle內(nèi)容如下:
(function (depsGraph) {
function require(modulePath){
const module ={
id:modulePath,
exports:{}
}
// 依賴文件的require
// relativePath:./add.js
function localRequire(relativePath){
const absolutePath = depsGraph[modulePath].deps[relativePath]
return require(absolutePath)
}
// 執(zhí)行第一層code
(function (exports,code,require) {
eval(code)
})(module.exports,depsGraph[modulePath].code,localRequire)
return module.exports
}
require('${this.options.entry}')
})(${JSON.stringify(depsGraph)})
立即執(zhí)行函數(shù)傳入關(guān)系圖锦庸,require第一層入口文件。在第一個(gè)入口文件中我們需要執(zhí)行內(nèi)部的code蒲祈,就需要再加一個(gè)立即執(zhí)行函數(shù)去執(zhí)行code甘萧。在第一個(gè)入口文件執(zhí)行時(shí),會(huì)遇到依賴的add.js
文件梆掸。執(zhí)行的時(shí)候會(huì)將其轉(zhuǎn)成require請(qǐng)求扬卷,所以我們還需要寫(xiě)一個(gè)請(qǐng)求內(nèi)部文件依賴的方法localRequire
,遞歸調(diào)用require酸钦。
可能會(huì)有人不理解在eval(code)
這個(gè)函數(shù)中參數(shù)意義怪得,是這樣的:
- exports:對(duì)外暴露的模塊
- code:需要執(zhí)行的文件內(nèi)容
- require:依賴再次請(qǐng)求時(shí),會(huì)觸發(fā)require函數(shù)卑硫,所以入?yún)⒌膔equire名稱不能改變徒恋。否則不會(huì)觸發(fā)立即執(zhí)行函數(shù)中的localRequire方法,會(huì)直接執(zhí)行外層的require函數(shù)欢伏,這樣會(huì)導(dǎo)致請(qǐng)求時(shí)入挣,傳入的是依賴文件的相對(duì)路徑,獲取不到相關(guān)文件內(nèi)容硝拧,執(zhí)行錯(cuò)誤径筏。
輸出資源
通過(guò)options獲取輸出文件的絕對(duì)路徑:
const filePath = path.resolve(this.options.output.path, this.options.output.filename)
創(chuàng)建打包輸出后的文件夾和文件資源葛假。由于我們只是實(shí)現(xiàn)簡(jiǎn)單版本的webpack,所以只考慮了打包文件夾下的一個(gè)輸出文件滋恬。
// 判斷是否存在相同文件夾名稱聊训,存在刪除后再寫(xiě)入
if (fs.existsSync(this.options.output.path)) {
fs.unlinkSync(filePath);
fs.rmdirSync(this.options.output.path)
}
fs.mkdirSync(this.options.output.path)
fs.writeFileSync(filePath, bundle, 'utf-8')
驗(yàn)證功能
執(zhí)行打包命令后,在html文件中引入恢氯。運(yùn)行html文件:
可以看到與使用webpack打包輸出結(jié)果一致魔眨。
這樣昆淡,一個(gè)簡(jiǎn)單的webpack打包流程功能就算完成啦??????