首先请原谅本文标题取的有点大, 但并非为了哗众取宠. 本文取这个标题主要有 3 个原因, 这也是写作本文的初衷:
(1)目前国内几乎搜索不到全面讲解如何搭建前后端分离框架的文章, 讲前后端分离框架思想的就更少了, 而笔者希望在本文中能够全面, 详细地阐述我们团队在前后端分离的摸索中所得到的搭建思路, 最佳实践以及架构思想;
(2)我们团队所搭建的前后端分离框架, 并非只是将网上传播的知识碎片简单拼装, 而是一开始就从全局出发, 按照整个系统对前后端分离框架的最高期望进行设计, 到目前为止, 可以说我们的框架完全实现了对我们前后端分离的全部期望;
(3)我们在搭建过程中产生了一些创新(比如最大的创新就是 API 文档服务器的搭建), 希望这些创新可以为您的团队在前后端分离的探索中提供一些有用的思路.
本文适合的读者对象: 对软件系统架构有一定经验 + 对 web 前端 / 客户端软件开发有一定经验 + 对服务器端开发有一定经验.
注: 本文中所提的 "前端" 主要指 WEB 前端, 当然在很多情况下也适用于客户端软件, 如桌面程序, APP 等.
第一章 为什么要前后端分离
1, 引用 "为什么我不喜欢前后端分离(个人观点, 欢迎来喷)"
文中有几个观点是笔者特别赞同的, 比如:
(1)前后端不分离的团队, 前端工程师都是页面仔话语权很弱, 技术大牛都在后端, 前端相当于给后端工程师打杂的; 前端工程师晋升机会很少, 薪资不高, 发展前景渺茫;
(2)前后端分离后, 更好招聘, 团队耦合度更低, 职责更分明.
但是文中也有一些观点是笔者不敢认同的. 比如作者最终推荐[全栈工程师] , 虽然笔者多年前就是一名全栈工程师, 但我深知前后端分离的好处远大于全栈工程师带来的好处. 原因有 4, 详见下一小节.
2, 为什么我们团队要采用前后端分离
我们团队最终决定进行前后端分离改造的 4 个主要原因:
(1)全栈工程师很难招聘, 很难培养, 很多后端开发人员不愿意学前端技术, 而很多前端开发人员怕学后端技术;
(2)如果前后端不分离, 前端工程师的工作就必须依赖后端工程师, 前端工程师变成打杂的, 职业生涯前景惨淡;
(3)前后端分离后, 前端和后端工程师独立开发, 大大提高开发效率;
(4)综合来讲, 前后端分离的用人成本远低于全栈工程师用人成本; 同时, 前后端分离的工作效率远大于耦合工作的工作效率;
然而我们团队也是从最近才开始全面实施前后端分离的, 因为笔者深知, 上面 4 个原因所描绘的美好愿景, 其实际效果将会极大的取决于一个关键环节: API 文档服务器. 一个将就的 API 文档服务器会使前后端开发工作痛不欲生, 团队矛盾日益尖锐, 最终将会使程序质量下降, 然后没有人愿意维护.
笔者在 2018 年 3 月终于完成一个近乎完美的 API 文档服务器的搭建方案, 该方案完全实现了我对 API 文档服务器所期望的全部特性, 然后花了一个周末开发完成.
在下一章中, 笔者将会详细阐述 API 文档服务器的重要性以及分享我们团队自创的搭建方案.
第二章 前后端分离最困难, 最关键的环节 --API 文档服务器
1,API 文档服务器是什么?
请先看下图.
正如上图所示, 前后端开发人员可以独立开发, 独立运行, 独立调试, 他们之间的接口就是通过 API 文档服务器定义的. 通常, 一个页面的加载或者表单的提交, 都有数据在客户端和服务器端之间传输, 而 API 文档服务器就是专门用来生成 API 文档的. 有了 API 文档, 前端开发人员就可以基于 API 文档产生模拟数据(mock), 然后使用 mock 的数据完成页面样式和页面交互; 有了 API 文档, 后端开发人员就可以基于 API 文档完成请求的处理以及返回响应数据.
而 API 文档服务器 (通常为 WEB 服务器) 是一个可以动态生成最新版本 API 文档的服务器, 并支持本地维护 + 远程访问.
(如果你们的项目还在使用静态的 API 文档, 比如 word 文档, 那你们必定经历着巨大的痛苦)
2, 为什么 API 文档服务器是最困难, 最关键的环节?
为什么本文会专门写这样一个章节来表述 API 文档服务器的重要性呢? 因为笔者认为, 很多团队在前后端分离的探索中举步维艰, 可能最重要的原因就是对 API 文档的搭建方案重视不够(因为现成的方案俯拾皆是, 如 word 文档做 API 载体). 因此, 笔者希望大家看清楚前后端分离过程中最重要的一个环节, 不是原型, 不是设计, 不是开发, 也不是测试, 而是 API 文档的编写 / 维护 / 发布 / 阅读.
然后让我们来回想一下软件开发流程中的几个关键环节:
(1)产品经理提需求, 画原型;
(2)UI 设计师根据原型出设计图;
(3)测试团队根据产品原型编写测试用例, 制定测试计划;
(4)架构师根据原型编写 API 文档;
(5)前后端工程师基于 API 文档完成业务开发;
(6)测试, 改 BUG, 发布.
在上面这个流程中, API 文档环节直接关系到了前端和后端两个开发团队, 也是整个软件开发流程中耗时最大的环节. 一旦 API 文档编写不合理, 或者维护太麻烦, 或者阅读不方便, 将极大的影响开发效率, 影响团队士气. 而其他环节通常不会跨部门耦合, 常用解决方案非常成熟, 并且所消耗的资源也远不及开发团队, 因此, API 文档环节就变成了前后端分离团队中最关键的环节.
如果你曾经参与过前后端分离团队的开发工作, 那你很可能对上文描述的场景深有体会.
接下来笔者说说为什么 API 文档服务器是最困难的环节. 这也是为了让大家做足心理准备: 搭建 API 文档服务器并没有想象那么容易.
首先, 最大的难点在于这个世界上已经有大量现成的 API 文档服务方案, 比如用 Word/Excel 来做 API 的载体, 或者使用类似 swagger 这样的框架. 我们大多数架构师, 包括笔者在内, 的第一反应就是找现成的, 然后去对比各个现成的方案, 对比每个方案的实施难度以及适用度. 我们 (指架构师们) 最终找来找去, 其实也没有找到一个完美的方案, 但不得不从各个现成的方案中选择一个, 我们很少去想有没有可能自己来搭建一个, 即使有时候想着自己去搭建一个, 但是往往一想到其中的困难 (甚至无从下手) 以及项目进度压力, 可能就浅尝辄止了.
笔者的团队在 2018 年以前的项目中也使用过 word 文档和 swagger, 但我是一个追求完美的人, 我一直没有停止思考去搭建一个完美的 API 文档服务器方案, 直到 2018 年 3 月, 终于灵光一现, 解决了搭建过程中的一个关键问题(API 版本管理), 于是才有了我们团队的前后端分离之路.
其实, 本文所阐述的方案对于使用者来讲也可以说是现成的了, 因为每个人注册个账号就可以使用. 但是本文的目的并不只是简单的告诉大家我们做了一个新的 API 文档服务器, 而是想跟大家分享我们为什么要做这个文档服务器, 以及为什么这样做.
3, 为什么我们不用 word 文档作为 API 文档的载体?
用 word 文档 (或者 Excel) 算是最落后的方式了吧, 其缺点很明显:
(1)API 一多, 维护和阅读就变得极其困难;
(2)每次 API 文档修改之后, 需要修改者自己去维护版本修改记录, 这操作是违背人性的, 并且如果想要基于单个 API 维护版本历史, 那可以算得上违背天理了;
(3)在 word 里面写 JSON 格式的数据结构是很难的, 如果用截图那就极大的增加了维护成本;
说完 word 文档的缺点, 按照套路应该说说 word 文档的优点了吧. 好吧, word 文档的优点就是方便传播, 离线阅读, 但是除非你们项目的 API 文档的维护频率是按年计的, 那还是早早放弃 word 吧.
4, 为什么我们不用 swagger(及类似方案)?
考虑到本文的读者有可能从未接触过 swagger, 笔者首先得说说 swagger 是做什么的, 以及它的先进性(没有一定的先进性怎么会得到这么多人的追捧是吧).
swagger 是一个 API 文档生成框架, 说白了它是一个类库, 集成到系统之后, 能够通过反射读取后端代码定义的 API 文档, java 和. NET 体系下都有对应的版本.
swagger 最大的先进性: 不需要专门有人来编写 API 文档, 自动根据代码生成 API 文档, API 文档维护成本极低. swagger 还有其他一些小优点本文就不细说了, 因为那些都不是大家选择 swagger 的主要原因.
那 swagger 到底有什么不够完美的地方让我们团队最终完全放弃了 swagger?
swagger 之不够完美的地方:
(1)通过 swagger 生成的 API 文档看不见版本修改记录, 你不知道什么时候后端开发人员悄悄改了下 API 文档而忘记 / 有意不通知前端开发人员, 这样的锅前端背了太多;
(2)接口文档由后端开发人员编写, 前端开发人员的地位实际上比后端的低, 并且前端开发人员仍然会经常找后端开发人员沟通修改 API 文档(强依赖仍然存在);
(3)本来 swagger 的 API 定义应该由架构师或项目经理编写, 但由于后端开发人员可以直接改文档, 这导致的实际效果就是: 基本上 API 文档都直接由开发人员编写(或修改), 其质量水平很难达到期望;
(4)swagger 生成 API 文档的类的定义可能是多层引用关联的, 但是这个类又太容易被开发人员修改到, 或者不小心改到, 如果开发人员忘记通知前端或者只通知了部分改动, 那就会造成严重的问题;
(5)使用 swagger 的语法来编写 API 文档, 其实还是很麻烦的, 我们希望这个语法能够超级简单;
(6)swagger 会污染你的代码.
关于上方的第 (1) 点, 笔者想跟大家分享一些工作中的趣事. 以前我们团队使用 swagger 的时候, 有时我们的 java 开发人员发现某个字段的命名写错了, 但是他以为客户端开发人员还没有做这个功能, 就悄悄的将命名修正了. 后来测试提交了 BUG....
本文列出的 swagger 的这些缺点, 其实每一个都不算大, 这也是这么多团队可以一直忍受它的缘故. swagger 的这些缺点其实综合来讲, 其主要的问题就在于它让 API 编写 / 修改太过容易, 让软件开发过程和管理过程太容易出错. 如果你们团队建立了严格的管理机制, 那还是可以将 swagger 用的很和谐的, 但这不是笔者, 作为一个架构师, 可以撇开责任的理由.
作为一个架构师, 笔者以为一个好的框架应该让开发人员不那么容易犯错, 甚至杜绝了开发人员写出错误的代码. 关于这一观点, 笔者会在另外的文章中以我们对 hibernate 的改造为例, 进行更详细的阐述: 如何让开发人员更不容易写出错误的代码是评价一个架构师能力的重要标准.
行文至此, 笔者已经残忍的批判了很多团队的 API 文档方案, 如果让您感觉不适, 我只能深表歉意了.
在说其他方案的缺点时, 其实笔者已经逐渐透露了我们自创的 API 文档服务框架将要解决的问题了. 那么, 接下来请看我们的解决方案, 以及我们为什么这么设计.
第三章 我们自创的 API 文档服务框架详解
1, 我心目中的 API 文档服务器应该是什么样子?
笔者在写代码或做架构的时候有一个思维习惯, 就是不管某个问题多么复杂具体解决方案是什么, 我会先去想想这个问题的最佳的处理方式应该是什么样子, 然后再去想这些最佳的处理方式哪些可以实现, 哪些实现不了而只能用次一点的方案, 然后次一点的方案是否可以接受.
对于 API 文档服务框架, 在我的心中早已有了期望:
(1)编写 API 文档的语法一定要非常简单, 同时又要非常灵活, 对于大多数常见 API 必须能够快速编写, 对于某些特殊 API, 又能够支持自定义编写;
(2)每一个 API 文档能够非常容易的定义请求和响应数据结构, 最好能够自动生成请求和响应示例, 更重要的是, 生成的示例必须看起来是符合业务需求的真实数据;
(3)能够非常方便的编写 JSON 格式的数据;
(4)API 文档的发布要非常简单, 最好能在几秒钟内完成;
(5)API 文档的源文件最好独立于项目源码, 不能污染项目源码;
(6)每次修改 API 文档, 最好能够自动创建版本历史记录, 同时要能够非常方便的查看历史版本;
(7)能够通过 WEB 浏览器访问;
(8)API 数量达到成千上万的时候, 能够呈现一个树形目录结构, 方便阅读和搜索;
好了, 大概就这 8 个特性吧, 下面请看我们是如何完成的.
2, 我们自创的 API 文档服务框架核心工作原理
首先请看简易框架示意图:
这个框架搭建起来并不复杂, 甚至可以说是很简单的. 所用到的技术和工具如下:
(1)使用 JavaScript 作为 API 文档源文件的编写语言;
(2)使用任何支持 JavaScript 的 IDE 作为 API 文档编写工具, 我们团队使用 intellij idea;
(3)使用 SVN 服务器作为版本管理工具, 用来管理 API 版本;
(4)WEB 服务器可以随便使用哪个框架搭建.
核心工作原理(4 步):
1, 获取 JS 目录结构. 当用户在浏览器中输入 API 文档服务地址 (如: http://api.some-domain.com) 时, WEB 服务器根据事先配置好的 SVN 地址和账户信息, 从 SVN 服务器获取 JS 文件 (即 API 定义源文件) 目录, 然后 WEB 服务器将这些 JS 文件的树形目录响应到浏览器(非 JS 文件内容, 仅仅是 JS 目录结构), 后面当用户点击某个 JS 文件名称时, 才会加载相应 JS 文件内容, 这样即使当 API 文档增加至上万个, 也不会太大影响加载速度.
2, 加载 apiHelper.js 文件. 在上一步响应完成后, 页面会通过 script 标签加载一个特别重要的 apiHelper.js 文件. 这个 JS 文件是做什么的呢? 它会极大的简化我们编写 API 文档的语法! 首先, 这个文件在 window 下面定义了一个 apiHelper 对象, 这个对象用来封装大量的静态方法, 这些静态方法主要是用于定义 API 文档数据结构的, 比如 apiHelper.response.page(object)方法将会直接根据 object 对象生成分页响应数据结构.
3, 加载单个 API. 当用户点击某个 API 时, 页面会动态将该 JS 文件加载到浏览器并且执行. 那么这个 JS 的 API 文件到底执行了什么代码呢? 其实很简单, 我们的每个 JS API 文件都在 window 下面定义了一个 api 对象 (当然, 是按照一定数据结构定义的对象), 当这个 JS 文件执行完成后, 当前页面的下的 window.api 对象已经被更新了, 这时我们只需要调一个 render() 方法, 将此 window.api 对象用 html 呈现出来即可. 在呈现 window.api 对象的 render 方法中, 我们充分利用 JS 这门语言的动态性及其反射机制, 从而极大的简化了 API 文档的定义.
4, 查看单个 API 的历史版本. 在加载完单个 API 的 JS 文件后, 我们可以通过 SVN 查找该 JS 文件的修改记录, 然后呈现一个 revision 记录列表, 点击每一个 revision 即可加载曾经某个版本的 JS, 这时 window.api 对象已经被替换成了历史版本, 这时我们只需要再次调用之前的 render 方法, 将这个 window.api 对象重新呈现出来即可, 这样就可完成快速版本切换和智能对比.
以上就是我们这个 API 框架的核心工作原理了, 是不是非常简单呢! 如果你拥有丰富的软件架构经验, 相信读到这里你已经完全明白我们的思路并可以搭建一套类似的 API 文档服务框架了. 但是可能对于大多数读者来说, 读到这里还是有些云里雾里. 别担心, 接下来笔者将会以我们项目中的一个 API 文档为例, 结合界面和代码对核心原理进行阐述.
首先请看我们某个 API 的阅读界面(包含 4 个 tab 的一个网页):
以上 4 张图片分别是我们某个 API 文档的请求, 响应, 请求示例和响应示例的定义文档. 这个文档就是通过对 window.api 对象进行解析得到的. 别看这个文档定义的内容这么多, 但这个 API 的源文件却是非常简单的, 请看下图:
数一数, 大概只有二十多行代码就完成了一个这么复杂的 API 的定义! 这全都得归功于 JavaScript 这门动态语言的强大!
如果你仔细阅读上方的 API 源文件, 你可能会发现我们定义了很多特殊语法, 比如下划线 "_" 属性代表当前对象的整体说明, 而 api 对象下面的 POST 属性则同时定义了 http method 和请求 URL. 还有 apiHelper.p()方法可以将一个请求 / 响应字段的定义放在一行代码里面, 其 4 个参数分别是: 数据类型, 是否可空 (字符串哦), 示例数据(不仅仅支持 string 哦) 和字段说明. 这个 JS 文件执行后的最终效果就是定义 window.api 对象, 然后浏览器加载完该 JS 后, 我们的 render()方法就可以将其呈现出来了.
下面, 我们到浏览器的控制台中来看看实际的 window.api 对象是长什么样子吧:
可以看出, 这个 window.api 对象实际上是非常复杂的, 之所以我们的 API 源文件这么简单, 那是因为 apiHelper.js 做了大量的事情, 从而简化了 API 的编写. 这就是一个架构师该做的事情: 将尽可能多的事情交给框架来完成, 在保证可扩展性的前提下, 让开发人员写尽量少的代码就可以完成工作.
最后, 似乎就只差网页中的 render()方法了, 但是笔者并不打算将其呈现在本文中, 因为它真的很简单了并且已经远离了本文的主旨. 如果基于上文的信息你还无法完成 render()方法的编写, 那么你可以使用我们现成的解决方案(加 jframe 官方 QQ 群了解吧: 651499479).
最终, 我们的 API 文档服务框架完全实现了我们最初的期望:
(1)API 文档编写语法超级简单, 可扩展性强;
(2)非常容易编写真实的请求和响应示例;
(3)非常容易编写 JSON 格式的数据;
(4)发布 API 文档超级简单: 提交 SVN 即可;
(5)API 文档的源文件完全独立于项目源码;
(6)修改 API 文档将自动创建版本历史记录, 在网页中可以非常容易的查看 / 对比历史版本;
(7)通过 WEB 浏览器访问;
(8)API 数量达到成千上万的时候, 能够呈现一个树形目录结构, 方便阅读和搜索;
到此为止, 我们的 API 文档服务框架已经介绍完毕. 篇幅有点长, 那是因为笔者认为 API 文档框架确实是前后端分离过程中最重要的一个环节, 因为相比后端框架或者前端框架, API 文档框架不确定性因素更大, 基于现有的开源项目很难完成高质量的 API 文档框架, 而不像前端或后端框架搭建过程中成熟的方案很多, 完成高质量前端和后端框架相对容易很多.
第四章 我们的前后端分离框架详解
我们的前后端分离框架主要采用了如下技术:
(1)使用 vuejs 作为前端模板引擎;
(2)使用 jQuery 以及我们多年积累的 JS 控件作为 DOM 操作函数库;
(3)后端采用 java 体系的 spring MVC 返回 HTML.
接下来笔者将会解释我们为什么会选择这些技术.
1, 笔者对 vueJs 的理解
首先, 笔者相信很多人对 VueJs 或者 react 到底有什么用都不是很清楚的, 因为他们的官方网站讲述了很多的特性, 以致让我们分不清楚 VueJs 的本职工作是什么. 笔者在权衡 VueJs 和 react 的过程中, 也感到非常的困惑. 因为按照 VueJs 官网的介绍, 似乎我应该建立以. vue 文件为主的项目工程, 然后通过编译器将其编译成 html,CSS 和 js. 并且 VueJs 官网还大量介绍了基于 NodeJs 的 Vue 服务端渲染, Vue Ruoter,Vue Loader, 规模化, 以及打包工具 webpack 等. 看上去这是一套全新的, 完整的, 包含服务器端的前端开发框架.
我相信 Vue 官网介绍的确实是一套全新的, 完整的前端开发框架. 但是, 在笔者看来这套框架并不是最好的前端开发框架. 对于一个不懂服务器后端架构的前端开发人员来讲, 使用 NodeJs 搭建 WEB 服务器确实是一个最优的选择, 因为 NodeJs 比 java,.NET 简单太多了, 我相信 Vue 官网也是基于这个原因才对服务器端解决方案做了大量的介入.
然而我们团队有非常成熟的 java 服务器后端框架, 只需要增加几行代码即可完成 Vue 官方所介绍的那一堆堆特性. 笔者也是把 Vue 官网上面介绍的这些服务端方案读了很久, 才明白 Vue 官网的真正意图, 并且最终明白, Vue 官网所描述的架构还没有基于我们的 java 服务端增加几行代码所得到的架构好.
这是笔者在阅读 Vue 官网时遇到的最大的一个困惑. 后面最终决定: 我们只把 Vue 当做一个 HTML 模板引擎, 这才是 Vue 的本职工作.
这个决定下的并不容易, 因为这会让我们的框架看上去不那么新潮. 因为 Vue 官网上介绍的知识, 除了把 Vue 当做模板引擎的知识外, 其他知识我们一点都没有用得上.
Vue 官网上还有一个隐形的基础认识没有介绍清楚, 因为笔者发现, Vue 官网上几乎全部的知识都是基于 SPA(单页应用)这个框架下进行描述的, 包括 webpack,Vue Ruoter 等. 但是我们的系统是非常庞大又复杂的, 根本不可能用 SPA 架构. 关于这一点, 笔者也是读了很久才发现, Vue 官网的默认设定场景就是 SPA 架构.
2, 为什么我们会选择 VueJs?
前后端分离之后, 前端工程师需要将通过 API 获取的数据呈现到页面上, 虽然也可以通过 jQuery 对页面一个一个赋值, 但是这种效率太低了, 或者也可通过在 JavaScript 中拼接 HTML, 但是这种方式太难维护 HTML 代码了, 也很难阅读. 因此最好的方式就是使用模板引擎.
前端的模板引擎跟后端模板引擎很相似, 比如 JSP 或 cshtml(razor), 他们的语法都非常相似, 他们所实现的功能也几乎一样: 将数据绑定到 HTML 模板. VueJs 和 react 都可以充当这样的模板引擎. 我们最终没有选用 react 而是选用了 VueJs 的原因只有一个, 那就是 VueJs 是真正的响应式, 而 react 改变 model 之后需要手工调用 setState 才会更新 UI, 这是完全无法忍受的.
因为这个原因, 我们只能选择 VueJS 作为模板引擎.
3, 我们的前端框架的工作原理
虽然本文写的有点长, 但我们的前端框架却是非常简单的, 这也是我们为什么不选择采用. vue 文件构建工程的原因, 因为那太复杂了.
核心工作原理:
(1)定义页面 URL 格式. 我们每个页面的地址格式大概长这样: http://www.your-domain.com/admin#home/index.admin 代表模块名称,# 后面的 home 代表子模块对应 controller, 而 index 对应 controller 里面的方法. 所以当在同一个模块中切换页面的时候, 只会改变地址的 hash 值, 浏览器不会进行跳转. 但是当切换页面之后, 如果用户点击浏览器的刷新按钮, 框架能够根据 hash 值加载当前页面.
(2)根据 URL 加载 layout.html. 当服务器收到 "/admin#home/index" 地址的请求时, 不管后面的 hash 值为多少, 直接返回一个 layout.html. 该 layout.html 文件包含页面的基本框架, 比如公共 js,css 页面, 导航, footer 等公共元素.
(3)初始化 layoutVue.Layout.html 加载完成之后, 其中的 JS 会根据 layout.html 的 HTML 结构生成一个 Vue 实例, 并将 layout.html 下面的全部动态 HTML 交给该 Vue 实例托管, 比如根据用户角色显示相应的导航菜单, 在页面 header 显示用户个人信息等.
(4)根据 location.hash 通过 jQuery ajax 加载相应的内容 HTML 文件. 这时我们会在服务器端定义另一个接口, 根据内容页的路径返回对于的 html 代码.
比如: GET /admin/getPage?path=home/index.
内容页的 html 除了返回 html 代码之外, 还会包含该页面所需的 JS 和 CSS. 这样, 当内容页的 html 呈现到 layout 中的某个容器 div 中后, 内容页的 JS 就会被加载并执行. 那么内容页的 JS 都有些什么逻辑呢? 当然是初始化内容页的 Vue 实例并接管内容页的动态 html 生成工作.
以上 4 个步骤可以用下图简单表示:
读到这里, 如果你对软件架构很有经验, 那么相信你已经完全明白了我们的前后端分离框架的工作原理了, 你也应该可以按照本文的思路完成你自己的前后端分离框架了. 但是对于大多数读者来说, 可能读到这里只是大概明白怎么回事, 如果说要自己动手开始搭建, 可能就会面临无从下手的尴尬了. 不用担心, 接下来笔者就以我们的框架为例, 一步一步通过代码来展示我们框架的搭建过程.
4, 一步一步搭建我们的前端框架
(1)页面地址生成. 服务器根据同步请求 URL(/pe#home/index...)响应一个 layout.html. 请看 java 源码:
上图中的 java 代码就是我们前端框架中全部的后端处理代码了(请仔细读这句话, 有点绕哈), 是不是非常简单呢? 虽然简单, 但是功能却是很强大的. 从上面的代码中可以看出:
a. 只要是以 "/pe#" 开头的 URL, 服务器将直接返回某个目录下的 layout.html 页面;
b. 前端开发人员可以在 "/modules/pe/views/" 下面随便建立目录, 子目录以及 HTML 文件, 然后即可通过 ajax 请求类似 "/pe/page?path=home/index" 这样的 URL 直接加载下面的 HTML 文件, 这样前端开发人员不需要动一行后端代码, 只需要按照约定建立目录和 HTML 文件就可以在浏览器中加载出来. 这样, 前端开发人员就完全不需要依赖后端开发人员来获得页面地址了, 前端开发人员自己就可以创建页面地址!
上面的 java 代码中, 还可以将同步请求跟异步请求合并成一个(@GetMapping("/pe/*")), 然后在方法内部判断是否是同步请求, 如果是同步请求就返回 layout.html, 否则返回内容页. 这样做就不是监听 hash 变化了, 而是每个 URL 地址都看上去像一个同步请求地址, 如 "/pe/home/index" 的 URL 进入服务器后, 服务器首先返回 layout.html, 然后 layout.html 再发起 ajax 请求 "/pe/home/index" 这时服务器返回内容页 HTML. 在我们项目中, 我们更愿意使用监听 hash 变化的方式. 当然笔者认为这两种方式都是 OK 的, 如果后期想切换也是比较容易的.
(2)layout.html. 下面让我们来看看服务器首先返回的 layout.html 大概长什么样子, 请看下图:
这个 HTML 文件就是服务器的同步响应内容, 也是我们每一个页面的入口. 这时我们需要把一个网页理解成一个应用(APP), 而这个 layout.html 只定义 APP 启动入口和框架. 可以看到该文件引用了基本的 css 和 js, 其最后的 core.js 内部完成了这个 APP 的初始化. 下面我们来看看这个 core.js 内部的主要部分代码.
(3)core.js. 完成 APP 初始化的代码如下:
上面的示例代码是笔者为了本文精简过的, 实际上这个 JS 文件在完成 APP 初始化的过程中还做了很多的操作, 比如获取用户登录信息, 获取页面动态导航菜单数据等, 然后将获取的数据通过一个 layoutVue 实例呈现到页面上, 从而 layoutVue 实例完全接管 layout.html 中全部的 HTML 呈现工作.
在 APP 初始化的最后一步, 就是根据 URL #后面的路径加载内容页 HTML 到一个 id 为 body 的 DIV 中了. 服务器如何异步响应 URL(/pe/page?path=home/index), 请参考本章第一小节中的异步请求 java 源码(pePage 方法).
(4)内容页 HTML. 请看 ajax 加载的内容页的 HTML:
从上方代码可以看出, 每个内容页对应一个 js 和一个 css 文件, 然后 html 代码以一个 id 为 page 的 DIV 开始. 当然, 这些都不是必须的, 只是我们的项目规范, 当然, 笔者也建议大家可以参考我们的规范.
当内容页 HTML 加载完成后, 就会执行其引用的 js 文件了. 接下来就让我们来看看内容页的 JS 代码.
(5)内容页 JS 代码. 请看下图:
这个 JS 文件首先是由一个自执行函数包裹, 好处是避免不经意将对象定义到 window 下面(编码开发人员写出错误的代码), 这也是我们的规范之一, 实际上我们项目的所有 JS 文件都由自执行函数包裹.
上方代码的主入口是 Vue.nextTick 方法的回调 function.Vue.nextTick 是一个非常重要的方法, 但是官网上并没有给他一个特别明显的位置, 因此笔者要在此多说两句, 这个方法有什么用. 这得要回到 Vue 的 render 机制了. 当 Vue 实例发现绑定的数据改变之后, Vue 采用了异步更新 UI 元素的方式, 因此, 当我们修改了数据的时候, 这时 DOM 元素还没有生成出来, 如果这时去操作 DOM(比如通过 jQuery), 那么就会报找不到该 DOM 元素, 所以一定要在改变 Vue 数据后使用 Vue.nextTick 去操作其影响的 DOM 元素.
再回到上方代码. 在 Vue.nextTick 回调中, 首先是初始化内容页的 Vue 实例, 从而接管 id 为 page 的 div 及其下的所有 DOM 元素的呈现工作.
至此, 一个完整的页面就算加载完成了, 用户在浏览器中就能看到这个完整的页面了.
这就是我们前后端分离框架的整个工作流程, 希望笔者已经把这个流程解释的足够清晰, 然后你可以开始动手搭建自己的前后端分离框架了. 但是在你真正开始之前, 笔者还想跟大家分享一个我们前后端分离的最佳实践: mock, 请看下一章.
第五章 前后端分离框架中的 API mock 思路
想要实现真正的前后端分离, 那就必须得用好 API mock(模拟数据). 使用 mock 数据的好处有两个:
(1)前端开发人员可以基于 API 文档生成 mock 数据, 在后端开发人员将 API 发布出来之前就可以完成整个业务流程的开发;
(2)使用 mock 数据能够更低成本, 更快速地, 通过直接修改 mock 数据的方式, 调试页面样式, 调试页面功能.
在本文中, 笔者不会给大家推荐任何 mock 框架, 因为我们根本用不着: 我们要用纯手工造数据的方式造出更真实的 mock 数据.
我们前后端分离框架中需要用到 mock 数据的地方, 主要就是 API, 因此其他使用场景 (如硬件 mock, 第三方系统 API) 本文不做示例介绍, 因为其 mock 思路其实是一样的.
1, 全局 mock 开关
API 的 mock 数据主要分为两种, 一种是零散的, 手工发起的 ajax API 请求; 另一种是被封装到控件内部的 ajax API 请求. 不管是哪一种 mock, 首先我们在每个页面都会加载的 core.js 里面定义了一个全局的 mock 开关: mvcApp.mock = true/false, 然后在页面加载完成后, 判断如果设置 mock==true, 则提示用户 / 开发者当前使用的是 mock 数据!
为什么要设置这样一个全局的 mock 开关呢? 主要基于以下两点考虑:
(1)设置全局的 mock 开关之后就不再需要针对每一个页面设置 mock 开关, 更容易维护, 避免项目中有多个 mock 开关而难以统一开关状态;
(2)如果发布时忘记将 mock 开关给关掉, 那么发布之后一运行发布者就会发现 mock 开关忘了关, 然后可以快速修复之后再重新发布, 从而避免不小心将正式服更新为 mock 数据源.
正是由于以上两点考虑, 我们的全局 mock 开关可以帮助程序开发者和发布者更不容易犯错.
下面笔者将会给大家展现全局的 mock 开关如何跟页面 API 配合, 从而完成整个站点的 mock 状态控制.
2, 普通 API 的 mock
在我们的前端框架中, 我们使用了 grunt 来将整个页面的全部 JS 文件打包成一个 JS 文件, 因此, 在我们的前端框架中, 每个页面对应一个 JS 源文件的文件夹, 在打包的时候, grunt 会将该文件夹中的全部 JS 文件合并打包(发布到生产环境时将执行压缩混淆). 下图所展示的是我们 admin 端的一个列表页面所对应的的 JS 源文件目录(index 文件夹):
可以看到该文件夹下面的第一个 JS 文件叫 01.page.js, 这个 JS 是整个页面的入口, 包括定义了页面全部的配置(比如用到的 ajax URL). 第 2 个文件是 02.api.js 文件, 该文件包含了所有的 ajax 请求. 我们把全部的 ajax 请求封装到这个文件中, 也是为了更好的 mock.
下面就让我们来看这个 02.api.js 大概长什么样子吧:
从上面的代码中可以看到, 我们定义了 page.api 这个对象两次, 而中间有一个 if 判断, 那就是判断我们全局的 mock 开关是否处于开启中, 如果 mock 开启, 则不会执行 return 而会继续第二段 page.api 对象赋值的代码, 这样第一段代码定义的 page.api 对象就被覆盖了, 于是这个页面中的其他 JS 文件就将使用 mock 的数据. 如果全局的 mock 开关处于关闭状态, 那么第一段 page.api 对象赋值代码执行完成之后, 就会调用 if 下面的 return 语句了, 这样就不会执行第二段 page.api 对象赋值, 于是这个页面的其他 JS 文件就将使用真实数据.
这就是全局 mock 开关在页面中的应用, 使用方法简单而灵活. 这样, 前端开发人员就可以在 API 开发出来之前通过 mock 的 API 完成样式和交互.
3, 以 Grid 控件为例的控件级 mock
在 WEB 前端开发过程中, 一定会用到大量的控件 (UI 组件). 如果这个控件(比如 Grid) 内部封装了 ajax 请求, 那么其 ajax 的 mock 操作就很难通过上一小节中的 mock 方法实现.
下面, 笔者就将以我们项目的 Grid 控件为例, 给大家详细阐述我们的改造过程.
由于我们项目中的 Grid 控件是我们自己开发的, 虽然只有 300 行代码, 但是功能很强大, 可定制性很高. 因此, 要改造我们的 Grid 就变得很容易了.
首先, 我们定义了一个 VueGrid 类继承自 Grid 类, 然后重写了其 loadData 这个 ajax 方法, 请看下图:
改造之后的 VueGrid 类多了一个 getMockDataFunction 这个属性, 在 loadData 方法中, 首先判断该 grid 实例是否设置了 getMockDataFunction 属性, 如果设置了再判断 getMockDataFunction 方法的返回值是否为空, 如果返回值为空则也使用真实数据, 因此使用 mock 数据的条件是很苛刻的: 必须设置 getMockDataFunction 属性并且其返回值不能为 null.
然后我们在 VueGrid 类中还公开了一个设置 getMockDataFunction 属性的方法, 如下图:
在初始化 Grid 和 Vue 的页面 JS 中(01.page.js), 我们像下方代码这样使用:
在上方代码中初始化 VueGrid 实例时, 设置了 mock 数据源为 page.api.mockSearchList 这个方法. 我们可以通过全局的 mock 开关控制 page.api.mockSearchList 这个方法是否为 null 从而控制该 grid 是否使用 mock 数据.
请看 02.api.js 中的代码:
上方代码中, 如果 mvcApp.mock 被设置为了 false, 那么 page.api.mockSearchList 就不会被定义, 也就是 undefined(null); 如果 mvcApp.mock 被设置为了 true, 那么 page.api.mockSearchList 才会被赋值, 这时 grid 将使用 mock 数据.
最后, 为了方便大家理解整个 Grid 控件的使用过程, 笔者再给大家看看我们自己写的 VueGrid 的 html 端的代码, 很简单, 很灵活, 支持排序, 分页, 支持 JSON 和 HTML 两种数据格式:
至此, 我们的 Grid 控件的 mock 改造就已经完全完成了. 改造后实现的效果: 不需要修改 01.page.js, 不需要 VueGrid.js, 也不需要修改 02.api.js, 只需要修改 mvcApp.mock 的值就可以切换是否使用 mock 数据.
这样, 我们 mock 开关的状态控制就非常的简单, 并且, 最关键的是, 不容易出错!
第六章 结语
最后, 笔者再带大家回顾一下本文中的提到的一些关键技术, 观点和看法:
(1)前后端分离最关键的环节是 API 文档服务框架, 没有一个好的 API 文档服务做支撑, 前后端分离之路举步维艰. 笔者建议大家按照本文提供的思路自行搭建, 或者考虑使用我们现成的框架;
(2)VueJs 官网介绍的规模化架构方案并不一定是最好的, 如果你们团队拥有后端架构师, 笔者建议仅仅把 VueJs 当做 HTML 模板引擎即可, 至于 VueJs 官网描述的其他特性可以忽略;
(3)VueJs 的异步渲染是一个非常重要的知识点, 但是 VueJs 官网并没有将之置于显眼的地方, 因此一定要熟练掌握 Vue.nextTick 方法及其工作原理;
(4)VueJs 是完全可以和其他第三方框架 / 库兼容的, 关键是要掌握其工作原理, 比如我们项目中 VueJs 就很好地与 jQuery,jQueryUI 以及我们自己的 JS 控件库交互, 虽然 VueJs 官网建议 DOM 操作全部交由 VueJs 接管, 但笔者仍然建议很多时候用 jQuery 操作 DOM 更有优势, 因为 jQuery 的封装性更好(比如我们的 Grid 控件, 之所以使用起来很简单, 那是因为内部通过 jQuery 封装了很多操作);
(5)真正的前后端分离一定离不开 mock, 不要觉得 mock 是一个多么复杂多么高深的东西, 在笔者看来, mock 仅仅是一种思想, 你只要明白其核心思想, 自己写出的 mock 框架才是最好用的;
(6)前后端分离, 如果做好了, 利远大于弊. 从我们团队实施分离之后的这段时间来看, 其对团队的正面影响非常明显; 从长远来看, 前后端分离促进社会分工, 让公司的人才培养之路更加清晰, 更加高效, 更加具有竞争力.
(完)
来源: https://www.cnblogs.com/leotsai/p/vuejs-front-backend-architecture.html