阅读更多

4顶
0踩

Web前端
前言

前后端分离已经是业界所共识的一种开发/部署模式了。所谓的前后端分离,并不是传统行业中的按部门划分,一部分人纯做前端(HTML/CSS/JavaScript/Flex),另一部分人纯做后端,因为这种方式是不工作的:比如很多团队采取了后端的模板技术(JSP, FreeMarker, ERB等等),前端的开发和调试需要一个后台Web容器的支持,从而无法做到真正的分离(更不用提在部署的时候,由于动态内容和静态内容混在一起,当设计动态静态分流的时候,处理起来非常麻烦)。关于前后端开发的另一个讨论可以参考这里。

即使通过API来解耦前端和后端开发过程,前后端通过RESTFul的接口来通信,前端的静态内容和后端的动态计算分别开发,分别部署,集成仍然是一个绕不开的问题 — 前端/后端的应用都可以独立的运行,但是集成起来却不工作。我们需要花费大量的精力来调试,直到上线前仍然没有人有信心所有的接口都是工作的。

一点背景

一个典型的Web应用的布局看起来是这样的:



前后端都各自有自己的开发流程,构建工具,测试集合等等。前后端仅仅通过接口来编程,这个接口可能是JSON格式的RESTFul的接口,也可能是XML的,重点是后台只负责数据的提供和计算,而完全不处理展现。而前端则负责拿到数据,组织数据并展现的工作。这样结构清晰,关注点分离,前后端会变得相对独立并松耦合。

上述的场景还是比较理想,我们事实上在实际环境中会有非常复杂的场景,比如异构的网络,异构的操作系统等等:



在实际的场景中,后端可能还会更复杂,比如用C语言做数据采集,然后通过Java整合到一个数据仓库,然后该数据仓库又有一层Web Service,最后若干个这样的Web Service又被一个Ruby的聚合Service整合在一起返回给前端。在这样一个复杂的系统中,后台任意端点的失败都可能阻塞前端的开发流程,因此我们会采用mock的方式来解决这个问题:



这个mock服务器可以启动一个简单的HTTP服务器,然后将一些静态的内容serve出来,以供前端代码使用。这样的好处很多:
1.前后端开发相对独立
2.后端的进度不会影响前端开发
3.启动速度更快
4.前后端都可以使用自己熟悉的技术栈(让前端的学maven,让后端的用gulp都会很不顺手)
但是当集成依然是一个令人头疼的难题。我们往往在集成的时候才发现,本来协商的数据结构变了:deliveryAddress字段本来是一个字符串,现在变成数组了(业务发生了变更,系统现在可以支持多个快递地址);price字段变成字符串,协商的时候是number;用户邮箱地址多了一个层级等等。这些变动在所难免,而且时有发生,这会花费大量的调试时间和集成时间,更别提修改之后的回归测试了。

所以仅仅使用一个静态服务器,然后提供mock数据是远远不够的。我们需要的mock应该还能做到:
1.前端依赖指定格式的mock数据来进行UI开发
2.前端的开发和测试都基于这些mock数据
3.后端产生指定格式的mock数据
4.后端需要测试来确保生成的mock数据正是前端需要的
简而言之,我们需要商定一些契约,并将这些契约作为可以被测试的中间格式。然后前后端都需要有测试来使用这些契约。一旦契约发生变化,则另一方的测试会失败,这样就会驱动双方协商,并降低集成时的浪费。

一个实际的场景是:前端发现已有的某个契约中,缺少了一个address的字段,于是就在契约中添加了该字段。然后在UI上将这个字段正确的展现了(当然还设置了字体,字号,颜色等等)。但是后台生成该契约的服务并没有感知到这一变化,当运行生成契约部分测试(后台)时,测试会失败了 — 因为它并没有生成这个字段。于是后端工程师就找前端来商量,了解业务逻辑之后,他会修改代码,并保证测试通过。这样,当集成的时候,就不会出现UI上少了一个字段,但是谁也不知道是前端问题,后端问题,还是数据库问题等。

而且实际的项目中,往往都是多个页面,多个API,多个版本,多个团队同时进行开发,这样的契约会降低非常多的调试时间,使得集成相对平滑。

在实践中,契约可以定义为一个JSON文件,或者一个XML的payload。只需要保证前后端共享同一个契约集合来做测试,那么集成工作就会从中受益。一个最简单的形式是:提供一些静态的mock文件,而前端所有发往后台的请求都被某种机制拦截,并转换成对该静态资源的请求。
1.moco,基于Java
2.wiremock,基于Java
3.sinatra,基于Ruby
看到sinatra被列在这里,可能熟悉Ruby的人会反对:它可是一个后端全功能的的程序库啊。之所以列它在这里,是因为sinatra提供了一套简洁优美的DSL,这个DSL非常契合Web语言,我找不到更漂亮的方式来使得这个mock server更加易读,所以就采用了它。
一个例子

我们以这个应用为示例,来说明如何在前后端分离之后,保证代码的质量,并降低集成的成本。这个应用场景很简单:所有人都可以看到一个条目列表,每个登陆用户都可以选择自己喜欢的条目,并为之加星。加星之后的条目会保存到用户自己的个人中心中。用户界面看起来是这样的:



不过为了专注在我们的中心上,我去掉了诸如登陆,个人中心之类的页面,假设你是一个已登录用户,然后我们来看看如何编写测试。

前端开发

根据通常的做法,前后端分离之后,我们很容易mock一些数据来自己测试:
[
    {
        "id": 1,
        "url": "http://abruzzi.github.com/2015/03/list-comprehension-in-python/",
        "title": "Python中的 list comprehension 以及 generator",
        "publicDate": "2015年3月20日"
    },
    {
        "id": 2,
        "url": "http://abruzzi.github.com/2015/03/build-monitor-script-based-on-inotify/",
        "title": "使用inotify/fswatch构建自动监控脚本",
        "publicDate": "2015年2月1日"
    },
    {
        "id": 3,
        "url": "http://abruzzi.github.com/2015/02/build-sample-application-by-using-underscore-and-jquery/",
        "title": "使用underscore.js构建前端应用",
        "publicDate": "2015年1月20日"
    }
]

然后,一个可能的方式是通过请求这个json来测试前台:
$(function() {
  $.get('/mocks/feeds.json').then(function(feeds) {
      var feedList = new Backbone.Collection(extended);
      var feedListView = new FeedListView(feedList);

      $('.container').append(feedListView.render());
  });
});

这样当然是可以工作的,但是这里发送请求的url并不是最终的,当集成的时候我们又需要修改为真实的url。一个简单的做法是使用Sinatra来做一次url的转换:
get '/api/feeds' do
  content_type 'application/json'
  File.open('mocks/feeds.json').read
end

这样,当我们和实际的服务进行集成时,只需要连接到那个服务器就可以了。

注意,我们现在的核心是mocks/feeds.json这个文件。这个文件现在的角色就是一个契约,至少对于前端来说是这样的。紧接着,我们的应用需要渲染加星的功能,这就需要另外一个契约:找出当前用户加星过的所有条目,因此我们加入了一个新的契约:
[
    {
        "id": 3,
        "url": "http://abruzzi.github.com/2015/02/build-sample-application-by-using-underscore-and-jquery/",
        "title": "使用underscore.js构建前端应用",
        "publicDate": "2015年1月20日"
    }
]

然后在sinatra中加入一个新的映射:
get '/api/fav-feeds/:id' do
  content_type 'application/json'
  File.open('mocks/fav-feeds.json').read
end

通过这两个请求,我们会得到两个列表,然后根据这两个列表的交集来绘制出所有的星号的状态(有的是空心,有的是实心):
$.when(feeds, favorite).then(function(feeds, favorite) {
    var ids = _.pluck(favorite[0], 'id');
    var extended = _.map(feeds[0], function(feed) {
        return _.extend(feed, {status: _.includes(ids, feed.id)});
    });

    var feedList = new Backbone.Collection(extended);
    var feedListView = new FeedListView(feedList);

    $('.container').append(feedListView.render());
});

剩下的一个问题是当点击红心时,我们需要发请求给后端,然后更新红心的状态:
toggleFavorite: function(event) {
    event.preventDefault();
    var that = this;
    $.post('/api/feeds/'+this.model.get('id')).done(function(){
        var status = that.model.get('status');
        that.model.set('status', !status);
    });
}

这里又多出来一个请求,不过使用Sinatra我们还是可以很容易的支持它:
post '/api/feeds/:id' do
end

可以看到,在没有后端的情况下,我们一切都进展顺利 — 后端甚至还没有开始做,或者正在由一个进度比我们慢的团队在开发,不过无所谓,他们不会影响我们的。

不仅如此,当我们写完前端的代码之后,可以做一个End2End的测试。由于使用了mock数据,免去了数据库和网络的耗时,这个End2End的测试会运行的非常快,并且它确实起到了端到端的作用。这些测试在最后的集成时,还可以用来当UI测试来运行。所谓一举多得。
#encoding: utf-8
require 'spec_helper'

describe 'Feeds List Page' do
  let(:list_page) {FeedListPage.new}

  before do
      list_page.load
  end

  it 'user can see a banner and some feeds' do
      expect(list_page).to have_banner
      expect(list_page).to have_feeds
  end

  it 'user can see 3 feeds in the list' do
      expect(list_page.all_feeds).to have_feed_items count: 3
  end

  it 'feed has some detail information' do
      first = list_page.all_feeds.feed_items.first
      expect(first.title).to eql("Python中的 list comprehension 以及 generator")
  end
end




关于如何编写这样的测试,可以参考之前写的这篇文章

后端开发

我在这个示例中,后端采用了spring-boot作为示例,你应该可以很容易将类似的思路应用到Ruby或者其他语言上。

首先是请求的入口,FeedsController会负责解析请求路径,查数据库,最后返回JSON格式的数据。
@Controller
@RequestMapping("/api")
public class FeedsController {

    @Autowired
    private FeedsService feedsService;

    @Autowired
    private UserService userService;

    public void setFeedsService(FeedsService feedsService) {
        this.feedsService = feedsService;
    }

    public void setUserService(UserService userService) {
        this.userService = userService;
    }

    @RequestMapping(value="/feeds", method = RequestMethod.GET)
    @ResponseBody
    public Iterable<Feed> allFeeds() {
        return feedsService.allFeeds();
    }


    @RequestMapping(value="/fav-feeds/{userId}", method = RequestMethod.GET)
    @ResponseBody
    public Iterable<Feed> favFeeds(@PathVariable("userId") Long userId) {
        return userService.favoriteFeeds(userId);
    }
}

具体查询的细节我们就不做讨论了,感兴趣的可以在文章结尾处找到代码库的链接。那么有了这个Controller之后,我们如何测试它呢?或者说,如何让契约变得实际可用呢?

sprint-test提供了非常优美的DSL来编写测试,我们仅需要一点代码就可以将契约用起来,并实际的监督接口的修改:
private MockMvc mockMvc;
private FeedsService feedsService;
private UserService userService;

@Before
public void setup() {
    feedsService = mock(FeedsService.class);
    userService = mock(UserService.class);

    FeedsController feedsController = new FeedsController();
    feedsController.setFeedsService(feedsService);
    feedsController.setUserService(userService);

    mockMvc = standaloneSetup(feedsController).build();
}

建立了mockmvc之后,我们就可以编写Controller的单元测试了:
@Test
public void shouldResponseWithAllFeeds() throws Exception {
    when(feedsService.allFeeds()).thenReturn(Arrays.asList(prepareFeeds()));

    mockMvc.perform(get("/api/feeds"))
            .andExpect(status().isOk())
            .andExpect(content().contentType("application/json;charset=UTF-8"))
            .andExpect(jsonPath("$", hasSize(3)))
            .andExpect(jsonPath("$[0].publishDate", is(notNullValue())));
}

当发送GET请求到/api/feeds上之后,我们期望返回状态是200,然后内容是application/json。然后我们预期返回的结果是一个长度为3的数组,然后数组中的第一个元素的publishDate字段不为空。

注意此处的prepareFeeds方法,事实上它会去加载mocks/feeds.json文件 — 也就是前端用来测试的mock文件:
private Feed[] prepareFeeds() throws IOException {
    URL resource = getClass().getResource("/mocks/feeds.json");
    ObjectMapper mapper = new ObjectMapper();
    return mapper.readValue(resource, Feed[].class);
}

这样,当后端修改Feed定义(添加/删除/修改字段),或者修改了mock数据等,都会导致测试失败;而前端修改mock之后,也会导致测试失败 — 不要惧怕失败 — 这样的失败会促进一次协商,并驱动出最终的service的契约。

对应的,测试/api/fav-feeds/{userId}的方式类似:
@Test
public void shouldResponseWithUsersFavoriteFeeds() throws Exception {
    when(userService.favoriteFeeds(any(Long.class)))
        .thenReturn(Arrays.asList(prepareFavoriteFeeds()));

    mockMvc.perform(get("/api/fav-feeds/1"))
            .andExpect(status().isOk())
            .andExpect(content().contentType("application/json;charset=UTF-8"))
            .andExpect(jsonPath("$", hasSize(1)))
            .andExpect(jsonPath("$[0].title", is("使用underscore.js构建前端应用")))
            .andExpect(jsonPath("$[0].publishDate", is(notNullValue())));
}

总结

前后端分离是一件容易的事情,而且团队可能在短期可以看到很多好处,但是如果不认真处理集成的问题,分离反而可能会带来更长的集成时间。通过面向契约的方式来组织各自的测试,可以带来很多的好处:更快速的End2End测试,更平滑的集成,更安全的分离开发等等。

代码

前后端的代码我都放到了Gitbub上,感兴趣的可以clone下来自行研究:
1.bookmarks-frontend
2.bookmarks-server

来自:http://icodeit.org/
  • 大小: 51.2 KB
  • 大小: 56.6 KB
  • 大小: 50.2 KB
  • 大小: 90.9 KB
  • 大小: 85.6 KB
4
0
评论 共 13 条 请登录后发表评论
13 楼 lalagu 2015-06-30 18:08
我最近做了一个项目,根据自己的理解,也是这样将前后端给分开了。
感觉不错。前后端分开开发,基本没影响。
12 楼 dwangel 2015-06-29 12:12
如果没有专门做前端的人, 前后端分太开 的风险很高
11 楼 CaryGao 2015-06-29 09:13
我认为,前后端分离在中小项目中适用于多客户端的情况,浏览器,Android,ios,单浏览器的中小项目,越简单越好,即便未来需要扩展,大部分情况都是整个系统重写,所以不用太考虑扩展性。
10 楼 yixiandave 2015-06-26 19:53
CaryGao 写道
如果分前后端开发,那么每个项目开发前必然要先商定好契约,而这个契约又必然随着开发进展变化,尤其是后期维护的时候,一个简单的提交新增用户功能都至少需要两个人搞,是否把项目变的更复杂?

确实有这个问题,或者说我们公司就面临这个问题,由于人手不足的原因结果还是以功能模块分开发人员,开发人员两头兼顾,前后端分离仅仅变成了前后端分成两个项目目录而已。。。
但这样做缺点非常突出,很多开发人员还是按照模板页的方式做开发,即确定一个页面需要哪些数据,然后发出一个ajax请求到后端一次性把所有数据捞出来。这样做出来的项目可维护性非常差,也没办法做产品化,因为一旦某个页面要去掉某个图表,改成另外的某个图表需要做的改动会包括前端页面、后端Controller、后端Service,然后你不知道之前的Service还有哪些地方在用,为了不影响原有功能只好重新写新的方法,成本极高。
我认为前后端分离的契约非常重要,一定要项目经理配合有一定架构和设计能力的开发人员统一制定。尽可能保持高复用
9 楼 CaryGao 2015-06-26 17:12
如果分前后端开发,那么每个项目开发前必然要先商定好契约,而这个契约又必然随着开发进展变化,尤其是后期维护的时候,一个简单的提交新增用户功能都至少需要两个人搞,是否把项目变的更复杂?
8 楼 yixiandave 2015-06-26 13:51
CaryGao 写道
前后端分离有哪些好处?能加速团队速度?是否适用于中小项目?

我认为主要是并行开发。前后端可以在脱离对方的情况下独立开发和测试,不需要互相等进度。
其次前端开发人员和后端开发人员可以专注在自己熟悉的领域,后端开发人员不用再去学习前端的js框架和样式调整,前端开发人员也不需要去关心后端的分布式架构和大数据分析之类
7 楼 CaryGao 2015-06-26 11:16
前后端分离有哪些好处?能加速团队速度?是否适用于中小项目?
6 楼 white_crucifix 2015-06-24 14:59
yixiandave 写道
tangyang332 写道
huazi221 写道
权限怎么控制呢?比如按钮的权限

权限控制应该在前端吧,后端无条件信任前端发来的请求

这个我认为应该都做限制比较好,否则很容易通过前端代码暴露的后端接口直接绕过权限控制体系。


其实权限也好,甚至参数验证也好,理论上是两端都要做。页面做是为了用户体验,后台验证是为了真正的安全。只不过大多数时候懒得去做。或者单从数量上统计,全世界对安全要求高的web系统,还是少数。
5 楼 white_crucifix 2015-06-24 14:54
让我想起我刚实习那会做的项目。页面是html+extjs,后台是php、java等。
我要写一个前端功能,发一个请求到后台
于是我同事写一个后台方法处理,然后放到测试环境上
我再通过页面去测试
出现问题了如果是后台的错,再去跟同事沟通

过程还是比较原始的,毕竟大家都没有这方面的经验
4 楼 yixiandave 2015-06-24 13:15
tangyang332 写道
huazi221 写道
权限怎么控制呢?比如按钮的权限

权限控制应该在前端吧,后端无条件信任前端发来的请求

这个我认为应该都做限制比较好,否则很容易通过前端代码暴露的后端接口直接绕过权限控制体系。
3 楼 tangyang332 2015-06-24 12:40
huazi221 写道
权限怎么控制呢?比如按钮的权限

权限控制应该在前端吧,后端无条件信任前端发来的请求
2 楼 yixiandave 2015-06-24 11:33
huazi221 写道
权限怎么控制呢?比如按钮的权限

相当于为前端和后端中间加了一层中间服务层,我在考虑是否用nodejs,然后这个中间服务层后期还可以过渡为反向代理服务器以及负载均衡服务器
1 楼 huazi221 2015-06-24 10:40
权限怎么控制呢?比如按钮的权限

发表评论

您还没有登录,请您登录后再发表评论

相关推荐

  • 前后端分离技术

    对目前的web来说,前后端分离已经变得越来越流行了,越来越多的企业/网站都开始往这个方向靠拢。那么,为什么要选择前后端分离呢?前后端分离对实际开发有什么好处呢?本文章将对此做出详细的讲解。

  • java url路径包含中文_JAVA开发教程实战系列1

    Java开发入门本章学习目标了解Java语言的特点熟练掌握Java开发环境的搭建熟练掌握环境变量的配置理解Java的运行机制Java经过了多年的快速发展,成为了最受欢迎的开发语言之一,截至目前有超过400万以上的程序员在使用Java语言,现在的Java是第9个主要版本。Java概述认识JavaJava是一门面向对象编程语言,它吸收了C++语言的各种优点,摒弃了C++中难以理解的多继承、指针等概念,...

  • Spring Boot Vue前后端分离开发实战.pdf

    Spring Boot Vue前后端分离开发实战.pdf 上手简单,文档讲述清晰,实在好用。

  • 详解Flask前后端分离项目案例

    主要介绍了Flask前后端分离项目案例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

  • c# .net core 3.1 好用的通用后台管理 前后端分离框架

    前后端分离框架,基于.NET5/.NET6实现的通用权限管理平台。整合最新技术高效快速开发,前后端分离模式,开箱即用。 代码量少、学习简单、通俗易懂、功能强大、易扩展、轻量级,让web开发更快速、简单高效(从此告别...

  • 小谈什么是前后端分离?

    什么是前后端分离? 学习目标什么是前后端分离?前后端分离初了解为什么要前后端分离?1、前后职责分离2、前后技术分离3、前后分离带来了用户用户体验和业务处理解耦4、前后分离,可以分别归约两端的设计前后分离...

  • 前后端分离怎么实现?

    前后端分离,相信这是很多人都听到的一个词,那么,前后端分离怎么实现?大家理解的前后端分离指的又是什么?

  • 【干货】前后端分离怎么部署?

    本文将分享给大家的是前后端分离如何部署的知识,满满的都是干货,希望您能有所收获。

  • 前后端分离项目中引入activiti工作流引擎

    基于前后端分离项目引入activiti工作流引擎,某些配置信息需根据自己项目情况修改

  • (自学/初学者普及)浅谈前后端与前后端分离(别再说你不懂什么是前后端分离)

    程序员都在说前后端分离,开发岗位也被很明确的分成了前后端工程师,很多大学的刚进入计算机专业的小伙伴和打算进入计算机行业的朋友,通常会有这些问题: 究竟什么是前后端呢? 前后端分离又是什么呢? 为什么会有...

  • 部署前后端分离式nginx配置的完整步骤

    主要给大家介绍了关于如何部署前后端分离式nginx配置的完整步骤,文中通过示例代码介绍的非常详细,对大家学习或者使用nginx具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧

  • 若依前后端分离项目部署文档(完整版)

    第一部分:部署linux + nginx 第二部分:部署Windows+tomcat 第三部分:调用第三方api的跨域问题处理。 以及常见的部署后页面显示404 的问题处理。...高效率开发,使用代码生成器可以一键生成前后端代码。)

  • 基于百度API实现的人脸识别demo-Java前后端分离实现

    本资源是基于百度API实现的人脸识别小demo,包含人脸注册、人脸登录、人脸检测、人脸在线活体检测、人脸识别等,Java语言前后端分离实现,sql导入数据库,前后端启动即可验证

  • SpringBoot+Vue.js实现前后端分离的文件上传功能

    主要介绍了SpringBoot+Vue.js实现前后端分离的文件上传功能,需要的朋友可以参考下

  • 轻松理解前后端分离(通俗易懂)

    文章目录一、前言二、前后端分离本质三、1.前后端分离2.不使用前后端分离会有什么问题 一、前言 该技术博客是关于楠哥教你学Java的前后端分离课程笔记总结,希望可以为大家带来帮助! 二、前后端分离本质 大家往往会...

  • ssm+vue前后端分离框架整合实现(附源码)

    主要介绍了ssm+vue前后端分离框架整合实现(附源码),文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

  • spring boot+vue 的前后端分离与合并方案实例详解

    主要介绍了spring boot+vue 的前后端分离与合并方案实例详解,需要的朋友可以参考下

  • 前后端分离的跨域问题

    在现在流行的前后端分离开发中,跨域问题突显了出来,跨域问题的根本原因:浏览器有同源策略限制,当前域名的js只能读取同域下的窗口属性,这是一个基础安全功能。那么什么是同源策略呢?即两资源的URL中 协议,域名...

Global site tag (gtag.js) - Google Analytics