FE.SRC-webpack道理梳理

webpack设想形式

统统资本皆Module

Module(模块)是webpack的中的症结实体。Webpack 会从设置的 Entry 最先递归找出一切依靠的模块. 经由历程Loaders(模块转换器),用于把模块原内容依据需求转换成新模块内容.

事宜驱动架构

webpack团体是一个事宜驱动架构,一切的功用都以Plugin(插件)的体式格局集成在构建流程中,经由历程宣布定阅事宜来触发各个插件实行。webpack中心运用tapable来完成Plugin(插件)的注册和挪用,Tapable是一个事宜宣布(tap)定阅(call)库

观点

Graph 模块之间的Dependency(依靠关联)构成的依靠图

CompilerTapable实例)定阅了webpack最顶层的生命周期事宜

ComplilationTapable实例)该对象由Compiler建立, 担任构建Graph,Seal,Render…是全部事情流程的中心生命周期,包含Dep Graph 遍历算法,优化(optimize),tree shaking…

Compiler 和 Compilation 的区分在于:Compiler 代表了全部 Webpack 从启动到封闭的生命周期,而 Compilation 只是代表了一次新的编译。

ResolverTapable实例)资本途径剖析器

ModuleFactoryTapable实例) 被Resolver胜利剖析的资本须要被这个工场类被实例化成Module

ParserTapable实例) 担任将Module(ModuleFactory实例化来的)转AST的剖析器 (webpack 默许用acorn),并剖析出差别范例的require/import 转成Dependency(依靠)

Template 模块化的模板. Chunk,Module,Dependency都有各自的模块模板,来自各自的工场类的实例

bundlechunk区分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…

全部事情流程

  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/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_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…

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和参数

    //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 阶段).

这两个阶段(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/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 之流程篇

    原文作者:seasonley
    原文地址: https://segmentfault.com/a/1190000018991848
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞