【工作小记】关于业务组件的思考

2,980 阅读8分钟

一、前言

我在上周的时候,写了自己的2021年年终总结。我们的节奏一般是双周迭代,大版本可能半个月到一个月,加上我偶尔会并行多个项目,2021年,我差不多做了30个功能迭代,这里面不包括日常的临时修改需求或者线上bug维护等,平均一个月2.5个版本迭代。其中有半数以上是业务功能的开发。

什么是业务功能开发?我是这样理解的,以售卖商品流程为例,想要实现整个流程,需要有前端的销售页面、完整的购买流程流转页面、购买成功页,后台的售卖商品管理页、订单管理页等。业务功能开发,需要开发者了解要做什么以及怎么做。如果开发者不熟悉业务,可能会出现用户想买A产品,结果付款之后发现自己买成了B产品的情况。

大部分时候,我们接到的业务需求是在原来的功能上优化或者增强,这个时候可能不需要开发者花太多的时间就能完成。比如我近期的一个需求,拆了十几个小的修改点,基本都是在原来的基础上进行功能增强,比如加个按钮按照某个规则进行列表页的筛选,再比如将原来添加表单中的某些项单独拿出来,放到一个新的表单里面进行维护。这些需求并不难实现,如果我没有做任何思考,只是将功能实现,那么我的开发能力可能会停滞不前,且我的思维模式会定式。

无论是B端业务还是C端业务,技术都需要更好的服务于产品的使用者,即我们的用户。业务与技术开发密切相关,纯功能开发已无法满足日益增长和增强的功能需求。

正如我PPT里面所总结的,我在思考开发如何“赋能”业务,首先想到的是业务组件的建设。

幻灯片1.jpeg

二、业务组件的理解

2.1 什么是业务组件

前端组件化开发,我们会将部分功能独立出来,将这部分功能的数据层、视图层、控制层全部封装在一个组件内,只暴露一些传参和方法,从而实现这部分功能的单独维护和重复使用。

业务组件是将某些和业务逻辑强相关的功能独立出来,封装在一个组件里,进行单独维护。和业务逻辑强相关意味着不会适用所有的需求开发,但是随着业务功能的壮大,我们还是能在星辰大海中,寻找某些闪光频率同步的星星,进行单独维护和管理的。

2.2 为什么封装业务组件

正如前面所讲的,业务功能在不断的壮大,我们项目中的代码会越来越多,代码逻辑也变得复杂。我们之所以要封装业务组件是因为:

1)可以将复杂功能拆解,便于后续的快速迭代;

2)解决跨项目复用的问题,减少重复代码和重复开发;

3)统一代码质量,可以在快速开发的同时保证代码质量。

2.3 如何界定某个业务功能能否封装为业务组件

界定主要看以下几点:

1)具有相似的页面展示和交互;

2)使用类似的数据;

3)一致的处理流程;

4)相似的业务目标。

2.4 业务组件和基础组件的区别

我举例说明会看得更加明白。比如我们将列表组件进行了封装,无论怎样的业务需求,如果需要新增一个带分页的列表页面,基本都可以使用列表组件进行快速开发,这个无关具体的业务功能,可以视为一个基础组件;但是如果是一个备注功能,只有一部分业务功能需要,而这些业务功能又属于不同的页面,比如订单管理列表页、产品管理列表页,页面交互和接口的是相同的,可能接口入参不一样,这个备注弹窗就可以封装为一个业务组件。

三、业务组件的实现

我们的项目是基于React+Antd开发的,所以UI组件直接使用Antd提供的,写法主要是JSX+Hooks的语法。

3.1 项目结构

我们的项目基本是如下结构,包括接口、业务组件、基础组件、常量、css模块、业务模块、工具类等几个部分,这样的结构方便开发和维护。基于的业务的理解和思考,我们会根据实际情况封装一些业务组件和业务工具类等。也是因为做了这些工作,使得我能够在大多数的迭代开发中,节约不少的开发时间,且开发质量是很高的(提测阶段和线上bug明显减少了很多)。

关于业务组件的思考-01.jpg

3.2 资料编辑/查看组件的实现

3.2.1 UI

资料查看

image.png

资料上传

image.png

3.2.2 组件封装

根据上面的UI不难看出资料查看和资料上传两个弹窗的主要区别是弹窗标题、弹窗内容、是否可操作、弹窗底部的按钮。所以我做了以下处理:

1)组件通信:父子通信,父组件向子组件通过props传参,主要参数有visible-弹窗是否展示的布尔值、data-操作数据、onCancel-取消操作的回调函数,使用PropTypes提供的验证器进行参数的类型验证;子组件向父组件通信通过回调函数-onCancel;

2)区分弹窗类型:设置了modalType变量区分弹窗类型,枚举值为:view:资料查看,edit:资料上传;

3)区分弹窗内容、操作、底部按钮等差异:设置了商品对象:productObj,用于区分差异内容、操作、底部按钮;

4)上传组件:我们将上传组件进行了二次封装,可以配合antd自带的From组件一起使用;

/**
 * @description 商品业务-资料编辑/查看
 */
import React, { useRef, useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import { Form, Modal, Input, Button, Space } from 'antd';
import { ExclamationCircleFilled } from '@ant-design/icons';
import { Upload } from '@/components';

const ProductMaterial = ({ visible, data, onCancel }) => {
  const formRef = useRef({});
  const layout = {
    labelCol: { span: 4 },
    wrapperCol: { span: 20 },
  };
  const [confirmLoading, setConfirmLoading] = useState(false);
  const [productItem, setProductItem] = useState({});

  /**
   * 操作-关闭弹框
   * @param {string} type 要关闭的弹框key值
   * @param {boolean} refresh 弹窗关闭后是否刷新列表
   * @return {void} 无
   */
  const handleCancel = refresh => {
    setConfirmLoading(false);
    setProductItem({});
    onCancel && onCancel(refresh);
  };

  /**
   * 操作-确定按钮
   * @param {void} 无
   * @return {void} 无
   */
  const handleOk = () => {
    formRef.current.submit();
  };

  /** @name 商品对象  */
  const productObj = {
    edit: {
      modalTitle: '资料上传', // 弹窗展示标题
      productLabel: '详情文件', // 详情项label值
      endorseLabel: '批注文件', // 批注项label值
      footer: (
        <>
          <Button onClick={() => handleCancel(false)}>取消</Button>
          <Button type="primary" onClick={handleOk}>
            确定
          </Button>
        </>
      ), // 底部按钮组
    },
    view: {
      modalTitle: '资料查看',
      productLabel: '详情查看',
      endorseLabel: '批注查看',
      footer: <Button onClick={() => handleCancel(false)}>关闭</Button>,
    },
  };

  useEffect(() => {
    if (data.modalType) {
      let productItemInit = productObj[data.modalType];
      productItemInit.editFlag = data.modalType === 'edit' ? true : false; // 是否可以编辑的布尔值
      setProductItem(productItemInit);
    }
  }, [visible]);

  /**
   * 操作-上传
   * @param {string} type 上传图片类型
   * @return {void} 无
   */
  const uploadCallback = type => {
    return url => {
      formRef.current.setFieldsValue({
        [type]: url,
      });
    };
  };

  /**
   * 操作-提交
   * @param {Object} value 表单数据对象
   * @return {void} 无
   */
  const handleSubmit = value => {
    // 请求接口提交表单数据,请求成功之后进行结果回调到父组件
    onCancel && onCancel(true);
  };

  return (
    <Modal title={productItem.modalTitle} width={800} visible={visible} confirmLoading={confirmLoading} footer={productItem.footer} onCancel={() => handleCancel(false)}>
      <Form {...layout} labelAlign="left" onFinish={handleSubmit} ref={formRef}>
        {productItem.editFlag ? (
          <Space style={{ marginBottom: '15px' }}>
            <ExclamationCircleFilled style={{ color: '#d80000', fontSize: '16px' }} /> 上传文件的格式不限
          </Space>
        ) : null}
        <Form.Item label={productItem.productLabel} name="productFileUrl" rules={[{ required: true, message: `请上传${productItem.productLabel}` }]}>
          <Upload callback={uploadCallback('productFileUrl')} accept="*" limit={Infinity} disabled={!productItem.editFlag} isArray="true" />
        </Form.Item>
        <Form.Item label={productItem.endorseLabel} name="endorseFileUrl">
          <Upload callback={uploadCallback('endorseFileUrl')} accept="*" limit={Infinity} disabled={!productItem.editFlag} isArray="true" />
        </Form.Item>
        <Form.Item label="其他资料" name="otherFileUrl">
          <Upload callback={uploadCallback('otherFileUrl')} accept="*" limit={Infinity} disabled={!productItem.editFlag} isArray="true" />
        </Form.Item>
        <Form.Item name="remark" label="修改备注">
          <Input.TextArea maxLength={1000} rows={3} placeholder="请填写修改备注" disabled={!productItem.editFlag} />
        </Form.Item>
      </Form>
    </Modal>
  );
};

ProductMaterial.propTypes = {
  visible: PropTypes.bool.isRequired, // 弹窗关闭控制变量 必传
  data: PropTypes.object.isRequired, // 组件入参 必传
  onCancel: PropTypes.func, // 弹窗关闭事件
};

ProductMaterial.defaultProps = {
  visible: false,
  data: {},
};

export default ProductMaterial;

3.2.3 组件引入

用法跟常见的基础组件基本一致

1)在需要展示资料弹窗的页面引入ProductMaterial组件且将组件放到视图层;

2)因为是列表操作,所以在表格数组中加入操作项,操作项里面放置操作按钮,我把查看和上传放一起了,正常需求中这两个按钮会放在表格不同的列里;

3)添加操作函数,控制弹窗的打开和关闭以及上传之后的回调等

/**
 * @description 商品管理-首页
 */
import React, { useState, useRef } from 'react';
import { Button } from 'antd';
import { List } from '@/components';
import { PRODUCT_COLUMNS, PRODUCT_FIELDS } from '@/constants/product';
import { list } from '@/api/product';
// 业务组件引入
import { ProductMaterial } from '@/bundleComponents';

const ProductList = () => {
  const listRef = useRef();
  let columns = _.cloneDeep(PRODUCT_COLUMNS);
  const [visible, setVisible] = useState(false);
  const [recordData, setRecordData] = useState(false);

  /**
   * 操作
   * @param {boolean} visibleType 弹窗是否展示布尔值
   * @param {Object} data 数据对象
   * @param {boolean} refresh 列表是否刷新布尔值
   * @return {void} 无
   */
  const operate = (visibleType, data = {}, refresh) => {
    setVisible(visibleType);
    setRecordData(data);
    // =>true: 刷新列表
    if (refresh) {
      // 刷新列表
    }
  };

  columns = columns.concat([
    {
      title: '操作',
      width: 200,
      fixed: 'right',
      // eslint-disable-next-line
      render: (text, record) => (
        <>
          {/* 查看操作 */}
          <Button
            onClick={() => {
              operate(true, { ...record, modalType: 'view' });
            }}
          >
            资料查看
          </Button>
          {/* 上传操作 */}
          <Button
            type="primary"
            onClick={() => {
              operate(true, { ...record, modalType: 'edit' });
            }}
            style={{ marginLeft: '10px' }}
          >
            资料上传
          </Button>
        </>
      ),
    },
  ]);

  return (
    <div>
      <List fields={PRODUCT_FIELDS} columns={columns} http={list} ref={listRef} />
      {/* 业务组件使用 */}
      <ProductMaterial visible={visible} data={recordData} onCancel={refresh => operate(false, {}, refresh)} />
    </div>
  );
};

export default ProductList;

四、大佬们是如何思考业务组件的

从着手业务组件,再到发展自己项目的业务组件库,下面的大佬们无论是开发思路还是最后的总结都写的非常好,值得阅读和借鉴。

五、总结

在大量且重复的业务需求中,寻找可以提炼、可以拆分的功能模块,即便是看似平常或者做习惯的功能,也能找到亮点,而这种亮点既能提升开发者的技术能力,又能提高开发质量,并且能帮助开发者跳出思维定式,可谓是一举多得。

遇到新的需求可以跳出一味的复制粘贴式的开发的思维定式,适当的思考如何设计自己的功能模块,进而让自己能更高质量和更高效率的完成迭代任务。

冬日里北风轻,今天是个好天气。