├── .gitignore ├── docs ├── advanced │ ├── NextSteps.md │ ├── UsageWithReactRouter.md │ ├── README.md │ ├── AsyncFlow.md │ ├── ExampleRedditAPI.md │ ├── Middleware.md │ └── AsyncActions.md ├── introduction │ ├── README.md │ ├── Motivation.md │ ├── ThreePrinciples.md │ ├── Examples.md │ ├── Ecosystem.md │ └── PriorArt.md ├── recipes │ ├── README.md │ ├── MigratingToRedux.md │ ├── ComputingDerivedData.md │ ├── WritingTests.md │ ├── ServerRendering.md │ └── ReducingBoilerplate.md ├── basics │ ├── README.md │ ├── Store.md │ ├── DataFlow.md │ ├── Actions.md │ ├── ExampleTodoList.md │ ├── Reducers.md │ └── UsageWithReact.md ├── api │ ├── README.md │ ├── compose.md │ ├── createStore.md │ ├── combineReducers.md │ ├── bindActionCreators.md │ ├── Store.md │ └── applyMiddleware.md ├── README.md ├── Troubleshooting.md └── Glossary.md ├── book.json ├── contributes.md ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | _book/ 3 | npm-debug.log 4 | -------------------------------------------------------------------------------- /docs/advanced/NextSteps.md: -------------------------------------------------------------------------------- 1 | # Next Steps 2 | 3 | Sorry, but we’re still writing this doc. 4 | Stay tuned, it will appear in a day or two. 5 | -------------------------------------------------------------------------------- /docs/advanced/UsageWithReactRouter.md: -------------------------------------------------------------------------------- 1 | # Usage with React Router 2 | 3 | Sorry, but we’re still writing this doc. 4 | Stay tuned, it will appear in a day or two. 5 | -------------------------------------------------------------------------------- /docs/introduction/README.md: -------------------------------------------------------------------------------- 1 | ## 介绍 2 | 3 | * [动机](Motivation.md) 4 | * [三大原则](ThreePrinciples.md) 5 | * [先前技术](PriorArt.md) 6 | * [生态](Ecosystem.md) 7 | * [示例](Examples.md) 8 | -------------------------------------------------------------------------------- /docs/recipes/README.md: -------------------------------------------------------------------------------- 1 | # 技巧 2 | 3 | 这一章是关于实现应用开发中会遇到的一些典型场景和代码片段。本章内容建立在你已经学会[基础章节](../basics/README.md)和[高级章节](../advanced/README.md)的基础上。 4 | 5 | * [迁移到 Redux](MigratingToRedux.md) 6 | * [减少样板代码](ReducingBoilerplate.md) 7 | * [服务端渲染](ServerRendering.md) 8 | * [编写测试](WritingTests.md) 9 | * [计算衍生数据](ComputingDerivedData.md) 10 | -------------------------------------------------------------------------------- /docs/advanced/README.md: -------------------------------------------------------------------------------- 1 | # 高级 2 | 3 | [基础章节](../basics/README.md)介绍了如何组织简单的 Redux 应用。在这一章节中,将要学习如何使用 AJAX 和路由。 4 | 5 | * [异步 Action](Async Actions.md) 6 | * [异步数据流](AsyncFlow.md) 7 | * [Middleware](Middleware.md) 8 | * [搭配 React Router](UsageWithReactRouter.md) 9 | * [示例:Reddit API](ExampleRedditAPI.md) 10 | * [下一步](NextSteps.md) 11 | -------------------------------------------------------------------------------- /docs/basics/README.md: -------------------------------------------------------------------------------- 1 | # 基础 2 | 3 | 不要被各种关于 reducers, middleware, store 的演讲所蒙蔽。Redux 实际是非常简单的。如果你有 Flux 开发经验,用起来会非常习惯。(没用过 Flux 也不怕,很容易!) 4 | 5 | 下面的教程将会一步步教你开发简单的 Todo 应用。 6 | 7 | * [Action](Actions.md) 8 | * [Reducer](Reducers.md) 9 | * [Store](Store.md) 10 | * [数据流](DataFlow.md) 11 | * [搭配 React](UsageWithReact.md) 12 | * [示例:Todo 列表](ExampleTodoList.md) 13 | -------------------------------------------------------------------------------- /book.json: -------------------------------------------------------------------------------- 1 | { 2 | "gitbook": "2.3.3", 3 | "structure": { 4 | "summary": "docs/README.md" 5 | }, 6 | "plugins": ["edit-link", "prism", "ga"], 7 | "pluginsConfig": { 8 | "edit-link": { 9 | "base": "https://github.com/camsong/redux-in-chinese/tree/master", 10 | "label": "开始纠错" 11 | }, 12 | "ga": { 13 | "token": "UA-66122997-1" 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /contributes.md: -------------------------------------------------------------------------------- 1 | 翻译计划列表 2 | ============ 3 | 4 | 5 | ``` 6 | url: advanced/AsyncActions.html 7 | work_in_progress: false 8 | ``` 9 | 10 | ``` 11 | url: api/combineReducers.html 12 | work_in_progress: false 13 | ``` 14 | 15 | ``` 16 | url: api/applyMiddleware.html 17 | work_in_progress: false 18 | ``` 19 | ``` 20 | url: api/introduction 21 | work_in_progress: true 22 | contributor: yuweiw823 23 | ``` -------------------------------------------------------------------------------- /docs/introduction/Motivation.md: -------------------------------------------------------------------------------- 1 | # 动机 2 | 3 | 随着 JavaScript 单页应用开发日趋复杂,**JavaScript 需要管理比任何时候都多的state (状态)**。 这些 state 可能包括服务器响应、缓存数据、本地生成尚未持久化到服务器的数据,也包括 UI 状态,如激活的路由,被选中的标签,是否显示加载中动效或者分页器等等。 4 | 5 | 管理不断变化的 state 非常难。如果一个 model 的变化会引起另一个 model 变化,那么当 view 变化时,就可能引起对应 model 以及另一个 model 的变化,依次地,可能会引起另一个 view 的变化... 直至你搞不清楚到底发生了什么。**state 在什么时候,为什么,如何变化已然不受控制。** 当系统变得错综复杂的时候,想重现问题或者添加新功能就会变得举步维艰。 6 | 7 | 如果这还不够糟糕,考虑一些**来自前端开发领域的新需求**,如更新调优、服务端渲染、路由跳转前请求数据等等。我们前端开发者正在经受前所未有的复杂性,[我们是时候该放弃了吗?](http://www.quirksmode.org/blog/archives/2015/07/stop_pushing_th.html) 8 | 9 | 这里的复杂性很大程度上来自于:**我们总是将两个难以厘清的概念混在一起:变化和异步**。 我称它们为[曼妥思糖与可乐](https://en.wikipedia.org/wiki/Diet_Coke_and_Mentos_eruption)。如果把二者都分开,能做的很好,但混到一起,就变得一团糟。一些库如 [React](http://facebook.github.io/react) 试图在视图层禁止异步和直接操作 DOM 来解决这个问题。美中不足的是,React 依旧把处理 state 中数据的问题留给了你自己。 10 | 11 | 跟随 [Flux](http://facebook.github.io/flux)、[CQRS](http://martinfowler.com/bliki/CQRS.html) 和 [Event Sourcing](http://martinfowler.com/eaaDev/EventSourcing.html) 的脚步,通过限制更新发生的时间和方式,**Redux 试图让 state 的变化变得可预测**,而这些限制条件反映在 Redux 的 [三大原则](ThreePrinciples.md)中。 12 | -------------------------------------------------------------------------------- /docs/advanced/AsyncFlow.md: -------------------------------------------------------------------------------- 1 | # 异步数据流 2 | 3 | 如果不使用 [middleware](Middleware.md) 的话,Redux 的 store 只支持 [同步数据流](../basics/DataFlow.md)。而这也是 [`createStore()`](../api/createStore.md) 所默认提供的创建方式。 4 | 5 | 你可以使用 [`applyMiddleware()`](../api/applyMiddleware.md) 来增强 [`createStore()`](../api/createStore.md)。这不是必须的,但它可以让你 [以更简便的方式来描述异步的 action](AsyncActions.md)。 6 | 7 | 像 [redux-thunk](https://github.com/gaearon/redux-thunk) 或 [redux-promise](https://github.com/acdlite/redux-promise) 这样支持异步的 middleware 都包装了 store 的 [`dispatch()`](../api/Store.md#dispatch) 方法,以此来让你 dispatch 一些除了 action 以外的其他内容,例如:函数或者 Promise。你所使用的任何 middleware 都可以以自己的方式解释你 dispatch 的任何内容,并继续传递 actions 给下一个 middleware。比如,一个 Promise middleware 能够 Promise,然后针对每个 Promise 异步地 dispatch 一对 begin/end actions。 8 | 9 | 当 middleware 链中的最后一个 middleware dispatch action 时,这个 action 必须是一个普通对象。这是 [同步式的 Redux 数据流](../basics/DataFlow.md) 开始的地方(译注:这里应该是指,你可以使用任意多异步的 middleware 去做你想做的事情,但是需要使用普通对象作为最后一个被 dispatch 的 action ,来将处理流程带回同步方式)。 10 | 11 | ## 下一步 12 | 13 | 现在你已经学完 Redux 数据流的全部内容!可以查看 [异步示例源码](ExampleRedditAPI.md),或者继续阅读 [集成 React Router](UsageWithReactRouter.md)。 -------------------------------------------------------------------------------- /docs/api/README.md: -------------------------------------------------------------------------------- 1 | # API 文档 2 | 3 | Redux 的 API 非常少。Redux 定义了一系列的约定(contract)来让你来实现(例如 [reducers](../Glossary.md#reducer)),同时提供少量辅助函数来把这些约定整合到一起。 4 | 5 | 这一章会介绍所有的 Redux API。记住,Redux 只关心如何管理 state。在实际的项目中,你还需要使用 UI 绑定库如 [react-redux](https://github.com/gaearon/react-redux)。 6 | 7 | ### 顶级暴露的方法 8 | 9 | * [createStore(reducer, [initialState])](createStore.md) 10 | * [combineReducers(reducers)](combineReducers.md) 11 | * [applyMiddleware(...middlewares)](applyMiddleware.md) 12 | * [bindActionCreators(actionCreators, dispatch)](bindActionCreators.md) 13 | * [compose(...functions)](compose.md) 14 | 15 | ### Store API 16 | 17 | * [Store](Store.md) 18 | * [getState()](Store.md#getState) 19 | * [dispatch(action)](Store.md#dispatch) 20 | * [subscribe(listener)](Store.md#subscribe) 21 | * [getReducer()](Store.md#getReducer) 22 | * [replaceReducer(nextReducer)](Store.md#replaceReducer) 23 | 24 | ### 引入 25 | 26 | 上面介绍的所有函数都是顶级暴露的方法。都可以这样引入: 27 | 28 | #### ES6 29 | 30 | ```js 31 | import { createStore } from 'redux'; 32 | ``` 33 | 34 | #### ES5 (CommonJS) 35 | 36 | ```js 37 | var createStore = require('redux').createStore; 38 | ``` 39 | 40 | #### ES5 (UMD build) 41 | 42 | ```js 43 | var createStore = Redux.createStore; 44 | ``` 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-in-chinese", 3 | "description": "Redux 中文文档", 4 | "scripts": { 5 | "clean": "rimraf _book", 6 | "build": "gitbook build -g camsong/redux-in-chinese", 7 | "watch": "gitbook serve", 8 | "publish": "npm run clean && npm run build && cd _book && git init && git commit --allow-empty -m 'update book' && git checkout -b gh-pages && touch .nojekyll && git add . && git commit -am 'update book' && git push git@github.com:camsong/redux-in-chinese gh-pages --force" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/camsong/redux-in-chinese.git" 13 | }, 14 | "keywords": [ 15 | "flux", 16 | "redux", 17 | "reducer", 18 | "react", 19 | "reactjs", 20 | "hot", 21 | "reload", 22 | "hmr", 23 | "live", 24 | "edit", 25 | "webpack" 26 | ], 27 | "author": "Cam Song (http://github.com/camsong)", 28 | "license": "MIT", 29 | "bugs": { 30 | "url": "https://github.com/camsong/redux-in-chinese/issues" 31 | }, 32 | "homepage": "https://github.com/camsong/redux-in-chinese", 33 | "devDependencies": { 34 | "gitbook-cli": "^0.3.4", 35 | "gitbook-plugin-prism": "^0.1.1", 36 | "gitbook-plugin-ga": "^0.2.1", 37 | "gitbook-plugin-edit-link": "^1.4.1", 38 | "rimraf": "^2.3.4" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /docs/recipes/MigratingToRedux.md: -------------------------------------------------------------------------------- 1 | # 迁移到 Redux 2 | 3 | Redux 不是一个整体的框架,而是一系列的约定和[一些让他们协同工作的函数](../api/README.md)。你的 Redux 项目中的主要代码不会是使用 Redux 的 API,因为大多数时间你都会在编写功能。 4 | 5 | 这让到 Redux 的双向迁移都非常的容易。 6 | 我们并不想限制你! 7 | 8 | ## 迁移 Flux 项目 9 | 10 | [Reducer](../Glossary.md#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 库。 11 | 12 | 同样你也可以将 Redux 的项目通过相同的步骤迁移回上述的这些 Flux 框架。 13 | 14 | 你的迁移过程大致包含几个步骤: 15 | 16 | * 创建一个叫做 `createFluxStore(reducer)` 的函数,通过 reducer 函数适配你当前项目的 Flux Store。从代码来看,这个函数很像 Redux 中 [`createStore`](../api/createStore.md) 的实现。它的 dispatch 处理器应该根据不同的 action 来调用不同的 `reducer`,保存新的 state 并抛出更新事件。 17 | 18 | * 通过创建 `createFluxStore(reducer)` 的方法来将每个 Flux Store 逐步重写为 Reducer,这个过程中你的应用中其他部分代码感知不到任何变化,仍可以和原来一样使用 Flux Store 。 19 | 20 | * 当重写你的 Store 时,你会发现你应该避免一些明显违反 Flux 模式的使用方法,例如在 Store 中请求 API、或者在 Store 中触发 action。一旦基于 reducer 来构建你的 Flux 代码,它会变得更易于理解。 21 | 22 | * 当你所有的 Flux Store 全部基于 reducer 来实现时,你就可以利用 [`combineReducers(reducers)`](../api/combineReducers.md) 将多个 reducer 合并到一起,然后在应用里使用这个唯一的 Redux Store。 23 | 24 | * 现在,剩下的就只是[使用 react-redux](../basics/UsageWithReact.md) 或者类似的库来处理你的UI部分。 25 | 26 | * 最后,你可以使用一些 Redux 的特性,例如利用 middleware 来进一步简化异步的代码。 27 | 28 | 29 | ## 迁移 Backbone 项目 30 | 31 | 对不起,你需要重写你的 Model 层。 32 | 它们区别太大了! 33 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | ## 目录 2 | 3 | * [自述](README.md) 4 | * [介绍](/docs/introduction/README.md) 5 | * [动机](/docs/introduction/Motivation.md) 6 | * [三大原则](/docs/introduction/ThreePrinciples.md) 7 | * [先前技术](/docs/introduction/PriorArt.md) 8 | * [生态](/docs/introduction/Ecosystem.md) 9 | * [示例](/docs/introduction/Examples.md) 10 | * [基础](/docs/basics/README.md) 11 | * [Action](/docs/basics/Actions.md) 12 | * [Reducer](/docs/basics/Reducers.md) 13 | * [Store](/docs/basics/Store.md) 14 | * [数据流](/docs/basics/DataFlow.md) 15 | * [搭配 React](/docs/basics/UsageWithReact.md) 16 | * [示例:Todo List](/docs/basics/ExampleTodoList.md) 17 | * [高级](/docs/advanced/README.md) 18 | * [异步 Action](/docs/advanced/AsyncActions.md) 19 | * [异步数据流](/docs/advanced/AsyncFlow.md) 20 | * [Middleware](/docs/advanced/Middleware.md) 21 | * [搭配 React Router](/docs/advanced/UsageWithReactRouter.md) 22 | * [示例:Reddit API](/docs/advanced/ExampleRedditAPI.md) 23 | * [下一步](/docs/advanced/NextSteps.md) 24 | * [技巧](/docs/recipes/README.md) 25 | * [迁移到 Redux](/docs/recipes/MigratingToRedux.md) 26 | * [减少样板代码](/docs/recipes/ReducingBoilerplate.md) 27 | * [服务端渲染](/docs/recipes/ServerRendering.md) 28 | * [计算衍生数据](/docs/recipes/ComputingDerivedData.md) 29 | * [编写测试](/docs/recipes/WritingTests.md) 30 | * [排错](/docs/Troubleshooting.md) 31 | * [词汇表](/docs/Glossary.md) 32 | * [API 文档](/docs/api/README.md) 33 | * [createStore](/docs/api/createStore.md) 34 | * [Store](/docs/api/Store.md) 35 | * [combineReducers](/docs/api/combineReducers.md) 36 | * [applyMiddleware](/docs/api/applyMiddleware.md) 37 | * [bindActionCreators](/docs/api/bindActionCreators.md) 38 | * [compose](/docs/api/compose.md) 39 | * [修改日志](/CHANGELOG.md) -------------------------------------------------------------------------------- /docs/api/compose.md: -------------------------------------------------------------------------------- 1 | # `compose(...functions)` 2 | 3 | 从右到左来组合多个函数。 4 | 5 | 这是函数式编程中的方法,为了方便,被放到了 Redux 里。 6 | 当需要把多个 [store 增强器](../Glossary.md#store-enhancer) 依次执行的时候,需要用到它。 7 | 8 | #### 参数 9 | 10 | 1. (*arguments*): 需要合成的多个函数。每个函数都接收一个函数作为参数,然后返回一个函数。 11 | 12 | #### 返回值 13 | 14 | (*Function*): 从右到左把接收到的函数合成后的最终函数。 15 | 16 | #### 示例 17 | 18 | 下面示例演示了如何使用 `compose` 增强 [store](Store.md),这个 store 与 [`applyMiddleware`](applyMiddleware.md) 和 [redux-devtools](https://github.com/gaearon/redux-devtools) 一起使用。 19 | 20 | ```js 21 | import { createStore, combineReducers, applyMiddleware, compose } from 'redux'; 22 | import thunk from 'redux-thunk'; 23 | import * as reducers from '../reducers/index'; 24 | 25 | let reducer = combineReducers(reducers); 26 | let middleware = [thunk]; 27 | 28 | let finalCreateStore; 29 | 30 | // 生产环境中,我们希望只使用 middleware。 31 | // 而在开发环境中,我们还希望使用一些 redux-devtools 提供的一些 store 增强器。 32 | // UglifyJS 会在构建过程中把一些不会执行的死代码去除掉。 33 | 34 | if (process.env.NODE_ENV === 'production') { 35 | finalCreateStore = applyMiddleware(...middleware)(createStore); 36 | } else { 37 | finalCreateStore = compose( 38 | applyMiddleware(...middleware), 39 | require('redux-devtools').devTools(), 40 | require('redux-devtools').persistState( 41 | window.location.href.match(/[?&]debug_session=([^&]+)\b/) 42 | ), 43 | createStore 44 | ); 45 | 46 | // 不使用 compose 来写是这样子: 47 | // 48 | // finalCreateStore = 49 | // applyMiddleware(middleware)( 50 | // devTools()( 51 | // persistState(window.location.href.match(/[?&]debug_session=([^&]+)\b/))( 52 | // createStore 53 | // ) 54 | // ) 55 | // ); 56 | } 57 | 58 | let store = finalCreateStore(reducer); 59 | ``` 60 | 61 | #### 小贴士 62 | 63 | * `compse` 做的只是让你不使用深度右括号的情况下来写深度嵌套的函数。不要觉得它很复杂。 64 | -------------------------------------------------------------------------------- /docs/api/createStore.md: -------------------------------------------------------------------------------- 1 | # `createStore(reducer, [initialState])` 2 | 3 | 创建一个 Redux [store](Store.md) 来以存放应用中所有的 state。 4 | 应用中应有且仅有一个 store。 5 | 6 | #### 参数 7 | 8 | 1. `reducer` *(Function)*: 接收两个参数,分别是当前的 state 树和要处理的 [action](../Glossary.md#action),返回新的 [state 树](../Glossary.md#state)。 9 | 10 | 2. [`initialState`] *(any)*: 初始时的 state。 11 | 在同构应用中,你可以决定是否把服务端传来的 state 水合(hydrate)后传给它,或者从之前保存的用户会话中恢复一个传给它。如果你使用 [`combineReducers`](combineReducers.md) 创建 `reducer`,它必须是一个普通对象,与传入的 keys 保持同样的结构。否则,你可以自由传入任何 `reducer` 可理解的内容。[TODO: Optimize] 12 | 13 | #### 返回值 14 | 15 | ([*`Store`*](Store.md)): 保存了应用所有 state 的对象。改变 state 的惟一方法是 [dispatch](Store.md#dispatch) action。你也可以 [subscribe 监听](Store.md#subscribe) state 的变化,然后更新 UI。 16 | 17 | #### 示例 18 | 19 | ```js 20 | import { createStore } from 'redux'; 21 | 22 | function todos(state = [], action) { 23 | switch (action.type) { 24 | case 'ADD_TODO': 25 | return state.concat([action.text]); 26 | default: 27 | return state; 28 | } 29 | } 30 | 31 | let store = createStore(todos, ['Use Redux']); 32 | 33 | store.dispatch({ 34 | type: 'ADD_TODO', 35 | text: 'Read the docs' 36 | }); 37 | 38 | console.log(store.getState()); 39 | // ['Use Redux', 'Read the docs'] 40 | ``` 41 | 42 | #### 小贴士 43 | 44 | * 应用中不要创建多个 store!相反,使用 [`combineReducers`](combineReducers.md) 来把多个 reducer 创建成一个根 reducer。 45 | 46 | * 你可以决定 state 的格式。你可以使用普通对象或者 [Immutable](http://facebook.github.io/immutable-js/) 这类的实现。如果你不知道如何做,刚开始可以使用普通对象。 47 | 48 | * 如果 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 }`。 49 | 50 | * 对于服务端运行的同构应用,为每一个请求创建一个 store 实例,以此让 store 相隔离。dispatch 一系列请求数据的 action 到 store 实例上,等待请求完成后再在服务端渲染应用。 51 | 52 | * 当 store 创建后,Redux 会 dispatch 一个 action 到 reducer 上,来用初始的 state 来填充 store。你不需要处理这个 action。但要记住,如果第一个参数也就是传入的 state 如果是 `undefined` 的话,reducer 应该返回初始的 state 值。 53 | -------------------------------------------------------------------------------- /docs/introduction/ThreePrinciples.md: -------------------------------------------------------------------------------- 1 | # 三大原则 2 | 3 | Redux 可以被描述成三大基础原则: 4 | 5 | ### 单一数据源 6 | 7 | **整个应用的 [state](../Glossary.md#state) 被储存在一棵 object tree 中,它只有一个单一的 [store](../Glossary.md#store) 。** 8 | 9 | 这让同构应用开发变得非常容易。来自服务端的 state 可以轻而易举地被序列化并融合到没有额外代码影响的客户端上。由于是单一的 state tree ,调试也变得非常容易。你也可以把应用的 state 保存下来加快开发速度。此外,受益于单一的 state tree ,以前难以实现的像“撤销/重做”这类的功能也变得轻而易举。 10 | 11 | ```js 12 | console.log(store.getState()); 13 | 14 | { 15 | visibilityFilter: 'SHOW_ALL', 16 | todos: [{ 17 | text: 'Consider using Redux', 18 | completed: true, 19 | }, { 20 | text: 'Keep all state in a single tree', 21 | completed: false 22 | }] 23 | } 24 | ``` 25 | 26 | ### State 是只读的 27 | 28 | **惟一改变 state 的办法就是触发 [action](../Glossary.md#action),action 是一个描述要发生什么的对象。** 29 | 30 | 这让视图和网络请求不能直接修改 state,相反只能表达出需要修改的意图。因为所有的修改都被集中化处理,且严格按照顺序一个接一个执行,因此没有模棱两可的情况需要提防。 Action 就是普通对象而已,因此它们可以被日志打印、序列化、储存、后期调试或测试时回放出来。 31 | 32 | ```js 33 | store.dispatch({ 34 | type: 'COMPLETE_TODO', 35 | index: 1 36 | }); 37 | 38 | store.dispatch({ 39 | type: 'SET_VISIBILITY_FILTER', 40 | filter: 'SHOW_COMPLETED' 41 | }); 42 | ``` 43 | 44 | ### 纯函数的形式来执行修改 45 | 46 | **为了描述 action 如何改变 state tree ,你需要编写 [reducers](../Glossary.md#reducer)。** 47 | 48 | Reducer 只是一些纯函数,它接收之前的 state 和 action,并返回新的 state。刚开始你可以只有一个 reducer,随着应用变大,你可以把它拆成多个小的 reducers,分别独立地操作 state tree 的不同部分,因为 reducer 只是普通函数,你可以控制它们被调用的顺序,传入附加数据,甚至编写可复用的 reducer 来做一些通用任务,如分页器。 49 | 50 | ```js 51 | function visibilityFilter(state = 'SHOW_ALL', action) { 52 | switch (action.type) { 53 | case 'SET_VISIBILITY_FILTER': 54 | return action.filter; 55 | default: 56 | return state; 57 | } 58 | } 59 | 60 | function todos(state = [], action) { 61 | switch (action.type) { 62 | case 'ADD_TODO': 63 | return [...state, { 64 | text: action.text, 65 | completed: false 66 | }]; 67 | case 'COMPLETE_TODO': 68 | return [ 69 | ...state.slice(0, action.index), 70 | Object.assign({}, state[action.index], { 71 | completed: true 72 | }), 73 | ...state.slice(action.index + 1) 74 | ] 75 | default: 76 | return state; 77 | } 78 | } 79 | 80 | import { combineReducers, createStore } from 'redux'; 81 | let reducer = combineReducers({ visibilityFilter, todos }); 82 | let store = createStore(reducer); 83 | ``` 84 | 85 | 就是这样,现在你已经明白 Redux 是怎么回事了。 86 | -------------------------------------------------------------------------------- /docs/basics/Store.md: -------------------------------------------------------------------------------- 1 | # Store 2 | 3 | 上面章节中,我们学会了使用 [action](Action.md) 来描述“发生了什么”,和使用 [reducers](Reducers.md) 来根据 action 更新 state 的用法。 4 | 5 | **Store** 就是把它们联系到一起的对象。Store 有以下职责: 6 | 7 | * 维持应用的 state; 8 | * 提供 [`getState()`](../api/Store.md#getState) 方法获取 state; 9 | * 提供 [`dispatch(action)`](../api/Store.md#dispatch) 方法更新 state; 10 | * 通过 [`subscribe(listener)`](../api/Store.md#subscribe) 注册监听器。 11 | 12 | 再次强调一下 **Redux 应用只有一个单一的 store**。当需要拆分处理数据的逻辑时,使用 [reducer 组合](Reducers.md#splitting-reducers) 而不是创建多个 store。 13 | 14 | 根据 reducer 创建 store 非常容易。例如,假如应用中只有一个 `todoApp` 的 reducer,可以这样写: 15 | 16 | ```js 17 | import { createStore } from 'redux'; 18 | import todoApp from './reducers'; 19 | 20 | let store = createStore(todoApp); 21 | ``` 22 | 23 | 为了提高可维护性,拆分成多个 reducer,这时需要使用 [`combineReducers()`](../api/combineReducers.md) 来把它们组合起来。 24 | 25 | ```js 26 | import { combineReducers, createStore } from 'redux'; 27 | import * as reducers from './reducers'; 28 | 29 | let todoApp = combineReducers(reducers); 30 | let store = createStore(todoApp); 31 | ``` 32 | 33 | [`createStore()`](../api/createStore.md) 的第二个参数可以设置初始状态。 34 | 这对开发同构应用时非常有用,可以用于把服务器端生成的 state 转变后在浏览器端传给应用。 35 | 36 | ```js 37 | let store = createStore(todoApp, window.STATE_FROM_SERVER); 38 | ``` 39 | 40 | ## 发起 Actions 41 | 42 | 创建好了 store 后,就可以验证程序是否工作。虽然还没有界面,我们已经可以测试更新逻辑了。 43 | 44 | ```js 45 | import { addTodo, completeTodo, setVisibilityFilter, VisibilityFilters } from './actions'; 46 | 47 | // 打印初始状态 48 | console.log(store.getState()); 49 | 50 | // 监听 state 更新时,打印日志 51 | let unsubscribe = store.subscribe(() => 52 | console.log(store.getState()) 53 | ); 54 | 55 | // 发起一系列 action 56 | store.dispatch(addTodo('Learn about actions')); 57 | store.dispatch(addTodo('Learn about reducers')); 58 | store.dispatch(addTodo('Learn about store')); 59 | store.dispatch(completeTodo(0)); 60 | store.dispatch(completeTodo(1)); 61 | store.dispatch(setVisibilityFilter(VisibilityFilters.SHOW_COMPLETED)); 62 | 63 | // 停止监听 state 更新 64 | unsubscribe(); 65 | ``` 66 | 67 | 可以看到 store 里的 state 是如何变化的: 68 | 69 | 70 | 71 | 可以看到,在还没有开发界面的时候,我们就可以定义程序的行为。而且这时候已经可以写 reducer 和 action 创建函数的测试。不需要模拟任何东西,因为它们都是纯函数。只需调用一下,对返回值做断言,写测试就是这么简单。 72 | 73 | ## 源码 74 | 75 | #### `index.js` 76 | 77 | ```js 78 | import { combineReducers, createStore } from 'redux'; 79 | import * as reducers from './reducers'; 80 | 81 | let todoApp = combineReducers(reducers); 82 | let store = createStore(todoApp); 83 | ``` 84 | 85 | ## 下一步 86 | 87 | 在创建 todo 应用界面之前,我们先穿插学习一下[数据在 Redux 应用中如何流动的](DataFlow.md) 88 | -------------------------------------------------------------------------------- /docs/introduction/Examples.md: -------------------------------------------------------------------------------- 1 | # 示例 2 | 3 | Redux [源码](https://github.com/gaearon/redux/tree/master/examples)里包含一些示例一起发行。 4 | **运行示例的方法是,先 clone 仓库,然后分别在根目录和示例目录下执行 `npm install`。** 5 | 6 | >##### 复制代码时注意 7 | >如果你把 Redux 示例代码复制到其它目录,删除 `webpack.config.js` 里的这几行: 8 | > 9 | >```js 10 | >alias: { 11 | > 'redux': path.join(__dirname, '..', '..', 'src') 12 | >}, 13 | >``` 14 | >还有 15 | >```js 16 | >{ 17 | > test: /\.js$/, 18 | > loaders: ['babel'], 19 | > include: path.join(__dirname, '..', '..', 'src') 20 | >}, 21 | ``` 22 | > 23 | > 否则运行时扔会试图从相对的 `src` 目录来定位 Redux,导致构建失败。 24 | 25 | ## Counter 计数器示例 26 | 27 | 运行 [Counter](https://github.com/gaearon/redux/tree/master/examples/counter) 示例: 28 | 29 | ``` 30 | git clone https://github.com/gaearon/redux.git 31 | 32 | cd redux 33 | npm install 34 | 35 | cd examples/counter 36 | npm install 37 | 38 | npm start 39 | open http://localhost:3000/ 40 | ``` 41 | 42 | 这个示例包含: 43 | 44 | * 基本的 Redux 应用开发流程; 45 | * 测试代码。 46 | 47 | ## TodoMVC 示例 48 | 49 | 运行 [TodoMVC](https://github.com/gaearon/redux/tree/master/examples/todomvc) 示例: 50 | 51 | ``` 52 | git clone https://github.com/gaearon/redux.git 53 | 54 | cd redux 55 | npm install 56 | 57 | cd examples/todomvc 58 | npm install 59 | 60 | npm start 61 | open http://localhost:3000/ 62 | ``` 63 | 64 | 这个示例包含: 65 | 66 | * Redux 中使用两个 reducer 的方法; 67 | * 嵌套数据的更新; 68 | * 测试样例。 69 | 70 | ## 异步 71 | 72 | 运行 [Async](https://github.com/gaearon/redux/tree/master/examples/async) 示例: 73 | 74 | ``` 75 | git clone https://github.com/gaearon/redux.git 76 | 77 | cd redux 78 | npm install 79 | 80 | cd examples/async 81 | npm install 82 | 83 | npm start 84 | open http://localhost:3000/ 85 | ``` 86 | 87 | 这个示例包含: 88 | 89 | * 使用 [redux-thunk](https://github.com/gaearon/redux-thunk) 处理简单的异步开发流程; 90 | * 缓存服务器响应数据和在获取数据过程中显示加载进度条; 91 | * 缓存数据过期方法。 92 | 93 | ## Real World 示例 94 | 95 | 运行 [Real World](https://github.com/gaearon/redux/tree/master/examples/real-world) 示例: 96 | 97 | ``` 98 | git clone https://github.com/gaearon/redux.git 99 | 100 | cd redux 101 | npm install 102 | 103 | cd examples/real-world 104 | npm install 105 | 106 | npm start 107 | open http://localhost:3000/ 108 | ``` 109 | 110 | 这个示例包含: 111 | 112 | * 实际应用中如何做异步处理; 113 | * 使用 [normalized](https://github.com/gaearon/normalizr) 结果集来保存并缓存数据; 114 | * 自定义 middleware 来做 API 请求; 115 | * 缓存服务器响应数据和在获取数据过程中显示加载进度条; 116 | * 分页; 117 | * 路由。 118 | 119 | ## 更多 120 | 121 | 参考 [Awesome Redux](https://github.com/xgrommx/awesome-redux) 获取更多示例。 122 | -------------------------------------------------------------------------------- /docs/api/combineReducers.md: -------------------------------------------------------------------------------- 1 | # `combineReducers(reducers)` 2 | 3 | 随着应用变得复杂,需要对 [reducer 函数](../Glossary.md#reducer) 进行拆分,拆分后的每一块独立负责管理 [state](../Glossary.md#state) 的一部分。 4 | 5 | `combineReducers` 辅助函数的作用是,把一个由多个不同 reducer 函数作为 value 的 object,合并成一个最终的 reducer 函数,然后就可以对这个 reducer 调用 [`createStore`](createStore.md)。 6 | 7 | 合并后的 reducer 可以调用各个子 reducer,并把它们的结果合并成一个 state 对象。state 对象的结构由传入的多个 reducer 的 key 决定。 8 | 9 | > ##### Flux 用户使用须知 10 | 11 | > 本函数可以帮助你组织多个 reducer,使它们分别管理自身相关联的 state。类似于 Flux 中的多个 store 分别管理不同的 state。在 Redux 中,只有一个 store,但是 `combineReducers` 让你拥有多个 reducer,同时保持各自负责逻辑块的独立性。 12 | 13 | #### 参数 14 | 15 | 1. `reducers` (*Object*): 一个对象,它的值(value) 对应不同的 reducer 函数,这些 reducer 函数后面会被合并成一个。下面会介绍传入 reducer 函数需要满足的规则。 16 | 17 | > 之前的文档曾建议使用 ES6 的 `import * as reducers` 语法来获得 reducer 对象。这一点造成了很多疑问,因此现在建议在 `reducers/index.js` 里使用 `combineReducers()` 来对外输出一个 reducer。下面有示例说明。 18 | 19 | #### 返回值 20 | 21 | (*Function*): 一个调用 `reducers` 对象里所有 reducer 的 reducer,并且构造一个与 `reducers` 对象结构相同的 state 对象。 22 | 23 | #### 注意 24 | 25 | 本函数设计的时候有点偏主观,就是为了避免新手犯一些常见错误。也因些我们故意设定一些规则,但如果你自己手动编写根 redcuer 时并不需要遵守这些规则。 26 | 27 | 每个传入 `combineReducers` 的 reducer 都需满足以下规则: 28 | 29 | * 所有未匹配到的 action,必须把它接收到的第一个参数也就是那个 `state` 原封不动返回。 30 | 31 | * 永远不能返回 `undefined`。当过早 `return` 时非常容易犯这个错误,为了避免错误扩散,遇到这种情况时 `combineReducers` 会抛异常。 32 | 33 | * 如果传入的 `state` 就是 `undefined`,一定要返回对应 reducer 的初始 state。根据上一条规则,初始 state 禁止使用 `undefined`。使用 ES6 的默认参数值语法来设置初始 state 很容易,但你也可以手动检查第一个参数是否为 `undefined`。 34 | 35 | 虽然 `combineReducers` 自动帮你检查 reducer 是否符合以上规则,但你也应该牢记,并尽量遵守。 36 | 37 | #### 示例 38 | 39 | #### `reducers/todos.js` 40 | 41 | ```js 42 | export default function todos(state = [], action) { 43 | switch (action.type) { 44 | case 'ADD_TODO': 45 | return state.concat([action.text]); 46 | default: 47 | return state; 48 | } 49 | } 50 | ``` 51 | 52 | #### `reducers/counter.js` 53 | 54 | ```js 55 | export default function counter(state = 0, action) { 56 | switch (action.type) { 57 | case 'INCREMENT': 58 | return state + 1; 59 | case 'DECREMENT': 60 | return state - 1; 61 | default: 62 | return state; 63 | } 64 | } 65 | ``` 66 | 67 | #### `reducers/index.js` 68 | 69 | ```js 70 | import { combineReducers } from 'redux'; 71 | import todos from './todos'; 72 | import counter from './counter'; 73 | 74 | export default combineReducers({ 75 | todos, 76 | counter 77 | }); 78 | ``` 79 | 80 | #### `App.js` 81 | 82 | ```js 83 | import { createStore } from 'redux'; 84 | import reducer from './reducers/index'; 85 | 86 | let store = createStore(reducer); 87 | console.log(store.getState()); 88 | // { 89 | // counter: 0, 90 | // todos: [] 91 | // } 92 | 93 | store.dispatch({ 94 | type: 'ADD_TODO', 95 | text: 'Use Redux' 96 | }); 97 | console.log(store.getState()); 98 | // { 99 | // counter: 0, 100 | // todos: ['Use Redux'] 101 | // } 102 | ``` 103 | 104 | #### 小贴士 105 | 106 | * 本方法只是起辅助作用!你可以自行实现[不同功能](https://github.com/acdlite/reduce-reducers)的 `combineReducers`,甚至像实现其它函数一样,明确地写一个根 reducer 函数,用它把子 reducer 手动组装成 state 对象。 107 | 108 | * 在 reducer 层级的任何一级都可以调用 `combineReducers`。并不是一定要在最外层。实际上,你可以把一些复杂的子 reducer 拆分成单独的孙子级 reducer,甚至更多层。 109 | -------------------------------------------------------------------------------- /docs/api/bindActionCreators.md: -------------------------------------------------------------------------------- 1 | # `bindActionCreators(actionCreators, dispatch)` 2 | 3 | 把 [action creators](../Glossary.md#action-creator) 转成拥有同名 keys 的对象,但使用 [`dispatch`](Store.md#dispatch) 把每个 action creator 包围起来,这样可以直接调用它们。 4 | 5 | 一般情况下你可以直接在 [`Store`](Store.md) 实例上调用 [`dispatch`](Store.md#dispatch)。如果你在 React 中使用 Redux,[react-redux](https://github.com/gaearon/react-redux) 会提供 [`dispatch`](Store.md#dispatch) 。 6 | 7 | 惟一使用 `bindActionCreators` 的场景是当你需要把 action creator 往下传到一个组件上,却不想让这个组件觉察到 Redux 的存在,而且不希望把 Redux store 或 [`dispatch`](Store.md#dispatch) 传给它。 8 | 9 | 为方便起见,你可以传入一个函数作为第一个参数,它会返回一个函数。 10 | 11 | #### 参数 12 | 13 | 1. `actionCreators` (*Function* or *Object*): 一个 [action creator](../Glossary.md#action-creator),或者键值是 action creators 的对象。 14 | 15 | 2. `dispatch` (*Function*): 一个 [`dispatch`](Store.md#dispatch) 函数,由 [`Store`](Store.md) 实例提供。 16 | 17 | #### 返回值 18 | 19 | (*Function* or *Object*): 一个与原对象类似的对象,只不过这个对象中的的每个函数值都可以直接 dispatch action。如果传入的是一个函数,返回的也是一个函数。 20 | 21 | #### 示例 22 | 23 | #### `TodoActionCreators.js` 24 | 25 | ```js 26 | export function addTodo(text) { 27 | return { 28 | type: 'ADD_TODO', 29 | text 30 | }; 31 | } 32 | 33 | export function removeTodo(id) { 34 | return { 35 | type: 'REMOVE_TODO', 36 | id 37 | }; 38 | } 39 | ``` 40 | 41 | #### `SomeComponent.js` 42 | 43 | ```js 44 | import { Component } from 'react'; 45 | import { bindActionCreators } from 'redux'; 46 | import { connect } from 'react-redux'; 47 | 48 | import * as TodoActionCreators from './TodoActionCreators'; 49 | console.log(TodoActionCreators); 50 | // { 51 | // addTodo: Function, 52 | // removeTodo: Function 53 | // } 54 | 55 | class TodoListContainer extends Component { 56 | componentDidMount() { 57 | // 由 react-redux 注入: 58 | let { dispatch } = this.props; 59 | 60 | // 注意:这样做行不通: 61 | // TodoActionCreators.addTodo('Use Redux'); 62 | 63 | // 你只是调用了创建 action 的方法。 64 | // 你必须要 dispatch action 而已。 65 | 66 | // 这样做行得通: 67 | let action = TodoActionCreators.addTodo('Use Redux'); 68 | dispatch(action); 69 | } 70 | 71 | render() { 72 | // 由 react-redux 注入: 73 | let { todos, dispatch } = this.props; 74 | 75 | // 这是应用 bindActionCreators 比较好的场景: 76 | // 在子组件里,可以完全不知道 Redux 的存在。 77 | 78 | let boundActionCreators = bindActionCreators(TodoActionCreators, dispatch); 79 | console.log(boundActionCreators); 80 | // { 81 | // addTodo: Function, 82 | // removeTodo: Function 83 | // } 84 | 85 | return ( 86 | 88 | ); 89 | 90 | // 一种可以替换 bindActionCreators 的做法是直接把 dispatch 函数 91 | // 和 action creators 当作 props 92 | // 传递给子组件 93 | // return ; 94 | } 95 | } 96 | 97 | export default connect( 98 | TodoListContainer, 99 | state => ({ todos: state.todos }) 100 | ) 101 | ``` 102 | 103 | #### 小贴士 104 | 105 | * 你或许要问:为什么不直接把 action creators 绑定到 store 实例上,就像传统 Flux 那样?问题是这样做的话如果开发同构应用,在服务端渲染时就不行了。多数情况下,你 每个请求都需要一个独立的 store 实例,这样你可以为它们提供不同的数据,但是在定义的时候绑定 action creators,你就可以使用一个唯一的 store 实例来对应所有请求了。 106 | 107 | * 如果你使用 ES5,不能使用 `import * as` 语法,你可以把 `require('./TodoActionCreators')` 作为第一个参数传给 `bindActionCreators`。惟一要考虑的是 `actionCreators` 的参数全是函数。模块加载系统并不重要。 108 | -------------------------------------------------------------------------------- /docs/basics/DataFlow.md: -------------------------------------------------------------------------------- 1 | # 数据流 2 | 3 | **严格的单向数据流**是 Redux 架构的设计核心。 4 | 5 | 这意味着应用中所有的数据都遵循相同的生命周期,这样可以让应用变得更加可预测且容易理解。同时也鼓励做数据范式化,这样可以避免使用多个,独立的无法相互引用的重复数据。 6 | 7 | 如何这些理由还不足以今你信服,读一下 [动机](../introduction/Motivation.md) 和 [Flux 案例](https://medium.com/@dan_abramov/the-case-for-flux-379b7d1982c6),这里面有更加详细的单向数据流优势分析。虽然 [Redux 就不是严格意义上的 [Flux](../introduction/Relation to Other Libraries.md),但它们有共同的设计思想。 8 | 9 | Redux 应用中数据的生命周期遵循下面 4 个步骤: 10 | 11 | 1. **调用** [`store.dispatch(action)`](../api/Store.md#dispatch)。 12 | 13 | action 就是一个描述“发生了什么”的普通对象。比如: 14 | 15 | ```js 16 | { type: 'LIKE_ARTICLE', articleId: 42 }; 17 | { type: 'FETCH_USER_SUCCESS', response: { id: 3, name: 'Megan' } }; 18 | { type: 'ADD_TODO', text: 'Read the Redux docs.'}; 19 | ``` 20 | 21 | 可以把 action 理解成新闻的摘要。如 “玛丽喜欢42号文章。” 或者 “任务列表里添加了'学习 Redux 文档'”。 22 | 23 | 你可以在任何地方调用 [`store.dispatch(action)`](../api/Store.md#dispatch),包括组件中、XHR 回调中、甚至定时器中。 24 | 25 | 2. **Redux store 调用传入的 reducer 函数。** 26 | 27 | Store 会把两个参数传入 reducer,当前的 state 树和 action。例如,在这个 todo 应用中,根 reducer 可能接收这样的数据: 28 | 29 | ```js 30 | // 当前应用的 state(todos 列表和选中的过滤器) 31 | let previousState = { 32 | visibleTodoFilter: 'SHOW_ALL', 33 | todos: [{ 34 | text: 'Read the docs.', 35 | complete: false 36 | }] 37 | }; 38 | 39 | // 将要执行的 action(添加一个 todo) 40 | let action = { 41 | type: 'ADD_TODO', 42 | text: 'Understand the flow.' 43 | }; 44 | 45 | // render 返回处理后的应用状态 46 | let nextState = todoApp(previousState, action); 47 | ``` 48 | 49 | 注意 reducer 是纯函数。它应该是完全可预测的:多次传入相同的输入必须产生相同的输出。它不应做有副作用的操作,如 API 调用或路由跳转。这些应该在 dispatch action 前发生。 50 | 51 | 3. **根 reducer 应该把多个子 reducer 输出合并成一个单一的 state 树。** 52 | 53 | 根 reducer 的结构完全由你决定。Redux 原生提供[`combineReducers()`](../api/combineReducers.md)辅助函数,来把根 reducer 拆分成多个函数,用于分别处理 state 树的一个分支。 54 | 55 | 下面演示 [`combineReducers()`](../api/combineReducers.md) 如何使用。假如你有一个 todos 列表,使用当前的选择过滤器来追踪两个 reducers(原文:and the currently selected filter setting to keep track of with two reducers): 56 | 57 | ```js 58 | function todos(state = [], action) { 59 | // 省略处理逻辑... 60 | return nextState; 61 | } 62 | 63 | function visibleTodoFilter(state = 'SHOW_ALL', action) { 64 | // 省略处理逻辑... 65 | return nextState; 66 | } 67 | 68 | let todoApp = combineReducers({ 69 | todos, 70 | visibleTodoFilter 71 | }); 72 | ``` 73 | 74 | 当你触发 action 后,`combineReducers` 返回的 `todoApp` 会负责调用两个 reducer: 75 | 76 | ```js 77 | let nextTodos = todos(state.todos, action); 78 | let nextVisibleTodoFilter = visibleTodoFilter(state.visibleTodoFilter, action); 79 | ``` 80 | 81 | 然后会把两个结果集合并成一个 state 树: 82 | 83 | ```js 84 | return { 85 | todos: nextTodos, 86 | visibleTodoFilter: nextVisibleTodoFilter 87 | }; 88 | ``` 89 | 90 | 虽然 [`combineReducers()`](../api/combineReducers.md) 是一个很方便的辅助工具,你也可以选择不用;你可以自行实现自己的根 reducer! 91 | 92 | 4. **Redux store 保存了根 reducer 返回的完整 state 树。** 93 | 94 | 这个新的树就是应用的下一个 state!所有订阅 [`store.subscribe(listener)`](../api/Store.md#subscribe) 的监听器都将被调用;监听器里可以调用 [`store.getState()`](../api/Store.md#getState) 获得当前 state。 95 | 96 | 现在,可以应用新的 state 来更新 UI。如果你使用了 [React Redux](https://github.com/gaearon/react-redux) 这类的绑定库,这时就应该调用 `component.setState(newState)` 来更新。 97 | 98 | ## 下一步 99 | 100 | 现在你已经理解了 Redux 如何工作,是时候[结合 React 开发应用](UsageWithReact.md)了。 101 | 102 | >##### 高级用户使用注意 103 | >如果你已经熟悉了基础概念且完成了这个教程,可以学习[高级教程](../advanced/README.md)中的[异步数据流](../advanced/AsyncFlow.md),你将学到如何使用 middleware 在 [异步 action](../advanced/AsyncActions.md) 到达 reducer 前处理它们。 104 | -------------------------------------------------------------------------------- /docs/basics/Actions.md: -------------------------------------------------------------------------------- 1 | # Action 2 | 3 | 首先,让我们来给 action 下个定义。 4 | 5 | **Actions** 是把数据从应用(译者注:这里之所以不叫 view 是因为这些数据有可能是服务器响应,用户输入或其它非 view 的数据 )传到 store 的有效载荷。它是 store 数据的**惟一**来源。用法是通过 [`store.dispatch()`](../api/Store.md#dispatch) 把 action 传到 store。 6 | 7 | 添加新 todo 任务的 action 是这样的: 8 | 9 | ```js 10 | { 11 | type: 'ADD_TODO', 12 | text: 'Build my first Redux app' 13 | } 14 | ``` 15 | 16 | Action 本质是 JavaScript 普通对象。我们约定,action 内使用一个字符串类型的 `type` 字段来表示将要执行的动作。多数情况下,`type` 会被定义成字符串常量。当应用规模越来越大时,建议使用单独的模块或文件来存放 action。 17 | 18 | ```js 19 | import { ADD_TODO, REMOVE_TODO } from '../actionTypes'; 20 | ``` 21 | 22 | >##### 样板文件使用提醒 23 | 24 | >使用单独的模块或文件来定义 action type 常量并不是必须的,甚至根本不需要定义。对于小应用来说,使用字符串做 action type 更方便些。不过,在大型应用中最多把它们显式地定义成常量。参照 [减少样板代码](../recipes/ReducingBoilerplate.md) 获取保持代码干净的实践经验。 25 | 26 | 除了 `type` 字段外,action 对象的结构完全取决于你。参照 [Flux 标准 Action](https://github.com/acdlite/flux-standard-action) 获取如何组织 action 的建议。 27 | 28 | 这时,我们还需要再添加一个 action type 来标记任务完成。因为数据是存放在数组中的,我们通过 `index` 来标识任务。实际项目中一般会在新建内容的时候生成惟一的 ID 做标识。 29 | 30 | ```js 31 | { 32 | type: COMPLETE_TODO, 33 | index: 5 34 | } 35 | ``` 36 | **action 中传递的数据越少越好**。比如,这里传递 `index` 就比把整个任务对象传过去要好。 37 | 38 | 最后,再添加一个 action 类型来表示当前展示的任务状态。 39 | 40 | ```js 41 | { 42 | type: SET_VISIBILITY_FILTER, 43 | filter: SHOW_COMPLETED 44 | } 45 | ``` 46 | 47 | ## Action 创建函数 48 | 49 | **Action 创建函数** 就是生成 action 的方法。“action” 和 “action 创建函数” 这两个概念很容易混在一起,使用时最好注意区分。 50 | 51 | 在 [传统的 Flux](http://facebook.github.io/flux) 实现中,当调用 action 创建函数时,一般会触发一个 dispatch,像这样: 52 | 53 | ```js 54 | function addTodoWithDispatch(text) { 55 | const action = { 56 | type: ADD_TODO, 57 | text 58 | }; 59 | dispatch(action); 60 | } 61 | ``` 62 | 不同的是,Redux 中的 action 创建函数是 **纯函数**,它没有任何副作用,只是返回 action 对象而已。 63 | 64 | ```js 65 | function addTodo(text) { 66 | return { 67 | type: ADD_TODO, 68 | text 69 | }; 70 | } 71 | ``` 72 | 73 | 这让代码更易于测试和移植。只需把 action 创建函数的结果传给 `dispatch()` 方法即可实例化 dispatch。 74 | 75 | ```js 76 | dispatch(addTodo(text)); 77 | dispatch(completeTodo(index)); 78 | ``` 79 | 80 | 或者创建一个 **被绑定的 action 创建函数** 来自动 dispatch: 81 | 82 | ```js 83 | const boundAddTodo = (text) => dispatch(addTodo(text)); 84 | const boundCompleteTodo = (index) => dispatch(CompleteTodo(index)); 85 | ``` 86 | 87 | 可以这样调用: 88 | 89 | ``` 90 | boundAddTodo(text); 91 | boundCompleteTodo(index); 92 | ``` 93 | 94 | store 里能直接通过 [`store.dispatch()`](../api/Store.md#dispatch) 调用 `dispatch()` 方法,但是多数情况下你会使用 [react-redux](http://github.com/gaearon/react-redux) 提供的 `connect()` 帮助器来调用。[`bindActionCreators()`](../api/bindActionCreators.md) 可以自动把多个 action 创建函数 绑定到 `dispatch()` 方法上。 95 | 96 | ## 源码 97 | 98 | ### `actions.js` 99 | 100 | ```js 101 | /* 102 | * action 类型 103 | */ 104 | 105 | export const ADD_TODO = 'ADD_TODO'; 106 | export const COMPLETE_TODO = 'COMPLETE_TODO'; 107 | export const SET_VISIBILITY_FILTER = 'SET_VISIBILITY_FILTER'; 108 | 109 | /* 110 | * 其它的常量 111 | */ 112 | 113 | export const VisibilityFilters = { 114 | SHOW_ALL: 'SHOW_ALL', 115 | SHOW_COMPLETED: 'SHOW_COMPLETED', 116 | SHOW_ACTIVE: 'SHOW_ACTIVE' 117 | }; 118 | 119 | /* 120 | * action 创建函数 121 | */ 122 | 123 | export function addTodo(text) { 124 | return { type: ADD_TODO, text }; 125 | } 126 | 127 | export function completeTodo(index) { 128 | return { type: COMPLETE_TODO, index }; 129 | } 130 | 131 | export function setVisibilityFilter(filter) { 132 | return { type: SET_VISIBILITY_FILTER, filter }; 133 | } 134 | ``` 135 | 136 | ## 下一步 137 | 138 | 现在让我们 [开发一些 reducers](Reducers.md) 来指定发起 action 后 state 应该如何更新。 139 | 140 | >##### 高级用户建议 141 | >如果你已经熟悉这些基本概念且已经完成了这个示例,不要忘了看一下在 [高级教程](../advanced/README.md) 中的 [异步 actions] (../advanced/AsyncActions.md),你将学习如何处理 AJAX 响应和如何把 action 创建函数组合成异步控制流。 -------------------------------------------------------------------------------- /docs/introduction/Ecosystem.md: -------------------------------------------------------------------------------- 1 | # 生态 2 | 3 | Redux 是一个体小精悍的库,但它相关的内容和 API 都是精挑细选的,足以衍生出丰富的工具集和可扩展的生态系统。 4 | 5 | 如果需要关于 Redux 所有内容的列表,推荐移步至 [Awesome Redux](https://github.com/xgrommx/awesome-redux)。它包含了示例、样板代码、中间件、工具库,还有很多其它相关内容。 6 | 7 | 本页将只列出其中由 Redux 维护者亲自维护的一部分内容。不要因此打消尝试其它工具的信心!整个生态发展得太快,我们没有足够的时间去关注所有内容。建议只把这些当作“内部推荐”,如果你使用 Redux 创建了很酷的内容,不要犹豫,马上发个 PR 吧。 8 | 9 | ## 不同框架绑定 10 | 11 | * [react-redux](https://github.com/gaearon/react-redux) —— React 12 | * [ng-redux](https://github.com/wbuchwalter/ng-redux) —— Angular 13 | * [ng2-redux](https://github.com/wbuchwalter/ng2-redux) —— Angular 2 14 | 15 | ## 中间件 16 | 17 | * [redux-thunk](http://github.com/gaearon/redux-thunk) —— 用最简单的方式搭建异步 action 构造器 18 | * [redux-promise](https://github.com/acdlite/redux-promise) —— 遵从 [FSA](https://github.com/acdlite/flux-standard-action) 的 promise 中间件 19 | * [redux-rx](https://github.com/acdlite/redux-rx) —— 给 Redux 用的 RxJS 工具,包括观察变量的中间件 20 | * [redux-logger](https://github.com/fcomb/redux-logger) —— 记录所有 Redux action 和下一次 state 的日志 21 | * [redux-immutable-state-invariant](https://github.com/leoasis/redux-immutable-state-invariant) —— 开发中的状态变更提醒 22 | 23 | ## 组件 24 | 25 | * [redux-form](https://github.com/erikras/redux-form) —— 在 Redux 中时时持有 React 表格的 state 26 | 27 | ## Store 增效器 28 | 29 | * [redux-batched-subscribe](https://github.com/tappleby/redux-batched-subscribe) —— 针对 store subscribers 的自定义批处理与防跳请求 30 | * [redux-history-transitions](https://github.com/johanneslumpe/redux-history-transitions) —— 基于独断的 action 的 history 库转换 31 | 32 | ## Reducer 增效器 33 | 34 | * [redux-optimist](https://github.com/ForbesLindesay/redux-optimist) —— 乐观使用将被提交或还原的 action 35 | * [redux-undo](https://github.com/omnidan/redux-undo) —— 36 | 使 reducer 具有便捷的重做/撤销,以及 action 记录功能 37 | 38 | ## 工具集 39 | 40 | * [reselect](https://github.com/faassen/reselect) —— 受 NuclearJS 启发,有效派生数据的选择器。 41 | * [normalizr](https://github.com/gaearon/normalizr) —— 通过内嵌 API 响应标准化,使 reducer 的处理更简便。 42 | * [redux-actions](https://github.com/acdlite/redux-actions) —— 在初始化 reducers 和 action 构造器时减少样板代码 (boilerplate)。 43 | * [redux-transducers](https://github.com/acdlite/redux-transducers) —— Redux 的编译器工具 44 | * [redux-immutablejs](https://github.com/indexiatech/redux-immutablejs) —— Redux 和 [Immutable](https://github.com/facebook/immutable-js/) 的交互工具 45 | * [redux-tcomb](https://github.com/gcanti/redux-tcomb) —— 在 Redux 中使用具有不可变特性、并经过类型检查的 state 和 action 。 46 | 47 | ## 开发者工具 48 | 49 | * [redux-devtools](http://github.com/gaearon/redux-devtools) —— 一个使用时间旅行 UI 、热加载和 reducers 错误处理器的 action 日志工具,[最早演示于 React Europe 会议](https://www.youtube.com/watch?v=xsSnOQynTHs) 50 | 51 | ## 教程与文章 52 | 53 | * [redux-tutorial](https://github.com/happypoulp/redux-tutorial) —— 一步步学习使用 Redux 54 | * [What the Flux?! Let’s Redux.](https://blog.andyet.com/2015/08/06/what-the-flux-lets-redux) —— Redux 介绍 55 | * [Handcrafting an Isomorphic Redux Application (With Love)](https://medium.com/@bananaoomarang/handcrafting-an-isomorphic-redux-application-with-love-40ada4468af4) —— 使用数据抓取与路由分发的同构应用创建指南 56 | * [Full-Stack Redux Tutorial](http://teropa.info/blog/2015/09/10/full-stack-redux-tutorial.html) —— 使用 Redux 、React 和 Immutable 的测试优先开发指南 57 | 58 | ## 演讲 59 | 60 | * [Live React: Hot Reloading and Time Travel](http://youtube.com/watch?v=xsSnOQynTHs) —— 了解 Redux 如何使用限制措施,让伴随时间旅行的热加载变得简单 61 | * [Cleaning the Tar: Using React within the Firefox Developer Tools](https://www.youtube.com/watch?v=qUlRpybs7_c) —— 了解如何从已有的 MVC 应用逐步迁移至 Redux 62 | 63 | ## 社区公约 64 | 65 | * [Flux Standard Action](https://github.com/acdlite/flux-standard-action) —— Flux 中 action objects 的人性化标准 66 | * [Canonical Reducer Composition](https://github.com/gajus/canonical-reducer-composition) —— 嵌套 reducer 组成的武断标准 67 | * [Ducks: Redux Reducer Bundles](https://github.com/erikras/ducks-modular-redux) —— 关于捆绑 reducers, action 类型 和 actions 的提案 68 | 69 | ## 更多 70 | 71 | [Awesome Redux](https://github.com/xgrommx/awesome-redux) 是一个展示丰富 Redux 相关信息的列表。 72 | -------------------------------------------------------------------------------- /docs/introduction/PriorArt.md: -------------------------------------------------------------------------------- 1 | # 先前技术 2 | 3 | Redux 是一个混合产物。它和一些设计模式及技术相似,但也有不同之处。让我们来探索一下这些相似与不同。 4 | 5 | ### Flux 6 | 7 | Redux 可以被看作 [Flux](https://facebook.github.io/flux/) 的一种实现吗? 8 | [是](https://twitter.com/fisherwebdev/status/616278911886884864),也可以说 [不是](https://twitter.com/andrestaltz/status/616270755605708800)。 9 | 10 | (不用担心,[Flux 的作者](https://twitter.com/jingc/status/616608251463909376) [认可它](https://twitter.com/fisherwebdev/status/616286955693682688),如果你好奇这一点。) 11 | 12 | Redux 是从很多有质量的 Flux 的实现中产生的灵感。和 Flux 一样,Redux 规定,将模型的更新逻辑全部集中于一个特定的层(Flux 里的 store,Redux 里的 reducers)。还想告诉你的是,它不使用应用代码直接变动数据,而用一个叫作 "action" 的普通对象来对变化进行描述。 13 | 14 | 而不同于 Flux ,**Redux 没有 dispatcher 的概念**。原因是它依赖纯函数来替代事件处理器。纯函数构建简单,也不需额外的实体来管理它们。你可以将点这看作两者的差异或细节实现,取决于你怎么看 Flux。Flux 常常[被表述为 `(state, action) => state`](https://speakerdeck.com/jmorrell/jsconf-uy-flux-those-who-forget-the-past-dot-dot-dot)。从这个意义上说,Redux 无疑是 Flux 架构的实现,且得益于其纯函数而更为简单。 15 | 16 | 和 Flux 的另一个重要区别,是 **Redux 设想你永远不会变动你的数据**。你可以很好地使用普通对象和数组来管理 state ,而不是在 reducers 里变动数据,这会让你深感挫折。正确的方式是,你应该在 reducer 中返回一个新对象来更新 state, 配合 [ES7 所提议的对象 spread 语法](https://github.com/sebmarkbage/ecmascript-rest-spread) 和 [Babel](http://babeljs.io),或者一个库,如 [Immutable](https://facebook.github.io/immutable-js) ,这种做法简单易行。 17 | 18 | 虽然出于性能方面的考虑,[写不纯的 reducers](https://github.com/gaearon/redux/issues/328#issuecomment-125035516) 来变动数据在技术上是*可行*的,但我们并不鼓励这么做。不纯的 reducer 会使一些开发特性,如时间旅行、记录/回放或热加载不可实现。此外,在大部分实际应用中,这种数据不可变动的特性并不会带来性能问题,就像 [Om](https://github.com/omcljs/om) 所表现的,即使对象分配失败,仍可以防止昂贵的重渲染和重计算。而得益于 reducer 的纯度,应用内的变化更是一目了然。 19 | 20 | ### Elm 21 | 22 | [Elm](http://elm-lang.org/) 是一种函数式编程语言,由 [Evan Czaplicki](https://twitter.com/czaplic) 受 Haskell 语言的启发开发。它执行一种 [“model view update” 的架构](http://elm-lang.org/guide/architecture) ,更新遵循 `(state, action) => state` 的规则。 从技术上说,Elm 的 "updaters" 等同于 Redux 里的 reducer。 23 | 24 | 不同于 Redux,Elm 是一门语言,因此它在执行纯度,静态类型,不可变动性,actions 和模式匹配等方面更具优势。即使你不打算使用 Elm,也可以读一读 Elm 的架构,尝试一把。基于此,有一个有趣的[使用 JavaScript 库实现类似想法](https://github.com/paldepind/noname-functional-frontend-framework) 的项目。我们能看到 Redux 从中取得的灵感! 为了更接近 Elm 的静态类型,[它使用了一个类似 Flow 的渐进类型解决方案](https://github.com/gaearon/redux/issues/290) 。 25 | 26 | ### Immutable 27 | 28 | [Immutable](https://facebook.github.io/immutable-js) 是一个可实现不可变数据结构的 JavaScript 库。它十分高性能,并拥有常用的 JavaScript API。 29 | 30 | Immutable 及类似的库都与 Redux 对接良好。尽可随意地一起使用! 31 | 32 | **Redux 并不在意你*如何*存储 state,state 可以是普通对象,可以是不可变对象,或者其它类型。** 为了从 server 端写同构应用或融合它们的 state ,你可能要用到序列化或反序列化的机制。但除此以外,你可以使用任何数据存储的库,*只要它支持数据的不可变动性*。举例说明,对于 Redux state ,Backbone 并无意义,因为 Backbone models 是可变的。 33 | 34 | 注意,即便具有不可变特性的库支持 cursors,也不应在 Redux 的应用中使用。整个 state tree 应被视为只读,并需通过 Redux 来更新 state 和订阅更新。因此,通过 cursor 来改写,对 Redux 来说没有意义。**而如果只是想用 cursor 把 state tree 从 UI tree 解耦并逐步细化 cursor,应使用 selector 来替代。** Selectors 是可组合的 getter 函数组。具体可参考 [reselect](http://github.com/faassen/reselect),这是一个优秀、简洁的可组合 selector 的实现。 35 | 36 | ### Baobab 37 | 38 | [Baobab](https://github.com/Yomguithereal/baobab) 是另一个流行的库,实现了数据不可变特性 API 用以更新纯 JavaScript 对象。你当然可以在 Redux 中使用它,但两者一起使用并没有什么优势。 39 | 40 | Baobab 所提供的大部分功能都与使用 cursors 更新数据相关,而 Redux 更新数据的唯一方法是分发一个 action 。可见,两者用不同方法,解决的却是同样的问题,相互并无增益。 41 | 42 | 不同于 Immutable ,Baobab 在引擎下还不能现实任何特别有效的数据结构,同时使用 Baobab 和 Redux 并无裨益。这种情形下,使用普通对象会更简便。 43 | 44 | ### Rx 45 | 46 | [Reactive Extensions](https://github.com/Reactive-Extensions/RxJS) (和它们正在进行的 [现代化重写](https://github.com/ReactiveX/RxJS)) 是管理复杂异步应用非常优秀的方案。[以外,还有致力于构建人机交互并将其视作相互依赖的可观测变量的库](http://cycle.js.org). 47 | 48 | 同时使用它和 Redux 有意义么? 当然! 它们配合得很好。将 Redux store 视作可观察变量非常简便,例如: 49 | 50 | ```js 51 | function toObservable(store) { 52 | return { 53 | subscribe({ onNext }) { 54 | let dispose = store.subscribe(() => onNext(store.getState())); 55 | onNext(store.getState()); 56 | return { dispose }; 57 | } 58 | } 59 | } 60 | ``` 61 | 62 | 使用类似方法,你可以组合不同的异步流,将其转化为 action ,再提交到 `store.dispatch()` 。 63 | 64 | 问题在于: 在已经使用了 Rx 的情况下,你真的需要 Redux 吗? 不一定。[通过 Rx 重新实现 Redux](https://github.com/jas-chen/rx-redux) 并不难。有人说仅需使用一两句的 `.scan()` 方法即可。这种做法说不定不错! 65 | 66 | 如果你仍有疑虑,可以去查看 Redux 的源代码 (并不多) 以及生态系统 (例如[开发者工具](https://github.com/gaearon/redux-devtools))。如果你无意于此,仍坚持使用交互数据流,可以去探索一下 [Cycle](http://cycle.js.org) 这样的库,或把它合并到 Redux 中。记得告诉我们它运作得如何! 67 | -------------------------------------------------------------------------------- /docs/api/Store.md: -------------------------------------------------------------------------------- 1 | # Store 2 | 3 | Store 就是用来维持应用所有的 [state 树](../Glossary.md#state) 的一个对象。 4 | 改变 store 内 state 的惟一途径是对它 dispatch 一个 [action](../Glossary.md#action)。 5 | 6 | Store 不是类。它只是有几个方法的对象。 7 | 要创建它,只需要把根部的 [reducing 函数](../Glossary.md#reducer) 传递给 [`createStore`](createStore.md)。 8 | 9 | >##### Flux 用户使用注意 10 | 11 | >如果你以前使用 Flux,那么你只需要注意一个重要的区别。Redux 没有 Dispatcher 且不支持多个 store。相反,只有一个单一的 store 和一个根级的 reduce 函数(reducer)。随着应用不断变大,你应该把根级的 reducer 拆成多个小的 reducers,分别独立地操作 state 树的不同部分,而不是添加新的 stores。这就像一个 React 应用只有一个根级的组件,这个根组件又由很多小组件构成。 12 | 13 | ### Store 方法 14 | 15 | - [`getState()`](#getState) 16 | - [`dispatch(action)`](#dispatch) 17 | - [`subscribe(listener)`](#subscribe) 18 | - [`replaceReducer(nextReducer)`](#replaceReducer) 19 | 20 | ## Store 方法 21 | 22 | ### [`getState()`](#getState) 23 | 24 | 返回应用当前的 state 树。 25 | 它与 store 的最后一个 reducer 返回值相同。 26 | 27 | #### 返回值 28 | 29 | *(any)*: 应用当前的 state 树。 30 | 31 |
32 | 33 | ### [`dispatch(action)`](#dispatch) 34 | 35 | 分发 action。这是触发 state 变化的惟一途径。 36 | 37 | 会使用当前 [`getState()`](#getState) 的结果和传入的 `action` 以同步方式的调用 store 的 reduce 函数。返回值会被作为下一个 state。从现在开始,这就成为了 [`getState()`](#getState) 的返回值,同时变化监听器(change listener)会被触发。 38 | 39 | >##### Flux 用户使用注意 40 | >当你在 [reducer](../Glossary.md#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,还有其它一些本来很正常的地方。 41 | 42 | >在 Redux 里,只会在根 reducer 返回新 state 结束后再会调用事件监听器,因此,你可以在事件监听器里再做 dispatch。惟一使你不能在 reducer 中途 dispatch 的原因是要确保 reducer 没有副作用。如果 action 处理会产生副作用,正确的做法是使用异步 [action 创建函数](../Glossary.md#action-creator)。 43 | 44 | #### 参数 45 | 46 | 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 的建议。 47 | 48 | #### 返回值 49 | 50 | (Object): 要 dispatch 的 action。 51 | 52 | #### 注意 53 | 54 | 使用 [`createStore`](createStore.md) 创建的 “纯正” store 只支持普通对象类型的 action,而且会立即传到 reducer 来执行。 55 | 56 | 但是,如果你用 [`applyMiddleware`](applyMiddleware.md) 来套住 [`createStore`](createStore.md) 时,middleware 可以修改 action 的执行,并支持执行 dispatch [intent(意图)](../Glossary.md#intent)。Intent 一般是异步操作如 Promise、Observable 或者 Thunk。 57 | 58 | Middleware 是由社区创建,并不会同 Redux 一起发行。你需要手动安装 [redux-thunk](https://github.com/gaearon/redux-thunk) 或者 [redux-promise](https://github.com/acdlite/redux-promise) 库。你也可以创建自己的 middleware。 59 | 60 | 想学习如何描述异步 API 调用?看一下 action 创建函数里当前的 state,执行一个有副作用的操作,或者以链式操作执行它们,参照 [`applyMiddleware`](applyMiddleware.md) 中的示例。 61 | 62 | #### 示例 63 | 64 | ```js 65 | import { createStore } from 'redux'; 66 | let store = createStore(todos, ['Use Redux']); 67 | 68 | function addTodo(text) { 69 | return { 70 | type: 'ADD_TODO', 71 | text 72 | }; 73 | } 74 | 75 | store.dispatch(addTodo('Read the docs')); 76 | store.dispatch(addTodo('Read about the middleware')); 77 | ``` 78 | 79 |
80 | 81 | ### [`subscribe(listener)`](#subscribe) 82 | 83 | 添加一个变化监听器。每当 dispatch action 的时候就会执行,state 树中的一部分可能已经变化。你可以在回调函数里调用 [`getState()`](#getState) 来拿到当前 state。 84 | 85 | 这是一个底层 API。多数情况下,你不会直接使用它,会使用一些 React(或其它库)的绑定。如果你想让回调函数执行的时候使用当前的 state,你可以 [把 store 转换成一个 Observable 或者写一个定制的 `observeStore` 工具](https://github.com/rackt/redux/issues/303#issuecomment-125184409)。 86 | 87 | 如果需要解绑这个变化监听器,执行 `subscribe` 返回的函数即可。 88 | 89 | #### 参数 90 | 91 | 1. `listener` (*Function*): 每当 dispatch action 的时候都会执行的回调。state 树中的一部分可能已经变化。你可以在回调函数里调用 [`getState()`](#getState) 来拿到当前 state。store 的 reducer 应该是纯函数,因此你可能需要对 state 树中的引用做深度比较来确定它的值是否有变化。 92 | 93 | ##### 返回值 94 | 95 | (*Function*): 一个可以解绑变化监听器的函数。 96 | 97 | ##### 示例 98 | 99 | ```js 100 | function select(state) { 101 | return state.some.deep.property; 102 | } 103 | 104 | let currentValue; 105 | function handleChange() { 106 | let previousValue = currentValue; 107 | currentValue = select(store.getState()); 108 | 109 | if (previousValue !== currentValue) { 110 | console.log('Some deep nested property changed from', previousValue, 'to', currentValue); 111 | } 112 | } 113 | 114 | let unsubscribe = store.subscribe(handleChange); 115 | handleChange(); 116 | ``` 117 | 118 |
119 | 120 | ### [`replaceReducer(nextReducer)`](#replaceReducer) 121 | 122 | 替换 store 当前用来计算 state 的 reducer。 123 | 124 | 这是一个高级 API。只有在你需要实现代码分隔,而且需要立即加载一些 reducer 的时候才可能会用到它。在实现 Redux 热加载机制的时候也可能会用到。 125 | 126 | #### 参数 127 | 128 | 1. `reducer` (*Function*) store 会使用的下一个 reducer。 129 | -------------------------------------------------------------------------------- /docs/Troubleshooting.md: -------------------------------------------------------------------------------- 1 | # 排错 2 | 3 | 这里会列出常见的问题和对应的解决方案。 4 | 虽然使用 React 做示例,即使你使用要其它库仍然会有帮助。 5 | 6 | ### dispatch action 后什么也没有发生 7 | 8 | 有时,你 dispatch action 后,view 却没有更新。这是为什么呢?可能有下面几种原因。 9 | 10 | #### 永远不要直接修改 reducer 的参数 11 | 12 | 如果你想修改 Redux 给你传入的 `state` 或 `action`,请住手! 13 | 14 | Redux 假定你永远不会修改 reducer 里传入的对象。**任何时候,你都应该返回一个新的 state 对象。**即使你没有使用 [Immutable](https://facebook.github.io/immutable-js/) 这样的库,也要保证做到不修改对象。 15 | 16 | 不变性(Immutability)可以让 [react-redux](https://github.com/gaearon/react-redux) 高效的监听 state 的细粗度更新。它也让 [redux-devtools](http://github.com/gaearon/redux-devtools) 能提供“时间旅行”这类强大特性。 17 | 18 | 例如,下面的 reducer 就是错误的,因为它改变了 state: 19 | 20 | ```js 21 | function todos(state = [], action) { 22 | switch (action.type) { 23 | case 'ADD_TODO': 24 | // 错误!这会改变 state.actions。 25 | state.actions.push({ 26 | text: action.text, 27 | completed: false 28 | }); 29 | case 'COMPLETE_TODO': 30 | // 错误!这会改变 state.actions[action.index]. 31 | state.actions[action.index].completed = true; 32 | } 33 | 34 | return state 35 | } 36 | ``` 37 | 38 | 应该重写成这样: 39 | 40 | ```js 41 | function todos(state = [], action) { 42 | switch (action.type) { 43 | case 'ADD_TODO': 44 | // 返回新数组 45 | return [...state, { 46 | text: action.text, 47 | completed: false 48 | }]; 49 | case 'COMPLETE_TODO': 50 | // 返回新数组 51 | return [ 52 | ...state.slice(0, action.index), 53 | // 修改之前复制数组 54 | Object.assign({}, state[action.index], { 55 | completed: true 56 | }), 57 | ...state.slice(action.index + 1) 58 | ]; 59 | default: 60 | return state; 61 | } 62 | } 63 | ``` 64 | 65 | 虽然需要写更多代码,但是让 Redux 变得可具有可预测性和高效。如果你想减少代码量,你可以用一些辅助方法类似 66 | [`React.addons.update`](https://facebook.github.io/react/docs/update.html) 来让这样的不可变转换操作变得更简单: 67 | 68 | ```js 69 | // 修改前 70 | return [ 71 | ...state.slice(0, action.index), 72 | Object.assign({}, state[action.index], { 73 | completed: true 74 | }), 75 | ...state.slice(action.index + 1) 76 | ] 77 | 78 | // 修改后 79 | return update(state, { 80 | [action.index]: { 81 | completed: { 82 | $set: true 83 | } 84 | } 85 | }); 86 | ``` 87 | 88 | 最后,如果需要更新 object,你需要使用 Underscore 提供的 `_.extend` 方法,或者更好的,使用 [`Object.assign`](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/assign) 的 polyfill 89 | 90 | 要注意 `Object.assign` 的使用方法。例如,在 reducer 里不要这样使用 `Object.assign(state, newData)`,应该用 `Object.assign({}, state, newData)`。这样它才不会覆盖以前的 `state`。 91 | 92 | 你也可以通过使用 [Babel 阶段 1](http://babeljs.io/docs/usage/experimental/) 模式来开启 [ES7 对象的 spread 操作](https://github.com/sebmarkbage/ecmascript-rest-spread): 93 | 94 | ```js 95 | // 修改前: 96 | return [ 97 | ...state.slice(0, action.index), 98 | Object.assign({}, state[action.index], { 99 | completed: true 100 | }), 101 | ...state.slice(action.index + 1) 102 | ] 103 | 104 | // 修改后: 105 | return [ 106 | ...state.slice(0, action.index), 107 | { ...state[action.index], completed: true }, 108 | ...state.slice(action.index + 1) 109 | ] 110 | ``` 111 | 112 | 注意还在实验阶段的特性注定经常改变,最好不要在大的项目里过多依赖它们。 113 | 114 | #### 不要忘记调用 [`dispatch(action)`](api/Store.md#dispatch) 115 | 116 | 如果你定义了一个 action 创建函数,调用它并**不**会自动 dispatch 这个 action。比如,下面的代码什么也不会做: 117 | 118 | #### `TodoActions.js` 119 | 120 | ```js 121 | export function addTodo(text) { 122 | return { type: 'ADD_TODO', text }; 123 | } 124 | ``` 125 | 126 | #### `AddTodo.js` 127 | 128 | ```js 129 | import { Component } from 'react'; 130 | import { addTodo } from './TodoActions'; 131 | 132 | class AddTodo extends Component { 133 | handleClick() { 134 | // 不起作用! 135 | addTodo('Fix the issue'); 136 | } 137 | 138 | render() { 139 | return ( 140 | 143 | ); 144 | } 145 | } 146 | ``` 147 | 148 | 它不起作用是因为你的 action 创建函数只是一个**返回** action 的函数而已。你需要手动 dispatch 它。我们不能在定义时把 action 创建函数绑定到指定的 Store 上,因为应用在服务端渲染时需要为每个请求都对应一个独立的 Redux store。 149 | 150 | 解法是调用 [store](api/Store.md) 实例上的 [`dispatch()`](api/Store.md#dispatch) 方法。 151 | 152 | ```js 153 | handleClick() { 154 | // 生效!(但你需要先以某种方式拿到 store) 155 | store.dispatch(addTodo('Fix the issue')); 156 | } 157 | ``` 158 | 159 | 如果组件的层级非常深,把 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 中。 160 | 161 | 修复后的代码是这样的: 162 | 163 | ```js 164 | import { Component } from 'react'; 165 | import { connect } from 'react-redux'; 166 | import { addTodo } from './TodoActions'; 167 | 168 | class AddTodo extends Component { 169 | handleClick() { 170 | // 生效! 171 | this.props.dispatch(addTodo('Fix the issue')); 172 | } 173 | 174 | render() { 175 | return ( 176 | 179 | ); 180 | } 181 | } 182 | 183 | // 除了 state,`connect` 还把 `dispatch` 放到 props 里。 184 | export default connect(AddTodo, state => ({})) 185 | ``` 186 | 187 | 如果你想的话也可以把 `dispatch` 手动传给其它组件。 188 | 189 | ## 其它问题 190 | 191 | 在 Slack [Reactiflux](http://reactiflux.com/) 里的 **redux** 频道里提问,或者[提交一个 issue](https://github.com/rackt/redux/issues)。 192 | 如果问题终于解决了,请把解法[写到文档里](https://github.com/rackt/redux/edit/master/docs/Troubleshooting.md),以便别人遇到同样问题时参考。 193 | -------------------------------------------------------------------------------- /docs/Glossary.md: -------------------------------------------------------------------------------- 1 | # 词汇表 2 | 3 | 这是有关Redux中的一些核心概念的词汇表,以及他们的类型签名。这些类型使用了 [流标注法](http://flowtype.org/docs/quick-reference.html)进行记录。 4 | 5 | ## State 6 | 7 | ```js 8 | type State = any; 9 | ``` 10 | 11 | *State* (也叫 *state tree*) 是一个宽泛的概念,但是在 Redux API 中它通常与被 store 所管理的,可以被 [`getState()`](api/Store.md#getState) 返回的,单独 state 值相关。 它表示了一个 Redux应用的全部状态,通常为一个多层嵌套的对象。 12 | 13 | 约定俗成,顶层 state 为一个对象,或几个像 Map 那样的键-值集合,当然是任意类型的话也成。当然,你仍然可以尽可能保持状态的串行化。不要把什么都放进去导致无法容易地转换成 JSON。 14 | 15 | ## Action 16 | 17 | ```js 18 | type Action = Object; 19 | ``` 20 | 21 | *Action* 是一个用以表示要改变的 state 的意图的普通对象。Action 是将数据拿到 store 里的唯一方法。无论是 UI 事件,网络回调,还是其他诸如 WebSocket 之类的其他源,任何数据都或多或少的被 dispatch 成 action。 22 | 23 | 约定俗成,action 应该有一个 `type` 域指明了需要被演算的 action 类型。Type 可以被定义为常数从其他 module 中导入。比起用 [Symbols](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Symbol) 表示 `type` 使用 String 是更好的方法因为 string 是可被串行化的。 24 | 25 | 除了 `type`之外,action 对象的结构其实完全取决于你自己。如果你感兴趣的话,请参考 [Flux Standard Action](https://github.com/acdlite/flux-standard-action) 作为如何组织 actions 的建议。 26 | 27 | 还有就是请看后面的 [异步 action](#async-action)。 28 | 29 | ## Reducer 30 | 31 | ```js 32 | type Reducer = (state: S, action: A) => S; 33 | ``` 34 | 35 | *Reducer* (也叫 *reducing function*) 是一个接受累积运算和一个值,返回新的累积函数的函数。用来把一个集合 reduce 到一个单独值。 36 | 37 | Reducer 并不是 Redux 特有的——它是函数式编程中的一个基本概念。甚至大部分的非函数式语言比如 JavaScript,都有一个内建的 reduce API。在 JavaScript 中的话是 [`Array.prototype.reduce()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce). 38 | 39 | 在 Redux 中,累计运算的结果是个 state 对象,被累积的值就是 action。Reducer 由上一个 state 和一个 action 计算得到一个新 state。它必须是 *纯函数* 也就是由完全相同的输入会返回完全相同的输出。它应该是没有副作用的。这使得一些很棒的功能诸如热重载和时间旅行成为可能。 40 | 41 | Reducer 是 Redux 之中最重要的概念。 42 | 43 | *不要在 reducer 中有 API 调用* 44 | 45 | ## dispatch function 46 | 47 | ```js 48 | type BaseDispatch = (a: Action) => Action; 49 | type Dispatch = (a: Action | AsyncAction) => any; 50 | ``` 51 | 52 | 一个 *dispatching function* (或者简单点叫 *dispatch function*) 是一个接收一个 action 或者[异步 action](#async-action)的函数,它可以或不可以分发一个或多个 action 到 store。 53 | 54 | 我们必须搞清 dispatch function 和由没有 middleware 的 store 实例提供的 base [`dispatch`](api/Store.md#dispatch) function 其中的区别。 55 | 56 | Base dispatch function *总是* 同步发 action 给 store 的 reducer,以及由 store 返回的上一个 state 计算出新 state。它期望 actions 会是一个准备好被 reducer 消费掉的普通对象。 57 | 58 | [ Middleware ](#middleware) 封装了base dispatch function。它允许了 dispatch function 处理 action 之外的 [异步 action](#async-action)。 middleware 可以被变形,延迟,忽略,以及其他在将 action 或异步 action 传递给下一个 middleware 之前作出解释。获取更多信息请往后看。 59 | 60 | ## Action Creator 61 | 62 | ```js 63 | type ActionCreator = (...args: any) => Action | AsyncAction; 64 | ``` 65 | 66 | *Action Creator* 很简单,就是一个创建 action 的函数。别把这两个概念搞混。Action 是一个信息的负载,而 action 创建者是一个创建 action 的工厂。 67 | 68 | 调用 action creator 只会生产出 action,但不分发。你需要调用 store 的 [`dispatch`](api/Store.md#dispatch) function 才会真正引起变化。有时我们讲 *bound action creator* 意味着函数调用 action creator并立即将结果分发给一个特定的 store 实例。 69 | 70 | 如果 action 创建者需要读取当前状态、做出 API 调用、或引起诸如路由变位等副作用,应该返回一个 [异步 action](#async-action) 而不是 action。 71 | 72 | ## 异步 Action 73 | 74 | ```js 75 | type AsyncAction = any; 76 | ``` 77 | 78 | *异步 action* 是一个发给分发函数,但还没有准备好被 reducer 消费的值。它会在被发往 base [`dispatch()`](api/Store.md#dispatch) function 之前,被 [ middleware ](#middleware) 变为一个或一组 action。异步 actions 可以有多个 type,取决于使用的 middleware。通常为 Promise 或者 thunk 之类的异步原生,虽然没有被马上传给 reducer,但是操作一旦完成就会触发 action 分发。 79 | 80 | ## Middleware 81 | 82 | ```js 83 | type MiddlewareAPI = { dispatch: Dispatch, getState: () => State }; 84 | type Middleware = (api: MiddlewareAPI) => (next: Dispatch) => Dispatch; 85 | ``` 86 | 87 | Middleware 是一个高阶函数,它将 [dispatch function](#dispatching-function) 组合并返回一个新的 dispatch function。它通常将 [异步 actions](#async-action) 变为 actions。 88 | 89 | Middleware 是使用了复合函数的可构建的。它可在 action 日志,表现副作用例如路由,或将异步 API 调用变为一组同步 actions。 90 | 91 | 请见 [`applyMiddleware(...middlewares)`](./api/applyMiddleware.md) 获取有关 middleware 的详细内容。 92 | 93 | ## Store 94 | 95 | ```js 96 | type Store = { 97 | dispatch: Dispatch; 98 | getState: () => State; 99 | subscribe: (listener: () => void) => () => void; 100 | getReducer: () => Reducer; 101 | replaceReducer: (reducer: Reducer) => void; 102 | }; 103 | ``` 104 | 105 | Store 是一个承载有应用 state tree 的对象。 106 | 一个 Redux 应用中应当只有一个 Store,因为构建发生于 reducer 级。 107 | 108 | - [`dispatch(action)`](api/Store.md#dispatch) 是上面描述过的 base dispatch function。 109 | - [`getState()`](api/Store.md#getState) 返回当前 store 的 state。 110 | - [`subscribe(listener)`](api/Store.md#subscribe) 注册 funtion 用于在 state 改变时调用。 111 | - [`getReducer()`](api/Store.md#getReducer) 和 [`replaceReducer(nextReducer)`](api/Store.md#replaceReducer) 可被用于实现热重载荷代码分割。通常你用不上他们。 112 | 113 | 请见完整的 [store API reference](api/Store.md#dispatch) 获取更多细节。 114 | 115 | ## Store Creator 116 | 117 | ```js 118 | type StoreCreator = (reducer: Reducer, initialState: ?State) => Store; 119 | ``` 120 | 121 | Store creator 是一个创建 Redux store 的函数。就像 dispatching function 那样,我们必须分清由 [`createStore(reducer, initialState)`](api/createStore.md) 从 Redux 包中导出的 base store creator,和从 store enhancer 返回的 store creator。 122 | 123 | ## Store enhancer 124 | 125 | ```js 126 | type StoreEnhancer = (next: StoreCreator) => StoreCreator; 127 | ``` 128 | 129 | Store enhancer 是一个高阶函数,将 store creator 组合,返回一个新的强化过的 store creator。这与允许你使用可组合方式变更 store 接口的 middleware 有点相似。 130 | 131 | Store enhancer 是与 React 中概念非常相同的高阶 component, 通常也会被叫做 “component enhancers”。 132 | 133 | 因为 store 并非一个实例,而更像是几个函数的集合普通对象。复制可以被简单的创建或修改而不需变动原先的 store。在 [`compose`](api/compose.md) 文档中有一个示例演示了这种做法。 134 | 135 | 大多数时候你不可能去写 store enhancer,但你会用得着 [developer tools](https://github.com/gaearon/redux-devtools) 提供的。它使得app对其发生无察觉的时间旅行变得可能。搞笑的是,[Redux middleware 的实现](api/applyMiddleware.md) 本身就是一个 store enhancer。 136 | -------------------------------------------------------------------------------- /docs/recipes/ComputingDerivedData.md: -------------------------------------------------------------------------------- 1 | # 计算衍生数据 2 | 3 | [Reselect](https://github.com/faassen/reselect.git) 是用来创建可记忆的(Memoized)、可组合的 **selector** 函数。Reselect selectors 可以用来高效地计算 Redux store 里的衍生数据。 4 | 5 | ### 可记忆的 Selectors 初衷 6 | 7 | 首先访问 [Todos 列表示例](../basics/UsageWithReact.md): 8 | 9 | #### `containers/App.js` 10 | 11 | ```js 12 | import React, { Component, PropTypes } from 'react'; 13 | import { connect } from 'react-redux'; 14 | import { addTodo, completeTodo, setVisibilityFilter, VisibilityFilters } from '../actions'; 15 | import AddTodo from '../components/AddTodo'; 16 | import TodoList from '../components/TodoList'; 17 | import Footer from '../components/Footer'; 18 | 19 | class App extends Component { 20 | render() { 21 | // 通过 connect() 注入: 22 | const { dispatch, visibleTodos, visibilityFilter } = this.props; 23 | return ( 24 |
25 | 27 | dispatch(addTodo(text)) 28 | } /> 29 | 32 | dispatch(completeTodo(index)) 33 | } /> 34 |
37 | dispatch(setVisibilityFilter(nextFilter)) 38 | } /> 39 |
40 | ); 41 | } 42 | } 43 | 44 | App.propTypes = { 45 | visibleTodos: PropTypes.arrayOf(PropTypes.shape({ 46 | text: PropTypes.string.isRequired, 47 | completed: PropTypes.bool.isRequired 48 | })), 49 | visibilityFilter: PropTypes.oneOf([ 50 | 'SHOW_ALL', 51 | 'SHOW_COMPLETED', 52 | 'SHOW_ACTIVE' 53 | ]).isRequired 54 | }; 55 | 56 | function selectTodos(todos, filter) { 57 | switch (filter) { 58 | case VisibilityFilters.SHOW_ALL: 59 | return todos; 60 | case VisibilityFilters.SHOW_COMPLETED: 61 | return todos.filter(todo => todo.completed); 62 | case VisibilityFilters.SHOW_ACTIVE: 63 | return todos.filter(todo => !todo.completed); 64 | } 65 | } 66 | 67 | function select(state) { 68 | return { 69 | visibleTodos: selectTodos(state.todos, state.visibilityFilter), 70 | visibilityFilter: state.visibilityFilter 71 | }; 72 | } 73 | 74 | // 把组件包起来,以此来注入 dispatch 和 state 75 | export default connect(select)(App); 76 | ``` 77 | 78 | 上面的示例中,`select` 调用了 `selectTodos` 来计算 `visibleTodos`。运行没问题,但有一个缺点:每当组件更新时都会计算 `visibleTodos`。如果 state tree 非常大,或者计算量非常大,每次更新都重新计算可能会带来性能问题。Reselect 能帮你省去这些没必要的重新计算。 79 | 80 | ### 创建可记忆的 Selector 81 | 82 | 我们需要一个可记忆的 selector 来替代这个 `select`,只在 `state.todos` or `state.visibilityFilter` 变化时重新计算 `visibleTodos`,而在其它部分(非相关)变化时不做计算。 83 | 84 | Reselect 提供 `createSelector` 函数来创建可记忆的 selector。`createSelector` 接收一个 input-selectors 数组和一个转换函数作为参数。如果 state tree 的改变会引起 input-selector 值变化,那么 selector 会调用转换函数,传入 input-selectors 作为参数,并返回结果。如果 input-selectors 的值的前一次的一样,它将会直接返回前一次计算的数据,而不会再调用一次转换函数。 85 | 86 | 让我们定义一个可记忆的 selector `visibleTodosSelector` 来替代 `select`: 87 | 88 | #### `selectors/TodoSelectors.js` 89 | 90 | ```js 91 | import { createSelector } from 'reselect'; 92 | import { VisibilityFilters } from './actions'; 93 | 94 | function selectTodos(todos, filter) { 95 | switch (filter) { 96 | case VisibilityFilters.SHOW_ALL: 97 | return todos; 98 | case VisibilityFilters.SHOW_COMPLETED: 99 | return todos.filter(todo => todo.completed); 100 | case VisibilityFilters.SHOW_ACTIVE: 101 | return todos.filter(todo => !todo.completed); 102 | } 103 | } 104 | 105 | const visibilityFilterSelector = (state) => state.visibilityFilter; 106 | const todosSelector = (state) => state.todos; 107 | 108 | export const visibleTodosSelector = createSelector( 109 | [visibilityFilterSelector, todosSelector], 110 | (visibilityFilter, todos) => { 111 | return { 112 | visibleTodos: selectTodos(todos, visibilityFilter), 113 | visibilityFilter 114 | }; 115 | } 116 | ); 117 | ``` 118 | 119 | 在上例中,`visibilityFilterSelector` 和 `todosSelector` 是 input-selector。因为他们并不转换数据,所以被创建成普通的非记忆的 selector 函数。但是,`visibleTodosSelector` 是一个可记忆的 selector。他接收 `visibilityFilterSelector` 和 `todosSelector` 为 input-selector,还有一个转换函数来计算过滤的 todos 列表。 120 | 121 | ### 组合 Selector 122 | 123 | 可记忆的 selector 自身可以作为其它可记忆的 selector 的 input-selector。下面的 `visibleTodosSelector` 被当作另一个 selector 的 input-selector,来进一步通过关键字(keyword)过滤 todos。 124 | 125 | ```js 126 | const keywordSelector = (state) => state.keyword; 127 | 128 | const keywordFilterSelector = createSelector( 129 | [visibleTodosSelector, keywordSelector], 130 | (visibleTodos, keyword) => visibleTodos.filter( 131 | todo => todo.indexOf(keyword) > -1 132 | ) 133 | ); 134 | ``` 135 | 136 | ### 连接 Selector 和 Redux Store 137 | 138 | 如果你在使用 react-redux,你可以使用 connect 来连接可忘记的 selector 和 Redux store。 139 | 140 | #### `containers/App.js` 141 | 142 | ```js 143 | import React, { Component, PropTypes } from 'react'; 144 | import { connect } from 'react-redux'; 145 | import { addTodo, completeTodo, setVisibilityFilter } from '../actions'; 146 | import AddTodo from '../components/AddTodo'; 147 | import TodoList from '../components/TodoList'; 148 | import Footer from '../components/Footer'; 149 | import { visibleTodosSelector } from '../selectors/todoSelectors.js'; 150 | 151 | class App extends Component { 152 | render() { 153 | // 通过 connect() 注入: 154 | const { dispatch, visibleTodos, visibilityFilter } = this.props; 155 | return ( 156 |
157 | 159 | dispatch(addTodo(text)) 160 | } /> 161 | 164 | dispatch(completeTodo(index)) 165 | } /> 166 |
169 | dispatch(setVisibilityFilter(nextFilter)) 170 | } /> 171 |
172 | ); 173 | } 174 | } 175 | 176 | App.propTypes = { 177 | visibleTodos: PropTypes.arrayOf(PropTypes.shape({ 178 | text: PropTypes.string.isRequired, 179 | completed: PropTypes.bool.isRequired 180 | })), 181 | visibilityFilter: PropTypes.oneOf([ 182 | 'SHOW_ALL', 183 | 'SHOW_COMPLETED', 184 | 'SHOW_ACTIVE' 185 | ]).isRequired 186 | }; 187 | 188 | // 把 selector 传递给连接的组件 189 | export default connect(visibleTodosSelector)(App); 190 | ``` 191 | -------------------------------------------------------------------------------- /docs/recipes/WritingTests.md: -------------------------------------------------------------------------------- 1 | # 编写测试 2 | 3 | 因为你写的大部分 Redux 代码都是些函数,而且大部分是纯函数,所以很好测,不需要 mock。 4 | 5 | ### 设置 6 | 7 | 我们建议用 [Mocha](http://mochajs.org/) 作为测试引擎。 8 | 注意因为是在 node 环境下运行,所以你不能访问 DOM。 9 | 10 | ``` 11 | npm install --save-dev mocha 12 | ``` 13 | 14 | 想结合 [Babel](http://babeljs.io) 使用的话,在 `package.json` 的 `scripts` 里加入这一段: 15 | 16 | ```js 17 | { 18 | ... 19 | "scripts": { 20 | ... 21 | "test": "mocha --compilers js:babel/register --recursive", 22 | "test:watch": "npm test -- --watch", 23 | }, 24 | ... 25 | } 26 | ``` 27 | 28 | 然后运行 `npm test` 就能单次运行了,或者也可以使用 `npm run test:watch` 在每次有文件改变时自动执行测试。 29 | 30 | ### Action Creators 31 | 32 | Redux 里的 action creators 是会返回普通对象的函数。在测试 action creators 的时候我们想要测试不仅是调用了正确的 action creator,还有是否返回了正确的 action。 33 | 34 | #### 示例 35 | 36 | ```js 37 | export function addTodo(text) { 38 | return { 39 | type: 'ADD_TODO', 40 | text 41 | }; 42 | } 43 | ``` 44 | 可以这么测: 45 | 46 | ```js 47 | import expect from 'expect'; 48 | import * as actions from '../../actions/TodoActions'; 49 | import * as types from '../../constants/ActionTypes'; 50 | 51 | describe('actions', () => { 52 | it('should create an action to add a todo', () => { 53 | const text = 'Finish docs'; 54 | const expectedAction = { 55 | type: types.ADD_TODO, 56 | text 57 | }; 58 | expect(actions.addTodo(text)).toEqual(expectedAction); 59 | }); 60 | } 61 | ``` 62 | 63 | ### Reducers 64 | 65 | Reducer 应该是把 action 应用到之前的 state,并返回新的 state。测试起来是下面这样的。 66 | 67 | #### 示例 68 | 69 | ```js 70 | import { ADD_TODO } from '../constants/ActionTypes'; 71 | 72 | const initialState = [{ 73 | text: 'Use Redux', 74 | completed: false, 75 | id: 0 76 | }]; 77 | 78 | export default function todos(state = initialState, action) { 79 | switch (action.type) { 80 | case ADD_TODO: 81 | return [{ 82 | id: (state.length === 0) ? 0 : state[0].id + 1, 83 | completed: false, 84 | text: action.text 85 | }, ...state]; 86 | 87 | default: 88 | return state; 89 | } 90 | } 91 | ``` 92 | 可以这么测: 93 | 94 | ```js 95 | import expect from 'expect'; 96 | import reducer from '../../reducers/todos'; 97 | import * as types from '../../constants/ActionTypes'; 98 | 99 | describe('todos reducer', () => { 100 | it('should return the initial state', () => { 101 | expect( 102 | reducer(undefined, {}) 103 | ).toEqual([{ 104 | text: 'Use Redux', 105 | completed: false, 106 | id: 0 107 | }]); 108 | }); 109 | 110 | it('should handle ADD_TODO', () => { 111 | expect( 112 | reducer([], { 113 | type: types.ADD_TODO, 114 | text: 'Run the tests' 115 | }) 116 | ).toEqual([{ 117 | text: 'Run the tests', 118 | completed: false, 119 | id: 0 120 | }]); 121 | 122 | expect( 123 | reducer([{ 124 | text: 'Use Redux', 125 | completed: false, 126 | id: 0 127 | }], { 128 | type: types.ADD_TODO, 129 | text: 'Run the tests' 130 | }) 131 | ).toEqual([{ 132 | text: 'Run the tests', 133 | completed: false, 134 | id: 1 135 | }, { 136 | text: 'Use Redux', 137 | completed: false, 138 | id: 0 139 | }]); 140 | }); 141 | ``` 142 | 143 | ### Components 144 | 145 | React components 有一点好,就是他们一般都很小而且依赖于他们的 props。所以很好测。 146 | 147 | 要测 components 我们要建一个叫 `setup()` 的辅助方法,用来把模拟过的(stubbed)回调函数当作 props 来传入,然后使用 [React 浅渲染](https://facebook.github.io/react/docs/test-utils.html#shallow-rendering) 来渲染组件。这样就可以通过做 “是否调用了回调函数” 这样的断言来写独立的测试。 148 | 149 | #### 示例 150 | 151 | ```js 152 | import React, { PropTypes, Component } from 'react'; 153 | import TodoTextInput from './TodoTextInput'; 154 | 155 | class Header extends Component { 156 | handleSave(text) { 157 | if (text.length !== 0) { 158 | this.props.addTodo(text); 159 | } 160 | } 161 | 162 | render() { 163 | return ( 164 |
165 |

todos

166 | 169 |
170 | ); 171 | } 172 | } 173 | 174 | Header.propTypes = { 175 | addTodo: PropTypes.func.isRequired 176 | }; 177 | 178 | export default Header; 179 | ``` 180 | 181 | 可以这么测: 182 | 183 | ```js 184 | import expect from 'expect'; 185 | import jsdomReact from '../jsdomReact'; 186 | import React from 'react/addons'; 187 | import Header from '../../components/Header'; 188 | import TodoTextInput from '../../components/TodoTextInput'; 189 | 190 | const { TestUtils } = React.addons; 191 | 192 | function setup() { 193 | let props = { 194 | addTodo: expect.createSpy() 195 | }; 196 | 197 | let renderer = TestUtils.createRenderer(); 198 | renderer.render(
); 199 | let output = renderer.getRenderOutput(); 200 | 201 | return { 202 | props: props, 203 | output: output, 204 | renderer: renderer 205 | }; 206 | } 207 | 208 | describe('components', () => { 209 | jsdomReact(); 210 | 211 | describe('Header', () => { 212 | it('should render correctly', () => { 213 | const { output } = setup(); 214 | 215 | expect(output.type).toBe('header'); 216 | expect(output.props.className).toBe('header'); 217 | 218 | let [h1, input] = output.props.children; 219 | 220 | expect(h1.type).toBe('h1'); 221 | expect(h1.props.children).toBe('todos'); 222 | 223 | expect(input.type).toBe(TodoTextInput); 224 | expect(input.props.newTodo).toBe(true); 225 | expect(input.props.placeholder).toBe('What needs to be done?'); 226 | }); 227 | 228 | it('should call call addTodo if length of text is greater than 0', () => { 229 | const { output, props } = setup(); 230 | let input = output.props.children[1]; 231 | input.props.onSave(''); 232 | expect(props.addTodo.calls.length).toBe(0); 233 | input.props.onSave('Use Redux'); 234 | expect(props.addTodo.calls.length).toBe(1); 235 | }); 236 | }); 237 | }); 238 | ``` 239 | 240 | #### `setState()` 异常修复 241 | 242 | 浅渲染目前的问题是 [如果调用 `setState` 便抛异常](https://github.com/facebook/react/issues/4019). React 貌似想要的是,如果想要使用 `setState`,DOM 就一定要存在(但测试运行在 node 环境下,是没有 DOM 的)。要解决这个问题,我们用了 jsdom,为了在 DOM 无效的时候,React 也不抛异常。按下面方法设置它: 243 | 244 | ``` 245 | npm install --save-dev jsdom mocha-jsdom 246 | ``` 247 | 248 | 然后添加 `jsdomReact()` 帮助函数,是这样的: 249 | 250 | ```js 251 | import ExecutionEnvironment from 'react/lib/ExecutionEnvironment'; 252 | import jsdom from 'mocha-jsdom'; 253 | 254 | export default function jsdomReact() { 255 | jsdom(); 256 | ExecutionEnvironment.canUseDOM = true; 257 | } 258 | ``` 259 | 260 | 要在运行任何的 component 测试之前调用。注意这么做不优雅,等以后 [facebook/react#4019](https://github.com/facebook/react/issues/4019) 解决了之后,这段代码就可以删除了。 261 | 262 | ### 词汇表 263 | 264 | - [React Test Utils](http://facebook.github.io/react/docs/test-utils.html): 跟 React 一块来的测试小助手。 265 | 266 | - [jsdom](https://github.com/tmpvar/jsdom): 一个 JavaScript 的内建 DOM 。Jsdom 允许没浏览器的时候也能跑测试。 267 | 268 | - [浅渲染(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 还能测试是否工作正常。 269 | -------------------------------------------------------------------------------- /docs/basics/ExampleTodoList.md: -------------------------------------------------------------------------------- 1 | # 示例: Todo 列表 2 | 3 | 这是我们在[基础教程](./README.md)里开发的迷你型的任务管理应用的完整源码。 4 | 5 | ## 入口文件 6 | 7 | #### `index.js` 8 | 9 | ```js 10 | import React from 'react'; 11 | import { createStore } from 'redux'; 12 | import { Provider } from 'react-redux'; 13 | import App from './containers/App'; 14 | import todoApp from './reducers'; 15 | 16 | let store = createStore(todoApp); 17 | 18 | let rootElement = document.getElementById('root'); 19 | React.render( 20 | // 为了解决 React 0.13 的问题, 21 | // 一定要把 child 用函数包起来。 22 | 23 | {() => } 24 | , 25 | rootElement 26 | ); 27 | ``` 28 | 29 | ## Action 创建函数和常量 30 | 31 | #### `actions.js` 32 | 33 | ```js 34 | /* 35 | * action types 36 | */ 37 | 38 | export const ADD_TODO = 'ADD_TODO'; 39 | export const COMPLETE_TODO = 'COMPLETE_TODO'; 40 | export const SET_VISIBILITY_FILTER = 'SET_VISIBILITY_FILTER' 41 | 42 | /* 43 | * 其它的常量 44 | */ 45 | 46 | export const VisibilityFilters = { 47 | SHOW_ALL: 'SHOW_ALL', 48 | SHOW_COMPLETED: 'SHOW_COMPLETED', 49 | SHOW_ACTIVE: 'SHOW_ACTIVE' 50 | }; 51 | 52 | /* 53 | * action 创建函数 54 | */ 55 | 56 | export function addTodo(text) { 57 | return { type: ADD_TODO, text }; 58 | } 59 | 60 | export function completeTodo(index) { 61 | return { type: COMPLETE_TODO, index }; 62 | } 63 | 64 | export function setVisibilityFilter(filter) { 65 | return { type: SET_VISIBILITY_FILTER, filter }; 66 | } 67 | ``` 68 | 69 | ## Reducers 70 | 71 | #### `reducers.js` 72 | 73 | ```js 74 | import { combineReducers } from 'redux'; 75 | import { ADD_TODO, COMPLETE_TODO, SET_VISIBILITY_FILTER, VisibilityFilters } from './actions'; 76 | const { SHOW_ALL } = VisibilityFilters; 77 | 78 | function visibilityFilter(state = SHOW_ALL, action) { 79 | switch (action.type) { 80 | case SET_VISIBILITY_FILTER: 81 | return action.filter; 82 | default: 83 | return state; 84 | } 85 | } 86 | 87 | function todos(state = [], action) { 88 | switch (action.type) { 89 | case ADD_TODO: 90 | return [...state, { 91 | text: action.text, 92 | completed: false 93 | }]; 94 | case COMPLETE_TODO: 95 | return [ 96 | ...state.slice(0, action.index), 97 | Object.assign({}, state[action.index], { 98 | completed: true 99 | }), 100 | ...state.slice(action.index + 1) 101 | ]; 102 | default: 103 | return state; 104 | } 105 | } 106 | 107 | const todoApp = combineReducers({ 108 | visibilityFilter, 109 | todos 110 | }); 111 | 112 | export default todoApp; 113 | ``` 114 | 115 | ## 智能组件 116 | 117 | #### `containers/App.js` 118 | 119 | ```js 120 | import React, { Component, PropTypes } from 'react'; 121 | import { connect } from 'react-redux'; 122 | import { addTodo, completeTodo, setVisibilityFilter, VisibilityFilters } from '../actions'; 123 | import AddTodo from '../components/AddTodo'; 124 | import TodoList from '../components/TodoList'; 125 | import Footer from '../components/Footer'; 126 | 127 | class App extends Component { 128 | render() { 129 | // Injected by connect() call: 130 | const { dispatch, visibleTodos, visibilityFilter } = this.props; 131 | return ( 132 |
133 | 135 | dispatch(addTodo(text)) 136 | } /> 137 | 140 | dispatch(completeTodo(index)) 141 | } /> 142 |
145 | dispatch(setVisibilityFilter(nextFilter)) 146 | } /> 147 |
148 | ); 149 | } 150 | } 151 | 152 | App.propTypes = { 153 | visibleTodos: PropTypes.arrayOf(PropTypes.shape({ 154 | text: PropTypes.string.isRequired, 155 | completed: PropTypes.bool.isRequired 156 | })), 157 | visibilityFilter: PropTypes.oneOf([ 158 | 'SHOW_ALL', 159 | 'SHOW_COMPLETED', 160 | 'SHOW_ACTIVE' 161 | ]).isRequired 162 | }; 163 | 164 | function selectTodos(todos, filter) { 165 | switch (filter) { 166 | case VisibilityFilters.SHOW_ALL: 167 | return todos; 168 | case VisibilityFilters.SHOW_COMPLETED: 169 | return todos.filter(todo => todo.completed); 170 | case VisibilityFilters.SHOW_ACTIVE: 171 | return todos.filter(todo => !todo.completed); 172 | } 173 | } 174 | 175 | // Which props do we want to inject, given the global state? 176 | // Note: use https://github.com/faassen/reselect for better performance. 177 | function select(state) { 178 | return { 179 | visibleTodos: selectTodos(state.todos, state.visibilityFilter), 180 | visibilityFilter: state.visibilityFilter 181 | }; 182 | } 183 | 184 | // Wrap the component to inject dispatch and state into it 185 | export default connect(select)(App); 186 | ``` 187 | 188 | ## 笨拙组件 189 | 190 | #### `components/AddTodo.js` 191 | 192 | ```js 193 | import React, { findDOMNode, Component, PropTypes } from 'react'; 194 | 195 | export default class AddTodo extends Component { 196 | render() { 197 | return ( 198 |
199 | 200 | 203 |
204 | ); 205 | } 206 | 207 | handleClick(e) { 208 | const node = findDOMNode(this.refs.input); 209 | const text = node.value.trim(); 210 | this.props.onAddClick(text); 211 | node.value = ''; 212 | } 213 | } 214 | 215 | AddTodo.propTypes = { 216 | onAddClick: PropTypes.func.isRequired 217 | }; 218 | ``` 219 | 220 | #### `components/Footer.js` 221 | 222 | ```js 223 | import React, { Component, PropTypes } from 'react'; 224 | 225 | export default class Footer extends Component { 226 | renderFilter(filter, name) { 227 | if (filter === this.props.filter) { 228 | return name; 229 | } 230 | 231 | return ( 232 | { 233 | e.preventDefault(); 234 | this.props.onFilterChange(filter); 235 | }}> 236 | {name} 237 | 238 | ); 239 | } 240 | 241 | render() { 242 | return ( 243 |

244 | Show: 245 | {' '} 246 | {this.renderFilter('SHOW_ALL', 'All')} 247 | {', '} 248 | {this.renderFilter('SHOW_COMPLETED', 'Completed')} 249 | {', '} 250 | {this.renderFilter('SHOW_ACTIVE', 'Active')} 251 | . 252 |

253 | ); 254 | } 255 | } 256 | 257 | Footer.propTypes = { 258 | onFilterChange: PropTypes.func.isRequired, 259 | filter: PropTypes.oneOf([ 260 | 'SHOW_ALL', 261 | 'SHOW_COMPLETED', 262 | 'SHOW_ACTIVE' 263 | ]).isRequired 264 | }; 265 | ``` 266 | 267 | #### `components/Todo.js` 268 | 269 | ```js 270 | import React, { Component, PropTypes } from 'react'; 271 | 272 | export default class Todo extends Component { 273 | render() { 274 | return ( 275 |
  • 281 | {this.props.text} 282 |
  • 283 | ); 284 | } 285 | } 286 | 287 | Todo.propTypes = { 288 | onClick: PropTypes.func.isRequired, 289 | text: PropTypes.string.isRequired, 290 | completed: PropTypes.bool.isRequired 291 | }; 292 | ``` 293 | 294 | #### `components/TodoList.js` 295 | 296 | ```js 297 | import React, { Component, PropTypes } from 'react'; 298 | import Todo from './Todo'; 299 | 300 | export default class TodoList extends Component { 301 | render() { 302 | return ( 303 |
      304 | {this.props.todos.map((todo, index) => 305 | this.props.onTodoClick(index)} /> 308 | )} 309 |
    310 | ); 311 | } 312 | } 313 | 314 | TodoList.propTypes = { 315 | onTodoClick: PropTypes.func.isRequired, 316 | todos: PropTypes.arrayOf(PropTypes.shape({ 317 | text: PropTypes.string.isRequired, 318 | completed: PropTypes.bool.isRequired 319 | }).isRequired).isRequired 320 | }; 321 | ``` 322 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [Redux 中文文档](http://github.com/camsong/redux-in-chinese) 2 | 3 | 在线 Gitbook 地址:http://camsong.github.io/redux-in-chinese/ 4 | 5 | 英文原版:http://rackt.github.io/redux/ 6 | 7 | **翻译正在进行中,[加入我们](#加入翻译)** 8 | 9 | Redux 是 JavaScript 状态容器,提供可预测化的状态管理。 10 | 11 | 可以让你构建一致化的应用,运行于不同的环境(客户端、服务器、原生应用),并且易于测试。不仅于此,它还提供 12 | 超爽的开发体验,比如有一个[时间旅行调试器可以编辑后实时预览](https://github.com/gaearon/redux-devtools)。 13 | 14 | Redux 除了和 [React](https://facebook.github.io/react/) 一起用外,还支持其它界面库。 15 | 它体小精悍(只有2kB)且没有任何依赖。 16 | 17 | ### 评价 18 | 19 | >[“Love what you’re doing with Redux”](https://twitter.com/jingc/status/616608251463909376) 20 | >Jing Chen,Flux 作者 21 | 22 | >[“I asked for comments on Redux in FB's internal JS discussion group, and it was universally praised. Really awesome work.”](https://twitter.com/fisherwebdev/status/616286955693682688) 23 | >Bill Fisher,Flux 作者 24 | 25 | >[“It's cool that you are inventing a better Flux by not doing Flux at all.”](https://twitter.com/andrestaltz/status/616271392930201604) 26 | >André Staltz,Cycle 作者 27 | 28 | ### 开发经历 29 | 30 | Redux 的开发最早开始于我在准备 React Europe 演讲[热加载与时间旅行](https://www.youtube.com/watch?v=xsSnOQynTHs)的时候,当初的目标是创建一个状态管理库,来提供最简化 API,但同时做到行为的完全可预测,因此才得以实现日志打印,热加载,时间旅行,同构应用,录制和重放,而不需要任何开发参与。 31 | 32 | ### 启示 33 | 34 | Redux 由 [Flux](http://facebook.github.io/flux/) 演变而来,但受 [Elm](http://elm-lang.org/guide/architecture) 的启发,避开了 Flux 的复杂性。 35 | 不管你有没有使用过它们,只需几分钟就能上手 Redux。 36 | 37 | ### 安装 38 | 39 | 安装稳定版: 40 | 41 | ``` 42 | npm install --save redux 43 | ``` 44 | 45 | 多数情况下,你还需要使用 [React 绑定库](http://github.com/gaearon/react-redux)和[开发者工具](http://github.com/gaearon/redux-devtools)。 46 | 47 | ``` 48 | npm install --save react-redux 49 | npm install --save-dev redux-devtools 50 | ``` 51 | 52 | ### 要点 53 | 54 | 应用中所有的 state 都以一个对象树的形式储存在一个单一的 *store* 中。 55 | 惟一改变 state 的办法是触发 *action*,一个描述发生什么的对象。 56 | 为了描述 action 如何改变 state 树,你需要编写 *reducers*。 57 | 58 | 就是这样! 59 | 60 | ```js 61 | import { createStore } from 'redux'; 62 | 63 | /** 64 | * 这是一个 reducer,形式为 (state, action) => state 的纯函数。 65 | * 描述了 action 如何把 state 转变成下一个 state。 66 | * 67 | * state 的形式取决于你,可以是基本类型、数组、对象、 68 | * 甚至是 Immutable.js 生成的数据结构。惟一的要点是 69 | * 当 state 变化时需要返回全新的对象,而不是修改传入的参数。 70 | * 71 | * 下面例子使用 `switch` 语句和字符串来做判断,但你可以写帮助类(helper) 72 | * 根据不同的约定(如方法映射)来判断,只要适用你的项目即可。 73 | */ 74 | function counter(state = 0, action) { 75 | switch (action.type) { 76 | case 'INCREMENT': 77 | return state + 1; 78 | case 'DECREMENT': 79 | return state - 1; 80 | default: 81 | return state; 82 | } 83 | } 84 | 85 | // 创建 Redux store 来存放应用的状态。 86 | // API 是 { subscribe, dispatch, getState }。 87 | let store = createStore(counter); 88 | 89 | // 可以手动订阅更新,也可以事件绑定到视图层。 90 | store.subscribe(() => 91 | console.log(store.getState()) 92 | ); 93 | 94 | // 改变内部 state 惟一方法是 dispatch 一个 action。 95 | // action 可以被序列化,用日记记录和储存下来,后期还可以以回放的方式执行 96 | store.dispatch({ type: 'INCREMENT' }); 97 | // 1 98 | store.dispatch({ type: 'INCREMENT' }); 99 | // 2 100 | store.dispatch({ type: 'DECREMENT' }); 101 | // 1 102 | ``` 103 | 你应该把要做的修改变成一个普通对象,这个对象被叫做 *action*,而不是直接修改 state。然后编写专门的函数来决定每个 action 如何改变应用的 state,这个函数被叫做 *reducer*。 104 | 105 | 如果你以前使用 Flux,那么你只需要注意一个重要的区别。Redux 没有 Dispatcher 且不支持多个 store。相反,只有一个单一的 store 和一个根级的 reduce 函数(reducer)。随着应用不断变大,你应该把根级的 reducer 拆成多个小的 reducers,分别独立地操作 state 树的不同部分,而不是添加新的 stores。这就像一个 React 应用只有一个根级的组件,这个根组件又由很多小组件构成。 106 | 107 | 用这个架构开发计数器有点杀鸡用牛刀,但它的美在于做复杂应用和庞大系统时优秀的扩展能力。由于它可以用 action 追溯应用的每一次修改,因此才有强大的开发工具。如录制用户会话并回放所有 action 来重现它。 108 | 109 | ### 文档 110 | 111 | * [介绍](http://camsong.github.io/redux-in-chinese//docs/introduction/index.html) 112 | * [基础](http://camsong.github.io/redux-in-chinese//docs/basics/index.html) 113 | * [高级](http://camsong.github.io/redux-in-chinese//docs/advanced/index.html) 114 | * [技巧](http://camsong.github.io/redux-in-chinese//docs/recipes/index.html) 115 | * [排错](http://camsong.github.io/redux-in-chinese//docs/Troubleshooting.html) 116 | * [词汇表](http://camsong.github.io/redux-in-chinese//docs/Glossary.html) 117 | * [API 文档](http://camsong.github.io/redux-in-chinese//docs/api/index.html) 118 | 119 | ### 示例 120 | 121 | * [Counter](http://camsong.github.io/redux-in-chinese//docs/introduction/Examples.html#counter) ([source](https://github.com/rackt/redux/tree/master/examples/counter)) 122 | * [TodoMVC](http://camsong.github.io/redux-in-chinese//docs/introduction/Examples.html#todomvc) ([source](https://github.com/rackt/redux/tree/master/examples/todomvc)) 123 | * [Async](http://camsong.github.io/redux-in-chinese//docs/introduction/Examples.html#async) ([source](https://github.com/rackt/redux/tree/master/examples/async)) 124 | * [Real World](http://camsong.github.io/redux-in-chinese//docs/introduction/Examples.html#real-world) ([source](https://github.com/rackt/redux/tree/master/examples/real-world)) 125 | 126 | 如果你是 NPM 新手,创建和运行一个新的项目有难度,或者不知道上面的代码应该放到哪里使用,请下载 [simplest-redux-example](https://github.com/jackielii/simplest-redux-example) 这个示例,它是一个集成了 React、Browserify 和 Redux 的最简化的示例项目。 127 | 128 | ### 交流 129 | 130 | 打开 Slack,加入 [Reactiflux](http://reactiflux.com/) 中的 **#redux** 频道。 131 | 132 | ### 感谢 133 | 134 | * [Elm 架构](https://github.com/evancz/elm-architecture-tutorial) 介绍了使用 reducers 来操作 state 数据; 135 | * [Turning the database inside-out](http://blog.confluent.io/2015/03/04/turning-the-database-inside-out-with-apache-samza/) 大开脑洞; 136 | * [ClojureScript 里使用 Figwheel](http://www.youtube.com/watch?v=j-kj2qwJa_E) for convincing me that re-evaluation should “just work”; 137 | * [Webpack](https://github.com/webpack/docs/wiki/hot-module-replacement-with-webpack) 热模块替换; 138 | * [Flummox](https://github.com/acdlite/flummox) 教我在 Flux 里去掉样板文件和单例对象; 139 | * [disto](https://github.com/threepointone/disto) 演示使用热加载 Stores 的可行性; 140 | * [NuclearJS](https://github.com/optimizely/nuclear-js) 证明这样的架构性能可以很好; 141 | * [Om](https://github.com/omcljs/om) 普及 state 惟一原子化的思想。 142 | * [Cycle](https://github.com/staltz/cycle) 介绍了 function 是如何在很多场景都是最好的工具; 143 | * [React](https://github.com/facebook/react) 实践启迪。 144 | 145 | 146 | ## 加入翻译 147 | 148 | 非常感谢你的关注,如果你也想为 Redux 推广贡献一份力量,欢迎加入我们一起翻译。目前翻译工作还没有完成,校对工作更是才刚刚开始。你可以按照以下的步骤进行认领提交。在认领之前,你需要先到 fork 一份代码,建议使用 Markdown 类编辑器开启横向双列模式, 149 | 150 | ### 认领制度 151 | 152 | 你应该在翻译之前进行翻译认领来保证没有人和你重复工作。很简单,只要在 contributes.md 中找到或者加入你想翻译的文章的资料, 153 | 154 | 加入: 155 | 156 | ``` 157 | work_in_progress: true 158 | contributor: +your_github_acount+ 159 | ``` 160 | 161 | 之后,发个 pull request 过来就代表你成功的占到了一个坑,你可以开始慢慢的翻译自己的文章了。 162 | 163 | 你可以在 contributor 一栏中加入自己的 github 用户名,以便其他人可以联系到你。另外如果你占坑太久你的占位可能会被取消。尽量在半个月内完成你的工作。 164 | 165 | ### 翻译约定 166 | 167 | * 专有名词保持大写:HTML, HAML, SASS, REST... 等等。 168 | * 英文和数字与中文之间要留空格。中文标点符号和中文之间不需要留空格。 169 | * 使用中文的标点符号。句号是 `。` 不是 `.`,破折号是 `——` 不是 `-`。 170 | * 英文有斜体、中文没有,可用强调取代 `**强调**`。 171 | * 译文和原文行数应保持一致,以便于后期同步更新。 172 | * 译完自己读一次,看看像不像中文。 173 | * 遇到不确定的翻译很正常,请在 Pull Request 里指出,大家一起解决。 174 | 175 | ### 约定翻译的名词 176 | 177 | 为了免除误解,这些词第一次出现时可以用 `()` 来显示原文。 178 | 179 | 英文 | 中文 180 | ------------ | ------------- 181 | plain object | 普通对象 182 | manage | 管理 183 | compose | 组合 184 | action creator | action 创建函数 185 | dispatch | 发起 186 | note | 注意/须知 187 | hold | 维持 188 | state shape | state 结构 189 | handle | 处理 190 | boilerplate | 样板代码 191 | normalized | 范式化 192 | function | 函数? 193 | composition | 合成 194 | helper utility | 辅助工具 195 | this won’t work | 这样做行不通 196 | this will work | 这样做行得通 197 | tips | 小贴士 198 | create | 创建 199 | flag | 标记位 200 | 201 | ### 保留不译的名词 202 | 203 | 前端开发常用的专有名词,在不造成读者理解困难的情况下,尽量保持原汁原味。 204 | 205 | 英文 | 说明 206 | ----- | ------ 207 | action | 动作 208 | reducer | - 209 | store | - 210 | middleware | 中间件 211 | dispatcher | 分发器 212 | state | 状态 213 | state tree | 状态树 214 | props | 属性 215 | UI | 用户界面 216 | monkeypatch | - 217 | currying | 柯里化 218 | 219 | ### 与原文同步机制 220 | 221 | TBD 222 | 223 | ### 翻译流程 224 | 225 | 一、fork 本 repo 226 | 227 | 二、如果是新翻译章节,应参照对应的[原文](https://github.com/rackt/redux/tree/master/docs)进行翻译;如果是校对则直接修改 228 | 229 | 三、翻译时启动 watch 来实时看结果 230 | ``` 231 | npm run watch 232 | ``` 233 | 打开:[localhost:4000](http://localhost:4000) 234 | 235 | 四、提交并发 Pull Request 236 | 237 | 五、fork 后的 repo 如何同步本 repo? 238 | 239 | ``` 240 | // 添加 upstream 源,只需执行一次 241 | git remote add upstream git@github.com:camsong/redux-in-chinese.git 242 | // 拉取远程代码 243 | git pull upstream master 244 | // 更新 fork 仓库 245 | git push origin master 246 | ``` 247 | 248 | 更多参考:https://help.github.com/articles/syncing-a-fork/ 249 | 250 | ### 建议与反馈 251 | 欢迎任何建议!直接开一个 github issues 就可以了。 252 | -------------------------------------------------------------------------------- /docs/api/applyMiddleware.md: -------------------------------------------------------------------------------- 1 | # `applyMiddleware(...middlewares)` 2 | 3 | 使用包含自定义功能的 middleware 来扩展 Redux 是一种推荐的方式。Middleware 可以让你包装 store 的 [`dispatch`](Store.md#dispatch) 方法来达到你想要的目的。同时, middleware 还拥有“可组合”这一关键特性。多个 middleware 可以被组合到一起使用,形成 middleware 链。其中,每个 middleware 都不需要关心链中它前后的 middleware 的任何信息。 4 | 5 | Middleware 最常见的使用场景是无需引用大量代码或依赖类似 [Rx](https://github.com/Reactive-Extensions/RxJS) 的第三方库实现异步 actions。这种方式可以让你像 dispatch 一般的 actions 那样 dispatch [异步 actions](../Glossary.md#async-action)。 6 | 7 | 例如,[redux-thunk](https://github.com/gaearon/redux-thunk) 支持 dispatch function,以此让 action creator 控制反转。被 dispatch 的 function 会接收 [`dispatch`](Store.md#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。 8 | 9 | Middleware 并不需要和 [`createStore`](createStore.md) 绑在一起使用,也不是 Redux 架构的基础组成部分,但它带来的益处让我们认为有必要在 Redux 核心中包含对它的支持。因此,虽然不同的 middleware 可能在易用性和用法上有所不同,它仍被作为扩展 [`dispatch`](Store.md#dispatch) 的唯一标准的方式。 10 | 11 | #### 参数 12 | 13 | * `...middlewares` (*arguments*): 遵循 Redux *middleware API* 的函数。每个 middleware 接受 [`Store`](Store.md) 的 [`dispatch`](Store.md#dispatch) 和 [`getState`](Store.md#getState) 函数作为命名参数,并返回一个函数。该函数会被传入 14 | 被称为 `next` 的下一个 middleware 的 dispatch 方法,并返回一个接收 action 的新函数,这个函数可以直接调用 `next(action)`,或者在其他需要的时刻调用,甚至根本不去调用它。调用链中最后一个 middleware 会接受真实的 store 的 [`dispatch`](Store.md#dispatch) 方法作为 `next` 参数,并借此结束调用链。所以,middleware 的函数签名是 `({ getState, dispatch }) => next => action`。 15 | 16 | #### 返回值 17 | 18 | (*Function*) 一个应用了 middleware 后的 store enhancer。这个 store enhancer 就是一个函数,并且需要应用到 `createStore`。它会返回一个应用了 middleware 的新的 `createStore`。 19 | 20 | #### 示例: 自定义 Logger Middleware 21 | 22 | ```js 23 | import { createStore, applyMiddleware } from 'redux'; 24 | import todos from './reducers'; 25 | 26 | function logger({ getState }) { 27 | return (next) => (action) => { 28 | console.log('will dispatch', action); 29 | 30 | // 调用 middleware 链中下一个 middleware 的 dispatch。 31 | let returnValue = next(action); 32 | 33 | console.log('state after dispatch', getState()); 34 | 35 | // 一般会是 action 本身,除非 36 | // 后面的 middleware 修改了它。 37 | return returnValue; 38 | }; 39 | } 40 | 41 | let createStoreWithMiddleware = applyMiddleware(logger)(createStore); 42 | let store = createStoreWithMiddleware(todos, ['Use Redux']); 43 | 44 | store.dispatch({ 45 | type: 'ADD_TODO', 46 | text: 'Understand the middleware' 47 | }); 48 | // (将打印如下信息:) 49 | // will dispatch: { type: 'ADD_TODO', text: 'Understand the middleware' } 50 | // state after dispatch: ['Use Redux', 'Understand the middleware'] 51 | ``` 52 | 53 | #### 示例: 使用 Thunk Middleware 来做异步 Action 54 | 55 | ```js 56 | import { createStore, combineReducers, applyMiddleware } from 'redux'; 57 | import thunk from 'redux-thunk'; 58 | import * as reducers from './reducers'; 59 | 60 | // 调用 applyMiddleware,使用 middleware 增强 createStore: 61 | let createStoreWithMiddleware = applyMiddleware(thunk)(createStore); 62 | 63 | // 像原生 createStore 一样使用。 64 | let reducer = combineReducers(reducers); 65 | let store = createStoreWithMiddleware(reducer); 66 | 67 | function fetchSecretSauce() { 68 | return fetch('https://www.google.com/search?q=secret+sauce'); 69 | } 70 | 71 | // 这些是你已熟悉的普通 action creator。 72 | // 它们返回的 action 不需要任何 middleware 就能被 dispatch。 73 | // 但是,他们只表达「事实」,并不表达「异步数据流」 74 | 75 | function makeASandwich(forPerson, secretSauce) { 76 | return { 77 | type: 'MAKE_SANDWICH', 78 | forPerson, 79 | secretSauce 80 | }; 81 | } 82 | 83 | function apologize(fromPerson, toPerson, error) { 84 | return { 85 | type: 'APOLOGIZE', 86 | fromPerson, 87 | toPerson, 88 | error 89 | }; 90 | } 91 | 92 | function withdrawMoney(amount) { 93 | return { 94 | type: 'WITHDRAW', 95 | amount 96 | }; 97 | } 98 | 99 | // 即使不使用 middleware,你也可以 dispatch action: 100 | store.dispatch(withdrawMoney(100)); 101 | 102 | // 但是怎样处理异步 action 呢, 103 | // 比如 API 调用,或者是路由跳转? 104 | 105 | // 来看一下 thunk。 106 | // Thunk 就是一个返回函数的函数。 107 | // 下面就是一个 thunk。 108 | 109 | function makeASandwichWithSecretSauce(forPerson) { 110 | 111 | // 控制反转! 112 | // 返回一个接收 `dispatch` 的函数。 113 | // Thunk middleware 知道如何把异步的 thunk action 转为普通 action。 114 | 115 | return function (dispatch) { 116 | return fetchSecretSauce().then( 117 | sauce => dispatch(makeASandwich(forPerson, sauce)), 118 | error => dispatch(apologize('The Sandwich Shop', forPerson, error)) 119 | ); 120 | }; 121 | } 122 | 123 | // Thunk middleware 可以让我们像 dispatch 普通 action 124 | // 一样 dispatch 异步的 thunk action。 125 | 126 | store.dispatch( 127 | makeASandwichWithSecretSauce('Me') 128 | ); 129 | 130 | // 它甚至负责回传 thunk 被 dispatch 后返回的值, 131 | // 所以可以继续串连 Promise,调用它的 .then() 方法。 132 | 133 | store.dispatch( 134 | makeASandwichWithSecretSauce('My wife') 135 | ).then(() => { 136 | console.log('Done!'); 137 | }); 138 | 139 | // 实际上,可以写一个 dispatch 其它 action creator 里 140 | // 普通 action 和异步 action 的 action creator, 141 | // 而且可以使用 Promise 来控制数据流。 142 | 143 | function makeSandwichesForEverybody() { 144 | return function (dispatch, getState) { 145 | if (!getState().sandwiches.isShopOpen) { 146 | 147 | // 返回 Promise 并不是必须的,但这是一个很好的约定, 148 | // 为了让调用者能够在异步的 dispatch 结果上直接调用 .then() 方法。 149 | 150 | return Promise.resolve(); 151 | } 152 | 153 | // 可以 dispatch 普通 action 对象和其它 thunk, 154 | // 这样我们就可以在一个数据流中组合多个异步 action。 155 | 156 | return dispatch( 157 | makeASandwichWithSecretSauce('My Grandma') 158 | ).then(() => 159 | Promise.all([ 160 | dispatch(makeASandwichWithSecretSauce('Me')), 161 | dispatch(makeASandwichWithSecretSauce('My wife')) 162 | ]) 163 | ).then(() => 164 | dispatch(makeASandwichWithSecretSauce('Our kids')) 165 | ).then(() => 166 | dispatch(getState().myMoney > 42 ? 167 | withdrawMoney(42) : 168 | apologize('Me', 'The Sandwich Shop') 169 | ) 170 | ); 171 | }; 172 | } 173 | 174 | // 这在服务端渲染时很有用,因为我可以等到数据 175 | // 准备好后,同步的渲染应用。 176 | 177 | store.dispatch( 178 | makeSandwichesForEverybody() 179 | ).then(() => 180 | response.send(React.renderToString()) 181 | ); 182 | 183 | // 也可以在任何导致组件的 props 变化的时刻 184 | // dispatch 一个异步 thunk action。 185 | 186 | import { connect } from 'react-redux'; 187 | import { Component } from 'react'; 188 | 189 | class SandwichShop extends Component { 190 | componentDidMount() { 191 | this.props.dispatch( 192 | makeASandwichWithSecretSauce(this.props.forPerson) 193 | ); 194 | } 195 | 196 | componentWillReceiveProps(nextProps) { 197 | if (nextProps.forPerson !== this.props.forPerson) { 198 | this.props.dispatch( 199 | makeASandwichWithSecretSauce(nextProps.forPerson) 200 | ); 201 | } 202 | } 203 | 204 | render() { 205 | return

    {this.props.sandwiches.join('mustard')}

    206 | } 207 | } 208 | 209 | export default connect( 210 | state => ({ 211 | sandwiches: state.sandwiches 212 | }) 213 | )(SandwichShop); 214 | ``` 215 | 216 | #### 小贴士 217 | 218 | * Middleware 只是包装了 store 的 [`dispatch`](Store.md#dispatch) 方法。技术上讲,任何 middleware 能做的事情,都可能通过手动包装 `dispatch` 调用来实现,但是放在同一个地方统一管理会使整个项目的扩展变的容易得多。 219 | 220 | * 如果除了 `applyMiddleware`,你还用了其它 store enhancer,一定要把 `applyMiddleware` 放到组合链的前面,因为 middleware 可能会包含异步操作。比如,它应该在 [redux-devtools](https://github.com/gaearon/redux-devtools) 前面,否则 DevTools 就看不到 Promise middleware 里 dispatch 的 action 了。 221 | 222 | * 如果你想有条件地使用 middleware,记住只 import 需要的部分: 223 | 224 | ```js 225 | let middleware = [a, b]; 226 | if (process.env.NODE_ENV !== 'production') { 227 | let c = require('some-debug-middleware'); 228 | let d = require('another-debug-middleware'); 229 | middleware = [...middleware, c, d]; 230 | } 231 | const createStoreWithMiddleware = applyMiddleware(...middleware)(createStore); 232 | ``` 233 | 234 | 这样做有利于打包时去掉不需要的模块,减小打包文件大小。 235 | 236 | * 有想过 `applyMiddleware` 本质是什么吗?它肯定是比 middleware 还强大的扩展机制。实际上,`applyMiddleware` 只是被称为 Redux 最强大的扩展机制的 [store enhancers](../Glossary.md#store-enhancer) 中的一个范例而已。你不太可能需要实现自己的 store enhancer。另一个 store enhancer 示例是 [redux-devtools](https://github.com/gaearon/redux-devtools)。Middleware 并没有 store enhancer 强大,但开发起来却是更容易的。 237 | 238 | * Middleware 听起来比实际难一些。真正理解 middleware 的唯一办法是了解现有的 middleware 是如何工作的,并尝试自己实现。需要的功能可能错综复杂,但是你会发现大部分 middleware 实际上很小,只有 10 行左右,是通过对它们的组合使用来达到最终的目的。 -------------------------------------------------------------------------------- /docs/advanced/ExampleRedditAPI.md: -------------------------------------------------------------------------------- 1 | # 示例:Reddit API 2 | 3 | 这是一个[高级教程](README.md)的例子,包含使用 Reddit API 请求文章标题的全部源码。 4 | 5 | ## 入口 6 | 7 | #### `index.js` 8 | 9 | ```js 10 | import 'babel-core/polyfill'; 11 | 12 | import React from 'react'; 13 | import Root from './containers/Root'; 14 | 15 | React.render( 16 | , 17 | document.getElementById('root') 18 | ); 19 | ``` 20 | 21 | ## Action Creators and Constants 22 | 23 | #### `actions.js` 24 | 25 | ```js 26 | import fetch from 'isomorphic-fetch'; 27 | 28 | export const REQUEST_POSTS = 'REQUEST_POSTS'; 29 | export const RECEIVE_POSTS = 'RECEIVE_POSTS'; 30 | export const SELECT_REDDIT = 'SELECT_REDDIT'; 31 | export const INVALIDATE_REDDIT = 'INVALIDATE_REDDIT'; 32 | 33 | export function selectReddit(reddit) { 34 | return { 35 | type: SELECT_REDDIT, 36 | reddit 37 | }; 38 | } 39 | 40 | export function invalidateReddit(reddit) { 41 | return { 42 | type: INVALIDATE_REDDIT, 43 | reddit 44 | }; 45 | } 46 | 47 | function requestPosts(reddit) { 48 | return { 49 | type: REQUEST_POSTS, 50 | reddit 51 | }; 52 | } 53 | 54 | function receivePosts(reddit, json) { 55 | return { 56 | type: RECEIVE_POSTS, 57 | reddit: reddit, 58 | posts: json.data.children.map(child => child.data), 59 | receivedAt: Date.now() 60 | }; 61 | } 62 | 63 | function fetchPosts(reddit) { 64 | return dispatch => { 65 | dispatch(requestPosts(reddit)); 66 | return fetch(`http://www.reddit.com/r/${reddit}.json`) 67 | .then(req => req.json()) 68 | .then(json => dispatch(receivePosts(reddit, json))); 69 | } 70 | } 71 | 72 | function shouldFetchPosts(state, reddit) { 73 | const posts = state.postsByReddit[reddit]; 74 | if (!posts) { 75 | return true; 76 | } else if (posts.isFetching) { 77 | return false; 78 | } else { 79 | return posts.didInvalidate; 80 | } 81 | } 82 | 83 | export function fetchPostsIfNeeded(reddit) { 84 | return (dispatch, getState) => { 85 | if (shouldFetchPosts(getState(), reddit)) { 86 | return dispatch(fetchPosts(reddit)); 87 | } 88 | }; 89 | } 90 | ``` 91 | 92 | ## Reducers 93 | 94 | #### `reducers.js` 95 | 96 | ```js 97 | import { combineReducers } from 'redux'; 98 | import { 99 | SELECT_REDDIT, INVALIDATE_REDDIT, 100 | REQUEST_POSTS, RECEIVE_POSTS 101 | } from '../actions'; 102 | 103 | function selectedReddit(state = 'reactjs', action) { 104 | switch (action.type) { 105 | case SELECT_REDDIT: 106 | return action.reddit; 107 | default: 108 | return state; 109 | } 110 | } 111 | 112 | function posts(state = { 113 | isFetching: false, 114 | didInvalidate: false, 115 | items: [] 116 | }, action) { 117 | switch (action.type) { 118 | case INVALIDATE_REDDIT: 119 | return Object.assign({}, state, { 120 | didInvalidate: true 121 | }); 122 | case REQUEST_POSTS: 123 | return Object.assign({}, state, { 124 | isFetching: true, 125 | didInvalidate: false 126 | }); 127 | case RECEIVE_POSTS: 128 | return Object.assign({}, state, { 129 | isFetching: false, 130 | didInvalidate: false, 131 | items: action.posts, 132 | lastUpdated: action.receivedAt 133 | }); 134 | default: 135 | return state; 136 | } 137 | } 138 | 139 | function postsByReddit(state = { }, action) { 140 | switch (action.type) { 141 | case INVALIDATE_REDDIT: 142 | case RECEIVE_POSTS: 143 | case REQUEST_POSTS: 144 | return Object.assign({}, state, { 145 | [action.reddit]: posts(state[action.reddit], action) 146 | }); 147 | default: 148 | return state; 149 | } 150 | } 151 | 152 | const rootReducer = combineReducers({ 153 | postsByReddit, 154 | selectedReddit 155 | }); 156 | 157 | export default rootReducer; 158 | ``` 159 | 160 | ## Store 161 | 162 | #### `configureStore.js` 163 | 164 | ```js 165 | import { createStore, applyMiddleware, combineReducers } from 'redux'; 166 | import thunkMiddleware from 'redux-thunk'; 167 | import loggerMiddleware from 'redux-logger'; 168 | import rootReducer from '../reducers'; 169 | 170 | const createStoreWithMiddleware = applyMiddleware( 171 | thunkMiddleware, 172 | loggerMiddleware 173 | )(createStore); 174 | 175 | export default function configureStore(initialState) { 176 | return createStoreWithMiddleware(rootReducer, initialState); 177 | } 178 | ``` 179 | 180 | ## 智能组件 181 | 182 | #### `containers/Root.js` 183 | 184 | ```js 185 | import React, { Component } from 'react'; 186 | import { Provider } from 'react-redux'; 187 | import configureStore from '../configureStore'; 188 | import AsyncApp from './AsyncApp'; 189 | 190 | const store = configureStore(); 191 | 192 | export default class Root extends Component { 193 | render() { 194 | return ( 195 | 196 | {() => } 197 | 198 | ); 199 | } 200 | } 201 | ``` 202 | 203 | #### `containers/AsyncApp.js` 204 | 205 | ```js 206 | import React, { Component, PropTypes } from 'react'; 207 | import { connect } from 'react-redux'; 208 | import { selectReddit, fetchPostsIfNeeded, invalidateReddit } from '../actions'; 209 | import Picker from '../components/Picker'; 210 | import Posts from '../components/Posts'; 211 | 212 | class AsyncApp extends Component { 213 | constructor(props) { 214 | super(props); 215 | this.handleChange = this.handleChange.bind(this); 216 | this.handleRefreshClick = this.handleRefreshClick.bind(this); 217 | } 218 | 219 | componentDidMount() { 220 | const { dispatch, selectedReddit } = this.props; 221 | dispatch(fetchPostsIfNeeded(selectedReddit)); 222 | } 223 | 224 | componentWillReceiveProps(nextProps) { 225 | if (nextProps.selectedReddit !== this.props.selectedReddit) { 226 | const { dispatch, selectedReddit } = nextProps; 227 | dispatch(fetchPostsIfNeeded(selectedReddit)); 228 | } 229 | } 230 | 231 | handleChange(nextReddit) { 232 | this.props.dispatch(selectReddit(nextReddit)); 233 | } 234 | 235 | handleRefreshClick(e) { 236 | e.preventDefault(); 237 | 238 | const { dispatch, selectedReddit } = this.props; 239 | dispatch(invalidateReddit(selectedReddit)); 240 | dispatch(fetchPostsIfNeeded(selectedReddit)); 241 | } 242 | 243 | render () { 244 | const { selectedReddit, posts, isFetching, lastUpdated } = this.props; 245 | return ( 246 |
    247 | 250 |

    251 | {lastUpdated && 252 | 253 | Last updated at {new Date(lastUpdated).toLocaleTimeString()}. 254 | {' '} 255 | 256 | } 257 | {!isFetching && 258 | 260 | Refresh 261 | 262 | } 263 |

    264 | {isFetching && posts.length === 0 && 265 |

    Loading...

    266 | } 267 | {!isFetching && posts.length === 0 && 268 |

    Empty.

    269 | } 270 | {posts.length > 0 && 271 |
    272 | 273 |
    274 | } 275 |
    276 | ); 277 | } 278 | } 279 | 280 | AsyncApp.propTypes = { 281 | selectedReddit: PropTypes.string.isRequired, 282 | posts: PropTypes.array.isRequired, 283 | isFetching: PropTypes.bool.isRequired, 284 | lastUpdated: PropTypes.number, 285 | dispatch: PropTypes.func.isRequired 286 | }; 287 | 288 | function mapStateToProps(state) { 289 | const { selectedReddit, postsByReddit } = state; 290 | const { 291 | isFetching, 292 | lastUpdated, 293 | items: posts 294 | } = postsByReddit[selectedReddit] || { 295 | isFetching: true, 296 | items: [] 297 | }; 298 | 299 | return { 300 | selectedReddit, 301 | posts, 302 | isFetching, 303 | lastUpdated 304 | }; 305 | } 306 | 307 | export default connect(mapStateToProps)(AsyncApp); 308 | ``` 309 | 310 | ## 笨拙组件 311 | 312 | #### `components/Picker.js` 313 | 314 | ```js 315 | import React, { Component, PropTypes } from 'react'; 316 | 317 | export default class Picker extends Component { 318 | render () { 319 | const { value, onChange, options } = this.props; 320 | 321 | return ( 322 | 323 |

    {value}

    324 | 332 |
    333 | ); 334 | } 335 | } 336 | 337 | Picker.propTypes = { 338 | options: PropTypes.arrayOf( 339 | PropTypes.string.isRequired 340 | ).isRequired, 341 | value: PropTypes.string.isRequired, 342 | onChange: PropTypes.func.isRequired 343 | }; 344 | ``` 345 | 346 | #### `components/Posts.js` 347 | 348 | ```js 349 | import React, { PropTypes, Component } from 'react'; 350 | 351 | export default class Posts extends Component { 352 | render () { 353 | return ( 354 |
      355 | {this.props.posts.map((post, i) => 356 |
    • {post.title}
    • 357 | )} 358 |
    359 | ); 360 | } 361 | } 362 | 363 | Posts.propTypes = { 364 | posts: PropTypes.array.isRequired 365 | }; 366 | ``` 367 | -------------------------------------------------------------------------------- /docs/recipes/ServerRendering.md: -------------------------------------------------------------------------------- 1 | # 服务端渲染 2 | 3 | 服务端渲染一个很常见的场景是当用户(或搜索引擎爬虫)第一次请求页面时,用它来做_初始渲染_。当服务器接收到请求后,它把需要的组件渲染成 HTML 字符串,然后把它返回给客户端(这里统指浏览器)。之后,客户端会接手渲染控制权。 4 | 5 | 下面我们使用 React 来做示例,对于支持服务端渲染的其它 view 框架,做法也是类似的。 6 | 7 | ### 服务端使用 Redux 8 | 9 | 当在服务器使用 Redux 渲染时,一定要在响应中包含应用的 state,这样客户端可以把它作为初始 state。这点至关重要,因为如果在生成 HTML 前预加载了数据,我们希望客户端也能访问这些数据。否则,客户端生成的 HTML 与服务器端返回的 HTML 就会不匹配,客户端还需要重新加载数据。 10 | 11 | 把数据发送到客户端,需要以下步骤: 12 | 13 | * 为每次请求创建全新的 Redux store 实例; 14 | * 按需 dispatch 一些 action; 15 | * 从 store 中取出 state; 16 | * 把 state 一同返回给客户端。 17 | 18 | 在客户端,使用服务器返回的 state 创建并初始化一个全新的 Redux store。 19 | Redux 在服务端**惟一**要做的事情就是,提供应用所需的**初始 state**。 20 | 21 | ## 安装 22 | 23 | 下面来介绍如何配置服务端渲染。使用极简的 [Counter 计数器应用](https://github.com/rackt/redux/tree/master/examples/counter) 来做示例,介绍如何根据请求在服务端提前渲染 state。 24 | 25 | ### 安装依赖库 26 | 27 | 本例会使用 [Express](http://expressjs.com/) 来做小型的 web 服务器。引入 [serve-static](https://www.npmjs.com/package/serve-static) middleware 来处理静态文件,稍后有代码。 28 | 29 | 还需要安装 Redux 对 React 的绑定库,Redux 默认并不包含。 30 | 31 | ``` 32 | npm install --save express serve-static react-redux 33 | ``` 34 | 35 | ## 服务端开发 36 | 37 | 下面是服务端代码大概的样子。使用 [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 函数。 38 | 39 | >##### 生产环境使用须知 40 | >在生产环境中,最好使用类似 nigix 这样的服务器来处理静态文件请求,只使用 Node 处理应用请求。虽然这个话题已经超出本教程讨论范畴。 41 | 42 | ##### `server.js` 43 | 44 | ```js 45 | import path from 'path'; 46 | import Express from 'express'; 47 | import React from 'react'; 48 | import { createStore } from 'redux'; 49 | import { Provider } from 'react-redux'; 50 | import counterApp from './reducers'; 51 | import App from './containers/App'; 52 | 53 | const app = Express(); 54 | const port = 3000; 55 | 56 | // 使用这个 middleware 处理 dist 目录下的静态文件请求 57 | app.use(require('serve-static')(path.join(__dirname, 'dist'))); 58 | 59 | // 每当收到请求时都会触发 60 | app.use(handleRender); 61 | 62 | // 接下来会补充这部分代码 63 | function handleRender(req, res) { /* ... */ } 64 | function renderFullPage(html, initialState) { /* ... */ } 65 | 66 | app.listen(port); 67 | ``` 68 | 69 | ### 处理请求 70 | 71 | 第一件要做的事情就是对每个请求创建一个新的 Redux store 实例。这个 store 惟一作用是提供应用初始的 state。 72 | 73 | 渲染时,使用 `` 来包住根组件 ``,以此来让组件树中所有组件都能访问到 store,就像之前的[搭配 React](../basics/UsageWithReact.md) 教程讲的那样。 74 | 75 | 服务端渲染最关键的一步是在**发送响应前**渲染初始的 HTML。这就要使用 [React.renderToString()](https://facebook.github.io/react/docs/top-level-api.html#react.rendertostring). 76 | 77 | 然后使用 [`store.getState()`](../api/Store.md#getState) 从 store 得到初始 state。`renderFullPage` 函数会介绍接下来如何传递。 78 | 79 | ```js 80 | function handleRender(req, res) { 81 | // 创建新的 Redux store 实例 82 | const store = createStore(counterApp); 83 | 84 | // 把组件渲染成字符串 85 | const html = React.renderToString( 86 | 87 | {() => } 88 | 89 | ); 90 | 91 | // 从 store 中获得 state 92 | const initialState = store.getState(); 93 | 94 | // 把渲染后的页面内容发送给客户端 95 | res.send(renderFullPage(html, initialState)); 96 | } 97 | ``` 98 | 99 | ### 注入初始组件的 HTML 和 State 100 | 101 | 服务端最后一步就是把初始组件的 HTML 和初始 state 注入到客户端能够渲染的模板中。如何传递 state 呢,我们添加一个 ` 120 | 121 | 122 | 123 | `; 124 | } 125 | ``` 126 | 127 | >##### 字符串插值语法须知 128 | 129 | >上面的示例使用了 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。 130 | 131 | ## 客户端开发 132 | 133 | 客户端代码非常直观。只需要从 `window.__INITIAL_STATE__` 得到初始 state,并传给 [`createStore()`](../api/createStore.md) 函数即可。 134 | 135 | 代码如下: 136 | 137 | #### `client.js` 138 | 139 | ```js 140 | import React from 'react'; 141 | import { createStore } from 'redux'; 142 | import { Provider } from 'react-redux'; 143 | import App from './containers/App'; 144 | import counterApp from './reducers'; 145 | 146 | // 通过服务端注入的全局变量得到初始 state 147 | const initialState = window.__INITIAL_STATE__; 148 | 149 | // 使用初始 state 创建 Redux store 150 | const store = createStore(counterApp, initialState); 151 | 152 | React.render( 153 | 154 | {() => } 155 | , 156 | document.getElementById('root') 157 | ); 158 | ``` 159 | 你可以选择自己喜欢的打包工具(Webpack, Browserify 或其它)来编译并打包文件到 `dist/bundle.js`。 160 | 161 | 当页面加载时,打包后的 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。 162 | 163 | 就是这样!这就是实现服务端渲染的所有步骤。 164 | 165 | 但这样做还是比较原始的。只会用动态代码渲染一个静态的 View。下一步要做的是动态创建初始 state 支持动态渲染 view。 166 | 167 | ## 准备初始 State 168 | 169 | 因为客户端只是执行收到的代码,刚开始的初始 state 可能是空的,然后根据需要获取 state。在服务端,渲染是同步执行的而且我们只有一次渲染 view 的机会。在收到请求时,可能需要根据请求参数或者外部 state(如访问 API 或者数据库),计算后得到初始 state。 170 | 171 | ### 处理 Request 参数 172 | 173 | 服务端收到的惟一输入是来自浏览器的请求。在服务器启动时可能需要做一些配置(如运行在开发环境还是生产环境),但这些配置是静态的。 174 | 175 | 请求会包含 URL 请求相关信息,包括请求参数,它们对于做 [React Router](https://github.com/rackt/react-router) 路由时可能会有用。也可能在请求头里包含 cookies,鉴权信息或者 POST 内容数据。下面演示如何基于请求参数来得到初始 state。 176 | 177 | #### `server.js` 178 | 179 | ```js 180 | import qs from 'qs'; // 添加到文件开头 181 | 182 | function handleRender(req, res) { 183 | // 如果存在的话,从 request 读取 counter 184 | const params = qs.parse(req.query); 185 | const counter = parseInt(params.counter) || 0; 186 | 187 | // 得到初始 state 188 | let initialState = { counter }; 189 | 190 | // 创建新的 Redux store 实例 191 | const store = createStore(counterApp, initialState); 192 | 193 | // 把组件渲染成字符串 194 | const html = React.renderToString( 195 | 196 | {() => } 197 | 198 | ); 199 | 200 | // 从 Redux store 得到初始 state 201 | const finalState = store.getState(); 202 | 203 | // 把渲染后的页面发给客户端 204 | res.send(renderFullPage(html, finalState)); 205 | } 206 | ``` 207 | 208 | 上面的代码首先访问 Express 的 `Request` 对象。把参数转成数字,然后设置到初始 state 中。如果你在浏览器中访问 [http://localhost:3000/?counter=100](http://localhost:3000/?counter=100),你会看到计数器从 100 开始。在渲染后的 HTML 中,你会看到计数显示 100 同时设置进了 `__INITIAL_STATE__` 变量。 209 | 210 | ### 获取异步 State 211 | 212 | 服务端渲染常用的场景是处理异步 state。因为服务端渲染天生是同步的,因此异步的数据获取操作对应到同步操作非常重要。 213 | 214 | 最简单的做法是往同步代码里传递一些回调函数。在这个回调函数里引用响应对象,把渲染后的 HTML 发给客户端。不要担心,并没有想像中那么难。 215 | 216 | 本例中,我们假设有一个外部数据源提供计算器的初始值(所谓的把计算作为一种服务)。我们会模拟一个请求并使用结果创建初始 state。API 请求代码如下: 217 | 218 | #### `api/counter.js` 219 | 220 | ```js 221 | function getRandomInt(min, max) { 222 | return Math.floor(Math.random() * (max - min)) + min; 223 | } 224 | 225 | export function fetchCounter(callback) { 226 | setTimeout(() => { 227 | callback(getRandomInt(1, 100)); 228 | }, 500); 229 | } 230 | ``` 231 | 232 | 再次说明一下,这只是一个模拟的 API,我们使用 `setTimeout` 模拟一个需要 500 毫秒的请求(实现项目中 API 请求一般会更快)。传入一个回调函数,它异步返回一个随机数字。如果你使用了基于 Promise 的 API 工具,那么要把回调函数放到 `then` 中。 233 | 234 | 在服务端,把代码使用 `fetchCounter` 包起来,在回调函数里拿到结果: 235 | 236 | #### `server.js` 237 | 238 | ```js 239 | // Add this to our imports 240 | import { fetchCounter } from './api/counter'; 241 | 242 | function handleRender(req, res) { 243 | // 异步请求模拟的 API 244 | fetchCounter(apiResult => { 245 | // 如果存在的话,从 request 读取 counter 246 | const params = qs.parse(req.query); 247 | const counter = parseInt(params.counter) || apiResult || 0; 248 | 249 | // 得到初始 state 250 | let initialState = { counter }; 251 | 252 | // 创建新的 Redux store 实例 253 | const store = createStore(counterApp, initialState); 254 | 255 | // 把组件渲染成字符串 256 | const html = React.renderToString( 257 | 258 | {() => } 259 | 260 | ); 261 | 262 | // 从 Redux store 得到初始 state 263 | const finalState = store.getState(); 264 | 265 | // 把渲染后的页面发给客户端 266 | res.send(renderFullPage(html, finalState)); 267 | }); 268 | } 269 | ``` 270 | 271 | 因为在回调中使用了 `res.send()`,服务器会保护连接打开并在回调函数执行前不发送任何数据。你会发现每个请求都有 500ms 的延时。更高级的用法会包括对 API 请求出错进行处理,比如错误的请求或者超时。 272 | 273 | ### 安全注意事项 274 | 275 | 因为我们代码中很多是基于用户生成内容(UGC)和输入的,不知不觉中,提高了应用可能受攻击区域。任何应用都应该对用户输入做安全处理以避免跨站脚本攻击(XSS)或者代码注入。 276 | 277 | 我们的示例中,只对安全做基本处理。当从请求中拿参数时,对 `counter` 参数使用 `parseInt` 把它转成数字。如果不这样做,当 request 中有 script 标签时,很容易在渲染的 HTML 中生成危险代码。就像这样的:`?counter=` 278 | 279 | 在我们极简的示例中,把输入转成数字已经比较安全。如果处理更复杂的输入,比如自定义格式的文本,你应该用安全函数处理输入,比如 [validator.js](https://www.npmjs.com/package/validator)。 280 | 281 | 此外,可能添加额外的安全层来对产生的 state 进行消毒。`JSON.stringify` 可能会造成 script 注入。鉴于此,你需要清洗 JSON 字符串中的 HTML 标签和其它危险的字符。可能通过字符串替换或者使用复杂的库如 [serialize-javascript](https://github.com/yahoo/serialize-javascript) 处理。 282 | 283 | ## 下一步 284 | 285 | 你还可以参考 [异步 Actions](../advanced/AsyncActions.md) 学习更多使用 Promise 和 thunk 这些异步元素来表示异步数据流的方法。记住,那里学到的任何内容都可以用于同构渲染。 286 | 287 | 如何你使用了 [React Router](https://github.com/rackt/react-router),你可能还需要在路由处理组件中使用静态的 `fetchData()` 方法来获取依赖的数据。它可能返回 [异步 action](../advanced/AsyncActions.md),以便你的 `handleRender` 函数可以匹配到对应的组件类,对它们均 dispatch `fetchData()` 的结果,在 Promise 解决后才渲染。这样不同路由需要调用的 API 请求都并置于路由处理组件了。在客户端,你也可以使用同样技术来避免在切换页面时,当数据还没有加载完成前执行路由。(Revision needed) 288 | -------------------------------------------------------------------------------- /docs/basics/Reducers.md: -------------------------------------------------------------------------------- 1 | # Reducer 2 | 3 | [Action](./Actions.md) 只是描述了**有事情发生了**这一事实,并没有指明应用如何更新 state。这是 reducer 要做的事情。 4 | 5 | ## 设计 State 结构 6 | 7 | 应用所有的 state 都被保存在一个单一对象中(我们称之为 state 树)。建议在写代码前先想一下这个对象的结构。如何才能以最简的形式把应用的 state 用对象描述出来? 8 | 9 | 以 todo 应用为例,需要保存两个不同的内容: 10 | 11 | * 当前选中的任务过滤条件; 12 | * 真实的任务列表。 13 | 14 | 通常,这个 state 树还需要存放其它一些数据,还有界面 state。这样做没问题,但尽量把这些数据与界面 state 分开。 15 | 16 | ```js 17 | { 18 | visibilityFilter: 'SHOW_ALL', 19 | todos: [{ 20 | text: 'Consider using Redux', 21 | completed: true, 22 | }, { 23 | text: 'Keep all state in a single tree', 24 | completed: false 25 | }] 26 | } 27 | ``` 28 | 29 | >##### 处理 Reducer 关系时注意 30 | 31 | >开发复杂的应用时,不可避免会有一些数据相互引用。建议你尽可能地把 state 范式化,不存在嵌套。把所有数据放到一个对象里,每个数据以 ID 为主键,不同数据相互引用时通过 ID 来查找。把应用的 state 想像成数据库。这种方法在 [normalizr](https://github.com/gaearon/normalizr) 文档里有详细阐述。例如,实际开发中,在 state 里同时存放 `todosById: { id -> todo }` 和 `todos: array` 是比较好的方式(虽然你可以觉得冗余)。 32 | 33 | ## Action 处理 34 | 35 | 现在我们已经确定了 state 对象的结构,就可以开始开发 reducer。reducer 就是一个函数,接收旧的 state 和 action,返回新的 state。 36 | 37 | ```js 38 | (previousState, action) => newState 39 | ``` 40 | 41 | 之所以称作 reducer 是因为和 [`Array.prototype.reduce(reducer, ?initialValue)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce) 格式很像。保持 reducer 纯净非常重要。永远**不要**在 reducer 里做这些操作: 42 | 43 | * 修改传入参数; 44 | * 执行有副作用的操作,如 API 请求和路由跳转。 45 | 46 | 在[高级篇](../advanced/README.md)里会介绍如何执行有副作用的操作。现在只需要谨记 reducer 一定要保持纯净。**只要传入参数一样,返回必须一样。没有特殊情况、没有副作用,没有 API 请求、没有修改参数,单纯执行计算。** 47 | 48 | 明白了这些之后,就可以开始编写 reducer,并让它来处理之前定义过的 [actions](Actions.md)。 49 | 50 | 我们在开始时定义默认的 state。Redux 首次执行时,state 为 `undefined`,这时候会返回默认 state。 51 | 52 | ```js 53 | import { VisibilityFilters } from './actions'; 54 | 55 | const initialState = { 56 | visibilityFilter: VisibilityFilters.SHOW_ALL, 57 | todos: [] 58 | }; 59 | 60 | function todoApp(state, action) { 61 | if (typeof state === 'undefined') { 62 | return initialState; 63 | } 64 | 65 | // 这里暂不处理任何 action, 66 | // 仅返回传入的 state。 67 | return state; 68 | } 69 | ``` 70 | 71 | 这里一个技巧是使用 [ES6 参数默认值语法](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Functions/default_parameters) 来精简代码。 72 | 73 | ```js 74 | function todoApp(state = initialState, action) { 75 | // 这里暂不处理任何 action, 76 | // 仅返回传入的 state。 77 | return state; 78 | } 79 | ``` 80 | 81 | 现在可以处理 `SET_VISIBILITY_FILTER`。需要做的只是改变 state 中的 `visibilityFilter`。 82 | 83 | ```js 84 | function todoApp(state = initialState, action) { 85 | switch (action.type) { 86 | case SET_VISIBILITY_FILTER: 87 | return Object.assign({}, state, { 88 | visibilityFilter: action.filter 89 | }); 90 | default: 91 | return state; 92 | } 93 | } 94 | ``` 95 | 96 | 注意: 97 | 98 | 1. **不要修改 `state`。** 使用 [`Object.assign()`](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/assign) 新建了一个副本。不能这样使用 `Object.assign(state, { visibilityFilter: action.filter })`,因为它会改变第一个参数的值。**一定**要把第一个参数设置为空对象。也可以使用 ES7 中还在试验阶段的特性 `{ ...state, ...newState }`,参考 [对象展开语法](https://github.com/sebmarkbage/ecmascript-rest-spread)。 99 | 100 | 2. **在 `default` 情况下返回旧的 `state`。**遇到未知的 action 时,一定要返回旧的 `state`。 101 | 102 | >##### `Object.assign` 使用提醒 103 | 104 | >[`Object.assign()`](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/assign) 是 ES6 特性,但多数浏览器并不支持。你要么使用 polyfill,[Babel 插件](https://github.com/babel-plugins/babel-plugin-object-assign),或者使用其它库如 [`_.assign()`](https://lodash.com/docs#assign) 提供的帮助方法。 105 | 106 | >##### `switch` 和样板代码提醒 107 | 108 | >`state` 语句并不是严格意义上的样板代码。Flux 中真实的样板代码是概念性的:更新必须要发送、Store 必须要注册到 Dispatcher、Store 必须是对象(开发同构应用时变得非常复杂)。为了解决这些问题,Redux 放弃了 event emitters(事件发送器),转而使用纯 reducer。 109 | 110 | >很不幸到现在为步,还有很多人存在一个误区:根据文档中是否使用 `switch` 来决定是否使用它。如果你不喜欢 `switch`,完全可以自定义一个 `createReducer` 函数来接收一个事件处理函数列表,参照["减少样板代码"](../recipes/ReducingBoilerplate.md#reducers)。 111 | 112 | ## 处理多个 action 113 | 114 | 还有两个 action 需要处理。让我们先处理 `ADD_TODO`。 115 | 116 | ```js 117 | function todoApp(state = initialState, action) { 118 | switch (action.type) { 119 | case SET_VISIBILITY_FILTER: 120 | return Object.assign({}, state, { 121 | visibilityFilter: action.filter 122 | }); 123 | case ADD_TODO: 124 | return Object.assign({}, state, { 125 | todos: [...state.todos, { 126 | text: action.text, 127 | completed: false 128 | }] 129 | }); 130 | default: 131 | return state; 132 | } 133 | } 134 | ``` 135 | 136 | 如上,不直接修改 `state` 中的字段,而是返回新对象。新的 `todos` 对象就相当于旧的 `todos` 在末尾加上新建的 todo。而这个新的 todo 又是在 action 中创建的。 137 | 138 | 最后,`COMPLETE_TODO` 的实现也很好理解: 139 | 140 | ```js 141 | case COMPLETE_TODO: 142 | return Object.assign({}, state, { 143 | todos: [ 144 | ...state.todos.slice(0, action.index), 145 | Object.assign({}, state.todos[action.index], { 146 | completed: true 147 | }), 148 | ...state.todos.slice(action.index + 1) 149 | ] 150 | }); 151 | ``` 152 | 153 | 因为我们不能直接修改却要更新数组中指定的一项数据,这里需要先把前面和后面都切开。如果经常需要这类的操作,可以选择使用帮助类 [React.addons.update](https://facebook.github.io/react/docs/update.html),[updeep](https://github.com/substantial/updeep),或者使用原生支持深度更新的库 [Immutable](http://facebook.github.io/immutable-js/)。最后,时刻谨记永远不要在克隆 `state` 前修改它。 154 | 155 | ## 拆分 Reducer 156 | 157 | 目前的代码看起来有些冗余: 158 | 159 | ```js 160 | function todoApp(state = initialState, action) { 161 | switch (action.type) { 162 | case SET_VISIBILITY_FILTER: 163 | return Object.assign({}, state, { 164 | visibilityFilter: action.filter 165 | }); 166 | case ADD_TODO: 167 | return Object.assign({}, state, { 168 | todos: [...state.todos, { 169 | text: action.text, 170 | completed: false 171 | }] 172 | }); 173 | case COMPLETE_TODO: 174 | return Object.assign({}, state, { 175 | todos: [ 176 | ...state.todos.slice(0, action.index), 177 | Object.assign({}, state.todos[action.index], { 178 | completed: true 179 | }), 180 | ...state.todos.slice(action.index + 1) 181 | ] 182 | }); 183 | default: 184 | return state; 185 | } 186 | } 187 | ``` 188 | 189 | 上面代码能否变得更通俗易懂?这里的 `todos` 和 `visibilityFilter` 的更新看起来是相互独立的。有时 state 中的字段是相互依赖的,需要认真考虑,但在这个案例中我们可以把 `todos` 更新的业务逻辑拆分到一个单独的函数里: 190 | 191 | ```js 192 | function todos(state = [], action) { 193 | switch (action.type) { 194 | case ADD_TODO: 195 | return [...state, { 196 | text: action.text, 197 | completed: false 198 | }]; 199 | case COMPLETE_TODO: 200 | return [ 201 | ...state.slice(0, action.index), 202 | Object.assign({}, state[action.index], { 203 | completed: true 204 | }), 205 | ...state.slice(action.index + 1) 206 | ]; 207 | default: 208 | return state; 209 | } 210 | } 211 | 212 | function todoApp(state = initialState, action) { 213 | switch (action.type) { 214 | case SET_VISIBILITY_FILTER: 215 | return Object.assign({}, state, { 216 | visibilityFilter: action.filter 217 | }); 218 | case ADD_TODO: 219 | case COMPLETE_TODO: 220 | return Object.assign({}, state, { 221 | todos: todos(state.todos, action) 222 | }); 223 | default: 224 | return state; 225 | } 226 | } 227 | ``` 228 | 229 | 注意 `todos` 依旧接收 `state`,但它变成了一个数组!现在 `todoApp` 只把需要更新的一部分 state 传给 `todos` 函数,`todos` 函数自己确定如何更新这部分数据。**这就是所谓的 **reducer 合成**,它是开发 Redux 应用最基础的模式。** 230 | 231 | 下面深入探讨一下如何做 reducer 合成。能否抽出一个 reducer 来专门管理 `visibilityFilter`?当然可以: 232 | 233 | ```js 234 | function visibilityFilter(state = SHOW_ALL, action) { 235 | switch (action.type) { 236 | case SET_VISIBILITY_FILTER: 237 | return action.filter; 238 | default: 239 | return state; 240 | } 241 | } 242 | ``` 243 | 244 | 现在我们可以开发一个函数来做为主 reducer,它调用多个子 reducer 分别处理 state 中的一部分数据,然后再把这些数据合成一个大的单一对象。主 reducer 并不需要设置初始化时完整的 state。初始时,如果给子 reducer 传入 `undefined` 只要返回它们的默认值即可。 245 | 246 | ```js 247 | function todos(state = [], action) { 248 | switch (action.type) { 249 | case ADD_TODO: 250 | return [...state, { 251 | text: action.text, 252 | completed: false 253 | }]; 254 | case COMPLETE_TODO: 255 | return [ 256 | ...state.slice(0, action.index), 257 | Object.assign({}, state[action.index], { 258 | completed: true 259 | }), 260 | ...state.slice(action.index + 1) 261 | ]; 262 | default: 263 | return state; 264 | } 265 | } 266 | 267 | function visibilityFilter(state = SHOW_ALL, action) { 268 | switch (action.type) { 269 | case SET_VISIBILITY_FILTER: 270 | return action.filter; 271 | default: 272 | return state; 273 | } 274 | } 275 | 276 | function todoApp(state = {}, action) { 277 | return { 278 | visibilityFilter: visibilityFilter(state.visibilityFilter, action), 279 | todos: todos(state.todos, action) 280 | }; 281 | } 282 | ``` 283 | 284 | **注意每个 reducer 只负责管理全局 state 中它负责的一部分。每个 reducer 的 `state` 参数都不同,分别对应它管理的那部分 state 数据。** 285 | 286 | 现在看过起来好多了!随着应用的膨胀,我们已经学会把 reducer 拆分成独立文件来分别处理不同的数据域了。 287 | 288 | 最后,Redux 提供了 [`combineReducers()`](../api/combineReducers.md) 工具类来做上面 `todoApp` 做的事情,这样就能消灭一些样板代码了。有了它,可以这样重构 `todoApp`: 289 | 290 | ```js 291 | import { combineReducers } from 'redux'; 292 | 293 | const todoApp = combineReducers({ 294 | visibilityFilter, 295 | todos 296 | }); 297 | 298 | export default todoApp; 299 | ``` 300 | 301 | 注意上面的写法和下面完全等价: 302 | 303 | ```js 304 | export default function todoApp(state, action) { 305 | return { 306 | visibilityFilter: visibilityFilter(state.visibilityFilter, action), 307 | todos: todos(state.todos, action) 308 | }; 309 | } 310 | ``` 311 | 312 | 你也可以给它们设置不同的 key,或者调用不同的函数。下面两种合成 reducer 方法完全等价: 313 | 314 | ```js 315 | const reducer = combineReducers({ 316 | a: doSomethingWithA, 317 | b: processB, 318 | c: c 319 | }); 320 | ``` 321 | 322 | ```js 323 | function reducer(state, action) { 324 | return { 325 | a: doSomethingWithA(state.a, action), 326 | b: processB(state.b, action), 327 | c: c(state.c, action) 328 | }; 329 | } 330 | ``` 331 | 332 | [`combineReducers()`](../api/combineReducers.md) 所做的只是生成一个函数,这个函数来调用你的一系列 reducer,每个 reducer **根据它们的 key 来筛选出 state 中的一部分数据并处理**,然后这个生成的函数所所有 reducer 的结果合并成一个大的对象。[没有任何魔法。](https://github.com/gaearon/redux/issues/428#issuecomment-129223274) 333 | 334 | >##### ES6 用户使用注意 335 | 336 | >`combineReducers` 接收一个对象,可以把所有顶级的 reducer 放到一个独立的文件中,通过 `export` 暴露出每个 reducer 函数,然后使用 `import * as reducers` 得到一个以它们名字作为 key 的 object: 337 | 338 | >```js 339 | >import { combineReducers } from 'redux'; 340 | >import * as reducers from './reducers'; 341 | > 342 | >const todoApp = combineReducers(reducers); 343 | >``` 344 | > 345 | >由于 `import *` 还是比较新的语法,为了避免[困惑](https://github.com/gaearon/redux/issues/428#issuecomment-129223274),我们不会在文档使用它。但在一些社区示例中你可能会遇到它们。 346 | 347 | ## 源码 348 | 349 | #### `reducers.js` 350 | 351 | ```js 352 | import { combineReducers } from 'redux'; 353 | import { ADD_TODO, COMPLETE_TODO, SET_VISIBILITY_FILTER, VisibilityFilters } from './actions'; 354 | const { SHOW_ALL } = VisibilityFilters; 355 | 356 | function visibilityFilter(state = SHOW_ALL, action) { 357 | switch (action.type) { 358 | case SET_VISIBILITY_FILTER: 359 | return action.filter; 360 | default: 361 | return state; 362 | } 363 | } 364 | 365 | function todos(state = [], action) { 366 | switch (action.type) { 367 | case ADD_TODO: 368 | return [...state, { 369 | text: action.text, 370 | completed: false 371 | }]; 372 | case COMPLETE_TODO: 373 | return [ 374 | ...state.slice(0, action.index), 375 | Object.assign({}, state[action.index], { 376 | completed: true 377 | }), 378 | ...state.slice(action.index + 1) 379 | ]; 380 | default: 381 | return state; 382 | } 383 | } 384 | 385 | const todoApp = combineReducers({ 386 | visibilityFilter, 387 | todos 388 | }); 389 | 390 | export default todoApp; 391 | ``` 392 | 393 | ## 下一步 394 | 395 | 接下来会学习 [创建 Redux store](Store.md)。store 能维持应用的 state,并在当你发起 action 的时候调用 reducer。 396 | -------------------------------------------------------------------------------- /docs/basics/UsageWithReact.md: -------------------------------------------------------------------------------- 1 | # 搭配 React 2 | 3 | 这里需要再强调一下:Redux 和 React 之间没有关系。Redux 支持 React、Angular、Ember、jQuery 甚至纯 JavaScript。 4 | 5 | 尽管如此,Redux 还是和 [React](http://facebook.github.io/react/) 和 [Deku](https://github.com/dekujs/deku) 这类框架搭配起来用最好,因为这类框架允许你以 state 函数的形式来描述界面,Redux 通过 action 的形式来发起 state 变化。 6 | 7 | 下面使用 React 来开发一个 todo 任务管理应用。 8 | 9 | ## 安装 React Redux 10 | 11 | Redux 默认并不包含 [React 绑定库](https://github.com/gaearon/react-redux),需要单独安装。 12 | 13 | ``` 14 | npm install --save react-redux 15 | ``` 16 | 17 | ## 智能组件(Smart Components)和笨拙组件(Dumb Components) 18 | 19 | Redux 的 React 绑定库拥抱了 [智能组件和笨拙组件相分离](https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0) 的开发思想。 20 | 21 | 明智的做法是只在最顶层组件(如路由操作)里使用 Redux。内部组件应该像木偶一样保持“呆滞”,所有数据都通过 props 传入。 22 | 23 |
    24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 40 | 41 | 42 | 43 | 44 | 45 | 47 | 48 | 49 | 50 |
    LocationAware of ReduxTo read dataTo change data
    “Smart” ComponentsTop level, route handlersYes 39 | Subscribe to Redux stateDispatch Redux actions
    “Dumb” ComponentsMiddle and leaf componentsNo 46 | Read data from propsInvoke callbacks from props
    51 |
    52 | 53 | 在这个 todo 应用中,只应有一个智能组件,它存在于组件的最顶层。在复杂的应用中,也有可能会有多个智能组件。虽然你也可以嵌套使用智能组件,但应该尽可能的使用传递 props 的形式。 54 | 55 | ## 设计组件层次结构 56 | 57 | 还记得当初如何 [设计 reducer 结构](Reducers.md) 吗?现在就要定义与它匹配的界面的层次结构。其实这不是 Redux 相关的工作,[React 开发思想](https://facebook.github.io/react/docs/thinking-in-react.html)在这方面解释的非常棒。 58 | 59 | Our design brief is simple. We want to show a list of todo items. On click, a todo item is crossed out as completed. We want to show a field where user may add a new todo. In the footer, we want to show a toggle to show all / only completed / only incompleted todos. 60 | 61 | I see the following components (and their props) emerge from this brief: 62 | 63 | * **`AddTodo`** is an input field with a button. 64 | - `onAddClick(text: string)` is a callback to invoke when a button is pressed. 65 | * **`TodoList`** is a list showing visible todos. 66 | - `todos: Array` is an array of todo items with `{ text, completed }` shape. 67 | - `onTodoClick(index: number)` is a callback to invoke when a todo is clicked. 68 | * **`Todo`** is a single todo item. 69 | - `text: string` is the text to show. 70 | - `completed: boolean` is whether todo should appear crossed out. 71 | - `onClick()` is a callback to invoke when a todo is clicked. 72 | * **`Footer`** is a component where we let user change visible todo filter. 73 | - `filter: string` is the current filter: `'SHOW_ALL'`, `'SHOW_COMPLETED'` or `'SHOW_ACTIVE'`. 74 | - `onFilterChange(nextFilter: string)`: Callback to invoke when user chooses a different filter. 75 | 76 | These are all “dumb” components. They don’t know *where* the data comes from, or *how* to change it. They only render what’s given to them. 77 | 78 | If you migrate from Redux to something else, you’ll be able to keep all these components exactly the same. They have no dependency on Redux. 79 | 80 | Let’s write them! We don’t need to think about binding to Redux yet. You can just give them fake data while you experiment until they render correctly. 81 | 82 | ## 笨拙组件 83 | 84 | 这就是普通的 React 组件,所以就不在详述。直接看代码: 85 | 86 | #### `components/AddTodo.js` 87 | 88 | ```js 89 | import React, { findDOMNode, Component, PropTypes } from 'react'; 90 | 91 | export default class AddTodo extends Component { 92 | render() { 93 | return ( 94 |
    95 | 96 | 99 |
    100 | ); 101 | } 102 | 103 | handleClick(e) { 104 | const node = findDOMNode(this.refs.input); 105 | const text = node.value.trim(); 106 | this.props.onAddClick(text); 107 | node.value = ''; 108 | } 109 | } 110 | 111 | AddTodo.propTypes = { 112 | onAddClick: PropTypes.func.isRequired 113 | }; 114 | ``` 115 | 116 | #### `components/Todo.js` 117 | 118 | ```js 119 | import React, { Component, PropTypes } from 'react'; 120 | 121 | export default class Todo extends Component { 122 | render() { 123 | return ( 124 |
  • 130 | {this.props.text} 131 |
  • 132 | ); 133 | } 134 | } 135 | 136 | Todo.propTypes = { 137 | onClick: PropTypes.func.isRequired, 138 | text: PropTypes.string.isRequired, 139 | completed: PropTypes.bool.isRequired 140 | }; 141 | ``` 142 | 143 | #### `components/TodoList.js` 144 | 145 | ```js 146 | import React, { Component, PropTypes } from 'react'; 147 | import Todo from './Todo'; 148 | 149 | export default class TodoList extends Component { 150 | render() { 151 | return ( 152 |
      153 | {this.props.todos.map((todo, index) => 154 | this.props.onTodoClick(index)} /> 157 | )} 158 |
    159 | ); 160 | } 161 | } 162 | 163 | TodoList.propTypes = { 164 | onTodoClick: PropTypes.func.isRequired, 165 | todos: PropTypes.arrayOf(PropTypes.shape({ 166 | text: PropTypes.string.isRequired, 167 | completed: PropTypes.bool.isRequired 168 | }).isRequired).isRequired 169 | }; 170 | ``` 171 | 172 | #### `components/Footer.js` 173 | 174 | ```js 175 | import React, { Component, PropTypes } from 'react'; 176 | 177 | export default class Footer extends Component { 178 | renderFilter(filter, name) { 179 | if (filter === this.props.filter) { 180 | return name; 181 | } 182 | 183 | return ( 184 | { 185 | e.preventDefault(); 186 | this.props.onFilterChange(filter); 187 | }}> 188 | {name} 189 | 190 | ); 191 | } 192 | 193 | render() { 194 | return ( 195 |

    196 | Show: 197 | {' '} 198 | {this.renderFilter('SHOW_ALL', 'All')} 199 | {', '} 200 | {this.renderFilter('SHOW_COMPLETED', 'Completed')} 201 | {', '} 202 | {this.renderFilter('SHOW_ACTIVE', 'Active')} 203 | . 204 |

    205 | ); 206 | } 207 | } 208 | 209 | Footer.propTypes = { 210 | onFilterChange: PropTypes.func.isRequired, 211 | filter: PropTypes.oneOf([ 212 | 'SHOW_ALL', 213 | 'SHOW_COMPLETED', 214 | 'SHOW_ACTIVE' 215 | ]).isRequired 216 | }; 217 | ``` 218 | 219 | 就这些,现在开发一个笨拙型的组件 `App` 把它们渲染出来,验证下是否工作。 220 | 221 | #### `containers/App.js` 222 | 223 | ```js 224 | import React, { Component } from 'react'; 225 | import AddTodo from '../components/AddTodo'; 226 | import TodoList from '../components/TodoList'; 227 | import Footer from '../components/Footer'; 228 | 229 | export default class App extends Component { 230 | render() { 231 | return ( 232 |
    233 | 235 | console.log('add todo', text) 236 | } /> 237 | 246 | console.log('todo clicked', todo) 247 | } /> 248 |
    251 | console.log('filter change', filter) 252 | } /> 253 |
    254 | ); 255 | } 256 | } 257 | ``` 258 | 259 | 渲染 `` 结果如下: 260 | 261 | 262 | 263 | 单独来看,并没有什么特别,现在把它和 Redux 连起来。 264 | 265 | ## 连接到 Redux 266 | 267 | 我们需要做出两个变化,将 `App` 组件连接到 Redux 并且让它能够 dispatch actions 以及从 Redux store 读取到 state。 268 | 269 | 首先,我们需要获取从之前安装好的 [`react-redux`](http://github.com/gaearon/react-redux) 提供的 `Provider`,并且在渲染之前**将根组件包装进 ``**。 270 | 271 | #### `index.js` 272 | 273 | ```js 274 | import React from 'react'; 275 | import { createStore } from 'redux'; 276 | import { Provider } from 'react-redux'; 277 | import App from './containers/App'; 278 | import todoApp from './reducers'; 279 | 280 | let store = createStore(todoApp); 281 | 282 | let rootElement = document.getElementById('root'); 283 | React.render( 284 | // 为了解决在 React 0.13 的一个问题 285 | // 子标签必须包装成一个 function。 286 | 287 | {() => } 288 | , 289 | rootElement 290 | ); 291 | ``` 292 | 293 | 这使得我们的 store 能为下面的组件所用。(在内部,这个是通过 React 的 [非文件“ context ”特性](http://www.youtube.com/watch?v=H7vlH-wntD4) 完成的,但它不直接暴露 API,所以不用担心。) 294 | 295 | 接着,我们**想要通过 [`react-redux`](http://github.com/gaearon/react-redux) 提供的 `connect()` 方法将包装好的组件连接到Redux**。尽量只做一个顶层的组件,或者 route 处理。从技术上来说你可以将应用中的任何一个组件 `connect()` 到 Redux store 中,但尽量要避免这么做,因为这个数据流很难追踪。 296 | 297 | **任何一个从 `contect()` 包装好的组件都可以得到一个 [`dispatch`](../api/Store.md#dispatch) 方法作为组件的 props。** `connect()` 的唯一参数是 **selector**。此方法可以从 Redux store 接收到全局的 state,然后返回一个你的组件中需要的 props。最简单的情况下,可以返回一个初始的 `state` ,但你可能希望它发生了变化。 298 | 299 | 为了组合 selectors 更有效率,不妨看看 [reselect](https://github.com/faassen/reselect)。在这个例子中我们不会用到它,但它适合更大的应用。 300 | 301 | #### `containers/App.js` 302 | 303 | ```js 304 | import React, { Component, PropTypes } from 'react'; 305 | import { connect } from 'react-redux'; 306 | import { addTodo, completeTodo, setVisibilityFilter, VisibilityFilters } from '../actions'; 307 | import AddTodo from '../components/AddTodo'; 308 | import TodoList from '../components/TodoList'; 309 | import Footer from '../components/Footer'; 310 | 311 | class App extends Component { 312 | render() { 313 | // Injected by connect() call: 314 | const { dispatch, visibleTodos, visibilityFilter } = this.props; 315 | return ( 316 |
    317 | 319 | dispatch(addTodo(text)) 320 | } /> 321 | 324 | dispatch(completeTodo(index)) 325 | } /> 326 |
    329 | dispatch(setVisibilityFilter(nextFilter)) 330 | } /> 331 |
    332 | ); 333 | } 334 | } 335 | 336 | App.propTypes = { 337 | visibleTodos: PropTypes.arrayOf(PropTypes.shape({ 338 | text: PropTypes.string.isRequired, 339 | completed: PropTypes.bool.isRequired 340 | })), 341 | visibilityFilter: PropTypes.oneOf([ 342 | 'SHOW_ALL', 343 | 'SHOW_COMPLETED', 344 | 'SHOW_ACTIVE' 345 | ]).isRequired 346 | }; 347 | 348 | function selectTodos(todos, filter) { 349 | switch (filter) { 350 | case VisibilityFilters.SHOW_ALL: 351 | return todos; 352 | case VisibilityFilters.SHOW_COMPLETED: 353 | return todos.filter(todo => todo.completed); 354 | case VisibilityFilters.SHOW_ACTIVE: 355 | return todos.filter(todo => !todo.completed); 356 | } 357 | } 358 | 359 | // Which props do we want to inject, given the global state? 360 | // Note: use https://github.com/faassen/reselect for better performance. 361 | function select(state) { 362 | return { 363 | visibleTodos: selectTodos(state.todos, state.visibilityFilter), 364 | visibilityFilter: state.visibilityFilter 365 | }; 366 | } 367 | 368 | // Wrap the component to inject dispatch and state into it 369 | export default connect(select)(App); 370 | ``` 371 | 372 | 到此为止,迷你型的任务管理应用就开发完毕。 373 | 374 | ## 下一步 375 | 376 | 参照 [本示例完整](ExampleTodoList.md) 来深化理解。然后就可以跳到 [高级教程](../advanced/README.md) 学习网络请求处理和路由。 377 | -------------------------------------------------------------------------------- /docs/recipes/ReducingBoilerplate.md: -------------------------------------------------------------------------------- 1 | # 减少样板代码 2 | 3 | Redux 很大部分 [受到 Flux 的启发](../introduction/PriorArt.md),并且最常见的关于 Flux 抱怨是它如何使得你写了一大堆的模板。在这个技巧中,我们将考虑 Redux 如何使得我们选择我们的代码会变得怎样繁复,取决于个人样式,团队选项,长期可维护等等。 4 | 5 | ## Actions 6 | 7 | Actions 是描述了在 app 中所发生的,以单独方式描述对象变异意图的服务的一个普通对象。很重要的一点是 **你必须分发的 action 对象并不是一个模板,而是 Redux 的一个[基本设计选项](../introduction/ThreePrinciples.md)**. 8 | 9 | 有些框架生成自己和 Flux 很像,不过缺少了 action 对象的概念。为了变得可预测,这是一个从 Flux or Redux 的倒退。如果没有可串行的普通对象 action,便无法记录或重放用户会话,或者无法实现 [带有时间旅行的热重载](https://www.youtube.com/watch?v=xsSnOQynTHs)。如果你更喜欢直接修改数据,那么你并不需要 Redux 。 10 | 11 | Action 一般长这样: 12 | 13 | ```js 14 | { type: 'ADD_TODO', text: 'Use Redux' } 15 | { type: 'REMOVE_TODO', id: 42 } 16 | { type: 'LOAD_ARTICLE', response: { ... } } 17 | ``` 18 | 19 | 一个约定俗成的是 actions 拥有一个定值 type 帮助 reducer (或 Flux 中的 Stores ) 识别它们。我们建议的你使用 string 而不是 [Symbols](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Symbol) 作为 action type ,因为 string 是可串行的,而使用 Symbols 的话你会把记录和重演变得比所需要的更难。 20 | 21 | 在 Flux 中,传统上认为你将每个 action type 定义为string定值: 22 | 23 | ```js 24 | const ADD_TODO = 'ADD_TODO'; 25 | const REMOVE_TODO = 'REMOVE_TODO'; 26 | const LOAD_ARTICLE = 'LOAD_ARTICLE'; 27 | ``` 28 | 29 | 这么做的优势?**人们通常声称定值不是必要的,对于小的项目可能是正确的。** 对于大的项目,将action types定义为定值有如下好处: 30 | 31 | * 帮助维护命名一致性,因为所有的 action type 汇总在同一位置。 32 | * 有的时候,在开发一个新功能之前你想看到所有现存的 actions 。可能的情况是你的团队里已经有人添加了你所需要的action,而你并不知道。 33 | * Action types 列表在Pull Request中能查到所有添加,删除,修改的记录。这能帮助团队中的所有人及时追踪新功能的范围与实现。 34 | * 如果你在导入一个 Action 定值的时候拼写错误,你会得到 `undefined` 。当你纳闷 action 被分发出去而什么也没发生的时候,一个拼写错误更容易被发现。 35 | 36 | 你的项目的约定取决与你自己。你开始的时候可能用的是inline string,之后转为定值,也许之后将他们归为一个独立文件。Redux 不会给予任何建议,选择你自己最喜欢的。 37 | 38 | ## Action Creators 39 | 40 | 另一个约定是,你创建生成 action 对象的函数,而不是在你分发的时候内联生成它们。 41 | 42 | 例如,用文字对象取代调用 `dispatch` : 43 | 44 | ```js 45 | // somewhere in an event handler 46 | dispatch({ 47 | type: 'ADD_TODO', 48 | text: 'Use Redux' 49 | }); 50 | ``` 51 | 52 | 你可以在单独的文件中写一个 action creator ,然后从 component 里导入: 53 | 54 | #### `actionCreators.js` 55 | 56 | ```js 57 | export function addTodo(text) { 58 | return { 59 | type: 'ADD_TODO', 60 | text 61 | }; 62 | } 63 | ``` 64 | 65 | #### `AddTodo.js` 66 | 67 | ```js 68 | import { addTodo } from './actionCreators'; 69 | 70 | // event handler 里的某处 71 | dispatch(addTodo('Use Redux')) 72 | ``` 73 | 74 | Action creators 总被当作模板受到批评。好吧,其实你并不用把他们写出来!**如果你觉得更适合你的项目你可以选用对象文字** 然而,你应该知道写 action creators 是存在某种优势的。 75 | 76 | 假设有个设计师看完我们的原型之后回来说,我们需要允许三个 todo 不能再多了。我们可以使用 [redux-thunk](https://github.com/gaearon/redux-thunk) 中间件添加一个提前退出,把我们的 action creator 重写成回调形式: 77 | 78 | ```js 79 | function addTodoWithoutCheck(text) { 80 | return { 81 | type: 'ADD_TODO', 82 | text 83 | }; 84 | } 85 | 86 | export function addTodo(text) { 87 | // Redux Thunk 中间件允许这种形式 88 | // 在下面的 “异步 Action Creators” 段落中有写 89 | return function (dispatch, getState) { 90 | if (getState().todos.length === 3) { 91 | // 提前退出 92 | return; 93 | } 94 | 95 | dispatch(addTodoWithoutCheck(text)); 96 | } 97 | } 98 | ``` 99 | 100 | 我们刚修改了 `addTodo` action creator 的行为,对调用它的代码完全不可见。**我们不用担心去看每个添加 todo 的地方保证他们有了这个检查** Action creator 让你可以解耦额外的分发 action 逻辑与实际的 components 发送这些 actions,而且当你在重开发经常要改变需求的时候也会非常有用。 101 | 102 | ### 生成 Action Creators 103 | 104 | 某些框架如 [Flummox](https://github.com/acdlite/flummox) 自动从 action creator 函数定义生成 action type 定值。这个想法是说你不需要 `ADD_TODO` 定值和 `addTodo()` action creator两个都自己定义。这样的方法在底层也生成 action type 定值,但他们是隐式生成的,也就是间接级。 105 | 106 | 我们不建议用这样的方法。如果你写像这样简单的 action creator 写烦了: 107 | 108 | ```js 109 | export function addTodo(text) { 110 | return { 111 | type: 'ADD_TODO', 112 | text 113 | }; 114 | } 115 | 116 | export function removeTodo(id) { 117 | return { 118 | type: 'REMOVE_TODO', 119 | id 120 | }; 121 | } 122 | ``` 123 | 124 | 你可以写一个生成 action creator 的函数: 125 | 126 | ```js 127 | function makeActionCreator(type, ...argNames) { 128 | return function(...args) { 129 | let action = { type }; 130 | argNames.forEach((arg, index) => { 131 | action[argNames[index]] = args[index]; 132 | }); 133 | return action; 134 | } 135 | } 136 | 137 | export const addTodo = makeActionCreator('ADD_TODO', 'todo'); 138 | export const removeTodo = makeActionCreator('REMOVE_TODO', 'id'); 139 | ``` 140 | 141 | 参见 [redux-action-utils](https://github.com/insin/redux-action-utils) 和 [redux-actions](https://github.com/acdlite/redux-actions) 获得更多介绍这样的常用工具。 142 | 143 | 注意这样的工具给你的代码添加了魔法。 144 | 魔法和间接声明真的值得多写一两行代码么? 145 | 146 | ## 异步 Action Creators 147 | 148 | [中间件](../Glossary.html#middleware) 让你注入一个定制逻辑,可以在每个 action 对象分发出去之前解释。异步 actions 是中间件的最常见用例。 149 | 150 | 没有中间件的话,[`dispatch`](../api/Store.md#dispatch) 只能接收一个普通对象。所以我们在 components 里面进行 AJAX 调用: 151 | 152 | #### `actionCreators.js` 153 | 154 | ```js 155 | export function loadPostsSuccess(userId, response) { 156 | return { 157 | type: 'LOAD_POSTS_SUCCESS', 158 | userId, 159 | response 160 | }; 161 | } 162 | 163 | export function loadPostsFailure(userId, error) { 164 | return { 165 | type: 'LOAD_POSTS_FAILURE', 166 | userId, 167 | error 168 | }; 169 | } 170 | 171 | export function loadPostsRequest(userId) { 172 | return { 173 | type: 'LOAD_POSTS_REQUEST', 174 | userId 175 | }; 176 | } 177 | ``` 178 | 179 | #### `UserInfo.js` 180 | 181 | ```js 182 | import { Component } from 'react'; 183 | import { connect } from 'react-redux'; 184 | import { loadPostsRequest, loadPostsSuccess, loadPostsFailure } from './actionCreators'; 185 | 186 | class Posts extends Component { 187 | loadData(userId) { 188 | // 调用 React Redux `connect()` 注入 props : 189 | let { dispatch, posts } = this.props; 190 | 191 | if (posts[userId]) { 192 | // 这里是被缓存的数据!啥也不做。 193 | return; 194 | } 195 | 196 | // Reducer 可以通过设置 `isFetching` 反应这个 action 197 | // 因此让我们显示一个 Spinner 控件。 198 | dispatch(loadPostsRequest(userId)); 199 | 200 | // Reducer 可以通过填写 `users` 反应这些 actions 201 | fetch(`http://myapi.com/users/${userId}/posts`).then( 202 | response => dispatch(loadPostsSuccess(userId, response)), 203 | error => dispatch(loadPostsFailure(userId, error)) 204 | ); 205 | } 206 | 207 | componentDidMount() { 208 | this.loadData(this.props.userId); 209 | } 210 | 211 | componentWillReceiveProps(nextProps) { 212 | if (nextProps.userId !== this.props.userId) { 213 | this.loadData(nextProps.userId); 214 | } 215 | } 216 | 217 | render() { 218 | if (this.props.isLoading) { 219 | return

    Loading...

    ; 220 | } 221 | 222 | let posts = this.props.posts.map(post => 223 | 224 | ); 225 | 226 | return
    {posts}
    ; 227 | } 228 | } 229 | 230 | export default connect(state => ({ 231 | posts: state.posts 232 | }))(Posts); 233 | ``` 234 | 235 | 然而,不久就需要再来一遍,因为不同的 components 从同样的 API 端点请求数据。而且,我们想要在多个components 中重用一些逻辑(比如,当缓存数据有效的时候提前退出)。 236 | 237 | **中间件让我们写的更清楚M的潜在的异步 action creators.** 它使得我们分发普通对象之外的东西,并且解释它们的值。比如,中间件能 “捕捉” 到已经分发的 Promises 并把他们变为一对请求和成功/失败 actions. 238 | 239 | 最简单的中间件例子是 [redux-thunk](https://github.com/gaearon/redux-thunk). **“Thunk” 中间件让你把 action creators 写成 “thunks”,也就是返回函数的函数。** 这使得控制被反转了: 你会像一个参数一样取得 `dispatch` ,所以你也能写一个多次分发的 action creator 。 240 | 241 | >##### 注意 242 | 243 | >Thunk 只是中间件的一个例子。中间件不是关于 “让你分发函数” 的:它是关于让你分发你用的特定中间件知道如何处理的任何东西的。Thunk 中间件添加了一个特定的行为用来分发函数,但这实际上取决于你用的中间件。 244 | 245 | 考虑上面的代码用 [redux-thunk](https://github.com/gaearon/redux-thunk) 重写: 246 | 247 | #### `actionCreators.js` 248 | 249 | ```js 250 | export function loadPosts(userId) { 251 | // 用 thunk 中间件解释: 252 | return function (dispatch, getState) { 253 | let { posts } = getState(); 254 | if (posts[userId]) { 255 | // 这里是数据缓存!啥也不做。 256 | return; 257 | } 258 | 259 | dispatch({ 260 | type: 'LOAD_POSTS_REQUEST', 261 | userId 262 | }); 263 | 264 | // 异步分发原味 actions 265 | fetch(`http://myapi.com/users/${userId}/posts`).then( 266 | response => dispatch({ 267 | type: 'LOAD_POSTS_SUCCESS', 268 | userId, 269 | respone 270 | }), 271 | error => dispatch({ 272 | type: 'LOAD_POSTS_FAILURE', 273 | userId, 274 | error 275 | }) 276 | ); 277 | } 278 | } 279 | ``` 280 | 281 | #### `UserInfo.js` 282 | 283 | ```js 284 | import { Component } from 'react'; 285 | import { connect } from 'react-redux'; 286 | import { loadPosts } from './actionCreators'; 287 | 288 | class Posts extends Component { 289 | componentDidMount() { 290 | this.props.dispatch(loadPosts(this.props.userId)); 291 | } 292 | 293 | componentWillReceiveProps(nextProps) { 294 | if (nextProps.userId !== this.props.userId) { 295 | this.props.dispatch(loadPosts(nextProps.userId)); 296 | } 297 | } 298 | 299 | render() { 300 | if (this.props.isLoading) { 301 | return

    Loading...

    ; 302 | } 303 | 304 | let posts = this.props.posts.map(post => 305 | 306 | ); 307 | 308 | return
    {posts}
    ; 309 | } 310 | } 311 | 312 | export default connect(state => ({ 313 | posts: state.posts 314 | }))(Posts); 315 | ``` 316 | 317 | 这样打得字少多了!如果你喜欢,你还是可以保留 “原味” action creators 比如从一个 “聪明的” `loadPosts` action creator 里用到的 `loadPostsSuccess` 。 318 | 319 | **最后,你可以重写中间件** 你可以把上面的模式泛化,然后代之以这样的异步 action creators : 320 | 321 | ```js 322 | export function loadPosts(userId) { 323 | return { 324 | // 要在之前和之后发送的 action types 325 | types: ['LOAD_POSTS_REQUEST', 'LOAD_POSTS_SUCCESS', 'LOAD_POSTS_FAILURE'], 326 | // 检查缓存 (可选): 327 | shouldCallAPI: (state) => !state.users[userId], 328 | // 进行取: 329 | callAPI: () => fetch(`http://myapi.com/users/${userId}/posts`), 330 | // 在 actions 的开始和结束注入的参数 331 | payload: { userId } 332 | }; 333 | } 334 | ``` 335 | 336 | 解释这个 actions 的中间件可以像这样: 337 | 338 | ```js 339 | function callAPIMiddleware({ dispatch, getState }) { 340 | return function (next) { 341 | return function (action) { 342 | const { 343 | types, 344 | callAPI, 345 | shouldCallAPI = () => true, 346 | payload = {} 347 | } = action; 348 | 349 | if (!types) { 350 | // 普通 action:传走 351 | return next(action); 352 | } 353 | 354 | if ( 355 | !Array.isArray(types) || 356 | types.length !== 3 || 357 | !types.every(type => typeof type === 'string') 358 | ) { 359 | throw new Error('Expected an array of three string types.'); 360 | } 361 | 362 | if (typeof callAPI !== 'function') { 363 | throw new Error('Expected fetch to be a function.'); 364 | } 365 | 366 | if (!shouldCallAPI(getState())) { 367 | return; 368 | } 369 | 370 | const [requestType, successType, failureType] = types; 371 | 372 | dispatch(Object.assign({}, payload, { 373 | type: requestType 374 | })); 375 | 376 | return callAPI().then( 377 | response => dispatch(Object.assign({}, payload, { 378 | response: response, 379 | type: successType 380 | })), 381 | error => dispatch(Object.assign({}, payload, { 382 | error: error, 383 | type: failureType 384 | })) 385 | ); 386 | }; 387 | }; 388 | } 389 | ``` 390 | 391 | 在传给 [`applyMiddleware(...middlewares)`](../api/applyMiddleware.md) 一次以后,你能用相同方式写你的 API-调用 action creators : 392 | 393 | ```js 394 | export function loadPosts(userId) { 395 | return { 396 | types: ['LOAD_POSTS_REQUEST', 'LOAD_POSTS_SUCCESS', 'LOAD_POSTS_FAILURE'], 397 | shouldCallAPI: (state) => !state.users[userId], 398 | callAPI: () => fetch(`http://myapi.com/users/${userId}/posts`), 399 | payload: { userId } 400 | }; 401 | } 402 | 403 | export function loadComments(postId) { 404 | return { 405 | types: ['LOAD_COMMENTS_REQUEST', 'LOAD_COMMENTS_SUCCESS', 'LOAD_COMMENTS_FAILURE'], 406 | shouldCallAPI: (state) => !state.posts[postId], 407 | callAPI: () => fetch(`http://myapi.com/posts/${postId}/comments`), 408 | payload: { postId } 409 | }; 410 | } 411 | 412 | export function addComment(postId, message) { 413 | return { 414 | types: ['ADD_COMMENT_REQUEST', 'ADD_COMMENT_SUCCESS', 'ADD_COMMENT_FAILURE'], 415 | callAPI: () => fetch(`http://myapi.com/posts/${postId}/comments`, { 416 | method: 'post', 417 | headers: { 418 | 'Accept': 'application/json', 419 | 'Content-Type': 'application/json' 420 | }, 421 | body: JSON.stringify({ message }) 422 | }), 423 | payload: { postId, message } 424 | }; 425 | } 426 | ``` 427 | 428 | ## Reducers 429 | 430 | Redux 用函数描述逻辑更新减少了模版里大量的 Flux stores 。函数比对象简单,比类更简单得多。 431 | 432 | 考虑这个 Flux store: 433 | 434 | ```js 435 | let _todos = []; 436 | 437 | export default const TodoStore = assign({}, EventEmitter.prototype, { 438 | getAll() { 439 | return _todos; 440 | } 441 | }); 442 | 443 | AppDispatcher.register(function (action) { 444 | switch (action.type) { 445 | case ActionTypes.ADD_TODO: 446 | let text = action.text.trim(); 447 | _todos.push(text); 448 | TodoStore.emitChange(); 449 | } 450 | }); 451 | ``` 452 | 453 | 用了 Redux 之后,同样的逻辑更新可以被写成 reducing function: 454 | 455 | ```js 456 | export function todos(state = [], action) { 457 | switch (action.type) { 458 | case ActionTypes.ADD_TODO: 459 | let text = action.text.trim(); 460 | return [...state, text]; 461 | default: 462 | return state; 463 | } 464 | } 465 | ``` 466 | 467 | `switch` 语句 *不是* 真正的模版。真正的 Flux 模版是概念性的:发送更新的需求,用 Dispatcher 注册 Store 的需求,Store 是对象的需求 (当你想要一个哪都能跑的 App 的时候复杂度会提升)。 468 | 469 | 不幸的是很多人仍然靠文档里用没用 `switch` 来选择 Flux 框架。如果你不爱用 `switch` 你可以用一个单独的函数来解决,下面会演示。 470 | 471 | ### 生成 Reducers 472 | 473 | 让我们写一个函数使得我们将 reducers 表达为 action types 到 handlers 的映射对象。例如,在我们的 `todos` reducer 里这样定义: 474 | 475 | ```js 476 | export const todos = createReducer([], { 477 | [ActionTypes.ADD_TODO](state, action) { 478 | let text = action.text.trim(); 479 | return [...state, text]; 480 | } 481 | } 482 | ``` 483 | 484 | 我们可以写下面的帮忙函数来完成: 485 | 486 | ```js 487 | function createReducer(initialState, handlers) { 488 | return function reducer(state = initialState, action) { 489 | if (handlers.hasOwnProperty(action.type)) { 490 | return handlers[action.type](state, action); 491 | } else { 492 | return state; 493 | } 494 | } 495 | } 496 | ``` 497 | 498 | 不难对吧?Redux 没有默认提供这样的帮忙函数,因为有好多种写的方法。可能你想要自动把普通 JS 对象变成不可变对象通过湿化服务器状态。可能你想合并返回状态和当前状态。有很多方法 “获取所有” handler。这些都取决于你为你的团队在特定项目中选择的约定。 499 | 500 | Redux reducer 的 API 是 `(state, action) => state`,但是怎么创建这些 reducers 由你来定。 501 | -------------------------------------------------------------------------------- /docs/advanced/Middleware.md: -------------------------------------------------------------------------------- 1 | # Middleware 2 | 3 | 我们已经在[异步Actions](../advanced/AsyncActions.md)一节的示例中看到了一些 middleware 的使用。如果你使用过 [Express](http://expressjs.com/) 或者 [Koa](http://koajs.com/) 等服务端框架, 那么应该对 *middleware* 的概念不会陌生。 在这类框架中,middleware 是指可以被嵌入在框架接收请求到产生响应过程之中的代码。例如,Express 或者 Koa 的 middleware 可以完成添加 CORS headers,记录日志,内容压缩等工作。middleware 最优秀的特性就是可以被链式组合。你可以在一个项目中使用多个独立的第三方 middleware。 4 | 5 | 相对于 Express 或者 Koa 的 middleware,Redux middleware 被用于解决不同的问题,但其中的概念是相类似的。**它提供的是位于 action 被发起之后,到达 reducer 之前的扩展点。** 你可以利用 Redux middleware 来进行日志记录、创建崩溃报告、调用异步接口或者路由等等。 6 | 7 | 这个章节分为两个部分,前面是帮助你理解相关概念的深度介绍,而后半部分则通过[一些实例](#seven-examples)来体现 middleware 的强大能力。对文章前后内容进行结合通读,会帮助你更好的理解枯燥的概念,并从中获得启发。 8 | 9 | ## 理解 Middleware 10 | 11 | 正因为 middleware 可以完成包括异步 API 调用在内的各种事情,了解它的演化过程是一件相当重要的事。我们将以记录日志和创建崩溃报告为例,引导你体会从分析问题到通过构建 middleware 解决问题的思维过程。 12 | 13 | ### 问题: 记录日志 14 | 15 | 使用 Redux 的一个益处就是它让 state 的变化过程变的可预知和透明。每当一个 action 发起完成,新的 state 就会被计算并保存下来。State 不能被自身修改,只能由特定的 action 引起变化。 16 | 17 | 试想一下,当我们的应用中每一个 action 被发起以及每次新的 state 被计算完成时都将它们记录下来,岂不是很好?当程序出现问题时,我们可以通过查阅日志找出是哪个 action 导致了 state 不正确。 18 | 19 | 20 | 21 | 我们如何通过 Redux 实现它呢? 22 | 23 | ### 尝试 #1: 手动记录 24 | 25 | 最直接的解决方案就是在每次调用 [`store.dispatch(action)`](../api/Store.md#dispatch) 前后手动记录被发起的 action 和新的 state。这称不上一个真正的解决方案,仅仅是我们理解这个问题的第一步。 26 | 27 | >##### 注意 28 | 29 | >如果你使用 [react-redux](https://github.com/gaearon/react-redux) 或者类似的绑定库,最好不要直接在你的组件中操作 store 的实例。在接下来的内容中,仅仅是假设你会显式的向下传递 store。 30 | 31 | 假设,你在创建一个 Todo 时这样调用: 32 | 33 | ```js 34 | store.dispatch(addTodo('Use Redux')); 35 | ``` 36 | 37 | 为了记录这个 action 一句产生的新的 state,你可以通过这种方式记录日志: 38 | 39 | ```js 40 | let action = addTodo('Use Redux'); 41 | 42 | console.log('dispatching', action); 43 | store.dispatch(action); 44 | console.log('next state', store.getState()); 45 | ``` 46 | 47 | 这样做达到了想要的效果,但是你一定不想每次都这么干。 48 | 49 | ### 尝试 #2: 封装 Dispatch 50 | 51 | 你可以将上面的操作抽取成一个函数: 52 | 53 | ```js 54 | function dispatchAndLog(store, action) { 55 | console.log('dispatching', action); 56 | store.dispatch(action); 57 | console.log('next state', store.getState()); 58 | } 59 | ``` 60 | 61 | 然后用它替换 `store.dispatch()`: 62 | 63 | ```js 64 | dispatchAndLog(store, addTodo('Use Redux')); 65 | ``` 66 | 你可以选择到此为止,但是每次都要导入一个外部方法总归还是不太方便。 67 | 68 | ### 尝试 #3: Monkeypatching Dispatch 69 | 70 | 如果我们直接替换 store 实例中的 `dispatch` 函数会怎么样呢?Redux store 只是一个包含[一些方法](../api/Store.md)的普通对象,同时我们使用的是 JavaScript,因此我们可以这样实现 `dispatch` 的 monkeypatch: 71 | 72 | ```js 73 | let next = store.dispatch; 74 | store.dispatch = function dispatchAndLog(action) { 75 | console.log('dispatching', action); 76 | let result = next(action); 77 | console.log('next state', store.getState()); 78 | return result; 79 | }; 80 | ``` 81 | 82 | 这离我们想要的已经非常接近了!无论我们在哪里发起 action,保证都会被记录。Monkeypatching 令人感觉还是不太舒服,但是利用它我们做到了我们想要的。 83 | 84 | ### 问题: 崩溃报告 85 | 86 | 如果我们想对 `dispatch` 附加**超过一个**的变换,又会怎么样呢? 87 | 88 | 我脑海中出现的另一个常用的变换就是在生产过程中报告 JavaScript 的错误。全局的 `window.onerror` 并不可靠,因为它在一些旧的浏览器中无法提供错误堆栈,而这是排查错误所需的至关重要信息。 89 | 90 | 试想当发起一个 action 的结果是一个异常时,我们将包含调用堆栈,引起错误的 action 以及当前的 state 等错误信息通通发到类似于 [Sentry](https://getsentry.com/welcome/) 这样的报告服务中,不是很好吗?这样我们可以更容易的在开发环境中重现这个错误。 91 | 92 | 然而,将日志记录和崩溃报告分离是很重要的。理想情况下,我们希望他们是两个不同的模块,也可能在不同的包中。否则我们无法构建一个由这些工具组成的生态系统。(提示:我们正在慢慢了解 middleware 的本质到底是什么!) 93 | 94 | 如果按照我们的想法,日志记录和崩溃报告是分离的工具,他们看起来应该像这样: 95 | 96 | ```js 97 | function patchStoreToAddLogging(store) { 98 | let next = store.dispatch; 99 | store.dispatch = function dispatchAndLog(action) { 100 | console.log('dispatching', action); 101 | let result = next(actionconsole.log('next state', store.getState());); 102 | console.log('next state', store.getState()); 103 | return result; 104 | }; 105 | } 106 | 107 | function patchStoreToAddCrashReporting(store) { 108 | let next = store.dispatch; 109 | store.dispatch = function dispatchAndReportErrors(action) { 110 | try { 111 | return next(action); 112 | } catch (err) { 113 | console.error('捕获一个异常!', err); 114 | Raven.captureException(err, { 115 | extra: { 116 | action, 117 | state: store.getState() 118 | } 119 | }); 120 | throw err; 121 | } 122 | }; 123 | } 124 | ``` 125 | 126 | 如果这些功能以不同的模块发布,我们可以在 store 中像这样使用它们: 127 | 128 | ```js 129 | patchStoreToAddLogging(store); 130 | patchStoreToAddCrashReporting(store); 131 | ``` 132 | 133 | 尽管如此,这种方式看起来还是不是够令人满意。 134 | 135 | ### 尝试 #4: 隐藏 Monkeypatching 136 | 137 | Monkeypatching 本质上是一种 hack。“将任意的方法替换成你想要的”,那是 API 会是什么样的呢?现在,让我们来看看这种替换的本质。 在之前,我们用自己的函数替换掉了 `store.dispatch`。如果我们不这样做,而是在函数中*返回*新的 `dispatch` 呢? 138 | 139 | ```js 140 | function logger(store) { 141 | let next = store.dispatch; 142 | 143 | // 我们之前的做法: 144 | // store.dispatch = function dispatchAndLog(action) { 145 | 146 | return function dispatchAndLog(action) { 147 | console.log('dispatching', action); 148 | let result = next(action); 149 | console.log('next state', store.getState()); 150 | return result; 151 | }; 152 | } 153 | ``` 154 | 155 | 我们可以在 Redux 内部提供一个可以将实际的 monkeypatching 应用到 `store.dispatch` 中的辅助方法: 156 | 157 | ```js 158 | function applyMiddlewareByMonkeypatching(store, middlewares) { 159 | middlewares = middlewares.slice(); 160 | middlewares.reverse(); 161 | 162 | // Transform dispatch function with each middleware. 163 | middlewares.forEach(middleware => 164 | store.dispatch = middleware(store) 165 | ); 166 | } 167 | ``` 168 | 169 | 然后像这样应用多个 middleware: 170 | 171 | ```js 172 | applyMiddlewareByMonkeypatching(store, [logger, crashReporter]); 173 | ``` 174 | 175 | 尽管我们做了很多,实现方式依旧是 monkeypatching。 176 | 因为我们仅仅是将它隐藏在我们的框架内部,并没有改变这个事实。 177 | 178 | ### 尝试 #5: 移除 Monkeypatching 179 | 180 | 为什么我们要替换原来的 `dispatch` 呢?当然,这样我们就可以在后面直接调用它,但是还有另一个原因:就是每一个 middleware 都可以操作(或者直接调用)前一个 middleware 包装过的 `store.dispatch`: 181 | 182 | ```js 183 | function logger(store) { 184 | // 这里的 next 必须指向前一个 middleware 返回的函数: 185 | let next = store.dispatch; 186 | 187 | return function dispatchAndLog(action) { 188 | console.log('dispatching', action); 189 | let result = next(action); 190 | console.log('next state', store.getState()); 191 | return result; 192 | }; 193 | } 194 | ``` 195 | 196 | 将 middleware 串连起来的必要性是显而易见的。 197 | 198 | 如果 `applyMiddlewareByMonkeypatching` 方法中没有在第一个 middleware 执行时立即替换掉 `store.dispatch`,那么 `store.dispatch` 将会一直指向原始的 `dispatch` 方法。也就是说,第二个 middleware 依旧会作用在原始的 `dispatch` 方法。 199 | 200 | 但是,还有另一种方式来实现这种链式调用的效果。可以让 middleware 以方法参数的形式接受一个 `next()` 方法来,而不是通过 store 的实例去获取。 201 | 202 | ```js 203 | function logger(store) { 204 | return function wrapDispatchToAddLogging(next) { 205 | return function dispatchAndLog(action) { 206 | console.log('dispatching', action); 207 | let result = next(action); 208 | console.log('next state', store.getState()); 209 | return result; 210 | }; 211 | } 212 | } 213 | ``` 214 | 现在是[“我们该更进一步”](http://knowyourmeme.com/memes/we-need-to-go-deeper)的时刻了,所以可能会多花一点时间来让它变的更为合理一些。这些层叠的函数让人很恐慌。ES6 的 arrow functions 可以让 [currying](https://en.wikipedia.org/wiki/Currying) 看起来更舒服一些: 215 | 216 | ```js 217 | const logger = store => next => action => { 218 | console.log('dispatching', action); 219 | let result = next(action); 220 | console.log('next state', store.getState()); 221 | return result; 222 | }; 223 | 224 | const crashReporter = store => next => action => { 225 | try { 226 | return next(action); 227 | } catch (err) { 228 | console.error('Caught an exception!', err); 229 | Raven.captureException(err, { 230 | extra: { 231 | action, 232 | state: store.getState() 233 | } 234 | }); 235 | throw err; 236 | } 237 | } 238 | ``` 239 | 240 | **这已经是真实的 Redux middleware 的样子了。** 241 | 242 | Middleware 接受一个 `next()` 发起函数,并返回一个发起函数,返回的函数会被作为下一个 middleware 的 `next()`,以此类推。由于 store 中类似 `getState()` 的方法依旧非常有用,我们将 `store` 作为顶层的参数,使得它可以在所有 middleware 中被使用。 243 | 244 | ### 尝试 #6: “青涩”地使用 Middleware 245 | 246 | 我们可以写一个 `applyMiddleware()` 方法替换掉原来的 `applyMiddlewareByMonkeypatching()`。在新的 `applyMiddleware()` 中,我们取得最终完整的被包装过的 `dispatch()` 函数,并返回一个 store 的副本: 247 | 248 | ```js 249 | // 警告:这只是一种朴素的实现 250 | // 这 *并不是* Redux 的 API. 251 | 252 | function applyMiddleware(store, middlewares) { 253 | middlewares = middlewares.slice(); 254 | middlewares.reverse(); 255 | 256 | let dispatch = store.dispatch; 257 | middlewares.forEach(middleware => 258 | dispatch = middleware(store)(dispatch) 259 | ); 260 | 261 | return Object.assign({}, store, { dispatch }); 262 | } 263 | ``` 264 | 265 | 这与 Redux 中 [`applyMiddleware()`](../api/applyMiddleware.md) 的实现已经很接近了,但是**有三个重要的不同之处**: 266 | 267 | * 它只暴露一个 [store API](../api/Store.md) 的子集给 middleware:[`dispatch(action)`](../api/Store.md#dispatch) and [`getState()`](../api/Store.md#getState). 268 | 269 | * 它用了一个非常巧妙的方式来保证你的 middleware 调用的是 `store.dispatch(action)` 而不是 `next(action)`,从而使这个 action 会在包括当前 middleware 在内的整个 middleware 链中被正确的传递。这对异步的 middleware 非常有用,正如我们在[之前的章节](AsyncActions.md)中看到的。 270 | 271 | * 为了保证你只能应用 middleware 一次,它作用在 `createStore()` 上而不是 `store` 本身。因此它的签名不是 `(store, middlewares) => store`, 而是 `(...middlewares) => (createStore) => createStore`。 272 | 273 | ### 最终的方法 274 | 275 | 这是我们刚刚所写的 middleware: 276 | 277 | ```js 278 | const logger = store => next => action => { 279 | console.log('dispatching', action); 280 | let result = next(action); 281 | console.log('next state', store.getState()); 282 | return result; 283 | }; 284 | 285 | const crashReporter = store => next => action => { 286 | try { 287 | return next(action); 288 | } catch (err) { 289 | console.error('Caught an exception!', err); 290 | Raven.captureException(err, { 291 | extra: { 292 | action, 293 | state: store.getState() 294 | } 295 | }); 296 | throw err; 297 | } 298 | } 299 | ``` 300 | 301 | 然后是将它们引用到 Redux store 中: 302 | 303 | ```js 304 | import { createStore, combineReducers, applyMiddleware } from 'redux'; 305 | 306 | // applyMiddleware 接收 createStore() 307 | // 并返回一个包含兼容 API 的函数。 308 | let createStoreWithMiddleware = applyMiddleware(logger, crashReporter)(createStore); 309 | 310 | // 像使用 createStore() 一样使用它。 311 | let todoApp = combineReducers(reducers); 312 | let store = createStoreWithMiddleware(todoApp); 313 | ``` 314 | 315 | 就是这样!现在任何被发送到 store 的 action 都会经过 `logger` and `crashReporter`: 316 | 317 | ```js 318 | // 将经过 logger 和 crashReporter 两个 middleware! 319 | store.dispatch(addTodo('Use Redux')); 320 | ``` 321 | 322 | ## 7个示例 323 | 324 | 如果读完上面的章节你已经觉得头都要爆了,那就想象以下它写出来之后的样子。下面的内容会让我们放松一下,并让你的思路延续。 325 | 326 | 下面的每个函数都是一个有效的 Redux middleware。它们并不都一样有用,但是至少他们一样有趣。 327 | 328 | ```js 329 | /** 330 | * 记录所有被发起的 action 以及产生的新的 state。 331 | */ 332 | const logger = store => next => action => { 333 | console.group(action.type); 334 | console.info('dispatching', action); 335 | let result = next(action); 336 | console.log('next state', store.getState()); 337 | console.groupEnd(action.type); 338 | return result; 339 | }; 340 | 341 | /** 342 | * 在 state 更新完成和 listener 被通知之后发送崩溃报告。 343 | */ 344 | const crashReporter = store => next => action => { 345 | try { 346 | return next(action); 347 | } catch (err) { 348 | console.error('Caught an exception!', err); 349 | Raven.captureException(err, { 350 | extra: { 351 | action, 352 | state: store.getState() 353 | } 354 | }); 355 | throw err; 356 | } 357 | } 358 | 359 | /** 360 | * 用 { meta: { delay: N } } 来让 action 延迟 N 毫秒。 361 | * 在这个案例中,让 `dispatch` 返回一个取消 timeout 的函数。 362 | */ 363 | const timeoutScheduler = store => next => action => { 364 | if (!action.meta || !action.meta.delay) { 365 | return next(action); 366 | } 367 | 368 | let timeoutId = setTimeout( 369 | () => next(action), 370 | action.meta.delay 371 | ); 372 | 373 | return function cancel() { 374 | clearTimeout(timeoutId); 375 | }; 376 | }; 377 | 378 | /** 379 | * 通过 { meta: { raf: true } } 让 action 在一个 rAF 循环帧中被发起。 380 | * 在这个案例中,让 `dispatch` 返回一个从队列中移除该 action 的函数。 381 | */ 382 | const rafScheduler = store => next => { 383 | let queuedActions = []; 384 | let frame = null; 385 | 386 | function loop() { 387 | frame = null; 388 | try { 389 | if (queuedActions.length) { 390 | next(queuedActions.shift()); 391 | } 392 | } finally { 393 | maybeRaf(); 394 | } 395 | } 396 | 397 | function maybeRaf() { 398 | if (queuedActions.length && !frame) { 399 | frame = requestAnimationFrame(loop); 400 | } 401 | } 402 | 403 | return action => { 404 | if (!action.meta || !action.meta.raf) { 405 | return next(action); 406 | } 407 | 408 | queuedActions.push(action); 409 | maybeRaf(); 410 | 411 | return function cancel() { 412 | queuedActions = queuedActions.filter(a => a !== action) 413 | }; 414 | }; 415 | }; 416 | 417 | /** 418 | * 使你除了 action 之外还可以发起 promise。 419 | * 如果这个 promise 被 resolved,他的结果将被作为 action 发起。 420 | * 这个 promise 会被 `dispatch` 返回,因此调用者可以处理 rejection。 421 | */ 422 | const vanillaPromise = store => next => action => { 423 | if (typeof action.then !== 'function') { 424 | return next(action); 425 | } 426 | 427 | return Promise.resolve(action).then(store.dispatch); 428 | }; 429 | 430 | /** 431 | * 让你可以发起带有一个 { promise } 属性的特殊 action。 432 | * 433 | * 这个 middleware 会在开始时发起一个 action,并在这个 `promise` resolve 时发起另一个成功(或失败)的 action。 434 | * 435 | * 为了方便起见,`dispatch` 会返回这个 promise 让调用者可以等待。 436 | */ 437 | const readyStatePromise = store => next => action => { 438 | if (!action.promise) { 439 | return next(action) 440 | } 441 | 442 | function makeAction(ready, data) { 443 | let newAction = Object.assign({}, action, { ready }, data); 444 | delete newAction.promise; 445 | return newAction; 446 | } 447 | 448 | next(makeAction(false)); 449 | return action.promise.then( 450 | result => next(makeAction(true, { result })), 451 | error => next(makeAction(true, { error })) 452 | ); 453 | }; 454 | 455 | /** 456 | * 让你可以发起一个函数来替代 action。 457 | * 这个函数接收 `dispatch` 和 `getState` 作为参数。 458 | * 459 | * 对于(根据 `getState()` 的情况)提前退出,或者异步控制流( `dispatch()` 一些其他东西)来说,这非常有用。 460 | * 461 | * `dispatch` 会返回被发起函数的返回值。 462 | */ 463 | const thunk = store => next => action => 464 | typeof action === 'function' ? 465 | action(store.dispatch, store.getState) : 466 | next(action); 467 | 468 | // 你可以使用以上全部的 middleware!(当然,这不意味着你必须全都使用。) 469 | let createStoreWithMiddleware = applyMiddleware( 470 | rafScheduler, 471 | timeoutScheduler, 472 | thunk, 473 | vanillaPromise, 474 | readyStatePromise, 475 | logger, 476 | crashReporter 477 | )(createStore); 478 | let todoApp = combineReducers(reducers); 479 | let store = createStoreWithMiddleware(todoApp); 480 | ``` -------------------------------------------------------------------------------- /docs/advanced/AsyncActions.md: -------------------------------------------------------------------------------- 1 | # 异步 Action 2 | 3 | 在[基础教程](../basics/README.md)中,我们创建了一个简单的 todo 应用。它只有同步操作。每当 dispatch action 时,state 会被立即更新。 4 | 5 | 在本教程中,我们将开发一个不同的,异步的应用。它将使用 Reddit API 来获取并显示指定 reddit 下的帖子列表。那么 Redux 究竟是如何处理异步数据流的呢? 6 | 7 | ## Action 8 | 9 | 当调用异步 API 时,有两个非常关键的时刻:发起请求的时刻,和接收到响应的时刻 (也可能是超时)。 10 | 11 | 这两个时刻都可能会更改应用的 state;为此,你需要 dispatch 普通的同步 action。一般情况下,每个 API 请求都至少需要 dispatch 三个不同的 action: 12 | 13 | * **一个通知 reducer 请求开始的 action。** 14 | 15 | 对于这种 action,reducer 可能会切换一下 state 中的 `isFetching` 标记。以此来告诉 UI 来显示进度条。 16 | 17 | * **一个通知 reducer 请求成功结束的 action。** 18 | 19 | 对于这种 action,reducer 可能会把接收到的新数据合并到 state 中,并重置 `isFetching`。UI 则会隐藏进度条,并显示接收到的数据。 20 | 21 | * **一个通知 reducer 请求失败的 action。** 22 | 23 | 对于这种 action,reducer 可能会重置 `isFetching`。或者,有些 reducer 会保存这些失败信息,并在 UI 里显示出来。 24 | 25 | 为了区分这三种 action,可能在 action 里添加一个专门的 `status` 字段作为标记位: 26 | 27 | ```js 28 | { type: 'FETCH_POSTS' } 29 | { type: 'FETCH_POSTS', status: 'error', error: 'Oops' } 30 | { type: 'FETCH_POSTS', status: 'success', response: { ... } } 31 | ``` 32 | 33 | 又或者为它们定义不同的 type: 34 | 35 | ```js 36 | { type: 'FETCH_POSTS_REQUEST' } 37 | { type: 'FETCH_POSTS_FAILURE', error: 'Oops' } 38 | { type: 'FETCH_POSTS_SUCCESS', response: { ... } } 39 | ``` 40 | 41 | 究竟使用带有标记位的同一个 action,还是多个 action type 呢,完全取决于你。这应该是你的团队共同达成的约定。使用多个 type 会降低犯错误的机率,但是如果你使用像 [redux-actions](https://github.com/acdlite/redux-actions) 这类的辅助库来生成 action creator 和 reducer 的话,这完成就不是问题了。 42 | 43 | 无论使用哪种约定,一定要在整个应用中保持统一。 44 | 在本教程中,我们将使用不同的 type 来做。 45 | 46 | ## 同步 Action Creator 47 | 48 | 下面先定义几个同步的 action type 和 action creator。比如,用户可以选择要显示的 reddit: 49 | 50 | ```js 51 | export const SELECT_REDDIT = 'SELECT_REDDIT'; 52 | 53 | export function selectReddit(reddit) { 54 | return { 55 | type: SELECT_REDDIT, 56 | reddit 57 | }; 58 | } 59 | ``` 60 | 61 | 也可以按 "刷新" 按钮来更新它: 62 | 63 | ```js 64 | export const INVALIDATE_REDDIT = 'INVALIDATE_REDDIT'; 65 | 66 | export function invalidateReddit(reddit) { 67 | return { 68 | type: INVALIDATE_REDDIT, 69 | reddit 70 | }; 71 | } 72 | ``` 73 | 74 | 这些是用户操作来控制的 action。也有另外一类 action,由网络请求来控制。后面会介绍如何使用它们,现在,我们只是来定义它们。 75 | 76 | 当需要请求指定 reddit 的帖子的时候,需要 dispatch `REQUEST_POSTS` action: 77 | 78 | ```js 79 | export const REQUEST_POSTS = 'REQUEST_POSTS'; 80 | 81 | export function requestPosts(reddit) { 82 | return { 83 | type: REQUEST_POSTS, 84 | reddit 85 | }; 86 | } 87 | ``` 88 | 89 | 把 `SELECT_REDDIT` 和 `INVALIDATE_REDDIT` 分开很重要。虽然它们的发生有先后顺序,随着应用变得复杂,有些用户操作(比如,预加载最流行的 reddit,或者一段时间后自动刷新过期数据)后需要马上请求数据。路由变化时也可能需要请求数据,所以一开始如果把请求数据和特定的 UI 事件耦合到一起是不明智的。 90 | 91 | 最后,当收到请求响应时,我们会 dispatch `RECEIVE_POSTS`: 92 | 93 | ```js 94 | export const RECEIVE_POSTS = 'RECEIVE_POSTS'; 95 | 96 | export function receivePosts(reddit, json) { 97 | return { 98 | type: RECEIVE_POSTS, 99 | reddit, 100 | posts: json.data.children.map(child => child.data), 101 | receivedAt: Date.now() 102 | }; 103 | } 104 | ``` 105 | 106 | 以上就是现在需要知道的所有内容。稍后会介绍如何把 dispatch action 与网络请求结合起来。 107 | 108 | >##### 错误处理须知 109 | 110 | >在实际应用中,网络请求失败时也需要 dispatch action。虽然在本教程中我们并不做错误处理,但是这个 [真实场景的案例](../introduction/Examples.html#real-world) 会演示一种实现方案。 111 | 112 | ## 设计 state 结构 113 | 114 | 就像在基础教程中,在功能开发前你需要 [设计应用的 state 结构](../basics/Reducers.md#designing-the-state-shape)。在写同步代码的时候,需要考虑更多的 state,所以我们要仔细考虑一下。 115 | 116 | 这部分内容通常让初学者感到迷惑,因为选择哪些信息才能清晰地描述异步应用的 state 并不直观,还有怎么用一个树来把这些信息组织起来。 117 | 118 | 我们以最通用的案例来打头:列表。Web 应用经常需要展示一些内容的列表。比如,贴子的列表,朋友的列表。首先要明确应用要显示哪些列表。然后把它们分开储存在 state 中,这样你才能对它们分别做缓存并且在需要的时候再次请求更新数据。 119 | 120 | "Reddit 头条" 应用会长这个样子: 121 | 122 | ```js 123 | { 124 | selectedReddit: 'frontend', 125 | postsByReddit: { 126 | frontend: { 127 | isFetching: true, 128 | didInvalidate: false, 129 | items: [] 130 | }, 131 | reactjs: { 132 | isFetching: false, 133 | didInvalidate: false, 134 | lastUpdated: 1439478405547, 135 | items: [{ 136 | id: 42, 137 | title: 'Confusion about Flux and Relay' 138 | }, { 139 | id: 500, 140 | title: 'Creating a Simple Application Using React JS and Flux Architecture' 141 | }] 142 | } 143 | } 144 | } 145 | ``` 146 | 147 | 下面列出几个要点: 148 | 149 | * 分开存储 reddit 信息,是为了缓存所有 reddit。当用户来回切换 reddit 时,可以立即更新,同时在不需要的时候可以不请求数据。不要担心把所有帖子放到内存中(会浪费内存):除非你需要处理成千上万条帖子,而且用户通常不会关闭标签,你不需要做任何清理。 150 | 151 | * 每个帖子的列表都需要使用 `isFetching` 来显示进度条,`didInvalidate` 来标记数据是否过期,`lastUpdated` 来存放数据最后更新时间,还有 `items` 存放列表信息本身。在实际应用中,你还需要存放 `fetchedPageCount` 和 `nextPageUrl` 这样分页相关的 state。 152 | 153 | >##### 嵌套内容须知 154 | 155 | >在这个示例中,接收到的列表和分页信息是存在一起的。但是,这种做法并不适用于有互相引用的嵌套内容的场景,或者用户可以编辑列表的场景。想像一下用户需要编辑一个接收到的帖子,但这个帖子在 state tree 的多个位置重复出现。这会让开发变得非常困难。 156 | 157 | >如果你有嵌套内容,或者用户可以编辑接收到的内容,你需要把它们分开存放在 state 中,就像数据库中一样。在分页信息中,只使用它们的 ID 来引用。这可以让你始终保持数据更新。[真实场景的案例](../introduction/Examples.html#real-world) 中演示了这种做法,结合 [normalizr](https://github.com/gaearon/normalizr) 来把嵌套的 API 响应数据范式化,最终的 state 看起来是这样: 158 | 159 | >```js 160 | > { 161 | > selectedReddit: 'frontend', 162 | > entities: { 163 | > users: { 164 | > 2: { 165 | > id: 2, 166 | > name: 'Andrew' 167 | > } 168 | > }, 169 | > posts: { 170 | > 42: { 171 | > id: 42, 172 | > title: 'Confusion about Flux and Relay', 173 | > author: 2 174 | > }, 175 | > 100: { 176 | > id: 100, 177 | > title: 'Creating a Simple Application Using React JS and Flux Architecture', 178 | > author: 2 179 | > } 180 | > } 181 | > }, 182 | > postsByReddit: { 183 | > frontend: { 184 | > isFetching: true, 185 | > didInvalidate: false, 186 | > items: [] 187 | > }, 188 | > reactjs: { 189 | > isFetching: false, 190 | > didInvalidate: false, 191 | > lastUpdated: 1439478405547, 192 | > items: [42, 100] 193 | > } 194 | > } 195 | > } 196 | >``` 197 | 198 | >在本教程中,我们不会对内容进行范式化,但是在一个复杂些的应用中你可能需要使用。 199 | 200 | ## 处理 Action 201 | 202 | 在讲 dispatch action 与网络请求结合使用细节前,我们为上面定义的 action 开发一些 reducer。 203 | 204 | >##### Reducer 组合须知 205 | 206 | >这里,我们假设你已经学习过 [`combineReducers()`](../api/combineReducers.md) 并理解 reducer 组合,还有 [基础章节](../basics/README.md) 中的 [拆分 Reducer](../basics/Reducers.md#splitting-reducers)。如果还没有,请[先学习](../basics/Reducers.md#splitting-reducers)。 207 | 208 | #### `reducers.js` 209 | 210 | ```js 211 | import { combineReducers } from 'redux'; 212 | import { 213 | SELECT_REDDIT, INVALIDATE_REDDIT, 214 | REQUEST_POSTS, RECEIVE_POSTS 215 | } from '../actions'; 216 | 217 | function selectedReddit(state = 'reactjs', action) { 218 | switch (action.type) { 219 | case SELECT_REDDIT: 220 | return action.reddit; 221 | default: 222 | return state; 223 | } 224 | } 225 | 226 | function posts(state = { 227 | isFetching: false, 228 | didInvalidate: false, 229 | items: [] 230 | }, action) { 231 | switch (action.type) { 232 | case INVALIDATE_REDDIT: 233 | return Object.assign({}, state, { 234 | didInvalidate: true 235 | }); 236 | case REQUEST_POSTS: 237 | return Object.assign({}, state, { 238 | isFetching: true, 239 | didInvalidate: false 240 | }); 241 | case RECEIVE_POSTS: 242 | return Object.assign({}, state, { 243 | isFetching: false, 244 | didInvalidate: false, 245 | items: action.posts, 246 | lastUpdated: action.receivedAt 247 | }); 248 | default: 249 | return state; 250 | } 251 | } 252 | 253 | function postsByReddit(state = {}, action) { 254 | switch (action.type) { 255 | case INVALIDATE_REDDIT: 256 | case RECEIVE_POSTS: 257 | case REQUEST_POSTS: 258 | return Object.assign({}, state, { 259 | [action.reddit]: posts(state[action.reddit], action) 260 | }); 261 | default: 262 | return state; 263 | } 264 | } 265 | 266 | const rootReducer = combineReducers({ 267 | postsByReddit, 268 | selectedReddit 269 | }); 270 | 271 | export default rootReducer; 272 | ``` 273 | 274 | 上面代码有两个有趣的点: 275 | 276 | * 使用 ES6 计算属性语法,使用 `Object.assign()` 来简洁高效地更新 `state[action.reddit]`。这个: 277 | 278 | ```js 279 | return Object.assign({}, state, { 280 | [action.reddit]: posts(state[action.reddit], action) 281 | }); 282 | ``` 283 | 与下面代码等价: 284 | 285 | ```js 286 | let nextState = {}; 287 | nextState[action.reddit] = posts(state[action.reddit], action); 288 | return Object.assign({}, state, nextState); 289 | ``` 290 | 291 | * 我们提取出 `posts(state, action)` 来管理指定帖子列表的 state。这仅仅使用 [reducer 组合](../basics/Reducers.md#splitting-reducers)而已!我们还可以借此机会把 reducer 分拆成更小的 reducer,这种情况下,我们把对象内列表的更新代理到了 `posts` reducer 上。在[真实场景的案例](../introduction/Examples.html#real-world)中甚至更进一步,里面介绍了如何做一个 reducer 工厂来生成参数化的分页 reducer。 292 | 293 | 记住 reducer 只是函数而已,所以你可以尽情使用函数组合和高阶函数这些特性。 294 | 295 | ## 异步 Action Creator 296 | 297 | 最后,如何把[之前定义](#synchronous-action-creators)的同步 action creator 和 网络请求结合起来呢?标准的做法是使用 [Redux Thunk middleware](https://github.com/gaearon/redux-thunk)。要引入 `redux-thunk` 这个专门的库才能使用。我们[后面](Middleware.md)会介绍 middleware 大体上是如何工作的;目前,你只需要知道一个要点:通过使用指定的 middleware,action creator 除了返回 action 对象外还可以返回函数。这时,这个 action creator 就成为了 [thunk](https://en.wikipedia.org/wiki/Thunk)。 298 | 299 | 当 action creator 返回函数时,这个函数会被 Redux Thunk middleware 执行。这个函数并不需要保持纯净;它还可以带有副作用,包括执行异步 API 请求。这个函数还可以 dispatch action,就像 dispatch 前面定义的同步 action 一样。 300 | 301 | 我们仍可以在 `actions.js` 里定义这些特殊的 thunk action creator。 302 | 303 | #### `actions.js` 304 | 305 | ```js 306 | import fetch from 'isomorphic-fetch'; 307 | 308 | export const REQUEST_POSTS = 'REQUEST_POSTS'; 309 | function requestPosts(reddit) { 310 | return { 311 | type: REQUEST_POSTS, 312 | reddit 313 | }; 314 | } 315 | 316 | export const RECEIVE_POSTS = 'RECEIVE_POSTS' 317 | function receivePosts(reddit, json) { 318 | return { 319 | type: RECEIVE_POSTS, 320 | reddit, 321 | posts: json.data.children.map(child => child.data), 322 | receivedAt: Date.now() 323 | }; 324 | } 325 | 326 | // 来看一下我们写的第一个 thunk action creator! 327 | // 虽然内部操作不同,你可以像其它 action creator 一样使用它: 328 | // store.dispatch(fetchPosts('reactjs')); 329 | 330 | export function fetchPosts(reddit) { 331 | 332 | // Thunk middleware 知道如何处理函数。 333 | // 这里把 dispatch 方法通过参数的形式参给函数, 334 | // 以此来让它自己也能 dispatch action。 335 | 336 | return function (dispatch) { 337 | 338 | // 首次 dispatch:更新应用的 state 来通知 339 | // API 请求发起了。 340 | 341 | dispatch(requestPosts(reddit)); 342 | 343 | // thunk middleware 调用的函数可以有返回值, 344 | // 它会被当作 dispatch 方法的返回值传递。 345 | 346 | // 这个案例中,我们返回一个等待处理的 promise。 347 | // 这并不是 redux middleware 所必须的,但是我们的一个约定。 348 | 349 | return fetch(`http://www.reddit.com/r/${reddit}.json`) 350 | .then(response => response.json()) 351 | .then(json => 352 | 353 | // 可以多次 dispatch! 354 | // 这里,使用 API 请求结果来更新应用的 state。 355 | 356 | dispatch(receivePosts(reddit, json)) 357 | ); 358 | 359 | // 在实际应用中,还需要 360 | // 捕获网络请求的异常。 361 | }; 362 | } 363 | ``` 364 | 365 | >##### `fetch` 使用须知 366 | 367 | >本示例使用了 [`fetch` API](https://developer.mozilla.org/en/docs/Web/API/Fetch_API)。它是替代 `XMLHttpRequest` 用来发送网络请求的非常新的 API。由于目前大多数浏览器原生还不支持它,建议你使用 [`isomorphic-fetch`](https://github.com/matthew-andrews/isomorphic-fetch) 库: 368 | 369 | >```js 370 | // 每次使用 `fetch` 前都这样调用一下 371 | >import fetch from 'isomorphic-fetch'; 372 | >``` 373 | 374 | >在底层,它在浏览器端使用 [`whatwg-fetch` polyfill](https://github.com/github/fetch),在服务器端使用 [`node-fetch`](https://github.com/bitinn/node-fetch),所以如果当你把应用改成[同构](https://medium.com/@mjackson/universal-javascript-4761051b7ae9)时,并不需要改变 API 请求。 375 | 376 | >注意,`fetch` polyfill 假设你已经使用了 [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) 的 polyfill。确保你使用 Promise polyfill 的一个最简单的办法是在所有应用代码前启用 Babel 的 ES6 polyfill: 377 | 378 | >```js 379 | >// 在应用中其它任何代码执行前调用一次 380 | >import 'babel-core/polyfill'; 381 | >``` 382 | 383 | 我们是如何在 dispatch 机制中引入 Redux Thunk middleware 的呢?我们使用了 [`applyMiddleware()`](../api/applyMiddleware.md),如下: 384 | 385 | #### `index.js` 386 | 387 | ```js 388 | import thunkMiddleware from 'redux-thunk'; 389 | import createLogger from 'redux-logger'; 390 | import { createStore, applyMiddleware } from 'redux'; 391 | import { selectReddit, fetchPosts } from './actions'; 392 | import rootReducer from './reducers'; 393 | 394 | const loggerMiddleware = createLogger(); 395 | 396 | const createStoreWithMiddleware = applyMiddleware( 397 | thunkMiddleware, // 允许我们 dispatch() 函数 398 | loggerMiddleware // 一个很便捷的 middleware,用来打印 action 日志 399 | )(createStore); 400 | 401 | const store = createStoreWithMiddleware(rootReducer); 402 | 403 | store.dispatch(selectReddit('reactjs')); 404 | store.dispatch(fetchPosts('reactjs')).then(() => 405 | console.log(store.getState()) 406 | ); 407 | ``` 408 | 409 | thunk 的一个优点是它的结果可以再次被 dispatch: 410 | 411 | #### `actions.js` 412 | 413 | ```js 414 | import fetch from 'isomorphic-fetch'; 415 | 416 | export const REQUEST_POSTS = 'REQUEST_POSTS'; 417 | function requestPosts(reddit) { 418 | return { 419 | type: REQUEST_POSTS, 420 | reddit 421 | }; 422 | } 423 | 424 | export const RECEIVE_POSTS = 'RECEIVE_POSTS' 425 | function receivePosts(reddit, json) { 426 | return { 427 | type: RECEIVE_POSTS, 428 | reddit, 429 | posts: json.data.children.map(child => child.data), 430 | receivedAt: Date.now() 431 | }; 432 | } 433 | 434 | function fetchPosts(reddit) { 435 | return dispatch => { 436 | dispatch(requestPosts(reddit)); 437 | return fetch(`http://www.reddit.com/r/${reddit}.json`) 438 | .then(response => response.json()) 439 | .then(json => dispatch(receivePosts(reddit, json))); 440 | }; 441 | } 442 | 443 | function shouldFetchPosts(state, reddit) { 444 | const posts = state.postsByReddit[reddit]; 445 | if (!posts) { 446 | return true; 447 | } else if (posts.isFetching) { 448 | return false; 449 | } else { 450 | return posts.didInvalidate; 451 | } 452 | } 453 | 454 | export function fetchPostsIfNeeded(reddit) { 455 | 456 | // 注意这个函数也接收了 getState() 方法 457 | // 它让你选择接下来 dispatch 什么。 458 | 459 | // 这对缓存命中时 460 | // 减少网络请求很有用。 461 | 462 | return (dispatch, getState) => { 463 | if (shouldFetchPosts(getState(), reddit)) { 464 | // 在 thunk 里 dispatch 另一个 thunk! 465 | return dispatch(fetchPosts(reddit)); 466 | } else { 467 | // 告诉调用代码不需要再等待。 468 | return Promise.resolve(); 469 | } 470 | }; 471 | } 472 | ``` 473 | 474 | 这可以让我们逐步开发复杂的异步控制流,同时保持代码整洁如初: 475 | 476 | #### `index.js` 477 | 478 | ```js 479 | store.dispatch(fetchPostsIfNeeded('reactjs')).then(() => 480 | console.log(store.getState()); 481 | ); 482 | ``` 483 | 484 | >##### 服务端渲染须知 485 | 486 | >异步 action creator 对于做服务端渲染非常方便。你可以创建一个 store,dispatch 一个异步 action creator,这个 action creator 又 dispatch 另一个异步 action creator 来为应用的一整块请求数据,同时在 Promise 完成和结束时才 render 界面。然后在 render 前,store 里就已经存在了需要用的 state。 487 | 488 | [Thunk middleware](https://github.com/gaearon/redux-thunk) 并不是 Redux 处理异步 action 的惟一方式。你也可以使用 [redux-promise](https://github.com/acdlite/redux-promise) 或者 [redux-promise-middleware](https://github.com/pburtchaell/redux-promise-middleware) 来 dispatch Promise 而不是函数。你也可以使用 [redux-rx](https://github.com/acdlite/redux-rx) dispatch Observable。你甚至可以写一个自定义的 middleware 来描述 API 请求,就像这个[真实场景的案例](../introduction/Examples.html#real-world)中的做法一样。你也可以先尝试一些不同做法,选择喜欢的,并使用下去,不论有没有使用到 middleware 都行。 489 | 490 | ## 连接到 UI 491 | 492 | Dispatch 同步 action 与异步 action 间并没有区别,所以就不展开讨论细节了。参照[搭配 React](../basics/UsageWithReact.md) 获得 React 组件中使用 Redux 的介绍。参照 [Example: Reddit API](ExampleRedditAPI.md) 来获取本例的完整代码。 493 | 494 | ## 下一步 495 | 496 | 阅读 [异步数据流](AsyncFlow.md) 来整理一下 异步 action 是如何适用于 Redux 数据流的。 --------------------------------------------------------------------------------