前言
不知不觉来北京已经两年半了,经手的项目有小程序,h5,pc端等等,当然主要还是以pc为主,作为个人来讲确实比较喜欢做pc端的项目,因为它相对于h5和小程序比较省心,不需要处理h5复杂的兼容性场景,也不需要处理小程序的一些认证过程,但这并不意味着pc端就很简单,一个好的h5前端往往有着很多强大的平台来支撑,例如CI,CD,数据监控等等,而作为pc端的好伙伴,antd的熟练掌握自然是必不可少的。
背景
我在使用中发现了很多antd自带的问题,还有是在特定业务场景下产生的问题,我把这些分享出来,做一个记录,以备不时之需。
问题汇总
公司使用的是版本3和版本4,这里会针对这两个版本来讨论
Mentions组件

在TextArea中回车需要发送接口请求,输入@之后提示框回车也会触发keyUp事件
解决办法:在onSelect中加入标识区分两个keyUp事件
            
            
              ini
              
              
            
          
          const [mentionSelect, setMentionSelect] = useState(false);
const onKeyUp = (e) => {
    if(e.keyCode === 13) {
        if(mentionSelect) {
            console.log('select------')
        } else {
            // 发送请求
            sendRequestXXX
        }
    }
}
const onSelect = () => {
    setMentionSelect(true)
}
<Mentions onKeyUp={onKeyUp} onSelect={onSelect}>
    <Mentions.Option value="sample">Sample</Mentions.Option>
</Mentions>
        但是这样的逻辑是有问题的:正常点击之后回车发送不了请求了(select事件用来拦截回车了)
所以只能在click的时候把mentionSelect重置为false。 但问题又来了,如果小于4.24版本 Option的click事件不生效。。。
我只是想加一个回车发送请求的功能,为啥介么复杂呀 这里其实有两个方案:
- 把版本升级到4.24,改用options
 
            
            
              ini
              
              
            
          
          <Mentions options={[{label: <div onClick={() => {
    setMentionSelect(false)
}}></div>}]}/>
        - 监听ant-mentions-active的点击事件
 
            
            
              scss
              
              
            
          
          useEffect(() => {
    document.querySelector('ant-mentions-active').addEventListener('click', () => {
        setMentionSelect(false)
    })
}, [])
        最后我选择了第一种,稳妥一些
如果提示项过多,上下按钮无法操作滚动
 这里提供三种思路:
- 监听滚动,计算scrollTop(这种办法兼容性好一些,但需要兼容到顶和到底的情况)
 
            
            
              ini
              
              
            
          
          let activeLi = document.getElementsByClassName('ant-mentions-dropdown-menu-item-active');
    if(activeLi.length > 0) {
      const scrollTop = document.getElementsByClassName('ant-mentions-dropdown-menu')[0].scrollTop;
      const offsetTop = document.getElementsByClassName('ant-mentions-dropdown-menu-item-active')[0].offsetTop;
      // 判断滚动到底了
      if(offsetTop - scrollTop > height) {
        document.getElementsByClassName('ant-mentions-dropdown-menu')[0].scrollTo({
          top: scrollTop + height
        })
      }
    }
        - scrollIntoView:调用它的元素滚动到浏览器窗口的可见区域
 
            
            
              dart
              
              
            
          
          document.querySelector('.ant-mentions-dropdown-menu-item-active')?.scrollIntoView({ block: 'nearest' })
        这里nearest代表保持接近边界
List To Tree
这其实是一个常见的需求
            
            
              ini
              
              
            
          
          let testConvertArray = [{
      name: 'aaa',
      id: 'a',
      parentkey: 0
    },
    {
      name: 'bbb',
      id: 'b',
      parentkey: 'a'
    },
    {
      name: 'ccc',
      id: 'c',
      parentkey: 'b'
}]
const transverse = (arr, rootId) => {
    return arr.filter((item) => {
        if(item.parentKey === rootId) {
            const tempItem = transverse(arr, item.key);
            item.children = tempItem.length > 0 ? tempItem : null;     
        }
        item.label = (
            <Link to={item.path}>
              {item.name}
            </Link>
        );
        return item.parentKey === rootId   
    });
}
return <Menu selectedKeys={[activeKey]} mode="horizontal" items={transverse(testConvertArray, 0)} />
        这里需要注意tempItem如果是空数组需要转成null,不然menu组件会出现小块空白情况
慎用onShowSizeChange
有这样一个需求,在切换pageSize的时候需要将pageIndex重置为1,但是 onChange方法又会将pageIndex设置回当前页面
            
            
              ini
              
              
            
          
          const onShowSizeChange = (current, size) => {
    setState((prev) => {
        prev.pageIndex = 1;
        prev.pageSize = size
    })
}
const changePage = (pageIndex, size) => { 
    setState((prev) => {
        prev.pageIndex = pageIndex; // 这里会将当前页设置回pageIndex
        prev.pageSize = size
    })
}
<Pagination
    total={state.total}
    onShowSizeChange={onShowSizeChange}
    onChange={changePage}
    pageSize={state.pageSize}
    current={state.pageIndex}
/>
        解决方法是去掉onShowSizeChange,改为以下的写法:
            
            
              ini
              
              
            
          
          const changePage = (pageIndex, size) => { 
    setState((prev) => {
        prev.pageIndex = state.pageSize !== size ? 1 : pageIndex; // 修改pageSize重置为第一页
        prev.pageSize = size
    })
}
        使用useWatch监听setFieldValue设置的最新值
场景:表单中多个选项之间相互关联影响,想要监听setFieldValue的最新值进行相应的逻辑操作
            
            
              scss
              
              
            
          
          const datasetValue = Form.useWatch('datasetId', form);
const onValuesChange = (changedValues, allValues) => {
    const { scene, datasetId } = changedValues;
    form.setFieldsValue({datasetId: 'aaa'})
}
useEffect(() => {
    if(datasetValue) {
        XXXX // 这里写相关的逻辑
    }
}, [datasetValue])
        这里不用onValuesChange的原因是:
- 可以让表单的值可以受变化项的控制,把对应的逻辑都写在一起,方便处理
 - onValuesChange无法监听到setFieldsValue设置的最新值
 
下拉框会随着body的滚动而滚动
getPopupContainer:在有下拉样式的组件中让下拉随容器滚动,这个容器默认指向body,可以修改成父容器,这样body滚动的时候下拉就不会随之滚动了
使用onCell回调确定Columns的dataIndex
在table表格中我们很容易区分是哪一行,但并不好区分到底是哪列,比如一个弹框的table点击某列之后需要将弹框关闭
            
            
              javascript
              
              
            
          
          <Table
  columns={[{dataIndex: 'aaa'}, {dataIndex: 'bbb'}].map((item) => {
      item.onCell={(record) => {
        return {
          dataIndex: item.dataIndex, // 这里能直接拿到
          onClick: (event) => {}, // 点击行
        };
      }}
  })}
/>
        如何在debounce中使用异步请求
常见的场景,在用户输入的时候通过接口校验用户名是否重复
            
            
              javascript
              
              
            
          
          export function debounceFn(fn, time) {
	let timer = null;
	return function(...args) {
		if(timer) {
			clearTimeout(timer);
		}
		return timer = setTimeout(() => {
      fn.call(null, ...args)
    }, time);
	}
}
const validateModelName = debounceFn(async (rule, value) => {
    const params = { modelName: value }
    const res = await modelNameCheck(params)
    if (res.result) {
      return Promise.reject('用户名称重复')
    } else {
      return Promise.resolve()
    }
  }, 500)
<Form.Item
  label={intl.getStr('common.name')}
  name="modelName"
  style={{ marginTop: 20 }}
  validateFirst
  rules={[{
    validator: validateModelName,
  }]}
>
  <Input />
</Form.Item>
        上面的代码不能起到debounce的效果,每次输入都会触发,原因是啥呢? 仔细观察,会发现promise链断了,因为async和await在经过debounceFn的包装之后返回的是一个正常的函数,而不是promise了,所以将debounceFn的返回结果Promise化就好了
            
            
              javascript
              
              
            
          
          export function debounceFn(fn, time) {
	let timer = null;
	return function(...args) {
		if(timer) {
			clearTimeout(timer);
		}
                // 这里进行promise化
		return new Promise((resolve) => {
			timer = setTimeout(() => {
				resolve(fn.call(null, ...args))
			}, time);
		});
	}
}
        关于Form.Item
在Form.Item中如果包含两个节点会导致setFieldValue设置失效,像这样:
            
            
              less
              
              
            
          
          <Form.Item name="userName">
    <span>Label:</span>
    <Input />
    <div>123</div>
</Form.Item>
        可以选择以下两种解法:
- 使用Row和Col去包裹Form.Item和div
 
            
            
              xml
              
              
            
          
              <Row>
      <Col><span>Label:</span></Col>
      <Col>
          <Form.Item name="userName">
              <Input />
          </Form.Item>
      </Col>
      <Col><div>123</div></Col>
    </Row>
        - 使用Form.Item的noStyle
 
            
            
              less
              
              
            
          
              <Form.Item noStyle>
        <span>Label:</span>
        <Form.Item name="userName">
            <Input />
        </Form.Item>
        <div>123</div>
    </Form.Item>
    <div>123</div>
    </Form.Item>
        noStyle加上formitem的shouleUpdate可以配合做表单项的动态渲染比较好
            
            
              typescript
              
              
            
          
              <Form.Item noStyle shouldUpdate={(prevValues, curValues) => prevValues.type !== curValues.type}>
    {({ getFieldValue }) => {
          const type = getFieldValue('type')
          if (type === 1) {
                <Form.Item name="userName">
                  <Select options={intervalTypeList} />
                </Form.Item>
          }
      }}
  </Form.Item>
        这里给大家一个建议,如果自己设计组件需要包裹在Form.Item中使用,像这样:
            
            
              xml
              
              
            
          
            <Form.Item name="aaa">
      <MyComp />
  </Form.Item>
        在MyComp中不要维护value状态和onChange事件,第一是如果组件需要拆分多层,层层传递会有遗漏,第二就是每层都有一个状态不好维护,最好是通过...restProps透传给下一层组件。
总结
其实这一篇比较偏向笔记的文章,对我来讲需要养成写文章总结的好习惯,因为antd的细节太多了,也希望大家多提一些宝贵的意见和建议,还有自己业务上遇到的场景也可以,这篇文章我也会持续更新下去,加油,继续搬砖去了!!!