关于webpack:webpack5源码导读如何实现自定义-target

57次阅读

共计 10127 个字符,预计需要花费 26 分钟才能阅读完成。

开发者能够通过 快利用转换工具 将本人的小程序源码一键转换为快利用转换版,而后上传本人的 rpk 包,审核通过后即能够在数亿安卓设施上运行本人的快利用。而快利用打包工具是构建于 webpack 之上的一个模块代码打包零碎,通过降级 webpack5,应用 webpack5 的长久化缓存,无望给快利用转换工具带来微小的性能晋升。

更多优质博文请拜访快利用官网博客

webpack5 正式版曾经公布三个月了,目前最新版本是 5.11.1。具体的迁徙指南请参考 webpack 官网的文档:

  • Webpack 5 release
  • migrating to webpack 5

倡议参考 webpack 官网的这些文档,比参考一些不齐全、不精确的翻译无效的多,惟一的问题是有可能每个词都意识,连在一起却不晓得是什么意思。

1. 浏览源码的初衷

在 webpack4 中,target 选项能够设置为 function,以实现自定义的代码生成形式。然而在 webpack5 中,target 只能设置为特定的字符串或字符串数组了。最坑的是,此性能间接就废除了,webpack 没有给任何过渡提醒。而在咱们的打包工具中,一些重要的性能是通过自定义 target 来实现的。所以咱们须要钻研 webpack5 源码来摸索一种代替的实现形式。

2. 本文写作的初衷

本文并不是一篇根底的 webpack5 配置教程,webpack5 配置参考下面介绍的官网文档即可。本文想要展现的有三点:

  • Webpack5 的性能晋升有多大?

最终降级 webpack5 后,配合 webpack5 的长久化缓存,打包工具的二次 打包速度 绝对于应用 cache-loader 缓存 进步了 40%,二次打包 内存占用 绝对于应用 cache-loader 缓存二次打包内存占用绝对于应用 cache-loader 缓存 升高了 40%绝对于不应用任何缓存 ,打包工具的 二次打包速度进步了约 70%二次打包内存占用升高了约 70%

  • 为什么咱们须要浏览源码

家喻户晓 webpack 的官网文档是有些烂的,中文文档更是烂的不行。如果你屡次浏览文档之后还是不分明某项配置应该怎么配,或者咱们有一些非凡的性能需要(比方实现与 webpack4 中一样的 target 为 function 时的性能),兴许你须要看看源码了。

同时 webpack 有些配置或 api 的应用是文档里没有介绍的,很多 hooks 的介绍也是不置可否,webpack 的源码能够看成是一个十分齐备的 webpack demo,浏览源码能给咱们应用形式的启发。

  • 实现自定义 target

本文提供了一种实现与 webpack4 中自定义 target 一样的性能的实现思路,并且不会影响到其余的指标环境代码。

3. webpack5 demo

咱们能够应用一个简略的 webpack demo 来配合咱们更不便的调试浏览源码。demo 的源码在这里

装置依赖后,增加 vscode 的调试配置文件,而后按 f5 开始调试。

// demo/.vscode/launch.json
{
    "version": "0.2.0",
    "configurations": [
        {
            "type": "pwa-node",
            "request": "launch",
            "name": "Launch Program",
            "skipFiles": ["<node_internals>/**"],
            "program": "${workspaceFolder}/../qa/bin/index.js",
            "args": ["build"] 
        }
    ]
}

间接进入 webpack 的依赖装置目录浏览源码即可,npm 上 webpack 的代码未压缩。(本我的项目中 webpack 的装置目录为 webpack5-demo/qa/node_modules/webpack)。webpack 精简后的我的项目目录如下:

webpack
      ├── bin
      │   └── webpack.js
      ├── lib
      │   ├── Cache.js
      │   ├── CacheFacade.js
      │   ├── Compilation.js
      │   ├── Compiler.js
      │   ├── Stats.js
      │   ├── Watching.js
      │   ├── WebpackOptionsApply.js
      │   ├── WebpackOptionsDefaulter.js
      │   ├── cache
      │   ├── electron
      │   ├── index.js
      │   ├── javascript
      │   ├── node
      │   ├── rules
      │   ├── schemes
      │   ├── stats
      │   ├── validateSchema.js
      │   ├── wasm
      │   ├── wasm-async
      │   ├── wasm-sync
      │   ├── web
      │   ├── webpack.js
      ├── node_modules
      ├── package.json
      ├── schemas
          ├── WebpackOptions.json
          └── plugins

4. Webpack5 的执行流程

如果通过 api 调用 webpack,require('webpack') 首先会 require 导入 lib/index.jslib/index.js 会调用 lib/webpack.js , lib/webpack.js 的执行流程如下。请从默认导出的 webpack 办法开始浏览。

// lib/webpack.js
// 一些模块导入...


// 当传给 webpack 办法的配置是多个配置对象组成的数组时,调用此办法创立 compiler 用于打包
const createMultiCompiler = childOptions => {
  // 省略不关注的代码...
    return compiler;
};

// 当传给 webpack 办法的配置是一个配置对象时,调用此办法创立 compiler 用于打包
const createCompiler = rawOptions => {
  // 上面两步是对咱们传给 webpack 的配置做一些默认解决
    const options = getNormalizedWebpackOptions(rawOptions);
    applyWebpackOptionsBaseDefaults(options);
  // 创立 compiler 对象用于编译
    const compiler = new Compiler(options.context);
    compiler.options = options;
    new NodeEnvironmentPlugin({infrastructureLogging: options.infrastructureLogging}).apply(compiler);
  // 顺次将咱们配置的 plugins 数组中的 plugin 挂载到 webpack 的 compiler 上,以便挂载 plugin 中的各种 hook
    if (Array.isArray(options.plugins)) {for (const plugin of options.plugins) {if (typeof plugin === "function") {plugin.call(compiler, compiler);
            } else {plugin.apply(compiler);
            }
        }
    }
  // 进行另外一些默认配置的解决
  // 跳转到 applyWebpackOptionsDefaults 办法定义的地位(看下一个代码块),// 能够看到咱们配置的 options.target 会通过影响 targetProperties 来管制生成的代码须要
  // 具备运行时(web/node/electron/...) 的哪些属性
  applyWebpackOptionsDefaults(options);
  
    compiler.hooks.environment.call();
    compiler.hooks.afterEnvironment.call();
  // WebpackOptionsApply 是一个十分重要的步骤,请看下一个代码块的解说
    new WebpackOptionsApply().process(options, compiler);
    compiler.hooks.initialize.call();
    return compiler;
};


const webpack = /** @type {WebpackFunctionSingle & WebpackFunctionMulti} */ ((
    options,
    callback
) => {
  // 用于创立 webpack 的 compiler
    const create = () => {
    // 首先对咱们传给 webpack 的配置进行 schema 校验,配置不合乎 webpackOptionsSchema 会间接进行打包
        validateSchema(webpackOptionsSchema, options);
        let compiler;
    // 是否开启 watch 模式,默认不开启
        let watch = false;
        let watchOptions;
    // 当传给 webpack 办法的配置是多个配置对象组成的数组时,开启多个打包
        if (Array.isArray(options)) {compiler = createMultiCompiler(options);
            watch = options.some(options => options.watch);
            watchOptions = options.map(options => options.watchOptions || {});
        } else {
      // 当传给 webpack 办法的配置是一个配置对象时
            compiler = createCompiler(options);
            watch = options.watch;
            watchOptions = options.watchOptions || {};}
        return {compiler, watch, watchOptions};
    };
  // 在 webpack5 中,webpack api 的调用形式产生了变动,就是对应上面这部分代码
  // ---- 当以 compiler = webpack(webpackConfig, compileCallback) 办法调用时,// 开启 / 不开启 watch 模式都能够
  // 如果须要给 webpack 传 callback,倡议始终以这种形式调用 webpack api
  
  // ---- 当以 webpack(webpackConfig).run(compileCallback) 形式调用时
  // watch 必须为 false
  // 如果不须要给 webpack 传 callback,倡议始终以这种形式调用 webpack api
  // ================================
  // 而在 webpack4 中通过 api 调用 webpack 时,如果 webpack 函数接管了回调 callback,// 则间接执行 compiler.run()办法,那么 webpack 主动开启编译之旅。// 如果未指定 callback 回调,则须要用户本人调用 run 办法来启动编译。if (callback) {
        try {const { compiler, watch, watchOptions} = create();
      // 对应 options.watch = true,开始打包,打包实现后监听文件变动
            if (watch) {compiler.watch(watchOptions, callback);
            } else {
        // 开始打包
                compiler.run((err, stats) => {
                    compiler.close(err2 => {
            // 运行结束触发咱们传给 webpack 的 callback
                        callback(err || err2, stats);
                    });
                });
            }
            return compiler;
        } catch (err) {process.nextTick(() => callback(err));
            return null;
        }
    } else {const { compiler, watch} = create();
        if (watch) {
            util.deprecate(() => {},
                "A'callback'argument need to be provided to the'webpack(options, callback)'function when the'watch'option is set. There is no way to handle the'watch'option without a callback.",
                "DEP_WEBPACK_WATCH_WITHOUT_CALLBACK"
            )();}
        return compiler;
    }
});

module.exports = webpack;

webpack 函数执行后返回 compiler 对象,在 webpack 中存在两个十分重要的外围对象,别离为 compilercompilation,它们在整个编译过程中被宽泛应用。

  • Compiler类 (./lib/Compiler.js):webpack 的次要引擎,在 compiler 对象记录了残缺的 webpack 环境信息,在 webpack 从启动到完结,compiler 只会生成一次。你能够在 compiler 对象上读取到 webpack config 信息,outputPath等;
  • Compilation类 (./lib/Compilation.js):代表了一次繁多的版本构建和生成资源。compilation 编译作业能够屡次执行,比方 webpack 工作在 watch 模式下,每次监测到源文件发生变化时,都会从新实例化一个 compilation 对象。一个 compilation 对象体现了以后的模块资源、编译生成资源、变动的文件、以及被跟踪依赖的状态信息。

两者的区别?
compiler 代表的是不变的 webpack 环境;compilation 代表的是一次编译作业,每一次的编译都可能不同;

举个栗子????:
compiler 就像一条手机生产流水线,通上电后它就能够开始工作,期待生产手机的指令;compliation 就像是生产一部手机,生产的过程基本一致,但生产的手机可能是小米手机也可能是魅族手机。物料不同,产出也不同。

5. applyWebpackOptionsDefaults

解释下面提到的 applyWebpackOptionsDefaults 办法

// ...
// lib/config/defaults.js#applyWebpackOptionsDefaults
const applyWebpackOptionsDefaults = options => {F(options, "context", () => process.cwd());
  // 规范化解决 options.target
    F(options, "target", () => {return getDefaultTarget(options.context);
    });

    const {mode, name, target} = options;

  // 当 options.target 是数组时,getTargetsProperties 会将不同运行时(web/node/electron/...) 须要的属性进行合并
    let targetProperties =
        target === false
            ? /** @type {false} */ (false)
            : typeof target === "string"
            ? getTargetProperties(target, options.context)
            : getTargetsProperties(target, options.context);
  // 省略...
  // 在省略的代码中,targetProperties 会影响 60 多处代码,所以批改 webpackOptionsSchema 再通过其余大量代码,// 以达到咱们最后的目标(容许 target 为 function), 这条路径是不可行的。}

既然不能间接容许 target 为 function,咱们就须要持续浏览源码,以期发现其余的实现形式。

6. WebpackOptionsApply

解释下面提到的 WebpackOptionsApply

// lib/WebpackOptionsApply.js/WebpackOptionsApply
// 一大堆模块导入...

class WebpackOptionsApply extends OptionsApply {constructor() {super();
    }

  // 在 WebpackOptionsApply.process 办法内 apply 了十分多的插件,代码有 500 多行
    process(options, compiler) {
        compiler.outputPath = options.output.path;
        compiler.recordsInputPath = options.recordsInputPath || null;
        compiler.recordsOutputPath = options.recordsOutputPath || null;
        compiler.name = options.name;

    // options.externalsPresets.node/electronMain 等的值由 options.target 管制
    // 从这也能看出咱们间接批改 target 为 function 是不可行的。if (options.externalsPresets.node) {
      // options.target 蕴含 node 时,会进入这里
      // 因为快利用的指标运行时坏境相似于浏览器,所以咱们只须要关注 options.externalsPresets.web = true 局部的代码
            const NodeTargetPlugin = require("./node/NodeTargetPlugin");
            new NodeTargetPlugin().apply(compiler);
        }
        if (options.externalsPresets.electronMain) {// ...}
        if (options.externalsPresets.electronPreload) {// ...}
        if (options.externalsPresets.electronRenderer) {// ...}
        if (
            options.externalsPresets.electron &&
            !options.externalsPresets.electronMain &&
            !options.externalsPresets.electronPreload &&
            !options.externalsPresets.electronRenderer
        ) {// ...}
        if (options.externalsPresets.nwjs) {// ...}
        if (options.externalsPresets.webAsync) {// ...} else if (options.externalsPresets.web) {
            //@ts-expect-error https://github.com/microsoft/TypeScript/issues/41697
      // 在 ExternalsPlugin 及其依赖的插件中咱们仍然没有发现能够达到目标的办法
      // 持续往下浏览
            const ExternalsPlugin = require("./ExternalsPlugin");
            new ExternalsPlugin("module", /^(https?:\/\/|std:)/).apply(compiler);
        }

        new ChunkPrefetchPreloadPlugin().apply(compiler);

        if (typeof options.output.chunkFormat === "string") {// ...}

        if (options.output.enabledChunkLoadingTypes.length > 0) {
      // 当 options.target = 'web',并且没有配置 output.outputenabledchunkloadingtypes 时
      // output.outputenabledchunkloadingtypes = ['jsonp', 'import-scripts']
            for (const type of options.output.enabledChunkLoadingTypes) {
        // 该局部的配置参考 webpack 官网文档 
        // https://webpack.js.org/configuration/output/#outputenabledchunkloadingtypes
        // 在官网文档上提到,enabledChunkLoadingTypes 的值个别是 webpack 主动生成的(受 options.target 影响),// 不须要用户配置。// 然而进入 EnableChunkLoadingPlugin(type).apply 咱们会发现,// 通过自定义 outputenabledchunkloadingtypes 的值
        // 配合自定义插件,咱们能够实现与 target 为 function 时同样的性能。// EnableChunkLoadingPlugin(type).apply 的解释请看上面一个代码块
                const EnableChunkLoadingPlugin = require("./javascript/EnableChunkLoadingPlugin");
                new EnableChunkLoadingPlugin(type).apply(compiler);
            }
        }
    // 一大堆外部插件的 apply...

}

7. EnableChunkLoadingPlugin(type).apply

解释下面提到的 EnableChunkLoadingPlugin(type).apply

// lib/javascript/EnableChunkLoadingPlugin.js
class EnableChunkLoadingPlugin {constructor(type) {this.type = type;}

    apply(compiler) {const { type} = this;

        // Only enable once
        const enabled = getEnabledTypes(compiler);
        if (enabled.has(type)) return;
        enabled.add(type);

        if (typeof type === "string") {
      // 下面提到
      // 当 options.target = 'web',并且没有配置 output.outputenabledchunkloadingtypes 时
      // output.outputenabledchunkloadingtypes = ['jsonp', 'import-scripts']
      // 咱们能够通过设置 output.outputenabledchunkloadingtypes = ['import-scripts']、// options.target = 'web' 来禁用掉 JsonpChunkLoadingPlugin 这个插件,而后通过自定义
      // 一个 myJsonpChunkLoadingPlugin 来实现自定义的 jsonp 加载逻辑或其余性能,以此达到与应用
      // webpack4 的自定义 target 一样的性能
            switch (type) {
                case "jsonp": {const JsonpChunkLoadingPlugin = require("../web/JsonpChunkLoadingPlugin");
                    new JsonpChunkLoadingPlugin().apply(compiler);
                    break;
                }
                case "import-scripts": {const ImportScriptsChunkLoadingPlugin = require("../webworker/ImportScriptsChunkLoadingPlugin");
                    new ImportScriptsChunkLoadingPlugin().apply(compiler);
                    break;
                }
                case "require": {// ...}
                case "async-node": {// ...}
                case "import":
                    // ...
                case "universal":
                    // ...
                default:
                    // ...
            }
        } else {
            // TODO support plugin instances here
            // apply them to the compiler
        }
    }
}

module.exports = EnableChunkLoadingPlugin;

通过下面的解说,咱们理解了 webpack 大抵的执行流程,也达到了咱们最后的指标——实现与 webpack4 中 target 为自定义 function 时一样的性能。并且咱们会发现,只有浏览了源码咱们能力更好的了解 webpack 的配置,甚至能配置出 webpack 文档上都没有提及的用法。

对于 Compiler 和 Compilation 对象,请参考这里

在下篇文章中,咱们将会探讨一下 webpack 长久换缓存的实现原理,以期了解为何 webpack 长久换缓存能带来如此微小的性能晋升。

正文完
 0