三方依賴管理工具使用(一) —— Carthage的使用(一)

版本記錄

版本號 時間
V1.0 2020.03.23 星期一

前言

在工程中,我們總要使用各種第三方依賴管理工具,方便我們進行管理。比如Cocoapods和Carthage等,這個模塊我們就一起看一下這些第三方依賴管理工具。

開始

首先看下主要內容:

在本教程中,您將了解什么是Carthage,如何安裝它,以及如何使用它來聲明、安裝和集成您的依賴項。

下面看下寫作環境

Swift 5, iOS 13, Xcode 11

iOS開發的一大優點是可以使用廣泛的第三方庫。如果您嘗試過其中的一些庫,您就會知道使用其他人的代碼而不是重新發明輪子的價值。

有這么多可用的庫,管理依賴關系可能會變得很棘手。這就是依賴管理器(dependency managers)的用武之地。

Carthage是一個簡單的macOSiOS依賴管理器,由GitHub的一群開發人員創建。

Carthage不僅是第一個與Swift一起工作的依賴管理器,而且它也是用Swift寫的!它只使用動態框架,而不是靜態庫。

在本教程中,您將:

  • 了解為什么以及何時使用依賴項管理器,以及Carthage與其他工具的不同之處。
  • 安裝Carthage
  • 聲明依賴項,在項目中安裝和集成它們。
  • 將依賴項升級到不同的版本。

您將通過構建一個應用程序來實現這一點,該應用程序使用DuckDuckGo API為搜索詞提供定義。

打開初始項目,它包括DuckDuckDefine的基本框架,這是一個使用DuckDuckGo API查找定義和圖像的簡單工具。有一個問題:它還沒有執行任何搜索!

Xcode中打開DuckDuckDefine.xcodeproj。注意兩個視圖控制器:SearchViewController為用戶執行搜索提供了一個搜索欄,DefinitionViewController顯示搜索項的定義。

這個操作的中心是在DuckDuckGo.swift的——好吧,等你完成的時候他們就會敏捷了!目前,performSearch(for:completion:)是一個懶惰的、無用的代碼塊。

要執行搜索并顯示結果,您需要做兩件事:

  • 使用DuckDuckGo API進行查詢。
  • 顯示檢索到的單詞的圖像。

有許多開源庫可以幫助完成這兩項任務。Alamofire是一個很棒的Swift庫,可以簡化web請求。AlamofireImage使處理圖像在Swift更愉快。

你猜怎么著?您將使用Carthage將這兩個依賴項都添加到您的項目中。


Advantages of Dependency Management

要將AlamofireAlamofireImage添加到您的項目中,您可以訪問它們各自的GitHub頁面,下載包含它們的源代碼的zip文件并將它們放入您的項目中。為什么要用Carthage這樣的工具呢?

依賴管理器執行一些方便的函數,包括:

  • 簡化和標準化獲取第三方代碼并將其合并到您的項目中的過程。如果沒有這個工具,您可以通過手動復制源代碼文件、放入預編譯的二進制文件或使用諸如Git子模塊之類的機制來實現這一點。
  • 使將來更新第三方庫更加容易。想象一下,必須訪問每個依賴項的GitHub頁面,下載源代碼,并在每次更新時將其放入項目中。你為什么要這樣對自己?
  • 為您使用的每個依賴項選擇適當的和兼容的版本。例如,當依賴項彼此依賴或共享另一個依賴項時,手動添加依賴項可能會很棘手。

大多數依賴關系管理器構建項目依賴關系及其子依賴關系的依賴關系圖dependency graph,然后確定每個依賴關系的最佳版本。

你可以手工做同樣的事情,依賴項管理器更簡單,更不容易出錯。


Comparing Dependency Managers

最流行的依賴項管理器之一是CocoaPods,它簡化了將庫集成到項目中的過程。它在iOS社區中被廣泛使用。

蘋果提供了自己的依賴管理器,叫做Swift包管理器Swift Package Manager,用于在Swift 3.0及以上版本中共享和分發包。

雖然CocoaPodsSwift Package Manager是很棒的工具,但Carthage提供了一個更簡單、更實用的選擇。那么哪一個更適合你呢?在下一節中,您將比較Carthage與其他一些流行的工具。

1. Carthage Versus CocoaPods

CarthageCocoaPods有何不同?除了最受歡迎的iOS依賴管理器,你還會使用其他工具嗎?

雖然CocoaPods很容易使用,但并不簡單。Carthage背后的哲學是依賴管理器應該是簡單的。

CocoaPods增加了應用程序開發和庫發布流程的復雜性:

  • 庫必須創建、更新和托管Podspec文件。如果應用程序開發人員希望使用的庫不存在,他們必須編寫自己的庫。
  • 當將pod添加到項目中時,CocoaPods創建一個新的Xcode項目,為每個單獨的pod創建一個目標,并添加一個包含它們的工作空間。您必須使用工作空間,并相信CocoaPods項目是有效的。您還必須維護那些額外的構建設置。
  • CocoaPods使用一個集中的Podspecs存儲庫。這可能會消失或變得無法訪問,從而導致問題。

Carthage的目標是提供一種更簡單、更靈活、更容易理解和維護的工具。

Carthage是這樣做到的:

  • 它不會改變您的Xcode項目,也不會強迫您使用工作空間。
  • 您不需要podspec或一個集中式的存儲庫來存放庫作者提交的pod。如果您可以將項目構建為框架,那么可以將其與Carthage一起使用,Carthage直接利用來自GitXcode的現有信息。
  • Carthage并沒有什么神奇的事;你總是在掌控一切。您將依賴項添加到Xcode項目中,然后Carthage獲取并構建它們。

注意:Carthage使用動態庫來實現其簡單性。這意味著你的項目必須支持iOS 8或更高版本。

2. Carthage Versus Swift Package Manager

那么CarthageSwift Package Manager有什么區別呢?

Swift Package Manager的主要關注點是以一種對開發者友好的方式共享Swift代碼。Carthage的重點是共享動態庫。動態庫是Swift包的超集。

包是Swift源文件和manifest文件的集合。清單文件定義包的名稱及其內容。一個包包含Swift代碼、Objective-C代碼、圖像等非代碼資產或這三者的任何組合。

SPM有很多優點,特別是Xcode 11內置了對SPM包的支持。然而,在撰寫本文時,SPM有一些限制,使許多庫無法支持它。例如,它不支持共享已經構建的二進制文件,只支持包的源代碼。如果你想要一個閉源庫,你不能使用SPM。此外,SPM支持向框架添加源代碼,但不支持向圖像或其他數據文件等資源添加源代碼。

這意味著您可能使用的許多庫都不會很快獲得SPM支持。現在,您仍然需要依賴Carthage或CocoaPods來實現這些庫。


Installing Carthage

現在你已經獲得了一些背景知識,是時候學習一下Carthage是多么的簡單了!

Carthage的核心是一個命令行工具,它可以幫助獲取和構建依賴項。

有兩種方式來安裝這個工具:

  • 下載并運行最新版本的.pkg安裝程序。
  • 使用Homebrew軟件包管理器。

就像Carthage幫助安裝用于Cocoa開發的包一樣,Homebrew幫助安裝用于macOS的有用的Unix工具。

出于本教程的目的,您將使用.pkg安裝程序。

GitHub下載Carthage的最新版本。然后,在Assets下,選擇Carthage.pkg

雙擊Carthage.pkg來運行安裝程序。單擊Continue,選擇要安裝到的位置,再次單擊Continue,最后單擊Install

注意:當您嘗試運行安裝程序時,您可能會看到一條消息:“Carthage.pkg can’t be opened because it is from an unidentified developer.”。如果是,Control-click安裝程序,并從上下文菜單中選擇Open

已經完成了,要檢查Carthage是否正確安裝,請打開終端并運行以下命令:

carthage version

這將顯示您安裝的Carthage版本。

接下來,您需要告訴Carthage使用Cartfile安裝哪些庫。


Creating Your First Cartfile

Cartfile是一個簡單的文本文件,它描述了您的項目對Carthage的依賴關系,因此它可以決定安裝什么。Cartfile中的每一行都指明了在哪里獲取一個依賴項,依賴項的名稱,以及使用哪個版本。Cartfile相當于CocoaPods Podfile

要創建第一個文件,請轉到Terminal,然后使用cd命令導航到項目的根目錄(包含.xcodeproj文件的目錄):

cd ~/Path/To/Starter/Project

使用touch命令創建一個空的Cartfile文件

touch Cartfile

然后在Xcode中打開文件進行編輯:

open -a Xcode Cartfile

如果您熟悉另一個文本編輯器,比如Vim,那么可以隨意使用它。但是,不要使用TextEdit來編輯文件。在TextEdit中,很容易不小心使用“聰明的引號”而不是直接的引號,這會使Carthage出現識別問題。

添加以下行到Cartfile并保存:

github "Alamofire/Alamofire" == 4.9.0
github "Alamofire/AlamofireImage" ~> 3.4

這兩行代碼告訴Carthage,您的項目需要Alamofire版本4.9.0和與版本3.4兼容的AlamofireImage的最新版本。


The Cartfile Format

您可以在OGDL:Ordered Graph Data Language的子集中編寫cartfile。這聽起來很神奇,但其實很簡單。在Cartfile的每一行都有兩個關鍵信息:

  • Dependency origin:這告訴Carthage在哪里獲取依賴項。Carthage支持兩種起源:

    • githubgithub用于github托管的項目(線索在名字里!)您可以使用Username/ProjectName格式指定GitHub項目,就像您在上面的Cartfile中所做的那樣。
    • git:用于托管在其他地方的通用git存儲庫。可以使用git關鍵字,然后是git存儲庫的路徑,不管是使用git://、http://或ssh://的遠程URL,還是開發機器上git存儲庫的本地路徑。
  • Dependency version:在這里,你告訴Carthage你想使用哪個依賴版本。根據你想要的具體程度,你可以有幾種選擇:

    • == 1.0:表示“完全使用1.0版本”。
    • >= 1.0:表示“使用1.0或更高版本”。
    • ~> 1.0:翻譯過來是“使用與1.0兼容的任何版本”,意思是直到下一個主要版本的任何版本。
    • Branch name / tag name / commit name:意思是“使用這個特定的git分支/標簽/提交”。例如,您可以指定master,或者像5c8a74a這樣的提交hash值。

以下是一些例子:

如果你指定了~> 1.7.5Carthage認為從1.7.52.0的任何版本都是兼容的,但不包括2.0

同樣,如果您指定~> 2.0,那么Carthage將使用2.0版或任何后續版本,但不包括3.0或以上版本。

Carthage使用語義版本semantic versioning控制來確定兼容性。

如果您沒有指定一個版本,Carthage將使用與您的其他依賴項兼容的最新版本。你可以在Carthage的自述文件 Carthage’s README file中看到這些選項的例子。


Building Dependencies

現在您已經有了一個Cartfile,是時候使用它并安裝一些依賴項了!

注意:這個Carthage教程使用的是Swift 5。在撰寫本文時,Swift 5僅在Xcode 11中可用。確保你已經配置你的命令行工具使用Xcode 11從終端運行以下命令:

sudo xcode-select -s <path to Xcode 11>/Xcode.app/Contents/Developer 

請確保將path to Xcode 11替換為您的機器到Xcode 11的特定路徑。

Xcode中關閉你的Cartfile,返回Terminal。運行以下命令:

carthage update --platform iOS

這指示CarthageCartfile中克隆Git存儲庫,然后將每個依賴項構建到框架中。您將看到顯示結果的輸出,如下所示:

*** Cloning AlamofireImage
*** Cloning Alamofire
*** Checking out Alamofire at "4.9.0"
*** Checking out AlamofireImage at "3.6.0"
*** xcodebuild output can be found in /var/folders/bj/3hftn5nn0qlfrs2tqrydgjc80000gn/T/carthage-xcodebuild.7MbtQO.log
*** Building scheme "Alamofire iOS" in Alamofire.xcworkspace
*** Building scheme "AlamofireImage iOS" in AlamofireImage.xcworkspace

--platform iOS確保Carthage只構建iOS框架。如果你不指定一個平臺,Carthage將為所有平臺構建框架——通常是MaciOS——由庫支持。

如果您想查看更多選項,請運行carthage help update

默認情況下,Carthage執行它的checkout并構建一個名為Carthage的新目錄,您將在與Cartfile相同的位置找到它。現在打開這個目錄運行:

open Carthage

您將看到出現一個Finder窗口,其中包含兩個目錄:Buildcheckout。花點時間看看Carthage為你創造了什么。


Building Artifacts

當您使用CocoaPods時,它會對Xcode項目進行幾處更改,并將結果與一個特殊的Pods項目綁定到Xcode工作空間中。

Carthage有點不同。它檢查依賴項的代碼并將結果構建到二進制框架中。然后由您來將框架集成到您的項目中。

這聽起來像是額外的工作,但它是有益的。只需幾個步驟,您就可以更清楚地了解項目的變化。

當你運行carthage update時,Carthage會為你創建兩個文件和目錄:

  • Cartfile.resolved:此文件作為Cartfile的伙伴。它精確地定義了Carthage選擇安裝的依賴項版本。強烈建議將此文件提交到版本控制存儲庫。它的存在確保了其他開發人員可以通過使用完全相同的依賴項版本快速起步。
  • Carthage目錄,包含兩個子目錄:
    • Build:它包含為每個依賴項構建的框架。您可以將這些集成到您的項目中,并且很快就會這樣做。Carthage要么從源代碼構建每個框架,要么從GitHub上的項目發布頁面下載框架。
    • Checkouts:這是Carthage簽出每個準備構建到框架中的依賴項的源代碼的地方。Carthage維護它自己的內部依賴存儲庫緩存,因此它不必為不同的項目多次克隆相同的源。

1. Avoiding Problems With Your Artifacts

您是否將BuildCheckouts文件夾提交到版本控制存儲庫取決于您自己。這不是必需的,但是這樣做意味著任何克隆您的存儲庫的人都將擁有每個可用依賴項的二進制文件和源代碼。

如果GitHub不可用,或者源存儲庫被刪除,有這個備份可能是一個有用的保險策略。

不要更改Checkouts文件夾中的任何代碼,因為將來的carthage updatecarthage checkout命令可能會在任何時候覆蓋其內容。你的努力工作一眨眼就會消失。

如果必須修改依賴項,請使--use-submodules選項運行carthage update

通過這個選項,CarthageCheckouts文件夾中的每個依賴項作為子模塊添加到Git存儲庫中,這意味著您可以更改依賴項的源代碼,并將這些更改提交到其他地方,而不必擔心覆蓋。

注意:如果其他開發人員使用您的項目,而您的代碼還沒有提交built框架,那么他們需要在檢查完您的項目后運行carthage bootstrap

bootstrap命令下載并構建在Cartfile.resolved中指定的依賴項的精確版本。carthage update將更新項目以使用每個依賴項的最新兼容版本,這可能是不可取的。

現在,如何實際使用這些您努力創建的構建工件呢?您將在下一節中完成這項工作。


Adding Frameworks to Your Project

回到Xcode,在項目導航器中單擊DuckDuckDefine項目。選擇DuckDuckDefine目標。選擇頂部的General選項卡,然后滾動到底部的Frameworks, Libraries, and Embedded Content部分。

Carthage Finder窗口中,導航到Build/iOS。將Alamofire.frameworkAlamofireImage.framework拖到Xcode中的鏈接框架和庫部分:

這告訴Xcode將您的應用程序鏈接到這些框架,允許您在代碼中使用它們。

接下來,切換到Build Phases。單擊編輯器左上方的+圖標,并選擇New Run Script Phase。在Run Script下的代碼塊中添加以下命令:

/usr/local/bin/carthage copy-frameworks

點擊Input Files下的+圖標,為每個框架添加一個條目:

$(SRCROOT)/Carthage/Build/iOS/Alamofire.framework
$(SRCROOT)/Carthage/Build/iOS/AlamofireImage.framework

結果如下所示:

嚴格地說,這個構建階段并不是您的項目運行所必需的。然而,對于一個App Store submission bug來說,這是一個巧妙的解決方案。在這個bug中,帶有iOS模擬器二進制圖像框架的應用程序會被自動拒絕。

carthage copy-frameworks命令去掉了這些額外的體系架構。

盡管現在還沒有什么新的東西可以看到,但是構建和運行以確保一切都如預期的那樣工作。當應用啟動時,你會看到搜索視圖控制器。

好了。一切看起來不錯。接下來,您將致力于升級依賴項。


Upgrading Frameworks

我要坦白一件事。

還記得你之前創建你的Cartfile時,我告訴過你應該安裝哪個版本的AlamofireAlamofireImage嗎?我給了你壞消息。我告訴過你用舊版本的Alamofire

別生氣!我做這件事是出于好意。將此視為學習如何升級依賴項的機會。

再次打開你的Cartfile。從你的項目的目錄在終端,運行:

open -a Xcode Cartfile

改變Alamofire那行

github "Alamofire/Alamofire" ~> 4.9.0

如前所述,此代碼指示Carthage使用與4.9.0兼容的任何版本的Alamofire。這包括任何版本,但不包括未來的5.0版本。

在添加與Carthage的依賴項時,您希望考慮兼容性并限制目標版本。這樣,您就知道它的API和功能的確切狀態。

例如,一個依賴項的5.0版本可能包括破壞應用程序的API更改。如果您使用4.9.0構建項目,就不會希望自動升級到該版本。

保存并關閉Cartfile并返回到終端。執行另一個更新:

carthage update --platform iOS

這告訴Carthage去尋找每個依賴項的更新版本。然后,如果需要,它將檢查并構建它們。在本例中,它獲取最新版本的Alamofire

因為您的項目已經包含了對Alamofire構建的.framework的引用,并且Carthage在磁盤上的相同位置重新構建了新版本,所以您可以坐下來讓Carthage來做這項工作。您的項目將自動使用最新版本的Alamofire!


Duck, Duck… GO!

現在您已經將AlamofireAlamofireImage集成到項目中,您可以通過執行一些web搜索來使用它們。

Xcode中,打開DuckDuckGo.swift。在文件的頂部,添加下面的導入

import Alamofire

下面,用下面替換performSearch(for:completion:)

// 1
let parameters: Parameters = [
  "q": term,
  "format": "json",
  "pretty": 1,
  "no_html": 1,
  "skip_disambig": 1
]

// 2
Alamofire.request(
  "https://api.duckduckgo.com",
  method: .get,
  parameters: parameters)
  .responseData { response in
    // 3
    guard 
      response.result.isSuccess, 
      let jsonData = response.result.value 
      else {
        completion(nil)
        return
    }
    
    // 4
    let decoder = JSONDecoder()
    
    guard 
      let definition = try? decoder.decode(Definition.self, from: jsonData),
      definition.resultType == .article 
      else {
        completion(nil)
        return
    }
    
    completion(definition)
}

它的作用如下:

  • 1) 首先,構建要發送給DuckDuckGo的參數。其中最重要的兩個是q(搜索項本身)和format(格式),后者告訴web服務使用JSON響應。
  • 2) 然后使用Alamofire執行請求。這個調用使用上面創建的參數字典向https://api.duckduckgo.com發出一個GET請求。
  • 3) 一旦響應返回,檢查請求是否失敗,并綁定JSON響應對象以確保它有一個值。如果出了問題,盡早退出。
  • 4) 接下來,使用JSONDecoder對定義進行反序列化,這遵循Codable協議。DuckDuckGo API可以返回一系列不同的結果類型,但是這里介紹的是Article,它提供了搜索詞的簡單定義。篩選文章,然后將檢索到的定義傳遞給完成處理程序。

注意:如果你想知道為什么skip_disambig參數存在,它是告訴DuckDuckGo不要返回disambiguation結果。

消除歧義的結果就像維基百科上克里斯·埃文斯this Christopher Evans page on Wikipedia的頁面一樣,需要說明的是無論你指的是電影演員克里斯·埃文斯,英國電視名人克里斯·埃文斯還是火車劫匪克里斯·埃文斯。

skip_disambig意味著API將選擇最可能的結果并返回它。

構建和運行。一旦應用程序啟動,在搜索欄中輸入“Duck”。您將在下一個屏幕上看到定義。

然而,少了一樣東西:一張照片!讀鴨子是一回事,但誰還會讀呢?照片是有價值的——好吧,我就不啰嗦了,你知道我的意思。

打開DefinitionViewController.swift并在頂部現有的UIKit導入下面添加一個新的導入:

import AlamofireImage

viewDidLoad下面添加下面代碼:

override func viewDidAppear(_ animated: Bool) {
  super.viewDidAppear(animated)
    
  if let imageURL = definition.imageURL {
    imageView.af_setImage(withURL: imageURL) { _ in
      self.activityIndicatorView.stopAnimating()
    }
  }
}

af_setImageAlamofireImage提供的UIImageView的擴展。您可以調用它來檢索在定義imageURL中找到的圖像。檢索后,活動指示器的動畫將停止。

構建并運行,然后再次執行搜索。

如果你想更多地了解Carthage,你的第一站應該是 Carthage README和關于Build Artifacts的文檔。

最后,如果你想了解更多關于Swift Package Manager的信息。

后記

本篇主要講述了Carthage的使用,感興趣的給個贊或者關注~~~

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

推薦閱讀更多精彩內容