记一次前后端分离开发经验 -- 前端部分

由于文章太长了,导致编辑器太卡,所以不得已将文章分成两部分。这部分是【记一次前后端分离开发经验】的第二部分,第一部分的链接为:

如果您第一次点开这篇文章,劳驾您移步至:记一次前后端分离开发经验 -- 后端部分

前端部分

这部分对应的环节如下所示:

1. 搭建前端项目结构

shell 复制代码
npx create-react-app mail-front
cd mail-front
code .
yarn add @ant-design/pro-table antd @ant-design/icons mobx@6.12.0 mobx-react@9.1.0 mobx-react-lite@4.0.5 lodash@4.17.11 moment

rm -rf src

mkdir -p src/Mail
touch src/index.js src/App.js src/Mail/UserModel.jsx src/Mail/ListTable.jsx src/Mail/MailList.jsx

mkdir src/store
touch src/store/index.jsx src/store/Mail.jsx

git add .
git commit -m "project init"

注意:这里使用jsx和js是一样的!看个人喜好。

2. 入口文件中的内容

由于刚把入口文件和App.js删了,所以需要手动将index.js和App.js中的内容补充进来

js 复制代码
// index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
    <App />
);

然后是App.js中的内容:

js 复制代码
import ListTable from './Mail/ListTable';

const App = () => {
    return (
        <ListTable />
    )
}

export default App;

3. 业务组件 ListTable.js 的构造

在ListTable.js中将proTable的实现分成了三个大的部分:

  • 表格主体dom结构渲染
  • proTable的列的配置
  • 网络请求相关的函数由mobx统一管理
  • 添加或者编辑用户的弹窗

3.1 表格主体dom结构渲染

此部分功能由src/Mail/ListTable.jsx本身提供,对应的代码为:

js 复制代码
import React, { useState, useRef, useContext } from 'react';
import ProTable from '@ant-design/pro-table';
import { Button } from 'antd';
import { PlusOutlined } from '@ant-design/icons';

import UserModal from './UserModal.jsx';

import Column from './MailList';
import { StoreContext } from '../store';

const ListTable = props => {
  const [addInfo, setAddInfo] = useState({
    visible: false,
    init: {},
  });
  const [pageSize, setPageSize] = useState(10);

  const defaultPagination = {
    pageSize,
    showTotal: (total, range) => `${range[0]}-${range[1]} of ${total} items`,
    showSizeChanger: true,
    showQuickJumper: true,
    size: 'small',
  };

  const actionRef = useRef(null);

  const getLatestData = () => {
    actionRef.current?.reload();
  };

  const { mailStore } = useContext(StoreContext);

  return (
    <>
      <ProTable
        actionRef={actionRef}
        headerTitle="interaction demo"
        columns={Column(getLatestData, setAddInfo)}
        rowKey="key"
        search={false}
        options={false}
        toolBarRender={() => [
          <Button
            key="addUser"
            type="primary"
            onClick={(...args) => {
              setAddInfo({
                visible: true,
                init: {
                  id: undefined,
                  mailAddress: undefined,
                  recipientName: undefined,
                  status: 0,
                },
              });
            }}
          >
            <PlusOutlined /> Add User
          </Button>,
        ]}
        pagination={defaultPagination}
        request={async (...args)=>{
          const rst = await mailStore.request(...args);
          const {pageSize} = rst;
          setPageSize(pageSize);
          return rst;
        }}
      />
      <UserModal
        visible={addInfo.visible}
        onCancel={() => {
          setAddInfo({
            visible: false,
            init: {
              id: undefined,
              mailAddress: undefined,
              recipientName: undefined,
              status: 0,
            },
          });
        }}
        originalData={addInfo.init}
        updateParent={getLatestData}
      />
    </>
  );
};

export default ListTable;

3.2 proTable列的配置

配置ant-design/pro-table组件所需要的列对象,由于配置项比较复杂,所以单独拆到src/Mail/MailList.jsx文件中完成。

该文件的内容为:

js 复制代码
// src/Mail/MailList.jsx
import React, { useContext, useRef, useCallback } from 'react';
import moment from 'moment';
import { Switch, Space, Popconfirm, message } from 'antd';
import { StoreContext } from '../store/index.jsx';
import { debounce } from 'lodash';

const Column = (
  getLatestData,
  setAddInfo,
) => {
  const { mailStore } = useContext(StoreContext);
  const hasClicked = useRef(false);
  const doSomething = row => {
    mailStore.testHandle(getLatestData, row);
  };

  const debouncedFunction = useCallback(
    debounce(() => {
      hasClicked.current = false;
    }, 3000),
    [],
  );

  const handleClick = row => {
    if (!hasClicked.current) {
      doSomething(row);
      hasClicked.current = true;
      debouncedFunction();
    } else {
      message.warning('Send too frequently. Please try again later.');
    }
  };
  return [
    {
      title: 'Mail address',
      dataIndex: 'mailAddress',
      key: 'mailAddress',
    },
    {
      title: 'User name',
      dataIndex: 'recipientName',
      key: 'recipientName',
    },
    {
      title: 'Modified Time',
      dataIndex: 'updateTime',
      render: (_, row) => {
        const { updateTime } = row;
        if (updateTime)
          return (
            <span style={{ textWrap: 'noWrap' }}>
              {moment(updateTime).format('MM/DD, YYYY HH:mm:ss')}
            </span>
          );
        return '-';
      },
    },
    {
      title: 'Status',
      dataIndex: 'status',
      key: 'status',
      render: (_, row) => (
        <Switch
          checked={row.status}
          onChange={val => {
            mailStore.statusHandle(getLatestData, val, row);
          }}
        />
      ),
    },
    {
      title: 'Action',
      key: 'action',
      valueType: 'option',
      render: (_, row) => {
        return (
          <Space size="middle">
            <a
              onClick={() =>{
                console.log('row:', row)

                setAddInfo({
                  visible: true,
                  init: { ...row },
                })
              }
                
              }
            >
              Edit
            </a>

            <Popconfirm
              key="delete"
              title={'sure to delete?'}
              onConfirm={() => {
                mailStore.deleteHandle(getLatestData, row);
              }} // dangerous
            >
              <a
                style={{
                  marginRight: 16,
                  textWrap: 'nowrap',
                  color: '#FF4D4F',
                }}
              >
                Delete
              </a>
            </Popconfirm>
          </Space>
        );
      },
    },
  ];
};

export default Column;

上述代码定义这个表格具有Mail address, User name, Modified Time, Status, 和Action共五列内容。而这五列的内容则是通过请求后端数据接口获取并渲染的,渲染过程由ProTable组件封装起来了,只需按照组件的使用规范进行使用即可。

3.3 使用mobx统一管理网络请求

使用mobx统一管理此功能对应的各种网络请求,这样做的话能够将统一业务的相关代码放在一起,便于管理。当然也可以不用mobx,但是在我实际开发过程中mobx还管理其他状态,因此索性就让mobx将网络请求一起管理了。

src/store/index.js中的内容

实际开发中,不可能只有一个store,因此需要将多个store结合起来统一向外暴露,因此index.js中的内容为:

js 复制代码
// src/store/index.js
import React from 'react';
import { mailStore } from './Mail.jsx';

export const store = {
  mailStore,
};

export const StoreContext = React.createContext(store);

src/store/Mail.jsx中的内容

这个文件管理了所有此示例中请求相关的函数:

js 复制代码
// Import necessary libraries
import { makeAutoObservable } from 'mobx';
import { message } from 'antd';
import axios from 'axios';

// Define the base URL for API requests
const BASE_URL = 'http://192.168.190.76:7777';

// Define the paths for different API endpoints
const Delete_Path = `${BASE_URL}/delete?id=`;
const Status_Path = `${BASE_URL}/chgStatus`;
const Submit_Path = `${BASE_URL}/save`;
const Update_Path = `${BASE_URL}/update`;
const Request_Path = `${BASE_URL}/pageList`;

// Create a class for managing mail-related state and actions using MobX
class Mail {
  constructor() {
    // Make the class observable to track state changes
    makeAutoObservable(this);
  }

  // Handle delete operation
  deleteHandle(getLatestData, row) {
    if (!row?.id) return;
    axios
      .get(`${Delete_Path}${row.id}`)
      .then(res => {
        getLatestData();
      });
  }

  // Handle status change operation
  statusHandle(getLatestData, val, row) {
    axios
      .post(Status_Path, {
        ...row,
        status: val ? 1 : 0,
      })
      .then(res => {
        getLatestData();
      });
  }

  // Handle form submission
  onSubmit(values, originalData, onCancel, updateParent) {
    axios
      .post(Submit_Path, {
        ...originalData,
        ...values,
      })
      .then(res => {
        if (updateParent) updateParent();
        const { success, msg } = res?.data ?? {};
        if (!success) {
          message.error(msg);
          return false;
        }
        if (success) message.success('Success');
        if (onCancel) onCancel();
        return true;
      });
  }

  // Handle form update
  onUpdate(values, originalData, onCancel, updateParent) {
    axios
      .post(Update_Path, {
        ...originalData,
        ...values,
      })
      .then(res => {
        if (updateParent) updateParent();
        const { success, msg } = res?.data ?? {};
        if (!success) {
          message.error(msg);
          return false;
        }

        if (success) message.success('Success');
        if (onCancel) onCancel();
        return true;
      });
  }

  // Perform API request to retrieve data
  async request(params = {}) {
    const { current, pageSize, ...restParams } = params; // Extract search parameters
    const payload = {
      ...restParams,
    };
    const response = await axios.post(
      Request_Path,
      {
        body: payload,
        query: {
          current,
          size: pageSize,
        },
      },
    );

    // Extract relevant data from the API response
    const {
      data: { records, total, current: pages },
    } = response?.data;

    // Return formatted data
    return {
      data: records,
      success: true,
      pages,
      total,
    };
  }
}

// Create an instance of the Mail class to be used as a MobX store
export const mailStore = new Mail();

3.4 添加或者编辑用户的弹窗

使用antd中的Model组件完成添加或者编辑用户信息时候的弹窗,该部分功能由src/Mail/UserModel.jsx组件提供,其文件内容如下所示:

js 复制代码
import React, { useEffect, useContext } from 'react';
import { Modal, Form, Input, Button } from 'antd';
import { StoreContext } from '../store/index.jsx';

// Component for handling user modal (Add/Edit)
const UserModal = props => {
  // Destructure props for readability
  const { onCancel, updateParent, name } = props;
  
  // Create a form instance using Ant Design Form
  const [form] = Form.useForm();
  
  // Get the mailStore instance from the context
  const { mailStore } = useContext(StoreContext);

  // Extract originalData and check if it's in edit mode
  const originalData = props?.originalData ?? {};
  const isEditMode = !!originalData?.id;

  // useEffect to reset or set form fields based on edit mode and original data
  useEffect(() => {
    if (!isEditMode) form.resetFields();
    if (isEditMode) form.setFieldsValue(originalData);
  }, [isEditMode, originalData]);

  // Define form item layout for Ant Design Form
  const formItemLayout = {
    labelCol: { span: 6 },
    wrapperCol: { span: 18 },
  };

  // Handle form submission
  const onFinish = values => {
    // Execute appropriate action based on edit mode
    const _exec = isEditMode
      ? values => void mailStore.onUpdate(values, originalData, onCancel, updateParent)
      : values => void mailStore.onSubmit(values, originalData, onCancel, updateParent);

    _exec(values);
  };

  // Render the UserModal component
  return (
    <Modal
      title={isEditMode ? 'Edit' : 'Add'}
      visible={props.visible}
      onCancel={props.onCancel}
      destroyOnClose
      footer={[
        <Button key="submit" type="primary" onClick={() => form.submit()}>
          {isEditMode ? 'Update' : 'Submit'}
        </Button>,
      ]}
    >
      <Form
        {...formItemLayout}
        form={form}
        onFinish={onFinish}
      >
        {/* Form items for mail address and user name */}
        <Form.Item
          label="Mail address"
          name="mailAddress"
          rules={[
            { required: true, message: 'Please enter' },
            {
              type: 'email',
              message: 'Invalid email address',
            },
          ]}
          initialValue={isEditMode ? originalData?.mailAddress : undefined}
        >
          <Input placeholder="Please enter" />
        </Form.Item>
        <Form.Item
          label="User name"
          name="recipientName"
          rules={[{ required: true, message: 'Please enter' }]}
          initialValue={isEditMode ? originalData?.recipientName : undefined}
        >
          <Input placeholder="Please enter" maxLength={50} />
        </Form.Item>
      </Form>
    </Modal>
  );
};

export default UserModal;

4. 效果展示

下面的视频展示了最终效果,在这个视频中你可以看到的前后端交互内容包括:

  • 请求列表数据并按照编辑时间倒序排列
  • 新增用户
  • 编辑用户
  • 删除用户
  • 列表分页
  • 修改用户状态

结尾

我真诚的希望得到大佬的指导和建议。如果喜欢的人多的话,我会把代码上传至github,或者说当成个人的笔记也是极好的。

相关推荐
FinGet11 小时前
那总结下来,react就是落后了
前端·react.js
王解14 小时前
Jest项目实战(2): 项目开发与测试
前端·javascript·react.js·arcgis·typescript·单元测试
AIoT科技物语1 天前
免费,基于React + ECharts 国产开源 IoT 物联网 Web 可视化数据大屏
前端·物联网·react.js·开源·echarts
初遇你时动了情1 天前
react 18 react-router-dom V6 路由传参的几种方式
react.js·typescript·react-router
番茄小酱0011 天前
ReactNative中实现图片保存到手机相册
react native·react.js·智能手机
王解1 天前
Jest进阶知识:深入测试 React Hooks-确保自定义逻辑的可靠性
前端·javascript·react.js·typescript·单元测试·前端框架
小牛itbull1 天前
ReactPress—基于React的免费开源博客&CMS内容管理系统
前端·react.js·开源·reactpress
~甲壳虫2 天前
react中得类组件和函数组件有啥区别,怎么理解这两个函数
前端·react.js·前端框架
用户8185216881172 天前
react项目搭建create-router-dom,redux详细解说
react.js
new Vue()2 天前
Vue vs React:两大前端框架的区别解析
vue.js·react.js·前端框架