前言
我们的节奏一般是双周迭代,大版本可能半个月到一个月,加上我偶尔会并行多个项目,去年,我差不多做了30个功能迭代,这里面不包括日常的临时修改需求或者线上bug维护等,平均一个月2.5个版本迭代。其中有半数以上是业务功能的开发。
对业务的思考
什么是业务功能开发?
我是这样理解的,以售卖商品流程为例,想要实现整个流程,需要有前端的销售页面、完整的购买流程流转页面、购买成功页,后台的售卖商品管理页、订单管理页等。业务功能开发,需要开发者了解要做什么以及怎么做。如果开发者不熟悉业务,可能会出现用户想买A产品,结果付款之后发现自己买成了B产品的情况。
业务功能干就完了?
大部分时候,我们接到的业务需求是在原来的功能上优化或者增强,这个时候可能不需要开发者花太多的时间就能完成。比如我近期的一个需求,拆了十几个小的修改点,基本都是在原来的基础上进行功能增强,比如加个按钮按照某个规则进行列表页的筛选,再比如将原来添加表单中的某些项单独拿出来,放到一个新的表单里面进行维护。这些需求并不难实现,如果我没有做任何思考,只是将功能实现,那么我的开发能力可能会停滞不前,且我的思维模式会定式。
无论是B端业务还是C端业务,技术都需要更好的服务于产品的使用者,即我们的用户。业务与技术开发密切相关,纯功能开发已无法满足日益增长和增强的功能需求。
正如我PPT里面所总结的,我在思考开发如何“赋能”业务,首先想到的是业务组件的建设。
业务组件的理解
什么是业务组件
前端组件化开发,我们会将部分功能独立出来,将这部分功能的数据层、视图层、控制层全部封装在一个组件内,只暴露一些传参和方法,从而实现这部分功能的单独维护和重复使用。
业务组件是将某些和业务逻辑强相关的功能独立出来,封装在一个组件里,进行单独维护。和业务逻辑强相关意味着不会适用所有的需求开发,但是随着业务功能的壮大,我们还是能在星辰大海中,寻找某些闪光频率同步的星星,进行单独维护和管理的。
为什么封装业务组件
正如前面所讲的,业务功能在不断的壮大,我们项目中的代码会越来越多,代码逻辑也变得复杂。我们之所以要封装业务组件是因为:
- 可以将复杂功能拆解,便于后续的快速迭代;
- 解决跨项目复用的问题,减少重复代码和重复开发;
- 统一代码质量,可以在快速开发的同时保证代码质量。
如何界定某个业务功能能否封装为业务组件
界定主要看以下几点:
- 具有相似的页面展示和交互;
- 使用类似的数据;
- 一致的处理流程;
- 相似的业务目标。
业务组件和基础组件的区别
我举例说明会看得更加明白。比如我们将列表组件进行了封装,无论怎样的业务需求,如果需要新增一个带分页的列表页面,基本都可以使用列表组件进行快速开发,这个无关具体的业务功能,可以视为一个基础组件;但是如果是一个备注功能,只有一部分业务功能需要,而这些业务功能又属于不同的页面,比如订单管理列表页、产品管理列表页,页面交互和接口的是相同的,可能接口入参不一样,这个备注弹窗就可以封装为一个业务组件。
业务组件的实现
我们的项目是基于React+Antd开发的,所以UI组件直接使用Antd提供的,写法主要是JSX+Hooks的语法。
项目结构
我们的项目基本是如下结构,包括接口、业务组件、基础组件、常量、css模块、业务模块、工具类等几个部分,这样的结构方便开发和维护。基于的业务的理解和思考,我们会根据实际情况封装一些业务组件和业务工具类等。也是因为做了这些工作,使得我能够在大多数的迭代开发中,节约不少的开发时间,且开发质量是很高的(提测阶段和线上bug明显减少了很多)。
资料编辑/查看组件的实现
UI
资料查看
资料上传
组件封装
根据上面的UI不难看出资料查看和资料上传两个弹窗的主要区别是弹窗标题、弹窗内容、是否可操作、弹窗底部的按钮。所以我做了以下处理:
- 组件通信:父子通信,父组件向子组件通过props传参,主要参数有visible-弹窗是否展示的布尔值、data-操作数据、onCancel-取消操作的回调函数,使用PropTypes提供的验证器进行参数的类型验证;子组件向父组件通信通过回调函数-onCancel;
- 区分弹窗类型:设置了modalType变量区分弹窗类型,枚举值为:view:资料查看,edit:资料上传;
- 区分弹窗内容、操作、底部按钮等差异:设置了商品对象:productObj,用于区分差异内容、操作、底部按钮;
- 上传组件:我们将上传组件进行了二次封装,可以配合antd自带的From组件一起使用。
/** * @description 商品业务-资料编辑/查看 */importReact, { useRef, useState, useEffect } from'react'; importPropTypesfrom'prop-types'; import { Form, Modal, Input, Button, Space } from'antd'; import { ExclamationCircleFilled } from'@ant-design/icons'; import { Upload } from'@/components'; constProductMaterial= ({ visible, data, onCancel }) => { constformRef=useRef({}); constlayout= { labelCol: { span: 4 }, wrapperCol: { span: 20 }, }; const [confirmLoading, setConfirmLoading] =useState(false); const [productItem, setProductItem] =useState({}); /** * 操作-关闭弹框 * @param {string} type 要关闭的弹框key值 * @param {boolean} refresh 弹窗关闭后是否刷新列表 * @return {void} 无 */consthandleCancel=refresh=> { setConfirmLoading(false); setProductItem({}); onCancel&&onCancel(refresh); }; /** * 操作-确定按钮 * @param {void} 无 * @return {void} 无 */consthandleOk= () => { formRef.current.submit(); }; /** @name 商品对象 */constproductObj= { edit: { modalTitle: '资料上传', // 弹窗展示标题productLabel: '详情文件', // 详情项label值endorseLabel: '批注文件', // 批注项label值footer: ( <><ButtononClick={() =>handleCancel(false)}>取消</Button><Buttontype="primary"onClick={handleOk}>确定</Button></> ), // 底部按钮组 }, view: { modalTitle: '资料查看', productLabel: '详情查看', endorseLabel: '批注查看', footer: <ButtononClick={() =>handleCancel(false)}>关闭</Button>, }, }; useEffect(() => { if (data.modalType) { letproductItemInit=productObj[data.modalType]; productItemInit.editFlag=data.modalType==='edit'?false : true; // 是否可以编辑的布尔值setProductItem(productItemInit); } }, [visible]); /** * 操作-上传 * @param {string} type 上传图片类型 * @return {void} 无 */constuploadCallback=type=> { returnurl=> { formRef.current.setFieldsValue({ [type]: url, }); }; }; /** * 操作-提交 * @param {Object} value 表单数据对象 * @return {void} 无 */consthandleSubmit=value=> { // 请求接口提交表单数据,请求成功之后进行结果回调到父组件onCancel&&onCancel(); }; return ( <Modaltitle={productItem.modalTitle} width={800} visible={visible} confirmLoading={confirmLoading} footer={productItem.footer} onCancel={() =>handleCancel(false)}><Form {layout} labelAlign="left"onFinish={handleSubmit} ref={formRef}> {productItem.editFlag? ( <Spacestyle={{ marginBottom: '15px' }}><ExclamationCircleFilledstyle={{ color: '#d80000', fontSize: '16px' }} /> 上传文件的格式不限</Space> ) : null} <Form.Itemlabel={productItem.productLabel} name="productFileUrl"rules={[{ required: true, message: `请上传${productItem.productLabel}` }]}><Uploadcallback={uploadCallback('productFileUrl')} accept="*"limit={Infinity} disabled={productItem.disabled} isArray="true"/></Form.Item><Form.Itemlabel={productItem.endorseLabel} name="endorseFileUrl"><Uploadcallback={uploadCallback('endorseFileUrl')} accept="*"limit={Infinity} disabled={productItem.disabled} isArray="true"/></Form.Item><Form.Itemlabel="其他资料"name="otherFileUrl"><Uploadcallback={uploadCallback('otherFileUrl')} accept="*"limit={Infinity} disabled={productItem.disabled} isArray="true"/></Form.Item><Form.Itemname="remark"label="修改备注"><Input.TextAreamaxLength={1000} rows={3} placeholder="请填写修改备注"disabled={productItem.disabled} /></Form.Item></Form></Modal> ); }; ProductMaterial.propTypes= { visible: PropTypes.bool.isRequired, // 弹窗关闭控制变量 必传data: PropTypes.object.isRequired, // 组件入参 必传onCancel: PropTypes.func, // 弹窗关闭事件}; ProductMaterial.defaultProps= { visible: false, data: {}, }; exportdefaultProductMaterial;
组件引入
用法跟常见的基础组件基本一致
- 在需要展示资料弹窗的页面引入ProductMaterial组件且将组件放到视图层;
- 因为是列表操作,所以在表格数组中加入操作项,操作项里面放置操作按钮,我把查看和上传放一起了,正常需求中这两个按钮会放在表格不同的列里;
- 添加操作函数,控制弹窗的打开和关闭以及上传之后的回调等。
/** * @description 商品管理-首页 */importReact, { 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'; constProductList= () => { constlistRef=useRef(); letcolumns=_.cloneDeep(PRODUCT_COLUMNS); const [visible, setVisible] =useState(false); const [recordData, setRecordData] =useState(false); /** * 操作 * @param {boolean} visibleType 弹窗是否展示布尔值 * @param {Object} data 数据对象 * @param {boolean} refresh 列表是否刷新布尔值 * @return {void} 无 */constoperate= (visibleType, data= {}, refresh) => { setVisible(visibleType); setRecordData(data); // =>true: 刷新列表if (refresh) { // 刷新列表 } }; columns=columns.concat([ { title: '操作', width: 200, fixed: 'right', // eslint-disable-next-linerender: (text, record) => ( <> {/* 查看操作 */} <ButtononClick={() => { operate(true, { record, modalType: 'view' }); }} >资料查看</Button> {/* 上传操作 */} <Buttontype="primary"onClick={() => { operate(true, { record, modalType: 'edit' }); }} style={{ marginLeft: '10px' }} >资料上传</Button></> ), }, ]); return ( <div><Listfields={PRODUCT_FIELDS} columns={columns} http={list} ref={listRef} /> {/* 业务组件使用 */} <ProductMaterialvisible={visible} data={recordData} onCancel={refresh=>operate(false, {}, refresh)} /></div> ); }; exportdefaultProductList;
总结
在大量且重复的业务需求中,寻找可以提炼、可以拆分的功能模块,即便是看似平常或者做习惯的功能,也能找到亮点,而这种亮点既能提升开发者的技术能力,又能提高开发质量,并且能帮助开发者跳出思维定式,可谓是一举多得。
遇到新的需求可以跳出一味的复制粘贴式的开发的思维定式,适当的思考如何设计自己的功能模块,进而让自己能更高质量和更高效率的完成迭代任务。
秋日里北风轻,今天是个好天气。