我們是袋鼠雲數棧 UED 團隊,致力於打造優秀的一站式數據中台產品。我們始終保持工匠精神,探索前端道路,為社區積累並傳播經驗價值。 相容性問題 第三方依賴相容問題 React - 最低 v16.9,部分組件使用 hooks 重構 react升級相關文檔 Less - 最低 v3.1.0,建議升級到 ...
我們是袋鼠雲數棧 UED 團隊,致力於打造優秀的一站式數據中台產品。我們始終保持工匠精神,探索前端道路,為社區積累並傳播經驗價值。
相容性問題
第三方依賴相容問題
- React - 最低 v16.9,部分組件使用 hooks 重構 react升級相關文檔
- 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 請完全剔除對該過渡包的依賴
升級步驟(只有一步)
1、@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文檔 中找。
-
下圖中是具體報錯的節點,可以看到 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
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 方法,
// antd v4
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
extra
<FormItem
label="過濾條件"
extra={
<Tooltip title={customSystemParams}>
系統參數配置
<QuestionCircleOutlined />
</Tooltip>
}
>
<Input.TextArea />
</FormItem>
Select
rc-select
底層重寫
- 解決些許歷史問題
- rc-select & rc-select-tree 的 inputValue & searchValue 之爭
rc-select-tree 是 rc-select 結合 tree 寫的一個組件,相似但又不同,searchValue 就是其中一點,也不是沒人提過 issue,只是人的忘性很大,時間長了就忘了,混了,導致在 rc-select 中甚至出現了 searchValue 的字樣 - inputValue 歷史問題,this.state.inputValue
也不是不想改,只是改了之後改出了一堆 bug,真真應了一句話,每一個歷史包袱的存在,都有他存在的原因,, - onSelect 清空了值,又會被 onChange 賦值回來
- rc-select & rc-select-tree 的 inputValue & searchValue 之爭
- 模塊復用
在新版的 rc-select
中,antd 官方抽取了一個 generator 方法。它主要接收一個 OptionList
的自定義組件用於渲染下拉框部分。這樣我們就可以直接復用選擇框部分的代碼,而自定義 Select 和 TreeSelect 對應的列表或者樹形結構了。
labelInValue
在 3.x 版本為
在 4.x 版本為
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');
}}
Tree
Tree 組件取消 value 屬性,現在只需要添加 key 屬性即可
特別註意, 此問題會導致功能出問題,需要重點關註!!!
在項目中經常在 TreeItem 中增加參數,如:<TreeItem value={value} data={data} >
。在拖拽等回調中就可以通過 nodeData.props.data
的方式獲取到 data 的值。
但在 antd4 中,獲取參數的數據結構發生了改變,原先直接通過 props 點出來的不行了。
有兩種方式取值
- 不使用props。直接採用 nodeData.data 的方式,也可以直接拿到
- 繼續使用 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 其中的步驟,以及團隊在實踐過程中發現的一些問題和對應的解決方案。