【react】pc项目教程

项目准备

1. 项目介绍

本节目标: 了解项目的定位和功能

  • 项目功能演示
    • 登录、退出
    • 首页
    • 内容(文章)管理:文章列表、发布文章、修改文章
  • 技术
    • React 官方脚手架 create-react-app
    • react hooks
    • 状态管理:mobx
    • UI 组件库:antd v4
    • ajax请求库:axios
    • 路由:react-router-dom 以及 history
    • 富文本编辑器:react-quill
    • CSS 预编译器:sass

2. 项目搭建

本节目标: 能够基于脚手架搭建项目基本结构

实现步骤

  1. 使用create-react-app生成项目 npx create-react-app geek-pc

  2. 进入根目录 cd geek-pc

  3. 启动项目 yarn start

  4. 调整项目目录结构

    /src
      /assets         项目资源文件,比如,图片 等
      /components     通用组件
      /pages          页面
      /store          mobx 状态仓库
      /utils          工具,比如,token、axios 的封装等
      App.js          根组件
      index.css       全局样式
      index.js        项目入口
    

保留核心代码

src/index.js

import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'

ReactDOM.render(
  
    
  ,
  document.getElementById('root')
)

src/App.js

export default function App() {
  return 
根组件
}

3. 使用gitee管理项目

本节目标: 能够将项目推送到gitee远程仓库

实现步骤

  1. 在项目根目录打开终端,并初始化 git 仓库(如果已经有了 git 仓库,无需重复该步),命令:git init
  2. 添加项目内容到暂存区:git add .
  3. 提交项目内容到仓库区:git commit -m '项目初始化'
  4. 添加 remote 仓库地址:git remote add origin [gitee 仓库地址]
  5. 将项目内容推送到 gitee:git push origin master -u

4. 使用scss预处理器

本节目标: 能够在CRA中使用sass书写样式

SASS 是一种预编译的 CSS,作用类似于 Less。由于 React 中内置了处理 SASS 的配置,所以,在 CRA 创建的项目中,可以直接使用 SASS 来写样式

实现步骤

  1. 安装解析 sass 的包:yarn add sass -D

  2. 创建全局样式文件:index.scss

    body {
      margin: 0;
    }
    
    #root {
      height: 100%;
    }
    

5. 配置基础路由

本节目标: 能够配置登录页面的路由并显示到页面中

实现步骤

  1. 安装路由:yarn add react-router-dom
  2. 在 pages 目录中创建两个文件夹:Login、Layout
  3. 分别在两个目录中创建 index.js 文件,并创建一个简单的组件后导出
  4. 在 App 组件中,导入路由组件以及两个页面组件
  5. 配置 Login 和 Layout 的路由规则

代码实现

pages/Login/index.js

const Login = () => {
  return 
login
} export default Login

pages/Layout/index.js

const Layout = () => {
  return 
layout
} export default Layout

app.js

// 导入路由
import { BrowserRouter, Route, Routes } from 'react-router-dom'

// 导入页面组件
import Login from './pages/Login'
import Layout from './pages/Layout'

// 配置路由规则
function App() {
  return (
    
      
}/> }/>
) } export default App

6. 组件库antd使用

本节目标: 能够使用antd的Button组件渲染按钮

实现步骤

  1. 安装 antd 组件库:yarn add antd
  2. 全局导入 antd 组件库的样式
  3. 导入 Button 组件
  4. 在 Login 页面渲染 Button 组件进行测试

代码实现

src/index.js

// 先导入 antd 样式文件
// https://github.com/ant-design/ant-design/issues/33327
import 'antd/dist/antd.min.css'
// 再导入全局样式文件,防止样式覆盖!
import './index.css'

pages/Login/index.js

import { Button } from 'antd'

const Login = () => (
  
)

易错总结

  1. 在哪个文件中导入 antd 的样式文件?
  2. antd 的样式文件和我们自己的全局样式文件的导入顺序?

7. 配置别名路径

本节目标: 能够配置@路径简化路径处理

自定义 CRA 的默认配置
craco 配置文档

  • CRA 将所有工程化配置,都隐藏在了 react-scripts 包中,所以项目中看不到任何配置信息
  • 如果要修改 CRA 的默认配置,有以下几种方案:
    1. 通过第三方库来修改,比如,@craco/craco (推荐)
    2. 通过执行 yarn eject 命令,释放 react-scripts 中的所有配置到项目中

实现步骤

  1. 安装修改 CRA 配置的包:yarn add -D @craco/craco
  2. 在项目根目录中创建 craco 的配置文件:craco.config.js,并在配置文件中配置路径别名
  3. 修改 package.json 中的脚本命令
  4. 在代码中,就可以通过 @ 来表示 src 目录的绝对路径
  5. 重启项目,让配置生效

代码实现

craco.config.js

const path = require('path')

module.exports = {
  // webpack 配置
  webpack: {
    // 配置别名
    alias: {
      // 约定:使用 @ 表示 src 文件所在路径
      '@': path.resolve(__dirname, 'src')
    }
  }
}

package.json

// 将 start/build/test 三个命令修改为 craco 方式
"scripts": {
  "start": "craco start",
  "build": "craco build",
  "test": "craco test",
  "eject": "react-scripts eject"
}

8. @别名路径提示

本节目标: 能够让vscode识别@路径并给出路径提示

实现步骤

  1. 在项目根目录创建 jsconfig.json 配置文件
  2. 在配置文件中添加以下配置

代码实现

{
  "compilerOptions": {
    "baseUrl": "./",
    "paths": {
      "@/*": ["src/*"]
    }
  }
}

vscode会自动读取jsconfig.json 中的配置,让vscode知道@就是src目录

9. 安装dev-tools调试工具

https://gitee.com/react-cp/react-pc-doc 这里找到dev-tools.crx文件

登录模块

1. 基本结构搭建

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

实现步骤

  1. 在 Login/index.js 中创建登录页面基本结构
  2. 在 Login 目录中创建 index.scss 文件,指定组件样式
  3. 将 logo.png 和 login.png 拷贝到 assets 目录中

代码实现

pages/Login/index.js

import { Card } from 'antd'
import logo from '@/assets/logo.png'
import './index.scss'

const Login = () => {
  return (
    
{/* 登录表单 */}
) } export default Login

pages/Login/index.scss

.login {
  width: 100%;
  height: 100%;
  position: absolute;
  left: 0;
  top: 0;
  background: center/cover url('~@/assets/login.png');

  .login-logo {
    width: 200px;
    height: 60px;
    display: block;
    margin: 0 auto 20px;
  }

  .login-container {
    width: 440px;
    height: 360px;
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
    box-shadow: 0 0 50px rgb(0 0 0 / 10%);
  }

  .login-checkbox-label {
    color: #1890ff;
  }
}

2. 创建表单结构

本节目标: 能够使用antd的Form组件创建登录表单

实现步骤

  1. 打开 antd Form 组件文档
  2. 找到代码演示的第一个示例(基本使用),点击<>(显示代码),并拷贝代码到组件中
  3. 分析 Form 组件基本结构
  4. 调整 Form 组件结构和样式

代码实现

pages/Login/index.js

import { Form, Input, Button, Checkbox } from 'antd'
const Login = () => {
  return (
    
我已阅读并同意「用户协议」和「隐私条款」
) }

3. 表单校验实现

本节目标: 能够为手机号和密码添加表单校验

实现步骤

  1. 为 Form 组件添加 validateTrigger 属性,指定校验触发时机的集合
  2. 为 Form.Item 组件添加 name 属性,这样表单校验才会生效
  3. 为 Form.Item 组件添加 rules 属性,用来添加表单校验

代码实现

page/Login/index.js

const Login = () => {
  return (
    
我已阅读并同意「用户协议」和「隐私条款」
) }

4. 获取登录表单数据

本节目标: 能够拿到登录表单中用户的手机号码和验证码

实现步骤

  1. 为 Form 组件添加 onFinish 属性,该事件会在点击登录按钮时触发
  2. 创建 onFinish 函数,通过函数参数 values 拿到表单值
  3. Form 组件添加 initialValues 属性,来初始化表单值

代码实现

pages/Login/index.js

// 点击登录按钮时触发 参数values即是表单输入数据
const onFinish = values => {
  console.log(values)
}

...

5. 封装http工具模块

本节目标: 封装axios,简化操作

实现步骤

  1. 创建 utils/http.js 文件
  2. 创建 axios 实例,配置 baseURL,请求拦截器,响应拦截器
  3. 在 utils/index.js 中,统一导出 http

代码实现

utils/http.js

import axios from 'axios'

const http = axios.create({
  baseURL: 'http://geek.itheima.net/v1_0',
  timeout: 5000
})
// 添加请求拦截器
http.interceptors.request.use((config)=> {
    return config
  }, (error)=> {
    return Promise.reject(error)
})

// 添加响应拦截器
http.interceptors.response.use((response)=> {
    // 2xx 范围内的状态码都会触发该函数。
    // 对响应数据做点什么
    return response
  }, (error)=> {
    // 超出 2xx 范围的状态码都会触发该函数。
    // 对响应错误做点什么
    return Promise.reject(error)
})

export { http }

utils/index.js

import { http } from './http'
export {  http }

6. 配置登录Mobx

本节目标: 基于mobx封装管理用户登录的store

store/login.Store.js

// 登录模块
import { makeAutoObservable } from "mobx"
import { http } from '@/utils'

class LoginStore {
  token = ''
  constructor() {
    makeAutoObservable(this)
  }
  // 登录
  login = async ({ mobile, code }) => {
    const res = await http.post('http://geek.itheima.net/v1_0/authorizations', {
      mobile,
      code
    })
    this.token = res.data.token
  }
}
export default LoginStore

store/index.js

import React from "react"
import LoginStore from './login.Store'

class RootStore {
  // 组合模块
  constructor() {
    this.loginStore = new LoginStore()
  }
}
// 导入useStore方法供组件使用数据
const StoresContext = React.createContext(new RootStore())
export const useStore = () => React.useContext(StoresContext)

7. 实现登录逻辑

本节目标: 在表单校验通过之后通过封装好的store调用登录接口

实现步骤

  1. 使用useStore方法得到loginStore实例对象
  2. 在校验通过之后,调用loginStore中的login函数
  3. 登录成功之后跳转到首页

代码实现

import { useStore } from '@/store'
const Login = () => {
  // 获取跳转实例对象
  const navigate = useNavigate()
  const { loginStore } = useStore()
  const onFinish = async values => {
    const { mobile, code } = values
    try {
      await loginStore.login({ mobile, code })
      navigate('/')
    } catch (e) {
      message.error(e.response?.data?.message || '登录失败')
    }
  }
  return (...)
}

8. token持久化

8.1 封装工具函数

本节目标: 能够统一处理 token 的持久化相关操作

实现步骤

  1. 创建 utils/token.js 文件
  2. 分别提供 getToken/setToken/clearToken/isAuth 四个工具函数并导出
  3. 创建 utils/index.js 文件,统一导出 token.js 中的所有内容,来简化工具函数的导入
  4. 将登录操作中用到 token 的地方,替换为该工具函数

代码实现

utils/token.js

const TOKEN_KEY = 'itcast_geek_pc'

const getToken = () => localStorage.getItem(TOKEN_KEY)
const setToken = token => localStorage.setItem(TOKEN_KEY, token)
const clearToken = () => localStorage.removeItem(TOKEN_KEY)

export { getToken, setToken, clearToken }

8.2 持久化设置

本节目标: 使用token函数持久化配置

实现步骤

  1. 拿到token的时候一式两份,存本地一份
  2. 初始化的时候优先从本地取,取不到再初始化为控制

代码实现

store/login.Store.js

// 登录模块
import { makeAutoObservable } from "mobx"
import { setToken, getToken, clearToken, http } from '@/utils'

class LoginStore {
  // 这里哦!!
  token = getToken() || ''
  constructor() {
    makeAutoObservable(this)
  }
  // 登录
  login = async ({ mobile, code }) => {
    const res = await http.post('http://geek.itheima.net/v1_0/authorizations', {
      mobile,
      code
    })
    this.token = res.data.token
    // 还有这里哦!!
    setToken(res.data.token)
  }
 
}
export default LoginStore

9. 请求拦截器注入token

本节目标: 把token通过请求拦截器注入到请求头中

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HYd0RfEu-1657264065747)(assets/token.png)]

拼接方式:config.headers.Authorization = Bearer ${token}}

utils/http.js

http.interceptors.request.use(config => {
  // if not login add token
  const token = getToken()
  if (token) {
    config.headers.Authorization = `Bearer ${token}`
  }
  return config
})

10. 路由鉴权实现

本节目标: 能够实现未登录时访问拦截并跳转到登录页面

实现思路

自己封装 AuthRoute 路由鉴权高阶组件,实现未登录拦截,并跳转到登录页面

思路为:判断本地是否有token,如果有,就返回子组件,否则就重定向到登录Login

实现步骤

  1. 在 components 目录中,创建 AuthRoute/index.js 文件
  2. 判断是否登录
  3. 登录时,直接渲染相应页面组件
  4. 未登录时,重定向到登录页面
  5. 将需要鉴权的页面路由配置,替换为 AuthRoute 组件渲染

代码实现

components/AuthRoute/index.js

// 1. 判断token是否存在
// 2. 如果存在 直接正常渲染
// 3. 如果不存在 重定向到登录路由

// 高阶组件:把一个组件当成另外一个组件的参数传入 然后通过一定的判断 返回新的组件
import { getToken } from '@/utils'
import { Navigate } from 'react-router-dom'

function AuthRoute ({ children }) {
  const isToken = getToken()
  if (isToken) {
    return <>{children}
  } else {
    return 
  }
}

//   
// 登录:<>
// 非登录:

export {
  AuthRoute
}

src/app.js


import { Router, Route } from 'react-router-dom'
import { AuthRoute } from '@/components/AuthRoute'
import Layout from '@/pages/Layout'
import Login from '@/pages/Login'

function App() {
  return (
    
      
          {/* 需要鉴权的路由 */}
          
              
            
          } />
          {/* 不需要鉴权的路由 */}
          } />
       
    
  )
}
export default App

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 } = Layout

const GeekLayout = () => {
  return (
    
      
user.name 退出
} key="1"> 数据概览 } key="2"> 内容管理 } key="3"> 发布文章 内容
) } 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 
Home
} export default Home

pages/Article/index.js

const Article = () => {
  return 
Article
} export default Article

pages/Publish/index.js

const Publish = () => {
  return 
Publish
} export default Publish

app.js


      
    
  }>
    {/* 二级路由默认页面 */}
    } />
    } />
    } />

}>

pages/Layout/index.js

// 配置Link组件

    } key="/">
      数据概览
    
    } key="/article">
      内容管理
    
    } key="/publish">
      发布文章
    


// 二级路由对应显示

  
    {/* 二级路由默认页面 */}
    
  

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 location = useLocation()
  // 这里是当前浏览器上的路径地址
  const selectedKey = location.pathname

  return (
    // ...
    
      } key="/">
        数据概览
      
      } key="/article">
        内容管理
      
      } key="/publish">
        发布文章
      
    
  )
}

4. 展示个人信息

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

实现步骤

  1. 在store中新增userStore.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 (
    
      
{userStore.userInfo.name}
{/* 省略无关代码 */}
) } 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

pages/Layout/index.js

// login out
const navigate = useNavigate()
const onLogout = () => {
    loginStore.loginOut()
    navigate('/login')
}


    
       退出
    

6. 处理Token失效

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

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FM82Evvt-1657264065748)(assets/historyoutside.png)]

实现步骤

  1. 安装:yarn add history
  2. 创建 utils/history.js 文件
  3. 在app.js中使用我们新建的路由并配置history参数
  4. 通过响应拦截器处理 token 失效

代码实现

utils/history.js

// https://github.com/remix-run/react-router/issues/8264

import { createBrowserHistory } from 'history'
import { unstable_HistoryRouter as HistoryRouter } from 'react-router-dom'
const history = createBrowserHistory()

export {
  HistoryRouter,
  history
}

app.js

import { HistoryRouter, history } from './utils/history'

function App() {
  return (
    
       ...省略无关代码
    
  )
}

export default App

utils/http.js

import { history } from './history'

http.interceptors.response.use(
  response => {
    return response.data
  },
  error => {
    if (error.response.status === 401) {
      // 删除token
      clearToken()
      // 跳转到登录页
      history.push('/login')
    }
    return Promise.reject(error)
  }
)

7. 首页Home图表展示

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yeyIGoia-1657264065748)(assets/home.png)]

需求描述:

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

代码实现

components/Bar/index.js

import * as echarts from 'echarts'
import { useEffect, useRef } from 'react'

function echartInit (node, xData, sData, title) {
  const myChart = echarts.init(node)
  // 绘制图表
  myChart.setOption({
    title: {
      text: title
    },
    tooltip: {},
    xAxis: {
      data: xData
    },
    yAxis: {},
    series: [
      {
        name: '销量',
        type: 'bar',
        data: sData
      }
    ]
  })
}

function Bar ({ style, xData, sData, title }) {
  // 1. 先不考虑传参问题  静态数据渲染到页面中
  // 2. 把那些用户可能定制的参数 抽象props (1.定制大小 2.data 以及说明文字)
  const nodeRef = useRef(null)
  useEffect(() => {
    echartInit(nodeRef.current, xData, sData, title)
  }, [xData, sData])

  return (
    
) } export default Bar

pages/Home/index.js


import Bar from "@/components/Bar"
import './index.scss'
const Home = () => {
  return (
    
) } export default Home

pages/Home/index.scss

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

内容管理

1. 筛选区结构

本节目标: 能够使用antd组件库搭建筛选区域结构

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-b4SxhpvU-1657264065748)(assets/search.png)]

重点关注

  1. 如何让RangePicker日期范围选择框选择中文

  2. Select组件配合Form.Item使用时,如何配置默认选中项

代码实现

import { Link } from 'react-router-dom'
import { Card, Breadcrumb, Form, Button, Radio, DatePicker, Select } from 'antd'
import 'moment/locale/zh-cn'
import locale from 'antd/es/date-picker/locale/zh_CN'
import './index.scss'

const { Option } = Select
const { RangePicker } = DatePicker

const Article = () => {
  return (
    
首页 内容管理 } style={{ marginBottom: 20 }} > 全部 草稿 待审核 审核通过 审核失败 {/* 传入locale属性 控制中文显示*/}
) } export default Article

2. 表格区域结构

本节目标: 能够基于Table组件搭建表格结构

重点关注

  1. 通过哪个属性指定Table组件的列信息
  2. 通过哪个属性指定Table数据
  3. 通过哪个属性指定Table列表用到的key属性

代码实现

import { Link } from 'react-router-dom'
import { Table, Tag, Space } from 'antd'
import { EditOutlined, DeleteOutlined } from '@ant-design/icons'

import img404 from '@/assets/error.png'

const Article = () => {
  const columns = [
    {
      title: '封面',
      dataIndex: 'cover',
      width:120,
      render: cover => {
        return 
      }
    },
    {
      title: '标题',
      dataIndex: 'title',
      width: 220
    },
    {
      title: '状态',
      dataIndex: 'status',
      render: data => 审核通过
    },
    {
      title: '发布时间',
      dataIndex: 'pubdate'
    },
    {
      title: '阅读数',
      dataIndex: 'read_count'
    },
    {
      title: '评论数',
      dataIndex: 'comment_count'
    },
    {
      title: '点赞数',
      dataIndex: 'like_count'
    },
    {
      title: '操作',
      render: data => {
        return (