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

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

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

前端部分

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

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,或者说当成个人的笔记也是极好的。

相关推荐
凯哥爱吃皮皮虾7 小时前
如何给 react 组件写单测
前端·react.js·jest
每一天,每一步10 小时前
react antd点击table单元格文字下载指定的excel路径
前端·react.js·excel
screct_demo20 小时前
詳細講一下在RN(ReactNative)中,6個比較常用的組件以及詳細的用法
javascript·react native·react.js
光头程序员1 天前
grid 布局react组件可以循数据自定义渲染某个数据 ,或插入某些数据在某个索引下
javascript·react.js·ecmascript
limit for me1 天前
react上增加错误边界 当存在错误时 不会显示白屏
前端·react.js·前端框架
浏览器爱好者1 天前
如何构建一个简单的React应用?
前端·react.js·前端框架
VillanelleS1 天前
React进阶之高阶组件HOC、react hooks、自定义hooks
前端·react.js·前端框架
某哈压力大1 天前
基于react-vant实现弹窗搜索功能
前端·react.js
傻小胖1 天前
React 中hooks之useInsertionEffect用法总结
前端·javascript·react.js
flying robot2 天前
React的响应式
前端·javascript·react.js