RESTful接口設(shè)計(jì)(譯)

原文鏈接:Web API design best practices - Azure Architecture Center | Microsoft Docs

現(xiàn)在網(wǎng)絡(luò)上已經(jīng)有了很多服務(wù)商的公開(kāi)API,可以讓各類客戶端調(diào)用,那么怎樣才是一個(gè)設(shè)計(jì)優(yōu)良的web API呢?一般來(lái)講應(yīng)該具備以下標(biāo)準(zhǔn):

  1. 平臺(tái)無(wú)關(guān)性:使用API的可以是任何客戶端,它們不用關(guān)心API是怎么實(shí)現(xiàn)的。這就要求了交互時(shí)使用到的協(xié)議要標(biāo)準(zhǔn)化,并且要存在一種機(jī)制,能確保客戶端和服務(wù)提供方在數(shù)據(jù)格式上達(dá)成一致。

  2. 服務(wù)演化: web API可以自行更新迭代自己的功能,使用它的客戶端不用做出任何修改就能繼續(xù)使用這些API。服務(wù)端提供的所有功能要具備可發(fā)現(xiàn)性,使得客戶端能充分使用到它們。

下面來(lái)說(shuō)說(shuō)設(shè)計(jì)web API時(shí)要考慮的一些關(guān)鍵問(wèn)題。

什么是REST?

在2000年,Roy Fielding提出使用表述性狀態(tài)轉(zhuǎn)移(Representational State Transfer ,簡(jiǎn)稱REST)來(lái)設(shè)計(jì)網(wǎng)絡(luò)服務(wù)的構(gòu)建方法。REST是一種基于超媒體來(lái)構(gòu)建分布式系統(tǒng)的架構(gòu)風(fēng)格,它不應(yīng)關(guān)注底層服務(wù)如何,也不用跟HTTP綁定,不過(guò)大部分REST API的實(shí)現(xiàn)還是基于了HTTP協(xié)議。讓我們先關(guān)注下如何使用HTTP來(lái)設(shè)計(jì)REST API接口。

在HTTP上使用REST的好處是它是一個(gè)有公開(kāi)標(biāo)準(zhǔn)的協(xié)議,不需要這些API的提供方或使用方依賴任何特定實(shí)現(xiàn)方案,服務(wù)方和使用方可以用任意語(yǔ)言、工具包來(lái)提供REST服務(wù)實(shí)現(xiàn),或創(chuàng)建HTTP請(qǐng)求以及解析HTTP響應(yīng)報(bào)文。

基于HTTP設(shè)計(jì)RESTful API的主要原則有:

  • REST API的核心是資源,可以是客戶能訪問(wèn)到的任意物體、數(shù)據(jù)或服務(wù)
  • 每種資源都要有一個(gè)獨(dú)一無(wú)二的URI來(lái)作為唯一標(biāo)識(shí)符定位到該資源。比如一種客戶訂單可以這樣描述:
https://adventure-works.com/orders/1
  • 客戶通過(guò)交換資源表述來(lái)與服務(wù)交互。許多web API使用JSON作為數(shù)據(jù)轉(zhuǎn)換格式。例如,一個(gè)針對(duì)上面URI的GET請(qǐng)求可能得到如下內(nèi)容:
{"orderId":1,"orderValue":99.90,"productId":1,"quantity":1}
  • REST API使用一套統(tǒng)一的接口,來(lái)幫助解耦調(diào)用端和服務(wù)實(shí)現(xiàn)。在HTTP上構(gòu)建REST API時(shí),統(tǒng)一接口使用標(biāo)準(zhǔn)的HTTP動(dòng)詞來(lái)執(zhí)行資源的操作,經(jīng)常使用到的操作有GET,POST,PUT,PATCH和DELETE。

  • REST API是無(wú)狀態(tài)的。由于發(fā)出的HTTP請(qǐng)求是獨(dú)立且無(wú)序的,因此沒(méi)辦法在請(qǐng)求間保持這種臨時(shí)會(huì)話狀態(tài)。能存儲(chǔ)信息的地方只能是API資源自身,而每個(gè)請(qǐng)求應(yīng)當(dāng)是原子操作。這樣的約束要求客戶和特定服務(wù)器間不能保留任何關(guān)聯(lián),從而使得服務(wù)具備了高度的可伸縮性。任意的服務(wù)器可以處理任何的客戶請(qǐng)求。但是,其他因素可能會(huì)限制這種可伸縮性,比如很多服務(wù)都要寫(xiě)數(shù)據(jù)到后端存儲(chǔ)中,而這種單一存儲(chǔ)就很難擴(kuò)展。關(guān)于這種數(shù)據(jù)存儲(chǔ)該如何擴(kuò)展的策略方法,可以參考Horizontal, vertical, and functional data partitioning.

  • REST API的重要驅(qū)動(dòng)核心是其表述內(nèi)容中包含的超媒體鏈接。舉個(gè)例子,下面展示了一個(gè)包含訂單信息的JSON格式內(nèi)容,它包含了一些鏈接,用來(lái)獲得或更新與該訂單關(guān)聯(lián)的客戶數(shù)據(jù)。

{
    "orderID":3,
    "productID":2,
    "quantity":4,
    "orderValue":16.60,
    "links": [
        {"rel":"product","href":"https://adventure-works.com/customers/3", "action":"GET" },
        {"rel":"product","href":"https://adventure-works.com/customers/3", "action":"PUT" }
    ]
}

在2008年,Leonard Richardson提出了一個(gè)web API成熟度模型:

  • Level 0:僅有一個(gè)URI,用來(lái)處理所有POST請(qǐng)求 .
  • Level 1:不同的資源提供單獨(dú)的URI請(qǐng)求路徑.
  • Level 2:使用HTTP的method來(lái)定義在資源上的不同操作.
  • Level 3:使用了超媒體(HATEOS,下面會(huì)提到)

圍繞資源設(shè)計(jì)API

web API需要暴露一些業(yè)務(wù)實(shí)體, 拿電商系統(tǒng)來(lái)舉例,主要的實(shí)體基本上就是客戶和訂單了。可以發(fā)送一個(gè)包含了訂單信息的HTTP POST請(qǐng)求來(lái)創(chuàng)建一個(gè)訂單,而響應(yīng)報(bào)文應(yīng)當(dāng)告知這個(gè)訂單是否創(chuàng)建成功。請(qǐng)記得,提供出來(lái)的這些資源操作URI應(yīng)盡量使用名詞(資源名)來(lái)描述,而非動(dòng)詞(操作動(dòng)作)。

https://adventure-works.com/orders // Good

https://adventure-works.com/create-order // Avoid

一個(gè)所謂的資源不一定要是一個(gè)真實(shí)的物理實(shí)體,比如對(duì)于訂單來(lái)說(shuō),它可能在內(nèi)部實(shí)現(xiàn)上用到了數(shù)張關(guān)系型數(shù)據(jù)庫(kù)中的表,但是對(duì)于客戶而言,它就是一個(gè)單獨(dú)的實(shí)體。不要?jiǎng)?chuàng)建一些僅僅是把數(shù)據(jù)庫(kù)對(duì)象做了個(gè)簡(jiǎn)單鏡像展示的API。REST的目標(biāo)是描述實(shí)體和可在其上執(zhí)行的操作,而客戶不應(yīng)接觸到內(nèi)部實(shí)現(xiàn)。

實(shí)體通常會(huì)有其集合形態(tài)(一組訂單、一些顧客)。相比與其中的單一實(shí)體,一個(gè)集合也是一種單獨(dú)的資源,也應(yīng)提供其獨(dú)有的訪問(wèn)URI。例如下面這個(gè)URI就表示了一組訂單:

https://adventure-works.com/orders

使用HTTP GET調(diào)用該URI,就會(huì)得到一組數(shù)據(jù),而其中每一個(gè)元素也應(yīng)有其單獨(dú)的訪問(wèn)URI,通過(guò)HTTP GET方法訪問(wèn)它們,就能得到每一個(gè)元素的詳細(xì)信息。

采用一個(gè)統(tǒng)一的命名規(guī)范來(lái)定義這些URI。通常提供的內(nèi)容是集合時(shí)應(yīng)該使用名字的復(fù)數(shù)形式。一個(gè)很好的做法是在集合和元素間提供層級(jí)結(jié)構(gòu),比如使用/customers來(lái)代表客戶集合,而/customers/5則指向ID是5的這個(gè)單獨(dú)客戶。這樣做會(huì)讓你的web API很直觀。并且很多web API框架提供了參數(shù)URI路徑的支持,所以你可以使用/customers/{id}來(lái)做資源路由。

另外需要考慮的是不同資源間的關(guān)系,和你應(yīng)該如何暴露出這種聯(lián)系。例如我們知道/customers/5/orders應(yīng)該代表了顧客5的所有訂單,但是如果路徑是從訂單開(kāi)始,列出其關(guān)聯(lián)的所有客戶的URI就可能是/orders/99/customer。然而,這種模式的過(guò)分?jǐn)U展實(shí)現(xiàn)起來(lái)會(huì)很繁雜笨重。一個(gè)更好的方案是,在HTTP的響應(yīng)報(bào)文體中提供關(guān)聯(lián)資源的導(dǎo)航鏈接。我們會(huì)在下面章節(jié)展開(kāi)詳細(xì)探討。

在一些復(fù)雜系統(tǒng)中,像/customers/1/orders/99/products這樣的提供給客戶端多級(jí)關(guān)系路徑訪問(wèn)的URI,看起來(lái)似乎很方便,但是這樣的復(fù)雜級(jí)別會(huì)讓維護(hù)變得困難,并且難以在將來(lái)調(diào)整資源間的層級(jí)關(guān)系。因此,好的做法是讓資源URI盡量簡(jiǎn)單明了,一旦應(yīng)用程序有了對(duì)資源的引用,就應(yīng)該可以使用這個(gè)引用來(lái)查找與該資源相關(guān)的項(xiàng)。比如前面查找客戶1所有訂單的查詢URI可以替換為/customers/1/orders,然后通過(guò)/orders/99/products來(lái)得到訂單中的所有商品。

注意:不要使用比/collection/item/collection更復(fù)雜的查詢URI。

另外要考慮每次請(qǐng)求對(duì)服務(wù)器的負(fù)載影響。請(qǐng)求越多,負(fù)載越高。因此,不要提供太多過(guò)于細(xì)小瑣碎的資源接口。這樣的接口可能需要客戶端執(zhí)行多次請(qǐng)求才能得到想到的數(shù)據(jù)。可以考慮把一些相關(guān)數(shù)據(jù)組合到一個(gè)資源中,以使得一次查詢請(qǐng)求便能滿足需求。不過(guò),你還是得做好權(quán)衡,來(lái)避免拿到過(guò)多的無(wú)用數(shù)據(jù)。另外,檢索的數(shù)據(jù)過(guò)大,還會(huì)讓請(qǐng)求變得緩慢,并產(chǎn)生額外的帶寬成本。跟多關(guān)于性能錯(cuò)誤模式的介紹,可參看Chatty I/OExtraneous Fetching.

要避免讓web API和底層數(shù)據(jù)庫(kù)產(chǎn)生依賴關(guān)系,例如,當(dāng)你使用了關(guān)系數(shù)據(jù)庫(kù)存儲(chǔ)數(shù)據(jù),web API并不需要把所有的表都暴露為資源集合,這樣設(shè)計(jì)很不好,合適的做法是,把web API當(dāng)成是數(shù)據(jù)庫(kù)的抽象,或者可以考慮使用一個(gè)映射層來(lái)建立數(shù)據(jù)庫(kù)和web API的映射關(guān)系。這樣客戶端就可以與底層數(shù)據(jù)庫(kù)隔離開(kāi)來(lái),從而在底層數(shù)據(jù)表變化時(shí)不受到影響。

最后,可能不太容易做到將所有web API實(shí)現(xiàn)的操作找到合適的資源描述來(lái)建立映射關(guān)系,一些場(chǎng)景里,發(fā)出的HTTP請(qǐng)求只是用來(lái)執(zhí)行了某個(gè)函數(shù),然后將結(jié)果作為HTTP響應(yīng)報(bào)文返回,比如用作簡(jiǎn)單加減計(jì)算的web API,可能會(huì)使用偽資源作為URI,并將查詢字符串當(dāng)作計(jì)算的參數(shù)。例如,一個(gè)URI為/add?operand1=99&operand2的GET請(qǐng)求,會(huì)得到一個(gè)報(bào)文內(nèi)容為100的返回結(jié)果。不過(guò)最好還是有節(jié)制的使用這種形式的URIs。

通過(guò)HTTP方法定義API操作

HTTP協(xié)議定義了一些有特殊語(yǔ)義的請(qǐng)求方法,在RESTful接口中最常用到的有:

  • GET 獲取指定URI表述的資源數(shù)據(jù),響應(yīng)報(bào)文里包含請(qǐng)求資源的詳細(xì)內(nèi)容。
  • POST 使用指定URI創(chuàng)建資源對(duì)象,并返回對(duì)象的詳細(xì)內(nèi)容。注意POST有時(shí)也用來(lái)觸發(fā)實(shí)際上并不會(huì)創(chuàng)建資源的操作。
  • PUT 根據(jù)請(qǐng)求消息的不同指定,創(chuàng)建或更新指定URI的資源對(duì)象
  • PATCH 可以在請(qǐng)求體中指定一系列變更內(nèi)容,來(lái)執(zhí)行對(duì)應(yīng)資源的部分更新動(dòng)作
  • DELETE 使用指定URI刪除資源

資源是集合還是單獨(dú)個(gè)體,會(huì)使得請(qǐng)求的效果有所不同。下面總結(jié)了一些電商系統(tǒng)中常見(jiàn)的RESTful實(shí)現(xiàn)慣例。有些請(qǐng)求沒(méi)有處理 - 這取決于場(chǎng)景。

Resource POST GET PUT DELETE
/customers 創(chuàng)建一個(gè)新客戶 檢索所有客戶 批量更新客戶 刪除所有客戶
/customers/1 無(wú) 檢索客戶1的詳細(xì)信息 更新客戶1的詳細(xì)信息(若存在) 刪除客戶1
customers/1/orders 為客戶1創(chuàng)建一個(gè)新訂單 檢索客戶1的所有訂單 批量更新客戶1的所有訂單 刪除客戶1的所有訂單

強(qiáng)調(diào)下POST、PUT、PATCH方法的差異:

  • POST請(qǐng)求會(huì)創(chuàng)建一個(gè)資源,服務(wù)端會(huì)給新創(chuàng)建的資源分配一個(gè)URI,并將其返回給客戶端。在REST模式下,經(jīng)常會(huì)對(duì)集合使用POST操作,新資源被創(chuàng)建后便被加入到集合中。POST請(qǐng)求也用在提交數(shù)據(jù)給現(xiàn)存資源來(lái)完成一些操作,過(guò)程中并不會(huì)有新資源被創(chuàng)建。
  • PUT請(qǐng)求用來(lái)創(chuàng)建新資源或更新已經(jīng)存在的資源。客戶端指定資源的URI,并在請(qǐng)求體中包含資源的完整表示。如果指定的資源已經(jīng)存在,它便會(huì)被請(qǐng)求內(nèi)容替換掉,否則便會(huì)創(chuàng)建一個(gè)新資源(如果服務(wù)端支持該操作)。PUT請(qǐng)求更多的用在單獨(dú)資源上,而不是集合資源,比如一個(gè)特定的客戶。服務(wù)端通常會(huì)支持PUT請(qǐng)求的更新操作,但不一定會(huì)支持創(chuàng)建資源,這取決于客戶端是否可以在資源不存在時(shí)分派新URI給它。如果不支持,請(qǐng)使用POST來(lái)創(chuàng)建新資源,然后用PUT或PATCH來(lái)更新它。
  • PATCH請(qǐng)求用來(lái)執(zhí)行已存在資源的部分更新。客戶端指定資源的URI,并在請(qǐng)求體中攜帶需要執(zhí)行變更的內(nèi)容集合。這種做法會(huì)比PUT更有效,鑒于客戶端只需要發(fā)送變更的部分,而不是整個(gè)資源數(shù)據(jù)。從技術(shù)上來(lái)說(shuō)PATCH也可以創(chuàng)建資源,通過(guò)把所有資源變更數(shù)據(jù)都指定為null來(lái)實(shí)現(xiàn),不過(guò)最終要看服務(wù)端是否支持這種做法。

PUT請(qǐng)求必須是冪等的。如果客戶端發(fā)送了多次同樣的請(qǐng)求,得到的結(jié)果應(yīng)該是一樣的(同樣的資源被變更為了同樣的內(nèi)容)。POST和PATCH則沒(méi)有這樣的要求。

遵守HTTP語(yǔ)義

本節(jié)介紹HTTP規(guī)范下需要考慮的一些常見(jiàn)注意事項(xiàng),沒(méi)有涵蓋所有可能細(xì)節(jié)和場(chǎng)景,如有疑問(wèn)請(qǐng)參閱HTTP規(guī)范。

Media types

上面提到,客戶端和服務(wù)端會(huì)交換資源數(shù)據(jù)。比如在POST請(qǐng)求中,請(qǐng)求體需包含要?jiǎng)?chuàng)建資源的表述,而GET請(qǐng)求中,響應(yīng)報(bào)文體中也會(huì)包含檢索資源的表述內(nèi)容。

在HTTP協(xié)議里,格式通過(guò)media types來(lái)指定,也被稱為MIME types。對(duì)于非二進(jìn)制數(shù)據(jù),大部分web API提供了JSON(media type = application/json)或者XML(media type = application/xml)格式的支持。

在請(qǐng)求或響應(yīng)中通過(guò)Content-Type消息頭來(lái)指定數(shù)據(jù)格式。下面有一個(gè)包含了JSON數(shù)據(jù)的POST請(qǐng)求示例:

POST https://adventure-works.com/orders HTTP/1.1
Content-Type: application/json; charset=utf-8
Content-Length: 57

{"Id":1,"Name":"Gizmo","Category":"Widgets","Price":1.99}

如果服務(wù)端不支持請(qǐng)求的media type,那么它應(yīng)該返回415這個(gè)HTTP狀態(tài)碼(Unsupported Media Type)。

客戶端請(qǐng)求可以攜帶一個(gè)Accept的消息頭來(lái)向服務(wù)端表明它可以接受報(bào)文的media type列表,例如:

GET https://adventure-works.com/orders/2 HTTP/1.1
Accept: application/json

如果服務(wù)端不能滿足列出的media type請(qǐng)求,它應(yīng)該返回HTTP狀態(tài)碼406(Not Acceptable)。

GET方法

GET請(qǐng)求成功后通常應(yīng)返回狀態(tài)碼200(OK)。如果請(qǐng)求的資源不存在,則應(yīng)返回404(Not Found)。

POST方法

如果POST請(qǐng)求創(chuàng)建了新資源,它應(yīng)返回HTTP狀態(tài)碼201(Created)。新創(chuàng)建資源的URI應(yīng)在響應(yīng)報(bào)文頭Location中攜帶,而報(bào)文體則應(yīng)包含該資源的表述。

如果方法做了一些處理,但是并沒(méi)有創(chuàng)建新資源,它同樣可以返回200狀態(tài)碼,并把處理結(jié)果放在響應(yīng)報(bào)文中,而當(dāng)并沒(méi)有任何處理結(jié)果需要返回時(shí),可以使用204狀態(tài)碼(No Content)來(lái)代替,并返回空響應(yīng)報(bào)文。

如果客戶端在請(qǐng)求中攜帶了無(wú)效數(shù)據(jù),服務(wù)端應(yīng)該返回狀態(tài)碼400(Bad Request),響應(yīng)報(bào)文中則可以放入關(guān)于錯(cuò)誤的詳細(xì)說(shuō)明信息,或者提供一個(gè)鏈接以用來(lái)查看更多信息。

PUT方法

如果PUT方法創(chuàng)建了新的資源,它會(huì)返回201狀態(tài)碼(Created),跟POST一樣。如果它更新了一個(gè)存在的資源,可以返回200(OK)或者204(No Content)。某些情況下,可能無(wú)法更新指定的資源,這時(shí)可以考慮返回HTTP狀態(tài)碼409(Conflict)。

可以考慮提供一個(gè)批量更新PUT方法,在方法體里放上要更新的所有資源內(nèi)容,并使用URI指明要更新的資源集合。這樣能降低網(wǎng)絡(luò)開(kāi)銷和提高性能。

PATCH方法

使用PATCH請(qǐng)求時(shí),客戶端使用一種 補(bǔ)丁文件 的格式,向已經(jīng)存在的數(shù)據(jù)資源發(fā)送更新請(qǐng)求。服務(wù)端會(huì)使用這個(gè)補(bǔ)丁文件處理更新。補(bǔ)丁文件并不需要描述資源的所有內(nèi)容,只需要告訴更新哪些部分即可。PATCH方法的規(guī)范 (RFC 5789)并沒(méi)有定義這個(gè)補(bǔ)丁文件要有什么樣的特定格式,格式需要根據(jù)請(qǐng)求的media type而定。

JSON應(yīng)該是web API屆最通用的數(shù)據(jù)格式了。而patch方法用到的基于JSON的補(bǔ)丁文件格式有兩個(gè),叫做JSON patch和JSON merge patch。

JSON merge patch相對(duì)簡(jiǎn)單一點(diǎn)。它的結(jié)構(gòu)就像是原始資源對(duì)象的JSON形式內(nèi)容,只是它包含的僅僅是需要更新或者添加的數(shù)據(jù)字段集合,在補(bǔ)丁文件中通過(guò)將字段指定為 null 甚至還可以刪除字段(不過(guò)不適用于原始資源可以顯式的包含'null'值的情況)

舉個(gè)例子,如果原始資源對(duì)象的JSON形式內(nèi)容為:

{
    "name":"gizmo",
    "category":"widgets",
    "color":"blue",
    "price":10
}

可能對(duì)應(yīng)的更新文件的內(nèi)容會(huì)是這樣:

{
    "price":12,
    "color":null,
    "size":"small"
}

這個(gè)文件告訴服務(wù)端,要更新 price ,刪掉 color ,并添加 size 字段,同時(shí) namecategory 字段沒(méi)有任何改動(dòng)。有關(guān)JSON merge patch的更多具體內(nèi)容可以參考 RFC 7396。它對(duì)應(yīng)的media type為 application/merge-patch+json

Merge patch并不適用于原本資源中包含顯式'null'值的情況,因?yàn)檠a(bǔ)丁文件中的null有特殊含義(代表刪除)。并且補(bǔ)丁文件不能指定更新被執(zhí)行的順序,不過(guò)這個(gè)有沒(méi)有具體影響要看數(shù)據(jù)對(duì)象的處理邏輯。在RFC 6902中定義的JSON patch,相對(duì)就靈活一些。它可以指明具體要執(zhí)行的操作序列,操作的類型可以是增加、刪除、替換、拷貝和測(cè)試(用來(lái)驗(yàn)證特定值)。它的media type是 application/json-patch+json

下表列出了一些處理PATCH請(qǐng)求時(shí)可能會(huì)遇到的一些典型錯(cuò)誤條件,和對(duì)應(yīng)適當(dāng)?shù)腍TTP狀態(tài)碼。

Error condition HTTP status code
The patch document format isn't supported. 415 (Unsupported Media Type)
Malformed patch document. 400 (Bad Request)
The patch document is valid, but the changes can't be applied to the resource in its current state. 409 (Conflict)

DELETE方法

如果一個(gè)刪除請(qǐng)求被成功執(zhí)行,web服務(wù)端應(yīng)該返回一個(gè)204狀態(tài)碼(No Content),這代表著執(zhí)行已經(jīng)成功,響應(yīng)報(bào)文不需要有任何內(nèi)容。如果請(qǐng)求的資源不存在,應(yīng)該得到一個(gè)HTTP 404(Not Found)。

異步操作

有時(shí)一個(gè)POST、PUT、PATCH或者DELETE操作可能會(huì)需要執(zhí)行一段時(shí)間,如果在成功相應(yīng)之前一直等待,過(guò)長(zhǎng)的延遲可能不太好接受。這時(shí)可以考慮將操作改成異步來(lái)執(zhí)行。返回一個(gè)202(Accepted)狀態(tài)碼來(lái)表明請(qǐng)求已經(jīng)接受并在處理中,但是還沒(méi)有完成。

你應(yīng)該暴露一個(gè)端點(diǎn),來(lái)提供異步請(qǐng)求執(zhí)行狀態(tài)的查詢,這樣客戶端就能輪詢它來(lái)監(jiān)測(cè)狀態(tài)。可以在202報(bào)文頭中包含這個(gè)狀態(tài)端點(diǎn)的地址,如:

HTTP/1.1 202 Accepted
Location: /api/status/12345

如果客戶端向該端點(diǎn)發(fā)起了一個(gè)GET請(qǐng)求,響應(yīng)報(bào)文中應(yīng)該包含操作的執(zhí)行狀態(tài)。另外,也可以提供其他一些信息,比如預(yù)計(jì)要消耗的時(shí)間和一個(gè)可以取消操作的鏈接。

HTTP/1.1 200 OK
Content-Type: application/json

{
    "status":"In progress",
    "link": { "rel":"cancel", "method":"delete", "href":"/api/status/12345" }
}

如果異步操作創(chuàng)建了新的資源,那么這個(gè)狀態(tài)端點(diǎn)應(yīng)該在操作執(zhí)行完成后返回一個(gè)303(See Other),并在響應(yīng)報(bào)文頭信息中提供新資源的訪問(wèn)地址。

HTTP/1.1 303 See Other
Location: /api/orders/12345

更多相關(guān)信息,可參考 Asynchronous Request-Reply pattern.

數(shù)據(jù)過(guò)濾和分頁(yè)

提供一個(gè)可以查詢資源集合的URI,可能會(huì)出現(xiàn)客戶端其實(shí)只需要一小部分?jǐn)?shù)據(jù),但卻發(fā)起了一個(gè)很大數(shù)據(jù)量的請(qǐng)求。比如,客戶程序需要查詢所有成本大于某個(gè)值的所有訂單,那么可能它會(huì)先請(qǐng)求 /orders 這個(gè)URI拿到所有訂單數(shù)據(jù),然后在客戶端把這些數(shù)據(jù)作一下過(guò)濾。很顯然這樣處理非常的不效率,而且浪費(fèi)了貸款和服務(wù)器算力。

相對(duì)的,這個(gè)API也可以提供請(qǐng)求URI時(shí)攜帶查詢字符串的支持,比如 /orders?minCost=n。然后服務(wù)端解析處理這個(gè) minCost 查詢項(xiàng),并返回過(guò)濾后的結(jié)果。

一個(gè)對(duì)資源集合的GET請(qǐng)求總是有可能會(huì)返回一大堆數(shù)據(jù),所以最好在你的web API中提供對(duì)單次請(qǐng)求返回結(jié)果數(shù)量的限制。可以考慮提供一個(gè)最大數(shù)量請(qǐng)求查詢參數(shù),來(lái)指明能得到的最大結(jié)果數(shù)量,同時(shí)需要提供一個(gè)偏移量參數(shù),例如:

/orders?limit=25&offset=50

同時(shí)為了避免可能受到的拒絕服務(wù)攻擊,要對(duì)這個(gè)最大數(shù)量參數(shù)設(shè)置個(gè)上限。另外,為了協(xié)助客戶應(yīng)用程序完成分頁(yè)配置,服務(wù)端對(duì)GET請(qǐng)求返回分頁(yè)數(shù)據(jù)時(shí),要在響應(yīng)報(bào)文中包含一些元數(shù)據(jù),用來(lái)標(biāo)明資源可用的總頁(yè)數(shù)等。

類似的,還可以提供一個(gè)sort參數(shù)用來(lái)對(duì)請(qǐng)求數(shù)據(jù)排序,可以使用結(jié)果字段中的任意一個(gè),比如 /orders?sort=ProductID。不過(guò)這種做法有一個(gè)壞處,因?yàn)橛行┚彺鎸?shí)現(xiàn)用查詢字符串作為緩存的key,sort參數(shù)的調(diào)整可能會(huì)讓之前的緩存失效。

在查詢數(shù)據(jù)的字段數(shù)量特別多時(shí),還可以對(duì)返回字段作一下限制。提供一個(gè)支持逗號(hào)分隔的查詢字符串參數(shù)來(lái)表示需要的字段列表,比如 /orders?fields=ProductID,Quantity

對(duì)每一個(gè)支持的查詢字符串參數(shù),提供一個(gè)有意義的默認(rèn)值。例如,如果支持分頁(yè),最好將數(shù)據(jù)條數(shù)默認(rèn)為10,偏移量(頁(yè)碼)默認(rèn)為0。如果支持排序,將sort列默認(rèn)為資源的主鍵,如果支持投影(可選查詢列),將參數(shù)默認(rèn)為所有列。

大型二進(jìn)制文件的部分響應(yīng)

有些資源可能包含二進(jìn)制字段,比如圖片和文件。這些內(nèi)容在不穩(wěn)定網(wǎng)絡(luò)環(huán)境下傳輸可能會(huì)出現(xiàn)問(wèn)題,比如連接中斷,或者處理時(shí)間過(guò)長(zhǎng),為了克服這些問(wèn)題,可以考慮分塊獲取。首先,API需要對(duì)GET請(qǐng)求中的Accept-Ranges這個(gè)用來(lái)請(qǐng)求大文件資源的消息頭提供支持,這個(gè)消息頭表明GET操作支持部分請(qǐng)求,客戶端可以發(fā)起一個(gè)指定了字節(jié)數(shù)范圍的GET請(qǐng)求,來(lái)得到一部分資源數(shù)據(jù)。

同時(shí),最好也實(shí)現(xiàn)對(duì)這些資源的HTTP HEAD請(qǐng)求處理。HEAD請(qǐng)求類似于GET,只不過(guò)它只返回描述資源的HTTP消息頭,而消息體是空的。客戶程序可以發(fā)出HEAD請(qǐng)求來(lái)判斷是否需要使用GET部分請(qǐng)求方式獲取資源。例如:

HEAD https://adventure-works.com/products/10?fields=productImage HTTP/1.1

對(duì)應(yīng)的響應(yīng)報(bào)文為:

HTTP/1.1 200 OK

Accept-Ranges: bytes
Content-Type: image/jpeg
Content-Length: 4580

Content-Length 報(bào)文頭給出了資源的總大小,而 Accept-Ranges 報(bào)文頭則表明對(duì)應(yīng)的GET操作支持部分結(jié)果的請(qǐng)求。有了這些信息,客戶程序便可以把圖片分成小塊獲取。第一個(gè)請(qǐng)求使用了 Range 頭獲取了2500個(gè)字節(jié):

GET https://adventure-works.com/products/10?fields=productImage HTTP/1.1
Range: bytes=0-2499

響應(yīng)報(bào)文使用HTTP狀態(tài)碼206來(lái)表明這是完整內(nèi)容的一部分。Content-Length 報(bào)文頭說(shuō)明了消息體內(nèi)的實(shí)際字節(jié)數(shù)(而非資源的完整大小),Content-Range 報(bào)文頭則表明這是整個(gè)資源的哪一部分(完整4580中的0-2499這部分)

HTTP/1.1 206 Partial Content

Accept-Ranges: bytes
Content-Type: image/jpeg
Content-Length: 2500
Content-Range: bytes 0-2499/4580

[...]

客戶程序可以發(fā)起后續(xù)請(qǐng)求來(lái)獲得資源的剩余部分。

使用資源導(dǎo)航 (HATEOAS)

REST有一個(gè)推崇的概念,應(yīng)當(dāng)在不需要先了解URI方案的情況下,做到對(duì)整個(gè)資源集的導(dǎo)航。每一個(gè)HTTP GET請(qǐng)求的響應(yīng)報(bào)文,都應(yīng)該包含一系列超鏈接信息,用來(lái)獲取跟請(qǐng)求對(duì)象有直接關(guān)系的資源,并提供這些資源上的可用操作說(shuō)明。這個(gè)原則被稱為 HATEOAS ,即 Hypertext as the Engine of Application State。這個(gè)系統(tǒng)實(shí)際上是一種有限狀態(tài)機(jī),請(qǐng)求的響應(yīng)報(bào)文包含了從一種狀態(tài)轉(zhuǎn)移到另一種狀態(tài)的必要信息,跟狀態(tài)轉(zhuǎn)換無(wú)關(guān)的內(nèi)容則不應(yīng)包含在內(nèi)。

當(dāng)前并沒(méi)有針對(duì)HATEOAS原則的通用建模標(biāo)準(zhǔn),本節(jié)示例只展示了一種特定用途下的方案。

比如,處理訂單和客戶關(guān)系時(shí),可以在訂單的表現(xiàn)上包含一些鏈接,用來(lái)標(biāo)識(shí)跟這個(gè)訂單相關(guān)客戶的一些可用操作。比如:

{
  "orderID":3,
  "productID":2,
  "quantity":4,
  "orderValue":16.60,
  "links":[
    {
      "rel":"customer",
      "href":"https://adventure-works.com/customers/3",
      "action":"GET",
      "types":["text/xml","application/json"]
    },
    {
      "rel":"customer",
      "href":"https://adventure-works.com/customers/3",
      "action":"PUT",
      "types":["application/x-www-form-urlencoded"]
    },
    {
      "rel":"customer",
      "href":"https://adventure-works.com/customers/3",
      "action":"DELETE",
      "types":[]
    },
    {
      "rel":"self",
      "href":"https://adventure-works.com/orders/3",
      "action":"GET",
      "types":["text/xml","application/json"]
    },
    {
      "rel":"self",
      "href":"https://adventure-works.com/orders/3",
      "action":"PUT",
      "types":["application/x-www-form-urlencoded"]
    },
    {
      "rel":"self",
      "href":"https://adventure-works.com/orders/3",
      "action":"DELETE",
      "types":[]
    }]
}

在這個(gè)例子里,links這個(gè)數(shù)組節(jié)點(diǎn)包含了一個(gè)鏈接數(shù)據(jù)集合,每個(gè)鏈接數(shù)據(jù)都代表了一個(gè)相關(guān)實(shí)體的可用操作。每個(gè)鏈接數(shù)據(jù)又包含了關(guān)系("customer")、URI(https://adventure-works.com/customers/3)、HTTP方法,和可用的MIME類型。這些信息足夠一個(gè)客戶程序用來(lái)執(zhí)行操作了。

鏈接數(shù)組里還包含了一些自引用信息,跟原本獲取到的請(qǐng)求資源有關(guān),它們的關(guān)系被標(biāo)記為 'self'。

根據(jù)不同的資源狀態(tài),links數(shù)組節(jié)點(diǎn)中的內(nèi)容可能會(huì)有所不同。這也正是超文本作為 "engine of application state." 的含義。

RESTful web API的版本控制

通常web API不會(huì)一成不變,隨著業(yè)務(wù)需求變化,會(huì)有更多資源集合出現(xiàn),資源之間的關(guān)系會(huì)變化,資源的數(shù)據(jù)結(jié)構(gòu)也會(huì)有所改變。更新web API來(lái)處理變化的需求或許沒(méi)什么難度,但是必須得考慮這些改動(dòng)對(duì)于那些正在使用這些API的客戶端程序的影響。設(shè)計(jì)和維護(hù)這些web API的程序員,對(duì)這些接口有完全的控制權(quán),但是對(duì)那些客戶端程序可并不一定是這樣,有可能開(kāi)發(fā)它們的是一些遠(yuǎn)處的第三方。所以關(guān)鍵的是能讓現(xiàn)存的客戶端程序繼續(xù)正常運(yùn)行,同時(shí)新的客戶端程序可以充分使用這些新功能和資源。

版本控制可以使web API表明它公開(kāi)的功能和資源,同時(shí)能讓客戶端程序向功能和資源的某個(gè)特定版本發(fā)起請(qǐng)求。下面幾節(jié)描述了不同的版本控制方法,各有其優(yōu)劣。

無(wú)版本控制

這是最簡(jiǎn)單的方法,一些內(nèi)部調(diào)用API可以接受這么做。重大的改動(dòng)一般體現(xiàn)為新的資源或新鏈接,對(duì)已經(jīng)存在的資源添加內(nèi)容一般也不會(huì)有什么問(wèn)題,因?yàn)檎?qǐng)求端程序遇到期望之外的數(shù)據(jù)內(nèi)容,一般會(huì)自動(dòng)忽略掉。

比如,向URI https://adventure-works.com/customers/3 發(fā)起請(qǐng)求,會(huì)返回一個(gè)包含了id、name和address字段的單個(gè)客戶明細(xì)數(shù)據(jù),這些數(shù)據(jù)符合請(qǐng)求端程序的預(yù)期:

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8

{"id":3,"name":"Contoso LLC","address":"1 Microsoft Way Redmond WA 98053"}

簡(jiǎn)單起見(jiàn),本節(jié)示例響應(yīng)報(bào)文并沒(méi)有包含 HATEOAS 鏈接.

如果向客戶資源的結(jié)構(gòu)中添加了 DateCreated 字段,響應(yīng)內(nèi)容會(huì)變?yōu)椋?/p>

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8

{"id":3,"name":"Contoso LLC","dateCreated":"2014-09-04T12:11:38.0376089Z","address":"1 Microsoft Way Redmond WA 98053"}

已經(jīng)存在的客戶程序會(huì)繼續(xù)正常運(yùn)行,只要它們可以忽略掉不認(rèn)識(shí)的字段,而新的客戶程序可以設(shè)計(jì)為能夠處理這個(gè)新添加的字段。不過(guò),如果資源的結(jié)構(gòu)發(fā)生了更徹底的改動(dòng)(比如刪除或者重命名了某些字段),或者資源間的關(guān)系發(fā)生了變化,那就有可能使得這些正在運(yùn)行的客戶程序無(wú)法再正確運(yùn)轉(zhuǎn)。這種情況下,你就得考慮下面的這些方法了。

URI 版本控制

每當(dāng)對(duì)web API或者資源結(jié)構(gòu)進(jìn)行改動(dòng)時(shí),可以對(duì)每個(gè)資源的URI添加一個(gè)版本號(hào),先前的URI應(yīng)繼續(xù)像之前那樣運(yùn)作,返回原本的資源數(shù)據(jù)。

對(duì)前面的例子作下擴(kuò)展,如果將 address 字段重構(gòu)為包含不同組成部分的子字段(比如街道、城市、省份和郵政編碼),這個(gè)版本的資源可以發(fā)布為一個(gè)包含了版本號(hào)的URI,例如 https://adventure-works.com/v2/customers/3:

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8

{"id":3,"name":"Contoso LLC","dateCreated":"2014-09-04T12:11:38.0376089Z","address":{"streetAddress":"1 Microsoft Way","city":"Redmond","state":"WA","zipCode":98053}}

這種版本控制機(jī)制非常簡(jiǎn)單,但是它依賴于服務(wù)器能將請(qǐng)求路由到正確的服務(wù)端點(diǎn)。而且,隨著web API的逐步迭代和服務(wù)器提供支持的版本數(shù)量增多,它會(huì)變得很笨重。另外,從純粹主義角度去看,返回了同樣數(shù)據(jù)(客戶3)的URI,它們的版本不應(yīng)該是不一樣的。這個(gè)方案還會(huì)讓 HATEOAS 的實(shí)現(xiàn)更加復(fù)雜,因?yàn)樗墟溄佣疾坏貌粚姹咎?hào)包含在它們的URI中。

查詢字符串版本控制

相比于提供多個(gè)不通的URI,更好的做法可能是在HTTP請(qǐng)求上附帶一個(gè)指定了資源版本的查詢字符串參數(shù),比如 https://adventure-works.com/customers/3?version=2 。給這個(gè)版本參數(shù)一個(gè)有意義的默認(rèn)值,比如1,這樣舊的客戶程序可以忽略掉它而不受到影響。

這種做法的好處是,在語(yǔ)義上,同樣的資源請(qǐng)求用了相同的URI,不過(guò)這需要相應(yīng)的代碼正確的解析請(qǐng)求中的查詢字符串,并給與正確的HTTP響應(yīng)。另外此方法同樣會(huì)遇到跟URI版本控制一樣的問(wèn)題,即實(shí)現(xiàn) HATEOAS 會(huì)比較麻煩。

一些舊版瀏覽器和網(wǎng)絡(luò)代理不會(huì)緩存包含查詢字符串的請(qǐng)求響應(yīng)內(nèi)容,在其上運(yùn)行的網(wǎng)絡(luò)程序調(diào)用web API時(shí)的性能可能會(huì)受到影響。

消息頭版本控制

除了將版本號(hào)放到查詢字符串參數(shù)中,還可以實(shí)現(xiàn)一個(gè)自定義消息頭來(lái)指明需要的資源版本。這個(gè)做法需要客戶端程序在所有請(qǐng)求上攜帶正確的消息頭,盡管服務(wù)代碼可以在請(qǐng)求忽略了這個(gè)消息頭時(shí)給一個(gè)默認(rèn)值(比如版本1)。下面這個(gè)例子使用了名為 Custom-Header 的消息頭,里面的值指明了web API的版本。

Version 1:


GET https://adventure-works.com/customers/3 HTTP/1.1
Custom-Header: api-version=1
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8

{"id":3,"name":"Contoso LLC","address":"1 Microsoft Way Redmond WA 98053"}

Version 2:

GET https://adventure-works.com/customers/3 HTTP/1.1
Custom-Header: api-version=2
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8

{"id":3,"name":"Contoso LLC","dateCreated":"2014-09-04T12:11:38.0376089Z","address":{"streetAddress":"1 Microsoft Way","city":"Redmond","state":"WA","zipCode":98053}}

和前面兩種版本控制方案一樣,實(shí)現(xiàn) HATEOAS 也需要在所有鏈接中帶上正確的消息頭。

媒體類型版本控制

在前面有介紹過(guò),當(dāng)客戶端程序發(fā)出一個(gè)HTTP GET請(qǐng)求到web服務(wù)端時(shí),它應(yīng)當(dāng)使用 Accept 消息頭明確它可以處理的內(nèi)容格式。Accept 消息頭經(jīng)常用來(lái)指明客戶端程序希望得到的響應(yīng)報(bào)文格式應(yīng)該是XML、JSON或者其他一些通用格式。不過(guò),也可以擴(kuò)展一些自定義媒體類型,使得客戶程序可以在其中加一些信息來(lái)指明需要的資源版本。

下面這個(gè)例子在 Accept 消息頭中指定了 application/vnd.adventure-works.v1+json,其中的 vnd.adventure-works.v1 就向web服務(wù)端表明了它需要的是1這個(gè)資源版本,并希望得到j(luò)son格式的響應(yīng)內(nèi)容:

GET https://adventure-works.com/customers/3 HTTP/1.1
Accept: application/vnd.adventure-works.v1+json

處理請(qǐng)求的代碼需要處理這個(gè)消息頭,并盡可能的滿足它(客戶端程序可能會(huì)在 Accept 中指定多個(gè)格式,服務(wù)端只需要選擇其中最合適的一個(gè)作為響應(yīng)格式即可)。Web服務(wù)端在響應(yīng)體中使用 Content-Type 報(bào)文頭來(lái)對(duì)數(shù)據(jù)格式進(jìn)行確認(rèn):

HTTP/1.1 200 OK
Content-Type: application/vnd.adventure-works.v1+json; charset=utf-8

{"id":3,"name":"Contoso LLC","address":"1 Microsoft Way Redmond WA 98053"}

如果 Accept 消息頭中指定了無(wú)法處理的媒體類型,服務(wù)端可以返回HTTP狀態(tài)碼406(Not Acceptable),或者使用一個(gè)默認(rèn)類型來(lái)處理。

這種做法被認(rèn)為是最純粹的版本控制方案了(譯者猜應(yīng)該是說(shuō)這種做法可以在連接上只是明確指向資源即可,不需要做其他調(diào)整,URI版本方案,和查詢字符串版本方案,都會(huì)影響這個(gè)URI的干凈度),并且它原生支持 HATEOAS ,因?yàn)榭梢栽阪溄庸?jié)點(diǎn)中指明 MIME 類型。

在選擇一種版本控制方案時(shí),需要同時(shí)考慮到對(duì)性能的潛在影響,特別是在web服務(wù)端的緩存處理。URI和查詢字符串版本控制方案是可以緩存的,因?yàn)閿y帶了版本信息的完整URI和查詢字符串會(huì)作為緩存的唯一標(biāo)識(shí),同樣的版本請(qǐng)求每次都指向同樣的數(shù)據(jù)。

The Header versioning and Media Type versioning mechanisms typically require additional logic to examine the values in the custom header or the Accept header. In a large-scale environment, many clients using different versions of a web API can result in a significant amount of duplicated data in a server-side cache. This issue can become acute if a client application communicates with a web server through a proxy that implements caching, and that only forwards a request to the web server if it does not currently hold a copy of the requested data in its cache.(這段譯者未能完全理解,猜其意思,可能跟代理的緩存策略有關(guān),因?yàn)镠eader和MediaType的版本控制方案,在請(qǐng)求不同版本時(shí)的URI和query string并沒(méi)有變化,代理可能會(huì)使用不正確的緩存數(shù)據(jù)直接返回,而不是向web服務(wù)端發(fā)起新的請(qǐng)求來(lái)獲取數(shù)據(jù))

Open API 計(jì)劃

Open API 計(jì)劃是由一個(gè)行業(yè)聯(lián)盟創(chuàng)建的,目的是標(biāo)準(zhǔn)化供應(yīng)商之間的REST API描述。作為這一計(jì)劃的一部分,Swagger 2.0規(guī)范被重新命名為OpenAPI規(guī)范(OAS),并納入了Open API 計(jì)劃。

如果你想要把自己的web API改造為OpenAPI,可以參考下面幾點(diǎn):

  • OpenAPI規(guī)范附帶了一套關(guān)于REST API應(yīng)該如何設(shè)計(jì)的固執(zhí)的指導(dǎo)方針。這對(duì)互操作性有好處,但在設(shè)計(jì)API以符合規(guī)范時(shí)需要更加小心。

  • OpenAPI提倡契約優(yōu)先的方法,而不是實(shí)現(xiàn)優(yōu)先的方法。契約優(yōu)先意味著首先設(shè)計(jì)API契約(接口),然后編寫(xiě)實(shí)現(xiàn)該契約的代碼。

  • Swagger這樣的工具可以從API契約直接生成客戶端代碼或文檔。可參考 ASP.NET Web API help pages using Swagger

More information

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,030評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,310評(píng)論 3 415
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 175,951評(píng)論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 62,796評(píng)論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,566評(píng)論 6 407
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 55,055評(píng)論 1 322
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,142評(píng)論 3 440
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 42,303評(píng)論 0 288
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,799評(píng)論 1 333
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,683評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,899評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,409評(píng)論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,135評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 34,520評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 35,757評(píng)論 1 282
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,528評(píng)論 3 390
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,844評(píng)論 2 372

推薦閱讀更多精彩內(nèi)容