关于前端:疯了吧这帮人居然用-Go-写前端二

6次阅读

共计 5571 个字符,预计需要花费 14 分钟才能阅读完成。


作者 | 郑嘉涛(羣青)
起源|尔达 Erda 公众号

前言

上篇咱们讲了故事产生的背景,也简略论述了组件及协定的构想:

一、丰盛的通用组件库。
二、组件渲染能力,将业务组件渲染成通用组件。
三、协定渲染能力,以解决简单交互。

以及这种开发模式带来的益处:

这样的设计初衷旨在大量缩小前端工作,尤其是前后端对接方面,甚至能够认为对接是“反转”的,体现在两个层面:接口定义的反转和开发时序的变动。

如果你对咱们的设计思路还不够理解,能够先浏览上篇:《疯了吧!这帮人竟然用 Go 写“前端”?(一)》。

本篇我将更粗疏地介绍组件渲染和协定渲染,以及如何通过这两种渲染做到前端彻底不关注业务。

当然最初你会发现是否 REST 并非重要,重要的是正当的切分关注点,而框架只是使用切分的帮忙伎俩。

组件渲染

具体而言,针对一个通用组件,如何实现业务逻辑?

比如说上面同样的一个卡片组件(Card),它由通用的元素形成和出现:

cardComp:
  props:
    titleIcon: bug-icon
    title: Title
    subContent: Sub Content
    description: Description

然而,通过不同的 props,能够渲染出不同的场景。

场景 1:需要卡片

kanbanCardComp:
  props:
    titleIcon: requirement-icon
    title: 一个简略的需要
    subContent: 实现容器扩容不抖动
    description: 须要存储记录用户的扩容改变,通过调用外部封装的 k8s 接口以实现。

场景 2:打包工作卡片

taskCardComp:
  props:
    titleIcon: flow-task-icon
    title: buildpack (java)
    subContent: ✅ success
    description: time 02:09, begin at 10:21 am ...

对于后端来说,只须要遵循通用组件的数据定义,依据组件渲染器的规定,实现渲染办法即可(须要强调的是,后端不须要晓得 UI 的长相,后端面对的始终是数据)。

func Render(ctx Context, c *Comp) error {
  // 1. query db or internal service
  // 2. construct comp
  return nil
}

在交互方面,咱们也须要通用组件定义所有的操作,操作(operation)能够认为是交互的影响或者说后果。举个例子,其实查问渲染就是最根底的一种操作;而对于需要卡片来说,点击查看详情,右上角的删除、编辑等都是操作:

不过在通用组件层面,无需感知业务,定义的都是通用的 click, menu-list 等操作,由业务组件实现具体的业务。

前端在出现层表述的交互(比方悬浮、点击、开释等),最终都会对应到通用组件定义的操作,而操作即是一次规范的组件渲染申请。能够这么思考:假如页面曾经出现在用户背后了,用户通过鼠标(也可能是触摸板)触发的浏览器交互事件,都由前端“出现器”翻译成组件操作(operation),比如说删除操作,一旦执行操作组件便会触发从新渲染。

上面的伪代码表述了操作在渲染中的体现:

// 伪代码,精简了数据结构和条件判断
func Render(ctx Context, c *Comp, ops string) error {
  if ops != "view" {doOps()
  }
  // continue render (aka re-render)
  return nil
}

是不是缺了点什么?没错,后端也无奈凭空变出一个卡片。组件渲染必须要有输出的局部,可能是用户间接或者间接的输出。比方用户说:“我想要看 id=42 的需要卡片”,这就是间接的输出,个别会在 url 上体现。另一种状况则是间接的输出:“我想要看 status = DONE 的所有需要卡片“,那么针对某一张需要卡片而言,它所需的 id,是从另一个组件 – 需要列表中取得的。

具体这个数据怎么在组件间绑定,咱们会在后续章节(协定渲染)中具体论述。当初只须要晓得,对于单个组件的渲染(也就是业务组件)而言,咱们标准了开发者只须要定义组件渲染必要的输出。这是一个很有吸引力的做法,通过参数屏蔽外界逻辑,可能无效地做到高内聚和低耦合。

当然有输出就有输入(要晓得数据绑定必定是把一个组件的输入绑定在另一个组件的输出)。当然交互其有状态的个性(在协定渲染中会具体论述),咱们最终让输入输出合并在一个 state 中体现,依然是需要卡片的例子:

kanbanCardComp:
  props:
    titleIcon: requirement-icon
    title: 一个简略的需要
    subContent: 实现容器扩容不抖动
    description: 须要存储记录用户的扩容改变,通过调用外部封装的 k8s 接口以实现。state:
    ticketId: 42

最初一张大图来总结一下组件的渲染过程:

协定渲染

这里咱们须要引申一个理论的问题,以 web ui 为例:当用户拜访一个页面时,这个页面并非只有一个组件,比方事项看板页面,就有诸如过滤器、看板甬道、事项卡片、类型切换器等多个组件。

并且,有个头疼的问题:组件之间显然是有联动的。比方过滤器的过滤条件管制了看板甬道的列表后果。
传统的 web 开发,这些联动必定是由前端代码来实现的。但如果前端来实现这些联动关系,显然就须要深度了解和参加业务了,这与咱们整个设计思路是违反的。

这里须要咱们有个清晰的认知:在理论的场景中,绝不是标准化单个组件的构造后,前后端就能彻底拆散的。换言之,仅将构造的定义由后端转移到前端,只达成了一半:在动态层面解耦了前后端。

而另一半,须要咱们将组件间联动、对组件的操作、操作导致从新渲染等,也能由渲染器进行适合解决,也就是在动静层面解耦前后端。

在讲组件渲染的时候咱们刻意留了一个悬念:为了放弃组件的高内聚低耦合,咱们将组件须要的所有输出都参数化,并将输出和输入参数合称为“状态”(state)。那如何将参数、状态串联起来,实现整个页面的逻辑呢?

想想其实也很简略,咱们须要有一个协定去标准定义这些依赖关系和传递形式,详见如下模式。

protocol.yaml:

// 组件初始值
component:
  kanbanCardComp:
      state:
      // ticketId: ??
    operations:
      click:
        reload: true
  ticketDetailDrawerComp:
    state:
      visible: false
      // ticketId: ??
    operations:
      close:
        reload: true
// 渲染过程
rendering:
  __Trigger__:
    kanbanCardComp:
      operations:
        click: set ticketDetailDrawerComp.state.visible = true
    ticketDetailDrawerComp:
      operations:
        close: set ticketDetailDrawerComp.state.visible = false
  __Default__:
    kanbanCardComp:
      state:
        ticketId: {{url.path.2}}
    ticketDetailDrawerComp:
      state:
        ticketId: {{kanbanCardComp.state.ticketId}}

在进行协定渲染时,首先 执行 __Trigger__ 局部,操作类型的渲染会临时性地批改局部组件的状态;其次 执行 __Default__ 局部,进行组件之间的数据绑定;最初 会进行单个业务组件的渲染,这部分在第一篇文章中曾经具体论述。

不过最终须要将这个协定渲染之后给到前端,因为 rendering 不过只是过程数据,最终须要转化成平庸的值。以这个例子而言,(假如用户进行了卡片的 click 操作)协定最终渲染成:

component:
  kanbanCardComp:
    props:
      // 后端组件基于 ticketId=42 渲染出的具体数据
      titleIcon: requirement-icon
      title: 一个简略的需要
      subContent: 实现容器扩容不抖动
      description: 须要存储记录用户的扩容改变,通过调用外部封装的 k8s 接口以实现。state:
      ticketId: 42
    operations:
      click:
        reload: true
  ticketDetailDrawerComp:
    props:
      // 后端组件基于 ticketId=42 渲染出的具体数据
      ...
    state:
      visible: true
      ticketId: 42
    operations:
      close:
        reload: true

值得强调的一点是,前端不须要晓得组件之间的联动。所有的联动,都通过从新渲染来实现。这意味着,每次操作,会导致从新渲染这个协定。而从外部来说,则是先进行操作的落实(比方删除、更新),即调用确定的接口执行操作,而后进行场景的从新渲染。

简略的说就是前端每次产生操作,只有通知后端我操作了什么(operation),后端执行操作之后立即刷新页面,当然理论的流程会略微简单。

从上图中咱们能够看到,每次的操作是十分“短视”的,尤其是前端能够说只须要“通知”后端做了什么操作,别的一律无需通晓。那么就会有人问了:如果某次操作须要传递数据怎么办?比方传统的对接形式,如果要删除一个资源,前端就必须传入后端资源的 ID。那就须要讲到协定必须要有的一个个性:状态。

RESTful API 是无状态的,然而业务逻辑须要有先后顺序,势必就须要存在状态。传统的做法是由前端维系这个状态,尤其是 SPA 更是将所有的状态都维系在内存。

举个例子,比方一个编辑表单,首先关上表单之后,前端须要调用后端接口传入资源 ID 获得数据,并将数据 copy 进表单进行渲染;当保留按钮 click 触发时,须要获得表单中以后值,并调用后端 save 接口进行保留。

咱们晓得,以后端不关怀业务时,状态的维系也随之破碎。这个状态必须要下沉到和渲染同一个地位,精确的说是协定渲染这一层(因为组件单体咱们刻意设计成内聚和无状态)。

如何做到状态的下移呢?其实也非常简单,咱们晓得一个事实,那就是操作之前必然渲染(也就是只有拜访了页面能力在页面上点击)。咱们只须要在渲染的时候提前预判之后操作所须要的全副数据,提前内置在协定中;而前端在执行操作时,将协定以及操作的对象等信息悉数上报即可。当组件渲染器接管到这个协定的时候,是能够拿到所有须要的参数的(因为原本就是我本人为本人筹备的),此时执行完操作后,就开启下一个预判,并从新渲染协定给予前端进行界面出现。

上面的例子中,能够看到当用户进入第一页(currentPageNo = 1)时,咱们早已料到用户会进行下一页(next)操作,就曾经把这个操作所须要的参数(pageNo = 2)置于协定之中了;随后用户针对组件 paginationBar 进行了一次操作 next,操作解决时便能拿到所需数据。

components:
  paginationBar:
    state:
      currentPageNo: 1
    operations:
      next:
        reload: true
        meta:
          pageNo: 2

所谓的“早已想到”并非难事,因为各个业务组件中会定义此业务组件实现了通用组件的那些操作,咱们要求在定义这些操作的时候,必须要定义这些操作所必须要的外界传入参数(之所以说外界,是因为有些业务参数在组件外部就能够自行处理,而无需依赖内部组件,比方 state 或者 props 的数据信息曾经短缺)。

最初针对出现而言,还须要补充组件之间的层级关系,最终造成一个树形的关系,为了布局也须要填充一些“无意义”的组件像 Container、LRContainer 等:

不过这些都是动态的数据,能够间接放入协定,也无需渲染:

hierarchy:
  root: ticketManage
  structure:
    ticketManage:
      - head
      - ticketKanban
    head:
      left: ticketFilter
      right: ticketViewGroup

components:
  ticketManage:
    type: Container
  head:
    type: LRContainer
  ...

临时告一段落

咱们通过组件渲染、协定渲染以及一个通用组件库实现了彻底的前后端拆散。不过咱们在实践中发现,很多时候彻底的前后端拆散会带来肯定的艰难,这也是咱们将认为协定承载的是场景而非页面。

如果是彻底的前后端拆散,那势必整个页面甚至整个网站就应该是一个协定,因为只有跳出协定或者说页面间切换,就会有业务含意。但真实情况是,如果一个协定中有太多的组件须要编排,这个简单编排对于开发者而言是十分繁琐的,并且这个复杂性带来的损失齐全吞没彻底前后端拆散带来的劣势。

从求实角度登程,咱们更应该实际“关注点拆散”而非是彻底的“前后端拆散”。在设计组件以及协定时,咱们总是问本人:

  • 前端关注什么?
  • 后端关注什么?
  • 框架 / 协定应该关注什么?

最终咱们框架抉择和传统对接形式共存的模式,并且可能敌对地相互操作。

比方前端在出现一个组件的时候,能够抉择“偷偷”调用一些 RESTful API 来实现特定的事件,也能够在一个页面中“拼凑“多个协定进行联动等等。

咱们也发现,当大量业务逻辑可能从前端下沉到后端时,前端出现层的逻辑将变得非常简单(数量无限的组件)。咱们意外获得了多端反对能力,比方能够实现 CLI 的出现层,也能够实现 IDE 插件的出现层等等。

当然咱们当初并没有实现这些,不过置信如果是聪慧的你,实现这个不难吧~

目前 Erda 的所有代码均已开源,真挚的心愿你也可能参加进来!

  • Erda Github 地址:https://github.com/erda-project/erda
  • Erda Cloud 官网:https://www.erda.cloud/
正文完
 0