前言前后端分离已经是业界公认的开发/部署模式。所谓前后端分离,在传统行业中并不是按部门划分的。有的人纯前端(HTML/CSS/JavaScript/Flex),有的人纯后端,因为这种方法行不通:比如很多团队采用了后端模板技术(JSP、FreeMarker、ERB等),而前端的开发调试需要后台Web容器的支持,无法实现真正??的分离(更何况在部署时,由于动态内容和静态内容混合在一起,设计动静分离时,处理起来很麻烦)。可以在此处找到关于前端和后端开发的另一个讨论。即使前后端开发过程通过API解耦,前后端通过RESTFul接口进行通信,前端静态内容开发,后端动态计算分开部署,集成仍然是一个无法回避的问题——前端/后端端的应用可以独立运行,但是集成了就不行了。我们需要花费大量的精力去调试,在上线之前没有人确信所有的接口都能正常工作。一点背景典型的Web应用程序布局如下所示:前端和后端各有自己的开发流程、构建工具、测试套件等。前端和后端仅通过接口进行编程。该接口可以是JSON格式的RESTFul接口,也可以是XML接口。关键是后台只负责数据提供和计算,根本不做presentation。前端负责获取数据,组织数据并展示。这样,结构清晰,关注点分离,前后端相对独立,松耦合。上面的场景是相当理想的。其实我们在实际环境中会有非常复杂的场景,比如异构网络,异构操作系统等:在实际场景中,后端可能会更加复杂,比如使用C语言进行数据采集,然后进行集成通过Java打入一个数据仓库,然后数据仓库有一层WebService,最后通过一个Ruby聚合Service把几个这样的WebService整合起来返回给前端。在这样一个复杂的系统中,后台任何端点的故障都可能会阻塞前端开发进程,所以我们会使用mock的方式来解决这个问题:这个mockserver可以启动一个简单的HTTP服务器,然后传输一些静态的供前端代码使用的内容。这样有很多好处:1.前端和前端开发相对独立2.后端的进度不会影响前端开发3.启动速度更快4.前端和前端都可以前端可以用自己熟悉的技术栈(让前端学maven,让后端用gulp会很吃力)但是集成还是很头疼。我们在集成的时候经常会发现原来协商好的数据结构变了:deliveryAddress字段原来是一个字符串,现在变成了一个数组(业务变了,现在系统可以支持多个快递地址);price字段变为String,协商时为number;用户邮箱多了一级等等。这些变化是不可避免的,时有发生,这会花费大量的调试时间和集成时间,更不用说修改后的回归测试了。所以仅仅使用静态服务器并提供模拟数据是不够的。我们需要的mock也应该可以做到:1.前端依赖指定格式的mock数据进行UI开发2.前端的开发和测试都是基于这些mock数据3.后端生成指定格式的mock数据4.后端需要Test,确保生成的mock数据正是前端需要的。简而言之,我们需要约定一些合约,并将这些合约作为可以测试的中间格式。然后前端和后端都需要进行测试才能使用这些合约。一旦契约发生变化,对方的测试就会失败,从而带动双方的协商,减少集成的浪费。一个实际的场景是:前端发现现有合约中缺少地址字段,因此将此字段添加到合约中。然后在UI上正确显示这个字段(当然还要设置字体、字号、颜色等)。但是在后台生成合约的服务并不知道这个变化,当运行测试(后台)的生成合约部分时,测试失败——因为它没有生成这个字段。于是后端工程师就来前端商量了。了解业务逻辑后,他会修改代码,确保测试通过。这样一来集成的时候,UI上不会出现缺失字段的情况,但是谁也不知道是前端的问题,还是后端的问题,还是数据库的问题。而且在实际项目中,往往同时开发多个页面、多个API、多个版本、多个团队。这样的合约会减少很多调试时间,让集成也比较顺利。实际上,合同可以定义为JSON文件或XML负载。你只需要保证前后端共享同一套测试契约,集成工作就会从中受益。最简单的一种形式是:提供一些静态的mock文件,前端对后台的所有请求都通过某种机制拦截,转化为对静态资源的请求。1.moco,基于Java2.wiremock,基于Java3.sinatra,基于Ruby看到这里列出sinatra,熟悉Ruby的人可能会反对:它是一个全功能的后端库。之所以列在这里是因为sinatra提供了一套简洁美观的DSL。这种DSL非常适合Web语言。我找不到更漂亮的方法来使这个模拟服务器更具可读性,所以我采用了它。#p#一个例子下面就以这个应用为例,来说明前后端分离后如何保证代码质量,降低集成成本。应用场景很简单:每个人都可以看到一个词条列表,每个登录用户都可以选择自己喜欢的词条并为其加星。加星后的条目将保存在用户的个人中心。用户界面如下所示:但为了专注于我们的中心,我删除了登录、个人中心等页面,假设您是登录用户,那么让我们看看如何编写测试。按照前端开发的惯例,前后端分离后,我们可以很方便地mock一些数据来测试自己:Js代码[{"id":1,"url":"http://abruzzi.github.com/2015/03/list-comprehension-in-python/","title":"Python中的列表理解和生成器","publicDate":"2015年3月20日"},{"id":2,"url":"http://abruzzi.github.com/2015/03/build-monitor-script-based-on-inotify/","title":"使用inotify/fswatch搭建自动监控script","publicDate":"2015February1"},{"id":3,"url":"http://abruzzi.github.com/2015/02/build-sample-application-by-using-underscore-and-jquery/","title":"使用underscore.js构建前端应用","publicDate":"January20,2015"}]然后,一种可能的方法是通过请求测试前台这个json:Js代码'.container').append(feedListView.rend呃());});});当然,这可以工作,但是这里发送请求的url不是最终的。集成的时候需要修改为真实的url。一个简单的方法就是使用Sinatra来做一个url转换:js代码get'/api/feeds'docontent_type'application/json'File.open('mocks/feeds.json').readend这样,当我们集成在实际服务中,我们只需要连接到那个服务器就可以了。请注意,我们当前的核心是mocks/feeds.json文件。这个文件目前的作用是一个契约,至少对前端来说是这样。接下来我们的应用需要渲染加星的功能,这就需要另外一个合约:找出当前用户加星的所有商品,所以我们新加了一个合约:js代码[{"id":3,"url":"http://abruzzi.github.com/2015/02/build-sample-application-by-using-underscore-and-jquery/","title":"使用underscore.js构建前端应用","publicDate":"January20,2015"}]然后在sinatra中添加一个新的映射:Js代码get'/api/fav-feeds/:id'docontent_type'application/json'File.open('mocks/fav-feeds.json').readend通过这两个请求,我们会得到两个列表,然后根据这两个列表的交集绘制所有星号的状态(有的是空心的,有的是实心的):Js代码$.when(feeds,favorite).then(function(feeds,favorite){varids=_.pluck(favorite[0],'id');varextended=_.map(feeds[0],function(feed){return_.extend(feed,{status:_.includes(ids,feed.id)});});varfeedList=newBackbone.Collection(extended);varfeedListView=newFeedListView(feedList);$('.container').追加(feedListView.render());});剩下的问题就是当点击红心的时候,我们需要向后台发送请求,然后更新红心的状态:js代码toggleFavorite:function(event){event.preventDefault();varthat=this;$.post('/api/feeds/'+this.model.get('id')).done(function(){varstatus=that.model.get('status');that.model.set('status',!status);});}这是另一个请求,但有了Sinatra我们仍然可以很容易地支持它:Jscodepost'/api/feeds/:id'doend如你所见,没有后端一切顺利——后端甚至还没有开始做,或者正在开发被一个进度比我们慢的团队,不过没关系,他们不会影响到我们。不仅如此,当我们写完前端代码后,还可以做一个End2End测试。由于使用了模拟数据,消除了耗时的数据库和网络。这个End2End测试会跑的很快,确实起到端到端的作用。这些测试也可以用作在最终集成期间运行的UI测试。所谓一举多得。js代码#encoding:utf-8require'spec_helper'describe'FeedsListPage'dolet(:list_page){FeedListPage.new}beforedolist_page.loadendit'usercanseeabannerandsomefeeds'doexpect(list_page).tohave_bannerexpect(list_page).tohave_feedsendit'usercansee3feedsinthelist_page'do).tohave_feed_itemscount:3endit'feedhassomedetailinformation'dofirst=list_page.all_feeds.feed_items.firstexpect(first.title).toeql("listcomprehensionandgeneratorinPython")endend这样的测试怎么写,可以参考之前写的这篇文章。#p#后端开发本例中后端以spring-boot为例,类似的思路应该可以很容易的应用到Ruby或者其他语言中。首先是请求的入口。FeedsController会负责解析请求路径,检查数据库,最后返回JSON格式的数据。js代码@Controller@RequestMapping("/api")"/feeds",method=RequestMethod.GET)@ResponseBodypublicIterable
