webpack原理梳理
webpack設計模式
一切資源皆Module
Module
(模組)是webpack的中的關鍵實體。Webpack 會從配置的 Entry 開始遞迴找出所有依賴的模組. 通過Loaders
(模組轉換器),用於把模組原內容按照需求轉換成新模組內容.
事件驅動架構
webpack整體是一個事件驅動架構,所有的功能都以Plugin
(外掛)的方式整合在構建流程中,通過釋出訂閱事件來觸發各個外掛執行。webpack核心使用tapable來實現Plugin
(外掛)的註冊和呼叫,Tapable
是一個事件釋出(tap)訂閱(call)庫
概念
Graph模組之間的Dependency
(依賴關係)構成的依賴圖
Compiler(Tapable
例項)訂閱了webpack最頂層的生命週期事件
Complilation(Tapable
例項)該物件由Compiler
建立, 負責構建Graph,Seal,Render...是整個工作流程的核心生命週期,包含Dep Graph 遍歷演算法,優化(optimize),tree shaking...
Compiler 和 Compilation 的區別在於:Compiler 代表了整個 Webpack 從啟動到關閉的生命週期,而 Compilation 只是代表了一次新的編譯。
Resolver(Tapable
例項)資源路徑解析器
ModuleFactory(Tapable
例項) 被Resolver
成功解析的資源需要被這個工廠類被例項化成Module
Parser(Tapable
例項) 負責將Module
(ModuleFactory
例項化來的)轉AST的解析器 (webpack 預設用acorn),並解析出不同規範的require/import 轉成Dependency(依賴)
Template模組化的模板. Chunk,Module,Dependency都有各自的模組模板,來自各自的工廠類的例項
bundle
和chunk
區別:https://github.com/webpack/we...
bundle
:由多個不同的模組打包生成生成最終的js檔案,一個js檔案即是1個bundle。
chunk
: Graph的組成部分。一般有n個入口=n個bundle=graph中有n個chunk。但假設由於n個入口有m個公共模組會被重複打包,需要分離,最終=n+m個bundle=graph中有n+m個chunk
有3類chunk:
/******/
chunk的依賴圖演算法
https://medium.com/webpack/th...整個工作流程
-
Compiler
讀取配置,建立Compilation
-
Compiler
建立Graph
的過程:-
Compilation
讀取資源入口 -
NMF(normal module factory)
Resolver
-
Parser
解析 ASTLoader
- 如果有依賴, 重複步驟 2
-
-
Compilation
優化Graph
-
Compilation
渲染Graph
-
根據Graph上的各類模組用各自的Template渲染
- chunk template
- Dependency template
- ...
- 合成IIFE的最終資源
-
Tapable
鉤子列表
鉤子名 | 執行方式 | 要點 |
---|---|---|
SyncHook | 同步序列 | 不關心監聽函式的返回值 |
SyncBailHook | 同步序列 |
只要監聽函式中有一個函式的返回值不為null
,則跳過剩下所有的邏輯 |
SyncWaterfallHook | 同步序列 | 上一個監聽函式的返回值可以傳給下一個監聽函式 |
SyncLoopHook | 同步迴圈 |
當監聽函式被觸發的時候,如果該監聽函式返回true
時則這個監聽函式會反覆執行,如果返回undefined
則表示退出迴圈 |
AsyncParallelHook | 非同步併發 | 不關心監聽函式的返回值 |
AsyncParallelBailHook | 非同步併發 |
只要監聽函式的返回值不為null
,就會忽略後面的監聽函式執行,直接跳躍到callAsync
等觸發函式繫結的回撥函式,然後執行這個被繫結的回撥函式 |
AsyncSeriesHook | 非同步序列 |
不關心callback
的引數 |
AsyncSeriesBailHook | 非同步序列 |
callback()
的引數不為null
,就會直接執行callAsync
等觸發函式繫結的回撥函式 |
AsyncSeriesWaterfalllHook | 非同步序列 |
上一個監聽函式中的callback(err,data)
的第二個引數,可以作為下一個監聽函式的引數 |
示例
//建立一個釋出訂閱中心 let Center=new TapableHook() //註冊監聽事件 Center.tap('eventName',callback) //觸發事件 Center.call(...args) //註冊攔截器 Center.intercept({ context,//事件回撥和攔截器的共享資料 call:()=>{},//鉤子觸發前 register:()=>{},//新增事件時 tap:()=>{},//執行鉤子前 loop:()=>{},//迴圈鉤子 })
Module
它有很多子類:RawModule, NormalModule ,MultiModule,ContextModule,DelegatedModule,DllModule,ExternalModule 等
ModuleFactory: 使用工廠模式建立不同的Module,有四個主要的子類: NormalModuleFactory,ContextModuleFactory , DllModuleFactory,MultiModuleFactory
Template
-
mainTemplate 和chunkTemplate
if(chunk.entry) { source = this.mainTemplate.render(this.hash, chunk, this.moduleTemplate, this.dependencyTemplates); } else { source = this.chunkTemplate.render(chunk, this.moduleTemplate, this.dependencyTemplates); }
-
不同模組規範封裝
MainTemplate.prototype.requireFn = "__webpack_require__"; MainTemplate.prototype.render = function(hash, chunk, moduleTemplate, dependencyTemplates) { var buf = []; // 每一個module都有一個moduleId,在最後會替換。 buf.push("function " + this.requireFn + "(moduleId) {"); buf.push(this.indent(this.applyPluginsWaterfall("require", "", chunk, hash))); buf.push("}"); buf.push(""); ... // 其餘封裝操作 };
-
- ModuleTemplate 是對所有模組進行一個程式碼生成
- HotUpdateChunkTemplate 是對熱替換模組的一個處理
webpack_require
function __webpack_require__(moduleId) { // 1.首先會檢查模組快取 if(installedModules[moduleId]) { return installedModules[moduleId].exports; } // 2. 快取不存在時,建立並快取一個新的模組物件,類似Node中的new Module操作 var module = installedModules[moduleId] = { i: moduleId, l: false, exports: {}, children: [] }; // 3. 執行模組,類似於Node中的: // result = compiledWrapper.call(this.exports, this.exports, require, this, filename, dirname); modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); //需要引入模組時,同步地將模組從暫存區取出來執行,避免使用網路請求導致過長的同步等待時間。 module.l = true; // 4. 返回該module的輸出 return module.exports; }
非同步模組載入
__webpack_require__.e = function requireEnsure(chunkId) { var promises = []; var installedChunkData = installedChunks[chunkId]; // 判斷該chunk是否已經被載入,0表示已載入。installChunk中的狀態: // undefined:chunk未進行載入, // null:chunk preloaded/prefetched // Promise:chunk正在載入中 // 0:chunk載入完畢 if(installedChunkData !== 0) { // chunk不為null和undefined,則為Promise,表示載入中,繼續等待 if(installedChunkData) { promises.push(installedChunkData[2]); } else { // 注意這裡installChunk的資料格式 // 從左到右三個元素分別為resolve、reject、promise var promise = new Promise(function(resolve, reject) { installedChunkData = installedChunks[chunkId] = [resolve, reject]; }); promises.push(installedChunkData[2] = promise); // 下面程式碼主要是根據chunkId載入對應的script指令碼 var head = document.getElementsByTagName('head')[0]; var script = document.createElement('script'); var onScriptComplete; script.charset = 'utf-8'; script.timeout = 120; if (__webpack_require__.nc) { script.setAttribute("nonce", __webpack_require__.nc); } // jsonpScriptSrc方法會根據傳入的chunkId返回對應的檔案路徑 script.src = jsonpScriptSrc(chunkId); onScriptComplete = function (event) { script.onerror = script.onload = null; clearTimeout(timeout); var chunk = installedChunks[chunkId]; if(chunk !== 0) { if(chunk) { var errorType = event && (event.type === 'load' ? 'missing' : event.type); var realSrc = event && event.target && event.target.src; var error = new Error('Loading chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')'); error.type = errorType; error.request = realSrc; chunk[1](error); } installedChunks[chunkId] = undefined; } }; var timeout = setTimeout(function(){ onScriptComplete({ type: 'timeout', target: script }); }, 120000); script.onerror = script.onload = onScriptComplete; head.appendChild(script); } } return Promise.all(promises); };
非同步模組快取
// webpack runtime chunk function webpackJsonpCallback(data) { var chunkIds = data[0]; var moreModules = data[1]; var executeModules = data[2]; var moduleId, chunkId, i = 0, resolves = []; // webpack會在installChunks中儲存chunk的載入狀態,據此判斷chunk是否載入完畢 for(;i < chunkIds.length; i++) { chunkId = chunkIds[i]; if(installedChunks[chunkId]) { resolves.push(installedChunks[chunkId][0]); } installedChunks[chunkId] = 0; } // 注意,這裡會進行“註冊”,將模組暫存入記憶體中 // 將module chunk中第二個陣列元素包含的 module 方法註冊到 modules 物件裡 for(moduleId in moreModules) { if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) { modules[moduleId] = moreModules[moduleId]; } } if(parentJsonpFunction) parentJsonpFunction(data); //先根據模組註冊時的chunkId,取出installedChunks對應的所有loading中的chunk,最後將這些chunk的promise進行resolve操作 while(resolves.length) { resolves.shift()(); } deferredModules.push.apply(deferredModules, executeModules || []); return checkDeferredModules(); };
保證chunk載入後才執行模組
function checkDeferredModules() { var result; for(var i = 0; i < deferredModules.length; i++) { var deferredModule = deferredModules[i]; var fulfilled = true; // 第一個元素是模組id,後面是其所需的chunk for(var j = 1; j < deferredModule.length; j++) { var depId = deferredModule[j]; // 這裡會首先判斷模組所需chunk是否已經載入完畢 if(installedChunks[depId] !== 0) fulfilled = false; } // 只有模組所需的chunk都載入完畢,該模組才會被執行(__webpack_require__) if(fulfilled) { deferredModules.splice(i--, 1); result = __webpack_require__(__webpack_require__.s = deferredModule[0]); } } return result; }
Module 被 Loader 編譯的主要步驟
-
webpack的配置options
- 在Compiler.js中會為將使用者配置與預設配置(WebpackOptionsDefaulter )合併,其中就包括了loader的預設配置 module.defaultRules (OptionsDefaulter則是一個封裝的配置項存取器,封裝了一些特殊的方法來操作配置物件)
//lib/webpack.js options = new WebpackOptionsDefaulter().process(options); compiler = new Compiler(options.context); compiler.options = options; /*options:{ entry: {},//入口配置 output: {}, //輸出配置 plugins: [], //外掛集合(配置檔案 + shell指令) module: { loaders: [ [Object] ] }, //模組配置 context: //工程路徑 ... }*/
-
建立Module
- 根據配置建立Module的工廠類Factory(Compiler.js)
- 通過loader的resolver來解析loader路徑
- 使用Factory建立 NormalModule例項
- 使用loaderResolver解析loader模組路徑
- 根據rule.modules建立RulesSet規則集
-
Loader編譯過程(詳見Loader章節 )
- NormalModule例項.build() 進行模組的構建
- loader-runner 執行編譯module
Compiler
compiler.hooks
class Compiler extends Tapable { constructor(context) { super(); this.hooks = { shouldEmit: new SyncBailHook(["compilation"]),//此時返回 true/false。 done: new AsyncSeriesHook(["stats"]),//編譯(compilation)完成。 additionalPass: new AsyncSeriesHook([]), beforeRun: new AsyncSeriesHook(["compiler"]),//compiler.run() 執行之前,新增一個鉤子。 run: new AsyncSeriesHook(["compiler"]),//開始讀取 records 之前,鉤入(hook into) compiler。 emit: new AsyncSeriesHook(["compilation"]),//輸出到dist目錄 afterEmit: new AsyncSeriesHook(["compilation"]),//生成資源到 output 目錄之後。 thisCompilation: new SyncHook(["compilation", "params"]),//觸發 compilation 事件之前執行(檢視下面的 compilation)。 compilation: new SyncHook(["compilation", "params"]),//編譯(compilation)建立之後,執行外掛。 normalModuleFactory: new SyncHook(["normalModuleFactory"]),//NormalModuleFactory 建立之後,執行外掛。 contextModuleFactory: new SyncHook(["contextModulefactory"]),//ContextModuleFactory 建立之後,執行外掛。 beforeCompile: new AsyncSeriesHook(["params"]),//編譯(compilation)引數建立之後,執行外掛。 compile: new SyncHook(["params"]),//一個新的編譯(compilation)建立之後,鉤入(hook into) compiler。 make: new AsyncParallelHook(["compilation"]),//從入口分析依賴以及間接依賴模組 afterCompile: new AsyncSeriesHook(["compilation"]),//完成構建,快取資料 watchRun: new AsyncSeriesHook(["compiler"]),//監聽模式下,一個新的編譯(compilation)觸發之後,執行一個外掛,但是是在實際編譯開始之前。 failed: new SyncHook(["error"]),//編譯(compilation)失敗。 invalid: new SyncHook(["filename", "changeTime"]),//監聽模式下,編譯無效時。 watchClose: new SyncHook([]),//監聽模式停止。 } } }
compiler其他屬性
this.name /** @type {string=} */ this.parentCompilation /** @type {Compilation=} */ this.outputPath = /** @type {string} */ this.outputFileSystem this.inputFileSystem this.recordsInputPath /** @type {string|null} */ this.recordsOutputPath/** @type {string|null} */ this.records = {}; this.removedFiles //new Set(); this.fileTimestamps/** @type {Map<string, number>} */ this.contextTimestamps /** @type {Map<string, number>} */ this.resolverFactory /** @type {ResolverFactory} */ this.options = /** @type {WebpackOptions} */ this.context = context; this.requestShortener this.running = false;/** @type {boolean} */ this.watchMode = false;/** @type {boolean} */ this._assetEmittingSourceCache /** @private @type {WeakMap<Source, { sizeOnlySource: SizeOnlySource, writtenTo: Map<string, number> }>} */ this._assetEmittingWrittenFiles/** @private @type {Map<string, number>} */
compiler.prototype.run(callback)執行過程
- compiler.hooks.beforeRun
- compiler.hooks.run
-
compiler.compile
-
params=this.newCompilationParams 建立NormalModuleFactory,contextModuleFactory例項。
- NMF.hooks.beforeResolve
- NMF.hooks.resolve 解析loader模組的路徑(例如css-loader這個loader的模組路徑是什麼)
- NMF.hooks.factory 基於resolve鉤子的返回值來建立NormalModule例項。
- NMF.hooks.afterResolve
- NMF.hooks.createModule
- compiler.hooks.compile.call(params)
-
compilation = new Compilation(compiler)
- this.hooks.thisCompilation.call(compilation, params)
- this.hooks.compilation.call(compilation, params)
- compiler.hooks.make
-
compilation.hooks.finish
-
compilation.hooks.seal
-
compiler.hooks.afterCompile
return callback(null, compilation)
-
Compilation
Compilation 物件包含了當前的模組資源、編譯生成資源、變化的檔案等。當 Webpack 以開發模式執行時,每當檢測到一個檔案變化,一次新的 Compilation 將被建立。Compilation 物件也提供了很多事件回撥供外掛做擴充套件。通過 Compilation 也能讀取到 Compiler 物件。
承接上文的compilation = new Compilation(compiler)
-
負責組織整個打包過程,包含了每個構建環節及輸出環節所對應的方法
- 如 addEntry() , _addModuleChain() , buildModule() , seal() , createChunkAssets() (在每一個節點都會觸發 webpack 事件去呼叫各外掛)。
- 該物件內部存放著所有 module ,chunk,生成的 asset 以及用來生成最後打包檔案的 template 的資訊。
compilation.addEntry()主要執行過程
-
comilation._addModuleChain()
- moduleFactory = comilation.dependencyFactories.get(Dep)
-
moduleFactory.create()
- comilation.addModule(module)
-
comilation.buildModule(module)
- afterBuild()
compilation.seal()主要執行過程
- comilation.hooks.optimizeDependencies
- 建立chunks
- 迴圈 comilation.chunkGroups.push(entrypoint)
- comilation.processDependenciesBlocksForChunkGroups(comilation.chunkGroups.slice())
- comilation.sortModules(comilation.modules);
- 優化modules
- comilation.hooks.optimizeModules
- 優化chunks
- comilation.hooks.optimizeChunks
- 優化tree
-
comilation.hooks.optimizeTree
- comilation.hooks.optimizeChunkModules
- comilation.sortItemsWithModuleIds
- comilation.sortItemsWithChunkIds
- comilation.createHash
- comilation.createModuleAssets 新增到compildation.assets[fileName]
- comilation.hooks.additionalChunkAssets
- comilation.summarizeDependencies
-
comilation.hooks.additionalAssets
- comilation.hooks.optimizeChunkAssets
- comilation.hooks.optimizeAssets
- comilation.hooks.afterSeal
Plugin
外掛可以用於執行範圍更廣的任務。包括:打包優化,資源管理,注入環境變數
plugin
: 一個具有 apply 方法的 JavaScript 物件。apply 方法會被 compiler 呼叫,並且 compiler 物件可在整個編譯生命週期訪問。這些外掛包通常以某種方式擴充套件編譯功能。
編寫Plugin示例
class MyPlugin{ apply(compiler){ compiler.hooks.done.tabAsync("myPlugin",(stats,cb)=>{ const assetsNames=[] for(let assetName in stats.compilation.assets) assetNames.push(assetName) console.log(assetsNames.join("\n")) cb() }) compiler.hooks.compilation.tap("MyPlugin",(compilation,params)=>{ new MyCompilationPlugin().apply(compilation) }) } } class MyCompilationPlugin{ apply(compilation){ compilation.hooks.additionalAssets.tapAsync('MyPlugin', callback => { download('https://img.shields.io/npm/v/webpack.svg', function(resp) { if(resp.status === 200) { compilation.assets['webpack-version.svg'] = toAsset(resp); callback() } else callback(new Error('[webpack-example-plugin] Unable to download the image')) }); }); } } module.exports=MyPlugin
其他宣告週期hooks和示例https://webpack.docschina.org...
Resolver
在 NormalModuleFactory.js 的resolver.resolve
中觸發
hooks在 WebpackOptionsApply.js的compiler.resolverFactory.hooks
中。
可以完全被替換,比如注入自己的fileSystem
Parser
在 CommonJSPulgin.js的new CommonJsRequireDependencyParserPlugin(options).appply(parser)
觸發,呼叫 CommonJsRequireDependencyParserPlugin.js 的apply(parser)
,負責新增Dependency,Template...
hooks在 CommonJsPlugin.js的normarlModuleFactory.hooks.parser
中
Loader
在make階段build中會呼叫doBuild去載入資源,doBuild中會傳入資源路徑和外掛資源去呼叫loader-runner外掛的runLoaders方法去載入和執行loader。執行完成後會返回如下圖的result結果,根據返回資料把原始碼和sourceMap儲存在module的_source屬性上;doBuild的回撥函式中呼叫Parser類生成AST,並根據AST生成依賴後回撥buildModule方法返回compilation類。
Loader的路徑
NormalModuleFactory 將loader分為preLoader、postLoader和loader三種
對loader檔案的路徑解析分為兩種:inline loader和config檔案中的loader。
require的inline loader路徑前面的感嘆號作用:
! !! -!
前面提到NormalModuleFactory 中的resolver鉤子中會先處理inline loader。
最終loader的順序:post
、inline
、normal
和pre
然而loader是從右至左執行的,真實的loader執行順序是倒過來的,因此inlineLoader是整體後於config中normal loader執行的。
路徑解析之 inline loader
-
正則解析loader和引數
//NormalModuleFactory.js let elements = requestWithoutMatchResource .replace(/^-?!+/, "") .replace(/!!+/g, "!") .split("!");
- 將“解析模組的loader陣列”與“解析模組本身”一起並行執行,用到了neo-async 這個庫(和async庫類似,都是為非同步程式設計提供一些工具方法,但是會比async庫更快。)
-
解析返回結果:
[ // 第一個元素是一個loader陣列 [ { loader: '/workspace/basic-demo/home/node_modules/html-webpack-plugin/lib/loader.js', options: undefined } ], // 第二個元素是模組本身的一些資訊 { resourceResolveData: { context: [Object], path: '/workspace/basic-demo/home/public/index.html', request: undefined, query: '', module: false, file: false, descriptionFilePath: '/workspace/basic-demo/home/package.json', descriptionFileData: [Object], descriptionFileRoot: '/workspace/basic-demo/home', relativePath: './public/index.html', __innerRequest_request: undefined, __innerRequest_relativePath: './public/index.html', __innerRequest: './public/index.html' }, resource: '/workspace/basic-demo/home/public/index.html' } ]
路徑解析之 config loader
-
NormalModuleFactory中有一個
ruleSet
的屬性,相當於一個規則過濾器,會將resourcePath應用於所有的module.rules規則,它可以根據模組路徑名,匹配出模組所需的loader。webpack編譯會根據使用者配置與預設配置,例項化一個RuleSet,它包含:normalizeRule() exec()
-
references {map}
key是loader在配置中的型別和位置,例如,ref-2表示loader配置陣列中的第三個。
pitch & normal
同一匹配(test)資源有多loader的時候:(類似先捕獲,再冒泡)
loader.pitch() loader()
這兩個階段(pitch
和normal
)就是loader-runner中對應的iteratePitchingLoaders()
和iterateNormalLoaders()
兩個方法。
如果某個 loader 在 pitch 方法中return結果,會跳過剩下的 loader。那麼pitch的遞迴就此結束,開始從當前位置從後往前執行normal
normal loaders 結果示例(apply-loader, pug-loader)
//webpack.config.js test: /\.pug/, use: [ 'apply-loader', 'pug-loader', ]
先執行pug-loader,得到 Modulepug-loader/index.js!./src/index.pug
的js程式碼:
var pug = __webpack_require__(/*! pug-runtime/index.js */ "pug-runtime/index.js"); function template(locals) {var pug_html = "", pug_mixins = {}, pug_interp;pug_html = pug_html + "\\u003Cdiv class=\"haha\"\\u003Easd\\u003C\\u002Fdiv\\u003E";return pug_html;}; module.exports = template; //# sourceURL=webpack:///./src/index.pug?pug-loader
再執行apply-loader,得到 Module"./src/index.pug"
的js程式碼:
var req = __webpack_require__(/*! !pug-loader!./src/index.pug */ "pug-loader/index.js!./src/index.pug"); module.exports = (req['default'] || req).apply(req, []) //# sourceURL=webpack:///./src/index.pug?
此時假設在入口檔案./src/index.js
引用
var html =__webpack_require__( './index.pug') console.log(html) //<div class="haha">asd</div>
這個入口檔案 Module 的js程式碼:
module.exports = __webpack_require__(/*! ./src/index.js */"./src/index.js"); //# sourceURL=webpack:///multi_./src/index.js?
build 後可看到控制檯輸出的 1個Chunk,2個Module(1個fs忽略),3箇中間Module和一些隱藏Module
AssetSizeChunksChunk Names main.js12.9 KiBmain[emitted]main Entrypoint main = main.js [0] multi ./src/index.js 28 bytes {main} [built] [1] fs (ignored) 15 bytes {main} [optional] [built] [pug-loader/index.js!./src/index.pug] pug-loader!./src/index.pug 288 bytes {main} [built] [./src/index.js] 51 bytes {main} [built] [./src/index.pug] 222 bytes {main} [built]
pitching loaders 結果示例 (style-loader, css-loader)
pitch:順序執行loader.pitch,例:
//webpack.config.js test: /\.css/, use: [ 'style-loader', 'css-loader', ]
style-loader(負責新增<style>
到頁面)
得到Module./src/a.css
的js程式碼:
// Load styles var content = __webpack_require__(/*! !css-loader/dist/cjs.js!./a.css */ "css-loader/dist/cjs.js!./src/a.css"); if(typeof content === 'string') content = [[module.i, content, '']]; // Transform styles var options = {"hmr":true} options.transform = undefined options.insertInto = undefined; // Add styles to the DOM var update = __webpack_require__(/*! style-loader/lib/addStyles.js */ "style-loader/lib/addStyles.js")(content, options); module.exports = content.locals; //# sourceURL=webpack:///./src/a.css?
build 後可看到控制檯輸出的 1個Chunk,1個最終Module,3箇中間Module,和一些隱藏Module
AssetSizeChunksChunk Names main.js24.3 KiBmain[emitted]main Entrypoint main = main.js [0] multi ./src/index.js 28 bytes {main} [built] [./node_modules/[email protected]@css-loader/dist/cjs.js!./src/a.css] 170 bytes {main} [built] [./src/a.css] 1.12 KiB {main} [built] [./src/index.js] 16 bytes {main} [built] + 3 hidden modules
Loader編譯過程
loader的內部處理流程:流水線機制,即挨個處理每個loader,前一個loader的結果會傳遞給下一個loader。
loader有一些主要的特性:同步&非同步; pitch&normal; context
runLoaders方法呼叫iteratePitchingLoaders去遞迴查詢執行有pich屬性的loader;若存在多個pitch屬性的loader則依次執行所有帶pitch屬性的loader,執行完後逆向執行所有帶pitch屬性的normal的normal loader後返回result,沒有pitch屬性的loader就不會再執行;若loaders中沒有pitch屬性的loader則逆向執行loader;執行正常loader是在iterateNormalLoaders方法完成的,處理完所有loader後返回result;
用 loader 編譯 Module 的主要步驟
-
compilation.addEntry()
方法中呼叫的_addModuleChain()
會執行一系列的模組方法,其中對於未build過的模組,最終會呼叫到NormalModule.doBuild()
方法。 -
loader中的this其實是一個叫
loaderContext
的物件 -
doBuild()
run Loaders後將js程式碼通過acorn轉為AST(原始碼) Parser中生產AST語法樹後呼叫walkStatements方法分析語法樹,根據AST的node的type來遞迴查詢每一個node的型別和執行不同的邏輯,並建立依賴。- loadLoader.js 一個相容性的模組載入器
-
LoaderRunner.js 核心
-
runLoaders()
-
iteratePitchingLoaders()
遞迴執行,並記錄loader的pitch狀態;loaderIndex++;當達到最大的loader序號時,處理實際的module(原始碼) :
//遞迴執行每個loader的pitch函式,並在所有pitch執行完後呼叫processResource if(loaderContext.loaderIndex >= loaderContext.loaders.length) return processResource(options, loaderContext, callback);
processResource() iterateNormalLoaders()
-
-
在pitch中返回值除了跳過餘下loader外,不僅會阻止
.addDependency()
觸發(不將該模組資源新增進依賴),而且無法讀取模組的檔案內容。loader會將pitch返回的值作為“檔案內容”來處理,並返回給webpack。- pitch 與loader本身方法的執行順序
-
runSyncOrAsync()
pitch與normal的實際執行(原始碼)往
context
上添加了async
和callback
函式.當我們編寫loader呼叫
this.async()
或this.callback()
時,會將loader變為一個非同步的loader,並返回一個非同步回撥,還可以直接返回一個Promise。只有isSync標識為true時,才會在loader function執行完畢後立即(同步)回撥callback來繼續loader-runner。
Loader的this物件(LoaderContext)屬性清單
version:number 2//版本 emitWarning(warning: Error)//發出一個警告 emitError(error: Error)//發出一個錯誤 resolve(context: String, request: String, callback: function(err, result: string)),//像 require 表示式一樣解析一個 request getResolve(),//? emitFile(name: string, content: Buffer|string, sourceMap: {...}),//產生一個檔案 rootContext:'/home/seasonley/workplace/webpack-demo',//從 webpack 4 開始,原先的 this.options.context 被改進為 this.rootContext webpack:true,//如果是由 webpack 編譯的,這個布林值會被設定為真(loader 最初被設計為可以同時當 Babel transform 用) sourceMap:false,//是否生成source map _module:[Object:NormalModule], _compilation:[Object:Compilation], _compiler:[Object:Compiler], fs:[Object:CachedInputFileSystem],//用於訪問 compilation 的 inputFileSystem 屬性。 target:'web',//編譯的目標。從配置選項中傳遞過來的。示例:"web", "node" loadModule(request: string, callback: function(err, source, sourceMap, module))],//解析給定的 request 到一個模組,應用所有配置的 loader ,並且在回撥函式中傳入生成的 source 、sourceMap 和 模組例項(通常是 NormalModule 的一個例項)。如果你需要獲取其他模組的原始碼來生成結果的話,你可以使用這個函式。 context: '/home/seasonley/workplace/webpack-demo/src',//模組所在的目錄。可以用作解析其他模組路徑的上下文。 loaderIndex: 0,//當前 loader 在 loader 陣列中的索引。 loaders:Array [ { path: '/home/seasonley/workplace/webpack-demo/src/myloader.js', query: '', options: undefined, ident: undefined, normal: [Function], pitch: undefined, raw: undefined, data: null, pitchExecuted: true, normalExecuted: true, request: [Getter/Setter] } ],//所有 loader 組成的陣列。它在 pitch 階段的時候是可以寫入的。 resourcePath: '/home/seasonley/workplace/webpack-demo/src/index.js',//資原始檔的路徑。 resourceQuery: '',//資源的 query 引數。 async(),//告訴 loader-runner 這個 loader 將會非同步地回撥。返回 this.callback。 callback(err,content,sourceMap,meta),/*一個可以同步或者非同步呼叫的可以返回多個結果的函式。如果這個函式被呼叫的話,你應該返回 undefined 從而避免含糊的 loader 結果。 this.callback( err: Error | null, content: string | Buffer, sourceMap?: SourceMap, meta?: any ); 可以將抽象語法樹AST(例如 ESTree)作為第四個引數(meta),如果你想在多個 loader 之間共享通用的 AST,這樣做有助於加速編譯時間。*/ cacheable(flag),/*設定是否可快取標誌的函式: cacheable(flag = true: boolean) 預設情況下,loader 的處理結果會被標記為可快取。呼叫這個方法然後傳入 false,可以關閉 loader 的快取。 一個可快取的 loader 在輸入和相關依賴沒有變化時,必須返回相同的結果。這意味著 loader 除了 this.addDependency 裡指定的以外,不應該有其它任何外部依賴。*/ addDependency(file),//加入一個檔案作為產生 loader 結果的依賴,使它們的任何變化可以被監聽到。例如,html-loader 就使用了這個技巧,當它發現 src 和 src-set 屬性時,就會把這些屬性上的 url 加入到被解析的 html 檔案的依賴中。 dependency(file),// addDependency的簡寫 addContextDependency(directory),//(directory: string)把資料夾作為 loader 結果的依賴加入。 getDependencies(),// getContextDependencies(),// clearDependencies(),//移除 loader 結果的所有依賴。甚至自己和其它 loader 的初始依賴。考慮使用 pitch。 resource: [Getter/Setter],//request 中的資源部分,包括 query 引數。示例:"/abc/resource.js?rrr" request: [Getter],/*被解析出來的 request 字串。"/abc/loader1.js?xyz!/abc/node_modules/loader2/index.js!/abc/resource.js?rrr"*/ remainingRequest: [Getter],// currentRequest: [Getter],// previousRequest: [Getter],// query: [Getter],/** 如果這個 loader 配置了 options 物件的話,this.query 就指向這個 option 物件。 如果 loader 中沒有 options,而是以 query 字串作為引數呼叫時,this.query 就是一個以 ? 開頭的字串。 使用 loader-utils 中提供的 getOptions 方法 來提取給定 loader 的 option。*/ data: [Getter]//在 pitch 階段和正常階段之間共享的 data 物件。 /* Object.defineProperty(loaderContext, "data", { enumerable: true, get: function() { return loaderContext.loaders[loaderContext.loaderIndex].data; } }); */
編寫Loader
function myLoader(resource) { if(/\.js/.test(this.resource)) return resource+';console.log(`wa js`);'; }; module.exports = myLoader
//webpack.config.js var path = require('path'); module.exports = { mode: 'production', entry: ['./src/index.js'], output: { path: path.resolve(__dirname, './dist'), filename: '[name].js' }, module: { rules: [ { test: /index\.js$/, use: 'bundle-loader' } ] }, resolveLoader: { modules: ['./src/myloader/'], } };
webpack原始碼分析方法
inspect-brk 啟動的時候自動在第一行自動加上斷點
- node --inspect-brk ./node_modules/webpack/bin/webpack.js --config ./webpack.config.js
- chrome輸入 chrome://inspect/
Tree Shaking
webpack 通過靜態語法分析,找出了不用的 export ,把他們改成 free variable(只是把 exports 關鍵字刪除了,變數的宣告並沒有刪除)
Uglify通過靜態語法分析,找出了不用的變數宣告,直接把他們刪了。
Watch
webpack-dev-server
當配置了watch時webpack-dev-middleware 將 webpack 原本的 outputFileSystem 替換成了MemoryFileSystem(memory-fs 外掛) 例項。
MemoryFileSystem 是個抽象的檔案系統庫,webpack將該部分解耦,可進一步設定redis或mongodb作為檔案系統,在多個webpack例項中共享資源
監控
當執行watch時會例項化一個Watching物件,監控和構建打包都是Watching例項來控制;在Watching建構函式中設定變化延遲通知時間(預設200),然後呼叫_go方法;webpack首次構建和後續的檔案變化重新構建都是_執行_go方法,在__go方法中呼叫this.compiler.compile啟動編譯。webpack構建完成後會觸發 _done方法,在 _done方法中呼叫this.watch方法,傳入compilation.fileDependencies和compilation.contextDependencies需要監控的資料夾和目錄;在watch中呼叫this.compiler.watchFileSystem.watch方法正式開始建立監聽。
Watchpack
在this.compiler.watchFileSystem.watch中每次會重新建立一個Watchpack例項,建立完成後監控aggregated事件和觸發this.watcher.watch(files.concat(missing), dirs.concat(missing), startTime)方法,並且關閉舊的Watchpack例項;在watch中會呼叫WatcherManager為每一個檔案所在目錄建立的資料夾建立一個DirectoryWatcher物件,在DirectoryWatcher物件的watch建構函式中呼叫chokidar外掛進行資料夾監聽,並且繫結一堆觸發事件並返回watcher;Watchpack會給每一個watcher註冊一個監聽change事件,每當有檔案變化時會觸發change事件。
在Watchpack外掛監聽的檔案變化後設置一個定時器去延遲觸發change事件,解決多次快速修改時頻繁觸發問題。
觸發
當檔案變化時NodeWatchFileStstem中的aggregated監聽事件根據watcher獲取每一個監聽檔案的最後修改時間,並把該物件存放在this.compiler.fileTimestamps上然後觸發 _go方法去構建。
在compile中會把this.fileTimestamps賦值給compilation物件,在make階段從入口開始,遞迴構建所有module,和首次構建不同的是在compilation.addModule方法會首先去快取中根據資源路徑取出module,然後拿module.buildTimestamp(module最後修改時間)和fileTimestamps中的該檔案最後修改時間進行比較,若檔案修改時間大於buildTimestamp則重新bulid該module,否則遞迴查詢該module的的依賴。
在webpack構建過程中是檔案解析和模組構建比較耗時,所以webpack在build過程中已經把檔案絕對路徑和module已經快取起來,在rebuild時只會操作變化的module,這樣可以大大提升webpack的rebuild過程。
模組熱更新(HMR)機制
https://github.com/lihongxun9...
當完成編譯的時候,就通過 websocket 傳送給客戶端一個訊息(一個 hash 和 一個ok)
向client傳送一條更新訊息 當有檔案發生變動的時候,webpack編譯檔案,並通過 websocket 向client傳送一條更新訊息
//webpack-dev-server/lib/Server.js compiler.plugin('done', (stats) => { // 當完成編譯的時候,就通過 websocket 傳送給客戶端一個訊息(一個 `hash` 和 一個`ok`) this._sendStats(this.sockets, stats.toJson(clientStats)); });
回顧webpack整體詳細流程
webpack主要是使用Compiler和Compilation類來控制webpack的整個生命週期,定義執行流程;他們都繼承了tabpable並且通過tabpable來註冊了生命週期中的每一個流程需要觸發的事件。
webpack內部實現了一堆plugin,這些內部plugin是webpack打包構建過程中的功能實現,訂閱感興趣的事件,在執行流程中呼叫不同的訂閱函式就構成了webpack的完整生命週期。
其中:[event-name]
代表 事件名
[---初始化階段---]
-
初始化引數:webpack.config.js / shell+yargs(optimist) 獲取配置
options
-
初始化 Compiler 例項 (全域性只有一個,繼承自Tapable,大多數面向使用者的外掛,都是首先在 Compiler 上註冊的)
Compiler Watching()
- 初始化 complier上下文,loader和file的輸入輸出環境
-
初始化礎外掛
WebpacOptionsApply()
(根據options) - [entry-option] :讀取配置的 Entrys,為每個 Entry 例項化一個對應的 EntryPlugin,為後面該 Entry 的遞迴解析工作做準備
- [after-plugins] :呼叫完所有內建的和配置的外掛的 apply 方法。
- [after-resolvers] :根據配置初始化完 resolver,resolver 負責在檔案系統中尋找指定路徑的檔案。
- [environment] : 開始應用 Node.js 風格的檔案系統到 compiler 物件,以方便後續的檔案尋找和讀取。
- [after-environment]
[----構建Graph階段 1----]
入口檔案出發,呼叫所有配置的 Loader 對模組進行翻譯,再找出該模組依賴的模組,再遞迴本步驟直到所有入口依賴的檔案都經過了本步驟的處理
- [before-run]
-
[run]啟動一次新的編譯
- 使用資訊`Compiler.readRecords(cb)` - 觸發`Compiler.compile(onCompiled)` (開始構建options中模組) - 建立引數`Compiler.newCompilationParams()`
-
[normal-module-factory] 引入
NormalModule
工廠函式 -
[context-module-factory] 引入
ContextModule
工廠函式 - [before-compile]執行一些編譯之前需要處理的外掛
-
[compile]
- 例項化`compilation`物件 - `Compiler.newCompilation(params)` - `Compiler.createCompilation()`
該物件負責組織整個編譯過程,包含了每個構建環節對應的方法。物件內部保留了對`compile`的引用,供plugin使用,並存放所有modules,chunks,assets(對應entry),template。根據test正則找到匯入,並分配唯一id
- [this-compilation]觸發 compilation 事件之前
- [compilation]通知訂閱的外掛,比如在compilation.dependencyFactories中新增依賴工廠類等操作
[----構建Graph階段 2----]
-
[make]是compilation初始化完成觸發的事件
- 通知在WebpackOptionsApply中註冊的EntryOptionPlugin外掛
-
EntryOptionPlugin外掛使用entries引數建立一個單入口(
SingleEntryDependency
)或者多入口(MultiEntryDependency
)依賴,多個入口時在make事件上註冊多個相同的監聽,並行執行多個入口 -
tapAsync註冊了一個
DllEntryPlugin
, 就是將入口模組通過呼叫compilation.addEntry()
方法將所有的入口模組新增到編譯構建佇列中,開啟編譯流程。 -
隨後在addEntry 中呼叫
_addModuleChain
開始編譯。在_addModuleChain
首先會生成模組,最後構建。在_addModuleChain
中根據依賴查詢對應的工廠函式,並呼叫工廠函式的create來生成一個空的MultModule
物件,並且把MultModule
物件存入compilation的modules中後執行MultModule.build
,因為是入口module,所以在build中沒處理任何事直接呼叫了afterBuild
;在afterBuild
中判斷是否有依賴,若是葉子結點直接結束,否則呼叫processModuleDependencies
方法來查詢依賴 -
上面講述的afterBuild肯定至少存在一個依賴,
processModuleDependencies
方法就會被呼叫;processModuleDependencies
根據當前的module.dependencies
物件查詢該module依賴中所有需要載入的資源和對應的工廠類,並把module和需要載入資源的依賴作為引數傳給addModuleDependencies
方法;在addModuleDependencies
中非同步執行所有的資源依賴,在非同步中呼叫依賴的工廠類的create去查詢該資源的絕對路徑和該資源所依賴所有loader的絕對路徑,並且建立對應的module後返回;然後根據該module的資源路徑作為key判斷該資源是否被載入過,若載入過直接把該資源引用指向載入過的module返回;否則呼叫this.buildModule
方法執行module.build
載入資源;build完成就得到了loader處理過後的最終module了,然後遞迴呼叫afterBuild
,直到所有的模組都載入完成後make階段才結束。 - 在make階段webpack會根據模組工廠(normalModuleFactory)的create去例項化module;例項化moduel後觸發this.hooks.module事件,若構建配置中註冊了DllReferencePlugin外掛,DelegatedModuleFactoryPlugin會監聽this.hooks.module事件,在該外掛裡判斷該moduel的路徑是否在this.options.content中,若存在則建立代理module(DelegatedModule)去覆蓋預設module;DelegatedModule物件的delegateData中存放manifest中對應的資料(檔案路徑和id),所以DelegatedModule物件不會執行bulled,在生成原始碼時只需要在使用的地方引入對應的id即可。
- make結束後會把所有的編譯完成的module存放在compilation的modules陣列中,通過單例模式保證同樣的模組只有一個例項,modules中的所有的module會構成一個圖。
- [before-resolve]準備建立Module
- [factory]根據配置建立Module的工廠類Factory(Compiler.js) 使用Factory建立 NormalModule例項 根據rule.modules建立RulesSet規則集
- [resolver]通過loader的resolver來解析loader路徑
- [resolve]使用loaderResolver解析loader模組路徑
- [resolve-step]
- [file]
- [directory]
- [resolve-step]
- [result]
- [after-resolve]
- [create-module]
- [module]
- [build-module] NormalModule例項.build() 進行模組的構建
- [normal-build-loader] acron對DSL進行AST分析
- [program] 遇到require建立依賴收集;非同步處理依賴的module,迴圈處理依賴的依賴
- [statement]
- [succeed-module]
[---- 優化Graph----]
-
compilation.seal(cb)
根據之前收集的依賴,決定生成多少檔案,每個檔案的內容是什麼. 對每個module和chunk整理,生成編譯後的原始碼,合併,拆分,生成 hash,儲存在compilation.assets,compilation.chunk- [seal]密封已經開始。不再接受任何Module
-
[optimize] 優化編譯. 觸發optimizeDependencies型別的一些事件去優化依賴 (比如tree shaking就是在這個地方執行的)
- 根據入口module建立chunk ,如果是單入口就只有一個chunk,多入口就有多個chunk;
- 根據chunk遞迴分析查詢module中存在的非同步導module,並以該module為節點建立一個chunk,和入口建立的chunk區別在於後面呼叫模版不一樣。
- 所有chunk執行完後會觸發optimizeModules和optimizeChunks等優化事件通知感興趣的外掛進行優化處理。
-
createChunkAssets生產assets給chunk生成hash然後呼叫createChunkAssets來根據模版生成原始碼物件.所有的module,chunk任然儲存的是通過一個個require聚合起來的程式碼,需要通過template產生最後帶有
__webpack__reuqire()
的格式。- createChunkAssets.jpg
- 根據chunks生產sourceMap 使用summarizeDependencies把所有解析的檔案快取起來,最後呼叫外掛生成soureMap和最終的資料
- 把assets中的物件生產要輸出的程式碼 assets是一個物件,以最終輸出名稱為key存放的輸出物件,每一個輸出檔案對應著一個輸出物件
- [after-optimize-assets]資產已經優化
- [after-compile] 一次 Compilation 執行完成。
[---- 渲染Graph----]
- [should-emit] 所有需要輸出的檔案已經生成好,詢問外掛哪些檔案需要輸出,哪些不需要。
Compiler.emitAssets()
-
[emit]
- 按照 output 中的配置項非同步將將最終的檔案輸出到了對應的 path 中
- output:plugin結束前,在記憶體中生成一個compilation物件檔案模組tree,枝葉節點就是所有的module(由import或者require為標誌,並配備唯一moduleId),主枝幹就是所有的assets,也就是我們最後需要寫入到output.path資料夾裡的檔案內容。
-
MainTemplate.render()
和ChunkTemplate.render()
處理入口檔案的module和非首屏需非同步載入的module -
MainTemplate.render()
- 處理不同的模組規範Commonjs,AMD...
- 生成好的js儲存在compilation.assets中
[asset-path]
[after-emit]
[done]
-
if needAdditionalPass
-
needAdditionalPass()
- 回到compiler.run
-
-
else
this.emitRecords(cb)
- 呼叫戶自定義callback
[failed]如果在編譯和輸出流程中遇到異常導致 Webpack 退出時,就會直接跳轉到本步驟,外掛可以在本事件中獲取到具體的錯誤原因。