摘要:執行完成后會返回如下圖的結果,根據返回數據把源碼和存儲在的屬性上的回調函數中調用類生成,并根據生成依賴后回調方法返回類。
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:
Entry chunk: 包含runtime code 的,就是開發模式下編譯出的有很長的/******/的部分 (是bundle)
Initial chunk:同步加載,不包含runtime code 的。(可能和entry chunk打包成一個bundle,也可能分離成多個bundle)
Normal chunk:延遲加載/異步 的module
chunk的依賴圖算法
https://medium.com/webpack/th...
Compiler 讀取配置,創建Compilation
Compiler創建Graph的過程:
Compilation讀取資源入口
NMF(normal module factory)
Resolver 解析
輸出NM
Parser 解析 AST
js json 用acorn
其他用Loader (執行loader runner)
如果有依賴, 重復步驟 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:()=>{},//循環鉤子 })
更多示例 https://juejin.im/post/5abf33...
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_requirefunction __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. (" + 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
CompilerCompiler源碼
compiler.hooksclass 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 {Mapcompiler.prototype.run(callback)執行過程} */ this.contextTimestamps /** @type {Map } */ 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
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(" ")) 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路徑前面的感嘆號作用:
! 禁用preLoaders (代碼檢查和測試,不生成module)
!! 禁用所有Loaders
-!禁用preLoaders和loaders,但不是postLoaders
前面提到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() 將配置值轉換為標準化的test對象,其上還會存儲一個this.references屬性
實例方法exec() 每次創建一個新的NormalModule時都會調用RuleSet實例的.exec()方法,只有當通過了各類測試條件,才會將該loader push到結果數組中。
references {map} key是loader在配置中的類型和位置,例如,ref-2表示loader配置數組中的第三個。
pitch & normal同一匹配(test)資源有多loader的時候:(類似先捕獲,再冒泡)
先順序loader.pitch()(源碼里是PitchingLoaders 不妨稱為 pitch 階段)
再倒序loader()(源碼里是NormalLoaders 不妨稱為 normal 階段).
這兩個階段(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,得到 Module pug-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"u003Easdu003Cu002Fdivu003E";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) //asd
這個入口文件 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
Asset Size Chunks Chunk Names main.js 12.9 KiB main [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(負責添加到頁面)
得到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
Asset Size Chunks Chunk Names main.js 24.3 KiB main [emitted] main Entrypoint main = main.js [0] multi ./src/index.js 28 bytes {main} [built] [./node_modules/_css-loader@2.1.1@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解析:bundle loader , style-loader , css-loader , file-loader, url-loader
happypack
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() 將目標module當做loaderContext的一個依賴,添加該模塊為依賴和讀取模塊內容
iterateNormalLoaders()遞歸執行normal,和pitch的流程大同小異,需要注意的是順序是反過來的,從后往前。,loaderIndex--
在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 Shakingwebpack 通過靜態語法分析,找出了不用的 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過程。
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:存放輸入輸出配置+編譯器Parser對象
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 退出時,就會直接跳轉到本步驟,插件可以在本事件中獲取到具體的錯誤原因。
參考資料webpack loader 機制源碼解析
【webpack進階】你真的掌握了loader么?- loader十問
webpack源碼解析
webpack tapable 原理詳解
webpack4源碼分析
隨筆分類 - webpack源碼系列
webpack the confusing parts
細說 webpack 之流程篇
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/103877.html
摘要:所有注冊的處理方法并行執行,相互獨立互不干擾。倘若某一個處理函數報錯,則執行傳入的,后續的處理函數將不被執行,否則最后一個處理函數調用。順序由注冊插件順序決定,而不是由函數的執行時間。如果為此名稱注冊了插件,則返回。 Tapable https://github.com/webpack/ta...https://segmentfault.com/a/11... var Tapable ...
摘要:前端安全的分類基本概念和縮寫跨站請求偽造,英文名,縮寫。攻擊原理不可缺少的兩大因素用戶一定在注冊網站登陸過網站的某一接口有漏洞引誘鏈接會自動攜帶,不會自動攜帶。 1.前端安全的分類 CSRF XSS 2.CSRF 基本概念和縮寫:跨站請求偽造,英文名Cross-site request forgery,縮寫CSRF。攻擊原理:showImg(https://segmentfault...
摘要:原型方法通過原型方法方法來掛載實例。當響應式屬性發生變化時,會通知依賴列表中的對象進行更新。此時,對象執行方法,重新渲染節點。在執行過程中,如果需要讀取響應式屬性,則會觸發響應式屬性的。總結響應式屬性的原理 vue實例 初始化 完成以后,接下來就要進行 掛載。 vue實例掛載,即為將vue實例對應的 template模板,渲染成 Dom節點。 原型方法 - $mount ? 通過原...
閱讀 1993·2021-11-24 10:45
閱讀 1849·2021-10-09 09:43
閱讀 1291·2021-09-22 15:38
閱讀 1219·2021-08-18 10:19
閱讀 2837·2019-08-30 15:55
閱讀 3056·2019-08-30 12:45
閱讀 2960·2019-08-30 11:25
閱讀 356·2019-08-29 11:30