React 开发 - 初始项目

这是一个 react 开发的一个项目记录。我们将从零开始,使用 react 开发项目。

首先,确保我们电脑上已经安装了需要的 nodeyarn 等环境。

初始项目

然后,我们参考 在 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);

这里,我们将编辑和新增放在同一个组件里面编写,方便管理。

后话

后面的查看列表项详情,编辑列表项,删除列表项的功能,感兴趣的读者可以自行添加。

该博文完成【✅】感谢阅读🌹

相关推荐
崔庆才丨静觅8 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60619 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了9 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅9 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅9 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅9 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment10 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅10 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊10 小时前
jwt介绍
前端
爱敲代码的小鱼10 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax