React/Vue,同水平哪个代码质量差距更大?

前言

前几天看到了有篇文章 知道尤雨溪为什么要放弃 $ 语法糖提案么? - 掘金 (juejin.cn),评论区有讨论不同水平的vue开发者,写出的代码质量大相径庭,react开发者的认为react的代码更加参差不齐。确实,我也是那么认为的,自从我接手了这个将近100M的源代码...

image.png

那么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>

image.png

这是项目中真实代码,先不说逻辑里的代码,光是看dom及布局都头疼,一起来分析下

布局样式

layout 有horizontal | vertical | inline 它们最大的不同是inline时最外层display是flex,其他两个是block,horizontal和vertical的区别是FormItem的flex盒子的flex-direction不同

  1. 去掉所有className、div,布局就统一horizontal、labelCol和wrapperCol去配置,labelCol/wrapperCol可以响应式写span:number,也可以手动写样式{style:{}}。如果要两列,在外层加Row、Col配置相应的属性就完事了
  2. 表单名称直接用label,如果想要label右对齐设置宽度并设置labelAlign:right(默认)
  3. 验证必填在formItem上写required或者rules,多看文档api。(手写红星星真是大无语)
  4. 头像上传官方有示例又为何不用

image.png

动态表单

前置知识:FormItem会透传value,onChange或者用valuePropName/trigger配置别的字段名到子组件中

image.png

在某些情况下在FormItem下的组件上写onChange是没有问题的,熟悉antd或看过源码的应该知道在Input上写了onChange和透传的onChange都会运行,并不会被覆盖。但是一般情况下是不需要的,见过很多代码喜欢在FormItem下的Input等组件加onChange,他们写onChange是为了动态表单,先onChange改变useState的值 然后用state去动态render,这种方式是最拉的如下图。除了用Form.useWatch,还有属性dependencies、shouldUpdate实现一般简易动态表单。Form.List和 @formily/react也可以不同的实现复杂动态表单

image.png

image.png

组件封装

很多组件封装都是为了在表单中使用,但不要将FormItem封到组件中,会限制灵活性和维护性,上面示例的房号组件如下:

image.png

// 上面的逻辑先不看,从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透传下去

image.png

提交及关闭

一般情况下都有提交、重置等按钮,提交和重置在表单域button上写htmlType:submit||reset 在form上传onFinish方法(自动校验),表单域中的button不要用form.validateFields().then,校验逻辑通用的尽量都封装,然后再引入(如身份证,电话号码,银行卡等) image.png

在Modal或者Drawer内使用Form组件管理状态时,关闭并不会清除表单的数据(编辑或新增时打开修改后关闭再打开),即使配置了destroyOnClose属性也不生效。常用解决方式:

前一种在于手动清空,后两种在于根据state(visible/open)变化清空

  1. 手动调用:在关闭时调form.resetFields(),借助Form组件的API清空状态(可能会冗余代码,如确认取消等操作成功后都要调用)
  2. 弹框afterClose/抽屉afterOpenChange 关闭后调用form.resetFields()
  3. 在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

image.png

这好用吗 用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不是啥都有

代码

  1. 状态值不要用props到处传,层级多就用redux等状态管理
  2. 经常会跳转到的页面里的状态最好是内部控制不要有props,需要的值用history.push({url,xxx})带过来
  3. 不写过多useState,相同类型的写一个useState({})
  4. 不写过多useEffect,业务逻辑清晰分离
  5. 恰当的用useRef代替useState,useRef除了用于获取dom和父调子通信,还会返回{current:xx}的对象
  6. 不用 arr.length && <>..</>, 0会显示在页面中
  7. 不要疯狂.then链式调用,async await用起来
  8. 还有些基础的数组map都用不明白的,非要用for循环
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,
    initParams: xxx
  });

  // 上拉加载更多
  const onScrollToLower = () => {
    getNextPageData();
  };
  return <>...</>
}

最后

相信经常上社区的jy们对于这些可能都不以为然,都懂,但是确实还是有那么多的这种代码存在,还有很多逻辑层的问题没有写出,也希望大家都能多提升自己的代码,谁也不想自己的代码被后面接手的人骂吧,也欢迎大家分享出更多的“经典案例”,还是那句话,不想着写的多么优雅NB,但要为了实用维护和自我提升努努力吧

说说别的,我也有一些代码洁癖,也会想着写出优雅的代码并被采纳,这个项目经常是改到某个地方可能看不下去就重构了。回想以前也写过很多垃圾代码,也有人说过我的代码问题。但是一定会改,后面每次写的新需求的时候也会想着较为好的编写方式,即便是自己能实现还是会百度看下别人的写法。以前我对前端初级中级高级的定义比较模糊,觉得除了代码还有很多方面的考量。但自从经历过招聘组员及CR组内人员的代码后,编程思维及开发思路都会在代码体现,所以很多时候代码便足以证明一切。

点点赞啦 end.

猜你喜欢

转载自juejin.im/post/7248431478241329213
今日推荐