原文连接(保持更新):https://github.com/kenberkele...css
写在前面
抛开需求讲实用性都是耍流氓,所以下面由我扮演您那可亲可爱的产品经理react
不知道您是否有后端的开发经验,后端通常会有记录访问日志的中间件
例如,在 Express 中实现一个简单的 Logger 以下:git
var loggerMiddleware = function(req, res, next) { console.log('[Logger]', req.method, req.originalUrl) next() } ... app.use(loggerMiddleware)
每次访问的时候,都会在控制台中留下相似下面的日志便于追踪调试:程序员
[Logger] GET / [Logger] POST /login [Logger] GET /user?uid=10086 ...
若是咱们把场景转移到前端,请问该如何实现用户的动做跟踪记录?
咱们可能会这样写:github
/** jQuery **/ $('#loginBtn').on('click', function(e) { console.log('[Logger] 用户登陆') ... }) $('#logoutBtn').on('click', function() { console.log('[Logger] 用户退出登陆') ... }) /** MVC / MVVM 框架(这里以纯 Vue 举例) **/ methods: { handleLogin () { console.log('[Logger] 用户登陆') ... }, handleLogout () { console.log('[Logger] 用户退出登陆') ... } }
上述 jQuery 与 MV* 的写法并无本质上的区别
记录用户行为代码的侵入性极强,可维护性与扩展性堪忧数据库
哼!最讨厌就是改需求了,这种简单的需求难道不是应该一开始就想好的吗?
呵呵,若是每位产品经理都能一开始就把需求完善好,咱们就不用加班了好伐redux
显然地,前端的童鞋又得一个一个去改(固然 编辑器 / IDE 都支持全局替换):后端
/** jQuery **/ $('#loginBtn').on('click', function(e) { console.log('[Logger] 用户登陆', new Date()) ... }) $('#logoutBtn').on('click', function() { console.log('[Logger] 用户退出登陆', new Date()) ... }) /** MVC / MVVM 框架(这里以 Vue 举例) **/ methods: { handleLogin () { console.log('[Logger] 用户登陆', new Date()) ... }, handleLogout () { console.log('[Logger] 用户退出登陆', new Date()) ... } }
然后端的童鞋只须要稍微修改一下原来的中间件便可:
var loggerMiddleware = function(req, res, next) { console.log('[Logger]', new Date(), req.method, req.originalUrl) next() } ... app.use(loggerMiddleware)
难道您觉得有了 UglifyJS,配置一个 drop_console: true
就行了吗?图样图森破,拿衣服!
请看清楚了,仅仅是去掉有关 Logger 的 console.log
,其余的要保留哦亲~~~
因而前端的童鞋又不得不乖乖地一个一个注释掉(固然也能够设置一个环境变量判断是否输出,甚至能够重写 console.log
)
而咱们后端的童鞋呢?只须要注释掉一行代码便可:// app.use(loggerMiddleware)
,真可谓是不费吹灰之力
收集用户报错仍是比较简单的,利用 window.error
事件,而后根据 Source Map 定位到源码(但通常查不出什么)
但要彻底还原出当时的使用场景,几乎是不可能的。由于您不知道这个报错,用户是怎么一步一步操做得来的
就算知道用户是如何操做得来的,但在您的电脑上,测试永远都是经过的(不是我写的程序有问题,是用户用的方式有问题)
相对地,后端的报错的收集、定位以及还原倒是至关简单。只要一个 API 有 bug,那不管用什么设备访问,都会获得这个 bug
还原 bug 也是至关简单:把数据库备份导入到另外一台机器,部署一样的运行环境与代码。如无心外,bug 确定能够完美重现
在这个问题上拿后端跟前端对比,确实有失公允。但为了鼓吹 Redux 的优越,只能勉为其难了
实际上 jQuery / MV* 中也能实现用户动做的跟踪,用一个数组往里面
push
用户动做便可
但这样操做的意义不大,由于仅仅只有动做,没法反映动做先后,应用状态的变更状况
为什么先后端对于这类需求的处理居然截然不同?后端为什么能够如此优雅?
缘由在于,后端具备统一的入口与统一的状态管理(数据库),所以能够引入中间件机制来统一实现某些功能
多年来,前端工程师忍辱负重,操着卖白粉的心,赚着买白菜的钱,一直处于程序员鄙视链的底层
因而有大牛就把后端 MVC 的开发思惟搬到前端,将应用中全部的动做与状态都统一管理,让一切有据可循
使用 Redux,借助 Redux DevTools 能够实现出“华丽如时光旅行通常的调试效果”
实际上就是开发调试过程当中能够撤销与重作,而且支持应用状态的导入和导出(就像是数据库的备份)
并且,因为可使用日志完整记录下每一个动做,所以作到像 Git 般,随时随地恢复到以前的状态
因为能够导出和导入应用的状态(包括路由状态),所以还能够实现先后端同构(服务端渲染)
固然,既然有了动做日志以及动做先后的状态备份,那么还原用户报错场景还会是一个难题吗?
首先要区分 store
和 state
state
是应用的状态,通常本质上是一个普通对象
例如,咱们有一个 Web APP,包含 计数器 和 待办事项 两大功能
那么咱们能够为该应用设计出对应的存储数据结构(应用初始状态):
/** 应用初始 state,本代码块记为 code-1 **/ { counter: 0, todos: [] }
store
是应用状态 state
的管理者,包含下列四个函数:
getState() # 获取整个 state
dispatch(action) # ※ 触发 state 改变的【惟一途径】※
subscribe(listener) # 您能够理解成是 DOM 中的 addEventListener
replaceReducer(nextReducer) # 通常在 Webpack Code-Splitting 按需加载的时候用
两者的关系是:state = store.getState()
Redux 规定,一个应用只应有一个单一的 store
,其管理着惟一的应用状态 state
Redux 还规定,不能直接修改应用的状态 state
,也就是说,下面的行为是不容许的:
var state = store.getState() state.counter = state.counter + 1 // 禁止在业务逻辑中直接修改 state
若要改变 state
,必须 dispatch
一个 action
,这是修改应用状态的不二法门
如今您只须要记住
action
只是一个包含type
属性的普通对象便可
例如{ type: 'INCREMENT' }
上面提到,state
是经过 store.getState()
获取,那么 store
又是怎么来的呢?
想生成一个 store
,咱们须要调用 Redux 的 createStore
:
import { createStore } from 'redux' ... const store = createStore(reducer, initialState) // store 是靠传入 reducer 生成的哦!
如今您只须要记住
reducer
是一个 函数,负责更新并返回一个新的state
而initialState
主要用于先后端同构的数据同步(详情请关注 React 服务端渲染)
上面提到,action
(动做)实质上是包含 type
属性的普通对象,这个 type
是咱们实现用户行为追踪的关键
例如,增长一个待办事项 的 action
多是像下面同样:
/** 本代码块记为 code-2 **/ { type: 'ADD_TODO', payload: { id: 1, content: '待办事项1', completed: false } }
固然,action
的形式是多种多样的,惟一的约束仅仅就是包含一个 type
属性罢了
也就是说,下面这些 action
都是合法的:
/** 以下都是合法的,但就是不够规范 **/ { type: 'ADD_TODO', id: 1, content: '待办事项1', completed: false } { type: 'ADD_TODO', abcdefg: { id: 1, content: '待办事项1', completed: false } }
虽然说没有约束,但最好仍是遵循规范
若是须要新增一个代办事项,实际上就是将 code-2
中的 payload
“写入” 到 state.todos
数组中(如何“写入”?在此留个悬念):
/** 本代码块记为 code-3 **/ { counter: 0, todos: [{ id: 1, content: '待办事项1', completed: false }] }
刨根问底,action
是谁生成的呢?
Action Creator 能够是同步的,也能够是异步的
顾名思义,Action Creator 是 action
的创造者,本质上就是一个函数,返回值是一个 action
(对象)
例以下面就是一个 “新增一个待办事项” 的 Action Creator:
/** 本代码块记为 code-4 **/ var id = 1 function addTodo(content) { return { type: 'ADD_TODO', payload: { id: id++, content: content, // 待办事项内容 completed: false // 是否完成的标识 } } }
将该函数应用到一个表单(假设 store
为全局变量,并引入了 jQuery ):
<--! 本代码块记为 code-5 --> <input type="text" id="todoInput" /> <button id="btn">提交</button> <script> $('#btn').on('click', function() { var content = $('#todoInput').val() // 获取输入框的值 var action = addTodo(content) // 执行 Action Creator 得到 action store.dispatch(action) // 改变 state 的不二法门:dispatch 一个 action!!! }) </script>
在输入框中输入 “待办事项2” 后,点击一下提交按钮,咱们的 state
就变成了:
/** 本代码块记为 code-6 **/ { counter: 0, todos: [{ id: 1, content: '待办事项1', completed: false }, { id: 2, content: '待办事项2', completed: false }] }
通俗点讲,Action Creator 用于绑定到用户的操做(点击按钮等),其返回值
action
用于以后的dispatch(action)
刚刚提到过,action
明明就没有强制的规范,为何 store.dispatch(action)
以后,
Redux 会明确知道是提取 action.payload
,而且是对应写入到 state.todos
数组中?
又是谁负责“写入”的呢?悬念即将揭晓...
Reducer 必须是同步的纯函数
用户每次 dispatch(action)
后,都会触发 reducer
的执行 reducer
的实质是一个函数,根据 action.type
来更新 state
并返回 nextState
最后会用 reducer
的返回值 nextState
彻底替换掉原来的 state
注意:上面的这个 “更新” 并非指
reducer
能够直接对state
进行修改
Redux 规定,须先复制一份state
,在副本nextState
上进行修改操做
例如,可使用 lodash 的deepClone
,也可使用Object.assign / map / filter/ ...
等返回副本的函数
在上面 Action Creator 中提到的 待办事项的 reducer
大概是长这个样子 (为了容易理解,在此不使用 ES6 / Immutable.js):
/** 本代码块记为 code-7 **/ var initState = { counter: 0, todos: [] } function reducer(state, action) { // ※ 应用的初始状态是在第一次执行 reducer 时设置的(除非是服务端渲染) ※ if (!state) state = initState switch (action.type) { case 'ADD_TODO': var nextState = _.deepClone(state) // 用到了 lodash 的深克隆 nextState.todos.push(action.payload) return nextState default: // 因为 nextState 会把原 state 整个替换掉 // 若无修改,必须返回原 state(不然就是 undefined) return state } }
通俗点讲,就是
reducer
返回啥,state
就被替换成啥
store
由 Redux 的 createStore(reducer)
生成
state
经过 store.getState()
获取,本质上通常是一个存储着整个应用状态的对象
action
本质上是一个包含 type
属性的普通对象,由 Action Creator (函数) 产生
改变 state
必须 dispatch
一个 action
reducer
本质上是根据 action.type
来更新 state
并返回 nextState
的函数
reducer
必须返回值,不然 nextState
即为 undefined
实际上,state
就是全部 reducer
返回值的汇总(本教程只有一个 reducer
,主要是应用场景比较简单)
Action Creator =>
action
=>store.dispatch(action)
=>reducer(state, action)
=>原 state
state = nextState
Redux | 传统后端 MVC |
---|---|
store |
数据库实例 |
state |
数据库中存储的数据 |
dispatch(action) |
用户发起请求 |
action: { type, payload } |
type 表示请求的 URL,payload 表示请求的数据 |
reducer |
路由 + 控制器(handler) |
reducer 中的 switch-case 分支 |
路由,根据 action.type 路由到对应的控制器 |
reducer 内部对 state 的处理 |
控制器对数据库进行增删改操做 |
reducer 返回 nextState |
将修改后的记录写回数据库 |
<!DOCTYPE html> <html> <head> <script src="//cdn.bootcss.com/redux/3.5.2/redux.min.js"></script> </head> <body> <script> /** Action Creators */ function inc() { return { type: 'INCREMENT' }; } function dec() { return { type: 'DECREMENT' }; } function reducer(state, action) { // 首次调用本函数时设置初始 state state = state || { counter: 0 }; switch (action.type) { case 'INCREMENT': return { counter: state.counter + 1 }; case 'DECREMENT': return { counter: state.counter - 1 }; default: return state; // 不管如何都返回一个 state } } var store = Redux.createStore(reducer); console.log( store.getState() ); // { counter: 0 } store.dispatch(inc()); console.log( store.getState() ); // { counter: 1 } store.dispatch(inc()); console.log( store.getState() ); // { counter: 2 } store.dispatch(dec()); console.log( store.getState() ); // { counter: 1 } </script> </body> </html>
由上可知,Redux 并不必定要搭配 React 使用。Redux 纯粹只是一个状态管理库,几乎能够搭配任何框架使用
(上述例子连 jQuery 都没用哦亲)