开始
基于CRA创建项目,示例代码:
1
| npx create-react-app react-jike
|
在src
目录下,创建如下文件夹:
apis
,项目接口函数。
assets
,项目资源文件,比如,图片等。
components
,通用组件。
pages
,页面组件。
store
,集中状态管理。
utils
,工具,比如,token、axios的封装等。
App.js
,根组件。
index.scss
,全局样式。
index.js
,项目入口。
安装插件:
AntD
:npm i antd
。
- 路由支持:
npm i react-router-dom
- 别名路径:
npm i @craco/craco
,该插件需要在项目中添加配置文件,具体可以参考《6.React [2/4]》。
- 新增VSCode提示配置,
jsconfig.json
,具体可以参考《6.React [2/4]》。
新增如下文件:
pages/Layout/index.js
:1 2 3 4
| const Layout = () => { return <div>this is layout</div> } export default Layout
|
pages/Login/index.js
:1 2 3 4
| const Login = () => { return <div>this is login</div> } export default Login
|
router/index.js
:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| 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
:1 2 3 4 5 6 7 8 9
| 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} /> )
|
登录
基本结构
pages/Login/index.js
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| 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
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| .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; } }
|
表单校验
步骤:
- 为
Form
组件添加validateTrigger
属性,指定校验触发时机的集合。
- 为
Form.Item
组件添加name
属性。
- 为
Form.Item
组件添加rules
属性,用来添加表单校验规则对象。
page/Login/index.js
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| 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 validateTrigger='onBlur'> <Form.Item name="mobile" rules={[ { required: true, message: '请输入手机号' }, { pattern: /^1[3-9]\d{9}$/, message: '手机号码格式不对' } ]}> <Input size="large" placeholder="请输入手机号" /> </Form.Item> <Form.Item name="code" rules={[ { required: true, message: '请输入验证码' }, ]}> <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
|
获取表单数据
步骤:
- 为
Form
组件添加onFinish
属性,该事件会在点击登录按钮时触发。
- 创建
onFinish
函数,通过函数参数values
拿到表单值。
pages/Login/index.js
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| const onFinish = formValue => { console.log(formValue) }
const Login = () => { return ( 【部分代码略】 {} <Form validateTrigger='onBlur' onFinish={ onFinish }> 【部分代码略】 </Form> 【部分代码略】 ) }
export default Login
|
封装request工具模块
在整个项目中会发送很多网络请求,并且:
- 几乎所有的接口都是一样的接口域名。
- 几乎所有的接口都需要设置一样的超时时间。
- 几乎所有的接口都需要做Token权限处理
我们可以做好统一封装,这样方便统一管理和复用。
步骤:
- 安装
axios
到项目。
- 创建
utils/request.js
文件。
- 创建
axios
实例,配置baseURL
(根域名配置)、超时时间、请求拦截器、响应拦截器。
- 在
utils/index.js
中,统一导出request
。
utils/request.js
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| import axios from 'axios'
const request = axios.create({ baseURL: 'http://geek.itheima.net/v1_0', timeout: 5000 })
request.interceptors.request.use((config)=> { return config }, (error)=> { return Promise.reject(error) })
request.interceptors.response.use((response)=> { return response.data }, (error)=> { return Promise.reject(error) })
export { request }
|
utils/index.js
:
1 2
| import { request } from './request' export { request }
|
解释说明:再用utils/index.js
封装一层,这么做的原因是,我们可能会在uitls
中封装多个模块。所以,统一中转工具模块函数,import { request } from '@/utils'
。
使用Redux管理token
Token
作为用户的标识数据,需要在很多个模块中共享,我们可以基于Redux解决状态共享问题。
安装Redux相关工具包:
1 2
| npm i react-redux npm i @reduxjs/toolkit
|
store/modules/user.js
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| import { createSlice } from '@reduxjs/toolkit' import { request } from '@/utils' const userStore = createSlice({ name: 'user', initialState: { token:'' }, reducers: { setToken (state, action) { state.token = action.payload } } })
const { setToken } = userStore.actions
const userReducer = userStore.reducer
const fetchLogin = (loginForm) => { return async (dispatch) => { const res = await request.post('/authorizations', loginForm) dispatch(setToken(res.data.token)) } }
export { fetchLogin }
export default userReducer
|
store/index.js
:
1 2 3 4 5 6 7 8 9 10
| import { configureStore } from '@reduxjs/toolkit'
import userReducer from './modules/user'
export default configureStore({ reducer: { user: userReducer } })
|
修改index.js
,添加Provider:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| import React from 'react' import ReactDOM from 'react-dom/client' import './index.scss' import router from './router' import { RouterProvider } from 'react-router-dom'
import store from './store'
import { Provider } from 'react-redux'
ReactDOM.createRoot(document.getElementById('root')).render( <Provider store={store}> <RouterProvider router={router} /> </Provider> )
|
登录逻辑
业务逻辑:
- 跳转到首页
- 提示用户登录成功
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| 【部分代码略】
import { fetchLogin } from '@/store/modules/user' import { useDispatch } from 'react-redux' import { useNavigate } from "react-router-dom" import { message } from 'antd'
const Login = () => { const dispatch = useDispatch() const navigate = useNavigate() const onFinish = async formValue => { await dispatch(fetchLogin(formValue)) navigate('/') message.success('登录成功') }
return ( 【部分代码略】 ) }
export default Login
|
token持久化
封装存取方法
utils/token.js
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
|
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 }
|
持久化逻辑
store/modules/user.js
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| 【部分代码略】
import { getToken, setToken as _setToken } from '@/utils'
const userStore = createSlice({ name: 'user', initialState: { token: getToken() || '' }, reducers: { setToken (state, action) { state.token = action.payload _setToken(state.token) } } })
【部分代码略】
|
请求拦截器注入token
业务背景: Token作为用户的数据标识,在接口层面起到了接口权限控制的作用,也就是说后端有很多接口都需要通过查看当前请求头信息中是否含有token数据,来决定是否正常返回数据。
拼接方式:
1
| config.headers.Authorization = Bearer ${token}
|
utils/request.js
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| 【部分代码略】
import { getToken } from './token'
【部分代码略】
request.interceptors.request.use((config)=> { const token = getToken() if (token) { config.headers.Authorization = `Bearer ${token}` } return config }, (error)=> { return Promise.reject(error) })
【部分代码略】
|
路由鉴权实现
封装AuthRoute
路由鉴权高阶组件,实现未登录拦截,并跳转到登录页面。判断本地是否有token,如果有,就返回子组件,否则就重定向到登录Login
步骤:
- 在
components
目录中,创建AuthRoute.js
文件。
- 登录时,直接渲染相应页面组件。
- 未登录时,重定向到登录页面。
- 将需要鉴权的页面路由配置,替换为
AuthRoute
组件渲染。
components/AuthRoute.js
:
1 2 3 4 5 6 7 8 9 10 11 12 13
| 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
|
router/index.js
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| import { createBrowserRouter } from 'react-router-dom'
import Login from '@/pages/Login' import Layout from '@/pages/Layout' import AuthRoute from '@/components/AuthRoute'
const router = createBrowserRouter([ { path: '/', element: <AuthRoute><Layout /></AuthRoute>, }, { path: '/login', element: <Login />, }, ])
export default router
|
Layout
基本结构
pages/Layout/index.js
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
| 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: <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"> <Menu mode="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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| .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; }
|
样式初始化
如图所示,在上文完成了基本结构,但是整个样式很奇怪,我们需要进行如下操作:
- 安装
normalize.css
:1
| npm install normalize.css
|
- 定义
index.scss
:1 2 3 4 5 6 7 8 9
| html, body { margin: 0; height: 100%; }
#root { height: 100%; }
|
- 在
index.js
中引入上述两个文件:1 2
| import 'normalize.css' import './index.scss'
|
二级路由配置
步骤:
- 在
pages
目录中,分别创建:Home
(数据概览)、Article
(内容管理)、Publish
(发布文章)页面文件夹。
- 分别在三个文件夹中创建
index.js
并创建基础组件后导出。
- 在
router/index.js
中配置嵌套子路由,在Layout
中配置二级路由出口。
- 使用
Link
修改左侧菜单内容,与子路由规则匹配实现路由切换。
pages/Home/index.js
:
1 2 3 4 5
| const Home = () => { return <div>Home</div> }
export default Home
|
pages/Article/index.js
:
1 2 3 4 5
| const Article = () => { return <div>Article</div> }
export default Article
|
pages/Publish/index.js
:
1 2 3 4 5
| const Publish = () => { return <div>Publish</div> }
export default Publish
|
router/index.js
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| import { createBrowserRouter } from 'react-router-dom'
import Login from '@/pages/Login' import GeekLayout from '@/pages/Layout' import Publish from '@/pages/Publish' import Article from '@/pages/Article' import Home from '@/pages/Home' import AuthRoute from '@/components/AuthRoute'
const router = createBrowserRouter([ { path: '/', element: <AuthRoute> <GeekLayout /> </AuthRoute> , children: [ { index: true, element: <Home />, }, { path: 'article', element: <Article />, }, { path: 'publish', element: <Publish />, }, ], }, { path: '/login', element: <Login />, }, ])
export default router
|
修改pages/Layout/index.js
,配置二级路由出口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
| import { Layout, Menu, Popconfirm } from 'antd' import { HomeOutlined, DiffOutlined, EditOutlined, LogoutOutlined, } from '@ant-design/icons' import './index.scss' import { Outlet } from "react-router-dom"
const { Header, Sider } = Layout
const 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"> <Menu mode="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 3 4 5
| import { Outlet } from "react-router-dom"
<Layout className="layout-content" style={{ padding: 20 }}> <Outlet /> </Layout>
|
路由菜单点击交互
点击菜单跳转路由
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| import { Outlet, useNavigate } from "react-router-dom"
const { Header, Sider } = Layout
const items = [ { label: '首页', key: '/', icon: <HomeOutlined />, }, { label: '文章管理', key: '/article', icon: <DiffOutlined />, }, { label: '创建文章', key: '/publish', icon: <EditOutlined />, }, ]
<Menu mode="inline" theme="dark" defaultSelectedKeys={['1']} items={items} style={{ height: '100%', borderRight: 0 }} onClick={ menuClick }> </Menu>
|
菜单反向高亮
实现效果:页面在刷新时可以根据当前的路由路径让对应的左侧菜单高亮显示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
| import { Outlet, useNavigate, useLocation } 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) }
const location = useLocation() const selectedKey = location.pathname
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"> <Menu mode="inline" theme="dark" selectedKeys={selectedKey} defaultSelectedKeys={['1']} items={items} style={{ height: '100%', borderRight: 0 }} onClick={ menuClick }> </Menu> </Sider> <Layout className="layout-content" style={{ padding: 20 }}> <Outlet /> </Layout> </Layout> </Layout> ) }
export default GeekLayout
|
展示个人信息
步骤:
- 在Redux的store中编写获取用户信息的相关逻辑。
- 在Layout组件中触发action的执行。
- 在Layout组件使用使用store中的数据进行用户名的渲染。
在store/modules/user.js
新增userInfo
相关代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| import { createSlice } from '@reduxjs/toolkit' import { request } from '@/utils'
import { getToken, setToken as _setToken } from '@/utils'
const userStore = createSlice({ name: 'user', initialState: { token: getToken() || '', userInfo: {} }, reducers: { setToken (state, action) { state.token = action.payload _setToken(state.token) }, setUserInfo (state, action){ state.userInfo = action.payload } } })
const { setToken, setUserInfo } = userStore.actions
const userReducer = userStore.reducer
const fetchLogin = (loginForm) => { return async (dispatch) => { const res = await request.post('/authorizations', loginForm) dispatch(setToken(res.data.token)) } }
const fetchUserInfo = () => { return async (dispatch) => { const res = await request.get('/user/profile') dispatch(setUserInfo(res.data)) } }
export { fetchLogin, fetchUserInfo }
export default userReducer
|
pages/Layout/index.js
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| import { useEffect } from "react" import { fetchUserInfo } from '@/store/modules/user' import { useDispatch, useSelector } from 'react-redux'
const dispatch = useDispatch() const name = useSelector(state => state.user.userInfo.name) useEffect(() => { dispatch(fetchUserInfo()) }, [dispatch])
<Header className="header"> <span className="user-name">{ name }</span>
</Header>
|
退出登录
步骤:
- 为气泡确认框添加确认回调事件
- 在
store/userStore.js
中新增退出登录的action
函数,在其中删除token
。
- 在回调事件中,调用
userStore
中的退出action
。
- 清除用户信息,返回登录页面。
store/modules/user.js
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| import { createSlice } from '@reduxjs/toolkit' import { request } from '@/utils'
import { getToken, setToken as _setToken, clearToken as _clearToken } from '@/utils'
const userStore = createSlice({ name: 'user', initialState: { token: getToken() || '', userInfo: {} }, reducers: { setToken (state, action) { state.token = action.payload _setToken(state.token) }, setUserInfo (state, action){ state.userInfo = action.payload }, clearToken (state) { state.token = '' state.userInfo = {} _clearToken() } } })
const { setToken, setUserInfo, clearToken } = userStore.actions
const userReducer = userStore.reducer
const fetchLogin = (loginForm) => { return async (dispatch) => { const res = await request.post('/authorizations', loginForm) dispatch(setToken(res.data.token)) } }
const fetchUserInfo = () => { return async (dispatch) => { const res = await request.get('/user/profile') dispatch(setUserInfo(res.data)) } }
export { fetchLogin, fetchUserInfo, clearToken }
export default userReducer
|
pages/Layout/index.js
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| import { clearToken, fetchUserInfo } from '@/store/modules/user'
【部分代码略】
const GeekLayout = () => { const navigate = useNavigate()
【部分代码略】
const logOut = () => { dispatch(clearToken()) navigate('/login') }
return (
【部分代码略】
<span className="user-logout"> <Popconfirm title="是否确认退出?" okText="退出" cancelText="取消" onConfirm={ logOut }> <LogoutOutlined /> 退出 </Popconfirm> </span>
【部分代码略】
) }
export default GeekLayout
|
处理Token失效
- 什么是Token失效?
为了用户的安全和隐私考虑,在用户长时间未在网站中做任何操作且规定的失效时间到达之后,当前的Token就会失效,一旦失效,不能再作为用户令牌标识请求隐私数据。
- 前端如何知道Token已经失效了?
通常在Token失效之后再去请求接口,后端会返回401状态码,前端可以监控这个状态做后续的操作
- Token失效了前端做什么?
- 在axios拦截中监控401状态码
- 清除失效Token,跳转登录
utils/request.js
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| import { getToken, clearToken } from './token' import router from '@/router'
request.interceptors.response.use((response)=> { return response.data }, (error)=> { console.dir(error) if (error.response.status === 401) { clearToken() router.navigate('/login') window.location.reload() } return Promise.reject(error) })
|
首页Home图表展示
echarts
ECharts,一款基于JavaScript的数据可视化图表库,提供直观,生动,可交互,可个性化定制的数据可视化图表。
图表基础Demo
安装ECharts:
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| import { useEffect, useRef } from 'react' import * as echarts from 'echarts'
const Home = () => { const chartRef = useRef(null) useEffect(() => { const myChart = echarts.init(chartRef.current) 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' } ] } myChart.setOption(option) }, [])
return ( <div> <div ref={chartRef} style={{ width: '400px', height: '300px' }} /> </div > ) }
export default Home
|
组件封装
pages/Home/components/BarChart.js
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| import { useRef, useEffect } from 'react' import * as echarts from 'echarts'
const BarChart = ({ xData, sData, style = { width: '400px', height: '300px' } }) => { const chartRef = useRef(null) useEffect(() => { const myChart = echarts.init(chartRef.current) const option = { xAxis: { type: 'category', data: xData }, yAxis: { type: 'value' }, series: [ { data: sData, type: 'bar' } ] } myChart.setOption(option) }, [sData, xData]) return <div ref={chartRef} style={style}></div> }
export { BarChart }
|
pages/Home/index.js
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| import { BarChart } from './components/BarChart'
const Home = () => { return ( <div> <BarChart xData={['Vue', 'React', 'Angular']} sData={[2000, 5000, 1000]} />
<BarChart xData={['Vue', 'React', 'Angular']} sData={[200, 500, 100]} style={{ width: '500px', height: '400px' }} /> </div > ) }
export default Home
|
API模块封装
我们再把上文的请求,都封装为API模块。
apis/user.js
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| import { request } from "@/utils"
export function loginAPI (formData) { return request({ url: '/authorizations', method: 'POST', data: formData }) }
export function getProfileAPI () { return request({ url: '/user/profile', method: 'GET' }) }
|
store/modules/user.js
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| import { getProfileAPI, loginAPI } from '@/apis/user'
const fetchLogin = (loginForm) => { return async (dispatch) => { const res = await loginAPI(loginForm) dispatch(setToken(res.data.token)) } }
const fetchUserInfo = () => { return async (dispatch) => { const res = await getProfileAPI() dispatch(setUserInfo(res.data)) } }
|
发布文章
基本结构
创建基本结构
pages/Publish/index.js
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
| 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 ( <div className="publish"> <Card title={ <Breadcrumb items={[ { title: <Link to={'/'}>首页</Link> }, { title: '发布文章' }, ]} /> } > <Form labelCol={{ span: 4 }} wrapperCol={{ span: 16 }} initialValues={{ type: 1 }} > <Form.Item label="标题" name="title" rules={[{ required: true, message: '请输入文章标题' }]} > <Input placeholder="请输入文章标题" style={{ width: 400 }} /> </Form.Item> <Form.Item label="频道" name="channel_id" rules={[{ required: true, message: '请选择文章频道' }]} > <Select placeholder="请选择文章频道" style={{ width: 400 }}> <Option value={0}>推荐</Option> </Select> </Form.Item> <Form.Item label="内容" 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
:
1 2 3 4 5 6 7 8 9 10 11
| .publish { position: relative; } .ant-upload-list { .ant-upload-list-picture-card-container, .ant-upload-select { width: 146px; height: 146px; } }
|
准备富文本编辑器
步骤:
- 安装富文本编辑器。
- 导入富文本编辑器组件以及样式文件。
- 渲染富文本编辑器组件。
- 调整富文本编辑器的样式。
react-quill
,富文本编辑器。
安装react-quill
:
导入资源渲染组件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| import ReactQuill from 'react-quill' import 'react-quill/dist/quill.snow.css'
<Form labelCol={{ span: 4 }} wrapperCol={{ span: 16 }} initialValues={{ type: 1 }}>
<Form.Item label="内容" name="content" rules={[{ required: true, message: '请输入文章内容' }]}>
<ReactQuill className="publish-quill" theme="snow" placeholder="请输入文章内容" /> </Form.Item>
<Form.Item wrapperCol={{ offset: 4 }}>
|
1 2 3 4 5
| .publish-quill { .ql-editor { min-height: 300px; } }
|
频道数据获取
步骤:
- 在API模块封装接口函数。
- 使用
useState
初始化数据和修改数据的方法。
- 在
useEffect
中调用接口并保存数据。
- 使用数据渲染对应模版。
apis/article.js
:
1 2 3 4 5 6 7 8 9
| import { request } from "@/utils"
export function getChannelsAPI () { return request({ url: '/channels', method: 'GET' }) }
|
pages/Publish/index.js
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| import { useState, useEffect } from 'react' import { request } from '@/utils'
const [channels, setChannels] = useState([])
useEffect(() => { async function fetchChannels() { const res = await getChannelsAPI() setChannels(res.data.channels) } fetchChannels()}, [])
<Form.Item label="频道" name="channel_id" rules={[{ required: true, message: '请选择文章频道' }]} > <Select placeholder="请选择文章频道" style={{ width: 400 }}> {channels.map(item => ( <Option key={item.id} value={item.id}> {item.name} </Option> ))} </Select> </Form.Item>
|
发布
步骤:
- 使用Form组件收集表单数据
- 按照接口文档封装接口函数
- 按照接口文档处理表单数据
- 提交接口并验证是否成功
apis/article.js
:
1 2 3 4 5 6 7 8 9 10 11
| import { request } from "@/utils"
export function createArticleAPI (data){ return request({ url: '/mp/articles?draft=false', method: 'POST', data }) }
|
pages/Publish/index.js
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| import { message } from 'antd'
const onFinish = async (formValue) => { const { channel_id, content, title } = formValue const params = { channel_id, content, title, type: 1, cover: { type: 1, images: [] } } await createArticleAPI(params) message.success('发布文章成功') }
<Form labelCol={{ span: 4 }} wrapperCol={{ span: 16 }} initialValues={{ type: 1 }} onFinish={ onFinish } > ······ </Form>
|
上传图片
基本结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <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> <Upload listType="picture-card" showUploadList > <div style={{ marginTop: 8 }}> <PlusOutlined /> </div> </Upload> </Form.Item>
|
上传功能
步骤:
- 为
Upload
组件添加action
属性,配置封面图片上传接口地址。
- 为
Upload
组件添加name
属性,接口要求的字段名。
- 为
Upload
添加onChange
属性,在事件中拿到当前图片数据,并存储到React状态中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| const [imageList, setImageList] = useState([]) const onChange = (info) => { setImageList(info.fileList) }
<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> <Upload listType="picture-card" showUploadList action={'http://geek.itheima.net/v1_0/upload'} name='image' onChange={onChange} > <div style={{ marginTop: 8 }}> <PlusOutlined /> </div> </Upload> </Form.Item>
|
切换图片Type
实现效果:只有当前模式为单图或者三图模式时才显示上传组件
步骤:
- 点击单选框时拿到当前的类型value
- 根据value控制上传组件的显示(大于零时才显示)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| const [imageType, setImageType] = useState(1) const onTypeChange = (e) => { console.log(e) setImageType(e.target.value) }
<Form.Item label="封面"> <Form.Item name="type"> <Radio.Group onChange={onTypeChange}> <Radio value={1}>单图</Radio> <Radio value={3}>三图</Radio> <Radio value={0}>无图</Radio> </Radio.Group> </Form.Item> {imageType > 0 && <Upload listType="picture-card" showUploadList action={'http://geek.itheima.net/v1_0/upload'} name='image' onChange={onChange} > <div style={{ marginTop: 8 }}> <PlusOutlined /> </div> </Upload> } </Form.Item>
|
控制最大上传图片数量
通过maxCount
属性限制图片的上传图片数量。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| {imageType > 0 && <Upload listType="picture-card" showUploadList action={'http://geek.itheima.net/v1_0/upload'} name='image' onChange={onChange} maxCount={imageType} multiple={imageType > 1} > <div style={{ marginTop: 8 }}> <PlusOutlined /> </div> </Upload> }
|
暂存图片列表
业务描述:如果当前为三图模式,已经完成了上传,选择单图只显示一张,再切换到三图继续显示三张。
实现思路:在上传完毕之后通过ref存储所有图片,需要几张就显示几张,其实也就是把ref当仓库,用多少拿多少。
步骤:
- 通过useRef创建一个暂存仓库,在上传完毕图片的时候把图片列表存入。
- 如果是单图模式,就从仓库里取第一张图,以数组的形式存入fileList。
- 如果是三图模式,就把仓库里所有的图片,以数组的形式存入fileList。
注意:需要给Upload组件添加fileList属性,以达成受控的目的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| import { useRef } from 'react'
const cacheImageList = useRef([])
const [imageList, setImageList] = useState([]) const onChange = (info) => { setImageList(info.fileList) cacheImageList.current = info.fileList }
const [imageType, setImageType] = useState(1)
const onTypeChange = (e) => { console.log(e) const type = e.target.value setImageType(e.target.value) if (type === 1) { const imgList = cacheImageList.current[0] ? [cacheImageList.current[0]] : [] setImageList(imgList) } else if (type === 3) { setImageList(cacheImageList.current) } }
{imageType > 0 && <Upload listType="picture-card" showUploadList action={'http://geek.itheima.net/v1_0/upload'} name='image' onChange={onChange} maxCount={imageType} multiple={imageType > 1} fileList={imageList} > <div style={{ marginTop: 8 }}> <PlusOutlined /> </div> </Upload> }
|
发布带封面的文章
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| const onFinish = async (formValue) => { if (imageType !== imageList.length) return message.warning('图片类型和数量不一致') const { channel_id, content, title } = formValue const params = { channel_id, content, title, type: 1, cover: { type: imageType, images: imageList.map(item => item.response.data.url) } } await createArticleAPI(params) message.success('发布文章成功') }
|
文章列表
基本架构
筛选区
pages/Article/index.js
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
| 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 ( <div> <Card title={ <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"> <Select placeholder="请选择文章频道" 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
|
解释说明:initialValues={ { status: '' } }
,设置表单中名为status
的字段的初始值为空字符串,即默认会选中<Radio value={''}>全部</Radio>
。
表格区
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85
| 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 => data === 1 ? <Tag color='warning'>待审核</Tag>:<Tag color='success'>审核通过</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 />} /> <Button type="primary" danger shape="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> ) }
|
渲染频道
又是频道数据,我们在上文也有这个。
我们可以:直接再写一遍、存到Redux中维护、使用自定义业务hook。
在本文,我们选择自定义业务hook。
步骤:
- 创建一个use打头的函数
- 在函数中封装业务逻辑,并return出组件中要用到的状态数据
- 组件中导入函数执行并解构状态数据使用
hooks/useChannel.js
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| import { getChannelsAPI } from "@/apis/article" import { useState, useEffect } from "react"
function useChannel() { const [channels, setChannels] = useState([])
useEffect(() => { async function fetchChannels() { const res = await getChannelsAPI() setChannels(res.data.channels) } fetchChannels() }, [])
return { channels } }
export { useChannel }
|
pages/Article/index.js
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| const { channels } = useChannel()
<Form.Item label="频道" name="channel_id"> <Select placeholder="请选择文章频道" defaultValue="" style={{ width: 120 }} > {channels.map(item => ( <Option key={item.id} value={item.id}> {item.name} </Option> ))} </Select> </Form.Item>
|
渲染表格
步骤:
- 声明列表相关数据管理。
- 使用
useState
声明参数相关数据管理。
- 调用接口获取数据。
- 使用接口数据渲染模板。
apis/article.js
:
1 2 3 4 5 6 7
| export function getArticleListAPI (params){ return request({ url: '/mp/articles', method: 'GET', params }) }
|
pages/Article/index.js
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| 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 getArticleListAPI(params) const { results, total_count } = res.data setArticleList({ list: results, count: total_count }) } fetchArticleList() }, [params])
<Card title={`根据筛选条件共查询到 ${article.count} 条结果:`}> <Table dataSource={article.list} columns={columns} /> </Card>
|
筛选功能
步骤:
- 为表单添加
onFinish
属性监听表单提交事件,获取参数。
- 根据接口字段格式要求格式化参数格式。
- 修改
params
参数并重新使用新参数重新请求数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
|
const [reqData, setReqData] = useState({ status: '', channel_id: '', begin_pubdate: '', end_pubdate: '', page: 1, per_page: 4 })
const [list, setList] = useState([]) const [count, setCount] = useState(0) useEffect(() => { async function getList () { const res = await getArticleListAPI(reqData) setList(res.data.results) setCount(res.data.total_count) } getList() }, [reqData])
const onFinish = (formValue) => { console.log(formValue) setReqData({ ...reqData, channel_id: formValue.channel_id, status: formValue.status, begin_pubdate: formValue.date[0].format('YYYY-MM-DD'), end_pubdate: formValue.date[1].format('YYYY-MM-DD') }) }
<Card title={`根据筛选条件共查询到 ${count} 条结果:`}> <Table dataSource={list} columns={columns} /> </Card>
|
分页功能
步骤:
- 为Table组件指定pagination属性来展示分页效果。
- 在分页切换事件中获取到筛选表单中选中的数据。
- 使用当前页数据修改params参数依赖引起接口重新调用获取最新数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| const onPageChange = (page) => { setReqData({ ...reqData, page }) }
<Table dataSource={list} columns={columns} pagination={{ current: reqData.page, pageSize: reqData.per_page, onChange: onPageChange, total: count }}/>
|
删除功能
步骤:
- 给删除文章按钮绑定点击事件。
- 弹出确认窗口,询问用户是否确定删除文章。
- 拿到参数调用删除接口,更新列表。
apis/article.js
:
1 2 3 4 5 6
| export function delArticleAPI (id) { return request({ url: `/mp/articles/${id}`, method: 'DELETE' }) }
|
pages/Article/index.js
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| import { delArticleAPI } from '@/apis/article'
const delArticle = async (data) => { await delArticleAPI(data.id) setReqData({ page: 1, per_page: 10 }) }
const columns = [
【部分代码略】
{ title: '操作', render: data => { return ( <Space size="middle"> <Button type="primary" shape="circle" icon={<EditOutlined />} /> <Popconfirm title="确认删除该条文章吗?" onConfirm={() => delArticle(data)} okText="确认" cancelText="取消" > <Button type="primary" danger shape="circle" icon={<DeleteOutlined />} /> </Popconfirm> </Space> ) } } ]
|
编辑文章跳转
1 2 3 4 5 6 7 8
| const navagite = useNavigate()
<Button type="primary" shape="circle" icon={<EditOutlined />} onClick={() => navagite(`/publish?id=${data.id}`)} />
|
编辑文章
基础数据回填
apis/article.js
:
1 2 3 4 5
| export function getArticleById (id) { return request({ url: `/mp/articles/${id}` }) }
|
pages/Article/index.js
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| import { getArticleById } from '@/apis/article'
const [searchParams] = useSearchParams() const articleId = searchParams.get('id') const [form] = Form.useForm() useEffect(() => { async function getArticleDetail () { const res = await getArticleById(articleId) console.log(res) const data = res.data form.setFieldsValue({ ...data }) } if (articleId) { getArticleDetail() } }, [articleId, form])
<Form labelCol={{ span: 4 }} wrapperCol={{ span: 16 }} initialValues={{ type: 1 }} onFinish={ onFinish } form={form} > ··· </Form>
|
注意,一定要在Form
上绑定form
属性。
回填封面信息
为什么上述方法无法回填封面。
因为,set方法,要求传入的数据,形如{type : 3}
;但是,我们现在传入的数据,形如{cover : {type : 3}}
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| useEffect(() => { async function getArticleDetail () { const res = await getArticleById(articleId) const data = res.data const { cover } = data form.setFieldsValue({ ...data, type: cover.type })
setImageType(res.data.cover.type) setImageList(cover.images.map(url => { return { url } })) } if (articleId) { getArticleDetail() } }, [articleId, form])
|
适配不同状态下的文案
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <Card title={ <Breadcrumb items={[ { title: <Link to={'/'}>首页</Link> }, { title: `${articleId ? '编辑文章' : '发布文章'}` }, ]} /> } >
<Button size="large" type="primary" htmlType="submit"> {articleId ? '更新文章' : '发布文章'} </Button>
|
更新文章
apis/article.js
:
1 2 3 4 5 6 7
| export function updateArticleAPI (data) { return request({ url: `/mp/articles/${data.id}?draft=false`, method: 'PUT', data }) }
|
pages/Publish/index.js
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| const onFinish = (formValue) => { console.log(formValue) if (imageList.length !== imageType) return message.warning('封面类型和图片数量不匹配') const { title, content, channel_id } = formValue const reqData = { title, content, cover: { type: imageType, images: imageList.map(item => { if (item.response) { return item.response.data.url } else { return item.url } }) }, channel_id } if (articleId) { updateArticleAPI({ ...reqData, id: articleId }) } else { createArticleAPI(reqData) } }
|
打包部署
打包命令
项目本地预览
- 全局安装本地服务包
npm i -g serve
该包提供了serve命令,用来启动本地服务器
- 在项目根目录中执行命令
serve -s ./build
在build目录中开启服务器
- 在浏览器中访问:
http://localhost:3000/
预览项目
路由懒加载
什么是路由懒加载
路由懒加载是指路由的JS资源只有在被访问时才会动态获取,目的是为了优化项目首次打开的时间。
配置方法
步骤:
- 使用
lazy
方法导入路由组件。
- 使用内置的
Suspense
组件渲染路由组件。
router/index.js
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| import { createBrowserRouter } from 'react-router-dom'
import Login from '@/pages/Login' import GeekLayout from '@/pages/Layout' import AuthRoute from '@/components/AuthRoute'
import { lazy, Suspense } from 'react'
const Publish = lazy(() => import('@/pages/Publish')) const Article = lazy(() => import('@/pages/Article')) const Home = lazy(() => import('@/pages/Article'))
const router = createBrowserRouter([ { path: '/', element: <AuthRoute> <GeekLayout /> </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
|
之前我们导入的方法是:
1 2 3
| import Publish from '@/pages/Publish' import Article from '@/pages/Article' import Home from '@/pages/Home'
|
现在,需要修改成:
1 2 3
| const Publish = lazy(() => import('@/pages/Publish')) const Article = lazy(() => import('@/pages/Article')) const Home = lazy(() => import('@/pages/Article'))
|
打包体积分析
什么是打包体积分析
通过可视化的方式,直观的体现项目中各种包打包之后的体积大小,方便做优化。
操作方法
步骤:
- 安装分析打包体积的包:
npm i source-map-explorer
。
- 在
package.json
中的scripts
标签中,添加分析打包体积的命令。1
| "analyze": "source-map-explorer 'build/static/js/*.js'"
|
- 对项目打包:
npm run build
(如果已经打过包,可省略这一步)。
- 运行分析命令:
npm run analyze
。
- 通过浏览器打开的页面,分析图表中的包体积。
配置CDN
哪些资源可以放到CDN服务器
体积较大的非业务JS文件,例如react
、react-dom
。
对于这些非业务JS文件,不需要经常做变动,CDN不用频繁更新缓存。
操作方法
步骤:
- 把需要做CDN缓存的文件排除在打包之外。
- 以CDN的方式重新引入资源。
我们通过craco
修改webpack
配置,从而实现CDN优化。
craco.config.js
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
|
const path = require('path') const { whenProd, getPlugin, pluginByName } = require('@craco/craco')
module.exports = { webpack: { alias: { '@': path.resolve(__dirname, 'src') }, configure: (webpackConfig) => { let cdn = { js:[] } whenProd(() => { webpackConfig.externals = { react: 'React', 'react-dom': 'ReactDOM' } 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', ] } })
const { isFound, match } = getPlugin( webpackConfig, pluginByName('HtmlWebpackPlugin') )
if (isFound) { match.userOptions.files = cdn }
return webpackConfig } } }
|
还需要修改public/index.html
:
1 2 3 4 5 6 7
| <body> <div id="root"></div> <% htmlWebpackPlugin.options.files.js.forEach(cdnURL => { %> <script src="<%= cdnURL %>"></script> <% }) %> </body>
|
基于NGINX部署注意事项
React项目部署到Nginx上,刷新浏览器页面,可能会出现404的问题。
原因:使用create-react-app
创建的React应用为单页应用,使用BrowserRouter的话如果刷新页面Nginx会根据页面url来发送数据,但是Nginx服务器中没有关于该路由的配置,导致找不到文件,因而返回404页面。
解决方案
- 配置Nginx,使所有访问重定向到
index.html
。
在location的配置上加一句try_files $uri /index.html
。1 2 3 4 5
| location / { root /root/build; index index.html index.htm; try_files $uri /index.html; }
|
- 在项目中使用HashRouter,这一方法一个缺点是不利于SEO,因为是客户端渲染,并且会在url上加一个
#
符号,具体设置可看业务需求。
完整版本react-jike.zip
提供下载链接。
版权声明: 本博客所有文章版权为文章作者所有,未经书面许可,任何机构和个人不得以任何形式转载、摘编或复制。