本节目标:  了解项目的定位和功能
- 项目功能演示
- 登录、退出
- 首页
- 内容(文章)管理:文章列表、发布文章、修改文章
 
- 技术
- React 官方脚手架 create-react-app/ vite
- react hooks
- 状态管理:mobx
- UI 组件库:antdv4
- ajax请求库:axios
- 路由:react-router-dom以及history
- 富文本编辑器:react-quill
- CSS 预编译器:sass
 
- React 官方脚手架 
本节目标:  能够基于脚手架搭建项目基本结构
实现步骤
- 使用create-react-app生成项目   npx create-react-app geek-pc/ 使用vite 生成项目npm create vite@latest geek-pc --template
- 进入根目录  cd geek-pc
- 启动项目   npm run start/npm run dev
- 调整项目目录结构
/src
  /assets         项目资源文件,比如,图片 等
  /components     通用组件
  /pages          页面
  /store          mobx 状态仓库
  /utils          工具,比如,token、axios 的封装等
  App.js          根组件
  index.css       全局样式
  index.js        项目入口
  (main.jsx)      (vite)
保留核心代码src/index.js
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
)src/App.js
export default function App() {
  return <div>根组件</div>
}本节目标:  能够将项目推送到gitee远程仓库
实现步骤
- 在项目根目录打开终端,并初始化 git 仓库(如果已经有了 git 仓库,无需重复该步),命令:git init
- 添加项目内容到暂存区:git add .
- 提交项目内容到仓库区:git commit -m '项目初始化'
- 添加 remote 仓库地址:git remote add origin [gitee 仓库地址]
- 将项目内容推送到 gitee:git push origin master -u
本节目标:  能够在CRA中使用sass书写样式
SASS是一种预编译的 CSS,作用类似于 Less。由于 React 中内置了处理 SASS 的配置,所以,在 CRA 创建的项目中,可以直接使用 SASS 来写样式
实现步骤
- 安装解析 sass 的包:npm i sass -D
- 创建全局样式文件:index.scss
body {
  margin: 0;
}
#root {
  height: 100%;
}本节目标: 能够配置配置别名 @为src 引入
实现步骤
- 在 vite.config.js 里面引入 path : import path from path
- 在 defineConfig 里的resolve 配置别名: alias: {'@': path.resolve(__dirname2, 'src')}
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'
// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()],
  resolve: {
    //配置别名 @为src 引入
    alias: {
      '@': path.resolve(__dirname2, 'src')
    }
  }
})
- CRA 将所有工程化配置,都隐藏在了
react-scripts包中,所以项目中看不到任何配置信息- 如果要修改 CRA 的默认配置,有以下几种方案:
- 通过第三方库来修改,比如,
@craco/craco(推荐)- 通过执行
yarn eject命令,释放react-scripts中的所有配置到项目中
实现步骤
- 安装修改 CRA 配置的包:yarn add -D @craco/craco
- 在项目根目录中创建 craco 的配置文件:craco.config.js,并在配置文件中配置路径别名
- 修改 package.json中的脚本命令
- 在代码中,就可以通过 @来表示 src 目录的绝对路径
- 重启项目,让配置生效
代码实现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识别@路径并给出路径提示
实现步骤
- 在项目根目录创建 jsconfig.json配置文件
- 在配置文件中添加以下配置
代码实现
{
  "compilerOptions": {
    "baseUrl": "./",
    "paths": {
      "@/*": ["src/*"]
    }
  }
}vscode会自动读取jsconfig.json 中的配置,让vscode知道@就是src目录
本节目标: 能够配置登录页面的路由并显示到页面中
实现步骤
- 安装路由:npm i react-router-dom
- 在 pages 目录中创建两个文件夹:Login、Layout
- 分别在两个目录中创建 index.js 文件,并创建一个简单的组件后导出
- 在 App 组件中,导入路由组件以及两个页面组件
- 配置 Login 和 Layout 的路由规则
代码实现pages/Login/index.js
const Login = () => {
  return <div>login</div>
}
export default Loginpages/Layout/index.js
const Layout = () => {
  return <div>layout</div>
}
export default Layoutapp.js
// 导入路由
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本节目标:  能够使用antd的Button组件渲染按钮
实现步骤
- 安装 antd 组件库:yarn add antd
- 全局导入 antd 组件库的样式
- 导入 Button 组件
- 在 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>
)https://gitee.com/react-cp/react-pc-doc 这里找到dev-tools.crx文件
本节目标:  能够在响应拦截器中处理token失效
说明:为了能够在非组件环境下拿到路由信息,需要我们安装一个history包 或者直接用
window.location.href = '/login'
实现步骤
- 安装history包:npm i history
- 创建 utils/history.js文件
- 在app.js中使用我们新建的路由并配置history参数
- 通过响应拦截器处理 token 失效,如果发现是401调回到登录页
代码实现utils/history.js
// https://github.com/remix-run/react-router/issues/8264
import { createBrowserHistory } from 'history'
import { unstable_HistoryRouter as HistoryRouter } from 'react-router-dom'
const history = createBrowserHistory()
export {
  HistoryRouter,
  history
}app.js
import { HistoryRouter, history } from './utils/history'
function App() {
  return (
    <HistoryRouter history={history}>
       ...省略无关代码
    </HistoryRouter>
  )
}
export default Apputils/http.js
import { history } from './history'
http.interceptors.response.use(
  response => {
    return response.data
  },
  error => {
    if (error.response.status === 401) {
      // 删除token
      clearToken()
      // 跳转到登录页
      history.push('/login')
    }
    return Promise.reject(error)
  }
)本节目标:  实现首页echart图表封装展示
需求描述:
- 使用eharts配合react封装柱状图组件Bar
- 要求组件的标题title,横向数据xData,纵向数据yData,样式style可定制
代码实现components/Bar/index.js
import * as echarts from 'echarts'
import { useEffect, useRef } from 'react'
function echartInit (node, xData, sData, title) {
  const myChart = echarts.init(node)
  // 绘制图表
  myChart.setOption({
    title: {
      text: title
    },
    tooltip: {},
    xAxis: {
      data: xData
    },
    yAxis: {},
    series: [
      {
        name: '销量',
        type: 'bar',
        data: sData
      }
    ]
  })
}
function Bar ({ style, xData, sData, title }) {
  // 1. 先不考虑传参问题  静态数据渲染到页面中
  // 2. 把那些用户可能定制的参数 抽象props (1.定制大小 2.data 以及说明文字)
  const nodeRef = useRef(null)
  useEffect(() => {
    echartInit(nodeRef.current, xData, sData, title)
  }, [xData, sData])
  return (
    <div ref={nodeRef} style={style}></div>
  )
}
export default Barpages/Home/index.js
import Bar from "@/components/Bar"
import './index.scss'
const Home = () => {
  return (
    <div className="home">
      <Bar
        style={{ width: '500px', height: '400px' }}
        xData={['vue', 'angular', 'react']}
        sData={[50, 60, 70]}
        title='三大框架满意度' />
      <Bar
        style={{ width: '500px', height: '400px' }}
        xData={['vue', 'angular', 'react']}
        sData={[50, 60, 70]}
        title='三大框架使用度' />
    </div>
  )
}
export default Homepages/Home/index.scss
.home {
  width: 100%;
  height: 100%;
  align-items: center;
}本节目标:  能够安装并初始化富文本编辑器
实现步骤
- 安装富文本编辑器:npm i react-quill@2.0.0-beta.2 --force[react-quill需要安装beta版本适配react18 否则无法输入中文]
- 导入富文本编辑器组件以及样式文件
- 渲染富文本编辑器组件
- 通过 Form 组件的 initialValues为富文本编辑器设置初始值,否则会报错
- 调整富文本编辑器的样式
代码实现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;
  }
}本节目标: 能够实现上传图片
实现步骤
- 为 Upload 组件添加 action 属性,指定封面图片上传接口地址
- 创建状态 fileList 存储已上传封面图片地址,并设置为 Upload 组件的 fileList 属性值
- 为 Upload 添加 onChange 属性,监听封面图片上传、删除等操作
- 在 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>
  )
}温馨提示
必须设置fileList,不然只执行一次,里面还拿不到 response onUploadChange就会执行三次,在上传之前,上传刚好结束,上传完成
参考链接 [CSDN网址]:https://blog.csdn.net/guxuehua/article/details/108501507
本节目标: 能够实现暂存已经上传的图片列表,能够在切换图片类型的时候完成切换
问题描述
如果当前为三图模式,已经完成了上传,选择单图只显示一张,再切换到三图继续显示三张,该如何实现?
实现思路
在上传完毕之后通过ref存储所有图片,需要几张就显示几张,其实也就是把ref当仓库,用多少拿多少
实现步骤 (特别注意useState异步更新的坑)
- 通过useRef创建一个暂存仓库,在上传完毕图片的时候把图片列表存入
- 如果是单图模式,就从仓库里取第一张图,以数组的形式存入fileList
- 如果是三图模式,就把仓库里所有的图片,以数组的形式存入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)
    }
  }
}本节目标: 能够通过命令对项目进行打包
使用步骤
- 在项目根目录下打开终端,输入打包命令:yarn build
- 等待打包完成,打包生成的内容被放在根下的build文件夹中
本节目标: 能够在本地预览打包后的项目
- 全局安装本地服务包 npm i -g serve该包提供了serve命令,用来启动本地服务
- 在项目根目录中执行命令 serve -s ./build在build目录中开启服务器
- 在浏览器中访问:http://localhost:3000/预览项目
本节目标:   能够分析项目打包体积
分析说明通过分析打包体积,才能知道项目中的哪部分内容体积过大,才能知道如何来优化
使用步骤
- 安装分析打包体积的包:yarn add source-map-explorer
- 在 package.json 中的 scripts 标签中,添加分析打包体积的命令
- 对项目打包:yarn build(如果已经打过包,可省略这一步)
- 运行分析命令:yarn analyze
- 通过浏览器打开的页面,分析图表中的包体积
核心代码:
package.json 中:
"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
    // 配置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>本节目标:   能够对路由进行懒加载实现代码分隔
使用步骤
- 在 App 组件中,导入 Suspense 组件
- 在 路由Router 内部,使用 Suspense 组件包裹组件内容
- 为 Suspense 组件提供 fallback 属性,指定 loading 占位内容
- 导入 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面板资源的请求情况,验证是否分隔成功

