fish-redux解析

1.案例实现

阿里巴巴闲鱼出品的fish-redux,针对于大型互联网项目推荐使用的一种状态管理框架,今天来撸一撸,还是老样子实现如下一种双向绑定吧:


00947f39-e932-43d9-ac57-77e498aff72d.gif

1.1.安装插件FishReduxTemplate

使用fish-redux时候先安装下插件FishReduxTemplate,不然一步步写代码估计你要疯;
https://plugins.jetbrains.com/plugin/12139-fishreduxtemplate

1.2.添加依赖

dependencies:
  flutter:
    sdk: flutter
  fish_redux: ^0.3.3

1.3. 根据插件一键生成action/effect/page/reducer/state/view等代码;

生存方式直接参考https://plugins.jetbrains.com/plugin/12139-fishreduxtemplate这个插件的截图;

1.4 Demo代码实现

按照官方案例代码,需要配置一个PageRoutes,其内部pages是key/value,就是路由的意思,其他可以先不用配置;

Widget createApp() {
  final AbstractRoutes routes = PageRoutes(
    pages: <String, Page<Object, dynamic>>{
      'counter': CounterPage(),
    },
  );

  return MaterialApp(
    title: 'FishDemo',
    debugShowCheckedModeBanner: false,
    theme: ThemeData(
      primarySwatch: Colors.blue,
    ),
    home: routes.buildPage('counter', null),
    onGenerateRoute: (RouteSettings settings) {
      return MaterialPageRoute<Object>(builder: (BuildContext context) {
        return routes.buildPage(settings.name, settings.arguments);
      });
    },
  );
}

page.dart代码如下,有一个initState,effect,reducer,view,dependencies需要制定,其中effect是副作用的意思,而reducer是状态管理,view是视图管理,initState就是初始化state;

import 'package:fish_redux/fish_redux.dart';

import 'effect.dart';
import 'reducer.dart';
import 'state.dart';
import 'view.dart';

class CounterPage extends Page<CounterState, Map<String, dynamic>> {
  CounterPage()
      : super(
            initState: initState,
            effect: buildEffect(),
            reducer: buildReducer(),
            view: buildView,
            dependencies: Dependencies<CounterState>(
                adapter: null,
                slots: <String, Dependent<CounterState>>{
                }),
            middleware: <Middleware<CounterState>>[
            ],);

}

先定义一个CounterState的state类,其中必须实现clone,由于redux关键核心是状态state是不可变类型,状态更新是需要拷贝的,因此需要实现Cloneable接口,重写clone方法,达到状态更新,initState就是初始化时候数据,代码如下:

import 'package:fish_redux/fish_redux.dart';

class CounterState implements Cloneable<CounterState> {
  int count;

  @override
  CounterState clone() {
    return CounterState()
      ..count = count;
  }
}

CounterState initState(Map<String, dynamic> args) {
  CounterState state = CounterState();
  state.count = 10;
  return state;
}

然后定义CounterAction去管理Action的生成,这里有4个action,一开始想定义两个,但是发现同一个action只能被 effect或者reducer处理,如果同时定义了则effect先处理:

import 'package:fish_redux/fish_redux.dart';

//TODO replace with your own action
enum CounterAction {
  effect_increment,
  effect_decrement,
  reducer_increment,
  reducer_decrement
}


class CounterActionCreator {
  static Action onEffectIncrement() {
    return const Action(CounterAction.effect_increment);
  }

  static Action onReducerIncrement() {
    return const Action(CounterAction.reducer_increment);
  }

  static Action onEffectDecrement(){
    return const Action(CounterAction.effect_increment);
  }

  static Action onReducerDecrement(){
    return const Action(CounterAction.reducer_decrement);
  }
}

来看官方数据给的一张示意图,其中View触发事件是会先经过Effect,在调用reducer,之后生成新state,更新视图View;


image.png

effect/redux主要定义了2个action和对应的action处理方法,注意这里他们的action是不相同的,现在来看effect/redux代码:

import 'package:fish_redux/fish_redux.dart';
import 'action.dart';
import 'state.dart';

Effect<CounterState> buildEffect() {
  return combineEffects(<Object, Effect<CounterState>>{
    CounterAction.effect_increment: _onIncrement,
    CounterAction.effect_decrement: _onDecrement,
  });
}

void _onIncrement(Action action, Context<CounterState> ctx) {
  print('Effect_onIncrement');
}

void _onDecrement(Action action, Context<CounterState> ctx) {
  print('Effect_onDecrement');
}
import 'package:fish_redux/fish_redux.dart';

import 'action.dart';
import 'state.dart';

Reducer<CounterState> buildReducer() {
  return asReducer(
    <Object, Reducer<CounterState>>{
      CounterAction.reducer_increment: _onIncrement,
      CounterAction.reducer_decrement: _onDecrement,
    },
  );
}

CounterState _onIncrement(CounterState state, Action action) {
  print('Reducer_onIncrement');
  state.count++;
  return state.clone();
}

CounterState _onDecrement(CounterState state, Action action) {
  print('Reducer_onDecrement');
  state.count--;
  return state.clone();
}

最后来看视图层代码,FloatingActionButton和ActioChip点击时候分别dispatch一个InCrement/Decrement的Action:

import 'package:fish_redux/fish_redux.dart';
import 'package:flutter/material.dart';

import 'action.dart';
import 'state.dart';

Widget buildView(
    CounterState state, Dispatch dispatch, ViewService viewService) {
  return Scaffold(
    appBar: AppBar(
      title: Text('Fish-Redux'),
    ),
    body: CounterHomeWapper(
        state: state, dispatch: dispatch, viewService: viewService),
    floatingActionButton: FloatingActionButtonWrapper(
        state: state, dispatch: dispatch, viewService: viewService),
  );
}

class FloatingActionButtonWrapper extends StatelessWidget {
  final CounterState state;
  final Dispatch dispatch;
  final ViewService viewService;

  const FloatingActionButtonWrapper(
      {Key key, this.state, this.dispatch, this.viewService})
      : super(key: key);

  @override
  Widget build(BuildContext context) {
    return FloatingActionButton(
      onPressed: (){
        dispatch(CounterActionCreator.onEffectIncrement());
        dispatch(CounterActionCreator.onReducerIncrement());
      },
      child: Text('${state.count}'),
    );
  }
}

class CounterHomeWapper extends StatelessWidget {
  final CounterState state;
  final Dispatch dispatch;
  final ViewService viewService;

  const CounterHomeWapper(
      {Key key, this.state, this.dispatch, this.viewService})
      : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Center(
      child: ActionChip(
        label: Text('${state.count}'),
        onPressed: () {
          dispatch(CounterActionCreator.onEffectDecrement());
          dispatch(CounterActionCreator.onReducerDecrement());
        }
      ),
    );
  }
}

总体看起来确实做到数据分离,和flutter-redux比起来,没有需要自己state-->model方式,使用起来成本是代码量增加,换来的效益是逻辑解耦,界面清晰;

2.源码分析

2.1.界面加载流程

现在来分析下其代码流程吧,从加载界面起,在main方法有如下:

void main() => runApp(createApp());

而createApp返回的是一个

return MaterialApp(
    title: 'FishDemo',
    debugShowCheckedModeBanner: false,
    theme: ThemeData(
      primarySwatch: Colors.blue,
    ),
    home: routes.buildPage('counter', null),
    onGenerateRoute: (RouteSettings settings) {
      return MaterialPageRoute<Object>(builder: (BuildContext context) {
        return routes.buildPage(settings.name, settings.arguments);
      });
    },
  );

home界面加载的就是布局界面routes.buildPage:

/// Define a basic behavior of routes.
abstract class AbstractRoutes {
  Widget buildPage(String path, dynamic arguments);
}

@(知识体系总结)immutable
class PageRoutes implements AbstractRoutes {
  @override
  Widget buildPage(String path, dynamic arguments) =>
      pages[path]?.buildPage(arguments);
}

Page#buildPage方法会创建_PageWidget,然后被protectedWrapper包裹

 Widget buildPage(P param) => protectedWrapper(_PageWidget<T, P>(
        page: this,
        param: param,
      ));

来看protectedWrapper

abstract class Component<T> extends Logic<T> implements AbstractComponent<T> {
  final WidgetWrapper _wrapper;
  WidgetWrapper get protectedWrapper => _wrapper;
}

Component({
    ...
    WidgetWrapper wrapper,
  })  : assert(view != null),
        _wrapper = wrapper ?? _wrapperByDefault,
        ...

Page({
    ...
    WidgetWrapper wrapper,
    ...
  })  : super(
          ...
          wrapper: wrapper,
          ...
        );

之前案例代码是没有在CounterPage构造方法中并没有定义这个wrapper,那么protectedWrapper使用的是_wrapperByDefault,如下代码发现,其实_wrapperByDefault放回就是里面的Widget;

  static Widget _wrapperByDefault(Widget child) => child;

至此,可以看出Page#buildPage就是生存一个_PageWidget,_PageWidget是继承自StatefulWidget;

 Widget buildPage(P param) => protectedWrapper(_PageWidget<T, P>(
        page: this,
        param: param,
      ));

class _PageWidget<T, P> extends StatefulWidget {
  final Page<T, P> page;
  final P param;

  const _PageWidget({
    Key key,
    @required this.page,
    @required this.param,
  }) : super(key: key);

  @override
  State<StatefulWidget> createState() => _PageState<T, P>();
}

class _PageState<T, P> extends State<_PageWidget<T, P>> {
   @override
  Widget build(BuildContext context) {
    ...
    //构建界面的地方
    return widget.page.buildComponent(
      _store,
      _store.getState,
      bus: _pageBus,
      enhancer: widget.page.enhancer,
    );
  } 
}

Component#buildComponent代码:

abstract class Component<T> extends Logic<T> implements AbstractComponent<T> {
   @override
  Widget buildComponent(
    ...
  }) {
    ...
    return protectedWrapper(
      isPureView()
          ? _PureViewWidget<T>(
              enhancer.viewEnhance(protectedView, this, store),
              getter,
              bus,
            )
          : ComponentWidget<T>(
              component: this,
              getter: _asGetter<T>(getter),
              store: store,
              key: key(getter()),
              bus: bus,
              enhancer: enhancer,
            ),
    );
  }
}

这里会判断isPureView,isPureView代码如下,

 bool isPureView() {
    return protectedReducer == null &&
        protectedEffect == null &&
        protectedDependencies == null;
  }

  Reducer<T> get protectedReducer => _reducer;
  ReducerFilter<T> get protectedFilter => _filter;
  Effect<T> get protectedEffect => _effect;
  Dependencies<T> get protectedDependencies => _dependencies;
  

一般情况下是指定了reducer,effect的,因此使用构建的ComponentWidget;

class ComponentWidget<T> extends StatefulWidget {
   @override
   ComponentState<T> createState() => component.createState(); 
}

class ComponentState<T> extends State<ComponentWidget<T>> {
  Widget build(BuildContext context) => _ctx.buildWidget();
}

class ComponentContext<T> extends LogicContext<T> implements ViewUpdater<T> {
  @override
  Widget buildWidget() {
    //_widgetCache是缓存,一开始是null
    Widget result = _widgetCache;
    if (result == null) {
      //第一次进会执行,view(state, dispatch, this)方法
      result = _widgetCache = view(state, dispatch, this);

      dispatch(LifecycleCreator.build(name));
    }
    return result;
  }
}

view(state, dispatch, this)追踪到如下就是protectedView

final ViewBuilder<T> view;

typedef ViewBuilder<T> = Widget Function(
  T state,
  Dispatch dispatch,
  ViewService viewService,
);

 @override
  ComponentContext<T> createContext(
   ...
  }) {
    assert(bus != null && enhancer != null);
    return ComponentContext<T>(
      ...
      view: enhancer.viewEnhance(protectedView, this, store),
      ...
    );
  }

  ViewBuilder<T> get protectedView => _view;  

其view就是page.dart中CounterPage构造方法中指定的buildView,至此界面从加载到显示过程告一段落;

2.2.Action分发流程

在ComponentContext#buildWidget时候,知道了view(state, dispatch, this)就是调用view.dart中的buildView方法,这里的dispatch是什么呢?

  @override
  Widget buildWidget() {
    Widget result = _widgetCache;
    if (result == null) {
      result = _widgetCache = view(state, dispatch, this);

      dispatch(LifecycleCreator.build(name));
    }
    return result;
  }

ComponentContext extends LogicContext,其dispatch是在LogicContext定义的;

abstract class LogicContext<T> extends ContextSys<T> with _ExtraMixin {
       /// create Dispatch
    _dispatch = logic.createDispatch(
      _effectDispatch,
      logic.createNextDispatch(
        this,
        enhancer,
      ),
      this,
    );
    
    @override
    dynamic dispatch(Action action) => _dispatch(action);
}

LogicContext#createDispatch调用的是父类 AbstractLogic#createDispatch,而实现类是Logic#createDispatch,代码如下

  @override
  Dispatch createDispatch(
    Dispatch effectDispatch,
    Dispatch nextDispatch,
    Context<T> ctx,
  ) =>
      helper.createDispatch<T>(effectDispatch, nextDispatch, ctx);

createDispatch代码如下:

/// return [Dispatch]
Dispatch createDispatch<T>(Dispatch onEffect, Dispatch next, Context<T> ctx) =>
    (Action action) {
      final Object result = onEffect?.call(action);
      if (result == null || result == false) {
        next(action);
      }

      return result == _SUB_EFFECT_RETURN_NULL ? null : result;
    };

这里判断了onEffect是否为空,如果不为空,需要先执行effect中处理Action的方法,之后根据result的值去看是否需要执行next(action);从这里可以得出一个结论:

1.effect,reducer两个都存在时候effect先执行;
2.effect,reducer两个都定义了相同的action时候,只有effect会执行,而reducer不会执行对应代码

接下来看看这个Dispatch onEffect, Dispatch next分别对应什么,这两个Dispatch都是从logic.createDispatch传过来的;

  /// create Dispatch
    _dispatch = logic.createDispatch(
      _effectDispatch,
      logic.createNextDispatch(
        this,
        enhancer,
      ),
      this,
    );

    _effectDispatch = logic.createEffectDispatch(this, enhancer);


  @override
  Dispatch createEffectDispatch(ContextSys<T> ctx, Enhancer<Object> enhancer) {
    return helper.createEffectDispatch<T>(

        /// enhance userEffect
        enhancer.effectEnhance(
          protectedEffect,
          this,
          ctx.store,
        ),
        ctx);
  }

/// return [EffectDispatch]
Dispatch createEffectDispatch<T>(Effect<T> userEffect, Context<T> ctx) {
  return (Action action) {
    final Object result = userEffect?.call(action, ctx);

    //skip-lifecycle-actions
    if (action.type is Lifecycle && (result == null || result == false)) {
      return _SUB_EFFECT_RETURN_NULL;
    }

    return result;
  };
}

  @override
  Dispatch createNextDispatch(ContextSys<T> ctx, Enhancer<Object> enhancer) =>
      helper.createNextDispatch<T>(ctx);

  /// return [NextDispatch]
  Dispatch createNextDispatch<T>(ContextSys<T> ctx) => (Action action) {
      ctx.broadcastEffect(action);
      ctx.store.dispatch(action);
    };

如上代码一步步跟踪:
onEffect对应createEffectDispatch
next对应createNextDispatch

接下来分析createEffectDispatch代码

/// return [EffectDispatch]
Dispatch createEffectDispatch<T>(Effect<T> userEffect, Context<T> ctx) {
  return (Action action) {
    //执行userEffect方法
    final Object result = userEffect?.call(action, ctx);

    //skip-lifecycle-actions
    if (action.type is Lifecycle && (result == null || result == false)) {
      return _SUB_EFFECT_RETURN_NULL;
    }

    return result;
  };
}

userEffect?.call(action, ctx)执行combineEffects响应的代码:

/// for action.type which override it's == operator
/// return [UserEffecr]
Effect<T> combineEffects<T>(Map<Object, SubEffect<T>> map) =>
    (map == null || map.isEmpty)
        ? null
        : (Action action, Context<T> ctx) {
            //1.
            final SubEffect<T> subEffect = map.entries
                .firstWhere(
                  (MapEntry<Object, SubEffect<T>> entry) =>
                      action.type == entry.key,
                  orElse: () => null,
                )
                ?.value;
            //2.
            if (subEffect != null) {
              return subEffect.call(action, ctx) ?? _SUB_EFFECT_RETURN_NULL;
            }

            //skip-lifecycle-actions
            if (action.type is Lifecycle) {
              return _SUB_EFFECT_RETURN_NULL;
            }

            /// no subEffect
            return null;
          };

标记1处中,这里面的map就是在effect.dart中定义的map,如之前定义了{CounterAction.effect_increment: _onIncrement,
CounterAction.effect_decrement: _onDecrement,},然后找到对应map的value,这value就是_onIncrement/_onDecrement方法
标记2处,就直接回调对应的逻辑方法,如果可以正常回调,返回的值就不是一个null或者_SUB_EFFECT_RETURN_NULL;

在回头看如下代码:

Dispatch createDispatch<T>(Dispatch onEffect, Dispatch next, Context<T> ctx) =>
    (Action action) {
      //1.
      final Object result = onEffect?.call(action);
      if (result == null || result == false) {
        //2
        next(action);
      }

      return result == _SUB_EFFECT_RETURN_NULL ? null : result;
    };

标记1处如果effect定义了action,则effect处理,result返回的不会是一个null,那么next就不会被执行;
标记2处如果effect没定义,则reducer处理,result是会返回一个null的,因此没有对应方法处理;

接下来分析next执行action对应的是createNextDispatch:

/// return [NextDispatch]
Dispatch createNextDispatch<T>(ContextSys<T> ctx) => (Action action) {
      ctx.broadcastEffect(action);
      //1
      ctx.store.dispatch(action);
    };

重点关系标记1处,其store创建代码如下:

Store<T> _createStore<T>(final T preloadedState, final Reducer<T> reducer) {
  return Store<T>()
    ..getState = (() => _state)
    ..dispatch = (Action action) {
       ...
       try {
        _isDispatching = true;
        //1
        _state = _reducer(_state, action);
      } finally {
        _isDispatching = false;
      }
      ...
}

重点标记1处,在创建dispatch时候,定义了方法(){},其中会回调_reducer方法,而这_reducer,就是这个asReducer,因此最后调用到如下代码:

Reducer<T> asReducer<T>(Map<Object, Reducer<T>> map) => (map == null ||
        map.isEmpty)
    ? null
    : (T state, Action action) =>
        map.entries
            .firstWhere(
                (MapEntry<Object, Reducer<T>> entry) =>
                    action.type == entry.key,
                orElse: () => null)
                //1
            ?.value(state, action) ??
        state;

重点标记1处,这里.value(state, action) 就触发了reducer.dart中调用的方法;

至此事件分发到effect和reducer逻辑告一段落;

2.3.状态共享实现方式

在之前版本上,状态管理更新state数据源是通过InheritedWidget,在之后版本是手动调用setState方法的;

@deprecated
class PageProvider extends InheritedWidget {}

在ComponentState#initState方法中有两个关键地方:

class ComponentState<T> extends State<ComponentWidget<T>> {
   @mustCallSuper
  @override
  void initState() {
    super.initState();

    /// init context
    _ctx = widget.component.createContext(
      widget.store,
      context,
      () => widget.getter(),
      //1
      markNeedsBuild: () {
        if (mounted) {
          setState(() {});
        }
      },
      bus: widget.bus,
      enhancer: widget.enhancer,
    );

    /// register store.subscribe
    //2
    _ctx.registerOnDisposed(widget.store.subscribe(() => _ctx.onNotify()));

    _ctx.onLifecycle(LifecycleCreator.initState());
  }
}

标记处1:设置markNeedsBuild回调方法设置setState;
标记处2:通过onNotify回调触发markNeedsBuild;

Store<T>()
..subscribe = (_VoidCallback listener) {
      ...
      _listeners.add(listener);
      ...
    }

可以看到ComponentState#initState标记2处会将_ctx.onNotify()存在_listeners中,接下来看dispatch方法回调:

Store<T>()
..dispatch = (Action action) {
      ...
      try {
        _isDispatching = true;
        _state = _reducer(_state, action);
      } finally {
        _isDispatching = false;
      }
      //1
      final List<_VoidCallback> _notifyListeners = _listeners.toList(
        growable: false,
      );
      for (_VoidCallback listener in _notifyListeners) {
        //2
        listener();
      }

      _notifyController.add(_state);
    }

标记处1:_listeners会转化成回调的_notifyListeners;
标记处2:遍历_notifyListeners方法去依次执行listener方法;

这个listener方法中必然有一个就是 _ctx.onNotify();因此在dispatch(Action)完成后,会触发onNotify方法,在看onNotify源码:

  //ComponentContext#onNotify
  @override
  void onNotify() {
    final T now = state;
    if (shouldUpdate(_latestState, now)) {
      _widgetCache = null;

      markNeedsBuild();

      _latestState = now;
    }
  }

其就是经过比较新旧的state,然后调用markNeedsBuild方法,而就是一开始在ComponentState#initState中传入的:

 markNeedsBuild: () {
        if (mounted) {
          setState(() {});
        }
      },

于是乎,在DisPatch(Action)后就自动更新数据到视图View上了,至此,state状态更新到视图View上逻辑也分析完毕;更多fish-redux功能还未尝试,后续会一步步解析其代码;

demo github:https://github.com/1036711153/fish_redux_demo

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

推荐阅读更多精彩内容