ReactNative解決方案研究

前言

2014年8月,faceBook內部傳出用一種新技術開發App的新方案,次年3月,該技術開源并正式發布,它的名字如雷貫耳——“React Native”,我們簡稱為“RN”。在實現媲美 NativeApp 用戶體驗的同時,RN允許Web開發者更多地基于現有經驗組件化開發App,并且具備跨平臺特性,開發效率和成本都相當可觀。不過它也有著一般跨平臺App共有的短板,就是它的兼容性,尤其是針對 國內層出不窮的 Android機型,逐個兼容似乎不太可能。不過好在這類系統提供商不停地升級系統,facebook 也孜孜不倦地提升RN的性能和兼容性,再配合它的強大社區和React生態圈,目前使用RN構建App這個技術已經日趨成熟。國內很多知名互聯網企業也開始應用這門技術,比如 京東App,攜程App,美團App里也混合了RN頁面。

目前主流的應用大體分為三類:NativeApp、WebApp和HybridApp,先羅列一下三者的特點:

目前App三者優缺點

NativeApp特點

  • 性能好
  • 完美的用戶體驗
  • 開發成本高,無法跨平臺
  • 升級困難(審核), 維護成本高

WebApp特點

  • 開發成本低,更新快,版本升級容易,自動升級
  • 跨平臺,”Write Once , Run Anywhere”
  • 無法調用系統級的API
  • 臨時入口,用戶留存度低
  • 性能差,體驗差,設計受限制
  • 相比Native App,Web App體驗中受限于以上5個因素:網絡環境,渲染性能,平臺特性,受限于瀏覽器,系統限制。

HybridApp特點

  • NativeApp 和 WebApp 折中的方案,保留了 NativeApp 和 WebApp 的優點。

  • 但是還是性能差。頁面渲染效率低,在Webview中繪制界面,實現動畫,資源消耗都比較大, 受限于技術, 網速等因素

    關系圖

為了解決上述問題,一套高效率,高性能的跨平臺方案成為了大家熱衷的話題,也就有了下面要比較的 ReactNative 或 Weex 這類解決方案。

ReactNative的特點

優勢相對HybirdApp或者WebApp

  1. 不用Webview,徹底擺脫了Webview讓人不爽的交互和性能問題
  2. 有較強的擴展性,這是因為Native端提供的是基本控件,JS可以自由組合使用
  3. 可以直接調用Native原生的模塊

優勢相對于NativeApp

  1. 可以通過更新遠端JS,直接更新app(熱更新)
  2. 跨平臺特性
  3. 學習成本低,組件式開發,代碼復用性高。

劣勢

[!RN的劣勢]

  1. 擴展性仍然遠遠不如web,也遠遠不如直接寫 Native code
  2. 從Native到Web,要做很多概念轉換,勢必造成雙方都要妥協。比如web要用一套CSS的閹割版,Native通過css-layout拿到最終樣式再轉換成native原生的表達方式(比如iOS的Constraint\origin\Center等屬性),再比如動畫。另外,若Android和iOS都要做相同的封裝,概念轉換就更復雜了。
  3. 內存占用較大。

RN與Weex的選擇

首先我覺得RN和Weex都是很棒的跨平臺解決方案,兩者都有各自的優秀之處,知乎和簡書上關于兩者比較的文章車載斗量,不過推薦RN的居多吧。我本人搞RN開發也1年多了,期間踩過的坑不少,不過基本能通過Google最后解決。這里順帶一提,RN的社區非常強大,不過很大一部分活躍在國外,所以很多優秀的解決方案和三方模塊來自國外,需要一定的外語閱讀能力和Google技巧,算是門檻上比Weex要高一些。我這里不帶太多的情緒,因為沒有實際在項目中運用過 Weex,不過多評價這門框架。我只是從其他角度去考慮:

  1. 技術棧。我們目前的前端技術棧用的React,那么切換到ReactNative的成本應該比 vue技術棧的 Weex 低一些。
  2. 現有積累。我對RN有一定的項目經驗,支持現階段的產品需求難度不大,而且目前國內介紹和總結RN的書籍很多,門檻已經沒有1年前那么高了。
  3. 長遠考慮。RN的版本更新速度快(性能和兼容性方面的改進工作迅速),社區活躍,靈活度高,國內成功案例很多,技術成熟度上應該比 Weex 好一些。

潛在隱患

Android稍微好一些,AppStore對于RN和其他跨平臺App開發始終抱有一絲敵意,從長遠考慮,HybridApp的方案應該保留,可以做為未知情況的降級方案。

RN實現原理簡析

普通的JS-OC通信實際上很簡單,OC向JS傳信息有現成的接口,像Webview提供的stringByEvaluatingJavaScriptFromString方法可以直接在當前context上執行一段JS腳本,并且可以獲取執行后的返回值,這個返回值就相當于JS向OC傳遞信息。ReactNative也是以此為基礎,通過各種手段,實現了在OC定義一個模塊方法,JS可以直接調用這個模塊方法并還可以無縫銜接回調。簡單地說就是:“模塊化,模塊配置表,傳遞ID,封裝調用,事件響應”

IOS端實現原理

OC原理

Android端實現原理

Java原理

RN常用的構建方案

RN目前有兩種構建方案:

(1)RN為主,Native為輔。整個App都由RN構建,Native把功能模塊按照RN的規則進行封裝,然后交給RN進行調用。適合交互場景不太復雜的App,如金融、物流管理類App,基于Labs的微社交類App。

(2) Native為主,RN為輔。整個App構建工作由Native完成,然后把某些功能模塊的入口換成RN,然后控制權交給RN,RN又可以切換回Native。簡單地說也就是替代Hybrid之前的位置。適合做一些功能不需要很酷炫,版本迭代頻繁的業務場景(如專場、活動頁、產品詳情等)。

*RN混合開發技術

也是上面說的第二類構建方案,核心是實現RN與Native之間的通信和互相調用。

IOS原生界面跳轉RN界面

現階段混合開發中,一般就是在原有原生項目基礎上面添加RN開發的頁面。那么這邊我們講解一下從原生界面跳轉到RN頁面的方法。其實是非常簡單的,就是普通push一個 ViewController 即可,在新打開的 ViewController 中加入 RCTRootView 視圖,具體承載RN頁面的控制器的代碼如下:

#import "TwoViewController.h"

#import "RCTRootView.h"

#import "ThreeViewController.h"

@implementation TwoViewController

- (void)viewDidLoad {

  [super viewDidLoad];

  self.title=@"RN界面";

  NSURL *jsCodeLocation = [NSURL URLWithString:@"http://localhost:8081/index.ios.bundle?platform=ios&dev=true"];

  RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation

                                                      moduleName:@"mixedDemo"

                                               initialProperties:nil

                                                   launchOptions:nil];

  self.view=rootView;

}

@end

RN訪問調用IOS原生方法

要實現這個功能,我們首先需用創建一個實現"RCTBridgeModule"協議的RNBridgeModule橋接類,看一下RNBridgeModule.h文件:


#import <Foundation/Foundation.h>

#import "RCTBridgeModule.h"

@interface RNBridgeModule : NSObject<RCTBridgeModule>

@end

接著我們需要在 RNBridgetModule 的實現類中,實現 RCT_EXPORT_MODULE() 宏定義,括號參數不填為默認橋接類的名稱,也可以自定義填寫。該名稱用來指定在 JavaScript 中訪問這個模塊的名稱。

使用Callback進行回調

接下來我們在 RNBridgeModule.m 文件里添加如下的方法:

//RN傳參數調用原生OC,并且返回數據給RN  通過CallBack

RCT_EXPORT_METHOD(RNInvokeOCCallBack:(NSDictionary *)dictionary callback:(RCTResponseSenderBlock)callback){

   NSLog(@"接收到RN傳過來的數據為:%@",dictionary);

   NSArray *events = [[NSArray alloc] initWithObjects:@"張三",@"李四", nil];

   callback(@[[NSNull null], events]);

}

如果原生的方法要被 JavaScript 進行訪問,那么該方法需要使用 RCT_EXPORT_METHOD() 宏定義進行聲明。該聲明的 RNInvokeOCCallBack 方法有兩個參數:第一個參數代表從 JavaScript 傳過來的數據,第二個參數是回調方法,通過該回調方法把原生信息發送到 JavaScript 中。其中上面的 callback 方法中傳入一個參數數組,其實該數組的第一個參數為一個 NSError 對象,如果沒有錯誤返回 null,其余的數據作為該方法的返回值回調給 JavaScritpt

最后我們需要在 JavaScript 文件中進行定義導出在原生封裝的模塊,然后調用封裝方法訪問即可:


var { NativeModules } = require('react-native')

var RNBridgeModule = NativeModules.RNBridgeModule

RNBridgeModule.RNInvokeOCCallBack(

  { 'name': 'jiangqq', 'description': 'http://www.lcode.org' },

  (error, events) => {

    if(error) {

      console.error(error)

    } else {

      this.setState({ events: events })

    }

  }

)

使用Promise進行回調

我們在 RNBridgetModule 的實現類添加如下的方法代碼:


//RN傳參數調用原生OC, 并且返回數據給RN通過Promise

RCT_EXPORT_METHOD(RNInvokeOCPromise:(NSDictionary *)dictionary resolver:(RCTPromiseResolveBlock)resolve

                  rejecter:(RCTPromiseRejectBlock)reject) {

   NSLog(@"接收到RN傳過來的數據為:%@",dictionary);

   NSString *value = [dictionary objectForKey:@"name"];

   if([value isEqualToString:@"jiangqq"]) {

     resolve(@"回調成功啦,Promise...");

   } else {

     NSError *error = [NSError errorWithDomain:@"傳入的name不符合要求,回調失敗啦,Promise..." code:100 userInfo:nil];

     reject(@"100",@"傳入的name不符合要求,回調失敗啦,Promise...",error);

   }

}

這邊定義了 RNInvokeOCPromise 方法,共有三個參數:

  • dictionary: JavaScript傳入的數據
  • resolve: 成功,回調數據
  • reject: 失敗,回調數據
    其中resove方法傳入具體的成功信息即可,但是reject方法必須傳入三個參數分別為,錯誤代碼code ,錯誤信息message以及NSError對象。最終看一下JavaScript中的調用方式:

var { NativeModules } = require('react-native')

var RNBridgeModule = NativeModules.RNBridgeModule

//獲取Promise對象處理

async _updateEvents() {

  try {

    var events = await RNBridgeModule.RNInvokeOCPromise({ 'name': 'jiangqqlmj' })

    this.setState({ events })

  } catch(e) {

    this.setState({ events: e.message })

  }

}

IOS原生訪問調用RN

如果我們需要從iOS原生方法發送數據到JavaScript中,那么可以使用eventDispatcher

首先我們需要在 RCTBridgeModule 的實現中中引入:

#import "RCTBridge.h"

#import "RCTEventDispatcher.h"

@synthesize bridge = _bridge;

接下來就能通過OC原生代碼來訪問JavaScript


self.bridge.eventDispatcher sendAppEventWithName:@"EventReminder" body:@{@"name":[NSString stringWithFormat:@"%@",value],@"errorCode":@"0",@"msg":@"成功"}];

這里補充說明一下 sendAppEventWithName 方法,它包含2個參數:

  • EventReminder:自定義的一個事件名稱
  • 具體摻入JavaScript 的數據信息
OC調用RN的具體代碼
// OC調用RN

RCT_EXPORT_METHOD(VCOpenRN:(NSDictionary *)dictionary) {

  NSString *value = [dictionary objectForKey:@"name"];

  if([value isEqualToString:@"jiangqq"]) {

    [self.bridge.eventDispatcher sendAppEventWithName:@"EventReminder" body:@{@"name":[NSString stringWithFormat:@"%@",value],@"errorCode":@"0",@"msg":@"成功"}];

  } else {

    [self.bridge.eventDispatcher sendAppEventWithName:@"EventReminder" body:@{@"name":[NSString stringWithFormat:@"%@",value],@"errorCode":@"0",@"msg":@"輸入的name不是jiangqq"}];

  }

}

然后在 JavaScript 端進行調用的方法如下:

import { NativeAppEventEmitter } from 'react-native'

// ...省略一部分代碼

  componentDidMount() {

    console.log('開始訂閱通知...')

    subscription = NativeAppEventEmitter.addListener(

      'EventReminder',

      (reminder) => {

        let errorCode = reminder.errorCode

        if (errorCode === 0) {

          this.setState({ msg: reminder.name })

        } else {

          this.setState( {msg: reminder.msg })

        }

      }

    )

  }

  componentWillUnmount() {

    subscription.remove()

  }

RN界面調用ANDROID原生界面

在Android原生開發中,我們知道打開一個Activity一般有兩種方法:顯式和隱式。隱式方法一般通過AndroidManifest配置文件中的Activity Intent-Filter中進行相關攔截配置。那么這邊我們主要講解的是顯示啟動Activity

下面我們這邊在創建繼承ReactContextBaseJavaModuleIntentModule模塊,具體代碼如下:


package com.mixeddemo;

import android.app.Activity;

import android.content.Intent;

import android.text.TextUtils;

import com.facebook.react.bridge.Callback;

import com.facebook.react.bridge.JSApplicationIllegalArgumentException;

import com.facebook.react.bridge.ReactApplicationContext;

import com.facebook.react.bridge.ReactContextBaseJavaModule;

import com.facebook.react.bridge.ReactMethod;

public class IntentModule  extends ReactContextBaseJavaModule {

  public IntentModule(ReactApplicationContext reactContext) {

    super(reactContext);

  }

  @Override

  public String getName() {

    return "IntentModule";

  }

  /**

   * 從JS頁面跳轉到原生activity   同時也可以從JS傳遞相關數據到原生

   * @param name  需要打開的Activity的class

   * @param params

   */

  @ReactMethod

  public void startActivityFromJS(String name, String params){

    try {

      Activity currentActivity = getCurrentActivity();

      if(null!=currentActivity) {

        Class toActivity = Class.forName(name);

        Intent intent = new Intent(currentActivity,toActivity);

        intent.putExtra("params", params);

        currentActivity.startActivity(intent);

      }

    } catch(Exception e) {

      throw new JSApplicationIllegalArgumentException(

        "不能打開Activity : "+e.getMessage());

    }

  }

}

我們在這邊加入了一個@ReactMethod注解的startActivityFromJS方法,該用于在JS代碼進行調用,從JS端傳過來是兩個參數,第一個參數為需要打開的Activityclass,第二個參數為傳遞過來的數據。至于為什么用class,那是因為原生代碼這邊用反射的形式更加方便了,其他方式都有局限性。這邊我們根據傳入的class,然后直接調用startActivity直接打開相應的Activity即可,甚至也可以攜帶一些參數值。具體調用方法方式如下:

const { NativeModules } = require('react-native')
NativeModules.IntentModule.startActivityFromJS("com.hunhedemo.TwoActivity", "我是從JS傳過來的參數信息.456")}

等待原生返回數據

如果我們的RN界面打開了原生界面,同時獲取到原生的返回數據呢?原生Activity中回調的數據一般在onActivityResult中或者其他回調方法中的,但是如果需要返回給RN的數據是通過封裝的原生模塊方法中的Callback進行傳輸的。為了解決這個問題,我們這邊創建一個阻塞的隊列來實現,一旦有原生回調數據加入到隊列中,那么數據就會從阻塞隊列中取出來,再通過回調方法傳入到RN界面中。

下面我們看一下重載了onActivityResult方法的MainActivity類中的寫法:

package com.mixeddemo;

import android.content.Intent;

import com.facebook.react.ReactActivity;

import com.facebook.react.ReactPackage;

import com.facebook.react.shell.MainReactPackage;

import java.util.Arrays;

import java.util.List;

import java.util.concurrent.ArrayBlockingQueue;

public class MainActivity extends ReactActivity {

  //構建一個阻塞的單一數據的隊列

  public static ArrayBlockingQueue<String> mQueue = new ArrayBlockingQueue<String>(1);

  /**

   * Returns the name of the main component registered from JavaScript.

   * This is used to schedule rendering of the component.

   */

  @Override

  protected String getMainComponentName() {

    return "hunheDemo";

  }

  /**

   * Returns whether dev mode should be enabled.

   * This enables e.g. the dev menu.

   */

  @Override

  protected boolean getUseDeveloperSupport() {

    return BuildConfig.DEBUG;

  }

  /**

   * A list of packages used by the app. If the app uses additional views

   * or modules besides the default ones, add more packages here.

   */

  @Override

  protected List<ReactPackage> getPackages() {

    return Arrays.<ReactPackage>asList(

      new MainReactPackage(),

      new IntentReactPackage()

    );

  }

  /**

   * 打開 帶返回的Activity

   * @param requestCode

   * @param resultCode

   * @param data

   */

  @Override

  public void onActivityResult(int requestCode, int resultCode, Intent data) {

    super.onActivityResult(requestCode, resultCode, data);

    if (resultCode == RESULT_OK && requestCode == 200) {

      String result = data.getStringExtra("three_result");

      if (result != null && !result.equals("")) {

        mQueue.add(result);

      } else {

        mQueue.add("無數據啦");

      }

    } else {

      mQueue.add("沒有回調...");

    }

  }

}

然后在IntentModule類中添加startActivityFromJSGetResult方法:

 * 從JS頁面跳轉到Activity界面,并且等待從Activity返回的數據給JS

 * @param className

 * @param successBack

 * @param errorBack

 */

@ReactMethod

public void startActivityFromJSGetResult(String className, int requestCode, Callback successBack, Callback errorBack){

  try {

    Activity currentActivity = getCurrentActivity();

    if(currentActivity!=null) {

      Class toActivity = Class.forName(className);

      Intent intent = new Intent(currentActivity, toActivity);

      currentActivity.startActivityForResult(intent, requestCode);

      //進行回調數據

      successBack.invoke(MainActivity.mQueue.take());

    }

  } catch (Exception e) {

    errorBack.invoke(e.getMessage());

    e.printStackTrace();

  }

}

請注意上面的方法中,啟動Activity是通過startActivityForResult()方法,這樣打開的Activity有數據返回之后,才會調用之前的onActivityResult()方法,然后我們在這個方法中把回調的數據添加到阻塞隊列中。

接下來RN里我們可以通過如下方式進行調用:

import { ToastAndroid } from 'react-native'

const { NativeModules } = require('react-native')

NativeModules.IntentModule.startActivityFromJSGetResult("com.mixedDemo.ThreeActivity", 200,

  (msg) => {

    ToastAndroid.show('JS界面:從Activity中傳輸過來的數據為:' + msg, ToastAndroid.SHORT)

  },

  (result) => {

    ToastAndroid.show('JS界面:錯誤信息為:' + result, ToastAndroid.SHORT)

  }

)

Android原生界面調用RN界面

從上面的介紹,我們發現Android原生界面打開RN界面,還是非常簡單的,直接啟動配置了React Native的界面Activity即可,但我們如果想在打開RN界面同時,從原生Activity中傳點數據過去該怎么實現呢?思路是 在承載RN界面的Activity中獲取當前Intent中的數據,然后通過Callback方法回調即可


package com.mixeddemo;

import android.app.Activity;

import android.content.Intent;

import android.text.TextUtils;

import com.facebook.react.bridge.Callback;

import com.facebook.react.bridge.JSApplicationIllegalArgumentException;

import com.facebook.react.bridge.ReactApplicationContext;

import com.facebook.react.bridge.ReactContextBaseJavaModule;

import com.facebook.react.bridge.ReactMethod;

public class IntentModule  extends ReactContextBaseJavaModule {

  public IntentModule(ReactApplicationContext reactContext) {

    super(reactContext);

  }

  @Override

  public String getName() {

    return "IntentModule";

  }

  /**

   * Activtiy跳轉到JS頁面,傳輸數據

   * @param successBack

   * @param errorBack

   */

  @ReactMethod

  public void dataToJS(Callback successBack, Callback errorBack){

    try {

      Activity currentActivity = getCurrentActivity();

      String result = currentActivity.getIntent().getStringExtra("data");

      if (TextUtils.isEmpty(result)) {

        result = "沒有數據";

      }

      successBack.invoke(result);

    } catch (Exception e) {

      errorBack.invoke(e.getMessage());

    }

  }

}

接著RN端我們就可以通過在componentDidMount()時調用原生封裝的方法去獲取傳過來的參數:

componentDidMount() {

  //進行從Activity中獲取數據傳輸到JS

  NativeModules.IntentModule.dataToJS((msg) => {

    console.log(msg)

    ToastAndroid.show('JS界面:從Activity中傳輸過來的數據為:' + msg, ToastAndroid.SHORT)

  },

  (result) => {

    ToastAndroid.show('JS界面:錯誤信息為:' + result, ToastAndroid.SHORT)

  })

}

如何運用RN

RN作為流行的跨平臺解決方案,不僅適用前端工程師,同樣適合客戶端工程師學習。一個強大的混合開發App需要雙方強強聯手,而RN可以作為中間的Bridge,讓前端和客戶端的銜接更加優雅。

UI控制權接力

針對前端工程師

主要負責跨平臺部分的頁面構建和業務邏輯實現,RN可以理解為用facebook提供的一套React框架開發移動端應用。所以需要扎實的ReactRedux功底,對ReactNative框架提供的組件和API有清晰的認識,并能熟練使用。

針對客戶端工程師

主要負責承載RN的視圖容器和入口,把UI的控制權轉交給RN。除此之外還要掌握Native模塊或SDK封裝成RN組件的技巧,最好能形成一套針對特定業務場景完整的底層組件庫,并配備清晰的說明文檔。

資源推薦

常用的開源組件

  • 微軟熱更新開源平臺 react-native-code-push
  • Google三方統計分析平臺 react-native-google-analytics-bridge
  • 極光三方推送平臺 jpush-react-native
  • sqlite數據庫組件 react-native-sqlite-storage
  • 圖像處理組件 react-native-transformable-image
  • 微信SDK組件(授權、分享、支付) react-native-wechat
  • QQSDK組件 react-native-qq-sdk
  • 支付寶支付組件 react-native-alipay
  • 獲取設備信息組件 react-native-device-info
  • 國際化處理組件 react-native-il8n

疑難雜癥

  • 小米手機調試需要關閉MUI優化引擎
  • Oppo手機、金立手機 小鍵盤在頁面路由跳回后無法再開啟。解決方案是 設置TextInput組件autoFocusedtrue

彩蛋 - RN解決方案研究思維導圖

image.png

@參考

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,572評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,071評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,409評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,569評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,360評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,895評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,979評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,123評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,643評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,559評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,742評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,250評論 5 356
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,981評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,363評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,622評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,354評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,707評論 2 370