软件架构之前后端分离的外行一知半解

作为一个产品经理,我正式在代码层面深入了解一个软件项目时,接手的就是一个前后端一体的WMS的项目。当时并不理解何为前后端一体或前后端分离,直到后来接手了前后端分离的项目,有了对比才有了理解。

我的WMS项目是基于 JSF(JavaServer Faces)/RichFaces 开发的,在 2018 年接手时,第一感觉就是信息密度大,响应快,但界面很丑,尤其默认字体使用的是衬线字体,控件类型或者控件支持的属性不够丰富。当时就想为什么 UI 设计不能像现在很多网站那样做的好看一些。后来接手的前后端分离 Vue/ViewUI 的项目,界面相对漂亮,但是响应速度慢,信息密度小,给人的感觉就是中看不中用。

另一点感知到的不同是根据前端定位后端逻辑上:JSF 架构的项目,前端控件绑定的是后端的某个 action 的某个方法,后端的 action 然后调用 service 层,service 层再调用 impl 层,impl 层再调用 dao 层, dao 层再调用数据库,或者是利用 hibernate 框架生成的 sql 操作数据库;而前后端分离的项目,前端无一例外都是通过 HTTP 请求调用后端 API,后端返回一个 JSON 实体。下面分别是两个例子:

<!-- 对比前后端一体 -->
<h:commandButton value ="确认-分拣框" styleClass="btnCla btn-m" action="#{reCheckAction.startCkeckBySortContainer}" id="startCkeckBySortContainerBtn" style="font-size: 16px;"/>

<!-- 对比前后端分离 -->
methods: {
            getQuotationProduct() {
                this.sceneList = [];
                this.sceneLoading = true;
                postAction(basePath + '/quotationScene/getQuotationProductByContractNo/' + this.contract.contractNo).then(res => {
                    this.sceneList = res.data;
                    this.setMergeCells();
                    setTimeout(()=>{this.$refs.vTable.$refs.xTable.setAllRowExpand(true)},500)
                }).catch(e => {
                    console.log(e);
                }).finally(f=>{
                    this.sceneLoading = false;
                })
            }
}

对于这些不同架构的项目,我之前始终有着这些困惑:

  1. 前后端是否分离的关键点在哪里?
  2. 为什么前后端一体的 UI 设计不能跟随时代审美变化更新成最新最好看的组件库?
  3. 我看上了某个组件库独有的控件,不能直接拿来用吗?
  4. 为什么 JSF 架构响应快,前后端分离的明显慢?
  5. 前后端分离的项目如果开发、产品也分离,需求文档怎么写,怎么沟通?怎么分工?

得益于和 Gemini 的对话,我这个外行对前后端是否分离有了下面这些理解。

关键区别点在于能否项目拆分,分别部署

前后端一体 (JSF/RichFaces):前端(.xhtml)与后端(.java)在同一个工程,因为前端的页面要直接调用后端 Java 对象方法,所以必须捆绑在一起才行。当然对应的开发势必是同一个人。如果要换前端框架,后端就要跟着一起重构。任何前端或后端的调整,整个项目都必须重新发布,用户感知会很明显。

前后端分离:前端(.html)与后端(.java)可以在不同的工程,对应的开发也可以分给不同的人合作,因为前端的页面通过 HTTP 请求调用后端 API,后端返回 JSON 数据,和两个独立系统之间通过接口通信没什么不同,所以可以独立开发、独立部署。前后端由于是分离的,只要二者间的接口定义未发生变化,任何一方都可以独立改动独立发布,用户感知不明显。

页面渲染方式不同

前者是通过 Java 服务器渲染目标 html 直接发送给浏览器,也就是 SSR(Server-Side Rendering),后者从后端拿到的是 json 实体,页面则是通过前端 JavaScript 来完成的,也就是 CSR(Client-Side Rendering)。所以前者靠的是后端服务器的性能,后者靠的是客户端的性能,以及客户端具体浏览器对内存的优化能力。这也是为什么前后端分离项目更常让人感觉慢的原因:客户端硬件性能差、浏览器内存优化弱、功能样式丰富的组件库带来的庞大的资源开销。

前端组件库的背后是对应的渲染方式,基于 Java 还是 JavaScript,是和具体选择的架构绑定的,像 RichFaces, PrimeFaces 就是和 JSF 绑定的,ViewUI 就是和 Vue 绑定的。所以如果想换一个组件库,不换架构就只能在同架构的里面选,否则必须整体重构。

后端方法的复用

前者如果有其他客户端,后端方法 action 是无法重用的(action 往往也会有部分逻辑),只能在具体的实现层(service 或 impl 层)进行方法复用。而后者由于使用的是 http 接口,天然的适合多类型客户端复用后端。

B2B 系统的“现代化”陷阱:快 vs 美

这是我感触最深的一点。现在的 UI 库(如 View UI)做得非常漂亮,但在处理 B2B 复杂业务(如密集录入、多 Tab 操作)时,体验反而不如老旧的 JSF。

  1. “假 Tab”带来的内存占用问题 一个系统界面常常设计成多标签页(Tab),看起来像浏览器的标签,但关闭时往往因为只是隐藏或缓存机制或者内存泄漏而没有释放内存。不像浏览器的标签页,在任务管理器里可以看到一个标签对应一个进程,关闭时一定会释放内存。所以如果开发人员没有处理好垃圾回收,系统给人的感觉就是刚打开时还挺顺畅,但用久了,内存占用会极高,系统越来越卡。而 JSF 每次跳转都是页面刷新,天然地“重置”了内存环境。
  2. 大表格卡顿问题 这就好比在 Excel 中,是选中一万个单元格批量设置格式,还是逐个给一万个单元格单独设置某个属性带来的表格文件大小变化一样,前者是压缩编码的信息,只需要记录范围和属性就可以,后者却要记录每个单元格的属性,所以文件大小会大。viewUI 有时候的表格就是后者,所以遇到表格展示十行还行,一旦选张展示一百条,就会感知到明显的卡顿问题,必须选用高性能的表格组件,或者至少采用“虚拟滚动”(显示多少,渲染多少,但页面搜索就废了)尽量规避。更要命的是在表格展示时同时要允许直接编辑的场景,嵌套输入框等更加要命。
  3. B2B 系统其实对 UI 的要求只要做到像《写给大家看的设计书》里所说:对比、重复、对齐、亲密性四大原则即可。信息密度一定要大,响应要够快。然而现在 UI 库大多默认设定都是针对 2C 场景,大量使用留白,页面控件总是有很大间距,很多动画效果浪费时间,美丽而不够突出的提示……

前后端分离给分工带来了更高的要求

  1. 分工一定会带来沟通成本的增加,前后端是由接口文档串联起来的,两个角色必须先沟通清楚 API 定义,如果一个公司开发流程不合理,经常扯皮,或者把定义接口的事情交给不太擅长的 PM,可能是噩梦。虽然PM可能不直接定义API,但是必须负责定义API中的业务数据的输入输出。
  2. PM 得注意一份文档两个人看,怎么处理文档结构方便不同角色的开发看的问题。
  3. 两个角色的开发周期不一致,必须依赖 Mock 解决进度不一致的独立开发问题。
  4. 开发可以分工,PM 可不行,难以想象一个函数的定义,一个人负责设计输入,另一个人负责设计过程和输出

前后端分离隐含的无状态管理大坑

上面提到前后端分离时,前端和后端是通过 http 接口通信的,和平时不同系统间对接接口没什么差别。我们平时在系统对接时设计接口要考虑的并发、重试等场景,在前后端分离的接口设计时同样要考虑。设计者不仅要考虑前端的无状态管理(通过 token 认证、禁用按钮等方式防止重复提交),还要考虑后端作为最后一道防线如何防止重复提交,以及如何处理并发请求。缺少了 JSF 框架的隐形保护,稍不注意就可能导致数据异常。

不要因为好看一个原因选择一个组件库

选择组件库,背后是选择架构,选择的是渲染方式、性能、工作流程。在 2B 业态里,高效的完成工作应该是首要考量,选型应该在此基础上再考虑是否好看。选择Javascript 生态的前后端分离组件库时,应该考虑下自己的业态是否能够利用其特点:

  • 多类型客户端
  • 前端交互复杂度高
  • 开发团队规模能够进行前后端分工
  • 有能力管理对应的开发流程

我所说的一切都只是我的观点。
我的观点都可能是错的。

访问量统计:0

发表了9篇文章 · 总计21.79k字
输出促进思考
Built with Hugo
主题 StackJimmy 设计