关于本文
本文通过webpack5的基础配置手动搭建了一个基础React脚手架,读完本文你将会对webpack的一些基础配置有所了解,必要时请结合官方文档进行解读。 概念 | webpack 中文文档 | webpack 中文文档 | webpack 中文网 (webpackjs.com)
概念
webpack 是一个用于现代 JavaScript 应用程序的 静态模块打包工具 。当 webpack 处理应用程序时,它会在内部从一个或多个入口点构建一个 依赖图(dependency graph),然后将你项目中所需的每一个模块组合成一个或多个 bundles,它们均为静态资源,用于展示你的内容
webpack 仅能编译 JS 中的 ES Module
语法,生产模式还能压缩 JS 和HTML代码;其本身的能力非常有限,这就不得不借助预置器,插件来扩展其能力。
初始化
js
// 创建文件夹:命令创建或手动创建
// 命令创建:
mkdir about-react
cd about-react
// 加了-y能跳过手动确认配置,若需要自定义的参数,生成pack.json后可修改
npm初始化:npm init -y
执行完以上命令,就会生成一个pack.json文件 以下配置的项目结构均以这个为例
开发环境配置
在项目根目录创建文件config/webpack.dev.js
基本配置
webpack五大核心基本配置:entry、output、loader(module.rules)、plugins、mode
js
module.exports = {
// 入口
entry: "",
// 输出
output: {},
// 加载器
module: {
rules: [],
},
// 插件
plugins: [],
// 模式
mode: "development", // 生产模式:production
};
配置webpack进行编译打包的入口entry
js
// src文件夹是项目源码,config文件夹和src文件夹同级,示例以main.js作为入口文件,可根据自身项目做调整
// 相对路径写法
entry: "../src/main.js",
// 绝对路径写法
entry:path.resolve(__dirname,'../src/main.js'),
配置output
js
output:{
path:path.resolve(__dirname,'../dist'),
// path:undefined, // 或开发环境不配置打包后的输出路径
filename:static/js/main.js, // 这里的地址是相对于path路径来配置
clean: true, // 清空上次打包资源
},
配置loader
Webpack 支持使用 loader 对文件进行预处理。你可以构建包括 JavaScript 在内的任何静态资源
接下来引用大致分三小点:处理样式loader、处理图片loder、处理其他资源loader
处理样式
现在基本是使用预处理器,这里以sass为例
js
// 先下载 npm i -D style-loader css-loader sass-loader sass postcss-loader postcss postcss-preset-env
module: {
rules: [
{
test: /.s[ac]ss$/, // 匹配.s[ac]ss 结尾的文件
use: ["style-loader", "css-loader", // use 数组里面Loader执行顺序是从右到左,顺序不能写错
{
loader: "postcss-loader",
options: {
postcssOptions: {
plugins: [
"postcss-preset-env", // 能解决大多数样式兼容性问题
],
},
},
},
"sass-loader"]
}
]
}
- sass-loader:加载 Sass/SCSS 文件并将他们编译为 CSS
- postcss-loader:使用众多插件来处理css,为css属性达到更高的兼容性
- css-loader:对
@import
和url()
进行处理,就像 js 解析import/require()
一样- style-loader:在html文件中生成style标签,并把最终生成的css属性添加到style标签中
兼容样式还需要在package.json文件中配置browerslist,详情见browserslist/browserslist: 🦔 Share target browsers between different front-end tools, like Autoprefixer, Stylelint and babel-preset-env (github.com)
js
"browerslist": [
"last 2 version", // 匹配最后两个版本
"> 1%", // 全球使用占比大于1%
"not dead" // 未停用的
]
处理js资源
js
// 先下载 npm i -D babel-loader @babel/core @babel/preset-env @babel/preset-react @babel/preset-typescript @babel/plugin-transform-runtime
{
test: /.(jsx|js)$/,
include: path.resolve(__dirname, "../src"), // 只匹配编写源码
loader: "babel-loader",
options: {
cacheDirectory: true, // 开启babel编译缓存
cacheCompression: false, // 缓存文件不压缩
plugins: [
"react-refresh/babel", // 开启js的HMR功能
],
/** presets: [ 这里的配置可以放在babel.config.js文件中
"@babel/preset-env"
"@babel/preset-react",
"@babel/preset-typescript"
],
** /
},
},
js
// babel.config.js
module.exports = {
// 先处理ts,再处理jsx,最后babel转换为低版本语法
presets: [
[
"@babel/preset-env",
// 按需加载core-js的polyfill
{ useBuiltIns: "usage", corejs: { version: "3", proposals: true } },
],
"@babel/preset-react",
"@babel/preset-typescript"
],
};
处理图片
Webpack4时,处理图片资源通过 file-loader
和 url-loader
进行处理 而Webpack5 已经将这两个 Loader 功能内置了,只需要配置asset资源处理即可,详情见资源模块 | webpack 中文文档 | webpack 中文文档 | webpack 中文网 (webpackjs.com)
js
module: {
rules: [
{
test: /.(png|jpe?g|gif|webp)$/,
type: "asset",
parser: {
dataUrlCondition: {
maxSize: 8 * 1024 // 小于8kb的图片会被base64处理
}
},
}
]
}
处理其他资源
以引入阿里巴巴字体图标为例,正确引入iconfont并使用字体图标后
js
module: {
rules: [
{
// |map4|map3|avi,一般用不到,这些资源一般是放在资源服务器上
test: /.(ttf|woff2?|map4|map3|avi)$/,
type: "asset/resource", // 直接导出资源,不做处理
generator: {
// 将图片文件输出到 static/images 目录中
// [contenthash:10]: hash值取10位
// [ext]: 使用之前的文件扩展名
// [query]: 添加之前的query参数
filename: "static/media/[contenthash:10][ext][query]",
}
}
]
}
资源模块类型(asset module type),通过添加 4 种新的模块类型,来替换所有这些 loader:
asset/resource
发送一个单独的文件并导出 URL。之前通过使用file-loader
实现。asset/inline
导出一个资源的 data URI。之前通过使用url-loader
实现。asset/source
导出资源的源代码。之前通过使用raw-loader
实现。asset
在导出一个 data URI 和发送一个单独的文件之间自动选择。之前通过使用url-loader
,并且配置资源体积限制实现。
配置plugins
plugins可以扩展webpack的能力,我们可以通过自定义配置plugin来实现更符合自己需求的脚手架
eslint-webpack-plugin
- 先下载 npm i eslint-webpack-plugin eslint -D
- 定义 Eslint 配置文件。详情查看官网配置文件(新) - ESLint 中文网 (nodejs.cn)
js
// .eslintrc.js
module.exports = {
extends: ["react-app"], // 继承 react 官方规则
// 若想覆盖掉react-app的规则,直接配置即可
rules: {
eqeqeq: ["warn", "smart"],
},
// 预设配置
parserOptions: {
babelOptions: {
presets: [
// 解决页面报错问题,支持import动态导入写法
["babel-preset-react-app", false],
"babel-preset-react-app/prod",
],
},
},
};
js
// webpack.config.js
const ESLintWebpackPlugin = require("eslint-webpack-plugin");
plugins: [
new ESLintWebpackPlugin({
context: path.resolve(__dirname, "src"), // 指定检查文件的根目录
exclude: "node_modules", // 默认值
cache: true, // 开启eslint检查缓存
cacheLocation: path.resolve( __dirname,"../node_modules/.cache/.eslintcache"), // 缓存目录
}),
],
处理html资源
js
// 先下载:npm i html-webpack-plugin -D
const HtmlWebpackPlugin = require("html-webpack-plugin");
plugins: [
new HtmlWebpackPlugin({
// 以 public/index.html 为模板创建文件
// 打包后的html=原html+自动引入打包生成的js等资源
template: path.resolve(__dirname, "public/index.html"),
}),
]
提取css成单独文件并压缩css
提一嘴,生产模式默认开启html 压缩和 js 压缩,所以不需要额外配置
js
// 先下载:npm i mini-css-extract-plugin css-minimizer-webpack-plugin -D
const MiniCssExtractPlugin = require("mini-css-extract-plugin"); // 提取css成单独文件
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin"); // 压缩css
/**由于前面我们使用了style-loader处理样式,
这个loader会直接在html文件中添加style标签并一股脑把处理好的css添加到style标签中
所以我们要提取css成单独的文件需要用 MiniCssExtractPlugin.loader代替style-loader**/
module: {
rules: [
{
test: /.s[ac]ss$/, // 匹配 .s[ac]ss 结尾的文件
use: [MiniCssExtractPlugin.loader, "css-loader", "sass-loader"] // use 数组里面 Loader 执行顺序是从右到左
}
]
}
plugins: [
new MiniCssExtractPlugin({
filename: "static/css/[name].css", // 定义输出文件名和目录
chunkFilename: "static/css/[name].chunk.css",
}),
new CssMinimizerPlugin(),
]
配置开发服务器
js
// 先下载 npm i webpack-dev-server -D
// 开发服务器
devServer: {
host: "localhost", // 启动服务器域名
port: "3000", // 启动服务器端口号
open: true, // 是否自动打开浏览器
hot:true
},
一些优化点
1.SourceMap(源代码映射)
js
{
devtool:"cheap-module-source-map" // 开启打包后的语句和行源码的映射,有利于定位问题
}
2.将静态内容直接复制到打包后的文件夹中
比如复制public文件夹下的图标文件等
js
const CopyPlugin = require("copy-webpack-plugin");
new CopyPlugin({
patterns: [
{
from: path.resolve(__dirname, "../public"),
to: path.resolve(__dirname, "../dist"),
toType: "dir",
noErrorOnMissing: true, // 不生成错误
globOptions: {
// 忽略文件,html-webpack-plugin已经复制过这个文件了,所以在这里要忽略
ignore: ["**/index.html"],
}
},
],
}),
3.code split 按需导入并提取重复代码
js
// 代码分割配置
splitChunks: {
chunks: "all", // 对所有模块都进行分割,其他内容用默认配置即可
},
4.利用contentHash根据内容缓存文件
- chunkhash:根据不同的入口文件(Entry)进行依赖文件解析、构建对应的 chunk,生成对应的哈希值。若A 和 B是同一个引入,会共享一个 hash 值。
- hash:根据文件内容生成 hash 值,只有文件内容变化了,hash 值才会变化。所有文件 hash 值是独享且不同的
js
output:{
filename: "static/js/[name].[contenthash:10].js", // 入口文件打包输出资源命名方式
chunkFilename: "static/js/[name].[contenthash:10].chunk.js", // 动态导入输出资源命名方式
},
plugins:[
new MiniCssExtractPlugin({
filename: "static/css/[name].[contenthash:10].css",
chunkFilename: "static/css/[name].[contenthash:10].chunk.css",
}),
]只只
5.将contentHash值单独保管在一个 runtime 文件中
第4点配置缓存后,还会存在一个问题:
假如A引用了B,当B的内容发生变化,其contentHash变化,缓存失效,webpack会重新生成新的文件替代ChunkB文件。但是由于A引用了B,A的contentHash也会变,那么A也需要重新进行各种检测编译压缩打包流程。如果项目中所有的修改都是这个样子,缓存将失去实际意义,这不是我们想要的效果。
想要解决也很简单,生成一个runtime文件,runtime文件只保存文件的contentHash值和对应文件的关系,当B变化时,只需要重新编译打包B和runtime文件即可。runtime文件体积就比较小,所以变化重新请求的开销很小
只需要简单配置,如下:
js
// 提取runtime文件
runtimeChunk: {
name: (entrypoint) => `runtime~${entrypoint.name}`, // runtime文件命名规则
},
生产环境配置
生产环境的配置基本可以复制开发环境的,不同之处有几点
- js压缩:terser-webpack-plugin
- 把压缩的配置放在optimization中
- mode改成生产模式的
- sourceMap换成source-map模式
- 添加输出路径
- 整体包压缩compression-webpack-plugin
- splitChunks分割出react react-dom react-router-dom 一起打包成一个js文件
js
output: {
filename: "statics/js/[name].[contenthash:10].js",
assetModuleFilename: "statics/assets/[hash][ext][query]",
path: path.join(__dirname, "../dist"),
clean: true,
},
optimization: {
// 压缩的操作
minimizer: [
new CssMinimizerPlugin(),
new TerserWebpackPlugin(),
new CompressionPlugin()
],
splitChunks: {
chunks: "all",
cacheGroups: {
// react react-dom react-router-dom 打包成一个js文件
react: {
test: /[\\/]node_modules[\\/]react(.*)?[\\/]/,
name: "chunk-react",
priority: 40,
},
// 剩下node_modules单独打包
libs: {
test: /[\\/]node_modules[\\/]/,
name: "chunk-libs",
priority: 20,
},
},
},
mode: "production",
devtool: "source-map",
提取开发环境和生产环境共有配置,并合并
生产和开发有相同的配置,我们可以提取出共用部分,使用webpack-merge给不同环境配置 那么最终会有一个webpack.common.js文件
js
// webpack.common.js
const path = require("path");
const ESLintWebpackPlugin = require("eslint-webpack-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const CopyPlugin = require("copy-webpack-plugin");
const isProduction = process.env.NODE_ENV === "production";
module.exports = {
entry:path.resolve(__dirname,'../src/main.js'),
module: {
rules: [
{
oneOf: [
{
test: /.s[ac]ss$/,
use: {
isProduction ? MiniCssExtractPlugin.loader : "style-loader",
"css-loader",
{
loader: "postcss-loader",
options: {
postcssOptions: {
plugins: [
"postcss-preset-env",
],
},
},
},
"sass-loader",
},
{
test: /.(png|jpe?g|gif|svg)$/,
type: "asset",
parser: {
dataUrlCondition: {
maxSize: 10 * 1024,
},
},
},
{
test: /.(ttf|woff2?)$/,
type: "asset/resource",
},
],
},
],
},
plugins: [
new ESLintWebpackPlugin({
context: path.resolve(__dirname, "../src"),
exclude: "node_modules",
cache: true,
cacheLocation: path.resolve( __dirname, "../node_modules/.cache/.eslintcache"),
}),
new HtmlWebpackPlugin({
template: path.resolve(__dirname, "../public/index.html"),
}),
new CopyPlugin({
patterns: [
{
from: path.resolve(__dirname, "../public"),
to: path.resolve(__dirname, "../dist"),
toType: "dir",
noErrorOnMissing: true,
globOptions: {
ignore: ["**/index.html"],
}
},
],
}),
],
optimization: {
splitChunks: {
chunks: "all",
},
runtimeChunk: {
name: (entrypoint) => `runtime~${entrypoint.name}`,
},
},
resolve: {
extensions: [".jsx", ".js", ".json"], // 自动补全文件扩展名,让jsx可以使用
}
};
webpack.dev.js
js
const path = require("path");
const { merge } = require("webpack-merge");
const common = require("./webpack.common");
module.exports = merge(common, {
mode: "development",
devtool: "cheap-module-source-map",
output: {
path: undefined,
filename: "static/js/[name].js",
chunkFilename: "static/js/[name].chunk.js",
assetModuleFilename: "static/js/[contenthash:10][ext][query]",
},
module:{
rules:[
{
test: /.(jsx|js)$/,
include: path.resolve(__dirname, "../src"),
loader: "babel-loader",
options: {
cacheDirectory: true,
cacheCompression: false,
plugins: [
"react-refresh/babel", // 开启js的HMR功能
],
},
},
]
},
devServer: {
static: {
directory: path.join(__dirname, "../dist"),
},
compress: true,
port: 3000,
hot: true,
open: true,
historyApiFallback: true,
},
});
webpack.prod.js
js
const path = require("path");
const { merge } = require("webpack-merge");
const common = require("./webpack.common");
const TerserWebpackPlugin = require("terser-webpack-plugin");
const CompressionPlugin = require("compression-webpack-plugin");
const CssMinimizerWebpackPlugin = require("css-minimizer-webpack-plugin");
module.exports = merge(common, {
mode: "production",
output: {
path: path.join(__dirname, "../dist"),
filename: "static/js/[name].[contenthash:10].js", // 入口文件打包输出资源命名方式
chunkFilename: "static/js/[name].[contenthash:10].chunk.js", // 动态导入输出资源命名方式
clean: true,
},
module:{
rules:[
{
test: /.(jsx|js)$/,
include: path.resolve(__dirname, "../src"),
loader: "babel-loader",
options: {
cacheDirectory: true,
cacheCompression: false,
},
},
]
},
optimization:{
splitChunks: {
chunks: "all",
cacheGroups: {
// react react-dom react-router-dom 打包成一个js文件
react: {
test: /[\\/]node_modules[\\/]react(.*)?[\\/]/,
name: "chunk-react",
priority: 40,
},
// 剩下node_modules单独打包
libs: {
test: /[\\/]node_modules[\\/]/,
name: "chunk-libs",
priority: 20,
},
},
},
minimizer:[
new CssMinimizerWebpackPlugin(),
new TerserWebpackPlugin(),
new CompressionPlugin()
],
},
devtool:"source-map",
performance: false, // 关闭性能分析,提升打包速度
});
package.json配置运行指令
js
"scripts": {
"start": "npm run dev",
"dev": "cross-env NODE_ENV=development webpack serve --config ./config/webpack.dev.js",
"build": "cross-env NODE_ENV=production webpack --config ./config/webpack.prod.js"
},
.eslintrc.js配置
js
module.exports = {
extends: ["react-app"],
parserOptions: {
babelOptions: {
presets: [
["babel-preset-react-app", false],
"babel-preset-react-app/prod",
],
},
},
};
babel.config.js配置
js
module.exports = {
// 使用react官方规则
presets: ["react-app"],
};
总结
开发配置
- 从五大主要因素开始,配置了入口文件
- 配置了输出文件的路径
- 样式处理:我们通过style-loader、css-loader、sass-loader、postcss-loader、postcss-preset-env等loader和插件处理
- 我们通过webpack内置的资源处理,配置asset处理图片资源
- 通过babel-loader,还有"@babel/preset-env" "@babel/preset-react", "@babel/preset-typescript"预制处理js
- 通过eslint-webpack-plugin规范我们的代码
- 通过html-webpack-plugin将源代码的html复制到指定文件夹下 -最后配置开发服务器
接着还进行了一些优化
- 源代码映射
- 将静态内容直接复制到打包后的文件夹中
- code split 按需导入并提取重复代码
- 利用contentHash根据内容缓存文件
- 将contentHash值单独保管在一个 runtime 文件中
合并
- 最后我们抽取出开发和生产的共同配置并应用webpack-merge将他们合并
- 提取后,配置文件需要获取通过cross-env获取环境变量
- 配置package.json将对应的环境变量传给webpack配置文件内部,通过process.env.NODE_ENV获取