中文字幕在线观看,亚洲а∨天堂久久精品9966,亚洲成a人片在线观看你懂的,亚洲av成人片无码网站,亚洲国产精品无码久久久五月天

前后端分離了,然后呢?

2018-07-20    來源:編程學(xué)習(xí)網(wǎng)

容器云強(qiáng)勢上線!快速搭建集群,上萬Linux鏡像隨意使用

前言

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

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

  一點背景

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  一個例子

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

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

  前端開發(fā)

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

[ {"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)建自動監(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日"} ]

  然后,一個可能的方式是通過請求這個json來測試前臺:

$(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ā)送請求的url并不是最終的,當(dāng)集成的時候我們又需要修改為真實的url。一個簡單的做法是使用Sinatra來做一次url的轉(zhuǎn)換:

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

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

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

[ {"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中加入一個新的映射:

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

  通過這兩個請求,我們會得到兩個列表,然后根據(jù)這兩個列表的交集來繪制出所有的星號的狀態(tài)(有的是空心,有的是實心):

$.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()); });

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

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

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

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

#encoding: utf-8require'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:3end 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

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

  后端開發(fā)

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

  首先是請求的入口,F(xiàn)eedsController會負(fù)責(zé)解析請求路徑,查數(shù)據(jù)庫,最后返回JSON格式的數(shù)據(jù)。

@Controller @RequestMapping("/api") publicclassFeedsController { @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() {returnfeedsService.allFeeds(); } @RequestMapping(value="/fav-feeds/{userId}", method =RequestMethod.GET) @ResponseBody public Iterable<Feed> favFeeds(@PathVariable("userId") Long userId) {returnuserService.favoriteFeeds(userId); } }

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

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

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()))); }

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

  注意此處的prepareFeeds方法,事實上它會去加載mocks/feeds.json文件 — 也就是前端用來測試的mock文件:

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

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

  對應(yīng)的,測試/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構(gòu)建前端應(yīng)用"))) .andExpect(jsonPath("$[0].publishDate",is(notNullValue()))); }

  總結(jié)

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

  代碼

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

  1. bookmarks-frontend
  2. bookmarks-server

標(biāo)簽: http服務(wù)器 安全 代碼 服務(wù)器 腳本 數(shù)據(jù)庫 通信 網(wǎng)絡(luò)

版權(quán)申明:本站文章部分自網(wǎng)絡(luò),如有侵權(quán),請聯(lián)系:west999com@outlook.com
特別注意:本站所有轉(zhuǎn)載文章言論不代表本站觀點!
本站所提供的圖片等素材,版權(quán)歸原作者所有,如需使用,請與原作者聯(lián)系。

上一篇:軟件開發(fā)所經(jīng)歷的生命周期

下一篇:如何為你的UIView添加邊緣分割線