一篇教你学会如何用Webpack从零搭建一个React脚手架
前言
前面已经学习了一些webpack的基本配置,然后下面用学过的一些配置来搭建一个React脚手架!
开发模式下webpack配置
// webpack.dev.js
const path = require('path')
const ESLintWebpackPlugin = require('eslint-webpack-plugin');
const HtmlWebpackPlugin = require("html-webpack-plugin");
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
// 获取处理样式的Loaders
const getStyleLoaders = (preProcessor) => {
return [
"style-loader",
"css-loader",
{
loader: "postcss-loader",
options: {
postcssOptions: {
plugins: [
"postcss-preset-env", // 能解决大多数样式兼容性问题
],
},
},
},
preProcessor,
].filter(Boolean);
};
module.exports = {
entry: './src/main.js',
output: {
path: undefined, //开发环境不需要指定输出目录
filename: 'static/js/[name].js', // 输出的文件名
chunkFilename: 'static/js/[name].chunk.js', // 动态导入的文件打包输出的文件名
assetModuleFilename: 'static/media/[name][hash:10][ext][query]', // 图片等公共资源打包后的名字
},
module: {
rules: [
/**
* 样式资源处理
*/
{
test: /\.css$/,
use: getStyleLoaders()
},
{
test: /\.less$/,
use: getStyleLoaders("less-loader"),
},
{
test: /\.s[ac]ss$/,
use: getStyleLoaders("sass-loader"),
},
{
test: /\.styl$/,
use: getStyleLoaders("stylus-loader"),
},
/**
* 图片资源处理
*/
{
test: /\.(png|jpe?g|gif|svg)$/,
type: "asset",
parser: {
dataUrlCondition: {
maxSize: 10 * 1024, // 小于10kb的图片会被base64处理
},
},
},
/**
* 字体图标处理
*/
{
test: /\.(ttf|woff2?)$/,
type: "asset/resource", // 原封不动输出
},
/**
* js资源处理
*/
{
test: /\.jsx?$/,
include: path.resolve(__dirname, "../src"),
loader: 'babel-loader',
options: {
cacheDirectory: true, // 开启babel编译缓存
cacheCompression: false, // 缓存文件不要压缩
plugins: [
'react-refresh/babel' // js热替换HMR
]
}
}
]
},
plugins: [
new ESLintWebpackPlugin({
context: path.resolve(__dirname, "../src"),// 要处理的文件范围
exclude: "node_modules",
cache: true,
cacheLocation: path.resolve(__dirname, "../node_modules/.cache/.eslintcache"), // 缓存目录
}),
// 处理html资源
new HtmlWebpackPlugin({
// 以 public/index.html 为模板创建文件
// 新的html文件有两个特点:1. 内容和源文件一致 2. 自动引入打包生成的js等资源
template: path.resolve(__dirname, "../public/index.html"),
}),
new ReactRefreshWebpackPlugin()
],
optimization: {
splitChunks: {
chunks: "all", // 代码分割
},
runtimeChunk: {
name: (entrypoint) => `runtime~${entrypoint.name}`,
},
},
mode: "development",
devtool: "cheap-module-source-map",
// webpack解析模块加载选项
resolve: {
// 自动补全扩展名
extensions: [".jsx", ".js", ".json"]
},
// 自动化配置
devServer: {
open: true, // 是否自动打开浏览器
host: "localhost",
port: 4000,
hot: true, // 热模块替换
historyApiFallback: true, // 解决react-router刷新404问题
},
}
// package.json
{
"name": "react-deom",
"version": "1.0.0",
"description": "",
"main": "main.js",
"scripts": {
"start": "npm run dev",
"dev": "cross-env NODE_ENV=development webpack serve --config ./config/webpack.dev.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.20.5",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.10",
"babel-loader": "^9.1.0",
"babel-preset-react-app": "^10.0.1",
"cross-env": "^7.0.3",
"css-loader": "^6.7.2",
"eslint": "^8.29.0",
"eslint-config-react-app": "^7.0.1",
"eslint-webpack-plugin": "^3.2.0",
"html-webpack-plugin": "^5.5.0",
"less": "^4.1.3",
"less-loader": "^7.3.0",
"postcss-loader": "^7.0.2",
"postcss-preset-env": "^7.8.3",
"react-refresh": "^0.14.0",
"sass": "^1.56.1",
"sass-loader": "^4.1.1",
"style-loader": "^3.3.1",
"stylus-loader": "^7.1.0",
"webpack": "^5.75.0",
"webpack-cli": "^5.0.1",
"webpack-dev-server": "^4.11.1"
},
"dependencies": {
"react": "^18.2.0",
"react-cli": "^0.3.1",
"react-dom": "^18.2.0",
"react-router-dom": "^6.4.4"
}
}
// eslintrc.js
module.exports = {
extends: ["react-app"], // 继承 react 官方规则
parserOptions: {
babelOptions: {
presets: [
// 解决页面报错问题
["babel-preset-react-app", false],
"babel-preset-react-app/prod",
],
},
},
};
// babel.config.js
module.exports = {
// 用来编译ES6的语法
presets: ["react-app"],
}
开发模式配置的一些问题
热替换(HMR)
在webpack从入门到原理(高级二)——HMR和oneOf(提高打包构建速度)写了当css已经通过style-loader
实现了热替换,但是js是通过module.hot.accept("");
来实现的,在实际开发中会使用其他的loader来解决这个问题,所以我们需要配置React热更新插件
安装:
npm install -D @pmmmwh/react-refresh-webpack-plugin react-refresh
// 配置
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
......
rules: [
test: /\.jsx?$/,
loader: 'babel-loader',
options: {
cacheDirectory: true, // 开启babel编译缓存
cacheCompression: false, // 缓存文件不要压缩
plugins: [
'react-refresh/babel' // js热替换HMR
]
}
]
......
plugins:[
new ReactRefreshWebpackPlugin()
]
devServer: {
hot: true, // 热模块替换
......
},
配置完成之后就可以进行js的热更新替换了,效果如下图:
前端路由刷新404
参考官方文档 当使用HTML5 History API时,如果页面返回404,启用devServer。historyApiFallback设置为true,就会页面404时返回index.html页面。
// 配置
......
devServer: {
historyApiFallback: true, // 解决react-router刷新404问题
}
生产模式下webpack配置
生产模式在开发模式基础上进行修改了如下配置:
- 打包输出目录
- 增加了contenthash来更好地做文件缓存
- 设置了clean打包时清空上一次打包的文件
- 提取样式成单独文件
- 压缩了css和js文件
- 压缩图片
- 更改mode模式和devtool
- 删除devServer配置
- 删除HMR热替换功能
- 增加了copy插件
- 配置了打包指令
// webpack.prod.js
/*
* @Author: fdhou
* @Date: 2022-12-06 14:50:19
* @LastEditors: fdhou
* @LastEditTime: 2022-12-07 15:46:08
* @Description: 开发环境webpack配置
*/
const path = require('path')
const ESLintWebpackPlugin = require('eslint-webpack-plugin');
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 ImageMinimizerPlugin = require("image-minimizer-webpack-plugin");
const CopyPlugin = require("copy-webpack-plugin");
// 获取处理样式的Loaders
const getStyleLoaders = (preProcessor) => {
return [
MiniCssExtractPlugin.loader,
"css-loader",
{
loader: "postcss-loader",
options: {
postcssOptions: {
plugins: [
"postcss-preset-env", // 能解决大多数样式兼容性问题
],
},
},
},
preProcessor,
].filter(Boolean);
};
module.exports = {
entry: './src/main.js',
output: {
path: path.resolve(__dirname, '../dist'),
filename: 'static/js/[name].[hash:10].js', // 输出的文件名
chunkFilename: 'static/js/[name].chunk.js', // 动态导入的文件打包输出的文件名
assetModuleFilename: 'static/media/[name][hash:10][ext][query]', // 图片等公共资源打包后的名字
clean: true,
},
module: {
rules: [
/**
* 样式资源处理
*/
{
test: /\.css$/,
use: getStyleLoaders()
},
{
test: /\.less$/,
use: getStyleLoaders("less-loader"),
},
{
test: /\.s[ac]ss$/,
use: getStyleLoaders("sass-loader"),
},
{
test: /\.styl$/,
use: getStyleLoaders("stylus-loader"),
},
/**
* 图片资源处理
*/
{
test: /\.(png|jpe?g|gif|svg)$/,
type: "asset",
parser: {
dataUrlCondition: {
maxSize: 10 * 1024, // 小于10kb的图片会被base64处理
},
},
},
/**
* 字体图标处理
*/
{
test: /\.(ttf|woff2?)$/,
type: "asset/resource", // 原封不动输出
},
/**
* js资源处理
*/
{
test: /\.jsx?$/,
include: path.resolve(__dirname, "../src"),
loader: 'babel-loader',
options: {
cacheDirectory: true, // 开启babel编译缓存
cacheCompression: false, // 缓存文件不要压缩
}
}
]
},
plugins: [
new ESLintWebpackPlugin({
context: path.resolve(__dirname, "../src"),// 要处理的文件范围
exclude: "node_modules",
cache: true,
cacheLocation: path.resolve(__dirname, "../node_modules/.cache/.eslintcache"), // 缓存目录
}),
// 处理html资源
new HtmlWebpackPlugin({
// 以 public/index.html 为模板创建文件
// 新的html文件有两个特点:1. 内容和源文件一致 2. 自动引入打包生成的js等资源
template: path.resolve(__dirname, "../public/index.html"),
}),
new MiniCssExtractPlugin({
filename: "static/css/[name].[hash:10].css",
chunkFilename: "static/css/[name].[hash:10].chunk.css",
}),
new CopyPlugin({
patterns: [
{
from: path.resolve(__dirname, '../public'),
to: path.resolve(__dirname, '../dist'),
globOptions: {
ignore: ["**/index.html"], // 忽略这个文件
},
},
],
}),
],
optimization: {
splitChunks: {
chunks: "all", // 代码分割
},
runtimeChunk: {
name: (entrypoint) => `runtime~${entrypoint.name}`,
},
minimizer: [
new CssMinimizerWebpackPlugin(),
new TerserWebpackPlugin(),
new ImageMinimizerPlugin({
minimizer: {
implementation: ImageMinimizerPlugin.imageminGenerate,
options: {
plugins: [
["gifsicle", { interlaced: true }],
["jpegtran", { progressive: true }],
["optipng", { optimizationLevel: 5 }],
[
"svgo",
{
plugins: [
"preset-default",
"prefixIds",
{
name: "sortAttrs",
params: {
xmlnsOrder: "alphabetical",
},
},
],
},
],
],
},
},
}),
]
},
mode: "production",
devtool: "source-map",
// webpack解析模块加载选项
resolve: {
// 自动补全扩展名
extensions: [".jsx", ".js", ".json"]
}
}
// package.json
{
"name": "react-deom",
"version": "1.0.0",
"description": "",
"main": "main.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"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.20.5",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.10",
"babel-loader": "^9.1.0",
"babel-preset-react-app": "^10.0.1",
"copy-webpack-plugin": "^11.0.0",
"cross-env": "^7.0.3",
"css-loader": "^6.7.2",
"css-minimizer-webpack-plugin": "^4.2.2",
"eslint": "^8.29.0",
"eslint-config-react-app": "^7.0.1",
"eslint-webpack-plugin": "^3.2.0",
"html-webpack-plugin": "^5.5.0",
"image-minimizer-webpack-plugin": "^3.8.1",
"imagemin": "^8.0.1",
"less": "^4.1.3",
"less-loader": "^7.3.0",
"mini-css-extract-plugin": "^2.7.2",
"postcss-loader": "^7.0.2",
"postcss-preset-env": "^7.8.3",
"react-refresh": "^0.14.0",
"sass": "^1.56.1",
"sass-loader": "^4.1.1",
"style-loader": "^3.3.1",
"stylus-loader": "^7.1.0",
"webpack": "^5.75.0",
"webpack-cli": "^5.0.1",
"webpack-dev-server": "^4.11.1"
},
"dependencies": {
"react": "^18.2.0",
"react-cli": "^0.3.1",
"react-dom": "^18.2.0",
"react-router-dom": "^6.4.4"
}
}
生产模式下的一些问题
public目录下一些资源的打包
在我们打包的时候HTML资源是原封不动的进行打包的,并不会进行解析,如果在index.html中引入了一些图标、公共样式等文件,这样他们在打包的时候并不会被打打包进去!所以我们需要安装一个插件将public目录下的内容原封不动复制一份到dist目录下。copy-webpack-plugin
安装
npm i copy-webpack-plugin
// 配置 webpack.prod.js
const CopyPlugin = require("copy-webpack-plugin");
plugins:[
new CopyPlugin({
patterns: [
{
from: path.resolve(__dirname, '../public'),
to: path.resolve(__dirname, '../dist'),
globOptions: {
ignore: ["**/index.html"], // 忽略这个文件
},
},
],
})
配置完成之后重新进行打包,public目录下的公共资源就会被复制到dist目录下完成打包!注意要忽略掉index.html,避免重复打包报错!
前端路由刷新404
(手动狗头!)生产模式下因为没有用到webpack的devServer配置,所以不能够在页面404时显示index.html,需要在代码上线打包部署的时候会有专门的配置来解决这个问题,我还不会(哈哈哈哈哈哈哈),等我学会了再写!
开发模式生产模式配置合并
通过上面开发模式和生产模式的配置,发现有很多配置是一样的,这样就导致了很多的重复,所以为了增强代码的复用性同时减小代码的体积,那么就需要进行代码合并!
主要就是通过process.env.NODE_ENV去获取环境变量,来判断是生产环境运行还是开发环境运行!
// 合并后 webpack.config.js
/*
* @Author: fdhou
* @Date: 2022-12-06 14:50:19
* @LastEditors: fdhou
* @LastEditTime: 2022-12-08 10:43:06
* @Description: 开发环境webpack配置
*/
const path = require('path')
const ESLintWebpackPlugin = require('eslint-webpack-plugin');
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 ImageMinimizerPlugin = require("image-minimizer-webpack-plugin");
const CopyPlugin = require("copy-webpack-plugin");
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
// 获取cross-env定义的环境变量
const isProduction = process.env.NODE_ENV === 'production'
// 获取处理样式的Loaders
const getStyleLoaders = (preProcessor) => {
return [
isProduction ? MiniCssExtractPlugin.loader : "style-loader",
"css-loader",
{
loader: "postcss-loader",
options: {
postcssOptions: {
plugins: ["postcss-preset-env"],
},
},
},
preProcessor && {
loader: preProcessor,
options:
preProcessor === "less-loader" ?
{
lessOptions: {
modifyVars: { '@primary-color': '#1DA57A' },
javascriptEnabled: true,
},
} : {},
},
].filter(Boolean);
};
module.exports = {
entry: './src/main.js',
output: {
path: isProduction ? path.resolve(__dirname, '../dist') : undefined,
filename: isProduction ? 'static/js/[name].[hash:10].js' : 'static/js/[name].js', // 输出的文件名
chunkFilename: isProduction ? 'static/js/[name].[hash:10].chunk.js' : 'static/js/[name].chunk.js', // 动态导入的文件打包输出的文件名
assetModuleFilename: 'static/media/[name][hash:10][ext][query]', // 图片等公共资源打包后的名字
clean: isProduction ? true : false,
},
module: {
rules: [
/**
* 样式资源处理
*/
{
test: /\.css$/,
use: getStyleLoaders()
},
{
test: /\.less$/,
use: getStyleLoaders("less-loader"),
},
{
test: /\.s[ac]ss$/,
use: getStyleLoaders("sass-loader"),
},
{
test: /\.styl$/,
use: getStyleLoaders("stylus-loader"),
},
/**
* 图片资源处理
*/
{
test: /\.(png|jpe?g|gif|svg)$/,
type: "asset",
parser: {
dataUrlCondition: {
maxSize: 10 * 1024, // 小于10kb的图片会被base64处理
},
},
},
/**
* 字体图标处理
*/
{
test: /\.(ttf|woff2?)$/,
type: "asset/resource", // 原封不动输出
},
/**
* js资源处理
*/
{
test: /\.jsx?$/,
include: path.resolve(__dirname, "../src"),
loader: 'babel-loader',
options: {
cacheDirectory: true, // 开启babel编译缓存
cacheCompression: false, // 缓存文件不要压缩
plugins: [
!isProduction && 'react-refresh/babel' // js热替换HMR
].filter(Boolean)
}
}
]
},
plugins: [
new ESLintWebpackPlugin({
context: path.resolve(__dirname, "../src"),// 要处理的文件范围
exclude: "node_modules",
cache: true,
cacheLocation: path.resolve(__dirname, "../node_modules/.cache/.eslintcache"), // 缓存目录
}),
// 处理html资源
new HtmlWebpackPlugin({
// 以 public/index.html 为模板创建文件
// 新的html文件有两个特点:1. 内容和源文件一致 2. 自动引入打包生成的js等资源
template: path.resolve(__dirname, "../public/index.html"),
}),
isProduction && new MiniCssExtractPlugin({
filename: "static/css/[name].[hash:10].css",
chunkFilename: "static/css/[name].[hash:10].chunk.css",
}),
isProduction && new CopyPlugin({
patterns: [
{
from: path.resolve(__dirname, '../public'),
to: path.resolve(__dirname, '../dist'),
globOptions: {
ignore: ["**/index.html"], // 忽略这个文件
},
},
],
}),
!isProduction && new ReactRefreshWebpackPlugin()
].filter(Boolean),
optimization: {
splitChunks: {
chunks: "all", // 代码分割
cacheGroups: {
// react react-dom react-router-dom 一起打包成单独文件
react: {
test: /[\\/]node_modules[\\/]react(.*)?[\\/]/,
name: "chunk_react", // 包名、
priority: 40, // 权重需要比node_moduels,否则会直接打包到ode_moduels里面了
},
// antd打包成单独文件
antd: {
test: /[\\/]node_modules[\\/]antd(.*)?[\\/]/,
name: "chunk_antd", // 包名、
priority: 30, // 权重需要比node_moduels,否则会直接打包到ode_moduels里面了
},
// 剩下node_modules的在单独打包成一个文件
lib: {
test: /[\\/]node_modules[\\/]/,
name: "chunk_libs", // 包名、
priority: 20,
}
}
},
runtimeChunk: {
name: (entrypoint) => `runtime~${entrypoint.name}`,
},
// 是否需要进行压缩
minimize: isProduction,
minimizer: [
new CssMinimizerWebpackPlugin(),
new TerserWebpackPlugin(),
new ImageMinimizerPlugin({
minimizer: {
implementation: ImageMinimizerPlugin.imageminGenerate,
options: {
plugins: [
["gifsicle", { interlaced: true }],
["jpegtran", { progressive: true }],
["optipng", { optimizationLevel: 5 }],
[
"svgo",
{
plugins: [
"preset-default",
"prefixIds",
{
name: "sortAttrs",
params: {
xmlnsOrder: "alphabetical",
},
},
],
},
],
],
},
},
}),
]
},
mode: isProduction ? "production" : 'development',
devtool: isProduction ? "source-map" : "cheap-module-source-map",
// webpack解析模块加载选项
resolve: {
// 自动补全扩展名
extensions: [".jsx", ".js", ".json"]
},
// 自动化配置
devServer: {
open: true, // 是否自动打开浏览器
host: "localhost",
port: 4000,
hot: true, // 热模块替换
historyApiFallback: true, // 解决react-router刷新404问题
},
performance: false, // 关闭性能分析,提示速度
}
完成以上更改之后可以分别运行npm start
和npm run build
进行测试:
优化配置
自定义主题配置
在开发的时候我们会用到一些库,比如Antd,So下面介绍一下对Antd的一些配置。
安装
npm i antd
安装完成之后在main.js中进行引入import 'antd/dist/antd'
,然后就可以开始使用了
在Antd中默认主题色是蓝色,假如我们需要改变主题色,那么就需要在webpack的配置中做一些更改,可以参考Antd官网,因为我是自己写的脚手架所以配置会有一点不同,配置如下,因为Antd的样式都是用less写的,所以需要修改less-loader的options选项,修改之后主题色就会全部发生变化,其他的主题颜色修改可以参考定制主题:
// webpack.config.js
const getStyleLoaders = (preProcessor) => {
return [
isProduction ? MiniCssExtractPlugin.loader : "style-loader",
"css-loader",
{
loader: "postcss-loader",
options: {
postcssOptions: {
plugins: ["postcss-preset-env"],
},
},
},
preProcessor && {
loader: preProcessor,
options:
preProcessor === "less-loader" ?
{
lessOptions: {
modifyVars: { '@primary-color': '#1DA57A' },
javascriptEnabled: true,
},
} : {},
},
].filter(Boolean);
};
注意: 在main.js中引入的时候需要修改引入为less:import 'antd/dist/antd.less'
否则样式是不会生效的。
再次打包运行会发现主题色就更改成了绿色。
代码体积优化
在运行npm run build
将代码打包后,提示打包后文件体积太大,会导致加载文件的速度比较慢,影响页面加载性能,那么我们可以将代码进行分割,这样就可以减小每个文件的体积,也能更好的去做按需加载!
配置如下,我们可以在splitChunks中配置cacheGroups手动配置将代码进行拆分!
// webpack.config.js
......
splitChunks: {
chunks: "all", // 代码分割
cacheGroups: {
// react react-dom react-router-dom 一起打包成单独文件
react: {
test: /[\\/]node_modules[\\/]react(.*)?[\\/]/,
name: "chunk_react", // 包名
priority: 40, // 权重需要比node_moduels,否则会直接打包到ode_moduels里面了
},
// antd打包成单独文件
antd: {
test: /[\\/]node_modules[\\/]antd(.*)?[\\/]/,
name: "chunk_antd", // 包名
priority: 30, // 权重需要比node_moduels,否则会直接打包到ode_moduels里面了
},
// 剩下node_modules的在单独打包成一个文件
lib: {
test: /[\\/]node_modules[\\/]/,
name: "chunk_libs", // 包名
priority: 20,
}
}
}
配置完成之后重新打包如下图文件已经被拆分:
tips
通过上面代码分割的方法已经将代码分割成小块,但是发现控终端还是在报warnings,看着有点烦(手动狗头!),那么下面就把它关掉,只需如下一行代码,终端立马successfully!
// webpack.config.js
performance: false, // 关闭性能分析,提示速度
小结
github链接如果下载安装依赖之后npm run build
报如下错误只需要参照webpack从入门到原理(高级五)——减少打包代码体积重新安装一下压缩图片的插件即可!
通过以上配置最终就完成了何用Webpack从零搭建一个React脚手架的过程,上面的大段代码都是配置完优化以及解决了遇到的问题的代码,可以直接使用!今天是更文的第19天,加油,我想要个暖脚宝(哈哈哈哈哈!)
转载自:https://juejin.cn/post/7174922870588440631