TypeScript 進(jìn)階:類型安全的依賴注入

原文拣宰。

本文敘述了如何使用 TypeScript 從頭創(chuàng)建一個(gè) 100% 類型安全的依賴注入框架掏呼。

在我作為專業(yè) TypeScript 講師的日子里筷黔,開(kāi)發(fā)者們經(jīng)常問(wèn)我:“為什么我們需要這么復(fù)雜的高級(jí)類型系統(tǒng)姐仅?”他們?cè)趯?shí)際項(xiàng)目中并沒(méi)有感受到對(duì)常量類型绰播、交叉類型骄噪、條件類型元組式的剩余參數(shù)的需求。這是一個(gè)很好的問(wèn)題蠢箩,如果沒(méi)有一個(gè)合適的場(chǎng)景链蕊,是很難回答的。

這就促使我去尋找一個(gè)合適的場(chǎng)景谬泌。幸運(yùn)的是滔韵,我確實(shí)找到了一個(gè)場(chǎng)景:依賴注入,或者簡(jiǎn)稱為 DI掌实。

本文奏属,我將帶著你一起探索。首先我會(huì)解釋類型安全的依賴注入是什么意思潮峦。接下來(lái)我會(huì)展示最終代碼形態(tài)囱皿,這樣你就知道具體要達(dá)到什么目標(biāo)了。然后忱嘹,我們逐一解決靜態(tài)類型的依賴注入框架所遇到的挑戰(zhàn)嘱腥。

閱讀本文的前提是你已經(jīng)具備了 TypeScript 基礎(chǔ)知識(shí)。

目標(biāo)

我的目標(biāo)是在 TypeScript 中創(chuàng)建 100% 類型安全的依賴注入(DI)框架拘悦。如果你還不知道 DI齿兔,建議先閱讀 samueleresca 寫(xiě)的這篇文章,文章介紹了什么是 DI础米,以及為什么要使用 DI分苇。同時(shí)文章中也介紹了 InversifyJS,它是目前最流行的 TypeScript DI 框架屁桑,借助 TypeScript 的裝飾器reflect-metadata在運(yùn)行時(shí)解析依賴医寿。

InversifyJS 確實(shí)實(shí)現(xiàn)了依賴注入……但是,卻不是類型安全的蘑斧。以下面代碼為例:

@injectable()
class Foo {
    constructor(@inject('bar') bar: string) {
        console.log(bar.substr(2));
    }
}

const context = new Context();
context.bind('bar').toConstantValue(42);
context.bind(Foo).toSelf();
context.get(Foo); // Error: bar.substr is not a function

在上述示例中靖秩,可以看到 bar 被聲明為 string 類型须眷,但是在運(yùn)行時(shí)它卻是一個(gè) number 類型。實(shí)際上沟突,在 DI 配置中很容易犯類似這樣的錯(cuò)誤花颗。由于 DI 的緣故而失去類型安全性,這太糟糕了惠拭。

我的目標(biāo)就是調(diào)研“是否能讓編譯器知道依賴及其類型”扩劝。如果你的代碼有編譯過(guò)程,那么這會(huì)很有用:字符串就是字符串职辅,數(shù)字就是數(shù)字棒呛,F(xiàn)oo 就是 Foo,不會(huì)出現(xiàn)任何其它可能性罐农。

最終結(jié)果

如果你對(duì)最終結(jié)果感興趣条霜,那么我可以告訴你:我成功了!你可以看看 GitHub 上的這個(gè)項(xiàng)目涵亏。下面是從 README 中提取出來(lái)的一段最簡(jiǎn)化代碼:

import { rootInjector, tokens } from 'typed-inject';

class Logger {
    info(message: string) {
        console.log(message);
    }
}

class HttpClient {
    constructor(private log: Logger) { }
    public static inject = tokens('logger');
}

class MyService {
    constructor(private http: HttpClient, private log: Logger) { }
    public static inject = tokens('httpClient', 'logger');
}

const appInjector = rootInjector
  .provideValue('logger', new Logger())
  .provideClass('httpClient', HttpClient);

const myService = appInjector.injectClass(MyService);
// Dependencies for MyService validated and injected

在類的 inject 靜態(tài)屬性中聲明依賴宰睡。可以使用 InjectorinjectClass 方法實(shí)例化一個(gè)類气筋,任何構(gòu)造器參數(shù)或者 inject 屬性中的錯(cuò)誤都會(huì)引起編譯錯(cuò)誤拆内。

很好奇原理吧?這就對(duì)了宠默。

挑戰(zhàn)

為了讓編譯器給出編譯錯(cuò)誤麸恍,有三個(gè)挑戰(zhàn):

  1. 如何靜態(tài)聲明依賴?
  2. 在構(gòu)造函數(shù)的參數(shù)中搀矫,怎么關(guān)聯(lián)上依賴的類型抹沪?
  3. 如何實(shí)現(xiàn)一個(gè) Injector,用于根據(jù)類型生成實(shí)例瓤球?

我們逐一解決上述挑戰(zhàn)融欧。

挑戰(zhàn)1:聲明依賴

我們從靜態(tài)聲明依賴開(kāi)始。InversifyJS 使用裝飾器卦羡,比如:@inject('bar') 用于尋找一個(gè)叫做 bar 的依賴并將其注入噪馏,由于裝飾器動(dòng)態(tài)運(yùn)行方式(裝飾器僅僅是一個(gè)運(yùn)行時(shí)執(zhí)行的函數(shù)),沒(méi)辦法在編譯階段確定 bar 依賴存在绿饵。

所以我們不能使用裝飾器欠肾,我們找找其他方式來(lái)聲明依賴。

在 Angular 仍叫 AngularJS 的時(shí)代拟赊,我們?cè)陬悾ó?dāng)時(shí)我們稱之為構(gòu)造函數(shù))上面的 $inject 靜態(tài)屬性上聲明依賴刺桃。在 $inject 屬性上的值,我們稱之為“tokens”要门,$inject 數(shù)組中聲明的 tokens 順序與構(gòu)造函數(shù)中參數(shù)的順序保持一一對(duì)應(yīng)關(guān)系虏肾。我們用 MyService 舉個(gè)相似的例子:

class MyService {
    constructor(private http: HttpClient, private log: Logger) { }
    public static inject = ['httpClient', 'logger'];
}

這是一個(gè)好的開(kāi)始廓啊,但是我們還沒(méi)達(dá)到目標(biāo)欢搜。通過(guò)字符串?dāng)?shù)組的方式初始化 inject 屬性封豪,編譯器只會(huì)將其解析為普通的字符串?dāng)?shù)組類型,編譯器沒(méi)辦法將 bar token 與 Bar 類型關(guān)聯(lián)起來(lái)炒瘟。

介紹:字面量類型

當(dāng)寫(xiě)錯(cuò)代碼的時(shí)候吹埠,我們期望編譯器會(huì)報(bào)錯(cuò)。為了在編譯時(shí)能知道 token 數(shù)組的值疮装,我們需要將其類型聲明為字符串字面量

class MyService {
    constructor(private http: HttpClient, private log: Logger) { }
    public static inject: ['httpClient', 'logger'] = ['httpClient', 'logger'];
}

我們告訴了 TypeScript 數(shù)組的類型是一個(gè)值為 ['httpClient', 'logger'] 的 元組缘琅,現(xiàn)在我們有了一絲進(jìn)展。但是廓推,我們是懶惰的開(kāi)發(fā)者刷袍,我們不想寫(xiě)重復(fù)的代碼。讓我們使其更加符合 DRY 原則樊展。

介紹:結(jié)合元組類型和剩余參數(shù)

我們可以創(chuàng)建一個(gè)簡(jiǎn)單的輔助方法呻纹,它接收任意數(shù)量的字面量字符串參數(shù),返回相應(yīng)的字面量元組值专缠,看起來(lái)大致這樣:

function tokens<Tokens extends string[]>(...theTokens: Tokens): Tokens {
    return theTokens;
}

如上所示雷酪,theTokens 參數(shù)聲明為剩余參數(shù),它能匹配到函數(shù)的所有參數(shù)涝婉,同時(shí)類型被定義為 Tokens哥力,繼承自 string[],因此能匹配到任何字符串類型墩弯。返回值是 theTokens吩跋,其類型是字面量字符串元組。這樣一來(lái)渔工,我們就能避免之前例子中的重復(fù)編碼:

class MyService {
    constructor(private http: HttpClient, private log: Logger) { }
    public static inject = tokens('httpClient', 'logger');
}

如上所示锌钮,只需要列舉 tokens 一次就行,inject 的類型就會(huì)是 ['httpClient'涨缚,'logger']轧粟。變得更棒了,你覺(jué)得呢脓魏?

TypeScript 中有望引入顯式的元組語(yǔ)法兰吟,因此以后我們不再需要額外的 tokens 輔助函數(shù)。

挑戰(zhàn)2:關(guān)聯(lián)依賴

說(shuō)到了有趣的部分:確泵瑁可注入類的構(gòu)造函數(shù)的參數(shù)與聲明的 tokens 相匹配混蔼。

首先,我們聲明 MyService 類(或者任何可注入的類)的靜態(tài)接口:

interface Injectable {
    new(...args: any): any;
    inject: string[];
}

Injectable 接口描述了一種類:有一個(gè)接收任意數(shù)量參數(shù)的構(gòu)造函數(shù)珊燎;有一個(gè)靜態(tài) inject 數(shù)組屬性惭嚣,包含了注入 tokens遵湖,類型為 string[]。這僅僅是個(gè)開(kāi)始晚吞,實(shí)際上用處不大延旧,不能夠?qū)?tokens 值與構(gòu)造函數(shù)參數(shù)的類型關(guān)聯(lián)起來(lái)。

介紹:查詢類型

因此槽地,我們需要告訴 TypeScript 編譯器迁沫,哪個(gè) token 對(duì)應(yīng)哪種類型。幸運(yùn)的是捌蚊,TypeScript 支持查詢類型:它是一種不必直接作為類型使用的簡(jiǎn)單 interface集畅,我們將其用作查詢類型的字典。聲明一個(gè) Context 查詢類型缅糟,其值可用于注入:

interface Context {
    httpClient: HttpClient;
    logger: Logger;
}

任何時(shí)候你想聲明一個(gè) Logger 實(shí)例挺智,都可以使用 Context 查詢類型,例如 let log: Context['logger']窗宦。有了這個(gè)接口赦颇,我們可以指定 MyService 類的 inject 屬性必須是 Context 的鍵:

interface Injectable {
    new(...arg: (Context[keyof Context])[]): any;
    inject: (keyof Context)[];
}

這更加接近目標(biāo)了。我們收窄了 inject 的有效值到一個(gè) keyof Context 數(shù)組迫摔,因此只能使用 'logger' 或者 'httpClient' 作為 token沐扳。構(gòu)造函數(shù)中的每一個(gè)參數(shù)的類型都是 Context[keyof Context],因此要么是 Logger句占,要么是 HttpClient沪摄。

但是,并沒(méi)有達(dá)到目的纱烘。我們?nèi)匀恍枰_關(guān)聯(lián)值杨拐,這就要用到泛型了。

介紹:泛型

展示一個(gè)泛型魔法:

interface Injectable<Token extends keyof Context, R> {
    new(arg: Context[Token]): R;
    inject: [Token];
}

現(xiàn)在我們有了新的進(jìn)展擂啥!我們聲明了一個(gè)泛型變量 Token哄陶,限定了取值只能是 Context 中的鍵。我們也在構(gòu)造函數(shù)中用 Context[Token] 關(guān)聯(lián)了確定的類型哺壶。同時(shí)屋吨,我們也添加了一個(gè)類型參數(shù) R,指代 Injectable(例如 MyService 實(shí)例)實(shí)例類型山宾。

仍然存在一個(gè)問(wèn)題至扰,如果我們想讓構(gòu)造函數(shù)支持更多的參數(shù),我們就需要為每一種參數(shù)數(shù)量聲明一個(gè)類型:

interface Injectable2<Token extends keyof Context, Token2 extends keyof Context, R> {
    new(arg: Context[Token], arg2: Context[Token2]): R;
    inject: [Token, Token2];
}

這是不可持續(xù)的资锰。理想情況下敢课,對(duì)于不同數(shù)量的構(gòu)造函數(shù)參數(shù),我們只需要定義一種類型就行了。

我們已經(jīng)知道了如何實(shí)現(xiàn)直秆!直接使用元組類型的剩余參數(shù):

interface Injectable<Tokens extends (keyof Context)[], R> {
    new(...args: CorrespondingTypes<Tokens>): R;
    inject: Tokens;
}

我們先仔細(xì)看一下 Tokens濒募。通過(guò)將 Tokens 聲明為 keyof Context 數(shù)組,我們能夠靜態(tài)地將 inject 屬性定義為一種元組類型圾结,TypeScript 編譯器會(huì)保持跟蹤每一個(gè) token瑰剃。舉個(gè)例子,對(duì)于 inject = tokens('httpClient', 'logger')疫稿,Tokens 類型會(huì)被解析為 ['httpClient', 'logger']培他。

構(gòu)造函數(shù)的剩余參數(shù)使用 CorrespondingTypes<Tokens> 映射類型鹃两,在下面一節(jié)中我們?cè)敿?xì)介紹這塊遗座。

介紹:條件映射元組類型

CorrespondingTypes 被實(shí)現(xiàn)為條件映射類型,代碼實(shí)現(xiàn)如下:

type CorrespondingTypes<Tokens extends (keyof Context)[]> = {
    [I in keyof Tokens]: Tokens[I] extends keyof Context ? Context[Tokens[I]] : never;
}

上述代碼“一言難盡”俊扳,我們逐層分析途蒋。

首先,我們需要知道 CorrespondingTypes映射類型:新類型的屬性名與源類型一致馋记,但是是一種不同的類型号坡。在上面代碼中,我們映射了 Tokens 的屬性梯醒。Tokens 是一個(gè)泛型元組類型(extends (keyof Context)[])宽堆。

但是,元組類型的屬性名是什么呢茸习?好吧畜隶,你可以認(rèn)為就是它的索引。因此号胚,對(duì)于 ['foo', 'bar']籽慢,屬性名就是 01。實(shí)際上猫胁,對(duì)于元組類型和映射類型的搭配支持箱亿,已經(jīng)在最近單獨(dú)的 PR 中支持了。一個(gè)超棒的特性弃秆。

現(xiàn)在届惋,看下關(guān)聯(lián)屬性值,我們使用了類型判斷:Tokens[I] extends keyof Context? Context[Tokens[I]] : never菠赚。因此脑豹,如果 token 是 Context 的一個(gè)鍵,就會(huì)返回對(duì)應(yīng)鍵的類型锈至;否則晨缴,返回 nerver 類型,意思就是告知 TypeScript 不會(huì)出現(xiàn)這種情況峡捡。

挑戰(zhàn)3:注入

既然我們有了 Injectable 接口击碗,是時(shí)候用起來(lái)了筑悴。先創(chuàng)建核心類:Injector

class Injector {
    injectClass<Tokens extends (keyof Context)[]>(Injectable: Injectable<Tokens, R>): R {
        const args = /* resolve inject tokens */;
        return new Injectable(...args);
    }
}

Injector 類有一個(gè) injectClass 方法稍途,接收一個(gè) Injectable 類作為參數(shù)阁吝,創(chuàng)建并返回需要的實(shí)例。該方法的具體實(shí)現(xiàn)已經(jīng)超出了本文的范疇械拍,但是你可以思考一下:通過(guò)迭代 inject 屬性配置的 tokens 來(lái)查詢需要注入的值突勇。

動(dòng)態(tài)上下文

到目前為止,我們靜態(tài)聲明了 Context 類型坷虑,它是一個(gè)查詢類型甲馋,用于關(guān)聯(lián) token 和其它類型。如果你在項(xiàng)目中需要這樣寫(xiě)迄损,會(huì)不怎么光彩黄锤。因?yàn)檫@意味著整個(gè) DI 上下文需要一次性初始化妈经,后續(xù)再也不能配置听哭,一點(diǎn)都不實(shí)用肩榕。

為了使 Context 動(dòng)態(tài)化,我們將其作為另外一個(gè)泛型傳入(我保證這會(huì)是最后一個(gè)泛型)氏捞。新的類型聲明如下:

interface Injectable<TContext, Tokens extends (keyof TContext)[], R> {
    new(...args: CorrespondingTypes<TContext, Tokens>): R;
    inject: Tokens;
}
type CorrespondingTypes<TContext, Tokens extends (keyof TContext)[]> = {
    [Token in keyof Tokens]: Tokens[Token] extends keyof TContext ? TContext[Tokens[Token]] : never;
}
class Injector<TContext> {
    inject<Tokens extends (keyof TContext)[]>(injectable: Injectable<TContext, Tokens, R>): R {
        /* out of scope */
    }
}

好了碧聪,所有的內(nèi)容看起來(lái)都還是比較熟悉的。我們引入了 TContext液茎,用于表示 DI 上下文的查詢接口逞姿。

現(xiàn)在,還剩最后一個(gè)問(wèn)題豁护,我們想要通過(guò)動(dòng)態(tài)添加值的方式來(lái)配置 Injector哼凯。看下這塊的示例代碼:

const appInjector = rootInjector
  .provideValue('logger', logger)
  .provideClass('httpClient', HttpClient);

如上所示楚里,InjectorprovideXXX 方法断部,每個(gè) provide 方法都會(huì)向 TContext 泛型中添加鍵,我們需要另外一個(gè) TypeScript 特性來(lái)實(shí)現(xiàn)這個(gè)效果班缎。

介紹:交叉類型

在 TypeScript 中蝴光,可以很輕松地用 & 組合兩種類型,因此 Foo & Bar 是一種同時(shí)擁有 FooBar 屬性的類型达址,這種類型被稱為交叉類型蔑祟。這有點(diǎn)像 C++ 的多重繼承或者 Scala 中的 traits。我們將 TContext 與使用字符串字面量 token 的映射類型關(guān)聯(lián)起來(lái):

class Injector<TContext> {
  provideValue<Token extends string, R>(token: Token, value: R)
  : Injector<{ [K in Token]: R } & TContext> {
      /* out of scope */
  }
}

如上所示沉唠,provideValue 有兩個(gè)泛型參數(shù):一個(gè)是 token 常量類型(Token)疆虚,一個(gè)是注入的值的類型(R)。該方法返回了一個(gè)新的 Injector 實(shí)例,其上下文為 { [K in Token]: R } & TContext径簿。也就是說(shuō)罢屈,可以注入任何當(dāng)前注入器支持的值,也可以是新提供的 token篇亭。

你可能想知道為什么新的 TContext 要和 { [k in Token]: R } 做交叉而不是簡(jiǎn)單地用 { [Token]: R }缠捌。這是因?yàn)?Token 本身可以表示一個(gè)字符串字面量聯(lián)合類型,舉個(gè)例子译蒂,'foo'| 'bar'曼月。雖然從 TypeScript 角度來(lái)看沒(méi)什么問(wèn)題,但是如果在調(diào)用 provideValue 的時(shí)候顯示地傳入一個(gè)聯(lián)合類型(provideValue<'foo' | 'bar', _>('foo', 42))將會(huì)破壞類型安全柔昼,它會(huì)在編譯時(shí)同時(shí)注冊(cè) 'foo''bar' 作為 token哑芹,并關(guān)聯(lián)同一個(gè)數(shù)字,但是在運(yùn)行時(shí)僅僅注冊(cè)了 'foo'岳锁。所以绩衷,在實(shí)際項(xiàng)目中不要這么做。

其它 provideXXX 方法也是類似的道理激率,它們返回新的 Injector 實(shí)例,提供新的 token勿决,同時(shí)合并進(jìn)了所有舊的 token乒躺。

結(jié)論

TypeScript 的類型系統(tǒng)很強(qiáng)大,在本文中我們結(jié)合了:

  • 字面量類型
  • 元組類型的剩余參數(shù)
  • 查詢類型
  • 泛型
  • 條件映射元組類型
  • 交叉類型

來(lái)創(chuàng)建類型安全的依賴注入框架低缩。

雖然嘉冒,你不會(huì)總是遇到這些特性,但是對(duì)這些特性保持關(guān)注是值得的咆繁,畢竟它們?yōu)楦玫鼐幋a提供了可能性讳推。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市玩般,隨后出現(xiàn)的幾起案子银觅,更是在濱河造成了極大的恐慌,老刑警劉巖坏为,帶你破解...
    沈念sama閱讀 216,372評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件究驴,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡匀伏,警方通過(guò)查閱死者的電腦和手機(jī)洒忧,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)够颠,“玉大人熙侍,你說(shuō)我怎么就攤上這事。” “怎么了蛉抓?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,415評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵牢硅,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我芝雪,道長(zhǎng)减余,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,157評(píng)論 1 292
  • 正文 為了忘掉前任惩系,我火速辦了婚禮位岔,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘堡牡。我一直安慰自己抒抬,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布晤柄。 她就那樣靜靜地躺著擦剑,像睡著了一般。 火紅的嫁衣襯著肌膚如雪芥颈。 梳的紋絲不亂的頭發(fā)上惠勒,一...
    開(kāi)封第一講書(shū)人閱讀 51,125評(píng)論 1 297
  • 那天,我揣著相機(jī)與錄音爬坑,去河邊找鬼纠屋。 笑死,一個(gè)胖子當(dāng)著我的面吹牛盾计,可吹牛的內(nèi)容都是我干的售担。 我是一名探鬼主播,決...
    沈念sama閱讀 40,028評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼署辉,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼族铆!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起哭尝,我...
    開(kāi)封第一講書(shū)人閱讀 38,887評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤哥攘,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后刚夺,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體献丑,經(jīng)...
    沈念sama閱讀 45,310評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評(píng)論 2 332
  • 正文 我和宋清朗相戀三年侠姑,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了创橄。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,690評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡莽红,死狀恐怖妥畏,靈堂內(nèi)的尸體忽然破棺而出邦邦,到底是詐尸還是另有隱情,我是刑警寧澤醉蚁,帶...
    沈念sama閱讀 35,411評(píng)論 5 343
  • 正文 年R本政府宣布燃辖,位于F島的核電站,受9級(jí)特大地震影響网棍,放射性物質(zhì)發(fā)生泄漏黔龟。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評(píng)論 3 325
  • 文/蒙蒙 一滥玷、第九天 我趴在偏房一處隱蔽的房頂上張望氏身。 院中可真熱鬧,春花似錦惑畴、人聲如沸蛋欣。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)陷虎。三九已至,卻和暖如春杠袱,著一層夾襖步出監(jiān)牢的瞬間尚猿,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,812評(píng)論 1 268
  • 我被黑心中介騙來(lái)泰國(guó)打工霞掺, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留谊路,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,693評(píng)論 2 368
  • 正文 我出身青樓菩彬,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親潮梯。 傳聞我的和親對(duì)象是個(gè)殘疾皇子骗灶,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評(píng)論 2 353

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