打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
Webpack 详解

作者: 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 注册缓存数据钩子

  • before-compile

  • compile 开始编译

  • make 从入口分析依赖以及间接依赖模块,创建模块对象

  • build-module 模块构建

  • seal 构建结果封装, 不可再更改

  • after-compile 完成构建,缓存数据

  • emit 输出到dist目录

编译&构建流程

webpack中负责构建和编译都是 Compilation

  1. class Compilation extends Tapable {

  2.    constructor(compiler) {

  3.        super();

  4.        this.hooks = {

  5.            // hooks

  6.        };

  7.        // ...

  8.        this.compiler = compiler;

  9.        // ...

  10.        // template

  11.        this.mainTemplate = new MainTemplate(this.outputOptions);

  12.        this.chunkTemplate = new ChunkTemplate(this.outputOptions);

  13.        this.hotUpdateChunkTemplate = new HotUpdateChunkTemplate(

  14.            this.outputOptions

  15.        );

  16.        this.runtimeTemplate = new RuntimeTemplate(

  17.            this.outputOptions,

  18.            this.requestShortener

  19.        );

  20.        this.moduleTemplates = {

  21.            javascript: new ModuleTemplate(this.runtimeTemplate),

  22.            webassembly: new ModuleTemplate(this.runtimeTemplate)

  23.        };

  24.        // 构建生成的资源

  25.        this.chunks = [];

  26.        this.chunkGroups = [];

  27.        this.modules = [];

  28.        this.additionalChunkAssets = [];

  29.        this.assets = {};

  30.        this.children = [];

  31.        // ...

  32.    }

  33.    //

  34.    buildModule(module, optional, origin, dependencies, thisCallback) {

  35.        // ...

  36.        // 调用module.build方法进行编译代码,build中 其实是利用acorn编译生成AST

  37.        this.hooks.buildModule.call(module);

  38.        module.build(/**param*/);

  39.    }

  40.    // 将模块添加到列表中,并编译模块

  41.    _addModuleChain(context, dependency, onModule, callback) {

  42.            // ...

  43.            // moduleFactory.create创建模块,这里会先利用loader处理文件,然后生成模块对象

  44.            moduleFactory.create(

  45.                {

  46.                    contextInfo: {

  47.                        issuer: '',

  48.                        compiler: this.compiler.name

  49.                    },

  50.                    context: context,

  51.                    dependencies: [dependency]

  52.                },

  53.                (err, module) => {

  54.                    const addModuleResult = this.addModule(module);

  55.                    module = addModuleResult.module;

  56.                    onModule(module);

  57.                    dependency.module = module;

  58.                    // ...

  59.                    // 调用buildModule编译模块

  60.                    this.buildModule(module, false, null, null, err => {});

  61.                }

  62.        });

  63.    }

  64.    // 添加入口模块,开始编译&构建

  65.    addEntry(context, entry, name, callback) {

  66.        // ...

  67.        this._addModuleChain( // 调用_addModuleChain添加模块

  68.            context,

  69.            entry,

  70.            module => {

  71.                this.entries.push(module);

  72.            },

  73.            // ...

  74.        );

  75.    }

  76.    seal(callback) {

  77.        this.hooks.seal.call();

  78.        // ...

  79.        const chunk = this.addChunk(name);

  80.        const entrypoint = new Entrypoint(name);

  81.        entrypoint.setRuntimeChunk(chunk);

  82.        entrypoint.addOrigin(null, name, preparedEntrypoint.request);

  83.        this.namedChunkGroups.set(name, entrypoint);

  84.        this.entrypoints.set(name, entrypoint);

  85.        this.chunkGroups.push(entrypoint);

  86.        GraphHelpers.connectChunkGroupAndChunk(entrypoint, chunk);

  87.        GraphHelpers.connectChunkAndModule(chunk, module);

  88.        chunk.entryModule = module;

  89.        chunk.name = name;

  90.         // ...

  91.        this.hooks.beforeHash.call();

  92.        this.createHash();

  93.        this.hooks.afterHash.call();

  94.        this.hooks.beforeModuleAssets.call();

  95.        this.createModuleAssets();

  96.        if (this.hooks.shouldGenerateChunkAssets.call() !== false) {

  97.            this.hooks.beforeChunkAssets.call();

  98.            this.createChunkAssets();

  99.        }

  100.        // ...

  101.    }

  102.    createHash() {

  103.        // ...

  104.    }

  105.    // 生成 assets 资源并 保存到 Compilation.assets 中 给webpack写插件的时候会用到

  106.    createModuleAssets() {

  107.        for (let i = 0; i < this.modules.length; i ) {

  108.            const module = this.modules[i];

  109.            if (module.buildInfo.assets) {

  110.                for (const assetName of Object.keys(module.buildInfo.assets)) {

  111.                    const fileName = this.getPath(assetName);

  112.                    this.assets[fileName] = module.buildInfo.assets[assetName];

  113.                    this.hooks.moduleAsset.call(module, fileName);

  114.                }

  115.            }

  116.        }

  117.    }

  118.    createChunkAssets() {

  119.     // ...

  120.    }

  121. }

在webpack make钩子中, tapAsync注册了一个 DllEntryPlugin, 就是将入口模块通过调用 compilation.addEntry方法将所有的入口模块添加到编译构建队列中,开启编译流程。

  1. compiler.hooks.make.tapAsync('DllEntryPlugin', (compilation, callback) => {

  2.        compilation.addEntry(

  3.            this.context,

  4.            new DllEntryDependency(

  5.                this.entries.map((e, idx) => {

  6.                    const dep = new SingleEntryDependency(e);

  7.                    dep.loc = `${this.name}:${idx}`;

  8.                    return dep;

  9.                }),

  10.                this.name

  11.            ),

  12.            // ...

  13.        );

  14.    });

随后在 addEntry 中调用 _addModuleChain开始编译。在 _addModuleChain首先会生成模块,最后构建。

  1. class NormalModuleFactory extends Tapable {

  2.    // ...

  3.    create(data, callback) {

  4.        // ...

  5.        this.hooks.beforeResolve.callAsync(

  6.            {

  7.                contextInfo,

  8.                resolveOptions,

  9.                context,

  10.                request,

  11.                dependencies

  12.            },

  13.            (err, result) => {

  14.                if (err) return callback(err);

  15.                // Ignored

  16.                if (!result) return callback();

  17.                // factory 钩子会触发 resolver 钩子执行,而resolver钩子中会利用acorn 处理js生成AST,再利用acorn处理前,会使用loader加载文件

  18.                const factory = this.hooks.factory.call(null);

  19.                factory(result, (err, module) => {

  20.                    if (err) return callback(err);

  21.                    if (module && this.cachePredicate(module)) {

  22.                        for (const d of dependencies) {

  23.                            d.__NormalModuleFactoryCache = module;

  24.                        }

  25.                    }

  26.                    callback(null, module);

  27.                });

  28.            }

  29.        );

  30.    }

  31. }

在编译完成后,调用 compilation.seal方法封闭,生成资源,这些资源保存在 compilation.assets, compilation.chunk, 在给webpack写插件的时候会用到。

  1. class Compiler extends Tapable {

  2.    constructor(context) {

  3.        super();

  4.        this.hooks = {

  5.            beforeRun: new AsyncSeriesHook(['compilation']),

  6.            run: new AsyncSeriesHook(['compilation']),

  7.            emit: new AsyncSeriesHook(['compilation']),

  8.            afterEmit: new AsyncSeriesHook(['compilation']),

  9.            compilation: new SyncHook(['compilation', 'params']),

  10.            beforeCompile: new AsyncSeriesHook(['params']),

  11.            compile: new SyncHook(['params']),

  12.            make: new AsyncParallelHook(['compilation']),

  13.            afterCompile: new AsyncSeriesHook(['compilation']),

  14.            // other hooks

  15.        };

  16.        // ...

  17.    }

  18.    run(callback) {

  19.        const startTime = Date.now();

  20.        const onCompiled = (err, compilation) => {

  21.            // ...

  22.            this.emitAssets(compilation, err => {

  23.                if (err) return callback(err);

  24.                if (compilation.hooks.needAdditionalPass.call()) {

  25.                    compilation.needAdditionalPass = true;

  26.                    const stats = new Stats(compilation);

  27.                    stats.startTime = startTime;

  28.                    stats.endTime = Date.now();

  29.                    this.hooks.done.callAsync(stats, err => {

  30.                        if (err) return callback(err);

  31.                        this.hooks.additionalPass.callAsync(err => {

  32.                            if (err) return callback(err);

  33.                            this.compile(onCompiled);

  34.                        });

  35.                    });

  36.                    return;

  37.                }

  38.                // ...

  39.            });

  40.        };

  41.        this.hooks.beforeRun.callAsync(this, err => {

  42.            if (err) return callback(err);

  43.            this.hooks.run.callAsync(this, err => {

  44.                if (err) return callback(err);

  45.                this.readRecords(err => {

  46.                    if (err) return callback(err);

  47.                    this.compile(onCompiled);

  48.                });

  49.            });

  50.        });

  51.    }

  52.    // 输出文件到构建目录

  53.    emitAssets(compilation, callback) {

  54.        // ...

  55.        this.hooks.emit.callAsync(compilation, err => {

  56.            if (err) return callback(err);

  57.            outputPath = compilation.getPath(this.outputPath);

  58.            this.outputFileSystem.mkdirp(outputPath, emitFiles);

  59.        });

  60.    }

  61.    newCompilationParams() {

  62.        const params = {

  63.            normalModuleFactory: this.createNormalModuleFactory(),

  64.            contextModuleFactory: this.createContextModuleFactory(),

  65.            compilationDependencies: new Set()

  66.        };

  67.        return params;

  68.    }

  69.    compile(callback) {

  70.        const params = this.newCompilationParams();

  71.        this.hooks.beforeCompile.callAsync(params, err => {

  72.            if (err) return callback(err);

  73.            this.hooks.compile.call(params);

  74.            const compilation = this.newCompilation(params);

  75.            this.hooks.make.callAsync(compilation, err => {

  76.                if (err) return callback(err);

  77.                compilation.finish();

  78.                // make 钩子执行后,调用seal生成资源

  79.                compilation.seal(err => {

  80.                    if (err) return callback(err);

  81.                    this.hooks.afterCompile.callAsync(compilation, err => {

  82.                        if (err) return callback(err);

  83.                        // emit, 生成最终文件

  84.                        return callback(null, compilation);

  85.                    });

  86.                });

  87.            });

  88.        });

  89.    }

  90. }

最后输出

seal执行后,便会调用 emit钩子,根据webpack config文件的output配置的path属性,将文件输出到指定的path.

最后

腾讯IVWEB团队的工程化解决方案feflow已经开源:https://github.com/feflow/feflow。

如果对您的团队或者项目有帮助,请给个Star支持一下哈

前端大学 - {技术圈}
持续关注互联网、web前端开发、IT编程资料分享。

本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
好程序员课堂:跟你讲讲webpack 的流程(收藏版)
为什么说 Compose 的声明式代码最简洁 ?Compose/React/Flutter/SwiftUI 语法对比
【实践篇】node实现mock小工具
Webpack4.0各个击破(10)integration篇
如何快速开发一个自己的项目脚手架?
Vue 服务端渲染实践 ——Web应用首屏耗时最优化方案
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服