欢迎来到我的博客
博主是一名大学在读本科生,主要学习方向是前端。
目前已经更新了【Vue】、【React–从基础到实战】、【TypeScript】等等系列专栏
目前正在学习的是 R e a c t / 小程序 React/小程序 React/小程序,中间穿插了一些基础知识的回顾
博客主页codeMak1r.小新的博客

本文目录

  • Layout模块
    • 1. 基本结构搭建
    • 2. 二级路由配置
    • 3. 菜单高亮显示
    • 4. 展示个人信息
    • 5. 退出登录实现
    • 6. 处理Token失效
    • 7. 首页Home图表展示

本文被专栏【React–从基础到实战】收录
坚持创作✏️,一起学习,码出未来‍!

最近在学习React过程中,找到了一个实战小项目,在这里与大家分享。
本文遵循项目开发流程,逐步完善各个需求
gitee完整项目地址:极客园完整代码

Layout模块

1. 基本结构搭建

本节目标: 能够使用antd搭建基础布局

实现步骤

  1. 打开 antd/Layout 布局组件文档,找到示例:顶部-侧边布局-通栏
  2. 拷贝示例代码到我们的 Layout 页面中
  3. 分析并调整页面布局

代码实现

pages/Layout/index.js

import { Layout, Menu, Popconfirm } from 'antd'import {  HomeOutlined,  DiffOutlined,  EditOutlined,  LogoutOutlined} from '@ant-design/icons'import './index.scss'const { Header, Sider } = Layoutconst GeekLayout = () => {  return (    <Layout>      <Header className="header">        <div className="logo" />        <div className="user-info">          <span className="user-name">user.name</span>          <span className="user-logout">            <Popconfirm title="是否确认退出?" okText="退出" cancelText="取消">              <LogoutOutlined /> 退出            </Popconfirm>          </span>        </div>      </Header>      <Layout>        <Sider width={200} className="site-layout-background">          <Menu            mode="inline"            theme="dark"            defaultSelectedKeys={['1']}            style={{ height: '100%', borderRight: 0 }}          >            <Menu.Item icon={<HomeOutlined />} key="1">              数据概览            </Menu.Item>            <Menu.Item icon={<DiffOutlined />} key="2">              内容管理            </Menu.Item>            <Menu.Item icon={<EditOutlined />} key="3">              发布文章            </Menu.Item>          </Menu>        </Sider>        <Layout className="layout-content" style={{ padding: 20 }}>内容</Layout>      </Layout>    </Layout>  )}export default GeekLayout

pages/Layout/index.scss

.ant-layout {  height: 100%;}.header {  padding: 0;}.logo {  width: 200px;  height: 60px;  background: url('~@/assets/logo.png') no-repeat center / 160px auto;}.layout-content {  overflow-y: auto;}.user-info {  position: absolute;  right: 0;  top: 0;  padding-right: 20px;  color: #fff;    .user-name {    margin-right: 20px;  }    .user-logout {    display: inline-block;    cursor: pointer;  }}.ant-layout-header {  padding: 0 !important;}

2. 二级路由配置

本节目标: 能够在右侧内容区域展示左侧菜单对应的页面内容

使用步骤

  1. 在 pages 目录中,分别创建:Home(数据概览)/Article(内容管理)/Publish(发布文章)页面文件夹
  2. 分别在三个文件夹中创建 index.js 并创建基础组件后导出
  3. 在app.js中配置嵌套子路由,在layout.js中配置二级路由出口
  4. 使用 Link 修改左侧菜单内容,与子路由规则匹配实现路由切换

代码实现

pages/Home/index.js

const Home = () => {  return <div>Home</div>}export default Home

pages/Article/index.js

const Article = () => {  return <div>Article</div>}export default Article

pages/Publish/index.js

const Publish = () => {  return <div>Publish</div>}export default Publish

src/routes/index.js

export default [  // 不需要鉴权的组件Login  {    path: "/login",    element: <Login />  },  // 需要鉴权的组件Layout  {    path: "/",    element: <AuthRoute>      <Layout />    </AuthRoute>,    children: [      {        path: "home",        element: <AuthRoute>          <Home />        </AuthRoute>      },      {        path: "article",        element: <AuthRoute>          <Article />        </AuthRoute>      },      {        path: "publish",        element: <AuthRoute>          <Publish />        </AuthRoute>      },      {        path: "",        element: <Navigate to="home" replace />      }    ]  }]

pages/Layout/index.js

// 配置Link组件<Menu mode="inline" theme="dark" defaultSelectedKeys={['1']} style={{ height: '100%', borderRight: 0 }}>  <Menu.Item icon={<HomeOutlined />} key="1" onClick={() => navigate('home')}>    数据概览</Menu.Item><Menu.Item icon={<DiffOutlined />} key="2" onClick={() => navigate('article')}>    内容管理  </Menu.Item>  <Menu.Item icon={<EditOutlined />} key="3" onClick={() => navigate('publish')}>发布文章  </Menu.Item></Menu><Layout className="layout-content" style={{ padding: 20 }}><Outlet /></Layout>

3. 菜单高亮显示

本节目标: 能够在页面刷新的时候保持对应菜单高亮

思路

  1. Menu组件的selectedKeys属性与Menu.Item组件的key属性发生匹配的时候,Item组件即可高亮
  2. 页面刷新时,将当前访问页面的路由地址作为 Menu 选中项的值(selectedKeys)即可

实现步骤

  1. 将 Menu 的key 属性修改为与其对应的路由地址
  2. 获取到当前正在访问页面的路由地址
  3. 将当前路由地址设置为 selectedKeys 属性的值

代码实现

pages/Layout/index.js

import { useLocation } from 'react-router-dom'const GeekLayout = () => {  const { pathname: selectedKey } = useLocation()  console.log(selectedKey)  return (    // ...    <Menu      mode="inline"      theme="dark"      selectedKeys={[selectedKey]}      style={{ height: '100%', borderRight: 0 }}    >      <Menu.Item icon={<HomeOutlined />} key="/home" onClick={() => navigate('home')}>  数据概览</Menu.Item><Menu.Item icon={<DiffOutlined />} key="/article" onClick={() => navigate('article')}>  内容管理</Menu.Item><Menu.Item icon={<EditOutlined />} key="/publish" onClick={() => navigate('publish')}>  发布文章</Menu.Item>    </Menu>  )}

4. 展示个人信息

本节目标: 能够在页面右上角展示登录用户名

实现步骤

  1. 在store中新增user.Store.js模块,在其中定义获取用户信息的mobx代码
  2. 在store的入口文件中组合新增的userStore模块
  3. 在Layout组件中调用action函数获取用户数据
  4. 在Layout组件中获取个人信息并展示

代码实现

store/user.Store.js

// 用户模块import { makeAutoObservable } from "mobx"import { http } from '@/utils'class UserStore {  userInfo = {}  constructor() {    makeAutoObservable(this)  }  async getUserInfo() {    const res = await http.get('/user/profile')    this.userInfo = res.data  }}export default UserStore

store/index.js

import React from "react"import LoginStore from './login.Store'import UserStore from './user.Store'class RootStore {  // 组合模块  constructor() {    this.loginStore = new LoginStore()    this.userStore = new UserStore()  }}const StoresContext = React.createContext(new RootStore())export const useStore = () => React.useContext(StoresContext)

pages/Layout/index.js

import { useEffect } from 'react'import { observer } from 'mobx-react-lite'const GeekLayout = () => {  const { userStore } = useStore()  // 获取用户数据  useEffect(() => {    try {      userStore.getUserInfo()    } catch { }  }, [userStore])      return (    <Layout>      <Header className="header">        <div className="logo" />        <div className="user-info">          <span className="user-name">{userStore.userInfo.name}</span>        </div>      </Header>      {/* 省略无关代码 */}    </Layout>  )}export default observer(GeekLayout)

5. 退出登录实现

本节目标: 能够实现退出登录功能

实现步骤

  1. 为气泡确认框添加确认回调事件
  2. store/login.Store.js 中新增退出登录的action函数,在其中删除token
  3. 在回调事件中,调用loginStore中的退出action
  4. 退出后,返回登录页面

代码实现

store/login.Store.js

class LoginStore {  // 退出登录  loginOut = () => {    this.token = ''    clearToken()  }}export default LoginStore

clearToken()是utils/token.js中定义好的清除token的工具函数。

pages/Layout/index.js

// login outconst navigate = useNavigate()const onLogout = () => {    loginStore.loginOut()    navigate('/login')}<span className="user-logout">    <Popconfirm title="是否确认退出?" okText="退出" cancelText="取消" onConfirm={onLogout}>      <LogoutOutlined /> 退出    </Popconfirm></span>

6. 处理Token失效

本节目标: 能够在响应拦截器中处理token失效

说明:为了能够在非组件环境下拿到路由信息,需要我们安装一个history包

实现步骤

  1. 安装history包:yarn add history
  2. 创建 utils/history.js文件
  3. 在app.js中使用我们新建的路由并配置history参数
  4. 通过响应拦截器处理 token 失效,如果发现是401跳回到登录页

代码实现

utils/history.js

// https://github.com/remix-run/react-router/issues/8264import { createBrowserHistory } from 'history'const history = createBrowserHistory()export { history }

index.js入口文件

...省略无关代码import { unstable_HistoryRouter as HistoryRouter } from "react-router-dom";import { history } from "./utils/history";const root = ReactDOM.createRoot(document.getElementById('root'))root.render(  <HistoryRouter history={history}>    <App />  </HistoryRouter>)

utils/http.js

import { history } from './history'http.interceptors.response.use(  response => {    return response.data  },  error => {    if (error.response.status === 401) {      // 清除失效的token      removeToken()      // 跳转到登录页      history.push('/login')    }    return Promise.reject(error)  })

7. 首页Home图表展示

本节目标: 实现首页echart图表封装展示

需求描述:

  1. 使用eharts配合react封装柱状图组件Bar
  2. 要求组件的标题title,横向数据xData,纵向数据yData,样式style可定制

代码实现

components/Bar/index.js

// 封装图表bar组件// 思路:// 1. 看官方文档 把echarts加入项目// 如何在react中获取dom => useRef// 在什么地方获取dom节点  => useEffect// 2. 不抽离定制化参数 先把最小化的demo跑起来// 3. 按照需求:哪些参数需要自定义 抽象出来import { useRef, useEffect } from 'react';import * as echarts from 'echarts'export default function Bar({ title, xData, yData, style }) {  const domRef = useRef()  // 执行这个初始化的函数  useEffect(() => {    const chartInit = () => {      // 基于准备好的dom,初始化echarts实例      const myChart = echarts.init(domRef.current);      // 绘制图表      myChart.setOption({        title: {          text: title        },        tooltip: {},        xAxis: {          data: xData        },        yAxis: {},        series: [          {            name: '框架',            type: 'bar',            data: yData          }        ]      });    }    chartInit()  }, [title, xData, yData])  return (    <div>      {/* 为echart准备一个dom节点 */}      <div ref={domRef} style={style}></div>    </div>  )}

pages/Home/index.js

import React from 'react'import Bar from '@/components/Bar'export default function Home() {  return (    <div>      <Bar        title='主流框架使用满意度'        xData={['React', 'Vue', 'Angular']}        yData={[40, 50, 30]}        style={{ width: '500px', height: '400px' }}      />      <Bar        title='主流框架使用满意度2'        xData={['React', 'Vue', 'Angular']}        yData={[70, 80, 40]}        style={{ width: '300px', height: '200px' }}      />    </div>  )}

pages/Home/index.scss

.home {  width: 100%;  height: 100%;  align-items: center;}

下篇文章:内容管理模块的实现
专栏订阅入口【React–从基础到实战】