知海

懒加载

webpackjsorg-main指南-教程

懒加载

T> 本指南是代码分离的补充说明。如果你尚未阅读该指南,请先阅读。

懒加载,或者称为“按需加载”,是一种优化站点或应用程序的绝佳方式。这种做法本质上涉及在逻辑断点处拆分代码,然后在用户执行了需要或将需要新代码块的操作时加载它。这加快了应用程序的初始加载速度,并减轻了其整体重量,因为某些代码块可能永远不会被加载。

动态导入示例

让我们以代码分离中的示例为基础,稍作调整以更深入地展示这一概念。那里的代码确实生成了一个单独的代码块 lodash.bundle.js,并且在脚本运行时就在技术上“懒加载”了它。问题在于,加载该代码块不需要任何用户交互——这意味着每次加载页面时,都会发起请求。这对我们帮助不大,而且会对性能产生负面影响。

让我们尝试一些不同的方法。我们将添加一个交互,当用户点击按钮时向控制台记录一些文本。但是,我们将等到交互首次发生时,才加载那段代码(print.js)。为此,我们将回顾并重新编写《代码分离》中的最终《动态导入》示例,并将 lodash 保留在主代码块中。

项目

diff 复制代码
webpack-demo
 ├── package.json
 ├── package-lock.json
 ├── webpack.config.js
 ├── /dist
 ├── /src
 │   ├── index.js
+│   └── print.js
 └── /node_modules

src/print.js

js 复制代码
console.log(
  "The print.js module has loaded! See the network tab in dev tools...",
);

export default () => {
  console.log('Button Clicked: Here\'s "some text"!');
};

src/index.js

diff 复制代码
+ import _ from 'lodash';
+
- async function getComponent() {
+ function component() {
    const element = document.createElement('div');
-   const _ = await import(/* webpackChunkName: "lodash" */ 'lodash');
+   const button = document.createElement('button');
+   const br = document.createElement('br');

+   button.innerHTML = 'Click me and look at the console!';
    element.innerHTML = _.join(['Hello', 'webpack'], ' ');
+   element.appendChild(br);
+   element.appendChild(button);
+
+   // Note that because a network request is involved, some indication
+   // of loading would need to be shown in a production-level site/app.
+   button.onclick = e => import(/* webpackChunkName: "print" */ './print').then(module => {
+     const print = module.default;
+
+     print();
+   });

    return element;
  }

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

W> 请注意,在 ES6 模块中使用 import() 时,你必须引用 .default 属性,因为当 promise 被 resolve 时,返回的是实际的 module 对象。

现在让我们运行 webpack,并查看我们新的懒加载功能:

bash 复制代码
...
          Asset       Size  Chunks                    Chunk Names
print.bundle.js  417 bytes       0  [emitted]         print
index.bundle.js     548 kB       1  [emitted]  [big]  index
     index.html  189 bytes          [emitted]
...

延迟导入示例

W> 此功能不是懒惰地“加载”模块,而是懒惰地“评估”模块。这意味着模块仍然会被下载和解析,但其评估是懒惰的。

在某些情况下,将模块的所有用法转换为异步可能会很烦人或很困难,因为这强制对所有函数进行不必要的异步化,而无法仅延迟同步评估工作。

TC39 提案延迟模块评估旨在解决此问题。

该提案是引入一种新的语法导入形式,它只会返回一个命名空间奇异对象。使用时,模块及其依赖项不会被执行,但会在模块图被视为已加载之前,完全加载到准备执行的状态。

只有当访问此模块的属性时,才会执行(如果需要)执行操作。

此功能可通过启用 experiments.deferImport 来使用。

W> 此功能仍处于实验阶段,在 webpack 的未来版本中可能会发生变化。

项目

diff 复制代码
 webpack-demo
  ├── package.json
  ├── package-lock.json
  ├── webpack.config.js
  ├── /dist
  ├── /src
  │   ├── index.js
+ │   └── print.js
  └── /node_modules

src/print.js

js 复制代码
console.log(
  "The print.js module has loaded! See the network tab in dev tools...",
);

export default () => {
  console.log('Button Clicked: Here\'s "some text"!');
};

src/index.js

diff 复制代码
  import _ from 'lodash';
+ import defer * as print from './print';

  function component() {
    const element = document.createElement('div');
    const button = document.createElement('button');
    const br = document.createElement('br');

    button.innerHTML = 'Click me and look at the console!';
    element.innerHTML = _.join(['Hello', 'webpack'], ' ');
    element.appendChild(br);
    element.appendChild(button);

-   // Note that because a network request is involved, some indication
-   // of loading would need to be shown in a production-level site/app.
+   // In this example, the print module is downloaded but not evaluated,
+   // so there is no network request involved after the button is clicked.
-   button.onclick = e => import(/* webpackChunkName: "print" */ './print').then(module => {
+   button.onclick = e => {
      const print = module.default;
+     //                  ^ The module is evaluated here.

      print();
-   });
+   };

    return element;
  }

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

这类似于 CommonJS 风格的懒加载:

src/index.js

diff 复制代码
  import _ from 'lodash';
- import defer * as print from './print';

  function component() {
    const element = document.createElement('div');
    const button = document.createElement('button');
    const br = document.createElement('br');

    button.innerHTML = 'Click me and look at the console!';
    element.innerHTML = _.join(['Hello', 'webpack'], ' ');
    element.appendChild(br);
    element.appendChild(button);

    // In this example, the print module is downloaded but not evaluated,
    // so there is no network request involved after the button is clicked.
    button.onclick = e => {
+     const print = require('./print');
+     //            ^ The module is evaluated here.
      const print = module.default;
-     //                  ^ The module is evaluated here.

      print();
    };

    return element;
  }

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

将 import.defer() 与上下文模块一起使用

import.defer() 也适用于上下文模块 - 导入路径可以是动态表达式。webpack 将所有匹配的模块包含在模块图中,但所选模块的评估会延迟到首次访问命名空间对象上的属性时。

以下示例演示了使用动态上下文路径对语言环境模块进行延迟评估:

src/locales/en.js

js 复制代码
export const greeting = "Hello";

src/locales/fr.js

js 复制代码
export const greeting = "Bonjour";

src/index.js

text 复制代码
const language = navigator.language.split("-")[0]; // "en", "fr", etc.
const locale = import.defer("./locales/" + language + ".js");

document.getElementById("btn").addEventListener("click", () => {
  // The locale module is evaluated here, on first property access.
  document.getElementById("output").textContent = locale.greeting;
});

webpack 会准备好所有匹配的语言环境模块,以便它们可以随时执行,但只有在首次访问 locale.greeting 时,才会评估所选模块。这允许你加载多个语言环境文件而无需立即执行它们。

框架

许多框架和库对于如何在其方法论中实现这一点有自己的建议。以下是一些示例:

帮助我们改进文档

发现翻译问题或内容错误?请告诉我们。