我们已经学了很多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会有一定的影响,即任何人都可以通过开发者工具看到工程源码。后面我们会介绍如何解决这个问题。
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://目录可以找到解析后的工程源码
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的自定义配置
下面的例子展示了如何自定义使用
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'
}
打包结果如下图所示
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
此时index.html具体的内容如下
html-webpack-plugin会自动将我们打包出来的资源名放入生成的index.html中,这样我们就不用手动更新资源URL了。
现在我们看到了html-webpack-plugin凭空创建了一个index.html,但现实情况中我们一般要在HTML中放入很多个性化内容,这时我们可以传入一个已有的HTML模板。请看下面的例子
我们通过以下配置来告诉webpack模板HTML的路径
new HtmlWebpackPlugin({
template: './template.html'
})
通过以上配置,我们打包出来的index.html结果如下
7. bundle体积监控和分析
为了保证良好的用户体验,我们可以对打包输出的bundle体积进行持续的监控,以防止不必要的冗余模块被添加进来。
VS Code中有一个插件Import Cost可以帮助我们对引入模块的大小进行实时检测,每当我们在代码中引入一个新模块(主要是node_modules中的模块)时,它会为我们计算该模块压缩后及gzip过后将占多大体积
当我们发现有些包过大时,可以寻找一些更小的代替方案或者只引用其中的某些子模块。如下图
另外一个很有用的工具是webpack-bundl-analyzer,它能够帮助我们分析一个bundle的构成。使用方法也很简单,只要将其添加进plugins配置即可
const Analyzer = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
module.export = {
plugin: [
new Analyzer()
]
}
它可以帮我们生成一张bundle的模块组成结构图,每个模块所占体积一目了然
最后我们还需要对资源体积进行监控,bundlesize可以帮助做到这一点,只需要在package.json进行一下配置即可
{
"name": "my-app",
"version": "1.0.0",
"bundlesize": [
{
"path": "./bundle.js",
"maxSize": "50 kB"
}
],
"scripts": {
"test:size": "bundlesize"
}
}
通过npm脚本可以执行bundlesize命令,它会根据我们配置的资源路径和最大体积验证最终的bundle是否超限。我们可以将其作为自动化测试的一部分,来保证输出的资源如果超限了不会在不知情的情况下发布出去。