Ant Design 除了標準元件外,還出了 Ant Design Pro 跟 ProCompoents 函式庫
Ant Design Pro 雖然功能強大,可是太龐大了,若非做一個大型系統沒必要用這麼大的框架,需要移除的設定可能比要寫的還多
所以最後選擇 ProCompoents 做為系統開發的主要元件庫,當然一些小元件還是使用 Ant Design 搭配使用
你可能會說怎麼不選全世界最多人用的 MUI 呢?一來是省錢二來則是 Ant Design 功能真得更強一些,省錢的部分是 MUI 也有推出 Pro 組件,不過這是付費功能,功能更強的部份則是 Ant Design 的 Table 組件只要改個設定就能做到 Inline Edit,而 MUI 則要使用 Grid 組件才有 Inline Edit
本文主要是做個紀錄,希望以後在使用 React + Ant Design 時能縮短開發時間,另外有關ProCompoents 的介紹真的太少,一些問題也能給大家做個參考
目前這個程式的功能不算多,主要有兩個頁面
第一個頁面用來呈現設定好的資料,資料庫存的是 foreign key,所以其他資訊需要關聯到另一個表格,API 返回的 JSON 大概如下
{
id: 1
master:{
id: 1,
name: 'abc'
},
sliver:{
id: 2,
name: 'def'
}
}
表格需要能將 master跟 sliver 的 name 呈現出來,且最後有個欄位可以刪除單筆資料
第二個頁面則是用來新增資料用的,會有兩個步驟,第一個挑選master(單選),第二個挑選sliver(複選),待資料送出後就能在第一個頁面查詢
下面就是最後的成果
新增的部分功能複雜很多,兩個步驟雖然都是同樣的組件,但須依參數顯示單選或複選,最後 submit 建立資料後還須將 Table 勾選的資料清空
ProCompoents 雖然幫我們組合了一堆元件,縮短開發時程,看起來很美好,但實際運行起來還是有些問題,尤其我是以後端開發為主,一些看似簡單的功能我也費了很大的功夫才搞出來,文章中介紹的內容有甚麼更好的方法也請讓我知道
ProCompoents 也是 Ant Design Pro 的底層的元件,算是加強版的 Ant Design,以 ProTable 來說,只要將 API 呼叫函式指定給 request 參數,網頁載入後就會自動獲取數據
const requestCompanyData = async () => {
return await fetch("[http://localhost/companies](http://localhost/plmcompanies)")
.then((response) => response.json())
.then((json) => {
return Promise.resolve(json);
});
};
<ProTable request={requestCompanyData} />
如上所示,只須把 fetch 函式指定給 request 參數,系統就能自動幫我們將資料渲染在網頁上
上圖是官網的一個範例,可以看到搜尋/分頁/編輯都能做到,不過當然許多功能都還得自己實做出來,UI 組件只是加快你完成 Layout 的部分,讓我們這些沒有美工底子的工程師也能做出還能看的網頁
不過幾個問題要特別注意
總之組合元件可以幫我們快速產生堪用的畫面,但要交付出去還是需要一些細部的調整
先說說如何拿掉表格的預設資訊吧,這裡的預設顯示簡體字真的蠻討厭的,也難怪老外用的人不多
pagination={{
showTotal: ()=>``,
pageSize: 10,
}}
如需調整顯示內容需要在 ProTable 中設定 pagination 屬性
pageSize 是每頁顯示的筆數,可自行調整
showTotal 則是總筆資訊的內容,需要放一個能返回字串的函式,若不想顯示,可以讓函式回傳空字串,若想顯示資料可以放一個模板字串,傳入兩個變數,第一個 total 用來顯示總筆數,第二個 range 是 array,表示目前頁面的區間,例如
showTotal: (total, range)=>`從${range[0]}到${range[1]},總共 ${total} 筆資料`
上圖就是 ProTable 沒資料時顯示的內容,只需要在 ProTable 增加下面設定就能改掉預設的文字
locale={{emptyText: '無數據'}}
其實更正確的做法是設定多國語系,不過只是一兩個地方想修改,而且沒有跨國的需求,所以就直接改屬性了,下面是改完後的顯示內容
有寫過視窗程式大概都很難理解切換Tab後還需要自己指定顯示的內容,不過網頁框架這部分就真的需要自己操作,Tab 跟顯示的內容基本上是兩個物件,點選Tab後的流程大致如下
來看看程式怎麼寫
下面是組件的渲染內容(就是return後的標籤)
<PageContainer
style={{
height: window.innerHeight,
width: '100%',
}}
tabList={[
{
tab: '清單1',
key: '/Tab1',
},
{
tab: '清單2',
key: '/Tab2',
},
]}
tabProps={{
type: 'card',
defaultActiveKey: "/",
}}
onTabChange={tabChangeHandle}
>
<Routes>
<Route path='/' element={<Page1 />} />
<Route path='/Tab1' element={<Page1 />} />
<Route path='/Tab2' element={<Page2 />} />
</Routes>
</PageContainer>
Key 是實際會跳轉的路徑,對應到 Route 的 Path,而 tabChangeHandle 就是使用 navigate 來切換路徑及顯示內容
const navigate = useNavigate();
const tabChangeHandle = (path: string) => navigate(path, { replace: false })
另外有一點要特別注意,Routes外面其實還要需要有 BrowserRouter 標籤,但因我在 App 中只載入 LandingPage,所以 BrowserRouter 需要放在 App 包裹整個 LandingPage
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<BrowserRouter>
<LandingPage />
</BrowserRouter>
</React.StrictMode>,
)
最後有個蠻討厭的地方,當我跳轉到某個頁面後若按下 F5,雖然該路徑的內容會更新,不過 Tab 卻會因為重新渲染而回到初始值,這部分我是使用 useEffect 來讓我按下 F5 後強制跳回初始路徑
useEffect(() => {
navigate("/", { replace: false })
},[]);
記得 useEffect 第二參數一定要給一個空陣列,這樣更新後就只會執行一次(第二個參數是監控變數,因為永遠為空不會發生變化,所以第一次執行後就不會再執行了),否則 State 一變動就會重新執行,變成無窮迴圈
若使用其他的 Navigator 或是 Manu 組件也都是差不多的做法,反正跳轉網頁就是使用 Router 來實作
對於熟悉 JavaScript 的前端工程師,把函式當成參數丟給其他組件執行是很理所當然的事,但對習慣物件導向語言的工程師來說卻很難理解,不過 React 有了 ref 後,也能取得組件的實例,可以用物件的方式來操作組件
const inputRef = useRef();
<Input ref = {inputRef }></Input>
透過上面的設定 inputRef 就能控制 Input 組件,例如要設定輸入框的內容或是將其Disable,都可透過 inputRef 來操作
另外我還透過下面的方式取得 ProTable 的 ref
<ProTable<CompanyDataType>
actionRef={actionRef}
透過 actionRef 能做的操作非常多,我目前在刪除以及搜尋時都會 reload 資料,只需呼叫actionRef.current?.reload() 就能重新執行 ProTable 的 request 函式
這裡是所有actionRef能呼叫的函式
但是自己寫的函式組件卻沒有 ref 參數,難道還得將 Function 當作參數丟給父組件控制嗎?
別怕,React 幫我們想到了,只要用 forwardRef 將函式組件封裝起來,就可以帶有 ref 參數
不過實際寫起來有點小複雜,下面是將組件增加 ref 的兩個步驟
原本的函式組件寫法
export default MyComponent: React.FC = (props: any) => {
forwardRef 封裝寫法
export default forwardRef((props: any, ref: any) => {
useImperativeHandle(ref, () => ({
cleanSelected() {
tableCleanSelected();
},
init(){
initAllData();
}
}));
完成以上動作就能用 ref 來操作 cleanSelected 跟 init 函式了,比較常見的場景是表單下有自己寫的組件,待表單 submit 後要清空數據,這時就能透過 ref 將子組件內容清空
refObj.current.cleanSelected()
如果JSON中包著其他物件,要在表格中顯示的值是物件裡的內容就需要特別指定
{
id: 1
obj1:{
id: 1,
name: abc
},
obj2:{
id: 2,
name: def
}
}
以上面的 JSON 為例,若我需要在表格第一個 Column 顯示 id,第二個 Coumn 顯示 obj1 的name,第三個 Column 顯示 obj2 的 name,設定如下
const columns: ProColumns<ObjType>[] = [
{
title: "ID",
key: "ID",
dataIndex: "id",
},
{
title: "物件1名稱",
key: "Obj1Name",
render: (_, row) => row?.obj1?.name,
},
{
title: "物件2名稱",
key: "Obj2Name",
render: (_, row) => row?.obj2?.name,
}]
如果只有一層的話 dataIndex 直接放上 field name 就能顯示內容,若是 Object,dataIndex 就無法指定了,需要自訂 render 顯示想要的內容
若是更進階的用法,例如在欄位上增加一個刪除的 Button,且按下後顯示是否刪除,可以參考下面寫法
{
title: 'Actions',
key: 'action',
render: (_, row) => (
<Popconfirm
title="是否刪除?"
onConfirm={
() => deleteDataById(String(row?.id))
}>
<Button key="delete">
刪除
</Button>
</Popconfirm>
)
},
由於新增頁面下面是我們自己寫的組件,需要在勾選後執行 props 傳入的函式,這樣父組件才能取得勾選的資料,在 ProTable 可透過下面方式自動觸發函式
<ProTable<CompanyDataType>
actionRef={actionRef}
columns={columns}
rowSelection={{
type: rowSelectType,
onChange(selectedRowKeys,selectedRows) {
console.log("selectedRows", selectedRows)
rowSelectType === 'radio' ? setMaster(selectedRows[0]) : setAvls(selectedRows)
},
}}
tableAlertRender={({ onCleanSelected }) => {
tableCleanSelected = onCleanSelected; //將清空函式掛載出去,之後透過useImperativeHandle以及forwardRef讓父階組件在送出資料後可以清除選項
return false;
}}
request={requestPlmCompanyData} //fetch包裝函式
其中 rowSelecttion 會在勾選後呼叫 onChange,這裡要特別注意參數的順序,第一個 key 雖然沒用到,還是要放個變數,不然只會取到 key 而不是物件
tableAlertRender 則會在勾選資料後於表格上方出現想顯示的結果,例如取消選擇以及已選擇的筆數,若不想顯示可以直接 return false,其實整個 tableAlertRender 都不寫也行,但這裏我們可以取得 onCleanSelected 函式,呼叫就能清空選擇,只要將函式掛在 useImperativeHandle 下就能讓父組件控制清空選擇內容
開發時我們可以透過 console.log 來查看變數,但正式上線總不能要使用者也按 F12 查看 log 吧,這時一些重要的訊息就需要透過 Message 顯示給用戶
原以為直接呼叫函式即可
message.success(’123')
畫面上也順利跳出成功的 message,可是看一下 log 卻出現一個警告
具體原因我就不特別說了,總之要避免警告訊息需要做以下幾個步驟
在需要顯示訊息的組件使用 useMessage 取得函式及一個掛載變數 contextHolder
const [messageApi, contextHolder] = message.useMessage();
將 contextHolder 變數放在渲染得根節點內
return (
<>
{contextHolder}
<Row>
<Col span="8" offset="12">
<Search>
之後呼叫就不會有錯誤了
messageApi.info("這是一般訊息")
messageApi.error("這是錯誤訊息")
messageApi.warning("這是警告訊息")
messageApi.loading("這是載入中訊息")