前言
作為一個(gè)前端最常遇見(jiàn)的需求場(chǎng)景就是寫表單、寫表格。寫多了會(huì)逐漸的積累一些開發(fā)心得,此文章根據(jù)我使用vue和react的經(jīng)驗(yàn)記錄了一些東西,拋磚引玉的給大家看看。
功能的實(shí)現(xiàn)盡量先看第三方組件庫(kù)上是否已經(jīng)提供
舉例react項(xiàng)目,在做表單的很多時(shí)候,我都是從antd上把其中一個(gè)form組件例子復(fù)制下來(lái),然后再看看提供了哪些api就開干了。
這樣做其實(shí)會(huì)忽略很多細(xì)節(jié)方面的東西。
比如我想在rules校驗(yàn)的時(shí)候拿到其他item的值,可能會(huì)通過(guò)form的實(shí)例調(diào)用getFieldValue
,但其實(shí)在rules的使用中已經(jīng)默認(rèn)傳入了該方法:
rules={[
({ getFieldValue }) => ({
validator(_, value) {
// 可以通過(guò)getFieldValue('password')獲取對(duì)應(yīng)其他item的值
},
}),
]}
類似的例子還有很多,所以有些需要的功能其實(shí)你細(xì)看整個(gè)form組件的頁(yè)面說(shuō)明后,說(shuō)不定就給你找到了你想要的。
這里提供一個(gè)注冊(cè)表單的例子,里面很多小細(xì)節(jié)可以注意下,沒(méi)準(zhǔn)你還有新發(fā)現(xiàn):
<Form labelCol={{ span: 6 }} wrapperCol={{ span: 16 }} onFinish={onFinish}>
<Form.Item
label="用戶名"
name="username"
rules={[
{ required: true, message: '請(qǐng)輸入用戶名' },
{ type: 'string', min: 5, max: 20, message: '字符長(zhǎng)度在 5-20 之間' },
{ pattern: /^\w+$/, message: '只能是字母數(shù)字下劃線' },
]}
>
<Input />
</Form.Item>
<Form.Item
label="密碼"
name="password"
rules={[{ required: true, message: '請(qǐng)輸入密碼' }]}
>
<Input.Password />
</Form.Item>
<Form.Item
label="確認(rèn)密碼"
name="confirm"
dependencies={['password']} // 依賴于 password ,password 變化,會(huì)重新觸發(fā) validator
rules={[
{ required: true, message: '請(qǐng)輸入密碼' },
({ getFieldValue }) => ({
validator(_, value) {
if (!value || getFieldValue('password') === value) {
return Promise.resolve()
} else {
return Promise.reject(new Error('兩次密碼不一致'))
}
},
}),
]}
>
<Input.Password />
</Form.Item>
<Form.Item label="昵稱" name="nickname">
<Input />
</Form.Item>
<Form.Item wrapperCol={{ offset: 6, span: 16 }}>
<Space>
<Button type="primary" htmlType="submit">
注冊(cè)
</Button>
<Link to={LOGIN_PATHNAME}>已有賬戶,登錄</Link>
</Space>
</Form.Item>
</Form>
其實(shí)不僅僅是表單組件,其他組件也是一樣的道理,所以建議沒(méi)事可以多去組件庫(kù)官網(wǎng)上翻翻,留些整體印象
當(dāng)一個(gè)頁(yè)面有多個(gè)表單組件時(shí),就要優(yōu)先考慮把值存在狀態(tài)管理中
這種場(chǎng)景太常見(jiàn)了,一個(gè)頁(yè)面有多個(gè)表單組件。我看很多時(shí)候大家都是把每個(gè)組件的數(shù)據(jù)狀態(tài)維護(hù)在各自的組件中,其實(shí)不太好。
- 指不定你下個(gè)頁(yè)面還可以返回上一步的表單填寫頁(yè),如果都把數(shù)據(jù)統(tǒng)一放在狀態(tài)管理維護(hù),回顯就很容易了。
- 各個(gè)表單組件可能是會(huì)聯(lián)動(dòng)的,例如a組件的某個(gè)item的值會(huì)改變b組件的一些item的顯隱,有狀態(tài)管理就比較容易去做。
當(dāng)有一個(gè)復(fù)雜的多表單頁(yè)面時(shí),建議做頁(yè)面緩存
場(chǎng)景舉例:
- 復(fù)雜的多表單頁(yè)面A —> 復(fù)雜的多表單頁(yè)面B,A頁(yè)面下一步到B頁(yè)面,然后再返回A頁(yè)面,A頁(yè)面回顯
- 移動(dòng)端中復(fù)雜的多表單頁(yè)面A ----> 某項(xiàng)輸入項(xiàng)具體選擇的頁(yè)面C,A頁(yè)面點(diǎn)擊某項(xiàng)輸入項(xiàng)到C頁(yè)面,然后再返回A頁(yè)面,A頁(yè)面回顯并且定位在剛剛的位置。
這些情況還用redux去做回顯就很笨了,增加代碼復(fù)雜度。
vue有官方的keepalive,react有網(wǎng)友給我推薦了keepalive-react-component,我個(gè)人還沒(méi)使用過(guò)。
如果一些表單比較簡(jiǎn)單且能確保后續(xù)不會(huì)有復(fù)雜功能的拓展,可以使用業(yè)務(wù)組件
例如后臺(tái)管理的一些列表搜索頁(yè)的搜索表單區(qū)域,這種一般就不會(huì)做的很復(fù)雜,很適合用業(yè)務(wù)組件。
但當(dāng)你覺(jué)得某個(gè)表單現(xiàn)在看來(lái)簡(jiǎn)單,但以后被改復(fù)雜的可能性很大時(shí),就不要考慮使用業(yè)務(wù)組件了。
react表單業(yè)務(wù)類組件例子:
/* eslint-disable react-hooks/exhaustive-deps */
/* 用于列表頁(yè)的搜索過(guò)濾區(qū) 類組件
necessaryField 必須返回字段值
initiative 主動(dòng)第一次觸發(fā)搜索
formatter: int
btnChildren 按鈕插槽
config :{
formItems: [],
colSpan: [], //
formLayout
}
*/
import React, { Component } from 'react'
import {
Button,
Form,
Input,
Select,
DatePicker,
Row,
Col,
InputNumber,
} from 'antd'
import { ReloadOutlined } from '@ant-design/icons'
import moment from 'moment'
import './TableFiltersSearch.less'
import { isEffectVar, deepClone, validateNumber } from './utils'
export default class TableFiltersSearchClass extends Component {
constructor(props) {
super(props)
this.state = {
formItems: [],
colSpan: [21, 3], // 表單和按鈕區(qū)的寬度設(shè)置
formLayout: 'inline',
formItemLayout: {
labelCol: {
flex: '1',
},
wrapperCol: {
flex: '1',
},
},
}
}
formRef = React.createRef()
submitRef = React.createRef()
// 布局
componentDidMount() {
/* 處理搜索區(qū)配置 */
this.init()
}
componentDidUpdate() {
// this.init()
}
init = () => {
if (this.props.config) {
// 先對(duì)子項(xiàng)做處理
this.setState({
formItems: this.itemConfigDeal(this.props.config.itemConfigs),
})
// 默認(rèn)值處理
this.defaultFormDeal(this.props.config.itemConfigs)
// 是否需要主動(dòng)初始化觸發(fā)
if (this.props.initiative) {
this.queryHandle()
}
// 表單和按鈕區(qū)的寬度設(shè)置
if (this.props?.config?.colSpan) {
this.setState({
colSpan: this.props.config.colSpan,
})
}
if (this.props?.config?.formLayout) {
this.setState({
formLayout: this.props.config.formLayout,
})
}
if (this.props?.config?.formItemLayout) {
this.setState({
formItemLayout: this.props.config.formItemLayout,
})
}
}
}
/* 子項(xiàng)的處理 */
itemConfigDeal = (configs) => {
return configs.map((item, index) => {
if (item.display === false) {
return null
}
if (!item.type || item.type === 'input') {
// 普通輸入框
return (
<Form.Item
label={item.label}
name={item.name}
key={index}
rules={item.rules ? item.rules : []}
>
<Input
style={{
width: item.width ? item.width : '220px',
}}
placeholder={
item.placeholder ? item.placeholder : '請(qǐng)輸入'
}
allowClear
onChange={(e) => {
this.inputOnChange(e, item)
}}
disabled={
item.disabled !== undefined
? item.disabled
: false
}
// {...item.attr}
/>
</Form.Item>
)
}
// 數(shù)字輸入框
if (!item.type || item.type === 'inputNumber') {
return (
<Form.Item
label={item.label}
name={item.name}
key={index}
rules={item.rules ? item.rules : []}
>
<InputNumber
style={{ width: item.width ? item.width : '220px' }}
placeholder={
item.placeholder ? item.placeholder : '請(qǐng)輸入'
}
allowClear
disabled={
item.disabled !== undefined
? item.disabled
: false
}
// onChange={e => e.target.value = validateNumber(e.target.value)}
/>
</Form.Item>
)
}
// 時(shí)間返回選擇器
if (item.type === 'dateRange') {
return (
<Form.Item
label={item.label}
name={item.name}
key={index}
rules={item.rules ? item.rules : []}
>
<DatePicker.RangePicker
style={{ width: item.width ? item.width : '220px' }}
separator="至"
format="YYYY-MM-DD"
disabled={
item.disabled !== undefined
? item.disabled
: false
}
/>
</Form.Item>
)
}
// 單選下拉框
if (item.type === 'select') {
return (
<Form.Item
label={item.label}
name={item.name}
key={index}
rules={item.rules ? item.rules : []}
>
<Select
style={{ width: item.width ? item.width : '220px' }}
allowClear
placeholder={
item.placeholder ? item.placeholder : '請(qǐng)選擇'
}
disabled={
item.disabled !== undefined
? item.disabled
: false
}
>
{item.option?.length > 0 &&
item.option.map((o, i) => {
return (
<Select.Option key={i} value={o.value}>
{o.label}
</Select.Option>
)
})}
</Select>
</Form.Item>
)
}
// 多選下拉框
if (item.type === 'selectMulti') {
return (
<Form.Item
label={item.label}
name={item.name}
key={index}
rules={item.rules ? item.rules : []}
>
<Select
style={{ width: item.width ? item.width : '220px' }}
mode="multiple"
allowClear
placeholder={
item.placeholder ? item.placeholder : '請(qǐng)選擇'
}
disabled={
item.disabled !== undefined
? item.disabled
: false
}
onChange={(value) => {
item.onChange && item.onChange(value)
}}
>
{item.option?.length > 0 &&
item.option.map((o, i) => {
return (
<Select.Option
key={i}
value={o.value}
maxTagCount="responsive"
>
{o.label}
</Select.Option>
)
})}
</Select>
</Form.Item>
)
}
return ''
})
}
defaultFormDeal = (configs) => {
configs.forEach((item, index) => {
if (isEffectVar(item.default)) {
this.formRef.current.setFieldsValue({
[item.name]: item.default,
})
}
})
}
/* 重置 */
resetHandle = () => {
this.formRef.current.resetFields()
this.props.resetFn && this.props.resetFn() // 重置的回調(diào)
}
/* 查詢 */
queryHandle = (extraData) => {
let data = this.formRef.current.getFieldsValue(true) // 返回的居然是淺拷貝 T^T
data = deepClone(data)
this.formRef.current
.validateFields()
.then((values) => {
this.queryDoing(data, extraData)
})
.catch((errorInfo) => {
return false // 返回不出去,默認(rèn)有undefined判斷
})
}
// 查詢做的事
queryDoing = (data, extraData) => {
// 記錄日期格式的子項(xiàng)的key
let dateTypeKeys = this.props.config.itemConfigs.map((item) => {
if (item.type === 'dateRange') {
return item.name
}
})
Object.keys(data).forEach((key) => {
// 把時(shí)間處理成后端需要的入?yún)?/span>
if (!isEffectVar(data[key])) {
data[key] = ''
}
if (dateTypeKeys.includes(key)) {
let dateArr = [...data[key]]
if (dateArr.length) {
// 原始數(shù)據(jù)只有null或者[x,x]
data[key][0] = moment(dateArr[0]).format('YYYYMMDD')
data[key][1] = moment(dateArr[1]).format('YYYYMMDD')
}
}
})
// 如果必須要返回所有字段,空的就返回undefined
if (this.props.necessaryField) {
this.props.config.itemConfigs.forEach((item) => {
if (!isEffectVar(data[item.name])) {
data[item.name] = undefined
}
})
}
console.log('經(jīng)過(guò)二次加工提交的原生數(shù)值', data)
this.props.queryFn && this.props.queryFn(data, extraData) // 重置的回調(diào)
}
getData = () => {
let data = this.formRef.current.getFieldsValue(true) // 返回的居然是淺拷貝 T^T
data = deepClone(data)
return data
}
validateFields = async () => {
const res = await this.formRef.current
.validateFields()
.catch((err) => null)
if (!res) {
return null
} else {
return this.getData()
}
}
/* input格式化 */
inputOnChange = (e, item) => {
let value = e.target.value
if (item.formatter === 'int') {
value = value.replace(/[^\d]/g, '')
}
this.formRef.current.setFieldsValue({
[item.name]: value,
})
}
onFinish = (values) => {
console.log('Success:', values)
this.queryHandle()
}
onFinishFailed = (errorInfo) => {
console.log('Failed:', errorInfo)
}
setFieldsValue = (data) => {
console.log('賦值', data)
this.formRef.current.setFieldsValue({
...data,
})
}
render() {
let { formItems, colSpan, formLayout, formItemLayout } = this.state
let { btnChildren } = this.props
return (
<div className="tfs-container">
{/* {...formItemLayout} */}
<Row>
<Col span={colSpan[0]}>
<Form
{...formItemLayout}
layout={formLayout}
ref={this.formRef}
onFinish={this.onFinish}
onFinishFailed={this.onFinishFailed}
className={
'tfs-table' + formLayout === 'horizontal'
? 'tfs-horizontal-table'
: ''
}
>
{/* 循環(huán)渲染子項(xiàng) */}
{formItems.length > 0 &&
formItems.map((item) => {
return item
})}
<Button
ref={this.submitRef}
style={{ display: 'none' }}
htmlType="submit"
></Button>
</Form>
</Col>
{colSpan[1] !== 0 && (
<Col span={colSpan[1]}>
{/* 按鈕區(qū) */}
<div className="tfs-btn-container">
<Button
type={'link'}
onClick={this.resetHandle}
>
重置 <ReloadOutlined />
</Button>
<Button
type={'primary'}
className="it2-primary-button"
htmlType="submit"
onClick={() => {
this.submitRef.current.click()
}}
loading={this.props.isSearching}
>
查詢
</Button>
{/* 按鈕插槽 */}
{btnChildren ? btnChildren : null}
</div>
</Col>
)}
</Row>
{/* 縱向表單的自定義按鈕 */}
{colSpan[1] === 0 && formLayout === 'horizontal' && (
<Row>{btnChildren ? btnChildren : null}</Row>
)}
</div>
)
}
}
所需工具函數(shù)
import moment, { isMoment } from "moment";
/* 是否是有效值 */
export function isEffectVar(val) {
return ![null, "", undefined].includes(val);
}
export function deepClone(target) {
let result;
if (typeof target === "object") {
if (Array.isArray(target)) {
result = [];
for (let i in target) {
result.push(deepClone(target[i]));
}
} else if (target === null) {
result = null;
} else if (isMoment(target)) {
result = target.clone();
} else if (target.constructor === RegExp) {
result = target;
} else {
result = {};
for (let i in target) {
result[i] = deepClone(target[i]);
}
}
} else {
result = target;
}
return result;
}
export function validateNumber(value) {
// 使用正則表達(dá)式驗(yàn)證輸入值是否為數(shù)字
const reg = /^\d*$/;
if (!reg.test(value)) {
// 如果輸入值不是數(shù)字,則清空輸入框
return '';
}
return value;
}
使用配置例子:
formConfig: {
colSpan: [24, 0],
formLayout: 'horizontal',
formItemLayout: {
labelCol: {
span: 7,
},
wrapperCol: {
span: 17,
},
},
itemConfigs: [
{
label: '場(chǎng)景',
name: 'signScenType',
type: 'selectMulti',
option: [
{ label: '采購(gòu)', value: 'P' },
{ label: '銷售', value: 'S' },
{ label: '其他', value: 'O' },
],
width: '500px',
rules: [{ required: true, message: '請(qǐng)輸入內(nèi)容' }],
onChange: (value) => {
},
},
{
label: 'aaaaa',
name: 'signOtherScen', // signOtherScen?
width: '500px',
rules: [{ required: true, message: '請(qǐng)輸入內(nèi)容' }],
display: false,
},
{
label: '簽署量(份/年)',
name: 'signVolume',
formatter: 'int',
width: '500px',
rules: [
{ required: true, message: '請(qǐng)輸入內(nèi)容' },
({ getFieldValue }) => ({
validator(_, value) {
if (value) {
if (Number(value) > 1000000000) {
return Promise.reject(
new Error('超出最大數(shù)值')
)
}
}
return Promise.resolve()
},
}),
],
},{
label: '聯(lián)系人手機(jī)號(hào)',
name: 'contactNumber',
formatter: 'int',
width: '500px',
rules: [{ required: true, message: '請(qǐng)輸入內(nèi)容' }],
},
{
label: 'bbbbbb',
name: 'FDD',
width: '500px',
disabled: true,
default: 'aaa',
rules: [{ required: true, message: '請(qǐng)輸入內(nèi)容' }],
},
],
},
配置寫的比較倡促,以后來(lái)寫個(gè)完整的
vue的我之前也寫過(guò)一個(gè)vue2版本的組件庫(kù)例子:【業(yè)務(wù)組件二次封裝】
表單做搜索區(qū)與表格怎么做聯(lián)動(dòng)
后臺(tái)中有種頁(yè)面場(chǎng)景很常見(jiàn),就是一個(gè)表單搜索區(qū)+分頁(yè)表格區(qū)+分頁(yè)區(qū)(就如上面例子的圖片)。這種組件怎么設(shè)計(jì)才好,并且盡量做到三者之間解耦。
還是用react舉例(需要配合ahooks第三方庫(kù)):
我們?nèi)ピO(shè)計(jì)一個(gè)這樣的頁(yè)面時(shí),會(huì)用一個(gè)容器組件,里面引入表單組件,表格組件和分頁(yè)組件。表單組件的值驅(qū)動(dòng)著表格數(shù)據(jù)的獲?。ǚ猪?yè)組件待會(huì)再說(shuō)),我們首先看看表單組件怎么驅(qū)動(dòng)最好。
咱們就舉例一個(gè)最簡(jiǎn)單的表單,就只有一個(gè)搜索輸入項(xiàng),我們想做到的是:
- 刷新頁(yè)面,表單原來(lái)填寫的值能夠回顯
- 容器組件和表格組件都能拿到表單的值,且盡量解耦
如果我們把每次搜索時(shí)表單的值都更新在url上,其他組件通過(guò)router的hook就可以拿到表單值,頁(yè)面刷新后表單組件也能獲取到表單值進(jìn)行回填。
import React, { FC, useEffect, useState } from 'react'
import type { ChangeEvent } from 'react'
import { useNavigate, useLocation, useSearchParams } from 'react-router-dom'
import { Input } from 'antd'
import { LIST_SEARCH_PARAM_KEY } from '../constant'
const { Search } = Input
const ListSearch: FC = () => {
const nav = useNavigate()
const { pathname } = useLocation()
// 輸入值
const [value, setValue] = useState('')
function handleChange(event: ChangeEvent<HTMLInputElement>) {
setValue(event.target.value)
}
// 獲取 url 參數(shù),回填表單數(shù)據(jù)
const [searchParams] = useSearchParams()
useEffect(() => {
const curVal = searchParams.get(LIST_SEARCH_PARAM_KEY) || ''
setValue(curVal)
}, [searchParams])
// 點(diǎn)擊搜索
function handleSearch(value: string) {
// 跳轉(zhuǎn)頁(yè)面,增加 url 參數(shù)
nav({
pathname,
search: `${LIST_SEARCH_PARAM_KEY}=${value}`, // 去掉了 page pageSize
})
}
return (
<Search
allowClear
placeholder="輸入關(guān)鍵字"
value={value}
onChange={handleChange}
onSearch={handleSearch}
/>
)
}
export default ListSearch
除了表單組件驅(qū)動(dòng)表格數(shù)據(jù),還有分頁(yè)組件的分頁(yè)數(shù)據(jù)也驅(qū)動(dòng)著表格數(shù)據(jù)的獲取,驅(qū)動(dòng)方式也和表單組件一樣即可:
import React, { FC, useEffect, useState } from 'react'
import { Pagination } from 'antd'
import { useSearchParams, useNavigate, useLocation } from 'react-router-dom'
import { LIST_PAGE_SIZE, LIST_PAGE_PARAM_KEY, LIST_PAGE_SIZE_PARAM_KEY } from '../constant/index'
type PropsType = {
total: number
}
const ListPage: FC<PropsType> = (props: PropsType) => {
const { total } = props
const [current, setCurrent] = useState(1)
const [pageSize, setPageSize] = useState(LIST_PAGE_SIZE)
// 從 url 參數(shù)中找到 page pageSize ,并且同步到 Pagination 組件中
const [searchParams] = useSearchParams()
useEffect(() => {
const page = parseInt(searchParams.get(LIST_PAGE_PARAM_KEY) || '') || 1
setCurrent(page)
const pageSize = parseInt(searchParams.get(LIST_PAGE_SIZE_PARAM_KEY) || '') || LIST_PAGE_SIZE
setPageSize(pageSize)
}, [searchParams])
// 當(dāng) page pageSize 改變時(shí),改變 url 對(duì)應(yīng)參數(shù)
const nav = useNavigate()
const { pathname } = useLocation()
function handlePageChange(page: number, pageSize: number) {
searchParams.set(LIST_PAGE_PARAM_KEY, page.toString())
searchParams.set(LIST_PAGE_SIZE_PARAM_KEY, pageSize.toString())
nav({
pathname,
search: searchParams.toString(), // 除了改變 page pageSize 之外,其他的 url 參數(shù)要帶著
})
}
return (
<Pagination current={current} pageSize={pageSize} total={total} onChange={handlePageChange} />
)
}
export default ListPage
我們可以把表格數(shù)據(jù)的獲取放在容器組件中,只需要監(jiān)聽路由的變化即可,還可以專門抽離成一個(gè)hook:
import { useSearchParams } from 'react-router-dom'
import { useRequest } from 'ahooks'
import { getQuestionListService } from '../services/question'
import {
LIST_SEARCH_PARAM_KEY,
LIST_PAGE_PARAM_KEY,
LIST_PAGE_SIZE_PARAM_KEY,
LIST_PAGE_SIZE,
} from '../constant/index'
// 可能多個(gè)表單表格頁(yè)面功能是一樣的,可以復(fù)用,用傳入的類型來(lái)區(qū)分,但是為了業(yè)務(wù)拓展性,建議還是分開寫hook
type OptionType = {
isStar: boolean
isDeleted: boolean
}
function useLoadQuestionListData(opt: Partial<OptionType> = {}) {
const { isStar, isDeleted } = opt
const [searchParams] = useSearchParams()
const { data, loading, error, refresh } = useRequest(
async () => {
// 從url中解構(gòu)出入?yún)?/span>
const keyword = searchParams.get(LIST_SEARCH_PARAM_KEY) || ''
const page = parseInt(searchParams.get(LIST_PAGE_PARAM_KEY) || '') || 1
const pageSize = parseInt(searchParams.get(LIST_PAGE_SIZE_PARAM_KEY) || '') || LIST_PAGE_SIZE
const data = await getQuestionListService({ keyword, isStar, isDeleted, page, pageSize })
return data
},
{
refreshDeps: [searchParams], // 刷新的依賴項(xiàng)
}
)
return { data, loading, error, refresh } // 返回接口數(shù)據(jù)、是否正在請(qǐng)求中的狀態(tài)、錯(cuò)誤情況、手動(dòng)更新函數(shù)(參數(shù)不變)
}
export default useLoadQuestionListData
代碼設(shè)計(jì)來(lái)自雙越老師的視頻
推薦的表單庫(kù)
react中推薦react-hook-form,formik,這倆個(gè)都很強(qiáng)大,但是其實(shí)antd提供的表單已經(jīng)能滿足大部分的需求了。
vue目前我只知道餓了么。
關(guān)于低代碼表單
這玩意個(gè)人認(rèn)為只能用于非常固定的業(yè)務(wù)場(chǎng)景,例如一般的問(wèn)卷調(diào)查,他就是非常固定的一些表單輸入項(xiàng),后期也不會(huì)加復(fù)雜的東西,那就非常適合用低代碼去搭一個(gè)后臺(tái)。
我還遇到過(guò)一個(gè)用法,就是之前待過(guò)的一個(gè)公司里后臺(tái)管理很多表單用的也是低代碼生成的。機(jī)制是這樣的這些表單配置都是在后臺(tái)配出來(lái)的,然后前端每次渲染頁(yè)面的時(shí)候會(huì)通過(guò)接口拉取表單的json配置數(shù)據(jù),前端開發(fā)人員把配置數(shù)據(jù)傳入對(duì)應(yīng)的低代碼組件,然后這個(gè)低代碼組件有一些拓展功能要開發(fā)人員自己調(diào)試。這樣做出來(lái)的表單的一個(gè)好處是,當(dāng)項(xiàng)目上線了,突然要改表單的某些選項(xiàng),直接后臺(tái)通過(guò)低代碼把對(duì)應(yīng)的輸入項(xiàng)做調(diào)整就可以了,完全不用再去改項(xiàng)目代碼,反應(yīng)迅速。
掘金上有篇討論可以看看【低代碼:現(xiàn)在我怎么樣了】
關(guān)于低代碼的開源項(xiàng)目參考:
- 這個(gè)比較成熟商用級(jí)別的:https://lowcode-engine.cn/index
- 這個(gè)比較適合用來(lái)學(xué)習(xí)簡(jiǎn)單低代碼的搭建:https://buqiyuan.gitee.io/vite-vue3-lowcode/#/
關(guān)于動(dòng)態(tài)表單
動(dòng)態(tài)表單我個(gè)人的方案是【業(yè)務(wù)組件二次封裝】里,把所有要顯示的表單項(xiàng)id維護(hù)在一個(gè)數(shù)組中,公共組件根據(jù)這個(gè)數(shù)組去顯示對(duì)應(yīng)的表單。
具體可以看PzFormGeneral.vue里的displayList部分
這種方式的好處就是很靈活,基本滿足所有場(chǎng)景,不好的地方就是每個(gè)能觸發(fā)動(dòng)態(tài)表單切換的表單項(xiàng)都要維護(hù)一個(gè)數(shù)組,稍顯麻煩。
另外一種方式是從渡一教育哪里看過(guò)來(lái)的,每個(gè)組件的配置都有一個(gè)屬性,這個(gè)屬性是一個(gè)函數(shù),他內(nèi)部處理對(duì)應(yīng)邏輯返回下一個(gè)要顯示或者隱藏的表單項(xiàng)配置。
就類似鏈表的設(shè)計(jì)了,好處就是簡(jiǎn)單,都維護(hù)在配置項(xiàng)中,不好的地方在于鏈表關(guān)系,無(wú)法做到第2項(xiàng)影響第4項(xiàng)之后的表單項(xiàng)。
一些數(shù)據(jù)上的處理技巧
數(shù)組轉(zhuǎn)字符串
有時(shí)候我們表單中的某個(gè)輸入項(xiàng)拿到的值是一個(gè)數(shù)組,但是后端需要的是用逗號(hào)分開區(qū)分的字符串。咱們可以直接:
let arr = [1,2,3]
console.log(arr.toString()) // '1,2,3'
console.log(arr.join(',')) // '1,2,3'
日期的轉(zhuǎn)換
咱們用組件庫(kù)表單的數(shù)據(jù)獲取api拿到的日期輸入項(xiàng)的數(shù)據(jù)一般都是帶有組件定義的格式的,例如ant-design拿到的一般是個(gè)Moment對(duì)象,后端可能需要的是yyyy-dd-mm
的格式,這些我們都可以提前準(zhǔn)備好公共函數(shù)統(tǒng)一處理的。文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-470791.html
類似的不止日期啦,學(xué)會(huì)舉一反三文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-470791.html
到了這里,關(guān)于【react框架】結(jié)合antd做表單組件的一些心得記錄的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!