环境配置的封装:
生产环境的配置与开发环境有所不同,比如要设置mode、环境变量,为文件名添加chunk hash作为版本号等。
让Webpack按照不同环境采用不同的配置,一般来说有两种方式:
- 使用相同的配置文件:比如令Webpack不管在什么环境下打包都使用webpack.config.js,只是在构建开始前将当前所属环境作为一个变量传进去,然后在webpack.config.js中通过各种判断条件来决定具体使用哪个配置。比如:
通过npm脚本命令传入了一个ENV环境变量,webpack.config.js则根据它的值来确定具体采用什么配置。//package.json { ... "scripts": { "dev" : "ENV=development webpack-dev-serve", "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 }
- 为不同的环境创建各自的配置文件。比如:可以单独创建一个webpack.production.config.js,开发环境的则可以叫webpack.development.config.js,然后修改package.json。
通过–config指定打包时使用的配置文件。但这种方法存在一个问题:即webpack.production.config.js和webpack.development.config.js肯定会有重复的部分,一改都要改,不利于维护。在这种情况下,可以将公共的配置提取出来,比如单独创建一个webpack.common.config.js,然后让另外两个JS分别引用该文件,并添加上自身环境的配置即可。{ ... "scripts": { "dev": "webpack-dev-serve --config=webpack.development.config.js", "build": "webpack --config=webpack.production.config.js" } }
//development和production共有配置 module.exports = { entry: './src/index.js' }
开启production模式:
在早期的Webpack版本中,不同环境所使用的的配置项太多,管理起来复杂,以至于webpack4中直接加了一个mode配置项,让开发者可以通过它来直接切换打包模式。
//webpack.config.js
module.exports = {
mode: 'production'
}
这意味着当前处于生产环境模式,Webpack会自动添加许多适用于生产环境的配置项,减少了人为手工的工作。
环境变量:
通常需要为生产环境和本地环境添加不同的环境变量,在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。
在值的外面加上了JSON.stringify(),是因为DefinePlugin在替换环境变量时对于字符串类型的值进行的是完全替换,假如不添加JSON.stringify()的话,在替换后就会成为变量名,而非字符串值。因此对于字符串环境变量及包含字符串的对象都要加上JSON.stringify()。
许多框架与库都采用process.env.NODE_ENV作为一个区别开发环境和生产环境的变量。process.env是Node.js用于存放当前进程环境变量的对象;而NODE_ENV则可以让开发者指定当前的运行时环境,当它的值为proction时即代表当前为生产环境,库和框架在打包时如果发现了它就可以去掉一些开发环境的代码,如警告信息和日志等。
new Webpack.DefinePlugin({
process.env.NODE_ENV: 'production'
})
如果启用mode:production,则Webpack已经设置好了process.env.NODE_ENV,不需要再人为添加了。
source map:
source map指的是将编译、打包、压缩后的代码映射回源代码的过程。
经过Webpack打包压缩后的代码基本上已经不具备可读性,此时若代码抛出了一个错误,要想回溯它的调用栈是非常困难的,而有了source map,再加上浏览器调试工具(dev tools),要做到这一点就非常容易了。
原理:
Webpack对于工程源代码的每一步处理都有可能会改变代码的位置、结构,甚至是所处文件,因此每一步都需要生成对应的source map。若启用了devtool配置项,source map就会跟随源代码一步步被传递,知道生成最后的map文件。这个文件默认就是打包后的文件名加上.map,如bundle.js.map。
在生成map文件的同时,bundle文件中会追加上一句注释来标识map文件的位置。如:
//bundle.js
(function(){
//bundle的内容
})();
//# sourceMappingURL=bundle.js,map
打开浏览器的开发者工具时,map文件会同时被加载,这时浏览器会使用它来对打包后的bundle文件进行解析,分析出源代码的目录结构和内容。
map文件有时会很大,但只要不打开开发者工具,浏览器是不会加载这些文件的,因此对于普通用户来说并没有影响。但是使用source map会有一定的安全隐患,即任何人都可以通过dev tools看到工程源码。
source map配置:
只要在webpack.config.js中添加devtools即可。
module.exports = {
devtools: 'source-map'
}
开启source map之后,打开Chrome的开发者工具,在Sources选项卡下面的webpack://目录中可以找到解析后的工程源码。
对于CSS、SCSS、Less来说,则需要添加额外的source map配置项。
const path = require('path');
module.exports = {
devtools: 'source-map',
module: {
rules: [
{
test: /\.scss$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
sourceMap: true
}
},
{
loader: 'sass-loader',
options: {
sourceMap: true
}
}
]
}
]
}
}
Webpack支持多种source map的形式。除了配置为devtools:'source-map’以外,还可以根据不同的需求选择cheap-source-map、eval-source-map等。通常它们都是source map的一些简略版本。因为生成完整的source map会延长整体构建时间,如果对打包构建速度需求比较高的话,可以选择一个简化版的source-map,比如:在开发环境中,cheap-module-eval-source-map是一个 不错的选择,属于打包速度和源码信息还原程度的一个良好折中。
在生产环境中由于会对代码进行压缩,而最常见的压缩插件UgifyjsWebpackPlugin目前只支持完全的source-map,因此只能使用source-map、hidden-source-map、nosources-source-map这三者之一。
安全:
source map不仅可以帮助开发者调试代码,当线上有问题产生时也有助于查看调用栈信息,是线上查错十分重要的线索。同时,有了source map也就意味着任何人通过浏览器的开发者工具都可以看到工程源码,对于安全性来说也是极大的隐患。
Webpack提供了hidden-source-map、nosources-source-map两种策略来提升source map的安全性,在保持其功能的同时,防止暴露源码给用户。
- hidden-source-map:意味着Webpack仍然会产出完整的map文件,只不过不会在bundle文件中添加对map文件的引用。这样一来,当打开浏览器的开发者工具时,是看不到map文件的,浏览器自然也无法对bundle进行解析。如果想要追述源码,则要利用一些第三方服务,将map文件上传那上面。目前最流行的解决方案是Sentry。
Sentry是一个错误跟踪平台,开发者接入后可以进行错误的收集和聚类,以便于更好地发现和解决线上的问题。
Sentry支持JavaScript的source map,可以通过它所提供的命令行工具或者Webpack插件来自动上传map文件,同时还要在工程代码中添加Sentry对应的工具包,每当JavaScript执行出错时就会上报给Sentry。Sentry在接收到错误后,就会去找对应的map文件进行源码解析,并给出源码中的错误栈。
- nosources-source-map:它对于安全性的保护则没有那么强,但是使用方式相对简单。打包部署之后,可以在浏览器开发者工具的Sources选项卡中看到源码的目录结构,但是文件的具体内容会被隐藏起来。对于错误来说,仍然可以在Console控制台中查看源代码的错误栈,或者console日志的准确行数。它对于追溯错误来说基本足够,并且其安全性相对于可以看到整个源码的source-map配置来说要略高一些。
- 还有一种选择,就是可以正常打包出source map,然后通过服务器的nginx设置(或其他类似工具),将.map文件只对固定的白名单开放,这样我们仍然能看到源码,而在一般用户的浏览器中就无法获取到它们了。
资源压缩:
在将资源发布到线上环境前,通常都会进行代码压缩(或者叫uglify),意思是移除多余的空格、换行及执行不到的代码,缩短变量名,在执行结果不变的前提下降代码更换为更短的形式。一般正常的代码在uglify之后整体体积都将会显著缩小,同时,uglify之后的代码基本上不可读,在一定程度上提升了代码的安全性。
压缩JavaScript:
压缩JavaScript大多数时候使用的工具有两个:一是UglifyJS(Webpack3已集成),另一个是terser(Webpack4已集成)。后者由于支持ES6+代码的压缩,更加面向未来,因此官方在Webpack4中默认使用了terser的插件terser-webpack-plugin。
-
UglifyJSPlugin:
在Webpack3中的话,开启压缩需调用webpack.optimize.UglifyJsPlugin。const Webpack = require('webpack'); module.exports = { entry: './app.js', output: { filename: 'bundle.js' }, plugins:[ new Webpack.optimize.UglifyJsPlugin() ] }
从Webpack34之后,这项配置被移到了config.optimization.minimize(如果开启了mode:production,则不需要人为设置)。
module.exports = { entry: './app.js', output: { filename: 'bundle.js' }, optimization: { minimize: true } }
-
terser-webpack-plugin插件支持自定义配置:
- test:terser的作用范围,默认值为
/\.m?js(\?.*)?$/i
。 - include:使terser额外对某些文件或目录生效,默认值为undefined。
- exclude: 排除某些文件或目录,默认值为undefined。
- cache:是否开启缓存,默认值为false。默认的缓存目录为
node_modules/.cache/terser-webpack-plugin
,通过传入字符串类型的值可以修改。 - parallel:强烈建议开启。允许使用多个进程来进行压缩。通过传入数字类型的值来指定,默认值为false。
- sourceMap:是否生成source map,默认值为false。
- terserOptions:terser压缩配置。
const TerserPlugin = require('terser-webpack-plugin'); module.exports = { ... optimization: { //覆盖默认的minimizer minimizer: [ new TerserPlugin ({ test: /\.js(\?.*)?$/i, exclude: /\/excludes/ }) ] } }
- test:terser的作用范围,默认值为
压缩CSS:
压缩CSS文件的前提是使用extract-text-webpack-plugin或mini-css-extract-plugin将样式提取出来,接着使用optimize-css-assets-webpack-plugin来进行压缩。这个插件本质上使用的是压缩器cssnano。
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
module.exports = {
...
module: {
rules: [
{
test: /\.css$/,
use: ExtractTextPlugin .extract({
fallback: 'style-loader',
use: 'css-loader'
})
}
]
},
plugins: [new ExtractTextPlugin ('style.css')],
optimization: {
minimizer: [
]
}
}