1. 引言
webpack
是前端开发中重要的打包工具, 对于很多初中级开发者来说都有一定难度,本文将结合webpack
的构建流程,梳理常用配置,带大家一起手写react
和vue3
项目的通用webpack
配置。希望通过阅读本文,大家能够掌握编写适合自己项目的webpack
配置。即使是零基础的新手,也可以直接复用最终配置到项目中,无需过度纠结于某个配置。
webpack
由于版本迭代,会删除一些参数与配置,同时会新增一些参数与配置,那么我们怎么保证我们掌握的webpack
知识,不会随着webpack
版本的变更而变更,也就说让我们已经学到的webpack
知识怎么更保值
就我个人的理解来说,就是抓住主线,不过分纠结细节,什么是主线,那就是webpack
的构建流程,如下图所示
这个过程是一般不会有太大变化,所以我们只要根据这个构建过程来理解,那么就知道,构建项目大概需要哪些webpack
配置参数
另外不需要过分纠结于细节是因为webpack
会随着迭代会进行优化,而优化之后与之前可能会有大的变动,比如webpack4
之前的chunk
提取使用CommonsChunkPlugin
,而webpack4
之后就是内置的optimization.splitChunks
参数,不仅从使用配置上变了,而且内部实现也变了,所以我们在使用webpack
的时候可以不过份纠结于这些细节。
下面是我自己总结的关于webpack5
相关的常用配置参数,然后在结合webpack5
的构建流程,就能够封装满足自己业务项目的通用webpack5
配置
如果不想了解webpack5
常用参数与进阶参数,可以直接跳过参数部分,直接阅读手写webpack5
配置部分
- 手写基于react项目的webpack配置
- 手写基于vue3的webpack配置
2. webpack常用配置
我们先了解一下webpack
的常用配置,然后在针对react
、vue
项目搭建通用的webpack
配置,零基础也可以轻松阅读,因为本篇基本不涉及原理,关注点在怎么使用
2.1 基础webpack配置
基础配置可以理解成实际项目使用的时候基本会用到的配置
2.1.1 指定模式(mode)
mode:指定webpack
的构建模式,控制webpack
的默认行为
mode参数是在webpack 4.0版本中增加的。该参数可以指定webpack构建时所处的环境,设置不同的mode会启用不同的内置优化策略,在webpack 4.0之前,开发者需要手动配置一些优化策略,如配置压缩代码的插件、使用DefinePlugin定义全局常量等。而在Webpack 4.0中引入了mode参数后,webpack会根据所设置的mode自动启用对应的优化策略与默认配置,更加方便快捷;说白了最终目的就是降低webpack配置难度
/*
development 开发环境模式
production 生产环境模式
development 与 production的区别是,production多了很多优化的行为,比如tree-shaking,还有一些moduleId等
开发环境构建mode设置为development,生产环境构建mode设置为production
*/
module.exports = {
mode: 'development' || 'production'
};
⚠️如果没设置,且环境变量NODE_ENV
不是development
、production
对应的值,构建的时候会给出警告
2.1.2 指定入口(entry)
entry:指定webpack
的入口文件。可以是一个路径字符串或一个对象等。如果是一个对象,则会对多个依赖关系进行分析。
webpack一直有的参数,如果没有值,默认值为src/index.[js ts jsx tsx]
/*
entry有多种写法,主要还是多entry与单entry的区别
单页应用一般使用单entry
多页应用一般使用多entry
*/
module.exports = {
// 单entry
entry: path.join(__dirname, '../src/app.js'),
// 多entry
entry: {
app: path.join(__dirname, '../src/app.js'),
app2: path.join(__dirname, '../src/app2.js')
}
};
2.1.3 指定输出配置(output)
output:指定webpack
的输出配置。包括输出目录、输出chunk
文件名,entry
文件名,publicPath
等
webpack一直有的参数,非必填,如果不填webpack会有默认值,但是我们一般都会填,因为会涉及到cdn地址,最终生成的文件目录与文件名称,暴露的全局变量等
module.exports = {
output: {
// 输出目录
path: path.join(__dirname, '../dist'),
// entry chunk文件名
filename: 'js/[name].[chunkhash].js',
// 提取的chunk文件名
chunkFilename: 'chunk/[name].[chunkhash].js',
// 静态资源的公共url
publicPath: '/'
},
};
⚠️生成最终文件名的配置,需要注意三种hash值,hash、contenthash、chunkhash的区别:
- Hash:每次
webpack
构建时,如果有文件变化,就会生成一个全新的hash
值,在这次构建中使用的所有文件的hash
值都是相同的。虽然这可以防止浏览器缓存不更新,但是无法做到精确控制。 - Chunkhash:根据不同的入口文件进行依赖关系的解析,构建对应的
Chunk
,每个Chunk
都有自己的hash
值。这意味着,只有该chunk
内的模块发生变化时,才会改变对应chunk
的hash
值。因此,使用chunkhash
可以更精确的控制缓存,避免浏览器缓存不更新(一般用于js、图片资源)。 - Contenthash:与
chunkhash
类似,但它的作用对象是文件的内容,而非文件本身。只有文件的内容发生了变化,才会改变对应的hash
值。因此,使用contenthash
可以针对性地使浏览器缓存更加智能,避免浏览器缓存不更新,同时也避免了无用的重新构建(一般用于css资源)。
看具体的例子,就是用上面的output
配置,第一次构建
不做任何改动第二次构建,hash
值没有发生变化
修改一个js文件,第三次构建,hash
值发生变化 从上面的例子看出
- 第二次相比于第一次构建,
hash
值、所有chunk
的chunkhash
值都没有发生变化,所以说明无文件变化时hash
、chunkhash
不发生变化(css
文件这里使用的是contenthash
,暂且不关注) - 第三次相比第二次构建,
hash
值发生变化,app
chunkhash
发生变化、runtime chunk
、vendors chunk
的chunkhash
没有发生变化,所以说明文件变化时,只有包含该文件的chunk
的chunkhash
会发生变化,不包含该变化文件的chunk
的chunkhash
不会发生变化
❤️小结一下: Hash:所有文件的hash
值都是相同的,文件的内容和文件名的改变都会影响到所有文件的hash
值。 Chunkhash:每个Chunk
都有自己的hash
值,同一个Chunk
内文件的内容和文件名的改变都会影响到该Chunk的hash值。 Contenthash:只有文件的内容发生了变化,才会改变对应的hash
值。 因此,在使用webpack
构建项目时,我们通常会根据具体需求选择合适的hash
策略,以达到更好的缓存效果。
涉及到hash
的配置一般有
- output:控制输出资源的文件名
- generator:控制图片等静态资源的文件名
- MiniCssExtractPlugin:控制提取的
css
文件名
output: {
path: '/Users/wangks/Documents/yunke/frontend-template/dist',
filename: 'js/[name].[chunkhash].js',
chunkFilename: 'chunk/[name].[chunkhash].js',
publicPath: '/'
},
{
test: /\.(png|jpe?g|gif|bpm|svg|webp)(\?.*)?$/,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 10240
}
},
generator: {
filename: 'image/[name].[hash][ext]'
}
},
new MiniCssExtractPlugin(
{
filename: 'css/[name].[contenthash].css',
chunkFilename: 'css/[name].[contenthash].css'
}
),
2.1.4 指定模块查找配置(resolve)
resolve:指定webpack
查找模块时的配置,比如指定别名、npm
包相关查找规则
webpack一直有的参数,非必填,但是我们一般都会填,比如设置别名alias,方便我们在开发的时候快速导入模块,比如设置extensions允许加载的拓展文件名,总体来说方便拓展加载的模块类型与提高开发效率,同时又因为涉及到模块加载的规则,所以会看到resolve是webpack优化时的其中一项
module.exports = {
resolve: {
symlinks: true,
// 模块别名,作用我们可以像import npm包那样,import我们src下的模块
alias: {
process: 'process/browser',
'@': path.join(__dirname, '../src'),
},
// 加载模块的时候,允许匹配的文件名后缀
extensions: [
'.tsx',
'.ts',
'.jsx',
'.js'
],
// 加载npm包的时候,通过哪个字段获取npm包入口文件
mainFields: [
'browser',
'main:h5',
'module',
'main'
],
// 加载npm包的时候,在哪些目录去查找
modules: [
path.join(__dirname, '../node_modules'),
]
},
};
比如
// 没有别名之前,通过相对路径引入模块
import { token } from '../../../utils/index'
// 有别名之后,可以直接通过别名的方式导入模块,这样导入更清晰与方便
import { token } from '@/utils/index'
2.1.5 模块转化规则(module)
module: webpack
默认是不能处理所有静态资源的,所以我们需要通过loader
来帮助我们,将webpack
不能识别的资源转化成webpack
可以识别的内容,module
指定loader
、parse
等配置
webpack一直有的参数,非必填,但是在真实的项目中都会配置,原因是需要处理js、css、图片等资源;主要包含rules参数,即使用正则表达式匹配特定文件类型,并使用相应的 loader 进行处理,noParse 来排除对某些模块的解析以提高构建性能,parser 选项来自定义 webpack 的模块解析器;其中rules参数是webpack2.0中引入的,在2.0之前的版本叫loaders;
我们在实际处理项目的时候一般会碰到以下需要loader
处理资源的场景
ts
转化成js
js
es6+转化成es5- 处理
css
,比如提取、添加前缀等 - 处理图片、音视频等静态资源
- 提升
loader
转化速度
下面我们分别来看看
处理js
pnpm add babel-loader thread-loader -D
- babel-loader:用于将
ts
转化为js
,并将es6+语法转化为es5 - thread-loader:加速
babel-loader
的转化速度
具体配置如下
module.exports = {
module: {
rules: [
{
test: /\.[tj]sx?$/i,
exclude: [
/(node_modules|bower_components)/
],
use: [
{
loader: 'thread-loader',
}
{
loader: 'babel-loader'
}
]
},
},
}
推荐直接使用babel-loader
处理ts,而不是使用ts-loader处理ts,之所以推荐babel-loader处理快之外(因为ts-loader相对于babel-loader多了一层类型检查),我们可以在vscode中使用typescript类型检查,同时也可以通过git hook在代码提交的时候进行类型检查,所以推荐直接使用babel-loader即可。
处理css
pnpm add css-loader style-loader mini-css-extract-plugin postcss-loader less-loader -D
css-loader
:将css
资源转化成webpack
能够处理的资源style-loader
:用于开发环境将css-loader
处理后的样式通过style
等方式插入到html
mini-css-extract-plugin.loader
: 用于生成环境构建时将css-loader
处理后的样式抽离到单独的文件中postcss-loader
: 用于处理css
,将css
添加前缀,支持css
新语法等less-loader
: 将less
转化为css-loader
能够处理样式sass-loader
: 将sass
转化为css-loader
能够处理样式
一般项目配置如下所示
module.exports = {
...
module: {
rules: [
{
test: /\.(css|less|s[a|c]ss)(\?.*)?$/i,
use: [
{
loader: process.env.NODE_ENV === 'development' ? 'style-loader' : miniCssExtractPlugin.loader
},
],
},
{
test: /\.css$/i,
use: [
{
loader: 'css-loader'
},
{
loader: 'postcss-loader'
},
],
},
{
test: /\.less$/i,
use: [
{
loader: 'css-loader'
},
{
loader: 'postcss-loader'
},
{
loader: 'less-loader'
},
],
},
{
test: /\.s[a|c]ss$/i,
use: [
{
loader: 'css-loader'
},
{
loader: 'postcss-loader',
options: {
plugins: [
require('autoprefixer')
]
}
},
{
loader: 'sass-loader'
},
],
},
],
},
}
处理图片等静态资源
处理静态资源可以直接依赖webpack5
自带的能力
module.exports = {
module: {
rules: [
{
test: /\.(png|jpe?g|gif|bpm|svg|webp)(\?.*)?$/,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 10240
}
},
generator: {
filename: 'image/[name].[hash][ext]'
}
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 10240
}
},
generator: {
filename: 'static/fonts/[name].[hash][ext]'
}
},
{
test: /\.(mp4|webm|ogg|mp3|m4a|wav|flac|aac)(\?.*)?$/,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 10240
}
},
generator: {
filename: 'static/media/[name].[hash][ext]'
}
},
{
resourceQuery: /raw/,
type: 'asset/source'
}
]
},
}
2.1.5 功能增强(plugins)
webpack一直有的参数,非必填,但是真实项目内使用时会配置插件;其作用是拓展webpack的功能
基础插件
html-webpack-plugin(html插件)
html-webpack-plugin支持webpack4+版本,其作用是指定入口html文件,然后将webpack构建好的js、css等静态资源按照加载顺序,插入到html内
安装依赖
pnpm add html-webpack-plugin -D
具体使用如下所示,更多配置参数查看文档
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
plugins: [
new HtmlWebpackPlugin(
{
filename: 'index.html',
template: path.join(__dirname, '../public/index.html'),
minify: {
collapseWhitespace: true,
minifyJS: true,
html5: true,
minifyCSS: true,
removeComments: true,
removeTagWhitespace: false
},
}
),
]
}
mini-css-extract-plugin
mini-css-extract-plugin支持webpack4+版本,其作用是提取css内容成单独文件
pnpm add mini-css-extract-plugin -D
使用方式
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
module: {
rules: [
{
test: /\.(css|less|s[a|c]ss)(\?.*)?$/,
use: [
{
loader: MiniCssExtractPlugin.loader
}
]
},
]
},
plugins: [
new MiniCssExtractPlugin(
{
filename: 'css/[name].[contenthash].css',
chunkFilename: 'css/[name].[contenthash].css'
}
)
]
}
⚠️这里的filename
与chunkFilename
为什么使用的是contenthash
而不是chunkhash
,原因是因为css
文件是从js
里面提取出来的,当我们某个js
文件变动了而css
文件实际上没有变动,但是最终的css chunkhash
还是发生了变化,原因就是js
变了,从而导致提取的css
时的css module
也发生了变化,最终提取的css chunkhash
也发生变化,具体实例如下所示
看一个具体的例子,第一次构建
第二次构建,没有文件发生变化
修改js
文件,第三次构建 从上面的例子看出
- 第二次相比于第一次构建,
hash
值,所有chunk
的chunkhash
值都会没有发生变化的,所以说明无文件变化时hash
、chunkhash
不发生变化 - 第三次相比第二次构建,hash值发生变化,
app chunkhash
、css chunkhash
发生变化、runtime chunk
、vendors chunk
的chunkhash
没有发生变化,所以说明文件变化时,包含该文件的chunkhash
会发生变化,不包含该变化文件的chunkhash
不会发生变化 - 为了避免因为
js
的变化,导致最终生成的css
文件名也发生变化,所以css
这里使用的是contenthash
而不是chunkhash
webpack.DefinePlugin
webpack.DefinePlugin webpack4+内置插件,其作用是编译时 将代码中的变量替换为其他值或表达式,方便我们做一些根据环境来区分运行的代码
module.exports = {
plugins: [
new webpack.DefinePlugin(
{
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV)
}
),
]
}
webpack.ProvidePlugin
webpack.ProvidePlugin webpack4+内置插件,其作用是为每个模块自动导入某个依赖,而不需要手动require or import导入
module.exports = {
plugins: [
new webpack.ProvidePlugin(
{
process: 'process/browser',
$: 'jquery',
}
),
]
}
辅助插件
webpack.ProgressPlugin
webpack.ProgressPlugin webpack4+内置插件,其作用是显示webpack构建进度
module.exports = {
plugins: [
new webpack.ProgressPlugin(
{
percentBy: 'entries',
profile: false
}
),
]
}
clean-webpack-plugin
clean-webpack-plugin插件支持webpack4+版本,其作用是清除上次打包产物
安装依赖
pnpm add clean-webpack-plugin -D
使用方式
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
module.exports = {
plugins: [
new CleanWebpackPlugin(
{
dry: false
}
),
]
}
FriendlyErrorsWebpackPlugin
@soda/friendly-errors-webpack-plugin插件支持webpack4+版本,其作用是美化webpack构建过程中抛出的错误
安装依赖
pnpm add @soda/friendly-errors-webpack-plugin -D
使用方式
const FriendlyErrorsWebpackPlugin = require('@soda/friendly-errors-webpack-plugin')
module.exports = {
plugins: [
new FriendlyErrorsWebpackPlugin(
{
percentBy: 'entries',
profile: false
}
),
]
}
copy-webpack-plugin
copy-webpack-plugin插件支持webpack4+版本,其作用是copy文件
pnpm add copy-webpack-plugin -D
使用方式
const CopyPlugin = require('copy-webpack-plugin')
module.exports = {
plugins: [
new CopyPlugin(
{
patterns: [
{
from: path.join(__dirname, 'public'),
to: path.join(__dirname, 'dist'),
globOptions: {
ignore: [
'**/*.html'
]
},
noErrorOnMissing: true
}
]
}
),
]
}
case-sensitive-paths-webpack-plugin
case-sensitive-paths-webpack-plugin插件支持webpack3+版本,其作用是检查文件的大小写,目的是为了解决osx系统上大小写不敏感,而其它系统上大小写敏感导致的bug
安装依赖
pnpm add case-sensitive-paths-webpack-plugin -D
使用方式
const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin')
module.exports = {
plugins: [
new CaseSensitivePathsPlugin(),
]
}
2.1.6 开发服务(devServer)
不属于webpack构建时的参数,webpack3.0引入,对webpack-dev-server生效,其作用是为项目添加开发服务器,提供本地开发服务访问,热更新等能力
安装依赖
pnpm add webpack-dev-server -D
使用方式
devServer: {
static: {
directory: path.join(__dirname, 'public'),
publicPath
},
compress: true,
port: 9000,
host: 'localhost',
hot: true,
https: false,
open: ['/react-webpack-config-demo/'],
historyApiFallback: {
rewrites: [{
from: /./,
to: publicPath,
}],
},
},
2.2 进阶webpack配置
进阶配置可以理解成优化配置,每个人的理解不一样,不强求,上面的参数基本上满足了大部分场景,但是我们在实际的使用的时候,考虑到环境、性能、缓存、调试等问题,会进一步使用一些webpack
提供的配置
- chunk优化:对应
optimization
配置 - source-map:对应
devtool
配置 - 缓存: 对应
cache
配置
2.2.1 指定chunk提取规则(optimization)
webpack4.0引入的一个参数,非必填,但是webpack会根据mode设置一些默认值;在webpack 4.0之前,很多优化选项都是作为独立的插件存在,需要手动添加和配置。而optimization 参数则将这些常见的优化策略整合到一个对象中,使得配置变得更加简单和清晰,比如压缩(minimize)、chunk提取(splitChunks)、提取runtime chunk(runtimeChunk)、标记导入导出用于tree-shaking(usedExports)等
⚠️在了解为什么要进行chunk
提取之前,先了解几个概念
Module
(模块):webpack
将所有的代码都视为模块,每个模块都有各自独立的作用域。可以是JS
文件、CSS
文件、图片等等。Chunk
(代码块):代码块指由多个模块组合而成的代码块,通常用于代码的分割和按需加载。可以是按页面划分的代码块、按路由划分的代码块、公共代码块等等。Asset
(资源文件):webpack
打包后产生的输出文件,可以是JS
文件、CSS
文件、图片等。Asset
通常是由多个Chunk
组合而成的,每个Chunk
都会生成一个或多个Asset
文件。
简单来说,Module
就是Webpack
中的基本单元,每一个文件差不多就是一个Module
;Chunk
则是将多个Module
组合在一起形成的一个代码块,可用于按需加载和优化性能;Asset
则是Chunk
编译后得到的资源文件,包括JS
、CSS
、图片等。
webpack中chunk分为三类
- initial chunk 也就是
entry chunk
- async chunk 也就是通过动态加载的
entry chunk
- split chunk 也就是提取出来的
chunk
在了解了chunk
之后,我们在了解为什么需要拆分chunk
?看一个例子,伪代码如下所示
// test1.js
import common from './common.js'
console.log(common);
// test2.js
import common from './common.js'
console.log(common);
webpack
配置
module.exports = {
// 多entry
entry: {
app: path.join(__dirname, 'entry.js'),
app2: path.join(__dirname, 'entry2.js')
}
};
上述的代码打包的时候,最开始会有两个initial chunk
,二这两个initial chunk
都会包含common.js
这个模块内容,显然当这种公共模块存在于多个chunk
的时候,明显会让总包体积变大,也存在重复内容,所以才会进行chunk
拆分,将initial chunk
、async chunk
中存在多份的模块,提取到一个公共的chunk
里面,这样就可以减少重复代码,降低总包体积
chunk提取(optimization.splitChunks)
主要作用就是将各个chunk中的公共module,提取出来只保留一份,最终目的就是尽可能的减少公共代码存在的分数,缩小代码尺寸
splitChunks:可以设置最小尺寸、最小Chunks数、按需加载等。可参考官方文档了解更多细节。
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
minSize: 102400,
minChunks: 3,
maxSize: 307200,
maxAsyncRequests: 30,
maxInitialRequests: 30,
automaticNameDelimiter: '~',
cacheGroups: {// 提取chunk规则不固定,根据自己项目提取,但是一般的提取规则为node_modules下的包(还可以抽一个react类orvue类的),common相关的抽一个包
'default': false,
vendors: {
test: /[\\\/]node_modules[\\\/]/,
priority: -10,
reuseExistingChunk: true,
name: 'vendors'
},
common: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true,
name: 'common'
},
react: {
test: /[\\\/]node_modules[\\\/](core-js|react.*|redux.*|props-type|immer|history|@reduxjs\/toolkit)[\\\/]/,
priority: 0,
reuseExistingChunk: true,
name: 'react',
minSize: 0
}
}
},
}
};
提取runtime chunk(optimization.runtimeChunk)
每个 entry chunk 都包含一个 runtime chunk,这会导致多个入口文件重复包含相同的运行时代码,从而增加了打包后的文件大小和加载时间,为了避免这种情况,webpack 从 4.x 版本开始提供了一个新特性,称为 runtimeChunk。通过配置 runtimeChunk,我们可以将所有 entry chunks 共享的运行时代码抽离出来,并将其保存到一个单独的文件中。这样就可以避免多个入口文件重复包含相同的运行时代码,从而减少了打包后的文件大小和加载时间。
module.exports = {
optimization: {
runtimeChunk: {
name: 'runtime'
},
},
}
压缩js && css
配置压缩的插件,如果不配置会使用默认的压缩插件terser
module.exports = {
optimization: {
minimizer: [
new TerserPlugin(
{
parallel: true,
terserOptions: {
output: {
comments: false,
keep_quoted_props: false,
quote_keys: false,
beautify: false
},
keep_fnames: true,
warnings: false,
compress: {
drop_console: true,
drop_debugger: true
}
}
}
)
new CssMinimizerPlugin(
{
parallel: true
}
)
]
},
}
2.2.2 指定缓存配置(cache)
webpack5中的重磅功能,相比cache-loader等缓存方式,提供了更全面及更多样的缓存方式
使用cache参数可以启用Webpack的持久化缓存。开启缓存后,Webpack会将每次构建结果缓存到磁盘中,提高下一次构建的速度。
module.exports = {
cache: true
};
2.2.3 指定source-map(devtool)
webpack一直存在的参数,非必填,不填时webpack有默认配置,用于指定在生成代码时应该为每个模块添加什么类型的 Source Map。Source Map 是一种映射,将编译后的代码映射回原始源代码,以便在调试时可以使用原始源代码而不是编译后的代码。每种生成方式都有其优点和缺点
使用devtool
参数可以配置Source Map
的生成策略。常用的配置项有:
- source-map:使用单独的文件生成完整的
Source Map
,方便调试,适用于开发环境。 - nosources-source-map:生成单独的
Source Map
文件,但是不包含源代码,适用于生产环境。
module.exports = {
// entry、output、module、devServer等配置与之前相同,此处省略
devtool: 'source-map'
};
3. 手写基于react项目的webpack配置
到这里我们已经了解了常用配置有哪些及其作用,那么我们在配置项目的webpack
配置时,一般会包含如下配置
- mode: 用于指定模式
- entry: 用于指定入口文件
- output: 用于指定输出目录、文件配置、cdn链接
- resolve: 用于指定模块查找配置,提高开发效率
- module: 用于指定处理相应资源的loader
- plugins: 用于指定html插件、css提取等插件
- devServer: 用于指定开发环境的开发服务配置
- optimization: 用于指定chunk提取规则及压缩插件等
- cache: 用于指定缓存方式
- devtool: 用于指定生成source-map方式
当我们使用上面的配置,在结合一些框架专用的loader
或者plugin
,那么就可以写出满足项目的webpack
配置
下面是满足react项目的通用webpack
配置,分为三份方便阅读与维护
- 公共配置
- 开发环境配置
- 生产环境配置
3.1 公共配置
包含生产环境与开发环境公用的webpack
配置
const path = require('path');
const { ProgressPlugin, DefinePlugin, ProvidePlugin } = require('webpack')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const FriendlyErrorsWebpackPlugin = require('@soda/friendly-errors-webpack-plugin')
const resolveDir = (dir) => path.join(__dirname, `../${dir}`)
const isDevelopment = process.env.NODE_ENV === 'development'
const cssLoader = {
loader: 'css-loader',
options: {
importLoaders: 1,
sourceMap: true,
modules: {
auto: true
}
}
}
const postcssLoader = {
loader: 'postcss-loader',
options: {
sourceMap: true,
postcssOptions: {
plugins: [require('autoprefixer')],
}
}
}
module.exports = {
resolve: {
symlinks: true,
alias: { // 设置别名
process: 'process/browser',
'@': resolveDir('src'),
},
extensions: [
'.tsx',
'.ts',
'.jsx',
'.js'
],
mainFields: [
'browser',
'main:h5',
'module',
'main'
],
},
module: {
rules: [
// 处理css最终生效的方式
{
test: /\.(css|less|s[a|c]ss)(\?.*)?$/,
use: [
{
loader: isDevelopment ? 'style-loader' : MiniCssExtractPlugin.loader
}
]
},
// 处理css
{
test: /\.css$/,
use: [
cssLoader,
postcssLoader
]
},
// 处理less
{
test: /\.less$/,
use: [
cssLoader,
postcssLoader,
{
loader: 'less-loader',
options: {
lessOptions: {
javascriptEnabled: true
}
}
}
]
},
// 处理sass
{
test: /\.s[a|c]ss$/,
use: [
cssLoader,
postcssLoader,
{
loader: 'sass-loader'
}
]
},
// 处理ts、js、tsx、jsx
{
test: /\.[tj]sx?$/i,
exclude: [
/(node_modules|bower_components)/
],
use: [
{
loader: 'babel-loader',
options: {
presets: [
[
'@babel/preset-env',
{
debug: false,
useBuiltIns: 'usage', // https://babeljs.io/docs/en/babel-preset-env
corejs: 3,
},
],
['@babel/preset-react'],
['@babel/preset-typescript'],
],
plugins: [
isDevelopment && [
require.resolve('react-refresh/babel'),
{
skipEnvCheck: true
}
],
[
'@babel/plugin-transform-runtime',
{
corejs: false,
helpers: true,
regenerator: true,
},
],
].filter(Boolean),
}
}
]
},
// 处理图片
{
test: /\.(png|jpe?g|gif|bpm|svg|webp)(\?.*)?$/,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 10240
}
},
generator: {
filename: 'image/[name].[hash][ext]'
}
},
// 处理字体
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 10240
}
},
generator: {
filename: 'static/fonts/[name].[hash][ext]'
}
},
// 处理视频
{
test: /\.(mp4|webm|ogg|mp3|m4a|wav|flac|aac)(\?.*)?$/,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 10240
}
},
generator: {
filename: 'static/media/[name].[hash][ext]'
}
},
]
},
plugins: [
new ProgressPlugin(
{
percentBy: 'entries',
profile: false
}
),
new FriendlyErrorsWebpackPlugin(
{}
),
new DefinePlugin(
{
FOO: process.env.FOO
}
),
new ProvidePlugin(
{
process: 'process/browser'
}
),
],
entry: {
app: [
resolveDir('src/app')
]
},
stats: {
hash: true,
}
}
3.2 开发环境配置
仅包含开发环境使用的webpack
配置
const path = require('path')
const { merge } = require('webpack-merge')
const { WatchIgnorePlugin } = require('webpack')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const ReactRefreshPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
const baseConfig = require('./webpack.base.config')
const resolveDir = (dir) => path.join(__dirname, `../${dir}`)
const publicPath = '/'
module.exports = merge(baseConfig, {
mode: 'development',
devtool: 'source-map', // 设置生成source-map方式
cache: { // 设置缓存方式
type: 'filesystem',
name: 'dev-cache',
version: 'development',
},
output: { // 设置输出
path: resolveDir('dist'),
filename: 'js/[name].js',
chunkFilename: 'chunk/[name].js',
publicPath
},
devServer: { // 设置开发服务配置
static: {
directory: path.join(__dirname, 'public'),
publicPath
},
compress: true,
port: 9000,
host: 'localhost',
hot: true,
https: false,
open: ['/react-webpack-config-demo/'],
historyApiFallback: {
rewrites: [{
from: /./,
to: publicPath,
}],
},
},
plugins: [
new WatchIgnorePlugin(
{
paths: [
/(css|less|s[a|c]ss)\.d\.ts$/
]
}
),
new HtmlWebpackPlugin(
{
filename: 'index.html',
template: resolveDir('public/index.html'),
}
),
// react热更新插件
new ReactRefreshPlugin(
{
overlay: false,
exclude: /node_modules/i,
include: /\.([cm]js|[jt]sx?|flow)$/i
}
),
],
})
3.3 生产环境配置
仅包含生产环境使用的webpack
配置
const path = require('path')
const { merge } = require('webpack-merge')
const TerserPlugin = require('terser-webpack-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const CopyPlugin = require('copy-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const baseConfig = require('./webpack.base.config')
const resolveDir = (dir) => path.join(__dirname, `../${dir}`)
const config = merge(baseConfig, {
mode: 'production',
devtool: 'nosources-source-map',
cache: {
type: 'filesystem',
name: 'production-cache',
version: 'production',
},
output: {
path: resolveDir('dist'),
filename: 'js/[name].[chunkhash].js', // 这里需要配置chunkhash
chunkFilename: 'chunk/[name].[chunkhash].js',
publicPath: '/'
},
optimization: {
minimize: true,
splitChunks: { // chunk提取规则
chunks: 'all',
minSize: 1024 * 100,
minChunks: 1,
maxSize: 307200,
maxAsyncRequests: 30,
maxInitialRequests: 30,
automaticNameDelimiter: '~',
cacheGroups: { // 设置chunk提取方式
'default': false,
vendors: {
test: /[\\\/]node_modules[\\\/]/,
priority: -10,
reuseExistingChunk: true,
name: 'vendors'
},
common: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true,
name: 'common'
}
}
},
runtimeChunk: {
name: 'runtime'
},
emitOnErrors: true,
minimizer: [
// js压缩插件
new TerserPlugin(
{
parallel: true
}
),
// css压缩插件
new CssMinimizerPlugin(
{
parallel: true
}
)
]
},
plugins: [
// css提取插件
new MiniCssExtractPlugin(
{
filename: 'css/[name].[chunkhash].css',
chunkFilename: 'css/[name].[chunkhash].css'
}
),
// 清除上一次构建产物
new CleanWebpackPlugin(
{
dry: false
}
),
new CopyPlugin(
{
patterns: [
{
from: resolveDir('public'), // 将public目录下的内容copy到dist
to: resolveDir('dist'),
globOptions: {
ignore: [
'**/*.html'
]
},
noErrorOnMissing: true
}
]
}
),
new HtmlWebpackPlugin(
{
filename: 'index.html',
template: resolveDir('public/index.html'),
}
),
],
});
module.exports = config
4. 手写基于vue3的webpack配置
下面是满足vue3
项目的通用webpack
配置,分为三份方便阅读与维护,支持单文件vue
,支持tsx
、jsx
等
- 公共配置
- 开发环境配置
- 生产环境配置
整体配置与react
项目大体一致,主要区分点
vue
项目相对react
项目多了vue-loader
及对应的plugin
vue
项目相对react
项目在css
处理的部分,多了oneOf
用来处理单文件.vue
中style module
babel
配置有点差异
4.1 公共配置
const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const { VueLoaderPlugin } = require('vue-loader')
const { DefinePlugin } = require('webpack')
const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin')
const FriendlyErrorsWebpackPlugin = require('@soda/friendly-errors-webpack-plugin')
const resolveDir = (dir) => path.join(__dirname, `../${dir}`)
const cssAutoLoader = {
loader: 'css-loader',
options: {
sourceMap: false,
importLoaders: 2,
modules: {
localIdentName: '[name]_[local]_[hash:base64:8]',
auto: true // 由文件名名中是否包含.module字段来判断是否是css module
}
}
}
const cssModuleLoader = {
loader: 'css-loader',
options: {
sourceMap: false,
importLoaders: 2,
modules: {
localIdentName: '[name]_[local]_[hash:base64:8]',
auto: () => true // 一定是css module
}
}
}
const postcssLoader = {
loader: 'postcss-loader',
options: {
sourceMap: false,
postcssOptions: {
plugins: [
'autoprefixer'
]
}
}
}
const styleLoader = process.env.NODE_ENV === 'development' ? {
loader: 'vue-style-loader',
options: {
sourceMap: false,
shadowMode: false
}
} : {
loader: MiniCssExtractPlugin.loader,
}
const sassLoader = {
loader: 'sass-loader',
options: {
sourceMap: false,
sassOptions: {
indentedSyntax: true
}
}
}
const lessLoader = {
loader: 'less-loader',
options: {
sourceMap: false
}
}
module.exports = {
entry: {
app: [
'./src/main.ts'
]
},
resolve: {
alias: {
'@': resolveDir('src'),
vue$: 'vue/dist/vue.runtime.esm-bundler.js'
},
extensions: [
'.tsx',
'.ts',
'.mjs',
'.js',
'.jsx',
'.vue',
'.json',
'.wasm'
]
},
module: {
// vue、vue-router这些库跳过解析
noParse: /^(vue|vue-router|vuex|vuex-router-sync)$/,
rules: [
// 避免导入模块缺少拓展名或者目录时,webpack主动抛出错误
{
test: /\.m?jsx?$/,
resolve: {
fullySpecified: false
}
},
// 处理.vue单文件
{
test: /\.vue$/,
use: [
{
loader: 'vue-loader',
options: {
babelParserPlugins: [
'jsx',
'classProperties',
'decorators-legacy'
]
}
}
]
},
// 表示.vue中的style那部分内容时无副作用的
{
test: /\.vue$/,
resourceQuery: /type=style/,
sideEffects: true
},
// 处理svg
{
test: /\.(svg)(\?.*)?$/,
type: 'asset/resource',
generator: {
filename: 'img/[name].[hash:8][ext]'
}
},
// 处理png等图片
{
test: /\.(png|jpe?g|gif|webp|avif)(\?.*)?$/,
type: 'asset',
generator: {
filename: 'img/[name].[hash:8][ext]'
}
},
// 处理mp4等视频资源
{
test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
type: 'asset',
generator: {
filename: 'media/[name].[hash:8][ext]'
}
},
// 处理字体文件
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/i,
type: 'asset',
generator: {
filename: 'fonts/[name].[hash:8][ext]'
}
},
// 处理css
{
test: /\.css$/,
oneOf: [
{
resourceQuery: /module/,
use: [
styleLoader,
cssModuleLoader,
postcssLoader
]
},
{
use: [
styleLoader,
cssAutoLoader,
postcssLoader
]
}
]
},
// 处理sass
{
test: /\.sass$/,
oneOf: [
{
resourceQuery: /module/,
use: [
styleLoader,
cssModuleLoader,
postcssLoader,
sassLoader
]
},
{
use: [
styleLoader,
cssAutoLoader,
postcssLoader,
sassLoader
]
}
]
},
// 处理less
{
test: /\.less$/,
oneOf: [
{
resourceQuery: /module/,
use: [
styleLoader,
cssModuleLoader,
postcssLoader,
lessLoader
]
},
{
use: [
styleLoader,
cssAutoLoader,
postcssLoader,
lessLoader
]
}
]
},
// 处理js、ts、jsx、tsx
{
test: /\.m?(js|jsx|ts|tsx)$/,
exclude: function (filepath) {
const SHOULD_SKIP = true
const SHOULD_TRANSPILE = false
if (!filepath) {
return SHOULD_SKIP
}
// Always transpile js in vue files
if (/\.vue\.jsx?$/.test(filepath)) {
return SHOULD_TRANSPILE
}
return /node_modules/.test(filepath) ? SHOULD_SKIP : SHOULD_TRANSPILE
},
use: [
{
loader: 'babel-loader',
}
]
},
],
},
plugins: [
new VueLoaderPlugin(),
new DefinePlugin(
{
__VUE_OPTIONS_API__: true, // vue3 开启 options api
__VUE_PROD_DEVTOOLS__: false, // vue3 在生产环境中禁用 devtools 支持
'process.env': {
NODE_ENV: JSON.stringify(process.env.NODE_ENV),
BASE_URL: JSON.stringify(process.env.BASE_URL || '/'),
}
}
),
new CaseSensitivePathsPlugin(),
new FriendlyErrorsWebpackPlugin(),
]
}
4.2 开发环境配置
const path = require('path');
const { merge } = require('webpack-merge')
const HtmlWebpackPlugin = require('html-webpack-plugin');
const baseConfig = require('./webpack.base.config')
const resolveDir = (dir) => path.join(__dirname, `../${dir}`)
module.exports = merge(baseConfig, {
mode: 'development',
devtool: 'source-map', // 指定生产source-map的方式
output: {
path: resolveDir('dist'),
filename: 'js/[name].js', // 使用name即可,无需使用chunkhash等
publicPath: '/',
chunkFilename: 'js/[name].js' // 使用name即可,无需使用chunkhash等
},
cache: {
type: 'filesystem', // 开发环境也可以使用内存模式
name: 'dev-cache',
version: process.env.NODE_ENV,
},
devServer: {
static: { // 保证public下的文件能过支持访问
directory: path.join(__dirname, 'public'),
},
compress: true,
port: 9000,
historyApiFallback: true, // 保证刷新浏览器的时候前端路由能够正常命中不会出现404
},
plugins: [
new HtmlWebpackPlugin(
{
title: 'webpack-vue3-demo',
scriptLoading: 'defer',
template: resolveDir('public/index.html') // 指定入口html文件
}
),
]
})
4.3 生产环境配置
const path = require('path');
const { merge } = require('webpack-merge')
const TerserPlugin = require('terser-webpack-plugin')
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const CopyPlugin = require('copy-webpack-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const baseConfig = require('./webpack.base.config')
const resolveDir = (dir) => path.join(__dirname, `../${dir}`)
module.exports = merge(baseConfig, {
mode: 'production',
devtool: 'nosources-source-map',
output: {
path: resolveDir('dist'),
filename: 'js/[name].[chunkhash:8].js', // 使用chunkhash即可
publicPath: '/',
chunkFilename: 'js/[name].[chunkhash:8].js' // 使用chunkhash即可
},
cache: {
type: 'filesystem', // 推荐文件缓存,方便CI场景复用缓存
name: 'prod-cache',
version: process.env.NODE_ENV,
},
optimization: {
splitChunks: { // 提取两个chunk,一个是chunk-vendors、一个是chunk-common,只针对initial chunk进行提取
cacheGroups: {
defaultVendors: {
name: 'chunk-vendors',
test: /[\\/]node_modules[\\/]/,
priority: -10,
chunks: 'initial'
},
common: {
name: 'chunk-common',
minChunks: 2,
priority: -20,
chunks: 'initial',
reuseExistingChunk: true
}
}
},
minimizer: [
// 设置js压缩插件
new TerserPlugin(
{
parallel: true,
extractComments: false
}
),
// 设置css压缩插件
new CssMinimizerPlugin(
{
parallel: true,
minimizerOptions: {
preset: [
'default'
]
}
}
)
]
},
plugins: [
// 清除上传构建产物
new CleanWebpackPlugin(
{
dry: false
}
),
// 提取css插件
new MiniCssExtractPlugin(
{
filename: 'css/[name].[contenthash:8].css', // 注意这里一定要使用contenthash,不然无法保证最好的缓存效果
chunkFilename: 'css/[name].[contenthash:8].css' // 注意这里一定要使用contenthash,不然无法保证最好的缓存效果
}
),
new HtmlWebpackPlugin(
{
title: 'webpack-vue3-demo',
scriptLoading: 'defer',
template: resolveDir('public/index.html')
}
),
new CopyPlugin(
{
patterns: [
{
from: resolveDir('public'), // 将public目录下的内容copy到dist目录
to: resolveDir('dist'),
toType: 'dir',
noErrorOnMissing: true,
globOptions: {
ignore: [
'**/.DS_Store',
resolveDir('public/index.html')
]
}
}
]
}
),
],
})
5. 总结
本文介绍了webpack
的基本使用方法,并提供了一份简单易懂的webpack
配置指南。同时,还介绍了webpack
的常用配置参数及其作用,能打造自己的webpack
配置;
参考链接