写点什么

「工作小记」关于业务组件的思考

作者:叶一一
  • 2022 年 9 月 06 日
    北京
  • 本文字数:5091 字

    阅读完需:约 17 分钟

「工作小记」关于业务组件的思考

前言

我们的节奏一般是双周迭代,大版本可能半个月到一个月,加上我偶尔会并行多个项目。

过去的一年,我差不多做了 30 个功能迭代,这里面不包括日常的临时修改需求或者线上 bug 维护等,平均一个月 2.5 个版本迭代。其中有半数以上是业务功能的开发。


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


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


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


于是年初的时候,做前端规划 PPT 时,我在思考开发如何“赋能”业务,首先想到的是业务组件的建设。


业务组件的理解

什么是业务组件

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


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

为什么封装业务组件

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


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

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

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

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

界定主要看以下几点:


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

  • 使用类似的数据;

  • 一致的处理流程;

  • 相似的业务目标。

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

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

业务组件的实现

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

项目结构

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


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

UI

资料查看



资料上传

组件封装

根据上面的 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;
复制代码

组件引入

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


  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;
复制代码

总结

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


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


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

发布于: 1 小时前阅读数: 10
用户头像

叶一一

关注

苍生涂涂,天下缭燎,诸子百家,唯我纵横。 2022.09.01 加入

非职业传道受业解惑前端程序媛,华夏美食、国漫、古风重度爱好者,刑侦、无限流小说初级玩家。

评论

发布
暂无评论
「工作小记」关于业务组件的思考_前端_叶一一_InfoQ写作社区