使用create-react-app构建react+ts项目(超详细)
本文主要介绍的是如何通过create-react-app快速搭建一个react+ts的web项目,同时引入了ant-design作为UI组件库,路由(react-router-dom)以及使用redux作为状态管理库;希望通过结合一些简单案例的实践掌握react开发的基本基础以及帮助一些有需要的同学.
写在前面
- 本文主要介绍的是如何通过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文件夹,并且在此文件夹下新增
- reducers文件夹,该文件夹下存储容器组件修改数据的方法,并且每个reducer函数都必须是纯函数
- actions文件夹,存放容器组件对数据操作的方法,例如新增,修改,删除等操作
- 创建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' },
]}
/>
<Button onClick={add}>+</Button>
<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 实现效果
-
最终实现效果如下:
-
项目结构
写在最后
- 仓库地址我也放这了,有需要的可以看看仓库传送门
- 以上就是本文的全部内容了,如果有写得不对或是不好的地方还望指正。
更多推荐
所有评论(0)