webpack 的魔法注释是怎样实现prefetch/preload的?

586 阅读3分钟

👆上一节我们讲可以通过 import() API 在 webpack 中进行代码分割,分割出一个新的 chunk,在浏览器中,将通过 JSONP 的方式加载该 chunk 的脚本。

没看之前的可以不用管,本节只需要理解一些概念即可,因为这节webpack打包出来的代码比较繁琐,所以不会有整个流程的代码,只会贴上一些核心代码。

你可能会觉得比较水,真的对不住了,我只是个人记录~

而在 webpack 中,还可以通过魔法注释,对 chunk 的异步加载进行一系列优化。一般常见的就是下面这三种了👇

import(
  /* webpackChunkName: "sum" */
  /* webpackPrefetch: true */
  /* webpackPreload: true */
  /* ...... */
  './sum'
);

具体有哪些魔法注释,详细请看 webpack文档

  1. webpackChunkName: 指定 name 作为替代 chunkId 来加载文件
  2. webpackPrefetch:告诉浏览器将来可能需要该资源进行某些导航跳转(浏览器在闲置时间加载chunk 文件
  3. 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标签中去。