WebViewJavaScriptBridge深入剖析

原文作者:CoderSpr1ngHall
原文地址:https://juejin.im/post/5cecd746e51d45778f076cac

前言

前一篇文章中,我們大致的講述了一下JavaScriptCore這個(gè)庫(kù)在iOS開(kāi)發(fā)中的應(yīng)用夯接。在文中最后的階段焕济,我們提到了WebViewJavaScriptBridge這個(gè)庫(kù)。提到這個(gè)庫(kù)盔几,可能有一些人就要說(shuō)了晴弃,現(xiàn)在都什么時(shí)代了,誰(shuí)還會(huì)用這個(gè)庫(kù)把放摹上鞠?全是坑!不錯(cuò)芯丧,早在三年前芍阎,這個(gè)庫(kù)有過(guò)一段輝煌的時(shí)光,在蘋(píng)果除了WKWebView之后缨恒,漸漸的使用這個(gè)庫(kù)的人越來(lái)越少谴咸,盡管這個(gè)庫(kù)也是支持了WKWebView的轮听。 但是一個(gè)事物的存在就有他的價(jià)值,就算使用也不是那么頻繁了岭佳,盡管他有很多的坑血巍。但是對(duì)于一個(gè)開(kāi)發(fā)者來(lái)說(shuō),我們應(yīng)該取其精華去其糟粕驼唱,現(xiàn)如今出的很多的交互的bridge依舊是有部分交互邏輯沿用了WebViewJavaScriptBridge的思想藻茂。 這里就不得不提味精大神的一片文章驹暑,這篇文章里面深入淺出的談了談現(xiàn)如今Hybrid開(kāi)發(fā)時(shí)常用的一些橋方法玫恳。有興趣的可以去關(guān)注一下。廢話不多說(shuō)优俘,那么我們今天就從源碼開(kāi)始解析這個(gè)庫(kù)的使用以及原理京办。

簡(jiǎn)介

簡(jiǎn)單的來(lái)說(shuō),在最開(kāi)始的UIWebView時(shí)帆焕,原生跟JS之間的交互一般是兩種方式:

  • Native -> JS:這種方式很簡(jiǎn)單惭婿,只是是原生調(diào)用stringByEvaluatingJavaScriptFromString:方法,傳入要執(zhí)行的JS代碼就可以實(shí)現(xiàn)叶雹;
  • JS -> Native:這種方式是在網(wǎng)頁(yè)上面加載一串Custom URL Scheme的URL财饥,然后通過(guò)原生去UIWebView的代理方法webView:shouldStartLoadWithRequest:navigationType:中攔截相應(yīng)的URL做處理。

當(dāng)然這個(gè)其實(shí)也就是WebViewJavaScriptBridge的理論核心折晦。但是上面這種實(shí)現(xiàn)方法為什么沒(méi)有人使用呢?原因就是钥星,通過(guò)在代理方法里面攔截,我們就必不可少的要寫(xiě)很多的if else的代碼满着。在項(xiàng)目中的混合插件越來(lái)越多的時(shí)候谦炒,就導(dǎo)致了這個(gè)代理方法里面的邏輯越來(lái)越臃腫,越來(lái)越難以維護(hù)风喇。 那么WebViewJavaScriptBridge的作用就是以更加優(yōu)雅的方式宁改,去實(shí)現(xiàn)Native與JS之間的互調(diào)。讓Native能像調(diào)用OC的方法一樣調(diào)用JS魂莫,同時(shí)JS也能像調(diào)用JS方法一樣去調(diào)用OC还蹲。這就在OC和JS中間搭起了一座友誼的橋梁。

使用

這里使用我就不多說(shuō)了耙考,直接pod 'WebViewJavascriptBridge'就可以引入到項(xiàng)目了谜喊。 附上源碼地址:WebViewJavaScriptBridge

目錄結(jié)構(gòu)

  • WebViewJavaScriptBridgeBase:bridge的核心類,用來(lái)初始化以及消息的處理琳骡;
  • WebViewJavaScriptBridge:判斷WebView的類型锅论,并通過(guò)不同的類型進(jìn)行分發(fā)。針對(duì)UIWebView和WebView做的一層封裝楣号,主要從來(lái)執(zhí)行JS代碼最易,以及實(shí)現(xiàn)UIWebView和WebView的代理方法怒坯,并通過(guò)攔截URL來(lái)通知WebViewJavaScriptBridgeBase做的相應(yīng)操作;
  • WKWebViewJavaScriptBridge:主要是針對(duì)WKWebView做的一些封裝藻懒,主要也是執(zhí)行JS代碼和實(shí)現(xiàn)WKWebView的代理方法的剔猿。同上面這個(gè)類類似;
  • WebViewJavaScriptBridge_JS:里面主要寫(xiě)了一些JS的方法嬉荆,JS端與Native”互動(dòng)“的JS端的方法基 本上都在這個(gè)里面归敬;

主要流程

WebViewJavaScriptBridge參與交互的流程包括三個(gè)部分:初始化、JS調(diào)用Native鄙早、Native調(diào)用JS汪茧。接下來(lái)我們就一一分析其中的過(guò)程。

1限番、初始化

這里必須要說(shuō)一下舱污,WebViewJavaScriptBridge的這個(gè)設(shè)計(jì)很巧妙,他在JS端和Native端弥虐,都各自初始化了一個(gè)WebViewJavaScriptBridge對(duì)象扩灯,就像是兩邊各自安排了一個(gè)”通訊兵“,讓這兩個(gè)對(duì)象去完成消息的收發(fā)工作霜瘪。同時(shí)兩邊還各自維護(hù)一個(gè)管理相應(yīng)事件的messageHandlers容器珠插、一個(gè)管理回調(diào)的callbackId容器。所以這里的初始化颖对,我們得分為兩個(gè)部分的初始化捻撑,一個(gè)部分是Native端的初始化,一個(gè)是JS端的初始化惜互。這里我們都以UIWebView為例子講解布讹,WKWebView其實(shí)也是相類似的原理,可以類比一下训堆。

(1)描验、Native端的初始化
  • 首先初始化WebViewJavaScriptBridge并且設(shè)置好代理
_bridge = [WebViewJavascriptBridge bridgeForWebView:webView];
[_bridge setWebViewDelegate:self];
- (void) _setupInstance:(WKWebView*)webView {
    _webView = webView;
    _webView.navigationDelegate = self;
    _base = [[WebViewJavascriptBridgeBase alloc] init];
    _base.delegate = self;
}

然后其內(nèi)部初始化了WebViewJavaScriptBridgeBase類和相關(guān)的屬性

- (id)init {
    if (self = [super init]) {
        self.messageHandlers = [NSMutableDictionary dictionary];
        self.startupMessageQueue = [NSMutableArray array];
        self.responseCallbacks = [NSMutableDictionary dictionary];
        _uniqueId = 0;
    }
    return self;
}
  • 注冊(cè)handler,這個(gè)handler是提供給JS調(diào)用的
[_bridge registerHandler:@"testObjcCallback" handler:^(id data, WVJBResponseCallback responseCallback) {
        NSLog(@"testObjcCallback called: %@", data);
        responseCallback(@"Response from testObjcCallback");
    }];

注冊(cè)其實(shí)就是在messageHandlers這個(gè)NSMutableDictionary里面保存一下

- (void)registerHandler:(NSString *)handlerName handler:(WVJBHandler)handler {
    _base.messageHandlers[handlerName] = [handler copy];
}
(2)坑鱼、web view端的初始化
  • 當(dāng)我們通過(guò)loadRequest加載URL之后膘流,網(wǎng)頁(yè)一加載就會(huì)執(zhí)行網(wǎng)頁(yè)JS中的bridge的初始化方法setupWebViewJavascriptBridge函數(shù)
function setupWebViewJavascriptBridge(callback) {
        if (window.WebViewJavascriptBridge) { return callback(WebViewJavascriptBridge); }
        if (window.WVJBCallbacks) { return window.WVJBCallbacks.push(callback); }
        window.WVJBCallbacks = [callback];
        var WVJBIframe = document.createElement('iframe');
        WVJBIframe.style.display = 'none';
        WVJBIframe.src = 'https://__bridge_loaded__';
        document.documentElement.appendChild(WVJBIframe);
        setTimeout(function() { document.documentElement.removeChild(WVJBIframe) }, 0)
    }

這里主要做了兩件事情,一個(gè)是保存要執(zhí)行的一直自定義初始化函數(shù)鲁沥,比如注冊(cè)JS中的handler呼股,第二個(gè)就是通過(guò)添加一個(gè)iframe加載初始化鏈接https://__bridge_loaded__

  • Native端會(huì)攔截https://__bridge_loaded__這個(gè)URL
  • 在webview中執(zhí)行本地WebViewJavaScriptBridge_JS中的代碼画恰,初始化window.WebViewJavaScriptBridge對(duì)象:首先在JS中創(chuàng)建一個(gè)WebViewJavaScriptBridge對(duì)象彭谁,設(shè)置成window一個(gè)屬性,然后定義幾個(gè)用于管理消息的全局變量允扇,接著給WebViewJavaScriptBridge對(duì)象定義幾個(gè)處理消息的方法和函數(shù)缠局,執(zhí)行Native端startupMessageQueue中保存的消息则奥,也就是本地JS文件還未加載時(shí)就發(fā)送了的消息。
window.WebViewJavascriptBridge = {
        registerHandler: registerHandler,
        callHandler: callHandler,
        disableJavscriptAlertBoxSafetyTimeout: disableJavscriptAlertBoxSafetyTimeout,
        _fetchQueue: _fetchQueue,
        _handleMessageFromObjC: _handleMessageFromObjC
    };

2狭园、JS調(diào)用Native

  • JS中調(diào)用callHandler()方法读处,發(fā)消息給原生
bridge.callHandler('testObjcCallback', {'foo': 'bar'}, function(response) {
                log('JS got response', response)
            })
復(fù)制代碼

然后我們看看callHandler是怎么定義的

function callHandler(handlerName, data, responseCallback) {
        if (arguments.length == 2 && typeof data == 'function') {
            responseCallback = data;
            data = null;
        }
        _doSend({ handlerName:handlerName, data:data }, responseCallback);
    }

那么這個(gè)_doSend是干嘛的?我們順著往下看

function _doSend(message, responseCallback) {
        if (responseCallback) {
            var callbackId = 'cb_'+(uniqueId++)+'_'+new Date().getTime();
            responseCallbacks[callbackId] = responseCallback;
            message['callbackId'] = callbackId;
        }
        sendMessageQueue.push(message);
        messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
    }

這下我們清楚了唱矛,原來(lái)我們?cè)趥魅?code>handlerName和data被包裝成了一個(gè)message傳入到_doSend函數(shù)罚舱,然后生成一個(gè)callbackId,也一道包裝到message中去绎谦。這樣三個(gè)數(shù)據(jù)都被打包成了一個(gè)message傳到Native管闷。 當(dāng)然為什么要傳入一個(gè)callbackId進(jìn)去呢?這是因?yàn)橛糜谔幚碓卣{(diào)的responseCallback是一個(gè)函數(shù)燥滑,是不能直接傳給原生的渐北,所以這里就把這個(gè)responseCallback存到了一個(gè)全局的responseCallbacks對(duì)象的屬性里面去,屬性名就是responseCallback對(duì)應(yīng)的id铭拧。這個(gè)地方就是為了后面Native回調(diào)JS時(shí),根據(jù)id找到對(duì)應(yīng)的responseCallback恃锉。

  • 在上圖中的最后一步指的是JS會(huì)在iframe中加載發(fā)送消息的URL搀菩,此時(shí)原生就可以在相應(yīng)的代理中攔截到這個(gè)URL,然后就知道JS端給我傳遞消息了破托,然后Native端會(huì)去調(diào)用JS肪跋,把sendMessageQueue中的message取出來(lái),轉(zhuǎn)成JSON string的格式土砂。接著原生把JSON string解析成字典州既,取出相應(yīng)的datacallbackIdhandlerName萝映。最后根據(jù)handlerName去先前的messageHanlers里面取出相對(duì)應(yīng)的block(handler)吴叶,然后調(diào)用這個(gè)blockdata作為第一個(gè)參數(shù)序臂,第二個(gè)參數(shù)是根據(jù)callbackId創(chuàng)建的responseCallback(block)蚌卤,然后原生就可以在block(handler)中處理接收到的data以及回調(diào)JS了。

  • 如果說(shuō)需要原生給JS回調(diào)的話奥秆,當(dāng)這個(gè)responseCallback被回調(diào)的時(shí)候逊彭,會(huì)執(zhí)行下面的代碼

- (void)sendData:(id)data responseCallback:(WVJBResponseCallback)responseCallback handlerName:(NSString*)handlerName {
    NSMutableDictionary* message = [NSMutableDictionary dictionary];

    if (data) {
        message[@"data"] = data;
    }

    if (responseCallback) {
        NSString* callbackId = [NSString stringWithFormat:@"objc_cb_%ld", ++_uniqueId];
        self.responseCallbacks[callbackId] = [responseCallback copy];
        message[@"callbackId"] = callbackId;
    }

    if (handlerName) {
        message[@"handlerName"] = handlerName;
    }
    [self _queueMessage:message];
}

這里就是直接創(chuàng)建了一個(gè)message(NSMutableDictionary)對(duì)象,把data构订、callbackIdhandlerName封裝之后轉(zhuǎn)換成為JSON string侮叮,最后調(diào)用WebViewJavascriptBridge._handleMessageFromObjC('%@')這個(gè)方法,把message傳給JS悼瘾。

- (void)_dispatchMessage:(WVJBMessage*)message {
    NSString *messageJSON = [self _serializeMessage:message pretty:NO];
    [self _log:@"SEND" json:messageJSON];
    messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\\" withString:@"\\\\"];
    messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\"" withString:@"\\\""];
    messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\'" withString:@"\\\'"];
    messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\n" withString:@"\\n"];
    messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\r" withString:@"\\r"];
    messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\f" withString:@"\\f"];
    messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\u2028" withString:@"\\u2028"];
    messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\u2029" withString:@"\\u2029"];

    NSString* javascriptCommand = [NSString stringWithFormat:@"WebViewJavascriptBridge._handleMessageFromObjC('%@');", messageJSON];
    if ([[NSThread currentThread] isMainThread]) {
        [self _evaluateJavascript:javascriptCommand];

    } else {
        dispatch_sync(dispatch_get_main_queue(), ^{
            [self _evaluateJavascript:javascriptCommand];
        });
    }
}

在JS接收到了這個(gè)message之后囊榜,會(huì)根據(jù)里面的callbackId找到之前的responseCallback谷异,把data作為參數(shù),回調(diào)這個(gè)responseCallback锦聊。

2歹嘹、Native調(diào)用JS

其Native調(diào)用JS和上面JS調(diào)用Native是有很多的相似之處的。當(dāng)然孔庭,其實(shí)也是可以直接通過(guò)web view執(zhí)行JS腳本去實(shí)現(xiàn)的尺上。但是WebViewJavaScriptBridge使用了一套更加規(guī)范的調(diào)用方式。接下來(lái)來(lái)介紹一下這種方式圆到。

  • Native調(diào)用callHandler()方法怎抛,把消息發(fā)送給JS
[_bridge callHandler:@"testJavascriptHandler" data:@{ @"foo":@"before ready" }];
復(fù)制代碼

這個(gè)方法跟JS里面的這個(gè)方法名是一樣的,當(dāng)然實(shí)際的作用其實(shí)也是相似的芽淡。 在這里都是將handlerName马绝、dataresponseCallback對(duì)應(yīng)的id包裝成一個(gè)message。然后把這個(gè)message對(duì)象轉(zhuǎn)成JSON string挣菲。最后在調(diào)用WebViewJavascriptBridge._handleMessageFromObjC(messageJSON)方法把數(shù)據(jù)給到JS富稻。這里至于為什么也是傳id,其實(shí)原理跟上面是一樣的白胀,block也是不能直接傳給JS的椭赋,所以這里把responseCallback的這個(gè)block存到了全局的responseCallbacks字典里面去了,key就是responseCallback對(duì)應(yīng)的id或杠。JS回調(diào)Native的時(shí)候哪怔,就會(huì)來(lái)這個(gè)字典里面去取對(duì)應(yīng)的block。其實(shí)思想都是差不多的向抢。

  • JS端拿到了這個(gè)message之后认境,會(huì)將它解析成為JS對(duì)象,然后去使用data挟鸠、callbackIdhandlerName叉信。然后根據(jù)handlerNamemessageHandlers里面去對(duì)應(yīng)的handler函數(shù),然后去執(zhí)行這個(gè)函數(shù)兄猩。第一個(gè)參數(shù)是傳過(guò)來(lái)的data茉盏,第二個(gè)參數(shù)就是根據(jù)callbackId創(chuàng)建的responseCallback的function。這里就可以在handler里面處理接收到的回調(diào)了枢冤。
  • 這里與前面JS調(diào)Native時(shí)Native回調(diào)JS的處理不太一樣鸠姨,因?yàn)镴S調(diào)Native是不能直接調(diào)的。但是怎么去通知Native呢淹真?其實(shí)他這里就是直接走了JS調(diào)用Native的流程讶迁,就是上面提到的這個(gè)流程。不過(guò)還是有不同的:
    • 一是message里面的東西不一樣了核蘸;
    • 二是Native對(duì)message的處理:
      • 跟上面JS調(diào)用Native不一樣的就是message里面現(xiàn)在不需要你傳一個(gè)callbackId了巍糯,因?yàn)檫@里本來(lái)就是JS回調(diào)給Native的啸驯,再傳這個(gè),兩邊就一直在回調(diào)來(lái)回調(diào)去了祟峦。但是呢罚斗,多了一個(gè)responseId,這是因?yàn)镹ative執(zhí)行JS回調(diào)的時(shí)候宅楞,會(huì)根據(jù)這個(gè)responseIdresponseCallbacks中去取對(duì)應(yīng)的block
        ```
        WVJBResponseCallback responseCallback = _responseCallbacks[responseId];
        ```
       

        *   Native在收到JS回調(diào)之后针姿,會(huì)根據(jù)`responseId`找到之前保存的`responseCallback`的block,然后把`message`中的`responseData`(其實(shí)就是data)作為參數(shù)回調(diào)給這個(gè)responseCallback厌衙。與JS調(diào)用Native不同的其實(shí)就是這里的`responseCallback`只有一個(gè)`data`參數(shù)了距淫,是沒(méi)有用于再次回調(diào)JS的block了。

總結(jié)

至此婶希,WebViewJavaScriptBridge的整體核心流程就基本上講完了榕暇。這樣看看,其實(shí)其中的原理還算是簡(jiǎn)單喻杈,但是很巧妙彤枢。兩邊都維護(hù)了一個(gè)WebViewJavaScriptBridge的對(duì)象,消息都封裝成為一個(gè)message奕塑,然后所有的callback堂污,都巧妙的轉(zhuǎn)換成了id。通過(guò)直接傳遞id龄砰,然后根據(jù)id分別去對(duì)應(yīng)的地方去尋找到對(duì)應(yīng)的callback。這種方式讨衣,其實(shí)也是值得我們?nèi)W(xué)習(xí)和使用的换棚。 接下來(lái)我會(huì)繼續(xù)的去研究現(xiàn)在比較火爆的JSCore的交互方式,對(duì)于Hybrid開(kāi)發(fā)有想法的朋友反镇,歡迎留言跟我交流固蚤。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市歹茶,隨后出現(xiàn)的幾起案子夕玩,更是在濱河造成了極大的恐慌,老刑警劉巖惊豺,帶你破解...
    沈念sama閱讀 218,755評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件燎孟,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡尸昧,警方通過(guò)查閱死者的電腦和手機(jī)揩页,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)烹俗,“玉大人爆侣,你說(shuō)我怎么就攤上這事萍程。” “怎么了兔仰?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,138評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵茫负,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我乎赴,道長(zhǎng)忍法,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,791評(píng)論 1 295
  • 正文 為了忘掉前任无虚,我火速辦了婚禮缔赠,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘友题。我一直安慰自己嗤堰,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布度宦。 她就那樣靜靜地躺著踢匣,像睡著了一般。 火紅的嫁衣襯著肌膚如雪戈抄。 梳的紋絲不亂的頭發(fā)上离唬,一...
    開(kāi)封第一講書(shū)人閱讀 51,631評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音划鸽,去河邊找鬼输莺。 笑死,一個(gè)胖子當(dāng)著我的面吹牛裸诽,可吹牛的內(nèi)容都是我干的嫂用。 我是一名探鬼主播,決...
    沈念sama閱讀 40,362評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼丈冬,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼嘱函!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起埂蕊,我...
    開(kāi)封第一講書(shū)人閱讀 39,264評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤往弓,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后蓄氧,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體函似,經(jīng)...
    沈念sama閱讀 45,724評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年匀们,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了缴淋。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,040評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡孝凌,死狀恐怖供搀,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情含长,我是刑警寧澤钟沛,帶...
    沈念sama閱讀 35,742評(píng)論 5 346
  • 正文 年R本政府宣布畔规,位于F島的核電站,受9級(jí)特大地震影響恨统,放射性物質(zhì)發(fā)生泄漏叁扫。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評(píng)論 3 330
  • 文/蒙蒙 一畜埋、第九天 我趴在偏房一處隱蔽的房頂上張望莫绣。 院中可真熱鬧,春花似錦悠鞍、人聲如沸对室。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,944評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)掩宜。三九已至,卻和暖如春么翰,著一層夾襖步出監(jiān)牢的瞬間牺汤,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,060評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工浩嫌, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留檐迟,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,247評(píng)論 3 371
  • 正文 我出身青樓码耐,卻偏偏與公主長(zhǎng)得像锅减,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子伐坏,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評(píng)論 2 355

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