疯了吧!这帮人居然用 Go 写“前端”?(二)
作者 | 郑嘉涛(羣青)
来源|尔达 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/
疯了吧!这帮人居然用 Go 写“前端”?(二)的更多相关文章
- 疯了吧!这帮人居然用 Go 写“前端”?(一)
作者 | 郑嘉涛(羣青) 来源 | 尔达 Erda 公众号 无一例外,谈到前后端分离"必定"是 RESTful API,算是定式了.但我们知道 REST 在资源划分上的设计总是 ...
- 一年三篇IF大于7的牛人告诉你怎么写SCI
一年三篇IF大于7的牛人告诉你怎么写SCI 1 研究生必备四本 俗话说好记性不如烂笔头,所以一定要首先养成做笔记的好习惯!作为研究生下面这几个本子是必不可少的: 1.实验记录本(包括试验准备本),这当 ...
- PIC12F629帮我用C语言写个程序,控制三个LED亮灭
http://power.baidu.com/question/240873584599025684.html?entry=browse_difficult PIC12F629帮我用C语言写个程序,控 ...
- 第一章-第六题(帮人抢票,帮人选课这些软件是否合法 你怎么看?)--By梁旭晖
我觉得这些软件是合法的,符合道德规范的. 计算机当初设计的初衷就是简化甚至替代人类的工作.而软件作为计算机硬件的驱动着,其设计就是体现这些原则. 现在互联网上的订票,选课类型的网站还是有很多的,比如: ...
- 大半夜吃饱了撑的帮人调IE玩
那高手的也是IE6,我也是IE6,但是他的IE6就总是进recv,我的IE6就进WSARecv,一点都不科学...擦..不调了.
- 期末人福音——用Python写个自动批改作业系统
一.亮出效果 最近一些软件的搜题.智能批改类的功能要下线. 退1024步讲,要不要自己做一个自动批改的功能啊?万一哪天孩子要用呢! 昨晚我做了一个梦,梦见我实现了这个功能,如下图所示:功能简介:作对了 ...
- 用ABP只要加人即可马上加快项目进展(二) - 分工篇
2018年和1998年其中两大区别就是: 前端蓬勃发展, 前后端分离是一个十分大的趋势. 专门的测试人员角色被取消, 多出了一个很重要的角色, 产品经理 ABP只要加入即可马上加快项目进展, 选择 ...
- sublime text帮你更好的写python
在Google的Python风格指南中,有这样的要求: 用4个空格来缩进代码 但是每次在敲代码的时候,用一个tab确实比敲四次空格方便的多.令人欣慰的是sublime text 2能够把tab转换成4 ...
- Java课程寒假之《人月神话》有感之二
一.外科手术队伍 即建立一个合理的团队,按照书上的说法就是,在开发一个大的系统的时候,原本精英的团队就可能无法在较短的时间内完成一个大型的程序,在这样的条件下,必须扩大团队的规模,即使这个精英程序员的 ...
随机推荐
- ESXi 6.7 的https服务挂掉处理方法 503 Service Unavailable
首先进入EXSi开启SSH(ESXi的主机控制台,非web控制台,是安装esxi的控制台) 然后 /etc/init.d/hostd status 显示已停止, 使用 /etc/init.d/host ...
- 修改 oracle 数据库的 sys 账号密码,ERROR at line 1: ORA-01034: ORACLE not available
挺久没有登录的 oracle 数据库,因为公司要求加固密码,登录后修改失败 1.启动数据库的同时启动控制文件.数据文件,提示:cannot mount database in EXCLUSIVE mo ...
- windows 系统文件夹挂载到 Linux 系统,拷贝(发送)文件到 windows 系统,实现异地备份
1.在windows 系统上配置好共享文件夹,用来接收Linux 系统的文件 注意:关闭windows 系统防火墙,或者添加进出站规则 2.在Linux 系统中,创建需要拷贝的文件目录 #mkdi ...
- 在代码生成工具Database2Sharp中增加Vue&Element 工作流页面的快速生成
在我们基于框架开发系统的时候,往往对一些应用场景的页面对进行了归纳总结,因此对大多数情况下的页面呈现逻辑都做了清晰的分析,因此在我们基于框架的基础上,增量式开发业务功能的时候,能够事半功倍.代码生成工 ...
- 解读Java8的Thread源码
1.创建的一个无参的Thread对象,默认会有一个线程名,以Thread-开头,从0开始计数,采用了一个static修饰的int变量,当对象初始化一次时一直存放在jvm方法区中 2.构造Thread的 ...
- CCCC-exercise
CCCC-exercise 1.L1 总结L1 1-27里面我觉得有东西可以总结的题目 贴了部分的代码 L1-006(20) 一个正整数 N 的因子中可能存在若干连续的数字.例如 630 可以分解为 ...
- Django 小实例S1 简易学生选课管理系统 4 实现登录页面
Django 小实例S1 简易学生选课管理系统 第4节--实现登录页面 点击查看教程总目录 作者自我介绍:b站小UP主,时常直播编程+红警三,python1对1辅导老师. 本文涉及到的新的额外知识点: ...
- 教你用SQL进行数据分析
摘要:采用 SQL 作为数据查询和分析的入口是一种数据全栈的思路. 本文分享自华为云社区<如何使用 SQL 对数据进行分析?>,作者:zuozewei . 前言 我们通过 OLTP(联机事 ...
- 接口返回图片,前端生成临时url实现展示、下载效果
请求一个后端接口 返回一张图片(打印后发现是二进制流) 瞬间不开心了(为什么不能后端处理好再让前端调用呢) 不过丝毫不慌好吧 先说处理逻辑:首先要将获取到的数据转换,这边选择以blob形式进行转换 主 ...
- OpenCV常用操作函数大全
https://blog.csdn.net/Vici__/article/details/100714822 目录 cv2常用类: 1.图片加载.显示和保存 2.图像显示窗口创建与销毁 3.图片的常用 ...