在上一篇文章中分享了關於運用webpack搭建vue項目的經驗和總結,但還僅僅停留在只是會用webpack搭建腳手架的階段,對webpack原理仍是不怎麼清楚,再加上各大論壇對webpack原理解析的精品文章較少,要麼是一些標題黨,通篇教你如何配置webpack,如何優化;要麼就是通篇copy源碼+簡單註解;固然也有大牛寫的文章,文章雖好,但晦澀難懂,誰讓小弟不才呢。css
種種緣由,決定狠下心研究下webpack的實現原理(真的是難啊)。但我相信,通讀此篇,就算是菜雞,也能對webpack的原理理解透徹。html
好了,閒話很少說,先看看webpack官網對本身的定義,從定義中尋找突破口!let's go~vue
本質上,webpack 是一個現代 JavaScript 應用程序的靜態模塊打包器(module bundler)。當 webpack 處理應用程序時,它會遞歸地構建一個依賴關係圖(dependency graph),其中包含應用程序須要的每一個模塊,而後將全部這些模塊打包成一個或多個 bundle。node
遞歸
、依賴
、生成一個或多個bundle
什麼是遞歸的構建,什麼又是依賴呢?以下圖webpack
模塊A.js引用模塊B.js,模塊B.js引用模塊C.js,此時A、B、C就構成了依賴關係,那爲何要遞歸的構建呢?請問,webpack配置文件是怎麼配置的?git
不論是單entry仍是多entry,配置文件的entry僅僅只有一個或多個入口文件。github
拿上個例子來講,將A.js設置爲entry,此時webpack打包時,就必須把A.js中全部require的模塊打包在一塊兒(B.js),但此時B.js也有依賴(C.js),這時候就必須遞歸的進行解析了(若是依賴中還有依賴,那接着遞歸)。先把C.js與B.js進行打包合併,而後把合併後的代碼與A.js合併,打包生成最終的bundle。web
是否是有點頭緒了?上面的例子僅僅是最爲簡單的分析了webpack是如何從entry解析構建依賴模塊。下面讓咱們從項目中分析下webpack的打包後的代碼。npm
項目結構目錄以下json
├── node_modules
├── src
│ ├── index.js
│ ├── ticket.js
│ ├── price.js
├── webpack.config.js
複製代碼
webpack配置項。
const path = require('path')
module.exports = {
entry: './src/index.js',
output: {
filename: 'min-bundle.js',
path: path.join(__dirname, 'dist')
},
}
複製代碼
模塊代碼
分析webpack打包的bundle文件
三個模塊文件,index.js
爲webpack的入口文件,它依賴了ticket.js
,ticket.js
依賴了price.js
。咱們但願webpack打包生成的min-bundle.js運行後,可以log出 「迪斯尼門票甩賣,門票價格爲299人民幣/人」,執行打包,果不其然。那麼問題來了,webpack怎麼作到的呢?
這就得看生成的min-bundle.js了,爲了更容易理解,將無關代碼儘量刪除後,主要代碼以下:
(臥槽,這都是啥?說好了不貼源碼呢?不要急,我們慢慢分析)發現了嗎?生成的bundle.js其實就是一個自調用函數,參數是一個對象,鍵爲當前項目中的入口文件和其依賴模塊,即./src/index.js
,./src/ticket.js
,./src/price.js
,值是一個函數,就是對應每一個模塊內的代碼,使用eval來執行內部代碼。自調用函數中,函數體內有一個__webpack_require__
函數。下面開始逐步分析:
自調用函數中直接return __webpack_require__('./src/index.js')
,因而開始執行__webpack_require__
函數,__webpack_require__
函數的參數moduleId,在第一次執行時就是項目的入口文件,即./src/index.js
。進入函數體內看看?發現有個 module
對象
const module = {
i: moduleId,
exports: {}
}
該module對象的主要做用是,爲每個模塊提供一個單獨的module對象,module對象內還有一個exports對象
這就使得每一個模塊均可以使用module.exports和exports來對外暴露
複製代碼
接下來開始執行下面這行代碼:modules[moduleId].call(module.exports, module, module.exports, __webpack_require__)
。一臉懵逼沒關係,跟上節奏,下面一步步解釋每行代碼的做用
modules是什麼?就是自調用函數中傳入的參數,也就是上圖中紅框內的對象
那麼moduleId呢?在第一次執行階段爲'./src/index.js'
因此代碼就變成了:modules['./src/index.js']
,什麼意思? 這一步,就是執行modules對象內,鍵爲'./src/index.js'
的函數,怎麼執行呢?this指向指給誰呢? ...call(module.exports, module, module.exports, __webpack_require__)
中,有四個參數
arg1:`module.exports`,明確this指向(由於每一個模塊都會有各自的module對象)
arg2:`module`對象,使得模塊內能夠經過module.exports來對外暴露
arg3:`module.exports`,使得模塊內能夠經過exports來對外暴露
arg4:`__webpack_require__`函數,爲何?既然要執行modules對象內全部的鍵對應的函數
那函數內使用`__webpack_require__()`來進一步添加依賴,這個函數從哪來呢?
就是從這傳進來的,也就是用來遞歸的調用`__webpack_require__`
複製代碼
開始執行 modules對象內鍵爲'./src/index.js'
的函數
function(module, exports, __webpack_require__) {
eval(`const ticket = __webpack_require__("./src/ticket.js");console.log('迪斯尼門票甩賣,' + ticket);`)
}
複製代碼
發現該函數調用了__webpack_require__("./src/ticket.js")
,那豈不是又要走一遍上面的流程? 沒錯,由於index.js
依賴了ticket.js
此時__webpack_require__(moduleId)
的實參就變成'./src/ticket.js'
,仍然重複上面的步驟,當執行modules['./src/ticket.js'].call()
時,就要執行 modules
對象中鍵爲'./src/ticket.js'
的函數了
function(module, exports, __webpack_require__) {
eval(`const price = __webpack_require__("./src/price.js");module.exports = '門票價格爲' + price.content;`)
}
複製代碼
發現該函數又依賴了price.js
,沒招啊,接着遞歸唄
此時__webpack_require__(moduleId)
的實參就變成'./src/price.js'
,仍然重複上面的步驟,執行modules
對象中鍵爲'./src/price.js'
的函數
function(module, exports, __webpack_require__) {
eval(`module.exports = {content: '299人民幣/人'};`)
}
複製代碼
price.js中沒有依賴項,因而直接返回{content: '299人民幣/人'}
price.js
是執行完了,ticket.js
還等着呢,因而開始賦值
未遞歸執行price.js時
eval(` const price = __webpack_require__("./src/price.js"); module.exports = '門票價格爲' + price.content; `)
遞歸執行price.js後
eval(` const price = {content: '299人民幣/人'} module.exports = '門票價格爲' + '299人民幣/人' `)
複製代碼
別急啊老弟,ticket.js
是執行完了,index.js
還等着呢
未遞歸執行ticket.js時
eval(` const ticket = __webpack_require__("./src/ticket.js"); console.log('迪斯尼門票甩賣,' + ticket); `)
遞歸執行ticket.js後
eval(` const ticket = '門票價格爲299人民幣/人' console.log('迪斯尼門票甩賣,' + '門票價格爲299人民幣/人'); `)
複製代碼
此時,全部依賴模塊解析完成,回到最初自調用函數的代碼, return __webpack_require__("./src/index.js")
此時的__webpack_require__("./src/index.js")
已經有告終果,即
'迪斯尼門票甩賣,門票價格爲299人民幣/人'
直接return,大功告成!可喜可賀!
階段總結:webpack將每一個js文件的名稱做爲鍵,該js文件的代碼做爲值,一一存入到對象中做爲參數。而後本身實現了一個
__webpack_require__
函數,經過該函數,遞歸導入依賴關係。
到這裏,分析webpack打包後的bundle.js就告一段落了。上面說了,webpack是把每一個js文件的名稱做爲鍵,該js文件的代碼做爲值,一一存入到對象中做爲參數的。那麼問題來了,它內部是怎麼操做的?若是配置了loader
和plugin
,又是如何處理模塊內的js代碼呢?
下面讓咱們實操一下,實現一個屬於本身的迷你webpack,深入的體會webpack的打包原理,loader原理和插件原理。
新建兩個項目,一個項目是min-pack的主程序,你能夠理解爲webpack,發佈到npm後,供開發者經過 npm install min-pack
後使用;另外一個項目是開發者本身的項目,也就是說,你要用min-pack
,得先有本身的程序代碼啊,否則你讓min-pack
打包誰?
首先新建一個目錄,命名爲min-pack
在根目錄下新建lib
目錄,目錄內新建Compiler.js
,該js用來實現解析打包,稍後會詳細解讀
在根目錄下,新建template
目錄,目錄內新建output.ejs
,使用ejs
模板來生成打包代碼
在該項目下新建bin
目錄,將打包工具主程序放入其中
#!/usr/bin/env node
const path = require('path')
// minpack.config.js 爲開發者本身的項目下的配置文件,webpack4默認是0配置的
// 咱們這裏就不作那麼複雜了,直接指定配置文件爲 minpack.config.js
// 也就是說,你要用個人 min-pack,你項目的根目錄下就必須有 minpack.config.js 配置文件
// 注意: path.resolve 能夠來解析開發者工做目錄下的 minpack.config.js
const config = require(path.resolve('minpack.config.js'))
// 引入打包的主程序代碼 Compiler.js
const Compiler = require('../lib/Compiler')
// 將配置文件傳入Compiler中,並執行start方法,開始打包
new Compiler(config).start()
複製代碼
注意:主程序的頂部應當有:#!/usr/bin/env node
標識,指定程序執行環境爲node
在該項目中的package.json
中配置bin
腳本
"bin": {
"min-pack": "./bin/min-pack.js"
}
// 這樣配置完後,在開發者本身的項目中,就可使用 `min-pack` 來進行打包了。
複製代碼
經過npm link
連接到全局包中,供本地測試使用。測試完成後再發布到npm上,供第三方開發者使用
完成了上述操做,你就能夠在另外一個項目中,也就是開發者要打包的項目裏運行 min-pack
了。但如今的Compiler.js
尚未實現,因此還作不到解析構建,下面讓咱們來實現下打包功能。
上面說了,你要用個人
min-pack
,你的項目根目錄下必須有minpack.config.js
配置文件
Compiler.js
接受傳入的 minpack.config.js
,獲取到配置文件中 entry
對應的值,也就是入口文件,如 ./src/index.js
使用 node 模塊中的 fs.readFileSync 讀取該模塊文件,得到模塊文件的源代碼
將該模塊源代碼轉換爲 AST
語法樹。what?什麼是AST
語法樹?
AST
語法樹,就是爲了讓咱們更高效,更簡潔的對JavaScript代碼進行操做。由於在下面第 4 步中會將模塊源代碼中require
替換成 __webpack_require__
,怎麼替換?難道你讓我寫正則?或是操做字符串?那就太Low了吧將源代碼中的 require
,所有替換成 __webpack_require__
(爲何?)
由於瀏覽器環境並不識別require
語法。你可能就要問了,我項目中全部的依賴都是使用 import A from 'xx'
來導入模塊,使用 export const xx = 1
或 exports default {...}
來導出模塊的,沒使用 require
啊。那麼請問,你是否是使用 babel
來處理js的,babel
內部會把你的 import
轉換爲 require
,把 export
和 export default
轉換爲 exports
。以下圖
再回憶下最開始咱們分析 webpack
打包出的 min-bundle.js
時,能夠發現,該js內部把咱們項目中的入口文件的及其全部依賴內部的require()
所有替換成了 __webpack_require__
,而後本身實現了 __webpack_require__
,該函數內部定義了 module
對象,對象內部有 exports: {}
,因此,你可使用exports或module.exports來導出模塊了,使用 __webpack_require__
來導入模塊。
將模塊文件的 require()
中的參數,也就是模塊文件的依賴模塊路徑,存入數組中,暫且將該數組命名爲 dependencies
將模塊文件的相對路徑,也就是 ./src/xxx.js
做爲鍵,處理後的源代碼做爲值,存儲到一個對象中,暫且把該對象定義爲 modules
。
min-bundle.js
了。它內部是一個自調用函數,該函數的參數就是剛剛定義的 modules
對象,函數體內經過 __webpack_require__
遞歸的調用 modules
對象中的每個鍵對應的值,也就是該鍵對應的源代碼。第一個模塊文件解析完畢,若是該模塊有依賴文件,就要開始解析它的依賴模塊了,怎麼解析呢?第 5 步驟中,將依賴模塊路徑存入到了 dependencies
數組中,ok,遍歷這個數組,遞歸的開始上面第 2 步,直到最後一個模塊沒有依賴模塊,完成遞歸。
此時 modules
,就是以模塊路徑爲鍵,該模塊源代碼爲值的對象,以下圖
如今 modules
也有了,怎麼生成打包代碼呢?別忘了,咱們有一份模板 output.ejs
,看看該模板內部:
min-bundle.js
嗎?咱們要作的,就是在 Compiler.js
內部,將入口文件路徑以及剛剛生成的 modules
對象,使用ejs
模板語法,進行嵌套 嵌套完成後,讀取配置文件中的output
路徑,經過 fs.writeFileSync
,將output.ejs
中的內容寫入到開發者項目中指定的目錄內
完成打包!
總結下,基本思路就是
- 遞歸的查找依賴, 並解析 AST 語法樹, 修改全部依賴的 require 爲__webpack_require__
- 利用 fs 模塊讀取全部的修改後的依賴代碼
- 將每個模塊依賴的相對路徑做爲鍵, 該模塊代碼做爲值, 存放到對象中, 用於生成最後的 bundle 文件
const path = require('path')
const fs = require('fs')
const ejs = require('ejs')
// 解析AST語法樹
const parser = require('@babel/parser')
// 維護整個AST 樹狀態,負責替換,刪除和添加節點
const traverse = require('@babel/traverse').default
// 將AST轉換爲代碼
const generator = require('@babel/generator').default
class Compiler {
constructor(config) {
this.config = config
this.entry = config.entry
// root: 執行 min-pack 指令的目錄的絕對路徑
this.root = process.cwd()
this.modules = {}
}
/** * 打包依賴分析 * @param {Object} modulePath 當前模塊的絕對路徑 */
depAnalyse(modulePath, relativePath) {
let self = this
// 1. 讀取模塊文件的代碼
let source = fs.readFileSync(modulePath, 'utf-8')
// 2. 聲明依賴數組, 存儲當前模塊的全部依賴
let dependencies = []
// 3. 將當前模塊代碼轉爲AST語法
let ast = parser.parse(source)
// 4. 修改 AST 語法樹
traverse(ast, {
CallExpression(p) {
if(p.node.callee.name === 'require') {
p.node.callee.name = '__webpack_require__'
// 提取並處理require()中傳入的文件路徑
p.node.arguments[0].value = './' + path.join('src', p.node.arguments[0].value))
// 處理路徑中的反斜槓 \
p.node.arguments[0].value = p.node.arguments[0].value.replace(/\\+/g, '/')
// 將處理好的當前模塊路徑存入dependencies數組中,用於遞歸調用 depAnalyse 函數
dependencies.push(p.node.arguments[0].value)
}
}
})
// 5. 將處理好的 AST 語法樹轉爲程序代碼
let resultSourceCode = generator(ast).code
// 6. 獲取 執行打包指令目錄的絕對路徑 與 當前模塊的絕對路徑的 相對路徑
let modulePathRelative = this.replaceSlash('./' + path.relative(this.root, modulePath))
// 7. 將 6 中獲取到的相對路徑爲鍵, 當前模塊AST處理後的代碼爲值, 存儲至 this.modules
this.modules[modulePathRelative] = resultSourceCode
dependencies.forEach(dep => {
return this.depAnalyse(path.resolve(this.root, dep), dep)
})
}
/** * 將生成的 this.modules 與獲取模板字符串進行拼接 */
emitFile() {
const templatePath = path.join(__dirname, '../template/output.ejs')
// 讀取模板文件
let template = fs.readFileSync(templatePath, 'utf-8')
// 進行模板渲染
let result = ejs.render(template, {
entry: this.entry,
modules: this.modules
})
// 讀取執行打包的配置文件中的output, 將生成好的 result 寫入配置output指定文件中
let outputPath = path.join(this.config.output.path, this.config.output.filename)
fs.writeFileSync(outputPath, result)
}
start() {
// 1. 依賴分析
this.depAnalyse(path.resolve(this.root, this.entry), this.entry
// 2. 生成最終的打包後的代碼
this.emitFile()
}
}
module.exports = Compiler
複製代碼
上面就是Compiler的代碼實現,完成了該步驟,意味着你的項目代碼就能夠經過
min-pack
進行打包了,趕忙動手嘗試一下吧~
固然,這個僅僅是超級無敵迷你的webpack版,讀到這,你可能忽略了 loader
和 plugin
的存在,也可能有一些疑問,如何在本身寫的 min-pack
中加入相似於 webpack
中的loader
和 plugin
功能呢?
webpack 可使用 loader 來預處理文件。這容許你打包除 JavaScript 以外的任何靜態資源。你可使用 Node.js 來很簡單地編寫本身的 loader
簡單地說,一個loader就是一個js文件,對外暴露一個函數,該函數用來處理模塊代碼,如
上圖中,就是一個既簡單的loader,該loader接受一個參數,這個參數就是 模塊的源代碼,函數體內對源代碼一系列進行操做那麼如何將loader與咱們本身寫好的 Compiler.js
結合呢?
minpack.config.js
也必須配置相應的 rules
,注意哦,rules 中 use 的值,多是字符串,多是對象,也多是數組。module: {
rules: [
{
test: /\.js$/,
use: [
'./loaders/loader1.js',
'./loaders/loader2.js',
'./loaders/loader3.js'
]
}
]
}
複製代碼
Compiler.js
中,讀取配置文件中的 rules
Compiler.js
中 depAnalyse
函數內部,讀取到模塊文件的源代碼,此時將模塊代碼做爲參數,倒序迭代調用全部loader函數(loader的加載順序從右到左,因此調用時也必須倒敘的調用)require
(以前的步驟).....loader
匹配到正確的文件類型,就要調用該loader函數,一個文件有n個loader
匹配到,該文件就會被處理n次,完成後,返回處理後的代碼,這也就是爲何webpack
打包在 loader
這一層上耗時最多的緣由,只有匹配到,就調用loader
函數處理啊,好累啊,有點寫不動了...
plugin可謂是 webpack 生態系統的重要組成部分之一,它同時對外提供了插件接口,可讓開發者直接觸及到編譯過程當中
官方定義:插件可以 鉤入(hook) 到在每一個編譯(compilation)中觸發的全部關鍵事件
簡單理解,插件就是在webpack編譯過程的生命週期鉤子中,進行編碼開發,實現對應功能。也就是你的插件是須要在編譯過程當中的哪個週期中執行,就調用對應的鉤子函數,在該鉤子內部,實現功能
附上webpack編譯時compiler的生命週期鉤子
疑問: webpack不是打包器嗎?爲何要有生命週期呢?它又是如何實現生命週期的?
經過上面 Compiler.js
中 loader
的實現,不難看出,webpack
的編譯流程就好像一條流水線,每個編譯 階段的就像是一個流水線工人對其進行加工,A加工完交給B,B加工完交給C...每一個工人的職責都是單一的,直到加工完成。
如今我有一個礦泉水加工廠,讓咱們看看一瓶水是怎麼生產出來的:
loader
,每一瓶水都要通過過濾器過濾JS css
代碼壓縮(uglifyjs, mini-css-extract-plugin)。咦,這不就是插件嗎?html-webpack-plugin
插件,將 bundle.js
自動引入生產的html中。咦,這不也是插件嗎?如今有個問題,加工礦泉水的機器,是怎麼知道何時殺菌,何時裝瓶,何時貼廣告呢? 同理 webpack
。
其實,webpack內部,經過
Tapable
這個小型 library ,有了它就能夠經過事件流的形式,將各個生成線串聯起來,其核心原理採用了發佈訂閱者的模式。Tapable
提供了一系列同步和異步鉤子,webpack
使用這些鉤子,定義本身的生命週期。webpack 在運行過程當中,在不一樣階段,發佈相應的事件,插件內部只須要訂閱你須要使用的事件,webpack編譯到了該階段時,會去執行你插件中訂閱事件的回調函數。
一臉懵逼?不要緊,讓咱們接着回到上一個例子中
webpack
中的插件,也會按照順序執行,個人代碼先通過A插件處理,處理完後把處理後的代碼交給B插件。那插件順序誰寫的?固然是你咯,因此,在使用插件時,必須知道每一個插件是作什麼的,而後按順序調用插件。是否是對插件的運行機制有所瞭解了?別急,讓咱們在本身實現的 min-pack
中利用 Tapable
這個庫,實現一個插件。
首先安裝 tapable
,如何使用 tapable
?傳送門
而後在 Compiler
類中,定義生命週期
class Compiler {
constructor(config) {
this.config = config
this.entry = config.entry
// root: 執行 min-pack 指令的目錄的絕對路徑
this.root = process.cwd()
this.hooks = {
start: new SyncHook(), // min-pack開始編譯鉤子
compile: new SyncHook(["relativePath"]), // 編譯中的鉤子 能夠知道當前編譯的模塊名
afterCompile: new SyncHook(), // 所有編譯完成鉤子
emit: new SyncHook(["filename"]), // 開始打包bundle.js鉤子
afterEmit: new SyncHook(["outputPath"]), // 打包bundle.js結束鉤子
done: new SyncHook() // min-pack編譯結束鉤子
}
this.modules = {}
}
}
複製代碼
上面,咱們定義了6個生命週期鉤子,那在何時發佈呢?
發佈生命週期鉤子
start() {
// 總體編譯開始鉤子(start)
this.hooks.start.call()
// 正在編譯鉤子(compile)
this.hooks.compile.call()
// 主編譯函數 開始編譯
this.depAnalyse(path.resolve(this.root, this.entry), this.entry)
// 編譯結束鉤子(afterCompile)
this.hooks.afterCompile.call()
// 總體編譯完成鉤子(done)
this.hooks.done.call()
}
複製代碼
在 函數內,發佈 emit 和 afterEmit 鉤子,具體代碼在上面講解過,此處省略部分代碼
emitFile() {
// ......此處省略代碼
// 開始打包bundle.js鉤子(emit)
this.hooks.emit.call(this.config.output.filename)
// fs 寫入文件(生成bundle.js)
fs.writeFileSync(outputPath, result)
// 打包bundle.js結束鉤子(afterEmit)
this.hooks.afterEmit.call(outputPath)
}
複製代碼
ok,咱們的生命週期有了,也在指定的階段發佈了相應的事件了,接下來幹嗎?寫插件啊!終於能寫一個屬於本身的插件了。
webpack
,因此並無 Compilation
對象,嗯?第一次據說,什麼是 Compilation
?稍後解釋。helloWorld
級別的,那就將他暫時命名爲 HelloWorldPlugins
吧HelloWorldPlugins
插件怎麼寫一個webpack插件呢? 官方定義:
webpack 插件由如下組成:
- 一個 JavaScript 命名函數。
- 在插件函數的 prototype 上定義一個 apply 方法。
- 指定一個綁定到 webpack 自身的事件鉤子。
- 處理 webpack 內部實例的特定數據。
- 功能完成後調用 webpack 提供的回調。
補充下第3條,並不必定只能是一個,當你的插件中須要在不一樣階段作不一樣操做時,也能夠綁定多個事件鉤子,只不過不推薦罷了,最好一個插件單獨作一個功能。
看代碼~
module.exports = class HelloWorldPlugins {
// apply方法
apply(compiler) {
// 指定一個(這個插件中爲多個)綁定到 webpack 自身的事件鉤子。
// 訂閱 start 鉤子
compiler.hooks.start.tap('HelloWorldPlugin', () => {
console.log('webpack開始編譯')
});
// 訂閱 compile 鉤子
compiler.hooks.compile.tap('HelloWorldPlugin', () => {
console.log('編譯中')
});
// 訂閱 afterCompile 鉤子
compiler.hooks.afterCompile.tap('HelloWorldPlugin', () => {
console.log('webpack編譯結束')
});
// 訂閱 emit 鉤子
compiler.hooks.emit.tap('HelloWorldPlugin', (filename) => {
console.log('開始打包文件,文件名爲: ', filename)
});
// 訂閱 afterEmit 鉤子
compiler.hooks.afterEmit.tap('HelloWorldPlugin', (path) => {
console.log('文件打包結束,打包後文件路徑爲: ', path)
});
// 訂閱 done 鉤子
compiler.hooks.done.tap('HelloWorldPlugin', () => {
console.log('webpack打包結束')
})
}
}
複製代碼
運行後看看日誌:
到此,咱們的 HelloWorldPlugins
插件就寫完了,由於沒有 Compilation
對象,因此並不能作什麼炫酷的功能,旨在理解webpack插件的運行原理便可。其實要寫一個真正的webpack插件也很簡單
一個函數->調用apply
方法->訂閱事件鉤子->寫你的程序代碼->調用 webpack
提供的回調
上面留個個疑問,什麼是Compilation,對於 Compiler
和 Compilation
的區別,網上也有不少文章,其實很簡單
compiler
對象表示不變的webpack環境,是針對webpack的,包括了options,loaders,plugins等信息,能夠理解爲 webpack
的實例,也就是咱們本身寫的 Compiler
類compilation
對象則是針對隨時可變的項目文件,即每一次編譯的過程,只要文件有改動,compilation
就會被從新建立。能夠經過 compilation.assets
來獲取全部須要輸出的資源文件,compilation
也能獲取到 compiler
對象。到此,webpack原理分析就告一段落了,能讀到這裏,我相信你對webpack的原理有了更深層次的理解,文章篇幅較多,若有不足之處,還請多多指正。github源碼地址webpack源碼剖析