前言
前几天看到了有篇文章 知道尤雨溪为什么要放弃 $ 语法糖提案么? – 掘金 (juejin.cn),评论区有讨论不同水平的vue开发者,写出的代码质量大相径庭,react开发者的认为react的代码更加参差不齐。确实,我也是那么认为的,自从我接手了这个将近100M源代码的shit…
那么react代码到底该如何写,不说写的多么优雅,至少说要够实用,不要让人看到了就想重构,
国内大部分都用的antd为组件库 所以在react+antd上做分析
表单
后台项目中一般无非就是表格表单弹框等操作 感觉差别最大的就是表单 动态表单为最
<Form
className="addPersonnelBox"
form={form}
onFinish={values => {
saveAdd(values)
}}
onFinishFailed={values => {
console.log('values', values)
}}
>
<div className="addList">
<div className="addListTitle">
<span>*</span>照片
</div>
<div className="addListContent">
<Form.Item className="file" name="photo" rules={[{ required: true, message: '请上传照片' }]}>
<div className="file-content">
<div className="fileImg">
{imgList.map((item, i) => {
return (
<div key={i} className="img-box">
<img
onClick={() => {
setImgIsVisible(true)
}}
key={i}
src={imgUrlHandle(item.fileUrl)}
alt="success"
/>
<div
className="clear-img"
onClick={() => {
const index = imgList.findIndex(k => {
return k.uid === item?.uid || k.id === item.id
})
if (index > -1) {
const newImgList = imgList
newImgList.splice(index, 1)
setImgList([...newImgList])
}
}}
>
<CloseCircleOutlined />
</div>
</div>
)
})}
</div>
<div
className="file-tip"
style={{
marginLeft: imgList.length ? '24px' : ''
}}
>
<Upload {...props}>
{imgList.length < 1 && (
<Button className="file-button">
上传文件
<UploadOutlined />
</Button>
)}
</Upload>
<p className="file-text">最多可以上传1张,单张大小不超过2M</p>
</div>
</div>
</Form.Item>
</div>
</div>
<div className="addList">
<div className="addListTitle">
<span>*</span>姓名
</div>
<div className="addListContent">
<Form.Item name="userName" rules={[{ required: true, message: '请输入姓名' }]}>
<Select
onChange={ChangePeople}
style={{ width: '400px' }}
placeholder={!userArr.length ? '暂无业主信息' : '请选择业主'}
>
{userArr.length && userArr.map(item => <Option key={item.userName}>{item.userName}</Option>)}
</Select>
</Form.Item>
</div>
</div>
<div className="addList">
<div className="addListTitle">
<span>*</span>职务
</div>
<div className="addListContent">
<Form.Item name="postTypeId" rules={[{ required: true, message: '请输入职位' }]}>
<Select style={{ width: '400px' }} placeholder={!postSource.length ? '暂无职务信息' : '请选择职务'}>
{postSource.length && postSource.map(item => <Option key={item.id}>{item.name}</Option>)}
</Select>
</Form.Item>
</div>
</div>
// 省略大段代码
<div className="fnBox">
<Button className="closeBtn" onClick={() => setVisibleVote(false)}>
取消
</Button>
<Button className="confirmBtn" htmlType="submit">
确定
</Button>
</div>
</Form>
这是项目中真实代码,先不说逻辑里的代码,光是看dom及布局都头疼,看到这个的时候已经不是心情复杂而是想提桶跑路了,一起来分析下
布局样式
layout 有horizontal
| vertical
| inline
它们最大的不同是inline时最外层display是flex,其他两个是block,horizontal和vertical的区别是FormItem的flex盒子的flex-direction不同
- 去掉所有className、div,布局就统一horizontal、labelCol和wrapperCol去配置,labelCol/apperCol可以响应式写span:number,也可以手动写样式{style:{}}。如果要两列,在外层加Row、Col配置相应的属性就完事了
- 表单名称直接用label,如果想要label右对齐设置宽度并设置labelAlign:right(默认)
- 验证必填在formItem上写required或者rules,多看文档api。(手写红星星真是大无语)
- 头像上传官方有示例又为何不用
动态表单
前置知识:FormItem会透传value,onChange等到children组件中
在某些情况下在FormItem下的组件上写onChange是没有问题的,熟悉antd或看过源码的应该知道在Input上写了onChange和透传的onChange都会运行,并不会被覆盖。但是一般情况下是不需要的,见过很多代码喜欢在FormItem下的Input等组件加onChange,他们写onChange是为了动态表单,先onChange改变useState的值 然后用state去动态render,这种方式是最拉的
如下图。除了用Form.useWatch,还有属性dependencies、shouldUpdate实现一般简易动态表单。Form.List和 @formily/react也可以不同的实现复杂动态表单
组件封装
很多组件封装都是为了在表单中使用,但不要将FormItem封到组件中,会限制灵活性和维护性,上面示例的房号组件如下:
// 上面的逻辑先不看,从dom分析下
return (
<Form.Item
label="房号"
name="houseId"
{...props}
rules={[
{
required,
message: '请输入'
}
]}
>
<div
className="flex"
style={{
display: 'flex'
}}
>
<Form.Item noStyle name="number">
<Select disabled={disabled} placeholder='选择楼栋' onSelect={e => handleSelect(e)}>
{HouseUser.map(item => (
<Option key={item.number}>{item.number}</Option>
))}
</Select>
</Form.Item>
<Form.Item name="buildingId" noStyle>
<Select disabled={disabled} placeholder='选择单元' onSelect={e => handleUnit(e)}>
{unitArr.map(item => (
<Option key={item.buildingId}>{item.unit}</Option>
))}
</Select>
</Form.Item>
<Form.Item disabled={disabled} name="floor" noStyle>
<Select placeholder='选择楼层' onSelect={e => onSelect(e)}>
{floorArr && floorArr.map(item => <Option key={item}>{item}</Option>)}
</Select>
</Form.Item>
<Form.Item disabled={disabled} name="houseId" noStyle>
<Select placeholder='选择房号' allowClear>
{houseIdArr && houseIdArr.map(item => <Option key={item.houseId}>{item.houseNo}</Option>)}
</Select>
</Form.Item>
</div>
</Form.Item>
)
它的逻辑是挂载完成请求一个楼栋的接口(HouseUser),选择楼栋后请求单元(unitArr),之后依次请求到最后并且要传四个字段,不仅是维护困难,交互体验也差。说说是大概应该怎么改
1.跟后端沟通,很多时候不要他说怎么返数据结构就怎么做,先根据数据量确定能否全量返树形结构(直接用级联,只需要一个字段传数组过去),如果数据量较大也可以用级联的动态加载选项。
2.再根据项目中使用的频次及其他分析,考虑是将房号组件单独封装(将api也封装,使用时不必再写请求数据),还是将请求api手动传
另外FormItem传下来的value onChange一定要用到组件上才能保证自动回显和自动form.setFieldValue()能成功,其他的属性可以通过props透传下去
提交及关闭
一般情况下都有提交、重置等按钮,提交和重置在表单域button上写htmlType:submit||reset
在form上传onFinish方法(自动校验),一般不要用form.validateFields().then,校验逻辑通用的尽量都封装,然后再引入(如身份证,电话号码,银行卡等)
在Modal或者Drawer内使用Form组件管理状态时,关闭并不会清除表单的数据(编辑或新增时打开修改后关闭再打开),即使配置了destroyOnClose属性也不生效。常用解决方式:
前一种在于手动清空,后两种在于根据state(visible/open)变化清空
手动调用:在关闭时调form.resetFields(),借助Form组件的API清空状态(可能会冗余代码,如确认取消等操作成功后都要调用)
弹框afterClose/抽屉afterOpenChange 关闭后调用form.resetFields()
在Form中设置preserve属性为false,在Modal或Drawer中设置destroyOnClose属性为true自动清空
新增编辑详情
新增编辑详情能用一个页面绝不写两个
回显:保持提交数据与回显数据的一致性,如antd DatePicker 选择后是moment,传到后端时需要转为字符串,回显时再从字符串转回moment,这时就可以将DatePicker二次封装,使选择后表单本身值是string(透传下来的onChange(moment.format(...)),回显时将透传的value自动转为moment也就能自动回显。
详情:回显数据+disabled(UI不满足时再考虑重写) / 用@formily/antd中PreviewText
表格
//× bad 到处都是这种代码 各种冗余
<Form.Item label="支付方式" name="payType">
<Select allowClear placeholder="请选择" size="large" style={{ width: '320px' }}>
<Option value="WEIXIN">微信</Option>
<Option value="ALIPAY">支付宝</Option>
<Option value="BANK">银行卡</Option>
<Option value="CASH">现金</Option>
<Option value="OTHER">其他</Option>
</Select>
</Form.Item>
{
title: '支付方式',
dataIndex: 'wayOfInOutType',
render: (text, record) => (
<a style={{ color: 'black' }}>
{text.wayOfInOutType == 'WEIXIN'
? '微信'
: text.wayOfInOutType == 'ALIPAY'
? '支付宝'
: text.wayOfInOutType == 'BANK'
? '银行卡'
: text.wayOfInOutType == 'CASH'
? '现金'
: '其他'}
</a>
)
}
//√ good
export const payTypeOption=[{label:'xx',vallue:'xx'}] //某文件导出
import {payTypeOption} .....
const getText =(val)=> payTypeOption.find(item=>item.value==val)?.label||'-' //util导入
<Form.Item label="支付方式" name="payType">
<Select option={payTypeOption} allowClear placeholder="请选择" size="large" />
</Form.Item>
{
title: '支付方式',
dataIndex: 'wayOfInOutType',
render: (text, record) => getText(text)
}
优化:某个业务模块多处使用枚举在模块目录下创建js文件并导出枚举对象,在全局多处使用便在全局目录下创建js文件并导出枚举数组对象。不理解这里支付方式不能点击颜色也不是亮色为啥要用a标签
表单提交时不要过度解构赋值
// × bad
const onFinish = values => {
if (values) {
delete values?.villageName
const {
verifyUserName,
cardType,
cardNo,
phone,
villageName,
cert,
propertyRecord,
rangeTimer,
unifyCode,
uploadFileType,
groupPhoto,
businessCard,
organizationCard,
taxRegistrationCard,
idCardFront,
idCardBehind,
propertyServiceContractCard,
serviceContentCard
} = form.getFieldsValue(true)
const par = {
verifyType,
verifyUserName,
cardType,
cardNo,
phone,
villageName
}
let params = {}
if (verifyType === 'ASSEMBLY') {
params = {
...par,
cert
}
}
if (verifyType === 'COMPANY_AGENT') {
params = {
...par,
serviceStartTime: rangeTimer.serviceStartTime,
serviceEndTime: rangeTimer.serviceEndTime,
unifyCode,
uploadFileType,
groupPhoto,
businessCard,
organizationCard,
taxRegistrationCard,
idCardFront,
idCardBehind,
propertyServiceContractCard,
serviceContentCard
}
}
if (verifyType === 'REALTY_MANAGEMENT_COMMITTEE') {
params = {
...par,
propertyRecord
}
}
}
values不需要判断, 如果是校验没通过不会到这个方法, 通过即使没有值也是空对象
动态表单时页面上的表单应该也会动态删减, 不需要再手动判断该传什么值, 直接全部传,
即便是多传了也无所谓,后端只会取需要的
// √ good
const onFinish = values => {
// 若是如上面回显所说二次封装,保持提交数据与回显数据的一致性就都不用再处理了,直接传values
axios({ method: 'xx', url: 'xx', data: { ...values, startTime:handle(value.Time)} })
}
Tabs
再给大家伙看个牛逼的,手写tabs,写就算了,还是每个用到tabs的地方都手动写
const PropertyArchives = () => {
const [arr, setArr] = useState([
{ id: 1, name: 'xxx1', click: true },
{ id: 2, name: 'xxx2', click: false }
])
const [count, setCount] = useState(1)
const handleClick = id => {
const newArr = arr.map(v => {
v.click = false
if (v.id == id) {
setCount(v.id)
v.click = true
}
return v
})
setArr(newArr)
}
return (
<Wrap>
<div className="tabber">
{arr.map(v => (
<div onClick={() => handleClick(v.id)} className={v.click ? 'tabberClickList' : 'tabberList'} key={v.id}>
{v.name}
</div>
))}
</div>
{count == 1 ? <WYArchives /> : <CategorySettings />}
</Wrap>
)
}
export default PropertyArchives
这好用吗 用antd的Tabs不香吗
还有写重复转换的代码的,咱就是说直接key为枚举里的值不就完事了
// bad ×
import React, { useState } from 'react'
import { Tabs } from 'antd'
export default function Index() {
const getData = (val)=>{
let status
switch(val){
case 1 :
status='success'
break;
case 2 :
status='fail'
break;
}
api(xxx,{status})....
}
return <>
<Tabs
onChange={(val) => getData(val)}
items={[
{
label: '成功',
key: 1,
},
{
label: '失败',
key: 2,
},
]}
/>
...
</>
}
CSS
再来两段css,这是后台管理啊,各种改组件样式,各种百分比和rem,大多数组件是可以传style的,要改也不应该是这种方式
.form {
margin-top: 32px;
width: 49%;
input,
.ant-select-selector {
height: 48px;
color: #1b1b1d;
font-size: 16px;
font-weight: 500;
}
.ant-select-selection-placeholder {
line-height: 48px;
}
.ant-form-item-label {
width: 22.5%;
}
.ant-form-item-label > label {
height: 48px;
justify-content: flex-end;
font-size: 16px;
font-weight: 500;
}
.ant-select-selection-item {
line-height: 48px;
}
.star {
position: relative;
&::after {
position: absolute;
left: 13%;
top: 20px;
display: inline-block;
color: #ff4d4f;
font-size: 14px;
font-family: SimSun, sans-serif;
line-height: 1;
content: '*';
}
}
.ant-picker {
width: 100%;
height: 48px;
}
}
.form {
width: 90%;
margin: -0.5rem auto 4rem;
.radio {
height: 4.8rem;
line-height: 5.8rem;
}
input,
.ant-select-selector {
height: 4.8rem;
color: #1b1b1d;
font-size: 1.6rem;
font-weight: 500;
}
.ant-form-item-label {
width: 30%;
}
.ant-form-item-control-input {
width: 74%;
}
.ant-form-item-control-input-content {
text-align: left;
}
.ant-form-item-with-help .ant-form-item-explain {
text-align: left;
}
.ant-select-selector {
height: 4.8rem;
color: #1b1b1d;
}
.ant-select-single .ant-select-selector .ant-select-selection-item {
line-height: 4.8rem;
font-size: 1.4rem;
}
.flex {
display: flex;
align-items: center;
}
.ant-select-selection-placeholder {
line-height: 4.8rem;
font-size: 1.4rem;
}
.ant-form-item-label > label {
height: 4.8rem;
justify-content: flex-end;
font-size: 1.6rem;
font-weight: 500;
}
弹性布局用Row,有间隔用Space,分割线用Divider,描述用Descriptions,排版用Typography,表单布局用Row+Col 或者labelCol+wapperCol
后台管理能少写样式就少写,能用组件库的就不要覆盖,特别是对于刚开始工作的人,不要UI图上怎么画就怎么写,要沟通
(之前还遇到过一个新手,UI图上的字体浏览器没有,去百度了很久,还专门引了个字体包到项目,但其实根本就不需要改字体,想太多了),这个项目现在之所以丑且难用,很大一部分是因为最开始没有定好规范,确定好组件库之后就是页面适应组件而不是组件适应页面
其他
架构
这个项目还有个坑就是它的创建不是cra、不是ice也不是umi,而是一个架构师自己写的脚手架,他当时用这个的时候一定觉得他很NB吧,却不知道现在坑那么多。公司也不是个大公司,业务也不是啥非要自己架构的业务,就是简单的后台管理,有必要自己去架构?用umi不是啥都有
代码
状态值不要用props到处传,层级多就用redux等状态管理
经常会跳转到的页面里的状态最好是内部控制不要有props,需要的值用history.push({url,xxx})带过来
不写过多useState,相同类型的写一个useState({})
不写过多useEffect,业务逻辑清晰分离
恰当的用useRef代替useState,useRef除了用于获取dom和父调子通信,还会返回{current:xx}的对象
不用 arr.length && <>..</>, 0会显示在页面中
不要疯狂.then链式调用,async await用起来
还有些基础的数组map都用不明白的...
for (let i = 0; i < data.length; i++) {
data[i].name = data[i].fileName
data[i].prefix = data[i].suffix
data[i].path = imgUrlHandle(
data[i].fileUrl
)
data[i].url = imgUrlHandle(
data[i].fileUrl
)
}
自定义Hook
之前小程序上拉分页加载逻辑代码
// × bad
import React, { useState, useEffect } from "react";
import Taro, {
getCurrentInstance,
stopPullDownRefresh,
usePullDownRefresh
} from "@tarojs/taro";
import { Input, View, Image, Text } from "@tarojs/components";
let one = true;
let first = true;
const Index = () => {
let isLoad = false;
const [arr, setArr] = useState([]);
const [name, setName] = useState("");
const [search, setSearch] = useState({
keyWords: "",
page: 0,
pageSize: 10
});
useEffect(() => {
if (isLoad) return;
api.xx({ form: search }).then(res => {
// 判断小于10条就没有了
if (res.content) {
if (res.content.length < 10) {
isLoad = true;
}
if (first) {
setArr(res.content);
} else {
setArr([...arr, ...res.content]);
first = false;
}
}
});
}, [search]);
const handleSearch = () => {
isLoad = false;
one = false;
first = true;
setSearch({ ...search, keyWords: name, page: 0 });
};
const onScrollToLower = () => {
if (isLoad) return;
one = false;
let page: number = search.page || 0;
let data = { ...search };
data.page = page + 1;
first = false;
setSearch(data);
};
usePullDownRefresh(() => {
one = false;
first = true;
isLoad = false;
setSearch({ ...search, page: 0 });
});
return (...)
}
用自定义hook后
√ good
import { usePageData } from "@/utils/hooks";
const Index = () => {
//content:列表数据 isLoad: 是否加载完 getNextPageData: 获取下一页并合并后的数据
const { content, getNextPageData,isLoad ,loading} = usePageData({
api: xxx,
paramsName: "form",
initParams: xxx
});
// 上拉加载更多
const onScrollToLower = () => {
getNextPageData();
};
return <>...</>
}
最后
相信经常上社区的jy们对于这些可能都不以为然,都懂,但是确实还是有那么多的这种代码存在,还有很多逻辑层的问题没有写出,也希望大家都能多提升自己的代码,谁也不想自己的代码被后面接手的人骂吧,也欢迎大家分享出更多的“经典案例”,还是那句话, 不想着写的多么优雅NB,但要为了实用和自我提升努努力吧
说说别的,我也有一些代码洁癖,也会想着写出优雅的代码并被采纳,这个项目经常是改到某个地方可能看不下去就重构了。回想以前也写过很多垃圾代码,也有人说过我的代码问题。但是一定会改,后面每次写的新需求的时候也会想着较为好的编写方式,即便是自己能实现还是会百度看下别人的写法。以前我对前端初级中级高级的定义比较模糊,觉得除了代码还有很多方面的考量。但自从经历过招聘组员及CR组内人员的代码后,编程思维及开发思路都会在代码体现,所以代码便足以证明一切。如果某一天想跑路,一定不是公司怎么样了,而是实在不想接手这个烂摊子,不想在shit上搅拌…所以也劝告大家,尽量找有发展空间能提升自己的公司。(除非给的实在太多了?)