【React】Day6,React Day 6学习笔记

马肤

温馨提示:这篇文章已超过450天没有更新,请注意相关的内容是否还可用!

【React】Day6摘要:,,在第六天的React学习旅程中,我们深入探讨了React的核心概念和组件化开发。通过实践,我们掌握了如何创建和组合组件,以及如何利用props传递数据。还学习了事件处理和组件间的通信方式。今天的学习使我们更加熟悉React的开发流程和最佳实践,为构建复杂的前端应用打下了坚实的基础。

项目搭建

基于CRA创建项目

CRA是一个底层基于webpack快速创建React项目的脚手架工具

# 使用npx创建项目
npx create-react-app react-jike
# 进入到项
cd react-jike
# 启动项目
npm start

【React】Day6,React Day 6学习笔记 第1张

调整项目目录结构

-src
  -apis           项目接口函数
  -assets         项目资源文件,比如,图片等
  -components     通用组件
  -pages          页面组件
  -store          集中状态管理
  -utils          工具,比如,token、axios 的封装等
  -App.js         根组件
  -index.css      全局样式
  -index.js       项目入口

src/index.js

import React from 'react'
import ReactDOM from 'react-dom/client'
import './index.scss'
import './App.js'
ReactDOM.createRoot(document.getElementById('root')).render(
  
    
  
)

src/App.js

const App = () => {
  return this is app
}
export default App

使用scss预处理器

SASS 是一种预编译的 CSS,支持一些比较高级的语法,可以提高编写样式的效率,CRA接入scss非常简单只需要我们装一个sass工具

实现步骤

  1. 安装解析 sass 的包:npm i sass -D
  2. 创建全局样式文件:index.scss
body {
  margin: 0;
  div {
    color: blue;
  }
}

组件库antd使用

我们的项目是一个传统的PC管理后台,有现成的组件库可以使用,帮助我们提升开发效率,其中使用最广的就是antD

Ant Design of React - Ant Design

实现步骤

  1. 安装 antd 组件库:npm i antd
  2. 导入 Button 组件
  3. 在 Login 页面渲染 Button 组件进行测试

测试Button

pages/Login/index.jsx

import { Button } from 'antd'
const Login = () => {
  return this is logintest
}
export default Login

【React】Day6,React Day 6学习笔记 第2张

配置基础路由

单页应用需要对应的路由支持,我们使用 react-router-dom 最新版本

实现步骤

  1. 安装路由包 npm i react-router-dom
  2. 准备 Layout和 Login俩个基础组件
  3. 配置路由

代码实现

pages/Layout/index.js

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

pages/Login/index.js

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

router/index.js

import { createBrowserRouter } from 'react-router-dom'
import Login from '../pages/Login'
import Layout from '../pages/Layout'
const router = createBrowserRouter([
  {
    path: '/',
    element: ,
  },
  {
    path: '/login',
    element: ,
  },
])
export default router

index.js

import React from 'react'
import ReactDOM from 'react-dom/client'
import './index.scss'
import router from './router'
import { RouterProvider } from 'react-router-dom'
ReactDOM.createRoot(document.getElementById('root')).render(
    
)

配置别名路径

项目背景:在业务开发过程中文件夹的嵌套层级可能会比较深,通过传统的路径选择会比较麻烦也容易出错,设置路径别名可以简化这个过程

路径编译配置

  1. 安装 craco 工具包
  2. 增加 craco.config.js 配置文件
  3. 修改 scripts 命令
  4. 测试是否生效
npm i @craco/craco -D
const path = require('path')
module.exports = {
  // webpack 配置
  webpack: {
    // 配置别名
    alias: {
      // 约定:使用 @ 表示 src 文件所在路径
      '@': path.resolve(__dirname, 'src')
    }
  }
}
"scripts": {
  "start": "craco start",
  "build": "craco build",
  "test": "craco test",
  "eject": "react-scripts eject"
}
import { createBrowserRouter } from 'react-router-dom'
import Login from '@/pages/Login'
import Layout from '@/pages/Layout'
const router = createBrowserRouter([
  {
    path: '/',
    element: ,
  },
  {
    path: '/login',
    element: ,
  },
])
export default router

VsCode提示配置

实现步骤

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

代码实现

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

:::warning

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

:::

使用gitee管理项目

基本结构搭建

【React】Day6,React Day 6学习笔记 第3张

实现步骤

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

代码实现

pages/Login/index.js

import './index.scss'
import { Card, Form, Input, Button } from 'antd'
import logo from '@/assets/logo.png'
const Login = () => {
  return (
    
      
        【React】Day6,React Day 6学习笔记 第4张
        {/* 登录表单 */}
        
          
            
          
          
            
          
          
            
              登录
            
          
        
      
    
  )
}
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;
  }
}

表单校验实现

【React】Day6,React Day 6学习笔记 第5张

实现步骤

  1. 为 Form 组件添加 validateTrigger 属性,指定校验触发时机的集合
  2. 为 Form.Item 组件添加 name 属性
  3. 为 Form.Item 组件添加 rules 属性,用来添加表单校验规则对象

代码实现

page/Login/index.js

const Login = () => {
  return (
    
      
        
      
      
        
      
    
      
        
          登录
        
      
    
  )
}

获取登录表单数据

实现步骤

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

代码实现

pages/Login/index.js

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

【React】Day6,React Day 6学习笔记 第6张

封装request工具模块

业务背景: 前端需要和后端拉取接口数据,axios是使用最广的工具插件,针对于项目中的使用,我们需要做一些简单的封装

实现步骤

  1. 安装 axios 到项目
  2. 创建 utils/request.js 文件
  3. 创建 axios 实例,配置 baseURL,请求拦截器,响应拦截器
  4. 在 utils/index.js 中,统一导出request
npm i axios
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.data
  }, (error)=> {
    // 超出 2xx 范围的状态码都会触发该函数。
    // 对响应错误做点什么
    return Promise.reject(error)
})
export { http }
import { request } from './request'
export { request }

Axios

使用Redux管理token

安装Redux相关工具包

npm i react-redux @reduxjs/toolkit

配置Redux

import { createSlice } from '@reduxjs/toolkit'
import { http } from '@/utils'
const userStore = createSlice({
  name: 'user',
  // 数据状态
  initialState: {
    token:''
  },
  // 同步修改方法
  reducers: {
    setUserInfo (state, action) {
      state.userInfo = action.payload
    }
  }
})
// 解构出actionCreater
const { setUserInfo } = userStore.actions
// 获取reducer函数
const userReducer = userStore.reducer
// 异步方法封装
const fetchLogin = (loginForm) => {
  return async (dispatch) => {
    const res = await http.post('/authorizations', loginForm)
    dispatch(setUserInfo(res.data.token))
  }
}
export { fetchLogin }
export default userReducer
import { configureStore } from '@reduxjs/toolkit'
import userReducer from './modules/user'
export default configureStore({
  reducer: {
    // 注册子模块
    user: userReducer
  }
})

实现登录逻辑

业务逻辑:

  1. 跳转到首页
  2. 提示用户登录成功
import { message } from 'antd'
import useStore from '@/store'
import { fetchLogin } from '@/store/modules/user'
import { useDispatch } from 'react-redux'
const Login = () => {
  const dispatch = useDispatch()
  const navigate = useNavigate()
  const onFinish = async formValue => {
    await dispatch(fetchLogin(formValue))
    navigate('/')
    message.success('登录成功')
  }
  return (
    
     
    
  )
}
export default Login

token持久化

业务背景: Token数据具有一定的时效时间,通常在几个小时,有效时间内无需重新获取,而基于Redux的存储方式又是基于内存的,刷新就会丢失,为了保持持久化,我们需要单独做处理

封装存取方法

// 封装存取方法
const TOKENKEY = 'token_key'
function setToken (token) {
  return localStorage.setItem(TOKENKEY, token)
}
function getToken () {
  return localStorage.getItem(TOKENKEY)
}
function clearToken () {
  return localStorage.removeItem(TOKENKEY)
}
export {
  setToken,
  getToken,
  clearToken
}

实现持久化逻辑

import { getToken, setToken } from '@/utils'
const userStore = createSlice({
  name: 'user',
  // 数据
  initialState: {
    token: getToken() || ''
  },
  // 同步修改方法
  reducers: {
    setUserInfo (state, action) {
      state.token = action.payload
      // 存入本地
      setToken(state.token)
    }
  }
})

刷新浏览器,通过Redux调试工具查看token数据

【React】Day6,React Day 6学习笔记 第7张

请求拦截器注入token

业务背景: Token作为用户的数据标识,在接口层面起到了接口权限控制的作用,也就是说后端有很多接口都需要通过查看当前请求头信息中是否含有token数据,来决定是否正常返回数据

【React】Day6,React Day 6学习笔记 第8张

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

utils/request.js

// 添加请求拦截器
request.interceptors.request.use(config => {
  // if not login add token
  const token = getToken()
  if (token) {
    config.headers.Authorization = `Bearer ${token}`
  }
  return config
})

路由鉴权实现

业务背景:封装 AuthRoute 路由鉴权高阶组件,实现未登录拦截,并跳转到登录页面

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

实现步骤

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

代码实现

components/AuthRoute/index.jsx

import { getToken } from '@/utils'
import { Navigate } from 'react-router-dom'
const AuthRoute = ({ children }) => {
  const isToken = getToken()
  if (isToken) {
    return {children}
  } else {
    return 
  }
}
export default AuthRoute

src/router/index.jsx

import { createBrowserRouter } from 'react-router-dom'
import Login from '@/pages/Login'
import Layout from '@/pages/Layout'
import AuthRoute from '@/components/Auth'
const router = createBrowserRouter([
  {
    path: '/',
    element: ,
  },
  {
    path: '/login',
    element: ,
  },
])
export default router

基本结构和样式reset

结构创建

【React】Day6,React Day 6学习笔记 第9张

实现步骤

  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 items = [
  {
    label: '首页',
    key: '1',
    icon: ,
  },
  {
    label: '文章管理',
    key: '2',
    icon: ,
  },
  {
    label: '创建文章',
    key: '3',
    icon: ,
  },
]
const GeekLayout = () => {
  return (
    
      
        
        
          柴柴老师
          
            
               退出
            
          
        
      
      
        
          { height: '100%', borderRight: 0 }}
        
        { padding: 20 }}
          内容
        
      
    
  )
}
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;
}

样式reset

npm install normalize.css
html,
body {
  margin: 0;
  height: 100%;
}
#root {
  height: 100%;
}

二级路由配置

使用步骤

  1. 在 pages 目录中,分别创建:Home(数据概览)/Article(内容管理)/Publish(发布文章)页面文件夹
  2. 分别在三个文件夹中创建 index.jsx 并创建基础组件后导出
  3. 在router/index.js 中配置嵌套子路由,在Layout中配置二级路由出口
  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

router/index.js

import { createBrowserRouter } from 'react-router-dom'
import Login from '@/pages/Login'
import Layout from '@/pages/Layout'
import Publish from '@/pages/Publish'
import Article from '@/pages/Article'
import Home from '@/pages/Home'
import { AuthRoute } from '@/components/Auth'
const router = createBrowserRouter([
  {
    path: '/',
    element: (
      
        
      
    ),
    children: [
      {
        index: true,
        element: ,
      },
      {
        path: 'article',
        element: ,
      },
      {
        path: 'publish',
        element: ,
      },
    ],
  },
  {
    path: '/login',
    element: ,
  },
])
export default router

配置二级路由出口

{ padding: 20 }}
  

路由菜单点击交互实现

【React】Day6,React Day 6学习笔记 第10张

点击菜单跳转路由

import { Outlet, useNavigate } from 'react-router-dom'
const items = [
  {
    label: '首页',
    key: '/',
    icon: ,
  },
  {
    label: '文章管理',
    key: '/article',
    icon: ,
  },
  {
    label: '创建文章',
    key: '/publish',
    icon: ,
  },
]
const GeekLayout = () => {
  const navigate = useNavigate()
  const menuClick = (route) => {
    navigate(route.key)
  }
  return (
      { height: '100%', borderRight: 0 }}
        onClick={menuClick}
      / 
  )
}
export default GeekLayout

菜单反向高亮

const GeekLayout = () => {
  // 省略部分代码
  const location = useLocation()
  const selectedKey = location.pathname
  
  return (
    
      
        
        
          {name}
          
            
               退出
            
          
        
      
      
        
          { height: '100%', borderRight: 0 }}
            onClick={menuClickHandler}
        
        { padding: 20 }}
          
        
      
    
  )
}

展示个人信息

【React】Day6,React Day 6学习笔记 第11张

实现步骤

  1. 在Redux的store中编写获取用户信息的相关逻辑
  2. 在Layout组件中触发action的执行
  3. 在Layout组件使用使用store中的数据进行用户名的渲染

代码实现

store/userStore.js

import { createSlice } from '@reduxjs/toolkit'
import { http } from '@/utils/request'
import { getToken, setToken } from '@/utils'
const userStore = createSlice({
  name: 'user',
  // 数据
  initialState: {
    token: getToken() || '',
    userInfo: {}
  },
  // 同步修改方法
  reducers: {
    setUserToken (state, action) {
      state.token = action.payload
      // 存入本地
      setToken(state.token)
    },
    setUserInfo (state, action) {
      state.userInfo = action.payload
    }
  }
})
// 解构出actionCreater
const { setUserToken, setUserInfo } = userStore.actions
// 获取reducer函数
const userReducer = userStore.reducer
const fetchLogin = (loginForm) => {
  return async (dispatch) => {
    const res = await http.post('/authorizations', loginForm)
    dispatch(setUserToken(res.data.token))
  }
}
const fetchUserInfo = () => {
  return async (dispatch) => {
    const res = await http.get('/user/profile')
    dispatch(setUserInfo(res.data))
  }
}
export { fetchLogin, fetchUserInfo }
export default userReducer

pages/Layout/index.js

// 省略部分代码
import { fetchUserInfo } from '@/store/modules/user'
import { useDispatch, useSelector } from 'react-redux'
const GeekLayout = () => {
  const dispatch = useDispatch()
  const name = useSelector(state => state.user.userInfo.name)
  useEffect(() => {
    dispatch(fetchUserInfo())
  }, [dispatch])
  return (
    
      
        
        
          {name}
          
            
               退出
            
          
        
      
      
        
          { height: '100%', borderRight: 0 }}
        
        { padding: 20 }}
          
        
      
    
  )
}
export default GeekLayout

退出登录实现

【React】Day6,React Day 6学习笔记 第12张

实现步骤

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

代码实现

store/modules/user.js

import { createSlice } from '@reduxjs/toolkit'
import { http } from '@/utils/request'
import { clearToken, getToken, setToken } from '@/utils'
const userStore = createSlice({
  name: 'user',
  // 数据
  initialState: {
    token: getToken() || '',
    userInfo: {}
  },
  // 同步修改方法
  reducers: {
    setUserToken (state, action) {
      state.token = action.payload
      // 存入本地
      setToken(state.token)
    },
    setUserInfo (state, action) {
      state.userInfo = action.payload
    },
    clearUserInfo (state) {
      state.token = ''
      state.userInfo = {}
      clearToken()
    }
  }
})
// 解构出actionCreater
const { setUserToken, setUserInfo, clearUserInfo } = userStore.actions
// 获取reducer函数
const userReducer = userStore.reducer
export { fetchLogin, fetchUserInfo, clearUserInfo }
export default userReducer

pages/Layout/index.js

const GeekLayout = () => {
  // 退出登录
  const loginOut = () => {
    dispatch(clearUserInfo())
    navigator('/login')
  }
  return (
    
      
        
        
          {name}
          
            
               退出
            
          
        
      
      
        
          { height: '100%', borderRight: 0 }}
            onClick={menuClickHandler}
        
        { padding: 20 }}
          
        
      
    
  )
}

【React】Day6,React Day 6学习笔记 第13张

处理Token失效

业务背景:如果用户一段时间不做任何操作,到时之后应该清除所有过期用户信息跳回到登录

http.interceptors.response.use((response) => {
  // 2xx 范围内的状态码都会触发该函数。
  // 对响应数据做点什么
  return response.data
}, (error) => {
  // 超出 2xx 范围的状态码都会触发该函数。
  // 对响应错误做点什么
  console.dir(error)
  if (error.response.status === 401) {
    clearToken()
    router.navigate('/login')
    window.location.reload()
  }
  return Promise.reject(error)
})

首页Home图表展示

【React】Day6,React Day 6学习笔记 第14张

图表基础Demo实现

图表类业务渲染,我们可以通过下面的顺序来实现

  1. 跑通基础DEMO
  2. 按照实际业务需求进行修改

安装echarts

npm i echarts

实现基础Demo

import { useEffect, useRef } from 'react'
import * as echarts from 'echarts'
const Home = () => {
  const chartRef = useRef(null)
  useEffect(() => {
    // 1. 生成实例
    const myChart = echarts.init(chartRef.current)
    // 2. 准备图表参数
    const option = {
      xAxis: {
        type: 'category',
        data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
      },
      yAxis: {
        type: 'value'
      },
      series: [
        {
          data: [120, 200, 150, 80, 70, 110, 130],
          type: 'bar'
        }
      ]
    }
    // 3. 渲染参数
    myChart.setOption(option)
  }, [])
  return (
    
      { width: '400px', height: '300px' }} />
    
  )
}
export default Home

【React】Day6,React Day 6学习笔记 第15张

组件封装

基础抽象

import { useRef, useEffect } from 'react'
import * as echarts from 'echarts'
const BarChart = () => {
  const chartRef = useRef(null)
  useEffect(() => {
    // 1. 生成实例
    const myChart = echarts.init(chartRef.current)
    // 2. 准备图表参数
    const option = {
      xAxis: {
        type: 'category',
        data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
      },
      yAxis: {
        type: 'value'
      },
      series: [
        {
          data: [120, 200, 150, 80, 70, 110, 130],
          type: 'bar'
        }
      ]
    }
    // 3. 渲染参数
    myChart.setOption(option)
  }, [])
  return { width: '400px', height: '300px' }}>
}
export { BarChart }

抽象可变参数

import { useRef, useEffect } from 'react'
import * as echarts from 'echarts'
const BarChart = ({ xData, sData, style = { width: '400px', height: '300px' } }) => {
  const chartRef = useRef(null)
  useEffect(() => {
    // 1. 生成实例
    const myChart = echarts.init(chartRef.current)
    // 2. 准备图表参数
    const option = {
      xAxis: {
        type: 'category',
        data: xData
      },
      yAxis: {
        type: 'value'
      },
      series: [
        {
          data: sData,
          type: 'bar'
        }
      ]
    }
    // 3. 渲染参数
    myChart.setOption(option)
  }, [sData, xData])
  return 
}
export { BarChart }
import { BarChart } from './BarChart'
const Home = () => {
  return (
    
      
      { width: '500px', height: '400px' }} /
    
  )
}
export default Home

实现基础文章发布

创建基础结构

【React】Day6,React Day 6学习笔记 第16张

import {
  Card,
  Breadcrumb,
  Form,
  Button,
  Radio,
  Input,
  Upload,
  Space,
  Select
} from 'antd'
import { PlusOutlined } from '@ant-design/icons'
import { Link } from 'react-router-dom'
import './index.scss'
const { Option } = Select
const Publish = () => {
  return (
    
      
        { span: 4 }}
          wrapperCol={{ span: 16 }}
          initialValues={{ type: 1 }}
        
          
            { width: 400 }} /
          
          
            { width: 400 }}
              推荐
            
          
          
          { offset: 4 }}
            
              
                发布文章
              
            
          
        
      
    
  )
}
export default Publish

pages/Publish/index.scss

.publish {
  position: relative;
}
.ant-upload-list {
  .ant-upload-list-picture-card-container,
  .ant-upload-select {
    width: 146px;
    height: 146px;
  }
}

准备富文本编辑器

实现步骤

  1. 安装富文本编辑器
  2. 导入富文本编辑器组件以及样式文件
  3. 渲染富文本编辑器组件
  4. 调整富文本编辑器的样式

代码落地

1-安装 react-quill

npm i react-quill@2.0.0-beta.2

2-导入资源渲染组件

import ReactQuill from 'react-quill'
import 'react-quill/dist/quill.snow.css'
const Publish = () => {
  return (
    // ...
    { span: 4 }}
      wrapperCol={{ span: 16 }}
    
      
        
      
    
  )
}
.publish-quill {
  .ql-editor {
    min-height: 300px;
  }
}

频道数据获取

【React】Day6,React Day 6学习笔记 第17张

实现步骤

  1. 使用useState初始化数据和修改数据的方法
  2. 在useEffect中调用接口并保存数据
  3. 使用数据渲染对应模版

代码实现

import { http } from '@/utils'
// 频道列表
const [channels, setChannels] = useState([])
// 调用接口
useEffect(() => {
    async function fetchChannels() {
      const res = await http.get('/channels')
      setChannels(res.data.channels)
    }
    fetchChannels()
}, [])
// 模板渲染
return (
 
    { width: 200 }}
      {channels.map(item => (
        
          {item.name}
        
      ))}
    
  
)

发布文章

// 发布文章
const onFinish = async (formValue) => {
  const { channel_id, content, title } = formValue
  const params = {
    channel_id,
    content,
    title,
    type: 1,
    cover: {
      type: 1,
      images: []
    }
  }
  await http.post('/mp/articles?draft=false', params)
  message.success('发布文章成功')
}

【React】Day6,React Day 6学习笔记 第18张

上传封面实现

准备上传结构

【React】Day6,React Day 6学习笔记 第19张

  
    
      单图
      三图
      无图
    
  
  
    { marginTop: 8 }}>
      
    
  

实现基础上传

实现步骤

  1. 为 Upload 组件添加 action 属性,配置封面图片上传接口地址
  2. 为 Upload组件添加 name属性, 接口要求的字段名
  3. 为 Upload 添加 onChange 属性,在事件中拿到当前图片数据,并存储到React状态中

代码实现

import { useState } from 'react'
const Publish = () => {
  // 上传图片
  const [imageList, setImageList] = useState([])
  const onUploadChange = (info) => {
      setImageList(info.fileList)
  }
  return (
   	
      
        
          单图
          三图
          无图
        
      
      
        { marginTop: 8 }}>
          
        
      
    
  )
}

切换图片Type

实现步骤

  1. 点击单选框时拿到当前的类型value
  2. 根据value控制上传组件的显示(大于零时才显示)
const Publish = ()=>{
  // 控制图片Type
  const [imageType, setImageType] = useState(0)
  const onTypeChange = (e) => {
    console.log(e)
    setImageType(e.target.value)
  }
  
  return (
    
      
        单图
        三图
        无图
      
      {imageType > 0 &&
      
        { marginTop: 8 }}>
          
        
      }
    
  )
}

【React】Day6,React Day 6学习笔记 第20张

控制最大上传图片数量

实现步骤

  1. 通过 maxCount 属性限制图片的上传图片数量
{imageType > 0 &&
 1}
>
  { marginTop: 8 }}>
    
  
}

暂存图片列表实现

业务描述

如果当前为三图模式,已经完成了上传,选择单图只显示一张,再切换到三图继续显示三张,该如何实现?

实现思路

在上传完毕之后通过ref存储所有图片,需要几张就显示几张,其实也就是把ref当仓库,用多少拿多少

实现步骤

  1. 通过useRef创建一个暂存仓库,在上传完毕图片的时候把图片列表存入
  2. 如果是单图模式,就从仓库里取第一张图,以数组的形式存入fileList
  3. 如果是三图模式,就把仓库里所有的图片,以数组的形式存入fileList

代码实现

const Publish = () => {
  // 上传图片
  const cacheImageList = useRef([])
  const [imageList, setImageList] = useState([])
  const onUploadChange = (info) => {
      setImageList(info.fileList)
      cacheImageList.current = info.fileList
  }
  // 控制图片Type
  const [imageType, setImageType] = useState(0)
  const onRadioChange = (e) => {
    const type = e.target.value
    setImageType(type)
    if (type === 1) {
      // 单图,截取第一张展示
      const imgList = cacheImageList.current[0] ? [cacheImageList.current[0]] : []
      setImageList(imgList)
    } else if (type === 3) {
      // 三图,取所有图片展示
      setImageList(cacheImageList.current)
    }
  }
  return (
    {imageType > 0 &&
     1}
      fileList={imageList}
      >
      { marginTop: 8 }}>
        
      
    }
)
}

注意:需要给Upload组件添加fileList属性,达成受控的目的

发布带封面的文章

校验图片类型和数量是否吻合

// 发布文章
  const onFinish = async (formValue) => {
    if (imageType !== imageList.length) return message.warning('图片类型和数量不一致')
    const { channel_id, content, title } = formValue
    const params = {
      channel_id,
      content,
      title,
      type: imageType,
      cover: {
        type: imageType,
        images: imageList.map(item => item.response.data.url)
      }
    }
    await http.post('/mp/articles?draft=false', params)
    message.success('发布文章成功')
  }

处理图片列表格式为接口格式

// 发布文章
const onFinish = async (formValue) => {
  const { channel_id, content, title } = formValue
  const params = {
    channel_id,
    content,
    title,
    type: imageType,
    cover: {
      type: imageType,
      images: imageList.map(item => item.response.data.url)
    }
  }
  await http.post('/mp/articles?draft=false', params)
  message.success('发布文章成功')
}

静态结构创建

筛选区结构搭建

【React】Day6,React Day 6学习笔记 第21张

  1. 如何让RangePicker日期范围选择框选择中文
  2. Select组件配合Form.Item使用时,如何配置默认选中项

    { status: null }}

代码实现

import { Link } from 'react-router-dom'
import { Card, Breadcrumb, Form, Button, Radio, DatePicker, Select } from 'antd'
import locale from 'antd/es/date-picker/locale/zh_CN'
const { Option } = Select
const { RangePicker } = DatePicker
const Article = () => {
  return (
    
      { marginBottom: 20 }}
      
        { status: '' }}
          
            
              全部
              草稿
              审核通过
            
          
          
            { width: 120 }}
            
              Jack
              Lucy
            
          
          
            {/* 传入locale属性 控制中文显示*/}
            
          
          
            { marginLeft: 40 }}
              筛选
            
          
        
      
    
  )
}
export default Article

表格区域结构

【React】Day6,React Day 6学习笔记 第22张

代码实现

// 导入资源
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 【React】Day6,React Day 6学习笔记 第23张
      }
    },
    {
      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 (
          
            
            
          
        )
      }
    }
  ]
  // 准备表格body数据
  const data = [
    {
      id: '8218',
      comment_count: 0,
      cover: {
        images: [],
      },
      like_count: 0,
      pubdate: '2019-03-11 09:00:00',
      read_count: 2,
      status: 2,
      title: 'wkwebview离线化加载h5资源解决方案'
    }
  ]
  return (
    
      {/*        */}
      
        
) }

渲染频道数据

实现步骤

  1. 使用axios获取数据
  2. 将使用频道数据列表改写下拉框组件

代码实现

pages/Article/index.js

const Article = ()=>{
  // 获取频道列表
  const [channels, setChannels] = useState([])
  useEffect(() => {
      async function fetchChannels() {
        const res = await http.get('/channels')
        setChannels(res.data.channels)
      }
      fetchChannels()
  }, [])
  // 渲染模板
return (
    
        { width: 200 }} 
          {channels.map(item => (
            
              {item.name}
            
          ))}
        
    
  )
}

渲染表格数据

实现步骤

  1. 声明列表相关数据管理
  2. 使用useState声明参数相关数据管理
  3. 调用接口获取数据
  4. 使用接口数据渲染模板

代码实现

const Article = ()=>{
  // 省略部分代码...
  // 文章列表数据管理
  const [article, setArticleList] = useState({
      list: [],
      count: 0
  })
  
  const [params, setParams] = useState({
    page: 1,
    per_page: 4,
    begin_pubdate: null,
    end_pubdate: null,
    status: null,
    channel_id: null
  })
  
  useEffect(() => {
    async function fetchArticleList () {
      const res = await http.get('/mp/articles', { params })
      const { results, total_count } = res.data
      setArticleList({
        list: results,
        count: total_count
      })
    }
    fetchArticleList()
  }, [params])
  
  // 模板渲染
  return (
   
      
) }

筛选功能实现

实现步骤

  1. 为表单添加onFinish属性监听表单提交事件,获取参数
  2. 根据接口字段格式要求格式化参数格式
  3. 修改params 参数并重新使用新参数重新请求数据

代码实现

// 获取文章列表
  const [list, setList] = useState([])
  const [count, setCount] = useState(0)
  async function getList (reqData = {}) {
    const res = await getArticleListAPI(reqData)
    setList(res.data.results)
    setCount(res.data.total_count)
  }
  useEffect(() => {
    getList()
  }, [])
  // 筛选文章列表
  const onFinish = async (formValue) => {
    console.log(formValue)
    // 1. 准备参数
    const { channel_id, date, status } = formValue
    const reqData = {
      status,
      channel_id,
      begin_pubdate: date[0].format('YYYY-MM-DD'),
      end_pubdate: date[1].format('YYYY-MM-DD'),
    }
    // 2. 使用参数获取新的列表
    getList(reqData)
  }

分页功能实现

实现步骤

  1. 为Table组件指定pagination属性来展示分页效果
  2. 在分页切换事件中获取到筛选表单中选中的数据
  3. 使用当前页数据修改params参数依赖引起接口重新调用获取最新数据

代码实现

const pageChange = (page) => {
    // 拿到当前页参数 修改params 引起接口更新
    setParams({
      ...params,
      page
    })
}
return (
   {
      current: params.page,
      pageSize: params.per_page,
      onChange: pageChange,
      total: article.count
    }} /
)

删除功能

【React】Day6,React Day 6学习笔记 第24张

实现步骤

  1. 给删除文章按钮绑定点击事件
  2. 弹出确认窗口,询问用户是否确定删除文章
  3. 拿到参数调用删除接口,更新列表

代码实现

// 删除回调
const delArticle = async (data) => {
    await http.delete(`/mp/articles/${data.id}`)
    // 更新列表
    setParams({
      page: 1,
      per_page: 10
    })
}
const columns = [
  // ...
  {
      title: '操作',
      render: data => {
        return (
          
            
             delArticle(data)}
              okText="确认"
              cancelText="取消"
            >
              
            
          
        )
      }
]

编辑文章跳转

代码实现

const columns = [
  // ...
  {
    title: '操作',
    render: data => (
      
         navagite(`/publish?id=${data.id}`)} />
        />
      
    )
  }
]

基础数据回填

【React】Day6,React Day 6学习笔记 第25张

const Publish = ()=>{
  // 回填数据
  const [searchParams] = useSearchParams()
  const articleId = searchParams.get('id')
  const [form] = Form.useForm()
  useEffect(() => {
    async function getArticle () {
      const res = await http.get(`/mp/articles/${articleId}`)
      const { cover, ...formValue } = res.data
      // 设置表单数据
      form.setFieldsValue({ ...formValue, type: cover.type })
    }
    if (articleId) {
      // 拉取数据回显
      getArticle()
    }
  }, [articleId, form])
  return (
     
  )
}

回填封面信息

【React】Day6,React Day 6学习笔记 第26张

useEffect(() => {
  async function getArticle () {
    const res = await http.get(`/mp/articles/${articleId}`)
    const { cover, ...formValue } = res.data
    // 1. 回填表单数据
    form.setFieldsValue({ ...formValue, type: cover.type })
    // 2. 回填封面图片
    setImageType(cover.type) // 封面类型
    setImageList(cover.images.map(url => ({ url }))) // 封面list
  }
  if (articleId) {
    getArticle()
  }
}, [articleId, form])

适配不同状态下的文案

{articleId ? '更新文章' : '发布文章'}

更新文章

 // 发布文章
  const onFinish = async (formValue) => {
    const { channel_id, content, title } = formValue
    const formatUrl = (list) => {
      return list.map(item => {
        if (item.response) {
          return item.response.data.url
        } else {
          return item.url
        }
      })
    }
    const data = {
      channel_id,
      content,
      title,
      type: imageType,
      cover: {
        type: imageType,
        images: formatUrl(imageList)
      }
    }
    if (imageType !== imageList.length) return message.warning('图片类型和数量不一致')
    if (articleId) {
      // 编辑
      await http.put(`/mp/articles/${articleId}?draft=false`, data)
    } else {
      // 新增
      await http.post('/mp/articles?draft=false', data)
    }
    message.success(`${articleId ? '编辑' : '发布'}文章成功`)
  }

项目打包

npm run build

【React】Day6,React Day 6学习笔记 第27张

项目本地预览

实现步骤

  1. 全局安装本地服务包 npm i -g serve 该包提供了serve命令,用来启动本地服务器
  2. 在项目根目录中执行命令 serve -s ./build 在build目录中开启服务器
  3. 在浏览器中访问:http://localhost:3000/ 预览项目

【React】Day6,React Day 6学习笔记 第28张

优化-路由懒加载

使用步骤

  1. 使用 lazy 方法导入路由组件
  2. 使用内置的 Suspense 组件渲染路由组件

代码实现

router/index.js

import { createBrowserRouter } from 'react-router-dom'
import { lazy, Suspense } from 'react'
import Login from '@/pages/Login'
import Layout from '@/pages/Layout'
import AuthRoute from '@/components/Auth'
const Publish = lazy(() => import('@/pages/Publish'))
const Article = lazy(() => import('@/pages/Article'))
const Home = lazy(() => import('@/pages/Article'))
const router = createBrowserRouter([
  {
    path: '/',
    element: (
      
        
      
    ),
    children: [
      {
        index: true,
        element: (
          
            
          
        )
      },
      {
        path: 'article',
        element: (
          
            
          
        )
      },
      {
        path: 'publish',
        element: (
          
            
          
        )
      },
    ],
  },
  {
    path: '/login',
    element: ,
  },
])
export default router

查看效果

我们可以在打包之后,通过切换路由,监控network面板资源的请求情况,验证是否分隔成功

打包-打包体积分析

业务背景

通过分析打包体积,才能知道项目中的哪部分内容体积过大,方便知道哪些包如何来优化

使用步骤

  1. 安装分析打包体积的包:npm i source-map-explorer
  2. 在 package.json 中的 scripts 标签中,添加分析打包体积的命令
  3. 对项目打包:npm run build(如果已经打过包,可省略这一步)
  4. 运行分析命令:npm run analyze
  5. 通过浏览器打开的页面,分析图表中的包体积

核心代码:

"scripts": {
  "analyze": "source-map-explorer 'build/static/js/*.js'",
}

【React】Day6,React Day 6学习笔记 第29张

优化-配置CDN

分析说明:通过 craco 来修改 webpack 配置,从而实现 CDN 优化

核心代码

craco.config.js

// 添加自定义对于webpack的配置
const path = require('path')
const { whenProd, getPlugin, pluginByName } = require('@craco/craco')
module.exports = {
  // webpack 配置
  webpack: {
    // 配置别名
    alias: {
      // 约定:使用 @ 表示 src 文件所在路径
      '@': path.resolve(__dirname, 'src')
    },
    // 配置webpack
    // 配置CDN
    configure: (webpackConfig) => {
      let cdn = {
        js:[]
      }
      whenProd(() => {
        // key: 不参与打包的包(由dependencies依赖项中的key决定)
        // value: cdn文件中 挂载于全局的变量名称 为了替换之前在开发环境下
        webpackConfig.externals = {
          react: 'React',
          'react-dom': 'ReactDOM'
        }
        // 配置现成的cdn资源地址
        // 实际开发的时候 用公司自己花钱买的cdn服务器
        cdn = {
          js: [
            'https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.production.min.js',
            'https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.production.min.js',
          ]
        }
      })
      // 通过 htmlWebpackPlugin插件 在public/index.html注入cdn资源url
      const { isFound, match } = getPlugin(
        webpackConfig,
        pluginByName('HtmlWebpackPlugin')
      )
      if (isFound) {
        // 找到了HtmlWebpackPlugin的插件
        match.userOptions.files = cdn
      }
      return webpackConfig
    }
  }
}

public/index.html

  
{ %>

0
收藏0
文章版权声明:除非注明,否则均为VPS857原创文章,转载或复制请以超链接形式并注明出处。

相关阅读

  • 【研发日记】Matlab/Simulink自动生成代码(二)——五种选择结构实现方法,Matlab/Simulink自动生成代码的五种选择结构实现方法(二),Matlab/Simulink自动生成代码的五种选择结构实现方法详解(二)
  • 超级好用的C++实用库之跨平台实用方法,跨平台实用方法的C++实用库超好用指南,C++跨平台实用库使用指南,超好用实用方法集合,C++跨平台实用库超好用指南,方法与技巧集合
  • 【动态规划】斐波那契数列模型(C++),斐波那契数列模型(C++实现与动态规划解析),斐波那契数列模型解析与C++实现(动态规划)
  • 【C++】,string类底层的模拟实现,C++中string类的模拟底层实现探究
  • uniapp 小程序实现微信授权登录(前端和后端),Uniapp小程序实现微信授权登录全流程(前端后端全攻略),Uniapp小程序微信授权登录全流程攻略,前端后端全指南
  • Vue脚手架的安装(保姆级教程),Vue脚手架保姆级安装教程,Vue脚手架保姆级安装指南,Vue脚手架保姆级安装指南,从零开始教你如何安装Vue脚手架
  • 如何在树莓派 Raspberry Pi中本地部署一个web站点并实现无公网IP远程访问,树莓派上本地部署Web站点及无公网IP远程访问指南,树莓派部署Web站点及无公网IP远程访问指南,本地部署与远程访问实践,树莓派部署Web站点及无公网IP远程访问实践指南,树莓派部署Web站点及无公网IP远程访问实践指南,本地部署与远程访问详解,树莓派部署Web站点及无公网IP远程访问实践详解,本地部署与远程访问指南,树莓派部署Web站点及无公网IP远程访问实践详解,本地部署与远程访问指南。
  • vue2技术栈实现AI问答机器人功能(流式与非流式两种接口方法),Vue2技术栈实现AI问答机器人功能,流式与非流式接口方法探究,Vue2技术栈实现AI问答机器人功能,流式与非流式接口方法详解
  • 发表评论

    快捷回复:表情:
    评论列表 (暂无评论,0人围观)

    还没有评论,来说两句吧...

    目录[+]

    取消
    微信二维码
    微信二维码
    支付宝二维码