【React】Day6

项目搭建

基于CRA创建项目

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

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

在这里插入图片描述

调整项目目录结构

-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(<React.StrictMode><App/></React.StrictMode>
)

src/App.js

const App = () => {return <div>this is app</div>
}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 <div>this is login<Button type='primary'>test</Button></div>
}
export default Login

在这里插入图片描述

配置基础路由

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

实现步骤

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

代码实现
pages/Layout/index.js

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

pages/Login/index.js

const Login = () => {return <div>this is login</div>
}
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: <Layout />,},{path: '/login',element: <Login />,},
])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(<RouterProvider router={router} />
)

配置别名路径

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

路径编译配置

  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: <Layout />,},{path: '/login',element: <Login />,},
])export default router

VsCode提示配置

实现步骤

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

代码实现

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

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

使用gitee管理项目

基本结构搭建

在这里插入图片描述

实现步骤

  1. Login/index.js 中创建登录页面基本结构
  2. 在 Login 目录中创建 index.scss 文件,指定组件样式
  3. logo.pnglogin.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 (<div className="login"><Card className="login-container"><img className="login-logo" src={logo} alt="" />{/* 登录表单 */}<Form><Form.Item><Input size="large" placeholder="请输入手机号" /></Form.Item><Form.Item><Input size="large" placeholder="请输入验证码" /></Form.Item><Form.Item><Button type="primary" htmlType="submit" size="large" block>登录</Button></Form.Item></Form></Card></div>)
}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;}
}

表单校验实现

在这里插入图片描述

实现步骤

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

代码实现
page/Login/index.js

const Login = () => {return (<Form validateTrigger={['onBlur']}><Form.Itemname="mobile"rules={[{ required: true, message: '请输入手机号' },{pattern: /^1[3-9]\d{9}$/,message: '手机号码格式不对'}]}><Input size="large" placeholder="请输入手机号" /></Form.Item><Form.Itemname="code"rules={[{ required: true, message: '请输入验证码' },]}><Input size="large" placeholder="请输入验证码" maxLength={6} /></Form.Item><Form.Item><Button type="primary" htmlType="submit" size="large" block>登录</Button></Form.Item></Form>)
}

获取登录表单数据

实现步骤

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

代码实现
pages/Login/index.js

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

在这里插入图片描述

封装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 (<div className="login"><!-- 省略... --></div>)
}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数据
在这里插入图片描述

请求拦截器注入token

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

在这里插入图片描述

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

utils/request.js

// 添加请求拦截器
request.interceptors.request.use(config => {// if not login add tokenconst 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 <Navigate to="/login" replace />}
}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: <AuthRoute><Layout /></AuthRoute>,},{path: '/login',element: <Login />,},
])export default router

基本结构和样式reset

结构创建

在这里插入图片描述

实现步骤

  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 items = [{label: '首页',key: '1',icon: <HomeOutlined />,},{label: '文章管理',key: '2',icon: <DiffOutlined />,},{label: '创建文章',key: '3',icon: <EditOutlined />,},
]const GeekLayout = () => {return (<Layout><Header className="header"><div className="logo" /><div className="user-info"><span className="user-name">柴柴老师</span><span className="user-logout"><Popconfirm title="是否确认退出?" okText="退出" cancelText="取消"><LogoutOutlined /> 退出</Popconfirm></span></div></Header><Layout><Sider width={200} className="site-layout-background"><Menumode="inline"theme="dark"defaultSelectedKeys={['1']}items={items}style={{ height: '100%', borderRight: 0 }}></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;
}

样式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 <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

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: (<AuthRoute><Layout /></AuthRoute>),children: [{index: true,element: <Home />,},{path: 'article',element: <Article />,},{path: 'publish',element: <Publish />,},],},{path: '/login',element: <Login />,},
])export default router

配置二级路由出口

<Layout className="layout-content" style={{ padding: 20 }}><Outlet />
</Layout>

路由菜单点击交互实现

在这里插入图片描述

点击菜单跳转路由

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

菜单反向高亮

const GeekLayout = () => {// 省略部分代码const location = useLocation()const selectedKey = location.pathnamereturn (<Layout><Header className="header"><div className="logo" /><div className="user-info"><span className="user-name">{name}</span><span className="user-logout"><Popconfirm title="是否确认退出?" okText="退出" cancelText="取消"><LogoutOutlined /> 退出</Popconfirm></span></div></Header><Layout><Sider width={200} className="site-layout-background"><Menumode="inline"theme="dark"selectedKeys={selectedKey}items={items}style={{ height: '100%', borderRight: 0 }}onClick={menuClickHandler}></Menu></Sider><Layout className="layout-content" style={{ padding: 20 }}><Outlet /></Layout></Layout></Layout>)
}

展示个人信息

在这里插入图片描述

实现步骤

  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.reducerconst 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 (<Layout><Header className="header"><div className="logo" /><div className="user-info"><span className="user-name">{name}</span><span className="user-logout"><Popconfirm title="是否确认退出?" okText="退出" cancelText="取消"><LogoutOutlined /> 退出</Popconfirm></span></div></Header><Layout><Sider width={200} className="site-layout-background"><Menumode="inline"theme="dark"defaultSelectedKeys={['1']}items={items}style={{ height: '100%', borderRight: 0 }}></Menu></Sider><Layout className="layout-content" style={{ padding: 20 }}><Outlet /></Layout></Layout></Layout>)
}
export default GeekLayout

退出登录实现

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
实现步骤

  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.reducerexport { fetchLogin, fetchUserInfo, clearUserInfo }export default userReducer

pages/Layout/index.js

const GeekLayout = () => {// 退出登录const loginOut = () => {dispatch(clearUserInfo())navigator('/login')}return (<Layout><Header className="header"><div className="logo" /><div className="user-info"><span className="user-name">{name}</span><span className="user-logout"><Popconfirm title="是否确认退出?" okText="退出" cancelText="取消" onConfirm={loginOut}><LogoutOutlined /> 退出</Popconfirm></span></div></Header><Layout><Sider width={200} className="site-layout-background"><Menumode="inline"theme="dark"selectedKeys={selectedKey}items={items}style={{ height: '100%', borderRight: 0 }}onClick={menuClickHandler}></Menu></Sider><Layout className="layout-content" style={{ padding: 20 }}><Outlet /></Layout></Layout></Layout>)
}

在这里插入图片描述

处理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图表展示

在这里插入图片描述

图表基础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 (<div><div ref={chartRef} style={{ width: '400px', height: '300px' }} /></div >)
}export default Home

在这里插入图片描述

组件封装

基础抽象

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 <div ref={chartRef} style={{ width: '400px', height: '300px' }}></div>
}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 <div ref={chartRef} style={style}></div>
}export { BarChart }

import { BarChart } from './BarChart'const Home = () => {return (<div><BarChartxData={['Vue', 'React', 'Angular']}sData={[2000, 5000, 1000]} /><BarChartxData={['Vue', 'React', 'Angular']}sData={[200, 500, 100]}style={{ width: '500px', height: '400px' }} /></div >)
}export default Home

实现基础文章发布

创建基础结构

在这里插入图片描述

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 } = Selectconst Publish = () => {return (<div className="publish"><Cardtitle={<Breadcrumb items={[{ title: <Link to={'/'}>首页</Link> },{ title: '发布文章' },]}/>}><FormlabelCol={{ span: 4 }}wrapperCol={{ span: 16 }}initialValues={{ type: 1 }}><Form.Itemlabel="标题"name="title"rules={[{ required: true, message: '请输入文章标题' }]}><Input placeholder="请输入文章标题" style={{ width: 400 }} /></Form.Item><Form.Itemlabel="频道"name="channel_id"rules={[{ required: true, message: '请选择文章频道' }]}><Select placeholder="请选择文章频道" style={{ width: 400 }}><Option value={0}>推荐</Option></Select></Form.Item><Form.Itemlabel="内容"name="content"rules={[{ required: true, message: '请输入文章内容' }]}></Form.Item><Form.Item wrapperCol={{ offset: 4 }}><Space><Button size="large" type="primary" htmlType="submit">发布文章</Button></Space></Form.Item></Form></Card></div>)
}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 (// ...<FormlabelCol={{ span: 4 }}wrapperCol={{ span: 16 }}><Form.Itemlabel="内容"name="content"rules={[{ required: true, message: '请输入文章内容' }]}><ReactQuillclassName="publish-quill"theme="snow"placeholder="请输入文章内容"/></Form.Item></Form>)
}
.publish-quill {.ql-editor {min-height: 300px;}
}

频道数据获取

在这里插入图片描述

实现步骤

  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 (<Form.Itemlabel="频道"name="channel_id"rules={[{ required: true, message: '请选择文章频道' }]}><Select placeholder="请选择文章频道" style={{ width: 200 }}>{channels.map(item => (<Option key={item.id} value={item.id}>{item.name}</Option>))}</Select></Form.Item>
)

发布文章

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

在这里插入图片描述

上传封面实现

准备上传结构

在这里插入图片描述

<Form.Item label="封面"><Form.Item name="type"><Radio.Group><Radio value={1}>单图</Radio><Radio value={3}>三图</Radio><Radio value={0}>无图</Radio></Radio.Group></Form.Item><UploadlistType="picture-card"showUploadList><div style={{ marginTop: 8 }}><PlusOutlined /></div></Upload>
</Form.Item>

实现基础上传

实现步骤

  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 (<Form.Item label="封面"><Form.Item name="type"><Radio.Group><Radio value={1}>单图</Radio><Radio value={3}>三图</Radio><Radio value={0}>无图</Radio></Radio.Group></Form.Item><Uploadname="image"listType="picture-card"showUploadListaction={'http://geek.itheima.net/v1_0/upload'}onChange={onUploadChange}><div style={{ marginTop: 8 }}><PlusOutlined /></div></Upload></Form.Item>)
}

切换图片Type

实现步骤

  1. 点击单选框时拿到当前的类型value
  2. 根据value控制上传组件的显示(大于零时才显示)
const Publish = ()=>{// 控制图片Typeconst [imageType, setImageType] = useState(0)const onTypeChange = (e) => {console.log(e)setImageType(e.target.value)}return (<FormItem><Radio.Group onChange={onTypeChange}><Radio value={1}>单图</Radio><Radio value={3}>三图</Radio><Radio value={0}>无图</Radio></Radio.Group>{imageType > 0 &&<Uploadname="image"listType="picture-card"showUploadListaction={'http://geek.itheima.net/v1_0/upload'}onChange={onUploadChange}><div style={{ marginTop: 8 }}><PlusOutlined /></div></Upload>}</FormItem>)
}

在这里插入图片描述

控制最大上传图片数量

实现步骤

  1. 通过 maxCount 属性限制图片的上传图片数量
{imageType > 0 &&
<Uploadname="image"listType="picture-card"className="avatar-uploader"showUploadListaction={'http://geek.itheima.net/v1_0/upload'}onChange={onUploadChange}maxCount={imageType}multiple={imageType > 1}
><div style={{ marginTop: 8 }}><PlusOutlined /></div>
</Upload>}

暂存图片列表实现

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

实现思路
在上传完毕之后通过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}// 控制图片Typeconst [imageType, setImageType] = useState(0)const onRadioChange = (e) => {const type = e.target.valuesetImageType(type)if (type === 1) {// 单图,截取第一张展示const imgList = cacheImageList.current[0] ? [cacheImageList.current[0]] : []setImageList(imgList)} else if (type === 3) {// 三图,取所有图片展示setImageList(cacheImageList.current)}}return ({imageType > 0 &&<Uploadname="image"listType="picture-card"className="avatar-uploader"showUploadListaction={'http://geek.itheima.net/v1_0/upload'}onChange={onUploadChange}maxCount={imageType}multiple={imageType > 1}fileList={imageList}><div style={{ marginTop: 8 }}><PlusOutlined /></div></Upload>}
)
}

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

发布带封面的文章

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

// 发布文章const onFinish = async (formValue) => {if (imageType !== imageList.length) return message.warning('图片类型和数量不一致')const { channel_id, content, title } = formValueconst 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 } = formValueconst 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('发布文章成功')
}

静态结构创建

筛选区结构搭建

在这里插入图片描述

  1. 如何让RangePicker日期范围选择框选择中文
  2. Select组件配合Form.Item使用时,如何配置默认选中项
    <Form initialValues={{ 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 } = DatePickerconst Article = () => {return (<div><Cardtitle={<Breadcrumb items={[{ title: <Link to={'/'}>首页</Link> },{ title: '文章列表' },]} />}style={{ marginBottom: 20 }}><Form initialValues={{ status: '' }}><Form.Item label="状态" name="status"><Radio.Group><Radio value={''}>全部</Radio><Radio value={0}>草稿</Radio><Radio value={2}>审核通过</Radio></Radio.Group></Form.Item><Form.Item label="频道" name="channel_id"><Selectplaceholder="请选择文章频道"defaultValue="lucy"style={{ width: 120 }}><Option value="jack">Jack</Option><Option value="lucy">Lucy</Option></Select></Form.Item><Form.Item label="日期" name="date">{/* 传入locale属性 控制中文显示*/}<RangePicker locale={locale}></RangePicker></Form.Item><Form.Item><Button type="primary" htmlType="submit" style={{ marginLeft: 40 }}>筛选</Button></Form.Item></Form></Card></div>)
}export default Article

表格区域结构

在这里插入图片描述

代码实现

// 导入资源
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 <img src={cover.images[0] || img404} width={80} height={60} alt="" />}},{title: '标题',dataIndex: 'title',width: 220},{title: '状态',dataIndex: 'status',render: data => <Tag color="green">审核通过</Tag>},{title: '发布时间',dataIndex: 'pubdate'},{title: '阅读数',dataIndex: 'read_count'},{title: '评论数',dataIndex: 'comment_count'},{title: '点赞数',dataIndex: 'like_count'},{title: '操作',render: data => {return (<Space size="middle"><Button type="primary" shape="circle" icon={<EditOutlined />} /><Buttontype="primary"dangershape="circle"icon={<DeleteOutlined />}/></Space>)}}]// 准备表格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 (<div>{/*        */}<Card title={`根据筛选条件共查询到 count 条结果:`}><Table rowKey="id" columns={columns} dataSource={data} /></Card></div>)
}

渲染频道数据

实现步骤

  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 (<Form.Item label="频道" name="channel_id" ><Select placeholder="请选择频道" style={{ width: 200 }} >{channels.map(item => (<Option key={item.id} value={item.id}>{item.name}</Option>))}</Select></Form.Item>)
}

渲染表格数据

实现步骤

  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.datasetArticleList({list: results,count: total_count})}fetchArticleList()}, [params])// 模板渲染return (<Card title={`根据筛选条件共查询到 ${article.count} 条结果:`}><TabledataSource={article.list}columns={columns}/></Card>)
}

筛选功能实现

实现步骤

  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 } = formValueconst 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 (<Table rowKey="id" columns={columns} dataSource={article.list} pagination={{current: params.page,pageSize: params.per_page,onChange: pageChange,total: article.count}} />
)

删除功能

在这里插入图片描述

实现步骤

  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 (<Space size="middle"><Button type="primary" shape="circle" icon={<EditOutlined />} /><Popconfirmtitle="确认删除该条文章吗?"onConfirm={() => delArticle(data)}okText="确认"cancelText="取消"><Buttontype="primary"dangershape="circle"icon={<DeleteOutlined />}/></Popconfirm></Space>)}
]

编辑文章跳转

代码实现

const columns = [// ...{title: '操作',render: data => (<Space size="middle"><Buttontype="primary"shape="circle"icon={<EditOutlined />}onClick={() => navagite(`/publish?id=${data.id}`)} />/></Space>)}
]

基础数据回填

在这里插入图片描述

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 (<Form form={form}/>)
}

回填封面信息

在这里插入图片描述

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])

适配不同状态下的文案

<Cardtitle={<Breadcrumb items={[{ title: <Link to={'/'}>首页</Link> },{ title: `${articleId ? '编辑文章' : '发布文章'}` },]}/>}
>{articleId ? '更新文章' : '发布文章'}

更新文章

 // 发布文章const onFinish = async (formValue) => {const { channel_id, content, title } = formValueconst 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

在这里插入图片描述

项目本地预览

实现步骤

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

在这里插入图片描述

优化-路由懒加载

使用步骤

  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: (<AuthRoute><Layout /></AuthRoute>),children: [{index: true,element: (<Suspense fallback={'加载中'}><Home /></Suspense>)},{path: 'article',element: (<Suspense fallback={'加载中'}><Article /></Suspense>)},{path: 'publish',element: (<Suspense fallback={'加载中'}><Publish /></Suspense>)},],},{path: '/login',element: <Login />,},
])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'",
}

在这里插入图片描述

优化-配置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// 配置CDNconfigure: (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资源urlconst { isFound, match } = getPlugin(webpackConfig,pluginByName('HtmlWebpackPlugin'))if (isFound) {// 找到了HtmlWebpackPlugin的插件match.userOptions.files = cdn}return webpackConfig}}
}

public/index.html

<body><div id="root"></div><!-- 加载第三发包的 CDN 链接 --><% htmlWebpackPlugin.options.files.js.forEach(cdnURL => { %><script src="<%= cdnURL %>"></script><% }) %>
</body>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/640902.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Rancher-Longhorn-新增磁盘以及卷创建原理和卷副本调度规则

一、添加磁盘-官网指引 重点在于&#xff1a; 1、比如你新增了一块盘&#xff0c;你需要做一下事情&#xff1a; 1、执行 lsblk 能找到你的盘。 2、然后执行 fdisk /dev/sdxx 分区你的盘。 3、然后对于分区部署文件系统&#xff0c; mkfs.xfs 4、然后执行 mount /dev/sdxxx 你…

从数据库中到处所有表的列、注释、类型、是否必填等信息

从数据库中到处所有中文表名、英文表名、所有列、注释、类型、长度、是否必填等信息&#xff0c;效果如下&#xff1a; 要实现上面的表格可以直接用SQL实现&#xff0c;实现SQL如下&#xff1a; #查询SQL select* FROMinformation_schema.COLUMNS as columns left join (sele…

华为机考入门python3--(17)牛客17- 坐标移动

分类&#xff1a;字符串 知识点&#xff1a; 正则匹配 re.match(pattern, move) 格式字符串&#xff0c;可以在字符串中直接引用变量 f"{x},{y}" 题目来自【牛客】 import re def is_valid_coordinate(move): # 使用正则表达式验证移动是否合法 # ^: …

并发编程之线程通信及Condition的原理分析

1. synchronized中的线程通信 调用wait方法会使线程处于等待状态&#xff0c;直到另一个线程调用notify线程才会唤醒等待中的某个线程&#xff0c;生产者和消费者模型可以很好的使用到该例子。 生产者代码: public class Producer implements Runnable {private Queue<Str…

CountDownLatch倒计时器源码解读与使用

&#x1f3f7;️个人主页&#xff1a;牵着猫散步的鼠鼠 &#x1f3f7;️系列专栏&#xff1a;Java全栈-专栏 &#x1f3f7;️个人学习笔记&#xff0c;若有缺误&#xff0c;欢迎评论区指正 目录 1. 前言 2. CountDownLatch有什么用 3. CountDownLatch底层原理 3.1. count…

如何查看redisson-spring-boot-starter和SpringBoot 对应版本

如何查看redisson-spring-boot-starter和SpringBoot 对应版本 我目前没有找到官网的地址来来查看对应关系。 所以我只能找pom.xml来查看 先在mvnrepository 找到redisson-spring-boot-starter的列表 具体地址是&#xff1a;https://mvnrepository.com/artifact/org.redisso…

Oracle故障处理:ORA-00600错误处理思路

提前说明&#xff1a; 该故障&#xff0c;我只是旁观者。 但处理该故障的DBA工程师&#xff0c;思路很清晰&#xff0c;我非常受教&#xff01;在此也将经验分享。 目录 项目场景 问题分析 优化建议 项目场景 在某项目数据库运维群&#xff0c;有现场同事发了张报错截图如下…

数据库服务类--Redis--未授权访问终端Getshell

免责声明:本文仅做技术交流与学习. 目录 前提条件: windows上开启redis服务: Linux上创建&开启redis服务: 操作: 1-连接靶机redis 2-写入webshell 3-访问后门 redis--->webshell Redis未授权访问漏洞复现与利用 - 知乎 (zhihu.com) 前提条件: 端口开放(6379) 目录…

C++:特殊成员函数

构造函数、析构函数和拷贝构造函数是C类中的三种特殊成员函数&#xff0c;它们分别用于对象的初始化、清理和拷贝操作。 1.构造函数&#xff08;Constructor&#xff09;&#xff1a;构造函数在对象创建时自动调用&#xff0c;用于初始化对象的成员变量。它的名称与类名相同&a…

《动手学深度学习(Pytorch版)》Task01:初识深度学习——4.22打卡

深度学习介绍 AI地图 自然语言处理&#xff1a;起源于符号学&#xff0c;如机器翻译&#xff0c;人在几秒钟能反应过来&#xff0c;属于感知问题计算机视觉&#xff1a;图片由像素组成&#xff0c;难以用符号学解释&#xff0c;在图片中进行推理&#xff0c;大部分用概率模型或…

vue3组件封装系列-表单请求

我们在开发一些后台管理系统时&#xff0c;总是会写很多的列表查询页面&#xff0c;如果不封装组件&#xff0c;就会无限的复制粘贴&#xff0c;而且页面很冗余&#xff0c;正常情况下&#xff0c;我们都是要把组件进行二次封装&#xff0c;来达到我们想要效果。这里我分享一下…

uni-app 的 扩展组件(uni-ui) 与uView UI

uni-app 的 扩展组件&#xff08;uni-ui&#xff09; 与uView UI uni-ui 官方背景&#xff1a;组件集&#xff1a;设计风格&#xff1a;文档与支持&#xff1a;社区与生态&#xff1a; uView UI 第三方框架&#xff1a;组件集&#xff1a;设计风格&#xff1a;文档与支持&#…