webpack5 - 代码分离

103 阅读3分钟

代码分离是 webpack 中最引人注目的特性之一。此特性能够把代码分离到不同的 bundle 中,然后可以按需加载或并行加载这些文件。代码分离可以用于获取更小的 bundle、控制资源加载优先级,如果使用合理,会极大减小加载时间。

入口起点

这是迄今为止最简单直观的分离代码的方式。但是不够灵活,使用场景有限。

通过配置多个入口文件,打包之后也会对应多个输出 bundle

// webpack.config.js
const path = require("path");

module.exports = {
  mode: "development",
  entry: {
    index: "./src/index.js",
    another: "./src/another-module.js",
  },
  output: {
    filename: "[name].bundle.js",
    path: path.resolve(__dirname, "dist"),
    clean: true,
  },
};
// src/index.js
import _ from "lodash";

function component() {
  const element = document.createElement("div");
  element.innerHTML = _.join(["Hello", "webpack"], " ");
  return element;
}

document.body.appendChild(component());

// src/another-module.js
import _ from "lodash";

console.log(_.join(["Another", "module", "loaded!"], " "));

打包结果:

1.png

可以看到,输出的两个 bundle 中都引用 lodash。这就是重复引用。

防止重复

如何避免重复引用,通过下面两种方法可以做到:

1. 入口依赖

在配置文件中配置 dependOn 选项,以在多个 chunk 之间共享模块:

const path = require("path");

module.exports = {
  mode: "development",
  entry: {
    index: {
      import: "./src/index.js",
      dependOn: "shared",
    },
    another: {
      import: "./src/another-module.js",
      dependOn: "shared",
    },
    shared: "lodash",
  },
  output: {
    filename: "[name].bundle.js",
    path: path.resolve(__dirname, "dist"),
    clean: true,
  },
};

打包结果:

2.png

可以看到,lodash 这个公共模块被打包到了一个单独的 bundle 文件。

2. SplitChunksPlugin

SplitChunksPlugin 插件可以将公共的依赖模块提取到已有的入口 chunk 中,或者提取到一个新生成的 chunk。这是一个内置插件。

const path = require("path");

module.exports = {
  mode: "development",
  entry: {
    index: "./src/index.js",
    another: "./src/another-module.js",
  },
  output: {
    filename: "[name].bundle.js",
    path: path.resolve(__dirname, "dist"),
    clean: true,
  },
  optimization: {
    splitChunks: {
      // webpack 使用 chunk 的来源和名称生成名称(例如 vendors~main.js)。该选项可以指定生成名称的分隔符。默认值:'~'
      automaticNameDelimiter: "~",
      // 对哪些chunk 进行优化。可选值:all,async 和 initial
      chunks: "all",
      // 按需加载时最大并行请求数,默认 30
      maxAsyncRequests: 30,
      // 最少被多少个 chunk 引用才会拆分,默认 1
      minChunks: 1,
      // 生成 chunk 的最小体积(以 bytes 为单位),默认是 20000
      minSize: 20000,
    },
  },
};

打包结果:

3.png

动态导入

webpack 提供了两个类似的技术实现动态拆分代码。

  • 第一种(推荐),使用符合 ECMAScript 提案 的 import() 语法
  • 第二种,是 webpack 的遗留功能,使用 webpack 特定的 require.ensure

调用 import() 会在内部使用 promise。因此如果在旧版本浏览器中(例如,IE 11)使用 import(),需要使用一个 polyfill 库(例如 es6-promisepromise-polyfill)来 shim Promise。

// webpack.config.js
const path = require("path");

module.exports = {
  mode: "development",
  entry: {
    index: "./src/index.js",
  },
  output: {
    filename: "[name].bundle.js",
    path: path.resolve(__dirname, "dist"),
    clean: true,
  },
};
// src/index.js
function getComponent() {
  return import("lodash")
    .then(({ default: _ }) => {
      const element = document.createElement("div");

      element.innerHTML = _.join(["Hello", "webpack"], " ");
      return element;
    })
    .catch((error) => "An error occurred while loading the component");
}

getComponent().then((component) => {
  document.body.appendChild(component);
});

打包结果:

4.png

预获取/预加载模块

Webpack v4.6.0+ 增加了对预获取(prefetch)和预加载(preload)的支持。

在声明 import 时,使用下面这些内置指令,可以让 webpack 输出“resource hint”,来告知浏览器:

  • prefetch(预获取):将来某些导航下可能需要的资源
  • preload(预加载):当前导航下可能需要资源
import(/* webpackPrefetch: true */ "./path/to/LoginModal.js");

这会生成 <link rel="prefetch" href="login-modal-chunk.js"> 并追加到页面头部,指示浏览器在闲置时间预取 login-modal-chunk.js 文件。

只要父 chunk 完成加载,webpack 就会添加预获取提示。

与预获取指令相比,预加载指令有许多不同之处:

  • 预加载 chunk 会在父 chunk 加载时,以并行方式开始加载。预加载 chunk 会在父 chunk 加载结束后开始加载。
  • 预加载 chunk 具有中等优先级,并立即下载。预加载 chunk 在浏览器闲置时下载。
  • 预加载 chunk 会在父 chunk 中立即请求,用于当下时刻。预加载 chunk 会用于未来的某个时刻。
  • 浏览器支持程度不同。
import(/* webpackPreload: true */ "ChartingLibrary");

通过 <link rel="preload"> 请求 charting-library-chunk