新聞中心
無論你用React,Vue,還是Angular,你還是要一遍一遍寫相似的CRUD 頁面,一遍一遍,一遍一遍,一遍又一遍……

麻陽網(wǎng)站建設(shè)公司創(chuàng)新互聯(lián)公司,麻陽網(wǎng)站設(shè)計(jì)制作,有大型網(wǎng)站制作公司豐富經(jīng)驗(yàn)。已為麻陽1000+提供企業(yè)網(wǎng)站建設(shè)服務(wù)。企業(yè)網(wǎng)站搭建\外貿(mào)營銷網(wǎng)站建設(shè)要多少錢,請找那個(gè)售后服務(wù)好的麻陽做網(wǎng)站的公司定做!
“天下苦秦久矣”~~
前端開發(fā)的“痛點(diǎn)”在哪里?
img
現(xiàn)在的前端開發(fā),我們有了世界一流的UI庫React,Vue,Angular,有了樣式豐富的UI組件庫Tea (騰訊云UI組件庫,類似Antd Design), 有了方便強(qiáng)大的腳手架工具(例如,create react app)。但是我們在真正業(yè)務(wù)代碼之前,通常還免不了寫大量的樣板代碼。
現(xiàn)在的CRUD頁面代碼通常:
- 太輕的“Model”或著“Service”,大多時(shí)候只是一些API調(diào)用的封裝。
- 胖”View“,View頁面中有展示UI邏輯,生命周期邏輯,CRUD的串聯(lián)邏輯,然后還要塞滿業(yè)務(wù)邏輯代碼。
- 不同的項(xiàng)目業(yè)務(wù)邏輯不同,但是列表頁,表單,搜索這三板斧的樣板代碼,卻要一遍一遍占據(jù)著前端工程師的寶貴時(shí)間。
特別是CRUD類應(yīng)用的樣板代碼受限于團(tuán)隊(duì)風(fēng)格,后端API風(fēng)格,業(yè)務(wù)形態(tài)等,通常內(nèi)在邏輯相似書寫上卻略有區(qū)別,無法通過一個(gè)通用的庫或者框架來解決(上圖中背景越深,越不容易有一個(gè)通用的方案)。
說好的“數(shù)據(jù)驅(qū)動(dòng)的前端開發(fā)”呢?
對于這個(gè)“痛點(diǎn)”——怎么盡可能的少寫模版代碼,就是本文嘗試解決的問題。
我們嘗試使用JavaScript新特性 Decorator和 Reflect元編程來解決這個(gè)問題。
前端元編程
從ECMAScript 2015 開始,JavaScript 獲得了 Proxy 和 Reflect 對象的支持,允許你攔截并定義基本語言操作的自定義行為(例如,屬性查找,賦值,枚舉,函數(shù)調(diào)用等)。借助這兩個(gè)對象,你可以在 JavaScript 元級別進(jìn)行編程。
在正式開始之前,我們先復(fù)習(xí)下 Decorator和 Reflect。
Decorator
這里我們簡單介紹Typescript的 Decorator,ECMAScript中 Decorator尚未定稿,但是不影響我們?nèi)粘5臉I(yè)務(wù)開發(fā)(Angular同學(xué)就在使用Typescript的 Decorator)。
簡單來說, Decorator是可以標(biāo)注修改類及其成員的新語言特性,使用 @expression的形式,可以附加到,類、方法、訪問符、屬性、參數(shù)上。
TypeScript中需要在 tsconfig.json中增加 experimentalDecorators來支持:
- {
- "compilerOptions": {
- "target": "ES5",
- "experimentalDecorators": true
- }
- }
比如可以使用類修飾器來為類擴(kuò)展方法。
- // offer type
- abstract class Base {
- log() {}
- }
- function EnhanceClass() {
- return function(Target) {
- return class extends Target {
- log() {
- console.log('---log---')
- }
- }
- }
- }
- @EnhanceClass()
- class Person extends Base { }
- const person = new Person()
- person.log()
- // ---log---
更多查看 typescript 官方的文檔:https://www.typescriptlang.org/docs/handbook/decorators.html
Reflect
Reflect 是ES6中就有的特性,大家可能對它稍微陌生,Vue3中依賴Reflect和Proxy來重寫它的響應(yīng)式邏輯。
簡單來說, Reflect是一個(gè)人內(nèi)置的對象,提供了攔截 JavaScript操作的方法。
- const _list = [1,2,3]
- const pList = new Proxy(_list,{
- get(target, key,receiver) {
- console.log('get value reflect:', key)
- return Reflect.get(target, key, receiver)
- },
- set(target,key,value,receiver) {
- console.log('set value reflect',key,value)
- return Reflect.set(target,key,value,receiver)
- }
- })
- pList.push(4)
- // get value reflect:push
- // get value reflect:length
- // set value reflect 3 4
- // set value reflect length 4
Reflect Metadata
Reflect Metadata 是ES7的一個(gè)提案,Typescript 1.5+就有了支持。要使用需要:
- npm i reflect-metadata--save
- 在 tsconfig.json 里配置 emitDecoratorMetadata 選項(xiàng)
簡單來說,Reflect Metadata能夠?yàn)閷ο筇砑雍妥x取元數(shù)據(jù)。
如下可以使用內(nèi)置的 design:key拿到屬性類型:
- function Type(): PropertyDecorator {
- return function(target,key) {
- const type = Reflect.getMetadata('design:type',target,key)
- console.log(`${key} type: ${type.name}`);
- }
- }
- class Person extends Base {
- @Type()
- name:string = ''
- }
- // name type: String
使用Decorator,Reflect減少樣板代碼
回到正題——使用Decorator和Reflect來減少CRUD應(yīng)用中的樣板代碼。
什么是CRUD頁面?
img
CRUD頁面無需多言,列表頁展示,表單頁修改 ……包括API調(diào)用, 都是圍繞某個(gè)數(shù)據(jù)結(jié)構(gòu)(圖中 Person)展開,增、刪、改、查。
基本思路
基本思路很簡單,就像上圖,Model是中心,我們就是借助 Decorator和 Reflect將CRUD頁面所需的樣板類方法屬性元編程在Model上。進(jìn)一步延伸數(shù)據(jù)驅(qū)動(dòng)UI的思路。
img
- 借助Reflect Matadata綁定CRUD頁面信息到Model的屬性上
- 借助Decorator增強(qiáng)Model,生成CRUD所需的樣板代碼
Show Me The Code
下文,我們用TypeScript和React為例,組件庫使用騰訊Tea component 解說這個(gè)方案。
首先我們有一個(gè)函數(shù)來生成不同業(yè)務(wù)的屬性裝飾函數(shù)。
- function CreateProperDecoratorF
() { - const metaKey = Symbol();
- function properDecoratorF(config:T): PropertyDecorator {
- return function (target, key) {
- Reflect.defineMetadata(metaKey, config, target, key);
- };
- }
- return { metaKey, properDecoratorF}
- }
一個(gè)類裝飾器,處理通過數(shù)據(jù)裝飾器收集上來的元數(shù)據(jù)。
- export function EnhancedClass(config: ClassConfig) {
- return function(Target) {
- return class EnhancedClass extends Target {
- }
- }
- }
API Model 映射
TypeScript項(xiàng)目中第一步自然是將后端數(shù)據(jù)安全地轉(zhuǎn)換為 type, interface或者 Class,這里Class能在編譯后在JavaScript存在,我們選用 Class。
- export interface TypePropertyConfig {
- handle?: string | ServerHandle
- }
- const typeConfig = CreateProperDecoratorF
() - export const Type = typeConfig.properDecoratorF;
- @EnhancedClass({})
- export class Person extends Base {
- static sexOptions = ['male' , 'female' , 'unknow'];
- @Type({
- handle: 'ID'
- })
- id: number = 0
- @Type({})
- name:string = ''
- @Type({
- handle(data,key) {
- return parseInt(data[key] || '0')
- }
- })
- age:number = 0
- @Type({
- handle(data,key) {
- return Person.sexOptions.includes(data[key]) ? data[key] : 'unknow'
- }
- })
- sex: 'male' | 'female' | 'unknow' = 'unknow'
- }
重點(diǎn)在 handle?:string|ServerHandle函數(shù),在這個(gè)函數(shù)處理API數(shù)據(jù)和前端數(shù)據(jù)的轉(zhuǎn)換,然后在 constructor中集中處理。
- export function EnhancedClass(config: ClassConfig) {
- return function(Target) {
- return class EnhancedClass extends Target {
- constructor(data) {
- super(data)
- Object.keys(this).forEach(key => {
- const config:TypePropertyConfig = Reflect.getMetadata(typeConfig.metaKey,this,key)
- this[key] = config.handle ? typeof config.handle === 'string' ? data[config.handle]:config.handle(data,key): data[key];
- })
- }
- }
- }
- }
列表頁TablePage
列表頁中一般使用Table組件,無論是Tea Component還是Antd Design Component中,樣板代碼自然就是寫那一大堆Colum配置了,配置哪些key要展示,表頭是什么,數(shù)據(jù)轉(zhuǎn)化為顯示數(shù)據(jù)……
首先我們收集Tea Table 所需的 TableColumn類型的column元數(shù)據(jù)。
- import {TableColumn} from 'tea-component/lib/table'
- export type EnhancedTableColumn
= TableColumn ; - export type ColumnPropertyConfig = Partial
>; - const columnConfig = CreateProperDecoratorF
() - export const Column = columnConfig.properDecoratorF;
- @EnhancedClass({})
- export class Person extends Base {
- static sexOptions = ['male' , 'female' , 'unknow'];
- id: number = 0
- @Column({
- header: 'person name'
- })
- name:string = ''
- @Column({
- header: 'person age'
- })
- age:number = 0
- @Column({})
- sex: 'male' | 'female' | 'unknow' = 'unknow'
- }
然后在EnhancedClass中收集,生成column列表。
- function getConfigMap
(F: any, cachekey: symbol,metaKey: symbol): Map { - if (F[cachekey]) {
- return F[cachekey]!;
- }
- const item = new F({});
- F[cachekey] = Object.keys(item).reduce((pre,cur) => {
- const config: T = Reflect.getMetadata(
- metaKey,
- item,
- cur
- );
- if (config) {
- pre.set(cur, config);
- }
- return pre
- }, new Map
()); - return F[cachekey];
- }
- export function EnhancedClass(config: ClassConfig) {
- const cacheColumnConfigKey = Symbol('cacheColumnConfigKey');
- return function(Target) {
- return class EnhancedClass extends Target {
- [cacheColumnConfigKey]: Map
| null - /**
- * table column config
- */
- static get columnConfig(): Map
{ - return getConfigMap
(EnhancedClass, cacheColumnConfigKey,columnConfig.metaKey) - }
- /**
- * get table colums
- */
- static getColumns
(): EnhancedTableColumn [] { - const list : EnhancedTableColumn
[] = [] - EnhancedClass.columnConfig.forEach((config, key) => {
- list.push({
- key,
- header: key,
- ...config
- })
- })
- return list
- }
- }
- }
- }
Table數(shù)據(jù)一般是分頁,而且調(diào)用方式通常很通用,也可以在EnhancedClass中實(shí)現(xiàn)。
- export interface PageParams {
- pageIndex: number;
- pageSize: number;
- }
- export interface Paginabale
{ - total: number;
- list: T[]
- }
- export function EnhancedClass(config: ClassConfig) {
- return function(Target) {
- return class EnhancedClass extends Target {
- static async getList
(params: PageParams): Promise > { - const result = await getPersonListFromServer(params)
- return {
- total: result.count,
- list: result.data.map(item => new EnhancedClass(item))
- }
- }
- }
- }
- }
自然我們封裝一個(gè)更簡易的Table 組件。
- import { Table as TeaTable } from "tea-component/lib/table";
- import React, { FC ,useEffect, useState} from "react";
- import { EnhancedTableColumn, Paginabale, PageParams } from './utils'
- import { Person } from "./person.service";
- function Table
(props: { - columns: EnhancedTableColumn
[]; - getListFun: (param:PageParams) => Promise
> - }) {
- const [isLoading,setIsLoading] = useState(false)
- const [recordData,setRecordData] = useState
>() - const [pageIndex, setPageIndex] = useState(1);
- const [pageSize, setPageSize] = useState(20);
- useEffect(() => {
- (async () => {
- setIsLoading(true)
- const result = await props.getListFun({
- pageIndex,
- pageSize
- })
- setIsLoading(false)
- setRecordData(result)
- })();
- },[pageIndex,pageSize]);
- return (
- columns={props.columns}
- records={recordData ? recordData.list : []}
- addons={[
- TeaTable.addons.pageable({
- recordCount:recordData ? recordData.total : 0,
- pageIndex,
- pageSize,
- onPagingChange: ({ pageIndex, pageSize }) => {
- setPageIndex(pageIndex || 0);
- setPageSize(pageSize || 20);
- }
- }),
- ]}
- />
- )
- }
- export default Table
- getConfigMap
(F:any,cachekey:symbol,metaKey:symbol):Map 收集元數(shù)據(jù)到Map - staticgetColumns
():EnhancedTableColumn [] 得到table可用column信息。
- const App = () => {
- const columns = Person.getColumns
(); - const getListFun = useCallback((param: PageParams) => {
- return Person.getList
(param) - }, [])
- return
columns={columns} getListFun={getListFun}/>
- }
效果很明顯,不是嗎?7行寫一個(gè)table page。
Form表單頁
表單,自然就是字段的name,label,require,validate,以及提交數(shù)據(jù)的轉(zhuǎn)換。
Form表單我們使用Formik + Tea Form Component + yup(數(shù)據(jù)校驗(yàn))。Formik 使用React Context來提供表單控件所需的各種方法數(shù)據(jù),然后借助提供的Field等組件,你可以很方便的封裝你的業(yè)務(wù)表單組件。
- import React, { FC } from 'react'
- import { Field, Form, Formik, FormikProps } from 'formik';
- import { Form as TeaForm, FormItemProps } from "tea-component/lib/form";
- import { Input, InputProps } from "tea-component/lib/input";
- import { Select } from 'tea-component/lib/select';
- type CustomInputProps = Partial
& Pick ; - type CustomSelectProps = Partial
& Pick & { - options: string[]
- }
- export const CustomInput:FC
= props => { - return (
- {
- ({
- field, // { name, value, onChange, onBlur }
- form: { touched, errors }, // also values, setXXXX, handleXXXX, dirty, isValid, status, etc.
- meta,
- }) => {
- return (
- {
- field.onChange(ctx.event)
- }} />
- )
- }
- }
- )
- }
- export const CustomSelect:FC
= props => { - return (
- {
- ({
- field, // { name, value, onChange, onBlur }
- form: { touched, errors }, // also values, setXXXX, handleXXXX, dirty, isValid, status, etc.
- meta,
- }) => {
- return (
- field.onChange(ctx.event)
- }} />
- )
- }
- }
- )
- }
照貓畫虎,我們還是先收集form所需的元數(shù)據(jù)
- import * as Yup from 'yup';
- export interface FormPropertyConfig {
- validationSchema?: any;
- label?: string;
- handleSubmitData?: (data:any,key:string) => {[key:string]: any},
- required?: boolean;
- initValue?: any;
- options?: string[]
- }
- const formConfig = CreateProperDecoratorF
() - export const Form = formConfig.properDecoratorF;
- @EnhancedClass({})
- export class Person extends Base {
- static sexOptions = ['male' , 'female' , 'unknow'];
- @Type({
- handle: 'ID'
- })
- id: number = 0
- @Form({
- label:"Name",
- validationSchema: Yup.string().required('Name is required'),
- handleSubmitData(data,key) {
- return {
- [key]: (data[key] as string).toUpperCase()
- }
- },
- required: true,
- initValue:'test name'
- })
- name:string = ''
- @Form({
- label:"Age",
- validationSchema: Yup.string().required('Age is required'),
- handleSubmitData(data,key) {
- return {
- [key]: parseInt(data[key] || '0')
- }
- },
- required: true,
- })
- age:number = 0
- @Form({
- label:"Sex",
- options: Person.sexOptions
- })
- sex: 'male' | 'female' | 'unknow' = 'unknow'
- }
有了元數(shù)據(jù),我們可以在EnhancedClass中生成form所需:
- initialValues
- 數(shù)據(jù)校驗(yàn)的validationSchema
- 各個(gè)表單組件所需的,name,label,required等
- 提交表單的數(shù)據(jù)轉(zhuǎn)換handle函數(shù)
- export type FormItemConfigType
= { - [key in keyof T]: {
- validationSchema?: any;
- handleSubmitData?: FormPropertyConfig['handleSubmitData'];
- form: {
- label: string;
- name: string;
- required: boolean;
- message?: string;
- options: string[];
- };
- };
- };
- export function EnhancedClass(config: ClassConfig) {
- return function(Target) {
- return class EnhancedClass extends Target {
- [cacheTypeConfigkey]: Map
| null - /**
- * table column config
- */
- static get formConfig(): Map
{ - return getConfigMap
(EnhancedClass, cacheTypeConfigkey,formConfig.metaKey) - }
- /**
- * get form init value
- */
- static getFormInitValues
(item?: T): Partial { - const data:any = {};
- const _item = new EnhancedClass({});
- EnhancedClass.formConfig.forEach((config,key) => {
- if (item && key in item) {
- data[key] = item[key]
- } else if ('initValue' in config) {
- data[key] = config.initValue
- } else {
- data[key] = _item[key] || ''
- }
- });
- return data as Partial
- }
- static getFormItemConfig
(overwriteConfig?: { - [key: string]: any;
- }): FormItemConfigType
{ - const formConfig: any = {};
- EnhancedClass.formConfig.forEach((config,key) => {
- formConfig[key] = {
- form: {
- label: String(config.label || key),
- name: String(key),
- required: !!config.validationSchema,
- options: config.options || [],
- ...overwriteConfig
- }
- };
- if (config.validationSchema) {
- formConfig[key].validationSchema = config.validationSchema;
- }
- if (config.handleSubmitData) {
- formConfig[key].handleSubmitData = config.handleSubmitData;
- }
- })
- return formConfig as FormItemConfigType
- }
- static handleToFormData
(item: T) { - let data = {}
- EnhancedClass.formConfig.forEach((config,key)=> {
- if (item.hasOwnProperty(key)) {
- data = {
- ...data,
- ...(EnhancedClass.formConfig
- .get(key).handleSubmitData ? EnhancedClass.formConfig
- .get(key).handleSubmitData(item, key) : {
- [key]: item[key] || ''
- })
- };
- }
- })
- return data
- }
- }
- }
- }
在FormPage中使用
- export const PersonForm:FC<{
- onClose: () => void
- }> = props => {
- const initialValues = Person.getFormInitValues
() - const formConfig = Person.getFormItemConfig
(); - const&nb
網(wǎng)站題目:前端元編程:使用注解加速你的前端開發(fā)
本文網(wǎng)址:http://www.dlmjj.cn/article/dhsjiii.html


咨詢
建站咨詢
