Flutter路由框架FluroRouter使用與源碼分析

前言

  • Flutter中企業(yè)級路由FluroRouter的使用和原理分析
  • 一些設(shè)計模式和思想

目錄

image.png

路由方式

原生跳轉(zhuǎn)

匿名方式

//Material方式循狰,動畫效果為從下到上漸變彈出頁面
Navigator.of(context).push(
                  MaterialPageRoute(builder: (context) {
                    return DoorbellSettingScreen(deviceId);
                  }),
                );
//IOS風(fēng)格,動畫效果為從右到左彈出頁面
Navigator.of(context).push(
            CupertinoPageRoute(builder: (context){
              return DoorbellSettingScreen();
            })
          );

FluroRouter

引入

1、Depend on it

Add this to your package's pubspec.yaml file:

dependencies:
  fluro: ^2.0.3

2、Install it

You can install packages from the command line:

$ flutter pub get

3、Import it

Now in your Dart code, you can use:

import 'package:fluro/fluro.dart';

使用

1柒凉、初始化

final router = FluroRouter();

2、注冊

var usersHandler = Handler(handlerFunc: (BuildContext context, Map<String, dynamic> params) {
  return UsersScreen(params["id"][0]);
});

void defineRoutes(FluroRouter router) {
  router.define("/users/:id", handler: usersHandler);
  
  // it is also possible to define the route transition to use
  // router.define("users/:id", handler: usersHandler, transitionType: TransitionType.inFromLeft);
}

3篓跛、跳轉(zhuǎn)

router.navigateTo(context, "/users/1234", transition: TransitionType.fadeIn);

對比

原生方式

  • 模塊之間相互耦合膝捞,缺乏層次性
  • 跳轉(zhuǎn)入口分散,統(tǒng)一攔截困難
  • 調(diào)用方要知道跳轉(zhuǎn)class信息愧沟,協(xié)作困難
  • 使用簡單蔬咬、代碼中可以直接跳轉(zhuǎn),閱讀方便

FluroRouter

  • 模塊解耦沐寺,通過url方式跳轉(zhuǎn)
  • 統(tǒng)一管理林艘,方便添加攔截器
  • 不需要知道跳轉(zhuǎn)類信息,可預(yù)寫邏輯混坞,并行提高效率
  • 可以通過onGenerateRoute做一些全局的路由跳轉(zhuǎn)前置處理邏輯
  • 對于不熟悉路由規(guī)則的新同事狐援,跟蹤代碼吃力

結(jié)合項目

UML類圖

UML

其中Routers是一個單例類,不同模塊相機究孕、門鈴等路由頁面在這里注冊啥酱,這里用了依賴倒置原則(DIP),用add方法依賴注入(DI)的方式實現(xiàn)了控制翻轉(zhuǎn)(IOC)厨诸,使Routers和具體的模塊路由頁面解耦镶殷,方便擴展

依賴倒置

設(shè)計模式的六大原則有:

  • Single Responsibility Principle:單一職責(zé)原則

  • Open Closed Principle:開閉原則

  • Liskov Substitution Principle:里氏替換原則

  • Law of Demeter:迪米特法則

  • Interface Segregation Principle:接口隔離原則

  • Dependence Inversion Principle:依賴倒置原則

把這六個原則的首字母聯(lián)合起來(兩個 L 算做一個)就是 SOLID (solid,穩(wěn)定的)泳猬,其代表的含義就是這六個原則結(jié)合使用的好處:建立穩(wěn)定批钠、靈活宇植、健壯的設(shè)計。

依賴倒置原則定義:

1埋心、上層模塊不應(yīng)該依賴底層模塊指郁,它們都應(yīng)該依賴于抽象。
2拷呆、抽象不應(yīng)該依賴于細(xì)節(jié)闲坎,細(xì)節(jié)應(yīng)該依賴于抽象。

pizza的例子:

pizza-1

倒置以后:

pizza-2

圖一的依賴箭頭都是從上往下的茬斧,圖二的箭頭出現(xiàn)了從下往上腰懂,依賴關(guān)系確實“倒置”了

另外,此例子也很好的解釋了“上層模塊不應(yīng)該依賴底層模塊项秉,它們都應(yīng)該依賴于抽象绣溜。”娄蔼,在最開始的設(shè)計中怖喻,高層模塊PizzaStroe直接依賴低層模塊(各種具體的Pizaa),調(diào)整設(shè)計后,高層模塊和低層模塊都依賴于抽象(Pizza)

依賴倒置岁诉、依賴注入的關(guān)系:

image.png

1.控制反轉(zhuǎn)是設(shè)計模式锚沸,遵從了依賴倒置原則
2.依賴注入是實現(xiàn)控制反轉(zhuǎn)的手段

示例代碼

策略模式

策略模式介紹

在軟件開發(fā)中常常遇到這樣的情況:實現(xiàn)一個功能可以有多種算法或策略,我們根據(jù)實際情況選擇不同的算法或策略完成該功能涕癣。例如哗蜈,排序算法,可以使用冒泡排序坠韩、快排距潘、歸并排序等等

針對這種情況,一種常規(guī)的方法是將多種算法寫在一個類中同眯。例如绽昼,需要提供多種排序算法唯鸭,可以將這些算法寫到一個類中须蜗,每一個方法對應(yīng)一個具體的排序算法;當(dāng)然目溉,也可以將這些排序算法封裝在一個統(tǒng)一的方法中明肮,通過if..else..或者case等條件判斷語句來選擇具體的算法。這兩種實現(xiàn)方式我們都可以稱為硬編碼缭付。然而柿估,當(dāng)很多個算法集中在一個類時,這個類就會變的臃腫陷猫,這個類的維護(hù)成本會變高秫舌,在維護(hù)時也更容易引發(fā)錯誤的妖。如果我們需要增加一種新的排序算法,需要修改封裝算法類的源代碼足陨。這就很明顯違反了OCP原則和單一職責(zé)原則

如果將這些算法或者策略抽象出來嫂粟,提供一個統(tǒng)一的接口,不同的算法或者策略有不同的實現(xiàn)類墨缘,這樣在程序客戶端就可以通過注入不同的實現(xiàn)對象來實現(xiàn)算法或者策略的動態(tài)替換星虹,這種模式的可擴展性、可維護(hù)性也就更高镊讼,也就是我們本章要說的策略模式

                                                                                                                                                          ——《Android源碼設(shè)計模式》

策略模式UML:

策略模式

Android 源碼中使用:

插值器

TimeInterpolator宽涌、AccelerateDecelerateInterpolatorDecelerateInterpolator蝶棋、BounceInterpolator

估值器

TypeEvaluator卸亮、ArgbEvaluatorFloatEvaluator

FluroRouter使用前后對比圖:

路由模型

可以看出路由表解除了不同模塊之間的相互依賴玩裙,為項目組件化設(shè)計提供支持嫡良,具體組件化的好處這里不再介紹

源碼分析

關(guān)鍵類

RouteTree

/// A [RouteTree]
class RouteTree {
  // private
  final List<RouteTreeNode> _nodes = <RouteTreeNode>[];
  // addRoute - add a route to the route tree
  void addRoute(AppRoute route) {
  ... //省略部分代碼
  }
  AppRouteMatch matchRoute(String path) {
    ... //省略部分代碼
  }
}

RouteTreeNode

/// A node on [RouteTree]
class RouteTreeNode {
  // constructors
  RouteTreeNode(this.part, this.type);

  // 節(jié)點名
  String part;
  In
  //節(jié)點類型
  RouteTreeNodeType type;
  //路由(要跳轉(zhuǎn)的頁面,只有子節(jié)點有值)
  List<AppRoute> routes = <AppRoute>[];
  //子節(jié)點們
  List<RouteTreeNode> nodes = <RouteTreeNode>[];
  //父親節(jié)點
  RouteTreeNode parent;
    //是否是參數(shù)節(jié)點
  bool isParameter() {
    return type == RouteTreeNodeType.parameter;
  }
}

添加路由

例如献酗,注冊一個:product/camera/device/:dn/settings : CameraSettingScreen()

// addRoute - add a route to the route tree
  void addRoute(AppRoute route) {
    var path = route.route;
    ... //省略部分代碼
    List<String> pathComponents = path.split('/');
    RouteTreeNode parent;
    for (int i = 0; i < pathComponents.length; i++) {
      //遍歷處理路徑寝受,拿到當(dāng)前路徑值
      String component = pathComponents[i];
      //判斷當(dāng)前值是否已創(chuàng)建過節(jié)點,如果有復(fù)用
      RouteTreeNode node = _nodeForComponent(component, parent);
      if (node == null) {
        //沒有創(chuàng)建過節(jié)點罕偎,按類型創(chuàng)建
        RouteTreeNodeType type = _typeForComponent(component);
        node = RouteTreeNode(component, type);
        //設(shè)置當(dāng)前節(jié)點的父親節(jié)點
        node.parent = parent;
        if (parent == null) {
          //如果當(dāng)前父親節(jié)點是null很澄,說明是一顆新樹,添加的總的_nodes樹集合中
          _nodes.add(node);
        } else {
          //添加到父親節(jié)點的nodes中颜及,添加到樹上
          parent.nodes.add(node);
        }
      }
     //當(dāng)前值是路徑當(dāng)中最后一個值時甩苛,再去添加路由(只有葉子節(jié)點,有路由)
      if (i == pathComponents.length - 1) {
        if (node.routes == null) {
          node.routes = [route];
        } else {
          node.routes.add(route);
        }
      }
      //當(dāng)前節(jié)點當(dāng)作父親俏站,循環(huán)處理下一節(jié)點
      parent = node;
    }
  }

_nodeForComponent中判斷了當(dāng)前值節(jié)點是否已經(jīng)創(chuàng)建讯蒲,看下源碼

RouteTreeNode _nodeForComponent(String component, RouteTreeNode parent) {
    //首先將根rootNode集合賦值給當(dāng)前變量
    List<RouteTreeNode> nodes = _nodes;
    if (parent != null) {
      //如果parent不為空,取parent的節(jié)點覆蓋肄扎,從父親節(jié)點下匹配
      // search parent for sub-node matches
      nodes = parent.nodes;
    }
    for (RouteTreeNode node in nodes) {
      //遍歷當(dāng)前父親的所有子節(jié)點墨林,有和自己相同的返回
      if (node.part == component) {
        return node;
      }
    }
    //沒找到,返回null
    return null;
  }

添加的邏輯主要是判斷了當(dāng)前路徑值犯祠,在路由表中存不存在旭等,不存在新建節(jié)點,并掛到對應(yīng)的樹叉上衡载,存在就繼續(xù)向下查找

在代碼中抓取了一段注冊過程debug截圖搔耕,如下

debug

從中可以觀察出以下幾個信息:

1、_nodes是一個樹的集合痰娱,也就是我們的路由表

2弃榨、part部分為樹節(jié)點值菩收,其中:dn節(jié)點為參數(shù)節(jié)點RouteTreeNodeType.parameter

3、只有葉子節(jié)點有路由地址

更直觀表示:

Tree

上圖的部分注冊流程為:

1鲸睛、device_add : DeviceAddScreen()

2坛梁、product/camera/device/:dn/settings : CameraSettingScreen()

3、product/doorbell/device/:dn/settings : DoorBellSettingScreen()

查找路由

下面看下匹配流程

對于 path = /product/camera/device/:dn/settings 的一條索引腊凶,查找要跳轉(zhuǎn)的頁面


/// Similar to [Navigator.push] but with a few extra features.
  Future navigateTo(BuildContext context, String path,
      {bool replace = false,
      bool clearStack = false,
      bool maintainState = true,
      bool rootNavigator = false,
      bool nullOk = false,
      RoutePredicate predicate,
      TransitionType transition,
      Duration transitionDuration,
      RouteTransitionsBuilder transitionBuilder,
      RouteSettings routeSettings}) {
    //從注冊表查找划咐,返回一個routeMatch
    final routeMatch = matchRoute(context, path,
        transitionType: transition,
        transitionsBuilder: transitionBuilder,
        transitionDuration: transitionDuration,
        maintainState: maintainState,
        routeSettings: routeSettings);
    var route = routeMatch.route;
    final completer = Completer<String>();
    Future future = completer.future;
    if (routeMatch.matchType == RouteMatchType.nonVisual) {
      //如果是不可見的Widget,返回不可見
      completer.complete("Non visual route type.");
    } else {
      //如果沒找到钧萍,并且設(shè)置了回調(diào)褐缠,返回默認(rèn)頁
      if (route == null && notFoundHandler != null) {
        route = _notFoundRoute(context, path, maintainState: maintainState);
      }
      if (route != null) {
        final navigator =
            Navigator.of(context, rootNavigator: rootNavigator, nullOk: nullOk);
        if (clearStack) {
          //是否是清棧
          future = navigator.pushAndRemoveUntil(
              route, predicate ?? (check) => false);
        } else {
          //是否是替換
          future = replace
              ? navigator.pushReplacement(route)
              : navigator.push(route);
        }
        completer.complete();
      } else {
        final error = "No registered route was found to handle '$path'.";
        print(error);
        completer.completeError(RouteNotFoundException(error, path));
      }
    }

    return future;
  }

/// Attempt to match a route to the provided [path].
  RouteMatch matchRoute(BuildContext buildContext, String path,
      {RouteSettings routeSettings,
      TransitionType transitionType,
      Duration transitionDuration,
      RouteTransitionsBuilder transitionsBuilder,
      bool maintainState = true}) {
    var settingsToUse = routeSettings;
    ... //省略部分
    final match = _routeTree.matchRoute(path);
    final route = match?.route;
        ... //省略
    // 沒有找到,返回
    if (route == null && notFoundHandler == null) {
      return RouteMatch(
          matchType: RouteMatchType.noMatch,
          errorMessage: "No matching route was found");
    }
    final parameters =
        match?.parameters ?? <String, List<String>>{};
    //處理類型是否是screen
    if (handler.type == HandlerType.function) {
      handler.handlerFunc(buildContext, parameters);
      return RouteMatch(matchType: RouteMatchType.nonVisual);
    }

    //工廠模式根據(jù)動畫類型創(chuàng)建PageRoute
    final creator =
        (RouteSettings routeSettings, Map<String, List<String>> parameters) {
      final isNativeTransition = transition == TransitionType.native ||
          transition == TransitionType.nativeModal;
      if (isNativeTransition) {
        return MaterialPageRoute<dynamic>(
            settings: routeSettings,
            fullscreenDialog: transition == TransitionType.nativeModal,
            maintainState: maintainState,
            builder: (BuildContext context) {
              return handler.handlerFunc(context, parameters);
            });
      } else if (transition == TransitionType.material ||
          transition == TransitionType.materialFullScreenDialog) {
        return MaterialPageRoute<dynamic>(
            settings: routeSettings,
            fullscreenDialog:
                transition == TransitionType.materialFullScreenDialog,
            maintainState: maintainState,
            builder: (BuildContext context) {
              return handler.handlerFunc(context, parameters);
            });
      } else if (transition == TransitionType.cupertino ||
          transition == TransitionType.cupertinoFullScreenDialog) {
        return CupertinoPageRoute<dynamic>(
            settings: routeSettings,
            fullscreenDialog:
                transition == TransitionType.cupertinoFullScreenDialog,
            maintainState: maintainState,
            builder: (BuildContext context) {
              return handler.handlerFunc(context, parameters);
            });
      } else {
        RouteTransitionsBuilder routeTransitionsBuilder;

        if (transition == TransitionType.custom) {
          routeTransitionsBuilder =
              transitionsBuilder ?? route?.transitionBuilder;
        } else {
          routeTransitionsBuilder = _standardTransitionsBuilder(transition);
        }

        return PageRouteBuilder<dynamic>(
          settings: routeSettings,
          maintainState: maintainState,
          pageBuilder: (BuildContext context, Animation<double> animation,
              Animation<double> secondaryAnimation) {
            return handler.handlerFunc(context, parameters);
          },
          transitionDuration: transition == TransitionType.none
              ? Duration.zero
              : transitionDuration ?? route?.transitionDuration,
          reverseTransitionDuration: transition == TransitionType.none
              ? Duration.zero
              : transitionDuration ?? route?.transitionDuration,
          transitionsBuilder: transition == TransitionType.none
              ? (_, __, ___, child) => child
              : routeTransitionsBuilder,
        );
      }
    };
    //返回匹配的可見類型的RouteMatch
    return RouteMatch(
      matchType: RouteMatchType.visual,
      route: creator(settingsToUse, parameters),
    );
  }

FlourRoutermatchRoute方法主要處理了返回是否可見的RouteMatch和原生路由的創(chuàng)建风瘦,具體查找的邏輯在_routeTree.matchRoute(path)

matchRoute

AppRouteMatch matchRoute(String path) {
    String usePath = path;
    if (usePath.startsWith("/")) {
      usePath = path.substring(1);
    }
    //按路徑分割字符串
    List<String> components = usePath.split("/");
    if (path == Navigator.defaultRouteName) {
      components = ["/"];
    }
        //記錄最終匹配的節(jié)點值
    Map<RouteTreeNode, RouteTreeNodeMatch> nodeMatches =
        <RouteTreeNode, RouteTreeNodeMatch>{};
    //將要檢查的節(jié)點队魏,最開始從根root表_nodes匹配
    List<RouteTreeNode> nodesToCheck = _nodes;
    for (String checkComponent in components) {
      //當(dāng)前節(jié)點的匹配值,用來提升效率
      Map<RouteTreeNode, RouteTreeNodeMatch> currentMatches =
          <RouteTreeNode, RouteTreeNodeMatch>{};
      //下一路徑值要匹配的節(jié)點
      List<RouteTreeNode> nextNodes = <RouteTreeNode>[];
      String pathPart = checkComponent;
      Map<String, List<String>> queryMap;
      //處理在路徑上拼接的參數(shù)
      if (checkComponent.contains("?")) {
        var splitParam = checkComponent.split("?");
        pathPart = splitParam[0];
        queryMap = parseQueryString(splitParam[1]);
      }
      //遍歷要匹配的節(jié)點集合
      for (RouteTreeNode node in nodesToCheck) {
        //路徑值相等或者當(dāng)前是參數(shù)節(jié)點万搔,匹配成功
        bool isMatch = (node.part == pathPart || node.isParameter());
        if (isMatch) {
          //處理參數(shù)
          RouteTreeNodeMatch parentMatch = nodeMatches[node.parent];
          RouteTreeNodeMatch match =
              RouteTreeNodeMatch.fromMatch(parentMatch, node);
          if (node.isParameter()) {
            //處理:dn的參數(shù)paramKey = dn胡桨,值等于路徑值
            String paramKey = node.part.substring(1);
            match.parameters[paramKey] = [pathPart];
          }
          //處理?拼接的參數(shù)
          if (queryMap != null) {
            match.parameters.addAll(queryMap);
          }
//          print("matched: ${node.part}, isParam: ${node.isParameter()}, params: ${match.parameters}");
          //記錄當(dāng)前匹配值
          currentMatches[node] = match;
          //記錄下一次要匹配的節(jié)點集合
          if (node.nodes != null) {
            nextNodes.addAll(node.nodes);
          }
        }
      }
      nodeMatches = currentMatches;
      nodesToCheck = nextNodes;
      //如果當(dāng)前路徑值沒有匹配上,說明沒注冊過瞬雹,退出
      if (currentMatches.values.length == 0) {
        return null;
      }
    }
    //當(dāng)前nodeMatches的值為最后一個節(jié)點的值昧谊,把包含的路由信息和參數(shù)包裹成AppRouteMatch返回
    List<RouteTreeNodeMatch> matches = nodeMatches.values.toList();
    if (matches.length > 0) {
      RouteTreeNodeMatch match = matches.first;
      RouteTreeNode nodeToUse = match.node;
//          print("using match: ${match}, ${nodeToUse?.part}, ${match?.parameters}");
      if (nodeToUse != null &&
          nodeToUse.routes != null &&
          nodeToUse.routes.length > 0) {
        List<AppRoute> routes = nodeToUse.routes;
        AppRouteMatch routeMatch = AppRouteMatch(routes[0]);
        routeMatch.parameters = match.parameters;
        return routeMatch;
      }
    }
    return null;
  }

以上就是路由的查找邏輯

總結(jié)

  • 基于FluroRouter的路由可以更高效的管理頁面,并將模塊解耦酗捌,方便后續(xù)擴展
  • 約定好路由規(guī)則后呢诬,可以提前寫好跳轉(zhuǎn)邏輯,并行開發(fā)
  • 樹型結(jié)構(gòu)方便擴展攔截器功能
  • 大量路由頁面時胖缤,共享樹干尚镰,節(jié)省內(nèi)存
  • printTree方便查看頁面間關(guān)系
  • url上拼接參數(shù)方式,傳遞簡單
  • 后期可擴展注解聲明方式哪廓,類似后臺controller
  • 模塊內(nèi)使用匿名方式狗唉,跨模塊使用FluroRouter

參考鏈接

https://pub.dev/packages/fluro

https://flutter.dev/docs/development/ui/navigation

https://flutterchina.club/

http://www.reibang.com/p/c3ce6762257c

https://blog.csdn.net/leila_W/article/details/90105192

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市涡真,隨后出現(xiàn)的幾起案子分俯,更是在濱河造成了極大的恐慌,老刑警劉巖综膀,帶你破解...
    沈念sama閱讀 216,470評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件澳迫,死亡現(xiàn)場離奇詭異局齿,居然都是意外死亡剧劝,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,393評論 3 392
  • 文/潘曉璐 我一進(jìn)店門抓歼,熙熙樓的掌柜王于貴愁眉苦臉地迎上來讥此,“玉大人拢锹,你說我怎么就攤上這事√言” “怎么了卒稳?”我有些...
    開封第一講書人閱讀 162,577評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長他巨。 經(jīng)常有香客問我充坑,道長,這世上最難降的妖魔是什么染突? 我笑而不...
    開封第一講書人閱讀 58,176評論 1 292
  • 正文 為了忘掉前任捻爷,我火速辦了婚禮,結(jié)果婚禮上份企,老公的妹妹穿的比我還像新娘也榄。我一直安慰自己,他們只是感情好司志,可當(dāng)我...
    茶點故事閱讀 67,189評論 6 388
  • 文/花漫 我一把揭開白布甜紫。 她就那樣靜靜地躺著,像睡著了一般骂远。 火紅的嫁衣襯著肌膚如雪囚霸。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,155評論 1 299
  • 那天激才,我揣著相機與錄音邮辽,去河邊找鬼。 笑死贸营,一個胖子當(dāng)著我的面吹牛吨述,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播钞脂,決...
    沈念sama閱讀 40,041評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼揣云,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了冰啃?” 一聲冷哼從身側(cè)響起邓夕,我...
    開封第一講書人閱讀 38,903評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎阎毅,沒想到半個月后焚刚,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,319評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡扇调,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,539評論 2 332
  • 正文 我和宋清朗相戀三年矿咕,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,703評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡碳柱,死狀恐怖捡絮,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情莲镣,我是刑警寧澤福稳,帶...
    沈念sama閱讀 35,417評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站瑞侮,受9級特大地震影響的圆,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜半火,卻給世界環(huán)境...
    茶點故事閱讀 41,013評論 3 325
  • 文/蒙蒙 一略板、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧慈缔,春花似錦叮称、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,664評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至娱节,卻和暖如春挠蛉,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背肄满。 一陣腳步聲響...
    開封第一講書人閱讀 32,818評論 1 269
  • 我被黑心中介騙來泰國打工谴古, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人稠歉。 一個月前我還...
    沈念sama閱讀 47,711評論 2 368
  • 正文 我出身青樓掰担,卻偏偏與公主長得像,于是被迫代替她去往敵國和親怒炸。 傳聞我的和親對象是個殘疾皇子带饱,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,601評論 2 353

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