如何无缝的将Flutter引入现有应用?

image

为什么写thrio?

在早期Flutter发布的时候,谷歌虽然提供了iOS和Android App上的Flutter嵌入方案,但主要针对的是纯Flutter的情形,混合开发支持的并不友好。

所谓的纯RN、纯weex应用的生命周期都不存在,所以也不会存在一个纯Flutter的App的生命周期,因为我们总是有需要复用现有模块。

所以我们需要一套足够完整的Flutter嵌入原生App的路由解决方案,所以我们自己造了个轮子 thrio ,现已开源,遵循MIT协议。

thrio的设计原则

  • 原则一,dart端最小改动接入
  • 原则二,原生端最小侵入
  • 原则三,三端保持一致的API

thrio所有功能的设计,都会遵守这三个原则。下面会逐步对功能层面一步步展开进行说明,后面也会有原理性的解析。

thrio的页面路由

以dart中的 Navigator 为主要参照,提供以下路由能力:

  • push,打开一个页面并放到路由栈顶
  • pop,关闭路由栈顶的页面
  • popTo,关闭到某一个页面
  • remove,删除任意页面

Navigator中的API几乎都可以通过组合以上方法实现,replace 方法暂未提供。

不提供iOS中存在的 present 功能,因为会导致原生路由栈被覆盖,维护复杂度会非常高,如确实需要可以通过修改转场动画实现。

页面的索引

要路由,我们需要对页面建立索引,通常情况下,我们只需要给每个页面设定一个 url 就可以了,如果每个页面都只打开一次的话,不会有任何问题。但是当一个页面被打开多次之后,仅仅通过url是无法定位到明确的页面实例的,所以在 thrio 中我们增加了页面索引的概念,具体在API中都会以 index 来表示,同一个url第一个打开的页面的索引为 1 ,之后同一个 url 的索引不断累加。

如此,唯一定位一个页面的方式为 url + index,在dart中 routename 就是由 '$url.$index' 组合而成。

很多时候,使用者不需要关注 index,只有当需要定位到多开的 url 的页面中的某一个时才需要关注 index。最简单获取 index 的方式为 push 方法的回调返回值。

页面的push

  1. dart 端打开页面
ThrioNavigator.push(url: 'flutter1');
// 传入参数
ThrioNavigator.push(url: 'native1', params: { '1': {'2': '3'}});
// 是否动画,目前在内嵌的dart页面中动画无法取消,原生iOS页面有效果
ThrioNavigator.push(url: 'native1', animated:true);
// 接收锁打开页面的关闭回调
ThrioNavigator.push(
    url: 'biz2/flutter2',
    params: {'1': {'2': '3'}},
    poppedResult: (params) => ThrioLogger.v('biz2/flutter2 popped: $params'),
);
  1. iOS 端打开页面
[ThrioNavigator pushUrl:@"flutter1"];
// 接收所打开页面的关闭回调
[ThrioNavigator pushUrl:@"biz2/flutter2" poppedResult:^(id _Nonnull params) {
    ThrioLogV(@"biz2/flutter2 popped: %@", params);
}];
  1. Android 端打开页面
ThrioNavigator.push(this, "biz1/flutter1",
        mapOf("k1" to 1),
        false,
        poppedResult = {
            Log.e("Thrio", "native1 popResult call params $it")
        }
)
  1. 连续打开页面
  • dart端只需要await push,就可以连续打开页面
  • 原生端需要等待push的result回调返回才能打开第二个页面
  1. 获取所打开页面关闭后的回调参数
  • 三端都可以通过闭包 poppedResult 来获取

页面的pop

  1. dart 端关闭顶层页面
// 默认动画开启
ThrioNavigator.pop();
// 不开启动画,原生和dart页面都生效
ThrioNavigator.pop(animated: false);
// 关闭当前页面,并传递参数给push这个页面的回调
ThrioNavigator.pop(params: 'popped flutter1'),
  1. iOS 端关闭顶层页面
// 默认动画开启
[ThrioNavigator pop];
// 关闭动画
[ThrioNavigator popAnimated:NO];
// 关闭当前页面,并传递参数给push这个页面的回调
[ThrioNavigator popParams:@{@"k1": @3}];
  1. Android 端关闭顶层页面
ThrioNavigator.pop(this, params, animated)

页面的popTo

  1. dart 端关闭到页面
// 默认动画开启
ThrioNavigator.popTo(url: 'flutter1');
// 不开启动画,原生和dart页面都生效
ThrioNavigator.popTo(url: 'flutter1', animated: false);
  1. iOS 端关闭到页面
// 默认动画开启
[ThrioNavigator popToUrl:@"flutter1"];
// 关闭动画
[ThrioNavigator popToUrl:@"flutter1" animated:NO];
  1. Android 端关闭到页面
ThrioNavigator.popTo(context, url, index)

页面的remove

  1. dart 端关闭特定页面
ThrioNavigator.remove(url: 'flutter1');
// 只有当页面是顶层页面时,animated参数才会生效
ThrioNavigator.remove(url: 'flutter1', animated: true);
  1. iOS 端关闭特定页面
[ThrioNavigator removeUrl:@"flutter1"];
// 只有当页面是顶层页面时,animated参数才会生效
[ThrioNavigator removeUrl:@"flutter1" animated:NO];
  1. Android 端关闭特定页面
ThrioNavigator.remove(context, url, index)

thrio的页面通知

页面通知一般来说并不在路由的范畴之内,但我们在实际开发中却经常需要使用到,由此产生的各种模块化框架一个比一个复杂。

那么问题来了,这些模块化框架很难在三端互通,所有的这些模块化框架提供的能力无非最终是一个页面通知的能力,而且页面通知我们可以非常简单的在三端打通。

鉴于此,页面通知作为thrio的一个必备能力被引入了thrio。

发送页面通知

  1. dart 端给特定页面发通知
ThrioNavigator.notify(url: 'flutter1', name: 'reload');
  1. iOS 端给特定页面发通知
[ThrioNavigator notifyUrl:@"flutter1" name:@"reload"];
  1. Android 端给特定页面发通知
ThrioNavigator.notify(url, index, params)

接收页面通知

  1. dart 端接收页面通知

使用 NavigatorPageNotify 这个 Widget 来实现在任何地方接收当前页面收到的通知。

NavigatorPageNotify(
      name: 'page1Notify',
      onPageNotify: (params) =>
          ThrioLogger.v('flutter1 receive notify: $params'),
      child: Xxxx());
  1. iOS 端接收页面通知

UIViewController实现协议NavigatorPageNotifyProtocol,通过 onNotify 来接收页面通知

- (void)onNotify:(NSString *)name params:(NSDictionary *)params {
  ThrioLogV(@"native1 onNotify: %@, %@", name, params);
}
  1. Android 端接收页面通知

Activity实现协议OnNotifyListener,通过 onNotify 来接收页面通知

class Activity : AppCompatActivity(), OnNotifyListener {
    override fun onNotify(name: String, params: Any?) {
    }
}

因为Android activity在后台可能会被销毁,所以页面通知实现了一个懒响应的行为,只有当页面呈现之后才会收到该通知,这也符合页面需要刷新的场景。

thrio的模块化

模块化在thrio里面只是一个非核心功能,仅仅为了实现原则二而引入原生端。

thrio的模块化能力由一个类提供,ThrioModule,很小巧,主要提供了 Module 的注册链和初始化链,让代码可以根据路由url进行文件分级分类。

注册链将所有模块串起来,字母块由最近的父一级模块注册,新增模块的耦合度最低。

初始化链将所有模块需要初始化的代码串起来,同样是为了降低耦合度,在初始化链上可以就近注册模块的页面的构造器,页面路由观察者,页面生命周期观察者等,也可以在多引擎模式下提前启动某一个引擎。

模块间通信的能力由页面通知实现。

mixin ThrioModule {
    /// A function for registering a module, which will call
    /// the `onModuleRegister` function of the `module`.
    ///
    void registerModule(ThrioModule module);
    
    /// A function for module initialization that will call
    /// the `onPageRegister`, `onModuleInit` and `onModuleAsyncInit`
    /// methods of all modules.
    ///
    void initModule();
    
    /// A function for registering submodules.
    ///
    void onModuleRegister() {}

    /// A function for registering a page builder.
    ///
    void onPageRegister() {}

    /// A function for module initialization.
    ///
    void onModuleInit() {}

    /// A function for module asynchronous initialization.
    ///
    void onModuleAsyncInit() {}
    
    /// Register an page builder for the router.
    ///
    /// Unregistry by calling the return value `VoidCallback`.
    ///
    VoidCallback registerPageBuilder(String url, NavigatorPageBuilder builder);

    /// Register observers for the life cycle of Dart pages.
    ///
    /// Unregistry by calling the return value `VoidCallback`.
    ///
    /// Do not override this method.
    ///
    VoidCallback registerPageObserver(NavigatorPageObserver pageObserver);
    
    /// Register observers for route action of Dart pages.
    ///
    /// Unregistry by calling the return value `VoidCallback`.
    ///
    /// Do not override this method.
    ///
    VoidCallback registerRouteObserver(NavigatorRouteObserver routeObserver);
}

thrio的页面生命周期

原生端可以获得所有页面的生命周期,Dart 端只能获取自身页面的生命周期

  1. dart 端获取页面的生命周期
class Module with ThrioModule, NavigatorPageObserver {
  @override
  void onPageRegister() {
    registerPageObserver(this);
  }

  @override
  void didAppear(RouteSettings routeSettings) {}

  @override
  void didDisappear(RouteSettings routeSettings) {}

  @override
  void onCreate(RouteSettings routeSettings) {}

  @override
  void willAppear(RouteSettings routeSettings) {}

  @override
  void willDisappear(RouteSettings routeSettings) {}
}
  1. iOS 端获取页面的生命周期
@interface Module1 : ThrioModule<NavigatorPageObserverProtocol>

@end

@implementation Module1

- (void)onPageRegister {
  [self registerPageObserver:self];
}

- (void)onCreate:(NavigatorRouteSettings *)routeSettings { }

- (void)willAppear:(NavigatorRouteSettings *)routeSettings { }

- (void)didAppear:(NavigatorRouteSettings *)routeSettings { }

- (void)willDisappear:(NavigatorRouteSettings *)routeSettings { }

- (void)didDisappear:(NavigatorRouteSettings *)routeSettings { }

@end

thrio的页面路由观察者

原生端可以观察所有页面的路由行为,dart 端只能观察 dart 页面的路由行为

  1. dart 端获取页面的路由行为
class Module with ThrioModule, NavigatorRouteObserver {
  @override
  void onModuleRegister() {
    registerRouteObserver(this);
  }

  @override
  void didPop(
    RouteSettings routeSettings,
    RouteSettings previousRouteSettings,
  ) {}

  @override
  void didPopTo(
    RouteSettings routeSettings,
    RouteSettings previousRouteSettings,
  ) {}

  @override
  void didPush(
    RouteSettings routeSettings,
    RouteSettings previousRouteSettings,
  ) {}

  @override
  void didRemove(
    RouteSettings routeSettings,
    RouteSettings previousRouteSettings,
  ) {}
}
  1. iOS 端获取页面的路由行为
@interface Module2 : ThrioModule<NavigatorRouteObserverProtocol>

@end

@implementation Module2

- (void)onPageRegister {
  [self registerRouteObserver:self];
}

- (void)didPop:(NavigatorRouteSettings *)routeSettings
 previousRoute:(NavigatorRouteSettings * _Nullable)previousRouteSettings {
}

- (void)didPopTo:(NavigatorRouteSettings *)routeSettings
   previousRoute:(NavigatorRouteSettings * _Nullable)previousRouteSettings {
}

- (void)didPush:(NavigatorRouteSettings *)routeSettings
  previousRoute:(NavigatorRouteSettings * _Nullable)previousRouteSettings {
}

- (void)didRemove:(NavigatorRouteSettings *)routeSettings
    previousRoute:(NavigatorRouteSettings * _Nullable)previousRouteSettings {
}

@end

thrio的额外功能

iOS 显隐当前页面的导航栏

原生的导航栏在 dart 上一般情况下是不需要的,但切换到原生页面又需要把原生的导航栏置回来,thrio 不提供的话,使用者较难扩展,我之前在目前一个主流的Flutter接入库上进行此项功能的扩展,很不流畅,所以这个功能最好的效果还是 thrio 直接内置,切换到 dart 页面默认会隐藏原生的导航栏,切回原生页面也会自动恢复。另外也可以手动隐藏原生页面的导航栏。

viewController.thrio_hidesNavigationBar = NO;

支持页面关闭前弹窗确认的功能

如果用户正在填写一个表单,你可能经常会需要弹窗确认是否关闭当前页面的功能。

在 dart 中,有一个 Widget 提供了该功能,thrio 完好的保留了这个功能。

WillPopScope(
    onWillPop: () async => true,
    child: Container(),
);

在 iOS 中,thrio 提供了类似的功能,返回 NO 表示不会关闭,一旦设置会将侧滑返回手势禁用

viewController.thrio_willPopBlock = ^(ThrioBoolCallback _Nonnull result) {
  result(NO);
};

关于 FlutterViewController 的侧滑返回手势,Flutter 默认支持的是纯Flutter应用,仅支持单一的 FlutterViewController 作为整个App的容器,内部已经将 FlutterViewController 的侧滑返回手势去掉。但 thrio 要解决的是 Flutter 与原生应用的无缝集成,所以必须将侧滑返回的手势加回来。

thrio的设计解析

目前开源 Flutter 嵌入原生的库,主要的还是通过切换 FlutterEngine 上的原生容器来实现的,这是 Flutter 原本提供的原生容器之上最小改动而实现,需要小心处理好容器切换的时序,否则在页面导航时会产生崩溃。基于 Flutter 提供的这个功能, thrio 构建了三端一致的页面管理API。

dart 的核心类

dart 端只管理 dart页面

  1. 基于 RouteSettings 进行扩展,复用现有的字段
  • name = url.index
  • isInitialRoute = !isNested
  • arguments = params
  1. 基于 MaterialPageRoute 扩展的 NavigatorPageRoute
  • 主要提供页面描述和转场动画的是否配置的功能
  1. 基于 Navigator 扩展,封装 NavigatorWidget,提供以下方法
  Future<bool> push(RouteSettings settings, {
    bool animated = true,
    NavigatorParamsCallback poppedResult,
  });
  
  Future<bool> pop(RouteSettings settings, {bool animated = true});
  
  Future<bool> popTo(RouteSettings settings, {bool animated = true});

  Future<bool> remove(RouteSettings settings, {bool animated = false});
  
  1. 封装 ThrioNavigator 路由API
abstract class ThrioNavigator {
    /// Push the page onto the navigation stack.
    ///
    /// If a native page builder exists for the `url`, open the native page,
    /// otherwise open the flutter page.
    ///
    static Future<int> push({
        @required String url,
        params,
        bool animated = true,
        NavigatorParamsCallback poppedResult,
    });
    
    /// Send a notification to the page.
    ///
    /// Notifications will be triggered when the page enters the foreground.
    /// Notifications with the same `name` will be overwritten.
    /// 
    static Future<bool> notify({
        @required String url,
        int index,
        @required String name,
        params,
    });
    
    /// Pop a page from the navigation stack.
    ///
    static Future<bool> pop({params, bool animated = true})

    static Future<bool> popTo({
        @required String url,
        int index,
        bool animated = true,
    });
    
    /// Remove the page with `url` in the navigation stack.
    ///  
    static Future<bool> remove({
        @required String url,
        int index,
        bool animated = true,
    });
}

iOS 的核心类

  1. NavigatorRouteSettings 对应于 dart 的 RouteSettings 类,并提供相同数据结构

@interface NavigatorRouteSettings : NSObject

@property (nonatomic, copy, readonly) NSString *url;

@property (nonatomic, strong, readonly) NSNumber *index;

@property (nonatomic, assign, readonly) BOOL nested;

@property (nonatomic, copy, readonly, nullable) id params;

@end

  1. NavigatorPageRoute 对应于 dart 的 NavigatorPageRoute
  • 存储通知、页面关闭回调、NavigatorRouteSettings
  • route的双向链表
  1. 基于 UINavigationController 扩展,功能类似 dart 的 NavigatorWidget
  • 提供一些列的路由内部接口
  • 并能兼容非 thrio 体系内的页面
  1. 基于 UIViewController 扩展
  • 提供 FlutterViewController 容器上的 dart 页面的管理功能
  • 提供 popDisable 等功能
  1. 封装 ThrioNavigator 路由API
@interface ThrioNavigator : NSObject

/// Push the page onto the navigation stack.
///
/// If a native page builder exists for the url, open the native page,
/// otherwise open the flutter page.
///
+ (void)pushUrl:(NSString *)url
         params:(id)params
       animated:(BOOL)animated
         result:(ThrioNumberCallback)result
   poppedResult:(ThrioIdCallback)poppedResult;

/// Send a notification to the page.
///
/// Notifications will be triggered when the page enters the foreground.
/// Notifications with the same name will be overwritten.
///
+ (void)notifyUrl:(NSString *)url
            index:(NSNumber *)index
             name:(NSString *)name
           params:(id)params
           result:(ThrioBoolCallback)result;

/// Pop a page from the navigation stack.
///
+ (void)popParams:(id)params
         animated:(BOOL)animated
           result:(ThrioBoolCallback)result;

/// Pop the page in the navigation stack until the page with `url`.
///
+ (void)popToUrl:(NSString *)url
           index:(NSNumber *)index
        animated:(BOOL)animated
          result:(ThrioBoolCallback)result;

/// Remove the page with `url` in the navigation stack.
///
+ (void)removeUrl:(NSString *)url
            index:(NSNumber *)index
         animated:(BOOL)animated
           result:(ThrioBoolCallback)result;

@end

dart 与 iOS 路由栈的结构

thrio-architecture
  1. 一个应用允许启动多个Flutter引擎,可让每个引擎运行的代码物理隔离,按需启用,劣势是启动多个Flutter引擎可能导致资源消耗过多而引起问题;
  2. 一个Flutter引擎通过切换可以匹配到多个FlutterViewController,这是Flutter优雅嵌入原生应用的前提条件
  3. 一个FlutterViewController可以内嵌多个Dart页面,有效减少单个FlutterViewController只打开一个Dart页面导致的内存消耗过多问题,关于内存消耗的问题,后续会有提到。

dart 与 iOS push的时序图

thrio-push
  1. 所有路由操作最终汇聚于原生端开始,如果始于 dart 端,则通过 channel 调用原生端的API
  2. 通过 url+index 定位到页面
  3. 如果页面是原生页面,则直接进行相关操作
  4. 如果页面是 Flutter 容器,则通过 channel 调用 dart 端对应的路由 API
  5. 接4步,如果 dart 端对应的路由 API 操作完成后回调,如果成功,则执行原生端的路由栈同步,如果失败,则回调入口 API 的result
  6. 接4不,如果 dart 端对应的路由 API操作成功,则通过 route channel 调用原生端对应的 route observer,通过 page channel 调用原生端对应的 page observer。

dart 与 iOS pop的时序图

thrio-pop
  1. pop 的流程与 push 基本一致;
  2. pop 需要考虑页面是否可关闭的问题;
  3. 但在 iOS 中,侧滑返回手势会导致问题, popViewControllerAnimated: 会在手势开始的时候调用,导致 dart 端的页面已经被 pop 掉,但如果手势被放弃了,则导致两端的页面栈不一致,thrio 已经解决了这个问题,具体流程稍复杂,源码可能更好的说明。

dart 与 iOS popTo的时序图

thrio-popTo.png
  1. popTo 的流程与 push 基本一致;
  2. 但在多引擎模式下,popTo需要处理多引擎的路由栈同步的问题;
  3. 另外在 Dart 端,popTo实际上是多个pop或者remove构成的,最终产生多次的didPop或didRemove行为,需要将多个pop或remove组合起来形成一个didPopTo行为。

dart 与 iOS remove的时序图

thrio-remove.png
  1. remove 的流程与 push 基本一致。

总结

目前 Flutter 接入原生应用主流的解决方案应该是boost,笔者的团队在项目深度使用过 boost,也积累了很多对 boost 改善的需求,遇到的最大问题是内存问题,每打开一个 Flutter 页面的内存开销基本到了很难接受的程度,thrio把解决内存问题作为头等任务,最终效果还是不错的,比如以连续打开 5 个 Flutter 页面计算,boost 的方案会消耗 91.67M 内存,thrio 只消耗 42.76 内存,模拟器上跑出来的数据大致如下:

demo 启动 页面 1 页面 2 页面 3 页面 4 页面 5
thrio 8.56 37.42 38.88 42.52 42.61 42.76
boost 6.81 36.08 50.96 66.18 78.86 91.67

同样连续打开 5 个页面的场景,thrio 打开第一个页面跟 boost 耗时是一样的,因为都需要打开一个新的 Activity,之后 4 个页面 thrio 会直接打开 Flutter 页面,耗时会降下来,以下单位为 ms:

demo 页面 1 页面 2 页面 3 页面 4 页面 5
thrio 242 45 39 31 37
boost 247 169 196 162 165

当然,thrio 跟 boost 的定位还是不太一样的,thrio 更多的偏向于解决我们业务上的需求,尽量做到开箱即用。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容

  • 亲爱的 你可曾知道时光飞逝 你可曾知你之前所谓的未来 都已经变成了过去 你可曾想起你儿时梦想 你可曾记得最初的美好...
    秋艳洞见阅读 181评论 0 0
  • 这是枣庄方糖读书会第35场复盘。 2019年第二场。 这一场我们共读《应用题的关键难点》 寻了一处好地方,...
    欢喜欣然阅读 539评论 0 3
  • 西湖上的摇橹人,保温杯里一泡上好的狮峰龙井,悠然地摇着。 天南海北的人,云淡风轻地诉说,一个时辰,伴着夕照雷锋,或...
    路灯下的雪人阅读 299评论 0 3