react新建项目(React–》从零开始搭建一个文章后台管理系统)
目录
项目准备
项目搭建
scss预处理器的使用
配置基础路由
组件库antd的使用
开发者工具的安装
登录模块
基本样式搭建
创建表单结构
获取表单数据并进行相关登录操作
对登录模块的token进行相关处理
路由鉴权实现
后台页面模块
基本页面结构搭建
菜单高亮显示
展示个人信息
退出登录实现
Token失效处理
首页Home页面展示
内容管理Article页面展示
发布文章Publish页面展示
项目的相关优化
项目git上线
项目打包
项目准备
本篇文章讲解的是一个简单的文章后台管理系统 ,系统的功能很简单 ,如下:
登录 、退出;首页;内容(文章)管理:文章列表 、发布文章 、修改文章 。
看完本篇文章你将了解学习到的知识如下:
React官方脚手架:create-react-app
react-hooks
状态管理:mobx
UI组件库:antd v5
ajax请求库:axios
路由:react-router-dom 以及 history
富文本编辑器:react-quill
CSS预编译器:sass
项目搭建
本系统是基于react官方脚手架搭建 ,具体的详细搭建 ,参考文章:React脚手架的搭建与使用 。
使用如下命令生成项目:
npx create-react-app article-pc将生成的文件拖到vscode编辑器 ,删除一些不必要的文件 ,然后终端执行 npm start 即可 ,如下:
scss预处理器的使用
SASS是一种预编译的CSS ,作用类似于Less 。由于React中内置了处理SASS的配置 ,所有在CRA创建的项目中可以直接使用SASS来写样式 ,实现如下:
安装解析sass的第三方包:
npm install sass创建全局样式文件并引入:
配置基础路由
前端路由是能够实现页面跳转的导航按钮 ,在前端领域中 ,路由是必不可少要掌握的技能之一,详情了解的话可以参加我之前的文章:ReactRouter5讲解 ,ReactRouter6讲解 。其实现步骤如下:
安装路由:
npm install react-router-dom实现过程如下:
在src目录下创建router文件夹并创建index.jsx文件 ,里面存放着配置路由的路由表,如下:
打开入口文件进行如下操作:
打开App根组件 ,将路由内容进行呈现 ,如下:
组件库antd的使用
antd是一个经常被react使用的一个组件库 ,大大提高了前端程序员的编码效率 ,详细了解的可以参考一下我之前的文章:UI组件库ant-design的介绍与使用 ,使用步骤如下:
安装 antd 组件库:
npm install antd安装完成之后 ,访问 antd官网 ,随机点击一个功能 ,例如一个按钮的功能 ,如下:
可见如下的效果 ,引入的没毛病
开发者工具的安装
如果你是第一次接触React开发或者说还没有安装react的开发者工具建议还是安装一下 ,如果不能打开谷歌网上商店的uu ,推荐国内安装插件的一个网站:极简插件 。如下:
下载解压,将文件拖到你的浏览器安装插件的界面即可 ,如下以谷歌浏览器为例:
登录模块
接下来开始实现登录页功能的实现 ,具体步骤分为以下几个方面:
基本样式搭建
登录页面需要一个简单的背景图片,有需要的还可以自行添加一个logo图片 ,如下:
import React from react import { Card } from antd import logo from ../../assets/logo.jpg import ./index.scss const Login = () => { return ( <div className=login> <Card className=login-container> <img src={logo} alt="图片" className=login-logo /> {/* 登录表单 */} </Card> </div> ) } export default Login .login{ width: 100%; height: 100%; position: absolute; left: 0; top: 0; background: center/cover url(../../assets/login.jpg); .login-container{ width: 600px; height: 400px; position: absolute; left: 50%; top: 50%; transform: translate(-50%,-50%); box-shadow: 0 0 50px rgb(0 0 0 /10%); } .login-logo{ width: 200px; height: 100px; display: flex; margin: 0 auto 20px; } .login-checkbox-label{ color: #1890ff; } }图片可以自行百度寻找满意的图片 ,如下是个人简单的实现效果:
创建表单结构
表单的登录解构可以参考antd的Form表单对登录框的书写方式 ,如下:
根据antd给出的登录框的书写样式 ,结合自身需求 ,给出如下代码:
import React from react import { Card,Form,Input,Checkbox,Button } from antd import logo from ../../assets/logo.jpg import ./index.scss // Form.Item的简写形式 const Item = Form.Item const Login = () => { return ( <div className=login> <Card className=login-container> <img src={logo} alt="图片" className=login-logo /> {/* 登录表单 */} <Form validateTrigger={[onBlur,onChange]} initialValues={{remember: true}}> <Item name=phone rules={[ { required:true,message:请输入手机号 }, { pattern:/^1[3-9]\d{9}$/, // 设置正则匹配规则 validateTrigger:onBlur, // 设置触发时机失去焦点时触发 message:请输入正确的手机号格式 } ]}> <Input size=large placeholder=请输入手机号 /> </Item> <Item name=password rules={[ { required:true,message:请输入密码 }, { len:6, // 设置密码长度为6位数 validateTrigger:onBlur, message:请输入6位密码 } ]}> <Input size=large placeholder=请输入密码 /> </Item> <Item name="remember" valuePropName=checked > <Checkbox className=login-checkbox-label> 我已经阅读并同意 [用户协议] 和 [隐私条款] </Checkbox> </Item> <Item> <Button type=primary htmlType=submit size=large block>登录</Button> </Item> </Form> </Card> </div> ) } export default Login具体的校验规则可在文章末尾给出的源码地址 ,自行下载探索:
获取表单数据并进行相关登录操作
在开始之前先安装好项目要准备的第三方库 ,如下:
安装发送ajax请求的第三方库:
npm install axios安装好发送ajax请求的第三方库之后 ,在src目录下新建utils文件夹 ,里面存放着项目的所有工具函数 ,当然发送ajax的工具函数也会放置在里面 ,命名为 http.jsx ,如下:
// 封装axios import axios from "axios"; const http = axios.create({ baseURL:"http://geek.itheima.net/v1_0", timeout:5000 // 超时时间定下5秒钟 }) // 添加请求拦截器 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 default http定义好发送ajax请求的工具函数后,在当前utils文件夹下新建一个index.jsx文件 ,用来封装整合所有的工具函数 ,以后所有书写的工具函数都会存放到这,便于调用:
// 先把所有的工具函数导出的模块在这里导入 ,整合在一起再统一导出 import http from ./http.jsx export { http }安装集中式状态管理工具Mobx:
npm install mobx mobx-react-lite安装好mobx状态管理工具之后 ,在src目录下新建一个store文件夹用来处理所有要使用的状态 ,如下将要使用的登录的token存放到状态中 ,并命名为 login.jsx文件 ,如下:
// login module import { makeAutoObservable } from mobx import { http } from ../utils class LoginStore { token = constructor(){ // 响应式 makeAutoObservable(this) } getToken = async({mobile,code}) => { // 调用登录接口 const res = await http.post(http://geek.itheima.net/v1_0/authorizations,{mobile,code}) // 存入token this.token = res.data.token } } export default LoginStore定义好状态文件后 ,还需要在store文件夹下新建一个index.jsx文件 ,用来管理所有的要操作状态的函数和方法 ,如下:
// 把所有模块进行一个统一的处理 ,导出一个统一的方法 useStore import React from "react"; import LoginStore from "./login"; class RootStore { constructor(){ this.LoginStore = new LoginStore() } } // 实例化根 导出useStore context const rootStore = new RootStore() const context = React.createContext(rootStore) const useStore = () => React.useContext(context) export default useStore登录的接口和状态都配置完成之后 ,便开始需要在登录页面调用要获取接口的getToken函数 ,得到自己的token之后 ,便进行编程式路由导航,进行页面的跳转 ,如下:
因为调用的接口是固定死的 ,密码必须是这个,用户名可以随便输入:
对登录模块的token进行相关处理
token对于登录模块而言至关重要 ,它保证着你登录后能够坚持登录后数据的时间 ,以及不同的token登录会获取不同的数据的一个身份凭证 ,所有在设计登录模块的时候 ,通常会进行如下操作:
token持久化:因为设置token持久化的也是一个工具函数 ,所以还是需要封装在utils文件夹下面并设置相关操作token的函数 ,如下:
// 定义操作token的函数 const key = pc-key // 存token const setToken = (token) =>{ return window.localStorage.setItem(key,token) } // 取token const getToken = ()=>{ return window.localStorage.getItem(key) } // 删token const removeToken = ()=>{ return window.localStorage.removeItem(key) } export { setToken, getToken, removeToken }将封装好的token工具函数也存放到当前文件夹下的index.jsx文件夹下:
// 先把所有的工具函数导出的模块在这里导入 ,整合在一起再统一导出 import http from ./http.jsx import{ setToken,getToken,removeToken} from ./token.jsx export { http, setToken, getToken, removeToken }接下来需要将设置好的操作token的函数导入到store状态管理工具里面即可 ,如下:
请求拦截器注入token: 在每次接口正式发送之前进行拦截 ,将获取到的token进行装入 ,凡是调用了自己设计的接口请求 ,就会自动拥有token ,不需要每次发送请求时都去请求一遍token接口函数,起到了一处配置多处生效的效果 。如下:
路由鉴权实现
先解释一下什么是路由鉴权 ,假设你知道登录后台主页的访问路径 ,在没有登录的情况下,你能直接访问后台主页的路径吗?答案是肯定的 (在没有设置路由鉴权的情况下) ,所以后台设置路由鉴权极为重要 。具体过程如下:
实现思路:自己封装一个路由鉴权的高阶组件 ,实现未登录拦截 ,并跳转到登录页面 。判断本地是否有token ,如果有就返回登录之后的子组件 ,否则就重定向到登录的Login组件 。
在component文件夹下新建authComponent文件 ,用来对登录页面进行鉴权 ,如果本地没有token值就强制跳转到登录页面 ,如下:
// 判断token是否存在 ,如果存在正常渲染 ,如果不存在重定向到登录路由 import { getToken } from "../utils"; import { Navigate } from "react-router-dom"; const AuthComponent = ({children}) =>{ const isToken = getToken() if(isToken){ return <>{children}</> }else{ return <Navigate to=/login replace /> } } export default AuthComponent设置好函数之后路由表对其进行判断 ,如下:
后台页面模块
接下来进行后台页面的搭建 ,这里也可以借助antd的Layout布局和Menu导航菜单 。
基本页面结构搭建
整出代码如下:
import React, { useState } from react; import { Outlet } from react-router-dom import { Layout, Menu, theme,Popconfirm } from antd; import { MenuFoldOutlined, MenuUnfoldOutlined, LogoutOutlined } from @ant-design/icons; import items from ../../config/index.jsx import ./index.scss const { Header, Sider, Content,Footer } = Layout; const App = () => { const [collapsed, setCollapsed] = useState(false); const { token: { colorBgContainer }, } = theme.useToken(); return ( <Layout> <Sider trigger={null} collapsible collapsed={collapsed} className=sider> <div className="logo" /> <Menu theme="dark" mode="inline" defaultSelectedKeys={[1]} items={items} /> </Sider> <Layout className="site-layout"> <Header style={{ padding: 0,background: colorBgContainer }}> {React.createElement(collapsed ? MenuUnfoldOutlined : MenuFoldOutlined, { className: trigger, onClick: () => setCollapsed(!collapsed), })} {/* <div className="user-info"> */} <span className="user-name">username</span> <span className="user-logout"> <Popconfirm // onConfirm={onConfirm} title="是否确认退出?" okText="退出" cancelText="取消"> <LogoutOutlined /> 退出 </Popconfirm> </span> {/* </div> */} </Header> <Content style={{ margin: 24px 16px 0px, padding: 24, minHeight: 280, background: colorBgContainer, overflowY: auto }} > <Outlet /> </Content> <Footer style={{ textAlign: center, }} > Ant Design ©2023 Created by Ant UED </Footer> </Layout> </Layout> ); }; export default App;将items文件单独抽离出来如下代码:
import { Link } from react-router-dom import { HomeOutlined,DiffOutlined,EditOutlined } from @ant-design/icons; function getItem(label, key, icon, children, type) { return { key, icon, children, label, type, }; } const items = [ getItem(<Link to=/layout/home>数据概览</Link>, 1, <HomeOutlined />), getItem(<Link to=/layout/article>内容管理</Link>, 2, <DiffOutlined />), getItem(<Link to=/layout/publish>发布文章</Link>, 3, <EditOutlined />), ]; export default items给出的代码样式为:
.ant-layout { height: 100%; } .ant-layout-sider{ flex: 0 0 235px !important; max-width: 300px !important; } .sider { padding: 0; } .logo { width: 225px; height: 60px; background: url(../../assets/logo.jpg) no-repeat center / 160px auto; margin: 10px auto 10px; } .ant-layout-header svg{ font-size: 15px; margin-left: 15px; } .user-name { position: absolute; right: 5%; margin-right: 10px; margin-left: 22px; } .user-logout { position: absolute; right: 2%; display: inline-block; cursor: pointer; } #components-layout-demo-custom-trigger .trigger { padding: 0 24px; font-size: 18px; line-height: 64px; cursor: pointer; transition: color 0.3s; } #components-layout-demo-custom-trigger .trigger:hover { color: #1890ff; } #components-layout-demo-custom-trigger .logo { height: 32px; margin: 16px; background: rgba(255, 255, 255, 0.3); }在单独为每个导航菜单创建一个单独的组件进行页面的切换显示,如下:
呈现的结果如下:
菜单高亮显示
配置完路由后 ,需要对菜单进行相应的高亮显示 ,在其刷新之后还是处于我们选中的菜单路由,点击浏览器的回退按钮会回退到上一个点击的菜单路由 ,如下:
具体思路:使用 useLocation 拿到当前的访问路径 ,根据路径修改key值 ,来选中当前的key来实现菜单路由的高亮 ,如下:
展示个人信息
接下来实现后台页面右上角的用户名信息的展示 ,这里需要借助状态管理工具mobx ,如下:
// 获取当前的用户名称即手机号 import { makeAutoObservable } from "mobx"; import { http } from "../utils"; class UserStore { userInfo = {} constructor(){ makeAutoObservable(this) } getUserInfo = async() => { // 调用接口数据 const res = await http.get(/user/profile) this.userInfo = res.data } } export default UserStore将定义好的管理用户名的状态存放到根store里面 ,如下:
// 把所有模块进行一个统一的处理 ,导出一个统一的方法 useStore import React from "react"; import LoginStore from "./login"; import UserStore from ./username class RootStore { constructor(){ this.LoginStore = new LoginStore() this.UserStore = new UserStore() } } // 实例化根 导出useStore context const rootStore = new RootStore() const context = React.createContext(rootStore) const useStore = () => React.useContext(context) export default useStore退出登录实现
退出登录需要对token进行删除 ,具体操作如下:
在处理登录的mobx状态文件中 ,新增一个退出的功能:
Token失效处理
在日常开发中我们不能让token一直保持活性 ,需要给其一定的寿命 ,超过时间token就失活,需要重新登录 ,这样会保证用户一定的安全性 ,而当token发送错误时应该如何操作,具体操作如下:
当token发生错误时 ,调用 window 去跳转到登录页面即可 。
首页Home页面展示
首页Home页面采用 echarts 图表封装进行数据显示 ,这里需要借助 exharts官网 ,进行相关操作如下 ,需要先安装 echarts 第三方插件包 ,并使用其第一个案例:
引入之后 ,在components组件中单独创建应该设置图表的组件 ,如下:
// 封装图表bar组件 import * as echarts from echarts import { useEffect, useRef } from react const Bar = ({ title, xData, yData, style }) =>{ const domRef = useRef() 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 } ] }) } useEffect(()=>{ chartInit() }) return ( <div> {/* 准备一个挂载节点 */} <div ref={domRef} style={style}></div> </div> ) } export default Bar在Home组件中导入设置图表的组件Bar ,如下:
import React from react import Bar from ../../components/Bar import ./index.scss const Home = () => { return ( <div className=home> {/* 渲染Bar组件 */} <Bar title=主流框架满意度 xData={[react, vue, angular]} yData={[30, 40, 50]} style={{ width: 500px, height: 400px }} /> <Bar title=主流框架使用度 xData={[react, vue, angular]} yData={[60, 70, 80]} style={{ width: 500px, height: 400px }} /> </div> ) } export default Home设置样式如下:
.home { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; }最后的界面如下:
内容管理Article页面展示
内容的article组件需要借助 antd中 Card 、Form 、DatePicker等功能块 ,给出代码如下:
import { useEffect, useState } from react import { Link, useNavigate } from react-router-dom import { observer } from mobx-react-lite import { Table, Space, Card, Breadcrumb, Form, Button, Radio, DatePicker, Select } from antd import { EditOutlined, DeleteOutlined } from @ant-design/icons import locale from antd/es/date-picker/locale/zh_CN import { http } from ../../utils import img404 from ../../assets/error.jpg const { Option } = Select const { RangePicker } = DatePicker const Article = () => { // 路由导航 const navigate = useNavigate() // 频道列表管理 const [channelList,setChannelList] = useState([]) // 文章列表管理 const [articleData,setArticleData] = useState({ list:[], // 文章列表 count:0 // 文章数量 }) // 文章参数管理 const [params,setParams] = useState({ page:1, per_page:10 }) // 获取频道管理的数据 const loadChannelList = async () =>{ const res = await http.get(/channels) setChannelList(res.data.channels) } useEffect(()=>{ loadChannelList() },[]) useEffect(()=>{ // 获取文章列表数据 const loadList = async () =>{ const res = await http.get(/mp/articles,{ params }) const { results,total_count } = res.data setArticleData({ list:results, count:total_count }) } loadList() },[params]) const onFinish = (values) =>{ console.log(values); // 获取表单数据 const { channel_id,date,status } = values // 数据处理 const _params = {} if(status !== -1){ _params.status = status } if(channel_id){ _params.channel_id = channel_id } if(date){ _params.begin_pubdate = date[0].format(YYYY-MM-DD) _params.end_pubdate = date[1].format(YYYY-MM-DD) } // 修改params数据 引起接口的重新发送 对象的合并是一个整体覆盖 改了对象的整体引用 setParams({ ...params, ..._params }) } // 翻页实现 const pageChange = (page) => { setParams({ ...params, page }) } // 删除文章 const delArticle = async (data) => { await http.delete(`/mp/articles/${data.id}`) // 刷新一下列表 setParams({ ...params, page: 1 }) } // 编辑文章 const goPublish = (data) => { navigate(`/layout/publish?id=${data.id}`) } 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, }, { 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 />} onClick={() => goPublish(data)} /> <Button type="primary" danger shape="circle" icon={<DeleteOutlined />} onClick={() => delArticle(data)} /> </Space> ) }, fixed: right } ] return ( <div> {/* 筛选区域 */} <Card title={ <Breadcrumb separator=">" items={[ {title:<Link to="/layout/home">首页</Link>}, {title:内容管理} ]} /> } style={{ marginBottom: 20 }} > <Form onFinish={onFinish} initialValues={{ status: }}> <Form.Item label="状态" name="status"> <Radio.Group> <Radio value=>全部</Radio> <Radio value={0}>草稿</Radio> <Radio value={1}>待审核</Radio> <Radio value={2}>审核通过</Radio> <Radio value={3}>审核失败</Radio> </Radio.Group> </Form.Item> <Form.Item label="频道" name="channel_id"> <Select placeholder="请选择文章频道" style={{ width: 120 }} > {channelList.map(channel => <Option key={channel.id} value={channel.id}>{channel.name}</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: 80 }}> 筛选 </Button> </Form.Item> </Form> </Card> {/* 文章列表区域 */} <Card title={`根据筛选条件共查询到 ${articleData.count} 条结果:`}> <Table rowKey="id" columns={columns} dataSource={articleData.list} pagination={ { pageSize: params.per_page, total: articleData.count, onChange: pageChange, current: params.page } } /> </Card> </div> ) } export default observer(Article)实现的界面如下:
发布文章Publish页面展示
发布文章这个界面需要使用富文本编辑器 ,这里需要借助第三方插件库 ,详细的使用教程可以参考一下github上的介绍,网址为:react-quill ,其安装命令如下:
npm install react-quill给出如下代码:
import { useEffect, useRef, useState } from react import { Link, useNavigate, useSearchParams } from react-router-dom import { observer } from mobx-react-lite import { Card,Breadcrumb,Form,Button,Radio,Input,Upload,Space,Select,message} from antd import { PlusOutlined } from @ant-design/icons import ./index.scss import ReactQuill from react-quill import react-quill/dist/quill.snow.css import useStore from ../../store import http from ../../utils/http const { Option } = Select const Item = Form.Item const Publish = () => { const navigate = useNavigate() // 获取频道数据 const { ChannelStore } = useStore() // 存放上传图片的列表 const [fileList,setFileList] = useState([]) // 保存的图片数量 const [imgCount,setImgCount] = useState(1) // 声明图片的暂存仓库 const cacheImgList = useRef() // 获取表单数据 const onFinish = async(values) =>{ // 数据的二次处理 重点是处理cover字段 const { channel_id, content, title, type } = values // 判断type fileList 是匹配的才能正常提交 const params = { channel_id, content, title, type, cover: { type: type, images: fileList.map(item => item.url) } } if (id) { await http.put(`/mp/articles/${id}?draft=false`, params) } else { await http.post(/mp/articles?draft=false, params) } // 跳转列表 提示用户 navigate(/layout/article) message.success(`${id ? 更新成功 : 发布成功}`) } const onUploadChange = ({fileList}) =>{ // 这里关键位置:需要做数据格式化 const formatList = fileList.map(file => { // 上传完毕 做数据处理 if (file.response) { return { url: file.response.data.url } } // 否则在上传中时 ,不做处理 return file }) // 存放data数据 setFileList(formatList) // 同时把图片列表存入仓库一份 cacheImgList.current = formatList } // 切换图片 const radioChange = (e) =>{ const rawValue = e.target.value setImgCount(rawValue) console.log(cacheImgList.current); // 从仓库里面获取对应的图片数量 交给用来渲染图片的fileList if(cacheImgList.current === undefined || 0){ return false } if( rawValue === 1 ){ const img = cacheImgList.current ? cacheImgList.current[0] : [] setFileList([img]) }else if ( rawValue === 3 ){ setFileList(cacheImgList.current) } } // 编辑功能 文案适配 路由参数id 判断条件 const [params] = useSearchParams() const id = params.get(id) // 数据回填 id调用接口 1.表单回填 2.暂存列表 3.Upload组件fileList const [form] = Form.useForm() useEffect(() => { %2创心域SEO版权声明:以上内容作者已申请原创保护,未经允许不得转载,侵权必究!授权事宜、对本内容有异议或投诉,敬请联系网站管理员,我们将尽快回复您,谢谢合作!