作者 | 郑嘉涛(羣青)

来源|尔达 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 的所有代码均已开源,真挚的希望你也能够参与进来!

疯了吧!这帮人居然用 Go 写“前端”?(二)的更多相关文章

  1. 疯了吧!这帮人居然用 Go 写“前端”?(一)

    作者 | 郑嘉涛(羣青) 来源 | 尔达 Erda 公众号 ​ 无一例外,谈到前后端分离"必定"是 RESTful API,算是定式了.但我们知道 REST 在资源划分上的设计总是 ...

  2. 一年三篇IF大于7的牛人告诉你怎么写SCI

    一年三篇IF大于7的牛人告诉你怎么写SCI 1 研究生必备四本 俗话说好记性不如烂笔头,所以一定要首先养成做笔记的好习惯!作为研究生下面这几个本子是必不可少的: 1.实验记录本(包括试验准备本),这当 ...

  3. PIC12F629帮我用C语言写个程序,控制三个LED亮灭

    http://power.baidu.com/question/240873584599025684.html?entry=browse_difficult PIC12F629帮我用C语言写个程序,控 ...

  4. 第一章-第六题(帮人抢票,帮人选课这些软件是否合法 你怎么看?)--By梁旭晖

    我觉得这些软件是合法的,符合道德规范的. 计算机当初设计的初衷就是简化甚至替代人类的工作.而软件作为计算机硬件的驱动着,其设计就是体现这些原则. 现在互联网上的订票,选课类型的网站还是有很多的,比如: ...

  5. 大半夜吃饱了撑的帮人调IE玩

      那高手的也是IE6,我也是IE6,但是他的IE6就总是进recv,我的IE6就进WSARecv,一点都不科学...擦..不调了.

  6. 期末人福音——用Python写个自动批改作业系统

    一.亮出效果 最近一些软件的搜题.智能批改类的功能要下线. 退1024步讲,要不要自己做一个自动批改的功能啊?万一哪天孩子要用呢! 昨晚我做了一个梦,梦见我实现了这个功能,如下图所示:功能简介:作对了 ...

  7. 用ABP只要加人即可马上加快项目进展(二) - 分工篇

    2018年和1998年其中两大区别就是: 前端蓬勃发展, 前后端分离是一个十分大的趋势. 专门的测试人员角色被取消, 多出了一个很重要的角色, 产品经理   ABP只要加入即可马上加快项目进展, 选择 ...

  8. sublime text帮你更好的写python

    在Google的Python风格指南中,有这样的要求: 用4个空格来缩进代码 但是每次在敲代码的时候,用一个tab确实比敲四次空格方便的多.令人欣慰的是sublime text 2能够把tab转换成4 ...

  9. Java课程寒假之《人月神话》有感之二

    一.外科手术队伍 即建立一个合理的团队,按照书上的说法就是,在开发一个大的系统的时候,原本精英的团队就可能无法在较短的时间内完成一个大型的程序,在这样的条件下,必须扩大团队的规模,即使这个精英程序员的 ...

随机推荐

  1. 决策树 机器学习,西瓜书p80 表4.2 使用信息增益生成决策树及后剪枝

    使用信息增益构造决策树,完成后剪枝 目录 使用信息增益构造决策树,完成后剪枝 1 构造决策树 1 根结点的选择 色泽 信息增益 根蒂 信息增益 敲声 信息增益 纹理 信息增益 脐部 信息增益 触感 信 ...

  2. 密码学基础:AES加密算法

    [原创]密码学基础:AES加密算法-密码应用-看雪论坛-安全社区|安全招聘|bbs.pediy.com 目录 基础部分概述: 第一节:AES算法简介 第二节:AES算法相关数学知识 素域简介 扩展域简 ...

  3. tar 解压分割压缩文件

    被分割后的压缩文件必须先合并成一个压缩文件才能正常的解压. 第一步.合并压缩文件 第二步.正常解压 $ls TINA-1.3.tar.gzaa TINA-1.3.tar.gzab TINA-1.3.t ...

  4. pycharm软件安装和破解

    pycharm安装 1. 进入pycharm的官网 --- 下载专业版的pycharm 2. 双击下载好的软件,下一步 3. 选择需要安装软件的路径 --- 注意: 尽量不要将软件装在C盘里 4. 默 ...

  5. 四种 AI 技术方案,教你拥有自己的 Avatar 形象

    大火的 Avatar到底是什么 ? 随着元宇宙概念的大火,Avatar 这个词也开始越来越多出现在人们的视野.2009 年,一部由詹姆斯・卡梅隆执导 3D 科幻大片<阿凡达>让很多人认识了 ...

  6. namaspace之pid namespace

    认识Namespace namespace 是 Linux 内核用来隔离内核资源的方式.通过 namespace 可以让一些进程只能看到与自己相关的一部分资源,而另外一些进程也只能看到与它们自己相关的 ...

  7. win10 vscode安装babel

    第一步:安装 babel-cli cd进入项目根目录,执行命令: npm install --global babel-cli 第二步:检测第一步是否成功,输入命令 babel --version,若 ...

  8. es聚合查询语法

    {    "size": 0,    "query": {        "bool": {            "filter ...

  9. vue.js3 学习笔记 (一)——mixin 混入

    vue 2 中采用选项式API.如:data.methods.watch.computed以及生命周期钩子函数等等. mixin 混入,提供了一种非常灵活的方式,来分发 vue 组件中的可复用功能,一 ...

  10. Python基础(条件判断)

    # age = 103 # if age < 90: # print('%s小于90' %age) # elif age > 90 and age < 95: # print('%s ...