WebViewJavascriptBridge源碼分析

博客鏈接 WebViewJavascriptBridge源碼分析

在APP的開發(fā)過程中,都會(huì)通過H5來實(shí)現(xiàn)部分功能,H5頁面是內(nèi)嵌在原生應(yīng)用的WebView組件中吞鸭。在有的場(chǎng)景下氯哮,當(dāng)兩端需要相互通信稍计,但是JavaScript的權(quán)限受到限制岸蜗,比如不能修改系統(tǒng)配置等,這個(gè)時(shí)候需要委托Native去實(shí)現(xiàn)某個(gè)功能重罪,并在完成后將結(jié)果通知JavaScript努隙。所以我們需要在Native和JavaScript之間就搭建一個(gè)通信的橋梁球恤,這個(gè)橋梁就是我們所說的JavaScript Bridge,簡稱 JS Bridge荸镊。

通常實(shí)現(xiàn)Native與JS橋接的方式有兩種:

  1. 通過JavaScriptCore框架
  2. 通過Webview攔截請(qǐng)求的方式(WebViewJavascriptBridge使用的方式)

marcuswestin/WebViewJavascriptBridge是使用第2種方式實(shí)現(xiàn)在用于在WKWebView和UIWebView中咽斧,JS與Native相互發(fā)送消息。

與其他OC的三方庫不同躬存,WebViewJavascriptBridge的實(shí)現(xiàn)包括OC和JS兩部分张惹,因此只看OC部分的代碼我們是無法理解這個(gè)bridge是如何實(shí)現(xiàn)兩端通信的。

WebViewJavascriptBridge中的類的作用

  • WebViewJavascriptBridgeBase:OC端橋接基礎(chǔ)服務(wù)類岭洲,維護(hù)OC端開放給JS端的方法以及OC回調(diào)方法宛逗,實(shí)現(xiàn)OC向JS發(fā)送數(shù)據(jù)的具體邏輯。
  • WebViewJavascriptBridge_JS:維護(hù)了一份JS代碼盾剩,用于JS環(huán)境的注入雷激。同時(shí)維護(hù)JS端的bridge對(duì)象替蔬,管理JS端注冊(cè)的方法集合以及回調(diào)方法集合,面向Web端提供注冊(cè)JS方法屎暇、調(diào)用OC端方法的接口进栽。
  • WKWebViewJavascriptBridge:基于WKWebView的OC端交互邏輯處理類,面向OC業(yè)務(wù)層恭垦,提供了注冊(cè)O(shè)C方法、調(diào)用JS方法等接口格嗅。
  • WebViewJavascriptBridge:基于UIWebView的的OC端交互邏輯處理類番挺,與WKWebViewJavascriptBridge的功能一致。

WebViewJavascriptBridge源碼解析

WebViewJavascriptBridge的實(shí)現(xiàn)可以說是雙向的過程屯掖,無論是JS端還是Native端都包含以下三部分內(nèi)容:

  • bridge初始化
  • 本端注冊(cè)函數(shù)共另一端調(diào)用
  • 調(diào)用另一端函數(shù)

目前App已經(jīng)取消對(duì)UIWebView的支持玄柏,所以我們只需要看WKWebView相關(guān)部分的實(shí)現(xiàn)即可。

bridge初始化

bridge初始化分為Native初始化bridge和JS初始化bridge贴铜。在使用WebView的時(shí)候粪摘,都是從Native端打開頁面開始,因此先分析Native初始化bridge绍坝。

Native初始化bridge

+ (instancetype)bridgeForWebView:(WKWebView*)webView {
    WKWebViewJavascriptBridge* bridge = [[self alloc] init];
    [bridge _setupInstance:webView];
    [bridge reset];
    
    return bridge;
}

- (void)_setupInstance:(WKWebView*)webView {
    _webView = webView;
    // 將webView的navigationDelegate設(shè)為WKWebViewJavascriptBridge對(duì)象自身
    _webView.navigationDelegate = self;
    _base = [[WebViewJavascriptBridgeBase alloc] init];
    // WKWebViewJavascriptBridge對(duì)象需要實(shí)現(xiàn)_evaluateJavascript:代理方法
    _base.delegate = self;
}

- (void)reset {
    [_base reset];
}

WKWebViewJavascriptBridge_evaluateJavascript:方法的實(shí)現(xiàn):

- (NSString*)_evaluateJavascript:(NSString*)javascriptCommand {
    [_webView evaluateJavaScript:javascriptCommand completionHandler:nil];
    return NULL;
}

關(guān)于JS代碼的注入徘意,可以使用WKUserContentController,也可以使用evaluateJavaScript:completionHandler:這個(gè)函數(shù)轩褐,WebViewJavascriptBridge使用后者實(shí)現(xiàn)JS注入椎咧,因此需要將webView的navigationDelegate設(shè)為WKWebViewJavascriptBridge對(duì)象自身,并在代理方法中調(diào)用evaluateJavaScript:completionHandler:函數(shù)把介。

在Native初始化后勤讽,就要使用load之類的方法加載頁面與JS代碼,這進(jìn)入到JS初始化bridge過程拗踢。

JS初始化bridge

相對(duì)于Native初始化bridge來說脚牍,JS初始化bridge就要顯得難一些。

<script>
    function setupWebViewJavascriptBridge(callback) {
        // window表示瀏覽器窗口
        // WebViewJavascriptBridge就是bridge對(duì)象巢墅。
        // 如果有bridge對(duì)象則直接調(diào)用callback并傳入bridge對(duì)象诸狭。
        if (window.WebViewJavascriptBridge) { return callback(WebViewJavascriptBridge); }
        // 如果有WVJBCallbacks則將回調(diào)函數(shù)push到數(shù)組里,后面初始化bridge時(shí)會(huì)統(tǒng)一遍歷調(diào)用callback砂缩,并傳入bridge作谚。
        if (window.WVJBCallbacks) { return window.WVJBCallbacks.push(callback); }
        window.WVJBCallbacks = [callback];
        var WVJBIframe = document.createElement('iframe');
        WVJBIframe.style.display = 'none';
        // 網(wǎng)頁會(huì)請(qǐng)求這個(gè)鏈接
        WVJBIframe.src = 'https://__bridge_loaded__';
        document.documentElement.appendChild(WVJBIframe);
        // setTimeout是Native端注入的一個(gè)函數(shù)
        setTimeout(function () { document.documentElement.removeChild(WVJBIframe) }, 0)
    }

    // 執(zhí)行調(diào)用setupWebViewJavascriptBridge函數(shù),bridge就是對(duì)象庵芭。
    // 在WebViewJavascriptBridge_JS.m文件中對(duì)bridge的定義
    // window.WebViewJavascriptBridge = {
    //      registerHandler: registerHandler,
    //      callHandler: callHandler,
    //      disableJavscriptAlertBoxSafetyTimeout: disableJavscriptAlertBoxSafetyTimeout,
    //      _fetchQueue: _fetchQueue,
    //      _handleMessageFromObjC: _handleMessageFromObjC
    // };
    setupWebViewJavascriptBridge(function (bridge) {
        // ...
        // JS注冊(cè)函數(shù)供Native調(diào)用
        bridge.registerHandler('testJavascriptHandler', function (data, responseCallback) {
           // ...
        })
        // ...
    })
</script>

在JS初始化bridge過程中妹懒,會(huì)直接調(diào)用setupWebViewJavascriptBridge(callback)函數(shù),callback相當(dāng)于block/閉包双吆。在這個(gè)過程中眨唬,callHandler会前、registerHandler等函數(shù)都是通過JS代碼注入的,這些代碼都在Native端匾竿,那它是如何成功執(zhí)行這些函數(shù)的呢瓦宜?關(guān)鍵在于https://__bridge_loaded__這個(gè)url。在使用了這個(gè)url后岭妖,WKWebView的NavigationDelegate會(huì)攔截這個(gè)請(qǐng)求临庇,并注入JS代碼。相關(guān)實(shí)現(xiàn)如下:

- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
    // ...
    // isBridgeLoadedURL函數(shù)中的kBridgeLoaded即為__bridge_loaded__
    if ([_base isBridgeLoadedURL:url]) {
        [_base injectJavascriptFile];
    }
    // ...
}

用泳道圖來描述初始化bridge的過程


WebViewJavascriptBridge初始化bridge

JS注冊(cè)函數(shù)昵慌,Native調(diào)用JS

JS注冊(cè)函數(shù)

// JS注冊(cè)函數(shù)給Native調(diào)用
bridge.registerHandler('testJavascriptHandler', function(data, responseCallback) {
    var responseData = { 'Javascript Says':'Right back atcha!' }
    responseCallback(responseData)
})

// WebViewJavascriptBridge_JS.m中的JS代碼
// 保存JS函數(shù)與函數(shù)名的映射關(guān)系
var messageHandlers = {};
    
function registerHandler(handlerName, handler) {
    messageHandlers[handlerName] = handler;
}

Native調(diào)用JS函數(shù)

id data = @{ @"greetingFromObjC": @"Hi there, JS!" };
[_bridge callHandler:@"testJavascriptHandler" data:data responseCallback:^(id response) {
    NSLog(@"testJavascriptHandler responded: %@", response);
}];

內(nèi)部調(diào)用了WebViewJavascriptBridgeBasesendData:responseCallback:handlerName:方法假夺。

- (void)sendData:(id)data responseCallback:(WVJBResponseCallback)responseCallback handlerName:(NSString*)handlerName {
    NSMutableDictionary* message = [NSMutableDictionary dictionary];
    // JS函數(shù)所需的參數(shù)
    if (data) message[@"data"] = data;
    // responseCallback:Native調(diào)用JS函數(shù)后的回調(diào)函數(shù)
    if (responseCallback) {
        NSString* callbackId = [NSString stringWithFormat:@"objc_cb_%ld", ++_uniqueId];
        // 保存Native回調(diào)
        self.responseCallbacks[callbackId] = [responseCallback copy];
        // 保存回調(diào)方法的id
        message[@"callbackId"] = callbackId;
    }
    // JS函數(shù)名
    if (handlerName) {
        message[@"handlerName"] = handlerName;
    }
    [self _queueMessage:message];
}

在Native調(diào)用JS的函數(shù)時(shí),有時(shí)Native需要JS調(diào)用Native的回調(diào)函數(shù)返回一些數(shù)據(jù)斋攀,因此需要保存回調(diào)函數(shù)的一些信息已卷,關(guān)于Native的回調(diào)函數(shù)是如何調(diào)用的,在后面會(huì)講到淳蔼。

- (void)_queueMessage:(WVJBMessage*)message {
    if (self.startupMessageQueue) {
        [self.startupMessageQueue addObject:message];
    } else {
        [self _dispatchMessage:message];
    }
}

在開發(fā)過程中侧蘸,有可能Native調(diào)用JS函數(shù)的時(shí)候,JS端還沒有完成bridge準(zhǔn)備工作鹉梨。bridge是在decidePolicyForNavigationAction:的代理方法中執(zhí)行injectJavascriptFile方法才完成的讳癌,但是
callHandler可能在viewWillAppear的時(shí)候調(diào)用,此時(shí)沒有完成JS端bridge的初始化俯画,所以先存入startupMessageQueue中析桥,等準(zhǔn)備完成后, 再統(tǒng)一調(diào)用 startupMessageQueue中的Message到JS艰垂,并將startupMessageQueue置為空泡仗。

- (void)injectJavascriptFile {
    // 注入JS bridge的環(huán)境代碼
    NSString *js = WebViewJavascriptBridge_js();
    [self _evaluateJavascript:js];
    // 對(duì)于一些提前調(diào)用的callHandler,在注入JS初始化代碼后猜憎,會(huì)統(tǒng)一發(fā)送娩怎,并清空startupMessageQueue
    if (self.startupMessageQueue) {
        NSArray* queue = self.startupMessageQueue;
        self.startupMessageQueue = nil;
        for (id queuedMessage in queue) {
            [self _dispatchMessage:queuedMessage];
        }
    }
}
- (void)_dispatchMessage:(WVJBMessage*)message {
    // 序列化message
    NSString *messageJSON = [self _serializeMessage:message pretty:NO];
    // 省略對(duì)messageJSON的處理
    NSString* javascriptCommand = [NSString stringWithFormat:@"WebViewJavascriptBridge._handleMessageFromObjC('%@');", messageJSON];
    if ([[NSThread currentThread] isMainThread]) {
        [self _evaluateJavascript:javascriptCommand];
    } else {
        dispatch_sync(dispatch_get_main_queue(), ^{
            [self _evaluateJavascript:javascriptCommand];
        });
    }
}

_dispatchMessage將之前拼裝好的message傳給JS,用JS bridge的 _handleMessageFromObjC函數(shù)處理Native的調(diào)用請(qǐng)求胰柑,_handleMessageFromObjC函數(shù)是在JS bridge初始化的時(shí)候注入的截亦。

function _handleMessageFromObjC(messageJSON) {
    _dispatchMessageFromObjC(messageJSON);
}

function _dispatchMessageFromObjC(messageJSON) {
    // 忽略dispatchMessagesWithTimeoutSafety部分
    _doDispatchMessageFromObjC();

    function _doDispatchMessageFromObjC() {
        var message = JSON.parse(messageJSON);
        var messageHandler;
        var responseCallback;

        // 是否有responseId,對(duì)于Native調(diào)用JS函數(shù)所傳過來的message來說是沒有該字段的
        // if (message.responseId) {
        // // ...
        // } 
        
        // 處理Native調(diào)用JS函數(shù)的message
        if (message.callbackId) {
            // 是否含有Native回調(diào)
            var callbackResponseId = message.callbackId;
            responseCallback = function (responseData) {
                // _doSend函數(shù)只傳了message柬讨,另外沒有responseCallback參數(shù)
                _doSend({
                    handlerName: message.handlerName,
                    // 如果Native傳過來的message有回調(diào)崩瓤,那么JS端需要傳入一個(gè)responseId,這樣Native端才能通過responseId這個(gè)key
                    // 在responseCallbacks字典中找到對(duì)應(yīng)的Native回調(diào)
                    responseId: callbackResponseId,
                    responseData: responseData
                });
            };
        }

        // 通過handlerName獲取到對(duì)應(yīng)的JS函數(shù)踩官,并調(diào)用却桶。
        // messageHandlers保存了JS bridge的函數(shù)名和回調(diào)函數(shù)
        var handler = messageHandlers[message.handlerName];
        if (!handler) {
            console.log("WebViewJavascriptBridge: WARNING: no handler for message from ObjC:", message);
        } else {
            handler(message.data, responseCallback);
        }
    }
}

// 這個(gè)_doSend是精簡之后的實(shí)現(xiàn),_doDispatchMessageFromObjC中的_doSend函數(shù)沒有傳遞responseCallback參數(shù)
function _doSend(message) {
    // sendMessageQueue保存message信息,這個(gè)message信息是給Native回調(diào)時(shí)候用的
    sendMessageQueue.push(message);
    // src = https://__wvjb_queue_message__颖系,WKWebView的代理方法優(yōu)惠攔截這個(gè)url嗅剖,從而調(diào)用WKWebViewJavascriptBridge的KFlushMessageQueue方法
    messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
}

當(dāng)WKWebView的代理方法攔截到https://__wvjb_queue_message__這個(gè)url的時(shí)候,就會(huì)調(diào)用WKFlushMessageQueue方法

if ([_base isQueueMessageURL:url]) {
    [self WKFlushMessageQueue];
}

- (void)WKFlushMessageQueue {
    [_webView evaluateJavaScript:@"WebViewJavascriptBridge._fetchQueue();" completionHandler:^(NSString* result, NSError* error) {
        NSLog(@"%@", result);
        if (error != nil) {
            NSLog(@"WebViewJavascriptBridge: WARNING: Error when trying to fetch data from WKWebView: %@", error);
        }
        [_base flushMessageQueue:result];
    }];
}

執(zhí)行_fetchQueue()這個(gè)JS函數(shù)嘁扼,并在completionHandler這個(gè)block內(nèi)返回JS中sendMessageQueue的信息信粮,從而獲取帶responseId的message。接著執(zhí)行Native的flushMessageQueue方法趁啸。

// 在flushMessageQueue方法中完成了Native調(diào)用JS函數(shù)后的回調(diào)
- (void)flushMessageQueue:(NSString *)messageQueueString{
    // 省略messageQueueString的有效性判斷
    id messages = [self _deserializeMessageJSON:messageQueueString];
    for (WVJBMessage* message in messages) {
        // 省略對(duì)Message類型的校驗(yàn)
        NSString* responseId = message[@"responseId"];
        if (responseId) {
            WVJBResponseCallback responseCallback = _responseCallbacks[responseId];
            responseCallback(message[@"responseData"]);
            [self.responseCallbacks removeObjectForKey:responseId];
        }
    }
}

用泳道圖來描述Native調(diào)用JS的過程


WebViewJavascriptBridgeNative調(diào)用JS

Native注冊(cè)函數(shù)强缘,JS調(diào)用Native

Native注冊(cè)函數(shù)

[_bridge registerHandler:@"testObjcCallback" handler:^(id data, WVJBResponseCallback responseCallback) {
    NSLog(@"testObjcCallback called: %@", data);
    //
    responseCallback(@"Response from testObjcCallback");
}];

- (void)registerHandler:(NSString *)handlerName handler:(WVJBHandler)handler {
    // messageHandlers用來保存OC函數(shù)與函數(shù)名的映射關(guān)系
    _base.messageHandlers[handlerName] = [handler copy];
}

JS調(diào)用Native函數(shù)

bridge.callHandler('testObjcCallback', { 'foo': 'bar' }, function (response) {
    log('JS got response', response)
})

// JS端callHandler的實(shí)現(xiàn)
function callHandler(handlerName, data, responseCallback) {
    if (arguments.length == 2 && typeof data == 'function') {
        responseCallback = data;
        data = null;
    }
    _doSend({ handlerName:handlerName, data:data }, responseCallback);
}

在Native調(diào)用JS的過程也使用了_doSend函數(shù),它的作用是為了能調(diào)用Native調(diào)用JS函數(shù)之后的回調(diào)函數(shù)不傅。在JS調(diào)用Native的過程中欺旧,_doSend函數(shù)是為了調(diào)用OC函數(shù)(與函數(shù)名對(duì)應(yīng)的block),responseCallback則是代表JS調(diào)用OC函數(shù)后的回調(diào)函數(shù)蛤签。

function _doSend(message, responseCallback) {
    // 如果有JS回調(diào),則使用responseCallbacks保存JS回調(diào)
    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;
}

之后的邏輯在OC調(diào)用JS中已經(jīng)描述過了栅哀,直到執(zhí)行flushMessageQueue:方法前都是一樣的
這里就不再重復(fù)震肮。接著看一下flushMessageQueue:方法在JS調(diào)用Native過程中的實(shí)現(xiàn):

- (void)flushMessageQueue:(NSString *)messageQueueString{
    // 省略messageQueueString的有效性判斷
    id messages = [self _deserializeMessageJSON:messageQueueString];
    for (WVJBMessage* message in messages) {
        // 省略對(duì)Message類型的校驗(yàn)
        // 忽略關(guān)于responseId的實(shí)現(xiàn)
        // 對(duì)于JS調(diào)用Native所傳過來的message來說是沒有responseId字段的
        WVJBResponseCallback responseCallback = NULL;
        NSString* callbackId = message[@"callbackId"];
        // 判斷是否有JS回調(diào)
        if (callbackId) {
            responseCallback = ^(id responseData) {
                if (responseData == nil) {
                    responseData = [NSNull null];
                }
                
                WVJBMessage* msg = @{ @"responseId":callbackId, @"responseData":responseData };
                // _queueMessage: -> _dispatchMessage: -> JS: _handleMessageFromObjC -> JS: _dispatchMessageFromObjC
                [self _queueMessage:msg];
            };
        } else {
            responseCallback = ^(id ignoreResponseData) {
                // Do nothing
            };
        }
        // 通過handlerName獲取到對(duì)應(yīng)的Native函數(shù),并調(diào)用留拾。
        // messageHandlers保存了Native bridge的函數(shù)名和回調(diào)函數(shù)
        WVJBHandler handler = self.messageHandlers[message[@"handlerName"]];
        
        if (!handler) {
            NSLog(@"WVJBNoHandlerException, No handler for message from JS: %@", message);
            continue;
        }
        
        // 執(zhí)行Native函數(shù)
        handler(message[@"data"], responseCallback);
    }
}

關(guān)于_queueMessage:方法前面已經(jīng)分析過戳晌,這里就不再重復(fù),接著看一下_dispatchMessageFromObjC函數(shù)在JS調(diào)用Native過程中的實(shí)現(xiàn):

// 精簡了_doDispatchMessageFromObjC痴柔,只保留調(diào)用JS回調(diào)的部分
function _doDispatchMessageFromObjC() {
    var message = JSON.parse(messageJSON);
    var responseCallback;
    // 如果有JS回調(diào)沦偎,那么OC傳過來的message必然存在responseId字段
    if (message.responseId) {
        responseCallback = responseCallbacks[message.responseId];
        if (!responseCallback) {
            return;
        }
        // 調(diào)用JS回調(diào)
        responseCallback(message.responseData);
        delete responseCallbacks[message.responseId];
    } 
}

接著用泳道圖來描述下JS調(diào)用Native的過程


WebViewJavascriptBridgeJS調(diào)用Native

NNWKWebViewJSBridge

NNWKWebViewJSBridge是我在了解WebViewJavascriptBridge的實(shí)現(xiàn)過程后,基于這個(gè)項(xiàng)目咳蔚,實(shí)現(xiàn)一個(gè)輕量級(jí)Swift版本JSBridge豪嚎,并且它僅需要支持WKWebView即可。
相對(duì)于WebViewJavascriptBridge谈火,我使用了WKUserContentController簡化了初始化和消息傳遞的實(shí)現(xiàn)過程侈询,相對(duì)來說會(huì)更好理解,消息傳遞性能也要比攔截Requests的方式要高糯耍。

項(xiàng)目地址:NNWKWebViewJSBridge
項(xiàng)目截圖:

WKWebViewJSBridge_demo

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末扔字,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子温技,更是在濱河造成了極大的恐慌革为,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,490評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件舵鳞,死亡現(xiàn)場(chǎng)離奇詭異震檩,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)系任,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,581評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門恳蹲,熙熙樓的掌柜王于貴愁眉苦臉地迎上來虐块,“玉大人,你說我怎么就攤上這事嘉蕾『氐欤” “怎么了?”我有些...
    開封第一講書人閱讀 165,830評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵错忱,是天一觀的道長儡率。 經(jīng)常有香客問我,道長以清,這世上最難降的妖魔是什么儿普? 我笑而不...
    開封第一講書人閱讀 58,957評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮掷倔,結(jié)果婚禮上眉孩,老公的妹妹穿的比我還像新娘。我一直安慰自己勒葱,他們只是感情好浪汪,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,974評(píng)論 6 393
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著凛虽,像睡著了一般死遭。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上凯旋,一...
    開封第一講書人閱讀 51,754評(píng)論 1 307
  • 那天呀潭,我揣著相機(jī)與錄音,去河邊找鬼至非。 笑死钠署,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的荒椭。 我是一名探鬼主播踏幻,決...
    沈念sama閱讀 40,464評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼戳杀!你這毒婦竟也來了该面?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,357評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤信卡,失蹤者是張志新(化名)和其女友劉穎隔缀,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體傍菇,經(jīng)...
    沈念sama閱讀 45,847評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡猾瘸,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,995評(píng)論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片牵触。...
    茶點(diǎn)故事閱讀 40,137評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡淮悼,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出揽思,到底是詐尸還是另有隱情袜腥,我是刑警寧澤,帶...
    沈念sama閱讀 35,819評(píng)論 5 346
  • 正文 年R本政府宣布钉汗,位于F島的核電站羹令,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏损痰。R本人自食惡果不足惜福侈,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,482評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望卢未。 院中可真熱鬧肪凛,春花似錦、人聲如沸辽社。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,023評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽爹袁。三九已至,卻和暖如春矮固,著一層夾襖步出監(jiān)牢的瞬間失息,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,149評(píng)論 1 272
  • 我被黑心中介騙來泰國打工档址, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留盹兢,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,409評(píng)論 3 373
  • 正文 我出身青樓守伸,卻偏偏與公主長得像绎秒,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子尼摹,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,086評(píng)論 2 355

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