做爲架構風格的 REST 究竟是什麼

不少人搞不明白 REST(Representational State Transfer 表述性狀態轉移)緣由在於一開始就是把它當作設計風格而不是架構風格來理解,於是一上來就大談特談什麼 RESTful API,結果是隻見樹木不見森林。html

僅從設計的角度去理解 REST(僅把它做爲 API 設計原則),最多僅能理解其資源、表述這些概念,卻很難理解狀態轉移究竟是怎麼回事。web

要想搞清楚 REST,必須透徹理解三個關鍵概念:資源、表述、狀態轉移編程

REST 架構風格提出者和 HTTP 1.1 規範主要設計者都是同一我的 Roy Fielding。事實上,HTTP 1.1 正是 REST 風格的實現,於是認識 REST 最好的方式是從基於 HTTP 的 Web 應用開始。json

場景:

咱們看一個典型場景。瀏覽器

李小四想在京東上買一部 iPhone。服務器

首先他在瀏覽器地址欄輸入 www.jd.com(固然也能夠經過搜索引擎進入),打開京東商城首頁,而後在首頁搜索欄輸入「iPhone」,回車,頁面切換到含有 iPhone 關鍵字的商品列表。微信

李小四用鼠標點擊其中一個商品,進入該商品詳情頁。架構

李小四看了看介紹,以爲中意,因而選定顏色、型號、規格、數量,點擊「加入購物車」,再點擊「去購物車結算「,填寫收貨人信息、支付方式、開票信息,點擊「提交訂單」,選擇一種支付方式支付並完成訂單。app

李小四這我的性子比較急,下了單後,每隔一段時間就點開「個人訂單」,點開物流信息看看手機到哪了。框架

終於,手機送到了,李小四從快遞員那裏簽收後,京東立馬經過微信給他推送一條貨物簽收通知,而且附上開票連接。李小四點擊進入開票頁面,獲取一張電子發票。

資源及其表述:

在整個購物過程當中,李小四與之交互的是一個叫「京東商城」的 Web 應用——這是 REST 的做用對象。做爲架構風格的 REST,其做用對象是一個完整的應用(或者系統)——確切地說是異構的分佈式應用——而不是某一兩個 API。這樣的視角是理解 REST 全貌的關鍵。

李小四是如何獲取到他想要的信息的?跑到賣家倉庫去看實體 iPhone?若是這樣,就沒有 Web 什麼事了。李小四在瀏覽器地址欄輸入了一串叫 URL 的東西,而後瀏覽器就顯示出京東商城首頁了。

到底什麼是資源?

本例中,真正的資源是 iPhone、物流、發票、錢等,但在談論 Web 的時候,咱們說的資源通常不是指這些真正的實物資源,而是指存儲在服務器上的特定數據,如這裏的 iPhone、訂單、物流、發票、帳戶的數據信息

對於實體 iPhone,咱們能夠去專賣店看看摸摸,那 Web 上的 iPhone 數據,咱們如何找到它,又如何看如何摸呢?

前輩們設計了個偉大的東西叫 URI,你每臺 iPhone 不是有惟一編號嘛,那 Web 上這些虛擬的數據咱們也以虛擬物品(資源)的方式給它作惟一編號(標識)。雖然是由資源(虛擬數據)的擁有者來給它作標識,但爲了統1、通用,前輩們對資源標識作了一些約束(協議),就造成了統一資源標識符(Uniform Resource Identifier,URI),這樣便解決了如何找到資源的問題。好比 URL 經過 schema、域名、端口定位到服務器(資源擁有者),服務器內部再經過 path 和其餘參數找到並處理資源。

從 URI (URL 是 URI 的一種實現方案)的定義看,它自己就是用來表達資源的,天生就是名詞特性,只是在實際使用過程當中不知爲啥就跑歪了,各類 /pathto/getuserinfo 動詞性的 URL 滿天飛(我的認爲是成也 HTTP 動詞,敗也 HTTP 動詞,更詳細的分析見後面)。

資源是找到了,但咱們如何跟它交互呢?

若是是在本機,咱們能夠經過程序直接操做資源(如經過程序指針直接操做內存數據),但 Web 是個分佈式環境,指針沒那麼長,夠不到對方的內存怎麼辦?

因而咱們須要在本地(客戶端)擁有一份資源的副本。在 C/S 架構中,只有服務器擁有資源自己,其它客戶端拿到的都是副本,並且擁有者(服務器)能夠決定提供什麼樣的副本給客戶端(提供哪些信息、以什麼樣的格式提供信息)。

這種帶特定格式的資源副本就是資源的表述

做爲資源擁有者,服務器固然能夠決定提供什麼樣的表述形式,但正如 URI 同樣,若是沒有你們都承認的、通用的、被普遍支持的格式,服務器們各說自話,互相語言不通,那萬維網恐怕就會成爲巴比倫塔了。

因而前輩們又定義了一些通用的資源表述格式,官方話叫媒體類型。Web 上用的最普遍的媒體類型應該是 text/html,其餘還有 image/jpeg、application/json、text/xml 等。

一個資源能夠有多種表述(多種媒體類型),也就是說客戶端(如瀏覽器)經過一個 URI(如 URL )能夠得到該資源的多種表述中的一種。那麼客戶端和服務器端是如何溝通以在表述形式上達成一致呢?

在 HTTP 中,經過頭信息協商。HTTP 有一系列 accept 請求頭就是用來幹這事的,如 accept、accept-encoding、accept-language。好比 accept: image/webp,image/apng,image/* 告知服務器「我能處理這些媒體類型,你給其中任意一種給我就行」。服務器端響應頭 content-type 則告知瀏覽器該資源表述的確切媒體類型,如 content-type: image/jpeg 表示它是一張 jpeg 格式的圖片。

另外,和現實世界同樣,Web 上的資源具備集合特性,好比 iphone,並非指某一個 iphone,而是指 iphone 集合。從中咱們得出如下推論:

  1. 用來表示資源的 URI 應該使用名詞複數形式;
  2. 對應集合的包含關係(集合中包含子集合),資源具備層級性;
  3. 集合中的元素具備集合範圍內的惟一標識,經過在 URI 中帶入該惟一標識來定位集合中的元素,如 /iphones/123456。

假設有這樣一個 url:http://www.jd.com/mobiles/iphones/123456

首先這裏體現了資源的層級性:手機是一個大資源集合,其下包含了 iphone 這個子集合,而經過資源標識 123456 定位到某一個 iphone。

那麼,瀏覽器訪問這個 url 時會返回什麼呢?

首先取決於服務器端決定提供哪些媒體類型,咱們假設服務器端提供了 text/html、application/json、application/xml 和 image/jpeg 類型。

瀏覽器會決定請求什麼類型呢?

若是咱們在地址欄輸入該 url,瀏覽器通常會發送以下頭部:accept: text/html,... 要求返回 html 文本。但若是咱們在 標籤裏面寫該 url,瀏覽器會發送諸如 accept:image/* 要求返回圖片格式——也就是說,取決於咱們在哪裏用這個 url,這是瀏覽器的工做機制,也是 HTML 的魅力所在(後面分析超媒體時再詳細分析)。

你可能會發現,現實中咱們見到的多數不是這樣,更多是這樣:

當要訪問 html 類型時:http://www.jd.com/mobiles/iphone.html?id=123456(或者是編程語言後綴)

當要訪問圖片時:http://www.jd.com/mobiles/iphones/123456.jpg

現實中,咱們不但在 URL 中寫入動詞來表達要進行的操做,還寫入類型後綴來表達要什麼樣的媒體類型——這二者都違背了 URI 和 REST 設計初衷,讓 URI 這個標識符同時承擔了操做和媒體類型,對外暴露了設計細節,且該 URI 只能用於極其狹隘的特定場景,違背了可擴展性設計原則(沒法給該 URI 擴展更多的操做能力,也沒法擴展其表述能力)。

如今咱們知道如何定位資源和如何傳遞(展現)資源,接下來的問題是,客戶端如何操做資源呢?客戶端沒法經過操做表述(資源副本)改變資源狀態,必須經過和服務器端交互來實現。

在 HTTP 中是經過幾個通用的動詞來表達客戶端的操做意圖的,最典型的 CRUD,對應 HTTP 動詞(Method)POST、GET、PUT/PATCH、DELETE
狀態轉移:

經過 URL 定位資源,經過 HTTP 動詞操做資源,經過狀態碼錶示操做結果——如今大部分聲稱 RESTful API 的也都是作到了且僅作到了這些,大部分分析 REST 的文章也是到此便結束了,但實際上這只是開始。

相比於資源表述,REST 中更重要的第二部分是狀態轉移。Roy Fielding 提出一個術語叫將超媒體做爲應用狀態的引擎(Hypermedia As The Engine Of Application State)。這句話過於拗口,翻譯過來更是難以理解,結果被不少人忽略掉了,但這正是 REST 的精髓。

咱們先解釋下這個術語。

超媒體:就是咱們再熟悉不過的超連接,HTML 標籤中的 a、script、img、link等都屬於超媒體連接。

應用狀態:這裏明確指出是應用的狀態而不是資源的。好比上面購物場景中的京東商城就是一個 Web 應用,而應用的狀態則是該應用在某時刻呈現出來的樣子(各個頁面)。

引擎:驅動狀態改變(遷移)的東西,說得白話一點就是京東商城的一個個超連接(主要是隻 a 標籤連接)驅動其從一個頁面切換到另外一個頁面。

應用爲什麼要發生狀態轉移?爲了完成一個完整的活動,好比上面的購物。應用本質上是一個有限狀態機,其中囊括的一個個活動就是一個個工做流,應用的狀態就是工做流中的節點。咱們把上面購物過程畫出來以下(只畫了主流程,實際中會有不少分支流程,好比用戶付款後取消訂單、簽收後退貨等):

李小四購物流程圖

這裏涉及到一次購物活動(一個大的流程圖)中的三個子流程:購物(下單-支付)、查看物流、開發票。每一個節點對應應用的一個狀態(也就是頁面,前兩個是京東商城的,後一個是微信的)。

回想一下李小四是怎樣在這些頁面(應用狀態)間跳來跳去的?不停地在地址欄輸入 URL?若是沒有超連接(那個小小的 a)恐怕就只能這樣了。若是沒有超連接,京東首頁就不是如今這個樣子了,而是一坨長長的 URL 列表,且附上難看的流程圖告訴用戶要想買一部 iPhone 得按照順序依次在地址欄輸入哪些 URL——這是多麼使人崩潰的事情。

因此超連接是個偉大的發明,它使資源(的表述)之間創建聯繫,用戶可以從應用的一個狀態轉移到另外一個狀態,進而完成整個工做流。並且,這種轉移是發現式的,即應用的狀態切換不是既定的,一個狀態的下一個狀態可能並不肯定,好比李小四打開京東商城首頁後,對某款手錶感興趣,因而點擊其連接進入手錶詳情頁——結果買了一款手錶而不是 iPhone。

那麼,資源的表述應用的狀態之間又是什麼關係呢?

應用的狀態就是資源的表述,或者說應用是經過不一樣的資源表述來展示本身的。應用狀態的轉移就是不一樣的資源表述之間或者同一個資源的不一樣狀態的表述之間的轉移。

上面購物流程中,首頁是一個特殊的資源;商品列表、商品詳情是不一樣層次的商品資源;添加購物車生成新的購物車資源(或者更新購物車資源),從建立購物車到購物車詳情頁屬於購物車資源的不一樣狀態之間的轉移;下單操做建立了新的訂單資源,支付則產生支付資源,而且在京東商城應用內部產生了一系列新資源好比物流資源;訂單簽收後開具發票則產生了發票資源。

至此咱們發現,整個 Web 應用的核心仍然是資源,但既不是某一個資源,也不是某幾個毫無關聯的資源,而是一系列經過超連接創建聯繫、可以造成工做流來完成一系列活動的有機資源池。

在資源的表述中歸入超連接,讓資源的表述帶有相關資源的 URI,從而讓應用可以自動進行狀態轉移,這種媒體類型(表述)叫超媒體類型。HTML(XHTML) 是最多見的一種超媒體類型,並且是超媒體文本類型(超文本)。雖然 XHTML 基於 XML,但 XML(以及 JSON)不是超媒體類型,它們的原生語義中不帶有超連接,沒法從 XML 形式的資源表述進入其它資源表述。

XHTML 之因此是超媒體類型,是它在 XML 基礎上作了語義化(標記)處理,HTML(XHTML)處理器知道,a 標籤表示超連接,點擊能夠打開新頁面,標籤表示須要從其指向的 URI 獲取圖像格式的資源表述,發起 HTTP 請求時會帶上諸如 accept: image/*(而不是 text/html)的請求頭。

基於 XML 的另外一個普遍使用的超媒體類型是 Atom。

咱們也能夠基於 XML 和 JSON 來設計本身的超媒體類型嗎?固然能夠。好比咱們能夠定義以下 JSON 格式:

{
	"id": 123,
	"money": 3000.00,
	...
	"links": [{
		"rel": "mydomain/logistics",
		"uri": "https://www.domain.com/v1/logistics/47589"
	}]
}

其中 links 表示相關資源連接列表,這裏給出了本訂單相關的物流資源連接。該 JSON 是一個超媒體類型,它不但表述了 123 這個訂單資源的信息,還給出了指向相關物流資源的連接。通常地,咱們還要編寫對應的 JSON Schema,讓其它 JSON 解析器可以理解咱們定義的類型協議。假如咱們將該超媒體類型定義爲 application/my.hyperproto+json,可以處理該媒體類型的客戶端發起 HTTP 請求時請求頭帶上 accept:application/my.hyperproto+json,咱們服務器響應時帶上 content-type:application/my.hyperproto+json,雙方即可以自如地你來我往了(這也正是設計 RESTful API 的一個要點,雖然事實上被大部分實現者忽略了)。

現實:

回顧歷史,最先人們並無重視 HTTP 動詞和超媒體類型,經過在 URI 中添加動詞和類型後綴來表達意圖,早期一些瀏覽器和庫甚至不支持除了 GET 和 POST 以外的動詞。URI 被動詞和類型後綴污染的後果是它再也不是「URI」(資源標識),而是操做者意圖傳輸工具,某些角度說,它影響了 URI 的通用性和可擴展性。

還有一種對 HTTP 協議的退化使用是 XML-RPC,經過一個 URL 搞定一切,其餘全部的信息都寫在 XML 請求體中——在這裏,HTTP 僅僅被當作傳輸協議而不是應用協議來使用,之
因此使用 HTTP 僅僅是由於它被各類庫普遍支持,較好地知足了異構系統環境。

後來,多是一些流行框架的支持,你們趕時髦式地談論起 RESTful API 起來。這些所謂的 RESTful API 不過是把動詞和類型後綴從 URI 中拿走了,給 URI「正了名」,從新用起 HTTP Method。他們並無用起超媒體特性,HTTP 響應類型僅僅是普通的 XML 或 JSON,資源表述自己不能驅動工做流的行進,使用者仍然須要經過帶外方式(文檔)獲取相關資源 URI。

我想,這多是 REST 和 HTTP 協議自身特質形成的。

將操做(動做)極度抽象化(通用化)是一項偉大的設計,但「成也蕭何敗也蕭何」。一方面 HTTP 動詞高度抽象化(標準化、通用化),迫使開發者須要絞盡腦汁去把現實世界中成百上千的操做映射到那幾個動詞上——這不是一項簡單的思想活動,同時它還要求開發者需合理的定義「資源」,有些多是極度抽象的。另外一方面,和嚴謹的動詞造成鮮明對比的是 URI(URL)的極度靈活性,開發者能夠任意書寫 URL,只要能定位到正確的服務器,然後即是「個人地盤我作主」,沒有任何硬性約束要求 URL 裏面只能出現名詞。因而爲了少死幾個腦細胞,開發人員廣泛性地忽略掉 HTTP 動詞(甚至忽略掉了媒體類型協商),把這些信息一股腦全塞入那個「萬能」的 URL 裏面。

使用超媒體的一個困惑是,當咱們使用自定義的超媒體類型時,客戶端須要進行額外的解析工做,還不如直接傳遞你們都認識的 JSON 或 XML 來得短平快。

另外,經過超媒體驅動,意味着應用(系統)僅須要對外公佈少數幾個入口 URI,其它 URI 都是經過上游資源表述的超連接獲取的。那麼,咱們到底要暴露哪些入口 URI 呢?這又是一個須要深刻思考的問題,而人都是懶惰的。

不過,REST 給咱們設計 API 提供了一些啓示或原則。

  • 在系統的頂層架構上,面向資源而不是操做去規劃系統,能站在全局的視角思考系統構架,讓系統規劃和對外暴露的 API 儘量趨向穩定。
  • URI 僅僅表明資源,經過 HTTP 動詞規範化操做,能倒逼咱們更合理地劃分資源邊界,使得系統更模塊化、層次化。另外,它能讓咱們更深層次地思考「資源」,好比登陸,好像是個純動詞,但若是進一步思考,登陸這個行爲是爲了建立會話,對應的登出則是銷燬會話,於是咱們操做的其實是「會話」(Session)資源。
  • 儘量使用超媒體類型。經過超連接對外暴露 URI 的一個好處是將具體的 URI 細節隱藏起來,好比上面的 JSON 中,客戶端僅關心 rel 的值,而後提取相應的 uri 的值,這裏 rel 是不變的,但 uri 可能會發生變化(好比咱們的某個服務外包給第三方了),當 URI變化時,咱們無需廣而告之全部的客戶端你要改連接哈,不然服務不可用了哈。

總結:

最後咱們總結下對 REST 中資源、表述、狀態轉移的理解:

  • 資源是服務器端的原始數據,好比訂單數據,它是應用的核心。資源經過 URI 對外暴露自身;
  • 在分佈式應用中(如 Web),客戶端沒法直接觸達資源自己,能觸達的是資源的表述。表述是某種格式的資源副本;
  • 客戶端沒法經過修改表述(資源副本)來改變資源自己。服務器端擁有資源的控制權,它決定能夠提供哪些表述給客戶端,也能決定提供什麼樣的操做(動詞);
  • 客戶端經過通用動詞來獲取資源表述以及修改資源狀態;
  • 狀態是指應用的狀態,狀態轉移體現爲應用中工做流程的行進(從一個頁面切換到另外一個頁面);
  • 狀態轉移是經過超連接驅動的。超連接由資源的表述攜帶,這種攜帶了超連接的表述稱爲超媒體;
  • 超媒體使得應用可以自我驅動狀態轉移(而不須要經過帶外方式);
相關文章
相關標籤/搜索