webpack生产环境的配置

170 阅读8分钟

我们已经学了很多webpack的使用方法,但到了生产环境(或者称为线上环境)中,资源打包将会遇到许多新的问题。我们关注的是如何让用户更快地加载资源,涉及如何压缩资源、如何添加环境变量优化打包、如何最大限度利用缓存

1. 环境配置的封装

生产环境的配置与开发环境有所不同,比如要设置mode、环境变量、为文件名添加chunk hash作为版本号等等。如何让webpack按照不同的环境采用不同配置呢?

我们可以令webpack不管在什么环境下打包都使用webpack.config.js,只是在构建开始前将当前所属环境作为一个变量传进去,然后在webpack.config.js中通过各种判断条件来决定具体使用哪个配置

//package.json
{
    "script":{
        "dev": 'ENV=development webpack-dev-server',
        "build": "ENV=production webpack'
    }
}

//webpack.config.js
const ENV = process.env.ENV
const isProd = ENV === 'production'
module.exports = {
    output: {
        filename: isProd ? 'bundle@[chunkhash].js' : 'bundle.js'
    },
    mode: ENV
}

在上面的例子中,我们通过npm脚本传入了一个ENV环境变量,webpack.config.js则根据他的值来确定具体采用什么配置。

2. 开启production模式

在早期的webpack版本中,开发者有时会抱怨不同环境所使用的配置项太多,以至于webpack 4中直接加入了一个mode配置项,让开发者通过它来直接切换打包模式。

当设置了mode:‘production’之后,webpack会自动添加许多适用于生产环境的配置项,减少了人为手动的工作。webpack这样做是希望隐藏许多具体配置的细节,而是转为更具有语义性、更简洁的配置提供出来。从webpack4开始我们已经能看到他的配置文件不应该越多越好,而是应该越写越少

大部分时候仅仅设置了mode是不够的,下面我们继续介绍其他与生产环境相关的自定义配置

3. (设置)环境变量(的方法)

通常我们需要为生产环境和本地环境添加不同的环境变量,在webpack中可以使用DefinePlugin进行设置。

//webpack.config.js
const webpack = require('webpack')
module.exports = {
    entry: './app.js',
    output: {
        filename: 'bundle.js'
    },
    mode: 'production',
    plugins: {
        new webpack.DefinePlugin({
            ENV: JSON.stringify('production')
        })
    }
}

//app.js
document.write(ENV)

上面的配置通过DefinePlugin设置了ENV环境变量,最终页面上输出的是字符串production。 不同于mode配置项只能设置production、development或者none中的一个,DefinePlugin可以设置不同域名、不同环境下的环境变量

new webpack.DefinePlugin({
            ENV: JSON.stringify('production'),
            IS_PRODUCTION: true,
            CONSTANTS: JSON.stringify({
                TYPES: ['foo','bar']
            })
        })

在上面的例子中我们加上了JSON.stringify,这是因为1DefinePlugin在替换环境变量时对于字符串类型的值进行的是完全替换。加入不添加JSON。stringify的话,替换后就会变成变量名,而非字符串的值。因此对于字符串环境变量以及包含字符串的对象都要加上JSON.stringify才行

许多库和框架都采用process.env.NODE_ENV作为区别开发环境和生产环境的变量。process.env是Node.js用于存放当前进程环境变量的对象;而NODE_ENV则可以让开发者指定当前运行时环境。当他的值为production是即代表生产环境,库和框架在打包时如果发现了它就可以去掉一些开发环境的代码,比如警告信息和日志等。

new webpack.DefinePlugin({
            process.env.NODE_ENV: 'production'
        })

如果启用了mode:production,则webpack已经设置好了process.env.NODE_ENV,不需要人为添加了。

以上就是设置环境变量的方法

4. source map

source map是指将编译、打包、压缩后的代码映射会源代码的过程。经过webpack打包之后,代码基本上已经不具备可读性,此时如果代码抛出了一个错误,要想回溯它的调用栈是十分困难的,而有了scource map之后做到这一点就很容易了

4.1 原理

我们先来了解一下source map的工作原理。webpack对于工程源代码的每一步处理都可能会改变代码的结构、位置甚至所处文件,因此每一步都会生成记录文件,也就是source map。开始了source map之后,每一步的source map都会跟随源文件一步步传递,直到生成最后的map文件。这个文件默认就是打包后的名字加上.map。如bundle.js.map。当我们打开浏览器的开发者工具时,map文件就会被同时加载,这时候浏览器会根据map文件对打包后的bundlewe文件进行解析,分析出源码的目录结构和内容

map文件有时候会很大,但是不用担心,只要不打开开发者工具,浏览器是不会加载这些文件的,因此对普通用户来说没有影响。但是使用source map会有一定的影响,即任何人都可以通过开发者工具看到工程源码。后面我们会介绍如何解决这个问题。

image.png

4.2 source map配置

对JavaScript来说,配置soucre map很简单

//webpack.config.js
module.exports = {
    devtool: 'source-map'
}

对于CSS、SCSS、Less来说,则需要添加额外的source map配置项

const path = require('path')
module.exports = {
     devtool: 'source-map',
     module: {
         relus: [
             {
                 test: /\.scss$/,
                 use: [
                     'style-loader',
                     {
                         loader: 'css-loader',
                         options: {
                             sourceMap: true
                         }
                     },
                     {
                         loader: 'sass-loader',
                         options: {
                             sourceMap: true
                         }
                     }
                 ]
             }
         ]
     }
}

开启source map之后,打开chrome的开发者工具,在sources选项卡下面的webpack://目录可以找到解析后的工程源码 image.png

source map有许多简化的版本,但是在生产环境中我们会对代码进行压缩,而最常见的压缩插件UglifyjsWebpack-Plugin目前只支持完全的source map,因此我们只在devtool设置source-map、hidden-source-map、nosources-source-map这3者之一。下面介绍这3种source map在安全性方面的不同

4.3 安全

source map意味着任何人通过浏览器的开发者工具都可以看到工程源码,这对安全也是极大的隐患,那么如何才能在保持其功能的同时,防止暴露源码给用户呢?webpack提供了hidden-source-map以及nosources-source-map两种策略来提升source map的安全性。

hidden-source-map意味着webpack仍然会产出完整的map文件(即记录了源代码的变化过程),只不过不会在bundle文案中添加对于map文件的引用,因此浏览器也无法对bundle进行解析。如果我们想追溯源码,则要利用第三方服务,将map文件上传到那上面。目前最流行的方法就是Sentry

Sentry是一个错误追踪平台。它支持JavaScript的source map,我们可以通过它提供的命令行工具自动上传map文件。同时可以在工程代码中添加Sentry对应的工具包,每当JavaScript执行出错时就会上报给Sentry,Sentry接收到错误之后就会去找对应的map文件进行源码即系,并给出源码的错误处

另一种配置是nosources-source-map,它对于安全性的保护则没那么强,但是使用方式相对简单。打包部署之后,我们可以在开发者工具的sources选项卡中看到源码的目录结构,但是文件的具体内容会被隐藏起来,我们可以在Console控制台中查看源代码的错误栈或者console日志的准确行数。它对于追溯错误来说基本够用,并且其安全性相对可以看到整个源码的source map要略高一些。

在这些配置之外还有一种选择,就是我们可以正常打包出source map,然后通过服务器的nginx设置将.map文件只对固定的白名单(比如公司内网)开发,而在一般用户的浏览器就无法获取到它们。

5. 资源压缩

在将资源发布到线上环境前,通常会进行代码压缩,或者叫uglify(丑化),意思是移除多余空格、换行及执行不到的代码,缩短变量名,在执行结果不变的前提下将代码替换为更短的形式。一般代码在压缩后整体体积会显著缩小,同时,压缩后的代码基本不具备可读性,在一定程度上提升了代码的安全性

5.1 压缩JavaScript

在webpack4之后,我们可以通过config.optimization.minimize来压缩JavaScript代码,下面是webpack4的示例(如果开启了mode: production,则不需要人为设置)

module.exports = {
    entry: './app.js',
    output: {
        filename: 'bundle.js'
    },
    optimization: {
        minimize: true
    }
}

上面的配置实际上是对terser-webpack-plugin配置的集成(封装),下面是对terser-webpack-plugin的自定义配置 image.png

下面的例子展示了如何自定义使用

const TerserPlugin = require('terser-webpack-plugin')
module.exports = {
    entry: './app.js',
    output: {
        filename: 'bundle.js'
    },
    optimization: {
        //覆盖默认的minimizer
        minimize: [
            new TerserPlugin({
                /* your config */
                test: /\.js(\?.*)?$/i,
                exclude: /\/excludes/
            })
        ]
    }
}

5.2 压缩CSS

压缩CSS的前提是使用extract-text-webpack-plugin或者mini-css-extract-plugin将样式提取出来(webpack样式处理 - 掘金 (juejin.cn)),接着使用optimize-css-assets-webpack-plugin来进行压缩,这个插件本质上使用的是压缩器cssnano,当然我们可以通过其配置来进行切换

const ExtractTextPlugin = require('extract-text-webpack-plugin')
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')

module.exports = {
    entry: './app.js',
    output: {
        filename: 'bundle.js'
    },
    module: {
        relus: [
            {
                test: /\.css$/,
                use: ExtractTextPlugin.extract({
                    fallback: 'style-loader',
                    use: 'css-loader'
                })
            }
        ]
    },
    plugins:[new ExtractTextPlugin('style.css')],
    optimization: {
        minimize: [
            new OptimizeCSSAssetsPlugin({
            // 生效范围:只压缩匹配到的资源
            assetNameRegExp: /\.optimize\.css$/,
            // 压缩处理器,默认为cssnano
            cssProcessor: require('cssnano'),
            // 压缩处理器的配置
            cssProcessorOptions: {discardComments: {removeAll: true}},
            // 是否展示log
            canPrint: true
            })
        ]
    }
}

6. 缓存

缓存是指重复利用浏览器已经获取过的资源。具体的缓存策略(如指定缓存时间)是由服务器决定的,在资源过期前会一直使用本地缓存进行响应。

这同时也带来一个问题,假如开发者想要对代码进行一个bug fix,并希望立刻更新到所有用户的浏览器上,最好的方法就是更改资源的url,这样可以迫使客户端都去下载最新的资源。

6.1 资源hash

一个常用的方法就是在每次打包的时候对资源的内容计算一次hash,并作为版本号放在文件名中。我们常常用chunkhash来做文件版本号,因为它会为每一个chunk单独计算一个hash

module.exports = {
    entry: './app.js',
    output: {
        filename: 'bundle@[chunkhash].js'
    },
    mode: 'production'
}

打包结果如下图所示
image.png

6.2 输出动态HTML

接下来我们面临的是,资源名的改变意味着HTML中引用路径的改变,比如src='bundle.js@111'会由于chunkhash变了,因此就会失效。理想的情况是打包结束后自动将最新的资源名同步过去。使用html-webpack-plugin可以帮我们做到这一点。

const HtmlWebpackPlugin = require('html-webpack-plugin')
module.export = {
    plugins: [
        new HtmlWebpackPlugin()
    ]
}

打包结果多出了一个index.html
image.png

此时index.html具体的内容如下

image.png

html-webpack-plugin会自动将我们打包出来的资源名放入生成的index.html中,这样我们就不用手动更新资源URL了。

现在我们看到了html-webpack-plugin凭空创建了一个index.html,但现实情况中我们一般要在HTML中放入很多个性化内容,这时我们可以传入一个已有的HTML模板。请看下面的例子 image.png
我们通过以下配置来告诉webpack模板HTML的路径

new HtmlWebpackPlugin({
    template: './template.html'
})

通过以上配置,我们打包出来的index.html结果如下

image.png

7. bundle体积监控和分析

为了保证良好的用户体验,我们可以对打包输出的bundle体积进行持续的监控,以防止不必要的冗余模块被添加进来。

VS Code中有一个插件Import Cost可以帮助我们对引入模块的大小进行实时检测,每当我们在代码中引入一个新模块(主要是node_modules中的模块)时,它会为我们计算该模块压缩后及gzip过后将占多大体积

image.png

当我们发现有些包过大时,可以寻找一些更小的代替方案或者只引用其中的某些子模块。如下图
image.png

另外一个很有用的工具是webpack-bundl-analyzer,它能够帮助我们分析一个bundle的构成。使用方法也很简单,只要将其添加进plugins配置即可

const Analyzer = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
module.export = {
    plugin: [
        new Analyzer()
    ]
}

它可以帮我们生成一张bundle的模块组成结构图,每个模块所占体积一目了然

image.png

最后我们还需要对资源体积进行监控,bundlesize可以帮助做到这一点,只需要在package.json进行一下配置即可

{
    "name": "my-app",
    "version": "1.0.0",
    "bundlesize": [
        {
            "path": "./bundle.js",
            "maxSize": "50 kB"
        }
    ],
    "scripts": {
        "test:size": "bundlesize"
    }    
}

通过npm脚本可以执行bundlesize命令,它会根据我们配置的资源路径和最大体积验证最终的bundle是否超限。我们可以将其作为自动化测试的一部分,来保证输出的资源如果超限了不会在不知情的情况下发布出去。