一、场景说明

webpack 构建的项目中,存在多个模块,这里举例存在两个模块,分别是 main.jsapp.js

正常构建过程中,我们会构建出 main.js 和 app.js 两个 filename 的文件,一旦项目更新,模块的缓存更新会存在问题,所以一般我们会采用 hash 的方式,然而在多模块构建的场景下,hash 并不能满足这种场景的需求。

比如项目中有 10+ 模块,如果通过 hash 构建,所有的模块的 hash 都会发生变化,所有的 hash 都需要更新,这并不符合最小更新原则。

示例项目中有两个入口,分别是 app.js 和 main.js,文件目录如下:

55188-9jzrqh89dqs.png

1、html template

通过插件 html-webpack-plugin 生成默认的 html 模板,留了两个 id 的口子:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <div id="app"></div>
    <div id="time"></div>
</body>
</html>

2、main.js

main.js 用于向 #app 注入标题

var app = document.querySelector('#app');

app.innerHTML = '<h1> APP  </h1>'

3、app.js

app.js 用于向 #time 动态注入时间

var time = document.querySelector('#time');

setInterval(function () {
    time.innerHTML = '<h3> ' + Date.now() + '  </h3>';
}, 1000);

二、hash 构建的场景

hash 表示什么?hash 在 webpack V1 中的定义是某个版本的资源的编译进程, 因此代表的是本次编译整体

如果 模块 output 的 filename 通过 hash 构建,则在输出过程中,所有的模块会被打上同一个 hash。

一个简单的 hash 构建示例:

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');

module.exports = {
    entry: {
        'main': path.resolve(__dirname, './src/main.js'),
        'app': path.resolve(__dirname, './src/app.js'),
    },
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: '[name].[hash:8].js'
    },
    plugins: [
        new CleanWebpackPlugin(),
        new HtmlWebpackPlugin({
            template: path.resolve(__dirname, './public/index.html')
        }),
    ],
}

构建结果:

39510-3dx9lbablj9.png

可以发现两个 js 文件的 hash 是完全一样的

如果我改动了 app.js 然后再次构建:

63226-edaun2uovgd.png

两者的 hash 还是一样,这就意味着,虽然我只改动了 app.js 但是项目浏览器更新的时候,无论是浏览器还是 CDN 都需要更新 main.js ,这显然不符合增量更新的需求。

三、chunkhash 构建

从上面可以看出,hash 并不适合增量更新的构建场景。

chunk 在 webpack 中表示的是散列模块经过合并后的 ,比如上面 main.js 和 app.js 都是不同的 chunk

使用 chunkhash 构建的配置:

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');

module.exports = {
    entry: {
        'main': path.resolve(__dirname, './src/main.js'),
        'app': path.resolve(__dirname, './src/app.js'),
    },
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: '[name].[chunkhash:8].js'
    },
    plugins: [
        new CleanWebpackPlugin(),
        new HtmlWebpackPlugin({
            template: path.resolve(__dirname, './public/index.html')
        }),
    ],
}

第一次构建结果,可以发现 app 和 main 是两个 chunk,因此 hash 也是不同的

81398-64snsgollq8.png

当修改了 app.js,重新构建:

25094-l9vkyzu7geb.png

可以发现,只有 app.js 的 hash 变化了,而 main.js 是没有任何变化的,因此 chunkhash 是可以满足增量更新的输出的。

需要注意的是,webpack V1 是无法做到的,因为对于 chunkhash 的实现上有点缺陷

四、contenthash 构建

上面只是对 js chunk 进行了处理,实现了增量更新的构建场景,但是一旦涉及到 css 的增量更新会出现问题。

1、加入 css 的项目

改造一下上面的项目,创建了两个 css 文件,并且都在 app.js 中引入,其中项目目录如下:

48783-b251tjuft8u.png

app.css 内容:

body{
    display: flex;
    justify-content: center;
    align-items: center;
}
#div {
    color:#FF5000;
}
#time { 
    font-size: 36px;
}

app2.css 内容:

#time {
   color: red;
}

app.js 文件内容:

require('./css/app.css');
require('./css/app2.css');

import querystring from 'querystring';

var time = document.querySelector('#time');
setInterval(function () {
    time.innerHTML = '<h3> ' + Date.now() + '  </h3>';
}, 1000);

2、通过 mini-css-extract-plugin chunkhash 分离 css 文件

webpack 配置如下,其中对于 css 文件,通过 nini-css-extract-plugin 处理,注意 webpack V4 之后,不能通过 extract-text-plugin 处理了

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
    mode: 'development',
    entry: {
        'main': path.resolve(__dirname, './src/main.js'),
        'app': path.resolve(__dirname, './src/app.js'),
    },
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: '[name].[chunkhash:8].js'
    },
    module: {
        rules: [
            {
                test: /\.css$/,
                use: [
                    {
                        loader: MiniCssExtractPlugin.loader,
                        options: {
                            // you can specify a publicPath here
                            publicPath: '../assets/',
                            hmr: process.env.NODE_ENV === 'development',
                        },
                    },
                    'css-loader'
                ]
            }
        ]
    },

    plugins: [
        new CleanWebpackPlugin(),
        new HtmlWebpackPlugin({
            template: path.resolve(__dirname, './public/index.html')
        }),
        new MiniCssExtractPlugin({
            filename: '[name].[chunkhash:8].css',
            chunkFilename: '[id].css',
        }),
    ],
}

可以发现,在插件的配置中,filename 我是用了 chunkhash:8 作为 chunk:

new MiniCssExtractPlugin({
            filename: '[name].[chunkhash:8].css',
            chunkFilename: '[id].css',
        }),

因此构建结果如下:

47720-jrzi69lzryj.png

可以发现的是,app.css 和 app.js 的 hash 是一样的, 这就意味着,如果我只变动了 app.css 或者 app2.css,没有变动 app.js ,我的 app.js 的文件名也是发生了变动

下面是修改了 app2.css 然后构建的结果:

47830-4vxn25qjwhm.png

可以发现,app.css 和 app.js 全部都发生了变化,这显然也不符合我们的要求

3、通过 mini-css-extract-plugin contenthash 分离 css

变动不多,只是将插件的配置和 output 中 filename 变化了一下:

    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: '[name].[contenthash:8].js'
    },
        new MiniCssExtractPlugin({
            filename: '[name].[contenthash:8].css',
            chunkFilename: '[id].css',
        }),

第一次构建结果如下:

74689-5z1omhw7bib.png

只修改 app.css ,然后进行构建,第二次构建结果如下:

52840-bz1y9ewzjzu.png

此时我们发现,即使我的 css 发生了变化,但是 app.js 的构建结果是没有变化的,因此也不需要 CDN 去更新这个文件

五、完整代码示例

github 地址:

https://github.com/postbird/wenpack-incremental-update-build-demo