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);

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

后话

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

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

相关推荐
gqkmiss17 分钟前
Chrome 浏览器 131 版本开发者工具(DevTools)更新内容
前端·chrome·浏览器·chrome devtools
Summer不秃23 分钟前
Flutter之使用mqtt进行连接和信息传输的使用案例
前端·flutter
旭日猎鹰27 分钟前
Flutter踩坑记录(二)-- GestureDetector+Expanded点击无效果
前端·javascript·flutter
Viktor_Ye33 分钟前
高效集成易快报与金蝶应付单的方案
java·前端·数据库
hummhumm35 分钟前
第 25 章 - Golang 项目结构
java·开发语言·前端·后端·python·elasticsearch·golang
乐闻x1 小时前
Vue.js 性能优化指南:掌握 keep-alive 的使用技巧
前端·vue.js·性能优化
一条晒干的咸魚1 小时前
【Web前端】创建我的第一个 Web 表单
服务器·前端·javascript·json·对象·表单
花海少爷1 小时前
第十章 JavaScript的应用课后习题
开发语言·javascript·ecmascript
Amd7941 小时前
Nuxt.js 应用中的 webpack:compiled 事件钩子
前端·webpack·开发·编译·nuxt.js·事件·钩子
生椰拿铁You1 小时前
09 —— Webpack搭建开发环境
前端·webpack·node.js