setVisible(true)}
className={classNames("icon", carts.length > 0 && "fill")}
>
{/* 添加visible类名 div会显示出来 */}
~~~

## ReactRouter
使用React这些工具所编写的项目通常都是单页应用(SPA),上述的案例都可以叫单页应用。单页应用中,整个应用中只含有一个页面,React会根据不同的状态在应用中显示出不同的组件。要想实现多页面应用,需要引入一个新的工具React Router,React Router为我们提供一种被称为客户端路由的东西,通过客户端路由可以将URL地址和React组件进行映射,当URL地址发生变化时,它会根据设置自动的切换到指定组件。并且这种切换完全不依赖于服务器。换句话说,在用户看来浏览器的地址栏确实发生了变化,但是这一变化并不由服务器处理,而是通过客户端路由进行切换。
官网:[Home v6.21.3 | React Router](https://reactrouter.com/en/main)
React Router6新出来的,虽然与5有很大的区别和新特性,5和6都认识下吧,相当于一个过度吧。
~~~cmd
npm install react-router-dom@5
~~~
在 `index.js` 中引入 `BrowserRouter` 并包裹App组件(或起个别名 `BrowserRouter as Router`)
~~~js
import ReactDOM from "react-dom/client";
import App from "./App";
import { BrowserRouter as Router } from "react-router-dom";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
);
~~~
然后就可以在项目中使用React Router了。
### Route组件
> - React Router 的url地址可以和组件进行映射(用户访问某个地址时,与其对应的组件会自动挂载)
~~~js
// components/Home/index.js
const Home = () => {
return (
Welcome to the Home Page!
);
};
export default Home;
// components/About/index.js
const About = () => {
return (
Welcome to the About Page!
- React
- Zustand
- Router
- Webpack
);
};
export default About;
// App.js
import { Route } from "react-router-dom/cjs/react-router-dom.min";
import Home from "./components/Home";
import About from "./components/About";
const App = () => {
return (
Hello React!
{/* 将组件与路由进行映射
使用 Route 来映射地址和组件
属性:
path 映射的url地址
component 要挂载的组件
*/}
);
};
export default App;
~~~

不知道看到这你会不会这样想,不是说用户访问某个地址时,与其对应的组件会自动挂载,那俺访问了 `/about` 后,`/` 路径的Home组件还会显示呢。
当 Route 的路径被访问,其对应组件就会对应挂载,其实这句话没毛病,你想的那是叫严格匹配。注意哈,默认情况下 Route 不能严格匹配,只要url地址地址的头部与path一致,组件就会挂载。要想做到你想的那样的严格匹配的话,添加个 `exact`
~~~js
{ /* exact 路径是否严格匹配 默认值:false */ }
~~~
1. `component` 用来指定路由匹配后被挂载的组件,需要直接传递组件的类,通过component构建的组件,它会自动创建组件并传递参数(直接在组件内传递props即可看到)
~~~js
// components/About/index.js
const About = (props) => {
console.log(props);
~~~

- match -> 匹配的信息
- **isExact** 检查路径是否完全匹配
- **params** 请求的参数
About组件的path修改下 `path="/about/:id"` ,然后控制台打印 `console.log(props.match.params)` :

改进下:
~~~js
// components/About/index.js
const data = [
{
id: 1,
title: "React"
},
{
id: 2,
title: "Zustand"
},
{
id: 3,
title: "Router"
},
{
id: 4,
title: "Webpack"
}
];
const About = (props) => {
const ds = data.find((item) => item.id === +props.match.params.id);
return (
Welcome to the About Page!
);
};
export default About;
~~~

- location -> 地址信息
- history -> 控制页面的跳转
- push() 跳转页面(可回退到之前页)
- replace() 替换页面(不可回退到之前页)
2. 除了 `component` 挂载组件,还有另一个方式 `render`
`render` 也可以用来指定被挂载的组件,它需要一个回调函数作为参数,回调函数的返回值会最终被挂载,它不会向 `component` 一样自动传递三个属性,要想得到这三个属性需要这样:
~~~js
{
return ;
}}
/>
~~~
组件通过props就可接收到这三个属性
3. 除了 `render` 挂载组件,还有另一个方式 `children`
`children` 也可以用来指定被挂载的组件,用法有两种:
- 和render类似,传递回调函数(直接把上图的render改为children就行),当children设置一个回调函数时,该组件无论路径是否匹配都会挂载
- 可以直接传递组件 `} />` (或者这样写 ``)
除了通过props获取三个对象外,还可以通过钩子函数来获取:
~~~js
import {
useHistory,
useLocation,
useRouteMatch,
useParams
} from "react-router-dom";
const match = useRouteMatch();
const location = useLocation();
const history = useHistory();
const params = useParams();
~~~
### 路由的嵌套
~~~js
~~~
同样也可以将嵌套路由放在About组件里,只是层级不一样了,使用还是不变的。
那如果about的嵌套路由有很多的话,想把 `/about/xxx/xxx~~~` 中的about动态获取(即父组件地址)
~~~js
// components/About/index.js
import { Route } from "react-router-dom";
import Good from "../Good";
import { useRouteMatch } from "react-router-dom";
const About = () => {
const { path } = useRouteMatch();
return (
<>
Welcome to the About Page!
>
);
};
export default About;
~~~
### Prompt组件
可以将路由Route统一放在一个Switch中,一个Switch中只会有一个路由显示
~~~js
// App.js
import { Route, Switch } from "react-router-dom";
~~~
当你使用到表单时,如果你没有写完表单内容跳转到其他页面了,再返回的话,表单的内容就会重置,这样会很麻烦,使用Prompt组件,会在你离开页面之前弹窗提示
~~~js
import { Prompt } from "react-router-dom";
const MyForm = () => {
return (
<>
Welcome to the MyForm Page!
>
);
};
export default MyForm;
~~~

改进下:(就是防止误操作导致数据丢失)
~~~js
import { useState } from "react";
import { Prompt } from "react-router-dom";
const MyForm = () => {
const [isPrompt, setIsPrompt] = useState(false);
return (
<>
{/* when 是否开启 */}
Welcome to the MyForm Page!
setIsPrompt(e.target.value.trim().length !== 0)}
/>
>
);
};
export default MyForm;
~~~

### Redirect组件 重定向
在About组件中使用Redirect组件重定向到form中
~~~js
// components/About/index.js
import { Route, useRouteMatch, Redirect } from "react-router-dom";
{/* to 重定向到xxx
from 从xxx 如 from={"/abc"} to={"/form"} 输入地址abc到form
*/}
~~~
### Link 和 NavLink
~~~js
import { Link } from "react-router-dom";
// components/Menu/index.js
const Menu = () => {
return (
);
};
export default Menu;
~~~
然后在App.js中导入并使用,当你点击两个链接时,它是不会向服务器重新发送请求的。但当你把 `Link` 换成 `a` 的话,那就不行了,`a` 会重新向服务器发送请求,不能这样干。
- `NavLink` 的功能会比 Link 更加丰富:它主要会涉及到样式
~~~css
/* components/Menu/index.module.css */
a:link,
a:visited {
color: orange;
text-decoration: none;
}
a:hover,
a.active {
color: hotpink;
text-decoration: underline;
}
~~~
~~~js
// components/Menu/index.js
import { NavLink } from "react-router-dom";
import classes from "./index.module.css";
const Menu = () => {
return (
-
{/* exact 严格匹配
activeClassName 激活的样式(或activeStyle={{}}) */}
Home
-
About
);
};
export default Menu;
~~~
看,不管你咋跳转,它都不会发请求,当你跳转到对应页面时,它就会显示对应路径的链接粉色加下划线。
### 两种 Router
- HashRouter 会通过url地址中的hash值来对地址进行匹配(就是根路径后多个 `#` 号)
- BrowserRouter 直接通过url地址进行组件的跳转,(不带 `#` 号)
当通过点击NavLink构建的链接进行跳转时,跳转并没经过服务器,这没问题
但当刷新页面时或通过普通链接跳转时,会向服务器发送请求加载数据,这时的请求并没有经过react router,所以返回404
- 解决方案:
- 使用HashRouter 重新构建并粘贴。服务器是不会处理带hash值的地址,所以使用HashRouter后请求将有react route处理
- 修改服务器的配置,将所有请求都转发到index.html。找到nginx下的conf目录,打开nginx.conf,找到对应字段并进行如下修改: `try_files $uri /index.html;` 意思就是不管你访问啥url地址,最后都给俺转发到index.html里

重新加载一下服务器 `.\nginx.exe -s reload` 。可能会打印错误,俺没用过nginx就用了比较死板的法子,打开任务管理器,停止nginx服务。然后再点击nginx.exe文件重启服务就行了。
nginx服务器:[nginx news](https://nginx.org/)
下载后,解压,点击 `nginx.exe` 即可启动服务器,访问 `127.0.0.1`

项目打包后,将其目录下所有文件粘贴到nginx下的html目录里。
要想停止服务的话,在nginx安装目录下打开终端:


## ReactRouter6
之前讲的ReactRouter都是5的知识点,ReactRouter6相对于5来讲改动还是很大的,把很多东西都化繁为简了,体积相对于之前的版本也小了很多比如 Switch,history,match等在6中都没有了,被其他东西替代了
~~~cmd
npm i react-router-dom
~~~
~~~js
import { Route, Routes } from "react-router-dom";
import Home from "./components/Home";
import About from "./components/About";
import Menu from "./components/Menu";
const App = () => {
return (
Hello React!
{/* Routes v6中的新组件 作用和Switch类似,都是用于Route的容器
v6中 component render children 都没了,需要通过element来挂载组件
并且那个 / 还可省略
*/}
} />
} />
);
};
export default App;
~~~
- 可以通过useParams() 来获取参数:
~~~js
import { useParams } from "react-router-dom";
const STU_DATA = [
{
id: 1,
title: "React"
},
{
id: 2,
title: "Zustand"
},
{
id: 3,
title: "Router"
},
{
id: 4,
title: "Webpack"
}
];
const Student = () => {
const { id } = useParams();
const stu = STU_DATA.find((item) => item.id === +id);
return (
<>
Welcome to the Student Page!
>
);
};
export default Student;
// App.js
} />
~~~
- useLocation() 还在:获取当前地址的信息
~~~js
const location = useLocation();
console.log(location);
~~~
- useMatch() 检查当前url是否匹配某个路由
~~~js
const match = useMatch("/about");
console.log(match);
~~~
如果路由匹配,则返回个对象,不匹配返回null
- useNavigate() 获取一个用于页面跳转的函数:
~~~js
const navigate = useNavigate();
const clickHandler = () => {
// navigate("/student/1"); // 使用push 会产生历史记录 可回退
navigate("/student/1", { replace: true }); // 使用replace 不会产生历史记录 不可回退
};
~~~
### v6中的嵌套路由
- 通过子路由来对Good进行映射 /about/good:
~~~js
// App.js
} />
// components/About/index.js
} />
~~~
v6中是默认严格匹配的,在后面加上 ***** 后就可不按照严格匹配来了。
**注意了:** 你看这,App.js中有路由,About组件中又有路由,要是每个组件都设置路由的话,那是不是非常不便于管理和维护。想要的是所有的路由可以统一写在一个位置,你可以这样:
~~~js
// App.js
}>
{/* 嵌套路由 */}
} />
~~~
嵌套路由也用了,但当你访问 `/about/good` 时并没有出现Good组件的页面,此时你还需要一步,在About组件中使用Outlet组件,它用来表示嵌套路由中的组件,当嵌套路由中的路径匹配成功,Outlet则表示嵌套路由中的组件,反之啥也不显示
~~~js
import { Outlet } from "react-router-dom";
// components/About/index.js
const About = () => {
return (
<>
Welcome to the About Page!
>
);
};
~~~
### Navigate组件 路由跳转
Navigate 组件用来跳转到指定的位置,默认使用push跳转,回退不了,要想回退的话,加个 replace
~~~js
~~~
### NavLink组件
改了一种方式去设置样式:
~~~js
{
return isActive ? { color: "black" } : null;
}}
to="/student/2"
>
Student
~~~
## ReactRouter 工程化
~~~js
// index.js
import ReactDOM from "react-dom/client";
import { createBrowserRouter, RouterProvider } from "react-router-dom";
import Home from "./components/Home";
import About from "./components/About";
// 创建router实例对象并配置路由对应关系
const router = createBrowserRouter([
{
path: "/",
element:
},
{
path: "/about",
element:
}
]);
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
// 路由绑定
);
~~~
### 抽离路由模块
工程化即模块化,router示例写在index.js中的话就不符合了。能不能创建个单独的模块,将router相关的放到其中,可以抽离路由模块。
- 在src下创建 `pages` : 用于放页面的;并在src下兴建 `router` ,专门用来放路由相关的

~~~js
// router/index.js
import { createBrowserRouter } from "react-router-dom";
import About from "../pages/About";
import Home from "../pages/Home";
const router = createBrowserRouter([
{
path: "/",
element:
},
{
path: "/about",
element:
}
]);
export default router;
~~~
接着再导入到index.js中并使用
~~~js
import router from "./router";
~~~
### 路由导航
1. 前面讲的Link、NavLink、useNavigate() 都可用于路由跳转,要说区别的话
- 声明式导航:`Link、NavLink` (会被转义为a链接)
- 编程式导航:`useNavigate()` 通过该钩子函数使用导航
2. 通过 `useSearchParams()` 也可以实现路由传参
~~~js
// Home/index.js
// About/index.js
const About = () => {
const [params] = useSearchParams();
const id = params.get("id");
return (
<>
我是关于页!{id}
>
);
};
~~~
### 路由嵌套
- 通过 `children` 配置子路由:
~~~js
const router = createBrowserRouter([
{
path: "/",
element:
,
children: [
{
path: "/home",
element:
},
{
path: "/about",
element:
}
]
}
]);
~~~
- 在Layout组件中添加二级路由出口:**
**
~~~js
const Layout = () => {
return (
<>
我是布局!
{/* 二级路由出口 */}
>
);
};
~~~
- 如何在显示Layout组件的同时,显示Home组件,默认二级路由就是将 `path: "/home"` 换成 `index: true`
- 当路由找不到时,用404路由兜底
~~~js
{
path: "*",
element:
}
~~~
- 要想将路由模式换成hash模式直接将 `createBrowserRouter` 换为 `createHashRouter`
## 样式相关
**补充:** 好像之前也没有说到样式相关的内容,那就在这里补充一下吧。希望不会太晚😅
### 内联样式和样式表
- 通过在标签内直接使用 `style={{}}` :
~~~js
// components/Home/index.js
const Home = () => {
return (
Welcome to the Home Page!
);
};
export default Home;
~~~
- 当内联样式多的话,可以把它提出来:
~~~js
// components/Home/index.js
const Home = () => {
const h1Style = {
color: "red",
backgroundColor: "#bfc",
border: "blue solid 2px"
};
return (
Welcome to the Home Page!
);
};
export default Home;
~~~
- 动态设置样式:需要借助 `useState`
~~~js
// components/Home/index.js
import { useState } from "react";
const Home = () => {
const [blackBorder, setBlackBorder] = useState(true);
const h1Style = {
color: "red",
backgroundColor: "#bfc",
border: blackBorder ? "blue solid 2px" : "black solid 2px"
};
return (
Welcome to the Home Page!
);
};
export default Home;
~~~
内联样式肯定不适合大项目。
- 样式表的话:
~~~css
/* components/Home/index.css */
.title {
color: red;
background-color: #bfc;
border: blue solid 2px;
}
.title1 {
border: black solid 2px;
}
~~~
~~~js
// components/Home/index.js
import { useState } from "react";
import "./index.css";
const Home = () => {
const [blackBorder, setBlackBorder] = useState(true);
return (
Welcome to the Home Page!
);
};
export default Home;
~~~
它也不适合,有缺点,`import "./index.css";` 这样如果是在App.js中引入那就是全局引入,如果再在 `src/index.js` 下引入的话那就会形成样式冲突(类名重复的话),不适合大范围的使用,这种方式适合引入一些通用样式。
### CSS Module
> 模块化CSS,适合大范围的使用(一种自动写类名的方式)。
>
> **使用步骤:**
>
> 1. 创建css文件时,以 `xxx.module.css` 命名,说明你创建的时一个css模块
> 2. 在组件中引入css,`import classes from "./index.module.css";`
> 3. 通过 `classes` 来设置类,`className={classes.title}`。CSS模块可以动态的设置唯一的class值(类名都是自动生成的,就是确保唯一性)
~~~css
/* components/Home/index.module.css */
.title {
color: red;
background-color: #bfc;
border: blue solid 2px;
}
.title1 {
border: black solid 2px;
}
~~~
~~~js
// components/Home/index.js
import { useState } from "react";
import classes from "./index.module.css";
const Home = () => {
const [blackBorder, setBlackBorder] = useState(true);
return (
<>
Welcome to the Home Page!
>
);
};
export default Home;
~~~
### Fragment
**拓展:** 在上图中,你可能会看到这个 `<>>` ,不知道你会不会对这玩意产生好奇,也不管你了,爱咋咋地,俺先简单说下这是个什么东西。
在react中,组件都必须有个跟容器,然后在跟标签里编写子标签。好,那俺在每个组件写个div父容器呗,但绝大多数都用不上这个div呀(就是个多鱼的结构)。但俺就是不想加那个div,俺就感觉难受。好,那俺有三种法子来解决:
1. 方法一:自定义个组件:组件直接引入父容器Out并使用就行了
~~~js
// src/Out.js
const Out = (porps) => {
// 只会返回子元素
return porps.children;
};
export default Out;
~~~
2. 方法二:使用 Fragment:它是由React提供的专门作为父容器的组件,它只会讲里面的子元素直接返回,不会创建任何多鱼的元素
~~~js
import { Fragment } from "react";
const Good = () => {
return (
Welcome to the Good Page!
);
};
~~~
3. 方法三:使用空标签 `<>>` ,它是react提供的语法糖
# 工程化
😁😁😁,总算是把React的常用知识点讲完了,接下来,俺将向工程化进发了

## 逗逗的钱包 —— doudou-wallet(记账本)
~~~powershell
npx create-react-app doudou-wallet
~~~
- 项目辅助依赖:
~~~powershell
npm i zustand react-router-dom dayjs classnames antd-mobile axios --save
~~~
移动端组件库:[Ant Design Mobile - Ant Design Mobile](https://mobile.ant.design/zh)
- 该留留该删删
### 路径解析
1. 路径解析配置(webpack),把 `@/` 解析为 `src/` :借助 `craco` 插件,不建议暴露内置的webpack配置
2. 路径联想配置(vscode)输入 `@/` 时,自动联想出来对应的 `wrc/` 下的子级目录:`jsconfig.json`
> 那如何使用 `craco` 插件:
>
> - 安装 `craco` :**npm i @craco/craco -D **
> - 根目录下创建配置文件 **craco.config.js** ,然后再里面添加需要的配置
> - `package.json` 中配置启动和打包命令


启动成功后,试下使用@后会不会报错。
有点纳闷,俺配置了路径联想配置,怕配置没生效,俺还关了vscode再打开重启项目,结果没想到,居然不行。算了,不纠结了,等会重置下电脑看看

配置的话就这样:

### Mock数据 json-server
在开发项目时,前后端一般都是同时进行的。当你需要用到数据时发现,后端没写好,就是不给你,此时你的心情

这个时候,你就知道Mock数据的好了😁
~~~cmd
npm i json-server -D
~~~
- 准备个json文件:在根目录创建个 `server/data.json`
- 添加启动命令:
~~~json
"server": "json-server --watch server/data.json --port 3001"
~~~
完事启动命令瞅瞅能不能运行成功。
# 工程化启动
😁😁😁,总算是把React的常用知识点讲完了,接下来,俺将向工程化进发了

俺就搭一个建议的后台管理系统的模板吧,通过模板,了解项目工程化的构建。
~~~cmd
npx create-react-app guten-admin
~~~
该删删,该改改。
## AntD
~~~cmd
npm install antd --save
~~~
AntD官网:[Ant Design - 一套企业级 UI 设计语言和 React 组件库 (antgroup.com)](https://ant-design.antgroup.com/index-cn);
## craco
官网:[Configure CRA without ejecting. | CRACO](https://craco.js.org/);
使用CRA创建的项目,若你想修改编译配置,你先别想着运行 `npm run eject` 暴露webpack配置,不建议哈,这玩意它不可逆呀,你咋晓得你改的就没毛病来。对于新手,你可以看看它 **craco** ,通过它可以覆盖 react-script 中的相关配置。
1. 安装:
~~~cmd
npm i @craco/craco -D
~~~
2. 项目根目录创建:`craco.config.js` (先来个路径解析配置,把 `@/` 解析为 `src/`)
~~~js
// craco.config.js
const path = require("path");
module.exports = {
webpack: {
alias: {
"@": path.resolve(__dirname, "src")
}
}
};
~~~
3. 修改 package.json 的 scripts 命令

启动成功后,试下使用@后会不会报错。
有点纳闷,俺配置了路径联想配置,怕配置没生效,俺还关了vscode再打开重启项目,结果没想到,居然不行。算了,不纠结了,等会重置下电脑看看

配置的话就这样:

1. 打包时,实时显示进度条:
~~~cmd
# 安装依赖
npm install webpackbar -D
~~~
~~~js
// craco.config.js
const path = require("path");
const WebpackBar = require("webpackbar");
module.exports = {
webpack: {
alias: {
"@": path.resolve(__dirname, "src")
},
plugins: [new WebpackBar()]
}
};
~~~
~~~cmd
# 打包
npm run build
~~~

2. 打包时实时显示`loader`和`plugin`的耗时情况:
~~~cmd
npm install --save-dev speed-measure-webpack-plugin
~~~
~~~js
// craco.config.js
const path = require("path");
const WebpackBar = require("webpackbar");
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const smp = new SpeedMeasurePlugin();
module.exports = {
webpack: smp.wrap({
alias: {
"@": path.resolve(__dirname, "src")
},
plugins: [new WebpackBar()]
})
};
~~~

3. 打包时,可将打包的模块内容可视化,以便分析项目结构:
~~~cmd
npm install --save-dev webpack-bundle-analyzer
~~~
~~~js
// craco.config.js
const path = require("path");
const WebpackBar = require("webpackbar");
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const smp = new SpeedMeasurePlugin();
const BundleAnalyzerPlugin =
require("webpack-bundle-analyzer").BundleAnalyzerPlugin;
module.exports = {
webpack: smp.wrap({
alias: {
"@": path.resolve(__dirname, "src")
},
plugins: [new WebpackBar(), new BundleAnalyzerPlugin()]
})
};
~~~
默认访问 `http://127.0.0.1:8888/` 查看结果

以上三个,一般在本地开发时,是用不上的,还延长了打包效率,只有当项目开发完后,大概率都会用上。项目开发完,它只是具备了能正常跑的能力,距离上线还远远不够,需要对项目进行层层优化。
### 环境区分
@craco/craco 提供了 `whenDev、whenProd、whenTest、when` 函数,用于区别不同的环境,其中:
- `whenDev、whenProd、whenTest` 分别对应的是 `process.env.NODE_ENV` 的 3 种值(`development`、`production`、`test`),需要接收两个参数
- 而 `when` 是区分环境的万金油函数,需要接收 3 个参数
### 拓展配置
> 要想拓展 webpack 配置的话,可以在configure 函数中编写,它可以扩展所有的 webpack 配置。
可修改下模块内容可视化的配置:其中的配置可看:[GitHub - webpack-contrib/webpack-bundle-analyzer](https://github.com/webpack-contrib/webpack-bundle-analyzer)
~~~js
// craco.config.js
const { whenDev, whenProd, when } = require("@craco/craco");
const path = require("path");
const WebpackBar = require("webpackbar");
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const smp = new SpeedMeasurePlugin();
const BundleAnalyzerPlugin =
require("webpack-bundle-analyzer").BundleAnalyzerPlugin;
module.exports = {
reactScriptsVersion: "react-scripts",
webpack: smp.wrap({
configure: (webpackConfig, { env, paths }) => {
if (env !== "development") {
webpackConfig.plugins = webpackConfig.plugins.concat(
new BundleAnalyzerPlugin({
analyzerMode: "server",
analyzerHost: "127.0.0.1",
analyzerPort: 8888,
openAnalyzer: true, // 构建完打开浏览器
reportFilename: path.resolve(__dirname, `analyzer/index.html`)
})
);
}
return webpackConfig;
},
alias: {
"@": path.resolve(__dirname, "src")
},
plugins: [new WebpackBar()]
})
};
~~~
当然还有很多其他的配置,可以看官网或者这篇文章 [基于craco配置的react项目的webpack构建优化 - 掘金 (juejin.cn)](https://juejin.cn/post/7154351787246354462) (还行,讲的挺全的)
## 目录结构
# 中后台工程启动
😁😁😁,总算是把React的常用知识点讲完了,接下来,俺将向工程化进发了

俺就搭一个建议的后台管理系统的模板吧,通过模板,了解项目工程化的构建。
## 技术栈
- 框架:[React](https://react.dev/);
- 构建和打包工具:[Vite](https://cn.vitejs.dev/);
- 状态管理:[Zustand](https://zustand-demo.pmnd.rs/);
- 组件库:[Ant Design](https://ant-design.antgroup.com/index-cn);
- 路由:[React Router](https://reactrouter.com/en/main);
- 包管理工具:[pnpm](https://pnpm.io/zh/);
- 脚本:[TypeScript](https://www.typescriptlang.org/zh/);
- 样式:**SASS**;
## 项目初始化
- 安装:
~~~cmd
pnpm create vite guten-admin --template
~~~
- css配置:
~~~cmd
pnpm i sass -D
pnpm i sass-loader -D # scss解析器
~~~
~~~ts
// vite.config.ts
export default defineConfig({
plugins: [react()],
css: {
preprocessorOptions: {
scss: {
additionalData: `@import "./src/styles/global.scss";`
}
}
}
})
~~~
- 配置文件引入:使用 **@**
~~~cmd
pnpm i @types/node -D
~~~
~~~js
// vite.config.ts
import { fileURLToPath, URL } from "node:url";
export default defineConfig({
plugins: [],
css: {},
resolve: {
alias: {
"@": fileURLToPath(new URL("./src", import.meta.url))
}
}
})
~~~
在 `tsconfig.json` 添加如下代码:vscode提示路径

有点纳闷,俺配置了路径联想配置,怕配置没生效,俺还关了vscode再打开重启项目,结果没想到,居然不行。算了,不纠结了,等会重置下电脑看看😂
## 工程目录
- 初始时项目目录
~~~diff
├─ /node_modules
├─ /public
- | └─ vite.svg
├─ /src
- | ├─ /assets
- | | └─ react.svg
- | ├─ App.css
| ├─ App.tsx
- | ├─ index.css
| └─ main.tsx
├─ .eslintrc.cjs
├─ .gitignore
├─ index.html
├─ package.json
├─ vite.config.js
└─ pnpm-lock.yaml
~~~
- 工程化后目录
~~~diff
~~~
## Mock数据
在开发项目时,前后端一般都是同时进行的。当你需要用到数据时发现,后端没写好,就是不给你,此时你的心情

这个时候,你就知道Mock数据的好了😁
~~~cmd
pnpm i json-server -D
~~~
- 准备个json文件:在根目录创建个 `server/data.json`
- 添加启动命令:
~~~json
"serve": "json-server --watch server/data.json --port 3001"
~~~
完事启动命令瞅瞅能不能运行成功。
## 反向代理
~~~ts
// https://vitejs.dev/config/
export default defineConfig({
plugins: [],
css: {},
resolve: {},
server: {
port: 3000,
open: true,
proxy: {
'/api': {
target: 'http://localhost:3001',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
}
})
~~~
## AntD
AntD官网:[Ant Design - 一套企业级 UI 设计语言和 React 组件库 (antgroup.com)](https://ant-design.antgroup.com/index-cn);
~~~cmd
pnpm install antd
pnpm install @ant-design/icons
~~~
## 代码规范
为了在团队开发中避免一些不必要的操作
### editorconfig
- 下载拓展:

- 在跟目录下新建 `.editorconfig` :
~~~diff
# http://editorconfig.org
root = true
[*] # 表示所有文件适用
charset = utf-8 # 设置文件字符集为 utf-8
indent_style = space # 缩进风格(tab | space)
indent_size = 2 # 缩进大小
end_of_line = lf # 控制换行类型(lf | cr | crlf)
trim_trailing_whitespace = true # 去除行首的任意空白字符
insert_final_newline = true # 始终在文件末尾插入一个新行
[*.md] # 表示仅 md 文件适用以下规则
max_line_length = off
trim_trailing_whitespace = false
~~~
### ESLint
~~~cmd
pnpm install eslint eslint-config-mature eslint-plugin-import -D
~~~
`eslint` 用于检查代码质量。进入 `.eslintrc.cjs` 文件,按自身需求添加[配置](https://link.juejin.cn/?target=https%3A%2F%2Fzh-hans.eslint.org%2Fdocs%2Flatest%2Fuse%2Fconfigure%2Frules)。
~~~js
module.exports = {
// 指定代码的运行环境
env: {
es2020: true,
node: true,
browser: true,
},
settings: {
// 自动检测React的版本,从而进行规范代码
react: {
pragma: 'React',
version: 'detect',
},
},
extends: [
// ESLint
'eslint:recommended',
// Typescript
'plugin:@typescript-eslint/recommended'
],
parser: '@typescript-eslint/parser', // 解析 TypeScript 代码
parserOptions: {
parser: '@babel/eslint-parser', // 指定 eslint 可以解析 JSX 语法。
sourceType: 'module',
ecmaVersion: 'latest',
ecmaFeatures: {
jsx: true
},
},
plugins: ['react-refresh', '@typescript-eslint'],
rules: {
'semi': "off", // 不强制在语句末尾使用分号
'arrow-body-style': 'off', // 不强制在箭头函数体周围使用大括号
'no-console': 'off', // 允许使用 console 语句
'no-undef': 'off', // 允许使用未声明的变量
'no-shadow': 'off', // 允许变量覆盖声明
'react/jsx-uses-react': 'off', // 无须在组件中显式 import React
'react/react-in-jsx-scope': 'off', // 确保已经正确引入了 React 库
"@typescript-eslint/no-explicit-any": ["off"], // 允许使用 any 类型
},
};
~~~
~~~js
module.exports = {
root: true,
env: { browser: true, es2023: true },
extends: [
'plugin:react/recommended',
'plugin:react/jsx-runtime',
'plugin:react-hooks/recommended',
'eslint-config-mature/prettier', // 主要用于格式化模板,优先级应更低
'eslint-config-mature',
'eslint-config-mature/ts',
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: "latest",
ecmaFeatures: {
"jsx": true
},
sourceType: 'module',
project: ['./tsconfig.json', './tsconfig.node.json'],
tsconfigRootDir: __dirname,
},
plugins: ['react', 'react-refresh'],
rules: {
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
"settings": {
"react": {
"version": "detect"
}
}
}
~~~
然后,在根目录下新建 `.eslintignore` 文件,告诉 `eslint` 在进行规则校验时要跳过的文件和目录。
~~~diff
.idea
.vscode
node_modules
/build
/public
/docs
/bin
*.sh
*.md
*.woff
*.ttf
.husky
.local
*.d.ts
~~~
### prettier
`prettier` 专注于代码格式化,对代码不做质量检查。首先,在项目中安装 `prettier`:
~~~cmd
pnpm i prettier -D
~~~
创建 `.prettierrc.cjs` 配置文件,按自身需求添加[配置](https://link.juejin.cn/?target=https%3A%2F%2Fwww.prettier.cn%2Fdocs%2Foptions.html)。以下是我自己使用的部分配置:
~~~js
module.exports = {
printWidth: 100, // 单行长度
tabWidth: 2, // 缩进长度
useTabs: false, // 使用空格代替tab缩进
semi: false, // 句末使用分号
singleQuote: true, // 使用单引号
quoteProps: 'as-needed', // 仅在必需时为对象的key添加引号
jsxSingleQuote: true, // jsx中使用单引号
trailingComma: 'all', // 多行时尽可能打印尾随逗号
bracketSpacing: true, // 在对象前后添加空格-eg: { foo: bar }
jsxBracketSameLine: true, // 多属性html标签的‘>’折行放置
arrowParens: 'always', // 箭头函数单参数时包裹圆括号
requirePragma: false, // 无需顶部注释即可格式化
insertPragma: false, // 在已被preitter格式化的文件顶部加上标注
htmlWhitespaceSensitivity: 'ignore', // 对HTML全局空白不敏感
endOfLine: 'auto', // 不检查结束行形式
embeddedLanguageFormatting: 'auto', // 对引用代码进行格式化
};
~~~
接下来,新建 `.prettierignore` 文件,告诉 `prettier` 在代码格式化时要跳过的文件和目录。
~~~diff
# Ignore artifacts:
build
coverage
server
public
# Ignore all HTML files:
*.html
~~~
## 初始化样式
~~~cmd
pnpm install reset-css
~~~
~~~tsx
// main.tsx
import React from 'react'
import ReactDOM from 'react-dom/client'
// 初始化样式
import 'reset-css'
// 全局样式
import '@/styles/global.scss'
import App from '@/App.tsx'
ReactDOM.createRoot(document.getElementById('root')!).render(
)
~~~
~~~scss
// styles/global.scss
body {
background-color: $--gd-global-color;
box-sizing: border-box;
// 禁止文字选中
user-select: none;
}
img {
// 禁止拖动图片
--webkit-user-drag: none;
}
~~~
## 路由
~~~cmd
pnpm install react-router-dom
~~~
## 打包
- 如果在文件中指定了 `minify: terser`,需要安装 `terser` 插件
~~~cmd
pnpm i terser -D
~~~
~~~ts
import { defineConfig } from 'vite';
import * as path from 'path';
import react from '@vitejs/plugin-react';
// https://vitejs.dev/config/
export default defineConfig({
base: './',
publicDir: 'public',
resolve: {
alias: [
{ find: '@', replacement: path.resolve(__dirname, 'src') }, // 配置路径别名
],
},
build: {
target: 'modules', // 浏览器兼容目标
outDir: 'dist', // 打包输出路径
assetsDir: 'assets', // 静态资源存放路径
cssCodeSplit: true, // 允许 css 代码拆分
sourcemap: false, // 不生成 sourceMap 文件
minify: 'terser', // 缩小文件体积
terserOptions: {
compress: {
drop_console: true, // 取消 console
drop_debugger: true, // 取消 debugger
},
},
},
server: {
host: 'localhost', // 指定监听的IP地址
port: 3333, // 指定服务器端口
open: true, // 开发服务器启动时,自动在浏览器打开
strictPort: false, // 若端口已被占用,就尝试下一个可用端口
https: false, // 不开启 https 服务
cors: true, // 允许跨域
// 配置代理
proxy: {
'/api': {
target: 'http://127.0.0.1: 8000', // 接口地址
changeOrigin: true, // 接口跨域
secure: false, // 启用 https 服务时需要配置
},
},
},
plugins: [react()],
});
~~~