Geek-PC项目 文档

news2024/11/17 1:41:46

一款后台管理项目 - React-geek-PC

项目介绍

● 项目功能演示

-  登录、退出
- 首页
- 内容(文章)管理:文章列表、发布文章、修改文章

● 技术

- React 官方脚手架 create-react-app
- react hooks
- 状态管理:mobx
- UI 组件库:antd v4
- ajax 请求库:axios
- 路由:react-router-dom 以及 history
- 富文本编辑器:react-quill
- CSS 预编译器:sass

项目开始准备

搭建

  1. 使用create-react-app生成项目 npx create-react-app projectName
  2. 进入根目录 cd projectName
  3. 启动项目 yarn start
    yarn 配置使用参考

调整项目结构

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

使用 Gitee 管理项目

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

使用 scss 预处理器

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

  1. 安装解析 sass 的包:yarn add sass -D
  2. 创建全局样式文件:index.scss

配置路由基础

  1. 安装路由:yarn add react-router-dom
  2. 在 pages 目录中创建两个文件夹:Login、Layout
  3. 分别在两个目录中创建 index.jsindex.scss 文件,并创建一个简单的组件后导出
  4. App 组件中,导入路由组件以及两个页面组件
  5. 配置 LoginLayout 的路由规则
// 导入路由
import { BrowserRouter, Route, Routes } from 'react-router-dom'

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

// 配置路由规则
function App() {
  return (
    <BrowserRouter>
      <div className="App">
        <Routes>
          <Route path="/" element={<Layout />} />
          <Route path="/login" element={<Login />} />
        </Routes>
      </div>
    </BrowserRouter>
  )
}

export default App

配置 antdesign

  1. 安装 antd 组件库:yarn add antd
  2. 全局导入 antd 组件库的样式
  3. 导入 Button 组件
  4. Login 页面渲染 Button 组件进行测试
    src/index.js
// 先导入 antd 样式文件
// https://github.com/ant-design/ant-design/issues/33327
import 'antd/dist/antd.min.css'
// 再导入全局样式文件,防止样式覆盖!
import './index.css'

pages/Login/index.js

import { Button } from 'antd'

const Login = () => (
  <div>
    <Button type="primary">Button</Button>
  </div>
)

配置路径别名

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

实现步骤

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

craco.config.js

const path = require('path')

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

package.json

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

vscode 识别@路径并给出路径提示

  1. 在项目根目录创建 jsconfig.json 配置文件
  2. 在配置文件中添加以下配置
{
  "compilerOptions": {
    "baseUrl": "./",
    "paths": {
      "@/*": ["src/*"]
    }
  }
}

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

登录模块

基本结构搭建

  1. Login/index.js 中创建登录页面基本结构
  2. 在 Login 目录中创建 index.scss 文件,指定组件样式

根据Antd文档
创建表单组件 -> 表单校验

表单校验

  1. Form 组件添加 validateTrigger 属性,指定校验触发时机的集合
  2. Form.Item 组件添加 name 属性,这样表单校验才会生效 [容易忽略]
  3. Form.Item 组件添加 rules 属性,用来添加表单校验
<Form validateTrigger={['onBlur', 'onChange']}>
  <Form.Item
    name="mobile"
    rules={[
      {
        pattern: /^1[3-9]\d{9}$/,
        message: '手机号码格式不对',
        validateTrigger: 'onBlur',
      },
      { required: true, message: '请输入手机号' },
    ]}>
    <Input size="large" placeholder="请输入手机号" />
  </Form.Item>
</Form>

获取表单登录数据

  1. 为 Form 组件添加 onFinish 属性,该事件会在点击登录按钮时触发
  2. 创建 onFinish 函数,通过函数参数 values 拿到表单值
  3. Form 组件添加 initialValues 属性,来初始化表单值
// 点击登录按钮时触发 参数values即是表单输入数据
const onFinish = (values) => {
  console.log(values)
}
;<Form
  onFinish={onFinish}
  initialValues={{
    mobile: '13911111111',
    code: '246810',
    remember: true,
  }}>
  ...
</Form>

封装http工具模块

封装 axios 简化操作
安装 axios: yarn add axios

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

utils/http.js

import axios from 'axios'

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

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

export { http }

utils/index.js

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

配置登录 mobx

store/login.Store.js

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

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

store/index.js

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

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

登录逻辑实现

  1. 使用 useStore 方法得到 loginStore 实例对象
  2. 在校验通过之后,调用 loginStore 中的 login 函数
  3. 登录成功之后跳转到首页
import { useStore } from '@/store'
const Login = () => {
  // 获取跳转实例对象
  const navigate = useNavigate()
  const { loginStore } = useStore()
  const onFinish = async values => {
    const { mobile, code } = values
    try {
      await loginStore.login({ mobile, code })
      navigate('/')
    } catch (e) {
      message.error(e.response?.data?.message || '登录失败')
    }
  }
  return (...)
}

token 持久化

统一处理 token 的持久化相关操作

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

utils/token.js

const TOKEN_KEY = 'geek_pc'

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

export { getToken, setToken, clearToken }

持久化设置

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

store/login.Store.js

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

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

请求拦截器注入 token

把 token 通过请求拦截器注入到请求头中
在这里插入图片描述

utils/htts.js

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

路由鉴权(判断是否已登录)

实现未登录时访问拦截并跳转到登录页面

自己封装 AuthRoute 路由鉴权高阶组件,实现未登录拦截,并跳转到登录页面
思路为:判断本地是否有 token,如果有,就返回子组件,否则就重定向到登录 Login

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

components/AuthRoute/index.js

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

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

function AuthRoute({ children }) {
  const isToken = getToken()
  if (isToken) {
    return <>{children}</>
  } else {
    return <Navigate to="/login" replace />
  }
}

// <AuthComponent> <Layout/> </AuthComponent>
// 登录:<><Layout/></>
// 非登录:<Navigate to="/login" replace />

export { AuthRoute }

app.js

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

function App() {
  return (
    <Router>
      <Routes>
        {/* 需要鉴权的路由 */}
        <Route
          path="/*"
          element={
            // 在这里!
            <AuthRoute>
              <Layout />
            </AuthRoute>
          }
        />
        {/* 不需要鉴权的路由 */}
        <Route path="/login" element={<Login />} />
      </Routes>
    </Router>
  )
}
export default App

layout 模块

基本结构搭建

  1. 打开 antd/Layout 布局组件文档,找到示例:顶部-侧边布局-通栏

  2. 边删边调 边分析并调整页面布局

二级路由配置

  1. 在 pages 目录中,分别创建:Home(数据概览)/Article(内容管理)/Publish(发布文章)页面文件夹
  2. 分别在三个文件夹中创建 index.js 并创建基础组件后导出
  3. app.js 中配置嵌套子路由,在 layout.js 中配置二级路由出口
  4. 使用 Link 修改左侧菜单内容,与子路由规则匹配实现路由切换

app.js

<Route path="/" element={
    <AuthRoute>
      <Layout />
    </AuthRoute>
  }>
    {/* 二级路由默认页面 */}
    <Route index element={<Home />} />
    <Route path="article" element={<Article />} />
    <Route path="publish" element={<Publish />} />
</Route>
<Route path="/login" element={<Login/>}></Route>

pages/layout/index.js

// 配置Link组件
<Menu
    mode="inline"
    theme="dark"
    style={{ height: '100%', borderRight: 0 }}
    selectedKeys={[selectedKey]}
    >
    <Menu.Item icon={<HomeOutlined />} key="/">
      <Link to="/">数据概览</Link>
    </Menu.Item>
    <Menu.Item icon={<DiffOutlined />} key="/article">
      <Link to="/article">内容管理</Link>
    </Menu.Item>
    <Menu.Item icon={<EditOutlined />} key="/publish">
      <Link to="/publish">发布文章</Link>
    </Menu.Item>
</Menu>

// 二级路由对应显示
<Layout className="layout-content" style={{ padding: 20 }}>
  {/* 二级路由默认页面 */}
  <Outlet />
</Layout>

菜单高亮显示

在页面刷新的时候保持对应菜单高亮

  1. Menu 组件的 selectedKeys 属性与 Menu.Item 组件的 key 属性发生匹配的时候,Item 组件即可高亮
  2. 页面刷新时,将当前访问页面的路由地址作为 Menu 选中项的值(selectedKeys)即可
  1. 将 Menu 的 key 属性修改为与其对应的路由地址
  2. 获取到当前正在访问页面的路由地址
  3. 将当前路由地址设置为 selectedKeys 属性的值

pages/Layout/index.js

import { useLocation } from 'react-router-dom'

const GeekLayout = () => {
  const location = useLocation()
  // 这里是当前浏览器上的路径地址
  const selectedKey = location.pathname

  return (
    // ...
    <Menu
      mode="inline"
      theme="dark"
      selectedKeys={[selectedKey]}
      style={{ height: '100%', borderRight: 0 }}>
      <Menu.Item icon={<HomeOutlined />} key="/">
        <Link to="/">数据概览</Link>
      </Menu.Item>
      <Menu.Item icon={<DiffOutlined />} key="/article">
        <Link to="/article">内容管理</Link>
      </Menu.Item>
      <Menu.Item icon={<EditOutlined />} key="/publish">
        <Link to="/publish">发布文章</Link>
      </Menu.Item>
    </Menu>
  )
}

处理 token 失效问题

在响应拦截器中处理 token 失效

去检索一下 看官方的解决方案
或者直接使用 window.location.href = ''

home 页面内容展示

自定义…

文章管理模块

筛选结构

使用 antd 组件库搭建筛选区域结构,自己造!!!

表格区域结构

基于 Table 组件搭建表格结构,去看着文档造!!!

渲染表格数据

  1. 声明列表相关数据管理
  2. 声明参数相关数据管理
  3. 调用接口获取数据
  4. 使用接口数据渲染模板
// 文章列表数据管理
const [article, setArticleList] = useState({
  list: [],
  count: 0,
})

// 参数管理
const [params, setParams] = useState({
  page: 1,
  per_page: 10,
})

// 发送接口请求
useEffect(() => {
  async function fetchArticleList() {
    const res = await http.get('/mp/articles', { params })
    const { results, total_count } = res.data
    setArticleList({
      list: results,
      count: total_count,
    })
  }
  fetchArticleList()
}, [params])

// 模板渲染
return (
  <Card title={`根据筛选条件共查询到 ${article.count} 条结果:`}>
    <Table dataSource={article.list} columns={columns} />
  </Card>
)

筛选功能实现

根据筛选条件筛选表格数据

  1. 为表单添加 onFinish 属性监听表单提交事件,获取参数
  2. 根据接口字段格式要求格式化参数格式(注意点)
  3. 修改 params 触发接口的重新发送
// 筛选功能
const onSearch = (values) => {
  const { status, channel_id, date } = values
  // 格式化表单数据
  const _params = {}
  // 格式化status
  _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,
  })
}
// Form绑定事件
return <Form onFinish={onSearch}></Form>

分页功能实现

简单,自己造…

删除功能实现

简单,自己造…

文章修改功能实现

看后续…

发布文章模块

基本结构

自己看着造!!!

富文本编辑器

安装并初始化富文本编辑器

  1. 安装富文本编辑器:yarn add react-quill@2.0.0-beta.2 [react-quill 需要安装 beta 版本适配 react18 否则无法输入中文]
  2. 导入富文本编辑器组件以及样式文件
  3. 渲染富文本编辑器组件
  4. 通过 Form 组件的 initialValues 为富文本编辑器设置初始值,否则会报错
  5. 调整富文本编辑器的样式

pages/Publish/index.js

import ReactQuill from 'react-quill'
import 'react-quill/dist/quill.snow.css'

const Publish = () => {
  return (
    // ...
    <Form
      labelCol={{ span: 4 }}
      wrapperCol={{ span: 16 }}
      // 注意:此处需要为富文本编辑表示的 content 文章内容设置默认值
      initialValues={{ content: '' }}>
      <Form.Item
        label="内容"
        name="content"
        rules={[{ required: true, message: '请输入文章内容' }]}>
        <ReactQuill
          className="publish-quill"
          theme="snow"
          placeholder="请输入文章内容"
        />
      </Form.Item>
    </Form>
  )
}

pages/Publish/index.scss

.publish-quill {
  .ql-editor {
    min-height: 300px;
  }
}

上传封面

  1. 为 Upload 组件添加 action 属性,指定封面图片上传接口地址
  2. 创建状态 fileList 存储已上传封面图片地址,并设置为 Upload 组件的 fileList 属性值
  3. 为 Upload 添加 onChange 属性,监听封面图片上传、删除等操作
  4. 在 change 事件中拿到当前图片数据,并存储到状态 fileList 中
import { useState } from 'react'

const Publish = () => {
  const [fileList, setFileList] = useState([])
  // 上传成功回调
  const onUploadChange = (info) => {
    const fileList = info.fileList.map((file) => {
      if (file.response) {
        return {
          url: file.response.data.url,
        }
      }
      return file
    })
    setFileList(fileList)
  }

  return (
    <Upload
      name="image"
      listType="picture-card"
      className="avatar-uploader"
      showUploadList
      action="http://geek.itheima.net/v1_0/upload"
      fileList={fileList}
      onChange={onUploadChange}>
      <div style={{ marginTop: 8 }}>
        <PlusOutlined />
      </div>
    </Upload>
  )
}

切换图片类型

  1. 创建状态 maxCount
  2. 给 Radio 添加 onChange 监听单图、三图、无图的切换事件
  3. 在切换事件中修改 maxCount 值
  4. 只在 maxCount 不为零时展示 Upload 组件
const Publish = () => {
  const [imgCount, setImgCount] = useState(1)

  const changeType = (e) => {
    const count = e.target.value
    setImgCount(count)
  }

  return (
    // ...
    <Form.Item label="封面">
      <Form.Item name="type">
        <Radio.Group onChange={changeType}>
          <Radio value={1}>单图</Radio>
          <Radio value={3}>三图</Radio>
          <Radio value={0}>无图</Radio>
        </Radio.Group>
      </Form.Item>
      {maxCount > 0 && (
        <Upload
          name="image"
          listType="picture-card"
          className="avatar-uploader"
          showUploadList
          action="http://geek.itheima.net/v1_0/upload">
          <div style={{ marginTop: 8 }}>
            <PlusOutlined />
          </div>
        </Upload>
      )}
    </Form.Item>
  )
}

控制最大上传数量

  1. 修改 Upload 组件的 maxCount(最大数量)属性控制最大上传数量
  2. 控制 multiple (支持多图选择)属性 控制是否支持选择多张图片

自己看看咋造
maxCount={ maxCount }
multiple={ maxCount > 1 }

暂存图片列表实现

实现暂存已经上传的图片列表,能够在切换图片类型的时候完成切换

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

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

实现步骤 (特别注意 useState 异步更新的坑)

  1. 通过 useRef 创建一个暂存仓库,在上传完毕图片的时候把图片列表存入
  2. 如果是单图模式,就从仓库里取第一张图,以数组的形式存入 fileList
  3. 如果是三图模式,就把仓库里所有的图片,以数组的形式存入 fileList
const Publish = () => {
  // 1. 声明一个暂存仓库
  const fileListRef = useRef([])

  // 2. 上传图片时,将所有图片存储到 ref 中
  const onUploadChange = (info) => {
    // ...
    fileListRef.current = imgUrls
  }

  // 3. 切换图片类型
  const changeType = (e) => {
    // 使用原始数据作为判断条件
    const count = e.target.value
    setMaxCount(count)

    if (count === 1) {
      // 单图,只展示第一张
      const firstImg = fileListRef.current[0]
      setFileList(!firstImg ? [] : [firstImg])
    } else if (count === 3) {
      // 三图,展示所有图片
      setFileList(fileListRef.current)
    }
  }
}

发布文章实现

  1. 给 Form 表单添加 onFinish 用来获取表单提交数据
  2. 在事件处理程序中,拿到表单数据按照接口需要 格式化数据

编辑文章适配

文章发布和编辑文章可以做一个单独的组件,根据是否有参数传入来显示不同的适配文案

编辑文案获取数据-数据回显

判断文章 id 是否存在,如果存在就根据 id 获取文章详情数据
调用 Form 组件的实例对象方法 setFieldsValue

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

回显 upload 相关

1.Upload 回显列表 fileList 2. 暂存列表 cacheImgList 3. 图片数量 imgCount
核心要点:fileList 和暂存列表要求格式统一

useEffect(() => {
  async function getArticle() {
    const res = await http.get(`/mp/articles/${articleId}`)
    const { cover, ...formValue } = res.data
    // 动态设置表单数据
    form.setFieldsValue({ ...formValue, type: cover.type })
    // 格式化封面图片数据
    const imageList = cover.images.map((url) => ({ url }))
    setFileList(imageList)
    setMaxCount(cover.type)
    fileListRef.current = imageList
  }
  if (articleId) {
    // 拉取数据回显
    getArticle()
  }
}, [articleId])

保存修改/提交文章

// 提交表单
const onFinish = async (values) => {
  const { type, ...rest } = values
  const data = {
    ...rest,
    // 注意:接口会按照上传图片数量来决定单图 或 三图
    cover: {
      type,
      images: fileList.map((item) => item.url),
    },
  }
  if (articleId) {
    // 编辑
    await http.put(`/mp/articles/${data.id}?draft=false`, data)
  } else {
    // 新增
    await http.post('/mp/articles?draft=false', data)
  }
}

项目打包

  1. 在项目根目录下打开终端,输入打包命令:yarn build
  2. 等待打包完成,打包生成的内容被放在根下的 build 文件夹中

本地预览

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

打包分析

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

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

"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
    // 配置CDN
    configure: (webpackConfig) => {
      // webpackConfig自动注入的webpack配置对象
      // 可以在这个函数中对它进行详细的自定义配置
      // 只要最后return出去就行
      let cdn = {
        js: [],
        css: [],
      }
      // 只有生产环境才配置
      whenProd(() => {
        // key:需要不参与打包的具体的包
        // value: cdn文件中 挂载于全局的变量名称 为了替换之前在开发环境下
        // 通过import 导入的 react / react-dom
        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',
          ],
          css: [],
        }
      })

      // 都是为了将来配置 htmlWebpackPlugin插件 将来在public/index.html注入
      // cdn资源数组时 准备好的一些现成的资源
      const { isFound, match } = getPlugin(
        webpackConfig,
        pluginByName('HtmlWebpackPlugin')
      )

      if (isFound) {
        // 找到了HtmlWebpackPlugin的插件
        match.userOptions.cdn = cdn
      }

      return webpackConfig
    },
  },
}

public/index.html

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

优化路由懒加载

对路由进行懒加载实现代码分隔

  1. 在 App 组件中,导入 Suspense 组件
  2. 在 路由 Router 内部,使用 Suspense 组件包裹组件内容
  3. 为 Suspense 组件提供 fallback 属性,指定 loading 占位内容
  4. 导入 lazy 函数,并修改为懒加载方式导入路由组件

APP.js

import { Routes, Route } from 'react-router-dom'
import { HistoryRouter, history } from './utils/history'
import { AuthRoute } from './components/AuthRoute'

// 导入必要组件
import { lazy, Suspense } from 'react'
// 按需导入路由组件
const Login = lazy(() => import('./pages/Login'))
const Layout = lazy(() => import('./pages/Layout'))
const Home = lazy(() => import('./pages/Home'))
const Article = lazy(() => import('./pages/Article'))
const Publish = lazy(() => import('./pages/Publish'))

function App() {
  return (
    <HistoryRouter history={history}>
      <Suspense
        fallback={
          <div
            style={{
              textAlign: 'center',
              marginTop: 200,
            }}>
            loading...
          </div>
        }>
        <Routes>
          {/* 需要鉴权的路由 */}
          <Route
            path="/"
            element={
              <AuthRoute>
                <Layout />
              </AuthRoute>
            }>
            {/* 二级路由默认页面 */}
            <Route index element={<Home />} />
            <Route path="article" element={<Article />} />
            <Route path="publish" element={<Publish />} />
          </Route>
          {/* 不需要鉴权的路由 */}
          <Route path="/login" element={<Login />} />
        </Routes>
      </Suspense>
    </HistoryRouter>
  )
}

export default App

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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

ESP32网络应用 -- ESP32-S3在STA模式下创建TCP-CLIENT应用程序

在ESP32-S3初始化为Station模式并且成功获取IP地址后,说明ESP32-S3芯片的底层设施已经具备Wi-Fi网络通信能力,但在实际的应用场景里面,仅仅建立数据链路层,还是不能够满足应用程序的数据通信需求。 TCP/IP是一种使用广泛的网络传输协议,网络上并不缺乏关于TCP/IP的具体原…

Atcoder Beginner Contest 304——A-D题讲解

蒟蒻来讲题&#xff0c;还望大家喜。若哪有问题&#xff0c;大家尽可提&#xff01; Hello, 大家好哇&#xff01;本初中生蒟蒻讲解一下AtCoder Beginner Contest 304这场比赛的A-D题&#xff01; A - First Player 题目描述 Problem Statement There are N N N people nu…

sequence2sequence

1. 基本模型 所谓的Seq2seq模型从字面上理解很简单&#xff0c;就是由一个序列到另一个序列的过程(比如翻译、语音等方面的应用)&#xff1a; 那么既然是序列模型&#xff0c;就需要搭建一个RNN模型(神经单元可以是GRU模型或者是LSTM模型) 下面两篇文章提出了这样的seq2seq的模…

NVM安装教程

我是小荣&#xff0c;给个赞鼓励下吧&#xff01; NVM安装教程 简介 nvm 是node.js的版本管理器&#xff0c;设计为按用户安装&#xff0c;并按 shell 调用。nvm适用于任何符合 POSIX 的 shell&#xff08;sh、dash、ksh、zsh、bash&#xff09;&#xff0c;特别是在这些平台…

ChatGPT 五个写论文的神技巧,让你的老师对你刮目相看!

导读&#xff1a;ChatGPT这款AI工具在推出两个月内就累积了超过1亿用户。我们向您展示如何使用ChatGPT进行写作辅助&#xff0c;以及其他一些有用的写作技巧。 本文字数&#xff1a;2000&#xff0c;阅读时长大约&#xff1a;12分钟 ChatGPT这款AI工具在推出两个月内就累积了超…

【Java|golang】2460. 对数组执行操作

给你一个下标从 0 开始的数组 nums &#xff0c;数组大小为 n &#xff0c;且由 非负 整数组成。 你需要对数组执行 n - 1 步操作&#xff0c;其中第 i 步操作&#xff08;从 0 开始计数&#xff09;要求对 nums 中第 i 个元素执行下述指令&#xff1a; 如果 nums[i] nums[i…

物联网Lora模块从入门到精通(三)按键的读取与使用

一、前言 在Lora例程中&#xff0c;为我们提供了三个按键标志位&#xff0c;我们不需要手动再次初始化按键&#xff0c;即可完成按键的读取。 二、代码实现 首先&#xff0c;我们一起来阅读hal_key.c中的代码&#xff1a; /* Includes -------------------------------------…

点云地面滤波--patchwork++

文章目录 1前言2 反射噪声去除RNR3区域垂直平面拟合 (R-VPF)4自适应地面似然估计(A-GLE)5时序地面恢复TGR总结 1前言 patchwork是在patchwork的基础上进行改进的&#xff0c;主要有2个贡献&#xff1a; 提出了自适应地面似然估计(adaptive ground likelihood estimation (A-G…

Java实训日记第一天——2023.6.6

这里写目录标题 一、关于数据库的增删改查总结&#xff1a;五步法1.增2.删3.改4.查 二、设计数据库的步骤第一步&#xff1a;收集信息第二步&#xff1a;标识对象第三步&#xff1a;标识每个实体的属性第四步&#xff1a;标识对象之间的关系 一、关于数据库的增删改查 总结&am…

Java框架学习--Spring

1.Spring概念【托管很多对象的框架】 一个包含了众多工具方法的IoC容器。 1.1 什么是容器? 容器是用来容纳各种物品的&#xff08;基本&#xff09;装置。--来自百度百科 之前常见的容器有&#xff1a; List/Map-》数据存储容器 Tomcat-》Web容器 1.2什么是IoC? IoCInve…

【Docker】Docker对用户的应用程序使用容器技术遵循的五个步骤和GRSEC详细讲解(文末赠书)

前言 Docker 是一个开源的应用容器引擎&#xff0c;让开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后发布到任何流行的Linux或Windows操作系统的机器上,也可以实现虚拟化,容器是完全使用沙箱机制,相互之间不会有任何接口。 &#x1f4d5;作者简介&#xff1a;热…

序列的有关知识

&#x1f4e2;博客主页&#xff1a;盾山狂热粉的博客_CSDN博客-C、C语言,机器视觉领域博主&#x1f4e2;努力努力再努力嗷~~~✨ &#x1f4a1;大纲 ⭕列表、元组、字符串都是序列&#xff0c;列表是可变序列&#xff0c;元组和字符串是不可变序列 一、跟序列相关的操作符 &am…

Hazel游戏引擎(008-009)事件系统

文中若有代码、术语等错误&#xff0c;欢迎指正 文章目录 008、事件系统-设计009、事件系统-自定义事件前言自定义事件类与使用声明与定义类代码包含头文件使用事件 事件调度器代码 C知识&#xff1a;FunctionBind用法function基本使用 012、事件系统-DemoLayer用EventDispache…

7-3 sdut-oop-6 计算各种图形的周长(多态)

定义接口或类 Shape&#xff0c;定义求周长的方法length()。 定义如下类&#xff0c;实现接口Shape或父类Shape的方法。 &#xff08;1&#xff09;三角形类Triangle &#xff08;2&#xff09;长方形类Rectangle &#xff08;3&#xff09;圆形类Circle等。 定义测试类Shap…

QT基础教程之一创建Qt项目

QT基础教程1创建Qt项目 根据模板创建 打开Qt Creator 界面选择 New Project或者选择菜单栏 【文件】-【新建文件或项目】菜单项 弹出New Project对话框&#xff0c;选择Qt Widgets Application 选择【Choose】按钮&#xff0c;弹出如下对话框 设置项目名称和路径&#xff0c;…

Cesium雷达追踪追踪(雷达探照效果)

Cesium雷达追踪追踪(圆锥体效果) 文章最后附有源码!!!!!!!!!!!!!!!!! 解析 第一步 、从gif图中可以看出,首先添加了两个运动的实体(在cesium entity与时间轴关联(添加运动轨迹))中有讲解 第二步、添加一个圆锥,修改圆锥朝向,来表示跟综照射效果,…

Windows安装MySQL及Python操作MySQL数据库脚本实例详解

1、Windows 上安装 MySQL 便于测试&#xff0c;笔者在 windows 上安装 MySQL&#xff0c;如有现成Linux下的MySQL和Python环境&#xff0c;也可直接使用。MySQL的官网下载链接安装步骤1)下载后的mysql-5.7.23-winx64.zip安装包解压至某一位置&#xff0c;在mysql-5.7.23-winx6…

Linux学习之用户管理useradd、userdel、passwd、usermod和chage

useradd 超级管理员root才能使用useradd 用户名添加用户&#xff0c;这条命令会新增一个用户&#xff0c;然后为新增用户在/home下新添一个用户名称相同的目录&#xff0c;在/var/spool/mail目录下添加一个用户名称相同的文件&#xff0c;而且还会在/etc/passwd、/etc/shadow和…

【Unity入门】25.入门结课Demo--神鸟大战怪兽

【Unity入门】入门结课Demo--神鸟大战怪兽 大家好&#xff0c;我是Lampard~~ 欢迎来到Unity入门系列博客&#xff0c;所学知识来自B站阿发老师~感谢 (一) 前言 经过了两个月的学习&#xff0c;我们也顺利的完成了入门课程&#xff0c;最后就用一个Demo作为我们的结课句号吧&am…

【夜深人静学数据结构与算法 | 第一篇】KMP算法

目录 前言&#xff1a; KMP算法简介&#xff1a; 引入概念&#xff1a; 前缀后缀 前缀表&#xff1a; 简单例子&#xff1a; 暴力遍历&#xff1a; KMP算法&#xff1a;​ KMP算法难点&#xff1a; 总结&#xff1a; 前言&#xff1a; 本篇我们将详细的从理论层面介绍一…