在使用過Webpack後,它強大的的靈活性給我留下了深入印象,經過Plugin和Loader幾乎能夠隨意擴展功能,因此決定探究Webpack的實現原理,學習做者的編程思想。node
可是在學習源碼過程當中仍是遇到了挺大困難,一是它的插件系統設計的錯綜複雜,剛開始看容易被繞暈,另外是它功能實現覆蓋的場景廣,有不少內置功能不太熟悉。所以在這記錄下學習的過程,將Webpack實現的精華內容提取出來,供後續學習參考。webpack
由於Webpack的代碼量也不算少,並且比較繞,若是光看代碼會比較枯燥。因此決定以本身實現一個簡易Webpack爲目標,分步探索實現細節,從構建運行到實現一個能打包代碼的工具。爲了簡化邏輯,不會徹底像Webpack同樣實現,如下部分是差別較大的地方:web
如下是完成計劃,但願能堅持 😄typescript
首先咱們須要瞭解一些基礎的Webpack概念,Webpack的構建流程基本是圍繞如下概念進行:shell
WebpackOptionsDefaulter
合併默認配置,在webpack裏已經默認了部分配置,如context設置爲當前目錄等。Compiler
WebpackOptionsApply
將選項設置給compiler,並加載各類默認插件,如用於引導入口的EntryOptionPlugin
插件,加載js文件解析的JavascriptModulesPlugin
等NormalModuleFactory
和ContextModuleFactory
,模塊工廠主要用於在後續建立和初始化模塊Compilation
,在這裏會經過鉤子調用各類插件來初始化編譯工具,如爲入口模塊添加解析器,爲js類型文件添加解析器,添加模版處理方法等ChunkGroup
和Chunk
,根據模塊依賴解析出ChunkGraphTemplate
根據Chunk建立輸出內容本次咱們實現的效果是將兩個簡單文件打包成一個js,而且能夠在瀏覽器運行,採用Commonjs模塊化,咱們再實現一個簡單的loader,將代碼中的log轉換爲warn:編程
// example/index.js
const inc = require('./increment')
const dec = require('./decrement')
console.log(inc(8))
console.log(dec(8))
// example/increment.js
exports.default = function(val) {
return val + 1;
};
// example/decrement.js
exports.default = function(val) {
return val - 1;
};
// example/loader.js
module.exports = function loader(source) {
return source.replace(/console.log/g, 'console.warn')
}
複製代碼
代碼使用typescript編寫,因此先安裝typescript相關依賴json
# typescript
"typescript": "^3.7.4"
# 幫助識別node相關的類型定義
"@types/node": "^13.1.4",
# 快速編譯運行ts項目
"ts-node": "^8.5.4",
複製代碼
在package.json添加運行腳本瀏覽器
"start": "npx ts-node index.js",
複製代碼
入口文件就是咱們運行Webpack的地方,這裏咱們定義一些簡單的配置,包括編譯入口文件entry
,輸出文件bundle.js
,還有自定義loader。引入咱們的核心編譯器Compiler
,傳入配置運行。bash
// index.js
const path = require('path')
const Compiler = require('./lib/Compiler').default
const options = {
entry: path.resolve(__dirname, './example/index.js'),
output: path.resolve(__dirname, './dist/bundle.js'),
loader: path.resolve(__dirname, './example/loader.js')
}
const compiler = new Compiler(options)
compiler.run()
複製代碼
編譯器負責封裝打包過程,輸入是用戶配置,輸出是打包結果,對外提供一個run
函數啓動編譯。
入口模塊是編譯器解析的起點,從入口文件開始遞歸加載模塊文件,這裏咱們沒有遞歸解析只簡單地解析了入口文件的依賴,收集到全部依賴後渲染出合併後的代碼,最後寫出到文件。模塊化
// lib/Compiler.ts
import * as fs from 'fs'
import * as path from 'path'
import Module from './Module'
export default class Compiler {
options: any
constructor(options: any) {
this.options = options
}
run() {
// 建立入口模塊
const name = path.basename(this.options.entry)
const entryModule = this.createModule(name, this.options.entry)
// 解析依賴模塊
const dependencies = this.parse(entryModule.source)
this.addModuleDependencies(entryModule, dependencies)
// 渲染出結果
const source = this.renderTemplate(entryModule)
// 寫入文件
this.write(source, this.options.output)
}
// ...
}
複製代碼
Webpack中將一切資源都當作模塊,因此咱們要解析的一個個js文件也是用模塊表示,首先先定義一個Module
類來表示模塊:
// lib/Module.ts
export default class Module {
id: string // 模塊惟一標誌,這裏咱們用文件名錶示
source: string // 文件源碼
absPath: string // 文件絕對路徑
dependencies: Module[] // 文件全部依賴
}
複製代碼
有了模塊類咱們就能夠封裝建立模塊功能了,除了初始化數據外,咱們還在這裏將文件讀取出來,而後使用loader對源碼進行處理。
// Compiler.createModule
createModule(id: string, absPath: string) {
const module = new Module()
module.id = id
module.absPath = absPath
module.source = fs.readFileSync(absPath).toString()
module.dependencies = []
const loader = require(this.options.loader)
module.source = loader(module.source)
return module
}
複製代碼
webpack的基本功能就是將模塊化代碼打包成瀏覽器可運行代碼。因爲瀏覽器不能直接識別模塊化代碼,就須要咱們將多個文件按依賴順序合併成一個文件,因此識別出模塊依賴是咱們要解決的第一個問題。
咱們使用CommonJS來組織代碼,就要在代碼中識別出require
這樣的關鍵字,因此這裏咱們簡單地使用正則匹配,通過循環匹配後,就能取出包含require('xxx')
中的依賴項了。
用正則匹配還須要考慮註釋換行等麻煩的校驗。Webpack則是將代碼解析成AST樹來分析依賴,AST裏包含了更豐富的信息且不容易出錯。
// Compiler.parse
parse(source: string) {
const dependencies: any[] = []
let result = []
let reg = /require[('"].([^']*)[)'"]./g
while((result = reg.exec(source))) {
dependencies.push({
id: result[1]
})
}
return dependencies
}
複製代碼
在這裏咱們已經獲取到了父模塊和他的全部依賴項,此時咱們就要將依賴也轉成一個個模塊,由於一個依賴也是一個文件,一個文件在webpack中就是一個模塊。
// Compiler.addModuleDependencies
addModuleDependencies(module: Module, dependencies: any[]) {
const dir = path.dirname(module.absPath)
for (const dependent of dependencies) {
const depModule = this.createModule(dependent.id, path.resolve(dir, dependent.id) + '.js')
module.dependencies.push(depModule)
}
return
}
複製代碼
上面說了,要想將模塊化代碼轉換成在瀏覽器環境下執行的代碼,咱們應該將全部將要執行的代碼合併在一塊兒,用一個js文件給瀏覽器執行,並且瀏覽器不識別的CommonJS語法也須要咱們給打上補丁,讓瀏覽器能正確識別require
和exports
,因此咱們的目標代碼應該長這樣:
(function (modules) {
function require(moduleId) {
var module = {
id: moduleId,
exports: {}
}
modules[moduleId](module, require)
return module.exports;
}
require("index.js");
})({
'index.js': (function (module, require) {
const inc = require('./increment')
const dec = require('./decrement')
console.warn(inc(8))
console.warn(dec(8))
}),
'./increment': (function (module, require) {
module.exports = function (val) {
return val + 1;
};
}),
'./decrement': (function (module, require) {
module.exports = function (val) {
return val - 1;
};
}),
})
複製代碼
當即執行函數傳入合併後的全部代碼,並建立了require
函數來加載合併後的對象,在咱們的代碼中遇到了require
就會帶入相應的函數,只要初始化後調用一次入口模塊代碼就能執行了。能夠看到除了傳參的代碼,其餘都是固定的模版代碼,參數代碼咱們能夠用前面解析的依賴來建立。
// Compiler.renderTemplate
renderTemplate(module: Module) {
const buffer = []
buffer.push(`(function(modules) { function require(moduleId) { var module = { id: moduleId, exports: {} } modules[moduleId](module, require) return module.exports; } require("${module.id}"); })({`)
buffer.push(`'${module.id}': (function(module, require) { \n ${module.source} \n }),`)
for (const dependent of module.dependencies) {
const src = `(function(module, require) { \n ${dependent.source.replace('exports.default', 'module.exports')} \n })`
buffer.push(`'${dependent.id}':${src},`)
}
buffer.push(`})`)
return buffer.reduce((pre, cur) => pre + cur, '')
}
複製代碼
輸出了模版代碼後,只要調用系統方法將其輸出到硬盤就能夠了,很是簡單
// Compiler.write
write(source: string, output: string) {
fs.writeFileSync(output, source)
}
複製代碼