chenos / blog

https://chenl.in/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

配置化的开发方式是什么样的(一)

chenos opened this issue · comments

过去的开发模式

以登录模块为例,登录可能的几种场景:

  1. 用户名、密码登录
  2. 手机+验证码登录
  3. 第三方登录

通常情况,需要做这么些事情:

  1. 建一张用户数据表,如果是复杂情况,可能还会有相关的关系表
  2. 后端实现基本的 MVC 之后,提供相关 API 接口给客户端调用
  3. 客户端实现登录相关 UI 界面
  4. 前后端调试,这个过程可能需要前后端一起配合

可能存在的一些细节

  • 前后端都需要对表单字段进行校验
  • 可能需要实现各种客户端的 UI
  • 实现相关功能模块的单元测试
  • 实现用户注册、用户找回密码等流程

为了完成这样一个模块,我们可能需要找一个全栈,或者一个后端+N个前端来完成相关工作,那怎么可以简化流程或者提升效率呢?

配置化的开发方式是什么样的?

回到刚才提到的登录模块,从用户视角,这个页面可能存在的一些元素:

  • 登录页面
    • LOGO
    • 表单字段
      • 用户名字段
      • 密码字段
      • 其他字段
    • copyright 文本

转换成机器可阅读的 json 方式:

{
  "type": "page",
  "title": "登录页面",
  "body": [
    {
      "type": "image",
      "url": "http://imgurl"
    },
    {
      "type": "form",
      "apiAction": {
        "method": "post",
        "url": "/login",
      },
      "controls": [
        {
          "type": "text",
          "name": "username",
          "required": true
        },
        {
          "type": "password",
          "name": "username",
          "required": true
        }
      ]
    },
    "copyright 文本"
  ],
}

转换的目的是为了让机器可以读懂,这份 json 配置就可以在各端共享

  • 客户端通过配置把相对应的组件输出到界面
  • 服务端通过配置实现表单字段的验证

服务端的配置

刚上面的 json 配置是从客户端用户角度阐述的,完善一下服务端部分吧。尝试把 Routing 也放到配置里,简单改造之后:

{
  "type": "form",
  "apiAction": {
    "method": "post",
    "uri": "/login",
    // 加了一行 action 来适配路由
    "action": "AuthController@login",
  },
  "controls": [
    {
      "type": "text",
      "name": "username",
      "required": true
    },
    {
      "type": "password",
      "name": "password",
      "required": true
    }
  ]
}

登录的情况,直接映射 action 方法是比较合适的,实际情况比较复杂,所以有必要,重点介绍一下 apiAction 的各种用法。

比如我想直接通过 sql 输出某些字段的数据(实际场景直接 Sql 是有风险的操作,可以把 Query Builder 结合进来)

{
  "type": "table",
  "apiAction": {
    "type": "sql",
    "sql": "select id, username from users limit 20",
  },
  "columns": [
    {
      "name": "id",
      "label": "ID",
    },
    {
      "name": "username",
      "label": "用户名",
    }
  ]
}

Graphql 场景。想象空间巨大,非常适合查询 api 的描述,而且跨平台。如获取 id=4 的用户的名字。

{
  "apiAction": {
    "type": "graphql",
    "graphql": "
      {
        user(id: 4) {
          name
        }
      }
    ",
  }
}

标准的 rest 情况

{
  "apiAction": {
    "rest": true,
    // 指定 model 或者 table
    "model": "App\\User",
    "table": "users",
    "method": "get",
    "uri": "/users"
  }
}

查询数据,输出表格,字段直接由 columns 提供

{
  "type": "table",
  "apiAction": {
    "rest": true,
    "method": "get",
    "uri": "/users"
  },
  "columns": [
    {
      "name": "id",
      "label": "ID",
    },
    {
      "name": "username",
      "label": "用户名",
    }
  ]
}

表单插入数据

{
  "type": "form",
  "apiAction": {
    "rest": true,
    "method": "post",
    "uri": "/users"
  },
  "controls": [ // 字段直接由 controls 提供
    {
      "type": "password",
      "name": "username",
      "required": true
    },
    {
      "type": "password",
      "name": "password",
      "required": true
    }
  ]
}

表单修改数据

{
  "apiAction": {
    "rest": true,
    "method": "put",
    "uri": "/users/{id}"
  },
  "controls": [ // 字段直接由 controls 提供
    {
      "type": "text",
      "name": "username",
      "required": true
    },
    {
      "type": "password",
      "name": "password",
      "required": true
    }
  ]
}

点击按钮,删除数据

{
  "type": "button",
  "label": "删除",
  "confirmText": "确定删除这条数据吗?",
  "apiAction": {
    "rest": true,
    "method": "delete",
    "uri": "/users/{id}"
  }
}

apiAction 的就说这些了,其他情况任由你想象了。

如何前后端共享配置

由服务端配置各个页面的 json schema,提取 schema 的 apiAction,生成对应的 router 供客户端 api 调用。

在前端的表现:用户打开某个 url,客户端把 url 转发给服务器,服务器识别 url 之后,输出对应的 json 配置,响应给客户端,客户端通过 json 配置渲染界面。这种方式也带来了一点好处,服务端可以控制客户端 router 了(有很大想象空间),权限的控制比在客户端处理便捷了许多。

配置化的好处

一次编写,多处使用。配置只是用来描述页面、资源或者行为等,具体的语言并没有限制,配置也可以理解为是一种协议,为各种跨平台提供便捷。比如有各种客户端需求,iOS、Android、各平台小程序等,服务端也可能不只是用 PHP 来实现。

配置化在前端的意义

回到最上面的登录页面,以 React 为例,代码如下:

import Renderer from './Renderer';

<Renderer schema={{
  "type": "page",
  "title": "登录页面",
  "body": [
    {
      "type": "image",
      "url": "http://imgurl"
    },
    {
      "type": "form",
      "apiAction": {
        "method": "post",
        "url": "/login",
      },
      "controls": [
        {
          "type": "text",
          "name": "username",
          "required": true
        },
        {
          "type": "password",
          "name": "password",
          "required": true
        }
      ]
    },
    "copyright 文本"
  ],
}}/>

还可以用一种更 Markup 的风格来书写:

import { Page, Image, Form } from 'Renderer';

<Page title={'title'}>
  <Image url={'http://imgurl'}/>
  <Form apiAction={{
    "method": "post",
    "url": "/login",
  }}>
    <Form.Text name={'username'} required/>
    <Form.Password name={'username'} required/>
  </Form>
  copyright 文本
</Page>

Page, Image, Form 并不是真实的组件,而是一种 Markup,用来描述配置/节点而已,真实的组件可以根据实际使用的 ui 组件注入,如:

registerRenderer('page', () => ...);

牵扯的细节很多,就不扩展了,这种书写风格带来的意义是,UI 组件抽离出来了,可以自由的注册各种组件进来,比如 web 端用 web 端的组件,小程序端用小程序的组件,React Native 也可以自由扩展。组件库可以不断壮大,这个组件库是可持续发展的,一次编写,多处使用。

组件的维护是集中、可持续的,第一个人编写,剩余的人只要拿来用就行了,只要大家都遵循一种标准即可,只要遵循标准的变化都可以采纳。维护这样的开发秩序,第一个项目我们可能需要从零开始,10 个项目之后,我们就是站在巨人的肩膀上。

回到最上面,如果我们只需要编写三四个配置文件,就实现了完整的用户登录、登录之后查看用户列表,以及用户的增删改查,而且这个模块多客户端通用,WEB 端、小程序端、APP 端都可以使用。

我们还需要一后端+N前端吗?

可能很多人就要因此下岗了。

开头,先说明一点,我相信有一类人会有这样一个想法:我就一程序员,你让我写晦涩的配置,情何以堪。

我承认配置在某种程度可能会有些封装过度或者矫枉过正,但是恰到好处的配置,绝对是开发利器,以上思路部分细节讲的不深入,大家理解的也可能有偏差,欢迎讨论。