学一点Webpack配置:Webpack的优化

发布于 大漠

上一篇中花了14小节主要和大家一起探讨了Webpack 4.x的一些基本配置,比如初始化项目,添加各种Loader、React、Typescript、Sass、PostCSS、CSS Modules等配置、图片加载、字体加载以及各种代码检测的能力,比如ESLint的配置、Prettier的配置和StyleLint的配置。在接下来这个部分,主要和大家一起来探讨Webpack 4.x中的一些优化方面的配置。比如开发环境的优化、Webpack自身的优化、文件压缩和依赖监控以及应用分析相关的配置。如果感兴趣的话,欢迎继续往下阅读。

在阅读接下来的内容之前,如果你没有阅读上一篇,建议你先花点时间阅读上一篇,这样会更有系统性的学习

Step15:开发环境优化

请把分支切换到step15查看代码

开启局部模块热重载

在这一节中我们来对基本环境做一些优化。在开发环境中引入webpack.HotModuleReplacementPlugin用于启用局部模块热重载,方便我们开发。

我们在Step03中将Webpack的环境分离出来了,有关于Webpack开发环境的配置都将在webpack.dev.js做调整。如果我们要在开发环境加上webpack.HotModuleReplacementPlugin就需要在webpack.dev.jsplugins来做调整:

// ...

module.exports = merge(commonConfig, {
    // ...

    // 开发环境下需要的相关插件配置
    plugins: [new webpack.HotModuleReplacementPlugin()],

    // ...
});

另外在devServer也做一些调整:

// 开发服务器
devServer: {
    hot: true, // 热更新,无需手动刷新
    contentBase: DIST_PATH, //
    host: '0.0.0.0', // host地址
    port: 8080, // 服务器端口
    historyApiFallback: true, // 该选项的作用所用404都连接到index.html
    overlay: {
        // 当出现编译错误或警告时,就在页面上显示一层黑色的背景层和错误信息
        errors: true,
    },
    inline: true,
},

使用静态资源路径

网页中总是会使用到一些静态资源,将这些静态资源部署到CDN服务上,使用得用户可以就近访问资源,加快访问速度。在Webpack中,我们可以通过publicPatch来配置CDN服务器对应的URL

就我们这个工程而言,可以在webpack.common.jsoutput中指定publicPath

// webpack.common.js

// 编译输出的JS入路径
// 告诉Webpack在哪里输出它所创建的bundle,以及如何命名这些文件
output: {
    path: DIST_PATH, // 创建的bundle生成到哪里
    filename: '[name].bundle.js', // 创建的bundle的名称
    sourceMapFilename: '[name].js.map', // 创建的SourceMap的文件名
    publicPath: '/', // 指定存放静态资源的CDN地址
},

为导出的JS文件添加hash

当我们修改代码,bundle被重新打包,很可能在客户端上看到的效果并不是最新的效果,这有可能是缓存所造成的,有的时候需要多次刷新浏览器,甚至是要手动去清除缓存。不管是在开发环境还是生产环境,这直接影响我们的开发效率。在Webpack中我们可以很轻易的解决这个问题。只需要在输出的JS文件上加上hash值,这样一来,每次输出的JS文件名都会不同,那么文件也就不会被缓存。

只需要在outputfilename值上添加hash值:

// webpack.common.js

output: {
    path: DIST_PATH, // 创建的bundle生成到哪里
    filename: '[name].bundle.[hash].js', // 创建的bundle的名称
    sourceMapFilename: '[name].js.map', // 创建的SourceMap的文件名
    publicPath: '/', // 指定存放静态资源的CDN地址
},

输出的bundle.js会每次都带上相应的hash值:

注意,如果你在Webpack配置中使用了webpack.HotModuleReplacementPlugin()的话,那么在output.filename中的值不能使用[chunkhash],只能使用[hash]

这样一来每次修改代码都会生成一个带hash的JS文件。要是你执行npm run build同样会这样,并且会让你项目中/dist目录下有关于bundle.js的文件会越来越多,比如:

编译前清理dist目录

上一节我们看到了,每次都会为bundle.js新增文件,如果多次修改,再次编译或打包就会在/dist目录下生成一堆的JS文件,但上一次打包的文件对于我们来说没有实际用处。因此,我们需要在每次打包时清除/dist目录下的旧文件。

实现该功能,可以借助Webpack的CleanWebpackPlugin来实现,在配置之前先安装该插件:

⇒  npm i clean-webpack-plugin -D

然后webpack.common.js中添加相应的配置:

// webpack.common.js
// 注意最新版本引用CleanWebpackPlugin 不能使用 const CleanWebpackPlugin = require('clean-webpack-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin');

module.exports = {
    // ...

    // 模块解析
    module: {
        rules: [
        // ...
        ]

    // 插件
    plugins: [
        new CleanWebpackPlugin(),
        new HtmlWebpackPlugin({
            inject: true,
            template: HtmlWebpackTemplate,
            appMountId: 'root',
            filename: 'index.html',
        }),
    ],
};

另外一个细节,如果配置中同时出现CleanWebpackPluginHtmlWebpackPlugin时,CleanWebpackPlugin需要放置在HtmlWebpackPlugin的前面。

配置完成之后,你在命令终端执行npm run build的时候,/dist目录下中以前的bundle.js会自动清除。

这里额外提一下,在HtmlWebpackPlugin的配置中我们可以添加minify相关的配置,这样会将打包生的.js.css文件引入到.html文件中:

// webpack.common.js
plugins: [
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
        inject: true,
        template: HtmlWebpackTemplate,
        appMountId: 'root',
        filename: 'index.html',
        minify: {
            removeComments: true, // 去掉注释
            collapseWhitespace: true, // 去掉多余空白
            removeAttributeQuotes: true, // 去掉一些属性的引号,例如id="moo" => id=moo
        },
    }),
],

有了上面的配置之后,在代码中的注释、空白等就会自动清除,比如App.tsx中的注释:

<!-- App.tsx -->
<div style={{ textAlign: 'center' }}>
    <img src={String(Security)} />
    {/* 下面这样引用,图片会报404错误 */}
    {/* <img src="../../../../assets/images/security.svg" alt=""/> */}
</div>

编译出来.html中就看不到了。如下图所示:

Step16:Webpack的优化

请把分支切换到step16查看代码

在这一步我们主要来一起探讨Webpack中的一些优化。

文件路径优化

在Webpack中我们配置了resolve.extension之后可以不用在requireimport的时候加文件扩展名,Webpack会依次尝试添加扩展名进行匹配。在resolve中还可以通过alias来配置别名,可以加快Webpack查找模块的速度。

// webpack.common.js

alias: {
    '@': path.resolve(__dirname, '../src'),
    '@components': path.resolve(__dirname, '../src/components'),
    '@pages': path.resolve(__dirname, '../src/pages'),
    '@images': path.resolve(__dirname, '../src/assets/images'),
    '@fonts': path.resolve(__dirname, '../src/assets/fonts'),
    '@icons': path.resolve(__dirname, '../src/assets/icons'),
},

在实际使用的时候,我们就可以使用定义好的别名了:

<!-- App.tsx -->
const Security = require('@images/security.svg');

// app.css
body {
    background: url("~@images/body-background.jpg") no-repeat center,
        linear-gradient(to bottom, #f560a9, #09aefa, #2390af) no-repeat center;
}

有一点需要特别注意,在TypeScript环境下,如果仅在Webpack中配置别名是不够的,比如:

<!-- App.tsx -->
import Button from '@components/Button/Button';

编译的时候会报错:

Failed to compile.

[at-loader] ./src/pages/index/components/App/App.tsx:5:20 
    TS2307: Cannot find module '@components/Button/Button'.

我们需要在tsconfig.json中添加paths的配置:

{
    "compilerOptions": {
        "sourceMap": true,
        "noImplicitAny": false,
        "noUnusedLocals": true,
        "noUnusedParameters": true,
        "module": "es2015",
        "target": "es6",
        "lib": [
            "es2015",
            "es2017",
            "dom"
        ],
        "removeComments": true,
        "allowSyntheticDefaultImports": true,
        "esModuleInterop": true,
        "moduleResolution": "node",
        "pretty": true,
        "jsx": "preserve",
        "allowJs": true,
        "baseUrl": ".",
        "rootDir": "./src",
        "outDir": "./dist/",
        "paths": {
            "@/*": ["./src/*"],
            "@components/*": ["./src/components/*"],
            "@pages/*": ["./src/pages/*"],
            "@images/*": ["./src/assets/images/*"],
            "@fonts/*": ["./src/assets/fonts/*"],
            "@icons/*": ["./src/assets/icons/*"],
        }
    },
    "include": ["./src/**/*"],
    "exclude": ["node_modules"]
}

上面的问题就解决了。

优化CSS文件

如果不做相关的配置,打包的时候CSS代码会直接打到JavaScript文件中。在Webpack中使用MiniCssExtractPlugin插件可以单独生成.css文件。这样CSS和JavaScript文件可以并行下载,提高页面加载性能。

先安装mini-css-extract-plugin

⇒  npm i mini-css-extract-plugin -D

由于MiniCssExtractPlugin插件还不支持**HMR**,为了不影响开发效率,因为需要对开发环境做一下判断。目前有两种方式:

  • webpack.common.js中有关于样式的配置分开到webpack.dev.jswebpack.prod.js中分别处理,在webpack.prod.js中配置MiniCssExtractPlugin插件
  • webpack.common.js中通过process.env.NODE_ENV的值来做判断,如果值为production时才配置MiniCssExtractPlugin插件

在这里我们采用第二种方式:

// webpack.common.js

// ...
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
// ...

// 模块解析
module: {
    rules: [
        // ...

        // CSS Loader
        {
            test: /\.(sc|sa|c)ss$/,
            exclude: /node_modules/,
            include: path.resolve(__dirname, '../src'),
            use: [
                {
                    loader: process.env.NODE_ENV !== 'dev' ? MiniCssExtractPlugin.loader : 'style-loader',
                    options: {
                        sourceMap: true,
                    },
                },
                {
                    loader: 'css-modules-typescript-loader',
                    options: {
                        namedExport: true,
                        camelCase: true,
                        sass: true,
                        modules: true,
                    },
                },
                {
                    loader: 'css-loader',
                    options: {
                        importLoaders: 1,
                        sourceMap: true,
                        modules: {
                            localIdentName: '[name]__[local]___[hash:base64:5]',
                        },
                    },
                },
                {
                    loader: 'postcss-loader',
                    options: {
                        sourceMap: true,
                    },
                },
                {
                    loader: 'sass-loader',
                    options: {
                        sourceMap: true,
                    },
                },
            ],
        },
        //...
        ],
    },

    // 插件
    plugins: [
        // ...
        new MiniCssExtractPlugin({
            filename: 'css/[name].[hash].css',
            chunkFilename: 'css/[name]-[id].[hash].css',
        }),
    ],
};

执行npm run build之后,CSS就抽取出来了,如下所示:

同时在/dist目录下会新增一个/css目录,抽出来的.css文件也会放置到这个/css目录下:

Webpack会把抽出来的CSS放置到/dist/index.html</head>中:

虽然MiniCssExtractPlugin成为CSS打包首选,相比之前可以异步加载、不重复编译,更易于使用,但其有一个缺陷,不支持CSS热更新。所以在开发环境之下,需要引入css-hot-loader,以便支持热更新:

⇒  npm i css-hot-loader -D

在开发环境上加上css-hot-loader

// webpack.common.js

// CSS Loader
{
    test: /\.(sc|sa|c)ss$/,
    exclude: /node_modules/,
    include: path.resolve(__dirname, '../src'),
    use: [
        {
            loader:
            process.env.NODE_ENV !== 'dev'
                ? MiniCssExtractPlugin.loader
                : ['css-hot-loader', 'style-loader'],
            options: {
                sourceMap: true,
            },
        },
        {
            loader: 'css-modules-typescript-loader',
            options: {
                namedExport: true,
                camelCase: true,
                sass: true,
                modules: true,
            },
        },
        {
            loader: 'css-loader',
            options: {
                importLoaders: 1,
                sourceMap: true,
                modules: {
                    localIdentName: '[name]__[local]___[hash:base64:5]',
                },
            },
        },
        {
            loader: 'postcss-loader',
            options: {
                sourceMap: true,
            },
        },
        {
            loader: 'sass-loader',
            options: {
                sourceMap: true,
            },
        },
    ],
},

Webpack除了把CSS抽取出来之外,还有另两个插件可以帮助我们优化CSS:

下面我们来看看这两个插件如何配置到Webpack中。先安装这两个插件:

⇒  npm i optimize-css-assets-webpack-plugin css-split-webpack-plugin -D

注意这两个插件都是应用在生产环境下的,所以在webpack.prod.js添加相应的配置:

// webpack.prod.js

const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const CSSSplitWebpackPlugin = require('css-split-webpack-plugin').default;

module.exports = merge(commonConfig, {
    mode: 'production', // 设置Webpack的mode模式
    devtool: 'cheap-module-source-map',

    optimization: {
        minimizer: [
            new OptimizeCSSAssetsPlugin({
                cssProcessorOptions: {
                    map: {
                        inline: false,
                        annotation: true,
                    },
                },
            }),
        ],
    },
    // 生产环境下需要的相关插件配置
    plugins: [
        new CSSSplitWebpackPlugin({
            size: 4000,
            filename: '[name]-[part].[ext]',
        }),
    ],
});

上面的代码中,我们看到了optimization配置。该配置可以取替一些Webpack的插件,这里就不一一表述了。只提其中一个:

Webpack v4 中optimization.splitChunksoptimization.runtimeChunk一起替代了CommonsChunkPlugin插件,其中optimization.splitChunks主要用于拆分代码;optimization.runtimeChunk主要用于提取runtime代码。

optimization提供了一些默认配置:

optimization: {
    minimize: env === 'production' ? true : false,  // 开发环境不压缩
    splitChunks: {                                  // 拆分代码
        chunks: 'async',                            // initial: 初始模块; async:按需加载模块; all: 全部模块
        minSize: 30000,                             //模块超过3k自动被抽离成公共模块
        minChunks: 1,                               // 模块被引用大于或等于1,便分割
        maxAsyncRequests: 5,                        // 异步加载chunk的并发请求数量小于或等于5
        maxInitialRequests: 3,                      // 一个入口并发加载的chunks数量小于或等于3
        name: true,                                 // 默认由模块名+hash命名,名称相同时多个模块将合并为一个,可以设置为function
        automaticNameDelimiter: '~',                // 命名分隔符
        cacheGroups: {                              // 缓存组,会继承和覆盖splitChunks的配置
            default: {                              // 模块缓存规则,设置为false,默认缓存组将禁用
                minChunks: 2,                       //模块引用大于或等于2,拆分至vendors公共模块
                priority: -20,                      // 优先级
                reuseExistingChunk: true,           // 默认使用已有的模块
            },
            vendors: {
                test: /[\\/]node_modules[\\/]/,     // 表示默认拆分node_modules中的模块
                priority: -10
            }
        }
    }
}

其中**optimization.splitChunks是拆包优化的重点**, 如果你的项目引用了第三文件组件库,建议可以单独拆包,比如:

optimization: {
    // ... 
    splitChunks: {
        // ...
        cacheGroups: {
            antDesign: {
                name: 'chunk-antDesign', //单独将AntD拆包
                prioprity: 15, // 权重需大于其他缓存组
                test: /[\/]node_modules[\/]ant-design[\/]/
            }
        }
    }
}

有关于SplitChunksPlugin更详细的介绍可以点击这里查阅

对于CSS方面的优化,其实还有CSS的压缩。这里暂时不做相关的介绍,会把图片压缩、JavaScript和CSS压缩放到一起来介绍。

浏览器缓存优化

在Step15步中为导出的JS文件添加hash值,也是为了清除缓存的这个目的。这里我们借助HashedModuleIdsPlugin来对其做一点优化。通过前面的学习,我们已经知道,使用HashedModuleIdsPlugin可以当更改某个文件时,只改变对应文件的hash值,而不是所有文件都会改变。那在前面的基础上,来调整一下Webpack相关的配置。

// webpack.common.js

output: {
    path: DIST_PATH, // 创建的bundle生成到哪里
    //filename: 'js/[name].bundle.[hash].js', // 创建的bundle的名称
    //sourceMapFilename: 'js/[name].js.map', // 创建的SourceMap的文件名
    //publicPath: '/', // 指定存放静态资源的CDN地址
},

webpack.common.jsoutput.filenameoutput.sourceMapFilenameoutput.publicPath几个选项注释掉,然后针对开发环境和生产环境分进行不同的配置。

// webpack.dev.js
module.exports = merge(commonConfig, {
    // ...

    output: {
        filename: 'js/[name].bundle.js',                 // 创建的bundle的名称
        chunkFilename: 'js/[name].bundle.js',
        sourceMapFilename: 'js/[name].bundle.js.map',   // 创建的SourceMap的文件名
        publicPath: '/',                                // 指定存放静态资源的CDN地址
    },

    // 开发环境下需要的相关插件配置
    plugins: [
        new webpack.NamedModulesPlugin(),         //用于启动HMR时可以显示模块的相对路径
        new webpack.HotModuleReplacementPlugin(), // 开启模块热更新,热加载和模块热更新不同,热加载是整个页面刷新
    ],

    // ...
}

// webpack.prod.js
output: {
    filename: 'js/[name].[contenthash].js',         // entry对应的key值
    chunkFilename: 'js/[name].[contenthash].js',   // 间接引用的文件会走这个配置
    publicPath: '/',                               // 指定存放静态资源的CDN地址
},

优化Loader配置

在Webpack配置中会使用到各种Loader,比如babel-loadercss-loaderfile-loader等。我们可以对各种Loader做精准配置:

  • exclude:指定“排除不处理的目录”
  • include: 精确指定“要处理的目录”

这样就会缩小Loader加载搜索的范围,高概率的命中文件。这样一来,就可以在webpack.common.js的各种Loader配置的地方,加上范围的约束:

// webpack.common.js
rules: [
    {
        test: /\.(j|t)sx?$/,
        exclude: /node_modules/,                    // 排除不处理的目录
        include: path.resolve(__dirname, '../src'), // 精确指定要处理的目录
        use: [
            // ...
        ],
    },

    // CSS Loader
    {
        test: /\.(sc|sa|c)ss$/,
        exclude: /node_modules/,                    // 排除不处理的目录
        include: path.resolve(__dirname, '../src'), // 精确指定要处理的目录
        use: [
            // ...
        ],
    },

    // images loader
    {
        test: /\.(png|jp(e*g)|gif|svg|webp)(\?.*)?$/,
        exclude: /node_modules/,                    // 排除不处理的目录
        include: path.resolve(__dirname, '../src'), // 精确指定要处理的目录
        use: [
            // ...
        ],
    },

    // font loader
    {
        test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
        exclude: /node_modules/,                    // 排除不处理的目录
        include: path.resolve(__dirname, '../src'), // 精确指定要处理的目录
        use: [
            // ...
        ],
    },
],

另外,Webpack4 的babel-loader很慢,其实我们可以在babel-loader后面显式的添加cacheDirectory参数来改变之样的一个现状。

cacheDirectory是Loader的一个特定的选项,其默认值是false。我们可以显式指定目录用来缓存Loader的执行结果,这样就有可能减少Webpack构建时Babel重新编译过程。配置方式很简单,只需要在babel-loader后加上cacheDirectory即可:

loader: 'babel-loader?cacheDirectory',

// 或 
loader: 'babel-loader?cacheDirectory=true',

resolve优化配置

Webpack的resolve.modules配置模块库(即/node_modules)所在的位置,在JavaScript里出现import * as React from "react"这样的代码时,其寻找的模块路径不是相对的,也不是绝对的,代码会去/node_modules目录下寻找。而默认的配置会采用向上递归搜索的方式去搜索。而一个项目中,通常只会有一个/node_modules目录,而且都是在项目根目录下,为了减少搜索范围,我们在Webpack配置的时候可以直接写明/node_modules的全路径。同样的原于,使用别名(alias)的配置时,也可以这样做。

// webpack.common.js

// 将路径转换为绝对路径
function resolve(dir) {
    return path.join(__dirname, dir);
}

resolve中就可以像下面这样写:

resolve: {
    // 配置之后可以不用在require或是import的时候加文件扩展名,会依次尝试添加扩展名进行匹配
    extensions: ['.ts', '.tsx', '.js', '.jsx', '.json', '.css', '.scss'],
    modules: [ // 优化模块查找路径
        path.resolve('src'),
        path.resolve('node_modules') // 指定node_modules所在位置 当你import 第三方模块时 直接从这个路径下搜索寻找
    ],
    alias: {
        '@': path.resolve(__dirname, '../src'),
        '@components': path.resolve(__dirname, '../src/components'),
        '@pages': path.resolve(__dirname, '../src/pages'),
        '@images': path.resolve(__dirname, '../src/assets/images'),
        '@fonts': path.resolve(__dirname, '../src/assets/fonts'),
        '@icons': path.resolve(__dirname, '../src/assets/icons'),
    },
}

配置好src目录所在位置后,如果我们要引用在/src下的资源就会变得更轻松一些。比如:

|--/src
|----/components
|------Button.js
|------Card.js
|----Appp.js

那么在App.js中引入/components下的资源可以像下面这样操作:

// App.js
import Button from 'component/Button';
import Card from 'component/Card'

DellPlugin提升打包速度

在开发的时候总是会用到第三方库,比如React库,这些库在很长一段时间内都有可能不会更新,打包的时候分开打包可以提升打包速度,DllPlugin动态链接库插件,其原理就是把网页依赖的基础块抽离出来打包到dll文件中,当需要导入的模块存在于某个dll中时,这个模块不再被打包,而是去dll中获取。

首先在/build目录下创建一个webpack.dll.js文件,并写入下面的配置:

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

module.exports = {
    mode: 'production',
    entry: {
        vendors: ['react', 'react-dom'],
    },
    output: {
        path: path.resolve(__dirname, '../static/dll'),
        filename: '[name].dll.js',
        library: '[name]_lib',
    },
    plugins: [
        new CleanWebpackPlugin({
            cleanOnceBeforeBuildPatterns: [path.resolve(__dirname, '../static/dll/**/*')],
        }),
        new webpack.DllPlugin({
            path: path.resolve(__dirname, '../static/dll', '[name]-manifest.json'),
            name: '[name]_lib',
            context: process.cwd(),
        }),
    ],
};

然后package.jsonscripts添加新的执行命令:

"build:dll": "webpack --mode production --progress --config ./build/webpack.dll.js",

保存之后,在命令终端执行:

⇒  npm run build:dll

会在项目的根目录下创建/static/dll目录,并且会生成vendors.dll.jsvendors-manifest.json文件:

因为第三方库不会轻易的更新,所以没有每次打包的时候都执行npm run build:dll。另外,如果不想手动来将生成出来的/dll目录下的.js文件插入到index.html的话,需要再安装一个依赖add-asset-html-webpack-plugin,它会将我们打包后的*.dll.js文件注入到生成的index.html中。

先在命令端执行:

⇒  npm i add-asset-html-webpack-plugin -D

然后在webpack.prod.js中添加相应的配置代码:

// ...
const AddAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin');;

module.exports = merge(commonConfig, {
    mode: 'production', // 设置Webpack的mode模式
    devtool: 'cheap-module-source-map',

    output: {
        filename: 'js/[name].[contenthash].js', // entry对应的key值
        chunkFilename: 'js/[name].[contenthash].js', // 间接引用的文件会走这个配置
        publicPath: '/', // 指定存放静态资源的CDN地址
    },

    optimization: {
        // ...
    },
    // 生产环境下需要的相关插件配置
    plugins: [
        // ...
        new AddAssetHtmlWebpackPlugin({
            filepath: path.resolve(__dirname, '../static/dll/vendors.dll.js'), // 对应的 dll 文件路径
        }),
        new webpack.DllReferencePlugin({
            manifest: path.resolve(__dirname, '..', 'static/dll/vendors-manifest.json'),
        }),
    ],
});

这个时候执行npm run build就可以Build出打包后的文件。

**注意:**这里有一个细节需要注意,如果在执行npm run build之前需要至少先执行一次npm run build:dll

此时可以尝试执行http-server dist/就可以访问打包好的页面。但会发现有一个报错信息:

修改一下webpack.dll.js配置:

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

module.exports = {
    mode: 'production',
    entry: {
        vendors: ['react', 'react-dom'],
    },
    output: {
        path: path.resolve(__dirname, '../static/dll'),
        filename: '[name].dll.js',
        library: '[name]_lib',
        libraryTarget: 'var',
        pathinfo: true,
    },
    plugins: [
        new CleanWebpackPlugin({
            cleanOnceBeforeBuildPatterns: [path.resolve(__dirname, '../static/dll/**/*')],
        }),
        new webpack.DllPlugin({
            path: path.resolve(__dirname, '../static/dll', '[name]-manifest.json'),
            name: '[name]_lib',
            context: path.resolve(__dirname),
        }),
    ],
};

重新编译打包之后,再次访问报错信息就处理完了:

压缩文件

浏览器从服务器访问Web页面时会获取各种资源(HTML、JavaScript、CSS和图片等),如果文件越大,页面加载时间就会越长。为了提升页面加载速度和减少网络传输流量,可以对这些资源进行压缩。压缩的方法除了可以通过gzip算法对文件压缩外,还可以对文件自身进行压缩。对文件进行压缩的作用除了有提升Web页面加载速度的优势外,还具有混淆源码的作用。

在Webpack的配置中,我们就可以做到对Web页面资源进行压缩。

注意,在开发阶段不需要考虑太多压缩这方面的事情,所以有关于压缩相关的配置都是webpack.prod.js文件下进行配置。

压缩JavaScript

目前压缩JavaScript常见的工具是UglifyJSParallelUglifyPlugin

  • UglifyJS:会分析JavaScript代码语法树,理解代码含义,从而能做到诸如去掉无效代码,去掉日志输出代码和缩短变量名等优化
  • ParallelUglifyPlugin:会开启多个子进程,把对多个文件的压缩工作分配给多个子进程去完成,每个子进程其实还是通过UglifyJS去压缩代码,但是变成了并行执行。所以能更快的完成对多个文件的压缩工作

一般情况,这两种插件选择其中一种即可。我们先来看UglifyJS插件在Webpack中的配置。

首先把分支切换到Step16-2,查看源码。

使用UglifyJS之前需要先安装相关的插件,可以在命令终端执行下面的命令:

⇒  npm i uglifyjs-webpack-plugin -D

然后在webpack.prod.js中添加类似下面这样的配置代码:

const UglifyJsPlugin = require('uglifyjs-webpack-plugin');

module.exports = merge(commonConfig, {
    // ...

    optimization: {
        minimizer: [
            new UglifyJsPlugin({
                uglifyOptions: {
                    cache: true, // 开启文件缓存
                    parallel: true, // 使用多进程并行来提高构建速度
                    sourceMap: true, // 开启source map
                    warnings: false, // 在UglifyJS删除没有用到的代码时不输出警告
                    compress: {
                        drop_console: true, // 删除所有的console语句
                        collapse_vars: true, // 内嵌定义了但是只用到一次的变量
                        reduce_vars: true, // 提取出出现多次但是没有定义成变量去引用的静态值
                    },
                    output: {
                        beautify: false, // 最紧凑的输出
                        comments: false, // 删除所有的注释
                    },
                },
            }),
            new OptimizeCSSAssetsPlugin({
                cssProcessorOptions: {
                map: {
                    inline: false,
                    annotation: true,
                },
                },
            }),
        ],
    },
    // ...
});

保存上面的配置,执行npm run build,你可以看到下面的相关反馈。

有关于UglifyJS可以查阅官网文档

注意,在Webpack 4.x版本以前,uglifyjs-webpack-plugin还是放在plugins中进行配置,在Webpack 4.x中新增了optimization,上面的示例就是在该选项中进行配置的。

接下来再来看看ParallelUglifyPlugin的配置。首先把分支切换到Step16-3

要使用该插件来压缩JavaScript代码,首先也需要安装该插件,所以在命令终端执行下面的命令:

⇒  npm i webpack-parallel-uglify-plugin -D

同样在webpack.prod.jsoptimization.minimizer添加相应的配置:

const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin');

module.exports = merge(commonConfig, {
    // ...

    optimization: {
        minimizer: [
            new ParallelUglifyPlugin({
                // 多进程压缩
                uglifyJS: {
                warnings: false,
                output: {
                    comments: false,
                    beautify: false,
                },
                compress: {
                    drop_console: true,
                    collapse_vars: true,
                    reduce_vars: true,
                },
                },
            }),
            // ...
        ],
    },
    // 生产环境下需要的相关插件配置
    // ...
});

保存文件,执行npm run build的时候,打包代码就会对JavaScript代码进行压缩。

有关于webpack-parallel-uglify-plugin更详细的配置选项,可以查阅其官方文档

压缩CSS

请把代码切换到Step16-4分支,查看源码。

CSS也可以像JavaScript那样进行压缩,以达到提升加载速度和代码混淆的作用。Webpack 4.x之后主要使用mini-css-extract-pluginoptimize-css-assets-webpack-plugincssnano等插件:

优化CSS一节中,我们其实介绍过了mini-css-extract-pluginoptimize-css-assets-webpack-plugin,并且在webpack.common.jswebpack.prod.js进行配置。事实上这两个插件帮我们优化了CSS代码,但并没有起到压缩CSS的作用。 时至今日,压缩CSS较好的插件是PostCSS插件cssnano。前两个插件已经安装好了,现在只需要安装cssnano

⇒  npm i cssnano -D

然后在webpack.prod.js中的new OptimizeCSSAssetsPlugin添加有关于cssnano的相关配置:

new OptimizeCSSAssetsPlugin({
    cssProcessor: require('cssnano'),
    cssProcessorOptions: {
        discardComments: {
            removeAll: true,
        },
        autoprefixer: false,
        map: {
            inline: false,
            annotation: true,
        },
    },
    canPrint: true,
}),

保存文件之后。一旦你执行npm run build之后,打包的CSS就会压缩:

压缩HTML

请把分支切换到Step16-5分支,查看源代码。

Step05中,我们安装了html-webpack-pluginhtml-webpack-template两个插件:

  • html-webpack-plugin:生成index.html文件,并且自动插入到/dist目录下
  • html-webpack-template:能够将元素id附加到mount

另外安装了add-asset-html-webpack-plugin插件让程序可以自动将生成出来的/dll目录下的.js文件插入到index.html,并且会将打包后的*.dll.js文件注入到生成的index.html中。

有关于add-asset-html-webpack-plugin相关的配置可以查阅DellPlugin提升打包速度一节。

为了可以内联嵌入JavaScript和CSS源码,HTML Webpack Plugin还有另一个插件html-webpack-inline-code-plugin可以帮助我们内联代码。

同样的,先安装该插件:

⇒  npm i html-webpack-inline-code-plugin -D

然后在webpack.common.js中添加如下配置:

const HtmlWebpackInlineSourcePlugin = require('html-webpack-inline-source-plugin');

plugins: [
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
        inject: true,
        template: HtmlWebpackTemplate,
        appMountId: 'root',
        filename: 'index.html',
        minify: {
            removeComments: true, // 去掉注释
            collapseWhitespace: true, // 去掉多余空白
            removeAttributeQuotes: true, // 去掉一些属性的引号,例如id="moo" => id=moo
        },
        inlineSource: '.(js|css)$',
    }),
    new HtmlWebpackInlineSourcePlugin(),
    new MiniCssExtractPlugin({
        filename: 'css/[name].[contenthash].css',
        chunkFilename: 'css/[name]-[id].[contenthash].chunk.css',
    }),
],

保存文件,执行npm run build打包文件之后,你看到的/dist/index.html文件如下:

压缩图片

请把分支切换到Step16-6,查看源代码。

可以说,图片在Web中有可能占用的大半的资源。而资源过大直接会影响Web页面的加载和渲染。在Webpack中,如果资源过大的话,在打包的时候会有一些警告信息提示,比如我们示例中的/src/assets/images/body-background.jpg,其大小大约是449 KB

大家都清楚,资源过大会占用很大一部分宽度。为了提升Web页面的性能,所以需要对图片进行优化。除了借助一些工具来优化图像之外,其实我们在Webpack中打包文件的时候就可以对图像做一些优化。在Webpack中可以使用url-loadersvg-url-loaderimage-webpack-loader来优化它们。

其实在Step11的时候,我们配置图片加载的时候就有介绍过url-loader。该插件可以将小型静态文件内联到应用程序中。如果不进行配置,它将把接受一个传递的文件,将其放在已编译的包旁边,并返回该文件的url。如果指定limit选项,它将把小于这个限制的文件编码为Base64数据的url并返回这个url,这将图像内联到JavaScript代码中,从而可以减少HTTP的请求。

// webpack.common.js

// images loader
{
    test: /\.(png|jp(e*g)|gif|svg|webp)(\?.*)?$/,
    exclude: /node_modules/, // 排除不处理的目录
    include: path.resolve(__dirname, '../src'), // 精确指定要处理的目录
    use: [
        {
            loader: 'url-loader',
            options: {
                limit: 1024, // 小于10kb的图片编译成base64编码,大于的单独打包成图片
                name: 'images/[hash]-[name].[ext]', // Placeholder占位符
                publicPath: '/assets/', // 最终生成的CSS代码中,图片URL前缀
                outputPath: 'assets', // 图片输出的实际路径(相对于/dist目录)
            },
        },
    ],
},

url-loader插件可以在加载图片的时候就做一些优化,除此之外,使用image-webpack-loader可以在打包的时候对JPGPNGGIFSVGWEBP图像使用imagemin进行压缩。

image-webpack-loader插件不会把图片内嵌到应用程序,所以它必须与url-loader(或file-loader或后面要介绍的svg-url-loader)一起使用。为了让该插件能正常使用,同样的需要先安装该插件:

⇒  npm i image-webpack-loader -D

然后在webpack.common.jsmodule.rulesurl-loader之后添加image-webpack-loader相关的配置:

// images loader
{
    test: /\.(png|jp(e*g)|gif|svg|webp)(\?.*)?$/,
    exclude: /node_modules/, // 排除不处理的目录
    include: path.resolve(__dirname, '../src'), // 精确指定要处理的目录
    use: [
        {
            loader: 'url-loader',
            options: {
                limit: 1024, // 小于10kb的图片编译成base64编码,大于的单独打包成图片
                name: 'images/[hash]-[name].[ext]', // Placeholder占位符
                publicPath: '/assets/', // 最终生成的CSS代码中,图片URL前缀
                outputPath: 'assets', // 图片输出的实际路径(相对于/dist目录)
            },
        },
        {
            loader: 'image-webpack-loader',
            options: {
                mozjpeg: {
                    progressive: true,
                    quality: 65,
                },
                // optipng.enabled: false will disable optipng
                optipng: {
                    enabled: false,
                },
                pngquant: {
                    quality: '65-90',
                    speed: 4,
                },
                gifsicle: {
                    interlaced: false,
                },
                // the webp option will enable WEBP
                webp: {
                    quality: 75,
                },
            },
        },
    ],
},

保存之后执行npm run build之后,发现.jpg文件并没有得到任何的压缩:

在网上查了好久,似乎很多同学都碰到类似的问题,但并没有找到相应的解决方案,路过的大神求指点

由于.jpg并未得到压缩,为了验证配置是正确的(image-webpack-loader能工作),我尝试着在页面中重新加载了一张.png的图片:

.test__image {
    background: url("~@images/test.png") no-repeat center;
    background-size: cover;
    width: 100vw;
    height: 300px;
}

再次执行npm run build之后,发现.png图片得到了一定的压缩:

有关于image-webpack-loader更详细的配置,可以查看其官方文档

对于.svg文件的优化,除了url-loader之外还可以使用svg-url-loader,它将以utf-8编码的DataUrl字符串加载.svg文件。其结果会比Base64更小,解析更快。

先安装该插件:

╰─➤  npm i svg-url-loader -D

然后在webpack.common.jsmodule.rules中添加相应的配置:

module: {
    rules: [
        // ...
        // images loader
        {
            test: /\.(png|jp(e*g)|gif|webp)(\?.*)?$/,
            exclude: /node_modules/, // 排除不处理的目录
            include: path.resolve(__dirname, '../src'), // 精确指定要处理的目录
            use: [
                {
                    loader: 'url-loader',
                    options: {
                        limit: 1024, // 小于10kb的图片编译成base64编码,大于的单独打包成图片
                        name: 'images/[hash]-[name].[ext]', // Placeholder占位符
                        publicPath: '/assets/', // 最终生成的CSS代码中,图片URL前缀
                        outputPath: 'assets', // 图片输出的实际路径(相对于/dist目录)
                    },
                },
                {
                    loader: 'image-webpack-loader',
                    options: {
                        mozjpeg: {
                            progressive: true,
                            quality: 65,
                        },
                        // optipng.enabled: false will disable optipng
                        optipng: {
                            enabled: false,
                        },
                        pngquant: {
                            quality: '65-90',
                            speed: 4,
                        },
                        gifsicle: {
                            interlaced: false,
                        },
                        // the webp option will enable WEBP
                        webp: {
                            quality: 75,
                        },
                    },
                },
            ],
        },

        // svg loader
        {
            test: /\.svg/,
            exclude: /node_modules/, // 排除不处理的目录
            include: path.resolve(__dirname, '../src'), // 精确指定要处理的目录
            use: [
                {
                    loader: 'svg-url-loader',
                    options: {
                        limit: 1024, // 小于10kb的图片编译成DataUrl编码,大于的单独打包成图片
                        name: 'images/[hash]-[name].[ext]', // Placeholder占位符
                        publicPath: '/assets/', // 最终生成的CSS代码中,图片URL前缀
                        outputPath: 'assets', // 图片输出的实际路径(相对于/dist目录)
                        noquotes: true,
                    },
                },
            ],
        },
    ],
},

执行npm run dev之后,你会发现小于10kb.svg文件编译之后和url-loader不同之处:

有关于图片和SVG的优化,我们在后面还会花更多的时间来探讨。因为有关于这部分需要一起探讨和讨论的东西比较多,比如在使用SVG的时候,还可以考虑:

对于图片,除了上述介绍的之外,还可以使用resize-image-loaderresponsive-loader生成srcset属性所需要的图片资源,让现代浏览器可以根据环境选择最为合适的图片资源。

对于小图片,还可以使用webpack-spritesmith将图像转为Sprite图和一些Sass、LESS或Stylus的混合宏。对于Sprtes图的生成和使用,还有其他类似的插件可供我们使用。

性能提示

如果想要在打包或者开发过程中展示一些性能提示,可以在 webpack.common.js 中加入如下配置:

module.exports = {
    //...

    performance: {
        // 性能提示,可以提示过大文件
        hints: 'warning', // 性能提示开关 false | "error" | "warning"
        maxAssetSize: 100000, // 生成的文件最大限制 整数类型(以字节为单位)
        maxEntrypointSize: 100000, // 引入的文件最大限制 整数类型(以字节为单位)
        assetFilter: function(assetFilename) {
            // 提供资源文件名的断言函数
            return /\.(png|jpe?g|gif|svg)(\?.*)?$/.test(assetFilename);
        },
    },
    // ...
}

Step17: 监控和分析应用程序

请把分支切换到Step17,查看源码。

不管是什么样的Web应用程序,随着不断的迭代开发,他加载的东西会越来越多,程序会越来越大。这个时候我们就需要配置一些监控和分析工作,让他们帮我们时时监控和分析所安装的依赖项。这让我们可以时刻保持对加载的依赖项有清晰的关注。

在Webpack配置中,我们可以使用以下几个插件来帮助我们做监控和分析:

  • webpack-dashboard:通过展示依赖项大小、进度和其他细节来增强 Webpack 输出,有助于跟踪大型依赖项
  • bundlesize:用于验证 Webpack 的资源不超过指定的大小,当应用程序变得太大时能够及时得知
  • webpack-bundle-analyzer:能够扫描 bundle 并对其内部内容进行可视化呈现,从而可以发现大型的或者不必要的依赖项

先在命令终端执行下面的命令,来安装这三个插件:

╰─➤  npm i webpack-dashboard bundlesize webpack-bundle-analyzer -D

先来看webpack-dashboard相关的配置,在webpack.common.js中引入该插件,并在plugins中调用该插件:

const Dashboard = require('webpack-dashboard');                // 引入dashboard
const DashboardPlugin = require('webpack-dashboard/plugin');   // 引入dashboard对应的插件
const dashboard = new Dashboard();                             // 创建一个dashboard的实例

然后在plugins中添加DashboardPlugin

plugins: [
    // ...
    new DashboardPlugin(dashboard.setData),
],

然后在package.json中的--quiet:

"scripts": {
    // ...

    "dev": "webpack-dev-server --mode development --quiet --inline --progress --open --config ./build/webpack.dev.js",

    //...
},

这样,在命令终端执行npm run start启动服务时,就可以看到类似下图这样的结果:

再来配置bundlesize。根据其官方文档所描述,我们可以在package.json或者单独创建配置文件bundlesize.config.json来指定具体的值。比如:

// package.json
{
    "bundlesize": [
        {
            "path": "./dist/assets/images/*.png",
            "maxSize": "50 kB",
        },
        {
            "path": "./dist/assets/images/*.jpg",
            "maxSize": "50 kB",
        },
        {
            "path": "./dist/assets/images/*.svg",
            "maxSize": "50 kB",
        },
        {
            "path": "./dist/css/*.css",
            "maxSize": "20 kB",
        },
        {
            "path": "./dist/js/*.js",
            "maxSize": "20 kB",
        },
        {
            "path": "./dist/vendors.*.js",
            "maxSize": "35 kB",
        }
    ]
}

我这里选择在项目根目录下单独创建一个配置文件bundlesize.config.json,并且添加上面示例代码的配置。

{
    "files": [
        {
            "path": "./dist/assets/images/*.png",
            "maxSize": "50 kB",
        },
        {
            "path": "./dist/assets/images/*.jpg",
            "maxSize": "50 kB",
        },
        {
            "path": "./dist/assets/images/*.svg",
            "maxSize": "50 kB",
        },
        {
            "path": "./dist/css/*.css",
            "maxSize": "20 kB",
        },
        {
            "path": "./dist/js/*.js",
            "maxSize": "20 kB",
        },
        {
            "path": "./dist/vendors.*.js",
            "maxSize": "35 kB",
        }
    ]
}

保存文件之后在package.jsonscripts中添加下面这样的配置:

"scripts": {
    // ...
    "check-size": "bundlesize --config ./bundlesize.config.json",
    // ...
},

在命令终端执行npm run check-size之后,你可以看到类似下图这样的结果:

就该示例配置来说,建议先执行npm run build,然后再执行npm run check-size。如果会有报错信息,根据相关的提示信息做相应调整,然后再重新执行这两个命令。

接着再来配置webpack-bundle-analyzer,同样的在webpack.common.js中引入该插件:

const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

然后在module.exports.plugins中调用BundleAnalyzerPlugin

module.exports = {
    // ...
    plugins: [
        // ...
        new BundleAnalyzerPlugin(),
    ],
};

保存webpack.common.js文件之后,在命令终端执行npm run start,浏览器会重启一个页面。该统计页面显示的是已解析文件的大小(当文件出现在包中时)。您可能想比较 gzip 之后的大小,因为它更接近实际用户体验,可以使用左边的边栏来切换大小。

对于报告,我们需要关注的点有:

  • 大型依赖项:为什么这么大?是否有更小的替代方案?您是否使用了该库包含的所有代码?总之寻找问题所在,然后找到相应替代方案
  • 重复的依赖关系:您是否看到同一个库在多个文件中重复出现?在Webpack4.x中使用 optimization.splitChunks.chunks 将重复的依赖关系移动到一个公共文件。或者某个包具有相同库的多个版本?
  • 相似的依赖关系:是否有类似的库可以做大致相同的工作?

有关于Webpack中配置依赖关系的监控和应用分析,更详细的介绍可以阅读@Ivan Akulov的《Monitor and analyze the app》一文。

小结

有关于Webpack配置还有很多知识可以探讨。在接下来我将还会继续和大家一起探讨这方面的知识。比如多页面的配置,脚手架的配置等。如果您对这方面知识感兴趣的话,欢迎持续关注后续的更新,如果您在这方面有相关的经验或更好的建议,欢迎在下面的评论中与我们一起共享。