compose
最后更新于:2022-04-01 05:01:10
# `compose(...functions)`
从右到左来组合多个函数。
这是函数式编程中的方法,为了方便,被放到了 Redux 里。当需要把多个 [store 增强器](#) 依次执行的时候,需要用到它。
#### 参数
1. (*arguments*): 需要合成的多个函数。每个函数都接收一个函数作为参数,然后返回一个函数。
#### 返回值
(*Function*): 从右到左把接收到的函数合成后的最终函数。
#### 示例
下面示例演示了如何使用 `compose` 增强 [store](#),这个 store 与 [`applyMiddleware`](#) 和 [redux-devtools](https://github.com/gaearon/redux-devtools) 一起使用。
~~~
import { createStore, combineReducers, applyMiddleware, compose } from 'redux';
import thunk from 'redux-thunk';
import * as reducers from '../reducers/index';
let reducer = combineReducers(reducers);
let middleware = [thunk];
let finalCreateStore;
// 生产环境中,我们希望只使用 middleware。
// 而在开发环境中,我们还希望使用一些 redux-devtools 提供的一些 store 增强器。
// UglifyJS 会在构建过程中把一些不会执行的死代码去除掉。
if (process.env.NODE_ENV === 'production') {
finalCreateStore = applyMiddleware(...middleware)(createStore);
} else {
finalCreateStore = compose(
applyMiddleware(...middleware),
require('redux-devtools').devTools(),
require('redux-devtools').persistState(
window.location.href.match(/[?&]debug_session=([^&]+)\b/)
),
createStore
);
// 不使用 compose 来写是这样子:
//
// finalCreateStore =
// applyMiddleware(middleware)(
// devTools()(
// persistState(window.location.href.match(/[?&]debug_session=([^&]+)\b/))(
// createStore
// )
// )
// );
}
let store = finalCreateStore(reducer);
~~~
#### 小贴士
- `compse` 做的只是让你不使用深度右括号的情况下来写深度嵌套的函数。不要觉得它很复杂。
bindActionCreators
最后更新于:2022-04-01 05:01:08
# `bindActionCreators(actionCreators, dispatch)`
把 [action creators](#) 转成拥有同名 keys 的对象,但使用 [`dispatch`](#) 把每个 action creator 包围起来,这样可以直接调用它们。
一般情况下你可以直接在 [`Store`](#) 实例上调用 [`dispatch`](#)。如果你在 React 中使用 Redux,[react-redux](https://github.com/gaearon/react-redux) 会提供 [`dispatch`](#) 。
惟一使用 `bindActionCreators` 的场景是当你需要把 action creator 往下传到一个组件上,却不想让这个组件觉察到 Redux 的存在,而且不希望把 Redux store 或 [`dispatch`](#) 传给它。
为方便起见,你可以传入一个函数作为第一个参数,它会返回一个函数。
#### 参数
1.
`actionCreators` (*Function* or *Object*): 一个 [action creator](#),或者键值是 action creators 的对象。
1.
`dispatch` (*Function*): 一个 [`dispatch`](#) 函数,由 [`Store`](#) 实例提供。
#### 返回值
(*Function* or *Object*): 一个与原对象类似的对象,只不过这个对象中的的每个函数值都可以直接 dispatch action。如果传入的是一个函数,返回的也是一个函数。
#### 示例
#### `TodoActionCreators.js`
~~~
export function addTodo(text) {
return {
type: 'ADD_TODO',
text
};
}
export function removeTodo(id) {
return {
type: 'REMOVE_TODO',
id
};
}
~~~
#### `SomeComponent.js`
~~~
import { Component } from 'react';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import * as TodoActionCreators from './TodoActionCreators';
console.log(TodoActionCreators);
// {
// addTodo: Function,
// removeTodo: Function
// }
class TodoListContainer extends Component {
componentDidMount() {
// 由 react-redux 注入:
let { dispatch } = this.props;
// 注意:这样做行不通:
// TodoActionCreators.addTodo('Use Redux');
// 你只是调用了创建 action 的方法。
// 你必须要 dispatch action 而已。
// 这样做行得通:
let action = TodoActionCreators.addTodo('Use Redux');
dispatch(action);
}
render() {
// 由 react-redux 注入:
let { todos, dispatch } = this.props;
// 这是应用 bindActionCreators 比较好的场景:
// 在子组件里,可以完全不知道 Redux 的存在。
let boundActionCreators = bindActionCreators(TodoActionCreators, dispatch);
console.log(boundActionCreators);
// {
// addTodo: Function,
// removeTodo: Function
// }
return (
<TodoList todos={todos}
{...boundActionCreators} />
);
// 一种可以替换 bindActionCreators 的做法是直接把 dispatch 函数
// 和 action creators 当作 props
// 传递给子组件
// return <TodoList todos={todos} dispatch={dispatch} />;
}
}
export default connect(
TodoListContainer,
state => ({ todos: state.todos })
)
~~~
#### 小贴士
-
你或许要问:为什么不直接把 action creators 绑定到 store 实例上,就像传统 Flux 那样?问题是这样做的话如果开发同构应用,在服务端渲染时就不行了。多数情况下,你 每个请求都需要一个独立的 store 实例,这样你可以为它们提供不同的数据,但是在定义的时候绑定 action creators,你就可以使用一个唯一的 store 实例来对应所有请求了。
-
如果你使用 ES5,不能使用 `import * as` 语法,你可以把 `require('./TodoActionCreators')` 作为第一个参数传给 `bindActionCreators`。惟一要考虑的是 `actionCreators` 的参数全是函数。模块加载系统并不重要。
applyMiddleware
最后更新于:2022-04-01 05:01:06
# `applyMiddleware(...middlewares)`
使用包含自定义功能的 middleware 来扩展 Redux 是一种推荐的方式。Middleware 可以让你包装 store 的 [`dispatch`](#) 方法来达到你想要的目的。同时, middleware 还拥有“可组合”这一关键特性。多个 middleware 可以被组合到一起使用,形成 middleware 链。其中,每个 middleware 都不需要关心链中它前后的 middleware 的任何信息。
Middleware 最常见的使用场景是无需引用大量代码或依赖类似 [Rx](https://github.com/Reactive-Extensions/RxJS) 的第三方库实现异步 actions。这种方式可以让你像 dispatch 一般的 actions 那样 dispatch [异步 actions](#)。
例如,[redux-thunk](https://github.com/gaearon/redux-thunk) 支持 dispatch function,以此让 action creator 控制反转。被 dispatch 的 function 会接收 [`dispatch`](#) 作为参数,并且可以异步调用它。这类的 function 就称为 *thunk*。另一个 middleware 的示例是 [redux-promise](https://github.com/acdlite/redux-promise)。它支持 dispatch 一个异步的 [Promise](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Promise) action,并且在 Promise resolve 后可以 dispatch 一个普通的 action。
Middleware 并不需要和 [`createStore`](#) 绑在一起使用,也不是 Redux 架构的基础组成部分,但它带来的益处让我们认为有必要在 Redux 核心中包含对它的支持。因此,虽然不同的 middleware 可能在易用性和用法上有所不同,它仍被作为扩展 [`dispatch`](#) 的唯一标准的方式。
#### 参数
- `...middlewares` (*arguments*): 遵循 Redux *middleware API* 的函数。每个 middleware 接受 [`Store`](#) 的 [`dispatch`](#) 和 [`getState`](#) 函数作为命名参数,并返回一个函数。该函数会被传入被称为 `next` 的下一个 middleware 的 dispatch 方法,并返回一个接收 action 的新函数,这个函数可以直接调用 `next(action)`,或者在其他需要的时刻调用,甚至根本不去调用它。调用链中最后一个 middleware 会接受真实的 store 的 [`dispatch`](#) 方法作为 `next` 参数,并借此结束调用链。所以,middleware 的函数签名是 `({ getState, dispatch }) => next => action`。
#### 返回值
(*Function*) 一个应用了 middleware 后的 store enhancer。这个 store enhancer 就是一个函数,并且需要应用到 `createStore`。它会返回一个应用了 middleware 的新的 `createStore`。
#### 示例: 自定义 Logger Middleware
~~~
import { createStore, applyMiddleware } from 'redux';
import todos from './reducers';
function logger({ getState }) {
return (next) => (action) => {
console.log('will dispatch', action);
// 调用 middleware 链中下一个 middleware 的 dispatch。
let returnValue = next(action);
console.log('state after dispatch', getState());
// 一般会是 action 本身,除非
// 后面的 middleware 修改了它。
return returnValue;
};
}
let createStoreWithMiddleware = applyMiddleware(logger)(createStore);
let store = createStoreWithMiddleware(todos, ['Use Redux']);
store.dispatch({
type: 'ADD_TODO',
text: 'Understand the middleware'
});
// (将打印如下信息:)
// will dispatch: { type: 'ADD_TODO', text: 'Understand the middleware' }
// state after dispatch: ['Use Redux', 'Understand the middleware']
~~~
#### 示例: 使用 Thunk Middleware 来做异步 Action
~~~
import { createStore, combineReducers, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import * as reducers from './reducers';
// 调用 applyMiddleware,使用 middleware 增强 createStore:
let createStoreWithMiddleware = applyMiddleware(thunk)(createStore);
// 像原生 createStore 一样使用。
let reducer = combineReducers(reducers);
let store = createStoreWithMiddleware(reducer);
function fetchSecretSauce() {
return fetch('https://www.google.com/search?q=secret+sauce');
}
// 这些是你已熟悉的普通 action creator。
// 它们返回的 action 不需要任何 middleware 就能被 dispatch。
// 但是,他们只表达「事实」,并不表达「异步数据流」
function makeASandwich(forPerson, secretSauce) {
return {
type: 'MAKE_SANDWICH',
forPerson,
secretSauce
};
}
function apologize(fromPerson, toPerson, error) {
return {
type: 'APOLOGIZE',
fromPerson,
toPerson,
error
};
}
function withdrawMoney(amount) {
return {
type: 'WITHDRAW',
amount
};
}
// 即使不使用 middleware,你也可以 dispatch action:
store.dispatch(withdrawMoney(100));
// 但是怎样处理异步 action 呢,
// 比如 API 调用,或者是路由跳转?
// 来看一下 thunk。
// Thunk 就是一个返回函数的函数。
// 下面就是一个 thunk。
function makeASandwichWithSecretSauce(forPerson) {
// 控制反转!
// 返回一个接收 `dispatch` 的函数。
// Thunk middleware 知道如何把异步的 thunk action 转为普通 action。
return function (dispatch) {
return fetchSecretSauce().then(
sauce => dispatch(makeASandwich(forPerson, sauce)),
error => dispatch(apologize('The Sandwich Shop', forPerson, error))
);
};
}
// Thunk middleware 可以让我们像 dispatch 普通 action
// 一样 dispatch 异步的 thunk action。
store.dispatch(
makeASandwichWithSecretSauce('Me')
);
// 它甚至负责回传 thunk 被 dispatch 后返回的值,
// 所以可以继续串连 Promise,调用它的 .then() 方法。
store.dispatch(
makeASandwichWithSecretSauce('My wife')
).then(() => {
console.log('Done!');
});
// 实际上,可以写一个 dispatch 其它 action creator 里
// 普通 action 和异步 action 的 action creator,
// 而且可以使用 Promise 来控制数据流。
function makeSandwichesForEverybody() {
return function (dispatch, getState) {
if (!getState().sandwiches.isShopOpen) {
// 返回 Promise 并不是必须的,但这是一个很好的约定,
// 为了让调用者能够在异步的 dispatch 结果上直接调用 .then() 方法。
return Promise.resolve();
}
// 可以 dispatch 普通 action 对象和其它 thunk,
// 这样我们就可以在一个数据流中组合多个异步 action。
return dispatch(
makeASandwichWithSecretSauce('My Grandma')
).then(() =>
Promise.all([
dispatch(makeASandwichWithSecretSauce('Me')),
dispatch(makeASandwichWithSecretSauce('My wife'))
])
).then(() =>
dispatch(makeASandwichWithSecretSauce('Our kids'))
).then(() =>
dispatch(getState().myMoney > 42 ?
withdrawMoney(42) :
apologize('Me', 'The Sandwich Shop')
)
);
};
}
// 这在服务端渲染时很有用,因为我可以等到数据
// 准备好后,同步的渲染应用。
store.dispatch(
makeSandwichesForEverybody()
).then(() =>
response.send(React.renderToString(<MyApp store={store} />))
);
// 也可以在任何导致组件的 props 变化的时刻
// dispatch 一个异步 thunk action。
import { connect } from 'react-redux';
import { Component } from 'react';
class SandwichShop extends Component {
componentDidMount() {
this.props.dispatch(
makeASandwichWithSecretSauce(this.props.forPerson)
);
}
componentWillReceiveProps(nextProps) {
if (nextProps.forPerson !== this.props.forPerson) {
this.props.dispatch(
makeASandwichWithSecretSauce(nextProps.forPerson)
);
}
}
render() {
return <p>{this.props.sandwiches.join('mustard')}</p>
}
}
export default connect(
state => ({
sandwiches: state.sandwiches
})
)(SandwichShop);
~~~
#### 小贴士
-
Middleware 只是包装了 store 的 [`dispatch`](#) 方法。技术上讲,任何 middleware 能做的事情,都可能通过手动包装 `dispatch` 调用来实现,但是放在同一个地方统一管理会使整个项目的扩展变的容易得多。
-
如果除了 `applyMiddleware`,你还用了其它 store enhancer,一定要把 `applyMiddleware` 放到组合链的前面,因为 middleware 可能会包含异步操作。比如,它应该在 [redux-devtools](https://github.com/gaearon/redux-devtools) 前面,否则 DevTools 就看不到 Promise middleware 里 dispatch 的 action 了。
-
如果你想有条件地使用 middleware,记住只 import 需要的部分:
~~~
let middleware = [a, b];
if (process.env.NODE_ENV !== 'production') {
let c = require('some-debug-middleware');
let d = require('another-debug-middleware');
middleware = [...middleware, c, d];
}
const createStoreWithMiddleware = applyMiddleware(...middleware)(createStore);
~~~
这样做有利于打包时去掉不需要的模块,减小打包文件大小。
-
有想过 `applyMiddleware` 本质是什么吗?它肯定是比 middleware 还强大的扩展机制。实际上,`applyMiddleware` 只是被称为 Redux 最强大的扩展机制的 [store enhancers](#) 中的一个范例而已。你不太可能需要实现自己的 store enhancer。另一个 store enhancer 示例是 [redux-devtools](https://github.com/gaearon/redux-devtools)。Middleware 并没有 store enhancer 强大,但开发起来却是更容易的。
-
Middleware 听起来比实际难一些。真正理解 middleware 的唯一办法是了解现有的 middleware 是如何工作的,并尝试自己实现。需要的功能可能错综复杂,但是你会发现大部分 middleware 实际上很小,只有 10 行左右,是通过对它们的组合使用来达到最终的目的。
combineReducers
最后更新于:2022-04-01 05:01:03
# `combineReducers(reducers)`
随着应用变得复杂,需要对 [reducer 函数](#) 进行拆分,拆分后的每一块独立负责管理 [state](#) 的一部分。
`combineReducers` 辅助函数的作用是,把一个由多个不同 reducer 函数作为 value 的 object,合并成一个最终的 reducer 函数,然后就可以对这个 reducer 调用 [`createStore`](#)。
合并后的 reducer 可以调用各个子 reducer,并把它们的结果合并成一个 state 对象。state 对象的结构由传入的多个 reducer 的 key 决定。
> ##### Flux 用户使用须知
> 本函数可以帮助你组织多个 reducer,使它们分别管理自身相关联的 state。类似于 Flux 中的多个 store 分别管理不同的 state。在 Redux 中,只有一个 store,但是 `combineReducers` 让你拥有多个 reducer,同时保持各自负责逻辑块的独立性。
#### 参数
1. `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`。
虽然 `combineReducers` 自动帮你检查 reducer 是否符合以上规则,但你也应该牢记,并尽量遵守。
#### 示例
#### `reducers/todos.js`
~~~
export default function todos(state = [], action) {
switch (action.type) {
case 'ADD_TODO':
return state.concat([action.text]);
default:
return state;
}
}
~~~
#### `reducers/counter.js`
~~~
export default function counter(state = 0, action) {
switch (action.type) {
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1;
default:
return state;
}
}
~~~
#### `reducers/index.js`
~~~
import { combineReducers } from 'redux';
import todos from './todos';
import counter from './counter';
export default combineReducers({
todos,
counter
});
~~~
#### `App.js`
~~~
import { createStore } from 'redux';
import reducer from './reducers/index';
let store = createStore(reducer);
console.log(store.getState());
// {
// counter: 0,
// todos: []
// }
store.dispatch({
type: 'ADD_TODO',
text: 'Use Redux'
});
console.log(store.getState());
// {
// counter: 0,
// todos: ['Use Redux']
// }
~~~
#### 小贴士
-
本方法只是起辅助作用!你可以自行实现[不同功能](https://github.com/acdlite/reduce-reducers)的 `combineReducers`,甚至像实现其它函数一样,明确地写一个根 reducer 函数,用它把子 reducer 手动组装成 state 对象。
-
在 reducer 层级的任何一级都可以调用 `combineReducers`。并不是一定要在最外层。实际上,你可以把一些复杂的子 reducer 拆分成单独的孙子级 reducer,甚至更多层。
Store
最后更新于:2022-04-01 05:01:01
# Store
Store 就是用来维持应用所有的 [state 树](#) 的一个对象。
改变 store 内 state 的惟一途径是对它 dispatch 一个 [action](#)。
Store 不是类。它只是有几个方法的对象。
要创建它,只需要把根部的 [reducing 函数](#) 传递给 [`createStore`](#)。
> ##### Flux 用户使用注意
> 如果你以前使用 Flux,那么你只需要注意一个重要的区别。Redux 没有 Dispatcher 且不支持多个 store。相反,只有一个单一的 store 和一个根级的 reduce 函数(reducer)。随着应用不断变大,你应该把根级的 reducer 拆成多个小的 reducers,分别独立地操作 state 树的不同部分,而不是添加新的 stores。这就像一个 React 应用只有一个根级的组件,这个根组件又由很多小组件构成。
### Store 方法
- [`getState()`](#)
- [`dispatch(action)`](#)
- [`subscribe(listener)`](#)
- [`replaceReducer(nextReducer)`](#)
### Store 方法
### [`getState()`](#)
返回应用当前的 state 树。
它与 store 的最后一个 reducer 返回值相同。
#### 返回值
*(any)*: 应用当前的 state 树。
### [`dispatch(action)`](#)
分发 action。这是触发 state 变化的惟一途径。
会使用当前 [`getState()`](#) 的结果和传入的 `action` 以同步方式的调用 store 的 reduce 函数。返回值会被作为下一个 state。从现在开始,这就成为了 [`getState()`](#) 的返回值,同时变化监听器(change listener)会被触发。
> ##### Flux 用户使用注意
> 当你在 [reducer](#) 内部调用 `dispatch` 时,将会抛出错误提示“Reducers may not dispatch actions.(Reducer 内不能 dispatch action)”。这就相当于 Flux 里的 “Cannot dispatch in a middle of dispatch(dispatch 过程中不能再 dispatch)”,但并不会引起对应的错误。在 Flux 里,当 Store 处理 action 和触发 update 事件时,dispatch 是禁止的。这个限制并不好,因为他限制了不能在生命周期回调里 dispatch action,还有其它一些本来很正常的地方。
> 在 Redux 里,只会在根 reducer 返回新 state 结束后再会调用事件监听器,因此,你可以在事件监听器里再做 dispatch。惟一使你不能在 reducer 中途 dispatch 的原因是要确保 reducer 没有副作用。如果 action 处理会产生副作用,正确的做法是使用异步 [action 创建函数](#)。
#### 参数
1. `action` (*Object*†): 描述应用变化的普通对象。Action 是把数据传入 store 的惟一途径,所以任何数据,无论来自 UI 事件,网络回调或者是其它资源如 WebSockets,最终都应该以 action 的形式被 dispatch。按照约定,action 具有 `type` 字段来表示它的类型。type 也可被定义为常量或者是从其它模块引入。最好使用字符串,而不是 [Symbols](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Symbol) 作为 action,因为字符串是可以被序列化的。除了 `type` 字段外,action 对象的结构完全取决于你。参照 [Flux 标准 Action](https://github.com/acdlite/flux-standard-action) 获取如何组织 action 的建议。
#### 返回值
(Object†): 要 dispatch 的 action。
#### 注意
† 使用 [`createStore`](#) 创建的 “纯正” store 只支持普通对象类型的 action,而且会立即传到 reducer 来执行。
但是,如果你用 [`applyMiddleware`](#) 来套住 [`createStore`](#) 时,middleware 可以修改 action 的执行,并支持执行 dispatch [intent(意图)](#)。Intent 一般是异步操作如 Promise、Observable 或者 Thunk。
Middleware 是由社区创建,并不会同 Redux 一起发行。你需要手动安装 [redux-thunk](https://github.com/gaearon/redux-thunk) 或者 [redux-promise](https://github.com/acdlite/redux-promise) 库。你也可以创建自己的 middleware。
想学习如何描述异步 API 调用?看一下 action 创建函数里当前的 state,执行一个有副作用的操作,或者以链式操作执行它们,参照 [`applyMiddleware`](#) 中的示例。
#### 示例
~~~
import { createStore } from 'redux';
let store = createStore(todos, ['Use Redux']);
function addTodo(text) {
return {
type: 'ADD_TODO',
text
};
}
store.dispatch(addTodo('Read the docs'));
store.dispatch(addTodo('Read about the middleware'));
~~~
### [`subscribe(listener)`](#)
添加一个变化监听器。每当 dispatch action 的时候就会执行,state 树中的一部分可能已经变化。你可以在回调函数里调用 [`getState()`](#) 来拿到当前 state。
这是一个底层 API。多数情况下,你不会直接使用它,会使用一些 React(或其它库)的绑定。如果你想让回调函数执行的时候使用当前的 state,你可以 [把 store 转换成一个 Observable 或者写一个定制的 `observeStore` 工具](https://github.com/rackt/redux/issues/303#issuecomment-125184409)。
如果需要解绑这个变化监听器,执行 `subscribe` 返回的函数即可。
#### 参数
1. `listener` (*Function*): 每当 dispatch action 的时候都会执行的回调。state 树中的一部分可能已经变化。你可以在回调函数里调用 [`getState()`](#) 来拿到当前 state。store 的 reducer 应该是纯函数,因此你可能需要对 state 树中的引用做深度比较来确定它的值是否有变化。
##### 返回值
(*Function*): 一个可以解绑变化监听器的函数。
##### 示例
~~~
function select(state) {
return state.some.deep.property;
}
let currentValue;
function handleChange() {
let previousValue = currentValue;
currentValue = select(store.getState());
if (previousValue !== currentValue) {
console.log('Some deep nested property changed from', previousValue, 'to', currentValue);
}
}
let unsubscribe = store.subscribe(handleChange);
handleChange();
~~~
### [`replaceReducer(nextReducer)`](#)
替换 store 当前用来计算 state 的 reducer。
这是一个高级 API。只有在你需要实现代码分隔,而且需要立即加载一些 reducer 的时候才可能会用到它。在实现 Redux 热加载机制的时候也可能会用到。
#### 参数
1. `reducer` (*Function*) store 会使用的下一个 reducer。
createStore
最后更新于:2022-04-01 05:00:59
# `createStore(reducer, [initialState])`
创建一个 Redux [store](#) 来以存放应用中所有的 state。应用中应有且仅有一个 store。
#### 参数
1.
`reducer`*(Function)*: 接收两个参数,分别是当前的 state 树和要处理的 [action](#),返回新的 [state 树](#)。
1.
[`initialState`] *(any)*: 初始时的 state。在同构应用中,你可以决定是否把服务端传来的 state 水合(hydrate)后传给它,或者从之前保存的用户会话中恢复一个传给它。如果你使用 [`combineReducers`](#) 创建 `reducer`,它必须是一个普通对象,与传入的 keys 保持同样的结构。否则,你可以自由传入任何 `reducer` 可理解的内容。[TODO: Optimize]
#### 返回值
([*`Store`*](#)): 保存了应用所有 state 的对象。改变 state 的惟一方法是 [dispatch](#) action。你也可以 [subscribe 监听](#) state 的变化,然后更新 UI。
#### 示例
~~~
import { createStore } from 'redux';
function todos(state = [], action) {
switch (action.type) {
case 'ADD_TODO':
return state.concat([action.text]);
default:
return state;
}
}
let store = createStore(todos, ['Use Redux']);
store.dispatch({
type: 'ADD_TODO',
text: 'Read the docs'
});
console.log(store.getState());
// ['Use Redux', 'Read the docs']
~~~
#### 小贴士
-
应用中不要创建多个 store!相反,使用 [`combineReducers`](#) 来把多个 reducer 创建成一个根 reducer。
-
你可以决定 state 的格式。你可以使用普通对象或者 [Immutable](http://facebook.github.io/immutable-js/) 这类的实现。如果你不知道如何做,刚开始可以使用普通对象。
-
如果 state 是普通对象,永远不要修改它!比如,reducer 里不要使用 `Object.assign(state, newData)`,应该使用 `Object.assign({}, state, newData)`。这样才不会覆盖旧的 `state`。也可以使用 [Babel 阶段 1](http://babeljs.io/docs/usage/experimental/) 中的 [ES7 对象的 spread 操作](https://github.com/sebmarkbage/ecmascript-rest-spread) 特性中的 `return { ...state, ...newData }`。
-
对于服务端运行的同构应用,为每一个请求创建一个 store 实例,以此让 store 相隔离。dispatch 一系列请求数据的 action 到 store 实例上,等待请求完成后再在服务端渲染应用。
-
当 store 创建后,Redux 会 dispatch 一个 action 到 reducer 上,来用初始的 state 来填充 store。你不需要处理这个 action。但要记住,如果第一个参数也就是传入的 state 如果是 `undefined` 的话,reducer 应该返回初始的 state 值。
API 文档
最后更新于:2022-04-01 05:00:57
# API 文档
Redux 的 API 非常少。Redux 定义了一系列的约定(contract)来让你来实现(例如 [reducers](#)),同时提供少量辅助函数来把这些约定整合到一起。
这一章会介绍所有的 Redux API。记住,Redux 只关心如何管理 state。在实际的项目中,你还需要使用 UI 绑定库如 [react-redux](https://github.com/gaearon/react-redux)。
### 顶级暴露的方法
- [createStore(reducer, [initialState])](#)
- [combineReducers(reducers)](#)
- [applyMiddleware(...middlewares)](#)
- [bindActionCreators(actionCreators, dispatch)](#)
- [compose(...functions)](#)
### Store API
- [Store](#)
- [getState()](#)
- [dispatch(action)](#)
- [subscribe(listener)](#)
- [getReducer()](#)
- [replaceReducer(nextReducer)](#)
### 引入
上面介绍的所有函数都是顶级暴露的方法。都可以这样引入:
#### ES6
~~~
import { createStore } from 'redux';
~~~
#### ES5 (CommonJS)
~~~
var createStore = require('redux').createStore;
~~~
#### ES5 (UMD build)
~~~
var createStore = Redux.createStore;
~~~
词汇表
最后更新于:2022-04-01 05:00:54
# 词汇表
这是有关Redux中的一些核心概念的词汇表,以及他们的类型签名。这些类型使用了 [流标注法](http://flowtype.org/docs/quick-reference.html)进行记录。
### State
~~~
type State = any;
~~~
*State* (也叫 *state tree*) 是一个宽泛的概念,但是在 Redux API 中它通常与被 store 所管理的,可以被 [`getState()`](#) 返回的,单独 state 值相关。 它表示了一个 Redux应用的全部状态,通常为一个多层嵌套的对象。
约定俗成,顶层 state 为一个对象,或几个像 Map 那样的键-值集合,当然是任意类型的话也成。当然,你仍然可以尽可能保持状态的串行化。不要把什么都放进去导致无法容易地转换成 JSON。
### Action
~~~
type Action = Object;
~~~
*Action* 是一个用以表示要改变的 state 的意图的普通对象。Action 是将数据拿到 store 里的唯一方法。无论是 UI 事件,网络回调,还是其他诸如 WebSocket 之类的其他源,任何数据都或多或少的被 dispatch 成 action。
约定俗成,action 应该有一个 `type` 域指明了需要被演算的 action 类型。Type 可以被定义为常数从其他 module 中导入。比起用 [Symbols](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Symbol) 表示 `type` 使用 String 是更好的方法因为 string 是可被串行化的。
除了 `type`之外,action 对象的结构其实完全取决于你自己。如果你感兴趣的话,请参考 [Flux Standard Action](https://github.com/acdlite/flux-standard-action) 作为如何组织 actions 的建议。
还有就是请看后面的 [异步 action](#)。
### Reducer
~~~
type Reducer<S, A> = (state: S, action: A) => S;
~~~
*Reducer* (也叫 *reducing function*) 是一个接受累积运算和一个值,返回新的累积函数的函数。用来把一个集合 reduce 到一个单独值。
Reducer 并不是 Redux 特有的——它是函数式编程中的一个基本概念。甚至大部分的非函数式语言比如 JavaScript,都有一个内建的 reduce API。在 JavaScript 中的话是 [`Array.prototype.reduce()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce).
在 Redux 中,累计运算的结果是个 state 对象,被累积的值就是 action。Reducer 由上一个 state 和一个 action 计算得到一个新 state。它必须是 *纯函数* 也就是由完全相同的输入会返回完全相同的输出。它应该是没有副作用的。这使得一些很棒的功能诸如热重载和时间旅行成为可能。
Reducer 是 Redux 之中最重要的概念。
*不要在 reducer 中有 API 调用*
### dispatch function
~~~
type BaseDispatch = (a: Action) => Action;
type Dispatch = (a: Action | AsyncAction) => any;
~~~
一个 *dispatching function* (或者简单点叫 *dispatch function*) 是一个接收一个 action 或者[异步 action](#)的函数,它可以或不可以分发一个或多个 action 到 store。
我们必须搞清 dispatch function 和由没有 middleware 的 store 实例提供的 base [`dispatch`](#) function 其中的区别。
Base dispatch function *总是* 同步发 action 给 store 的 reducer,以及由 store 返回的上一个 state 计算出新 state。它期望 actions 会是一个准备好被 reducer 消费掉的普通对象。
[ Middleware ](#) 封装了base dispatch function。它允许了 dispatch function 处理 action 之外的 [异步 action](#)。 middleware 可以被变形,延迟,忽略,以及其他在将 action 或异步 action 传递给下一个 middleware 之前作出解释。获取更多信息请往后看。
### Action Creator
~~~
type ActionCreator = (...args: any) => Action | AsyncAction;
~~~
*Action Creator* 很简单,就是一个创建 action 的函数。别把这两个概念搞混。Action 是一个信息的负载,而 action 创建者是一个创建 action 的工厂。
调用 action creator 只会生产出 action,但不分发。你需要调用 store 的 [`dispatch`](#) function 才会真正引起变化。有时我们讲 *bound action creator* 意味着函数调用 action creator并立即将结果分发给一个特定的 store 实例。
如果 action 创建者需要读取当前状态、做出 API 调用、或引起诸如路由变位等副作用,应该返回一个 [异步 action](#) 而不是 action。
### 异步 Action
~~~
type AsyncAction = any;
~~~
*异步 action* 是一个发给分发函数,但还没有准备好被 reducer 消费的值。它会在被发往 base [`dispatch()`](#) function 之前,被 [ middleware ](#) 变为一个或一组 action。异步 actions 可以有多个 type,取决于使用的 middleware。通常为 Promise 或者 thunk 之类的异步原生,虽然没有被马上传给 reducer,但是操作一旦完成就会触发 action 分发。
### Middleware
~~~
type MiddlewareAPI = { dispatch: Dispatch, getState: () => State };
type Middleware = (api: MiddlewareAPI) => (next: Dispatch) => Dispatch;
~~~
Middleware 是一个高阶函数,它将 [dispatch function](#) 组合并返回一个新的 dispatch function。它通常将 [异步 actions](#) 变为 actions。
Middleware 是使用了复合函数的可构建的。它可在 action 日志,表现副作用例如路由,或将异步 API 调用变为一组同步 actions。
请见 [`applyMiddleware(...middlewares)`](#) 获取有关 middleware 的详细内容。
### Store
~~~
type Store = {
dispatch: Dispatch;
getState: () => State;
subscribe: (listener: () => void) => () => void;
getReducer: () => Reducer;
replaceReducer: (reducer: Reducer) => void;
};
~~~
Store 是一个承载有应用 state tree 的对象。一个 Redux 应用中应当只有一个 Store,因为构建发生于 reducer 级。
- [`dispatch(action)`](#) 是上面描述过的 base dispatch function。
- [`getState()`](#) 返回当前 store 的 state。
- [`subscribe(listener)`](#) 注册 funtion 用于在 state 改变时调用。
- [`getReducer()`](#) 和 [`replaceReducer(nextReducer)`](#) 可被用于实现热重载荷代码分割。通常你用不上他们。
请见完整的 [store API reference](#) 获取更多细节。
### Store Creator
~~~
type StoreCreator = (reducer: Reducer, initialState: ?State) => Store;
~~~
Store creator 是一个创建 Redux store 的函数。就像 dispatching function 那样,我们必须分清由 [`createStore(reducer, initialState)`](#) 从 Redux 包中导出的 base store creator,和从 store enhancer 返回的 store creator。
### Store enhancer
~~~
type StoreEnhancer = (next: StoreCreator) => StoreCreator;
~~~
Store enhancer 是一个高阶函数,将 store creator 组合,返回一个新的强化过的 store creator。这与允许你使用可组合方式变更 store 接口的 middleware 有点相似。
Store enhancer 是与 React 中概念非常相同的高阶 component, 通常也会被叫做 “component enhancers”。
因为 store 并非一个实例,而更像是几个函数的集合普通对象。复制可以被简单的创建或修改而不需变动原先的 store。在 [`compose`](#) 文档中有一个示例演示了这种做法。
大多数时候你不可能去写 store enhancer,但你会用得着 [developer tools](https://github.com/gaearon/redux-devtools) 提供的。它使得app对其发生无察觉的时间旅行变得可能。搞笑的是,[Redux middleware 的实现](#) 本身就是一个 store enhancer。
排错
最后更新于:2022-04-01 05:00:52
# 排错
这里会列出常见的问题和对应的解决方案。虽然使用 React 做示例,即使你使用要其它库仍然会有帮助。
### dispatch action 后什么也没有发生
有时,你 dispatch action 后,view 却没有更新。这是为什么呢?可能有下面几种原因。
#### 永远不要直接修改 reducer 的参数
如果你想修改 Redux 给你传入的 `state` 或 `action`,请住手!
Redux 假定你永远不会修改 reducer 里传入的对象。**任何时候,你都应该返回一个新的 state 对象。**即使你没有使用 [Immutable](https://facebook.github.io/immutable-js/) 这样的库,也要保证做到不修改对象。
不变性(Immutability)可以让 [react-redux](https://github.com/gaearon/react-redux) 高效的监听 state 的细粗度更新。它也让 [redux-devtools](http://github.com/gaearon/redux-devtools) 能提供“时间旅行”这类强大特性。
例如,下面的 reducer 就是错误的,因为它改变了 state:
~~~
function todos(state = [], action) {
switch (action.type) {
case 'ADD_TODO':
// 错误!这会改变 state.actions。
state.actions.push({
text: action.text,
completed: false
});
case 'COMPLETE_TODO':
// 错误!这会改变 state.actions[action.index].
state.actions[action.index].completed = true;
}
return state
}
~~~
应该重写成这样:
~~~
function todos(state = [], action) {
switch (action.type) {
case 'ADD_TODO':
// 返回新数组
return [...state, {
text: action.text,
completed: false
}];
case 'COMPLETE_TODO':
// 返回新数组
return [
...state.slice(0, action.index),
// 修改之前复制数组
Object.assign({}, state[action.index], {
completed: true
}),
...state.slice(action.index + 1)
];
default:
return state;
}
}
~~~
虽然需要写更多代码,但是让 Redux 变得可具有可预测性和高效。如果你想减少代码量,你可以用一些辅助方法类似 [`React.addons.update`](https://facebook.github.io/react/docs/update.html) 来让这样的不可变转换操作变得更简单:
~~~
// 修改前
return [
...state.slice(0, action.index),
Object.assign({}, state[action.index], {
completed: true
}),
...state.slice(action.index + 1)
]
// 修改后
return update(state, {
[action.index]: {
completed: {
$set: true
}
}
});
~~~
最后,如果需要更新 object,你需要使用 Underscore 提供的 `_.extend` 方法,或者更好的,使用 [`Object.assign`](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/assign) 的 polyfill
要注意 `Object.assign` 的使用方法。例如,在 reducer 里不要这样使用 `Object.assign(state, newData)`,应该用 `Object.assign({}, state, newData)`。这样它才不会覆盖以前的 `state`。
你也可以通过使用 [Babel 阶段 1](http://babeljs.io/docs/usage/experimental/) 模式来开启 [ES7 对象的 spread 操作](https://github.com/sebmarkbage/ecmascript-rest-spread):
~~~
// 修改前:
return [
...state.slice(0, action.index),
Object.assign({}, state[action.index], {
completed: true
}),
...state.slice(action.index + 1)
]
// 修改后:
return [
...state.slice(0, action.index),
{ ...state[action.index], completed: true },
...state.slice(action.index + 1)
]
~~~
注意还在实验阶段的特性注定经常改变,最好不要在大的项目里过多依赖它们。
#### 不要忘记调用 [`dispatch(action)`](#)
如果你定义了一个 action 创建函数,调用它并**不**会自动 dispatch 这个 action。比如,下面的代码什么也不会做:
#### `TodoActions.js`
~~~
export function addTodo(text) {
return { type: 'ADD_TODO', text };
}
~~~
#### `AddTodo.js`
~~~
import { Component } from 'react';
import { addTodo } from './TodoActions';
class AddTodo extends Component {
handleClick() {
// 不起作用!
addTodo('Fix the issue');
}
render() {
return (
<button onClick={() => this.handleClick()}>
Add
</button>
);
}
}
~~~
它不起作用是因为你的 action 创建函数只是一个**返回** action 的函数而已。你需要手动 dispatch 它。我们不能在定义时把 action 创建函数绑定到指定的 Store 上,因为应用在服务端渲染时需要为每个请求都对应一个独立的 Redux store。
解法是调用 [store](#) 实例上的 [`dispatch()`](#) 方法。
~~~
handleClick() {
// 生效!(但你需要先以某种方式拿到 store)
store.dispatch(addTodo('Fix the issue'));
}
~~~
如果组件的层级非常深,把 store 一层层传下去很麻烦。因此 [react-redux](https://github.com/gaearon/react-redux) 提供了 `connect` 这个 [高阶组件](https://medium.com/@dan_abramov/mixins-are-dead-long-live-higher-order-components-94a0d2f9e750),它除了可以帮你监听 Redux store,还会把 `dispatch` 注入到组件的 props 中。
修复后的代码是这样的:
~~~
import { Component } from 'react';
import { connect } from 'react-redux';
import { addTodo } from './TodoActions';
class AddTodo extends Component {
handleClick() {
// 生效!
this.props.dispatch(addTodo('Fix the issue'));
}
render() {
return (
<button onClick={() => this.handleClick()}>
Add
</button>
);
}
}
// 除了 state,`connect` 还把 `dispatch` 放到 props 里。
export default connect(AddTodo, state => ({}))
~~~
如果你想的话也可以把 `dispatch` 手动传给其它组件。
### 其它问题
在 Slack [Reactiflux](http://reactiflux.com/) 里的 **redux** 频道里提问,或者[提交一个 issue](https://github.com/rackt/redux/issues)。如果问题终于解决了,请把解法[写到文档里](https://github.com/rackt/redux/edit/master/docs/Troubleshooting.md),以便别人遇到同样问题时参考。
实现撤销重做
最后更新于:2022-04-01 05:00:50
# 实现撤销历史
在应用中内建撤消和重做功能往往需要开发者有意识的做出一些努力。对于经典的 MVC 框架来说这不是一个简单的问题,因为你需要通过克隆所有相关的 model 来追踪每一个历史状态。此外,你需要关心整个撤消堆栈,因为用户初始化的更改也应该是可撤消。
这意味着在一个 MVC 应用中实现撤消和重做,通常迫使你用一些类似于 [Command](https://en.wikipedia.org/wiki/Command_pattern) 的特殊的数据修改模式来重写应用中的部分代码。
而在 Redux 中,实现撤销历史却是轻而易举的。有以下三个原因:
- 你想要跟踪的 state 子树不会包含多个模型(models—just)。
- state 是不可变的,所有修改已经被描述成分离的 action,而这些 action 与预期的撤销堆栈模型很接近了。
- reducer 的签名 `(state, action) => state` 让它可以自然的实现 “reducer enhancers” 或者 “higher order reducers”。它们可以让你在为 reducer 添加额外的功能时保持这个签名。撤消历史就是一个典型的应用场景。
在动手之前,确认你已经阅读过[基础教程](#)并且良好掌握了 [reducer 合成](#)。本文中的代码会构建于 [基础教程](#) 的示例之上。
文章的第一部分,我们将会解释实现撤消和重做功能所用到的基础概念。
在第二部分中,我们会展示如何使用 [Redux Undo](https://github.com/omnidan/redux-undo) 库来无缝地实现撤消和重做。
[![demo of todos-with-undo](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-11-19_564dae5c401a3.gif)](https://twitter.com/dan_abramov/status/647038407286390784)
### 理解撤消历史
### 设计状态结构
撤消历史也是你的应用 state 的一部分,我们没有任何原因通过不同的方式实现它。无论 state 如何随着时间不断变化,当你实现撤消和重做这个功能时,你就必须追踪 state 在不同时刻的**历史记录**。
例如,一个计数器应用的 state 结构看起来可能是这样:
~~~
{
counter: 10
}
~~~
如果我们希望在这样一个应用中实现撤消和重做的话,我们必须保存更多的 state 以解决下面几个问题:
- 撤消或重做留下了哪些信息?
- 当前的状态是什么?
- 撤销堆栈中过去(和未来)的状态是什么?
这是一个对于 state 结构的修改建议,可以回答上述问题的:
~~~
{
counter: {
past: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
present: 10,
future: []
}
}
~~~
现在,如果我们按下“撤消”,我们希望恢复到过去的状态:
~~~
{
counter: {
past: [0, 1, 2, 3, 4, 5, 6, 7, 8],
present: 9,
future: [10]
}
}
~~~
再来一次:
~~~
{
counter: {
past: [0, 1, 2, 3, 4, 5, 6, 7],
present: 8,
future: [9, 10]
}
}
~~~
当我们按下“重做”,我们希望往未来的状态移动一步:
~~~
{
counter: {
past: [0, 1, 2, 3, 4, 5, 6, 7, 8],
present: 9,
future: [10]
}
}
~~~
最终,如果处于撤销堆栈中,用户发起了一个操作(例如,减少计数),我们将会丢弃所有未来的信息:
~~~
{
counter: {
past: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
present: 8,
future: []
}
}
~~~
有趣的一点是,我们在撤销堆栈中保存数字,字符串,数组或是对象都没有关系。整个结构始终完全一致:
~~~
{
counter: {
past: [0, 1, 2],
present: 3,
future: [4]
}
}
~~~
~~~
{
todos: {
past: [
[],
[{ text: 'Use Redux' }],
[{ text: 'Use Redux', complete: true }]
],
present: [{ text: 'Use Redux', complete: true }, { text: 'Implement Undo' }],
future: [
[{ text: 'Use Redux', complete: true }, { text: 'Implement Undo', complete: true }]
]
}
}
~~~
它看起来通常都是这样:
~~~
{
past: Array<T>,
present: T,
future: Array<T>
}
~~~
我们可以保存单一的顶层历史记录:
~~~
{
past: [
{ counterA: 1, counterB: 1 },
{ counterA: 1, counterB: 0 },
{ counterA: 0, counterB: 0 }
],
present: { counterA: 2, counterB: 1 },
future: []
}
~~~
也可以分离的历史记录,用户可以独立地执行撤消和重做操作:
~~~
{
counterA: {
past: [1, 0],
present: 2,
future: []
},
counterB: {
past: [0],
present: 1,
future: []
}
}
~~~
接下来我们将会看到如何选择合适的撤消和重做的颗粒度。
### 设计算法
无论何种特定的数据类型,重做历史记录的 state 结构始终一致:
~~~
{
past: Array<T>,
present: T,
future: Array<T>
}
~~~
让我们讨论一下如何通过算法来操作上文所述的 state 结构。我们可以定义两个 action 来操作该 state:`UNDO` 和 `REDO`。在 reducer 中,我们希望以如下步骤处理这两个 action:
#### 处理 Undo
- 移除 `past` 中的**最后一个**元素。
- 将上一步移除的元素赋予 `present`。
- 将原来的 `present` 插入到 `future` 的**最前面**。
#### 处理 Redo
- 移除 `future` 中的**第一个**元素。
- 将上一步移除的元素赋予 `present`。
- 将原来的 `present` 追加到 `past` 的**最后面**。
#### 处理其他 Action
- 将当前的 `present` 追加到 `past` 的**最后面**。
- 将处理完 action 所产生的新的 state 赋予 `present`。
- 清空 `future`。
### 第一次尝试: 编写 Reducer
~~~
const initialState = {
past: [],
present: null, // (?) 我们如何初始化当前状态?
future: []
};
function undoable(state = initialState, action) {
const { past, present, future } = state;
switch (action.type) {
case 'UNDO':
const previous = past[past.length - 1];
const newPast = past.slice(0, past.length - 1);
return {
past: newPast,
present: previous,
future: [present, ...future]
};
case 'REDO':
const next = future[0];
const newFuture = future.slice(1);
return {
past: [...past, present],
present: next,
future: newFuture
};
default:
// (?) 我们如何处理其他 action?
return state;
}
}
~~~
这个实现是无法使用的,因为它忽略了下面三个重要的问题:
- 我们从何处获取初始的 `present` 状态?我们无法预先知道它。
- 当外部 action 被处理完毕后,我们在哪里完成将 `present` 保存到 `past` 的工作?
- 我们如何将 `present` 状态的控制委托给一个自定义的 reducer?
看起来 reducer 并不是正确的抽象方式,但是我们已经非常接近了。
### 遇见 Reducer Enhancers
你可能已经熟悉 [higher order function](https://en.wikipedia.org/wiki/Higher-order_function) 了。如果你使用过 React,也应该熟悉 [higher order component](https://medium.com/@dan_abramov/mixins-are-dead-long-live-higher-order-components-94a0d2f9e750)。对于 reducer 来说,也有一种对应的实现模式。
一个 **reducer enhancer**(或者一个 **higher order reducer**)作为一个函数,接收 reducer 作为参数,并返回一个新的 reducer,这个新的 reducer 可以处理新的 action,或者维护更多的 state,亦或者将它无法处理的 action 委托给原始的 reducer 处理。这不是什么新的模式技术(pattern—technically),[`combineReducers()`](#)就是一个 reducer enhancer,因为它同样接收多个 reducer 并返回一个新的 reducer。
这是一个没有任何额外功能的 reducer enhancer 的示例:
~~~
function doNothingWith(reducer) {
return function (state, action) {
// 仅仅是调用被传入的 reducer
return reducer(state, action);
};
}
~~~
一个可以组合 reducer 的 reducer enhancer 看起来应该像这样:
~~~
function combineReducers(reducers) {
return function (state = {}, action) {
return Object.keys(reducers).reduce((nextState, key) => {
// 调用每一个 reducer,并将由它管理的部分 state 传给它
nextState[key] = reducers[key](state[key], action);
return nextState;
}, {});
};
}
~~~
### 第二次尝试: 编写 Reducer Enhancer
现在我们对 reducer enhancer 有了更深的了解,我们可以明确所谓的`可撤销`到底是什么:
~~~
function undoable(reducer) {
// 以一个空的 action 调用 reducer 来产生初始的 state
const initialState = {
past: [],
present: reducer(undefined, {}),
future: []
};
// 返回一个可以执行撤销和重做的新的reducer
return function (state = initialState, action) {
const { past, present, future } = state;
switch (action.type) {
case 'UNDO':
const previous = past[past.length - 1];
const newPast = past.slice(0, past.length - 1);
return {
past: newPast,
present: previous,
future: [present, ...future]
};
case 'REDO':
const next = future[0];
const newFuture = future.slice(1);
return {
past: [...past, present],
present: next,
future: newFuture
};
default:
// 将其他 action 委托给原始的 reducer 处理
const newPresent = reducer(present, action);
if (present === newPresent) {
return state;
}
return {
past: [...past, present],
present: newPresent,
future: []
};
}
};
}
~~~
我们现在可以将任意的 reducer 通过这个拥有`可撤销`能力的 reducer enhancer 进行封装,从而让它们可以处理 `UNDO` 和 `REDO` 这两个 action。
~~~
// 这是一个 reducer。
function todos(state = [], action) {
/* ... */
}
// 处理完成之后仍然是一个 reducer!
const undoableTodos = undoable(todos);
import { createStore } from 'redux';
const store = createStore(undoableTodos);
store.dispatch({
type: 'ADD_TODO',
text: 'Use Redux'
});
store.dispatch({
type: 'ADD_TODO',
text: 'Implement Undo'
});
store.dispatch({
type: 'UNDO'
});
~~~
还有一个重要注意点:你需要记住当你恢复一个 state 时,必须把 `.present` 追加到它上面。你也不能忘了需要通过检查 `.past.length` 和 `.future.length` 来决定撤销和重做按钮是否可用。
你可能听说过 Redux 受 [Elm 架构](https://github.com/evancz/elm-architecture-tutorial/) 影响颇深。所以这个示例与 [elm-undo-redo package](http://package.elm-lang.org/packages/TheSeamau5/elm-undo-redo/2.0.0) 也不会太令人吃惊。
### 使用 Redux Undo
以上这些都非常有用,但是有没有一个库能帮助我们实现`可撤销`功能,而不是由我们自己编写呢?当然有!来看看 [Redux Undo](https://github.com/omnidan/redux-undo),它可以为你的 Redux 状态树中的任何部分提供撤销和重做功能。
在这个部分中,你会学到如何让 [示例:Todo List](#) 拥有可撤销的功能。你可以在 [`todos-with-undo`](https://github.com/rackt/redux/tree/master/examples/todos-with-undo)找到完整的源码。
### 安装
首先,你必须先执行
~~~
npm install --save redux-undo
~~~
这一步会安装一个提供`可撤销`功能的 reducer enhancer 的库。
### 封装 Reducer
你需要通过 `undoable` 函数强化你的 reducer。例如,如果使用了 [`combineReducers()`](#),你的代码看起来应该像这样:
#### `reducers.js`
~~~
import undoable, { distinctState } from 'redux-undo';
/* ... */
const todoApp = combineReducers({
visibilityFilter,
todos: undoable(todos, { filter: distinctState() })
});
~~~
`distinctState()` 过滤器将会忽略那些没有引起 state 变化的 action。还有一些[其他选项](https://github.com/omnidan/redux-undo#configuration)来配置你可撤销的 reducer,例如为撤销和重做动作指定 action 的类型。
你可以在 reducer 合并层次中的任何级别对一个或多个 reducer 执行 `undoable`。由于 `visibilityFilter` 的变化并不会影响撤销历史,我们选择只对 `todos` reducer 进行封装,而不是整个顶层的 reducer。
### 更新 Selector
现在 `todos` 相关的 state 看起来应该像这样:
~~~
{
visibilityFilter: 'SHOW_ALL',
todos: {
past: [
[],
[{ text: 'Use Redux' }],
[{ text: 'Use Redux', complete: true }]
],
present: [{ text: 'Use Redux', complete: true }, { text: 'Implement Undo' }],
future: [
[{ text: 'Use Redux', complete: true }, { text: 'Implement Undo', complete: true }]
]
}
}
~~~
这意味着你必须要通过 `state.todos.present` 操作 state,而不是原来的 `state.todos`:
#### `containers/App.js`
~~~
function select(state) {
const presentTodos = state.todos.present;
return {
visibleTodos: selectTodos(presentTodos, state.visibilityFilter),
visibilityFilter: state.visibilityFilter
};
}
~~~
为了确认撤销和重做按钮是否可用,你必须检查 `past` 和 `future` 数组是否为空:
#### `containers/App.js`
~~~
function select(state) {
return {
undoDisabled: state.todos.past.length === 0,
redoDisabled: state.todos.future.length === 0,
visibleTodos: selectTodos(state.todos.present, state.visibilityFilter),
visibilityFilter: state.visibilityFilter
};
}
~~~
### 添加按钮
现在,你需要做的全部事情就只是为撤销和重做操作添加按钮了。
首先,你需要从 `redux-undo` 中导入 `ActionCreators`,并将他们传递给 `Footer` 组件:
#### `containers/App.js`
~~~
import { ActionCreators } from 'redux-undo';
/* ... */
class App extends Component {
render() {
const { dispatch, visibleTodos, visibilityFilter } = this.props;
return (
<div>
{/* ... */}
<Footer
filter={visibilityFilter}
onFilterChange={nextFilter => dispatch(setVisibilityFilter(nextFilter))}
onUndo={() => dispatch(ActionCreators.undo())}
onRedo={() => dispatch(ActionCreators.redo())}
undoDisabled={this.props.undoDisabled}
redoDisabled={this.props.redoDisabled} />
</div>
);
}
}
~~~
在 footer 中渲染它们:
#### `components/Footer.js`
~~~
export default class Footer extends Component {
/* ... */
renderUndo() {
return (
<p>
<button onClick={this.props.onUndo} disabled={this.props.undoDisabled}>Undo</button>
<button onClick={this.props.onRedo} disabled={this.props.redoDisabled}>Redo</button>
</p>
);
}
render() {
return (
<div>
{this.renderFilters()}
{this.renderUndo()}
</div>
);
}
}
~~~
就是这样!在[示例文件夹](https://github.com/rackt/redux/tree/master/examples/todos-with-undo)下执行 `npm install` 和 `npm start` 试试看吧!
计算衍生数据
最后更新于:2022-04-01 05:00:47
# 计算衍生数据
[Reselect](https://github.com/faassen/reselect.git) 是用来创建可记忆的(Memoized)、可组合的 **selector** 函数。Reselect selectors 可以用来高效地计算 Redux store 里的衍生数据。
### 可记忆的 Selectors 初衷
首先访问 [Todos 列表示例](#):
#### `containers/App.js`
~~~
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import { addTodo, completeTodo, setVisibilityFilter, VisibilityFilters } from '../actions';
import AddTodo from '../components/AddTodo';
import TodoList from '../components/TodoList';
import Footer from '../components/Footer';
class App extends Component {
render() {
// 通过 connect() 注入:
const { dispatch, visibleTodos, visibilityFilter } = this.props;
return (
<div>
<AddTodo
onAddClick={text =>
dispatch(addTodo(text))
} />
<TodoList
todos={this.props.visibleTodos}
onTodoClick={index =>
dispatch(completeTodo(index))
} />
<Footer
filter={visibilityFilter}
onFilterChange={nextFilter =>
dispatch(setVisibilityFilter(nextFilter))
} />
</div>
);
}
}
App.propTypes = {
visibleTodos: PropTypes.arrayOf(PropTypes.shape({
text: PropTypes.string.isRequired,
completed: PropTypes.bool.isRequired
})),
visibilityFilter: PropTypes.oneOf([
'SHOW_ALL',
'SHOW_COMPLETED',
'SHOW_ACTIVE'
]).isRequired
};
function selectTodos(todos, filter) {
switch (filter) {
case VisibilityFilters.SHOW_ALL:
return todos;
case VisibilityFilters.SHOW_COMPLETED:
return todos.filter(todo => todo.completed);
case VisibilityFilters.SHOW_ACTIVE:
return todos.filter(todo => !todo.completed);
}
}
function select(state) {
return {
visibleTodos: selectTodos(state.todos, state.visibilityFilter),
visibilityFilter: state.visibilityFilter
};
}
// 把组件包起来,以此来注入 dispatch 和 state
export default connect(select)(App);
~~~
上面的示例中,`select` 调用了 `selectTodos` 来计算 `visibleTodos`。运行没问题,但有一个缺点:每当组件更新时都会计算 `visibleTodos`。如果 state tree 非常大,或者计算量非常大,每次更新都重新计算可能会带来性能问题。Reselect 能帮你省去这些没必要的重新计算。
### 创建可记忆的 Selector
我们需要一个可记忆的 selector 来替代这个 `select`,只在 `state.todos` or `state.visibilityFilter` 变化时重新计算 `visibleTodos`,而在其它部分(非相关)变化时不做计算。
Reselect 提供 `createSelector` 函数来创建可记忆的 selector。`createSelector` 接收一个 input-selectors 数组和一个转换函数作为参数。如果 state tree 的改变会引起 input-selector 值变化,那么 selector 会调用转换函数,传入 input-selectors 作为参数,并返回结果。如果 input-selectors 的值的前一次的一样,它将会直接返回前一次计算的数据,而不会再调用一次转换函数。
让我们定义一个可记忆的 selector `visibleTodosSelector` 来替代 `select`:
#### `selectors/TodoSelectors.js`
~~~
import { createSelector } from 'reselect';
import { VisibilityFilters } from './actions';
function selectTodos(todos, filter) {
switch (filter) {
case VisibilityFilters.SHOW_ALL:
return todos;
case VisibilityFilters.SHOW_COMPLETED:
return todos.filter(todo => todo.completed);
case VisibilityFilters.SHOW_ACTIVE:
return todos.filter(todo => !todo.completed);
}
}
const visibilityFilterSelector = (state) => state.visibilityFilter;
const todosSelector = (state) => state.todos;
export const visibleTodosSelector = createSelector(
[visibilityFilterSelector, todosSelector],
(visibilityFilter, todos) => {
return {
visibleTodos: selectTodos(todos, visibilityFilter),
visibilityFilter
};
}
);
~~~
在上例中,`visibilityFilterSelector` 和 `todosSelector` 是 input-selector。因为他们并不转换数据,所以被创建成普通的非记忆的 selector 函数。但是,`visibleTodosSelector` 是一个可记忆的 selector。他接收 `visibilityFilterSelector` 和 `todosSelector` 为 input-selector,还有一个转换函数来计算过滤的 todos 列表。
### 组合 Selector
可记忆的 selector 自身可以作为其它可记忆的 selector 的 input-selector。下面的 `visibleTodosSelector` 被当作另一个 selector 的 input-selector,来进一步通过关键字(keyword)过滤 todos。
~~~
const keywordSelector = (state) => state.keyword;
const keywordFilterSelector = createSelector(
[visibleTodosSelector, keywordSelector],
(visibleTodos, keyword) => visibleTodos.filter(
todo => todo.indexOf(keyword) > -1
)
);
~~~
### 连接 Selector 和 Redux Store
如果你在使用 react-redux,你可以使用 connect 来连接可忘记的 selector 和 Redux store。
#### `containers/App.js`
~~~
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import { addTodo, completeTodo, setVisibilityFilter } from '../actions';
import AddTodo from '../components/AddTodo';
import TodoList from '../components/TodoList';
import Footer from '../components/Footer';
import { visibleTodosSelector } from '../selectors/todoSelectors.js';
class App extends Component {
render() {
// 通过 connect() 注入:
const { dispatch, visibleTodos, visibilityFilter } = this.props;
return (
<div>
<AddTodo
onAddClick={text =>
dispatch(addTodo(text))
} />
<TodoList
todos={this.props.visibleTodos}
onTodoClick={index =>
dispatch(completeTodo(index))
} />
<Footer
filter={visibilityFilter}
onFilterChange={nextFilter =>
dispatch(setVisibilityFilter(nextFilter))
} />
</div>
);
}
}
App.propTypes = {
visibleTodos: PropTypes.arrayOf(PropTypes.shape({
text: PropTypes.string.isRequired,
completed: PropTypes.bool.isRequired
})),
visibilityFilter: PropTypes.oneOf([
'SHOW_ALL',
'SHOW_COMPLETED',
'SHOW_ACTIVE'
]).isRequired
};
// 把 selector 传递给连接的组件
export default connect(visibleTodosSelector)(App);
~~~
编写测试
最后更新于:2022-04-01 05:00:45
# 编写测试
因为你写的大部分 Redux 代码都是些函数,而且大部分是纯函数,所以很好测,不需要 mock。
### 设置
我们建议用 [Mocha](http://mochajs.org/) 作为测试引擎。
注意因为是在 node 环境下运行,所以你不能访问 DOM。
~~~
npm install --save-dev mocha
~~~
想结合 [Babel](http://babeljs.io) 使用的话,在 `package.json` 的 `scripts` 里加入这一段:
~~~
{
...
"scripts": {
...
"test": "mocha --compilers js:babel/register --recursive",
"test:watch": "npm test -- --watch",
},
...
}
~~~
然后运行 `npm test` 就能单次运行了,或者也可以使用 `npm run test:watch` 在每次有文件改变时自动执行测试。
### Action Creators
Redux 里的 action creators 是会返回普通对象的函数。在测试 action creators 的时候我们想要测试不仅是调用了正确的 action creator,还有是否返回了正确的 action。
#### 示例
~~~
export function addTodo(text) {
return {
type: 'ADD_TODO',
text
};
}
~~~
可以这么测:
~~~
import expect from 'expect';
import * as actions from '../../actions/TodoActions';
import * as types from '../../constants/ActionTypes';
describe('actions', () => {
it('should create an action to add a todo', () => {
const text = 'Finish docs';
const expectedAction = {
type: types.ADD_TODO,
text
};
expect(actions.addTodo(text)).toEqual(expectedAction);
});
}
~~~
### Reducers
Reducer 应该是把 action 应用到之前的 state,并返回新的 state。测试起来是下面这样的。
#### 示例
~~~
import { ADD_TODO } from '../constants/ActionTypes';
const initialState = [{
text: 'Use Redux',
completed: false,
id: 0
}];
export default function todos(state = initialState, action) {
switch (action.type) {
case ADD_TODO:
return [{
id: (state.length === 0) ? 0 : state[0].id + 1,
completed: false,
text: action.text
}, ...state];
default:
return state;
}
}
~~~
可以这么测:
~~~
import expect from 'expect';
import reducer from '../../reducers/todos';
import * as types from '../../constants/ActionTypes';
describe('todos reducer', () => {
it('should return the initial state', () => {
expect(
reducer(undefined, {})
).toEqual([{
text: 'Use Redux',
completed: false,
id: 0
}]);
});
it('should handle ADD_TODO', () => {
expect(
reducer([], {
type: types.ADD_TODO,
text: 'Run the tests'
})
).toEqual([{
text: 'Run the tests',
completed: false,
id: 0
}]);
expect(
reducer([{
text: 'Use Redux',
completed: false,
id: 0
}], {
type: types.ADD_TODO,
text: 'Run the tests'
})
).toEqual([{
text: 'Run the tests',
completed: false,
id: 1
}, {
text: 'Use Redux',
completed: false,
id: 0
}]);
});
~~~
### Components
React components 有一点好,就是他们一般都很小而且依赖于他们的 props。所以很好测。
要测 components 我们要建一个叫 `setup()` 的辅助方法,用来把模拟过的(stubbed)回调函数当作 props 来传入,然后使用 [React 浅渲染](https://facebook.github.io/react/docs/test-utils.html#shallow-rendering) 来渲染组件。这样就可以通过做 “是否调用了回调函数” 这样的断言来写独立的测试。
#### 示例
~~~
import React, { PropTypes, Component } from 'react';
import TodoTextInput from './TodoTextInput';
class Header extends Component {
handleSave(text) {
if (text.length !== 0) {
this.props.addTodo(text);
}
}
render() {
return (
<header className='header'>
<h1>todos</h1>
<TodoTextInput newTodo={true}
onSave={this.handleSave.bind(this)}
placeholder='What needs to be done?' />
</header>
);
}
}
Header.propTypes = {
addTodo: PropTypes.func.isRequired
};
export default Header;
~~~
可以这么测:
~~~
import expect from 'expect';
import jsdomReact from '../jsdomReact';
import React from 'react/addons';
import Header from '../../components/Header';
import TodoTextInput from '../../components/TodoTextInput';
const { TestUtils } = React.addons;
function setup() {
let props = {
addTodo: expect.createSpy()
};
let renderer = TestUtils.createRenderer();
renderer.render(<Header {...props} />);
let output = renderer.getRenderOutput();
return {
props: props,
output: output,
renderer: renderer
};
}
describe('components', () => {
jsdomReact();
describe('Header', () => {
it('should render correctly', () => {
const { output } = setup();
expect(output.type).toBe('header');
expect(output.props.className).toBe('header');
let [h1, input] = output.props.children;
expect(h1.type).toBe('h1');
expect(h1.props.children).toBe('todos');
expect(input.type).toBe(TodoTextInput);
expect(input.props.newTodo).toBe(true);
expect(input.props.placeholder).toBe('What needs to be done?');
});
it('should call call addTodo if length of text is greater than 0', () => {
const { output, props } = setup();
let input = output.props.children[1];
input.props.onSave('');
expect(props.addTodo.calls.length).toBe(0);
input.props.onSave('Use Redux');
expect(props.addTodo.calls.length).toBe(1);
});
});
});
~~~
#### `setState()` 异常修复
浅渲染目前的问题是 [如果调用 `setState` 便抛异常](https://github.com/facebook/react/issues/4019). React 貌似想要的是,如果想要使用 `setState`,DOM 就一定要存在(但测试运行在 node 环境下,是没有 DOM 的)。要解决这个问题,我们用了 jsdom,为了在 DOM 无效的时候,React 也不抛异常。按下面方法设置它:
~~~
npm install --save-dev jsdom mocha-jsdom
~~~
然后添加 `jsdomReact()` 帮助函数,是这样的:
~~~
import ExecutionEnvironment from 'react/lib/ExecutionEnvironment';
import jsdom from 'mocha-jsdom';
export default function jsdomReact() {
jsdom();
ExecutionEnvironment.canUseDOM = true;
}
~~~
要在运行任何的 component 测试之前调用。注意这么做不优雅,等以后 [facebook/react#4019](https://github.com/facebook/react/issues/4019) 解决了之后,这段代码就可以删除了。
### 词汇表
-
[React Test Utils](http://facebook.github.io/react/docs/test-utils.html): 跟 React 一块来的测试小助手。
-
[jsdom](https://github.com/tmpvar/jsdom): 一个 JavaScript 的内建 DOM 。Jsdom 允许没浏览器的时候也能跑测试。
-
[浅渲染(shallow renderer)](http://facebook.github.io/react/docs/test-utils.html#shallow-rendering): 浅渲染的中心思想是,初始化一个 component 然后得到它的`渲染`方法作为结果,比起渲染成 DOM 那么深的只有一级那么深。浅渲染的结果是一个 [ReactElement](https://facebook.github.io/react/docs/glossary.html#react-elements) ,意味着可以访问它的 children, props 还能测试是否工作正常。
服务端渲染
最后更新于:2022-04-01 05:00:43
# 服务端渲染
服务端渲染一个很常见的场景是当用户(或搜索引擎爬虫)第一次请求页面时,用它来做*初始渲染*。当服务器接收到请求后,它把需要的组件渲染成 HTML 字符串,然后把它返回给客户端(这里统指浏览器)。之后,客户端会接手渲染控制权。
下面我们使用 React 来做示例,对于支持服务端渲染的其它 view 框架,做法也是类似的。
### 服务端使用 Redux
当在服务器使用 Redux 渲染时,一定要在响应中包含应用的 state,这样客户端可以把它作为初始 state。这点至关重要,因为如果在生成 HTML 前预加载了数据,我们希望客户端也能访问这些数据。否则,客户端生成的 HTML 与服务器端返回的 HTML 就会不匹配,客户端还需要重新加载数据。
把数据发送到客户端,需要以下步骤:
- 为每次请求创建全新的 Redux store 实例;
- 按需 dispatch 一些 action;
- 从 store 中取出 state;
- 把 state 一同返回给客户端。
在客户端,使用服务器返回的 state 创建并初始化一个全新的 Redux store。
Redux 在服务端**惟一**要做的事情就是,提供应用所需的**初始 state**。
### 安装
下面来介绍如何配置服务端渲染。使用极简的 [Counter 计数器应用](https://github.com/rackt/redux/tree/master/examples/counter) 来做示例,介绍如何根据请求在服务端提前渲染 state。
### 安装依赖库
本例会使用 [Express](http://expressjs.com/) 来做小型的 web 服务器。引入 [serve-static](https://www.npmjs.com/package/serve-static) middleware 来处理静态文件,稍后有代码。
还需要安装 Redux 对 React 的绑定库,Redux 默认并不包含。
~~~
npm install --save express serve-static react-redux
~~~
### 服务端开发
下面是服务端代码大概的样子。使用 [app.use](http://expressjs.com/api.html#app.use) 挂载 [Express middleware](http://expressjs.com/guide/using-middleware.html) 处理所有请求。`serve-static` middleware 以同样的方式处理来自客户端的 javascript 文件请求。如果你还不熟悉 Express 或者 middleware,只需要了解每次服务器收到请求时都会调用 handleRender 函数。
> ##### 生产环境使用须知
> 在生产环境中,最好使用类似 nigix 这样的服务器来处理静态文件请求,只使用 Node 处理应用请求。虽然这个话题已经超出本教程讨论范畴。
##### `server.js`
~~~
import path from 'path';
import Express from 'express';
import React from 'react';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import counterApp from './reducers';
import App from './containers/App';
const app = Express();
const port = 3000;
// 使用这个 middleware 处理 dist 目录下的静态文件请求
app.use(require('serve-static')(path.join(__dirname, 'dist')));
// 每当收到请求时都会触发
app.use(handleRender);
// 接下来会补充这部分代码
function handleRender(req, res) { /* ... */ }
function renderFullPage(html, initialState) { /* ... */ }
app.listen(port);
~~~
### 处理请求
第一件要做的事情就是对每个请求创建一个新的 Redux store 实例。这个 store 惟一作用是提供应用初始的 state。
渲染时,使用 `<Provider>` 来包住根组件 `<App />`,以此来让组件树中所有组件都能访问到 store,就像之前的[搭配 React](#) 教程讲的那样。
服务端渲染最关键的一步是在**发送响应前**渲染初始的 HTML。这就要使用 [React.renderToString()](https://facebook.github.io/react/docs/top-level-api.html#react.rendertostring).
然后使用 [`store.getState()`](#) 从 store 得到初始 state。`renderFullPage` 函数会介绍接下来如何传递。
~~~
function handleRender(req, res) {
// 创建新的 Redux store 实例
const store = createStore(counterApp);
// 把组件渲染成字符串
const html = React.renderToString(
<Provider store={store}>
{() => <App />}
</Provider>
);
// 从 store 中获得 state
const initialState = store.getState();
// 把渲染后的页面内容发送给客户端
res.send(renderFullPage(html, initialState));
}
~~~
### 注入初始组件的 HTML 和 State
服务端最后一步就是把初始组件的 HTML 和初始 state 注入到客户端能够渲染的模板中。如何传递 state 呢,我们添加一个 `<script>` 标签来把 `initialState` 赋给 `window.__INITIAL_STATE__`。
客户端可以通过 `window.__INITIAL_STATE__` 获取 `initialState`。
同时使用 script 标签来引入打包后的 js bundle 文件。之前引入的 `serve-static` middleware 会处理它的请求。下面是代码。
~~~
function renderFullPage(html, initialState) {
return `
<!doctype html>
<html>
<head>
<title>Redux Universal Example</title>
</head>
<body>
<div id="app">${html}</div>
<script>
window.__INITIAL_STATE__ = ${JSON.stringify(initialState)};
</script>
<script src="/bundle.js"></script>
</body>
</html>
`;
}
~~~
> ##### 字符串插值语法须知
> 上面的示例使用了 ES6 的[模板字符串](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/template_strings)语法。它支持多行字符串和字符串插补特性,但需要支持 ES6。如果要在 Node 端使用 ES6,参考 [Babel require hook](https://babeljs.io/docs/usage/require/) 文档。你也可以继续使用 ES5。
### 客户端开发
客户端代码非常直观。只需要从 `window.__INITIAL_STATE__` 得到初始 state,并传给 [`createStore()`](#) 函数即可。
代码如下:
#### `client.js`
~~~
import React from 'react';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import App from './containers/App';
import counterApp from './reducers';
// 通过服务端注入的全局变量得到初始 state
const initialState = window.__INITIAL_STATE__;
// 使用初始 state 创建 Redux store
const store = createStore(counterApp, initialState);
React.render(
<Provider store={store}>
{() => <App />}
</Provider>,
document.getElementById('root')
);
~~~
你可以选择自己喜欢的打包工具(Webpack, Browserify 或其它)来编译并打包文件到 `dist/bundle.js`。
当页面加载时,打包后的 js 会启动,并调用 [`React.render()`](https://facebook.github.io/react/docs/top-level-api.html#react.render),然后会与服务端渲染的 HTML 的 `data-react-id` 属性做关联。这会把新生成的 React 实例与服务端的虚拟 DOM 连接起来。因为同样使用了来自 Redux store 的初始 state,并且 view 组件代码是一样的,结果就是我们得到了相同的 DOM。
就是这样!这就是实现服务端渲染的所有步骤。
但这样做还是比较原始的。只会用动态代码渲染一个静态的 View。下一步要做的是动态创建初始 state 支持动态渲染 view。
### 准备初始 State
因为客户端只是执行收到的代码,刚开始的初始 state 可能是空的,然后根据需要获取 state。在服务端,渲染是同步执行的而且我们只有一次渲染 view 的机会。在收到请求时,可能需要根据请求参数或者外部 state(如访问 API 或者数据库),计算后得到初始 state。
### 处理 Request 参数
服务端收到的惟一输入是来自浏览器的请求。在服务器启动时可能需要做一些配置(如运行在开发环境还是生产环境),但这些配置是静态的。
请求会包含 URL 请求相关信息,包括请求参数,它们对于做 [React Router](https://github.com/rackt/react-router) 路由时可能会有用。也可能在请求头里包含 cookies,鉴权信息或者 POST 内容数据。下面演示如何基于请求参数来得到初始 state。
#### `server.js`
~~~
import qs from 'qs'; // 添加到文件开头
function handleRender(req, res) {
// 如果存在的话,从 request 读取 counter
const params = qs.parse(req.query);
const counter = parseInt(params.counter) || 0;
// 得到初始 state
let initialState = { counter };
// 创建新的 Redux store 实例
const store = createStore(counterApp, initialState);
// 把组件渲染成字符串
const html = React.renderToString(
<Provider store={store}>
{() => <App />}
</Provider>
);
// 从 Redux store 得到初始 state
const finalState = store.getState();
// 把渲染后的页面发给客户端
res.send(renderFullPage(html, finalState));
}
~~~
上面的代码首先访问 Express 的 `Request` 对象。把参数转成数字,然后设置到初始 state 中。如果你在浏览器中访问 [http://localhost:3000/?counter=100](http://localhost:3000/?counter=100),你会看到计数器从 100 开始。在渲染后的 HTML 中,你会看到计数显示 100 同时设置进了 `__INITIAL_STATE__` 变量。
### 获取异步 State
服务端渲染常用的场景是处理异步 state。因为服务端渲染天生是同步的,因此异步的数据获取操作对应到同步操作非常重要。
最简单的做法是往同步代码里传递一些回调函数。在这个回调函数里引用响应对象,把渲染后的 HTML 发给客户端。不要担心,并没有想像中那么难。
本例中,我们假设有一个外部数据源提供计算器的初始值(所谓的把计算作为一种服务)。我们会模拟一个请求并使用结果创建初始 state。API 请求代码如下:
#### `api/counter.js`
~~~
function getRandomInt(min, max) {
return Math.floor(Math.random() * (max - min)) + min;
}
export function fetchCounter(callback) {
setTimeout(() => {
callback(getRandomInt(1, 100));
}, 500);
}
~~~
再次说明一下,这只是一个模拟的 API,我们使用 `setTimeout` 模拟一个需要 500 毫秒的请求(实现项目中 API 请求一般会更快)。传入一个回调函数,它异步返回一个随机数字。如果你使用了基于 Promise 的 API 工具,那么要把回调函数放到 `then` 中。
在服务端,把代码使用 `fetchCounter` 包起来,在回调函数里拿到结果:
#### `server.js`
~~~
// Add this to our imports
import { fetchCounter } from './api/counter';
function handleRender(req, res) {
// 异步请求模拟的 API
fetchCounter(apiResult => {
// 如果存在的话,从 request 读取 counter
const params = qs.parse(req.query);
const counter = parseInt(params.counter) || apiResult || 0;
// 得到初始 state
let initialState = { counter };
// 创建新的 Redux store 实例
const store = createStore(counterApp, initialState);
// 把组件渲染成字符串
const html = React.renderToString(
<Provider store={store}>
{() => <App />}
</Provider>
);
// 从 Redux store 得到初始 state
const finalState = store.getState();
// 把渲染后的页面发给客户端
res.send(renderFullPage(html, finalState));
});
}
~~~
因为在回调中使用了 `res.send()`,服务器会保护连接打开并在回调函数执行前不发送任何数据。你会发现每个请求都有 500ms 的延时。更高级的用法会包括对 API 请求出错进行处理,比如错误的请求或者超时。
### 安全注意事项
因为我们代码中很多是基于用户生成内容(UGC)和输入的,不知不觉中,提高了应用可能受攻击区域。任何应用都应该对用户输入做安全处理以避免跨站脚本攻击(XSS)或者代码注入。
我们的示例中,只对安全做基本处理。当从请求中拿参数时,对 `counter` 参数使用 `parseInt` 把它转成数字。如果不这样做,当 request 中有 script 标签时,很容易在渲染的 HTML 中生成危险代码。就像这样的:`?counter=</script><script>doSomethingBad();</script>`
在我们极简的示例中,把输入转成数字已经比较安全。如果处理更复杂的输入,比如自定义格式的文本,你应该用安全函数处理输入,比如 [validator.js](https://www.npmjs.com/package/validator)。
此外,可能添加额外的安全层来对产生的 state 进行消毒。`JSON.stringify` 可能会造成 script 注入。鉴于此,你需要清洗 JSON 字符串中的 HTML 标签和其它危险的字符。可能通过字符串替换或者使用复杂的库如 [serialize-javascript](https://github.com/yahoo/serialize-javascript) 处理。
### 下一步
你还可以参考 [异步 Actions](#) 学习更多使用 Promise 和 thunk 这些异步元素来表示异步数据流的方法。记住,那里学到的任何内容都可以用于同构渲染。
如果你使用了 [React Router](https://github.com/rackt/react-router),你可能还需要在路由处理组件中使用静态的 `fetchData()` 方法来获取依赖的数据。它可能返回 [异步 action](#),以便你的 `handleRender` 函数可以匹配到对应的组件类,对它们均 dispatch `fetchData()` 的结果,在 Promise 解决后才渲染。这样不同路由需要调用的 API 请求都并置于路由处理组件了。在客户端,你也可以使用同样技术来避免在切换页面时,当数据还没有加载完成前执行路由。(Revision needed)
减少样板代码
最后更新于:2022-04-01 05:00:41
# 减少样板代码
Redux 很大部分 [受到 Flux 的启发](#),并且最常见的关于 Flux 抱怨是它如何使得你写了一大堆的模板。在这个技巧中,我们将考虑 Redux 如何使得我们选择我们的代码会变得怎样繁复,取决于个人样式,团队选项,长期可维护等等。
### Actions
Actions 是描述了在 app 中所发生的,以单独方式描述对象变异意图的服务的一个普通对象。很重要的一点是 **你必须分发的 action 对象并不是一个模板,而是 Redux 的一个[基本设计选项](#)**.
有些框架生成自己和 Flux 很像,不过缺少了 action 对象的概念。为了变得可预测,这是一个从 Flux or Redux 的倒退。如果没有可串行的普通对象 action,便无法记录或重放用户会话,或者无法实现 [带有时间旅行的热重载](https://www.youtube.com/watch?v=xsSnOQynTHs)。如果你更喜欢直接修改数据,那么你并不需要 Redux 。
Action 一般长这样:
~~~
{ type: 'ADD_TODO', text: 'Use Redux' }
{ type: 'REMOVE_TODO', id: 42 }
{ type: 'LOAD_ARTICLE', response: { ... } }
~~~
一个约定俗成的是 actions 拥有一个定值 type 帮助 reducer (或 Flux 中的 Stores ) 识别它们。我们建议的你使用 string 而不是 [Symbols](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Symbol) 作为 action type ,因为 string 是可串行的,而使用 Symbols 的话你会把记录和重演变得比所需要的更难。
在 Flux 中,传统上认为你将每个 action type 定义为string定值:
~~~
const ADD_TODO = 'ADD_TODO';
const REMOVE_TODO = 'REMOVE_TODO';
const LOAD_ARTICLE = 'LOAD_ARTICLE';
~~~
这么做的优势?**人们通常声称定值不是必要的,对于小的项目可能是正确的。** 对于大的项目,将action types定义为定值有如下好处:
- 帮助维护命名一致性,因为所有的 action type 汇总在同一位置。
- 有的时候,在开发一个新功能之前你想看到所有现存的 actions 。可能的情况是你的团队里已经有人添加了你所需要的action,而你并不知道。
- Action types 列表在Pull Request中能查到所有添加,删除,修改的记录。这能帮助团队中的所有人及时追踪新功能的范围与实现。
- 如果你在导入一个 Action 定值的时候拼写错误,你会得到 `undefined` 。当你纳闷 action 被分发出去而什么也没发生的时候,一个拼写错误更容易被发现。
你的项目的约定取决与你自己。你开始的时候可能用的是inline string,之后转为定值,也许之后将他们归为一个独立文件。Redux 不会给予任何建议,选择你自己最喜欢的。
### Action Creators
另一个约定是,你创建生成 action 对象的函数,而不是在你分发的时候内联生成它们。
例如,用文字对象取代调用 `dispatch` :
~~~
// somewhere in an event handler
dispatch({
type: 'ADD_TODO',
text: 'Use Redux'
});
~~~
你可以在单独的文件中写一个 action creator ,然后从 component 里导入:
#### `actionCreators.js`
~~~
export function addTodo(text) {
return {
type: 'ADD_TODO',
text
};
}
~~~
#### `AddTodo.js`
~~~
import { addTodo } from './actionCreators';
// event handler 里的某处
dispatch(addTodo('Use Redux'))
~~~
Action creators 总被当作模板受到批评。好吧,其实你并不用把他们写出来!**如果你觉得更适合你的项目你可以选用对象文字** 然而,你应该知道写 action creators 是存在某种优势的。
假设有个设计师看完我们的原型之后回来说,我们需要允许三个 todo 不能再多了。我们可以使用 [redux-thunk](https://github.com/gaearon/redux-thunk) 中间件添加一个提前退出,把我们的 action creator 重写成回调形式:
~~~
function addTodoWithoutCheck(text) {
return {
type: 'ADD_TODO',
text
};
}
export function addTodo(text) {
// Redux Thunk 中间件允许这种形式
// 在下面的 “异步 Action Creators” 段落中有写
return function (dispatch, getState) {
if (getState().todos.length === 3) {
// 提前退出
return;
}
dispatch(addTodoWithoutCheck(text));
}
}
~~~
我们刚修改了 `addTodo` action creator 的行为,对调用它的代码完全不可见。**我们不用担心去看每个添加 todo 的地方保证他们有了这个检查** Action creator 让你可以解耦额外的分发 action 逻辑与实际的 components 发送这些 actions,而且当你在重开发经常要改变需求的时候也会非常有用。
### 生成 Action Creators
某些框架如 [Flummox](https://github.com/acdlite/flummox) 自动从 action creator 函数定义生成 action type 定值。这个想法是说你不需要 `ADD_TODO` 定值和 `addTodo()` action creator两个都自己定义。这样的方法在底层也生成 action type 定值,但他们是隐式生成的,也就是间接级。
我们不建议用这样的方法。如果你写像这样简单的 action creator 写烦了:
~~~
export function addTodo(text) {
return {
type: 'ADD_TODO',
text
};
}
export function removeTodo(id) {
return {
type: 'REMOVE_TODO',
id
};
}
~~~
你可以写一个生成 action creator 的函数:
~~~
function makeActionCreator(type, ...argNames) {
return function(...args) {
let action = { type };
argNames.forEach((arg, index) => {
action[argNames[index]] = args[index];
});
return action;
}
}
export const addTodo = makeActionCreator('ADD_TODO', 'todo');
export const removeTodo = makeActionCreator('REMOVE_TODO', 'id');
~~~
参见 [redux-action-utils](https://github.com/insin/redux-action-utils) 和 [redux-actions](https://github.com/acdlite/redux-actions) 获得更多介绍这样的常用工具。
注意这样的工具给你的代码添加了魔法。魔法和间接声明真的值得多写一两行代码么?
### 异步 Action Creators
[中间件](#) 让你注入一个定制逻辑,可以在每个 action 对象分发出去之前解释。异步 actions 是中间件的最常见用例。
没有中间件的话,[`dispatch`](#) 只能接收一个普通对象。所以我们在 components 里面进行 AJAX 调用:
#### `actionCreators.js`
~~~
export function loadPostsSuccess(userId, response) {
return {
type: 'LOAD_POSTS_SUCCESS',
userId,
response
};
}
export function loadPostsFailure(userId, error) {
return {
type: 'LOAD_POSTS_FAILURE',
userId,
error
};
}
export function loadPostsRequest(userId) {
return {
type: 'LOAD_POSTS_REQUEST',
userId
};
}
~~~
#### `UserInfo.js`
~~~
import { Component } from 'react';
import { connect } from 'react-redux';
import { loadPostsRequest, loadPostsSuccess, loadPostsFailure } from './actionCreators';
class Posts extends Component {
loadData(userId) {
// 调用 React Redux `connect()` 注入 props :
let { dispatch, posts } = this.props;
if (posts[userId]) {
// 这里是被缓存的数据!啥也不做。
return;
}
// Reducer 可以通过设置 `isFetching` 反应这个 action
// 因此让我们显示一个 Spinner 控件。
dispatch(loadPostsRequest(userId));
// Reducer 可以通过填写 `users` 反应这些 actions
fetch(`http://myapi.com/users/${userId}/posts`).then(
response => dispatch(loadPostsSuccess(userId, response)),
error => dispatch(loadPostsFailure(userId, error))
);
}
componentDidMount() {
this.loadData(this.props.userId);
}
componentWillReceiveProps(nextProps) {
if (nextProps.userId !== this.props.userId) {
this.loadData(nextProps.userId);
}
}
render() {
if (this.props.isLoading) {
return <p>Loading...</p>;
}
let posts = this.props.posts.map(post =>
<Post post={post} key={post.id} />
);
return <div>{posts}</div>;
}
}
export default connect(state => ({
posts: state.posts
}))(Posts);
~~~
然而,不久就需要再来一遍,因为不同的 components 从同样的 API 端点请求数据。而且,我们想要在多个components 中重用一些逻辑(比如,当缓存数据有效的时候提前退出)。
**中间件让我们写的更清楚M的潜在的异步 action creators.** 它使得我们分发普通对象之外的东西,并且解释它们的值。比如,中间件能 “捕捉” 到已经分发的 Promises 并把他们变为一对请求和成功/失败 actions.
最简单的中间件例子是 [redux-thunk](https://github.com/gaearon/redux-thunk). **“Thunk” 中间件让你把 action creators 写成 “thunks”,也就是返回函数的函数。** 这使得控制被反转了: 你会像一个参数一样取得 `dispatch` ,所以你也能写一个多次分发的 action creator 。
> ##### 注意
> Thunk 只是中间件的一个例子。中间件不是关于 “让你分发函数” 的:它是关于让你分发你用的特定中间件知道如何处理的任何东西的。Thunk 中间件添加了一个特定的行为用来分发函数,但这实际上取决于你用的中间件。
考虑上面的代码用 [redux-thunk](https://github.com/gaearon/redux-thunk) 重写:
#### `actionCreators.js`
~~~
export function loadPosts(userId) {
// 用 thunk 中间件解释:
return function (dispatch, getState) {
let { posts } = getState();
if (posts[userId]) {
// 这里是数据缓存!啥也不做。
return;
}
dispatch({
type: 'LOAD_POSTS_REQUEST',
userId
});
// 异步分发原味 actions
fetch(`http://myapi.com/users/${userId}/posts`).then(
response => dispatch({
type: 'LOAD_POSTS_SUCCESS',
userId,
respone
}),
error => dispatch({
type: 'LOAD_POSTS_FAILURE',
userId,
error
})
);
}
}
~~~
#### `UserInfo.js`
~~~
import { Component } from 'react';
import { connect } from 'react-redux';
import { loadPosts } from './actionCreators';
class Posts extends Component {
componentDidMount() {
this.props.dispatch(loadPosts(this.props.userId));
}
componentWillReceiveProps(nextProps) {
if (nextProps.userId !== this.props.userId) {
this.props.dispatch(loadPosts(nextProps.userId));
}
}
render() {
if (this.props.isLoading) {
return <p>Loading...</p>;
}
let posts = this.props.posts.map(post =>
<Post post={post} key={post.id} />
);
return <div>{posts}</div>;
}
}
export default connect(state => ({
posts: state.posts
}))(Posts);
~~~
这样打得字少多了!如果你喜欢,你还是可以保留 “原味” action creators 比如从一个 “聪明的” `loadPosts` action creator 里用到的 `loadPostsSuccess` 。
**最后,你可以重写中间件** 你可以把上面的模式泛化,然后代之以这样的异步 action creators :
~~~
export function loadPosts(userId) {
return {
// 要在之前和之后发送的 action types
types: ['LOAD_POSTS_REQUEST', 'LOAD_POSTS_SUCCESS', 'LOAD_POSTS_FAILURE'],
// 检查缓存 (可选):
shouldCallAPI: (state) => !state.users[userId],
// 进行取:
callAPI: () => fetch(`http://myapi.com/users/${userId}/posts`),
// 在 actions 的开始和结束注入的参数
payload: { userId }
};
}
~~~
解释这个 actions 的中间件可以像这样:
~~~
function callAPIMiddleware({ dispatch, getState }) {
return function (next) {
return function (action) {
const {
types,
callAPI,
shouldCallAPI = () => true,
payload = {}
} = action;
if (!types) {
// 普通 action:传走
return next(action);
}
if (
!Array.isArray(types) ||
types.length !== 3 ||
!types.every(type => typeof type === 'string')
) {
throw new Error('Expected an array of three string types.');
}
if (typeof callAPI !== 'function') {
throw new Error('Expected fetch to be a function.');
}
if (!shouldCallAPI(getState())) {
return;
}
const [requestType, successType, failureType] = types;
dispatch(Object.assign({}, payload, {
type: requestType
}));
return callAPI().then(
response => dispatch(Object.assign({}, payload, {
response: response,
type: successType
})),
error => dispatch(Object.assign({}, payload, {
error: error,
type: failureType
}))
);
};
};
}
~~~
在传给 [`applyMiddleware(...middlewares)`](#) 一次以后,你能用相同方式写你的 API-调用 action creators :
~~~
export function loadPosts(userId) {
return {
types: ['LOAD_POSTS_REQUEST', 'LOAD_POSTS_SUCCESS', 'LOAD_POSTS_FAILURE'],
shouldCallAPI: (state) => !state.users[userId],
callAPI: () => fetch(`http://myapi.com/users/${userId}/posts`),
payload: { userId }
};
}
export function loadComments(postId) {
return {
types: ['LOAD_COMMENTS_REQUEST', 'LOAD_COMMENTS_SUCCESS', 'LOAD_COMMENTS_FAILURE'],
shouldCallAPI: (state) => !state.posts[postId],
callAPI: () => fetch(`http://myapi.com/posts/${postId}/comments`),
payload: { postId }
};
}
export function addComment(postId, message) {
return {
types: ['ADD_COMMENT_REQUEST', 'ADD_COMMENT_SUCCESS', 'ADD_COMMENT_FAILURE'],
callAPI: () => fetch(`http://myapi.com/posts/${postId}/comments`, {
method: 'post',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({ message })
}),
payload: { postId, message }
};
}
~~~
### Reducers
Redux 用函数描述逻辑更新减少了模版里大量的 Flux stores 。函数比对象简单,比类更简单得多。
考虑这个 Flux store:
~~~
let _todos = [];
export default const TodoStore = assign({}, EventEmitter.prototype, {
getAll() {
return _todos;
}
});
AppDispatcher.register(function (action) {
switch (action.type) {
case ActionTypes.ADD_TODO:
let text = action.text.trim();
_todos.push(text);
TodoStore.emitChange();
}
});
~~~
用了 Redux 之后,同样的逻辑更新可以被写成 reducing function:
~~~
export function todos(state = [], action) {
switch (action.type) {
case ActionTypes.ADD_TODO:
let text = action.text.trim();
return [...state, text];
default:
return state;
}
}
~~~
`switch` 语句 *不是* 真正的模版。真正的 Flux 模版是概念性的:发送更新的需求,用 Dispatcher 注册 Store 的需求,Store 是对象的需求 (当你想要一个哪都能跑的 App 的时候复杂度会提升)。
不幸的是很多人仍然靠文档里用没用 `switch` 来选择 Flux 框架。如果你不爱用 `switch` 你可以用一个单独的函数来解决,下面会演示。
### 生成 Reducers
让我们写一个函数使得我们将 reducers 表达为 action types 到 handlers 的映射对象。例如,在我们的 `todos` reducer 里这样定义:
~~~
export const todos = createReducer([], {
[ActionTypes.ADD_TODO](state, action) {
let text = action.text.trim();
return [...state, text];
}
}
~~~
我们可以写下面的帮忙函数来完成:
~~~
function createReducer(initialState, handlers) {
return function reducer(state = initialState, action) {
if (handlers.hasOwnProperty(action.type)) {
return handlers[action.type](state, action);
} else {
return state;
}
}
}
~~~
不难对吧?Redux 没有默认提供这样的帮忙函数,因为有好多种写的方法。可能你想要自动把普通 JS 对象变成不可变对象通过湿化服务器状态。可能你想合并返回状态和当前状态。有很多方法 “获取所有” handler。这些都取决于你为你的团队在特定项目中选择的约定。
Redux reducer 的 API 是 `(state, action) => state`,但是怎么创建这些 reducers 由你来定。
迁移到 Redux
最后更新于:2022-04-01 05:00:38
# 迁移到 Redux
Redux 不是一个整体的框架,而是一系列的约定和[一些让他们协同工作的函数](#)。你的 Redux 项目中的主要代码不会是使用 Redux 的 API,因为大多数时间你都会在编写功能。
这让到 Redux 的双向迁移都非常的容易。我们并不想限制你!
### 迁移 Flux 项目
[Reducer](#) 抓住了 Flux Store 的本质,所以这让逐步迁移一个 Flux 项目到 Redux 上面来变成了可能,无论你使用了 [Flummox](http://github.com/acdlite/flummox)、[Alt](http://github.com/goatslacker/alt)、[traditional Flux](https://github.com/facebook/flux) 还是其他 Flux 库。
同样你也可以将 Redux 的项目通过相同的步骤迁移回上述的这些 Flux 框架。
你的迁移过程大致包含几个步骤:
-
创建一个叫做 `createFluxStore(reducer)` 的函数,通过 reducer 函数适配你当前项目的 Flux Store。从代码来看,这个函数很像 Redux 中 [`createStore`](#) 的实现。它的 dispatch 处理器应该根据不同的 action 来调用不同的 `reducer`,保存新的 state 并抛出更新事件。
-
通过创建 `createFluxStore(reducer)` 的方法来将每个 Flux Store 逐步重写为 Reducer,这个过程中你的应用中其他部分代码感知不到任何变化,仍可以和原来一样使用 Flux Store 。
-
当重写你的 Store 时,你会发现你应该避免一些明显违反 Flux 模式的使用方法,例如在 Store 中请求 API、或者在 Store 中触发 action。一旦基于 reducer 来构建你的 Flux 代码,它会变得更易于理解。
-
当你所有的 Flux Store 全部基于 reducer 来实现时,你就可以利用 [`combineReducers(reducers)`](#) 将多个 reducer 合并到一起,然后在应用里使用这个唯一的 Redux Store。
-
现在,剩下的就只是[使用 react-redux](#) 或者类似的库来处理你的UI部分。
-
最后,你可以使用一些 Redux 的特性,例如利用 middleware 来进一步简化异步的代码。
### 迁移 Backbone 项目
对不起,你需要重写你的 Model 层。它们区别太大了!
技巧
最后更新于:2022-04-01 05:00:36
# 技巧
这一章是关于实现应用开发中会遇到的一些典型场景和代码片段。本章内容建立在你已经学会[基础章节](#)和[高级章节](#)的基础上。
- [迁移到 Redux](#)
- [减少样板代码](#)
- [服务端渲染](#)
- [编写测试](#)
- [计算衍生数据](#)
- [实现撤销重做](#)
下一步
最后更新于:2022-04-01 05:00:34
# Next Steps
Sorry, but we’re still writing this doc.
Stay tuned, it will appear in a day or two.
示例:Reddit API
最后更新于:2022-04-01 05:00:31
# 示例:Reddit API
这是一个[高级教程](#)的例子,包含使用 Reddit API 请求文章标题的全部源码。
### 入口
#### `index.js`
~~~
import 'babel-core/polyfill';
import React from 'react';
import Root from './containers/Root';
React.render(
<Root />,
document.getElementById('root')
);
~~~
### Action Creators and Constants
#### `actions.js`
~~~
import fetch from 'isomorphic-fetch';
export const REQUEST_POSTS = 'REQUEST_POSTS';
export const RECEIVE_POSTS = 'RECEIVE_POSTS';
export const SELECT_REDDIT = 'SELECT_REDDIT';
export const INVALIDATE_REDDIT = 'INVALIDATE_REDDIT';
export function selectReddit(reddit) {
return {
type: SELECT_REDDIT,
reddit
};
}
export function invalidateReddit(reddit) {
return {
type: INVALIDATE_REDDIT,
reddit
};
}
function requestPosts(reddit) {
return {
type: REQUEST_POSTS,
reddit
};
}
function receivePosts(reddit, json) {
return {
type: RECEIVE_POSTS,
reddit: reddit,
posts: json.data.children.map(child => child.data),
receivedAt: Date.now()
};
}
function fetchPosts(reddit) {
return dispatch => {
dispatch(requestPosts(reddit));
return fetch(`http://www.reddit.com/r/${reddit}.json`)
.then(req => req.json())
.then(json => dispatch(receivePosts(reddit, json)));
}
}
function shouldFetchPosts(state, reddit) {
const posts = state.postsByReddit[reddit];
if (!posts) {
return true;
} else if (posts.isFetching) {
return false;
} else {
return posts.didInvalidate;
}
}
export function fetchPostsIfNeeded(reddit) {
return (dispatch, getState) => {
if (shouldFetchPosts(getState(), reddit)) {
return dispatch(fetchPosts(reddit));
}
};
}
~~~
### Reducers
#### `reducers.js`
~~~
import { combineReducers } from 'redux';
import {
SELECT_REDDIT, INVALIDATE_REDDIT,
REQUEST_POSTS, RECEIVE_POSTS
} from '../actions';
function selectedReddit(state = 'reactjs', action) {
switch (action.type) {
case SELECT_REDDIT:
return action.reddit;
default:
return state;
}
}
function posts(state = {
isFetching: false,
didInvalidate: false,
items: []
}, action) {
switch (action.type) {
case INVALIDATE_REDDIT:
return Object.assign({}, state, {
didInvalidate: true
});
case REQUEST_POSTS:
return Object.assign({}, state, {
isFetching: true,
didInvalidate: false
});
case RECEIVE_POSTS:
return Object.assign({}, state, {
isFetching: false,
didInvalidate: false,
items: action.posts,
lastUpdated: action.receivedAt
});
default:
return state;
}
}
function postsByReddit(state = { }, action) {
switch (action.type) {
case INVALIDATE_REDDIT:
case RECEIVE_POSTS:
case REQUEST_POSTS:
return Object.assign({}, state, {
[action.reddit]: posts(state[action.reddit], action)
});
default:
return state;
}
}
const rootReducer = combineReducers({
postsByReddit,
selectedReddit
});
export default rootReducer;
~~~
### Store
#### `configureStore.js`
~~~
import { createStore, applyMiddleware, combineReducers } from 'redux';
import thunkMiddleware from 'redux-thunk';
import loggerMiddleware from 'redux-logger';
import rootReducer from '../reducers';
const createStoreWithMiddleware = applyMiddleware(
thunkMiddleware,
loggerMiddleware
)(createStore);
export default function configureStore(initialState) {
return createStoreWithMiddleware(rootReducer, initialState);
}
~~~
### 智能组件
#### `containers/Root.js`
~~~
import React, { Component } from 'react';
import { Provider } from 'react-redux';
import configureStore from '../configureStore';
import AsyncApp from './AsyncApp';
const store = configureStore();
export default class Root extends Component {
render() {
return (
<Provider store={store}>
{() => <AsyncApp />}
</Provider>
);
}
}
~~~
#### `containers/AsyncApp.js`
~~~
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import { selectReddit, fetchPostsIfNeeded, invalidateReddit } from '../actions';
import Picker from '../components/Picker';
import Posts from '../components/Posts';
class AsyncApp extends Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.handleRefreshClick = this.handleRefreshClick.bind(this);
}
componentDidMount() {
const { dispatch, selectedReddit } = this.props;
dispatch(fetchPostsIfNeeded(selectedReddit));
}
componentWillReceiveProps(nextProps) {
if (nextProps.selectedReddit !== this.props.selectedReddit) {
const { dispatch, selectedReddit } = nextProps;
dispatch(fetchPostsIfNeeded(selectedReddit));
}
}
handleChange(nextReddit) {
this.props.dispatch(selectReddit(nextReddit));
}
handleRefreshClick(e) {
e.preventDefault();
const { dispatch, selectedReddit } = this.props;
dispatch(invalidateReddit(selectedReddit));
dispatch(fetchPostsIfNeeded(selectedReddit));
}
render () {
const { selectedReddit, posts, isFetching, lastUpdated } = this.props;
return (
<div>
<Picker value={selectedReddit}
onChange={this.handleChange}
options={['reactjs', 'frontend']} />
<p>
{lastUpdated &&
<span>
Last updated at {new Date(lastUpdated).toLocaleTimeString()}.
{' '}
</span>
}
{!isFetching &&
<a href='#'
onClick={this.handleRefreshClick}>
Refresh
</a>
}
</p>
{isFetching && posts.length === 0 &&
<h2>Loading...</h2>
}
{!isFetching && posts.length === 0 &&
<h2>Empty.</h2>
}
{posts.length > 0 &&
<div style={{ opacity: isFetching ? 0.5 : 1 }}>
<Posts posts={posts} />
</div>
}
</div>
);
}
}
AsyncApp.propTypes = {
selectedReddit: PropTypes.string.isRequired,
posts: PropTypes.array.isRequired,
isFetching: PropTypes.bool.isRequired,
lastUpdated: PropTypes.number,
dispatch: PropTypes.func.isRequired
};
function mapStateToProps(state) {
const { selectedReddit, postsByReddit } = state;
const {
isFetching,
lastUpdated,
items: posts
} = postsByReddit[selectedReddit] || {
isFetching: true,
items: []
};
return {
selectedReddit,
posts,
isFetching,
lastUpdated
};
}
export default connect(mapStateToProps)(AsyncApp);
~~~
### 笨拙组件
#### `components/Picker.js`
~~~
import React, { Component, PropTypes } from 'react';
export default class Picker extends Component {
render () {
const { value, onChange, options } = this.props;
return (
<span>
<h1>{value}</h1>
<select onChange={e => onChange(e.target.value)}
value={value}>
{options.map(option =>
<option value={option} key={option}>
{option}
</option>)
}
</select>
</span>
);
}
}
Picker.propTypes = {
options: PropTypes.arrayOf(
PropTypes.string.isRequired
).isRequired,
value: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired
};
~~~
#### `components/Posts.js`
~~~
import React, { PropTypes, Component } from 'react';
export default class Posts extends Component {
render () {
return (
<ul>
{this.props.posts.map((post, i) =>
<li key={i}>{post.title}</li>
)}
</ul>
);
}
}
Posts.propTypes = {
posts: PropTypes.array.isRequired
};
~~~
搭配 React Router
最后更新于:2022-04-01 05:00:29
Middleware
最后更新于:2022-04-01 05:00:27
# Middleware
我们已经在[异步Actions](#)一节的示例中看到了一些 middleware 的使用。如果你使用过 [Express](http://expressjs.com/) 或者 [Koa](http://koajs.com/) 等服务端框架, 那么应该对 *middleware* 的概念不会陌生。 在这类框架中,middleware 是指可以被嵌入在框架接收请求到产生响应过程之中的代码。例如,Express 或者 Koa 的 middleware 可以完成添加 CORS headers,记录日志,内容压缩等工作。middleware 最优秀的特性就是可以被链式组合。你可以在一个项目中使用多个独立的第三方 middleware。
相对于 Express 或者 Koa 的 middleware,Redux middleware 被用于解决不同的问题,但其中的概念是相类似的。**它提供的是位于 action 被发起之后,到达 reducer 之前的扩展点。** 你可以利用 Redux middleware 来进行日志记录、创建崩溃报告、调用异步接口或者路由等等。
这个章节分为两个部分,前面是帮助你理解相关概念的深度介绍,而后半部分则通过[一些实例](#)来体现 middleware 的强大能力。对文章前后内容进行结合通读,会帮助你更好的理解枯燥的概念,并从中获得启发。
### 理解 Middleware
正因为 middleware 可以完成包括异步 API 调用在内的各种事情,了解它的演化过程是一件相当重要的事。我们将以记录日志和创建崩溃报告为例,引导你体会从分析问题到通过构建 middleware 解决问题的思维过程。
### 问题: 记录日志
使用 Redux 的一个益处就是它让 state 的变化过程变的可预知和透明。每当一个 action 发起完成,新的 state 就会被计算并保存下来。State 不能被自身修改,只能由特定的 action 引起变化。
试想一下,当我们的应用中每一个 action 被发起以及每次新的 state 被计算完成时都将它们记录下来,岂不是很好?当程序出现问题时,我们可以通过查阅日志找出是哪个 action 导致了 state 不正确。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-11-19_564dae5c0606a.png)
我们如何通过 Redux 实现它呢?
### 尝试 #1: 手动记录
最直接的解决方案就是在每次调用 [`store.dispatch(action)`](#) 前后手动记录被发起的 action 和新的 state。这称不上一个真正的解决方案,仅仅是我们理解这个问题的第一步。
> ##### 注意
> 如果你使用 [react-redux](https://github.com/gaearon/react-redux) 或者类似的绑定库,最好不要直接在你的组件中操作 store 的实例。在接下来的内容中,仅仅是假设你会显式的向下传递 store。
假设,你在创建一个 Todo 时这样调用:
~~~
store.dispatch(addTodo('Use Redux'));
~~~
为了记录这个 action 一句产生的新的 state,你可以通过这种方式记录日志:
~~~
let action = addTodo('Use Redux');
console.log('dispatching', action);
store.dispatch(action);
console.log('next state', store.getState());
~~~
这样做达到了想要的效果,但是你一定不想每次都这么干。
### 尝试 #2: 封装 Dispatch
你可以将上面的操作抽取成一个函数:
~~~
function dispatchAndLog(store, action) {
console.log('dispatching', action);
store.dispatch(action);
console.log('next state', store.getState());
}
~~~
然后用它替换 `store.dispatch()`:
~~~
dispatchAndLog(store, addTodo('Use Redux'));
~~~
你可以选择到此为止,但是每次都要导入一个外部方法总归还是不太方便。
### 尝试 #3: Monkeypatching Dispatch
如果我们直接替换 store 实例中的 `dispatch` 函数会怎么样呢?Redux store 只是一个包含[一些方法](#)的普通对象,同时我们使用的是 JavaScript,因此我们可以这样实现 `dispatch` 的 monkeypatch:
~~~
let next = store.dispatch;
store.dispatch = function dispatchAndLog(action) {
console.log('dispatching', action);
let result = next(action);
console.log('next state', store.getState());
return result;
};
~~~
这离我们想要的已经非常接近了!无论我们在哪里发起 action,保证都会被记录。Monkeypatching 令人感觉还是不太舒服,但是利用它我们做到了我们想要的。
### 问题: 崩溃报告
如果我们想对 `dispatch` 附加**超过一个**的变换,又会怎么样呢?
我脑海中出现的另一个常用的变换就是在生产过程中报告 JavaScript 的错误。全局的 `window.onerror` 并不可靠,因为它在一些旧的浏览器中无法提供错误堆栈,而这是排查错误所需的至关重要信息。
试想当发起一个 action 的结果是一个异常时,我们将包含调用堆栈,引起错误的 action 以及当前的 state 等错误信息通通发到类似于 [Sentry](https://getsentry.com/welcome/) 这样的报告服务中,不是很好吗?这样我们可以更容易的在开发环境中重现这个错误。
然而,将日志记录和崩溃报告分离是很重要的。理想情况下,我们希望他们是两个不同的模块,也可能在不同的包中。否则我们无法构建一个由这些工具组成的生态系统。(提示:我们正在慢慢了解 middleware 的本质到底是什么!)
如果按照我们的想法,日志记录和崩溃报告是分离的工具,他们看起来应该像这样:
~~~
function patchStoreToAddLogging(store) {
let next = store.dispatch;
store.dispatch = function dispatchAndLog(action) {
console.log('dispatching', action);
let result = next(actionconsole.log('next state', store.getState()););
console.log('next state', store.getState());
return result;
};
}
function patchStoreToAddCrashReporting(store) {
let next = store.dispatch;
store.dispatch = function dispatchAndReportErrors(action) {
try {
return next(action);
} catch (err) {
console.error('捕获一个异常!', err);
Raven.captureException(err, {
extra: {
action,
state: store.getState()
}
});
throw err;
}
};
}
~~~
如果这些功能以不同的模块发布,我们可以在 store 中像这样使用它们:
~~~
patchStoreToAddLogging(store);
patchStoreToAddCrashReporting(store);
~~~
尽管如此,这种方式看起来还是不是够令人满意。
### 尝试 #4: 隐藏 Monkeypatching
Monkeypatching 本质上是一种 hack。“将任意的方法替换成你想要的”,那是 API 会是什么样的呢?现在,让我们来看看这种替换的本质。 在之前,我们用自己的函数替换掉了 `store.dispatch`。如果我们不这样做,而是在函数中*返回*新的 `dispatch` 呢?
~~~
function logger(store) {
let next = store.dispatch;
// 我们之前的做法:
// store.dispatch = function dispatchAndLog(action) {
return function dispatchAndLog(action) {
console.log('dispatching', action);
let result = next(action);
console.log('next state', store.getState());
return result;
};
}
~~~
我们可以在 Redux 内部提供一个可以将实际的 monkeypatching 应用到 `store.dispatch` 中的辅助方法:
~~~
function applyMiddlewareByMonkeypatching(store, middlewares) {
middlewares = middlewares.slice();
middlewares.reverse();
// Transform dispatch function with each middleware.
middlewares.forEach(middleware =>
store.dispatch = middleware(store)
);
}
~~~
然后像这样应用多个 middleware:
~~~
applyMiddlewareByMonkeypatching(store, [logger, crashReporter]);
~~~
尽管我们做了很多,实现方式依旧是 monkeypatching。因为我们仅仅是将它隐藏在我们的框架内部,并没有改变这个事实。
### 尝试 #5: 移除 Monkeypatching
为什么我们要替换原来的 `dispatch` 呢?当然,这样我们就可以在后面直接调用它,但是还有另一个原因:就是每一个 middleware 都可以操作(或者直接调用)前一个 middleware 包装过的 `store.dispatch`:
~~~
function logger(store) {
// 这里的 next 必须指向前一个 middleware 返回的函数:
let next = store.dispatch;
return function dispatchAndLog(action) {
console.log('dispatching', action);
let result = next(action);
console.log('next state', store.getState());
return result;
};
}
~~~
将 middleware 串连起来的必要性是显而易见的。
如果 `applyMiddlewareByMonkeypatching` 方法中没有在第一个 middleware 执行时立即替换掉 `store.dispatch`,那么 `store.dispatch` 将会一直指向原始的 `dispatch` 方法。也就是说,第二个 middleware 依旧会作用在原始的 `dispatch` 方法。
但是,还有另一种方式来实现这种链式调用的效果。可以让 middleware 以方法参数的形式接受一个 `next()` 方法来,而不是通过 store 的实例去获取。
~~~
function logger(store) {
return function wrapDispatchToAddLogging(next) {
return function dispatchAndLog(action) {
console.log('dispatching', action);
let result = next(action);
console.log('next state', store.getState());
return result;
};
}
}
~~~
现在是[“我们该更进一步”](http://knowyourmeme.com/memes/we-need-to-go-deeper)的时刻了,所以可能会多花一点时间来让它变的更为合理一些。这些层叠的函数让人很恐慌。ES6 的 arrow functions 可以让 [currying](https://en.wikipedia.org/wiki/Currying) 看起来更舒服一些:
~~~
const logger = store => next => action => {
console.log('dispatching', action);
let result = next(action);
console.log('next state', store.getState());
return result;
};
const crashReporter = store => next => action => {
try {
return next(action);
} catch (err) {
console.error('Caught an exception!', err);
Raven.captureException(err, {
extra: {
action,
state: store.getState()
}
});
throw err;
}
}
~~~
**这已经是真实的 Redux middleware 的样子了。**
Middleware 接受一个 `next()` 发起函数,并返回一个发起函数,返回的函数会被作为下一个 middleware 的 `next()`,以此类推。由于 store 中类似 `getState()` 的方法依旧非常有用,我们将 `store` 作为顶层的参数,使得它可以在所有 middleware 中被使用。
### 尝试 #6: “青涩”地使用 Middleware
我们可以写一个 `applyMiddleware()` 方法替换掉原来的 `applyMiddlewareByMonkeypatching()`。在新的 `applyMiddleware()` 中,我们取得最终完整的被包装过的 `dispatch()` 函数,并返回一个 store 的副本:
~~~
// 警告:这只是一种朴素的实现
// 这 *并不是* Redux 的 API.
function applyMiddleware(store, middlewares) {
middlewares = middlewares.slice();
middlewares.reverse();
let dispatch = store.dispatch;
middlewares.forEach(middleware =>
dispatch = middleware(store)(dispatch)
);
return Object.assign({}, store, { dispatch });
}
~~~
这与 Redux 中 [`applyMiddleware()`](#) 的实现已经很接近了,但是**有三个重要的不同之处**:
-
它只暴露一个 [store API](#) 的子集给 middleware:[`dispatch(action)`](#) and [`getState()`](#).
-
它用了一个非常巧妙的方式来保证你的 middleware 调用的是 `store.dispatch(action)` 而不是 `next(action)`,从而使这个 action 会在包括当前 middleware 在内的整个 middleware 链中被正确的传递。这对异步的 middleware 非常有用,正如我们在[之前的章节](#)中看到的。
-
为了保证你只能应用 middleware 一次,它作用在 `createStore()` 上而不是 `store` 本身。因此它的签名不是 `(store, middlewares) => store`, 而是 `(...middlewares) => (createStore) => createStore`。
### 最终的方法
这是我们刚刚所写的 middleware:
~~~
const logger = store => next => action => {
console.log('dispatching', action);
let result = next(action);
console.log('next state', store.getState());
return result;
};
const crashReporter = store => next => action => {
try {
return next(action);
} catch (err) {
console.error('Caught an exception!', err);
Raven.captureException(err, {
extra: {
action,
state: store.getState()
}
});
throw err;
}
}
~~~
然后是将它们引用到 Redux store 中:
~~~
import { createStore, combineReducers, applyMiddleware } from 'redux';
// applyMiddleware 接收 createStore()
// 并返回一个包含兼容 API 的函数。
let createStoreWithMiddleware = applyMiddleware(logger, crashReporter)(createStore);
// 像使用 createStore() 一样使用它。
let todoApp = combineReducers(reducers);
let store = createStoreWithMiddleware(todoApp);
~~~
就是这样!现在任何被发送到 store 的 action 都会经过 `logger` and `crashReporter`:
~~~
// 将经过 logger 和 crashReporter 两个 middleware!
store.dispatch(addTodo('Use Redux'));
~~~
### 7个示例
如果读完上面的章节你已经觉得头都要爆了,那就想象以下它写出来之后的样子。下面的内容会让我们放松一下,并让你的思路延续。
下面的每个函数都是一个有效的 Redux middleware。它们并不都一样有用,但是至少他们一样有趣。
~~~
/**
* 记录所有被发起的 action 以及产生的新的 state。
*/
const logger = store => next => action => {
console.group(action.type);
console.info('dispatching', action);
let result = next(action);
console.log('next state', store.getState());
console.groupEnd(action.type);
return result;
};
/**
* 在 state 更新完成和 listener 被通知之后发送崩溃报告。
*/
const crashReporter = store => next => action => {
try {
return next(action);
} catch (err) {
console.error('Caught an exception!', err);
Raven.captureException(err, {
extra: {
action,
state: store.getState()
}
});
throw err;
}
}
/**
* 用 { meta: { delay: N } } 来让 action 延迟 N 毫秒。
* 在这个案例中,让 `dispatch` 返回一个取消 timeout 的函数。
*/
const timeoutScheduler = store => next => action => {
if (!action.meta || !action.meta.delay) {
return next(action);
}
let timeoutId = setTimeout(
() => next(action),
action.meta.delay
);
return function cancel() {
clearTimeout(timeoutId);
};
};
/**
* 通过 { meta: { raf: true } } 让 action 在一个 rAF 循环帧中被发起。
* 在这个案例中,让 `dispatch` 返回一个从队列中移除该 action 的函数。
*/
const rafScheduler = store => next => {
let queuedActions = [];
let frame = null;
function loop() {
frame = null;
try {
if (queuedActions.length) {
next(queuedActions.shift());
}
} finally {
maybeRaf();
}
}
function maybeRaf() {
if (queuedActions.length && !frame) {
frame = requestAnimationFrame(loop);
}
}
return action => {
if (!action.meta || !action.meta.raf) {
return next(action);
}
queuedActions.push(action);
maybeRaf();
return function cancel() {
queuedActions = queuedActions.filter(a => a !== action)
};
};
};
/**
* 使你除了 action 之外还可以发起 promise。
* 如果这个 promise 被 resolved,他的结果将被作为 action 发起。
* 这个 promise 会被 `dispatch` 返回,因此调用者可以处理 rejection。
*/
const vanillaPromise = store => next => action => {
if (typeof action.then !== 'function') {
return next(action);
}
return Promise.resolve(action).then(store.dispatch);
};
/**
* 让你可以发起带有一个 { promise } 属性的特殊 action。
*
* 这个 middleware 会在开始时发起一个 action,并在这个 `promise` resolve 时发起另一个成功(或失败)的 action。
*
* 为了方便起见,`dispatch` 会返回这个 promise 让调用者可以等待。
*/
const readyStatePromise = store => next => action => {
if (!action.promise) {
return next(action)
}
function makeAction(ready, data) {
let newAction = Object.assign({}, action, { ready }, data);
delete newAction.promise;
return newAction;
}
next(makeAction(false));
return action.promise.then(
result => next(makeAction(true, { result })),
error => next(makeAction(true, { error }))
);
};
/**
* 让你可以发起一个函数来替代 action。
* 这个函数接收 `dispatch` 和 `getState` 作为参数。
*
* 对于(根据 `getState()` 的情况)提前退出,或者异步控制流( `dispatch()` 一些其他东西)来说,这非常有用。
*
* `dispatch` 会返回被发起函数的返回值。
*/
const thunk = store => next => action =>
typeof action === 'function' ?
action(store.dispatch, store.getState) :
next(action);
// 你可以使用以上全部的 middleware!(当然,这不意味着你必须全都使用。)
let createStoreWithMiddleware = applyMiddleware(
rafScheduler,
timeoutScheduler,
thunk,
vanillaPromise,
readyStatePromise,
logger,
crashReporter
)(createStore);
let todoApp = combineReducers(reducers);
let store = createStoreWithMiddleware(todoApp);
~~~