封裝元件是為了能在開發過程中高度複用功能和樣式相似的元件,以便我們只關注於業務邏輯層的處理,提高開發效率,提高逼格,降低程式碼重複率,降低勞動時間,減少加班的可能。
本次元件的封裝採用了函式式元件即無狀態元件的方式來提高頁面渲染效能,由於無狀態元件在資料變更後不會主動觸發頁面的重新渲染,所以本次的封裝也用到了React Hooks。下面簡要介紹一下函式式元件和React Hooks。
函式式元件是被精簡成一個render方法的函式來實現的,由於是無狀態元件,所以無狀態元件就不會再有元件例項化的過程,無例項化過程也就不需要分配多餘的記憶體,從而效能得到一定的提升。但函式式元件是沒有this和ref的,如果強行給函式式元件新增ref會報一個Function components cannot have string refs. We recommend using useRef() instead.的錯誤。函式式元件也沒有生命週期,沒有所謂的componentWillMount、componentDidMount、componentWillReceiveProps、shouldComponentUpdate
等方法。
React Hooks是react16.8引入的特性,他允許你在不寫class的情況下操作state和react的其他特性。Hook就是JavaScript函式,但是使用它們會有兩個額外的規則:
-
只能在函式最外層呼叫Hook,不能在迴圈、條件判斷或者子函式中呼叫;
-
只能在React的函式元件中呼叫Hook,不能在其他JavaScript函式中呼叫(自定義Hook除外)。
React Hooks中用的比較頻繁的方法:
-
useState
-
useEffect
-
useContext
-
useReducer
-
useMemo
-
useRef
-
useImperativeHandle
由於以上方法的具體介紹及使用的篇幅過大,故請自行查閱API或資料,這裡不再展開描述。
另外,本次封裝一部分採用了JSX來建立虛擬dom節點,一部分採用了createElement方法來建立虛擬dom節點,createElement方法接收三個引數:
第一個引數:必填,可以是一個html標籤名稱字串如span、div、p等,也可以是一個react元件;
第二個引數:選填,建立的標籤或元件的屬性,用物件方式表示;
第三個引數:選填,建立的標籤或元件的子節點,可以是一個字串或一個元件,也可以是一個包含了字串或元件的陣列,還可以是一個採用createElement建立的虛擬dom節點。
createElement方法可能用的人不會很多,因為現在有了類似於html的JavaScript語法糖JSX,使用和理解起來也較為直觀和方便,符合我們對html結構的認知,但其實JSX被babel編譯後的呈現方式就是使用createElement方法建立的虛擬dom,至於為何使用createElement方法,私心覺得可以提升編譯打包效率。另外本次封裝元件時有些地方也使用了JSX,是覺得在那些地方使用JSX更舒服,而有些地方使用createElement方法私心也覺得更符合js的編寫習慣,如果你覺得在一個元件中既有JSX又有createElement會很亂的話,你可以統一使用一種即可。
本次封裝所使用到的方法的介紹基本完畢,以下是元件封裝的具體實現部分。
先貼一張最後實現的效果圖:
1、所封裝的antd table元件table.js
import React, { createElement, useState, useImperativeHandle } from 'react'
import PropTypes from 'prop-types'
import { Link } from "react-router-dom"
import { Table } from 'antd';
import { timestampToTime, currency } from '@/utils'
const h = createElement;
const TableComp = ({columns, dataSource, hasCheck, cRef, getCheckboxProps}) => {
const empty = '-',
[selectedRowKeys, setSelectedRowKeys ] = useState([]),
[selectedRows, setSelectedRows] = useState([]),
render = {
Default: v => {
if(!v) return empty
return v
},
Enum: (v, row, {Enum}) => {
if(!v || !Enum[v]) return empty
return Enum[v]
},
Loop: v => {
if(v.length < 1) return empty
return v.map((t, idx) => <span key={idx} style={{marginRight: '5px'}}>{t.total}</span>);
},
Action: (v, row, {action}) => {
let result = action.filter(n => {
let {filter = () => true} = n
return filter(row)
})
return result.map(c => <span className="table-link" onClick={() => c.click(row)} key={c.label}>{c.label}</span>)
},
Currency: v => {
if(!v) return empty
return currency(v)
},
Date: v => {
if(!v) return empty
return timestampToTime(v, 'second')
},
Link: (v, row, {url}) => {
if(!v) return empty
return <Link to={url}>{v}</Link>
}
}
columns = columns.map(n => {
let { type = 'Default', title, dataIndex, key } = n;
return {title, dataIndex, key, render: (v, row) => render[type](v, row, n) }
})
//父元件獲取selectedRowKeys的方法-cRef就是父元件傳過來的ref
useImperativeHandle(cRef, () => ({
//getSelectedRowKeys就是暴露給父元件的方法
getSelectedRowKeys: () => selectedRowKeys,
getSelectedRows: () => selectedRows
}));
const onSelectChange = (selectedRowKeys, selectedRows) => {
setSelectedRowKeys(selectedRowKeys);
setSelectedRows(selectedRows);
}
const rowSelection = {
selectedRowKeys,
onChange: onSelectChange,
getCheckboxProps: record => getCheckboxProps(record)
};
if(hasCheck) return h(Table, {columns, dataSource, rowSelection})
return h(Table, {columns, dataSource})
}
TableComp.propTypes = {
columns: PropTypes.array.isRequired, //表格頭部
dataSource: PropTypes.array.isRequired, //表格資料
hasCheck: PropTypes.bool, //表格行是否可選擇
cRef: PropTypes.object, //父元件傳過來的獲取元件例項物件或者是DOM物件
getCheckboxProps: PropTypes.func, //選擇框的預設屬性配置
}
export default TableComp
2、時間戳格式化timestampToTime.js
export const timestampToTime = (timestamp, type) => {
let date = new Date(timestamp); //時間戳為10位需*1000,時間戳為13位的話不需乘1000
let Y = date.getFullYear() + '-';
let M = (date.getMonth() + 1 < 10 ? '0' + (date.getMonth() + 1) : date.getMonth() + 1) + '-';
let D = date.getDate() < 10 ? '0' + date.getDate() + ' ' : date.getDate() + '';
let h = date.getHours() < 10 ? '0' + date.getHours() + ':' : date.getHours() + ':';
let m = date.getMinutes() < 10 ? '0' + date.getMinutes() + ':' : date.getMinutes() + ':';
let s = date.getSeconds() < 10 ? '0' + date.getSeconds() : date.getSeconds();
if (type == 'second') {
return Y + M + D + ' ' + h + m + s;
} else {
return Y + M + D
}
}
3、金額千分位currency.js
export const currency = v => {
let [n, d = []] = v.toString().split('.');
return [n.replace(/(\d)(?=(?:\d{3})+$)/g, '$1,')].concat(d).join('.');
};
簡單介紹下封裝的table元件,本元件基本涵蓋了大多數情況下我們所用到的表格元件的情況,包括:操作按鈕的許可權控制、某一列的跳轉、某一列的欄位對映以及金額千分位、時間戳格式化和某一列陣列資料的迴圈展示、表格是否可選擇等等。如還有其他需求,可自行新增即可。
4、table元件的使用方法:
import React, { useRef } from 'react'
import { Button } from 'antd'
import Table from './components'
const TableDemo = () => {
const permission = ["handle", "pass", "refuse", "reApply", 'export'], Enum = {
CREATED: '代辦理',
PASS: '待審批',
REJECT: '駁回',
REFUSE: '拒絕',
};
const columns = [
{ title: '姓名', dataIndex: 'name', key: 'name', type: 'Link', url: 'https://www.baidu.com' },
{ title: '年齡', dataIndex: 'age', key: 'age' },
{ title: '狀態', dataIndex: 'status', key: 'status', type: 'Enum', Enum, },
{ title: '預警統計', dataIndex: 'statistic', key: 'statistic', type: 'Loop'},
{ title: '存款', dataIndex: 'money', key: 'money', type: 'Currency' },
{ title: '日期', dataIndex: 'date', key: 'date', type: 'Date'},
{ title: '操作', dataIndex: 'action', key: 'action', type: 'Action', action: [
{label: "檢視", click: data => {console.log(data)}},
{label: "辦理", click: data => {}, filter: ({status}) => status == 'CREATED' && permission.some(n => n == 'handle')},
{label: "通過", click: data => {}, filter: ({status}) => status == 'PASS' && permission.some(n => n == 'pass')},
{label: "駁回", click: data => {}, filter: ({status}) => status == 'REJECT' && permission.some(n => n == 'reject')},
{label: "拒絕", click: data => {}, filter: ({status}) => status == 'CREATED' && permission.some(n => n == 'refuse')},
{label: "重新付款", click: data => {}, filter: ({status}) => status == 'REAPPLY' && permission.some(n => n == 'reApply')},
]},
]
const dataSource = [
{key: 1, name: '小壞', age: 20, status: 'CREATED', date: 1596791666000, statistic: [{level: 3, total: 5}, {level: 2, total: 7}, {level: 1, total: 20}, {level: 0, total: 0}], money: 200000000000},
{key: 2, name: 'tnnyang', age: 18, status: 'PASS', date: 1596791666000, statistic: [], money: 2568912357893},
{key: 3, name: 'xiaohuai', status: 'REJECT', statistic: [], money: 6235871},
{key: 4, name: '陳公子', age: 21, status: 'REAPPLY', date: 1596791666000, statistic: []},
]
const config = {
columns,
dataSource,
hasCheck: true, //是否顯示錶格第一列的checkbox核取方塊
getCheckboxProps: record => {return {disabled: record.status == 'REJECT'}} //table表格rowSelection的禁用
}
//點選獲取通過checkbox核取方塊選中的表格
const childRef = useRef();
const getTableChecked = () => {
const selectedRowKeys = childRef.current.getSelectedRowKeys(), selectedRows = childRef.current.getSelectedRows();
console.log(selectedRowKeys)
console.log(selectedRows)
}
return <div>
<Table {...config} cRef={childRef} />
<Button type="primary" onClick={getTableChecked}>獲取選擇的列表項</Button>
</div>
}
export default TableDemo
最後再貼一下本次封裝所用到的各個包的版本:
react: 16.8.6,
react-dom: 16.8.6,
react-router-dom: 5.0.0,
antd: 4.3.5,
@babel/core: 7.4.4,
babel-loader: 8.0.5
其實最主要的是react和antd的版本,其中antd4和antd3在table元件上的差異還是很大的,在其他元件上的差異也是很大的。