前言
前面了解了一些Webpack的基础配置,那么下面就了解一些在实际开发过程中的应用。
这里会分为开发环境和生产环境来分别简述。
注:以下功能都是基于Webpack版本^4.43.0调试。
开发环境
开发过程中,每次修改后需要编译代码时,如果手动运行npm run build会显得很麻烦,Webpack中提供了几种工具来进行处理。
为了区分于生产环境,Webpack的配置文件我们命名为webpack.dev.config.js。
module.exports = {
entry: {
main: './src/main.js',
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js',
},
mode: 'development',
module: {
rules: [
...
],
},
}
文件监听(watch mode)
文件监听是在发现源码发⽣生变化时,⾃自动重新构建出新的输出⽂文件。
其原理是Webpack轮询判断文件的最后编辑时间是否变化。如果某个文件发生了变化,会先缓存起来,等一定延迟时间(aggregateTimeout)后再一起执行。
如果需要开启监听模式,有两种方式:
- 启动Webpack命令时,带上
--watch参数 - 在配置文件中设置
watch: true
// package.json
"scripts": {
"dev": "webpack --config webpack.dev.config.js --watch"
},
// webpack.dev.config.js
module.exports = {
...
plugins: [
// 避免在 watch 触发增量构建后删除 index.html 文件
new CleanWebpackPlugin({ cleanStaleWebpackAssets: false }),
new HtmlWebpackPlugin({
title: '管理输出',
}),
],
watch: true,
// 启动文件监听的相关配置信息
watchOptions: {
// 当第一个文件更改,会在重新构建前增加延迟。单位毫秒,默认值200
aggregateTimeout: 600,
// 可以使用正则来设置不需要监听的文件或文件夹
ignored: /node_modules/,
// 指定毫秒为单位进行轮询。默认为false,如果监听没生效,可以先把这个配置打开
poll: 1000
}
}
watchoptions的相关配置可以查看这里。
文件监听的配置很简单,但是其有一个最大的缺点:每次都需要手动刷新浏览器。为了能自动刷新浏览器,下面我们了解webpack-dev-server。
webpack-dev-server
webpack-dev-server为你提供了一个简单的web server,并且具有 live reloading(实时重新加载) 功能。
webpack-dev-server在编译之后不会写入到任何输出文件。而是将 bundle 文件保留在内存中,然后将它们 serve 到 server 中,就好像它们是挂载在 server 根路径上的真实文件一样。
我们需要先进行安装:
npm install -D webpack-dev-server
安装完毕后需要修改配置文件:
module.exports = {
...
devServer: {
contentBase: './dist', // 加载dist文件夹下的文件到server
},
}
接下来,使用命令npm run dev就可以启动。
// package.json
"scripts": {
"dev": "webpack-dev-server --open --config webpack.dev.config.js",
},
当启动后,默认打开localhost:8080。
我们可以参考这里的devServer配置项来修改默认配置,下面说一些其他的重要配置。
- inline
布尔值,表示在开发服务器的两种不同模式之间切换。默认情况下,应用程序将启用inline模式,即bundle文件插入脚本通过自动刷新页面来重新加载项目。
若设置为false,则使用iframe模式,将bundle文件插入页面的<iframe>中,通过重新加载<iframe>来重新加载项目。
- hot 布尔值,表示是否启用热加载。可以用下面几种方式表示启用。
// package.json
"scripts": {
"dev": "webpack-dev-server --open --config webpack.dev.config.js --hot"
},
// webpack.dev.config.js
devServer: {
...
hot: true,
},
在Webpack中,也可以通过Node来启用。
在 Node.js API 中使用 webpack dev server 时,不要将 dev server 选项放在 webpack 配置对象中。而是在创建时, 将其作为第二个参数传递。
const webpackDevServer = require('webpack-dev-server');
const webpack = require('webpack');
const config = require('./webpack.config.js');
const options = {
contentBase: './dist',
hot: true,
...
};
webpackDevServer.addDevServerEntrypoints(config, options);
const compiler = webpack(config);
const server = new webpackDevServer(compiler, options);
server.listen(5000, 'localhost', () => {
console.log('dev server listening on port 5000');
});
这里需要多说明的是,对于一般的css和img,HMR开箱可用。但是针对js,HRM没有通用方案支持,需要HotModuleReplacementPlugin提供的api进行手动的处理需要进行HRM的模块(可以参考官网上的示例)。万幸的是,我们一般使用框架进行开发,不管是Vue(使用vue-loade)也好,React(使用react-hot-loader)也好都集成了HRM解决方案,我们可以开箱可用。
- proxy
webpack-dev-server使用http-proxy-middleware去把请求代理到一个外部的服务器,我们可以在proxy进行配置。
module.exports = {
//...
devServer: {
proxy: {
'/api': 'http://localhost:3000'
}
}
};
上面的例子中,对/api/users的请求会将请求代理到http://localhost:3000/api/users。(更多信息可以查看这里)
webpack-dev-middleware
webpack-dev-middleware是一个封装器(wrapper),它可以把webpack处理过的文件发送到一个server。webpack-dev-server在内部使用了它,然而它也可以作为一个单独的package来使用,以便根据需求进行更多自定义设置。
下面按照官网的示例进行说明。我们先安装相关npm:
npm install --save-dev express webpack-dev-middleware
然后,调整配置文件。
module.exports = {
//...
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
publicPath: '/',
},
};
在添加server.js,用于webpack-dev-middleware相关配置。
// server.js
const express = require('express');
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');
const app = express();
const config = require('./webpack.config.js');
const compiler = webpack(config);
// 告知 express 使用 webpack-dev-middleware,
// 以及将 webpack.config.js 配置文件作为基础配置。
app.use(
webpackDevMiddleware(compiler, {
publicPath: config.output.publicPath,
})
);
// 将文件 serve 到 port 3000。
app.listen(3000, function () {
console.log('Example app listening on port 3000!\n');
});
最后,添加npm script,就可以正常启动了。
// package.json
"scripts": {
"server": "node server.js",
}
当执行npm run server,就可以在http://localhost:3000中看见项目初始页。这里需要注意一个配置属性output.publicPath,官网上表示此选项指定在浏览器中所引用的「此输出目录对应的公开 URL」。
webpack-dev-server 也会默认从publicPath为基准,使用它来决定在哪个目录下启用服务,来访问 webpack 输出的文件。
所以,webpackDevMiddleware方法中的publicPath需要和output.publicPath保持一致,且打开初始页面链接也需要添加该信息。比如若设置output.publicPath为/assets/,则初始页面URL为http://localhost:3000/assets/。
source map
在开发过程中,除了需要修改代码后进行热更新,我们也需要能进行代码错误的排查。但是Webpack会将多个模块打包为一个bundle文件中,而一个模块的错误会指向该bundle文件。若想准确知道是哪个模块哪一行的错误,则需要source map功能。
source map使用devtool进行配置,相关详细说明可以参考这篇文章【[webpack] devtool里的7种SourceMap模式是什么鬼?】,这里就不在过多描述。
在开发环境中,倾向于使用eval-cheap-module-source-map配置。
- 使用
cheap模式可以大幅提高souremap生成的效率。大多数情况下,我们调试时仅需知道行数就行,不需要列数信息。 - 使用
eval方式可大幅提高持续构建效率。 - 使用
module可支持babel这种预编译工具(在Webpack里做为loader使用)。我们需要定位debug到最原始的资源,比如定位错误到jsx,ts的原始代码,而不是经编译后的js代码。 - 使用
eval-source-map模式可以减少网络请求。DataURL内联在文件中,可以稍微提高点网络请求效率。
生产环境
在开发环境中,我们需要实时重新加载或热模块替换能力的server,相对完整的source map。但是在生产环境中,我们更加关注更小的bundle(压缩输出), 更轻量的source map, 还有更优化的资源等。
所以,为了遵循逻辑分离原则, 我们需要将开发环境和生产环境编写彼此独立的配置文件;为了遵守不重复原则,我们保留一个通用配置,通过webpack-merge将其和不同环境的配置文件结合起来。
首先,进行安装:
npm install --save-dev webpack-merge
配置文件:
// webpack.base.config.js
module.exports = {
entry: {
main: './src/main.js',
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js',
},
...
}
// webpack.pord.config.js
const merge = require('webpack-merge')
const baseConfig = require('./webpack.base.config')
module.exports = merge(baseConfig, {
mode: 'production',
output: {
filename: '[name].[contenthash].js',
},
...
})
mode模式
Webpack中有mode配置选项,告知Webpack使用相应模式的内置优化,默认值为production。
当设置mode为production时,
会将 process.env.NODE_ENV 的值设为 production。启用 FlagDependencyUsagePlugin, FlagIncludedChunksPlugin, ModuleConcatenationPlugin, NoEmitOnErrorsPlugin, OccurrenceOrderPlugin, SideEffectsFlagPlugin 和 UglifyJsPlugin.
详细内容可以查看之前的一篇文章【webpack学习记录(1)-mode模式】。
process.env.NODE_ENV
process.env.NODE_ENV的作用主要是帮我们判断是开发环境(development)还是生产环境(production)。
需要注意的是,process.env.NODE_ENV可以在src/*下的所有本地代码中进行访问,在webpack.*.config.js中无法正常获取,仅能获取到undefined。
上面我们了解到,通过设置mode可以设置process.env.NODE_ENV。我们也可以自己使用Webpack内置的DefinePlugin插件来修改这个变量。
从 webpack v4 开始, 指定
mode会自动地配置DefinePlugin
// webpack.pord.config.js
const webpack = require('webpack');
module.exports = merge(baseConfig, {
...
plugins: [
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: '"production"' // 也可以写为:JSON.stringify('production')
}
}),
...
]
})
注:DefinePlugin插件修改后的process.env.NODE_ENV,优先级比mode设置的process.env.NODE_ENV高。
此外,在webpack.*.config.js中,除了通过process.env.NODE_ENV判断,我们可以通过命令行中传入变量NODE_ENV的方式来进行判断当前环境。
// package.json
"scripts": {
"combine": "webpack --config webpack.combine.config.js --env.NODE_ENV=production"
},
// webpack.combine.config.js
const prodWebpackConfig = require('./webpack.prod.config');
module.exports = env => {
return merge(prodWebpackConfig, {
output: {
filename: env.NODE_ENV === 'production' ? '[name].[hash].bundle.combine.js' : '[name].bundle.js',
},
})
}
文件指纹
为了优化体验,浏览器中存在缓存机制。我们一般会对文件添加后缀值,当文件资源变化时,修改其后缀值。
Webpack中,我们可以通过配置来实现自动给资源文件添加后缀值,即文件指纹。
之前针对文件指纹有过文章【webpack学习记录(3)-文件指纹】总结,这里我说下配置方案。
- 针对js文件
可直接在
output.filename中使用[chunkhash]占位符。这样当代码修改时,仅其所在模块对应的bundle文件的指纹会变化。
// webpack.prod.config.js
output: {
filename: '[name].[chunkhash].js',
}
- 针对css文件
由于css和js会一起打包到
bundle文件中,所以为了避免css文件的修改而引起bundle文件指纹变化,我们使用插件mini-css-extract-plugin将css文件抽取出来,再设置css的文件指纹。
// webpack.prod.config.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = merge(baseConfig, {
...
module: {
rules: [
{
test: /.css$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader'
],
}
],
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name].[contenthash].css',
}),
...
]
})
注:使用mini-css-extract-plugin时,需要在loader和plugins都进行配置。
- 针对图片&字体
我们使用
url-loader来处理图片&字体,可以根据文件内容生成hash。
{
loader: 'file-loader',
options: {
name: '[path][name][hash].[ext]',
}
}
压缩
- js压缩
当
mode的值为production,Webpack会默认使用TerserPlugin来进行代码压缩。
注:之前压缩使用的UglifyjsWebpackPlugin,但其不支持ES6+语法,所以替换为TerserPlugin。
我们也可以通过设置optimization.minimizer来覆盖默认的压缩方式。
const TerserPlugin = require("terser-webpack-plugin");
module.exports = {
optimization: {
minimizer: [
new TerserPlugin({
cache: true,
parallel: true, // 多进程并发运行
sourceMap: true, // 如果在生产环境中使用 source-maps,必须设置为 true
...
}),
],
}
};
- css压缩
css压缩需要使用插件CssMinimizerWebpack。
const TerserPlugin = require("terser-webpack-plugin");
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
module.exports = {
...
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
cache: true,
parallel: true, // 多进程并发运行
sourceMap: true, // 如果在生产环境中使用 source-maps,必须设置为 true
...
}),
new CssMinimizerPlugin(),
],
},
};
注:这里需要在optimization.minimizer中添加压缩方案,需要把TerserPlugin添加上,不然会覆盖掉TerserPlugin对js的压缩。
此外,也可以使用插件optimize-css-assets-webpack-plugin来进行css压缩。
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
module.exports = {
...
plugins: [
...
new OptimizeCssAssetsPlugin({
assetNameRegExp: /\.optimize\.css$/g,
cssProcessor: require('cssnano'),
cssProcessorPluginOptions: {
preset: ['default', { discardComments: { removeAll: true } }],
},
canPrint: true
})
],
};
- html压缩
当我们使用插件
html-webpack-plugin来生成HTML文件时,若mode的值为production,会自动压缩HTML文件。也可以通过设置minify来控制是否压缩。
source map
在生产环境中,最好不要生成source map文件。
若为了方便调试,可使用模式cheap-module-source-map,并且使用Sentry来保存调试source map文件(可以参考这篇文章)或者通过nginx设置将.map文件只对白名单开放(公司内网)。
总结
- 开发环境,基础构建需要重新加载,热更新功能,需要尽可能完整的
source map - 生产环境,基础构建需要压缩文件,文件指纹功能,需要尽可能小的
source map