作為架構風格的 REST 到底是什麼

来源:https://www.cnblogs.com/linvanda/archive/2020/07/26/13381251.html

很多人搞不明白 REST(Representational State Transfer 表述性狀態轉移)原因在於一開始就是把它當做設計風格而不是架構風格來理解,因而一上來就大談特談什麼 RESTful API,結果是只見樹木不見森林。 僅從設計的角度去理解 REST(僅把它作為 API 設計原則) ...


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

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

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

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

場景:

我們看一個典型場景。

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

首先他在瀏覽器地址欄輸入 www.jd.com(當然也可以通過搜索引擎進入),打開京東商城首頁,然後在首頁搜索欄輸入“iPhone”,回車,頁面切換到含有 iPhone 關鍵字的商品列表。

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

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

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

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

資源及其表述:

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

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

到底什麼是資源?

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

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

前輩們設計了個偉大的東西叫 URI,你每台 iPhone 不是有唯一編號嘛,那 Web 上這些虛擬的數據我們也以虛擬物品(資源)的方式給它做唯一編號(標識)。雖然是由資源(虛擬數據)的擁有者來給它做標識,但為了統一、通用,前輩們對資源標識做了一些約束(協議),就形成了統一資源標識符(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),客戶端無法直接觸達資源本身,能觸達的是資源的表述。表述是某種格式的資源副本;
  • 客戶端無法通過修改表述(資源副本)來改變資源本身。伺服器端擁有資源的控制權,它決定可以提供哪些表述給客戶端,也能決定提供什麼樣的操作(動詞);
  • 客戶端通過通用動詞來獲取資源表述以及修改資源狀態;
  • 狀態是指應用的狀態,狀態轉移體現為應用中工作流程的行進(從一個頁面切換到另一個頁面);
  • 狀態轉移是通過超鏈接驅動的。超鏈接由資源的表述攜帶,這種攜帶了超鏈接的表述稱為超媒體;
  • 超媒體使得應用能夠自我驅動狀態轉移(而不需要通過帶外方式);

您的分享是我們最大的動力!

更多相關文章
  • 接下來我們來看鏈表題 206. 反轉鏈表反轉一個單鏈表。 示例: 輸入: 1->2->3->4->5->NULL 輸出: 5->4->3->2->1->NULL 解題:鏈表題需要我們設立更多的指針來保存我們當前操作的細節;1.我們需要定義3個指針 pre,cur ,next,pre為當前鏈表的前一個 ...
  • 我們今天繼續研究數組在演算法中的應用 167. 兩數之和 II - 輸入有序數組 給定一個已按照升序排列 的有序數組,找到兩個數使得它們相加之和等於目標數。 函數應該返回這兩個下標值 index1 和 index2,其中 index1 必須小於 index2。 說明: 返回的下標值(index1 和 ...
  • 前端程式員怎麼才能學好演算法呢?目前演算法優秀的視頻集中在c++,java,python,本人通過幾個月專心看c++的視頻掌握了演算法的基本思路,都翻譯成前端代碼一一寫出來,從真題到思維全面提升演算法思維面對演算法面試,不畏懼 二分查找法O(logn)尋找數組中的最大/最小值O(N)歸併排序演算法 O(nlog ...
  • (一)兩種打包表單區別 |屬性 |特點 |應用 | | | | | |get |加到url,直接可見 |書簽,歷史瀏覽 | |post |間接可見,請求發送量多 |私密,訂購,評論,反饋 | (二)三種溯源區別 |屬性 |特點 |應用 | | | | | |url(uniform resource ...
  • 自媒體的發展越來越快,今日頭條旗下的西瓜視頻,火山和抖音在網路上的地位也在發生微妙的變化。包括快手和抖音,微視小視頻平臺的競爭和衝擊也是不容小覷。但是很多用戶還是執著於今日頭條搬運,不離不棄。今天老生常談,跟大家說說,如何利用批量去水印下載西瓜視頻的短視頻。工具不僅僅是支持西瓜的,快手的、抖音的、微 ...
  • 數據類型的分類和判斷 基本(值)類型 Number 任意數值 typeof String 任意字元串 typeof Boolean true/false typeof undefined undefined typeof/ null null 對象(引用)類型 Object typeof/insta ...
  • 《應用框架的設計與實現 .NET 平臺》 [作者] (美) Xin Chen[譯者] (中) 溫昱 靳向陽[出版] 電子工業出版社[版次] 2005年07月 第1版[印次] 2005年07月 第1次 印刷[定價] 39.80元 【第01章】 【應用框架介紹】 (P004) 使用應用框架有五大優點 : ...
  • 前言 本篇文章收錄於專輯:http://dwz.win/HjK,點擊解鎖更多數據結構與演算法的知識。 你好,我是彤哥,一個每天爬二十六層樓還不忘讀源碼的硬核男人。 上一節,我們一起學習了複雜度分析的套路和常見的複雜度。 但是,我們的案例基本都是以時間複雜度為主,很少接觸到空間複雜度。 那麼,到底什麼才 ...
一周排行
  • 比如要拆分“呵呵呵90909086676喝喝999”,下麵當type=0返回的是中文字元串“呵呵呵,喝喝”,type=1返回的是數字字元串“90909086676,999”, private string GetStrings(string str,int type=0) { IList<strin ...
  • Swagger一個優秀的Api介面文檔生成工具。Swagger可以可以動態生成Api介面文檔,有效的降低前後端人員關於Api介面的溝通成本,促進項目高效開發。 1、使用NuGet安裝最新的包:Swashbuckle.AspNetCore。 2、編輯項目文件(NetCoreTemplate.Web.c ...
  • 2020 年 7 月 30 日, 由.NET基金會和微軟 將舉辦一個線上和為期一天的活動,包括 微軟 .NET 團隊的演講者以及社區的演講者。本次線上大會 專註.NET框架構建微服務,演講者分享構建和部署雲原生應用程式的最佳實踐、模式、提示和技巧。有關更多信息和隨時瞭解情況:https://focu... ...
  • #abp框架Excel導出——基於vue #1.技術棧 ##1.1 前端採用vue,官方提供 UI套件用的是iview ##1.2 後臺是abp——aspnetboilerplate 即abp v1,https://github.com/aspnetboilerplate/aspnetboilerp ...
  • 前言 本文的文字及圖片來源於網路,僅供學習、交流使用,不具有任何商業用途,版權歸原作者所有,如有問題請及時聯繫我們以作處理。 作者:碧茂大數據 PS:如有需要Python學習資料的小伙伴可以加下方的群去找免費管理員領取 input()輸入 Python提供了 input() 內置函數從標準輸入讀入一 ...
  • 從12年到20年,python以肉眼可見的趨勢超過了java,成為了當今It界人人皆知的編程語言。 python為什麼這麼火? 網路編程語言搜索指數 適合初學者 Python具有語法簡單、語句清晰的特點,這就讓初學者在學習階段可以把精力集中在編程對象和思維方法上。 大佬都在用 Google,YouT ...
  • 在社會上存在一種普遍的對培訓機構的學生一種歧視的現象,具體表現在,比如:當你去公司面試的時候,一旦你說了你是培訓機構出來的,那麼基本上你就涼了,那麼你瞞著不說,然後又通過了面試成功入職,但是以後一旦在公司被髮現有培訓經歷,可能會面臨被降薪,甚至被辭退,培訓機構出來的學生,在用人單位眼裡就是能力低下的 ...
  • from typing import List# 這道題看了大佬寫的代碼,經過自己的理解寫出來了。# 從最外圍的四周找有沒有為O的,如果有的話就進入深搜函數,然後深搜遍歷# 判斷上下左右的位置是否為Oclass Solution: def solve(self, board: List[List[s ...
  • import requests; import re; import os; # 1.請求網頁 header = { "user-agent":'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_5) AppleWebKit/537.36 (KHTML, li ...
  • import requests; import re; import os; import parsel; 1.請求網頁 header = { "user-agent":'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_5) AppleWebKit/537. ...