iOS WebView的Hybrid框架設計

前言

隨著移動互聯(lián)網(wǎng)的發(fā)展,APP 開發(fā)模式也在不斷的創(chuàng)新收班,從最初的 Native 開發(fā)到后來的 Hybrid 混合開發(fā)故痊,再到最近比較火爆的 React Native、Weex 等項目看靠,這些都標志著 APP 開發(fā)已經(jīng)不再是純 Native 的工作赶促,還要涉及很多跨平臺的技術。

作為一種混合開發(fā)模式挟炬,Hybrid APP 底層依賴 Native 端的 Web 容器(UIWebview 和 WKWebview)鸥滨,上層使用前端 Html5、CSS谤祖、Javascript 做業(yè)務開發(fā)婿滓,這種開發(fā)模式非常適合業(yè)務快速拓展和迭代,在不發(fā)版本的前提下直接更新線上資源粥喜,受到不少公司的青睞與關注凸主。

對于 Hybrid APP 開發(fā),雖然業(yè)內(nèi)早已出現(xiàn) Cordova(PhoneGap)额湘、jQuery Mobile 等框架卿吐,但是由于性能、維護成本等原因锋华,并沒有在業(yè)內(nèi)非常流行嗡官,有些公司轉(zhuǎn)而選擇自己開發(fā)一套 Hybrid 框架,但是由于沒有豐富的經(jīng)驗和應用場景導致開發(fā)出來的 Hybrid 框架后期維護成本很高毯焕。本文我將對在公司開發(fā)的 Hybrid 解決方案跟大家做一個介紹衍腥,希望對各位的技術選型起到幫助,也歡迎大家積極交流纳猫。

Hybrid APP 特點

Hybrid APP 優(yōu)勢很明顯:

  • 跨平臺婆咸,開發(fā)效率高,節(jié)約開發(fā)成本
  • 業(yè)務快速拓展和迭代
  • 及時修復線上 Bug芜辕,不需發(fā)版

但是 Hybrid 也有自己的劣勢尚骄,比如體驗上肯定比不了 Native,而且對于一個 Native 開發(fā)者而言要理解前后端的技術物遇,對開發(fā)者的要求較高乖仇,但我相信這是好事兒~~

根據(jù)之前的經(jīng)驗,我覺得 Hybrid 需要找到自己的應用場景询兴,比如營銷乃沙、活動等需要快速試錯和占領市場的團隊來說,Hybrid 很適用诗舰,但是對于像 APP 首頁這樣要求體驗高的場景 Hybrid 就不太適用警儒,具體情況可以根據(jù)自己公司的 APP 場景做適當?shù)恼{(diào)整。

Hybrid APP 框架

一個完整的 Hybrid APP 框架主要包括 WebView 容器、Bridge蜀铲、UI边琉、預加載、緩存等模塊兒记劝,當然 Bridge变姨、預加載、緩存等也需要相應前后端的支持厌丑,比如發(fā)布平臺定欧、灰度平臺、增量更新怒竿、CDN 平臺等等砍鸠。

框架結(jié)構(gòu)如下:

在設計這套框架之前,需要弄清楚 Native 與前端的分工耕驰,Native 主要提供一個宿主環(huán)境爷辱,對 WebView 進行封裝,提供 Bridge 方法朦肘,Header 組件設計饭弓,賬號信息設計,底層提供預加載和緩存機制厚骗,框架的業(yè)務方是各個前端團隊示启,所以我們需要站在前端的角度對以上方面進行考慮兢哭。本文主要對 WebView领舰、Bridge、Header 設計進行介紹迟螺,后續(xù)文章會對賬號信息設計冲秽、預加載和緩存進行持續(xù)跟進。

UIWebView 和 WKWebView 兼容

iOS8 以后蘋果推出了一套新的 WKWebView矩父,對于 UIWebView 和 WKWebView 的區(qū)別锉桑,總結(jié)如下:

Feature UIWebView WKWebView
JS執(zhí)行速度
內(nèi)存占用
進度條
Cookie 自動存儲 需手動存儲
緩存
NSURLProtocol攔截 可以 不可以

WKWebView 的主要優(yōu)點是 JS 執(zhí)行速度快、內(nèi)存占用小窍株,剛一推出就被開發(fā)者所追捧民轴,但是不知道是不是因為蘋果爸爸太任性,WKWebView 設計上并沒有與 UIWebView 保持一致球订,無法自動存儲 Cookie 和不能通過 NSURLProtocol 自定義請求等坑~導致 WKWebView 并沒有被開發(fā)者大規(guī)模推薦使用后裸。

本套框架的預加載和緩存模塊兒需要借助 NSURLProtocol 實現(xiàn),所以這里還是優(yōu)先使用 UIWebView(想吐個槽冒滩,其實如果預加載和緩存這套系統(tǒng)做好以后微驶,UIWebView 的效果并沒不比 WKWebView 差),這里也不能把 WKWebView 一棒子打死不用,對于那些對無需預加載和緩存的頁面因苹,可以為前端提供參數(shù)(比如 wkwebview=true)讓前端自己的去選擇是否使用 WKWebView苟耻,所以這里需要對 WKWebView 進行兼容。

YZWebView 是對 UIWebView 和 WKWebView 進行封裝的類扶檐,結(jié)構(gòu)設計如下:

YZWebViewDelegate凶杖,UIWebView 和 WKWebView 代理的回調(diào)代理。

@protocol YZWebViewDelegate <NSObject>

@optional
- (BOOL)webView:(YZWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType;
- (void)webViewDidStartLoad:(YZWebView *)webView;
- (void)webViewDidFinishLoad:(YZWebView *)webView;
- (void)webView:(YZWebView *)webView didFailLoadWithError:(NSError *)error;

@end

NJKWebViewProgressDelegate款筑,進度條代理方法官卡。

@protocol NJKWebViewProgressDelegate <NSObject>

- (void)webViewProgress:(NJKWebViewProgress *)webViewProgress updateProgress:(float)progress;

@end

YZWebView 初始化方法,通過參數(shù) usingUIWebView 來決定初始化 WKWebView 或者 UIWebView醋虏,

- (instancetype)initWithFrame:(CGRect)frame usingUIWebView:(BOOL)usingUIWebView {
    self = [super initWithFrame:frame];
    if (self) {
        _usingUIWebView = usingUIWebView;
        [self p_initSelf];
    }
    return self;
}

- (void)p_initSelf {
    Class wkWebView = NSClassFromString(@"WKWebView");
    if (wkWebView && !self.usingUIWebView) {
        [self initWKWebView];     //初始化WKWebView
    } else {
        [self initUIWebView];     //初始化UIWebView
    }
    [self addSubview:self.currentWebView];
}

- (void)initWKWebView {
    ......
    WKWebView *webView =
    [[NSClassFromString(@"WKWebView") alloc] initWithFrame:self.bounds
                                             configuration:webViewConfig];
    webView.UIDelegate = self;
    webView.navigationDelegate = self;
    [webView setAutoresizesSubviews:YES];
    [webView.scrollView setDecelerationRate:UIScrollViewDecelerationRateNormal];
    [webView setAutoresizingMask:UIViewAutoresizingFlexibleHeight|UIViewAutoresizingFlexibleBottomMargin];
    webView.backgroundColor = [UIColor clearColor];
    [webView.scrollView setShowsHorizontalScrollIndicator:NO];

    [webView addObserver:self
              forKeyPath:@"estimatedProgress"
                 options:NSKeyValueObservingOptionNew
                 context:nil];
    _currentWebView = webView;
}

//NJKWebViewProgress 沒兼容WKWebView寻咒,這里需要通過KVO進行監(jiān)測
- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context {
    if ([keyPath isEqualToString:@"estimatedProgress"]) {
        self.estimatedProgress = [change[NSKeyValueChangeNewKey] doubleValue];
        if (_progressDelegate && [_progressDelegate respondsToSelector:@selector(webViewProgress:updateProgress:)]) {
            [_progressDelegate webViewProgress:nil updateProgress:_estimatedProgress];
        }
    }
}

- (void)initUIWebView {
    ......
    UIWebView *uiWebView = [[UIWebView alloc] initWithFrame:self.bounds];
    [uiWebView setAutoresizesSubviews:YES];
    [uiWebView setScalesPageToFit:YES];
    [uiWebView.scrollView setDecelerationRate:UIScrollViewDecelerationRateNormal];
    [uiWebView setAutoresizingMask:UIViewAutoresizingFlexibleHeight|UIViewAutoresizingFlexibleBottomMargin];
    uiWebView.keyboardDisplayRequiresUserAction = NO;
    uiWebView.backgroundColor = [UIColor clearColor];
    [uiWebView.scrollView setShowsHorizontalScrollIndicator:NO];
    uiWebView.delegate = self;

    self.njkWebViewProgress = [[NJKWebViewProgress alloc] init];
    uiWebView.delegate = _njkWebViewProgress;
    _njkWebViewProgress.webViewProxyDelegate = self;
    _njkWebViewProgress.progressDelegate = self;
    _currentWebView = uiWebView;
}

WebView 最關鍵的地方就是能捕獲到前端資源的請求,UIWebView 的捕獲方法是 webView:shouldStartLoadWithRequest:request navigationType:颈嚼,WKWebView 的捕獲方法是 webView:decidePolicyForNavigationAction:decisionHandler:毛秘,同時 WebView 有完整的生命周期回調(diào)(start,finish阻课,fail等)叫挟。

#pragma mark - UIWebViewDelegate

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
    self.currentRequest = request;
    BOOL result = [self callback_webViewShouldStartLoadWithRequest:request navigationType:navigationType];
    return result;
}

......

#pragma mark - WKNavigationDelegate

- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
    self.currentRequest = navigationAction.request;
    BOOL result = [self callback_webViewShouldStartLoadWithRequest:navigationAction.request
                                      navigationType:navigationAction.navigationType];
    if (result) {
        decisionHandler(WKNavigationActionPolicyAllow);
    } else {
        decisionHandler(WKNavigationActionPolicyCancel);
    }
}

......

#pragma mark - YZWebViewCallback

- (BOOL)callback_webViewShouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(NSInteger)navigationType {
    BOOL result = YES;
    if ([self.delegate respondsToSelector:@selector(webView:
                                                    shouldStartLoadWithRequest:
                                                    navigationType:)]) {
        if (navigationType == -1) {
            navigationType = UIWebViewNavigationTypeOther;
        }
        result = [self.delegate webView:self shouldStartLoadWithRequest:request navigationType:navigationType];
    }
    return result;
}

......

#pragma mark - NJKWebViewProgressDelegate

- (void)webViewProgress:(NJKWebViewProgress *)webViewProgress updateProgress:(float)progress {
    self.estimatedProgress = progress;
    if (_progressDelegate && [_progressDelegate respondsToSelector:@selector(webViewProgress:updateProgress:)]) {
        [_progressDelegate webViewProgress:webViewProgress updateProgress:_estimatedProgress];
    }
}

需要強調(diào)的一點是:(UIWebView 的捕獲方法是 webView:shouldStartLoadWithRequest:request navigationType:,WKWebView 的捕獲方法是 webView:decidePolicyForNavigationAction:decisionHandler)這兩個方法只能控制一個請求可不可以被 WebView 發(fā)出限煞,比如 Bridge 就可以在這層進行捕獲抹恳,但是并不可以做請求定制的功能。請求的定制需要借助 NSURLProtocol署驻。

Bridge設計

Hybrid APP 的交互無非是 Native 調(diào)用前端頁面的 JS 方法奋献,或者前端頁面通過 JS 調(diào)用 Native 提供的接口,兩者交互的橋梁皆 Webview:

通過調(diào)研旺上,前端可以通過在 DOM 注入 iframe 發(fā)起 Bridge 請求瓶蚂,該請求可以被 webView:shouldStartLoadWithRequest:request navigationType: 方法捕獲,從而執(zhí)行相應的操作宣吱,但是屬于異步操作窃这;還有一種前端可以通過 Ajax 發(fā)起 Bridge 請求,可以有同步異步兩種方式征候,不過在 WebView 這層捕獲不到此請求杭攻,只能通過 NSURLProtocol 攔截,所以這也是 WKWebView 的一個限制疤坝。

WebViewJavascriptBridge是一個不錯的JavaScript與Native之間雙向通信的庫憾股,多個廠家包括Facebook在使用匪凉,并且新的版本開始支持WKWebView爬范,對了解Native與JS的交互非常有幫助行疏。

Bridge 設計至關重要,設計的好壞對后續(xù)開發(fā)、前端框架維護會造成深遠的影響衣撬,并且這種影響往往是不可逆的乖订,所以這里需要前端與 Native 好好配合,提供通用的接口具练。對于一個公司來說乍构,往往一套底層框架需要服務于多條業(yè)務線、多個 APP扛点,這就需要在設計的時候考慮好哪些橋接可以在框架層實現(xiàn)(比如跳轉(zhuǎn) Web 頁面哥遮,設置數(shù)據(jù),獲取數(shù)據(jù)陵究,Back 事件眠饮,Close 事件,Alert 彈框铜邮,獲取定位等等)仪召,而與業(yè)務相關的橋接需要框架提供接口讓業(yè)務方去注冊(比如跳轉(zhuǎn) Native 頁面,授權跳轉(zhuǎn)等等)松蒜。

首先設計數(shù)據(jù)格式扔茅,根據(jù) URL 格式:

scheme://host/path?query

與前端約定請求的格式是:

hybrid_scheme://hybrid_api?hybrid_params={params need encode}&callback=callback_ID

客戶端需要根據(jù)約定,在 Bridge 處理結(jié)束后通過 WebView window 對象中的 callback_ID 調(diào)用回調(diào)秸苗,數(shù)據(jù)返回的格式約定為:

{
data : {},
err : 0, //非0提示msg
msg : "success or fail message"
}

Native 解析 Bridge 代碼邏輯:

#pragma mark - YZWebViewDelegate

- (BOOL)webView:(YZWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {

    ......

    if ([_jsBridge webViewShouldStartLoadWithRequest:request navigationType:navigationType]) {
        //符合橋接規(guī)則
        return NO;
    }

    return YES;
}

- (BOOL)webViewShouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
    NSURL *url = [request URL];
    if ([self isWebViewJavascriptBridgeURL:url]) {
        if ([self isWebViewJavascriptBridgeHost:url.host]) {
            YZURLParseModel* model = [self p_parseWebViewRequestWithURL:url];
            if ([[_messageHandlers allKeys] containsObject:model.method]) {
                YZJSBridgeHandler messageHandler = _messageHandlers[model.method];
                if (messageHandler) {
                    messageHandler(model.params);
                }
                .....
            } else {
            ......
            }
        }
        return YES;
    } else {
        return NO;
    }
}


- (void)registerHandler:(NSString *)handlerName handler:(YZJSBridgeHandler)handler {
    if (![_hostsArray containsObject:handlerName]) {
        [_hostsArray addObject:handlerName];
    }
    _messageHandlers[handlerName] = [handler copy];
}

//業(yè)務注冊橋接接口
static NSMutableDictionary* vocationalAsyncJSBridge = nil;

+ (void)setVocationalJSBridgeWithHandler:(NSString *)handlerName handler:(YZJSBridgeHandler)handler {
    if (!vocationalAsyncJSBridge) {
        vocationalAsyncJSBridge = [[NSMutableDictionary alloc] initWithCapacity:1];
    }
    vocationalAsyncJSBridge[handlerName] = handler;
}

+ (void)setVocationalJSBridge:(NSMutableDictionary*)dic {
    if (!vocationalAsyncJSBridge) {
        vocationalAsyncJSBridge = [[NSMutableDictionary alloc] initWithCapacity:1];
    }
    [vocationalAsyncJSBridge addEntriesFromDictionary:dic];
}

公共 Bridge 設計

  • 跳轉(zhuǎn)
    跳轉(zhuǎn)包括三類:
    ① 頁面內(nèi)跳轉(zhuǎn)召娜,無需走 hybrid。
    ② H5 跳轉(zhuǎn)新開 WebView 頁面惊楼。
    ③ H5 跳轉(zhuǎn) Native 頁面玖瘸。

H5 跳轉(zhuǎn)新開 WebView 頁面:

協(xié)議標準 hybrid_scheme://gotoWebview?params={url:~}

__weak typeof(self) weakSelf = self;
//gotoWebview橋接
    [self.jsBridge registerHandler:@"gotoWebview" handler:^(id data) {
        __strong typeof(self) strongSelf = weakSelf;
        if (strongSelf.jsBridge.delegate &&
            [strongSelf.jsBridge.delegate respondsToSelector:@selector(gotoWebPageWithDatas:)]) {
            if ([strongSelf.jsBridge.delegate gotoWebPageWithDatas:data])  //業(yè)務子類進行拓展
                return;
        }
        if ([data isKindOfClass:[NSDictionary class]]) {
            NSString* url = [data objectForKey:@"url"];
            if (url) {
                YZWebViewContainerViewControllerBase* vc = [[strongSelf getCurrentViewController] routeWithParams:@{@"url" : [NSURL URLWithString:url]}];
                [strongSelf.navigationController pushViewController:vc animated:YES];
            }
        }
    }];

H5 跳轉(zhuǎn) Native 頁面:

協(xié)議標準 hybrid_scheme://gotoNative?params={page:~}

__weak typeof(self) weakSelf = self;
//gotoNative橋接
    [self.jsBridge registerHandler:@"gotoNative" handler:^(id data) {
        __strong typeof(self) strongSelf = weakSelf;
        if (strongSelf.jsBridge.delegate &&
            [strongSelf.jsBridge.delegate respondsToSelector:@selector(gotoNativeWithDatas:)]) {
            if ([strongSelf.jsBridge.delegate gotoNativeWithDatas:data]) //業(yè)務子類進行拓展
                return;
        }
    }];
  • 功能 API
    例:Back 事件、Reload 事件胁后、Share 方法等店读。
協(xié)議標準 hybrid_scheme://doAction?params={action:back}
協(xié)議標準 hybrid_scheme://doAction?params={action:reload}
協(xié)議標準 hybrid_scheme://doAction?params={action:share, title:, subtitle:, context:, imgUrl:}

//doAction橋接
    [self.jsBridge registerHandler:@"doAction" handler:^(id data) {
        __strong typeof(self) strongSelf = weakSelf;
        if (strongSelf.jsBridge.delegate &&
            [strongSelf.jsBridge.delegate respondsToSelector:@selector(doActionWithDatas:)]) {
            if ([strongSelf.jsBridge.delegate doActionWithDatas:data]) //業(yè)務子類進行拓展
                return;
        }
        if ([data isKindOfClass:[NSDictionary class]]) {
            NSString* action = [data objectForKey:@"action"];
            if (action) {
                if ([action isEqualToString:@"back"]) {
                    [strongSelf p_back];
                } else if ([action isEqualToString:@"page_reload"]) {
                    [strongSelf p_reload];
                } else if ([action isEqualToString:@"share"]) {
                    [strongSelf p_shareWithParams:data];
                }
            }
        }
    }];
  • Header 組件設計
    對于 Header 組件,需要完成以下功能:
    ① Header 的左側(cè)具有返回鍵和關閉鍵(類似微信等 APP)攀芯,右側(cè)可配置文字和圖標,并且可以控制回調(diào)文虏。
    ② Title 通常在 WebView 加載完成后去獲取 document.title 來顯示侣诺,這里可以做到可配置。
    ③ Title 可以設置一些特別的 TitleView氧秘,比如 SegmentView年鸳、ListView 等等。

以設置右側(cè)按鈕為例:

協(xié)議標準 hybrid_scheme://configNative?params={configs:[{type:nav_item_right, title:, icon_url:, action:, action_parameters: }]}

//configNative橋接
    [self.jsBridge registerHandler:@"configNative" handler:^(id data) {
        __strong typeof(self) strongSelf = weakSelf;
        if (strongSelf.jsBridge.delegate &&
            [strongSelf.jsBridge.delegate respondsToSelector:@selector(configNativeWithDatas:)]) {
            if ([strongSelf.jsBridge.delegate configNativeWithDatas:data])  //業(yè)務子類進行拓展
                return;
        }
        if ([data isKindOfClass:[NSDictionary class]]) {
            NSString* strOfConfigs = data[@"configs"];
            if (strOfConfigs == nil || [strOfConfigs isEqualToString:@""])
                return;
            NSError *error = nil;
            NSArray *arrConfigs = [NSJSONSerialization JSONObjectWithData:[strOfConfigs dataUsingEncoding:NSUTF8StringEncoding]
                                                                  options:NSJSONReadingAllowFragments error:&error];
            if (error || !arrConfigs)
                return;

            for (NSDictionary* config in arrConfigs) {
                NSString *strType = config[@"type"];
                if (!strType || [strType isEqualToString:@""])
                    continue;
                if ([strType isEqualToString:@"nav_item_right"]) {
                    YZNavigationItemConfigModel *model = [[YZNavigationItemConfigModel alloc] initWithDictionary:config error:&error];
                    if (error) {
                        error = nil;
                        continue;
                    }
                    [strongSelf.rightMenuBarButtonItems addObject:[strongSelf p_configRightBarButtonItemsWithModel:model]];
                    if (strongSelf.rightMenuBarButtonItems) {
                        [strongSelf.navigationItem setRightBarButtonItems:strongSelf.rightMenuBarButtonItems];
                    }
                }
            }
        }
    }];

總結(jié)

Hybrid 框架依靠快速迭代丸相,快速試錯在業(yè)務開發(fā)中使用非常廣泛搔确。本文初衷是想為那些準備使用Hybrid框架的人提供設計上的思路,并通過實際的事例去展示結(jié)果,希望對 Hybrid 感興趣的朋友一起來把 Hybrid 一整套解決方案落地并且能夠提供開源膳算。

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末座硕,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子涕蜂,更是在濱河造成了極大的恐慌华匾,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件机隙,死亡現(xiàn)場離奇詭異蜘拉,居然都是意外死亡,警方通過查閱死者的電腦和手機有鹿,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進店門旭旭,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人葱跋,你說我怎么就攤上這事您机。” “怎么了年局?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵际看,是天一觀的道長。 經(jīng)常有香客問我矢否,道長仲闽,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任僵朗,我火速辦了婚禮赖欣,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘验庙。我一直安慰自己顶吮,他們只是感情好,可當我...
    茶點故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布粪薛。 她就那樣靜靜地躺著悴了,像睡著了一般。 火紅的嫁衣襯著肌膚如雪违寿。 梳的紋絲不亂的頭發(fā)上湃交,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天,我揣著相機與錄音藤巢,去河邊找鬼搞莺。 笑死,一個胖子當著我的面吹牛掂咒,可吹牛的內(nèi)容都是我干的才沧。 我是一名探鬼主播迈喉,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼温圆!你這毒婦竟也來了挨摸?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤捌木,失蹤者是張志新(化名)和其女友劉穎油坝,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體刨裆,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡澈圈,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了帆啃。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片瞬女。...
    茶點故事閱讀 39,785評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖努潘,靈堂內(nèi)的尸體忽然破棺而出诽偷,到底是詐尸還是另有隱情,我是刑警寧澤疯坤,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布报慕,位于F島的核電站,受9級特大地震影響压怠,放射性物質(zhì)發(fā)生泄漏眠冈。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一菌瘫、第九天 我趴在偏房一處隱蔽的房頂上張望蜗顽。 院中可真熱鬧,春花似錦雨让、人聲如沸雇盖。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽崔挖。三九已至,卻和暖如春娃闲,著一層夾襖步出監(jiān)牢的瞬間虚汛,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工皇帮, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人蛋辈。 一個月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓属拾,卻偏偏與公主長得像将谊,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子渐白,可洞房花燭夜當晚...
    茶點故事閱讀 44,713評論 2 354

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