React中的Redux

学习必备要点:

  1. 首先弄明白,Redux在使用React开发应用时,起到什么作用——状态集中管理
  2. 弄清楚Redux是如何实现状态管理的——store、action、reducer三个概念
  3. 在React中集成Redux:redux + react-redux(多了一个概念——selector)
  4. Redux调试工具:redux devtools
  5. redux相关很好用的插件:redux-saga的相关介绍

redux结构图

react-redux.png

其中红色虚线部分为redux的内部集成,不能显示的看到。

  • action:是事件,它本质上是JavaScript的普通对象,它描述的是“发生了什么”。action由type:string和其他构成。
  • reducer是一个监听器,只有它可以改变状态。是一个纯函数,它不能修改state,所以必须是生成一个新的state。在default情况下,必须但会旧的state。
  • store是一个类似数据库的存储(或者可以叫做状态树),需要设计自己的数据结构来在状态树中存储自己的数据。

Redux入门

Redux简介

Redux是一个状态集中管理库。

安装

npm install --save redux

附加包

多数情况下我们需要使用 React 绑定库开发者工具

npm install --save react-redux
npm install --save-dev redux-devtools

三大原则

单一数据源

整个应用的state被存储在一棵object tree中,并且这个object tree只存在于唯一一个store中。

State是只读的

惟一改变 state 的方法就是触发 action,action 是一个用于描述已发生事件的普通对象。

使用纯函数来执行修改

为了描述action如何改变状态树,我们需要编写reducers。Reducer只是一些纯函数,他接受先前的state和action,并返回新的state对象。

react-redux.png

上图是Redux如何实现状态管理的框架,View(视图) 可以通过store.dispatch()方法传递action。 Action相当于事件模型中的事件,它描述发生了什么。Reducer相当于事件模型中的监听器,它接收一个旧的状态和一个action,从而处理state的更新逻辑,返回一个新的状态,存储到Store中。而从store-->view 的部分,则是通过mapStateToProps 这个函数来从Store中读取状态,然后通过props属性的方式注入到展示组件中。图中红色虚线部分是Redux内部处理,我们不必过多考虑这部分的实现。

Action

Action 是把数据从应用传到store的有效载荷,它是store数据的唯一来源,一般来说,我们通过store.dispatch()将action传到store。

Action创建函数

Action 创建函数 就是生成 action 的方法。“action” 和 “action 创建函数” 这两个概念很容易混在一起,使用时最好注意区分。

Redux中action创建函数只是简单返回一个action。

改变userName的示例:

export function changeUserName(userName) {  // action创建函数
    return {                             // 返回一个action
        type: 'CHANGE_USERNAME',
        payload: userName,
    };
}

Action 本质上是JavaScript 普通对象。我们规定,action 内必须使用一个字符串类型的 type 字段来表示将要执行的动作。多数情况下,type 会被定义成字符串常量。当应用规模越来越大时,建议使用单独的模块或文件来存放 action。

除了 type 字段外,action 对象的结构完全由你自己决定。参照 Flux 标准 Action 获取关于如何构造 action 的建议,另外还需要注意的是,我们应该尽量减少在action中传递数据

Reducer

Action只是描述有事情发生这一事实,而Reducer用来描述应用是如何更新state。

设计State结构

在 Redux 应用中,所有的 state 都被保存在一个单一对象中。在写代码之前我们首先要想清楚这个对象的结构,要用最简单的形式把应用中的state用对象描述出来。

HelloApp应用的state结构很简单,只需要保存userName即可:

{userName: 'World'}
处理 Reducer 关系时的注意事项

开发复杂的应用时,不可避免会有一些数据相互引用。建议你尽可能地把 state 范式化,不存在嵌套。把所有数据放到一个对象里,每个数据以 ID 为主键,不同实体或列表间通过 ID 相互引用数据。把应用的 state 想像成数据库。这种方法在 normalizr 文档里有详细阐述

Action处理

确定了 state 对象的结构,就可以开始开发 reducer。reducer 就是一个纯函数,接收旧的 state 和 action,返回新的 state。

(state, action) => newState

之所以称作 reducer 是因为它将被传递给 Array.prototype.reduce(reducer, ?initialValue) 方法。保持 reducer 纯净非常重要。永远不要在 reducer 里做以下操作:

  • 修改传入参数;
  • 执行有副作用的操作,如 API 请求和路由跳转;
  • 调用非纯函数,如 Date.now()Math.random()

在后续的学习终将会介绍如何执行有副作用的操作,现在只需谨记reducer一定要保持纯净。只要传入参数相同,返回计算得到的下一个 state 就一定相同。没有特殊情况、没有副作用,没有 API 请求、没有变量修改,单纯执行计算。

我们将写一个reducer,让它来处理之前定义过的action。我们可以首先指定state的初始状态。

const initState = {       /** 指定初始状态 */
    userName: 'World!'
}

export default function helloAppReducer(state=initState, action) {
    switch(action.type) {
        case 'CHANGE_USERNAME':
            return {
                userName: action.payload,   // 改变状态
            };
        default:
            return state;    // 返回旧状态
    }
}

警告:

  1. 不要修改state。如果涉及多个状态时,可以采用对象展开运算符的支持,来返回一个新的状态。 假设我们的实例中还存在其它状态,但是我们只需要改变userName的值,那么上述示例我们可以采用以下方式返回新的状态:

    return {
      ...state,
      userName: action.payload
    }
    
  2. 在default情况下返回旧的state 遇到未知的action时,一定要返回旧的state

Reducer拆分

这里我们以redux中文文档 中的todo应用为例来说明,在应用的需求中,有添加todo项,设置todo列表的过滤条件等多个action,同理我们就需要写多个reducer来描述状态是怎么改变的,建议把todo列表的更新和设置过滤条件放在两个reducer中去实现:

function todos(state = [], action) {
  switch (action.type) {
    case ADD_TODO:
      return [
        ...state,
        {
          text: action.text,
          completed: false
        }
      ]
    case TOGGLE_TODO:
      return state.map((todo, index) => {
        if (index === action.index) {
          return {
            ...todo,
            completed: !todo.completed
          }
        }
        return todo
      })
    default:
      return state
  }
}

function todoApp(state = initialState, action) {
  switch (action.type) {
    case SET_VISIBILITY_FILTER:
      return {
        ...state,
        visibilityFilter: action.filter
      }
    case ADD_TODO:
    case TOGGLE_TODO:
      return {
        ...state,
        todos: todos(state.todos, action)
      }
    default:
      return state
  }
}

todos 依旧接收 state,但它变成了一个数组!现在 todoApp 只把需要更新的一部分 state 传给 todos 函数,todos 函数自己确定如何更新这部分数据。这就是所谓的 reducer 合成,它是开发 Redux 应用最基础的模式。

现在我们可以开发一个函数来做为主 reducer,它调用多个子 reducer 分别处理 state 中的一部分数据,然后再把这些数据合成一个大的单一对象。主 reducer 并不需要设置初始化时完整的 state。初始时,如果传入 undefined, 子 reducer 将负责返回它们的默认值。这个过程就是reducer合并。

下面的这段代码是reducer合并的两种方式:

export default function todoApp(state = {}, action) {
  return {
    visibilityFilter: visibilityFilter(state.visibilityFilter, action),
    todos: todos(state.todos, action)
  }
}

每个 reducer 只负责管理全局 state 中它负责的一部分。每个 reducer 的 state 参数都不同,分别对应它管理的那部分 state 数据.

import { combineReducers } from 'redux';

const todoApp = combineReducers({
  visibilityFilter,
  todos
})

export default todoApp;

combineReducers() 所做的只是生成一个函数,这个函数来调用你的一系列 reducer,每个 reducer 筛选出 state 中的一部分数据并处理,然后这个生成的函数再将所有 reducer 的结果合并成一个大的对象。

Store

前面的部分,我们学会使用action来描述发生了什么,使用reducers来根据action更新state的用法。

Store则是把action和reducers联系到一起的对象,它有以下职责:

再次说明Redux应用只有一个单一的store。 当需要拆分处理数据逻辑时,我们应该使用 reducer 组合 而不是创建多个 store。

根据已有的reducer来创建store是非常容易的。在我们的HelloApp应用中,我们将helloAppReducer 导入,并传递给createStore()

import { createStore } from 'redux'
import helloAppReducer from './reducers'

let store = createStore(helloAppReducer)   // 创建store

createStore() 的第二个参数是可选的, 用于设置 state 初始状态。

备注:
其实这种数据结构是有reducer确定的,就像helloAPP的例子中,

const reducer = combineReducers({
  hello: hello,
  city: cityReducer
})

而由redux-devtools工具查看到的是下图这样的:

store-tree.png

so,存储在store中的数据结构是由reducer确定的。

数据流

严格的单向数据流 是Redux架构的核心设计。这就意味着应用中所有的数据都遵循相同的生命周期,这样可以让应用变得更加可预测且容易理解。同时也鼓励做数据范式化,这样可以避免使用多个且独立的无法相互引用的重复数据。

Redux应用中数据的生命周期遵循以下4个步骤:

  1. 调用store.dispatch(action)

    Action 就是一个描述“发生了什么”的普通对象。比如:

    { type: 'CHANGE_USERNAME', payload: "Welcome to Redux" };
    

    我们可以在任何地方调用store.dispatch(action) 包括组件中、XHR回调中、甚至是定时器中。

  2. Redux store 调用传入的 reducer 函数。

    Store 会把两个参数传入 reducer: 当前的 state 树和 action。

    const initState = {       /** 指定初始状态 */
        userName: 'World!'
    }
    
    export default function helloAppReducer(state=initState, action) {  // 传入两个参数
        switch(action.type) {
            case 'CHANGE_USERNAME':
                return {
                    userName: action.payload,   // 改变状态
                };
            default:
                return state;    // 返回当前状态
        }
    }
    

    reducer 是纯函数。它仅仅用于计算下一个 state。它应该是完全可预测的:多次传入相同的输入必须产生相同的输出。它不应做有副作用的操作,如 API 调用或路由跳转。这些应该在 dispatch action 前发生。

  3. 根 reducer 应该把多个子 reducer 输出合并成一个单一的 state 树。

    根 reducer 的结构完全由我们自己决定。Redux 原生提供combineReducers()辅助函数,来把根 reducer 拆分成多个函数,用于分别处理 state 树的一个分支。

  4. Redux store 保存了根 reducer 返回的完整 state 树。

    这个新的树就是应用的下一个state。所有订阅store.subscribe(listener) 的监听器都将被调用;监听器里可以调用store.getState() 获取当前的state。

示例: Hello App

如果想查看示例的源码,请查看这里。Hello App源码
开始之前我们需要清楚实际上Redux和React之间并没有关系。Redux支持React、Angular、Ember、jQuery甚至纯JavaScript。即便如此,Redux 还是和 ReactDeku 这类框架搭配起来用最好,因为这类框架允许你以 state 函数的形式来描述界面,Redux 通过 action 的形式来发起 state 变化。

下面我们将用React来开发一个Hello World的简单应用。

安装React Redux

Redux默认并不包含 React 绑定库,需要单独安装。

npm install --save react-redux

容器组件和展示组件

Redux 的 React 绑定库是基于 容器组件和展示组件相分离 的开发思想。而容器组件和展示组件大致有以下不同:

展示组件 容器组件
作用 描述如何展现内容、样式 描述如何运行(数据获取、状态更新)
是否能直接使用Redux
数据来源 props(属性) 监听Redux state
数据修改 从props中调用回调函数 向Redux派发actions
调用方式 手动 通常由React Redux生成

大部分的组件都应该是展示型的,但一般需要少数的几个容器组件把它们和Redux store连接起来。

技术上来说我们可以直接使用 store.subscribe() 来编写容器组件。但不建议这么做,因为这样写就无法使用 React Redux 带来的性能优化。同样,不要手写容器组件,我们直接使用 React Redux 的 connect() 方法来生成,后面会详细介绍。

需求分析

我们的需求很简单,我们只是想要展示hello + userName,默认为“Hello World!”,当我们在输入框中输入不同的值时,会显示不同的“hello,___”问候语,由此可以分析出该应用只有一个状态,那就是{ userName: '张三'}

展示组件

该应用只有一个展示组件HelloPanel:

  • HelloPanel 用于显示输入框及展示数据
    • userName: 要展示的数据
    • onChange(userName) : 当输入值发生变化时调用的回调函数

该组件之定义外观并不涉及数据从哪里来,如果改变它,传入什么就渲染什么,如果你把代码从Redux迁移到别的架构,该组件可以不做任何改动直接使用。

容器组件

还需要一个容器组件来把展示组件连接到Redux。例如HelloPanel 组件需要一个状态类似HelloApp的容器来监听Redux store变化并处理如何过滤出要展示的数据。

HelloApp 根据当前显示状态来对展示组件进行渲染。

组件编码

  • Action创建函数

    action.js

    export function changeUserName(userName) {
        return {
            type: 'CHANGE_USERNAME',
            payload: userName,
        };
    }
    
  • Reducer

    index.js

    const initState = {       /** 指定初始状态 */
        userName: 'World!'
    }
    
    export default function helloAppReducer(state=initState, action) {
        switch(action.type) {
            case 'CHANGE_USERNAME':
                return {
                    userName: action.payload,   // 改变状态
                };
            default:
                return state;    // 返回当前状态
        }
    }
    
  • 展示组件

    HelloPanel.js

    import React from 'react';
    
    export default function HelloPanel(props) {
      let input
      return (
        <div>
        <p>Hello, {props.userName}</p>
        <input ref={node => {
              input = node
            }}  onChange={()=>props.onChange(input.value)}/>
        </div>
      );
    }
    
  • 容器组件

    使用 connect() 创建容器组件前,需要先定义 mapStateToProps 这个函数来指定如何把当前 Redux store state 映射到展示组件的 props 中。例如:HelloApp 中需要计算

    const mapStateToProps = (state) => {
        return { userName: state.userName }  // 返回期望注入到展示组件的props中的参数
    };
    

    除了读取state,容器组件还能分发action。类似的方式,可以定义 mapDispatchToProps() 方法接收 dispatch() 方法并返回期望注入到展示组件的 props 中的回调方法。

    const mapDispatchToProps = (dispatch) => ({
        onChange: (userName) => {
            dispatch(changeUserName(userName))  // 返回期望注入到展示组件的 props 中的回调方法
        }
    })
    

    最后,使用 connect() 创建 HelloApp,并传入这两个函数。

    import { connect } from 'react-redux';
    import HelloPanel from './HelloPanel';
    
    const HelloApp = connect(  // 产生一个新的组件
        mapStateToProps,
        mapDispatchToProps,
    )(HelloPanel)
    
    

    这就是 React Redux API 的基础,但还漏了一些快捷技巧和强大的配置。建议仔细学习 React Redux文档。如果你担心 mapStateToProps 创建新对象太过频繁,可以学习如何使用 reselect计算衍生数据

传入Store

所有容器组件都可以访问 Redux store,所以可以手动监听它。一种方式是把它以 props 的形式传入到所有容器组件中。但这太麻烦了,因此必须要用 store 把展示组件包裹一层,恰好在组件树中渲染了一个容器组件。

建议的方式是使用指定的 React Redux 组件 <Provider> 来让所有容器组件都可以访问 store,而不必显示地传递它。只需要在渲染根组件时使用即可。

import React from 'react'
import { render } from 'react-dom'
import { createStore } from 'redux'
import { Provider } from 'react-redux'
import HelloApp from './HelloApp'
import HelloReducer from './reducers'

let store = createStore(HelloReducer)

render(
  <Provider store={store}>
    <HelloApp />
  </Provider>,
  document.getElementById('root')
)

到这里,我们已经基本掌握了Redux的基础及核心概念,有了这些,我们就可以开发简单的应用,关于Redux的更多实例、高级应用、技巧、API文档等可以查看redux中文文档

子状态树与combineReducers(reducers)

简介

随着应用变得复杂,需要对 reducer 函数 进行拆分,拆分后的每一块独立负责管理 state 的一部分。

combineReducers 辅助函数的作用是,把一个由多个不同 reducer 函数作为 value 的 object,合并成一个最终的 reducer 函数,然后就可以对这个 reducer 调用 createStore

合并后的 reducer 可以调用各个子 reducer,并把它们的结果合并成一个 state 对象。state 对象的结构由传入的多个 reducer 的 key 决定

最终,state 对象的结构会是这样的:

{
  reducer1: ...
  reducer2: ...
}

使用:

combineReducers({
  hello, cityReducer
})

state 对象的结构:

// 实际例子
{
  "hello":{"userName":"张三"}, 
  "cityReducer":{"city":"北京"}
}

通过为传入对象的 reducer 命名不同来控制 state key 的命名。

e.g.:

你可以调用 combineReducers({hello: hello,city: cityReducer}) 将 state 结构变为{ hello, city }

通常的做法是命名 reducer,然后 state 再去分割那些信息,因此你可以使用 ES6 的简写方法:combineReducers({ hello, city })。这与 combineReducers({ hello: hello,city: cityReducer }) 一样。

对于reducer的结构,我们规定只能是一级的,也就是

{
  "hello":{"userName":"张三"}, 
  "cityReducer":{"city":"北京"}
}

这种结构,不能再有子树,这样是为了方便进行管理。

参数

reducers (Object)是一个对象,它的值(value) 对应不同的 reducer 函数,这些 reducer 函数后面会被合并成一个。下面会介绍传入 reducer 函数需要满足的规则。

之前的文档曾建议使用 ES6 的 import * as reducers 语法来获得 reducer 对象。这一点造成了很多疑问,因此现在建议在 reducers/index.js 里使用 combineReducers() 来对外输出一个 reducer。下面有示例说明。

返回值

(Function):一个调用 reducers 对象里所有 reducer 的 reducer,并且构造一个与 reducers 对象结构相同的 state 对象。

注意

本函数设计的时候有点偏主观,就是为了避免新手犯一些常见错误。也因些我们故意设定一些规则,但如果你自己手动编写根 redcuer 时并不需要遵守这些规则。

每个传入 combineReducers 的 reducer 都需满足以下规则:

  • 所有未匹配到的 action,必须把它接收到的第一个参数也就是那个 state 原封不动返回。
  • 永远不能返回 undefined。当过早 return 时非常容易犯这个错误,为了避免错误扩散,遇到这种情况时 combineReducers 会抛异常。
  • 如果传入的 state 就是 undefined,一定要返回对应 reducer 的初始 state。根据上一条规则,初始 state 禁止使用 undefined。使用 ES6 的默认参数值语法来设置初始 state 很容易,但你也可以手动检查第一个参数是否为 undefined

实例:

const hello = (state = {userName: 'Hehe'}, action) => { // 设置了初始值
  switch (action.type) {
    case 'USER_CHANGE':
      return {
        userName: action.userName
      }
    // 所有未匹配到的 action,必须把它接收到的第一个参数也就是那个 state 原封不动返回。
    default: 
      return state
  }
}

export default hello

异步action

学习到这里,我们所接触的下图上的所有实现,都是针对同步事件的。如果只是这样,那么我们肯定不能放心大胆的使用redux在我们的项目中,因为我们实际项目中,更多的都是异步事件。所以接下来,让我们来介绍一个复杂的场景,我们来看看redux是如何应用在大型复杂充满异步事件的场景中的。

react-redux.png

我们仍然会遵守上图,这是我们的核心,不能改变,下面我们来看一个实际的例子,工资列表页面。

工资列表页面

也就是一个普通的通过网络请求,去请求列表数据的列表的展示。我们先来分析一下状态,列表页面的状态。

状态(state)

是一种数据结构,存储在store中的数据

异步加载的页面的状态:“加载中;加载成功,展示列表;加载失败” 这三种状态。我们给这三种状态来取一个名字,并设置0,1,2来顺序表示不同的状态。

loadingListStatus:0|1|2

我们主要做的是列表页的展示,那么还有一个最重要的数据结构就是列表数据,我们来取一个名字:

salaryList:[]

接下来我们再来分析一下,action,也就是事件。

事件

列表展示过程中的数据,也就是:“开始加载;加载成功;加载失败”这三个事件。其实整个过程和之前使用promise来实现的异步操作是一样的。我们是监听action,然后产生异步操作,执行dispatch方法,将数据结构保存到store中。

例子

我们来看一个获取列表的请求:

function fetchSalayList(subreddit) {
  return dispatch => {
    dispatch(loadingAction(subreddit))// 开始加载
    return fetch(`http://www.reddit.com/r/${subreddit}.json`)
      .then(response => response.json())
      .then(json => { // 加载成功
        dispatch(loadingSucessAction(subreddit, json))
      }, (error) => { // 加载失败
        dispatch(loadingErroeAction(subreddit))
      }
  }
}

上述这种方式,完全符合我们的核心图表,并且实现了异步操作。

在异步操作这块,我们建议使用 redux-saga 中间件来创建更加复杂的异步 action。其中涉及到es6中的Generators可以在文档中查看。另外,还有 redux-saga的使用的一个例子可以看这里

异步数据流

默认情况下,createStore() 所创建的 Redux store 没有使用 middleware,所以只支持 同步数据流

你可以使用 applyMiddleware() 来增强 createStore()。虽然这不是必须的,但是它可以帮助你用简便的方式来描述异步的 action

redux-thunkredux-promise 这样支持异步的 middleware 都包装了 store 的 dispatch() 方法,以此来让你 dispatch 一些除了 action 以外的其他内容,例如:函数或者 Promise。你所使用的任何 middleware 都可以以自己的方式解析你 dispatch 的任何内容,并继续传递 actions 给下一个 middleware。比如,支持 Promise 的 middleware 能够拦截 Promise,然后为每个 Promise 异步地 dispatch 一对 begin/end actions。

当 middleware 链中的最后一个 middleware 开始 dispatch action 时,这个 action 必须是一个普通对象。这是 同步式的 Redux 数据流 开始的地方(译注:这里应该是指,你可以使用任意多异步的 middleware 去做你想做的事情,但是需要使用普通对象作为最后一个被 dispatch 的 action ,来将处理流程带回同步方式)。

参考

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

推荐阅读更多精彩内容