👆上一节我们讲可以通过 import() API 在 webpack 中进行代码分割,分割出一个新的 chunk,在浏览器中,将通过 JSONP 的方式加载该 chunk 的脚本。
没看之前的可以不用管,本节只需要理解一些概念即可,因为这节webpack打包出来的代码比较繁琐,所以不会有整个流程的代码,只会贴上一些核心代码。
你可能会觉得比较水,真的对不住了,我只是个人记录~
而在 webpack 中,还可以通过魔法注释,对 chunk 的异步加载进行一系列优化。一般常见的就是下面这三种了👇
import(
/* webpackChunkName: "sum" */
/* webpackPrefetch: true */
/* webpackPreload: true */
/* ...... */
'./sum'
);
具体有哪些魔法注释,详细请看 webpack文档
- webpackChunkName: 指定 name 作为替代 chunkId 来加载文件
- webpackPrefetch:告诉浏览器将来可能需要该资源进行某些导航跳转(浏览器在闲置时间加载chunk 文件
- webpackPreload:告诉浏览器在当前导航期间可能需要该资源(浏览器在父chunk加载时并行加载
我们这里主要讨论一下webpack中是如何实现 prefetch/preload 的。我们将分别输出三组构建产物来进行对比
代码
webpack 配置如下👇
const webpack = require('webpack')
const path = require('path')
function f1() {
return webpack([
{
entry: './src/comment/index.js',
mode: 'none',
output: {
filename: '[name].[contenthash].js',
chunkFilename: '[name].[id].chunk.[contenthash].js',
path: path.resolve(__dirname, 'dist/comment'),
clean: true
}
},
{
entry: './src/prefetch/index.js',
mode: 'none',
output: {
filename: '[name].[contenthash].js',
chunkFilename: '[name].[id].chunk.[contenthash].js',
path: path.resolve(__dirname, 'dist/prefetch'),
clean: true
}
},
{
entry: './src/preload/index.js',
mode: 'none',
output: {
filename: '[name].[contenthash].js',
chunkFilename: '[name].[id].chunk.[contenthash].js',
path: path.resolve(__dirname, 'dist/preload'),
clean: true
}
}
])
}
f1().run(() => {
console.log('✅')
})
comment
的内容就不做展示了,只是作为对照组,代码在👉这里
Prefetch
示例代码如下👇
// index.js
setTimeout(() => {
import(
/* webpackChunkName: 'sum' */
/* webpackPrefetch: true */
'./sum').then(m => {
console.log(m.default(3, 4))
})
}, 3000)
// sum.js
const sum = (...args) => args.reduce((a, b) => a + b, 0)
export default sum
运行打包命令以后,可以发现在运行时代吗中能看到,在 webpack 中如果当前加载的 chunk 中有通过 webpackPrefetch 的依赖 chunk 时,就会创建一个<link rel="prefetch" href="依赖的chunk的路径">
标签添加到head
中。这样就实现了 magic comments 中的 prefetch
__webpack_require__.F.j = (chunkId) => {
if ((!__webpack_require__.o(installedChunks, chunkId) || installedChunks[chunkId] === undefined) && true) {
installedChunks[chunkId] = null;
var link = document.createElement('link');
if (__webpack_require__.nc) {
link.setAttribute("nonce", __webpack_require__.nc);
}
link.rel = "prefetch";
link.as = "script";
link.href = __webpack_require__.p + __webpack_require__.u(chunkId);
document.head.appendChild(link);
}
};
Preload
在使用 webpackPreload 时稍微需要注意一下使用方法,我看很多人都说 webpackPreload 使用了不生效👇
将魔法注释中的 webpackPrefetch 修改为 webpackPreload
这种方式打包出来会发现没有跟preload有任何关系的代码,是因为webpack在打包的过程中,确定了我们这个index.js
文件中的sum.js
是必然要被加载的(因为index.js
是入口文件是必然要被加载的),所以并没有做额外处理。
这也就意味着我们想要让preload生效不能让webpack能感知到preload
的文件是必然要被加载的文件。我在写文章的时候并没有想太明白,所以额外嵌套了一层import
,你们也可以尝试使用条件判断试试看。代码在这儿
我们看下 webpack 运行时代码,是怎样实现 preload 的
// 引入模块
__webpack_require__.e = (chunkId) => {
return Promise.all(Object.keys(__webpack_require__.f).reduce((promises, key) => {
// 遍历 __webpack_require__.f 上的方法进行执行
__webpack_require__.f[key](chunkId, promises);
return promises;
}, []));
};
// 通过 script 加载模块
__webpack_require__.f.j = (chunkId, promises) => {
// ......
}
// preload 最终会调用到下面的 __webpack_require__.H.j 完成 preload
__webpack_require__.f.preload = (chunkId) => {
var chunks = chunkToChildrenMap[chunkId];
Array.isArray(chunks) && chunks.map(__webpack_require__.G);
};
// 创建 link 通过 preload 加载 script
__webpack_require__.H.j = (chunkId) => {
if ((!__webpack_require__.o(installedChunks, chunkId) || installedChunks[chunkId] === undefined) && true) {
installedChunks[chunkId] = null;
var link = document.createElement('link');
link.charset = 'utf-8';
if (__webpack_require__.nc) {
link.setAttribute("nonce", __webpack_require__.nc);
}
link.rel = "preload";
link.as = "script";
link.href = __webpack_require__.p + __webpack_require__.u(chunkId);
document.head.appendChild(link);
}
};
可以看到,preload 的chunk 是在加载父chunk后(并不是等加载完成后)会进行加载 preload 的chunk。
prefetch 与 preload 的实现思路基本是一致的,在加载父chunk时会创建一个link
标签添加到head
标签中去。