FE.SRC-webpack原理梳理

webpack設計模式

一切資源皆Module

Module(模塊)是webpack的中的關鍵實體。Webpack 會從配置的 Entry 開始遞歸找出全部依賴的模塊. 經過Loaders(模塊轉換器),用於把模塊原內容按照需求轉換成新模塊內容.css

事件驅動架構

webpack總體是一個事件驅動架構,全部的功能都以Plugin(插件)的方式集成在構建流程中,經過發佈訂閱事件來觸發各個插件執行。webpack核心使用tapable來實現Plugin(插件)的註冊和調用,Tapable是一個事件發佈(tap)訂閱(call)庫html

概念

Graph 模塊之間的Dependency(依賴關係)構成的依賴圖node

CompilerTapable實例)訂閱了webpack最頂層的生命週期事件webpack

ComplilationTapable實例)該對象由Compiler建立, 負責構建Graph,Seal,Render...是整個工做流程的核心生命週期,包含Dep Graph 遍歷算法,優化(optimize),tree shaking...git

Compiler 和 Compilation 的區別在於:Compiler 表明了整個 Webpack 從啓動到關閉的生命週期,而 Compilation 只是表明了一次新的編譯。github

ResolverTapable實例)資源路徑解析器web

ModuleFactoryTapable實例) 被Resolver成功解析的資源須要被這個工廠類被實例化成Moduleredis

ParserTapable實例) 負責將Module(ModuleFactory實例化來的)轉AST的解析器 (webpack 默認用acorn),並解析出不一樣規範的require/import 轉成Dependency(依賴)算法

Template 模塊化的模板. Chunk,Module,Dependency都有各自的模塊模板,來自各自的工廠類的實例mongodb

bundlechunk區別:https://github.com/webpack/webpack.js.org/issues/970

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/the-chunk-graph-algorithm-week-26-29-7c88aa5e4b4e

整個工做流程

  1. Compiler 讀取配置,建立Compilation
  2. Compiler建立Graph的過程:
    • Compilation讀取資源入口
    • NMF(normal module factory)
      • Resolver 解析
      • 輸出NM
    • Parser 解析 AST
      • js json 用acorn
      • 其餘用Loader (執行loader runner)
    • 若是有依賴, 重複步驟 2
  3. Compilation優化Graph
  4. 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/5abf33f16fb9a028e46ec352

Module

它有不少子類:RawModule, NormalModule ,MultiModule,ContextModule,DelegatedModule,DllModule,ExternalModule 等

ModuleFactory: 使用工廠模式建立不一樣的Module,有四個主要的子類: NormalModuleFactory,ContextModuleFactory , DllModuleFactory,MultiModuleFactory

Template

  • mainTemplate 和 chunkTemplate
    js if(chunk.entry) { source = this.mainTemplate.render(this.hash, chunk, this.moduleTemplate, this.dependencyTemplates); } else { source = this.chunkTemplate.render(chunk, this.moduleTemplate, this.dependencyTemplates); }
    • 不一樣模塊規範封裝
      js 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
    //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源碼

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源碼
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/api/compilation-hooks/

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的順序:postinlinenormalpre

然而loader是從右至左執行的,真實的loader執行順序是倒過來的,所以inlineLoader是總體後於config中normal loader執行的。

路徑解析之 inline loader

  • 正則解析loader和參數
    js //NormalModuleFactory.js let elements = requestWithoutMatchResource .replace(/^-?!+/, "") .replace(/!!+/g, "!") .split("!");
  • 將「解析模塊的loader數組」與「解析模塊自己」一塊兒並行執行,用到了neo-async這個庫(和async庫相似,都是爲異步編程提供一些工具方法,可是會比async庫更快。)
  • 解析返回結果:
    js [ // 第一個元素是一個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 階段).

這兩個階段(pitchnormal)就是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\"\\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

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(負責添加<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

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。

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上添加了asynccallback函數.

      當咱們編寫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/lihongxun945/diving-into-webpack/blob/master/7-hmr.md

當完成編譯的時候,就經過 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 之流程篇

相關文章
相關標籤/搜索