【spring 指南系列】如何更好的設計RESTful API

譯者:知秋(極樂科技專欄作者)
來源:https://zhuanlan.zhihu.com/p/24592119


當您的數據模型已開始穩定,您可以為您的網絡應用程序創建公共API。 你意識到,很難對你的API進行重大更改,一旦它發布,并希望盡可能得到盡可能多的前面。 現在,互聯網對API設計的意見有很多。 但是,因為沒有一個廣泛采用的標準在所有情況下都有效,所以你前面有一堆選擇:你應該接受什么格式? 你應該如何認證? 你的API是否應該版本化?

構建API是您可以做的最重要的事情之一,以提高您的服務的價值。 通過使用API,您的服務/核心應用程序有可能成為其他服務增長的平臺。 看看當前巨大的科技公司:Facebook,Twitter,谷歌,GitHub,亞馬遜,Netflix …沒有一個人會像今天一樣大,如果他們沒有通過API打開他們的數據。 事實上,整個行業存在的唯一目的是消費由所述平臺提供的數據。

你的API越簡單明了,使用的人就越多。

許多在網絡上發現的API設計觀點是圍繞主觀的模糊標準解釋的學術討論,而不是在現實世界中有意義的。 我的目標是描述一個為當今的Web應用程序設計的務實的API的最佳實踐。 我沒有嘗試滿足一個標準,如果它不覺得正確。 為了幫助指導決策過程,我寫了一些API必須努力達到的要求:

  • 它應該使用Web標準,他們有意義
  • 它應該對開發人員友好,可以通過瀏覽器地址欄探索
  • 它應該簡單,直觀和一致,使采用不僅容易,而且愉快
  • 它應該提供足夠的靈活性來支持大部分我們所設計的UI
  • 應該是有效的,同時保持與其他要求的平衡

API是開發人員的UI - 就像任何UI一樣,確保用戶的體驗被仔細考慮是非常重要的!

RESTful API設計定義

以下是我將在本文檔中使用的一些重要術語:

  • **Resource **:對象的單個實例。 例如,一只動物。
  • 集合:對象的集合。 例如,動物。
  • **HTTP **:用于通過網絡通信的協議。
  • **Consumer **:能夠發出HTTP請求的客戶端計算機應用程序。
  • 第三方開發人員:不是您項目的一部分,但希望使用您的數據服務的開發人員。
  • 服務器:可通過網絡從客戶端訪問的HTTP服務器/應用程序。
  • 端點:服務器上的API網址,表示資源或整個集合。
  • 冪等:無邊際效應,多次操作得到相同的結果。
  • 網址區段:網址中的斜線分隔的信息。

數據設計和抽象

首先將從你寫的開發文檔API開始(比如我們可以看到各個開發平臺的暴露出來的API文檔),您需要決定如何設計數據,以及您的核心服務/應用程序如何工作。 如果你在做的API是第一次開發,這應該很容易。 如果您要將API附加到現有項目,則可能需要提供更多抽象(畢竟是要按照已有的文檔規范來做)。

有時,集合可以表示數據庫表,資源可以表示該表中的一行。 然而,這不是通常的情況。 事實上,你的API應該盡可能多地抽象出你的數據和業務邏輯。 非常重要的一點是,如果您不希望使用你的API很難使用,就不要使用任何復雜的應用程序數據來為難第三方開發人員(讓開發人員覺得還得對這些數據進一步處理而浪費更多精力)。

還有你的服務的很多部分,你不應該通過API公開。 一個常見的例子是許多API不允許第三方創建用戶。

設計資源請求

當然你知道GET和POST請求。當您的瀏覽器訪問不同的網頁時,這兩個最常用的請求。POST是如此受歡迎,它甚至流行語我們的平常的說話中,即使那些不知道互聯網如何工作的人也知道他們可以“發布”的東西在朋友的Facebook上。

有四個半非常重要的HTTP動詞,你需要知道。我說“一半”,因為PATCH動詞非常類似于PUT動詞,兩個通常由許多API開發人員組合。這里是動詞,在他們旁邊是他們相關的數據庫調用(我假設大多數人讀這個知道更多關于寫入數據庫而不是設計一個API)。

  • **GET **(SELECT):從服務器檢索特定資源,或資源列表。
  • **POST **(CREATE):在服務器上創建一個新的資源。
  • **PUT **(UPDATE):更新服務器上的資源,提供整個資源。
  • **PATCH **(UPDATE):更新服務器上的資源,僅提供更改的屬性。
  • **DELETE **(DELETE):從服務器刪除資源。

這里有兩個較少知名的HTTP動詞:

  • **HEAD **- 檢索有關資源的元數據,例如數據的哈希或上次更新時間。
  • **OPTIONS **- 檢索關于客戶端被允許對資源做什么的信息。

一個好的RESTful API將使用四個半HTTP動詞,允許第三方與其數據進行交互,并且不會將動作/動詞作為URL段。

通常,GET請求可以被緩存(通常是!)在瀏覽器,例如將緩存請求頭用于第二次用戶的POST請求。 HEAD請求基本上是一個沒有響應主體的GET,并且也可以被緩存。

版本控制

無論你正在構建什么,無論你事先做了多少規劃,你的核心應用程序總會改變,你的數據關系總會改變,屬性添加和從你的資源中刪除。這只是軟件開發的工作原理,尤其是如果你的項目還活著并被許多人使用(如果你正在構建一個API,情況可能就會如此)。

記住,API是服務器和客戶端之間的已發布約定。如果您更改了服務器API,這些更改會破壞向后兼容性,那么你就打破了這個約定,客戶端又會要求你重新支持它(誰讓客戶端依然是之前的版本,調用的還是之前的API)。為了避免這樣的事情,并讓您的客戶端滿意,您需要偶爾引入新版本的API,同時仍允許訪問舊版本。

注意,如果你只是為你的API添加新的特性,例如資源上的新屬性,或者如果你添加新的端點(比如之前只有查詢,現在增加一個修改),你不需要增加您的API版本號,因為這些更改不會破壞向后兼容性。當然,您將需要更新您的API文檔。

隨著時間的推移,您可以棄用API的舊版本。棄用某個功能并不意味著關閉它或者降低它的質量,而是告訴客戶端您的API,舊版本將在特定日期刪除,并且他們應該升級到較新的版本。

一個好的RESTful API設計將跟蹤URL中的版本。另一個最常見的解決方案是將版本號放在請求頭中,但在與許多不同的第三方開發人員合作之后,我可以告訴您,添加這些請求頭信息并不像添加網址細分那么容易。

分析

跟蹤客戶端使用的API的版本/端點。 這可以像每次請求時在數據庫中增加一個整數一樣簡單。 有很多原因跟蹤API Analytics是一個好主意,例如,最常用的API調用應該是高效的。
為了構建第三方開發者所喜歡的API,最重要的是,當您棄用某個版本的API時,實際上可以使用已棄用的API功能與開發人員聯系(在兩個異構系統中當對方的開發人員調用本服務時順帶告知對方)。 這是提醒他們在棄用舊API版本之前升級的完美方法。
第三方開發者通知的過程可以自動化,例如。 每當對一個已棄用的功能發出10,000個請求時,發郵件通知開發人員。

API Root URL

無論你相信與否,您的API的根位置是重要的。當開發人員使用您的API接手舊項目并需要構建新功能時,他們可能根本不知道您有哪些服務。幸好他們知道客戶端對外調用的那些URL列表。重要的是,進入您的API的根入口點盡可能簡單,因為長的復雜URL將顯得令人生畏,并可能使開發人員直接略過而不會采用。

這里有兩個常見的URL根:

如果您的應用程序龐大,或者您預計它會變得龐大,將API放在自己的子域(例如** api。**)上是一個不錯的選擇。這可以允許在路上一些更靈活的可擴展性。

如果您預計您的API將不會增長到那么大,或者您想要一個更簡單的應用程序設置(例如,您希望從同一個框架托管網站和API),將您的API放置在域根的URL段(例如** / api / **)也有效。

將內容設為您的API根目錄是個好主意。例如,點擊GitHub的API的根會返回一個端點列表。就個人而言,我喜歡使用根網址提供給開發人員認為有用的信息,例如,如何獲取API的開發人員文檔。

此外,請注意HTTPS前綴。作為一個好的RESTful API,您必須在HTTPS之后托管您的API(一個好的RESTful API總是基于HTTPS來發布的)。

端點

端點是您的API中指向特定資源或資源集合的URL。

如果你正在構建一個虛擬的API來代表幾個不同的動物園,每個動物園包含許多動物,員工(可以在多個動物園工作)和跟蹤每個動物的物種,你可能有以下端點:

當引用每個端點可以做什么時,您需要列出有效的HTTP動詞和端點組合。例如,這里有一個半全面的行動列表,可以使用我們虛構的API執行。請注意,我在每個端點之前都有HTTP動詞,因為這是在HTTP請求標頭中使用的相同符號。

  • GET / zoos:列出所有動物園(ID和名稱,不要太多細節)
  • POST / zoos:創建一個新的Zoo
  • GET / zoos / ZID:檢索整個Zoo對象
  • PUT / zoos / ZID:更新Zoo(整個對象)
  • PATCH / zoos / ZID:更新Zoo(部分對象)
  • DELETE / zoos / ZID:刪除動物園
  • GET / zoos / ZID / animals:檢索動物列表(ID和名稱)。
  • GET / animals:列出所有動物(ID和名稱)。
  • POST / animals:創建一個新的動物
  • GET / animals / AID:檢索動物對象
  • PUT / animals / AID:更新動物(整個對象)
  • PATCH / animals / AID:更新動物(部分對象)
  • GET / animal_types:檢索所有動物類型的列表(ID和名稱)
  • GET / animal_types / ATID:檢索整個動物類型對象
  • GET / employees:檢索完整的員工列表
  • GET / employees / EID:檢索特定員工
  • GET / zoos / ZID / employees:檢索在此動物園工作的員工(ID和名稱)的列表
  • POST / employees:創建一個新員工
  • POST / zoos / ZID / employees:在特定動物園雇用員工
  • DELETE / zoos / ZID / employees / EID:從特定的動物園中解雇員工

在上面的列表中,ZID表示Zoo ID,AID表示動物ID,EID表示Employee ID,ATID表示動物類型ID。在你的文檔中有一個鍵,你選擇的任何約定是一個好主意。

為了簡潔,我在上面的示例中省略了常見的API網址前綴。雖然這在通訊期間可能很好,但在實際的API文檔中,您應該始終顯示每個端點的完整網址(例如GEThttp://api.example.com/v1/animal_type/ATID)
注意數據之間的關系如何顯示,特別是雇員和動物園之間的多對多關系。通過添加其他網址細分,您可以執行更具體的互動。當然,對于“FIRE(解雇)”沒有HTTP動詞,但是通過對位于Zoo內的Employee執行DELETE,我們能夠實現相同的效果。

過濾器

當客戶端請求對象列表時,請務必為它們提供符合所請求條件的每個對象的列表。這個列表可能是巨大的。但是,重要的是不要對數據執行任何任意限制。正是這些任意的限制使第三方開發者很難知道發生了什么。如果他們請求某個集合,并迭代結果,他們從來沒有看到超過100個結果,接下來他們就不得不去查找這個限制條件的出處(提供服務端沒有問題,就只能是調用端的問題了)。到底是他們的ORM的bug導致的,還是因為網絡截斷了大數據包?

盡可能減少那些會影響到第三方開發者開發的無謂限制

然而,重要的是,您確實為客戶端提供了指定某種過濾/結果限制的能力。這么做最重要的一個原因是可以最小化網絡傳輸,客戶端盡快得到結果。第二個重要的原因是客戶端可能是懶惰的,如果服務器可以為他們做過濾和分頁,一切都更好。還有一個不那么重要的原因,請求資源越少,對服務器的一個很大的好處是,減少了負載。

過濾主要用于對資源集合執行GET。由于這些是GET請求,因此應通過URL傳遞過濾信息。以下是您可能想要添加到API的過濾類型的一些示例:

  • ?limit = 10:減少返回給Consumer的結果數(用于分頁)
  • ?offset = 10:向客戶端發送信息集(用于分頁)
  • ?animal_type_id = 1:過濾符合以下條件的記錄(WHERE animal_type_id = 1)
  • ?sortby = name&order = asc:根據指定的屬性對結果進行排序(ORDER BYname ASC)

其中一些過濾可能與端點URLS冗余。例如我之前提到的GET / zoo / ZID / animals。這與GET / animals是一樣的嗎?zoo_id = ZID。為客戶端提供的專用端點將使他們的開發更輕松,這對于您預期他們會做很多的請求尤其如此。在文檔中,提及這種冗余,以便第三方開發人員不會留意是否存在差異。

還有一個要說的是,每當您執行數據的過濾或排序時,請確保您列出客戶端可以過濾和排序的列。我們不希望將任何數據庫錯誤發送給客戶端!

狀態碼

作為RESTful API,使用正確的HTTP狀態代碼非常重要;他們是一個標準!各種網絡設備能夠讀取這些狀態碼,例如,負載平衡器可以配置為避免向發送大量50x錯誤的Web服務器發送請求。有很多HTTP狀態代碼可供選擇,但此列表應該是一個很好的起點:

  • **200 **OK - [GET]
  • 客戶端從服務器請求數據,服務器為它們找到它(等冪)
  • **201 **CREATED - [POST / PUT / PATCH]
  • 客戶端提供了服務器數據,并且服務器創建了一個資源
  • **204 **無內容 - [刪除]
  • 客戶端要求服務器刪除資源,并且服務器將其刪除
  • **400 **無效請求 - [POST / PUT / PATCH]
  • 客戶端給服務器的數據不良,服務器沒有做任何事情(冪等)
  • *錯誤404 - []

*客戶端引用了一個不存在的資源或集合,并且服務器什么也不做(冪等)

  • *500內部服務器錯誤 - []

*服務器遇到錯誤,并且客戶端不知道請求是否成功

狀態碼范圍

**1xx **范圍保留用于底層HTTP的東西,你很可能永遠也用不到。
**2xx **范圍保留用于成功消息,盡可能確保您的服務器盡可能多地向客戶端發送這些消息。
**3xx **范圍保留用于重定向。大多數API不使用這些請求很多(不像SEO人使用它們那么頻繁),然而,較新的超媒體風格API將更多地使用這些請求。
**4xx **范圍保留用于響應客戶端做出的錯誤,例如。他們提供不良數據或要求不存在的東西。這些請求應該是冪等的,而不是更改服務器的狀態。
**5xx **范圍的狀態碼是保留給服務器端錯誤用的。這些錯誤常常是從底層的函數拋出來的,甚至開發人員也通常沒法處理,發送這類狀態碼的目的以確保客戶端獲得某種響應。當收到5xx響應時,客戶端不可能知道服務器的狀態,所以這類狀態碼是要盡可能的避免。
預期的返回文檔
當使用不同的HTTP動詞對服務器端點執行操作時,客戶端需要在返回結果里面拿到一系列的信息。下面的列表是非常典型的RESTful API:

  • GET / collection:返回資源對象的列表(數組)
  • GET / collection / resource:返回單個Resource對象
  • POST / collection:返回新創建的Resource對象
  • PUT / collection / resource:返回完整的Resource對象
  • PATCH / collection / resource:返回完整的Resource對象
  • DELETE / collection / resource:返回一個空文檔

請注意,當Consumer創建資源時,他們通常不知道正在創建的資源的ID(也不知道其他屬性,如創建和修改的時間戳)(如果適用)。 這些附加屬性與后續請求一起返回,當然作為對初始POST的響應。

認證

大多數時候,一個服務器想要知道誰正在做哪些請求。當然,一些API提供公共用戶(匿名用戶)使用的,但大多數時間的工作是代表某人執行。

OAuth 2.0提供了一個很好的方法。對于每個請求,您可以確定知道哪個客戶正在發出請求,代表他們請求哪個用戶,并提供一種(大部分)標準化的方式來過期訪問或允許用戶撤消來自客戶端的訪問權,需要第三方客戶端知道用戶登錄憑據。

還有OAuth 1.0xAuth同樣適用這樣的場景。無論您選擇哪種方法,請確保它是常見的,并且有許多不同的庫為您的客戶端可能使用的語言/平臺編寫的文檔(比如redis提供Java調用的API)。

我可以誠實地告訴你,OAuth 1.0a,雖然它是最安全的選項,但是實現起來很痛苦。建議你選擇一個替代品。

內容類型

目前,最令人興奮的API提供來自RESTful接口的JSON數據。這包括Facebook,Twitter,GitHub,你命名。 XML似乎已經失去了優勢(除了在大型企業環境中)。 SOAP,不幸的是,它過時了,我們真的沒有看到太多的API把HTML作為結果返回給客戶端(除非你在構建一個爬蟲程序)。

只要你返回給他們有效的數據格式,開發者就可以使用流行的語言和框架進行解析。如果你正在構建一個通用的響應對象并使用不同的序列化器,你也可以很容易的提供之前所提到的那些數據格式(不包括SOAP)。而你所要做的就是把使用方式放在響應數據的接收頭里面。

一些API創建者建議向URL(端點之后)添加.json,.xml或.html文件擴展名以指定要返回的內容類型,但我個人不喜歡這一點。我真的很喜歡Accept頭(它是內置在HTTP規范),并且我覺得這么做也比較適當一些。

超媒體API

超媒體API很可能是RESTful API設計的未來。 實際上是一個非常好的概念,它回歸到了HTTP和HTML如何運作的“本質”。

當使用非超媒體RESTful API時,URL端點是服務器和使用者之間的約定的一部分。這些端點必須由客戶端提前知道,并且更改這些端點意味著客戶端不再能夠按預期與服務器通信。你可以先假定這是一個限制。

現在,API客戶端已經不僅僅只有那些創建HTTP請求的用戶代理了。大多數HTTP請求是由人們通過瀏覽器產生的。人們不會被哪些預先定義好的RESTful API端點URL所約束。是什么讓人們變的如此與眾不同?人們可以閱讀內容,點擊鏈接,看看有趣的標題,一般來說,探索一個網站,解釋內容,去他們想去的地方。即使一個URL改變,人們也不受影響(除非,他們事先給某個頁面做了書簽,在這種情況下,他們去主頁并發現原來有一條新的路徑可以去往之前的頁面)。

超媒體API概念的工作方式與人類相同。請求API的根返回一個URL列表,它可能指向每個信息集合,并以客戶端可以理解的方式描述每個集合。為每個資源提供ID并不重要(或必需),只要提供了一個URL即可。

隨著超媒體API的客戶端爬行鏈接和收集信息,URL在響應中始終是最新的,并且不需要事先知道作為約定的一部分。如果URL被緩存,并且后續請求返回404,則客戶端可以簡單地返回到根并再次發現內容。

在檢索集合中的資源列表時,將返回包含各個資源的完整URL的屬性。當執行POST / PATCH / PUT時,響應可以是3xx重定向到完整的資源。
JSON不僅告訴了我們需要定義哪些屬性作為URL,也告訴了我們如何將URL與當前文檔關聯的語義。正如你猜的那樣,HTML就提供了這樣的信息。我們可能很樂意看到我們的API走完了完整的周期,并回到了處理HTML上來。想一下我們與CSS一起前行了多遠,有一天我們甚至可能會看到,API和網站使用完全相同的URL和內容是常見的做法。

文檔

老實說,即便你不能百分之百的遵循指南中的條款,你的API不一定是糟糕的。但是,如果你不為API準備文檔的話,沒有人會知道如何使用它,那它真的會成為一個糟糕的API。

使您的文檔可用于未經身份驗證的開發人員。

不要使用自動文檔生成器,或者如果你這樣做,你也要保證自己審閱過并使其具有更好的版式。

不要截斷示例請求和響應正文,要展示完整的東西。在文檔中使用語法高亮指示符。

記錄每個端點的預期響應代碼和可能的錯誤消息,以及導致這些錯誤消息可能出現的錯誤。

如果您有空閑時間,請構建一個開發人員API控制臺,以便開發人員可以立即試用您的API。這不像你想象的那么難,開發者(內部和第三方)也會因此而擁戴你!

確保您的文檔可以打印; CSS是一個強大的東西;不要害怕在打印文檔時隱藏側邊欄。即使沒有人打印過物理副本,你會驚奇的發現有多少開發者喜歡打印到PDF以供離線閱讀。

勘誤:原始的HTTP封包

因為我們所做的一切都是通過HTTP,我將向你展示一個HTTP包的剖析。 我經常感到驚訝的是,有多少人不知道這些東西是什么樣子的! 當客戶端向服務器發送請求時,它們提供一組鍵/值對,稱為標題,以及兩個換行符,最后是請求體。 這都是在同一個數據包中發送的。

服務器然后以所述鍵/值對格式,用兩個換行符然后響應主體進行響應。 HTTP是一個請求/響應協議; 沒有“推送”支持(服務器向客戶端發送數據未經安全),除非您使用不同的協議,如Websockets。

在設計API時,您應該能夠使用允許查看原始HTTP數據包的工具。 例如,考慮使用Wireshark。 此外,請確保您使用的框架/ Web服務器,允許您閱讀和更改盡可能多的這些字段。

Example HTTP Request

POST /v1/animal HTTP/1.1
Host: api.example.org
Accept: application/json
Content-Type: application/json
Content-Length: 24
 
{
  "name": "Gir",
  "animal_type": 12
}

Example HTTP Response


HTTP/1.1 200 OK
Date: Wed, 18 Dec 2013 06:08:22 GMT
Content-Type: application/json
Access-Control-Max-Age: 1728000
Cache-Control: no-cache
 
{
  "id": 12,
  "created": 1386363036,
  "modified": 1386363036,
  "name": "Gir",
  "animal_type": 12
}

參考文章:

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

推薦閱讀更多精彩內容