微信号:FrontDev

介绍:分享 Web 前端相关的技术文章、工具资源、精选课程、热点资讯

Webpack 详解

2018-03-21 21:05 前端大全

(点击上方公众号,可快速关注)

作者: ixlei

https://segmentfault.com/a/1190000013657042


webpack是现代前端开发中最火的模块打包工具,只需要通过简单的配置,便可以完成模块的加载和打包。那它是怎么做到通过对一些插件的配置,便可以轻松实现对代码的构建呢?

webpack的配置

 
           
  1. const path = require('path');

  2. module.exports = {

  3.  entry: "./app/entry", // string | object | array

  4.  // Webpack打包的入口

  5.  output: {  // 定义webpack如何输出的选项

  6.    path: path.resolve(__dirname, "dist"), // string

  7.    // 所有输出文件的目标路径

  8.    filename: "[chunkhash].js", // string

  9.    // 「入口(entry chunk)」文件命名模版

  10.    publicPath: "/assets/", // string

  11.    // 构建文件的输出目录

  12.    /* 其它高级配置 */

  13.  },

  14.  module: {  // 模块相关配置

  15.    rules: [ // 配置模块loaders,解析规则

  16.      {

  17.        test: /\.jsx?$/,  // RegExp | string

  18.        include: [ // 和test一样,必须匹配选项

  19.          path.resolve(__dirname, "app")

  20.        ],

  21.        exclude: [ // 必不匹配选项(优先级高于test和include)

  22.          path.resolve(__dirname, "app/demo-files")

  23.        ],

  24.        loader: "babel-loader", // 模块上下文解析

  25.        options: { // loader的可选项

  26.          presets: ["es2015"]

  27.        },

  28.      },

  29.  },

  30.  resolve: { //  解析模块的可选项

  31.    modules: [ // 模块的查找目录

  32.      "node_modules",

  33.      path.resolve(__dirname, "app")

  34.    ],

  35.    extensions: [".js", ".json", ".jsx", ".css"], // 用到的文件的扩展

  36.    alias: { // 模块别名列表

  37.      "module": "new-module"

  38.      },

  39.  },

  40.  devtool: "source-map", // enum

  41.  // 为浏览器开发者工具添加元数据增强调试

  42.  plugins: [ // 附加插件列表

  43.    // ...

  44.  ],

  45. }

从上面我们可以看到,webpack配置中需要理解几个核心的概念 EntryOutputLoadersPluginsChunk

  • Entry:指定webpack开始构建的入口模块,从该模块开始构建并计算出直接或间接依赖的模块或者库

  • Output:告诉webpack如何命名输出的文件以及输出的目录

  • Loaders:由于webpack只能处理javascript,所以我们需要对一些非js文件处理成webpack能够处理的模块,比如sass文件

  • Plugins: Loaders将各类型的文件处理成webpack能够处理的模块, plugins有着很强的能力。插件的范围包括,从打包优化和压缩,一直到重新定义环境中的变量。但也是最复杂的一个。比如对js文件进行压缩优化的 UglifyJsPlugin插件

  • Chunk:coding split的产物,我们可以对一些代码打包成一个单独的chunk,比如某些公共模块,去重,更好的利用缓存。或者按需加载某些功能模块,优化加载时间。在webpack3及以前我们都利用 CommonsChunkPlugin将一些公共代码分割成一个chunk,实现单独加载。在webpack4 中 CommonsChunkPlugin被废弃,使用 SplitChunksPlugin

webpack详解

读到这里,或许你对webpack有一个大概的了解,那webpack 是怎么运行的呢?我们都知道,webpack是高度复杂抽象的插件集合,理解webpack的运行机制,对于我们日常定位构建错误以及写一些插件处理构建任务有很大的帮助。

不得不说的tapable

webpack本质上是一种事件流的机制,它的工作流程就是将各个插件串联起来,而实现这一切的核心就是Tapable,webpack中最核心的负责编译的 Compiler和负责创建bundles的 Compilation都是Tapable的实例。在Tapable1.0之前,也就是webpack3及其以前使用的Tapable,提供了包括:

  • plugin(name:string,handler:function)注册插件到Tapable对象中

  • apply(…pluginInstances:(AnyPlugin|function)[])调用插件的定义,将事件监听器注册到Tapable实例注册表中

  • applyPlugins*(name:string,…)多种策略细致地控制事件的触发,包括 applyPluginsAsync、 applyPluginsParallel等方法实现对事件触发的控制,实现

(1)多个事件连续顺序执行

(2)并行执行

(3)异步执行

(4)一个接一个地执行插件,前面的输出是后一个插件的输入的瀑布流执行顺序

(5)在允许时停止执行插件,即某个插件返回了一个 undefined的值,即退出执行

我们可以看到,Tapable就像nodejs中 EventEmitter,提供对事件的注册 on和触发 emit,理解它很重要,看个栗子:比如我们来写一个插件

 
           
  1. function CustomPlugin() {}

  2. CustomPlugin.prototype.apply = function(compiler) {

  3.  compiler.plugin('emit', pluginFunction);

  4. }

在webpack的生命周期中会适时的执行:

 
           
  1. this.apply*("emit",options)

当然上面提到的Tapable都是1.0版本之前的,如果想深入学习,可以查看Tapable和事件流(https://segmentfault.com/a/1190000008060440)。

那1.0的Tapable又是什么样的呢?1.0版本发生了巨大的改变,不再是此前的通过 plugin注册事件,通过 applyPlugins*触发事件调用,那1.0的Tapable是什么呢?

暴露出很多的钩子,可以使用它们为插件创建钩子函数

 
           
  1. const {

  2.    SyncHook,

  3.    SyncBailHook,

  4.    SyncWaterfallHook,

  5.    SyncLoopHook,

  6.    AsyncParallelHook,

  7.    AsyncParallelBailHook,

  8.    AsyncSeriesHook,

  9.    AsyncSeriesBailHook,

  10.    AsyncSeriesWaterfallHook

  11. } = require("tapable");

我们来看看怎么使用。

 
           
  1. class Order {

  2.    constructor() {

  3.        this.hooks = { //hooks

  4.            goods: new SyncHook(['goodsId', 'number']),

  5.            consumer: new AsyncParallelHook(['userId', 'orderId'])

  6.        }

  7.    }

  8.    queryGoods(goodsId, number) {

  9.        this.hooks.goods.call(goodsId, number);

  10.    }

  11.    consumerInfoPromise(userId, orderId) {

  12.        this.hooks.consumer.promise(userId, orderId).then(() => {

  13.            //TODO

  14.        })

  15.    }

  16.    consumerInfoAsync(userId, orderId) {

  17.        this.hooks.consumer.callAsync(userId, orderId, (err, data) => {

  18.            //TODO

  19.        })

  20.    }

  21. }

对于所有的hook的构造函数均接受一个可选的string类型的数组。

 
           
  1. const hook = new SyncHook(["arg1", "arg2", "arg3"]);

 
           
  1. // 调用tap方法注册一个consument

  2. order.hooks.goods.tap('QueryPlugin', (goodsId, number) => {

  3.    return fetchGoods(goodsId, number);

  4. })

  5. // 再添加一个

  6. order.hooks.goods.tap('LoggerPlugin', (goodsId, number) => {

  7.    logger(goodsId, number);

  8. })

  9. // 调用

  10. order.queryGoods('10000000', 1)

对于一个 SyncHook,我们通过 tap来添加消费者,通过 call来触发钩子的顺序执行。

对于一个非 sync*类型的钩子,即 async*类型的钩子,我们还可以通过其它方式注册消费者和调用

 
           
  1. // 注册一个sync 钩子

  2. order.hooks.consumer.tap('LoggerPlugin', (userId, orderId) => {

  3.   logger(userId, orderId);

  4. })

  5. order.hooks.consumer.tapAsync('LoginCheckPlugin', (userId, orderId, callback) => {

  6.    LoginCheck(userId, callback);

  7. })

  8. order.hooks.consumer.tapPromise('PayPlugin', (userId, orderId) => {

  9.    return Promise.resolve();

  10. })

  11. // 调用

  12. // 返回Promise

  13. order.consumerInfoPromise('user007', '1024');

  14. //回调函数

  15. order.consumerInfoAsync('user007', '1024')

通过上面的栗子,你可能已经大致了解了 Tapable的用法,它的用法:

  • 插件注册数量

  • 插件注册的类型(sync, async, promise)

  • 调用的方式(sync, async, promise)

  • 实例钩子的时候参数数量

  • 是否使用了 interception

Tapable详解

对于 Sync*类型的钩子来说:

  • 注册在该钩子下面的插件的执行顺序都是顺序执行。

  • 只能使用 tap注册,不能使用 tapPromise和 tapAsync注册

 
           
  1. // 所有的钩子都继承于Hook

  2. class Sync* extends Hook {

  3.    tapAsync() { // Sync*类型的钩子不支持tapAsync

  4.        throw new Error("tapAsync is not supported on a Sync*");

  5.    }

  6.    tapPromise() {// Sync*类型的钩子不支持tapPromise

  7.        throw new Error("tapPromise is not supported on a Sync*");

  8.    }

  9.    compile(options) { // 编译代码来按照一定的策略执行Plugin

  10.        factory.setup(this, options);

  11.        return factory.create(options);

  12.    }

  13. }

对于 Async*类型钩子:支持 taptapPromisetapAsync注册。

 
           
  1. class AsyncParallelHook extends Hook {

  2.    constructor(args) {

  3.        super(args);

  4.        this.call = this._call = undefined;

  5.    }

  6.    compile(options) {

  7.        factory.setup(this, options);

  8.        return factory.create(options);

  9.    }

  10. }

 
           
  1. class Hook {

  2.    constructor(args) {

  3.        if(!Array.isArray(args)) args = [];

  4.        this._args = args; // 实例钩子的时候的string类型的数组

  5.        this.taps = []; // 消费者

  6.        this.interceptors = []; // interceptors

  7.        this.call = this._call =  // 以sync类型方式来调用钩子

  8.        this._createCompileDelegate("call", "sync");

  9.        this.promise =

  10.        this._promise = // 以promise方式

  11.        this._createCompileDelegate("promise", "promise");

  12.        this.callAsync =

  13.        this._callAsync = // 以async类型方式来调用

  14.        this._createCompileDelegate("callAsync", "async");

  15.        this._x = undefined; //

  16.    }

  17.    _createCall(type) {

  18.        return this.compile({

  19.            taps: this.taps,

  20.            interceptors: this.interceptors,

  21.            args: this._args,

  22.            type: type

  23.        });

  24.    }

  25.    _createCompileDelegate(name, type) {

  26.        const lazyCompileHook = (...args) => {

  27.            this[name] = this._createCall(type);

  28.            return this[name](...args);

  29.        };

  30.        return lazyCompileHook;

  31.    }

  32.    // 调用tap 类型注册

  33.    tap(options, fn) {

  34.        // ...

  35.        options = Object.assign({ type: "sync", fn: fn }, options);

  36.        // ...

  37.        this._insert(options);  // 添加到 this.taps中

  38.    }

  39.    // 注册 async类型的钩子

  40.    tapAsync(options, fn) {

  41.        // ...

  42.        options = Object.assign({ type: "async", fn: fn }, options);

  43.        // ...

  44.        this._insert(options); // 添加到 this.taps中

  45.    }

  46.    注册 promise类型钩子

  47.    tapPromise(options, fn) {

  48.        // ...

  49.        options = Object.assign({ type: "promise", fn: fn }, options);

  50.        // ...

  51.        this._insert(options); // 添加到 this.taps中

  52.    }

  53. }

每次都是调用 taptapSynctapPromise注册不同类型的插件钩子,通过调用 callcallAsyncpromise方式调用。其实调用的时候为了按照一定的执行策略执行,调用 compile方法快速编译出一个方法来执行这些插件。

 
           
  1. const factory = new Sync*CodeFactory();

  2. class Sync* extends Hook {

  3.    // ...

  4.    compile(options) { // 编译代码来按照一定的策略执行Plugin

  5.        factory.setup(this, options);

  6.        return factory.create(options);

  7.    }

  8. }

  9. class Sync*CodeFactory extends HookCodeFactory {

  10.    content({ onError, onResult, onDone, rethrowIfPossible }) {

  11.        return this.callTapsSeries({

  12.            onError: (i, err) => onError(err),

  13.            onDone,

  14.            rethrowIfPossible

  15.        });

  16.    }

  17. }

compile中调用 HookCodeFactory#create方法编译生成执行代码。

 
           
  1. class HookCodeFactory {

  2.    constructor(config) {

  3.        this.config = config;

  4.        this.options = undefined;

  5.    }

  6.    create(options) {

  7.        this.init(options);

  8.        switch(this.options.type) {

  9.            case "sync":  // 编译生成sync, 结果直接返回

  10.                return new Function(this.args(),

  11.                "\"use strict\";\n" + this.header() + this.content({

  12.                    // ...

  13.                    onResult: result => `return ${result};\n`,

  14.                    // ...

  15.                }));

  16.            case "async": // async类型, 异步执行,最后将调用插件执行结果来调用callback,

  17.                return new Function(this.args({

  18.                    after: "_callback"

  19.                }), "\"use strict\";\n" + this.header() + this.content({

  20.                    // ...

  21.                    onResult: result => `_callback(null, ${result});\n`,

  22.                    onDone: () => "_callback();\n"

  23.                }));

  24.            case "promise": // 返回promise类型,将结果放在resolve中

  25.                // ...

  26.                code += "return new Promise((_resolve, _reject) => {\n";

  27.                code += "var _sync = true;\n";

  28.                code += this.header();

  29.                code += this.content({

  30.                    // ...

  31.                    onResult: result => `_resolve(${result});\n`,

  32.                    onDone: () => "_resolve();\n"

  33.                });

  34.                // ...

  35.                return new Function(this.args(), code);

  36.        }

  37.    }

  38.    // callTap 就是执行一些插件,并将结果返回

  39.    callTap(tapIndex, { onError, onResult, onDone, rethrowIfPossible }) {

  40.        let code = "";

  41.        let hasTapCached = false;

  42.        // ...

  43.        code += `var _fn${tapIndex} = ${this.getTapFn(tapIndex)};\n`;

  44.        const tap = this.options.taps[tapIndex];

  45.        switch(tap.type) {

  46.            case "sync":

  47.                // ...

  48.                if(onResult) {

  49.                    code += `var _result${tapIndex} = _fn${tapIndex}(${this.args({

  50.                        before: tap.context ? "_context" : undefined

  51.                    })});\n`;

  52.                } else {

  53.                    code += `_fn${tapIndex}(${this.args({

  54.                        before: tap.context ? "_context" : undefined

  55.                    })});\n`;

  56.                }

  57.                if(onResult) { // 结果透传

  58.                    code += onResult(`_result${tapIndex}`);

  59.                }

  60.                if(onDone) { // 通知插件执行完毕,可以执行下一个插件

  61.                    code += onDone();

  62.                }

  63.                break;

  64.            case "async": //异步执行,插件运行完后再将结果通过执行callback透传

  65.                let cbCode = "";

  66.                if(onResult)

  67.                    cbCode += `(_err${tapIndex}, _result${tapIndex}) => {\n`;

  68.                else

  69.                    cbCode += `_err${tapIndex} => {\n`;

  70.                cbCode += `if(_err${tapIndex}) {\n`;

  71.                cbCode += onError(`_err${tapIndex}`);

  72.                cbCode += "} else {\n";

  73.                if(onResult) {

  74.                    cbCode += onResult(`_result${tapIndex}`);

  75.                }

  76.                cbCode += "}\n";

  77.                cbCode += "}";

  78.                code += `_fn${tapIndex}(${this.args({

  79.                    before: tap.context ? "_context" : undefined,

  80.                    after: cbCode //cbCode将结果透传

  81.                })});\n`;

  82.                break;

  83.            case "promise": // _fn${tapIndex} 就是第tapIndex 个插件,它必须是个Promise类型的插件

  84.                code += `var _hasResult${tapIndex} = false;\n`;

  85.                code += `_fn${tapIndex}(${this.args({

  86.                    before: tap.context ? "_context" : undefined

  87.                })}).then(_result${tapIndex} => {\n`;

  88.                code += `_hasResult${tapIndex} = true;\n`;

  89.                if(onResult) {

  90.                    code += onResult(`_result${tapIndex}`);

  91.                }

  92.            // ...

  93.                break;

  94.        }

  95.        return code;

  96.    }

  97.    // 按照插件的注册顺序,按照顺序递归调用执行插件

  98.    callTapsSeries({ onError, onResult, onDone, rethrowIfPossible }) {

  99.        // ...

  100.        const firstAsync = this.options.taps.findIndex(t => t.type !== "sync");

  101.        const next = i => {

  102.            // ...

  103.            const done = () => next(i + 1);

  104.            // ...

  105.            return this.callTap(i, {

  106.                // ...

  107.                onResult: onResult && ((result) => {

  108.                    return onResult(i, result, done, doneBreak);

  109.                }),

  110.                // ...

  111.            });

  112.        };

  113.        return next(0);

  114.    }

  115.    callTapsLooping({ onError, onDone, rethrowIfPossible }) {

  116.        const syncOnly = this.options.taps.every(t => t.type === "sync");

  117.        let code = "";

  118.        if(!syncOnly) {

  119.            code += "var _looper = () => {\n";

  120.            code += "var _loopAsync = false;\n";

  121.        }

  122.        code += "var _loop;\n";

  123.        code += "do {\n";

  124.        code += "_loop = false;\n";

  125.        // ...

  126.        code += this.callTapsSeries({

  127.            // ...

  128.            onResult: (i, result, next, doneBreak) => { // 一旦某个插件返回不为undefined,  即一只调用某个插件执行,如果为undefined,开始调用下一个

  129.                let code = "";

  130.                code += `if(${result} !== undefined) {\n`;

  131.                code += "_loop = true;\n";

  132.                if(!syncOnly)

  133.                    code += "if(_loopAsync) _looper();\n";

  134.                code += doneBreak(true);

  135.                code += `} else {\n`;

  136.                code += next();

  137.                code += `}\n`;

  138.                return code;

  139.            },

  140.            // ...

  141.        })

  142.        code += "} while(_loop);\n";

  143.        // ...

  144.        return code;

  145.    }

  146.    // 并行调用插件执行

  147.    callTapsParallel({ onError, onResult, onDone, rethrowIfPossible, onTap = (i, run) => run() }) {

  148.        // ...

  149.        // 遍历注册都所有插件,并调用

  150.        for(let i = 0; i < this.options.taps.length; i++) {

  151.            // ...

  152.            code += "if(_counter <= 0) break;\n";

  153.            code += onTap(i, () => this.callTap(i, {

  154.                // ...

  155.                onResult: onResult && ((result) => {

  156.                    let code = "";

  157.                    code += "if(_counter > 0) {\n";

  158.                    code += onResult(i, result, done, doneBreak);

  159.                    code += "}\n";

  160.                    return code;

  161.                }),

  162.                // ...

  163.            }), done, doneBreak);

  164.        }

  165.        // ...

  166.        return code;

  167.    }

  168. }

HookCodeFactory#create中调用到 content方法,此方法将按照此钩子的执行策略,调用不同的方法来执行编译 生成最终的代码。

SyncHook中调用 callTapsSeries编译生成最终执行插件的函数, callTapsSeries做的就是将插件列表中插件按照注册顺序遍历执行。

 
           
  1. class SyncHookCodeFactory extends HookCodeFactory {

  2.    content({ onError, onResult, onDone, rethrowIfPossible }) {

  3.        return this.callTapsSeries({

  4.            onError: (i, err) => onError(err),

  5.            onDone,

  6.            rethrowIfPossible

  7.        });

  8.    }

  9. }

SyncBailHook中当一旦某个返回值结果不为 undefined便结束执行列表中的插件。

 
           
  1. class SyncBailHookCodeFactory extends HookCodeFactory {

  2.    content({ onError, onResult, onDone, rethrowIfPossible }) {

  3.        return this.callTapsSeries({

  4.            // ...

  5.            onResult: (i, result, next) => `if(${result} !== undefined) {\n${onResult(result)};\n} else {\n${next()}}\n`,

  6.            // ...

  7.        });

  8.    }

  9. }

SyncWaterfallHook中上一个插件执行结果当作下一个插件的入参。

 
           
  1. class SyncWaterfallHookCodeFactory extends HookCodeFactory {

  2.    content({ onError, onResult, onDone, rethrowIfPossible }) {

  3.        return this.callTapsSeries({

  4.            // ...

  5.            onResult: (i, result, next) => {

  6.                let code = "";

  7.                code += `if(${result} !== undefined) {\n`;

  8.                code += `${this._args[0]} = ${result};\n`;

  9.                code += `}\n`;

  10.                code += next();

  11.                return code;

  12.            },

  13.            onDone: () => onResult(this._args[0]),

  14.        });

  15.    }

  16. }

  • AsyncParallelHook调用 callTapsParallel并行执行插件

 
           
  1. class AsyncParallelHookCodeFactory extends HookCodeFactory {

  2.    content({ onError, onDone }) {

  3.        return this.callTapsParallel({

  4.            onError: (i, err, done, doneBreak) => onError(err) + doneBreak(true),

  5.            onDone

  6.        });

  7.    }

  8. }

webpack流程篇

本文关于webpack 的流程讲解是基于webpack4的。

webpack 入口文件

从webpack项目的package.json文件中我们找到了入口执行函数,在函数中引入webpack,那么入口将会是 lib/webpack.js,而如果在shell中执行,那么将会走到 ./bin/webpack.js,我们就以 lib/webpack.js为入口开始吧!

 
           
  1. {

  2.  "name": "webpack",

  3.  "version": "4.1.1",

  4.  ...

  5.  "main": "lib/webpack.js",

  6.  "web": "lib/webpack.web.js",

  7.  "bin": "./bin/webpack.js",

  8.  ...

  9.  }

webpack入口
 
           
  1. const webpack = (options, callback) => {

  2.    // ...

  3.    // 验证options正确性

  4.    // 预处理options

  5.    options = new WebpackOptionsDefaulter().process(options); // webpack4的默认配置

  6.    compiler = new Compiler(options.context); // 实例Compiler

  7.    // ...

  8.    // 若options.watch === true && callback 则开启watch线程

  9.    compiler.watch(watchOptions, callback);

  10.    compiler.run(callback);

  11.    return compiler;

  12. };

webpack 的入口文件其实就实例了 Compiler并调用了 run方法开启了编译,webpack的编译都按照下面的钩子调用顺序执行:

  • before-run 清除缓存

  • run 注册�

 
前端大全 更多文章 我所理解的前端 CSS布局解决方案(终结版) 50道CSS基础面试题(附答案) 因内部闹矛盾,PhantomJS 宣布封存归档暂停开发 从输入URL到页面加载的过程?由一道题完善自己的前端知识体系!
猜您喜欢 【智库】霍尼韦尔罗文中 科技界“新秀” SOC嵌入式架构设计之五:API设计方法 int main()还是void main() 【有趣在哪里】你不知道的5个科技趣事 收藏:让你代码精简60%的逆天技巧