RN:Redux

目录

一. 为什么要使用Redux
二. Redux是什么
 1. Redux的三大组成部分
 2. Redux的工作流程
三. 怎么使用Redux
四. 异步Action
五. 中间件

需要导入的组件:

  • redux
  • react-redux
  • redux-thunk
  • redux-devtools(可选):Redux开发者工具,支持热加载、action重放、自定义UI等功能。
  • redux-persist(可选):支持store本地持久化。
  • redux-observable(可选):实现可取消的action

yarn add redux
yarn add react-redux
yarn add redux-thunk



一. 为什么要使用Redux


我们知道每个组件都有它自己的state,当state发生变化时,该组件就会重新渲染。通常情况下,我们使用组件自己的state就可以顺利完成开发了。但如果遇见如下情况:

  • 某个组件的state,需要共享
  • 一个组件需要改变另一个组件的state

  • 某个state,需要在任何地方都能拿到——即它应该是一个全局state
  • 一个组件需要改变某个全局state

我们通过属性传值、回调函数传值、通知传值等通信方式也不是不能实现,只是会使得程序的数据流向越来越不清晰,模块与模块之间的耦合度越来越大,使用Redux可以很好的解决这两个问题。


二. Redux是什么


简单地说,Redux就是一个state管理器,我们一般用它来管理一些全局state或组件需要共享的state,从而很方便地实现一些本来需要通过各种通信方式绕来绕去才能实现的业务功能,程序的数据流向变得非常清晰,模块之间的耦合度也大大降低。

1. Redux的三大组成部分

Redux由Store、Action、Reducer三部分组成,这三者相互勾连、协同完成了对应用State的管理(应用State,其实就是Redux内部提供的一个JS对象,我们的全局state或组件需要共享的state都需要放在它里面),其中Store是司令官,Action是命令,Reducer是执行者。

1.1 Store

store是一个JS对象,我们可以把它看成是一个更大的容器,应用State就存放在它里面,它还提供了三个方法来进行应用State的读取、修改、发布操作,整个应用只能有一个store

  • createStore()方法

Redux提供了createStore()方法,用来生成一个store,该方法接收一个reducer函数作为参数。(store创建完成后,它内部的应用State也就跟着创建成功了,只是没有值而已,应用State的初始值由reducer函数提供)

import {createStore} from 'redux';
const store = createStore(reducer);

下面是createStore(reducer)内部实现的伪代码,我们可以看看用它生成的store内部结构是怎样的,这将有助于我们理解store是什么。

const createStore = (reducer) => {
  // store里存放着应用State
  let state;
  // store里存放着所有监听应用State变化的回调函数
  let callbacks = [];

  // 提供getState方法
  const getState = () => state;

  // 提供dispatch方法
  const dispatch = (action) => {
    // dispatch action的时候,dispatch方法内部会自动调用作为参数传进来的reducer函数
    // 并把当前时刻的应用State和action作为参数传给reducer函数
    // reducer函数执行后会返回一个全新的应用State
    const newState = reducer(state, action);
    // store更新应用State
    state = newState;
    // 一旦应用State发生变化,就触发所有的回调函数
    callbacks.forEach(callback => callback());
  };

  // 提供subscribe方法
  const subscribe = (callback) => {
    callbacks.push(callback);
    return () => {
      // 过滤掉重复的回调函数
      callbacks = callbacks.filter(cb => cb !== callback);
    }
  };

  // 可见store仅仅对外暴露了三个方法,应用State并不会直接暴露出去,因此我们无论是读取、修改还是发布应用State,都是通过这三个方法来完成
  return {getState, dispatch, subscribe};
};
  • getState()方法

store提供了getState()方法,用来读取当前时刻的应用State。

import {createStore} from 'redux';
const store = createStore(reducer);

const state = store.getState();
  • dispatch(action)方法

store提供了dispatch(action)方法,用来发出一个action,目的是修改应用State。我们可以在任意的地方调用store.dispatch(action),组件中、网络请求的回调中、定时器中都可以。

import {createStore} from 'redux';
const store = createStore(reducer);

store.dispatch(action);
  • subscribe(callback)方法

store提供了subscribe(callback)方法,用来发布最新的应用State,组件设置对应用State的监听,并设置回调函数,一旦应用State发生变化,store就会自动触发这个回调函数。

import {createStore} from 'redux';
const store = createStore(reducer);

store.subscribe(callback);

当我们调用store.subscribe(callback)方法时,该方法会返回一个函数,我们调用这个返回的函数就可以解除组件对应用State的监听。

let unsubscribe = store.subscribe(callback);
unsubscribe();
1.2 Action、Action Creator

(1)Action

上面Store部分,我们说到store.dispatch(action)方法是修改应用State的唯一方式,可以看到该方法发出了一个action,那这个action是什么呢?

action是一个JS对象,这个JS对象必须得有一个type属性,是命令的名字,起名字时要做到见名知意,其它的属性我们可以自由设置,用来携带命令的负载信息。一个action,它的名字描述了应用State要做什么样的变化,它的负载信息则是提供了应用State做这个变化所需要的原料(reducer是接触不到组件的,但是action可以,所以如果reducer需要某些组件传进来的数据,就只能通过action负载信息给带进来)。

const action = {
  type: ...,
  pros1: ...,
  pros2: ...,
  ...
};

举个实际例子,下面就定义了一个action,执行者在收到这个命令后,一看到命令的名字是ADD,它就知道是要对应用State做加法操作,再一看命令的负载信息,它就得到了应用State要加1

const action = {
  type: 'ADD',
  payload: 1,
};

多数情况下,type会被定义成字符串常量,放在单独的actionTypes.js文件里,方便我们管理和使用。而当应用规模越来越大时,action也会越来越多,也建议放在单独的rootAction.js文件里,方便我们管理和使用。

// actionTypes.js
export default {
    ADD: 'ADD',
} 


// rootAction.js
import Type from '../type';

const action = {
  type: Type.ADD,
  payload: 1,
};

(2)Action Creator

我们的项目中,应用State值的变化肯定会很多,那就要需要创建很多action,如果我们一个一个写action,那烦都要烦死了,因此我们可以定义一个函数来专门生成action,这个的函数就是Action Creator。

例如,我们现在要实现加2,加3的操作,如果没有Action Creator,则要再写两个action

const action1 = {
  type: 'ADD',
  payload: 1,
};

const action2 = {
  type: 'ADD',
  payload: 2,
};


const action3 = {
  type: 'ADD',
  payload: 3,
};

但是如果编写一个Action Creator,将会是这样。

function add(payload) {
    // action就是JS对象,我们返回一个JS对象
    return {
        type: 'ADD',
        payload,
    }
}

const action1 = add(1);
const action2 = add(2);
const action3 = add(3);

上面的add()函数就是一个Action Creator,可见有了它的确方便了不少。

1.3 Reducer、Reducer的拆分、Reducer的合并

(1)Reducer

上面Store部分,我们说到在使用createStore(reducer)方法创建一个store时,该方法会接收一个reducer函数作为参数,并且在store.dispatch(action)时会自动调用这个reducer函数,那这个reducer是什么呢?

reducer是一个函数,它接受当前时刻的应用State和action作为参数,在函数执行体里完成应用State的修改(利用原State的数据和action的负载信息完成修改,当然如果reducer内部本身就知道怎么修改应用State,也可以直接修改,不用action的负载信息),生成一个全新的应用State并返回(注意一定要生成一个全新的应用State,而不是修改原应用State后返回)。也就是说,reducer函数描述了应用State的具体修改过程。

const reducer = (state, action) => {
    // 根据原State和action,修改应用State,生成一个全新的应用State
    const newState = ...

    // 返回全新的应用State
    return newState;
}

通常情况下,我们还会在创建reducer的时候,把应用State的初始值也设置好。

const defaultState = {number: 0};
const reducer = (state = defaultState, action) => {
    switch (action.type) {
        case 'ADD':
            return {
                ...state,
                number: state.number + action.payload,
            };
        case 'SUB':
            return {
                ...state,
                number: state.number - action.payload,
            };
    }
}

(2)Reducer的拆分

由于整个应用只有一个State,那对于大型应用来说,这个应用State必然十分庞大,而应用State修改过程是在reducer函数体内进行的,所以如果应用State的修改多,那这个reducer函数也将十分庞大。请看下面的例子。

const defaultState = {
    isLogin: true,
    friends: ['张三', '李四'],
    tabbarThemeColor: 'red',
};
const reducer = (state = defaultState, action) => {
    switch (action.type) {
        case 'CHANGE_LOGIN_STATUS':
            return {
                ...state,
                isLogin: action.payload,
            };
        case 'ADD_FRIEND':
            return {
                ...state,
                friends: state.friends.push(action.payload),
            };
        case 'CHANGE_TABBAR_THEME_COLOR':
            return {
                ...state,
                tabbarThemeColor: action.payload,
            };

        default:
            return state;
    }
}

上面代码中,isLoginfriendstabbarThemeColor三个东西的状态没有什么联系,可以理解为它们属于三个相互独立的模块,它们修改应用State其实也是相互独立的,因此为了避免reducer过于庞大,我们在开发中通常会把reducer拆分成多个子reducer,放在多个.js文件里,一个reducer专门负责修改一个东西。

-----------isLoginReducer.js-----------

const defaultState = {
    isLogin: true,
};
const isLoginReducer = (state = defaultState, action = {}) => {
    const {type, payload} = action;
    switch (type) {
        case 'CHANGE_LOGIN_STATUS':
            return {
                ...state,
                isLogin: action.payload,
            };
        default:
            return state;
    }
}
-----------friendsReducer.js-----------

const defaultState = {
    friends: ['张三', '李四'],
};
const friendsReducer = (state = defaultState, action = {}) => {
    const {type, payload} = action;
    switch (type) {
        case 'ADD_FRIEND':
            return {
                ...state,
                friends: state.friends.push(action.payload),
            };
        default:
            return state;
    }
}
-----------tabbarThemeColorReducer.js-----------

const defaultState = {
    tabbarThemeColor: 'red',
};
const tabbarThemeColorReducer = (state = defaultState, action = {}) => {
    const {type, payload} = action;
    switch (type) {
        case 'CHANGE_TABBAR_THEME_COLOR':
            return {
                ...state,
                tabbarThemeColor: action.payload,
            };
        default:
            return state;
    }
}

(3)Reducer的合并

上面我们已经成功把一个庞大的reducer拆分成了若干个子reducer,但是我们知道Redux在创建store的时候,createStore(reducer)方法只能接收一个reducer,也就是说store只认一个reducer,所以我们还得把这若干个子reducer合并成一个大reducer供创建store时使用。

Redux提供了combineReducers()方法把若干个子reducer合并成一个根reducer该函数接收一个JS对象为参数,该JS对象其实就是应用State,这是根reducer函数在为应用State赋值,所有的子reducer在修改它负责的那个模块的state后都会来这里重新给应用State赋值。为了方便理解,我们给应用State里的key起名字时都起作xxxState,而value必须是某个对应的子reducer函数,它可以返回一个state,所以这一对key-value就专门负责描述xxxstate

// rootReducer.js

const reducer = combineReducers({// 该JS对象其实就是应用State
    // 应用State的属性名:该属性对应的reducer
    isLoginState: isLoginReducer,
    tabbarThemeColorState: tabbarThemeColorReducer,
    friendsState: friendsReducer,
});

不过虽然说是合并,但合并之后和合并之前的应用State的数据结构是发生变化了的,这从应用State的默认值可以看得出来。比如,合并之前应用State的默认值为

const defaultState = {
    isLogin: true,
    friends: ['张三', '李四'],
    tabbarThemeColor: 'red',
};

而合并之后应用State的默认值为

const defaultState = {
    isLoginState: {isLogin: true},
    friendsState: {friends: ['李四', '王五']},
    tabbarThemeColorState: {tabbarThemeColor: 'red'},
};

此时,你可能又会问,合并之前很好理解,dispatch(action)方法里会自动找到那个大的reducer(),进去一判断就知道要怎么修改应用State,那合并之后呢?我们dispatch一个dispatchdispatch方法里是怎么找对应的子reducer()的呢?我暂时也不确认,但我打断点看了一下,修改任意一个属性,确实会走所有的子reducer,也许合并后的reducer函数还是合并前那样?

2. Redux的工作流程

到了这里,我们就可以对Redux的工作流程做一下梳理了。

  • 组件和Action环节:点击组件,构建actionaction需携带有效的负载信息),调用storedispatch(action)方法把这个action给发出去。

  • Store环节:storedispatch(action)出一个action之后,会立即自动触发它创建时接收的那个reducer函数,并把当前时刻的应用State和action作为参数传给reducer

  • Reducer环节:reducer函数在接收到应用State和action两个参数后,会根据原应用State和actionpayload完成对应用State的改变,然后返回新的应用State给store

  • Store环节:store接收到新应用State后,就会更新应用State,而应用State一旦发生改变,就会立即触发监听了应用State变化的组件的回调函数。

  • 组件环节:回调函数触发后,我们可以在回调函数里做一些自定义的处理。


三. 怎么使用Redux


抓住storeactionreducer这三个关键词,然后按下面的流程来就可以了。我们先来看一个最简单的计数器实例,后面的文章中会有复杂一点的例子。

// TestPage.js

/**
 * 使用Redux写的计数器,目的只是为了练习一下Redux使用方法。
 * 你可以尝试用this.state写一下计数器,对比一下,肯定会觉得使用Redux反而更麻烦了,是的,因为这个例子太简单了,而Redux有它专门的适用场景。
 */

import React, {Component} from 'react';
import {Platform, StyleSheet, Text, View, Button} from 'react-native';

// 导入Redux的相关东西
import {createStore, combineReducers} from 'redux';

// 第2步:
// 创建根reducer,合并所有子reducer
// 刚创建根reducer时,我们可能不知道将来会有那些组件的state会被放在应用state里来统一管理,所以可以先空着,什么时候需要什么时候往这里添

// 计数器的初始state
const defaultState = {
    number: 100,
}
// 编写子reducer,负责计数器state具体变化的过程
const counterReducer = (state = defaultState, action) => {
    switch (action.type) {
        case 'ADD_NUMBER':
            return {
                ...state,
                number: state.number + 1
            };
        default:
            return state;
    }
}

const rootReducer = combineReducers({// 这个对象就是应用State
    // 应用State赋值
    counterState: counterReducer,
});
// 接下来第3步,就是结合该组件reducer里action.type的规定,为该组件创建对应的action,预备好action,到时候组件一被触摸就dispatch一个action


// 第1步:
// 创建项目唯一的store,此时应用State也跟着创建好了
// 发现需要一个reducer,所以接下来第2步,我们去创建一个reducer,回过头来填在这里
const store = createStore(rootReducer);

// 第3步:
// 为该组件创建对应的action,预备好action,到时候组件一被触摸就dispatch一个action
// 负责描述state要做什么变化以及变化所需的原料,用来dispatch
const addNumberAction = {type: 'ADD_NUMBER'};


// 编写UI组件
class Counter extends Component {
    constructor(props) {
        super(props);

        this.state = {
            number: store.getState().counterState.number,
        };
    }

    render() {
        return (
            <View style={styles.counterViewStyle}>
                <Text style={{fontSize: 24}}>{this.state.number}</Text>

                <View>
                    <Text
                        style={{color: 'black', fontSize: 20}}
                        // 第5步:点击组件的时候发出一个action
                        onPress={() => store.dispatch(addNumberAction)}
                    >{'+'}</Text>
                </View>
            </View>
        );
    };

    componentDidMount() {
        // 第4步:设置监听
        store.subscribe(() => {
            this.setState({
                number: store.getState().counterState.number,
            });
        });
    }
}

// 导出组件
export default class TestPage extends Component {
    render() {
        return (
            <View style={styles.container}>
                <Counter/>
            </View>
        );
    }
}

const styles = StyleSheet.create({
    container: {
        flex: 1,
        justifyContent: 'center',
        alignItems: 'center',
        backgroundColor: '#F5FCFF',
    },

    counterViewStyle: {
        backgroundColor: 'pink',
        width: 200,
        height: 60,

        flexDirection: 'row',
        alignItems: 'center',
        justifyContent: 'space-around',
    },
});


四. 异步Action


上面的情况都是在同步操作下,我们修改应用State很简单,只需要在想要修改的地方dispatch()一个action就可以了。

但是在异步操作下(例如网络数据请求、数据库数据加载等),我们通常需要dispatch()至少两个action,即:

  • 异步操作开始时,需要发出一个action,触发state更新为“正在操作”状态,组件重新渲染。
  • 异步操作结束后,需要再发出一个action,触发state更新为“操作结束”状态,组件再一次重新渲染。

我们假设有一个网络请求,将会做如下编写:

export function fetchFriends(url) {
    // 异步操作开始前,发送一个action
    dispatch({type: 'FETCH_FRIENDS'});

    // 异步操作
    fetch(url)
        .then(response.json())
        .then(json => {
            // 异步操作结束后,再发送一个action
            dispatch({type: 'RECEIVE_FRIENDS', payload: json});
        });
}

但是我们现在要用Redux来管理应用State,所以就不能直接触发这个函数执行数据请求,而是发出一个action来执行,因此会改写如下:

export function fetchFriends(url) {
    // 可见fetchFriends的返回值为一个箭头函数
    return dispatch => {
        // 异步操作开始前,发出一个action
        dispatch({type: 'FETCH_FRIENDS'});

        // 异步操作
        fetch(url)
            .then(response.json())
            .then(json => {
                // 异步操作结束后,再发出一个action
                dispatch({type: 'RECEIVE_FRIENDS', payload: json});
            });
    }
}

当编写了fetchFriends()函数后,触发组件开始请求数据,此时就需要dispatch()一下,而dispatch()的内容正是fetchFriends()函数,即:

store.dispatch(fetchFriends());

现在有点蒙,fetchFriends函数执行后返回的值明明是一个函数,怎么可以作为store.dispatch()方法的参数呢?store.dispatch()方法不是只能接收一个action吗?而action不是只能是一个JS对象吗?

对了一半错了一半,通常情况下action只能是一个JS对象,但其实它可以是随便一个东西,当然也就可以是一个函数。没错,store.dispatch()方法是只能接收一个JS对象的action,但我们使用redux-thunk中间件的作用就是把store.dispatch()方法改造得可以接收函数了。

于是,上面的fetchFriends方法本身就成了一个Action Creator,而它执行后返回的函数就是一个action,我们把这样的action称为异步action,因为在它体内执行了一个异步操作。

简单一句话,同步action是一个JS对象,而异步action是一个体内执行了某个异步操作的函数。


五. 中间件


1. 什么是中间件

为了理解中间件,让我们站在框架作者的角度思考问题:在使用Redux的过程中,如果我们想要添加一个功能,会添加在哪个环节?

  • 组件环节:与State一一对应,可以看作State的视觉层,不适合承担其它操作。
  • Action环节:Action仅仅是描述State如何变化,及变化所需原料的JS对象,也不适合承担其它操作。
  • Reducer环节:Reducer是一个纯函数,它的内部不应该做其它任何多余的操作,而只应负责负责了State的具体变化过程,并返回一个全新的State。

既然这几个环节都不适合的话,就剩下一个环节了呀——Store环节。而不是getState()subscribe()方法的功能本身就很明确,发挥空间不大,所以看来这个功能就只能添加在store.dispatch(action)的时候。

比如说我们要添加打印actionstate的功能,则会改造storedispatch(action)方法如下。

// 获取store原来的dispatch方法
let originalDispatch = store.dispatch;

// 编写中间件,dispatchAndLog函数就是中间件
function dispatchAndLog(action) {
    // 自定义实现
    console.log('dispatching', action);
    // 调用一下dispatch方法的原生实现
    originalDispatch(action);
    // 自定义实现
    console.log('new state', store.getState());
};

// 改造store原来的dispatch方法
store.dispatch = dispatchAndLog;

这样我们就得出结论:中间件其实就是一个函数,它就是对dispatch方法进行了改造,以便我们添加一些自定义的操作。

2. 怎么使用中间件

至于我们为什么要使用中间件,就不多说了,因为使用Redux的时候,我们可能要添加一些自定义的操作,就得使用中间件。

同时我们也不会涉及如何编写中间件,因为常用的中间件别人都写好了,我们只需要导入使用即可。

下面仅举个例子,看下如何使用中间件。

// 导入Redux提供的应用中间件的方法:applyMiddleware
import {createStore, applyMiddleware} from 'redux';
// 导入需要使用的中间件组件
import thunk from 'redux-thunk';
import createLogger from 'redux-logger';
// 创建中间件
const logger = createLogger();

let middlewares = [
    thunk,
    logger
];

const store = createStore(reducer, applyMiddleware(...middlewares));

可见中间件的使用也非常简单,只需要:

  • 导入你需要的中间件或创建中间件。
  • 然后在使用createStore方法创建store的时候,第二个参数使用applyMiddleware()方法应用一下这个中间件就可以了。

我们把中间件apply到那个store,Redux会在applyMiddleware()方法里自动完成该storedispatch方法的改造,我们不必去关心。不过要注意在使用applyMiddleware()方法时,中间件的顺序是有要求的,使用前要查一下文档,比如logger就一定要放在最后,否则输出结果会不正确。


参考博客:

阮一峰:Redux入门教程——基本用法
阮一峰:Redux入门教程——中间件与异步操作


©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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

推荐阅读更多精彩内容