写在前面

  • 本文主要介绍的是如何通过create-react-app快速搭建一个react+ts的web项目,同时引入了ant-design组件库,路由(react-router-dom)以及使用redux作为状态管理库;
  • 希望通过结合一些简单案例的实践掌握react开发的基本基础以及帮助一些有需要的同学。

核心库版本: antd: 5.4.7 react: 18.2.0 redux:4.2.1 react-redux:8.0.5
node版本: 16.17.0


一、create-react-app脚手架

1.依赖安装create-react-app

npm install -g create-react-app
或是
yarn add -g create-react-app

2.项目初始化

  • 快速构建出项目名为my-app的react+ts项目
create-react-app my-app --template typescript
  • 进入项目中可以看到默认已经安装好了部分依赖,此时运行npm run start命令项目默认会在3000端口地址启动,就像下图这样:
    在这里插入图片描述

  • 项目目录结构如下,可以看到有许多多余的文件内容,这里的多余指的是咱们构建的是web项目,因此接下来删减一些用不上文件的:
    在这里插入图片描述

  • 删减之后如下:
    在这里插入图片描述

  • react项目默认隐藏了webpack相关配置文件,如果想要暴露在项目当中,需要执行npm run eject,并且此操作无法回退,此操作根据自行需要执行;

3.配置路径别名

  • 在引入文件时如果都是../ ../../这种相对路径方式引用可读性很差
  • 安装依赖
npm install react-app-rewired customize-cra --save-dev
  • 在项目根路径下创建config-overrides.js文件,添加如下配置
const { override, addWebpackAlias } = require('customize-cra')
const path = require('path')
module.exports = override(
  addWebpackAlias({
    // 指定@符指向src目录
    '@': path.resolve(__dirname, 'src'),
  })
)
  • 修改package.json配置,重启项目npm run serve即可
    在这里插入图片描述
  • 页面组件引用方式由 …/方式可以改为@/方式
import About from '@/pages/About'
// 等价于
import Home from '../pages/About'
  • 如果提示找不到类型声明,那么就检查tsconfig.json文件看看是否缺配置

二、引入Ant-Design组件库

1.安装依赖

npm install antd --save

2.在App.tsx文件中引入两个按钮

  • 查看效果,按钮正常显示说明就是成功了;
    在这里插入图片描述

3.语言汉化

  • 使用ConfigProvider包裹App根组件
    在这里插入图片描述

● 在App组件内引入日期选择组件查看效果,可以看到由默认的英文已经切换为中文
在这里插入图片描述

三、引入react路由

1.安装依赖

npm i react-router-dom

2.注册路由

  • 进入index.tsx引入并注册路由,这里我们使用history模式
    在这里插入图片描述

3.路由跳转例子

  • 在src下新建pages文件夹创建两个路由组件;新建routes文件夹,创建index.tsx文件用于存放路由表,引入路由组件并且向外暴露,就像下面这样:
    在这里插入图片描述

● 为了模拟路由跳转功能,咱们从antd扒一些布局代码,加入到App.tsx组件当中
● 因为都是些静态数据,可以自行定义;路由跳转核心是useNavigate, useRoutes两个方法,通过useRoutes()获得路由表,使用useNavigate()获得navigate函数(navigate是自定义的变量,可以是任意字符串),它接收两个参数,第一个是路径,第二个是可配置对象

import React, { useState } from 'react'
import {
  LaptopOutlined,
  NotificationOutlined,
  UserOutlined,
} from '@ant-design/icons'
import type { MenuProps } from 'antd'
import { Layout, Menu, theme } from 'antd'
import { useNavigate, useRoutes } from 'react-router-dom'
import routes from './routes/index'
const { Header, Content, Sider } = Layout

const titleMenu: MenuProps['items'] = ['1', '2', '3'].map((key) => ({
  key,
  label: `标题 ${key}`,
}))

const siderMenu: MenuProps['items'] = [
  {
    key: 'home',
    icon: <UserOutlined />,
    label: '人员管理',
  },
  {
    key: 'about',
    icon: <NotificationOutlined />,
    label: '关于系统',
  },
  {
    key: 'info',
    icon: <LaptopOutlined />,
    label: '信息管理',
    children: [
      {
        key: 'info-detail',
        label: '信息详情',
      },
      {
        key: 'info-look',
        label: '信息查询',
      },
    ],
  },
  {
    key: 'statistics',
    icon: <NotificationOutlined />,
    label: '数量统计',
  },
]

const App: React.FC = () => {
  const {
    token: { colorBgContainer },
  } = theme.useToken()
  // 获得路由表
  const routeView = useRoutes(routes)
  const navigate = useNavigate()
  // 面包屑名称
  const [breadcrumbName, setBreadcrumbName] = useState('home')
  // 点击菜单
  const handleSiderClick: MenuProps['onClick'] = ({ key, keyPath }) => {
    const name = keyPath.reverse().join('/') || ''
    setBreadcrumbName(name)
    if (key !== 'home' && key !== 'about') return
    // 路由跳转
    navigate(key, {
      replace: false,
      state: {
        id: key,
      },
    })
  }

  return (
    <Layout>
      <Header className='header'>
        <div className='logo' />
        <Menu
          theme='dark'
          mode='horizontal'
          defaultSelectedKeys={['1']}
          items={titleMenu}
        />
      </Header>
      <Layout>
        <Sider width={200} style={{ background: colorBgContainer }}>
          <Menu
            mode='inline'
            defaultSelectedKeys={['1']}
            defaultOpenKeys={['sub1']}
            style={{ height: '100%', borderRight: 0 }}
            items={siderMenu}
            onClick={handleSiderClick}
          />
        </Sider>
        <Layout style={{ padding: '0 24px 24px' }}>
          <div style={{ margin: '16px 0' }}>{breadcrumbName}</div>
          <Content
            style={{
              padding: 24,
              margin: 0,
              minHeight: 280,
              background: colorBgContainer,
            }}
          >
            {routeView}
          </Content>
        </Layout>
      </Layout>
    </Layout>
  )
}

export default App

● 最后运行起来就像这样:
在这里插入图片描述

四、引入状态管理集redux

● 如果你使用过vue,那么你一定知道vuex或是pinia,它们核心就是方便各组件的数据共享,同时也是组件通信方式之一
● 另外,redux并不是单纯为react使用的js库,其他框架也能使用,但是react框架通常使用它作为状态管理集

1.依赖安装

npm i redux react-redux 

2.关于react-redux

  • 除了redux,还安装了react-redux,这个库能为我们监测redux数据的变化,从而进行数据更新;因为redux本身不会监测数据的变化,手动监测redux数据方式为需要使用store实例上的subscribe方法,这里就不模拟了;另外react-redux提供了Provider,方便将store实例注入所有组件
  • 关于react-redux的相关概念文档传送门,核心就是引入了容器组件和展示组件的概念,它把需要使用redux数据的组件称为容器组件,在容器组件中,可以使用redux任意的api,方便进行共享数据的获取和操作
  • 总结下,容器组件咱们放在containers文件夹下,展示组件放在components文件夹,路由组件放在pages文件夹

3.例子准备

  • 接下来咱们实现一个crud的例子,在src下新建一个containers文件夹用于存放容器组件,自行创建两个容器组件,我这里叫Count(加减计数的简单组件)和Tiger(展示列表信息的组件),在App.tsx组件中引入
  • 在src目录下新建redux文件夹,并且在此文件夹下新增
    1. reducers文件夹,该文件夹下存储容器组件修改数据的方法,并且每个reducer函数都必须是纯函数
    2. actions文件夹,存放容器组件对数据操作的方法,例如新增,修改,删除等操作
    3. 创建store.ts文件,用于创建store仓库

4.组件实例

4.1 Count容器组件相关
  • 在actions文件夹下新增count.ts文件,定义新增,删除方法
export const increment = (data: any) => ({ type: 'increment', data })
export const decrement = (data: any) => ({ type: 'decrement', data })
// 模拟异步操作
export const incrementAsync = (data: any, delay = 500) => {
  // 默认会传入dispatch方法,直接声明形参调用即可
  return (dispatch: (arg0: { type: string; data: any }) => void) => {
    setTimeout(() => {
      dispatch(increment(data))
    }, delay)
  }
}
  • 在reducers文件夹下新增count.ts文件,定义修改数据的方法
  • 函数接收两个参数,分别为之前的状态(preState默认定义为0,否则初始值没有的话默认为undefined)动作对象(action包含type 和data)
export default function countReducer(
  preState = 0,
  action: { type: string; data: any }
): number {
  const { type, data } = action
  // 根据type进行数据操作
  switch (type) {
    case 'increment':
      return preState + data
    case 'decrement':
      return preState - data
    default:
      return preState
  }
}
  • 在Count容器组件中添加相关代码以及引入redux
// 容器组件作为redux与UI组件的桥梁
import { useState } from 'react'
// 引入connect方法链接组件与redux
import { connect } from 'react-redux'
import { decrement, increment, incrementAsync } from '../../redux/actions/count'
import { Select, Button } from 'antd'

function Count(props: {
  increment: Function
  decrement: Function
  incrementAsync: Function
  count: number
}) {
  const { increment, decrement, incrementAsync, count } = props
  let [selectNum, setSelectNum] = useState(1)
  const add = () => {
    increment(Number(selectNum))
  }
  const del = () => {
    decrement(Number(selectNum))
  }
  const addAsync = () => {
    incrementAsync(Number(selectNum))
  }
  // 切换数字
  const handleSelectChange = (value: string) => {
    setSelectNum(Number(value))
  }
  return (
    <div>
      <h1>总数为:{count}</h1>
      <Select
        defaultValue='1'
        style={{ width: 120 }}
        onChange={handleSelectChange}
        options={[
          { value: '1', label: '1' },
          { value: '2', label: '2' },
          { value: '3', label: '3' },
        ]}
      />
      &nbsp; <Button onClick={add}>+</Button>
      &nbsp; <Button onClick={del}>-</Button>
      <Button onClick={addAsync}>异步加</Button>
    </div>
  )
}

export default connect((state: { count: number }) => ({ count: state.count }), {
  increment,
  decrement,
  incrementAsync,
})(Count)
4.2 Tiger容器组件相关
  • actions
import { TigerVo } from "../reducers/tiger"
export const increTiger = (data: TigerVo) => ({ type: 'add', data })
export const delTigerById = (data: TigerVo) => ({ type: 'del', data })
  • reducer
    ○ 关于数组的操作,因为reducer需要是一个纯函数
    ○ 纯函数原则:
    ⅰ. 不得改写参数数据【unshift方式改写了原参数preState】
    ⅱ. 不会产生副作用,例如网络请求,输入输出
    ⅲ. 不能调用 Date.now()或Math.random()
export interface TigerVo {
  id: string
  name?: string
  age?: number
}
const initList: TigerVo[] = [
  {
    id: '001',
    name: '狮美丽',
    age: 4,
  },
]
export default function tigerReducer(
  preState = initList,
  action: { type: string; data: TigerVo }
) {
  const { type, data } = action
  // 根据type进行数据操作
  switch (type) {
    case 'add':
      // preState.unshift(data) 操作上来说和[preState, ...initList]是等价的
      // 但是[preState, ...initList]此方式返回的是一个新数组,不会影响原数组
      return [data, ...preState]
    case 'del':
      return preState?.filter((item) => item.id !== data.id)
    default:
      return preState
  }
}
  • 在Tiger组件添加样式代码,展示数据;同时通过redux能够实现数据源的共享,因此在Tiger组件中也能够拿到Count组件的总数进行展示
import { increTiger, delTigerById } from '../../redux/actions/tiger'
import { connect } from 'react-redux'
import { TigerVo } from '../../redux/reducers/tiger'
import { Button, List, Typography } from 'antd'
// 用于生成唯一标识的id,使用npm i nanoid进行安装;或者自行定义唯一key值也可以
import { nanoid } from 'nanoid'

function Tiger(props: {
  increTiger: Function
  delTigerById: Function
  tigerArr: TigerVo[]
  countNum?: number
}) {
  const add = () => {
    const obj = {
      id: nanoid(),
      name: '雄狮院长',
      age: 7,
    }
    props.increTiger(obj)
  }
  const delOne = (id: string) => {
    props.delTigerById({ id })
  }
  return (
    <List
      header={
        <div>
          <Button type='primary' onClick={add}>
            添加一个
          </Button>
          <div>
            展示一下<strong>Count组件</strong>的总数: {props.countNum}
          </div>
        </div>
      }
      bordered
      dataSource={props.tigerArr}
      renderItem={(item) => (
        <List.Item key={item.id}>
          <Typography.Text mark>ID</Typography.Text>
          {item.id}---
          <Typography.Text mark>名称:</Typography.Text>
          {item.name} ---
          <Typography.Text mark>年龄:</Typography.Text>
          {item.age}
          <Button
            type='primary'
            danger
            style={{ marginLeft: '20px' }}
            onClick={() => delOne(item.id)}
            >
            删除
          </Button>
        </List.Item>
      )}
      />
  )
}

export default connect(
  (state: { tigerArr: TigerVo[]; count: number }) => ({
    tigerArr: state.tigerArr,
    countNum: state.count,
  }),
  {
    // 此处定义的名称就是组件调用方法时的名称
    increTiger,
    delTigerById,
  }
)(Tiger)
4.3 统一暴露所有reducer文件
  • 在reducers文件夹下新建index.ts文件,统一暴露所有reducer集合,需要使用combineReducers方法
import { combineReducers } from 'redux'
import countReducer from './count'
import tigerReducer from './tiger'

// 合并所有reducers
export default combineReducers({
  count: countReducer,
  tigerArr: tigerReducer,
})
4.4 创建store实例
  • 在store.ts文件中创建store实例,通过createStore方法创建
  • 考虑到异步操作,需要引入redux-thunk中间件来支持actions中的异步操作;这情况在count组件的action中有存在
// 引入api,creatStore用于创建store对象
import { legacy_createStore as createStore, applyMiddleware } from 'redux'
import allReducers from './reducers/index'
import thunk from 'redux-thunk'
export default createStore(allReducers, applyMiddleware(thunk))
4.5为组件注入store实例

● 打开项目入口文件,index.tsx, 使用Provider包裹App组件,通过prop的方式传入store

import React from 'react'
import ReactDOM from 'react-dom/client'
import './assets/css/index.css'
import App from './App'
import { ConfigProvider } from 'antd'
// 语言汉化
import zhCN from 'antd/locale/zh_CN'
import dayjs from 'dayjs'
import 'dayjs/locale/zh-cn'
import store from './redux/store'
import { Provider } from 'react-redux'
import { BrowserRouter } from 'react-router-dom'
dayjs.locale('zh-cn')
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement)
root.render(
  <React.StrictMode>
    <BrowserRouter>
      <ConfigProvider locale={zhCN}>
        <Provider store={store}>
          <App />
        </Provider>
      </ConfigProvider>
    </BrowserRouter>
  </React.StrictMode>
)
4.6 实现效果
  • 最终实现效果如下:
    在这里插入图片描述

  • 项目结构
    在这里插入图片描述

写在最后

  • 仓库地址我也放这了,有需要的可以看看仓库传送门
  • 以上就是本文的全部内容了,如果有写得不对或是不好的地方还望指正。

更多推荐