ReactNative系列(五):react-natigation 3.x全解(中)

ReactNative.jpg

ReactNative整理:《ReactNative系列》

内容目录

1、navigationOptionsAppContainer
2、导航器属性参数
3、StackNavigator用法详解
4、SwitchNavigator用法详解
5、BottomTabNavigator用法详解
6、DrawerNavigator用法详解
7、结语


一、navigationOptionsAppContainer

AppContainer:负责管理应用的state并将顶层的navigator链接到整个应用环境。创建各种navigator时,已经将要用到的页面整合到一起,并生成了一个导航组件,但是该组件并没有接入到应用中,所以需要AppContainer将导航组件包裹,同时链接到整个应用环境,这样生成的导航器就可以使用了。

1、关于createAppContainer的简单示例:

import { createAppContainer, createStackNavigator } from 'react-navigation';

const StackControllers = createStackNavigator({
  // 路由配置对象
}, {
  // 导航属性配置
});

// 将生成的导航器组件 StackControllers 包裹到 Container 中
const StackContainer = createAppContainer(StackControllers);

// 现在 StackContainer 变成了 React 渲染的主要组件
export default StackContainer;

2、React Native 中的 createAppContainer prop:

<StackContainer
   onNavigationStateChange={this.handleNavigationChange()}
   uriPrefix={'/app'}
/>
  • onNavigationStateChange(prevState, newState, action)
    每当导航器管理的navigation state 发生变化时,都会调用该函数。它接收之前的 state、navigation 的新 state 以及发布状态更改的 action。 默认情况下,它将 state 的更改打印到控制台。
  • uriPrefix
    应用可能会处理的URI前缀,在处理深度链接以提取传递给路由器的路径时使用。

navigationOptions:导航器内部页面的选项配置。可以在导航器RouteConfigsNavigatorConfig中配置;也可以在页面中配置。优先级为:RouteConfigs配置 > 页面中navigationOptions配置 > NavigatorConfig配置。
以StackNavigator为例:

// 导航器中配置
const StackControllers = createStackNavigator({
  stack1: {
    screen: StackController1,
    navigationOptions: () => ({
      title: 'controller1'
    })
  },
  stack2: {
    screen: StackController2
  }
}, {
  initialRouteName: 'stack1',
});
// 页面中配置 navigationOptions
export default class StackController1 extends Component {
  static navigationOptions = {
    title: 'StackController1'
  };
  ...
}

如果RouteConfigs中和页面中都存在navigationOptions,则以RouteConfigs中的配置为准。
navigationOptions是用来配置页面头部或者手势等属性的,RouteConfigs和页面中静态配置,是针对单个页面的;而在XXNavigatorConfig中配置,则是针对导航内所有screen生效。

二、 导航器属性参数

1、Navigation prop reference

应用中的每个页面组件都会自动提供navigation prop,该属性包含便捷的方法用于触发导航操作,如下所示:

  • this.props.navigation
    • navigate - 跳转到另一个屏幕,计算出需要执行的操作
    • goBack - 关闭活动屏幕并在堆栈中向后移动
    • addListener - 订阅导航生命周期的更新
    • isFocused - 如果屏幕获取焦点,函数返回true,否则返回false
    • state - 当前state,路由状态
    • setParams - 更改路由的参数
    • getParam - 获取具有回退功能的特定参数
    • dispatch - 向路由发送action
    • dangerouslyGetParent - 返回父级navigator的函数

重点是要强调navigation属性不会传递给所有组件;只有screen页面组件会自动收到此属性。

2、Navigator-dependent functions

this.props.navigation上有些取决于当前navigator的附加函数
如果是StackNavigator,除了navigategoBack,还提供了如下方法:

  • this.props.navigation
    • push - 推一个新的路由到堆栈
    • pop - 返回堆栈中的上一个页面
    • popToTop - 跳转到堆栈中最顶层的页面
    • replace - 用新路由替换当前路由
    • reset - 擦除整个导航状态,并将其替换为多个操作的结果
    • dismiss - 关闭当前堆栈

如果是DrawerNavigator,则还可以使用以下选项:

  • this.props.navigation
    • openDrawer - 打开
    • closeDrawer - 关闭
    • toggleDrawer - 切换,如果是打开则关闭,反之亦然

三、StackNavigator用法详解

堆栈式导航:提供一种在每个新屏幕放置在堆栈顶部的屏幕之间转换的方法。该导航器是以栈的形式管理页面,每新建一个页面都会压入栈中,最新创建的页面在栈顶。默认情况下的配置具有熟悉的Android和iOS外观&效果:iOS上从右侧滑入,Android上从底部淡入。
StackNavigator配置代码示例:

/**
 * 堆栈导航:
 * 将页面配置到导航器中,不能跳转到导航没有配置的页面
 * @type {NavigationContainer}
 */
const StackControllers = createStackNavigator({
  // RouteConfig 配置
  stack1: {
    screen: StackController1,
    navigationOptions: {
      title: 'Controller1',
      headerStyle: {
        backgroundColor: '#ffffff'
      }
    }
  },
  stack2: {
    screen: StackController2,
    navigationOptions: {
      title: '页面2'
    }
  },
  stack3: {
    screen: StackController3
  },
  stack4: {
    screen: StackController4
  }
}, { // 
  initialRouteName: 'stack1',
  defaultNavigationOptions: {
    headerStyle: {
      backgroundColor: 'grey',
    },
    headerTintColor: 'blue',
    headerTitleStyle: {
      fontSize: 20,
    },
  }
});

const StackContainer = createAppContainer(StackControllers);

export default StackContainer;

RouteConfig - 配置的页面必须含有screen属性值,用来定义页面标识;navigationOptions用来初始化页面的一些配置,例如:Header样式,手势等。
需要注意的是:
StackNavigatorConfig - 配置中用的是defaultNavigationOptions控制导航内所有页面Header展示;用navigationOptions配置没有效果,react-navigation版本号是3.8.1,各位同学可以自己尝试下。
initialRouteName属性值是配置导航器的默认页面;在没有设置的情况下,默认为RouteConfig中配置的第一个页面。
页面代码:

/**
 * 展示页面
 * 跳转方法:handleOnPress
 * 返回方法:backPress
 */
export default class StackController2 extends Component {
  constructor(props) {
    super(props);
    this.handleOnPress = this.handleOnPress.bind(this);
  }

  componentDidMount() {
    console.log('-did-mount-stack2--');
  }

  componentWillUnmount() {
    console.log('-un-mount-stack2--');
  }

  /* 点击跳转到第三个页面 stack3 */
  handleOnPress() {
    this.props.navigation.navigate('stack3');
  }

  /* 点击返回上层页面 */
  backPress() {
    this.props.navigation.goBack();
  }
  
  render() {
    return(
      <View style={pageStyle.container}>
        <Text
          style={pageStyle.contentText}
          onPress={this.handleOnPress}
        >
          Controller2 To Controller3
        </Text>
        <Text
          style={pageStyle.backText}
          onPress={this.backPress}
        >
          返回
        </Text>
      </View>
    );
  }
}

看过ReactNative官方文档的同学应该知道:
常用的点击事件组件有TouchableOpacityButtonText,它们都包含onPress属性,可以调用点击方法。我这里用的是Text组件实现点击切换页面和页面返回。
StackNavigator的页面创建与跳转相对比较简单,比较麻烦的是多层页面的关闭。这里给出几种多层页面退出的解决办法:
例如:从A -> B -> C -> D页面,要从D返回到A
1、利用页面key
导航器中每个页面都包含navigation属性值,可以通过this.props.navigation取到,该属性中有许多方法和数据,其中包括state。在state中包含keyrouteNameparamskey - 是页面在导航器中的唯一标识ID,根据这个标识能找到对应页面;routeName - 是当前页面在导航器中配置的路由名称;params - 传递的参数,是由上一个页面传入。
注意:从D返回到A,用到的是B页面的key值,而不是A的。

 /**
   * 点击跳转到第三个页面 stack3
   * 其中 B: navigate.state.key 为传递的参数
   */
  handleOnPress() {
    const navigate = this.props.navigation;
    navigate.navigate('stack3', {
      B: navigate.state.key
    });
  }

可以用类似的代码结构做出四个页面,测试跳转和返回。

/**
   * 最后一个页面的点击事件
   * 点击返回到A页面
   */
  handleOnPress() {
    const navigate = this.props.navigation;
    navigate.goBack(navigate.state.params.B);
  }

这样,在点击最后一个页面的文本时,就能返回到A页面,而且没有多余的退栈动画。
2、拦截路由actionstate改变
createStackNavigator关联到的源码:创建时传递两个参数生成的是NavigationContainer,而该NavigationContainer是个接口,包含router属性。继续向下查看源码发现router的value值是NavigationRouter,其中有导航调用的方法,getStateForAction就是我们需要用到的,它能监听交互的action和导航的state。下面摘出来源码:

...
// 堆栈导航的创建
export function createStackNavigator(
  routeConfigMap: NavigationRouteConfigMap,
  stackConfig?: StackNavigatorConfig
): NavigationContainer;
...

// NavigationContainer接口及属性
export interface NavigationContainer extends React.ComponentClass<
      NavigationContainerProps & NavigationNavigatorProps<any>
    > {
    new (
      props: NavigationContainerProps & NavigationNavigatorProps<any>,
      context?: any
    ): NavigationContainerComponent;

    router: NavigationRouter<any, any>;
    screenProps: ScreenProps;
    navigationOptions: any;
    state: { nav: NavigationState | null };
}

// NavigationRouter -- 导航路由接口
export interface NavigationRouter<State = NavigationState, Options = {}> {
    /**
     * The reducer that outputs the new navigation state for a given action, with
     * an optional previous state. When the action is considered handled but the
     * state is unchanged, the output state is null.
     */
    getStateForAction: (
      action: NavigationAction,
      lastState?: State
    ) => State | null;

    /**
     * Maps a URI-like string to an action. This can be mapped to a state
     * using `getStateForAction`.
     */
    getActionForPathAndParams: (
      path: string,
      params?: NavigationParams
    ) => NavigationAction | null;

    getPathAndParamsForState: (
      state: State
    ) => {
      path: string;
      params?: NavigationParams;
    };

    getComponentForRouteName: (routeName: string) => NavigationComponent;

    getComponentForState: (state: State) => NavigationComponent;

    /**
     * Gets the screen navigation options for a given screen.
     *
     * For example, we could get the config for the 'Foo' screen when the
     * `navigation.state` is:
     *
     *  {routeName: 'Foo', key: '123'}
     */
    getScreenOptions: NavigationScreenOptionsGetter<Options>;
}

了解依据之后,来修改我们自己的代码:

// 定义拦截器,用来将修改的action和state作为新的数据传入
const StackInterceptor = StackControllers.router.getStateForAction;
/**
 * 拦截思路:
 *  1、过滤action,只有是 action.type === 'Navigation/BACK' 时拦截处理
 *  2、根据拦截到的action中的key值,在state.routes中找到对应数据
 *  3、找到的对应的route数据下标index,取 index + 1 的数据的key值赋给 action.key
 *  4、把新修改的 action 和 state 传入定义好的拦截器中
 */
StackControllers.router.getStateForAction = (action, state) => {
  console.log(action, '---action--');
  console.log(state, '---state--');
  let nextAction = action;
  if (state && action && action.type === 'Navigation/BACK') {
    const routeLength = state.routes.length;
    const isExist = state.routes.findIndex(route => action.key === route.routeName);
    if (isExist > -1 && isExist + 1 <= routeLength) {
      nextAction = {
        ...action,
        key: state.routes[isExist + 1].key
      };
    }
    console.log(nextAction, '--nextAction--');
    console.log(state, '--nextstate---');
  }
  return StackInterceptor(nextAction, state);
};

该方法中,我们用到的goBack方法能直接传递导航器中配置的路由名称为参数。例如:this.props.navigation.goBack('stack1')。同样的,返回A页面用到的还是B页面的Key值,所以在拦截查找位置的时候要取到stack1的下标index的下一个位置index + 1的数据才行。
依据同样的方法,可以通过修改state.routes的数据来实现由D -> A,我们通过日志可以看到 B、C、D三个页面的componentWillUnmount方法都没有执行,虽然页面仍然能创建跳转和关闭,但是毕竟影响了组件的生命周期,所以不推荐大家使用。修改action.keystate.routes都能达到相同的效果,但建议使用前者更稳妥。

另:其实还可以通过修改源码方法实现多层页面返回。3.x版本之前可以通过修改react-navigation源码的StackRouter.js中针对action.type === NavigationActions.BACK修改返回方式;但是3.x之后版本光修改该文件不生效了,还需要修改别的地方(还在摸索)。而且开发中会碰到有些问题需要删除node_modules文件夹重新npm install的情况,这时node_modules文件夹会重置,需要再重新修改react-navigation源码,很麻烦,所以建议大家不要修改源码。

四、SwitchNavigator用法详解

SwitchNavigator的用途是一次只显示一个页面。 默认情况下,它不处理返回操作,并在你切换时将路由重置为默认状态。项目中我们时常会碰到进入应用时展示启动页、广告页或者校验身份的需求,而且这些页面展示一次后就不再返回。此时就可以用SwitchNavigator来实现。

const SwitchControllers = createSwitchNavigator({
  switch1: { // 广告页面或者身份验证页面
    screen: SwitchController1
  },
  switch2: { // 主页面
    screen: SwitchController2
  }
}, {
  initialRouteName: 'switch1',
  resetOnBlur: true,
  backBehavior: 'none'
});

export default SwitchContainer = createAppContainer(SwitchControllers);

SwitchNavigator单独使用局限性比较大,往往适合与别的导航器嵌套使用:比如,示例中的SwitchController1和SwitchController2都可以用其他类型导航器代替,其中可以包含多个页面。

需要注意的属性有两个:

  • resetOnBlur - 用来标识切换离开屏幕时是否需要重置所有嵌套的导航器状态,默认值是true
  • backBehavior - 设置后退按钮是否会导致标签切换到初始路由,如果是,设置为initialRoute;否则为none。默认是none

SwitchNavigator可以实现切换路由后,返回键不能回到上一个页面的功能,在某些特定情况下可以使用该特质。

五、BottomTabNavigator用法详解

TabNavigator标签导航是我们最常见的一种导航样式,在3.x中将TabNavigator被移除,改用BottomTabNavigatorMaterialTopTabNavigator,两者类似。这里只对前者进行讲解。
还是和其他导航器创建一样,需要两个参数配置对象。

const tab_home_select = require('../../../resource/tabbar_home_select.png');
const tab_home = require('../../../resource/tabbar_home.png');
const tab_list_select = require('../../../resource/tabbar_list_select.png');
const tab_list = require('../../../resource/tabbar_list.png');
const tab_self_select = require('../../../resource/tabbar_self_select.png');
const tab_self = require('../../../resource/tabbar_self.png');

const BottomTabControllers = createBottomTabNavigator({
  Home: {
    screen: TabHomeController,
    navigationOptions: {
      title: '首页',
      tabBarLabel: '首页',
      tabBarIcon: ({ focused, horizontal, tintColor }) => (
        <Image
          source={focused ? tab_home_select : tab_home}
          style={{ width: 20, height: 20 }}
          resizeMode={'contain'}
        />
      )
    }
  },
  List: {
    screen: TabListController,
    navigationOptions: {
      title: '书单',
      tabBarLabel: '书单',
      tabBarIcon: ({ focused, horizontal, tintColor }) => (
        <Image
          source={focused ? tab_list_select : tab_list}
          style={{ width: 20, height: 20 }}
          resizeMode={'contain'}
        />
      ),
      // tabBarOnPress: ({navigation, defaultHandler}) => {
      //   console.log(navigation, '--navigation--');
      //   console.log(defaultHandler, '--defaultHandler--');
      // }
    }
  },
  Self: {
    screen: TabMineController,
    navigationOptions: {
      title: '我的',
      tabBarLabel: '我的',
      tabBarIcon: ({ focused, horizontal, tintColor }) => (
        <Image
          source={focused ? tab_self_select : tab_self}
          style={{ width: 20, height: 20 }}
          resizeMode={'contain'}
        />
      )
    }
  }
}, {
  lazy: true,
  initialRouteName: 'Home',
  order: ['Home', 'List', 'Self'],
  tabBarOptions: {
    activeTintColor: '#FF8800',
    inactiveTintColor: '#666666',
    showIcon: true,
    labelStyle: {
      fontSize: 12
    },
    style: {
      backgroundColor: 'white',
      height: 45
    }
  }
});

有几个比较重要的属性这里需要提一下:

  • tabBarIcon - 该方法中有三个参数:
    • focused - 当前Tab是否获取焦点,如果是我们一般都会设置当前Tab高亮;
    • horizontal - 当前是否横屏,如果横屏为true,否则为false;
    • tintColor - 对应activeTintColorinactiveTintColor,如果获取焦点则为activeTintColor设置的rgba色值字符串,否则为inactiveTintColor;如果没有设置则返回默认色值。
  • lazy - 是否懒加载。和原生一样,懒加载除了能提高渲染性能之外,还可以提升交互体验。
  • order - 底部Tab 的位置,是在左侧还是中间,都可以通过这个属性调整。
  • tabBarOptions - 设置TabBar的一些属性:激活与非激活状态下的颜色、是否显示图标或者图标文本样式等。

有个特殊属性可以对Tab的点击进行监测:
tabBarOnPress - 用来添加自定义逻辑处理,该方法在切换到下一个页面之前调用,包含的参数可以直接用一个event表示,或者可以拆分成navigationdefaultHandler
方法中需要调用defaultHandler(),否则会页面切换失效。

// 两个参数navigation、defaultHandler
tabBarOnPress: ({navigation, defaultHandler}) => {
   defaultHandler();
}

// 一个event参数
tabBarOnPress: (event) => {
   event.defaultHandler();
}

六、DrawerNavigator用法详解

DrawerNavigator抽屉式导航也是常见导航类型之一,原生应用中经常会见到 -- 由侧滑菜单来控制页面跳转。但是一个应用肯定会有不少页面,如果都用侧滑菜单来控制的话,不仅混乱,而且体验也不是很好,所以抽屉式导航往往是与别的导航器嵌套使用的。

const DrawerControllers = createDrawerNavigator({
  Main: {
    screen: DrawerMainController,
  },
  List: {
    screen: DrawerListController,
  },
  Self: {
    screen: DrawerSelfController,
  },
  Setting: {
    screen: DrawerSettingController,
  }
}, {
  drawerWidth: 300,
  drawerPosition: 'left',
  initialRouteName: 'Main',
  order: ['Main', 'List', 'Self', 'Setting'],
  drawerLockMode: 'locked-closed',
  drawerType: 'slide',
  contentComponent: (props) => {
    console.log(props, '--props--');
    return (
      <ScrollView style={{flex: 1}}>
        <SafeAreaView forceInset={{ top: 'always', horizontal: 'never' }}>
          <DrawerItems {...props}/>
        </SafeAreaView>
      </ScrollView>
    );
  },
  contentOptions: {
    activeTintColor: '#FF8800',
    inactiveTintColor: '#666666',
  }
});

export default DrawerContainer = createAppContainer(DrawerControllers);

其中有几个比较重要的属性需要注意:

  • drawerIcon - 侧滑item的图标,在页面的各自navigationOptions中配置。会回传两个参数:focused 状态是否选中标识;tintColor item选中时的色值。
  • drawerLockMode - 设置抽屉的锁定模式:unlocked,是默认值,用手势可以打开和关闭抽屉;locked-closed,锁定关闭,在抽屉保持关闭的状态下,用手势不能打开;locked-open,锁定打开,在抽屉打开的状态下,用手势不能关闭抽屉。
  • contentComponent - 该属性是用来设置侧滑内容组件的,可以自定义组件样式,默认情况下为DrawerItems(该组件可以从react-navigation中导入)。方法中会传递props属性给item组件,通过打印可以查看里面包含的值以及方法(选其中几个,大部分比较好理解):
    • activeItemKey - 是当前选中页面的key值标签
    • items - 抽屉的路由数组,可以修改或覆盖。其中元素为页面路由对象state值,包含keyrouteNameparams三个参数
    • descriptors - 我理解为描述元,里面包含抽屉页面的常用属性值,例如:keynavigationstateoptions(这个不知道具体用处)
  • contentOptions - 内容选项,用来设置item的属性值。
    • activeTintColor - 当前选项卡的标签和图标颜色
    • inactiveTintColor - 非当前选项卡的标签和图标颜色
    • onItemPress - 当item被点击时调用
    • itemStyle - 子组件item的样式

七、结语

  以上是对几个导航器的拆分理解,其中的属性只是挑出了一部分,比较重要、难理解或者典型的,并不是全部。剩余的需要大家自己去对照上一篇尝试或者log日志输出对比,这里就不再挨个讲解了。相信自己动手尝试过的肯定要记忆更深,理解也更透彻。
  单个导航理解以后,它们的优缺点也就有了基本的认识,之后就是各种搭配组合使用了。多个导航嵌套能实现更复杂的业务需求,也能提高交互体验。

  下一篇:ReactNative系列(六):react-natigation 3.x全解(下)

如果有不对的地方欢迎指出,大家互相讨论,如果喜欢请点赞关注

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

推荐阅读更多精彩内容