袋鼠云数栈从 2016 年公布第⼀个版本开始,就始终保持着以技术为核⼼、平安为底线、提效为⽬标、中台为策略的思维,坚韧不拔地⾛国产化信创路线,一直推动产品性能迭代、技术创新、服务细化和性能降级。
在数栈过来的产品迭代中受限于以后组件的版本,积攒了很多待解决的问题,随着新的性能需要一直减少,很多原先的组件以及交互设计须要进行优化。
2 月,随同着数栈 UI5.0 的焕新降级,数栈前端团队一起将组件框架 antd 从 v3.x 降级到了 v4.x,更新组件的 UI,晋升产品的交互体验,使数栈产品可能更加灵便地适应将来产品性能迭代的需要。
本文将总结演绎袋鼠云数栈前端框架 Antd 从 3.x 降级到 4.x 的相干步骤,及在这个过程中踩过的坑,解决的问题。
兼容性问题
第三方依赖兼容问题
· React – 最低 v16.9,局部组件应用 hooks 重构(react 降级相干文档:https://sourl.cn/6bM4Ep)
· Less – 最低 v3.1.0,倡议降级到 less 4.x;
· @ant-design/icons-antd – 不再内置 Icon 组件,请应用独立的包。
对 3.x 的兼容性解决
或者是思考到局部组件降级的破坏性,antd4.x 中仍然保留了对 3.x 版本的兼容,废除的组件通过 @ant-design/compatible 放弃兼容,例如 Icon、Form。
留神:倡议 @ant-design/compatible 仅在降级过程中稍作依赖,降级 4.x 请齐全剔除对该过渡包的依赖。
降级步骤
只有一步
@ant-design/codemod-v4 自带降级脚本,会主动替换代码。
# 通过 npx 间接运行
npx -p @ant-design/codemod-v4 antd4-codemod apps/xxxx
# 或者全局装置
# 应用 npm
npm i -g @ant-design/codemod-v4
# 或者应用 yarn
yarn global add @ant-design/codemod-v4
# 运行
antd4-codemod src
留神:该命令和脚本只会进行代码替换,不会进行 AntD 的版本升级,须要手动将其降级至 4.22.5。
该命令实现的工作:1. 将 Form 与 Mention 组件通过 @ant-design/compatible 包引入
2. 用新的 @ant-design/icons 替换字符串类型的 icon 属性值
3. 将 Icon 组件 + type =“”通过 @ant-design/icons 引入
4. 将 v3 LocaleProvider 组件转换成 v4 ConfigProvider 组件
5. 将 Modal.method() 中字符串 icon 属性的调用转换成从 @ant-design/icons 中引入
antd4-codemod
上图这类报错是 Icon 组件主动替换谬误,有 2 种解决形式:
· 报错文件的 Icon 比拟少的状况,能够间接手动替换该文件中的 Icon 组件,具体替换成 Icon 中的哪个组件能够依据 type 在 Icon 文档中找(Icon 文档:https://sourl.cn/neHBVS);
· 下图中是具体报错的节点,能够看到 JSXSpreadAttribute 节点也就是拓展运算符中没有 name 属性,所以把 Icon 组件的拓展运算符改一下再执行替换脚本就能够了。
antd4 问题修复
styled-components
styled-components 依赖须要转换写法。
Icon
不要应用兼容包的 icon。
在 3.x 版本中,Icon 会全量引入所有 svg 图标文件,减少了打包产物;
在 4.x 版本中,对 Icon 进行了按需加载,将每个 svg 封装成一个组件。
留神:antd 不再内置 Icon 组件,请应用独立的包 @ant-design/icons。
· 应用
import {Icon} from 'antd';
mport {SmileOutlined} from '@ant-design/icons';
const Demo = () => (
<div>
<Icon type="smile" />
<SmileOutlined />
<Button icon={<SmileOutlined />} />
</div>
);
· 兼容
import {Icon} from '@ant-design/compatible';
const Demo = () => (
<div>
<Icon type="smile" />
<Button icon="smile" />
</div>
);
Form
antd Form 从 v3 到 v4:https://sourl.cn/7TiRfp
● Form.create()
在 3.x 中,表单中任意一项的批改,都会导致 Form.create() 包裹的表单从新渲染,造成性能耗费;
在 4.x 中,Form.create() 不再应用。
如果须要应用 form 的 api,例如 setFieldsValue 等,须要通过 Form.useForm() 创立 Form 实体进行操作。
· 函数组件写法
// antd v4
const Demo = () => {const [form] = Form.useForm();
React.useEffect(() => {
form.setFieldsValue({username: 'Bamboo',});
}, []);
return (<Form form={form} {...props}> ... </Form>
)
};
· 如果是 class component,也能够通过 ref 获取
class Demo extends React.Component {formRef = React.createRef();
componentDidMount() {
this.formRef.current.setFieldsValue({username: 'Bamboo',});
}
render() {
return (<Form ref={this.formRef}>
<Form.Item name="username" rules={[{required: true}]}>
<Input />
</Form.Item>
</Form>
);
}
}
当咱们应用 From.create() 的时候,可能会传入参数,做数据处理,例如:
export const FilterForm: any = Form.create<Props>({onValuesChange: (props, changedValues, allValues) => {const { onChange} = props;
onChange(allValues);
},
})(Filter);
因为 Form.create 的删除,须要放到 < Form> 中。
<Form
ref={this.formRef}
layout="vertical"
className="meta_form"
onValuesChange={(_, allValues) => {const { onChange} = this.props;
onChange(allValues);
}}
>
● getFieldDecorator
在 4.x 中,不在须要 getFieldDecorator 对 Item 进行包裹。留神以下问题:
· 将之前写在 getFieldDecorator 中的 name/rules 等移到属性中;
· 初始化在 form 中解决,防止同名字段抵触问题;
· 对于表单联动的问题,官网提供了 shouldUpdate 办法。
const Demo = () => (<Form initialValues={{ username: 'yuwan'}}>
<Form.Item name="username" rules={[{required: true}]}>
<Input />
</Form.Item>
</Form>
);
● initialValue
· 历史问题
initialValue 从字面意来看,就是初始值 defaultValue,然而可能会有局部同学应用他的时候会误以为 initialValue 等同于 value。造成这样的误会是因为在 3.x 的版本中,始终存在一个很神奇的问题,受控组件的值会追随 initialValue 扭转。
看上面的例子,点击 button 批改 username, input 框的 value 也会随之扭转。
const Demo = ({form: { getFieldDecorator} }) => (const [username, setUsername] = useState('');
const handleValueChange = () => {setUsername('yuwan');
}
return (
<Fragment>
<Form>
<Form.Item>
{getFieldDecorator('username', {
initialValue: username,
rules: [{required: true}],
})(<Input />)}
</Form.Item>
</Form>
<Button onClick={handleValueChange}>Change</Button>
</Fragment>
)
);
const WrappedDemo = Form.create()(Demo);
但当 input 框被编辑过,initialValue 和 input 的绑定成果就隐没了,正确的做法应该是通过 setFieldsVlaue 办法去 set 值。
· 4.x 版本的 initialValue
在 4.x,antd 团队曾经把这个 bug 给解了,并且一是为了 name 重名问题,二是再次强调其初始值的性能,当初提到 Form 中了。当然,如果持续写在 Form. Item 中也是能够的,但须要留神优先级。
● shouldUpdate
后面有说过,form 表单不再会因为表单外部某个值的扭转而从新渲染整个构造,而设有 shouldUpdate 为 true 的 Item,任意变动都会使该 Form. Item 从新渲染。
它会接管 render props,从而容许你对此进行管制。这里略微留神一下,请勿在设置 shouldUpdate 的外层 Form. Item 上增加 name,否则,你会失去一个 error。
<Form.Item shouldUpdate={(prev, next) => prev.name !== next.name}>
{form => form.getFieldValue('name') === 'antd' && (
<Form.Item name="version">
<Input />
</Form.Item>
)}
</Form.Item>
在应用 shouldUpdate 的时候,须要在第一个 Form.Item 上加上 noStyle,否则就会呈现上面这种留白占位的状况。
● validateTrigger
onBlur 时不再批改选中值,且返回 React 原生的 event 对象。如果你在应用兼容包的 Form 且配置了 validateTrigger 为 onBlur,请改至 onChange 以做兼容。
● validator
在 antd3 时,咱们应用 callback 返回报错。然而 antd4 对此做了批改,自定义校验,接管 Promise 作为返回值。示例参考如下:
· antd3 的写法
<FormItem label="具体工夫" {...formItemLayout}>
{getFieldDecorator('specificTime', {
rules: [
{
required: true,
validator: (_, value, callback) => {if (!value || !value.hour || !value.min) {return callback('具体工夫不可为空');
}
callback();},
},
],
})(<SpecificTime />)}
</FormItem>
· antd4 的写法
<FormItem
label="具体工夫"
{...formItemLayout}
name="specificTime"
rules={[
{
required: true,
validator: (_, value) => {if (!value || !value.hour || !value.min) {return Promise.reject('具体工夫不可为空');
}
return Promise.resolve();},
},
]}
>
<SpecificTime />)
</FormItem>
● validateFields
不再反对 callback,该办法会间接返回一个 Promise,能够通过 then / catch 解决。
this.formRef.validateFields()
.then((values) => {onOk({ ...values, id: appInfo.id || ''});
})
.catch(({errorFields}) {this.formRef.scrollToField(errorFields[0].name);
})
或者应用 async/await。
try {const values = await validateFields();
} catch ({errorFields}) {scrollToField(errorFields[0].name);
}
● validateFieldsAndScroll
该 api 被拆分了,将其拆分为更为独立的 scrollToField 办法。
onFinishFailed = ({errorFields}) => {form.scrollToField(errorFields[0].name);
};
● form.name
在 antd 3.x 版本,绑定字段时,能够采纳 . 宰割的形式。如:
getFieldDecorator('sideTableParam.primaryKey')
getFieldDecorator('sideTableParam.primaryValue')
getFieldDecorator('sideTableParam.primaryName')
在最终获取 values 时,antd 3.x 的版本会对字段进行汇总,失去如下:
const values = {
sideTableParam: {
primaryKey: xxx,
primaryValue: xxx,
primaryName: xxx,
}
}
而在 antd 4.x 下,会失去如下的 values 后果:
const values = {
'sideTableParam.primaryKey': xxx,
'sideTableParam.primaryValue': xxx,
'sideTableParam.primaryName': xxx,
}
· 解决办法
在 antd 4.x 版本传入数组。
name={['sideTableParam', 'primaryKey']}
name={['sideTableParam', 'primaryValue']}
name={['sideTableParam', 'primaryName']}
应用 setFieldsValue 设置值:
setFieldsValue({
sideTableParam: [
{
primaryKey: 'xxx',
primaryValue: 'xxx',
primaryName: 'xxx',
},
],
});
当咱们应用 name={[‘sideTableParam’, ‘primaryKey’]} 形式绑定值的时候,与其关联的 dependencies/getFieldValue 都须要设置为 [‘sideTableParam’, ‘primaryKey’],例如:
<FormItem dependencies={[['alert', 'sendTypeList']]} noStyle>
{({getFieldValue}) => {const isShowWebHook = getFieldValue(['alert', 'sendTypeList'])?.includes(ALARM_TYPE.DING);
return (
isShowWebHook &&
RenderFormItem({
item: {
label: 'WebHook',
key: ['alert', 'dingWebhook'],
component: <Input placeholder="请输出 WebHook 地址" />,
rules: [
{
required: true,
message: 'WebHook 地址为必填项',
},
],
initialValue: taskInfo?.alert?.dingWebhook || '',
},
})
);
}}
</FormItem>
当咱们心愿通过 validateFields 拿到的数据是数组时,例如这样:
咱们能够设置为这样:
const formItems = keys.map((k: React.Key) => (<Form.Item key={k} required label="名称">
<Form.Item
noStyle
name={['names', k]}
rules={[{ required: true, message: '请输出标签名称'},
{validator: utils.validateInputText(2, 20) },
]}
>
<Input placeholder="请输出标签名称" style={{width: '90%', marginRight: 8}} />
</Form.Item>
<i className="iconfont iconicon_deletecata" onClick={() => this.removeNewTag(k)} />
</Form.Item>
));
● Tooltip
Form 反对属性 tooltip,可能在 label 后产生一个问号间接做提醒。
● extra
针对于想搁置于组件上面内容能够应用 extra 来实现。
<FormItem
label="过滤条件"
extra={<Tooltip title={customSystemParams}>
零碎参数配置
<QuestionCircleOutlined />
</Tooltip>
}
>
<Input.TextArea />
</FormItem>
● Form 在数栈的变动
通过这次 UI 降级和 antd 降级之后,Form 表单在数栈中的利用产生了较大的变动,从老版本的 label/component 横向排版改为了纵向改版,在横向空间不⾜的状况下,使⽤高低构造能无效提⾼填写表单的效率。
Select
● rc-select
· 底层重写
• 解决些许历史问题
1)rc-select & rc-select-tree 的 inputValue & searchValue 之争。rc-select-tree 是 rc-select 联合 tree 写的一个组件,类似但又不同,searchValue 就是其中一点,也不是没人提过 issue,只是人的记性很大,工夫长了就忘了、混同了,导致在 rc-select 中甚至呈现了 searchValue 的字样。
2)inputValue 历史问题,this.state.inputValue。
3)onSelect 清空了值,又会被 onChange 赋值回来。
• 模块复用
在新版的 rc-select 中,antd 官网抽取了一个 generator 办法。它次要接管一个 OptionList 的自定义组件用于渲染下拉框局部。这样咱们就能够间接复用抉择框局部的代码,而自定义 Select 和 TreeSelect 对应的列表或者树形构造了。
● labelInValue
在 3.x 版本为 {key: string, label: ReactNode}
在 4.x 版本为 {value: string, label: ReactNode}
Table
● fixed
固定列时,文字过长导致错位的问题,被完满解决了。
· 历史起因
3.x 中对 table fixed 的实现,是写了两个 table, 顶层 fixed 的是一个,底层滚动的是一个,这样呈现这种错位的问题就很好了解了。
要解决也不是没有方法,能够在特定的节点去测算表格列的高度,然而这个行为会导致重排,影响性能问题。
· 解决方案
4.x 中,table fixed 不在通过两个 table 来实现,他应用了一个 position 的新个性:position: sticky;
元素依据失常文档流进行定位,而后绝对它的最近滚动先人(nearest scrolling ancestor)和 containing block (最近块级先人 nearest block-level ancestor),包含 table-related 元素,基于 top、right、bottom 和 left 的值进行偏移,偏移值不会影响任何其余元素的地位。
长处:
• 依据失常文档流进行定位
• 绝对最近滚动先人 & 最近块级先人进行偏移
毛病:
• 不兼容 <= IE11
解决了应用 absolute | fixed 脱离文档流无奈撑开高度的问题,也不再须要对高度进行测量。
● table.checkbox
· 问题形容
降级后,checkbox 宽度被挤压了。
· 解决方案
通过在 rowSelection 中设置 columnWidth 和 fixed 解决。
const rowSelection = {
fixed: true,
columnWidth: 45,
selectedRowKeys,
onChange: this.onSelectChange,
};
● 渲染条件
antd4 Table 对渲染条件进行了优化,对 props 进行“浅比拟”,如果没有变动不会触发 render。
● 类名更改
.ant-table-content 更改为 .ant-table-container
.ant-form-explain 更改为 .ant-form-item-explain
● dataIndex 批改
在 antd3.0 的时候,咱们采纳 user.userName 可能读到嵌套的属性。
{
title: '账号',
dataIndex: 'user.userName',
key: 'userName',
width: 200,
}
antd4.0 对此做了批改,同 Form 的 name。
{
title: '账号',
dataIndex: ['user', 'userName'],
key: 'userName',
width: 200,
}
● table pagination showSizeChanger
· 问题形容
降级 antd4 后,发现一些表格分页器多了 pageSize 切换的性能,代码中 onChange 又未对 size 做解决,会导致底局部页器 pageSize 和数据对不上,因而须要各自排查 Table 的 pagination 和 Pagination 组件,和申请列表接口的参数。
<Table
rowKey="userId"
pagination={{
total: users.totalCount,
defaultPageSize: 10,
}}
onChange={this.handleTableChange}
style={{height: tableScrollHeight}}
loading={this.state.loading}
columns={this.initColumns()}
dataSource={users.data}
scroll={{x: 1100, y: tableScrollHeight}}
/>
handleTableChange = (pagination: any) => {
this.setState(
{current: pagination.current,},
this.search
);
};
search = (projectId?: any) => {const { name, current} = this.state;
const {project} = this.props;
const params: any = {
projectId: projectId || project.id,
pageSize: 10,
currentPage: current || 1,
name: name || undefined,
removeAdmin: true,
};
this.loadUsers(params);
};
antd4.0 对此做了批改,同 Form 的 name。
<Table
rowKey="userId"
pagination={{showTotal: (total) => ` 共 ${total} 条 `,
total: users.totalCount,
current,
pageSize,
}}
onChange={this.handleTableChange}
style={{height: tableScrollHeight}}
loading={this.state.loading}
columns={this.initColumns()}
dataSource={users.data}
scroll={{x: 1100, y: tableScrollHeight}}
/>
handleTableChange = (pagination: any) => {
this.setState(
{
current: pagination.current,
pageSize: pagination.pageSize,
},
this.search
);
};
search = (projectId?: any) => {const { name, current, pageSize} = this.state;
const {project} = this.props;
const params: any = {
projectId: projectId || project.id,
pageSize,
currentPage: current || 1,
name: name || undefined,
removeAdmin: true,
};
this.loadUsers(params);
};
另外,一些同学在 Table 中既写了 onChange,也写了 onShowSizeChange,这个时候要留神,当切换页码条数的时候两个办法都会触发,onShowSizeChange 先触发,onChange 后触发,这个时候如果 onChange 内未对 pageSize 做解决可能导致切页失败,看上面代码就明确了,写的时候略微留神一下即可。
● table sorter columnKey
· 问题形容
表格中如果要对表格某一字段进行排序须要在 columns item 里设置 sorter 字段,而后在 onChange 里拿到 sorter 对象进行参数解决,再申请数据。
须要留神的是,很多用到了 sorter.columnKey 来进行判断,容易呈现问题,sorter.columnKey === columns item.key,如果未设置 key,那么获取到的 columnKey 就为空,导致搜寻生效,要么设置 key,再进行获取。同理,sorter.field === columns item.dataIndex,设置 dataIndex,通过 sorter.field 进行获取,两者都能够。
columns={
[
{
title: '创立工夫',
dataIndex: 'gmtCreate1',
key: 'aa',
sorter: true,
render(n: any, record: any) {return DateTime.formatDateTime(record.gmtCreate);
}
},
...
]
}
onChange={(pagination: any, filters: any, sorter: any) {console.log(pagination, '--pagination');
console.log(filters, '--filters');
console.log(sorter, '--sorter');
}}
● Table 在数栈的变动
通过这次 UI 降级和 antd 降级之后,表格在数栈中的利用产生了较大的变动,减⼩了表格默认⾼度,减少⼀屏可浏览的数据数量。
Tree
Tree 组件勾销 value 属性,当初只须要增加 key 属性即可。
特地留神,此问题会导致性能出问题,须要重点关注!!!
在我的项目中常常在 TreeItem 中减少参数,如:< TreeItem value={value} data={data} >。在拖拽等回调中就能够通过 nodeData.props.data 的形式获取到 data 的值。但在 antd4 中,获取参数的数据结构产生了扭转,原先间接通过 props 点进去的不行了。
· 有两种形式取值:
1)不应用 props。间接采纳 nodeData.data 的形式,也能够间接拿到。
2)持续应用 props。在 antd4 中,还是能够通过 props 找到参数,只不过 antd 会把所有参数应用 data 进行包裹,就须要改成 nodeData.props.data.data。
· 新版数据结构如下:
· drag
拖拽节点地位的确定与 3.x 相比进行了变更,官网并没有阐明。具体如下图:
左侧为 3.x,右侧为 4.x。
在 3.x 版本,只有把节点拖拽成指标节点的上中下,即代表着指标节点的同级上方,子集,同级下方;
在 4.x 版本,是依据以后拖拽节点与指标节点的绝对地位进行确定最终的拖拽后果。
当拖拽节点处于指标节点的下方,且绝对左侧对齐的地位趋近于零,则最终的地位为指标节点的同级下方。
当拖拽节点处于指标节点的下方,且绝对左侧一个缩近的地位,则最终的地位为指标节点的子集。
当拖拽节点处于指标节点的上方,且绝对左侧对齐的地位趋近于零,则最终的地位为指标节点的同级上方。
Pagination
Pagination 自 4.1.0 版本起,会默认将 showSizeChanger 参数设置为 true,因此在数据条数超过 50 时,pageSize 切换器会默认显示。这个变动同样实用于 Table 组件,可通过 showSizeChanger: false 敞开。
如果 size 属性值为 small,则删除 size 属性。
Drawer
当咱们在 Drawer 上 设置了 getContainer={false} 属性之后,Drawer 会增加上 .ant-drawer-inline 的类名导致咱们 position: fixed 生效。
Button
在 antd 3.0 中危险按钮采纳 type。
应用如下:波及改变点 type、dangr 属性。
Tabs
使标签页不被选中。
// 3.x
activeKey={undefined}
// 4.x
activeKey={null}
总结
该篇文章具体解说了数栈前端团队如何从 antd3 降级到 antd4 的具体步骤,以及团队在实际过程中发现的一些问题和对应的解决方案,为后续产品的开发体验打了根底,也提供了一种更好的交互体验。
将来数栈前端团队将会继续关注产品体验以及开发中的技术痛点,以开发更好的产品为导向,助力业务倒退。
《数据治理行业实际白皮书》下载地址:https://fs80.cn/380a4b
想理解或征询更多无关袋鼠云大数据产品、行业解决方案、客户案例的敌人,浏览袋鼠云官网:https://www.dtstack.com/?src=szsf
同时,欢送对大数据开源我的项目有趣味的同学退出「袋鼠云开源框架钉钉技术 qun」,交换最新开源技术信息,qun 号码:30537511,我的项目地址:https://github.com/DTStack