思考:

  • 提取基础组件和公用方法会带来什么好处?
  • 一窥全貌: 当你在阅读一个500行代码的文件时,你会怎么做?
    • 我们该如何写代码才能十秒钟就可以看完代码全貌呢
  • 步骤与细节:

    • 在你写代码的时候,在你要修改某个缺陷bug的时候,你是不是总是会用vscode工具去折叠那些我们不去关注的代码和方法呢,也就是其实我们在写完代码的时候,我们就已经知道这些方法是用来做什么的了。我们只需要知道方法名,就知道他的功能,而不再需要去了解该方法是如何实现的。
    • 总结来说: 在我们阅读代码的时候,我们只需要关注我们实现组件的步骤就能知道这个组件是做什么的了,而不需要去了解这些组件步骤是怎么去实现的。

      一个Hooks组件是如何构成的

  • 这里是一个示例,可快速下拉略过此处代码()。

    造成的原因

    是什么原因导致这个组件主文件的代码能达到569行的呢?

  • 在该文件UI渲染阶段前,含有众多已经实现了的方法。在这里我想问个问题,在阅读这个文件时,我们第一时间会去关注这些方法是怎么实现的吗?我们第一时间只会关注这些方法用途是什么?而不是去关注这些方法是怎么实现的(所以我们是不是只需要知道它们的名字就好了)。

  • 这里呢?就关系到步骤与细节的区别了。因为我们没有分清楚什么是步骤,什么是实现的细节。当我们把步骤和细节写在一起的时候,灾难也就发生了。

    分析主文件含有的步骤

    根据上面的例子,按照功能模块分类,大体分为以下三块

  • Hooks的相关操作

  • 不涉及到Hooks的逻辑处理函数
  • UI渲染部分

因此我们可以将我们的主文件代码目录分解成如下的样子:

  1. |— container
  2. |— entity // 存储常量
  3. |— hooks.js // 各种自定义的 hooks
  4. |— handler.js // 转换函数,以及不需要 hooks 的事件处理函数
  5. |— component // 子组件
  6. |— handle.js
  7. |— hooks.js
  8. |— index.js
  9. |— index.less
  10. |— index.js // 主文件,只保留实现的业务步骤
  11. |— index.css // css 文件

拆分后的代码内容

主文件代码

主文件只保留业务步骤

  • hooks的引入
  • handle方法的引入
  • UI渲染

按照这样拆解后,主文件代码立马降到了84行,这个组件用到了哪些方法,哪些hooks变量也因此一目了然。

  1. /*
  2. * @Author: SQYun
  3. * @Name: 组件名称
  4. * @State: [ enums]
  5. * @Function: [function1, function2, ....]
  6. * @Component: [Table(内部组件库的table组件)]
  7. * @Date: 2020-10-14 14:11:48
  8. */
  9. import React, { useEffect } from 'react'
  10. import { Input, Button, message, Modal, Col, ConfigProvider, DatePicker } from 'antd'
  11. import { Table } from '@hz-components/react-base'
  12. import SearchBar from './SearchBar'
  13. import styles from './index.less'
  14. import handle from './handle' // 逻辑方法
  15. import useHooks from './hooks'
  16. function FaceControl() {
  17. const [
  18. dictionary,
  19. tableRef,
  20. showReducer,
  21. showDispatch
  22. ] = useHooks()
  23. const {
  24. createColumns,
  25. createPromise,
  26. createHandleBarOptions,
  27. createSearchBarOptions,
  28. getListFromDictionaryByItem
  29. } = handle
  30. return (
  31. <div
  32. className={styles.main}
  33. >
  34. <Table
  35. rowKey="id"
  36. columns={createColumns(tableRef, dictionary, showReducer, showDispatch)}
  37. createPromise={createPromise}
  38. setRef={listRef => {
  39. tableRef.current = listRef
  40. }}
  41. hasSerialNo
  42. hasDefaultLayout
  43. antdProps={。。。}
  44. handleBarOptions={createHandleBarOptions(tableRef)}
  45. searchBarOptions={createSearchBarOptions(dictionary,)}
  46. transformQuery={params => {
  47. 。。。
  48. }}
  49. />
  50. </div>
  51. )
  52. }
  53. export default FaceControl

HandleJs代码

  1. /*
  2. * @Name: 函数的逻辑处理
  3. * @ToolFunctions: [
  4. * getListFromDictionary(获取对应的值),
  5. * clearBlankStringFromObject(清除空字符串),
  6. * getValueByKey(根据key查询对应的值)
  7. * ]
  8. * @Function: [
  9. * createColumns(table列的生成),
  10. * createPromise(table数据的请求),
  11. * createHandleBarOptions(操作配置项生成),
  12. * createSearchBarOptions(搜索栏配置项生成)
  13. * ]
  14. * @Author: SQYun
  15. * @Date: 2020-10-22 10:47:00
  16. */
  17. import React from 'react'
  18. import { Table } from '@hz-components/react-base'
  19. import SearchContent from './../SearchContent'
  20. import MapTransfer from './../MapTransfer'
  21. import { Input, Button, message, Modal, Col, DatePicker } from 'antd'
  22. import { fromPairs } from 'lodash'
  23. import { showWarnModalByTotalCount } from '@/utils/exportFile'
  24. import entity from './../entity'
  25. import { postSync, getTableData, exportFile } from '@/services/faceControl'
  26. const {
  27. Ellipsis,
  28. EnumSelect,
  29. // RangePicker,
  30. ValidateWrapper,
  31. OPERATE_SPAN,
  32. VALIDATE_TIPS_TYPE_NORMAL,
  33. VALIDATE_TIPS_TYPE_POPOVER,
  34. } = Table;
  35. const { RangePicker } = DatePicker
  36. const { enums } = entity
  37. /**********工具函数 */
  38. /**
  39. * @description: 获取字典中对应的值
  40. * @param {Array} dictionary 字典字段
  41. * @return {function} dictionary中所有item与type相等的列表
  42. */
  43. const getListFromDictionaryByItem = (dictionary = []) => {
  44. /**
  45. * @param {string} item
  46. * @return {Array} dictionary中所有item与type相等的列表
  47. */
  48. return function(item) {
  49. const list = dictionary.filter((dictionaryItem) => {
  50. return dictionaryItem.item === item
  51. })
  52. return list
  53. }
  54. }
  55. /**
  56. * @description: 清除对象中属性值为空字符串的属性
  57. * @param {Object} obj 对象
  58. * @return {Object} 返回的对象中属性值无空字符串
  59. */
  60. const clearBlankStringFromObject = (obj) => {
  61. const noBlankStringList = Object.entries(obj).filter(([key, value]) => {
  62. if (value === '') { return false }
  63. return true
  64. })
  65. const noBlankStringObj = fromPairs(noBlankStringList)
  66. return noBlankStringObj
  67. }
  68. /**
  69. * @description: 根据list中的key查找对应的值
  70. * @param {Array} list 存储一系列数据的值
  71. * @return {String} 对应的值,如没有,改为默认值 '--'
  72. */
  73. const getValueByKey = (list) => (key) => {
  74. return list.find((item) => {
  75. return String(item.key) === String(key)
  76. })?.value ?? '--'
  77. }
  78. /**
  79. * @description: 根据state.[id]的值判断该值是否显示,不显示默认为'*'
  80. * @param {Object} state reducer的值
  81. * @param {any} id 该行数据的id值
  82. * @param {any} value 该行数据实际的值
  83. * @return {any} 结果值,如不显示则为"*"
  84. */
  85. function isShow (value, record) {
  86. const result = record?.isShow ? value : '*'
  87. return result
  88. }
  89. const showFunc = async (record, dispatch, ref) => {
  90. const list = await ref.current?.getList()
  91. const newList = list.map((val, index, arr) => {
  92. if(val.id === record.id) {
  93. val.isShow = !val.isShow
  94. }
  95. return val
  96. })
  97. const result = ref.current?.updateList(newList)
  98. }
  99. /**********将会返回的逻辑函数 */
  100. /**
  101. * table列表的生成
  102. * @param {Object} dictionary 后台的字典值
  103. * @param {Object} showReducer 存储该列是否显示的reducer状态
  104. * @param {Object} showDispatch 控制该列是否显示的reducer行为方法
  105. */
  106. const createColumns = (tableRef, dictionary, showReducer, showDispatch) => {
  107. const getListByItem = getListFromDictionaryByItem(dictionary)
  108. // 性别
  109. const genderList = getListByItem('gender')
  110. // 状态列表
  111. const statusList = getListByItem('defence_status')
  112. // 民族
  113. const nationList = getListByItem('nation')
  114. // 布控目标
  115. const targetList = getListByItem('person_target_type')
  116. const getGenderByKey = getValueByKey(genderList)
  117. const getNationByKey = getValueByKey(nationList)
  118. const getTargetByKey = getValueByKey(targetList)
  119. return [
  120. 。。。
  121. ];
  122. }
  123. /**
  124. * table数据的请求
  125. */
  126. const createPromise = async (params) => {
  127. const paramsNoBlankString = clearBlankStringFromObject(params)
  128. const tableData = await getTableData({
  129. ...paramsNoBlankString,
  130. biz_obj: 2,
  131. }).then((res) => {
  132. const totalCount = res?.total ?? 0;
  133. const currentPageResult = res?.elements.map((item) => {
  134. return {
  135. ...item?.biz_map,
  136. ...item,
  137. }
  138. }) ?? [];
  139. const pageIndex = res?.cur_page;
  140. return {
  141. totalCount,
  142. currentPageResult,
  143. pageIndex,
  144. }
  145. })
  146. return tableData
  147. }
  148. /**
  149. * 操作栏配置项定义
  150. */
  151. const createHandleBarOptions = (tableRef) => {
  152. return {
  153. handleOptions: {
  154. elements: [
  155. 。。。,
  156. ]
  157. },
  158. /* 自定义内容检索区 */
  159. searchOptions: {
  160. render: () => {
  161. return (
  162. 。。。
  163. )
  164. }
  165. }
  166. }
  167. }
  168. /**
  169. * 搜索栏配置项定义
  170. * @param {Array} dictionary 从后台获取的字典
  171. */
  172. const createSearchBarOptions = (dictionary) => {
  173. const getListByItem = getListFromDictionaryByItem(dictionary)
  174. // 性别
  175. const genderList = getListByItem('gender')
  176. // 状态列表
  177. const statusList = getListByItem('defence_status')
  178. // 民族
  179. const nationList = getListByItem('nation')
  180. // 布控目标
  181. const targetList = getListByItem('person_target_type')
  182. return {
  183. /* trigger 自定义查询按钮 */
  184. // trigger: "分析",
  185. trigger: null,
  186. conditions: [
  187. 。。。
  188. ]
  189. }
  190. }
  191. export default { createColumns, createPromise, createHandleBarOptions, createSearchBarOptions, getListFromDictionaryByItem }

HooksJs代码

  1. /*
  2. * @Name: 组件名称
  3. * @Props: [props1(参数1), props2(参数2), ...]
  4. * @State: [state1(状态1), state2(状态2), ...]
  5. * @Ref: [ref1(ref对象1), ref2(ref对象2), ]
  6. * @Effect: [effect1(), effect2()]
  7. * @Function: [function1, function2, ...]
  8. * @Return: [
  9. * state1(返回的状态1),
  10. * state2(返回的状态2),
  11. * function1(返回的方法1),
  12. * function2(返回的方法2),
  13. * ...
  14. * ]
  15. * @Author: SQYun
  16. * @Date: 2020-10-22 11:22:46
  17. */
  18. import { useState, useRef, useEffect, useReducer } from 'react'
  19. import { message } from 'antd'
  20. import { postSync } from '@/services/faceControl'
  21. function useHooks() {
  22. // 获取字典
  23. const [dictionary, setDictionary] = useState([])
  24. const tableRef = useRef()
  25. useEffect(() => {
  26. postSync().then((res) => {
  27. setDictionary(res)
  28. }).catch((err) => {
  29. message.error(err)
  30. })
  31. }, [])
  32. // 更改列表文字显示状态
  33. const initialState = {}
  34. function reducer(state, action) {
  35. const value = !state?.[action]
  36. const result = {
  37. ...state,
  38. [action]: value,
  39. }
  40. return result
  41. }
  42. const [showReducer, showDispatch] = useReducer(reducer, initialState)
  43. return [dictionary, tableRef, showReducer, showDispatch]
  44. }
  45. export default useHooks

优点

  • 可读性和可维护性的提高:这样拆分后组件做了什么,用到了哪些方法,已经一目了然了。这带来的可读性和可维护性的提升肯定是比之前的代码结构无法比较的。
  • 更易于提取复用的逻辑代码:当你发现该页面组件所使用的的hooks或方法在另外一个页面也可以用到时,你就可以直接将这些方法提取出来。

    当Hooks与Hanler文件逐渐繁琐时

    当hooks与handle文件逐渐繁琐时,我们其实还可以更进一步的拆分我们的hooks.js与handle.js。拆分目录示例如下
    1. |— container
    2. |— entity // 存储常量
    3. |— hooks // 各种自定义的 hooks
    4. |— useHooks1.js // hooks示例1
    5. |— useHooks2.js // hooks示例1
    6. |— index.js // hooks的主要出口
    7. |— handler // 转换函数,以及不需要 hooks 的事件处理函数
    8. |— handler1.js
    9. |— handler2.js
    10. |— index.js // handle的主要出口
    11. |— component //组件
    12. |— index.js
    13. |— index.less
    14. |— index.js // 主文件,只保留实现步骤
    15. |— index.css // css 文件

    组件所用的方法被公用时

    原则上只被一个组件使用到的hooks或handle方法是不用提取到公共文件夹下的。只有当hooks或handle方法被两个及以上组件共用时。才将其提取到公共文件夹下,也就是我们在src文件夹下的hooks文件夹与utils文件夹

    如何让开发者一眼就能看懂页面的注释案例

    主文件的拆分主要是为了提高代码的可读性和可维护性。那么为了让代码可读性和可维护性得到提高,注释也是必不可少的。因此我们该如何去写组件级别的注释呢?
    如何让开发者一眼就能看懂页面的注释案例

备注:

  • 在编写handle函数时,尽量编写纯函数: 一个函数的返回结果只依赖于它的参数,并且在执行过程里面没有副作用
  • 在提取公用组件或公用方法时,请尽量采用DRY(Don’t Repeat Yourserl)原则与让代码更容易易读的原则。