【EasyPage 教你写表单01】- EasyPage 框架介绍

简介

EasyPage 正如其名,含义是:简单的页面。它能让我们的前端页面开发更加简单,目前我们专注于解决表单问题。

  • Make Front-End Development Easier

它提供了一套描述式的 API,来帮助你高效地开发用户界面。无论是简单还是复杂的界面,EasyPage 都可以胜任。

下面是一个最基本的示例,创建一个姓名的表单:

// ./name.tsx
import { nodeUtil } from '@easy-page/antd-ui';

export const name = nodeUtil.createField('name', '姓名', {
  value: '',
  required: true,
});

上面的字段定义,展示了 EasyPage 的两个特性:

  • 简洁性
    • 我想用任何的写法去描述一个姓名字段,都会比上面更长。
    • 随着场景的复杂,EasyPage 的简介优势则更加明显。
  • 描述性
    • 基于描述,与 UI ”解耦“,底层可做更多适配(antd、acro 等 UI),为业务逻辑的复用性提供了更多的想象空间。

你可能已经有了些疑问——先别急,在后续的文档中我们会详细介绍每一个细节。现在,请继续看下去,以确保你对 EasyPage 作为一个框架到底提供了什么有一个宏观的了解。

EasyPage 能做什么?

  • 它可以用于开发表单页面,也可直接用于开发前端页面
  • 它可以帮我们做页面状态管理,再也不用手动在组件间各种透传 Props。
  • 它可以帮我们做页面的精确渲染,再也不用为因为状态变化,导致刷新过多,降低页面性能而烦恼。
  • 它可以帮我们监听页面内任意状态变化,并执行相关副作用处理,去改变组件的:值、组件 Props、选项、显隐状态等。
  • 它可以帮我们更好的复用逻辑,减少代码中大量的 if else 判断以及相互影响。
  • 它可以帮助我们解决复杂的业务场景难题,如:父子表单多层嵌套内外联动、数组表单增删等。
  • 在后续的文档中,我们将一一见识到上述好处。

EasyPage 的优势是什么?

简洁性

上述示例可以初步看到其简洁性,下面我们描述如下复杂逻辑,再看其简洁性。

  • 新建一个性别字段,两个选项:男、女
  • 当选中“男”时,出现字段:喜欢看的书(输入框)
  • 当选中“女”时,不出现任何内容。

/** 选项 **/
const manOption = nodeUtil.createNode('man', { name: '男' });
const womenOption = nodeUtil.createNode('female', { name: '女' })

/** 字段 **/
const sexField = nodeUtil.createField('sex','性别', { value: '', mode: 'single' })

/** 子表单字段 **/
const hobby =  nodeUtil.createField('like', '喜欢看的书', { value: '' })

export const sex = sexField.appendChildren([
    manOption.appendChildren([hobby]),
    womenOption,
  ]);

  • 若此场景有更简洁的写法,请在 Github 下评论。

高内聚、低耦合

你可以想象到的关于字段的一切,都可以在自身定义中,完整独立逻辑闭环。

以下还是以表单元素为一个实际的例子,来展示上述特点:

  • 新建一个年龄字段,以输入框展示,必填。

    import { Empty, InputEffectedType, nodeUtil } from '@easy-page/antd-ui';
    import React from 'react';
    
    export const age = nodeUtil.createField<
      string,
      { name: string },
      Empty,
      InputEffectedType
    >(
      'age',
      '年龄',
      {
        /** 默认值 */
        value: '',
        /** 必填 * 号 */
        required: true,
      }
    );
    
    
  • 【联动属性】基于姓名字段,来展示 placeholder: ${name} 的年龄

        import { Empty, InputEffectedType, nodeUtil } from '@easy-page/antd-ui';
        import React from 'react';
    
        export const age = nodeUtil.createField<string,{ name: string },Empty,InputEffectedType>(
          'age',
          '年龄',
          {
            ...,
            actions: [
              {
                effectedKeys: ['name'],
                /** 加载时,立即执行 */
                initRun: true,
                action: ({ effectedData }) => {
                  return {
                    effectResult: {
                      inputProps: {
                        placeholder: `${effectedData.name || '-'} 的年龄`,
                      },
                    },
                  };
                },
              },
            ],
        );
    
  • 【联动显隐】当 姓名=a 时,隐藏年龄

      import { Empty, InputEffectedType, nodeUtil } from '@easy-page/antd-ui';
      import React from 'react';
    
      export const age = nodeUtil.createField<string,{ name: string },Empty,InputEffectedType>(
        'age',
        '年龄',
        {
        ...,
          /** 字段显示与隐藏 */
          when: {
            effectedKeys: ['name'],
            show({ effectedData }) {
              return effectedData.name !== 'a';
            },
          },
        },
        {
          ...
        }
      );
    
    
    • 【联动提示】当 age < 10 时,提示:儿童

      import { Empty, InputEffectedType, nodeUtil } from '@easy-page/antd-ui';
      import React from 'react';
      
      export const age = nodeUtil.createField<string,{ name: string },Empty,InputEffectedType>(
        'age',
        '年龄',
        {
          ...
        },
        {
          /** 输入框配置 */
          input: { trigger: 'onChange' },
          /** FormItem 配置 */
          formItem: {
            /** 自定义提示语:带上下文 */
            customExtra: ({ value }) => {
              return <div>{value && +value < 10 ? '儿童' : ''}</div>;
            },
          },
        }
      );
      
      
    • 【提交时,数据处理】将数据处理成数字

      import { Empty, InputEffectedType, nodeUtil } from '@easy-page/antd-ui';
      import React from 'react';
      
      export const age = nodeUtil.createField<string,{ name: string },Empty,InputEffectedType>(
        'age',
        '年龄',
        {
          ...,
          /** 数据预处理 */
          postprocess(context) {
            return {
              age: Number(context.value),
            };
          },
          ...
        },
        {
          ...
        }
      );
      
      
  • 【联动校验】当 age < 0 || age > 200 时,提示:请输入合法年龄;当 姓名=pk 时,age > 200 合法;当姓名变化时,触发 age 校验。

    import { Empty, InputEffectedType, nodeUtil } from '@easy-page/antd-ui';
    import React from 'react';
    
    export const age = nodeUtil.createField<string,{ name: string },Empty,InputEffectedType>(
      'age',
      '年龄',
      {
        ...,
        /** 字段验证 */
        validate({ value, pageState }) {
          if (!value) {
            return { success: false, errorMsg: '请输入年龄' };
          }
          if (+value < 0 || (+value > 200 && pageState.name !== 'pk')) {
            return { success: false, errorMsg: '请输入合法年龄' };
          }
          return { success: true };
        },
        
      },
      {
        ...
        },
      }
    );
    
    

从上可见,上述字段逻辑比较复杂,依赖 name 的填写,做相关联动,但从头到尾,并没有去其他地方做任何事情,自身就可以把逻辑完全描述。
充分可见其:高内聚、低耦合特点。

  • 小惊喜:我们的 API 描述对于类型提示很友好哦!
    [图片上传失败...(image-ddde6e-1720432245229)]

复用性

对于组件复用,相信大家都不陌生,在这种场景下,我们可以抽离公共组件:

  • 梳理抽象业务逻辑、定义组件接口

但随着业务的愈发复杂,我们发现:

  • 【抽象难】组件很难抽象的很完美,几乎都会有不满足的情况。
  • 【场景复杂】组件内渐渐基于业务场景,逐步增多的 if else。
  • 【耦合】牵一发而动全身,有时改了某个组件,却影响了其他页面逻辑。

这种现象和问题在表单里体现的更加淋漓尽致。JAVA 面向对象思想中,提出了:继承的概念来解决复用性问题,但在前端 hooks 的写法下,很难实践。也造成了现在的困扰。

但,EasyPage 提出了针对这一场景的解决方案,以下还是承接上面的例子,来展示其复用性。

  • 继承 age 的属性,定义一个 newAge 字段,变化如下:
    • 字段名改为:我的年龄
    • 字段改为:非必填
    • 字段校验:允许为空
    • 字段显示:任何时候都展示
    • 字段增加 Tooltip 提示:这是新的年龄
import { nodeUtil } from '@easy-page/antd-ui';
import { age } from './age';

export const newAge = nodeUtil.extends(age, {
  name: '我的年龄',
  required: false,
  validate(oldValidate) {
    return (options) => {
      if (!options.value) {
        return { success: true };
      }
      return oldValidate?.(options);
    };
  },
  when(oldWhen) {
    return {
      effectedKeys: oldWhen?.effectedKeys || [],
      show(context) {
        return true;
      },
    };
  },
  fieldUIConfig: (oldConfig) => {
    const newConfig = { ...(oldConfig || {}) };
    newConfig.formItem = newConfig.formItem || {};
    newConfig.formItem.tooltip = '这是新的年龄';
    return newConfig;
  },
});

运行效果见官网。

可以看到,上述年龄字段继承了原来的所有逻辑,并进行了修改和更新,并和原来的代码无任何冲突和交集,也无需做任何的抽象处理。

扩展性

扩展性主要是从开发角度来描述,如何更灵活的应对各种场景。

首先,我们想创建一个输入框字段:

import { nodeUtil } from '@easy-page/antd-ui';

export const name = nodeUtil.createField('name', '姓名', {
  value: '',
  required: true,
});

一般创建的默认组件:

  • 输入型,默认:输入框组件
  • 选择型,默认:单选-RadioGroup、多选-CheckBoxGroup

其余,靠指定组件,如:写一个描述字段:

export const desc = nodeUtil.createField(
  'desc',
  '介绍',
  {
    value: '',
  },
  {
    /** 指定 TextArea 组件 **/
    ui: UI_COMPONENTS.TEXTAREA,
  }
);

  • @easy-page/antd-ui 包里的组件即目前所支持的组件。

如果默认的组件,还无法满足要求。此时,有两种方式:

  • 采用自定义节点完成
    export const desc1 = nodeUtil.createCustomField(
      'desc',
      '介绍',
      ({ value, onChange }) => {
        /** 自定义输入框组件 */
        return <Input value={value} onChange={onChange} />;
      },
      {
        value: '',
      },
    );
    
  • 扩展一个通用组件,再通过 ui 属性进行配置。

我们除了可以:扩展一个通用组件外,可能还因为不同的公司,要求的基础组件库不一样,如:

  • 在字节可能是 arco、在阿里可能是 antd

我们可以参考:@easy-page/antd-ui 扩展一个自己的组件库,扩展成本大概在 1 - 2 天左右。

  • 在框架的设计上,预留了支持 vue 的可能
  • 除了编写页面外,做 cli 的问题列表开发,也可以扩展

EasyPage 和 Formily

相同点

  • 都是基于 Schema 描述 + 解析引擎模式渲染页面
  • 都能做到精确渲染

差异

  • 定位不同
    • 我们专注于研发提效,面向代码开发,不面向低代拖拽。本质是:更优雅、更简洁的写表单,而不是不用代码拖拽搭建表单,生成静态代码。
  • 面向场景不同
    • 我们不单单解决表单开发的问题,更解决前端页面开发的问题。
  • Schema 设计不同
    • Formily 最初设计更倾向于用 Schema 描述一切,Schema 比较庞大,只能是 JSON 模式,而非 JS。
    • EasyPage 只描述数据和数据之间的关系,Schema + UIConfig,数据和 UI 信息分离,Schema 比较简洁,是 JS 协议,非纯粹 JSON Schema。
  • 解决研发提效思路不同
    • Formily 更倾向于通过:Schema 描述(拖拉拽等方式)减少基础代码开发量,提升开发效率。
    • 我们则是倾向于建立研发标准,减少代码量,在具有非常好的可维护性同时,提升研发效率。
  • 使用方式不同
    • Formily 可以通过:拖拉拽、组件描述、schema 描述等多种方式来开发页面。
    • EasyPage 通过轻量的 API,来开发页面。(后续会支持更赞的方式,埋一个彩蛋!)
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 227,283评论 6 530
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 97,947评论 3 413
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 175,094评论 0 373
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 62,485评论 1 308
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 71,268评论 6 405
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 54,817评论 1 321
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 42,906评论 3 440
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 42,039评论 0 285
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 48,551评论 1 331
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 40,502评论 3 354
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 42,662评论 1 366
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 38,188评论 5 356
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 43,907评论 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 34,304评论 0 25
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 35,563评论 1 281
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 51,255评论 3 389
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 47,637评论 2 370

推荐阅读更多精彩内容