node版本: v18.18.0
使用yarn作为包管理工具
技术选型选用 react 18、react-router v6、redux、ant design、vite、typescript 组合。
下载插件:
在实际开发过程中,由于这些组件和函数组件它的结构都是固定的,所以可以使用一些插件将其生成出来
使用rcc生成类组件,使用rfc生成函数组件
1. 搭建项目
1.1 create-vite 创建
使用命令
yarn create vite 或者 npm create vite
默认创建的就是 react18
1.2 配置别名 resolve.alias
步骤一: 在项目根目录tsconfig.json配置
compilerOptions对象下添加:
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
},
步骤二:在vue.config.ts(VUE CLI构建的项目)或vite.config.ts(VITE构建的项目)文件中添加。
安装
yarn add @types/node -D
在vite.config.ts配置
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
}
})
1.3 安装 css预处理环境less
只需要安装 less ,无需配置 即可在项目中使用
yarn add less
1.4 css module
要文件名 *.module.less 才行
2. 集成 react-router
yarn add react-router-dom
我这里用 BrowserRouter 组件
2.1 useRoutes 配置路由
react router dom v6 支持配置路由 useRoutes(hook)实现
在根目录新建views文件和router文件, 分别添加如下文件, 根据自己的情况配置,views用来存放项目中的所有路由文件,提前通过快捷键 rfc初始化函数组件
router/RouterIndex.tsx路由文件配置如下:
import { useRoutes,Navigate } from "react-router-dom";
import Home from "@/views/manage/Home";
import User from "@/views/manage/User";
import Role from "@/views/manage/Role";
import Login from '@/views/Login'
import Register from "@/views/Register";
function RouterIndex() {
let element = useRoutes([
{
path: "/home",
element: <Home />,
children: [
{ path: "user", element: <User />, },
{ path: "role", element: <Role /> },
],
},
{ path: "/", element: <Navigate to={'/home'} /> },
{ path: "login", element: <Login /> },
{ path: "reg", element: <Register /> },
]);
return element;
}
export default RouterIndex
Home组件是父路由, 只要是父路由, 必须在页面中有用来渲染子路由的容器Outlet
然后在根组件中引入RouterIndex组件
import {BrowserRouter,NavLink} from 'react-router-dom'
import RouterIndex from './router/RouterIndex'
function App() {
return (
<BrowserRouter>
<ul className='menu-list'>
<NavLink to={'/login'}>登录</NavLink>
<NavLink to={'/reg'}>注册</NavLink>
<NavLink to={'/'}>系统页面</NavLink>
</ul>
<RouterIndex></RouterIndex>
</BrowserRouter>
)
}
export default App
说明: 必须用BrowserRouter包裹住所有跟路由相关的组建
react router dom v6 已经抛弃了 Switch(组件),改用 Routes(组件),写组件路由需要注意
然后就可以正常在浏览器访问到路由中配置的页面了
2.2 配置路由的懒加载
router/RouterIndex.tsx修改如下:
import {Suspense,lazy} from 'react'
import { useRoutes,Navigate } from "react-router-dom";
// 路由懒加载
const modules = import.meta.glob('../views/**/*.tsx')
export const lazyFun = (pathName: string) => {
let comFun = modules['../views/' + pathName+'.tsx']
const ComName = lazy(comFun as any);
return <Suspense fallback={<div>loading</div>}><ComName></ComName></Suspense>
}
function RouterIndex() {
let element = useRoutes([
{
path: "/home",
element: lazyFun('manage/Home'),
children: [
{ path: "user", element: lazyFun('manage/User') },
{ path: "role", element: lazyFun('manage/Role') },
],
},
{ path: "/", element: <Navigate to={'/home'} /> },
{ path: "login", element: lazyFun('Login') },
{ path: "reg", element: lazyFun('Register') },
]);
return element;
}
export default RouterIndex
项目中可正常使用了
2.3 路由传参
react router v6 获取传参需要用两个 hook,分别是 useParams(params)和 useSearchParams(search)
1.search传参模式,常用
navigate('/layout/home?name=alice&id=99')
接收search模式的参数
import {useSearchParams} from 'react-router-dom'
export default function Home() {
const [searchArr] = useSearchParams();
console.log(searchArr.get('name'),'name'); // 获取name这个参数
console.log(searchArr.get('id'),'id');
// 可以给获取到的某个参数再添加值
searchArr.append('name','11111')
console.log(searchArr.getAll('name'));
return (
<div>Home</div>
)
}
注意:useSearchParams(), 解构的时候需要使用中括号, 获取某个search类型的参数,使用get
2.params传参模式
动态理由配置
路由里配置
{
path: '/about/:id',
element: <About></About>
}
注意:配置动态路由的时候, 使用:+key名, 与vue动态配置路由一样
跳转的时候
const navigate = useNavigate();
navigate('/about/1')
获取params参数
import {useParams} from 'react-router-dom'
...
const paramsObj = useParams();
console.log(paramsObj,'参数');
...
3.state模式传参
useLocation接收参数
navigate('/layout/users',{
state:{
age:20,
username:'alice'
}
})
接收参数, 通过useLocation
import React from 'react'
import {useLocation} from 'react-router-dom'
export default function User() {
const obj = useLocation();
console.log(obj); //接收state模式的参数
return (
<div>User </div>
)
}
3.页面系统布局
这里使用左右布局演示
3.1 根目录新建components, 添加Header.tsx和Menus.tsx组件
Menus.tsx组件如下:
import React from 'react'
import {NavLink} from 'react-router-dom'
export default function Menus() {
return (
<div><ul className='menu-list'>
<NavLink to={'/home/user'}>用户</NavLink>
<NavLink to={'/home/role'}>角色</NavLink>
</ul></div>
)
}
3.2 系统框架组件Home.tsx配置如下:
import { Outlet } from 'react-router-dom'
import Menus from '@/components/Menus'
import Header from '@/components/Header'
export default function Home() {
return (
<div className='manage-outer'>
<div className='sider-left'>
<Menus></Menus>
</div>
<div className='content'>
<Header></Header>
<Outlet></Outlet>
</div>
</div>
)
}
布局样式文件参考:
.menu-list {
display: flex;
flex-direction: column;
a { margin-right: 10px;}
}
.manage-outer {
display: flex;
height: 100vh;
.sider-left {
flex-basis: 200px;
flex-shrink: 0;
border-right: 1px solid blue;
}
.content {
flex-grow: 1;
}
}
目前整体布局完成
3.2 useOutletContext 子路由状态共享
// 父路由组件
function Parent() {
const [count, setCount] = React.useState(0);
return <Outlet context={[count, setCount]} />;
}
// 子路由组件
import { useOutletContext } from "react-router-dom";
function Child() {
const [count, setCount] = useOutletContext();
const increment = () => setCount((c) => c + 1);
return <button onClick={increment}>{count}</button>;
}
4.axios的使用
4.1 安装axios
yarn add axios
在src目录下新建utils/request.ts,
4.2 axios封装配置如下:
import axios from 'axios'
import type { AxiosResponse } from 'axios'
interface IAxiosProps {
code: number,
msg: string,
data: any,
rows:any,
}
declare module 'axios' {
interface AxiosInstance {
(config: AxiosRequestConfig): Promise<IAxiosProps>
}
}
const fetchData = axios.create({
baseURL: 'http://127.0.0.1:3000',
timeout: 5000
})
fetchData.interceptors.request.use(res => {
// res.headers.Authorization = 'Bearer ' + localStorage.getItem('token')
return res
})
fetchData.interceptors.response.use((res: AxiosResponse<any, any>) => {
return res.data
}, err => {
return Promise.reject(err)
})
export default fetchData
4.3 项目中使用
import fetchData from '@/utils/request';
....
fetchData({
url:'/api/getCHCSRouterByUserId',
method:"post",
data:{
userId:userid
}
}).then((res)=>{
console.log(res);
})
4.4 反向代理处理跨域
如果后端已经设置了cors跨域, 可以直接在axios实例化的时候直接使用 baseURL配置;
如果为了安全考虑,接口不支持跨域, 需要在本地开发的时候做反向代理,
使用反向代理就不需要在axios的配置中设置baseURL
配置如下:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { fileURLToPath, URL } from 'node:url'
import requireTransform from 'vite-plugin-require-transform';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
react(),
requireTransform({
fileRegex: /.ts$|.tsx$/
}),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},
server:{
host:'0.0.0.0',
port:8081, //端口号
open:true, //是否项目启动就在浏览器打开
proxy:{
// '/test':{
// target:'https://test.iot.store',//后端真实服务器地址
// },
//看到: http://localhost:8081/api/getAllChcsService
// 实际: http://127.0.0.1:3000/api/getAllChcsService
'/api':{
target:'http://127.0.0.1:3000',//后端真实服务器地址
changeOrigin:true,//允许跨域
}
}
}
})
5.引入antd UI框架
5.1 安装antd
yarn add antd
页面中按需引入antd组件
import {Button} from 'antd'
...
<Button type="primary">Button</Button>
...
5.2 定制主题色
统一定制系统的主题色, 需要在根组件 app.jsx配置
import {ConfigProvider} from 'antd'
<ConfigProvider theme={{
token:{
colorPrimary:'#ff6600'
}
}}>
{路由配置}
</ConfigProvider>
然后系统中所有的关于 primary类型的颜色, 统一会改变
5.3 国际化语言
antd 目前的默认文案是英文,如果需要使用其他语言,可以参考下面的方案。
ConfigProvider
antd 提供了一个 React 组件 ConfigProvider 用于全局配置国际化文案。
import zhCN from 'antd/locale/zh_CN';
return (
<ConfigProvider locale={zhCN}>
<App />
</ConfigProvider>
);
注意:zh_CN 是文件名,以下表格也遵循同样的规则。
6.二次封装Menu菜单组件
为了项目的健壮性, 支持菜单的多级级联显示, 可以使用递归的思想进行封装, Menus.tsx组件整体代码如下:
import {useEffect, useState} from 'react'
import {useNavigate} from 'react-router-dom'
import {Menu} from 'antd'
const menusOld = [{
name:'用户',
path:'/home/user',
children:[{
name:'线上用户',
path:'/home/user/online',
},{
name:'线下用户',
path:'/home/user/noline',
}]
},{
name:'角色',
path:'/home/role'
}]
export default function Menus() {
const [menuList, setMenuList] = useState([])
const navigate = useNavigate();
function formatMenu(menus:any){
return menus.flatMap((item:any)=>{
return {
key:item.path,
// icon:item.icon,
label:item.name,
children: item.children && formatMenu(item.children)
}
})
}
const selectMenu = (key: any) => {
navigate(key.key)
}
useEffect(()=>{
const menusAll = formatMenu(menusOld) // 格式化菜单
setMenuList(menusAll)
},[])
return (
<Menu
mode="inline"
items={menuList}
onClick={selectMenu}
defaultSelectedKeys={[location.pathname]}
/>
)
}
说明: menusOld在项目中是需要通过接口获取到项目所有菜单的,主要通过formatMenu对菜单进行递归,得到Menu组件需要的菜单格式
目前效果如下:
7. 项目中使用redux
- 单一数据源
- state 是只读的
- 使用纯函数来执行修改
这里通过react-redux演示使用
7.1 安装redux
yarn add redux
7.2 创建store仓库
在src目录下新建store/index.js, 并创建仓库
import {legacy_createStore as createStore} from 'redux'
//使用别名 方便使用
createStore接收2个参数
createStore创建仓库
第一个参数: reducer函数, 用来管理仓库中的数据, 类似vuex中的mutaions函数
第二个参数: 是redux仓库中的初始数据
let store = createStore(reducerFun,仓库中初始数据)
functuon countReducer(state,action){
return state;
}
let store = createStore(countReducer,{count:10});
export default store;
这里提供一个比较全的store的配置
import {legacy_createStore as createStore,combineReducers} from 'redux';
// 仓库的模块
interface IStoreModule {
user:{
permissions:any,
userInfo:any,
menuList:any
}
}
interface IActionProps {
type:string,
payload:any
}
const initData:IStoreModule = {
user:{
permissions:[], // 按钮级别的权限
userInfo:{},
menuList:[]
}
}
function userReducer(state=initData.user,action:IActionProps){
switch(action.type) {
case 'user/update':
state.permissions = action.payload.permissions;
state.userInfo = action.payload.userInfo;
return {...state}
case 'user/menus':
state.menuList = action.payload;
return {...state}
}
return state
}
const reducerFun = combineReducers({
user:userReducer
})
const store = createStore(reducerFun);
export default store;
7.3 安装react-redux
为了能使用useSelector,useDispatch 获取仓库中的数据以及修改仓库中的数据, 需要安装react-redux
7.3.1安装
yarn add react-redux
7.3.2 Provider派发数据
在入口文件main.tsx中派发store, 以供其他组件可以获取数据
import {Provider} from 'react-redux'
import store from './store/index.ts'; // 7.2步导出的store
<Provider store={store}>
<BrowserRouter>
<App />
</BrowserRouter>
</Provider>
7.4 项目中实时获取仓库中的数据
import {useSelector,useDispatch} from 'react-redux'
const menuList = useSelector((state:any)=>{
return [...state.user.menuList]
})
7.5 项目中派发数据
import {useSelector,useDispatch} from 'react-redux'
...
const dispatch = useDispatch();
const sendData = ()=>{
// res.data是接口中获取的数据
dispatch({
type:'user/update',
payload:{
permissions:res.data.permissions,
userInfo:res.data.user
}
})
}
。。。。
7.6 数据的持久化
借助插件 redux-persist
1. 安装
yarn add redux-persist
2.配置持久化
2.1 在store/index.js文件配置
import {persistReducer} from 'redux-persist'
// 存储机制,可换成其他机制,当前使用sessionStorage机制
import storageSession from 'redux-persist/lib/storage/session'
// 持久化配置
let storeConfig = {
key:'root',
storage:storageSession,
blacklist:['todo'] // reducer里不想持久化的名单, 是模块化的名字
}
persistReducer函数用来持久化所有的reducers函数, 接收2个参数, 第一个就是 持久化配置 storeConfig, 第二个是合并后的reducer函数
说明: createStore创建的仓库的时候, 第二参数初始化数据非必填, 但是不填的时候, 必须要在reducer函数中初始化的时候加上, 比如
function countReducer(state=storeData.count,action){
switch(action.type){
case 'count/add':
state.num += action.payload;
return {...state} // react中数据不可变性, 必须通过拷贝一个新的对象,不能直接修改原对象
default : return state
}
}
2.2 在入口文件main.jsx中添加PersistGate 配置
import store from './store/index.js'
import {Provider} from 'react-redux'
// 持久化
import {PersistGate} from 'redux-persist/lib/integration/react'
import {persistStore} from 'redux-persist'
const persistor = persistStore(store) //使用persistStore持久化store
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<App />
</PersistGate>
</Provider>
PersistGate:接收persistor这个api的目的是持久化store
7. 环境变量的配置
可能有多个环境, 一般情况会有开发环境, 测试环境, 线上环境, 每个环境对应的服务域名不一样, 然后配置后 可以方便不同环境的接口调用
配置步骤:
第一步: 在项目根目录新建环境文件 名字 .env+环境名字
比如这里有3个环境
.env.dev:开发环境
.env.test:测试环境
.env.pro:线上环境
每个环境配置相应的变量,以 .env.dev:开发环境
VITE_APP_URL = http://localhost:8002
VITE_IMG_SERVER = http://localhost:8002
注明: 以Vite创建的项目, 环境变量名必须是 VITE开头, VITE_自定义名字
第二步:在项目根目录的文件package.json 配置 mode模式, mode第一步中的环境名字
"scripts": {
"dev": "vite --mode dev",
"build": "vite build --mode pro",
"test": "vite build --mode test",
"lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
第三步:服务重启后, 通过 import.meta.env访问环境变量,然后做相应配置
console.log(import.meta.env);
// 因为可能有多个服务 - 》 多个域名接口
// 一般需要实例化一个axios对象
const axiosEl = axios.create({
baseURL:import.meta.env.VITE_APP_URL,
timeout:5000
})
特别说明: env环境提供了2个默认mode, 开发 development, 线上production , 如果你在第一步 env.development, 可以省略第二步中 mode 中dev的配置
8.打包优化
项目开发完毕, 需要通过npm run build上线, 但是有些第三方的包跟业务没有关系, 可以单独打包做cdn缓存, 提高网页加载速度
修改配置文件:vite.config.ts
build:{
chunkSizeWarningLimit: 1600, // 如果你打包的时候, 有文件超过了 1600k, 项目打包户提示错误
rollupOptions:{
output:{
// echarts , react-dom , react, axios
manualChunks:{
echarts:['echarts','react-echarts-core'],
react:['react','react-dom'],
axios:['axios']
}
}
}
},
整体vite.config.ts文件的整体代码参考:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { fileURLToPath, URL } from 'node:url'
import requireTransform from 'vite-plugin-require-transform';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
react(),
requireTransform({
fileRegex: /.ts$|.tsx$/
}),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},
build:{
chunkSizeWarningLimit: 1600, // 如果你打包的时候, 有文件超过了 1600k, 项目打包户提示错误
rollupOptions:{
output:{
// echarts , react-dom , react, axios
manualChunks:{
echarts:['echarts','react-echarts-core'],
immer:['immer','immutable','use-immer'],
react:['react','react-dom'],
axios:['axios']
}
}
}
},
server:{
host:'0.0.0.0',
port:8081,
open:true,
proxy:{
// '/test':{
// target:'https://test.iot.store',//后端真实服务器地址
// },
//看到: http://localhost:8081/api/getAllChcsService
// 实际: http://127.0.0.1:3000/api/getAllChcsService
'/api':{
target:'http://127.0.0.1:3000',//后端真实服务器地址
changeOrigin:true,//允许跨域
}
}
}
})
9.上线部署
通过npm run build打包后的文件, 可以通过nginx进行部署;
如果路由选择的是hash,不需要在nginx里面配置;
如果路由选择是history模式, 需要在nginx里做反向代理的配置;
history模式打包后想要使用正常访问的话,需要后端服务器进行配置才可以,否则可能会出现刷新后404的问题。一般情况下,服务器端使用nginx服务器进行配置。
server {
listen 9903;
server_name localhost;
location / {
root 'G:\dist';
index /index.html;
try_files $uri $uri/ /index.html;
}
}
如果是history模式, 上线的时候必须设置 try_files ,不然页面刷新的时候 会包404