这是一个 react 开发的一个项目记录。我们将从零开始,使用 react
开发项目。
首先,确保我们电脑上已经安装了需要的 node
,yarn
等环境。
初始项目
然后,我们参考 在 Vite 中使用 文章,创建一个 todo-list
的项目。
bash
yarn create vite todo-list
这里,我们选择安装的框架是 react
,选择了 typescript + SWC
。接着,我们根据提示,进入 todo-list
根目录,安装依赖后,运行项目 yarn run dev
。
如果运行不了,请升级 node 的版本,笔者这里的 node 版本是 v18.19.1 稳定版本
我们开发控制台提示的地址 http://localhost:5173/
, 就可以看到欢迎的页面👇
更改监听的端口
我们可以通过 vite.config.ts
配置需要打开的端口👇
typescript
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react-swc'
// https://vitejs.dev/config/
export default defineConfig({
server: { // + 添加
port: 3000,
},
plugins: [react()],
})
这个时候,我们打开地址 http://localhost:3000
能够看到上面的初始内容。
添加 antd
这里我们选择的框架是 antd 👇
bash
npm install antd --save
然后我们就可以引用该框架的组件。
typescript
import React from 'react';
import { Button } from 'antd'; // 引入组件
// 函数组件
const App = () => (
<div className="App"> <Button type="primary">Button</Button> </div>
);
export default App;
引入路由
我们这里选择了 react-router-dom👇
bash
yarn add react-router-dom
然后,我们这里编写一个添加,编辑,查看列表,查看详情的路由信息:
typescript
// AppRouter.ts
export interface IAppRouterProps extends Component.IBaseComponentProps {}
const AppRouter_Base: FC<IAppRouterProps> = (p) => {
return (
<div>
<Routes>
<Route path="/" element={<BasicContainer />}>
{/* 列表 */}
<Route path="home" element={<HomePage />}></Route>
{/* 新增 */}
<Route path="home/add" element={<AddPage />}></Route>
{/* 编辑 */}
<Route path="home/edit/:id" element={<EditPage />}></Route>
{/* 详情 */}
<Route path="home/detail/:id" element={<DetailPage />}></Route>
{/* 重定向 */}
<Route path="" element={<Navigate to="/home" replace />}></Route>
</Route>
</Routes>
</div>
);
}
export const AppRouterContainer = memo(AppRouter_Base);
这里进入页面中,重定向到 /home
路由页面。
然后,我们在页面的首页上引入👇
typescript
// App.tsx
function App() {
return (
<BrowserRouter>
<AppRouterContaier />
</BrowserRouter>
);
}
引入 redux
这里,我们演示使用 redux 进行数据的管理,后面的文章,我们会提到使用 zustand 来管理数据。
运行下面的命令行进行安装👇
bash
$ npm install @reduxjs/toolkit
$ npm install redux
然后,我们引用它,这里编写一个简单的 Demo
,下面是入口文件
typescript
// infrastructure/store/index.ts
import { configureStore } from "@reduxjs/toolkit";
import { reducer } from "./reducer";
const store = configureStore({
reducer,
});
export type RootState = ReturnType<typeof store.getState >;
export type ReduxDispatch = typeof store.dispatch;
export default store;
我们编写一个 counter
案例:
typescript
// infrastructure/store/reducer/index.ts
import counterReducer from "../../../pages/home/service/store/counter/counterSlice";
export const reducer = {
todo: todoReducer
}
然后我们 createSlice
:
typescript
// pages/home/service/store/counter/counterSlice.ts
import { createSlice } from '@reduxjs/toolkit'
export interface CounterState {
value: number
}
const initialState: CounterState = {
value: 0,
};
export const counterSlice = createSlice({
name: "counter",
initialState,
reducers: {
increment: (state) => {
state.value += 1
},
decrement: (state) => {
state.value -= 1
},
incrementByAmount: (state, action) => {
state.value += action.payload
},
},
})
export const { increment, decrement, incrementByAmount } = counterSlice.actions
export default counterSlice.reducer
我们在入口页面引入 store
👇
typescript
// main.ts
createRoot(document.getElementById('root')!).render(
<Provider store={store}>
<App />
</Provider>
)
至此,我们已经添加完开发一个静态项目 的相关依赖。下面,我们将使用它们来开发一个 todo-list
的项目。
todoList 案例
todo list
的案例是老生常谈的项目了。所以这里我们实现简单的功能如下:
- 列表展示
- 列表项添加
我们使用 localStorage
来管理数据。
列表展示
typescript
// pages/home/Home.tsx
import { memo, FC } from "react";
import "./Home.scss";
import ListComp from "../../components/list";
import { Button } from "antd";
import { useNavigate } from "react-router-dom";
export interface IHomeProps extends Component.IBaseComponentProps {}
const Home_Base: FC<IHomeProps> = (p) => {
const navigate = useNavigate();
return (
<>
<div style={{display: "flex", justifyContent: "flex-end", alignItems: "center"}}>
{/* 跳转到新增的页面 */}
<Button type="primary" style={{marginTop: "20px"}} onClick={() => {
navigate('/home/add');
}}>添加事件</Button>
</div>
{/* 列表数据 */}
<ListComp></ListComp>
</>
);
}
export const HomePage = memo(Home_Base);
这里我们将 list
提出一个组件 ListComp
:
typescript
// src/components/list/index.tsx
import { memo, useCallback, useEffect, useRef, useState } from "react"
import { useDispatch, useSelector } from "react-redux";
import { ReduxDispatch, RootState } from "../../infrastructure/store";
import { ExclamationCircleFilled } from '@ant-design/icons';
import { Modal as ModalPc, List, Button } from 'antd';
import { TodoItem } from "../form/model";
import { searchItem } from "../../pages/home/service/store/todo/todoSlice";
import { useNavigate } from "react-router-dom";
import InputComp from '../../components/input';
function List_Comp() {
const navigate = useNavigate();
const dispatch = useDispatch<ReduxDispatch>();
const todoList = useSelector((state: RootState) => state.todo.list);
const getListSearch = (val: string) => {
dispatch(searchItem({search: val}));
}
return (
<div>
<h2>列表数据</h2>
<InputComp getSearchVal={getListSearch}></InputComp>
<List
itemLayout="horizontal"
dataSource={todoList}
renderItem={(item: TodoItem) => (
<List.Item
style={{ borderBottom: '1px solid #e8e8e8' }}
actions={[]}
>
<div>{item.title}</div>
</List.Item>
)}
>
</List>
</div>
)
}
export default memo(List_Comp);
上面是简单的列表展示。数据我们从 store
获取👇
typescript
// pages/home/service/store/todo/todoSlice.ts
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { TodoItem } from "../../../../../components/form/model";
const LIST_LOCALSTORAGE: string = 'todo-list-storage';
export interface TodoState {
list: TodoItem[]
}
// demo 使用 localstorage 来存储
const initialState: TodoState = {
list: JSON.parse(localStorage.getItem(LIST_LOCALSTORAGE) || '[]')
}
function setTodoListToLocalstorage(list: TodoItem[]) {
localStorage.setItem(LIST_LOCALSTORAGE, JSON.stringify(list));
}
export const todoSlice = createSlice({
name: "todo",
initialState,
reducers: {
// 新增
addItem: (state, action: PayloadAction<TodoItem>) => {
state.list.push(action.payload);
setTodoListToLocalstorage(state.list);
},
// 编辑
editItem: (state, action: PayloadAction<TodoItem>) => {
const index = state.list.findIndex((element: TodoItem) => element.id === action.payload.id);
if(index >= 0) {
state.list.splice(index, 1, action.payload);
}
setTodoListToLocalstorage(state.list);
},
// 删除
deleteItem: (state, action: PayloadAction<TodoItem>) => {
const index = state.list.findIndex((element: TodoItem) => element.id === action.payload.id);
if(index >= 0) {
state.list.splice(index, 1);
}
setTodoListToLocalstorage(state.list);
},
// 搜索
searchItem: (state, action: PayloadAction<{search: string}>) => {
// 原始数据
const originList: TodoItem[] = JSON.parse(localStorage.getItem(LIST_LOCALSTORAGE) || '[]');
if(action.payload.search === '') {
state.list = originList;
} else {
const searchResult: TodoItem[] = [];
for(let i = 0; i < originList.length; i += 1) {
const item: TodoItem = originList[i];
if(item.title.indexOf(action.payload.search) >= 0) {
searchResult.push(item);
}
}
state.list = searchResult;
}
},
}
});
export const { addItem, editItem, deleteItem, searchItem} = todoSlice.actions;
export default todoSlice.reducer;
我们关注获取列表就行了。然后参考上面 couterSlice
引入的方式引入 todoSlice
。
列表项添加
获取列表的表格,我们同样是引入 antd
来编写:
typescript
// pages/add/Add.tsx
import { memo, FC } from "react";
import FormComp from "../../components/form";
export interface IAddProps extends Component.IBaseComponentProps {}
const Add_Base: FC<IAddProps> = (p) => {
return (
<>
<h2>添加</h2>
<FormComp></FormComp>
</>
);
};
export const AddPage = memo(Add_Base);
同样的,我们提取出公共的组件 FormComp
:
typescript
// src/components/form/index.tsx
import { FC, memo, useEffect } from "react";
import type { FormProps } from 'antd';
import { Button, Form, Input, DatePicker } from 'antd';
import { useDispatch, useSelector } from "react-redux";
import { useNavigate } from 'react-router-dom';
import { ReduxDispatch, RootState } from "../../infrastructure/store";
import { addItem, editItem } from "../../pages/home/service/store/todo/todoSlice";
import { TodoItem } from "./model";
import moment from "moment";
const { TextArea } = Input;
type FieldType = {
title: string;
datetime: Date;
remark?: string;
};
interface IFormProps {
id?: number
}
const Form_Comp: FC<IFormProps> = ({id}) => {
const [form] = Form.useForm();
const navigate = useNavigate();
const dispatch = useDispatch<ReduxDispatch>();
const todoList = useSelector((state: RootState) => state.todo.list);
const onFinish: FormProps<FieldType>['onFinish'] = async (values: FieldType) => {
if(id) { // 编辑
await dispatch(editItem({
id: id,
title: values.title,
datetime: values.datetime.valueOf(),
remark: values.remark
}));
} else { // 新增
await dispatch(addItem({
id: Date.now(),
title: values.title,
datetime: values.datetime.valueOf(),
remark: values.remark
}));
}
navigate(-1);
};
useEffect(() => {
if(id) { // 编辑
// 模拟请求
fetchItem(id)
}
}, []);
function fetchItem(id: number) {
const todo: TodoItem | undefined = todoList.find((element: TodoItem) => element.id === id);
// 数据回填
if(todo?.id) {
form.setFieldsValue({
title: todo.title,
remark: todo.remark,
datetime: moment(todo.datetime) // TODO: 会造成时间重复问题???
})
}
}
return (
<>
<Form
form={form}
labelCol={{ span: 8 }}
wrapperCol={{ span: 16 }}
style={{ maxWidth: 600 }}
onFinish={onFinish}
>
<Form.Item<FieldType>
label="标题"
name="title"
rules={[{ required: true, message: '请输入标题' }]}
>
<Input />
</Form.Item>
<Form.Item<FieldType>
label="完成时间"
name="datetime"
rules={[{ required: true, message: '请选择时间' }]}
>
<DatePicker format="YYYY-MM-DD" />
</Form.Item>
<Form.Item<FieldType>
label="备注"
name="remark"
>
<TextArea rows={4} />
</Form.Item>
<Form.Item wrapperCol={{ offset: 8, span: 16 }}>
<Button type="primary" htmlType="submit">
提交
</Button>
</Form.Item>
</Form>
</>
);
};
export default memo(Form_Comp);
这里,我们将编辑和新增放在同一个组件里面编写,方便管理。
后话
后面的查看列表项详情,编辑列表项,删除列表项的功能,感兴趣的读者可以自行添加。
该博文完成【✅】感谢阅读🌹