如何搭建多環(huán)境支持铸屉,為 App CI作準備

前言

在開始之前钉蒲,先問幾個問題,在測試的時候彻坛,App 一般需要連接測試服務(wù)器顷啼,那么在上架后踏枣,還需要連生產(chǎn)服務(wù)器嗎?在發(fā)布前钙蒙,App 需要通過 Ad-hoc 分發(fā)給內(nèi)部測試組嗎茵瀑?在發(fā)布到 App Store 的時候,App 需要同時支持免費版和收費版嗎仪搔?

如果回答是“是”瘾婿,那么App 就需要搭建多環(huán)境支持蜻牢,優(yōu)化開發(fā)的工作流程烤咧。多環(huán)境提供很多好處,比如能基于同一套源代碼自動構(gòu)建出有差異功能的 App抢呆;能支持多個團隊并行開發(fā)煮嫌,也能分離測試和生產(chǎn)環(huán)境,提高產(chǎn)品的迭代速度抱虐,保證上架的 App 通過嚴格測試和功能驗證昌阿。

在 App 項目中,一般使用了三個不同的環(huán)境恳邀,分別是開發(fā)環(huán)境懦冰,測試環(huán)境和生產(chǎn)環(huán)境。它們到底有什么區(qū)別呢谣沸?

  • 開發(fā)環(huán)境刷钢,用于日常的開發(fā),一般有未完成的功能模塊乳附。編譯時内地,也不進行任何優(yōu)化,可以打印更多的日志赋除,幫助開發(fā)者快速定位問題阱缓。
  • 測試環(huán)境,主要是用于測試举农,以及為產(chǎn)品經(jīng)理進行功能驗證荆针,包括部分完成的功能模塊,也提供一些隱藏功能颁糟,方便進行開發(fā)和迭代航背,例如快速切換用戶,清理 Cache滚停,連接到不同后臺服務(wù)器等等沃粗。
  • 生產(chǎn)環(huán)境,只包含通過了測試并驗證過的功能模塊键畴,它是最終提交到 App Store 供終端用戶使用的版本最盅。

多環(huán)境支持需要用到 Xcode 的構(gòu)建配置突雪,下面會一一闡述。

Xcode 構(gòu)建基礎(chǔ)概念

一般在構(gòu)建一個 iOS App 的時候涡贱,需要用到 Xcode Project咏删,Xcode Target,Build Settings问词,Build Configuration 和 Xcode Scheme 等構(gòu)建配置督函。它們各有什么用呢?

Xcode Project

Xcode Project用于組織源代碼文件和資源文件激挪。一個 Project 可以包含多個 Target辰狡,例如當我們新建一個 Xcode Project 的時候,它會自動生成 App 的主 Target垄分,Unit Test Target 和 UI Test Target宛篇。

例如在 Moments App 項目中,主 Target 就是 Moments薄湿,Unit Test Target 是 MomentsTests叫倍, UI Test Target 就是 MomentsUITests。

Xcode Target

Xcode Target用來定義如何構(gòu)建出一個產(chǎn)品(例如 App豺瘤, Extension 或者 Framework)吆倦,Target 可以指定需要編譯的源代碼文件和需要打包的資源文件,以及構(gòu)建過程中的步驟坐求。

例如在 Moments App 項目中蚕泽,負責單元測試的MomentsTestsTarget 就指定了 14 個測試文件需要構(gòu)建(見下圖的 Compile Sources),并且該 Target 依賴了主 App TargetMoments(見下圖的 Dependencies)瞻赶。


有了 Target 的定義赛糟,構(gòu)建系統(tǒng)就可以讀取相關(guān)的源代碼文件進行編譯,然后把相關(guān)的資源文件進行打包砸逊,并嚴格按照 Target 所指定的設(shè)置和步驟執(zhí)行璧南。那么 Target 所指定的設(shè)置哪里來的呢?來自 Build Settings师逸。

Build Settings

Build Setting保存了構(gòu)建過程中需要用到的信息司倚,它以一個變量的形式而存在,例如所支持的設(shè)備平臺篓像,或者支持操作系統(tǒng)的最低版本等动知。

通常,一條 Build Setting 信息由兩部分組成:名字和值员辩。比如下面是一條 Setting 信息盒粮,iOS Development Target是名字,而iOS 14.0是值奠滑。

有了這些基礎(chǔ)以后丹皱,接下來就結(jié)合 Moments App 來介紹如何進行多環(huán)境配置妒穴,從而生成不同環(huán)境版本的 App。

Moments App 構(gòu)建配置

一般用 Xcode 編譯出不同環(huán)境版本的 App 有多種辦法摊崭,例如拷貝復制所有源代碼讼油,建立多個 Target 來包含不同的源碼文件等等。不過呢簸,推薦使用 Build Configuration 和 Xcode Scheme 來管理多環(huán)境矮台,進而構(gòu)建出不同環(huán)境版本的 App。為什么根时?因為這兩個是目前管理成本最低的辦法瘦赫。一一介紹下。

Build Configuration

當在 Xcode 上新建一個項目的時候啸箫,Xcode 會自動生成兩個 Configuration:Debug和Release耸彪。Debug 用于日常的本地開發(fā)伞芹,Release 用于構(gòu)建和分發(fā) App忘苛。在 Moments App 項目中,有三個 configuration:Debug唱较,Internal 和 AppStore扎唾。它們分別用于構(gòu)建開發(fā)環(huán)境、測試環(huán)境和生產(chǎn)環(huán)境南缓。 其中 Internal 和 AppStore 是從自動生成的 Release 拷貝而來的胸遇。

那什么是 Build Configuration 呢?

Build Configuration就是一組 Build Setting汉形。 我們可以通過 Build Configuration 來分組和管理不同組合的 Build Setting 集合纸镊,然后傳遞給 Xcode 構(gòu)建系統(tǒng)進行編譯。

有了 Build Configuration 以后概疆,我們就能為同一個 Build Setting 設(shè)置不同的值逗威。例如Build Active Architecture Only在 Debug configuration 是Yes,而在 Internal 和 AppStore configuration 則是No岔冀。這樣就能做到同一份源代碼通過使用不同的 Build Configuration 來構(gòu)建出功能不一樣的 App 了凯旭。

那么,在構(gòu)建過程中怎樣才能選擇不同的 Build Configuration 呢使套?答案是使用 Xcode Scheme罐呼。

Xcode Scheme

Xcode Scheme用于定義一個完整的構(gòu)建過程,其包括指定哪些 Target 需要進行構(gòu)建侦高,構(gòu)建過程中使用了哪個 Build Configuration 嫉柴,以及需要執(zhí)行哪些測試案例等等。在項目新建的時候只有一個 Scheme奉呛,但可以為同一個項目建立多個 Scheme计螺。不過這么多 Scheme 中期奔,同一時刻只能有一個 Scheme 生效。

下圖是 Moments App 項目的 Scheme 危尿。 Moments App 項目有三個 Scheme 來分別代表三個環(huán)境呐萌,Moments Scheme 用于開發(fā)環(huán)境,Moments-Internal Scheme 用于測試環(huán)境谊娇,而 Moments-AppStore Scheme 用于生產(chǎn)環(huán)境肺孤。

下面是MomentsScheme 的配置。

左邊是該 Scheme 的各個操作济欢,如當前選擇了 Build 操作赠堵;右邊是對應(yīng)該操作的配置,比如 Build 對應(yīng)的 Scheme 可以構(gòu)建三個不同的 Targets法褥。不同的 Scheme 所構(gòu)建的 Target 數(shù)量可以不一樣茫叭,例如下面是Moments-InternalScheme 的配置。

該 Scheme 只構(gòu)建主 App TargetMoments半等,而不能構(gòu)建其他兩個測試 Target揍愁。

當選擇 Run、Test杀饵、Profile莽囤、 Analyze 和 Archive 等操作時,在右欄有一個很關(guān)鍵的配置是叫作 Build Configuration切距,可以通過下拉框來選擇 Moments App 項目里面三個 Configuration (Debug朽缎,Internal 和 AppStore) 中的其中一個。

為了方便管理谜悟,通常的做法是话肖,一個 Scheme 對應(yīng)一個 Configuration。有了這三個 Scheme 以后葡幸,我們就可以很方便地構(gòu)建出 Moments α(開發(fā)環(huán)境)最筒,Moments β(測試環(huán)境)和 Moments(生產(chǎn)環(huán)境)三個功能差異的 App。

這三個 App 的名字都不一樣礼患,怎么做到的呢是钥?實際上是為不同的 Configuration 設(shè)置了不一樣的 Build Setting。其中決定 App 名字的 Build Setting 叫作PRODUCT_BUNDLE_NAME缅叠,然后在 Info.plist 文件里面為 Bundle name 賦值悄泥,就能構(gòu)建出名字不一樣的 App。

QQ20210608-104945@2x.png

為了構(gòu)建出不同環(huán)境版本的 App肤粱,需要經(jīng)常為各個 Build Configuration 下的 Build Setting 設(shè)置不一樣的值弹囚。 在這其中,使用好 xcconfig 配置文件就顯得非常重要领曼。

xcconfig 配置文件

xcconfig 會起到什么作用呢鸥鹉?

一般修改 Build Setting 的辦法是在 Xcode 的 Build Settings 界面上進行蛮穿。 例如下面的例子中修改 Suppress Warnings。

這樣做有一些不好的地方毁渗,首先是手工修改很容易出錯践磅,例如有時候很難看出來修改的 Setting 到底是 Project 級別的還是 Target 級別的。其次灸异,最關(guān)鍵的是每次修改完畢以后都會修改了 xcodeproj 項目文檔 (如下圖所示)府适,導致 Git 歷史很難查看和對比。

所以Xcode 提供了一個統(tǒng)一管理這些 Build Setting 的便利方法肺樟,那就是使用 xcconfig 配置文件來管理檐春。

xcconfig 概念及其作用

xcconfig也叫作 Build configuration file(構(gòu)建配置文件),可以使用它來為 Project 或 Target 定義一組 Build Setting么伯。由于它是一個純文本文件疟暖,可以使用 Xcode 以外的其他文本編輯器來修改,而且可以保存到 Git 進行統(tǒng)一管理。 這樣遠比在 Xcode 的 Build Settings 界面上手工修改要方便很多,而且還不容易出錯。

在 xcconfig 文件里面的每一條 Setting 都是下面的格式:

BUILD_SETTING_NAME = value

其中,BUILD_SETTING_NAME表示 Build Setting 的名字瞄勾,而value是該 Setting 的值。下面是一個例子虑瀑。

SWIFT_VERSION = 5.0

SWIFT_VERSION是用于定義 Swift 語言版本的 Build Setting馆纳,其值是5.0。Setting 的名字都是由大寫字母糠亩,數(shù)值和下劃線組成虐骑。這種命名法我們一般成為蛇型命名法,例如SNAKE_CASE_NAME赎线。

如何獲取Build Setting 的鍵值呢廷没,在build setting選項上command+c就可以直接復制獲取,delete的話會恢復xcode初始默認值垂寥。

當使用 xcconfig 時颠黎,Xcode 構(gòu)建系統(tǒng)會按照下面的優(yōu)先級來計算出 Build Setting 的最后生效值:

  • Platform Defaults (平臺默認值)
  • Xcode Project xcconfig File(Project 級別的 xcconfig 文件)
  • Xcode Project File Build Settings(Project 級別的手工配置的 Build Setting)
  • Target xcconfig File (Target 級別的 xcconfig 文件)
  • Target Build Settings(Target 級別的手工配置的 Build Setting)

Xcode 構(gòu)建系統(tǒng)會按照上述列表從上而下讀取 Build Setting,如果發(fā)現(xiàn)同樣的 Setting 滞项,就會把下面的 Setting 覆蓋掉上面的狭归,越往下優(yōu)先級別越高。

例如在 Project 級別的 xcconfig 文件配置了SWIFT_VERSION = 5.0而在Target 級別的 xcconfig 文件配置了SWIFT_VERSION = 5.1文判,那么Target 級別的 Build Setting 會覆蓋 Project 級別的SWIFT_VERSION設(shè)置过椎,最終SWIFT_VERSION生效的值是5.1。

那么戏仓,要怎樣做才能做到不覆蓋原有的 Build Setting 呢疚宇?可以使用下面例子中的$(inherited)來實現(xiàn)亡鼠。

BUILD_SETTING_NAME = $(inherited) additional value

可以保留原先的 Setting,然后把新的值添加到后面去敷待。比如:

FRAMEWORK_SEARCH_PATHS = $(inherited) ./Moments/Pods

其中的FRAMEWORK_SEARCH_PATHS會保留原有的值间涵,然后加上./Moments/Pods作為新值。
在配置 Build Setting 時榜揖,還可以引用其他已定義的 Build Setting浑厚。

例如下面的例子中,F(xiàn)RAMEWORK_SEARCH_PATHS使用了另外一個 Build Setting PROJECT_DIR根盒。

FRAMEWORK_SEARCH_PATHS = $(inherited) $(PROJECT_DIR)

為了重用钳幅,可以通過#include引入其他 xcconfig 文件。

#include "path/to/OtherFile.xcconfig"

Moments App xcconfig 配置文件

下面是 Moments App 項目如何管理 xcconfig 配置文件炎滞。

把所有 xcconfig 文件分成三大類:Shared敢艰、 Project 和 Targets。

其中 Shared 文件夾用于保存分享到整個 App 的 Build Setting册赛,例如 Swift 的版本號钠导、App 所支持的 iOS 版本號等各種共享的基礎(chǔ)信息。 下面是 SDKAndDeviceSupport.xcconfig 文件里面所包含的信息:

TARGETED_DEVICE_FAMILY = 1
IPHONEOS_DEPLOYMENT_TARGET = 14.0

TARGETED_DEVICE_FAMILY表示支持的設(shè)備森瘪,1表示 iPhone牡属。而IPHONEOS_DEPLOYMENT_TARGET表示支持 iOS 的最低版本,我們的 Moments App 所支持的最低版本是 iOS 14.0扼睬。

Project 文件夾用于保存 Xcode Project 級別的 Build Setting逮栅,其中 BaseProject.xcconfig 會引入 Shared 文件夾下所有的 xcconfig 配置文件,如下所示:

#include "CompilerAndLanguage.xcconfig"
#include "SDKAndDeviceSupport.xcconfig"
#include "BaseConfigurations.xcconfig"

然后根據(jù)三個不同的環(huán)境分別建了三個xcconfig 配置文件窗宇,如下:

  • DebugProject.xcconfig 文件
#include "BaseProject.xcconfig"
SWIFT_ACTIVE_COMPILATION_CONDITIONS = $(inherited) DEBUG
  • InternalProject.xcconfig 文件
#include "BaseProject.xcconfig"
SWIFT_ACTIVE_COMPILATION_CONDITIONS = $(inherited) INTERNAL
  • AppStoreProject.xcconfig 文件
#include "BaseProject.xcconfig"
SWIFT_ACTIVE_COMPILATION_CONDITIONS = $(inherited) PRODUCTION

它們的共同點是都引入了用于共享的 BaseProject.xcconfig 文件措伐,然后分別定義了 Swift 編譯條件配置SWIFT_ACTIVE_COMPILATION_CONDITIONS。其中$(inherited)表示繼承原有的配置军俊,$(inherited)后面的DEBUG或者INTERNAL表示在原有配置的基礎(chǔ)上后面添加了一個新條件侥加。有了這些編譯條件,就可以在代碼中這樣使用

#if DEBUG
    print("Debug Environment")
#endif

該段代碼只在開發(fā)環(huán)境執(zhí)行粪躬,因為只有開發(fā)環(huán)境的SWIFT_ACTIVE_COMPILATION_CONDITIONS才有DEBUG的定義担败。這樣做能有效分離各個環(huán)境,保證同一份代碼構(gòu)建出對應(yīng)不同環(huán)境的 App镰官。

Targets 文件夾用于保存 Xcode Target 級別的 Build Setting提前,也是由一個 BaseTarget.xcconfig 文件來共享所有 Target 都需要使用的信息。

PRODUCT_BUNDLE_NAME = Moments

這里的PRODUCT_BUNDLE_NAME是 App 的名字朋魔。
下面是三個不同環(huán)境的 Target xcconfig 文件岖研。

  • DebugTarget.xcconfig
#include "../Pods/Target Support Files/Pods-Moments/Pods-Moments.debug.xcconfig"
#include "BaseTarget.xcconfig"
PRODUCT_BUNDLE_NAME = $(inherited) α
PRODUCT_BUNDLE_IDENTIFIER = com.ibanimatable.moments.development
  • InternalTarget.xcconfig
#include "../Pods/Target Support Files/Pods-Moments/Pods-Moments.internal.xcconfig"
#include "BaseTarget.xcconfig"
PRODUCT_BUNDLE_NAME = $(inherited) β
PRODUCT_BUNDLE_IDENTIFIER = com.ibanimatable.moments.internal
  • AppStoreTarget.xcconfig
#include "../Pods/Target Support Files/Pods-Moments/Pods-Moments.appstore.xcconfig"
#include "BaseTarget.xcconfig"
PRODUCT_BUNDLE_NAME = $(inherited)
PRODUCT_BUNDLE_IDENTIFIER = com.ibanimatable.moments

它們都需要引入 CocoaPods 所生成的 xcconfig 和共享的 BaseTarget.xcconfig 文件,然后根據(jù)需要改寫 App 的名字。例如DebugTarget 覆蓋了PRODUCT_BUNDLE_NAME的值為Moments α*, 其所構(gòu)建的 App 叫作Moments α孙援。

一般在 App Store 上所有 App 的標識符都必須是唯一的害淤。如果你的項目通過 Configuration 和 Scheme 來生成免費版和收費版的 App,那么拓售,你必須在兩個 Configuration 中分別為PRODUCT_BUNDLE_IDENTIFIER配置對應(yīng)的標識符窥摄,例如com.company.free和com.company.paid。

在 Moments App 中础淤,也為各個環(huán)境下的 App 使用了不同的標識符崭放,以方便通過 CI 自動構(gòu)建,并分發(fā)到內(nèi)部測試組或者 App Store鸽凶。同時币砂,這也能為各個環(huán)境版本的 App 分離用戶行為數(shù)據(jù),方便統(tǒng)計分析玻侥。

一旦有了這些 xcconfig 配置文件决摧,今后就可以在 Xcode 的 Project Info 頁面里的 Configurations 上引用它們。

下面是所有 Configurations 所引用的 xcconfig 文件

在配置好所有 xcconfig 文件的引用以后凑兰,可以在 Build Settings 頁面查看某個 Build Setting 的生效值掌桩。我們以IPHONEOS_DEPLOYMENT_TARGET為例,一起看看姑食。

當選擇All和Levels時波岛,可以看到所有配置信息分成了不同的列。這些列分別代表前面的 Build Settng 優(yōu)先級:

  • 平臺默認值
  • Project 級別的 xcconfig 文件
  • Xcode 項目文件中的 Project 級別配置
  • Target 級別的 xcconfig 文件
  • Xcode 項目文件中的 Target 級別配置

Build Settng 的優(yōu)先級是從左到右排序的音半。越是左邊優(yōu)先級就越高则拷。例如,在 Project 級別的 xcconfig 文件里面定義了IPHONEOS_DEPLOYMENT_TARGET的值為14.0祟剔,那么Project 級別的 xcconfig 文件(Project Config File) 一列上就會顯示iOS 14.0隔躲,它覆蓋了系統(tǒng)的默認值 (iOS Default)iOS 14.2。這就是因為 Project 級別的 xcconfig 文件物延,它的優(yōu)先級高于系統(tǒng)默認值,因此最后生效的值是iOS 14.0仅父。

總結(jié)

在使用 xcconfig 配置時叛薯,需要注意以下兩點:

  • 首先,我們必須把所有 Build Setting 都配置在 xcconfig 文件里面笙纤,并通過 Git 進行統(tǒng)一管理耗溜;

  • 其次,我們千萬不要在 Xcode 的 Build Settings 頁面修改任何 Setting省容,否則該配置會覆蓋 xcconfig 文件里面的配置抖拴。如果你不小心修改了,可以通過點擊delete鍵把頁面設(shè)置的配置刪掉。

  • 最后阿宅,如何獲取Build Setting 的鍵值呢候衍,在build setting選項上command+c就可以直接復制獲取,delete的話 會恢復xcode初始默認值洒放。

  • 最后蛉鹿,如何獲取Build Setting 的鍵值呢,在build setting選項上command+c就可以直接復制獲取往湿,delete的話會恢復xcode初始默認值妖异。

  • 最后,如何獲取Build Setting 的鍵值呢领追,在build setting選項上command+c就可以直接復制獲取他膳,delete的話會恢復xcode初始默認值。

  • 重要的事情說三遍??

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末绒窑,一起剝皮案震驚了整個濱河市棕孙,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌回论,老刑警劉巖散罕,帶你破解...
    沈念sama閱讀 221,198評論 6 514
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異傀蓉,居然都是意外死亡欧漱,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,334評論 3 398
  • 文/潘曉璐 我一進店門葬燎,熙熙樓的掌柜王于貴愁眉苦臉地迎上來误甚,“玉大人,你說我怎么就攤上這事谱净∫ぐ睿” “怎么了?”我有些...
    開封第一講書人閱讀 167,643評論 0 360
  • 文/不壞的土叔 我叫張陵壕探,是天一觀的道長冈钦。 經(jīng)常有香客問我,道長李请,這世上最難降的妖魔是什么瞧筛? 我笑而不...
    開封第一講書人閱讀 59,495評論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮导盅,結(jié)果婚禮上较幌,老公的妹妹穿的比我還像新娘。我一直安慰自己白翻,他們只是感情好乍炉,可當我...
    茶點故事閱讀 68,502評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般岛琼。 火紅的嫁衣襯著肌膚如雪底循。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,156評論 1 308
  • 那天衷恭,我揣著相機與錄音此叠,去河邊找鬼。 笑死随珠,一個胖子當著我的面吹牛灭袁,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播窗看,決...
    沈念sama閱讀 40,743評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼茸歧,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了显沈?” 一聲冷哼從身側(cè)響起软瞎,我...
    開封第一講書人閱讀 39,659評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎拉讯,沒想到半個月后涤浇,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,200評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡魔慷,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,282評論 3 340
  • 正文 我和宋清朗相戀三年只锭,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片院尔。...
    茶點故事閱讀 40,424評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡蜻展,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出邀摆,到底是詐尸還是另有隱情纵顾,我是刑警寧澤,帶...
    沈念sama閱讀 36,107評論 5 349
  • 正文 年R本政府宣布栋盹,位于F島的核電站施逾,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏例获。R本人自食惡果不足惜音念,卻給世界環(huán)境...
    茶點故事閱讀 41,789評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望躏敢。 院中可真熱鬧,春花似錦整葡、人聲如沸件余。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,264評論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽啼器。三九已至旬渠,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間端壳,已是汗流浹背告丢。 一陣腳步聲響...
    開封第一講書人閱讀 33,390評論 1 271
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留损谦,地道東北人岖免。 一個月前我還...
    沈念sama閱讀 48,798評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像照捡,于是被迫代替她去往敵國和親颅湘。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,435評論 2 359