[原创]基于VueJs的前后端分离框架搭建之完全攻略
首先请原谅本文标题取的有点大,但并非为了哗众取宠。本文取这个标题主要有3个原因,这也是写作本文的初衷:
(1)目前国内几乎搜索不到全面讲解如何搭建前后端分离框架的文章,讲前后端分离框架思想的就更少了,而笔者希望在本文中能够全面、详细地阐述我们团队在前后端分离的摸索中所得到的搭建思路、最佳实践以及架构思想;
(2)我们团队所搭建的前后端分离框架,并非只是将网上传播的知识碎片简单拼装,而是一开始就从全局出发,按照整个系统对前后端分离框架的最高期望进行设计,到目前为止,可以说我们的框架完全实现了对我们前后端分离的全部期望;
(3)我们在搭建过程中产生了一些创新(比如最大的创新就是API文档服务器的搭建),希望这些创新可以为您的团队在前后端分离的探索中提供一些有用的思路。
本文适合的读者对象:对软件系统架构有一定经验+对WEB前端/客户端软件开发有一定经验+对服务器端开发有一定经验。
注:本文中所提的“前端”主要指WEB前端,当然在很多情况下也适用于客户端软件,如桌面程序、APP等。
第一章 为什么要前后端分离
1、引用“为什么我不喜欢「前后端分离」(个人观点,欢迎来喷)”
首先大家可以阅读一下中文版原文:https://www.v2ex.com/t/298014?p=4,很有意思的一篇文章,作者文笔幽默,阅读起来很轻松。
文中有几个观点是笔者特别赞同的,比如:
(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)前后端分离,如果做好了,利远大于弊。从我们团队实施分离之后的这段时间来看,其对团队的正面影响非常明显;从长远来看,前后端分离促进社会分工,让公司的人才培养之路更加清晰,更加高效,更加具有竞争力。
(完)