前言
在上期函数式编程的分享上,我有提过一个研究“开发者们花在维护代码上的时间有70%都是用来阅读并理解它的,然后全球程序员每天的平均代码量只有10行。我们一天花费8小时只是阅读代码,来搞清楚那10行应该写在哪里”。为什么要再提一下这个研究呢,是想强调一下我们代码的可维护性和可读性的重要性。
现状
我在本次分享前呢,查阅了许多我们gitlab现有的前端代码,发现有一些我们需要注意的问题
- 单元组件的提取: 在几个不同的页面组件中,有着相同的UI也用着相同的方法。但是他们都被写了一遍。
- 这样写会带来什么缺点。
- 一窥全貌: 当前,在一个主文件中,代码往往都是几百,几千甚至几万行,我们在阅读代码的时候,想要一窥代码全貌的时候,需要一直往下滑,可能好几十秒才能大概知道这个代码写的是什么?这就好像一个美女都已经躺好在你的床上了,可是她穿得衣服超级多,你完全不知道该怎么脱一样。我们该如何写代码才能十秒钟就可以看完代码全貌呢
步骤与细节:在你写代码的时候,在你要修改某个缺陷bug的时候,你是不是总是会用vscode工具去折叠那些我们不去关注的代码和方法呢,也就是其实我们在写完代码的时候,我们就已经知道这些方法是用来做什么的了。我们只需要知道方法名,就知道他的功能,而不再需要去了解该方法是如何实现的。总结来说: 在我们阅读代码的时候,我们只需要关注我们实现组件的步骤就能知道这个组件是做什么的了,而不需要去了解这些组件步骤是怎么去实现的。
Hooks主文件应如拆分
一个典型组件的构成 ```javascript /*
- @Author: SQYun
- @Name: 组件名称
- @State: [ enums]
- @Function: [function1, function2, ….]
- @Component: [Table(内部组件库的table组件)]
- @Date: 2020-10-14 14:11:48 */ import React, { useEffect, useState, useReducer, useRef } 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 MapTransfer from ‘./MapTransfer’ import SearchContent from ‘./../SearchContent’ import { fromPairs } from ‘lodash’
import { postSync, getTableData, exportFile } from ‘@/services/faceControl’
function FaceControl() {
// 获取字典 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)
/** handle **/
const { Ellipsis, EnumSelect, // RangePicker, ValidateWrapper, OPERATE_SPAN, VALIDATE_TIPS_TYPE_NORMAL, VALIDATE_TIPS_TYPE_POPOVER, } = Table; const { RangePicker } = DatePicker
const { 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 [ { title: “状态”, dataIndex: “status”, render: (key, record) => {
const text = key === 0 ? '未处理' : '已处理'
return (
<Ellipsis>
{text}
</Ellipsis>
)
}, createEditComp: {
component: "Input",
options: {
rules: [
{
required: true,
message: "请输入设备名称",
},
]
}
}, }, { title: “布控照”, dataIndex: “target_picture”, width: 80, render: (text) => {
let image = text?.replace(/$$$/, "");
return (
<img style={{ width: 50, height: 50 }} src={image} />
)
} }, { title: “抓拍照”, dataIndex: “pic_address”, width: 80, render: (text) => {
let image = text?.replace(/$$$/, "");
return (
<img style={{ width: 50, height: 50 }} src={image} />
)
} }, { title: “相似度”, dataIndex: “similarity”, render: text =>
{ }, { title: “姓名”, dataIndex: “name”, render: (text, record) =>${text}%
}{isShow(text, record)} }, { title: “性别”, dataIndex: “gender_code”, render: (text, record) => {return <Ellipsis>{isShow(getGenderByKey(text), record)}</Ellipsis>
} }, { title: “民族”, dataIndex: “ethic_code”, render: (text, record) =>
{isShow(getNationByKey(text), record)} }, { title: “告警时间”, dataIndex: “report_time”, render: text =>{text} }, { title: “告警点位”, dataIndex: “channel_name”, render: (text, record) =>{isShow(text, record)} }, { title: “布控目标”, dataIndex: “target_feature”, render: (text, record) =>{isShow(getTargetByKey(text), record)} }, { title: “布控名称”, dataIndex: “disposition_name”, render: (text, record) =>{isShow(text, record)} }, { title: “操作”, dataIndex: “$id”, render: (text, record, index) => {return (
<React.Fragment>
<a style={{ marginRight: OPERATE_SPAN }} onClick={() => showFunc(record, showDispatch, tableRef)}>{ record?.isShow ? '隐藏' : '显示'}</a>
{/* <a style={{ marginRight: OPERATE_SPAN }} onClick={() => message.warning(`删除 ${index + 1}`)}>删除</a> */}
</React.Fragment>
)
} } ]; }
/**
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: [
{
antdProps: {
icon: "hz-export",
children: "导出",
// disabled: this.state.selectedRows.length === 0,
onClick: (...rest) => {
const totalCount = tableRef?.current?.pagination?.total ?? 0
const query = tableRef?.current?.query ?? {}
showWarnModalByTotalCount(totalCount, query, exportFile)
}
},
},
// {
// elementType: "custom", // elementType 为 custom 时用户可通过 render 方法渲染任何内容
// render: () => {
// return <a>导入模板下载</a>
// }
// },
] }, / 自定义内容检索区 / searchOptions: { render: () => {
return (
<SearchContent
onChange={({ fuzzy_search }) => {
debugger
tableRef.current.updateQuery({
fuzzy_search,
});
}}
onSearch={({ fuzzy_search }) => {
tableRef.current.dataLoad({
fuzzy_search,
});
}}
/>
)
} } } }
/**
- 搜索栏配置项定义
@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, // trigger: (onTrigger) => { // return ( // // ) // }, // trigger: (onTrigger, form) => { // return ( // <> // // // </> // ); // }, conditions: [ {
label: "告警点位",
width: '20%',
render: (getFieldDecorator) => {
return (
getFieldDecorator("ga_codes", {
initialValue: "",
})(
// <EnumSelect
// codeKey = 'key'
// labelKey = 'value'
// list={enums.facilityType}
// hasAll
// />
<MapTransfer />
)
)
}
}, {
label: "布控目标",
render: (getFieldDecorator) => {
return (
getFieldDecorator("target_feature", {
initialValue: "",
})(
<EnumSelect
codeKey = 'key'
labelKey = 'value'
list={targetList}
hasAll
/>
)
)
}
}, {
label: "状态",
render: (getFieldDecorator) => {
return (
getFieldDecorator("status", {
initialValue: "",
})(
<EnumSelect
codeKey = 'key'
labelKey = 'value'
list={enums.status}
hasAll
/>
)
)
}
}, {
label: "告警时间",
render: (getFieldDecorator) => {
return (
getFieldDecorator("alarmTime", {
initialValue: "",
})(
<RangePicker
// list={enums.facilityType}
// hasAll
placeholder={['开始时间','结束时间']}
/>
)
)
}
}, {
label: "相似度",
render: (getFieldDecorator) => {
return (
getFieldDecorator("similarity", {
initialValue: "",
})(
<EnumSelect
codeKey = 'key'
labelKey = 'value'
list={enums.similarity}
hasAll
/>
)
)
}
}, {
label: "相似度TopK",
render: (getFieldDecorator) => {
return (
getFieldDecorator("top_k", {
initialValue: "",
})(
<EnumSelect
codeKey = 'key'
labelKey = 'value'
list={enums.topK}
hasAll
/>
)
)
}
}, {
label: "民族",
render: (getFieldDecorator) => {
return (
getFieldDecorator("ethic_code", {
initialValue: "",
})(
<EnumSelect
codeKey = 'key'
labelKey = 'value'
list={nationList}
hasAll
/>
)
)
}
}, {
label: "性别",
render: (getFieldDecorator) => {
return (
getFieldDecorator("gender_code", {
initialValue: "",
})(
<EnumSelect
codeKey = 'key'
labelKey = 'value'
list={genderList}
hasAll
// hasAllCode="0"
/>
)
)
}
}, ] } }
return (
export default FaceControl
<a name="hlMKX"></a>
### 看了上面的例子后,你是什么样的感受呢
这是一个我们经常用到的table组件代码,这还是在组件库各种封装过后的组件,也就是已经精简过跟多了。但是看到了这已经到570行的代码,你们的感受是如何呢?你会想着去维护它吗?<br />为什么我们这样一个普通组件的主文件代码这么多,这么繁杂呢?<br />答:**这是因为我们没有分清楚什么是步骤,什么是实现细节。当我们把步骤和细节写在一起的时候,灾难也就发生了。**<br />现在我们该怎样去让我们的写的组件能一眼看懂呢?<br />**我们现在来分析一下我们上面的这个table组件例子,了解一下它是怎么构成的。**
```javascript
- import
- hooks(useState(状态声明), useEffect(副作用修改))
- handler(处理函数, 接收props或者state转换为组件渲染需要用到的数据结构以及不需要公用的函数)
- UI渲染
当我们把代码按照上面的分类提取出来后,主文件代码呈现的的效果如下:
/*
* @Author: SQYun
* @Name: 组件名称
* @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
} = handle
return (
<div
className={styles.main}
>
<Table
rowKey="id"
columns={createColumns(tableRef, dictionary, showReducer, showDispatch)}
createPromise={createPromise}
setRef={listRef => {
tableRef.current = listRef
}}
hasSerialNo
hasDefaultLayout
antdProps={{
rowSelection: {
onChange: (selectedRowKeys, selectedRows) => {
console.log("selectedRows: ", selectedRows);
}
}
}}
handleBarOptions={createHandleBarOptions(tableRef)}
searchBarOptions={createSearchBarOptions(dictionary,)}
transformQuery={params => {
/* 处理分页字段 */
const { pageIndex: page_no, pageSize: page_size, alarmTime, gender_code, ethic_code, target_feature, similarity, top_k } = params;
const [start_time, end_time, ] = alarmTime
delete params.pageIndex;
delete params.pageSize;
const biz_map = {
gender_code,
ethic_code,
target_feature,
similarity,
top_k,
}
return {
...params,
page_no,
page_size,
biz_map,
start_time: start_time?.format('YYYY-MM-DD HH:mm:ss'),
end_time: end_time?.format('YYYY-MM-DD HH:mm:ss'),
}
}}
/>
</div>
)
}
export default FaceControl
细节
在提取时,我们需要注意一些细节
- 在编写handle函数时,尽量编写纯函数: 一个函数的返回结果只依赖于它的参数,并且在执行过程里面没有副作用
- 在有遇到多组件共用的方法或者hooks时,只需将其提取出来到src下面的utils或hooks文件夹下
- 带来的好处:让维护和重构代码变得更加容易。不必操心没注意到的副作用搞乱了整个应用。
|— container
|— entity.js // 存储常量
|— hooks.js // 各种自定义的 hooks
|— handler.js // 工具函数,以及不需要 hooks 的事件处理函数
|— component // 子组件
|— handle.js
|— hooks.js
|— index.js
|— index.less
|— index.js // 主文件,只保留实现步骤
|— index.css // css 文件
- 带来的优点
- 在这样一样样的分门别类后,你会发现组件的主文件里,只剩下了一些步骤,而怎么去实现这些步骤的细节,都已经被隐藏掉了。这样我们几乎能一眼就看清组件全貌,并能清楚的知道这个组件是做什么的,用了哪些方法。
- 当你遇见可共用的hooks或者工具方法时,只需要将其从该组件的handleJs提取出来使用就行。
- 可维护性(是怎么带来的)
当hooks与handleJs文件内容变的越来越多的时候,我们应怎么进行进一步的拆分呢
必要的注释
- 目录划分完后,为了增加代码的可维护性与可读性,注释也是必不可少的,我们应如何在组件层次写我们的注释呢
总结
本次分享呢,主要是从组件层面,来增加我们的项目管理中的代码可维护性和可读性