前后端分離API設計指南

原文首發:http://www.zhoulujun.cn/zhoulujun/html/theory/model/8026.html

談前后端分工,接口設計,resetful啥,不得不談談web的發展史。

在web元年,每個web開發工程師都是真正的全棧工程師。哪有什么前后不搭邊的事兒!

到MVC時代,術業開始專工了。有最流行的Spring,有了iBatis這樣的數據持久層框架,即ORM,對象關系映射,有……%……

于是,package就會有這樣的幾個文件夾:

|____mappers

|____model

|____service

|____utils

|____controller

在mappers這一層,我們所做的莫過于如下所示的數據庫相關查詢:

@Insert(

"INSERT?INTO?users(username,?password,?enabled)?"?+

"VALUES?(#{userName},?#{passwordHash},?#{enabled})"

)

@Options(keyProperty?=?"id",?keyColumn?=?"id",?useGeneratedKeys?=?true)

void?insert(User?user);

model文件夾和mappers文件夾都是數據層的一部分,只是兩者間的職責不同,如:

public?String?getUserName()?{

return?userName;

}

public?void?setUserName(String?userName)?{

this.userName?=?userName;

}

而他們最后都需要在Controller,又或者稱為ModelAndView中處理:

@RequestMapping(value?=?{"/disableUser"},?method?=?RequestMethod.POST)

public?ModelAndView?processUserDisable(HttpServletRequest?request,?ModelMap?model)?{

String?userName?=?request.getParameter("userName");

User?user?=?userService.getByUsername(userName);

userService.disable(user);

Map?map?=?new?HashMap();

Map??usersWithRoles=?userService.getAllUsersWithRole();

model.put("usersWithRoles",usersWithRoles);

return?new?ModelAndView("redirect:users",map);

}

在多數時候,Controller不應該直接與數據層的一部分,而將業務邏輯放在Controller層又是一種錯誤,這時就有了Service層,如下圖:

3

Domain(業務)是一個相當復雜的層級,這里是業務的核心。一個合理的Controller只應該做自己應該做的事,它不應該處理業務相關的代碼

if?(isNewnameEmpty?==?false?&&?newuser?==?null){

user.setUserName(newUsername);

List?myPosts?=?postService.findMainPostByAuthorNameSortedByCreateTime(principal.getName());

for?(int?k?=?0;k?<?myPosts.size();k++){

Post?post?=?myPosts.get(k);

post.setAuthorName(newUsername);

postService.save(post);

}

userService.update(user);

Authentication?oldAuthentication?=?SecurityContextHolder.getContext().getAuthentication();

Authentication?authentication?=?null;

if(oldAuthentication?==?null){

authentication?=?new?UsernamePasswordAuthenticationToken(newUsername,user.getPasswordHash());

}else{

authentication?=?new?UsernamePasswordAuthenticationToken(newUsername,user.getPasswordHash(),oldAuthentication.getAuthorities());

}

SecurityContextHolder.getContext().setAuthentication(authentication);

map.clear();

map.put("user",user);

model.addAttribute("myPosts",?myPosts);

model.addAttribute("namesuccess",?"User?Profile?updated?successfully");

return?new?ModelAndView("user/profile",?map);

}

我們在Controller層應該做的事是:

處理請求的參數

渲染和重定向

選擇Model和Service

處理Session和Cookies

業務是善變的,昨天我們可能還在和對手競爭誰先推出新功能,但是今天可能已經合并了。我們很難預見業務變化,但是我們應該能預見Controller是不容易變化的。在一些設計里面,這種模式就是Command模式。

View層是一直在變化的層級,人們的品味一直在更新,有時甚至可能因為競爭對手而產生變化。在已經取得一定市場的情況下,Model-Service-Controller通常都不太會變動,甚至不敢變動。企業意識到創新的兩面性,要么帶來死亡,要么占領更大的市場。但是對手通常都比你想象中的更聰明一些,所以這時開創新的業務是一個更好的選擇。

高速發展期的企業和發展初期的企業相比,更需要前端開發人員。在用戶基數不夠、業務待定的情形中,View只要可用并美觀就行了,這時可能就會有大量的業務代碼放在View層:

${errors.username}?${errors.password}

Woohoo,?User?${user.userName}?has?been?created?successfully!

不同的情形下,人們都會對此有所爭議,但只要符合當前的業務便是最好的選擇。作為一個前端開發人員,在過去我需要修改JSP、PHP文件,

到了web2.0,阿賈克斯(ajax)這玩意蹦的挺歡,把交互玩high了,

各種流氓架構師,把Server Side Render ?提到Client Side Render

總的來說,從原來的cs到bs,到app,h5,前端, 只是gui程序而已,他應該只是負責數據層面的展示和反饋。

但是,我們還是基于類MVC模式。只是數據的獲取方式變成了Ajax,我們就犯了一個錯誤——將大量的業務邏輯放在前端。這時候我們已經不能再從View層直接訪問Model層,從安全的角度來說有點危險。

離開了JSP,將View層變成了Template與Controller。而原有的Services層并不是只承擔其原來的責任,這些Services開始向ViewModel改變。

于是,在本來就脆弱的網絡上,加載 巨無霸的 頁面請求。

前端,后端,都得熟悉業務。但是,業務的需求總是在變,而且一個人做好多項目。如果來了一個新人接手,將業務就是半天。

業務,也分拆從數據流,有清晰數據走向圖。前后臺接口并聯開發,業務整合串聯調試。

為了前后端更好的分工,接口文檔是必須的,前后端都根據接口文檔寫代碼,然后對接接口就行了。

但是,后端跟不上前端節奏,接口跟不上來怎么辦?即便接口跟上來了,大后端數據跟不上又怎么辦?

第一種想到的方法就是模擬返回數據,根據接口文檔定義好的返回數據格式,新建一個json文件夾,里面放一堆*.json文件,像這樣:

然后請求json數據,像這樣:

所以為了前端有數據,就會有很多很多的請求json文件。當后端接口上來后,又要一個一個挨著去把json請求改為真實接口名,這就要求代碼需要寫的比較規范,不然接口的對接真的很難受,而且在修改某些復雜邏輯的地方的時候還必須的小心翼翼,不然就只有等測試發來bug單了。

造json文件模擬請求對于小的項目確實還是挺方便的,但是項目大了呢,上百個接口甚至更多呢?

一堆一堆的json看著都煩,還不說前后端對接了。

2.第二,寫好接口,返回假數據,但是,這個比直接寫json延后的工期更長……

……………………………………………………華麗的等等等

目前大部分公司都實行了前后端分離開發。在項目開發過程當中,經常會遇到以下幾個尷尬的場景;

1、沒有文檔的庫。就好像在操縱一個黑盒一樣,預期不了它的正常行為是什么。比如:輸入了一個 A,預期返回的是一個 B,結果它什么也沒有。有的時候,還拋出了一堆異常,導致你的應用崩潰。對此你是狗咬烏龜

2、文檔老舊,并且不夠全面。這個問題相比于沒有文檔來說,愈加的可怕。我們需要的接口不在文檔上,文檔上的接口不存在庫里,又或者是少了一行關鍵的代碼。

3、前端開發依賴于后端接口數據,需要與后端接口聯調才能獲得數據展示,從而拖慢了開發進度;

4、沒有一個很好的結構化接口文檔管理工具,能夠對項目中所用到的接口進行管理。如一個請求的地址、有幾個參數、參數名稱及類型含義等等。同時支持項目、歷史版本的切換。

5、沒有一個號的接口管理、版本控制工具。

……………………………………………………華麗的等等等

API 都搞不好,還怎么當程序員?如果 API 設計只是后臺的活,為什么還需要前端工程師。

在前后端分離的項目里,API 也是這樣一個煩人的存在。我們就經常遇到各種各樣的問題:

*API 的字段更新了

*API 的路由更新了

*API 返回了未預期的值

*API 返回由于某種原因被刪除了

……………………………………………………華麗的等等等

**API 的維護是一件煩人的事,所以最好能一次設計好 API。**可是這是不可能的,API 在其的生命周期里,應該是要不斷地演進的。它與精益創業的思想是相似的,當一個 API 不合適現有場景時,應該對這個 API 進行更新,以滿足需求。也因此,API 本身是面向變化的,問題是這種變化是雙向的、單向的、聯動的?還是靜默的?

API 設計是一個非常大的話題,這里我們只討論:演進、設計及維護

新的業務需求來臨時,前端、后臺是一起開始工作的。而不是后臺在前,又或者前端先完成。他們開始與業務人員溝通,需要在頁面上顯示哪些內容,需要做哪一些轉換及特殊處理。

然后便配合著去設計相應的 API:請求的 API 路徑是哪一個、請求里要有哪些參數、是否需要鑒權處理等等。對于返回結果來說,仍然也需要一系列的定義:返回哪些相應的字段、額外的顯示參數、特殊的 header 返回等等。除此,還需要討論一些異常情況,如用戶授權失敗,服務端沒有返回結果。

整理出一個相應的文檔約定,前端與后臺便去編寫相應的實現代碼。

最后,再經歷痛苦的集成,便算是能完成了工作。

可是,API 在這個過程中是不斷變化的,因此在這個過程中需要的是協作能力。它也能從側面地反映中,團隊的協作水平。

API 的協作設計

API 設計應該由前端開發者來驅動的。后臺只提供前端想要的數據,而不是反過來的。后臺提供數據,前端從中選擇需要的內容。

我們常報怨后臺 API 設計得不合理,主要便是因為后臺不知道前端需要什么內容。這就好像我們接到了一個需求,而 UX 或者美工給老板見過設計圖,但是并沒有給我們看。我們能設計出符合需求的界面嗎?答案,不用想也知道。

因此,當我們把 API 的設計交給后臺的時候,也就意味著這個 API 將更符合后臺的需求。那么它的設計就趨向于對后臺更簡單的結果,比如后臺返回給前端一個 Unix 時間,而前端需要的是一個標準時間。又或者是反過來的,前端需要的是一個 Unix 時間,而后臺返回給你的是當地的時間。

與此同時,按前端人員的假設,我們也會做類似的、『不正確』的 API 設計。

因此,API 設計這種活動便像是一個博弈。

使用文檔規范 API的糟點

不論是異地,或者是坐一起協作開發,使用 API 文檔來確保對接成功,是一個“低成本”、較為通用的選擇。在這一點上,使用接口及函數調用,與使用 REST API 來進行通訊,并沒有太大的區別。

先寫一個 API 文檔,雙方一起來維護,文檔放在一個公共的地方,方便修改,方便溝通。慢慢的再隨著這個過程中的一些變化,如無法提供事先定好的接口、不需要某個值等等,再去修改接口及文檔。

可這個時候因為沒有一個可用的 API,因此前端開發人員便需要自己去 Mock 數據,或者搭建一個 Mock Server 來完成后續的工作。

因此,這個時候就出現了兩個問題:

維護 API 文檔很痛苦

需要一個同步的 Mock Server

而在早期,開發人員有同樣的問題,于是他們有了 JavaDoc、JSDoc 這樣的工具。它可以根據代碼文件中注釋信息,生成應用程序或庫、模塊的API文檔的工具。

然而,它并不能解決沒有人維護文檔的問題,并且無法及時地通知另外一方。當前端開發人員修改契約時,后臺開發人員無法及時地知道,反之亦然。但是持續集成與自動化測試則可以做到這一點。

然而,它并不能解決沒有人維護文檔的問題,并且無法及時地通知另外一方。當前端開發人員修改契約時,后臺開發人員無法及時地知道,反之亦然。但是持續集成與自動化測試則可以做到這一點。

契約測試:基于持續集成與自動化測試

當我們定好了這個 API 的規范時,這個 API 就可以稱為是前后端之間的契約,這種設計方式也可以稱為『契約式設計』。(定義來自維基百科)

這種方法要求軟件設計者為軟件組件定義正式的,精確的并且可驗證的接口,這樣,為傳統的抽象數據類型又增加了先驗條件、后驗條件和不變式。這種方法的名字里用到的“契約”或者說“契約”是一種比喻,因為它和商業契約的情況有點類似。

按傳統的『瀑布開發模型』來看,這個契約應該由前端人員來創建。因為當后臺沒有提供 API 的時候,前端人員需要自己去搭建 Mock Server 的??墒?,這個 Mock API 的準確性則是由后臺來保證的,因此它需要共同去維護。

與其用文檔來規范,不如嘗試用持續集成與測試來維護 API,保證協作方都可以及時知道。

在 2011 年,Martin Folwer 就寫了一篇相關的文章:集成契約測試,介紹了相應的測試方式:

其步驟如下:

編寫契約(即 API)。即規定好 API 請求的 URL、請求內容、返回結果、鑒權方式等等。

根據契約編寫 Mock Server。可以采用 Moco

編寫集成測試將請求發給這個 Mock Server,并驗證

前端測試與 API 適配器

因為前端存在跨域請求的問題,我們就需要使用代理來解決這個問題,如 node-http-proxy,并寫上不同環境的配置:

這個代理就像一個適配器一樣,為我們匹配不同的環境。

在前后端分離的應用中,對于表單是要經過前端和后臺的雙重處理的。同樣的,對于前端獲取到的數據來說,也應該要經常這樣的雙重處理。因此,我們就可以簡單地在數據處理端做一層適配。

寫前端的代碼,我們經常需要寫下各種各樣的:

if(response?&&?response.data?&&?response.data.length?>?0){}

即使后臺向前端保證,一定不會返回 null 的,但是我總想加一個判斷。剛開始寫 React 組件的時候,發現它自帶了一個名為 PropTypes 的類型檢測工具,它會對傳入的數據進行驗證。而諸如 TypeScript 這種強類型的語言也有其類似的機制。

而后臺,單沒有數據的時候,比如數組,返回空素組,對象,返回空對象。

比如,

總之,API 使用的第一原則:不要『相信』前端提供的數據,不要『相信』后臺返回的數據。

什么是RAP?

RAP是阿里團隊出的一款WEB接口管理工具,幫助開發人員更高效的管理接口文檔,同時通過分析接口結構自動生成Mock數據、校驗真實接口的正確性,使接口文檔成為開發流程中的強依賴。

引用官方文檔上的說明:

在前后端分離的開發模式下,我們通常需要定義一份接口文檔來規范接口的具體信息。如一個請求的地址、有幾個參數、參數名稱及類型含義等等。RAP 首先方便團隊錄入、查看和管理這些接口文檔,并通過分析結構化的文檔數據,重復利用并生成自測數據、提供自測控制臺等等... 大幅度提升開發效率

為什么要使用RAP?

1.在實際開發中,前后端的協作往往存在一些不可避免的問題,影響工作效率;

2.RAP提供Mock服務,自動根據接口文檔生成Mock接口,這些接口會自動生成模擬數據,支持復雜的生成邏輯;

3.面對需求不斷變更或需求拿捏不定的客戶,可以使用RAP模擬數據,前端快速對接,將其作為演示用途供客戶參考,避免一些后臺開發的無用功;

4.RAP提供團隊管理,項目管理,可視化編輯,以及完善的版本控制;

5.Mock接口和實際接口的切換,僅一句js代碼引用與否,十分方便;

6.接口先于開發,接口驅動開發,前后端開發互不干擾,互不依賴,能夠更好的利于團隊協作

RAP集中解決了兩個問題:1、出色的接口文檔化處理; 2、完善mock接口數據,支持自定義拓展mock.js;

具體操作步驟:4


原文首發:http://www.zhoulujun.cn/zhoulujun/html/theory/model/8026.html

參考文章:

如何處理好前后端分離的 API 問題

使用RAP搭建前端Mock Server

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容