一、项目搭建
1、创建项目
-
使用vite生成项目
npx create-react-app react-ts-project --template typescript
-
启动项目
yarn start
-
删除无用组件
2、设计目录结构
资源 | 说明 |
---|---|
http | 网络请求 |
assets | 公共资源 |
components | 组件 |
router | 路由配置 |
utils | 工具模块 |
store | 状态机 |
App.tsx | 应用根组件 |
index.tsx | 入口ts文件 |
3、配置sass环境
-
安装sass包
yarn add sass
-
创建全局样式文件
4、craco插件配置
如果要修改CRA的默认配置,有以下几种方案
-
通过第三方库来修改,比如@craco/craco
-
通过执行
yarn eject
命令,释放react-scripts
中的所有配置到项目中
实现步骤
-
安装修改CRA的配置的包
yarn add -D @craco/craco
-
在项目的根目录中创建craco的配置文件(craco.config.js),并在配置中配置别名
const path=require('path')
module.exports={
webpack:{
alias:{
'@':path.resolve(__dirname,'src')
}
}
}
-
修改packge.json中的脚本命令
"scripts": {
"start": "craco start",
"build": "craco build",
"test": "craco test",
"eject": "react-scripts eject"
}
-
在代码中,就可以通过@来表示src目录的绝对路径
-
重启项目,让配置生效
@别名路径提示
-
在项目根目录创建tsconfig.json配置文件
-
在配置文件中添加如下配置
{
"compilerOptions": {
"baseUrl": "./",
"paths": {
"@/*": ["src/*"]
}
}
}
5、配置路由
-
安装路由
yarn add react-router-dom
yarn add react-loadable
yarn add @types/react-loadable
下载react-loadable依赖包进行路由懒加载,如果你是typescript,你还需要额外安装@types/react-loadable
这个依赖包
-
建立一个loadable.ts,放在src/utils/loadable.ts
import Loadable from 'react-loadable';
export default function withLoadable(comp:any) {
return Loadable({
//懒加载组件页面
loader: comp,
loading: () => null
})
}
-
在src下创建router目录,在该目录的index.tsx编写路由配置文件
import loadable from '@/utils/loadable'
import {RouteObject} from 'react-router-dom'
const Login=loadable(()=>import('@/views/Login'))
const Home=loadable(()=>import('@/views/Home'))
const Main=loadable(()=>import('@/views/Main'))
const Product=loadable(()=>import('@/views/Product'))
const Category=loadable(()=>import('@/views/Category'))
const routes:Array<RouteObject>=[
{
path:'/login',
element:<Login/>
},
{
path:'/',
element:<Home/>,
children:[
{
index:true,
element:<Main></Main>
},
{
path:'product/list',
element:<Product></Product>
},
{
path:'product/category',
element:<Category></Category>
}
]
}
]
export default routes
-
在App.tsx中通过useRoutes钩子函数来进行集中式配置
import React,{Suspense} from 'react'
import {useRoutes} from 'react-router-dom'
import routes from '@/router'
export default function App() {
return (
<Suspense fallback={<>loading</>}>
{useRoutes(routes)}
</Suspense>
)
}
-
在项目根目录下的index.tsx中使用
<BrowserRouter>
包裹<App>
root.render(
<BrowserRouter>
<App/>
</BrowserRouter>
)
-
在src/Home.tsx组件中配置二级路由出口
import React from 'react'
import {Outlet} from 'react-router-dom'
export default function Home() {
return (
<div>
Home
<Outlet></Outlet>
</div>
)
}
6、安装AntD
-
安装AntD组件库
yarn add antd
二、用户登录
1、登录静态页面实现
-
首先在utils目录下创建type.ts文件,这个文件里主要编写接口
export interface IUser{
account:string,
password:string
}
-
在views目录下的Login.tsx文件中编写登录的静态页面
import {Button,Form,Input,message} from 'antd'
import api from '@/api'
import {useNavigate} from 'react-router-dom'
export default ()=>{
const nav=useNavigate()
const onFinish=async(values:any)=>{
console.log(values)
}
return (
<Form onFinish={onFinish} style={{ maxWidth: 600 }}>
<Form.Item label='用户名' name="username">
<Input/>
</Form.Item>
<Form.Item label='密码' name="password">
<Input.Password/>
</Form.Item>
<Form.Item>
<Button type="primary" htmlType='submit'>登录</Button>
</Form.Item>
</Form>
)
}
2、axios的二次封装
import axios,{ InternalAxiosRequestConfig,AxiosResponse,AxiosError} from "axios";
import {message} from 'antd'
//设置根路径
axios.defaults.baseURL="http://www.zhaijizhe.cn:3005"
//设置请求拦截器
axios.interceptors.request.use((config: InternalAxiosRequestConfig)=>{
const token=localStorage.getItem('token')
if(token){
config.headers.Authorization=token
}
return config
})
//设置响应拦截器
axios.interceptors.response.use((response:AxiosResponse)=>{
return response.data
},(error:AxiosError)=>{
switch(error.response?.status){
case 500:
message.error('服务端出现500错误')
break
case 401:
message.error('服务端出现400错误')
break;
case 404:
message.error("没有找到服务端相应资源");
break;
}
return Promise.reject(error)
})
export default axios
3、登录功能的实现
-
在src/types文件夹下编写接口
export default interface IUser{
username:string
password:string
}
-
编写登录的后端请求API
import request from '@/utils/request'
import IUser from '@/types/IUser'
export default{
login:(user:IUser)=>request.post('/users/login',user)
}
-
将用户模块汇总到api.tsx中
import users from "./modules/users"
export default{
users
}
-
在登录组件中调用登录API,完成登录功能
import {Button,Form,Input,message} from 'antd'
import api from '@/api'
import {useNavigate} from 'react-router-dom'
export default ()=>{
const nav=useNavigate()
const onFinish=async(values:any)=>{
const result=await api.users.login(values)
if(result.data.code){
localStorage.setItem('token',result.data.token)
nav('/')
}else{
message.warning('登录失败')
}
}
return (
<Form onFinish={onFinish} style={{ maxWidth: 600 }}>
<Form.Item label='用户名' name="username">
<Input/>
</Form.Item>
<Form.Item label='密码' name="password">
<Input.Password/>
</Form.Item>
<Form.Item>
<Button type="primary" htmlType='submit'>登录</Button>
</Form.Item>
</Form>
)
}
三、路由权限
1、静态菜单
import {Outlet,NavLink,useNavigate} from 'react-router-dom'
import React,{useState,useEffect} from 'react'
import '@/assets/css/home.scss'
import {Layout,Menu} from 'antd'
import {WindowsOutlined,TrademarkCircleOutlined,UserOutlined} from '@ant-design/icons'
const {Header,Sider,Content,Footer}=Layout
export default ()=>{
const[menuList,setMenuList]=useState<any>([])
const nav=useNavigate()
useEffect(()=>{
const list=[
{
key:'sub1',
label:'日常业务',
icon:<WindowsOutlined />,
children:[
{
label:'学员管理',
key:'/students'
},
{
label:'班级管理',
key:'/classes'
}
]
},
{
key:'sub2',
label:'校区管理',
icon:<TrademarkCircleOutlined />,
children:[
{
label:'教师管理',
key:'/teachers'
},
{
label:'班主任管理',
key:'/directors'
},
{
label:'专业管理',
key:'/subjects'
}
]
},
{
key:'sub3',
label:'系统管理',
icon:<UserOutlined />,
children:[
{
key:'/users',
label:'用户管理'
}
]
}
]
setMenuList(list)
},[])
const go=(item:any)=>{
nav(item.key)
}
return (<>
<Layout>
<Header>
<div>蜗牛BOSS管理系统</div>
</Header>
<Layout style={{height:'750px'}}>
<Sider>
<Menu
items={menuList}
onClick={go}
mode="inline"
theme="dark"
defaultOpenKeys={['sub1','sub2']}
defaultSelectedKeys={['/home/students']}></Menu>
</Sider>
<Content>
{/* 设置子路由出口 */}
<Outlet></Outlet>
</Content>
</Layout>
<Footer style={{ textAlign: 'center'}}>Ant Design ©2023 Created by Ant UED</Footer>
</Layout>
</>
)
}
2、动态菜单
-
在api/modules/users下编写获取权限菜单的接口
import request from '@/utils/request'
export default{
getAuthMenus:()=>request.get('/menus/getAuthMenus')
}
-
在Home.tsx中中调用getAuthMenus接口来完成权限菜单数据的获取
useEffect(() => {
getAuthMenus()
}, [])
const getAuthMenus = async () => {
const result = await api.users.getAuthMenus()
console.log(result.data.data)
const rList = transformDataToMenus(result.data.data)
setMenuList(rList)
}
-
将后台的权限菜单数据转成antd格式的菜单数据
/**
* 将后台的权限菜单数据转成antd格式的菜单数据
*/
interface IMenuData {
title: string
path: string
icon: string
children?: Array<IMenuData>
}
interface IMenu {
label: string
key: string
icon: ReactElement
children?: Array<IMenu>
}
const transformDataToMenus = (list: Array<IMenuData>) => {
return list.map((item: IMenuData) => {
let name = item.icon as string
let iis: any = icons[name as keyof typeof icons]
let menuItem: IMenu = { label: item.title, key: item.path, icon: React.createElement(iis) }
if (item.children) {
menuItem.children = transformDataToMenus(item.children)
}
return menuItem
})
}
-
渲染导航列表
<Menu
items={menuList}
mode="inline"
theme="dark"
onClick={goNav}>
</Menu>
3、动态添加路由表
1、安装和配置RTX
在终端执行
yarn add redux
yarn add react-redux
yarn add @reduxjs/toolkit
在src目录下创建store文件夹,目录结构如下
src
|-store
|-moudles
|-xx.tsx
|-yy.tsx
|-index
2、创建slice模块
在src/store/reducers目录下创建routeReducer.tsx
import {createSlice,PayloadAction,Dispatch} from '@reduxjs/toolkit'
import {RouteObject} from 'react-router-dom'
import api from '@/api'
import {withLoadable} from '@/utils/loadable'
interface IAuthMenu{
rows:Array<any>
}
const initialState:IAuthMenu={
rows:[]
}
export const routesSlice=createSlice({
name:'getAuthMenu',
initialState,
reducers:{
setAuthRoutes(state,{payload}:PayloadAction<any[]>){
state.rows=payload
}
}
})
export const getAuthRoutesAsync=(routes:any)=>{
return async(dispath:Dispatch)=>{
console.log('----网络请求-----------');
let result=await api.users.getAuthMenus()
console.log('aaaa',result.data.data)
result.data.data.forEach((item:RouteObject)=>{
if(item.children){
item.children.forEach((subItem:RouteObject)=>{
console.log('ss',subItem)
const Module=withLoadable(()=>import(`@/views${subItem.path}`))
routes[1].children.push({
path:subItem.path,
element:<Module/>
})
})
}
})
dispath(setAuthRoutes(routes))
}
}
export const {setAuthRoutes}=routesSlice.actions
export default routesSlice.reducer
3、创建store对象
import {configureStore} from '@reduxjs/toolkit'
import menuReducer from './reducers/menus'
const store=configureStore({
reducer:{
menuReducer
}
})
export default store
// Infer the `RootState` and `AppDispatch` types from the store itself
export type RootState = ReturnType<typeof store.getState>
// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}
export type AppDispatch = typeof store.dispatch
4、创建hooks文件
在src/store目录下新建hooks文件
import { useDispatch, useSelector } from 'react-redux'
import type { TypedUseSelectorHook } from 'react-redux'
import type { RootState, AppDispatch } from './index'
// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch: () => AppDispatch = useDispatch
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
Usage With TypeScript | Redux 中文官网
5、全局注册store
import ReactDOM from 'react-dom/client';
import App from '@/App'
import {BrowserRouter} from 'react-router-dom'
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<BrowserRouter><App /></BrowserRouter>
);
6、在组件中使用
import { Outlet, NavLink } from 'react-router-dom'
import '@/assets/css/home.scss'
import { Layout,Menu } from 'antd'
import React,{ useEffect, useState } from 'react'
import {WindowsOutlined,TrademarkCircleOutlined,UserOutlined} from '@ant-design/icons'
//导入api
import api from '@/api'
//导入接口
import IMenuData from '@/types/IMenuData'
import IMenu from '@/types/IMenu'
import IMenuFun from '@/types/IMenuFun'
//导入图标
import * as icons from '@ant-design/icons'
//导入useSelector和useDispatch
// import {useSelector,useDispatch} from 'react-redux'
//使用ts的话,hooks使用的是useAppSelector来代替useSelector,使用useAppDispatch来替换useDispatch
import {useAppSelector,useAppDispatch} from '@/store/hooks'
//导入通知对象
import {getRoutesAsync} from '@/store/modules/routesReducer'
//导入routes
import routes from '@/router'
import {useNavigate} from 'react-router-dom'
const { Header, Sider, Content, Footer } = Layout
export default () => {
const[menuList,setMenuList]=useState<Array<any>>([])
useEffect(()=>{
getAuthMenus()
},[])
const dispatch=useAppDispatch()
const nav=useNavigate()
/**
* 通过token向后端服务端获取权限数据
*/
const getAuthMenus=async()=>{
const result=await api.users.getAuthMenus()
//进行转换
const list:Array<IMenu>=transformDataToMenu(result.data.data)
setMenuList(list)
//向状态机发送通知操作路由表,关键代码
dispatch(getRoutesAsync(routes))
}
//代码略
const transformDataToMenu:IMenuFun=(list:Array<IMenuData>)=>{
}
//进行路由跳转的方法
const go=(values:any)=>{
nav(values.key)
}
return (
<Layout>
<Header style={{ color: '#fff' }}>蜗牛BOSS系统</Header>
<Layout style={{ height: '750px' }}>
<Sider>
<Menu
onClick={go}
items={menuList}
theme="dark"
mode="inline">
</Menu>
</Sider>
<Content>
<Outlet></Outlet>
</Content>
</Layout>
<Footer>©版权前端10期班所有</Footer>
</Layout>
)
}
7、App.tsx
import {useRoutes} from 'react-router-dom'
import routes from '@/router'
import {useAppSelector} from '@/store/hooks'
import {useEffect,useState} from 'react'
export default ()=>{
const rows=useAppSelector((state)=>{
return state.routesReducer.rows
})
const[rout,setRout]=useState(routes)
useEffect(()=>{
console.log('********')
console.log('rows',rows)
setRout(rows)
},[rows])
return (<>
{useRoutes(rows.length==0?routes:rout)}
</>)
}