React18+Webpack5搭建React-cli
1. 主要目录解析
react-cli # 项目根目录
├── package.json
├── public
│ ├── favicon.ico # 网站页签图标
│ ├── index.html # 主页面
└── src
│ ├── page # 项目页面文件夹
│ ├── app.jsx # App组件
│ ├── main.js # 入口文件
└── config
├── webpack.config.js
├── webpack.dev.js
├── webpack.prod.js
2. 基本配置
module.exports = {
// 入口
entry: "",
// 输出
output: {},
// 加载器
module: {
rules: [],
},
// 插件
plugins: [],
// 模式
mode: "",
};
Webpack 是基于 Node.js 运行的,所以采用 Common.js 模块化规范
3. 安装webpack
yarn add webpack webpack-cli -D
4. 处理样式与加载器
安装依赖
yarn add less less-loader sass sass-loader style-loader css-loader -D
// 返回处理样式loader函数
const getStyleLoaders = (pre) => {
return [
'style-loader',
'css-loader',
{
// 处理css兼容性问题
// 配合package.json中browserslist来指定兼容性
loader: 'postcss-loader',
},
pre,
].filter(Boolean);
};
module: {
rules: [
// 处理css
{
test: /\.css$/,
use: getStyleLoaders(),
},
// 处理less
{
test: /\.less$/,
use: getStyleLoaders('less-loader'),
},
// 处理sass
{
test: /\.s[ac]ss$/,
use: getStyleLoaders('sass-loader'),
},
],
},
在项目根目录创建 postcss.config.js 文件,并添加以下代码:
module.exports = {
plugins: [
require('autoprefixer')
]
}
Webpack 就会自动处理样式兼容性和添加前缀
5. 处理图片资源
module: {
rules: [
// 处理图片文件
{
test: /\.(png|jpe?g|gif|webpp)$/,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 16 * 1024, // 小于16kb的图片会被base64处理
},
},
generator: {
// 将图片文件输出到 static/imgs 目录中
// 将图片文件命名 [hash:8][ext][query]
// [hash:8]: hash值取8位
// [ext]: 使用之前的文件扩展名
// [query]: 添加之前的query参数
filename: 'static/imgs/[hash:8][ext][query]',
},
},
],
},
6. 处理其他资源
module: {
rules: [
{
test: /\.(ttf|woff2?|map4|map3|avi)$/,
type: 'asset/resource',
generator: {
filename: 'static/media/[hash:9][ext][query]',
},
},
],
},
7. 处理js、jsx资源
- Babel 是一个 JavaScript 编译器,可以将 ES6+转化成ES5。
安装依赖
yarn add babel-loader @babel/core @babel/preset-env @babel/preset-react -D
babel-loader是webpack和Babel之间的桥梁,它将Babel与webpack集成。
@babel/core是Babel的核心库。
@babel/preset-env和@babel/preset-react是预设,用于指定Babel要如何转换代码
rules: [
{
// 处理JSX语法
test: /\.jsx?$/,
exclude: /node_modules/,
use: {
loader: "babel-loader",
options: {
presets: ["@babel/preset-env", "@babel/preset-react"]
}
}
}
]
- 激活js的HMR
修改App.jsx,浏览器会自动刷新后再显示修改后的内容,我们需要在不刷新浏览器的前提下模块热更新,并且能够保留react组件的状态。
可以借助 @pmmmwh/react-refresh-webpack-plugin 插件来实现,该插件又依赖于react-refresh
yarn add react-refresh @pmmmwh/react-refresh-webpack-plugin -D
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
module.exports = {
// ...
module: {
rules: [
{
// 处理JSX语法
test: /\.jsx?$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env', '@babel/preset-react'],
plugins: ['react-refresh/babel'],
},
},
},
]
},
plugins: [
new ReactRefreshWebpackPlugin()
],
devServer: {
hot: true, // 开启HMR
liveReload:true // 开启浏览器自动刷新功能
}
};
8. 解析模块加载器选项
/* 解析模块加载器选项 */
resolve: {
// 自动补全文件扩展名
extensions: ['.jsx', '.js', '.json'],
},
9. 配置路径别名
resolve: {
// ...
// 配置路径别名
alias: {
'@src': path.resolve(__dirname, '/src'),
},
},
在项目根目录下创建 jsconfig.json 文件,并添加以下内容
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@src/*": ["src/*"]
}
}
}
10. 自动拆分代码块splitChunks
optimization.splitChunks 是 Webpack 中用于代码分割的优化配置项.
optimization.runtimeChunk,可以将运行时代码块从代码块中拆分出来,生成一个独立的代码块。这样做有如下好处:
- 代码体积更小:独立的运行时代码块可以被浏览器缓存,减少每个代码块的大小,降低网络传输的成本。
- 缓存效果更佳:运行时代码块不受业务代码影响,只有当 Webpack 升级或配置发生变化时才会变化,因此可以更好地利用浏览器缓存。
通过适当地配置 optimization.runtimeChunk,可以实现更好的代码优化效果,提高页面加载速度。
/* 优化配置 */
optimization: {
splitChunks: {
chunks: 'all',
},
runtimeChunk: {
// 这样可以为每个入口生成一个独立的运行时代码块
name: (entrypoint) => `runtime~${entrypoint.name}.js`,
},
},
11. 安装React相关依赖
yarn add react react-dom react-router-dom -S
/src/App.jsx
import React, { lazy, Suspense } from 'react';
import { Link, Route, Routes } from 'react-router-dom';
// import About from './page/About/About';
// import Home from './page/Home/Home';
const About = lazy(() => import(/* webpackChunkName: 'About' */'./page/About/About'));
const Home = lazy(() => import(/* webpackChunkName: 'Home' */'./page/Home/Home'));
const App = () => {
return (
<div>
<h1>Hello React</h1>
<ul>
<li>
<Link to="/home">Home</Link>
</li>
<li>
<Link to="/about">About</Link>
</li>
</ul>
<Suspense fallback={'loading......'}>
<Routes>
<Route path={"/"} element={<Home />} />
<Route path={"/home"} element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</Suspense>
</div>
);
};
export default App;
/src/index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import APP from './App.jsx';
const root = ReactDOM.createRoot(document.getElementById('app'));
root.render(
<BrowserRouter>
<APP />
</BrowserRouter>
);
/public/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">
<link rel="shortcut icon" href="favicon.ico" type="image/x-icon">
<title>React Cli</title>
</head>
<body>
<div id="app"></div>
</body>
</html>
12.配置devServer
/* 开发服务器 */
devServer: {
host: 'localhost',
port: 9527,
open: true,
hot: true, // 开启热更新
// liveReload: false, // 禁用浏览器自动刷新功能
historyApiFallback: true, // 解决前端路由刷新404问题
},
package.json
"scripts": {
"start": "npm run dev",
"dev": "webpack serve --config ./config/webpack.dev.js"
},
最后yarn start
13.优化配置
13.1. 提取CSS成一个单独的文件
安装依赖:
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
// ... 其他配置 ...
module: {
rules: [
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader'
]
}
]
},
plugins: [
new MiniCssExtractPlugin({
filename: 'static/css/[name].[contenthash:10].css',
chunkFilename: 'static/css/[name].[contenthash:10].chunk.css',
}),
]
};
13.2. 压缩CSS、JS
const CssMinimizerWebpackPlugin = require('css-minimizer-webpack-plugin');
// TerserWebpackPlugin不需要安装,webpack已集成
const TerserWebpackPlugin = require('terser-webpack-plugin');
module.exports = {
// ... 其他配置 ...
module: {
rules: [
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader'
]
}
]
},
/* 优化配置 */
optimization: {
minimizer: [
new CssMinimizerWebpackPlugin(),
new TerserWebpackPlugin()
],
},
};
13.4. 压缩图片
yarn add image-webpack-loader imagemin-webpack-plugin -D
file-loader:将文件输出到指定的目录,并返回相对路径url-loader:与file-loader类似,但可以将文件转换为Base64编码,减少请求次数image-webpack-loader:加载和压缩图片imagemin-webpack-plugin:在打包过程中进一步压缩图片
const path = require('path');
const ImageminPlugin = require('imagemin-webpack-plugin').default;
module.exports = {
// ... 其他配置 ...
module: {
rules: [
{
test: /\.(png|jpe?g|gif)$/i,
type: 'asset/resource', // 用asset模块类型处理图片
generator: {
filename: 'images/[name].[hash:8][ext]', // 输出文件名,使用哈希值
},
use: [
{
loader: 'image-webpack-loader',
options: {
disable: process.env.NODE_ENV === 'development', // 开发模式下不压缩图片
},
},
],
},
],
},
plugins: [
new ImageminPlugin({
test: /\.(png|jpe?g|gif)$/i,
}),
],
};
14.复制文件
const CopyPlugin = require("copy-webpack-plugin");
const path = require('path');
const ImageminPlugin = require('imagemin-webpack-plugin').default;
module.exports = {
// ... 其他配置 ...
plugins: [
new CopyPlugin({
patterns: [
{
// 打包public文件夹
from: path.resolve(__dirname, "../public"),
to: path.resolve(__dirname, "../dist"),
globOptions: {
// 忽略index.html文件
ignore: ["**/index.html"],
},
},
],
}),
],
};
15. 打包优化optimization
-
minimize: 是否对打包后的代码进行压缩,默认值为 true。 -
minimizer: 配置压缩器,可以使用 UglifyJsPlugin、TerserPlugin 等插件来进行压缩。 -
splitChunks: 配置代码分离,可以将多个入口文件共同使用的代码提取出来,形成一个共享的代码块(chunk),从而减小整体的文件大小,提高页面加载速度。chunks: 决定哪些块会被优化,默认值为 "async",表示只对异步加载的块进行优化。还可以设置为 "all",表示对所有块进行优化,或者设置为函数,以返回 true 或 false 来决定哪些块会被优化。cacheGroups是一个对象,其属性名是缓存组的名称,属性值是一个对象,包含如下属性:test: 用于匹配符合要求的模块。priority: 用于设置缓存组的优先级,值越大表示优先级越高。默认值为 0。reuseExistingChunk: 如果当前的模块已经被打包到一个共享代码块中,可以设置为 true,表示重用这个共享代码块。默认值为 true。enforce: 设置为 true,表示将模块强制打包到当前的缓存组中,而不是被其他的缓存组复用。默认值为 false。
-
runtimeChunk: 配置运行时代码的分离,可以将运行时代码提取出来,形成一个单独的代码块,避免重复打包。
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
// react react-dom react-router-dom 一起打包成一个js文件
react: {
test: /[\\/]node_modules[\\/]react(.*)?[\\/]/,
name: 'chunk-react',
priority: 40, // 权重 优先级
},
// antd 单独打包
antd: {
test: /[\\/]node_modules[\\/]antd[\\/]/,
name: 'chunk-antd',
priority: 30,
},
// 剩下的node_modules单独打包
libs: {
test: /[\\/]node_modules[\\/]/,
name: 'chunk-libs',
priority: 20,
},
},
},
runtimeChunk: {
name: (entrypoint) => `runtime~${entrypoint.name}.js`,
},
// 是否需要进行压缩
minimize: isProduction,
minimizer: [new CssMinimizerWebpackPlugin(), new TerserWebpackPlugin()],
},
16. 代码质量
16.1. 集成eslint+preitter
npm install eslint eslint-config-prettier eslint-plugin-prettier prettier -D
新建.eslintrc.js文件
module.exports = {
env: {
browser: true, // 允许在浏览器环境中使用全局变量,如document和window
es2021: true, // 启用对 ES2021 的支持
node: true, // 允许在Node.js环境中使用全局变量,如process和__dirname
},
extends: [
'eslint:recommended', // 使用 ESLint 推荐的规则
'plugin:react/recommended', // 使用 React 推荐的规则
'plugin:react-hooks/recommended', // 使用 React Hooks 推荐的规则
'plugin:prettier/recommended', // 使用 Prettier 推荐的规则
],
ignorePatterns: ['/node_modules/**', '/build/**', '/dist/**'],
overrides: [],
// 指定解析器选项
parserOptions: {
// 启用 ECMAScript 模块
sourceType: 'module',
// 允许使用 import 语句
ecmaVersion: 2021,
// JSX 解析器配置
ecmaFeatures: {
jsx: true,
},
},
plugins: ['react', 'react-hooks', 'prettier'],
rules: {
'no-unused-vars': 'warn', // 未使用变量的警告
'no-undef': 'error', // 未定义变量的错误
'no-console': 'warn', // 禁止使用 console 的警告
'no-use-before-define': 'error', // 不允许在定义之前使用
'react/jsx-no-undef': 'error', // 未定义 JSX 元素的错误
'react/react-in-jsx-scope': 'off', // 不需要引入 React 就可以使用 JSX
'react-hooks/rules-of-hooks': 'error', // 使用 Hooks 规则的错误
'react-hooks/exhaustive-deps': 'warn', // useEffect 依赖项检查的警告
'prettier/prettier': 'error', // 使用 Prettier 推荐的格式化规则
},
};
新建.prettierrc.js文件
module.exports = {
// 一行的字符数,如果超过会进行换行,默认为 80
printWidth: 80,
// 一个 tab 代表几个空格数,默认为 2
tabWidth: 2,
// 是否使用 tab 进行缩进,默认为 false,表示用空格进行缩减
useTabs: false,
// 行尾是否需要分号,默认为 true
semi: true,
// 是否使用单引号,默认为 false,使用双引号
singleQuote: true,
// jsx 是否使用单引号,默认为 false,使用双引号
jsxSingleQuote: true,
// 末尾是否需要逗号,有三个可选值 "<none|es5|all>"
// "<none>" 代表不需要逗号
// "<es5>" 代表ES5中需要逗号
// "<all>" 代表所有对象最后一个属性添加逗号
trailingComma: 'all',
// 对象大括号直接是否有空格,默认为 true,效果:{ foo: bar }
bracketSpacing: true,
// jsx 标签的反尖括号是否在最后一行中间换行,默认为 false,效果:<br
// className=""
// />
jsxBracketSameLine: false,
// 箭头函数,只有一个参数的时候,是否需要括号,默认为 avoid,有两个可选值 "avoid" 和 "always"
arrowParens: 'avoid',
};
// 修复eslint
npx eslint --fix .
// 格式化代码
npx prettier --write .
16.2. 代码提交前检验并格式化代码
执行命令,生成.husky文件夹
npx mrm lint-staged
在package.json中添加
"lint-staged": {
"*.{js,jsx,ts,tsx}": "eslint --fix"
}
18. 切换为TS版本的React脚手架
18.1. 安装
yarn add typescript @babel/preset-typescript @types/react @types/react-dom -D
18.2. 修改webpack.config.js
module.exports = {
// ... 其他配置 ...
module: {
rules: [
{
// 处理tsx语法
test: /\.(ts|js)x?$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env', '@babel/preset-react', '@babel/preset-typescript'],
},
},
},
],
},
};
18.3. 删除jsconfig.json文件,添加tsconfig.json文件
{
"compilerOptions": {
"baseUrl": "./src",
"paths": {
"@/*": ["*"]
},
// 指定TypeScript使用的模块系统。
"module": "esnext",
// 将ECMAScript目标版本设置为要编译的版本。
"target": "es5",
// 列出要在编译中包含的库文件。
"lib": ["dom", "dom.iterable", "esnext"],
// 是否生成源映射文件。
"sourceMap": true,
// 是否启用esModuleInterop编译选项。
"esModuleInterop": true,
// 是否允许编译器编译JavaScript文件。
"allowJs": true,
// 指定JSX输出格式。
"jsx": "react-jsx",
// 解析模块时要遵循的规则。
"moduleResolution": "node",
// 是否禁用输出。
"noEmit": true,
// 是否启用严格模式。
"strict": true,
// 是否启用独立模块编译模式。
"isolatedModules": true,
// 是否允许编译器解析JSON模块。
"resolveJsonModule": true,
// 是否启用noImplicitAny编译选项。
"noImplicitAny": false
},
// 指定要包含在编译中的文件和目录。
"include": ["src"]
}
18.4. 模块声明语句,告诉编译器如何处理特定类型的文件src/types/index.d.ts
declare module '*.svg' {
const value: string;
export default value;
}
declare module '*.bmp' {
const value: string;
export default value;
}
declare module '*.gif' {
const value: string;
export default value;
}
declare module '*.jpg' {
const value: string;
export default value;
}
declare module '*.jpeg' {
const value: string;
export default value;
}
declare module '*.png' {
const value: string;
export default value;
}
18.5. 配置eslint
module.exports = {
parser: '@typescript-eslint/parser', // 指定 ESLint 解析器为 TypeScript
settings: {
react: {
version: '18.2.0', // 指定react版本
},
},
env: {
browser: true, // 启用浏览器全局变量,例如 window 和 document
es2021: true, // 启用 ES2021 功能,例如 async/await
node: true,
},
extends: [
'eslint:recommended',
'plugin:react/recommended', // 启用推荐的 React 规则
'plugin:@typescript-eslint/recommended', // 启用推荐的 TypeScript 规则
'plugin:prettier/recommended', // 启用 Prettier 与 ESLint 的集成
],
parserOptions: {
ecmaFeatures: {
jsx: true, // 启用解析 JSX 语法
},
ecmaVersion: 12, // 指定 ECMAScript 版本
sourceType: '', // 指定 ECMAScript 版本
},
plugins: ['react', 'react-hooks', '@typescript-eslint', 'prettier'], // 指定 ECMAScript 版本
rules: {
'no-undef': 'error', // 未定义变量的错误
'no-console': 'warn', // 禁止使用 console 的警告
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-var-requires': 'off',
'react/react-in-jsx-scope': 'off', // 关闭在 React 18 中不必要的警告
'react/prop-types': 'off', // 关闭 prop-types 规则,如果你不使用 prop-types 库
'react-hooks/rules-of-hooks': 'error', // 启用 React Hooks 规则
'react-hooks/exhaustive-deps': 'warn', // 警告 React Hooks 中缺失的依赖项
'@typescript-eslint/explicit-module-boundary-types': 'off', // 关闭函数需要显式返回类型的警告
'prettier/prettier': 'error',
eqeqeq: 'error', // 要求使用 === 和 !==
},
ignorePatterns: ['/node_modules/**', '/build/**', '/dist/**'],
overrides: [
{
files: ['src/**/*.ts', 'src/**/*.tsx'],
excludedFiles: 'node_modules/**',
rules: {
// 在这里添加只对src文件夹生效的规则
},
},
],
};
至此,大功告成
19. 拓展
..... 未完待续