久久国产成人av_抖音国产毛片_a片网站免费观看_A片无码播放手机在线观看,色五月在线观看,亚洲精品m在线观看,女人自慰的免费网址,悠悠在线观看精品视频,一级日本片免费的,亚洲精品久,国产精品成人久久久久久久

分享

前后端分離了,,然后呢?

 icecity1306 2015-07-01

前言

前后端分離已經(jīng)是業(yè)界所共識(shí)的一種開(kāi)發(fā)/部署模式了,。所謂的前后端分離,,并不是傳統(tǒng)行業(yè)中的按部門劃分,一部分人純做前端(HTML/CSS/JavaScript/Flex),,另一部分人純做后端,,因?yàn)檫@種方式是不工作的:比如很多團(tuán)隊(duì)采取了后端的模板技術(shù)(JSP, FreeMarker, ERB等等),前端的開(kāi)發(fā)和調(diào)試需要一個(gè)后臺(tái)Web容器的支持,,從而無(wú)法做到真正的分離(更不用提在部署的時(shí)候,,由于動(dòng)態(tài)內(nèi)容和靜態(tài)內(nèi)容混在一起,當(dāng)設(shè)計(jì)動(dòng)態(tài)靜態(tài)分流的時(shí)候,,處理起來(lái)非常麻煩),。關(guān)于前后端開(kāi)發(fā)的另一個(gè)討論可以參考這里

即使通過(guò)API來(lái)解耦前端和后端開(kāi)發(fā)過(guò)程,,前后端通過(guò)RESTFul的接口來(lái)通信,,前端的靜態(tài)內(nèi)容和后端的動(dòng)態(tài)計(jì)算分別開(kāi)發(fā),分別部署,,集成仍然是一個(gè)繞不開(kāi)的問(wèn)題 — 前端/后端的應(yīng)用都可以獨(dú)立的運(yùn)行,,但是集成起來(lái)卻不工作。我們需要花費(fèi)大量的精力來(lái)調(diào)試,,直到上線前仍然沒(méi)有人有信心所有的接口都是工作的,。

一點(diǎn)背景

一個(gè)典型的Web應(yīng)用的布局看起來(lái)是這樣的:

typical web application

前后端都各自有自己的開(kāi)發(fā)流程,構(gòu)建工具,,測(cè)試集合等等,。前后端僅僅通過(guò)接口來(lái)編程,這個(gè)接口可能是JSON格式的RESTFul的接口,,也可能是XML的,重點(diǎn)是后臺(tái)只負(fù)責(zé)數(shù)據(jù)的提供和計(jì)算,,而完全不處理展現(xiàn),。而前端則負(fù)責(zé)拿到數(shù)據(jù),組織數(shù)據(jù)并展現(xiàn)的工作,。這樣結(jié)構(gòu)清晰,,關(guān)注點(diǎn)分離,,前后端會(huì)變得相對(duì)獨(dú)立并松耦合。

上述的場(chǎng)景還是比較理想,,我們事實(shí)上在實(shí)際環(huán)境中會(huì)有非常復(fù)雜的場(chǎng)景,,比如異構(gòu)的網(wǎng)絡(luò),異構(gòu)的操作系統(tǒng)等等:

real word application

在實(shí)際的場(chǎng)景中,,后端可能還會(huì)更復(fù)雜,,比如用C語(yǔ)言做數(shù)據(jù)采集,然后通過(guò)Java整合到一個(gè)數(shù)據(jù)倉(cāng)庫(kù),,然后該數(shù)據(jù)倉(cāng)庫(kù)又有一層Web Service,,最后若干個(gè)這樣的Web Service又被一個(gè)Ruby的聚合Service整合在一起返回給前端。在這樣一個(gè)復(fù)雜的系統(tǒng)中,,后臺(tái)任意端點(diǎn)的失敗都可能阻塞前端的開(kāi)發(fā)流程,,因此我們會(huì)采用mock的方式來(lái)解決這個(gè)問(wèn)題:

mock application

這個(gè)mock服務(wù)器可以啟動(dòng)一個(gè)簡(jiǎn)單的HTTP服務(wù)器,然后將一些靜態(tài)的內(nèi)容serve出來(lái),,以供前端代碼使用,。這樣的好處很多:

  1. 前后端開(kāi)發(fā)相對(duì)獨(dú)立
  2. 后端的進(jìn)度不會(huì)影響前端開(kāi)發(fā)
  3. 啟動(dòng)速度更快
  4. 前后端都可以使用自己熟悉的技術(shù)棧(讓前端的學(xué)maven,讓后端的用gulp都會(huì)很不順手)

但是當(dāng)集成依然是一個(gè)令人頭疼的難題,。我們往往在集成的時(shí)候才發(fā)現(xiàn),,本來(lái)協(xié)商的數(shù)據(jù)結(jié)構(gòu)變了:deliveryAddress字段本來(lái)是一個(gè)字符串,現(xiàn)在變成數(shù)組了(業(yè)務(wù)發(fā)生了變更,,系統(tǒng)現(xiàn)在可以支持多個(gè)快遞地址),;price字段變成字符串,協(xié)商的時(shí)候是number,;用戶郵箱地址多了一個(gè)層級(jí)等等,。這些變動(dòng)在所難免,而且時(shí)有發(fā)生,,這會(huì)花費(fèi)大量的調(diào)試時(shí)間和集成時(shí)間,,更別提修改之后的回歸測(cè)試了。

所以僅僅使用一個(gè)靜態(tài)服務(wù)器,,然后提供mock數(shù)據(jù)是遠(yuǎn)遠(yuǎn)不夠的,。我們需要的mock應(yīng)該還能做到:

  1. 前端依賴指定格式的mock數(shù)據(jù)來(lái)進(jìn)行UI開(kāi)發(fā)
  2. 前端的開(kāi)發(fā)和測(cè)試都基于這些mock數(shù)據(jù)
  3. 后端產(chǎn)生指定格式的mock數(shù)據(jù)
  4. 后端需要測(cè)試來(lái)確保生成的mock數(shù)據(jù)正是前端需要的

簡(jiǎn)而言之,我們需要商定一些契約,,并將這些契約作為可以被測(cè)試的中間格式,。然后前后端都需要有測(cè)試來(lái)使用這些契約。一旦契約發(fā)生變化,,則另一方的測(cè)試會(huì)失敗,,這樣就會(huì)驅(qū)動(dòng)雙方協(xié)商,并降低集成時(shí)的浪費(fèi),。

一個(gè)實(shí)際的場(chǎng)景是:前端發(fā)現(xiàn)已有的某個(gè)契約中,,缺少了一個(gè)address的字段,,于是就在契約中添加了該字段。然后在UI上將這個(gè)字段正確的展現(xiàn)了(當(dāng)然還設(shè)置了字體,,字號(hào),,顏色等等)。但是后臺(tái)生成該契約的服務(wù)并沒(méi)有感知到這一變化,,當(dāng)運(yùn)行生成契約部分測(cè)試(后臺(tái))時(shí),,測(cè)試會(huì)失敗了 — 因?yàn)樗](méi)有生成這個(gè)字段。于是后端工程師就找前端來(lái)商量,,了解業(yè)務(wù)邏輯之后,,他會(huì)修改代碼,并保證測(cè)試通過(guò),。這樣,,當(dāng)集成的時(shí)候,就不會(huì)出現(xiàn)UI上少了一個(gè)字段,,但是誰(shuí)也不知道是前端問(wèn)題,,后端問(wèn)題,還是數(shù)據(jù)庫(kù)問(wèn)題等,。

而且實(shí)際的項(xiàng)目中,,往往都是多個(gè)頁(yè)面,多個(gè)API,,多個(gè)版本,,多個(gè)團(tuán)隊(duì)同時(shí)進(jìn)行開(kāi)發(fā),這樣的契約會(huì)降低非常多的調(diào)試時(shí)間,,使得集成相對(duì)平滑,。

在實(shí)踐中,契約可以定義為一個(gè)JSON文件,,或者一個(gè)XML的payload,。只需要保證前后端共享同一個(gè)契約集合來(lái)做測(cè)試,那么集成工作就會(huì)從中受益,。一個(gè)最簡(jiǎn)單的形式是:提供一些靜態(tài)的mock文件,,而前端所有發(fā)往后臺(tái)的請(qǐng)求都被某種機(jī)制攔截,并轉(zhuǎn)換成對(duì)該靜態(tài)資源的請(qǐng)求,。

  1. moco,,基于Java
  2. wiremock,基于Java
  3. sinatra,,基于Ruby

看到sinatra被列在這里,,可能熟悉Ruby的人會(huì)反對(duì):它可是一個(gè)后端全功能的的程序庫(kù)啊。之所以列它在這里,是因?yàn)?code>sinatra提供了一套簡(jiǎn)潔優(yōu)美的DSL,,這個(gè)DSL非常契合Web語(yǔ)言,我找不到更漂亮的方式來(lái)使得這個(gè)mock server更加易讀,,所以就采用了它,。

一個(gè)例子

我們以這個(gè)應(yīng)用為示例,來(lái)說(shuō)明如何在前后端分離之后,,保證代碼的質(zhì)量,,并降低集成的成本。這個(gè)應(yīng)用場(chǎng)景很簡(jiǎn)單:所有人都可以看到一個(gè)條目列表,,每個(gè)登陸用戶都可以選擇自己喜歡的條目,,并為之加星。加星之后的條目會(huì)保存到用戶自己的個(gè)人中心中,。用戶界面看起來(lái)是這樣的:

bookmarks

不過(guò)為了專注在我們的中心上,,我去掉了諸如登陸,個(gè)人中心之類的頁(yè)面,,假設(shè)你是一個(gè)已登錄用戶,,然后我們來(lái)看看如何編寫測(cè)試。

前端開(kāi)發(fā)

根據(jù)通常的做法,,前后端分離之后,,我們很容易mock一些數(shù)據(jù)來(lái)自己測(cè)試:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[
    {
        "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構(gòu)建自動(dòng)監(jiān)控腳本",
        "publicDate": "2015年2月1日"
    },
    {
        "id": 3,
        "url": "http://abruzzi.github.com/2015/02/build-sample-application-by-using-underscore-and-jquery/",
        "title": "使用underscore.js構(gòu)建前端應(yīng)用",
        "publicDate": "2015年1月20日"
    }
]

然后,一個(gè)可能的方式是通過(guò)請(qǐng)求這個(gè)json來(lái)測(cè)試前臺(tái):

1
2
3
4
5
6
7
8
$(function() {
  $.get('/mocks/feeds.json').then(function(feeds) {
      var feedList = new Backbone.Collection(extended);
      var feedListView = new FeedListView(feedList);

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

這樣當(dāng)然是可以工作的,,但是這里發(fā)送請(qǐng)求的url并不是最終的,,當(dāng)集成的時(shí)候我們又需要修改為真實(shí)的url。一個(gè)簡(jiǎn)單的做法是使用Sinatra來(lái)做一次url的轉(zhuǎn)換:

1
2
3
4
get '/api/feeds' do
  content_type 'application/json'
  File.open('mocks/feeds.json').read
end

這樣,,當(dāng)我們和實(shí)際的服務(wù)進(jìn)行集成時(shí),,只需要連接到那個(gè)服務(wù)器就可以了。

注意,,我們現(xiàn)在的核心是mocks/feeds.json這個(gè)文件,。這個(gè)文件現(xiàn)在的角色就是一個(gè)契約,至少對(duì)于前端來(lái)說(shuō)是這樣的,。緊接著,,我們的應(yīng)用需要渲染加星的功能,這就需要另外一個(gè)契約:找出當(dāng)前用戶加星過(guò)的所有條目,,因此我們加入了一個(gè)新的契約:

1
2
3
4
5
6
7
8
[
    {
        "id": 3,
        "url": "http://abruzzi.github.com/2015/02/build-sample-application-by-using-underscore-and-jquery/",
        "title": "使用underscore.js構(gòu)建前端應(yīng)用",
        "publicDate": "2015年1月20日"
    }
]

然后在sinatra中加入一個(gè)新的映射:

1
2
3
4
get '/api/fav-feeds/:id' do
  content_type 'application/json'
  File.open('mocks/fav-feeds.json').read
end

通過(guò)這兩個(gè)請(qǐng)求,,我們會(huì)得到兩個(gè)列表,然后根據(jù)這兩個(gè)列表的交集來(lái)繪制出所有的星號(hào)的狀態(tài)(有的是空心,,有的是實(shí)心):

1
2
3
4
5
6
7
8
9
10
11
$.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());
});

剩下的一個(gè)問(wèn)題是當(dāng)點(diǎn)擊紅心時(shí),,我們需要發(fā)請(qǐng)求給后端,然后更新紅心的狀態(tài):

1
2
3
4
5
6
7
8
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);
    });
}

這里又多出來(lái)一個(gè)請(qǐng)求,不過(guò)使用Sinatra我們還是可以很容易的支持它:

1
2
post '/api/feeds/:id' do
end

可以看到,,在沒(méi)有后端的情況下,,我們一切都進(jìn)展順利 — 后端甚至還沒(méi)有開(kāi)始做,或者正在由一個(gè)進(jìn)度比我們慢的團(tuán)隊(duì)在開(kāi)發(fā),,不過(guò)無(wú)所謂,,他們不會(huì)影響我們的。

不僅如此,,當(dāng)我們寫完前端的代碼之后,,可以做一個(gè)End2End的測(cè)試。由于使用了mock數(shù)據(jù),,免去了數(shù)據(jù)庫(kù)和網(wǎng)絡(luò)的耗時(shí),,這個(gè)End2End的測(cè)試會(huì)運(yùn)行的非常快,,并且它確實(shí)起到了端到端的作用,。這些測(cè)試在最后的集成時(shí),還可以用來(lái)當(dāng)UI測(cè)試來(lái)運(yùn)行,。所謂一舉多得,。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#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

end 2 end

關(guān)于如何編寫這樣的測(cè)試,可以參考之前寫的這篇文章,。

后端開(kāi)發(fā)

我在這個(gè)示例中,,后端采用了spring-boot作為示例,你應(yīng)該可以很容易將類似的思路應(yīng)用到Ruby或者其他語(yǔ)言上,。

首先是請(qǐng)求的入口,,FeedsController會(huì)負(fù)責(zé)解析請(qǐng)求路徑,查數(shù)據(jù)庫(kù),,最后返回JSON格式的數(shù)據(jù),。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@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);
    }
}

具體查詢的細(xì)節(jié)我們就不做討論了,感興趣的可以在文章結(jié)尾處找到代碼庫(kù)的鏈接,。那么有了這個(gè)Controller之后,,我們?nèi)绾螠y(cè)試它呢?或者說(shuō),,如何讓契約變得實(shí)際可用呢,?

sprint-test提供了非常優(yōu)美的DSL來(lái)編寫測(cè)試,我們僅需要一點(diǎn)代碼就可以將契約用起來(lái),,并實(shí)際的監(jiān)督接口的修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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的單元測(cè)試了:

1
2
3
4
5
6
7
8
9
10
@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())));
}

當(dāng)發(fā)送GET請(qǐng)求到/api/feeds上之后,我們期望返回狀態(tài)是200,,然后內(nèi)容是application/json,。然后我們預(yù)期返回的結(jié)果是一個(gè)長(zhǎng)度為3的數(shù)組,,然后數(shù)組中的第一個(gè)元素的publishDate字段不為空。

注意此處的prepareFeeds方法,,事實(shí)上它會(huì)去加載mocks/feeds.json文件 — 也就是前端用來(lái)測(cè)試的mock文件:

1
2
3
4
5
private Feed[] prepareFeeds() throws IOException {
    URL resource = getClass().getResource("/mocks/feeds.json");
    ObjectMapper mapper = new ObjectMapper();
    return mapper.readValue(resource, Feed[].class);
}

這樣,,當(dāng)后端修改Feed定義(添加/刪除/修改字段),或者修改了mock數(shù)據(jù)等,,都會(huì)導(dǎo)致測(cè)試失?。欢岸诵薷膍ock之后,,也會(huì)導(dǎo)致測(cè)試失敗 — 不要懼怕失敗 — 這樣的失敗會(huì)促進(jìn)一次協(xié)商,并驅(qū)動(dòng)出最終的service的契約,。

對(duì)應(yīng)的,,測(cè)試/api/fav-feeds/{userId}的方式類似:

1
2
3
4
5
6
7
8
9
10
11
12
@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構(gòu)建前端應(yīng)用")))
            .andExpect(jsonPath("$[0].publishDate", is(notNullValue())));
}

總結(jié)

前后端分離是一件容易的事情,而且團(tuán)隊(duì)可能在短期可以看到很多好處,,但是如果不認(rèn)真處理集成的問(wèn)題,,分離反而可能會(huì)帶來(lái)更長(zhǎng)的集成時(shí)間。通過(guò)面向契約的方式來(lái)組織各自的測(cè)試,,可以帶來(lái)很多的好處:更快速的End2End測(cè)試,,更平滑的集成,更安全的分離開(kāi)發(fā)等等,。

代碼

前后端的代碼我都放到了Gitbub上,,感興趣的可以clone下來(lái)自行研究:

  1. bookmarks-frontend
  2. bookmarks-server

    本站是提供個(gè)人知識(shí)管理的網(wǎng)絡(luò)存儲(chǔ)空間,所有內(nèi)容均由用戶發(fā)布,,不代表本站觀點(diǎn),。請(qǐng)注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購(gòu)買等信息,,謹(jǐn)防詐騙,。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請(qǐng)點(diǎn)擊一鍵舉報(bào),。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評(píng)論

    發(fā)表

    請(qǐng)遵守用戶 評(píng)論公約

    類似文章 更多