調試webpack過程瞭解執行流程
開始-合併配置------------實例化compile-------設置node文件讀寫能力-----通過循環掛載plugins-----處理webpack內部默認的插件(入口文件)
開始-compiler.beforeRun-compiler.run--------compiler.beforeCompile-compiler.compile-------compile.make
在Compiler類中,構造函數內會掛載大量的鈎子,這些鈎子都來自tapable,掛載之後在後續的操作中,這些鈎子等待被觸發執行。
this.hooks = {
/** @type {SyncBailHook<Compilation>} */
shouldEmit: new SyncBailHook(["compilation"]),
/** @type {AsyncSeriesHook<Stats>} */
done: new AsyncSeriesHook(["stats"]),
/** @type {AsyncSeriesHook<>} */
additionalPass: new AsyncSeriesHook([]),
/** @type {AsyncSeriesHook<Compiler>} */
beforeRun: new AsyncSeriesHook(["compiler"]),
/** @type {AsyncSeriesHook<Compiler>} */
run: new AsyncSeriesHook(["compiler"]),
/** @type {AsyncSeriesHook<Compilation>} */
emit: new AsyncSeriesHook(["compilation"]),
/** @type {AsyncSeriesHook<string, Buffer>} */
assetEmitted: new AsyncSeriesHook(["file", "content"]),
/** @type {AsyncSeriesHook<Compilation>} */
afterEmit: new AsyncSeriesHook(["compilation"]),
/** @type {SyncHook<Compilation, CompilationParams>} */
thisCompilation: new SyncHook(["compilation", "params"]),
/** @type {SyncHook<Compilation, CompilationParams>} */
compilation: new SyncHook(["compilation", "params"]),
/** @type {SyncHook<NormalModuleFactory>} */
normalModuleFactory: new SyncHook(["normalModuleFactory"]),
/** @type {SyncHook<ContextModuleFactory>} */
contextModuleFactory: new SyncHook(["contextModulefactory"]),
/** @type {AsyncSeriesHook<CompilationParams>} */
beforeCompile: new AsyncSeriesHook(["params"]),
/** @type {SyncHook<CompilationParams>} */
compile: new SyncHook(["params"]),
/** @type {AsyncParallelHook<Compilation>} */
make: new AsyncParallelHook(["compilation"]),
/** @type {AsyncSeriesHook<Compilation>} */
afterCompile: new AsyncSeriesHook(["compilation"]),
/** @type {AsyncSeriesHook<Compiler>} */
watchRun: new AsyncSeriesHook(["compiler"]),
/** @type {SyncHook<Error>} */
failed: new SyncHook(["error"]),
/** @type {SyncHook<string, string>} */
invalid: new SyncHook(["filename", "changeTime"]),
/** @type {SyncHook} */
watchClose: new SyncHook([]),
/** @type {SyncBailHook<string, string, any[]>} */
infrastructureLog: new SyncBailHook(["origin", "type", "args"]),
// TODO the following hooks are weirdly located here
// TODO move them for webpack 5
/** @type {SyncHook} */
environment: new SyncHook([]),
/** @type {SyncHook} */
afterEnvironment: new SyncHook([]),
/** @type {SyncHook<Compiler>} */
afterPlugins: new SyncHook(["compiler"]),
/** @type {SyncHook<Compiler>} */
afterResolvers: new SyncHook(["compiler"]),
/** @type {SyncBailHook<string, Entry>} */
entryOption: new SyncBailHook(["context", "entry"])
};
實現迷你版webpack暫時不需要這麼多鈎子.
文件目錄結構,lib文件夾下package.json中 "main": "./lib/webpack.js",
流程中一部分內容是編譯前的處理,內容較多,主要是兩步,一是實例化 compiler 對象( 它會貫穿整個webpack工作的過程 ),調用 compile 方法,實例化了一個compilation 對象,觸發 make 監聽 ,addEntry (攜帶context name entry 相關字段信息)方法。然後由 compiler 調用 run 方法
在compiler 實例化操作中
- compiler 繼承 tapable,因此它具備鈎子的操作能力(包括監聽事件,觸發事件,因為webpack是一個事件流)
- 在實例化了 compiler 對象之後就往它的身上掛載很多屬性,其中 NodeEnvironmentPlugin 這個操作就讓它具備了文件讀寫的能力
- 具備了 fs 操作能力之後又將 plugins 中的插件都掛載到了 compiler 對象身上
- 將內部默認的插件與 compiler 建立關係,其中 EntryOptionPlugin 處理了入口模塊的 id
- 在實例化 compiler 的時候只是監聽了 make 鈎子(SingleEntryPlugin),在
SingleEntryPlugin模塊的 apply 方法中有兩個鈎子監聽, 其中 compilation 鈎子就是讓 compilation 具備了利用 normalModuleFactory 工廠創建一個普通模塊的能力,它就是利用一個自己創建的模塊來加載需要被打包的模塊。make 鈎子 在 compiler.run 的時候會被觸發,代碼執行到這裏就意味着某個模塊執行打包之前的所有準備工作就完成了,由addEntry 方法調用
run方法的執行就是一堆鈎子按着順序觸發(beforeRun run compile),compile 方法執行中
- 準備參數(其中 normalModuleFactory 是我們後續用於創建模塊的)
- 觸發beforeCompile
- 將第一步的參數傳給一個函數,開始創建一個 compilation (newCompilation)
- 在調用 newCompilation 的內部,調用了 createCompilation ,觸發了 this.compilation 鈎子 和 compilation 鈎子的監聽
- 當創建了 compilation 對象之後就觸發了 make 鈎子
- 當觸發 make 鈎子監聽的時候,將 compilation 對象傳遞了過去
template文件夾下main.ejs為模板文件
Chunk.js中處理chunk數據結構基本信息
// 處理chunk結構信息
class Chunk {
constructor(entryModule) {
this.entryModule = entryModule
this.name = entryModule.name
this.files = [] // 記錄當前chunk的文件信息
this.modules = [] // 記錄當前chunk包含的所有模塊
}
}
module.exports = Chunk
Compilation.js處理編譯時具體要做的事情
- make 鈎子在被觸發的時候,接收到了 compilation 對象實現,它的身上掛載了很多內容
- 從 compilation 當中解構了三個值, entry : 當前需要被打包的模塊的相對路徑(./src/index.js), name: main ,context: 當前項目的根路徑
- dep 是對當前的入口模塊中的依賴關係進行處理
- 在 compilation實例的身上有一個 addEntry 方法,然後內部調用了 _addModuleChain 方法,去處理依賴
- 在 compilation 當中我們可以通過 NormalModuleFactory 工廠來創建一個普通的模塊對象
- 在 webpack 內部默認啓了一個 100 併發量的打包操作,這裏用 normalModule.create()模擬實現
- 在 beforeResolve 裏面會觸發一個 factory 鈎子監聽,這個部分的操作其實是處理 loader
- 上述操作完成之後獲取到了一個函數被存在 factory 裏,然後對它進行了調用,在這個函數調用裏又觸發了一個叫 resolver 的鈎子,這是在處理 loader拿到的,resolver方法就意味着所有的Loader 處理完畢
- ] 調用 resolver() 方法之後,就會進入到 afterResolve 這個鈎子裏,然後就會觸發 new NormalModule
- 在完成上述操作之後就將module 進行了保存和一些其它屬性的添加
- 調用 buildModule 方法開始編譯、調用 build 、doBuild
const {
Tapable,
SyncHook
} = require('tapable')
const path = require('path')
const async = require('neo-async')
const Parser = require('./Parser')
const NormalModuleFactory = require('./NormalModuleFactory')
const Chunk = require('./Chunk')
const ejs = require('ejs')
// 實例化一個 normalModuleFactory parser
const normalModuleFactory = new NormalModuleFactory()
const parser = new Parser()
class Compilation extends Tapable {
constructor(compiler) {
super()
this.compiler = compiler
this.context = compiler.context
this.options = compiler.options
// 讓compilation具備文件讀寫能力
this.inputFileSystem = compiler.inputFileSystem
this.outputFileSystem = compiler.outputFileSystem
this.entries = [] // 存放所有入口模塊的數組
this.modules = [] // 存放所有模塊的數據
this.chunks = [] // 存放當前次打包過程中產出的chunk
this.assets = []
this.files = []
this.hooks = {
succeedModule: new SyncHook(['module']),
seal: new SyncHook(),
beforeChunks: new SyncHook(),
afterChunks: new SyncHook()
}
}
// 完成模塊編譯操作 context:當前項目的根,entry:當前入口的相對路徑,name:chunkName mian, callback:回調
addEntry(context, entry, name, callback) {
this._addModuleChain(context, entry, name, (err, module) => {
callback(err, module)
})
}
_addModuleChain(context, entry, name, callback) {
this.createModule({
name,
context,
rawRequest: entry,
resource: path.posix.join(context, entry), // 當前操作的核心作用就是返回entry入口的絕對路徑
parser,
moduleId: './' + path.posix.relative(context, path.posix.join(context, entry)),
}, (entryModule) => {
this.entries.push(entryModule)
}, callback)
}
/**
* 定義一個創建模塊的方法,達到複用的目的(抽象代碼來源於_addModuleChain)
* data 創建模塊時所需要的一些屬性值
* doAddEntry 可選參數,在加載入口模塊的時候,將入口模塊的id 寫入 this.entries
* callback
*/
createModule(data, doAddEntry, callback) {
// 創建模塊工廠實例
let module = normalModuleFactory.create(data)
// let entryModule = normalModuleFactory.create({
// name,
// context,
// rawRequest: entry,
// resource: path.posix.join(context, entry), // 當前操作的核心作用就是返回entry入口的絕對路徑
// parser
// })
const afterBuild = (err, module) => {
// 在afterBuild當中我們需要判斷一下當前次的module加載完成之後是否需要處理依賴加載
// NormalModule裏面有一個dependencies
if (module.dependencies.length > 0) {
// 當前邏輯標識module有需要依賴加載的模塊,我們可以單獨定義一個方法來實現
this.processDependencies(module, err => {
callback(err, module)
})
} else {
callback(err, module)
}
}
this.buildModule(module, afterBuild)
// 完成本次的build操作之後,講module進行保存
doAddEntry && doAddEntry(module) // 判斷第二個參數是否存在
// this.entries.push(entryModule)
this.modules.push(module)
}
// 完成具體的build行為,module代表要被編譯的模塊,
buildModule(module, callback) {
module.build(this, err => {
// 回調函數代表當前module的編譯操作已經完成
this.hooks.succeedModule.call(module)
callback(err, module)
})
}
// 當前的函數核心功能就是實現一個被依賴模塊的遞歸加載
processDependencies(module, callback) {
// 加載模塊的思想都是創建一個模塊,然後將被加載模塊的內容拿進來
// 當前 module 依賴模塊是未知數, 此時我們需要想辦法讓所有的被依賴的模塊都加載完成之後再執行 callback?[neo-async]
let dependencies = module.dependencies
async.forEach(dependencies, (dependency, done) => {
// 和NormalModule.js中定義的dependencies數組push的數據結構類似
this.createModule({
name: dependency.name,
context: dependency.context,
rawRequest: dependency.rawRequest,
moduleId: dependency.moduleId,
resource: dependency.resource,
parser,
}, null, done)
}, callback)
}
// 封裝
seal(callback) {
this.hooks.seal.call()
this.hooks.beforeChunks.call()
// 當前所有入口模塊存放在 compilation對象的entries數組裏
// 封裝chunks指的就是依據某個入口,然後找到他的所有依賴,將他們的源代碼放在一起,之後再做合併
for (const entryModule of this.entries) {
// 分步處理chunk
// 核心步驟:創建模塊加載已有模塊內容,同時記錄模塊信息
const chunk = new Chunk(entryModule)
// 保存chunk信息
this.chunks.push(chunk)
// 給chunk屬性賦值
chunk.modules = this.modules.filter(module => module.name === chunk.name)
}
// chunk流程梳理之後就進入到chunk代碼處理環節,可以根據 模板文件和模塊中的源代碼生成chunk.js
this.hooks.afterChunks.call(this.chunks)
// 生成代碼內容
this.createChunkAssets()
callback()
}
createChunkAssets() {
for (let i = 0; i < this.chunks.length; i++) {
const chunk = this.chunks[i]
const filename = chunk.name + '.js'; // 文件名
chunk.files.push(filename)
// 生成具體的chunk內容
// 獲取模板文件路徑
let templatePath = path.posix.join(__dirname, 'template/main.ejs')
// 讀取模板文件中的內容
let templateCode = this.inputFileSystem.readFileSync(templatePath, 'utf-8')
// 獲取渲染函數
let templateRender = ejs.compile(templateCode)
// 根據ejs語法渲染數據
let source = templateRender({
entryModuleId: chunk.entryModule.moduleId,
modules: chunk.modules
})
console.log('filename', filename, source)
// 輸出到文件
this.emitAssets(filename, source)
}
}
emitAssets(fileName, source) {
this.assets[fileName] = source
this.files.push(fileName)
}
}
module.exports = Compilation
Compiler.js文件實現compiler實例化,掛載鈎子,寫入文件,實現run方法。
const {
Tapable,
AsyncSeriesHook,
SyncBailHook,
SyncHook,
AsyncParallelHook
} = require('tapable')
const Stats = require('./Stats')
const path = require('path')
const mkdirp = require('mkdirp')
const Compilation = require('./Compilation')
const NormalModuleFactory = require('./NormalModuleFactory')
const {
emit
} = require('process')
class Compiler extends Tapable {
constructor(context) {
super()
this.context = context
// 源碼中的鈎子會有很多
this.hooks = {
done: new AsyncSeriesHook(["stats"]),
entryOption: new SyncBailHook(["context", "entry"]),
beforeRun: new AsyncSeriesHook(["compiler"]),
run: new AsyncSeriesHook(["compiler"]),
thisCompilation: new SyncHook(["compilation", "params"]),
compilation: new SyncHook(["compilation", "params"]),
beforeCompile: new AsyncSeriesHook(["params"]),
compile: new SyncHook(["params"]),
make: new AsyncParallelHook(["compilation"]),
afterCompile: new AsyncSeriesHook(["compilation"]),
emit: new AsyncSeriesHook(['compilation'])
}
}
newCompilationParams() {
const params = {
normalModuleFactory: new NormalModuleFactory()
}
return params
}
createCompilation() {
return new Compilation(this)
}
newCompilation(params) {
const compilation = this.createCompilation()
this.hooks.thisCompilation.call(compilation, params)
this.hooks.compilation.call(compilation, params)
return compilation
}
compile(callback) {
const params = this.newCompilationParams()
this.hooks.beforeRun.callAsync(params, err => {
this.hooks.compile.call(params)
const compilation = this.newCompilation(params)
this.hooks.make.callAsync(compilation, err => {
// 觸發SingleEntryPlugin埋的鈎子
// console.log('make hook run')
// callback(err, compilation)
// 這裏開始處理chunk
compilation.seal(err => {
this.hooks.afterCompile.callAsync(compilation, err => {
callback(err, compilation)
})
})
})
})
}
emitAssets(compilation, callback) {
// 創建dist,在目錄創建完成之後完成文件的寫操作
// 定義一個工具方法用於執行文件的生成操作
const emitFiles = err => {
const assests = compilation.assets // 鍵值對結構,鍵是文件名稱,值是文件代碼
// 根據webpack.js中實例化時創建的結構拿到相應信息
let outputPath = this.options.output.path
for (let file in assests) {
let source = assests[file]
let targetPath = path.posix.join(outputPath, file)
console.log(targetPath, source)
this.outputFileSystem.writeFileSync(targetPath, source, 'utf-8')
}
callback(err)
}
// 創建目錄後啓動文件寫入
this.hooks.emit.callAsync(compilation, err => {
mkdirp.sync(this.options.output.path)
emitFiles()
})
}
run(callback) {
console.log('run function')
const finalCallback = function (err, stats) {
callback(err, stats)
}
const onCompilied = (err, compilation) => {
// console.log('onCompiled function')
// finalCallback(err, new Stats(compilation))
// finalCallback(err, {
// toJson() {
// return {
// entries: [], // 當前打包入口信息
// chunks: [], // 當前打包代碼塊信息
// modules: [], // 模塊信息
// assets: [], // 打包生成的資源
// }
// }
// })
// 這裏將處理好的chunk寫入到指定的dist目錄
this.emitAssets(compilation, err => {
let stats = new Stats(compilation)
finalCallback(err, stats)
})
}
this.hooks.beforeRun.callAsync(this, (err) => {
this.hooks.run.callAsync(this, err => {
this.compile(onCompilied)
})
})
// callback(null, {
// toJson() {
// return {
// entries: [], // 當前打包入口信息
// chunks: [], // 當前打包代碼塊信息
// modules: [], // 模塊信息
// assets: [], // 打包生成的資源
// }
// }
// })
}
}
module.exports = Compiler
EntryOptionPlugin和SingleEntryPlugin給文件打包入口添加監聽鈎子
const SingleEntryPlugin = require("./SingleEntryPlugin")
const itemToPlugin = function (context, item, name) {
return new SingleEntryPlugin(context, item, name)
}
class EntryOptionPlugin {
apply(compiler) {
// 添加鈎子監聽
compiler.hooks.entryOption.tap('EntryOptionPlugin', (context, entry) => {
itemToPlugin(context, entry, 'main').apply(compiler)
})
}
}
module.exports = EntryOptionPlugin
class SingleEntryPlugin {
constructor(context, entry, name) {
this.context = context
this.entry = entry
this.name = name
}
apply(compiler) {
// add hook
compiler.hooks.make.tapAsync('SingleEntryPlugin', (compilation, callback) => {
const {
context,
entry,
name
} = this
console.log('make hook run')
compilation.addEntry(context, entry, name, callback)
})
}
}
module.exports = SingleEntryPlugin
NodeEnvironmentPlugin.js文件主要給compiler掛載node讀寫文件功能
const fs = require('fs')
class NodeEnvironmentPlugin {
constructor(options) {
this.options = options || {}
}
apply(complier) {
// 源碼中還有處理日誌的功能,這裏暫不需要,這裏只需要使compiler具備文件讀寫能力即可
complier.inputFileSystem = fs
complier.outputFileSystem = fs
}
}
module.exports = NodeEnvironmentPlugin
NormalModule模塊處理源碼轉換成AST語法樹,替換一些關鍵字段,之後轉換成可執行代碼
const path = require('path')
const types = require('@babel/types')
const generator = require('@babel/generator').default // ast轉換成代碼
const traverse = require('@babel/traverse').default
class NormalModule {
constructor(data) {
this.context = data.context
this.name = data.name
this.rawRequest = data.rawRequest
this.moduleId = data.moduleId // createMdoule參數而來
this.parser = data.parser // 等待完成
this.resource = data.resource
this._source // 存放某個模塊的源代碼
this._ast // 存放某個模塊源代碼對於AST
this.dependencies = [] // 定義一個空數組用於保存被依賴加載的模塊信息
}
build(compilation, callback) {
// 從文件中讀取到將來需要被加載的module內容
// 如果當前不是js模塊則需要Loader進行處理,最終返回js模塊
// 上述的操作完成之後就可以將js代碼轉換為AST語法樹
// 當前js模塊內部可能又引用了很多其他的模塊,因此我們需要遞歸完成,前面的完成之後,重複執行即可
this.doBuild(compilation, err => {
this._ast = this.parser.parse(this._source)
// 這裏的_ast就是當前的module的語法樹,我們可以對它進行修改,最後再將ast轉換為code代碼
traverse(this._ast, {
CallExpression: (nodepath) => {
let node = nodepath.node
// 定位require所在的節點
if (node.callee.name === 'require') {
// 獲取原始請求路徑
let modulePath = node.arguments[0].value // './extra
// 獲取當前被加載的模塊名稱
let moduleName = modulePath.split(path.posix.sep).pop() // extra
// 當前打包器只處理js
let extName = moduleName.indexOf('.') === -1 ? '.js' : ''
moduleName += extName // extra.js
// 最終我們想要讀取當前js裏面的內容,我們需要一個絕對路徑
let depResource = path.posix.join(path.posix.dirname(this.resource), moduleName)
// 將當前的模塊id定義
let depmoduleID = './' + path.posix.relative(this.context, depResource) // 得到了'./src/extra.js'
console.log(depmoduleID) // ./src/extra
// 記錄當前被依賴的模塊信息,方便後面遞歸加載
this.dependencies.push({
name: this.name, // 將來需要動態修改
context: this.context,
rawRequest: moduleName,
moduleId: depmoduleID,
resource: depResource
})
// 替換內容,1,require-> __webpack_require__
node.callee.name = "__webpack_require__"
node.arguments = [types.stringLiteral(depmoduleID)]
}
}
})
// 將修改後的ast轉換為可執行code
let {
code
} = generator(this._ast)
this._source = code
callback(err)
})
}
doBuild(compilation, callback) {
this.getSource(compilation, (err, source) => { // 獲取源碼
// 處理讀到的文件內容
this._source = source
callback()
})
}
getSource(compilation, callback) {
compilation.inputFileSystem.readFile(this.resource, 'utf-8', callback)
}
}
module.exports = NormalModule
NormalModuleFactory工廠類,創建NormalModule實例
const NormalModule = require('./NormalModule')
class NormalModuleFactory {
create(data) {
return new NormalModule(data)
}
}
module.exports = NormalModuleFactory
Parser編譯工具類
const {
Tapable
} = require('tapable')
const babylon = require('babylon')
class Parser extends Tapable {
parse(source) {
return babylon.parse(source, {
sourceType: 'module', // 代表是一個模塊
plugins: ['dynamicImport'], // 當前插件可以支持import()動態導入的語法
})
}
}
module.exports = Parser
SingleEntryPlugin單文件打包入口,埋入鈎子
class SingleEntryPlugin {
constructor(context, entry, name) {
this.context = context
this.entry = entry
this.name = name
}
apply(compiler) {
// add hook
compiler.hooks.make.tapAsync('SingleEntryPlugin', (compilation, callback) => {
const {
context,
entry,
name
} = this
console.log('make hook run')
compilation.addEntry(context, entry, name, callback)
})
}
}
module.exports = SingleEntryPlugin
Stats提取compilation主要字段給寫入文件使用
class Stats {
constructor(compilation) {
this.entries = compilation.entries
this.modules = compilation.modules
this.chunks = compilation.chunks
this.files = compilation.files
}
toJson() {
return this
}
}
module.exports = Stats
WebpackOptionsApply文件打包執行
const EntryOptionPlugin = require("./EntryOptionPlugin")
class WebpackOptionsApply {
process(options, compiler) {
new EntryOptionPlugin().apply(compiler)
compiler.hooks.entryOption.call(options.context, options.entry)
}
}
module.exports = WebpackOptionsApply
webpack.js實現webpack流程主要步驟
const Compiler = require("./Compiler")
const NodeEnvironmentPlugin = require('./node/NodeEnvironmentPlugin')
const webpack = function (options) {
// 實例化 compiler 對象
let compiler = new Compiler(options.context)
compiler.options = options
// 初始化 NodeEnvironmentPlugin(讓compiler具體文件讀寫能力)
new NodeEnvironmentPlugin().apply(compiler)
// 掛載所有 plugins 插件至 compiler 對象身上
if (options.plugins && Array.isArray(options.plugins)) {
for (const plugin of options.plugins) {
plugin.apply(compiler)
}
}
// 掛載所有 webpack 內置的插件(入口)
compiler.options = new WebpackOptionsApply().process(options, compiler)
// 最後返回
return compiler
}
module.exports = webpack
webpack.js處理打包主流程
const Compiler = require("./node/Compiler")
const NodeEnvironmentPlugin = require('./node/NodeEnvironmentPlugin')
const WebpackOptionsApply = require("./node/WebpackOptionsApply")
const webpack = function (options) {
// 實例化 compiler 對象
let compiler = new Compiler(options.context)
compiler.options = options
// 初始化 NodeEnvironmentPlugin(讓compiler具體文件讀寫能力)
new NodeEnvironmentPlugin().apply(compiler)
// 掛載所有 plugins 插件至 compiler 對象身上
if (options.plugins && Array.isArray(options.plugins)) {
for (const plugin of options.plugins) {
plugin.apply(compiler)
}
}
// 掛載所有 webpack 內置的插件(入口)
new WebpackOptionsApply().process(options, compiler)
// 最後返回
return compiler
}
module.exports = webpack
測試代碼
let webpack = require('./myPack')
let options = require('./webpack.config.js')
let complier = webpack(options)
complier.run((err, stats) => {
console.log(err)
console.log(stats)
})
運行測試代碼,執行通過,符合預期