项目制作流程

一、使用 CRA 创建项目

javascript 复制代码
npx create-react-app name

二、按照业务规范整理项目目录 (重点src目录)

三、安装插件

javascript 复制代码
npm install sass -D

npm install antd --save

npm install react-router-dom

四、配置基础路由 Router

  1. 安装路由包 react-router-dom

  2. 准备两个基础路由组件 Layout 和 Login

  3. 在 router/index.js 文件中引入组件进行路由配置,导出 router 实例

  4. 在入口文件中渲染 <RouterProvider />,传入 router 实例

router/index.js

javascript 复制代码
import { createBrowserRouter } from "react-router-dom";

// 配置路由实例
const router = createBrowserRouter([
  {
    path: "/",
    element: <Layout />,
  },
  {
    path: "/login",
    element: <Login />,
  },
]);

export default router;
javascript 复制代码
import { RouterProvider } from "react-router-dom";
import router from "./router";

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<RouterProvider router={router} />);

五、登录

使用 AntD 现成的组件 创建登录页的内容结构

主要组件:Card、Form、Input、Button

html 复制代码
    <div>
      <Card>
        <img  />
        {/* 登录表单 */}
        <Form>
          <Form.Item>
              <Input />
          </Form.Item> 
          <Form.Item>
              <Input />
          </Form.Item> 
        </Form>
      </Card>
    </div>

1. 表单校验实现

表单校验可以在提交登录之前,校验用户的输入是否符合预期,如果不符合就阻止提交,显示错误信息

javascript 复制代码
    <Form.Item
      label="Username"
      name="username"
      rules={[{ required: true, message: '请输入用户名!' }]}
    >
      <Input size="large" placeholder="请输入手机号" />
    </Form.Item>

增加失焦时校验

html 复制代码
<Form validateTrigger="onBlur">
    ...
</Form>

手机号为有效格式

html 复制代码
          <Form.Item
            name="mobile"
            // 多条校验逻辑 先校验第一条 第一条通过之后再校验第二条
            rules={[
              {
                required: true,
                message: '请输入手机号',
              },
              {
                pattern: /^1[3-9]\d{9}$/,
                message: '请输入正确的手机号格式'
              }
            ]}>
            <Input size="large" placeholder="请输入手机号" />
          </Form.Item>

2. 获取表单数据

当用户输入了正确的表单内容,点击确认按钮时,需要收集用户输入的内容,用来提交接口请求

解决方案:给 Form 组件绑定 onFinish 回调函数,通过回调函数的参数获取用户输入的内容

javascript 复制代码
  const onFinish = async (values) => {
    console.log(values)
  }


  <Form onFinish={onFinish} validateTrigger="onBlur">
      ...
          <Form.Item>
            <Button type="primary" htmlType="submit" size="large" block>
              登录
            </Button>
          </Form.Item>
  </Form>

3. 封装 request 请求模块

在整个项目中会发送很多网络请求,使用 axios 三方库做好统一封装,方便统一管理和复用

javascript 复制代码
npm i axios

utils/request.js

javascript 复制代码
// axios的封装处理
import axios from "axios"

// 1. 根域名配置
// 2. 超时时间
// 3. 请求拦截器 / 响应拦截器

const request = axios.create({
  baseURL: 'http://geek.itheima.net/v1_0',
  timeout: 5000
})

// 添加请求拦截器
// 在请求发送之前 做拦截 插入一些自定义的配置 [参数的处理]
request.interceptors.request.use((config) => {
  return config
}, (error) => {
  return Promise.reject(error)
})

// 添加响应拦截器
// 在响应返回到客户端之前 做拦截 重点处理返回的数据
request.interceptors.response.use((response) => {
  // 2xx 范围内的状态码都会触发该函数。
  // 对响应数据做点什么
  return response.data
}, (error) => {
  // 超出 2xx 范围的状态码都会触发该函数。
  // 对响应错误做点什么
  return Promise.reject(error)
})

export { request }

utils/index.js

javascript 复制代码
// 统一中转工具模块函数
// import {request} from '@/utils'

import { request } from './request'

export {
  request,
}

4. 使用 redux 管理 token

Token 作为一个用户的标识数据,需要在很多个模块中共享,Redux 可以方便的解决共享问题

(1)redux 中编写获取 Token 的 异步获取和同步修改

(2)Login 组件负责提交 action 并且把表单数据传递过来

javascript 复制代码
npm i react-redux @reduxjs/toolkit

store/modules/user.js

javascript 复制代码
// 和用户相关的状态管理
import { createSlice } from '@reduxjs/toolkit'
import { setToken as _setToken, getToken } from '@/utils'

const userStore = createSlice({
  name: "user",
  // 数据状态
  initialState: {
    token: getToken() || '',
  },
  // 同步修改方法
  reducers: {
    setToken (state, action) {
      state.token = action.payload
      _setToken(action.payload)
    },
  }
})

// 解构出actionCreater
const { setToken } = userStore.actions

// 获取reducer函数
const userReducer = userStore.reducer

// 登录获取token异步方法封装
const fetchLogin = (loginForm) => {
  return async (dispatch) => {
    // 1. 发送异步请求
    const res = await request.post('authorizations', loginForm)
    // 2. 提交同步 action 进行 token 存入
    dispatch(setToken(res.data.token))
  }
}

export { fetchLogin, setToken }

export default userReducer

store/index.js

javascript 复制代码
// 组合redux子模块 + 导出store实例
import { configureStore } from '@reduxjs/toolkit'
import userReducer from './modules/user'

export default configureStore({
  reducer: {
    user: userReducer
  }
})

index.js

javascript 复制代码
import { RouterProvider } from 'react-router-dom'
import router from './router'
import { Provider } from 'react-redux'
import store from './store'

const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(
  <Provider store={store}>
    <RouterProvider router={router} />
  </Provider>
)

封装 localStorage - Token 持久化

现存问题:

Redux 存入 Token 之后如果刷新浏览器,Token 会丢失(持久化就是防止刷新时丢失 Token)

问题原因:

Redux 是基于浏览器内存的储存方式,刷新时状态恢复为初始值

utils/token.js

javascript 复制代码
// 封装基于ls存取删三个方法
const TOKENKEY = 'token_key'

function setToken (token) {
  return localStorage.setItem(TOKENKEY, token)
}

function getToken () {
  return localStorage.getItem(TOKENKEY)
}

function removeToken () {
  return localStorage.removeItem(TOKENKEY)
}

export {
  setToken,
  getToken,
  removeToken
}

utils/index.js

javascript 复制代码
// 统一中转工具模块函数
// import {request} from '@/utils'

import { request } from './request'
import { setToken, getToken, removeToken } from './token'

export {
  request,
  setToken,
  getToken,
  removeToken
}

pages/login/index.js

javascript 复制代码
import { useDispatch } from 'react-redux'
import { fetchLogin } from '@/store/modules/user'
import { useNavigate } from 'react-router-dom'

const Login = () => {
  const dispatch = useDispatch()
  const navigate = useNavigate()
  const onFinish = async (values) => {
    console.log(values)
    // 触发异步action fetchLogin
    await dispatch(fetchLogin(values))
    // 1. 跳转到首页
    navigate('/')
    // 2. 提示一下用户
    message.success('登录成功')
  }
 
  return (...)
}

5. Axios 请求拦截器注入 Token

Token 作为用户的一个标识数据,后端很多接口都会以它作为接口权限判断的依据;请求拦截器注入 Token 之后,所有用到 Axios 实例的接口请求都自动携带了 Token

utils/request.js

javascript 复制代码
import { getToken } from "./token"

// 添加请求拦截器
// 在请求发送之前 做拦截 插入一些自定义的配置 [参数的处理]
request.interceptors.request.use((config) => {
  // 操作这个config 注入token数据
  // 1. 获取到token
  // 2. 按照后端的格式要求做token拼接
  const token = getToken()
  if (token) {
    config.headers.Authorization = `Bearer ${token}`
  }
  return config
}, (error) => {
  return Promise.reject(error)
})

6. 使用 Token 做路由权限控制

有些路由页面的内容信息比较敏感,如果用户没有经过登录获取到有效 Token,是没有权限跳转的,根据 Token 的有无控制当前路由是否可以跳转,就是路由的权限控制

components/AuthRoute.js

javascript 复制代码
// 封装高阶组件
// 核心逻辑: 有token 正常跳转  无token 去登录

import { getToken } from '@/utils'
import { Navigate } from 'react-router-dom'

export function AuthRoute ({ children }) {
  const token = getToken()
  if (token) {
    return <>{children}</>
  } else {
    return <Navigate to={'/login'} replace />
  }
}

router/index.js

javascript 复制代码
import { createBrowserRouter } from 'react-router-dom'
import { AuthRoute } from '@/components/AuthRoute'

// 配置路由实例
const router = createBrowserRouter([
  {
    path: "/",
    element: <AuthRoute> <Layout /></AuthRoute>,
  },
  {
    path: "/login",
    element: <Login />
  }
])

export default router

六、Layout

1. 样式初始化

样式 reset

javascript 复制代码
npm install normalize.css

index.js

javascript 复制代码
import 'normalize.css'

const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(
  <Provider store={store}>
    <RouterProvider router={router} />
  </Provider>
)

index.scss

javascript 复制代码
html,
body {
  margin: 0;
  height: 100%;
}

#root {
  height: 100%;
}

2. 二级路由配置

(1)准备三个二级路由

(2)router 中通过 children 配置项进行配置

(3)Layout 组件中配置二级路由出口

router/index.js

javascript 复制代码
const router = createBrowserRouter([
  {
    path: "/",
    element: <AuthRoute> <Layout /></AuthRoute>,
    children: [
      {
        index: true,
        element: <Home />
      },
      {
        path: 'article',
        element: <Article />
      },
      {
        path: 'publish',
        element: <Publish />
      }
    ]
  },
  {
    path: "/login",
    element: <Login />
  }
])

export default router

pages/Layout/index.js

javascript 复制代码
    <Layout>
      <Header>
        ...
      </Header>
      <Layout>
        <Sider>
          <Menu></Menu>
        </Sider>
        <Layout style={{ padding: 20 }}>
          {/* 二级路由的出口 */}
          <Outlet />
        </Layout>
      </Layout>
    </Layout>

3. 点击菜单跳转路由

实现效果:点击左侧菜单可以跳转到对应的目标路由

思路分析:

(1)左侧菜单要和路由形成一一对应的关系

(2)点击时拿到路由路径,调用路由方法跳转(跳转到对应的路由下面)

具体操作:

(1)菜单参数 Item 中 key 属性换成路由的路由地址

(2)点击菜单时通过 key 获取路由地址跳转

pages/Layout/index.js

javascript 复制代码
const items = [
  {
    label: '首页',
    key: '/',
    icon: <HomeOutlined />,
  },
  {
    label: '文章管理',
    key: '/article',
    icon: <DiffOutlined />,
  },
  {
    label: '创建文章',
    key: '/publish',
    icon: <EditOutlined />,
  },
]

const GeekLayout = () => {
  const navigate = useNavigate()
  const onMenuClick = (route) => {
    const path = route.key
    navigate(path)
  }

  return (
    <Layout>
      <Header className="header">
       ...
      </Header>
      <Layout>
        <Sider>
          <Menu
            mode="inline"
            theme="dark"
            defaultSelectedKeys={['1']}
            onClick={onMenuClick}
            items={items}
            style={{ height: '100%', borderRight: 0 }}></Menu>
        </Sider>
        <Layout>
          {/* 二级路由的出口 */}
          <Outlet />
        </Layout>
      </Layout>
    </Layout>
  )
}
export default GeekLayout

4. 根据当前路由路径高亮菜单

实现效果:页面在刷新时可以根据当前的路由路径让对应的左侧菜单高亮显示

思路分析;

(1)获取当前 url 上的路由路径

(2)找到菜单组件负责高亮的属性,绑定当前的路由路径

javascript 复制代码
  // 反向高亮
  // 1. 获取当前路由路径
  const location = useLocation()
  console.log(location.pathname)
  const selectedkey = location.pathname


        <Sider width={200} className="site-layout-background">
          <Menu
            mode="inline"
            theme="dark"
           // 修改
            selectedKeys={selectedkey}
            onClick={onMenuClick}
            items={items}
            style={{ height: '100%', borderRight: 0 }}></Menu>
        </Sider>

5. 展示个人信息

关键问题:用户信息应该放到哪里维护?

和 Token 令牌类似,用户的信息通常很有可能在多个组件中都需要共享使用,所以同样应该放到Redux 中维护

(1)使用 Redux 进行信息管理

(2)Layout 组件中提交 action

(3)Layout 组件中完成渲染

store/modules/user.js

javascript 复制代码
// 和用户相关的状态管理
import { createSlice } from '@reduxjs/toolkit'
import { setToken as _setToken, getToken, removeToken } from '@/utils'
import { loginAPI, getProfileAPI } from '@/apis/user'

const userStore = createSlice({
  name: "user",
  // 数据状态
  initialState: {
    token: getToken() || '',
    userInfo: {}
  },
  // 同步修改方法
  reducers: {
    setToken (state, action) {
      state.token = action.payload
      _setToken(action.payload)
    },
    setUserInfo (state, action) {
      state.userInfo = action.payload
    },
  }
})

// 解构出actionCreater
const { setToken, setUserInfo } = userStore.actions

// 获取reducer函数
const userReducer = userStore.reducer

// 登录获取token异步方法封装
const fetchLogin = (loginForm) => {
  return async (dispatch) => {
    const res = await request.post('/authorizations', loginForm)
    dispatch(setToken(res.data.token))
  }
}

// 获取个人用户信息异步方法
const fetchUserInfo = () => {
  return async (dispatch) => {
    const res = await request.get('/user/profile')
    dispatch(setUserInfo(res.data))
  }
}

export { fetchLogin, fetchUserInfo }

export default userReducer

pages/Layout/index.js

javascript 复制代码
  // 触发个人用户信息action
  const dispatch = useDispatch()
  useEffect(() => {
    dispatch(fetchUserInfo())
  }, [dispatch])

  const name = useSelector(state => state.user.userInfo.name)

   
  <span className="user-name">{name}</span>

6. 退出登录

(1)提示用户是否确认要退出(危险操作,二次确认)

(2)用户确认之后清除用户信息(Token 以及其他个人信息)

(3)跳转到登录页(为下次登录做准备)

pages/Layout/index.js

javascript 复制代码
  // 退出登录确认回调
  const onConfirm = () => {
    console.log('确认退出')
    dispatch(clearUserInfo())
    navigate('/login')
  }
  ...
    <span className="user-logout">
       <Popconfirm title="是否确认退出?" okText="退出" cancelText="取消" onConfirm={onConfirm}>
          <LogoutOutlined /> 退出
      </Popconfirm>
    </span>

store/modules/user.js

javascript 复制代码
  // 同步修改方法
  reducers: {
    setToken (state, action) {
      state.token = action.payload
      _setToken(action.payload)
    },
    setUserInfo (state, action) {
      state.userInfo = action.payload
    },
    clearUserInfo (state) {
      state.token = ''
      state.userInfo = {}
      removeToken()
    }
  }
})

7. 处理 Token 失效

什么是 Token 失效?

为了用户的安全和隐私考虑,在用户长时间未在网络中做出任何操作且规定的失效时间到达之后,当前的 Token 就会失效。一旦失效,不能再作为用户令牌标识请求隐私数据

前端如何知道 Token 已经失效了?

通常在 Token 失效之后再去请求接口,后端会返回401状态码,前端可以监控这个状态,做后续的操作

Token 失效了前端做什么?

(1)在 axios 拦截中监控 401 状态码

(2)清除失效 Token, 跳转登录

utils/request.js

javascript 复制代码
import router from "@/router"

// 添加响应拦截器
// 在响应返回到客户端之前 做拦截 重点处理返回的数据
request.interceptors.response.use((response) => {
  // 2xx 范围内的状态码都会触发该函数。
  // 对响应数据做点什么
  return response.data
}, (error) => {
  // 超出 2xx 范围的状态码都会触发该函数。
  // 对响应错误做点什么
  // 监控 401 token失效
  console.dir(error)
  if (error.response.status === 401) {
    removeToken()
    router.navigate('/login')
   // 强制刷新
    window.location.reload()
  }
  return Promise.reject(error)
})

七、Home

1. Echarts 基础图表渲染

三方图表插件如何在项目中快速使用起来?

(1)按照图表插件文档中的"快速开始",快速跑起来 Demo

(2)按照业务需求修改配置项做定制处理

javascript 复制代码
npm install echarts

pages/Home/index.js

javascript 复制代码
// 柱状图组件
import * as echarts from 'echarts'
import { useEffect, useRef } from 'react'

const Home = () => {
  const chartRef = useRef(null)
  useEffect(() => {
    // 保证dom可用 才进行图表的渲染
    // 1. 获取渲染图表的dom节点
    const chartDom = chartRef.current

    // 2. 图表初始化生成图表实例对象
    const myChart = echarts.init(chartDom)

    // 3. 准备图表参数
    const option = {
      title: {
        text: title
      },
      xAxis: {
        type: 'category',
        data: ['Vue', 'React', 'Angular']
      },
      yAxis: {
        type: 'value'
      },
      series: [
        {
          data: [10, 40, 70],
          type: 'bar'
        }
      ]
    }
    // 4. 使用图表参数完成图表的渲染
    option && myChart.setOption(option)

  }, [title])

  return (
     <div ref={chartRef} style={{ width: '500px', height: '400px' }}></div>
  )
}

export default Home

2. Echarts 组件封装实现

pages/Home/index.js

javascript 复制代码
import BarChart from "./components/BarChart"

const Home = () => {

  return (
    <div>
      <BarChart title={'三大框架满意度'} />
      <BarChart title={'三大框架使用度'} />
    </div>
  )
}

export default Home

pages/Home/components/BarCharts.js

javascript 复制代码
// 柱状图组件
import * as echarts from 'echarts'
import { useEffect, useRef } from 'react'
// 1. 把功能代码都放到这个组件中
// 2. 把可变的部分抽象成prop参数

const BarChart = ({ title }) => {
  const chartRef = useRef(null)
  useEffect(() => {
    // 保证dom可用 才进行图表的渲染
    // 1. 获取渲染图表的dom节点
    const chartDom = chartRef.current

    // 2. 图表初始化生成图表实例对象
    const myChart = echarts.init(chartDom)

    // 3. 准备图表参数
    const option = {
      title: {
        text: title
      },
      xAxis: {
        type: 'category',
        data: ['Vue', 'React', 'Angular']
      },
      yAxis: {
        type: 'value'
      },
      series: [
        {
          data: [10, 40, 70],
          type: 'bar'
        }
      ]
    }
    // 4. 使用图表参数完成图表的渲染
    option && myChart.setOption(option)

  }, [title])
  return <div ref={chartRef} style={{ width: '500px', height: '400px' }}></div>
}

export default BarChart

八、拓展 - API 模块封装

现存问题:

当前的接口请求放到了功能实现的位置,没有在固定的模块内维护,后期查找维护困难

解决思路:

把项目中的所有接口按照业务模块以函数的形式统一封装到 apis 模块中

apis/user.js

javascript 复制代码
// 用户相关的所有请求
import { request } from "@/utils"
// 1. 登录请求
export function loginAPI (formData) {
  return request({
    url: '/authorizations',
    method: 'POST',
    data: formData
  })
}

// 2. 获取用户信息
export function getProfileAPI () {
  return request({
    url: '/user/profile',
    method: 'GET'
  })
}

store/modules/user.js

javascript 复制代码
import { loginAPI, getProfileAPI } from '@/apis/user'

// 登录获取token异步方法封装
const fetchLogin = (loginForm) => {
  return async (dispatch) => {
    const res = await loginAPI(loginForm)
    dispatch(setToken(res.data.token))
  }
}

// 获取个人用户信息异步方法
const fetchUserInfo = () => {
  return async (dispatch) => {
    const res = await getProfileAPI()
    dispatch(setUserInfo(res.data))
  }
}

九、文章发布

相关推荐
ZHOU_WUYI1 小时前
React与Docker中的MySQL进行交互
mysql·react.js·docker
霸王蟹4 小时前
React 19中如何向Vue那样自定义状态和方法暴露给父组件。
前端·javascript·学习·react.js·typescript
GISer_Jing6 小时前
Vue 和 React 状态管理的性能优化策略对比
vue.js·react.js·性能优化
chenbin___14 小时前
react native text 显示 三行 超出部分 中间使用省略号
javascript·react native·react.js
霸王蟹1 天前
React Fiber 架构深度解析:时间切片与性能优化的核心引擎
前端·笔记·react.js·性能优化·架构·前端框架
outstanding木槿1 天前
react中安装依赖时的问题 【集合】
前端·javascript·react.js·node.js
霸王蟹1 天前
React中useState中更新是同步的还是异步的?
前端·javascript·笔记·学习·react.js·前端框架
霸王蟹1 天前
React Hooks 必须在组件最顶层调用的原因解析
前端·javascript·笔记·学习·react.js
Coding的叶子2 天前
React Flow 节点事件处理实战:鼠标 / 键盘事件全解析(含节点交互代码示例)
react.js·交互·鼠标事件·fgai·react agent