对于采用单页应用作为前端架构的网站来说,会面临着一个网页需要加载的代码量很大的问题,因为许多功能都集中的做到了一个 HTML 里。 这会导致网页加载缓慢、交互卡顿,用户体验将非常糟糕。
导致这个问题的根本原因在于一次性的加载所有功能对应的代码,但其实用户每一阶段只可能使用其中一部分功能。 所以解决以上问题的方法就是用户当前需要用什么功能就只加载这个功能对应的代码,也就是所谓的按需加载。
如何使用按需加载
在给单页应用做按需加载优化时,一般采用以下原则:
- 把整个网站划分成一个个小功能,再按照每个功能的相关程度把它们分成几类。
- 把每一类合并为一个 Chunk,按需加载对应的 Chunk。
- 对于用户首次打开你的网站时需要看到的画面所对应的功能,不要对它们做按需加载,而是放到执行入口所在的 Chunk 中,以降低用户能感知的网页加载时间。
- 对于个别依赖大量代码的功能点,例如依赖 Chart.js 去画图表、依赖 flv.js 去播放视频的功能点,可再对其进行按需加载。
被分割出去的代码的加载需要一定的时机去触发,也就是当用户操作到了或者即将操作到对应的功能时再去加载对应的代码。 被分割出去的代码的加载时机需要开发者自己去根据网页的需求去衡量和确定。
由于被分割出去进行按需加载的代码在加载的过程中也需要耗时,你可以预言用户接下来可能会进行的操作,并提前加载好对应的代码,从而让用户感知不到网络加载时间。
用 Webpack 实现按需加载
Webpack 内置了强大的分割代码的功能去实现按需加载,实现起来非常简单。
举个例子,现在需要做这样一个进行了按需加载优化的网页:
- 网页首次加载时只加载 main.js 文件,网页会展示一个按钮, main.js 文件中只包含监听按钮事件和加载按需加载的代码。
- 当按钮被点击时才去加载被分割出去的 show.js 文件,加载成功后再执行 show.js 里的函数。
其中 main.js 文件内容如下:
window.document.getElementById('btn').addEventListener('click', function () { // 当按钮被点击后才去加载 show.js 文件,文件加载成功后执行文件导出的函数 import(/* webpackChunkName: "show" */ './show').then((show) => { show('Webpack'); }) }); show.js 文件内容如下: module.exports = function (content) { window.alert('Hello ' + content); };
代码中最关键的一句是 import(/* webpackChunkName: "show" */ './show') ,Webpack 内置了对 import(*) 语句的支持,当 Webpack 遇到了类似的语句时会这样处理:
- 以 ./show.js 为入口新生成一个 Chunk;
- 当代码执行到 import 所在语句时才会去加载由 Chunk 对应生成的文件。
import
返回一个 Promise,当文件加载成功时可以在 Promise 的 then 方法中获取到 show.js 导出的内容。
在使用 import()
分割代码后,你的浏览器并且要支持 Promise API 才能让代码正常运行, 因为 import()
返回一个 Promise,它依赖 Promise。对于不原生支持 Promise 的浏览器,你可以注入 Promise polyfill。
/* webpackChunkName: "show" */ 的含义是为动态生成的 Chunk 赋予一个名称,以方便我们追踪和调试代码。 如果不指定动态生成的 Chunk 的名称,默认名称将会是 [id].js。 /* webpackChunkName: "show" */ 是在 Webpack3 中引入的新特性,在 Webpack3 之前是无法为动态生成的 Chunk 赋予名称的。
为了正确的输出在 /* webpackChunkName: "show" */ 中配置的 ChunkName,还需要配置下 Webpack,配置如下:
module.exports = { // JS 执行入口文件 entry: { main: './main.js', }, output: { // 为从 entry 中配置生成的 Chunk 配置输出文件的名称 filename: '[name].js', // 为动态加载的 Chunk 配置输出文件的名称 chunkFilename: '[name].js', } };
其中最关键的一行是 chunkFilename: '[name].js ',
,它专门指定动态生成的 Chunk 在输出时的文件名称。 如果没有这行,分割出的代码的文件名称将会是 [id].js 。