概述

前面所了解到的一些用法和特性都是为了可以让我们在开发阶段拥有更好的开发体验,而这些体验提高的同事我们的打包结果也会随之变得越来越臃肿。

那这是因为在这个过程中webpack为了实现这些特性他会自动往打包结果中添加一些额外的内容,例如我们之前所使用到的source-map和HMR, 他们都会往输出结果中添加额外的代码来去实现各自的功能。

但是这些额外的代码对于生产环境来讲是冗余的,因为生产环境和开发环境是有很大的差异。

在生产环境中我们强调的是以更少量,更高效的代码去完成业务功能。也就是我们会更注重运行效率,而在开发环境中我们会只注重开发效率。

那针对于这个问题,webpack4+当中就推出了mode用法,那他为我们提供了不同模式下的一些预设的配置,那其中生产模式中就已经包括了很多我们在生产环境中所需要的优化配置。

那同时webpack也建议我们为不同的工作环境去创建不同的配置,以便于让我们的打包结果可以适用于不同的环境。

那接下来我们一起来探索一下生产环境中有哪些值得我们优化的地方,以及一些注意事项。

不同环境下的配置

下面我们先来尝试为不同的工作环境去创建不同的webpack配置,那创建不同的环境配置的方式主要有两种。

第一种就是在我们的配置文件当中去添加相应的判断条件,然后根据环境的判断条件不同导出不同的配置。

第二种就是为我们不同的对应一个配置文件。那这种就确保我们每一个环境下面都会有一个对应的环境配置文件。

那我们分别来尝试下这两种方式下如何为我们开发环境和生产环境去创建不同的配置。

  1. 首先来看我们在配置文件当中来添加判断的这种方式。

我们回到配置文件当中,webpack的配置文件还支持导出一个函数,然后在这个函数当中去返回我们所需要的配置对象。

那这个函数可以接受到两个参数,第一个是env也就是我们通过cli传递的环境名参数

第二个是argv,那这个是指我们运行cli过程中所传递的所有参数,那我们就可以借助这样一个特点来去实现为我们的开发环境和生产环境去分别返回不同的配置。

我们先将这里的开发模式配置定义在config这样一个变量当中。

然后我们再去判断一下env是不是等于production,这里我们约定的生产环境的env就是production。

如果说是生产环境的话我们这里就将mode属性的字段设置为production,然后我们再将devtool设置为false,也就是禁用掉source-map,最后我们再来添加cleanWebpackPlugin和CopyWebpackPlugin这两个插件。

那这两个插件我们之前介绍的时候也说到了他实际上在开发阶段可以省略的插件,他是在上线打包之前才有他实际的价值。

这里我们使用的是ES6扩展运算符的方式把这两个插件和之前所有的插件放在一起去创建一个新的数组。

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

module.exports = (env, argv) => {
    const config = {
        entry: './src/main.js',
        output: {
            filename: 'bundle.js',
            path: path.join(__dirname, 'dist'),
            publicPath: 'dist/'
        },
        devtool: 'cheap-eval-module-source-map',
        devServer: {
            hot: true,
            contentBase: 'public'
        },
        module: {
            rules: [
                {
                    test: /.js$/,
                    use: {
                        loader: 'babel-loader',
                        options: {
                            presets: ['@babel/preset-env']
                        }
                    }
                }
                {
                    test: /.css$/,
                    use: [
                        'style-loader',
                        'css-loader'
                    ]
                },
                {
                    test: /.png$/,
                    use: 'url-loader'
                },
                {
                    test: /.html$/,
                    use: {
                        loader: 'html-loader',
                        options: {
                            attrs: ['img:src', 'a:href']
                        }
                    }
                }
            ]
        },
        plugins: [
            new HtmlWebpackPlugin({
                title: 'webpack',
                template: './src/index.html'
            }),
            new webpack.HotModuleReplacementPlugin(),
        ]
    }
    if (env === 'production') {
        config.mode = 'production';
        config.devtool = false;
        config.plugins = [
            ...config.plugins,
            new CleanWebpackPlugin(),
            new CopyWebpackPlugin(['public'])
        ]
    }
    return config;
}

完成以后我们打开命令行终端,我们先尝试直接去运行webpack

yarn webpack

此时我们并没有传递任何参数,这里我们的webpack就会以开发模式运行打包,打包完成过后我们可以展开dist目录,此时目录中并不会有public目录copy过来的文件。

然后我们再回到命令行,我们这里运行一下webpack --env production。那这个时候就相当于给webpack传递了一个env参数, 这个参数的值是production。

yarn webpack --env producton

那我们的配置文件接收到这样一个参数他就会返回生产模式下的配置,那也就意味着此时我们webpack会以生产模式运行打包。

那我们这些额外的插件也就会工作起来,这里我们就能看到public下的文件已经被copy到dist目录了。

那这就是我们通过在导出函数中对环境进行判断从而实现为不同的环境导出不同的配置,当然你也可以在全局去判断环境变量直接导出不同的配置,这样也是可以的。

多配置文件

通过判断环境名参数去返回不同的配置对象这种方式只适用于中小型项目,因为一旦项目变得复杂那我们的配置文件也会一起变得复杂起来。

所以说对于大型的项目我们还是建议大家使用不同环境去对应不同配置文件的方式来实现,一般在这种方式下面我们项目当中至少会有三个webpack配置文件。

其中两个是用来适配不同的环境的,另外一个是一个公共的配置,因为我们的开发环境和生产环境并不是所有的配置都完全不同,说一说我们需要一个公共的文件来去抽象两者之间相同的配置。

我们具体来看,首先我们在项目的跟目录下去新建一个webpack.common.js, 那在这个文件当中我们把刚刚复制的公共配置粘贴进来。

module.exports = {
    entry: './src/main.js',
    output: {
        filename: 'bundle.js',
        path: path.join(__dirname, 'dist'),
        publicPath: 'dist/'
    },
    devtool: 'cheap-eval-module-source-map',
    devServer: {
        hot: true,
        contentBase: 'public'
    },
    module: {
        rules: [
            {
                test: /.js$/,
                use: {
                    loader: 'babel-loader',
                    options: {
                        presets: ['@babel/preset-env']
                    }
                }
            }
            {
                test: /.css$/,
                use: [
                    'style-loader',
                    'css-loader'
                ]
            },
            {
                test: /.png$/,
                use: 'url-loader'
            },
            {
                test: /.html$/,
                use: {
                    loader: 'html-loader',
                    options: {
                        attrs: ['img:src', 'a:href']
                    }
                }
            }
        ]
    },
    plugins: [
        new HtmlWebpackPlugin({
            title: 'webpack',
            template: './src/index.html'
        }),
        new webpack.HotModuleReplacementPlugin(),
    ]
}

然后我们再去新建一个webpack.dev.js和一个webpack.prod.js分别去用来为我们的开发和生产环境去定义特殊的配置。

在生产环境的配置当中(webpack.prod.js)我们先去导入公共的配置对象,这里我们可以使用Object.assign方法把我们公共配置对象复制到我们这里的配置对象当中,并且我们可以通过最后一个对象去覆盖掉这个公共配置当中的一些配置。

但是熟悉Object.assign这个方法的人都应该知道,这个方法是全完覆盖掉前一个对象当中的同名属性,那这样一个特点对应我们普通的值类型属性覆盖都没有什么问题,但是像我们配置当中的plugins这种数组,那我们是希望是可以在公共配置的原有基础之上我们去添加一两个插件。

const common = require('./webpack.common');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
module.exports = Object.assign({}, common, {
    mode: 'production',
    plugins: [
        new CleanWebpackPlugin(),
        new CopyWebpackPlugin()
    ]
})

而Object.assgin这个方法呢,他会导致我们这里的特殊配置会覆盖掉公共配置,所以说Object.assign是不合适的,那这里我们就需要一个更合适的方法去合并这里的配置和公共的配置。

你可以使用loadash所提供的merge方法来去实现,不过社区当中提供了更为专业的webpack-merge这样一个模块。

那这个模块呢他可以专门用来满足这里合并webpack配置的这样一个需求。我们需要安装这样一个模块。

yarn webpack-merge --dev

那安装完成过后我们回到配置文件当中,我们先去载入这样一个模块,这个模块导出的就是一个merge函数。

我们这里使用这个函数来去合并我们这里的配置和公共的配置,使用webpack-merge这个模块过后呢我们这里所配置的这个对象他就可以跟普通的webpack配置一样,需要什么就配置什么。

merge函数的内部会自动去处理合并的逻辑。

const common = require('./webpack.common');
const merge = require('webpack-merge');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
module.exports = merge(common, {
    mode: 'production',
    plugins: [
        new CleanWebpackPlugin(),
        new CopyWebpackPlugin()
    ]
})

同理webpack.dev.js这样一个js文件当中也可以通过这样一个方式来去实现一些额外的配置,这里我们就不重复尝试了。

完成过后我们再次回到命令行终端然后尝试运行webpack打包。

不过这里因为我们已经没有了默认的配置文件,所以这里我们运行webpack时需要通过--config这样一个参数来去指定我们所使用的配置文件也就是我们刚刚的webpack.prod.js。

yarn webpack --config webpack.prod.js

那此时我们就可以以生产环境这种模式的配置去打包我们的应用了。

那当然如果你觉得这样去使用的话我们的命令变得复杂了,那你同样可以把这个构建的命令定义到package.json的script当中,方便我们的使用。

DefinePlugin

在webpack4x中新增的production模式下面内部就自动开启了很多通用的优化功能。

对于使用者而言,这种开箱即用的体验是非常方便的,但是对于学习者而言这种开箱即用他会导致我们忽略掉很多需要了解的东西。以致于我们出现问题之后无从下手。

如果说我们需要深入了解webpack的使用那我建议你可以去单独研究一下每一个配置背后的作用,那我们这里先一起来学习一下其中几个主要的优化配置。顺便去了解一下webpack是如何优化我们的打包结果的。

首先第一个是一个插件叫做define-plugin, 那define-plugin是用来为我们的代码去注入全局成员的。

在production模式下,默认这个插件就会启用起来并且往我们的代码当中注入了一个process.env.NODE_ENV这样一个常量。

很多第三方的模块都是通过这个成员去判断当前的运行环境,从而去决定是否去执行例如打印日志这样一些操作。

那这里我们先来单独使用一下这个插件。我们回到配置文件当中,那define-plugin是一个内置的插件所以说我们先要导入webpack模块。

然后我们再到plugins这个数组当中去添加一下这个插件,那这个插件他的构造函数接收的是一个对象,这个对象中每一个键值都会被注入到我们的代码当中。

例如我们这里在这个对象当中去定义一个API_BASE_URL的一个值,用来为我们的代码去注入我们的api服务地址,那他的值是一个字符串我们这里使用https://api.github.com。

const webpack = require('webpack');

module.exports = {
    mode: 'node',
    entry: './src/main.js',
    output: {
        filename: 'bundle.js'
    },
    plugins: [
        new webpack.DefinePlugin({
            API_BASE_URL: 'https://api.github.com'
        })
    ]
}

然后我们回到我们的代码当中, 简单的来吧这个API_BASE_URL打印出来,完成以后我们打开命令行终端。然后运行webpack打包,找到打包结果,找到刚刚打印的位置。

这里我们发现,define-plugin其实就是把我们注入成员的值直接替换到了代码当中,而我们刚刚设置的值呢,内容就是https://api.github.com,字符串中并没有包含引号,所以说我们这里替换进来是没有引号的。

其实define-plugin的设计并不是只是用来帮我们替换一个数据进来,我们这所传递的字符串内容他要求的实际上是一个代码片段,也就是一段符合js语法的代码,所以说我们这样去传的话是不对的。

那正确的做法是传入一个字符串,这个字符串的内容呢就是一个我们js代码中的字符串字面量语句。

当然了,如果说你需要注入其他的代码也是可以的。

const webpack = require('webpack');

module.exports = {
    mode: 'node',
    entry: './src/main.js',
    output: {
        filename: 'bundle.js'
    },
    plugins: [
        new webpack.DefinePlugin({
            API_BASE_URL: '"https://api.github.com"'
        })
    ]
}

完成以后我们再来查看打包结果,就会发现变成我们想要的样子了。另外呢这里还有一个非常常用的小技巧。

就是如果说我们需要注入的是一个值的话,那我们可以先通过JSON.stringfiy的方式来去将这个值去转换成一个表示这个值的代码片段,那这样的话就不会错了。

const webpack = require('webpack');

module.exports = {
    mode: 'node',
    entry: './src/main.js',
    output: {
        filename: 'bundle.js'
    },
    plugins: [
        new webpack.DefinePlugin({
            API_BASE_URL: JSON.stringify('https://api.github.com')
        })
    ]
}

那这个插件的作用非常简单, 但是他确非常有用,那我们可以用它为我们的代码去注入一些可能会发生变化的值,例如我们刚刚使用的河中API的根路径,那我们的开发环境和生产环境他们的路径肯定是不一样的,我们就可以借助于define-plugin去注入我们想要的api路径。