• <noscript id="eom2a"><optgroup id="eom2a"></optgroup></noscript>
    <tt id="eom2a"><small id="eom2a"></small></tt>
    <input id="eom2a"></input>
  • <div id="eom2a"><small id="eom2a"></small></div>
    <td id="eom2a"><small id="eom2a"></small></td>
  • 您的位置:知識庫 ? Web前端

    前后端分離了,然后呢?

    來源: icodeit.org  發布時間: 2015-07-02 12:00  閱讀: 53157 次  推薦: 47   原文鏈接   [收藏]  

      前言

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

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

      一點背景

      一個典型的Web應用的布局看起來是這樣的:

    typical web application

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

      上述的場景還是比較理想,我們事實上在實際環境中會有非常復雜的場景,比如異構的網絡,異構的操作系統等等:

    real word application

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

    mock application

      這個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更加易讀,所以就采用了它。

      一個例子

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

    bookmarks

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

      前端開發

      根據通常的做法,前后端分離之后,我們很容易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

    end 2 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
    47
    3

    Web前端熱門文章

      Web前端最新文章

        最新新聞

          熱門新聞

            黄色网_免费在线黄色电影_黄色成人快播电影_伦理电影_黄色片