Redux 简明教程
原文链接(坚持更新):https://github.com/kenberkele…
写在前面
§ 为什么要用 Redux
抛开需求讲实用性都是耍流氓,因而下面由我饰演您那可亲可爱的产物司理
⊙ 需求 1:在掌握台上纪录用户的每一个行动
不晓得您是不是有后端的开辟履历,后端平常会有纪录接见日记的中间件
比方,在 Express 中完成一个简朴的 Logger 以下:
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
...
假如我们把场景转移到前端,叨教该怎样完成用户的行动跟踪纪录?
我们可能会如许写:
/** 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* 的写法并没有本质上的辨别
纪录用户行动代码的侵入性极强,可维护性与扩展性堪忧
⊙ 需求 2:在上述需求的基础上,纪录用户的操纵时候
哼!最憎恶就是改需求了,这类简朴的需求岂非不是应当一开始就想好的吗?
呵呵,假如每位产物司理都能一开始就把需求圆满好,我们就不必加班了好伐
明显地,前端的童鞋又得一个一个去改(固然 编辑器 / 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)
⊙ 需求 3:正式上线的时刻,把掌握台中有关 Logger 的输出悉数去掉
岂非您认为有了 UglifyJS,设置一个 drop_console: true
就好了吗?图样图森破,拿衣服!
请看清晰了,仅仅是去掉有关 Logger 的 console.log
,其他的要保存哦亲~~~
因而前端的童鞋又不得不乖乖地一个一个解释掉(固然也能够设置一个环境变量推断是不是输出,以至能够重写 console.log
)
而我们后端的童鞋呢?只须要解释掉一行代码即可:// app.use(loggerMiddleware)
,真可谓是不费吹灰之力
⊙ 需求 4:正式上线后,自动网络 bug,并复原出当时的场景
网络用户报错照样比较简朴的,运用 window.error
事宜,然后依据 Source Map 定位到源码(但平常查不出什么)
但要完全复原出当时的运用场景,险些是不可能的。因为您不晓得这个报错,用户是怎样一步一步操纵得来的
就算晓得用户是怎样操纵得来的,但在您的电脑上,测试永远都是经由过程的(不是我写的顺序有题目,是用户用的体式格局有题目)
相对地,后端的报错的网络、定位以及复原倒是相称简朴。只需一个 API 有 bug,那不管用什么装备接见,都邑取得这个 bug
复原 bug 也是相称简朴:把数据库备份导入到另一台机械,布置一样的运转环境与代码。如无不测,bug 一定能够圆满重现
在这个题目上拿后端跟前端对比,确切有失公道。但为了宣扬 Redux 的优胜,只能勉为其难了
实际上 jQuery / MV* 中也能完成用户行动的跟踪,用一个数组往里面
push
用户行动即可
但如许操纵的意义不大,因为仅仅只要行动,没法反应行动前后,运用状况的更改状况
※ 小结
为什么前后端关于这类需求的处置惩罚居然天差地别?后端为什么能够云云文雅?
缘由在于,后端具有一致的进口与一致的状况治理(数据库),因而能够引入中间件机制来一致完成某些功用
多年来,前端工程师委曲求全,操着卖白粉的心,赚着买白菜的钱,一向处于顺序员蔑视链的底层
因而有大牛就把后端 MVC 的开辟头脑搬到前端,将运用中一切的行动与状况都一致治理,让统统有据可循
运用 Redux,借助 Redux DevTools 能够完成出“华美如时间游览平常的调试结果”
实际上就是开辟调试过程当中能够打消与重做,而且支撑运用状况的导入和导出(就像是数据库的备份)
而且,因为能够运用日记完全纪录下每一个行动,因而做到像 Git 般,随时随地恢复到之前的状况
因为能够导出和导入运用的状况(包括路由状况),因而还能够完成前后端同构(服务端衬着)
固然,既然有了行动日记以及行动前后的状况备份,那末复原用户报错场景还会是一个困难吗?
§ Store
首先要辨别 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
上面提到,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 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
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 的对比
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 都没用哦亲)