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阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345