现在,我们只能手工的来调整 mode选项,实现生产环境和开发环境的切换,且很多配置在生产环境和开发环境中存在不一致的情况,比如开发环境没有必要设置缓存,生产环境还需要设置公共路径等等
本章介绍拆分开发环境和生产环境,让打包更灵活
1. 公共路径
publicPath配置选项在各种场景中都非常有用,可以通过它来指定应用程序中所有资源的基础路径
① 不使用公共路径
在使用公共路径之前,打包后的 index.html :
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Webpack5</title><link href="/css/bb19a26be8ae85c3b88a.css" rel="stylesheet"></head><body><script defer src="/js/vendors.6f0475eed10a5b2f3992.js"></script><script defer src="/js/index.9c58ba4a38a06ada3563.js"></script></body></html>
发现 script 、link 标签上引用的都是相对路径
② 使用公共路径
然后我们使用公共路径:配置output.publicPath
output: {filename: 'js/[name].[contenthash].js',path: path.resolve(__dirname, './dist'),// 清除上一次打包,但这一次打包不需要的文件clean: true,// 公共路径:用于指定所有资源的基础路径publicPath: 'http://localhost:8080/'},
查看打包后的 index.html
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Webpack5</title><link href="http://localhost:8080/css/bb19a26be8ae85c3b88a.css" rel="stylesheet"></head><body><script defer src="http://localhost:8080/js/vendors.6f0475eed10a5b2f3992.js"></script><script defer src="http://localhost:8080/js/index.9c58ba4a38a06ada3563.js"></script></body></html>
发现 script 、link 标签上的引用路径都带有我们设置的公共路径publicPath
③ 基于环境设置公共路径
在开发环境中,我们通常有一个 assets/ 文件夹,它与索引页面位于同一级别。这没太大问题,但是,如果我们将所有静态资源托管至 CDN,然后想在生产环境中使用呢?
想要解决这个问题,可以直接使用一个环境变量(environment variable),而且我们还可以结合 webpack 的内置插件webpack.DefinePlugin帮助我们在代码中安全的使用环境变量
const webpack = require('webpack')// 根据环境的不同,修改文件的路径地址process.env.ASSET_PATH = 'http://localhost:8080/'// 尝试使用环境变量,否则使用根路径const ASSET_PATH = process.env.ASSET_PATH || '/';module.exports = {output: {filename: 'js/[name].[contenthash].js',path: path.resolve(__dirname, './dist'),clean: true,// 公共路径:用于指定所有资源的基础路径publicPath: ASSET_PATH},plugins: [// 这可以帮助我们在代码中安全地使用环境变量new webpack.DefinePlugin({'process.env.ASSET_PATH': JSON.stringify(ASSET_PATH)})]}
④ Automatic publicPath
有可能你事先不知道publicPath是什么,webpack 会自动根据import.meta.url、document.currentScrip、script.src或者self.location变量设置publicPath
你需要做的是将output.publicPath设为'auto':
module.exports = {output: {publicPath: 'auto'}}
请注意在某些情况下不支持document.currentScript,例如:IE 浏览器,你不得不引入一个 polyfill,例如currentScript Polyfill
2. 环境变量
想要消除webpack.config.js在 开发环境 和 生产环境 之间的差异,需要环境变量
在使用环境变量之前,对于我们的webpack配置,有一个必须要修改之处。通常,module.exports指向配置对象。要使用env变量,你必须将module.exports转换成一个函数:
const CssMinimizerWebpackPlugin = require('css-minimizer-webpack-plugin')// 尝试使用环境变量,否则使用根路径process.env.ASSET_PATH = 'http://localhost:8080/'const ASSET_PATH = process.env.ASSET_PATH || '/'module.exports = (env) => {return {entry: {index: './src/js/index.js',},output: {filename: 'js/[name].[contenthash].js',path: path.resolve(__dirname, './dist'),clean: true,publicPath: ASSET_PATH},mode: 'development',optimization: {minimizer: [new CssMinimizerWebpackPlugin()]}}}
webpack命令行环境配置的--env参数,可以允许你传入任意数量的环境变量。而在webpack.config.js中可以访问到这些环境变量。例如,--env production或 --env goal=local
修改mode:
// 根据命令行参数 env 来设置不同环境的 modemode: env.production ? 'production' : 'development',
npx webpack --env production
然后控制台就会发出警告提醒你是在开发环境下打包的。接着我们观察 dist/js 文件夹下的 js 文件,发现它们并没有被压缩:
这里webpack有个开箱即用的插件terser可以使 js 文件中的代码压缩,但是这里为什么webpack这个开箱即用的功能没有生效呢?原因是我们配置了optimization.minimizer,做了一个 CSS 压缩。如果我们做了这个配置以后,那么我们需要单独配置一下tersernpm i terser-webpack-plugin
const CssMinimizerWebpackPlugin = require('css-minimizer-webpack-plugin')// 1.引用const TerserWebpackPlugin = require('terser-webpack-plugin')module.exports = (env) => {return {optimization: {minimizer: [new CssMinimizerWebpackPlugin(),// 2.使用new TerserWebpackPlugin()]}}}
执行npx webpack --env production后查看 dist/js 中的 js 文件:
发现生产环境下代码已压缩
再执行npx webpack --env development后查看 dist/js 中的 js 文件:
发现开发环境下代码不会被压缩
这就是我们使用环境变量的意义
3. 拆分配置文件
目前,生产环境和开发环境使用的是一个配置文件,mode: env.production ? 'production' : 'development'是通过三元运算符来区分生产环境和开发环境的,那么我们所有的属性都通过三元运算符来区分生产环境和开发环境,那未免也太麻烦了。所以我们需要拆分一下webpack.config.js文件
在项目根目录下创建 config 文件夹,在其中创建两个文件:webpack.config.dev.js(开发环境的配置文件) 和webpack.config.prod.js(生产环境的配置文件)
webpack.config.js的配置如下:
const path = require('path')const HtmlWebpackPlugin = require('html-webpack-plugin')const MiniCssExtractPlugin = require('mini-css-extract-plugin')const CssMinimizerWebpackPlugin = require('css-minimizer-webpack-plugin')const TerserWebpackPlugin = require('terser-webpack-plugin')const svgToMiniDataURI = require('mini-svg-data-uri')const toml = require('toml')const yaml = require('yaml')const json5 = require('json5')const webpack = require('webpack')// 尝试使用环境变量,否则使用根路径process.env.ASSET_PATH = 'http://localhost:8080/'const ASSET_PATH = process.env.ASSET_PATH || '/';module.exports = (env) => {return {// 设置项目打包的入口文件entry: {index: './src/js/index.js',// anthor: './src/js/another-module.js'},// 设置项目打包的出口文件output: {// 指定输出文件的文件名filename: 'js/[name].[contenthash].js',// 指定文件的输出路径(只能是绝对路径)path: path.resolve(__dirname, './dist'),// 清除上一次打包,但这一次打包不需要的文件clean: true,// 定义输出文件的文件名(优先级小于generator)// assetModuleFilename: 'img/[contenthash][ext][query]',// 公共路径:用于指定所有资源的基础路径publicPath: ASSET_PATH},// 根据命令行参数 env 来设置不同环境的 modemode: env.production ? 'production' : 'development',// 能够准确的捕获代码方式错误的位置// inline-source-map:在开发模式下追踪代码(mode: 'development',)devtool: 'inline-source-map',// 插件plugins: [new HtmlWebpackPlugin({// 指定哪个html文件为模板template: './src/index.html',// 指定输出文件的文件名filename: 'index.html',// 指定生成的 script 标签在 head 中,还是在 body 中inject: 'body'}),new MiniCssExtractPlugin({filename: 'css/[contenthash].css'}),// 这可以帮助我们在代码中安全地使用环境变量new webpack.DefinePlugin({'process.env.ASSET_PATH': JSON.stringify(ASSET_PATH)})],// 修改代码后实现浏览器热更新(结合 npx webpack-dev-server 命令开启本地服务器一起使用)devServer: {static: './dist'},// 配置资源文件module: {rules: [{test: /\.js$/,// node_modules 中的代码无需编译exclude: /node_modules/,use: {loader: 'babel-loader',options: {// 通过 presets 设置预设presets: ['@babel/preset-env'],plugins: [['@babel/plugin-transform-runtime']]}},},{// 校验文件格式test: /\.jpg$/,// 资源模块类型type: 'asset/resource',// 定义输出文件的文件名generator: {filename: 'img/[contenthash][ext][query]'}},{test: /\.svg$/,// 注:inline资源模块类型不会导出文件type: 'asset/inline',// 自定义data urlgenerator: {dataUrl: context => {return svgToMiniDataURI(context.toString())}}},{test: /\.txt$/,// 注:source资源模块类型不会导出文件type: 'asset/source'},{test: /\.(woff|woff2|eot|ttf|otf)$/,type: 'asset/resource'},{test: /\.png$/,type: 'asset',generator: {filename: 'img/[contenthash][ext][query]'},parser: {dataUrlCondition: {// 默认4*1024(4kb)maxSize: 4*1024}}},{test: /\.(css|less)$/,// 抽离 css 使用插件 MiniCssExtractPlugin 自身的 loaderuse: [MiniCssExtractPlugin.loader, 'css-loader', 'less-loader']},{test: /\.(tsv|csv)$/,use: 'csv-loader'},{test: /\.xml$/,use: 'xml-loader'},{test: /\.toml$/,type: 'json',parser: {parse: toml.parse}},{test: /\.yaml$/,type: 'json',parser: {parse: yaml.parse}},{test: /\.json5$/,type: 'json',parser: {parse: json5.parse}}]},// 优化配置optimization: {minimizer: [new CssMinimizerWebpackPlugin(),new TerserWebpackPlugin()],splitChunks: {// 缓存组cacheGroups: {vendor: {// 第三方库的文件都在 node_modules 文件夹里test: /[\\/]node_modules[\\/]/,name: 'vendors',chunks: 'all'}}}}}}
webpack.config.dev.js的配置如下:
const path = require('path')const HtmlWebpackPlugin = require('html-webpack-plugin')const MiniCssExtractPlugin = require('mini-css-extract-plugin')const svgToMiniDataURI = require('mini-svg-data-uri')const toml = require('toml')const yaml = require('yaml')const json5 = require('json5')module.exports = {entry: {index: './src/js/index.js',},output: {// 开发环境中不需要缓存,所以不用[name].[contexthash].jsfilename: 'js/[name].js',// 目录路径修改为上一级 ../dist,否则打包的dist目录会在config文件夹下而不是项目根目录下path: path.resolve(__dirname, '../dist'),clean: true,assetModuleFilename: 'other/[contenthash][ext][query]',// 开发环境不需要publicPath},mode: 'development',devtool: 'inline-source-map',devServer: {static: path.resolve(__dirname, '../dist')},plugins: [new HtmlWebpackPlugin({template: './src/index.html',filename: 'index.html',inject: 'head'}),new MiniCssExtractPlugin({filename: 'css/[contenthash].css'})],module: {rules: [{test: /\.js$/,exclude: /node_modules/,use: {loader: 'babel-loader',options: {presets: ['@babel/preset-env'],plugins: [['@babel/plugin-transform-runtime']]}},},{test: /\.jpg$/,type: 'asset/resource',generator: {filename: 'img/[contenthash][ext][query]'}},{test: /\.svg$/,type: 'asset/inline',generator: {dataUrl: context => {return svgToMiniDataURI(context.toString())}}},{test: /\.txt$/,type: 'asset/source'},{test: /\.(woff|woff2|eot|ttf|otf)$/,type: 'asset/resource'},{test: /\.png$/,type: 'asset',generator: {filename: 'img/[contenthash][ext][query]'},parser: {dataUrlCondition: {maxSize: 4*1024}}},{test: /\.(css|less)$/,use: [MiniCssExtractPlugin.loader, 'css-loader', 'less-loader']},{test: /\.(tsv|csv)$/,use: 'csv-loader'},{test: /\.xml$/,use: 'xml-loader'},{test: /\.toml$/,type: 'json',parser: {parse: toml.parse}},{test: /\.yaml$/,type: 'json',parser: {parse: yaml.parse}},{test: /\.json5$/,type: 'json',parser: {parse: json5.parse}}]},optimization: {// 开发环境不需要压缩,所以不需要minimizersplitChunks: {cacheGroups: {vendor: {test: /[\\/]node_modules[\\/]/,name: 'vendors',chunks: 'all'}}}}}
webpack.config.prod.js的配置如下:
const path = require('path')const HtmlWebpackPlugin = require('html-webpack-plugin')const MiniCssExtractPlugin = require('mini-css-extract-plugin')const CssMinimizerWebpackPlugin = require('css-minimizer-webpack-plugin')const TerserWebpackPlugin = require('terser-webpack-plugin')const svgToMiniDataURI = require('mini-svg-data-uri')const toml = require('toml')const yaml = require('yaml')const json5 = require('json5')module.exports = {entry: {index: './src/js/index.js',},output: {// 生产环境需要缓存 [contexthash]filename: 'js/[name].[contenthash].js',// 修改成 ../distpath: path.resolve(__dirname, '../dist'),clean: true,assetModuleFilename: 'other/[contenthash][ext][query]',// 生产环境需要publicPathpublicPath: 'http://localhost:8080/'},mode: 'production',// 开发环境不需要 devtool 和 devServerplugins: [new HtmlWebpackPlugin({template: './src/index.html',filename: 'index.html',inject: 'head'}),new MiniCssExtractPlugin({filename: 'css/[contenthash].css'})],module: {rules: [{test: /\.js$/,exclude: /node_modules/,use: {loader: 'babel-loader',options: {presets: ['@babel/preset-env'],plugins: [['@babel/plugin-transform-runtime']]}},},{test: /\.jpg$/,type: 'asset/resource',generator: {filename: 'img/[contenthash][ext][query]'}},{test: /\.svg$/,type: 'asset/inline',generator: {dataUrl: context => {return svgToMiniDataURI(context.toString())}}},{test: /\.txt$/,type: 'asset/source'},{test: /\.(woff|woff2|eot|ttf|otf)$/,type: 'asset/resource'},{test: /\.png$/,type: 'asset',generator: {filename: 'img/[contenthash][ext][query]'},parser: {dataUrlCondition: {maxSize: 4*1024}}},{test: /\.(css|less)$/,use: [MiniCssExtractPlugin.loader, 'css-loader', 'less-loader']},{test: /\.(tsv|csv)$/,use: 'csv-loader'},{test: /\.xml$/,use: 'xml-loader'},{test: /\.toml$/,type: 'json',parser: {parse: toml.parse}},{test: /\.yaml$/,type: 'json',parser: {parse: yaml.parse}},{test: /\.json5$/,type: 'json',parser: {parse: json5.parse}}]},optimization: {// 开发环境需要压缩 CSS 和 JSminimizer: [new CssMinimizerWebpackPlugin(),new TerserWebpackPlugin()],splitChunks: {cacheGroups: {vendor: {test: /[\\/]node_modules[\\/]/,name: 'vendors',chunks: 'all'}}}}}
拆分成两个配置文件后,分别运行这两个文件:
- 开发环境:
npx webpack -c ./config/webpack.config.dev.js
观察 dist ,发现文件名上没有 hash 字符串,以及 index.html 中的引用也没有了缓存,CSS 文件夹中的代码也没有压缩
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Webpack5</title><script defer src="js/vendors.js"></script><script defer src="js/index.js"></script><link href="css/a4f4120816239f5386dd.css" rel="stylesheet"></head><body></body></html>

可以在开发环境中启动服务:npx webpack serve -c ./config/webpack.config.dev.js
- 生产环境
npx webpack -c ./config/webpack.config.prod.js
观察 dist ,发现文件名上有 hash 字符串,以及 index.html 中的引用也有了缓存,CSS 文件夹中的代码也压缩了
4. 提取公共配置
在此之前,我们已经将webpack.config.js抽离成webpack.config.dev.js和webpack.config.prod.js,但是这两个文件中含有很多重复代码,我们可以手动的将这些重复的代码单独提取到一个文件里
在 config 文件夹下创建webpack.config.common.js,用来存放重复代码:
const path = require('path')const HtmlWebpackPlugin = require('html-webpack-plugin')const MiniCssExtractPlugin = require('mini-css-extract-plugin')const svgToMiniDataURI = require('mini-svg-data-uri')const toml = require('toml')const yaml = require('yaml')const json5 = require('json5')module.exports = {entry: {index: './src/js/index.js',},output: {path: path.resolve(__dirname, './dist'),clean: true,assetModuleFilename: 'other/[contenthash][ext][query]',},plugins: [new HtmlWebpackPlugin({template: './src/index.html',filename: 'index.html',inject: 'head'}),new MiniCssExtractPlugin({filename: 'css/[contenthash].css'})],module: {rules: [{test: /\.js$/,exclude: /node_modules/,use: {loader: 'babel-loader',options: {presets: ['@babel/preset-env'],plugins: [['@babel/plugin-transform-runtime']]}},},{test: /\.jpg$/,type: 'asset/resource',generator: {filename: 'img/[contenthash][ext][query]'}},{test: /\.svg$/,type: 'asset/inline',generator: {dataUrl: context => {return svgToMiniDataURI(context.toString())}}},{test: /\.txt$/,type: 'asset/source'},{test: /\.(woff|woff2|eot|ttf|otf)$/,type: 'asset/resource'},{test: /\.png$/,type: 'asset',generator: {filename: 'img/[contenthash][ext][query]'},parser: {dataUrlCondition: {maxSize: 4*1024}}},{test: /\.(css|less)$/,use: [MiniCssExtractPlugin.loader, 'css-loader', 'less-loader']},{test: /\.(tsv|csv)$/,use: 'csv-loader'},{test: /\.xml$/,use: 'xml-loader'},{test: /\.toml$/,type: 'json',parser: {parse: toml.parse}},{test: /\.yaml$/,type: 'json',parser: {parse: yaml.parse}},{test: /\.json5$/,type: 'json',parser: {parse: json5.parse}}]},optimization: {splitChunks: {cacheGroups: {vendor: {test: /[\\/]node_modules[\\/]/,name: 'vendors',chunks: 'all'}}}}}
修改webpack.config.dev.js
module.exports = {output: {filename: 'js/[name].js',},mode: 'development',devtool: 'inline-source-map',devServer: {static: '../dist'}}
修改webpack.config.prod.js
const CssMinimizerWebpackPlugin = require('css-minimizer-webpack-plugin')const TerserWebpackPlugin = require('terser-webpack-plugin')module.exports = {output: {filename: 'js/[name].[contenthash].js',publicPath: 'http://localhost:8080/'},mode: 'production',optimization: {minimizer: [new CssMinimizerWebpackPlugin(),new TerserWebpackPlugin()]}}
代码抽离完后需要进行三个文件的合并,而且是需要一个所谓对象的深合并(merge),我们可以使用webpack-merge来完成这个功能
5. 合并配置文件
配置文件拆分好后,新的问题来了,如何保证配置合并没用问题呢?webpack-merge工具可以完美解决这个问题
可以先去 npm 官网查看这个工具的用法:https://www.npmjs.com/package/webpack-merge
① 安装依赖npm i webpack-merge
② 在 config 文件夹下新建文件webpack.config.js,合并代码:
const { merge } = require('webpack-merge')const commonConfig = require('./webpack.config.common.js')const productionConfig = require('./webpack.config.prod.js')const developmentConfig = require('./webpack.config.dev')module.exports = (env) => {switch(true) {case env.development: return merge(commonConfig, developmentConfig);break;case env.production: return merge(commonConfig, productionConfig);break;default: throw new Error('No matching configuration was found!');}}
6. npm 脚本
配置package.json中的script,配置 npm 脚本来简化命令行的输入,这时可以省略 npx :
"scripts": {
"serve": "webpack serve -c ./config/webpack.config.js --env development ",
"build": "webpack -c ./config/webpack.config.js --env production"
}
现在就可以直接在控制台执行命令了:
npm run serve
npm run build
