{"id":13672404,"url":"https://github.com/kenberkeley/redux-simple-tutorial","last_synced_at":"2025-05-15T13:02:25.913Z","repository":{"id":50276272,"uuid":"65563244","full_name":"kenberkeley/redux-simple-tutorial","owner":"kenberkeley","description":"Redux 简明教程。本教程深入浅出，配套入门、进阶源码解读以及文档注释丰富的 Demo 等一条龙服务","archived":false,"fork":false,"pushed_at":"2021-02-07T03:17:07.000Z","size":93,"stargazers_count":2620,"open_issues_count":1,"forks_count":381,"subscribers_count":114,"default_branch":"master","last_synced_at":"2025-05-13T09:17:44.635Z","etag":null,"topics":["redux","tutorial"],"latest_commit_sha":null,"homepage":"","language":null,"has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/kenberkeley.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2016-08-12T15:24:50.000Z","updated_at":"2025-05-07T10:21:18.000Z","dependencies_parsed_at":"2022-08-25T14:11:55.989Z","dependency_job_id":null,"html_url":"https://github.com/kenberkeley/redux-simple-tutorial","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kenberkeley%2Fredux-simple-tutorial","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kenberkeley%2Fredux-simple-tutorial/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kenberkeley%2Fredux-simple-tutorial/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kenberkeley%2Fredux-simple-tutorial/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/kenberkeley","download_url":"https://codeload.github.com/kenberkeley/redux-simple-tutorial/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254346616,"owners_count":22055808,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["redux","tutorial"],"created_at":"2024-08-02T09:01:34.506Z","updated_at":"2025-05-15T13:02:25.832Z","avatar_url":"https://github.com/kenberkeley.png","language":null,"readme":"# Redux 简明教程\r\n\u003e 原文链接（保持更新）：[https://github.com/kenberkeley/redux-simple-tutorial][this-github]\r\n\r\n\u003e ### 写在前面 \r\n\u003e 本教程深入浅出，配套 简明教程、[进阶教程][advanced-tutorial]（源码精读）以及文档注释丰满的 [Demo][react-demo] 等一条龙服务  \r\n\r\n## \u0026sect; 为什么要用 Redux\r\n\u003e 当然还有 [Flux][flux]、[Reflux][reflux]、[Mobx][mobx] 等状态管理库可供选择\r\n\r\n抛开需求讲实用性都是耍流氓，因此下面由我扮演您那可亲可爱的产品经理\r\n\r\n### ⊙ 需求 1：在控制台上记录用户的每个动作\r\n\r\n不知道您是否有后端的开发经验，后端一般会有记录访问日志的**中间件**  \r\n例如，在 Express 中实现一个简单的 Logger 如下：\r\n\r\n```js\r\nvar loggerMiddleware = function(req, res, next) {\r\n  console.log('[Logger]', req.method, req.originalUrl)\r\n  next()\r\n}\r\n...\r\napp.use(loggerMiddleware)\r\n```\r\n\r\n每次访问的时候，都会在控制台中留下类似下面的日志便于追踪调试：\r\n\r\n```\r\n[Logger] GET  /\r\n[Logger] POST /login\r\n[Logger] GET  /user?uid=10086\r\n...\r\n```\r\n\r\n如果我们把场景转移到前端，请问该如何实现用户的动作跟踪记录？  \r\n我们可能会这样写：\r\n\r\n```js\r\n/** jQuery **/\r\n$('#loginBtn').on('click', function(e) {\r\n  console.log('[Logger] 用户登录')\r\n  ...\r\n})\r\n$('#logoutBtn').on('click', function() {\r\n  console.log('[Logger] 用户退出登录')\r\n  ...\r\n})\r\n\r\n/** MVC / MVVM 框架（这里以纯 Vue 举例） **/\r\nmethods: {\r\n  handleLogin () {\r\n    console.log('[Logger] 用户登录')\r\n    ...\r\n  },\r\n  handleLogout () {\r\n    console.log('[Logger] 用户退出登录')\r\n    ...\r\n  }\r\n}\r\n```\r\n\r\n上述 jQuery 与 MV* 的写法并没有本质上的区别  \r\n记录用户行为代码的侵入性极强，可维护性与扩展性堪忧\r\n\r\n### ⊙ 需求 2：在上述需求的基础上，记录用户的操作时间\r\n\u003e 哼！最讨厌就是改需求了，这种简单的需求难道不是应该一开始就想好的吗？  \r\n\u003e 呵呵，如果每位产品经理都能一开始就把需求完善好，我们就不用加班了好伐\r\n\r\n显然地，前端的童鞋又得一个一个去改（当然 编辑器 / IDE 都支持全局替换）：\r\n```js\r\n/** jQuery **/\r\n$('#loginBtn').on('click', function(e) {\r\n  console.log('[Logger] 用户登录', new Date())\r\n  ...\r\n})\r\n$('#logoutBtn').on('click', function() {\r\n  console.log('[Logger] 用户退出登录', new Date())\r\n  ...\r\n})\r\n\r\n/** MVC / MVVM 框架（这里以 Vue 举例） **/\r\nmethods: {\r\n  handleLogin () {\r\n    console.log('[Logger] 用户登录', new Date())\r\n    ...\r\n  },\r\n  handleLogout () {\r\n    console.log('[Logger] 用户退出登录', new Date())\r\n    ...\r\n  }\r\n}\r\n```\r\n\r\n而后端的童鞋只需要稍微修改一下原来的中间件即可：\r\n\r\n```js\r\nvar loggerMiddleware = function(req, res, next) {\r\n  console.log('[Logger]', new Date(), req.method, req.originalUrl)\r\n  next()\r\n}\r\n...\r\napp.use(loggerMiddleware)\r\n```\r\n\r\n### ⊙ 需求 3：正式上线的时候，把控制台中有关 Logger 的输出全部去掉\r\n难道您以为有了 UglifyJS，配置一个 `drop_console: true` 就好了吗？图样图森破，拿衣服！  \r\n请看清楚了，仅仅是去掉有关 Logger 的 `console.log`，其他的要保留哦亲~~~  \r\n于是前端的童鞋又不得不乖乖地一个一个注释掉（当然也可以设置一个环境变量判断是否输出，甚至可以重写 `console.log`）\r\n\r\n而我们后端的童鞋呢？只需要注释掉一行代码即可：`// app.use(loggerMiddleware)`，真可谓是不费吹灰之力\r\n\r\n### ⊙ 需求 4：正式上线后，自动收集 bug，并还原出当时的场景\r\n收集用户报错还是比较简单的，[利用 `window.error` 事件][global-err-handler]，然后根据 Source Map 定位到源码（但一般查不出什么）\r\n\r\n但要完全还原出当时的使用场景，几乎是不可能的。因为您不知道这个报错，用户是怎么一步一步操作得来的  \r\n就算知道用户是如何操作得来的，但在您的电脑上，测试永远都是通过的（不是我写的程序有问题，是用户用的方式有问题）\r\n\r\n相对地，后端的报错的收集、定位以及还原却是相当简单。只要一个 API 有 bug，那无论用什么设备访问，都会得到这个 bug  \r\n还原 bug 也是相当简单：把数据库备份导入到另一台机器，部署同样的运行环境与代码。如无意外，bug 肯定可以完美重现\r\n\r\n\u003e 在这个问题上拿后端跟前端对比，确实有失公允。但为了鼓吹 Redux 的优越，只能勉为其难了  \r\n\u003e\r\n\u003e 实际上 jQuery / MV* 中也能实现用户动作的跟踪，用一个数组往里面 `push` 用户动作即可  \r\n\u003e 但这样操作的意义不大，因为仅仅只有动作，无法反映动作前后，应用状态的变动情况\r\n\r\n### ※ 小结\r\n\r\n为何前后端对于这类需求的处理竟然大相径庭？后端为何可以如此优雅？  \r\n原因在于，后端具有**统一的入口**与**统一的状态管理（数据库）**，因此可以引入**中间件机制**来**统一**实现某些功能  \r\n\r\n多年来，前端工程师忍辱负重，操着卖白粉的心，赚着买白菜的钱，一直处于程序员鄙视链的底层  \r\n于是有大牛就把后端 MVC 的开发思维搬到前端，**将应用中所有的动作与状态都统一管理**，让一切**有据可循**\r\n  \r\n使用 Redux，借助 [Redux DevTools][redux-devtools] 可以实现出“华丽如时光旅行一般的调试效果”  \r\n实际上就是开发调试过程中可以**撤销与重做**，并且支持应用状态的导入和导出（就像是数据库的备份）  \r\n而且，由于可以使用日志完整记录下每个动作，因此做到像 Git 般，随时随地恢复到之前的状态\r\n\r\n\u003e 由于可以导出和导入应用的状态（包括路由状态），因此还可以实现前后端同构（服务端渲染）  \r\n\u003e 当然，既然有了动作日志以及动作前后的状态备份，那么还原用户报错场景还会是一个难题吗？\r\n\r\n## \u0026sect; Store\r\n首先要区分 `store` 和 `state`\r\n\r\n`state` 是应用的状态，一般本质上是一个普通**对象**  \r\n例如，我们有一个 Web APP，包含 计数器 和 待办事项 两大功能  \r\n那么我们可以为该应用设计出对应的存储数据结构（应用初始状态）：\r\n\r\n```js\r\n/** 应用初始 state，本代码块记为 code-1 **/\r\n{\r\n  counter: 0,\r\n  todos: []\r\n}\r\n```\r\n\r\n`store` 是应用状态 `state` 的管理者，包含下列四个函数：\r\n\r\n* `getState()                  # 获取整个 state`\r\n* `dispatch(action)            # ※ 触发 state 改变的【唯一途径】※`\r\n* `subscribe(listener)         # 您可以理解成是 DOM 中的 addEventListener`\r\n* `replaceReducer(nextReducer) # 一般在 Webpack Code-Splitting 按需加载的时候用`\r\n\r\n二者的关系是：`state === store.getState()`\r\n\r\nRedux 规定，一个应用只应有一个单一的 `store`，其管理着唯一的应用状态 `state`  \r\nRedux 还规定，不能直接修改应用的状态 `state`，也就是说，下面的行为是不允许的：\r\n\r\n```js\r\nvar state = store.getState()\r\nstate.counter = state.counter + 1 // 禁止在业务逻辑中直接修改 state\r\n```\r\n\r\n**若要改变 `state`，必须 `dispatch` 一个 `action`，这是修改应用状态的不二法门**  \r\n\r\n\u003e 现在您只需要记住 `action` 只是一个包含 **`type`** 属性的普通**对象**即可  \r\n\u003e 例如 `{ type: 'INCREMENT' }`\r\n\r\n上面提到，`state` 是通过 `store.getState()` 获取，那么 `store` 又是怎么来的呢？  \r\n想生成一个 `store`，我们需要调用 Redux 的 `createStore`：\r\n\r\n```js\r\nimport { createStore } from 'redux'\r\n...\r\nconst store = createStore(reducer, initialState) // store 是靠传入 reducer 生成的哦！\r\n```  \r\n\u003e 现在您只需要记住 `reducer` 是一个 **函数**，负责**更新**并返回一个**新的** `state`  \r\n\u003e 而 `initialState` 主要用于前后端同构的数据同步（详情请关注 React 服务端渲染）   \r\n\r\n## \u0026sect; Action\r\n上面提到，`action`（动作）实质上是包含 `type` 属性的普通对象，这个 `type` 是我们实现用户行为追踪的关键  \r\n例如，增加一个待办事项 的 `action` 可能是像下面一样：\r\n\r\n```js\r\n/** 本代码块记为 code-2 **/\r\n{\r\n  type: 'ADD_TODO',\r\n  payload: {\r\n    id: 1,\r\n    content: '待办事项1',\r\n    completed: false\r\n  }\r\n}\r\n```\r\n\r\n当然，`action` 的形式是多种多样的，唯一的约束仅仅就是包含一个 `type` 属性罢了  \r\n也就是说，下面这些 `action` 都是合法的：\r\n\r\n```js\r\n/** 如下都是合法的，但就是不够规范 **/\r\n{\r\n  type: 'ADD_TODO',\r\n  id: 1,\r\n  content: '待办事项1',\r\n  completed: false\r\n}\r\n\r\n{\r\n  type: 'ADD_TODO',\r\n  abcdefg: {\r\n    id: 1,\r\n    content: '待办事项1',\r\n    completed: false\r\n  }\r\n}\r\n```\r\n\r\n\u003e 虽说没有约束，但最好还是遵循[规范][flux-action-pattern]\r\n\r\n如果需要新增一个代办事项，实际上就是将 `code-2` 中的 `payload` **“写入”** 到 `state.todos` 数组中（如何“写入”？在此留个悬念）：  \r\n\r\n```js\r\n/** 本代码块记为 code-3 **/\r\n{\r\n  counter: 0,\r\n  todos: [{\r\n    id: 1,\r\n    content: '待办事项1',\r\n    completed: false\r\n  }]\r\n}\r\n```\r\n\r\n刨根问底，`action` 是谁生成的呢？\r\n\r\n### ⊙ Action Creator\r\n\u003e Action Creator 可以是同步的，也可以是异步的\r\n\r\n顾名思义，Action Creator 是 `action` 的创造者，本质上就是一个**函数**，返回值是一个 `action`（**对象**）  \r\n例如下面就是一个 “新增一个待办事项” 的 Action Creator：\r\n\r\n```js\r\n/** 本代码块记为 code-4 **/\r\nvar id = 1\r\nfunction addTodo(content) {\r\n  return {\r\n    type: 'ADD_TODO',\r\n    payload: {\r\n      id: id++,\r\n      content: content, // 待办事项内容\r\n      completed: false  // 是否完成的标识\r\n    }\r\n  }\r\n}\r\n```\r\n\r\n将该函数应用到一个表单（假设 `store` 为全局变量，并引入了 jQuery ）：\r\n\r\n```html\r\n\u003c!-- 本代码块记为 code-5 --\u003e\r\n\u003cinput type=\"text\" id=\"todoInput\" /\u003e\r\n\u003cbutton id=\"btn\"\u003e提交\u003c/button\u003e\r\n\r\n\u003cscript\u003e\r\n$('#btn').on('click', function() {\r\n  var content = $('#todoInput').val() // 获取输入框的值\r\n  var action = addTodo(content) // 执行 Action Creator 获得 action\r\n  store.dispatch(action) // 改变 state 的不二法门：dispatch 一个 action！！！\r\n})\r\n\u003c/script\u003e\r\n```\r\n\r\n在输入框中输入 “待办事项2” 后，点击一下提交按钮，我们的 `state` 就变成了：\r\n\r\n```js\r\n/** 本代码块记为 code-6 **/\r\n{\r\n  counter: 0,\r\n  todos: [{\r\n    id: 1,\r\n    content: '待办事项1',\r\n    completed: false\r\n  }, {\r\n    id: 2,\r\n    content: '待办事项2',\r\n    completed: false\r\n  }]\r\n}\r\n```\r\n\r\n\u003e 通俗点讲，Action Creator 用于绑定到用户的操作（点击按钮等），其返回值 `action` 用于之后的 `dispatch(action)`\r\n\r\n刚刚提到过，`action` 明明就没有强制的规范，为什么 `store.dispatch(action)` 之后，  \r\nRedux 会明确知道是提取 `action.payload`，并且是对应写入到 `state.todos` 数组中？  \r\n又是谁负责“写入”的呢？悬念即将揭晓...\r\n\r\n## \u0026sect; Reducer\r\n\u003e Reducer 必须是同步的纯函数  \r\n\r\n用户每次 `dispatch(action)` 后，都会触发 `reducer`  的执行  \r\n`reducer` 的实质是一个**函数**，根据 `action.type` 来**更新** `state` 并返回 `nextState`  \r\n最后会用 `reducer` 的返回值 `nextState` **完全替换掉**原来的 `state`\r\n\r\n\u003e 注意：上面的这个 “更新” 并不是指 `reducer` 可以直接对 `state` 进行修改  \r\n\u003e Redux 规定，须先复制一份 `state`，在副本 `nextState` 上进行修改操作  \r\n\u003e 例如，可以使用 lodash 的 `cloneDeep`，也可以使用 `Object.assign / map / filter/ ...` 等返回副本的函数\r\n\r\n在上面 Action Creator 中提到的 待办事项的 `reducer` 大概是长这个样子 (为了容易理解，在此不使用 ES6 / [Immutable.js][immutable])：\r\n\r\n```js\r\n/** 本代码块记为 code-7 **/\r\nvar initState = {\r\n  counter: 0,\r\n  todos: []\r\n}\r\n\r\nfunction reducer(state, action) {\r\n  // ※ 应用的初始状态是在第一次执行 reducer 时设置的 ※\r\n  if (!state) state = initState\r\n  \r\n  switch (action.type) {\r\n    case 'ADD_TODO':\r\n      var nextState = _.cloneDeep(state) // 用到了 lodash 的深克隆\r\n      nextState.todos.push(action.payload) \r\n      return nextState\r\n\r\n    default:\r\n    // 由于 nextState 会把原 state 整个替换掉\r\n    // 若无修改，必须返回原 state（否则就是 undefined）\r\n      return state\r\n  }\r\n}\r\n```\r\n\r\n\u003e 通俗点讲，就是 `reducer` 返回啥，`state` 就被替换成啥\r\n\r\n## \u0026sect; 总结\r\n\r\n* `store` 由 Redux 的 `createStore(reducer)` 生成\r\n* `state` 通过 `store.getState()` 获取，本质上一般是一个存储着整个应用状态的**对象**\r\n* `action` 本质上是一个包含 `type` 属性的普通**对象**，由 Action Creator (**函数**) 产生\r\n* 改变 `state` 必须 `dispatch` 一个 `action`\r\n* `reducer` 本质上是根据 `action.type` 来更新 `state` 并返回 `nextState` 的**函数**\r\n* `reducer` 必须返回值，否则 `nextState` 即为 `undefined`\r\n* 实际上，**`state` 就是所有 `reducer` 返回值的汇总**（本教程只有一个 `reducer`，主要是应用场景比较简单）\r\n\r\n\u003e Action Creator =\u003e `action` =\u003e `store.dispatch(action)` =\u003e `reducer(state, action)` =\u003e ~~`原 state`~~ `state = nextState`\r\n\r\n### ⊙ Redux 与传统后端 MVC 的对照\r\nRedux | 传统后端 MVC\r\n---|---\r\n`store` | 数据库实例\r\n`state` | 数据库中存储的数据\r\n`dispatch(action)` | 用户发起请求\r\n`action: { type, payload }` | `type` 表示请求的 URL，`payload` 表示请求的数据\r\n`reducer` | 路由 + 控制器（handler）\r\n`reducer` 中的 `switch-case` 分支 | 路由，根据 `action.type` 路由到对应的控制器\r\n`reducer` 内部对 `state` 的处理 | 控制器对数据库进行增删改操作\r\n`reducer` 返回 `nextState` | 将修改后的记录写回数据库\r\n\r\n## \u0026sect; 最简单的例子 ( [在线演示][jsbin] )\r\n\r\n```html\r\n\u003c!DOCTYPE html\u003e\r\n\u003chtml\u003e\r\n\u003chead\u003e\r\n  \u003cscript src=\"//cdn.bootcss.com/redux/3.5.2/redux.min.js\"\u003e\u003c/script\u003e\r\n\u003c/head\u003e\r\n\u003cbody\u003e\r\n\u003cscript\u003e\r\n/** Action Creators */\r\nfunction inc() {\r\n  return { type: 'INCREMENT' };\r\n}\r\nfunction dec() {\r\n  return { type: 'DECREMENT' };\r\n}\r\n\r\nfunction reducer(state, action) {\r\n  // 首次调用本函数时设置初始 state\r\n  state = state || { counter: 0 };\r\n\r\n  switch (action.type) {\r\n    case 'INCREMENT':\r\n      return { counter: state.counter + 1 };\r\n    case 'DECREMENT':\r\n      return { counter: state.counter - 1 };\r\n    default:\r\n      return state; // 无论如何都返回一个 state\r\n  }\r\n}\r\n\r\nvar store = Redux.createStore(reducer);\r\n\r\nconsole.log( store.getState() ); // { counter: 0 }\r\n\r\nstore.dispatch(inc());\r\nconsole.log( store.getState() ); // { counter: 1 }\r\n\r\nstore.dispatch(inc());\r\nconsole.log( store.getState() ); // { counter: 2 }\r\n\r\nstore.dispatch(dec());\r\nconsole.log( store.getState() ); // { counter: 1 }\r\n\u003c/script\u003e\r\n\u003c/body\u003e\r\n\u003c/html\u003e\r\n```\r\n\r\n\u003e 由上可知，Redux 并不一定要搭配 React 使用。Redux 纯粹只是一个状态管理库，几乎可以搭配任何框架使用  \r\n\u003e （上述例子连 jQuery 都没用哦亲）\r\n\r\n## [\u0026sect; 下一章：Redux 进阶教程][advanced-tutorial]\r\n\r\n[![tip](https://img.shields.io/badge/Tip-%E6%89%93%E8%B5%8F-brightgreen.svg)](https://github.com/kenberkeley/tip)\r\n\r\n[this-github]: https://github.com/kenberkeley/redux-simple-tutorial\r\n[advanced-tutorial]: https://github.com/kenberkeley/redux-simple-tutorial/blob/master/redux-advanced-tutorial.md\r\n[react-demo]: https://github.com/kenberkeley/react-demo\r\n[flux]: https://github.com/facebook/flux\r\n[reflux]: https://github.com/reflux/refluxjs\r\n[mobx]: https://github.com/mobxjs/mobx\r\n[redux]: https://github.com/reactjs/redux\r\n[flux-action-pattern]: https://github.com/acdlite/flux-standard-action\r\n[global-err-handler]: http://stackoverflow.com/questions/5328154/#5328206\r\n[redux-devtools]: https://github.com/gaearon/redux-devtools\r\n[immutable]: https://github.com/facebook/immutable-js\r\n[jsbin]: http://jsbin.com/zivare/edit?html,console\r\n","funding_links":[],"categories":["miscellaneous","Others"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkenberkeley%2Fredux-simple-tutorial","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkenberkeley%2Fredux-simple-tutorial","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkenberkeley%2Fredux-simple-tutorial/lists"}