前言
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: 1master:{id: 1,name: 'abc'},sliver:{id: 2,name: 'def'}}
表格需要能将 master跟 sliver 的 name 呈现出来,且最后有个栏位可以删除单笔资料
第二个页面则是用来新增资料用的,会有两个步骤,第一个挑选master(单选),第二个挑选sliver(複选),待资料送出后就能在第一个页面查询
下面就是最后的成果
新增的部分功能複杂很多,两个步骤虽然都是同样的组件,但须依参数显示单选或複选,最后 submit 建立资料后还须将 Table 勾选的资料清空
ProCompoents 看起来功能强大,但…
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 的部分,让我们这些没有美工底子的工程师也能做出还能看的网页
不过几个问题要特别注意
API 产生的 JSON 格式需包含以下几个 fieldsdata: 搜寻的资料必须以Array的形式放在data下success: 成功要传回true,否则资料会显示异常,若是后端资料有问题需传回falsetotal: 若要系统自动帮你分页,这里就要由后端抛出总笔数fetch 若传回失败并非后端伺服器错误,而是比较底层的错误,例如网路异常,此时可丢出网站更新中或是网路繁忙中的错误提示,而不是直接显示一个 500 错误有注意到底部分页前的文字吗?没错,这是简体字,而且没有参数能直接修改,后面在教大家如何调整(没资料时也显示简体)总之组合元件可以帮我们快速产生堪用的画面,但要交付出去还是需要一些细部的调整
简体字的调整
先说说如何拿掉表格的预设资讯吧,这里的预设显示简体字真的蛮讨厌的,也难怪老外用的人不多
1. 修改 ProTable 总笔数资讯
pagination={{showTotal: ()=>``,pageSize: 10,}}
如需调整显示内容需要在 ProTable 中设定 pagination 属性
pageSize 是每页显示的笔数,可自行调整
showTotal 则是总笔资讯的内容,需要放一个能返回字串的函式,若不想显示,可以让函式回传空字串,若想显示资料可以放一个模板字串,传入两个变数,第一个 total 用来显示总笔数,第二个 range 是 array,表示目前页面的区间,例如
showTotal: (total, range)=>`从${range[0]}到${range[1]},总共 ${total} 笔资料`
2. 置换没资料时的显示内容
上图就是 ProTable 没资料时显示的内容,只需要在 ProTable 增加下面设定就能改掉预设的文字
locale={{emptyText: '无数据'}}
其实更正确的做法是设定多国语系,不过只是一两个地方想修改,而且没有跨国的需求,所以就直接改属性了,下面是改完后的显示内容
PageContainer 的迷失
有写过视窗程式大概都很难理解切换Tab后还需要自己指定显示的内容,不过网页框架这部分就真的需要自己操作,Tab 跟显示的内容基本上是两个物件,点选Tab后的流程大致如下
切换 Tab 后触发 onTabChange 事件onTabChange 中可取得不同 Tab 的 Key,需自己判断不同的 Key 该产生甚么内容切换不同内容在前端框架几乎都是搭配 Router 来实作(除非你想自己用DOM换掉内容),我们可以把 Key 设成要跳转的 Route path,这样在 onTabChange 中就能直接使用 navigate 切换内容来看看程式怎么写
下面是组件的渲染内容(就是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 来实作
习惯物件导向程式会喜欢用 ref
对于熟悉 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 当作参数丢给父组件控制吗?
透过forwardRef所有的函式组件都能有ref
别怕,React 帮我们想到了,只要用 forwardRef 将函式组件封装起来,就可以带有 ref 参数
不过实际写起来有点小複杂,下面是将组件增加 ref 的两个步骤
将函式组件使用 forwardRef 封装原本的函式组件写法
export default MyComponent: React.FC = (props: any) => {
forwardRef 封装写法
export default forwardRef((props: any, ref: any) => {
公开函式需加进useImperativeHandle下useImperativeHandle(ref, () => ({cleanSelected() {tableCleanSelected();},init(){initAllData();}}));
完成以上动作就能用 ref 来操作 cleanSelected 跟 init 函式了,比较常见的场景是表单下有自己写的组件,待表单 submit 后要清空数据,这时就能透过 ref 将子组件内容清空
refObj.current.cleanSelected()
ProTable 中需要特别注意的部分
如果JSON中包着其他物件,要在表格中显示的值是物件里的内容就需要特别指定
{id: 1obj1:{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 下就能让父组件控制清空选择内容
使用率最高的组件 Message
开发时我们可以透过 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("这是载入中讯息")