思考:
- 提取基础组件和公用方法会带来什么好处?
- 一窥全貌: 当你在阅读一个500行代码的文件时,你会怎么做?
- 我们该如何写代码才能十秒钟就可以看完代码全貌呢
步骤与细节:
-
造成的原因
是什么原因导致这个组件主文件的代码能达到569行的呢?
在该文件UI渲染阶段前,含有众多已经实现了的方法。在这里我想问个问题,在阅读这个文件时,我们第一时间会去关注这些方法是怎么实现的吗?我们第一时间只会关注这些方法用途是什么?而不是去关注这些方法是怎么实现的(所以我们是不是只需要知道它们的名字就好了)。
这里呢?就关系到步骤与细节的区别了。因为我们没有分清楚什么是步骤,什么是实现的细节。当我们把步骤和细节写在一起的时候,灾难也就发生了。
分析主文件含有的步骤
根据上面的例子,按照功能模块分类,大体分为以下三块
Hooks的相关操作
- 不涉及到Hooks的逻辑处理函数
- UI渲染部分
因此我们可以将我们的主文件代码目录分解成如下的样子:
|— container|— entity // 存储常量|— hooks.js // 各种自定义的 hooks|— handler.js // 转换函数,以及不需要 hooks 的事件处理函数|— component // 子组件|— handle.js|— hooks.js|— index.js|— index.less|— index.js // 主文件,只保留实现的业务步骤|— index.css // css 文件
拆分后的代码内容
主文件代码
主文件只保留业务步骤
- hooks的引入
- handle方法的引入
- UI渲染
按照这样拆解后,主文件代码立马降到了84行,这个组件用到了哪些方法,哪些hooks变量也因此一目了然。
/** @Author: SQYun* @Name: 组件名称* @State: [ enums]* @Function: [function1, function2, ....]* @Component: [Table(内部组件库的table组件)]* @Date: 2020-10-14 14:11:48*/import React, { useEffect } from 'react'import { Input, Button, message, Modal, Col, ConfigProvider, DatePicker } from 'antd'import { Table } from '@hz-components/react-base'import SearchBar from './SearchBar'import styles from './index.less'import handle from './handle' // 逻辑方法import useHooks from './hooks'function FaceControl() {const [dictionary,tableRef,showReducer,showDispatch] = useHooks()const {createColumns,createPromise,createHandleBarOptions,createSearchBarOptions,getListFromDictionaryByItem} = handlereturn (<divclassName={styles.main}><TablerowKey="id"columns={createColumns(tableRef, dictionary, showReducer, showDispatch)}createPromise={createPromise}setRef={listRef => {tableRef.current = listRef}}hasSerialNohasDefaultLayoutantdProps={。。。}handleBarOptions={createHandleBarOptions(tableRef)}searchBarOptions={createSearchBarOptions(dictionary,)}transformQuery={params => {。。。}}/></div>)}export default FaceControl
HandleJs代码
/** @Name: 函数的逻辑处理* @ToolFunctions: [* getListFromDictionary(获取对应的值),* clearBlankStringFromObject(清除空字符串),* getValueByKey(根据key查询对应的值)* ]* @Function: [* createColumns(table列的生成),* createPromise(table数据的请求),* createHandleBarOptions(操作配置项生成),* createSearchBarOptions(搜索栏配置项生成)* ]* @Author: SQYun* @Date: 2020-10-22 10:47:00*/import React from 'react'import { Table } from '@hz-components/react-base'import SearchContent from './../SearchContent'import MapTransfer from './../MapTransfer'import { Input, Button, message, Modal, Col, DatePicker } from 'antd'import { fromPairs } from 'lodash'import { showWarnModalByTotalCount } from '@/utils/exportFile'import entity from './../entity'import { postSync, getTableData, exportFile } from '@/services/faceControl'const {Ellipsis,EnumSelect,// RangePicker,ValidateWrapper,OPERATE_SPAN,VALIDATE_TIPS_TYPE_NORMAL,VALIDATE_TIPS_TYPE_POPOVER,} = Table;const { RangePicker } = DatePickerconst { enums } = entity/**********工具函数 *//*** @description: 获取字典中对应的值* @param {Array} dictionary 字典字段* @return {function} dictionary中所有item与type相等的列表*/const getListFromDictionaryByItem = (dictionary = []) => {/*** @param {string} item* @return {Array} dictionary中所有item与type相等的列表*/return function(item) {const list = dictionary.filter((dictionaryItem) => {return dictionaryItem.item === item})return list}}/*** @description: 清除对象中属性值为空字符串的属性* @param {Object} obj 对象* @return {Object} 返回的对象中属性值无空字符串*/const clearBlankStringFromObject = (obj) => {const noBlankStringList = Object.entries(obj).filter(([key, value]) => {if (value === '') { return false }return true})const noBlankStringObj = fromPairs(noBlankStringList)return noBlankStringObj}/*** @description: 根据list中的key查找对应的值* @param {Array} list 存储一系列数据的值* @return {String} 对应的值,如没有,改为默认值 '--'*/const getValueByKey = (list) => (key) => {return list.find((item) => {return String(item.key) === String(key)})?.value ?? '--'}/*** @description: 根据state.[id]的值判断该值是否显示,不显示默认为'*'* @param {Object} state reducer的值* @param {any} id 该行数据的id值* @param {any} value 该行数据实际的值* @return {any} 结果值,如不显示则为"*"*/function isShow (value, record) {const result = record?.isShow ? value : '*'return result}const showFunc = async (record, dispatch, ref) => {const list = await ref.current?.getList()const newList = list.map((val, index, arr) => {if(val.id === record.id) {val.isShow = !val.isShow}return val})const result = ref.current?.updateList(newList)}/**********将会返回的逻辑函数 *//*** table列表的生成* @param {Object} dictionary 后台的字典值* @param {Object} showReducer 存储该列是否显示的reducer状态* @param {Object} showDispatch 控制该列是否显示的reducer行为方法*/const createColumns = (tableRef, dictionary, showReducer, showDispatch) => {const getListByItem = getListFromDictionaryByItem(dictionary)// 性别const genderList = getListByItem('gender')// 状态列表const statusList = getListByItem('defence_status')// 民族const nationList = getListByItem('nation')// 布控目标const targetList = getListByItem('person_target_type')const getGenderByKey = getValueByKey(genderList)const getNationByKey = getValueByKey(nationList)const getTargetByKey = getValueByKey(targetList)return [。。。];}/*** table数据的请求*/const createPromise = async (params) => {const paramsNoBlankString = clearBlankStringFromObject(params)const tableData = await getTableData({...paramsNoBlankString,biz_obj: 2,}).then((res) => {const totalCount = res?.total ?? 0;const currentPageResult = res?.elements.map((item) => {return {...item?.biz_map,...item,}}) ?? [];const pageIndex = res?.cur_page;return {totalCount,currentPageResult,pageIndex,}})return tableData}/*** 操作栏配置项定义*/const createHandleBarOptions = (tableRef) => {return {handleOptions: {elements: [。。。,]},/* 自定义内容检索区 */searchOptions: {render: () => {return (。。。)}}}}/*** 搜索栏配置项定义* @param {Array} dictionary 从后台获取的字典*/const createSearchBarOptions = (dictionary) => {const getListByItem = getListFromDictionaryByItem(dictionary)// 性别const genderList = getListByItem('gender')// 状态列表const statusList = getListByItem('defence_status')// 民族const nationList = getListByItem('nation')// 布控目标const targetList = getListByItem('person_target_type')return {/* trigger 自定义查询按钮 */// trigger: "分析",trigger: null,conditions: [。。。]}}export default { createColumns, createPromise, createHandleBarOptions, createSearchBarOptions, getListFromDictionaryByItem }
HooksJs代码
/** @Name: 组件名称* @Props: [props1(参数1), props2(参数2), ...]* @State: [state1(状态1), state2(状态2), ...]* @Ref: [ref1(ref对象1), ref2(ref对象2), ]* @Effect: [effect1(), effect2()]* @Function: [function1, function2, ...]* @Return: [* state1(返回的状态1),* state2(返回的状态2),* function1(返回的方法1),* function2(返回的方法2),* ...* ]* @Author: SQYun* @Date: 2020-10-22 11:22:46*/import { useState, useRef, useEffect, useReducer } from 'react'import { message } from 'antd'import { postSync } from '@/services/faceControl'function useHooks() {// 获取字典const [dictionary, setDictionary] = useState([])const tableRef = useRef()useEffect(() => {postSync().then((res) => {setDictionary(res)}).catch((err) => {message.error(err)})}, [])// 更改列表文字显示状态const initialState = {}function reducer(state, action) {const value = !state?.[action]const result = {...state,[action]: value,}return result}const [showReducer, showDispatch] = useReducer(reducer, initialState)return [dictionary, tableRef, showReducer, showDispatch]}export default useHooks
优点
- 可读性和可维护性的提高:这样拆分后组件做了什么,用到了哪些方法,已经一目了然了。这带来的可读性和可维护性的提升肯定是比之前的代码结构无法比较的。
- 更易于提取复用的逻辑代码:当你发现该页面组件所使用的的hooks或方法在另外一个页面也可以用到时,你就可以直接将这些方法提取出来。
当Hooks与Hanler文件逐渐繁琐时
当hooks与handle文件逐渐繁琐时,我们其实还可以更进一步的拆分我们的hooks.js与handle.js。拆分目录示例如下|— container|— entity // 存储常量|— hooks // 各种自定义的 hooks|— useHooks1.js // hooks示例1|— useHooks2.js // hooks示例1|— index.js // hooks的主要出口|— handler // 转换函数,以及不需要 hooks 的事件处理函数|— handler1.js|— handler2.js|— index.js // handle的主要出口|— component //组件|— index.js|— index.less|— index.js // 主文件,只保留实现步骤|— index.css // css 文件
组件所用的方法被公用时
原则上只被一个组件使用到的hooks或handle方法是不用提取到公共文件夹下的。只有当hooks或handle方法被两个及以上组件共用时。才将其提取到公共文件夹下,也就是我们在src文件夹下的hooks文件夹与utils文件夹如何让开发者一眼就能看懂页面的注释案例
主文件的拆分主要是为了提高代码的可读性和可维护性。那么为了让代码可读性和可维护性得到提高,注释也是必不可少的。因此我们该如何去写组件级别的注释呢?
如何让开发者一眼就能看懂页面的注释案例
备注:
- 在编写handle函数时,尽量编写纯函数: 一个函数的返回结果只依赖于它的参数,并且在执行过程里面没有副作用
- 在提取公用组件或公用方法时,请尽量采用DRY(Don’t Repeat Yourserl)原则与让代码更容易易读的原则。
