浅析webpack里module/chunk/bundle区别、filename/chunkFilename区别、webpackChunkName作用、webpackPrefetch预拉取/webpackPreloa预加载作用与区别

一、modulechunkbundle 的区别

  看 webpack 文档的时候,对这 3 个名词云里雾里的,感觉他们都在说打包文件,但是一会儿 chunk 一会儿 bundle 的,逐渐就迷失在细节里了,所以我们要跳出来,从宏观的角度来看这几个名词。

  webpack 官网对 chunk 和 bundle 做出了解释,说实话太抽象了,我这里举个例子,给大家形象化的解释一下。

  首先我们在 src 目录下写我们的业务代码,引入 index.js、utils.js、common.js 和 index.css 这 4 个文件,目录结构如下

src/
├── index.css
├── index.html # 这个是 HTML 模板代码
├── index.js
├── common.js
└── utils.js
// index.css 写一点儿简单的样式:
    body {
        background-color: red;
    }
// utils.js 文件写个求平方的工具函数:
    export function square(x) {
        return x * x;
    }
// common.js 文件写个 log 工具函数:
    module.exports = {
      log: (msg) => {
        console.log('hello ', msg)
      }
    }
// index.js 文件做一些简单的修改,引入 css 文件和 common.js:
    import './index.css';
    const { log } = require('./common');
    log('webpack');

  webpack 的配置如下:

{
    entry: {
        index: "../src/index.js",
        utils: '../src/utils.js',
    },
    output: {
        filename: "[name].bundle.js", // 输出 index.js 和 utils.js
    },
    module: {
        rules: [
            {
                test: /\.css$/,
                use: [
                    MiniCssExtractPlugin.loader, // 创建一个 link 标签
                    'css-loader', // css-loader 负责解析 CSS 代码, 处理 CSS 中的依赖
                ],
            },
        ]
    }
    plugins: [
        // 用 MiniCssExtractPlugin 抽离出 css 文件,以 link 标签的形式引入样式文件
        new MiniCssExtractPlugin({
            filename: 'index.bundle.css' // 输出的 css 文件名为 index.css
        }),
    ]
}

  我们运行一下 webpack,看一下打包的结果,可以看出:

(1)index.css 和 common.js 在 index.js 中被引入,打包生成的 index.bundle.css 和 index.bundle.js 都属于 chunk 0

(2)utils.js 因为是独立打包的,它生成的 utils.bundle.js 属于 chunk 1

1、具体解释:

  对于一份同逻辑的代码,当我们手写下一个一个的文件,它们无论是 ESM 还是 commonJS 或是 AMD,他们都是 module

  当我们写的 module 源文件传到 webpack 进行打包时,webpack 会根据文件引用关系生成 chunk 文件,webpack 会对这个 chunk 文件进行一些操作;

  webpack 处理好 chunk 文件后,最后会输出 bundle 文件,这个 bundle 文件包含了经过加载和编译的最终源文件,所以它可以直接在浏览器中运行。

2、一般来说一个 chunk 对应一个 bundle,比如上图中的 utils.js -> chunks 1 -> utils.bundle.js;但也有例外,比如说上图中,我就用 MiniCssExtractPlugin 从 chunks 0 中抽离出了 index.bundle.css 文件。

3、小结:

  modulechunkbundle 其实就是同一份逻辑代码在不同转换场景下的取了三个名字:我们直接写出来的是 module,webpack 处理时是 chunk,最后生成浏览器可以直接运行的 bundle。

二、filenamechunkFilename 的区别

1、filename 是一个很常见的配置,就是对应于 entry 里面的输入文件,经过webpack 打包后输出文件的文件名。比如说经过下面的配置,生成出来的文件名为 index.min.js

{
    entry: {
        index: "../src/index.js"
    },
    output: {
        filename: "[name].min.js", // index.min.js
    }
}

2、chunkFilename 指未被列在 entry 中,却又需要被打包出来的 chunk 文件的名称。一般来说,这个 chunk 文件指的就是要懒加载的代码。比如说我们业务代码中写了一份懒加载 lodash 的代码:

// 文件:index.js
// 创建一个 button
let btn = document.createElement("button");
btn.innerHTML = "click me";
document.body.appendChild(btn);
// 异步加载代码
async function getAsyncComponent() {
    var element = document.createElement('div');
    const { default: _ } = await import('lodash');
    element.innerHTML = _.join(['Hello!', 'dynamic', 'imports', 'async'], ' ');
    return element;
}
// 点击 button 时,懒加载 lodash,在网页上显示 Hello! dynamic imports async
btn.addEventListener('click', () => {
    getAsyncComponent().then(component => {
        document.body.appendChild(component);
    })
})

  我们的 webpack 不做任何配置,还是原来的配置代码。

{
    entry: {
        index: "../src/index.js"
    },
    output: {
        filename: "[name].min.js",
    }
}

  这时候的打包结果会多出一个 1.min.js, 这个1.min.js就是异步加载的 chunk 文件。文档里这么解释:

output.chunkFilename 默认使用 [id].js 或从 output.filename 中推断出的值([name] 会被预先替换为 [id][id].

  文档写的太抽象,我们不如结合上面的例子来看:

(1)output.filename 的输出文件名是 [name].min.js[name] 根据 entry 的配置推断为 index,所以输出为 index.min.js

(2)由于 output.chunkFilename 没有显示指定,就会把 [name] 替换为 chunk 文件的 id 号,这里文件的 id 号是 1,所以文件名就是 1.min.js

  如果我们显式配置 chunkFilename,就会按配置的名字生成文件:

{
    entry: {
        index: "../src/index.js"
    },
    output: {
        filename: "[name].min.js",  // index.min.js
        chunkFilename: 'bundle.js', // bundle.js
    }
}

3、小结

  filename列在entry 中,打包后输出的文件的名称。

  chunkFilename未列在entry 中,却又需要被打包出来的文件的名称。

三、webpackPrefetchwebpackPreloadwebpackChunkName 是干什么的?

1、webpackChunkName

  前面举了个异步加载 lodash 的例子,我们最后把 output.chunkFilename 写死成 bundle.js。在我们的业务代码中,不可能只异步加载一个文件,所以写死肯定是不行的,但是写成 [name].bundle.js 时,打包的文件又是意义不明、辨识度不高的 chunk id

{
    entry: {
        index: "../src/index.js"
    },
    output: {
        filename: "[name].min.js",  // index.min.js
        chunkFilename: '[name].bundle.js', // 1.bundle.js,chunk id 为 1,辨识度不高
    }
}

  这时候 webpackChunkName 就可以派上用场了。我们可以在 import 文件时,在 import 里以注释的形式为 chunk 文件取别名

async function getAsyncComponent() {
    var element = document.createElement('div');
    // 在 import 的括号里 加注释 /* webpackChunkName: "lodash" */ ,为引入的文件取别名
    const { default: _ } = await import(/* webpackChunkName: "lodash" */ 'lodash');
    element.innerHTML = _.join(['Hello!', 'dynamic', 'imports', 'async'], ' ');
    return element;
}

2、webpackPrefetch 和 webpackPreload

  这两个配置一个叫预拉取(Prefetch),一个叫预加载(Preload),两者有些细微的不同。

(1)我们先说说 webpackPrefetch。在上面的懒加载代码里,我们是点击按钮时,才会触发异步加载 lodash 的动作,这时候会动态的生成一个 script 标签,加载到 head 头里。

  如果我们 import 的时候添加 webpackPrefetch

const { default: _ } = await import(/* webpackChunkName: "lodash" */ /* webpackPrefetch: true */ 'lodash');

  就会以 <link rel="prefetch" as="script"> 的形式预拉取 lodash 代码,这个异步加载的代码不需要手动点击 button 触发,webpack 会在父 chunk 完成加载后,闲时加载 lodash 文件。

(2)webpackPreload 是预加载当前导航下可能需要资源,他和 webpackPrefetch 的主要区别是:

  preload chunk 会在父 chunk 加载时,以并行方式开始加载。prefetch chunk 会在父 chunk 加载结束后开始加载。

  preload chunk 具有中等优先级,并立即下载。prefetch chunk 在浏览器闲置时下载。

  preload chunk 会在父 chunk 中立即请求,用于当下时刻。prefetch chunk 会用于未来的某个时刻

预拉取:父chunk结束后加载;闲时加载,优先级低;未来时刻。

预加载:父chunk加载时并行加载;立即加载,中等优先级;当下时刻。

3、小结:

  webpackChunkName 是为预加载的文件取别名,webpackPrefetch 会在浏览器闲置下载文件,webpackPreload 会在父 chunk 加载时并行下载文件。