你尚未登录,仅允许查看本站部分内容。请登录使用邀请码注册
Coocier

【翻译】React 升级:Redux 0个回复 专栏 @ Javascript

Coocier 发布于 6 月前

前言

近期接触React项目,学到许多新知识点,网上教程甚多,但大多都把知识点分开来讲,初学者容易陷入学习的误区,摸不着头脑,本人在学习中也遇到许多坑。此篇文章是笔者看过的写得比较详细的具体的,同时能把所有的知识点统一串联起来,非常适合初学者学习。由于文档是英文版,故做此次翻译,一来深化自己对Redux的体系认知,二来方便大家理解阅读。

由于文中出现大量技术名词,应适当结合原文进行阅读,原文连接: 《Leveling Up with React: Redux》 By Brad Westfall On March 28, 2016

此篇教程是原文作者一系列教程的最后一篇,这里只对该篇进行翻译,剩余的几篇有时间会继续进行翻译,对于文中出现的翻译错误,欢迎大家积极指正。


本教程是 Brad Westfall 三部分系列教程的最后一篇。我们将学习如何有效地管理状态,使其跨越整个应用程序,并且可以在没有严重复杂度的情况下进行衡量。在React的学习道路上我们已经走了这么远,现在是时候来跨过终点,获得这个物超所值的全部成长历程。

系列文章

Redux 是一个用来管理JavaScript应用中 data-state(数据状态)和UI-state(UI状态)的工具,对于那些随着时间推移状态管理变得越来越复杂的单页面应用(SPAs)它是比较理想的,同时,它又是和框架无关的,因此,尽管它是提供给React使用的,但它也可以结合Angular 或者 jQuery来使用。

另外,它的设想来自一个叫做“时间旅行”的实验,这是真实的,我们后面会讲到。

正如我们前面的教程所提到的,React 在组件之间流通数据.更确切的说,这被叫做“单向数据流”——数据沿着一个方向从父组件流到子组件。由于这个特性,对于没有父子关系的两个组件之间的数据交流就变得不是那么显而易见。

React 不推荐组件对组件直接交流的这种方式,尽管它确实有一些特征可以支持这个方法,但在许多组件之间进行直接的组件对组件的交流被认为是不好的做法,因为这样会容易出错,并且导致spaghetti code —— 过时的代码, 很难维护。

React 提供了一个建议,但是他们希望你能自己来实现它。这里是React官方文档里的一段话:

想让两个没有父子关系的组件进行交流,你可以通过设置你自己的全局事件机制…… Flux 模式就是其中一个可行的方案

这里 Redux 就排上用场了。Redux提供了一个解决方案,通过将应用程序所有的状态都存储在一个地方,叫做“store”。然后组件就可以“dispatch”状态的改变给这个store,而不是直接跟另外的组件交流。所有的组件都应该意识到状态的改变可以“subscribe”给store。

可以把store想象成是应用程序中所有状态改变的中介。随着Redux的介入,所有的组件不再相互直接交流,而是所有的状态改变必须通过store这个单一的真实来源。

这和那些应用程序中不同的部分直接交流的策略有很大的不同。有时,那些策略被认为是容易出错和混乱的原因:

有了Redux,所有的组件都从store中来获取他们的状态,变得非常清晰。同样,组件状态的改变都发送给了store,也很清晰。组件初始化状态的改变只需要关心如何派发给store,而不用去关心一系列其它的组件是否需要状态的改变。这就是Redux如何使数据流变得更简单的原因。

使用store来协调应用之间状态改变的概念就是Flux模式。它是一种倾向单向数据流(比如 React)的设计模式。Redux像Flux,但是他们又有多少关系呢?

Redux is "Flux-like"

Flux 是一种模式,不像Redux那样是可以下载的工具,Redux 是受Flux模式,此外,它比较像Elm。这里有许多有关于Redux和Flux之间比较的指南。它们中的大多数都会得出Redux就是Flux,或者Redux和Flux比较类似的结论,这取决于给Flux定义的规则到底有多严格。然而说到底,这些都无关紧要。Facebook 非常喜欢并且支持Redux,这从它们雇佣了Redux的主要开发者 Dan Abramov 就可以看出。

这篇文章假设你一点都不熟悉Flux的设计模式。不过如果你熟悉,你会注意到许多微小的不同,尤其考虑到Redux的三大指导原则

1. 单一真实源

Redux只使用一个store来处理应用的状态。因为所有的状态都驻留在同一个地方,Redux称这个为单一真实源。

store中数据的结构完全取决于你,但通常都是针对应用的一个深层嵌套的对象。

Redux的单一store方法是区分Flux多个store方法的最主要区别。

2. 状态是只读的

Redux的文档指出,唯一改变状态的方法就是发出一个action,一个用来描述发生了什么的对象。

这意味着应用不能直接改变状态,相反,“actions” 被派发给store,用来描述一个改变状态的意图。

store对象自己有几个小型的API,对应4个方法:

  • store.dispatch(action)
  • store.subscribe(listener)
  • store.getState()
  • replaceReducer(nextReducer)

所以你可以看到,这里没有设置状态的方法。因此,派发一个action是处理应用状态更改的唯一办法

var action = {
  type: 'ADD_USER',
  user: {name: 'Dan'}
};

// Assuming a store object has been created already
store.dispatch(action);

dispatch() 方法发送了一个对象给Redux,这个对象就被叫做action。这个action可以被描述成一个携带了一个 type 属性以及其它可以被用来更新状态的数据(在这个例子里就是user)的有效负载。记住,在 type 属性之后,这个action对象的设计完全取决于你。

3. 所有的状态改变使用的都是纯函数

就像刚才所描述的,Redux不允许应用直接改变状态,而是用被分派的action来“描述”状态改变或者改变状态的意图。而一个个Reducer就是你自己写的函数,用来处理分派的action,事实上是它真正改变了状态。

一个reducer接受当前的状态(state)作为参数,而且必须返回一个新的状态才能改变之前的状态。

// Reducer Function
var someReducer = function(state, action) {
  ...
  return state;
}

reducer 必须使用 “纯”函数 , 一个可以用以下这些特征来描述的术语:

  • 没有任何的网络或数据库请求操作
  • 返回的值仅依赖于参数
  • 参数必须是“不可改变的”,以为着它们将不能被更改。
  • 调用具有相同参数集的纯函数将始终返回相同的值

它们被称为“纯”函数是因为它们什么都不做仅仅返回一个基于参数的值。它们在系统的任何其他部分都没有副作用。

第一个 Redux Store

开始之前,需要先用 Redux.createStore() 创建一个store,然后将所有的reducer作为参数传递进去,让我们看一下这个只传递了一个reducer的小例子:

// Note that using .push() in this way isn't the
// best approach. It's just the easiest to show
// for this example. We'll explain why in the next section.

// The Reducer Function
var userReducer = function(state, action) {
  if (state === undefined) {
    state = [];
  }
  if (action.type === 'ADD_USER') {
    state.push(action.user);
  }
  return state;
}

// Create a store by passing in the reducer
var store = Redux.createStore(userReducer);

// Dispatch our first action to express an intent to change the state
store.dispatch({
  type: 'ADD_USER',
  user: {name: 'Dan'}
});

上面的程序干了些什么呢:

  1. 这个store只由一个reducer创建。
  2. 这个reducer 初始化状态的时候使用了一个空数组 。*
  3. 在被分派的这个action里面使用了新的user对象。
  4. 这个reducer将这个新的user对象附加到state上,并将它返回,用来更新store。

*在这个例子里reducer实际上被调用了两次 —— 一次是在创建store的时候,一次是在分派action之后。

当store被创建之后,Redux立即调用了所有的reducer,并且将它们的返回值作为初始状态。第一次调用reducer传递了一个 undefined 给state。经过reducer内部的代码处理之后返回了一个空数组给这个store的state作为开始。

所有的reducer在每次action被分派之后都会被调用。因为reducer返回的状态将会成为新的状态存储在store中,所以 Redux总是希望所有的reducer都要返回一个状态

在这个例子中,reducer第二次的调用发生在分派之后。记住,一个被分派的action描述了一个改变状态的意图,而且通常携带有数据用来更新状态。这一次,Redux将当前的状态(仍旧是空数组)和action对象一起传递给了reducer。这个action对象,现在有了一个值为‘ADD_USER’的type属性, 让reducer知道怎样改变状态。

我们很容易就能将reducers和漏斗联想起来,允许状态通过他们。这是因为reducers总是接受和返回状态用来更新store。

基于这个例子,我们的store将会变成一个只有一个user对象的数组:

store.getState();   // => [{name: 'Dan'}]

不要改变状态,复制它

在我们上面的例子中这个reducer从技术上来讲是可行的,但是它改变了状态,这是一种不好的做法。尽管reducers 负责改变状态,但是不应该直接改变“现有的状态”。所以我们不应该在reducer的state这个参数上使用.push()这个变异的方法

传递给reducer的参数应该被视为不可改变的。换句话说,他们不应该被直接改变。我们可以使用不变异的方法比如.concat()来拷贝这个数组,然后我们将拷贝的数组返回。

var userReducer = function(state = [], action) {
  if (action.type === 'ADD_USER') {
    var newState = state.concat([action.user]);
    return newState;
  }
  return state;
}

在这个新的reducer中,我们添加了一个新的user对象作为state参数的副本被改变和返回。当没有添加新的用户的时候,注意返回的是原始的state而不是它的拷贝。

有一大节关于不可变数据结构的最佳尝试,我们应该更多的去了解

你也许已经注意到初始化参数使用了ES2015的默认参数方法。到目前为止,在这一些列的文章中,我们一直避免使用ES2015来使你更专心于主题内容。然而,Redux和ES2015结合使用会变得非常完美。因此,我们最终开始在这篇文章中使用ES2015。然而不用担心,每次采用新的ES2015的特性,我们都会指出来并且解释

多个reducer

上一个例子是一个很好的入门,但是大多数的应用都需要更复杂的state来满足整个应用。因为Redux仅使用一个store,所以我们需要使用嵌套的对象来组织不同模块的state。假设我们的想要我们的store类似于这种样子:

{
  userState: { ... },
  widgetState: { ... }
}

整个应用对应的还是 “一个store = 一个对象”,但是它嵌套了 userStatewidgetState 对象,可以包含各种数据。这似乎过于简单了,但是实际上和一个真实的Redux store没多少差别。

为了创建具有嵌套对象的store,我们需要定义每一块的reducer:

import { createStore, combineReducers } from 'redux';

// The User Reducer
const userReducer = function(state = {}, action) {
  return state;
}

// The Widget Reducer
const widgetReducer = function(state = {}, action) {
  return state;
}

// Combine Reducers
const reducers = combineReducers({
  userState: userReducer,
  widgetState: widgetReducer
});

const store = createStore(reducers);

ES2015 提示! 在这个例子中四个主要的变量都不会被改变, 所以我们将它们定义成常量. 同时我们也使用了ES2015 modules and destructuring

combineReducers()允许我们用不同的逻辑块来描述store,将reducer分配给每一个块。现在,每一个reducer返回的初始状态会进入到它们store中各自对应的userState或者widgState块。

有些非常重要的点需要注意,现在每一个reducer中所传递的只是全部状态中各自的部分,不再像之前只有一个reducer时传递的是整个store的状态。然后每个reducer返回的状态应用于它们各自的部分。

在分派之后调用的是哪一个Reducer?

当我们考虑每次action被分派的时候,把上面全部的reducer想想成一个个漏斗会变得更加明了,所有的reducer都会被调用,都将有机会来更新各自的状态:

我很小心地说“它们的”状态是因为reducer的“当前状态”参数和它的返回“更新”状态仅仅影响到store中reducer里面的部分。记住,像前面所说的,每一个reducer只获得它们各自的状态,而不是整个状态。

Action 策略

实际上有大量的关于创建和管理action及其类型的策略。虽然它们都很棒,但是它们不像本文中的其他一些信息那样重要。为了减少文章的篇幅,我们整理了这些基本的action策略,你可以在 GitHub repo上获得这一系列的策略。

不可变的数据结构

state的样式由你自己决定: 它可以是原始值,数组,对象,或者一个Immutable.js的数据结构。 唯一重要的部分就是你不能改变state对象,而且需要返回一个更改后的新对象 -- Redux 文档

上面的陈诉说了很多,我们已经在本教程中提到了这一点。如果我们开始讨论什么是可变的什么是不可变的的来龙去脉和利弊,我们可以在 《blog article's worth of information》找到更有价值的信息。所以事实上,我只是想突出一些要点。

开始前:

  • JavaScript的原始数据类型(Number, String, Boolean, Undefined, and Null) 已经是不可变得了。
  • 对象、数组、函数是可变的。

有人说数据结构的可变性容易产生问题。因为我们的store是有state对象和数组所组成,我们需要实施一种策略来保持状态不可变。

让我们假设需要改变一个state对象的属性,这里有三种方式:

// Example One
state.foo = '123';

// Example Two
Object.assign(state, { foo: 123 });

// Example Three
var newState = Object.assign({}, state, { foo: 123 });

第一个和第二个例子都改变了state对象。第二个例子是因为Object.assign()把所有的参数都合并到了第一个参数里。但这也就是为什么第三个例子没有改变state对象的原因。

第三个例子将state的内容和{foo: 123}合并到了一个新的空对象中。这是一种常见的技巧,允许我们创建一个state对象的副本,在副本上进行修改,本质上不会影响原始的state

对象的“扩展运算符”是保持state不可变的另一种方式:

const newState = { ...state, foo: 123 };

有关于上述代码究竟发生了什么,为什么它对Redux是友好的详细解释,可以参考这个主题的文档

Object.assign()和扩展运算符都是ES2015的特性。

总结来说,有许多方法可以明确地保持对象和数组不可变。许多开发者使用第三方库比如 seamless-immutableMori 甚至Facebook自己的Immutable.js 来达到这个目的。

我非常小心的选择了一些相关的博客和教程。如果你不是非常明白不变性,可以看一下上面给出的这些链接。这在Redux的学习中是一个非常重要的概念。

初始化状态 和 时间旅行

如果你读过文档,你也许会注意到createStore()这个方法里的第二个参数是用来“初始化状态”的,这也许是对reducer创建初始化状态方式的一种替代。然而,这个初始化的状态只会被用来“state hydration”。

想象一下一个用户刷新了你的单页面应用,store中的状态被重置为reducer中的初始状态,这样可能是不理想的。

相反,想象一个你可以使用一种策略来保持store,然后在刷新的时候重新将它化合到Redux中。这就是传送一个初始化状态到createStore()中的原因。

这带来了一个有趣的概念,如果重新化合老的状态变得这么容易,我们可以将app中的状态想象成是时间旅行。这可以被用来进行调试或者撤销/重做某些特性。所以将所有的状态存储在一个store中变得很有意义。这就是为什么不可变的状态能够帮助我们的其中一个原因。

在一次面谈中,Dan Abramov 被问到“为什么你要开发Redux?”

我并不是有意要创建Flux框架。当React第一次被宣布的时候,我提议来谈一谈‘热加载和时间旅行’,但是老实说,我自己也不知道该怎么实施时间旅行

Redux with React

就像我们已经讨论过的,Redux与框架无关。在我们开始考虑Redux跟React怎么结合之前,明白Redux的核心概念是非常重要的。但是现在我们已经准备好从上一篇文章中拿一个容器组件,然后将Redux应用在它上面了。

首先,这是没有使用Redux的原始组件代码:

import React from 'react';
import axios from 'axios';
import UserList from '../views/list-user';

const UserListContainer = React.createClass({
  getInitialState: function() {
    return {
      users: []
    };
  },

  componentDidMount: function() {
    axios.get('/path/to/user-api').then(response => {
      this.setState({users: response.data});
    });
  },

  render: function() {
    return <UserList users={this.state.users} />;
  }
});

export default UserListContainer;

ES2015 说明!这个例子已经在原始代码的基础上做了部分转换,使用了ES2015的模块功能和箭头函数。

它所做的就是发送一个Ajax请求,然后更新它的本地状态。但是,如果该应用的其它区域也要根据这个新获取到的用户列表进行改变呢,这个策略是不够的。

有了Redux策略,我们可以在Ajax请求的时候分派一个action而不是进行 this.setState(),然后这个组件和其它组件可以订阅状态的改变。但是事实上这带给我们一个问题,我们应该怎么设置store.subscribe()来更新组件的状态呢?

我想我可以提供几个例子来手动的连接一些组件到Redux store。你也可以想象一下用你的方法会怎么做。但是最终,在这些例子的最后我会解释有一个更好的办法,然后忘掉这些手动的例子。然后我会介绍官方的连接React和Redux的模块,叫做react-redux,所以还是直接跳到那一步吧。

使用 react-redux 进行连接

为了说明白,reactreduxreact-redux是npm上三个独立的模块。其中,react-redux模块允许我们以更方便的方式“connect” React组件和Redux

下面给出例子:

import React from 'react';
import { connect } from 'react-redux';
import store from '../path/to/store';
import axios from 'axios';
import UserList from '../views/list-user';

const UserListContainer = React.createClass({
  componentDidMount: function() {
    axios.get('/path/to/user-api').then(response => {
      store.dispatch({
        type: 'USER_LIST_SUCCESS',
        users: response.data
      });
    });
  },

  render: function() {
    return <UserList users={this.props.users} />;
  }
});

const mapStateToProps = function(store) {
  return {
    users: store.userState.users
  };
}

export default connect(mapStateToProps)(UserListContainer);

这里面有许多的新东西:

1、我们从 react-redux 中引入了 connect 函数。
2、这段代码可能从最底下的连接操作开始往上看会更容易理解。connect()方法实际上接收两个参数,但是我们这里只显示了一个 mapStateToProps()

connect()() 多了一个括号看起来好像很奇怪,实际上这是两个函数的调用。首先,connect()返回了另外一个函数,我想我们可以把这个函数赋值给一个变量名,然后调用它,但是既然在后面多加一个括号就可以直接调用这个函数,我们为什么还要给它设置一个函数名呢?而且,在这个函数调用结束之后,我们根本不需要这个额外的函数名。这第二个函数需要你传递一个React组件。在这个例子中,传递的是我们的容器组件。我敢打赌你肯定正在思考“为什么要把它变得这么复杂?”,然而,这实际上是一种常见的“函数式编程”范式,所以,学习如何使用它是非常有好处的。

3、connect()第一个参数是需要返回一个对象的函数。这个对象的属性会成为这个组件的“props”。你可以看到它们的状态值。现在,我希望“mapStateToProps”变得更有意义。同时,我们也看到mapStateToProps()这个函数接收了一个参数,这个参数就是整个Redux的store。mapStateToProps()函数的主体思想就是将这个组件需要用到的部分状态从全部状态中隔离出来作为它的props属性。

4、根据第3点中所说的,我们将不再需要getInitialState()的存在。同时,我们也看到,自从users这个数组变成了props属性而不是本地组件状态之后,我们参考使用this.props.users而不是this.state.users

5、Ajax的返回现在变成了一个action的分派,而不是本地状态的更新。为了更简单明了的展示,我们没有使用action构造器和action type常量

下面的代码提供了一种在用户自定义的reducer没有出现的时候也可以工作的假设。注意store的userState属性,但是这个名字是哪里来的呢?

const mapStateToProps = function(store) {
  return {
    users: store.userState.users
  };
}

这个名字来自我们合并所有的reducer的时候:

const reducers = combineReducers({
  userState: userReducer,
  widgetState: widgetReducer
});

userState.users属性又是什么?它又来自哪里?

在这个例子中,我们并没有展示一个实际的reducer(因为它会出现在另一个文件中),reducer决定了它所负责状态的子属性。为了确保.usersuserState的一个属性,上述例子对应的reducer可能看起来是这样的:

const initialUserState = {
  users: []
}

const userReducer = function(state = initialUserState, action) {
  switch(action.type) {
  case 'USER_LIST_SUCCESS':
    return Object.assign({}, state, { users: action.users });
  }
  return state;
}

在 Ajax 不同生命周期进行分派

在我们Ajax的例子中,我们仅仅分派了一个action。它被特意叫做“USERLISTSUCCESS”,因为我们同时也希望在Ajax调用开始的时候分派一个“USERLISTREQUEST”的action,在Ajax调用失败的时候分派一个“USERLISTFAILED”的action。请确保读取异步操作的文档

分派事件

在之前的文章中,我们看到事件应该通过容器组件传递到表现组件。原来 react-redux同时也可以处理这个,一个事件只需要分派一个action:

...

const mapDispatchToProps = function(dispatch, ownProps) {
  return {
    toggleActive: function() {
      dispatch({ ... });
    }
  }
}

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(UserListContainer);

在表现组件中,就像我们之前做过的,可以通过onClick={this.props.toggleActive}来调用事件,不需要再编写事件本身。

容器组件省略

有时,一个容器组件只需要订阅store,不需要任何像componentDidMount()这样的方法来开始Ajax 请求。它只需要一个render()方法传递给表现组件。在这个例子中,我们可以像这样构造容器组件:

import React from 'react';
import { connect } from 'react-redux';
import UserList from '../views/list-user';

const mapStateToProps = function(store) {
  return {
    users: store.userState.users
  };
}

export default connect(mapStateToProps)(UserList);

是的,父老乡亲们,这就是新的容器组件的整个文件。但是等一下,容器组件在哪里?为什么我们在这里没有用到任何的React.createClass()

事实证明,connect()方法为我们构造了一个容器组件。注意到这一次我们直接传递的是一个表现组件,而不是我们自己创建的容器组件。如果你真的在想容器组件干了什么,记住,它们的存在是为了表现组件专心于视图,而不是状态。它们也传递状态给子视图作为props。而这就是connect()实际所做的,它传递了状态(作为props)给我们的表现组件,然后返回一个React组件来包裹这个表现组件。从本质上来说,这个包裹,就是容器组件。

所以是不是意味着上面的例子中其实有两个容器组件包裹着一个表现组件?当然,你可以这样子认为。但这并没有什么问题,只有当我们的容器组件需要除了render()方法之外的其它方法的时候它才是必须的。

想象这两个容器组件是具有不同但是相关服务的角色:

嗯,也许这就是为什么React的logo看起来这么像原子的原因吧

Provider

为了保证任何react-redux的代码能正常工作,你需要使用一个<Provider />组件来让你的应用知道怎样使用react-redux。这个组件包裹了你的整个React应用。如果你正在使用 React Router,它看起来也许是这样的:

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import store from './store';
import router from './router';

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

通过react-redux真正“连接”React和Redux的东西是附加给Provider的store,这里有个例子,关于主要入口点大概是怎么样

Redux with React Router

这个不做要求,但是有另一个npm项目叫做 react-router-redux ,因为从技术上来说,路由是UI-state的一部分,而且React Router不认识Redux,所以这个项目帮助我们连接这两个东西。

你看到我做了什么吗?我们走了一圈,又回到了第一篇文章!

项目最后

遵照这一系列教程,最终你可以实现一个叫做“用户控件”的单页面应用。

与本系列其他文章一样,每个都有相关指导文档,在Github上也都有相关代码指导你怎么做。

总结

我真的希望你能喜欢我写的这一系列文章,我意识到有许多关于React的主题我们都没有覆盖到,但我试图在保持真实的前提下,给新用户一种跨越React基础知识的认知,以及制作一个单页面应用所带来的感受。


系列文章


翻译文献Leveling Up with React: Redux By Brad Westfall On March 28, 2016

翻译作者:coocier

等待第一条回复
登录后回复,如无账号,请使用邀请码注册