Redux: 不要再使用action type了
這篇文章主要是分享一個想法(文章最後有落地的實現),如標題所指:不要在redux中裸寫action type了。
先吐個槽,苦redux中的action type久矣,這下終於不用再寫了!
回到正文,我們先粗略地看下redux的使用情況,選了比較有代表性的庫,沒有全面包含。從純粹的redux代碼,到redux-thunk、redux-saga,再到dva。
純粹的redux
actions.js
/* * action types */
export const ADD_TODO = ADD_TODO;
export const TOGGLE_TODO = TOGGLE_TODO;
export const SET_VISIBILITY_FILTER = SET_VISIBILITY_FILTER;
/* * other constants */
export const VisibilityFilters = {
SHOW_ALL: SHOW_ALL,
SHOW_COMPLETED: SHOW_COMPLETED,
SHOW_ACTIVE: SHOW_ACTIVE
};
/* * action creators */
export function addTodo(text) {
return { type: ADD_TODO, text }
};
export function toggleTodo(index) {
return { type: TOGGLE_TODO, index }
};
export function setVisibilityFilter(filter) {
return { type: SET_VISIBILITY_FILTER, filter }
};
// 通過dispatch來使用這些action對象
reducers.js
import { combineReducers } from redux
import { ADD_TODO, TOGGLE_TODO, SET_VISIBILITY_FILTER, VisibilityFilters } from ./actions
const { SHOW_ALL } = VisibilityFilters
function visibilityFilter(state = SHOW_ALL, action) {
switch (action.type) {
case SET_VISIBILITY_FILTER:
return action.filter;
default:
return state
}
}
function todos(state = [], action) {
switch (action.type) {
case ADD_TODO:
return [ ...state, { text: action.text, completed: false}]
case TOGGLE_TODO:
return state.map((todo, index) => {
if (index === action.index) {
return Object.assign({}, todo, {completed: !todo.completed })
}
return todo
})
default:
return state
}
}
const todoApp = combineReducers({ visibilityFilter, todos })
export default todoApp
// 根據action的type來區分處理
store.js
import { createStore } from redux;
import todoApp from ./reducers;
const store = createStore(todoApp);
actions.js和reducers.js中各種字元串和不停地switch...case...,簡直不忍直視!
後來需要處理非同步action,出現了redux-thunk
redux-thunk
中間件---處理非同步action
和普通action區別的是:redux-thunk處理的非同步action是一個函數
actions.js
// 其他同步的action省略,和上面代碼一致
function fetchUser(id) {
return (dispatch, getState, { api, whatever }) => {
// you can use api and something else here
}
}
/*
這裡的action creator返回的不再是一個 { type: xxx, ... } 的對象,而是一個函數。使用和其他普通action creator並無差異。
dispatch(fetchUser(id)):傳遞的是一個函數,這個函數在中間件redux-thunk中會被識別,然後執行。
redux-thunk大概長這樣:
(store) => (next) => (action) => {
if (typeof action === function) {
return action(store.dispatch, store.getState, ...someotherParams);
}
return next(action);
}
*/
reducers.js
處理數據的形式同上面,也是很多字元串和switch...case...
store.js
中間件使用需要配置一下
import { createStore, applyMiddleware } from redux;
import thunk from redux-thunk;
import rootReducer from ./reducers;
// Note: this API requires redux@>=3.1.0
const store = createStore(
rootReducer,
applyMiddleware(thunk)
);
export default store;
到這裡基本能滿足需求了,但是...同上不忍直視!
當然非同步action解決方案不止這一種,類似的就不舉例了。
redux-saga
使用generator進行非同步流程式控制制
這個中間件就比較厲害了!專門定了一層saga抽象來處理非同步流程,功能比較強大!
sagas.js
import { call, put, takeEvery, takeLatest } from redux-saga/effects
import Api from ...
function* fetchUser(action) {
try {
const user = yield call(Api.fetchUser, action.payload.userId);
yield put({type: "USER_FETCH_SUCCEEDED", user: user});
} catch (e) {
yield put({type: "USER_FETCH_FAILED", message: e.message});
}}
function* mySaga() {
yield takeLatest("USER_FETCH_REQUESTED", fetchUser);
}
export default mySaga;
// 通過dispatch({type: xxx, payload: xxx})來觸發對應的非同步處理邏輯
// 比如 dispatch({type: USER_FETCH_REQUESTED, payload: 123})會觸發 fetchUser請求
// 然後通過提供的helper方法 put去分發 action 更新數據:put({type: "USER_FETCH_SUCCEEDED", user: user})
這裡的非同步action和redux-thunk處理的非同步action不一樣,它就是普通的action object。
actions.js
action的形式同 原始的redux代碼 的actions.js,這裡就不再重複了。
reducers.js
reducer的處理邏輯的形式和 原始的redux代碼 的reducers.js一樣,也不重複寫了。
store.js
中間件的使用都要先配置
import { createStore, applyMiddleware } from redux
import createSagaMiddleware from redux-saga
import reducer from ./reducers
import mySaga from ./sagas
const sagaMiddleware = createSagaMiddleware();
const store = createStore(
reducer,
applyMiddleware(sagaMiddleware));
sagaMiddleware.run(mySaga);
export default store;
雖然非同步處理能力變強了,但是action type的使用依舊沒有變少,reducer裡面也沒什麼變化,另外還增加了一層saga抽象和若干api...貌似讓事情變複雜了??
Dva
dva不同於上面提到的中間件,它是一個集合體,集成了redux、redux-saga、react-redux等。dva做出了按模塊劃分數據的設計。
model.js
export default {
namespace: count,
state: 0,
reducers: {
add(count: number) {
return count + 1;
},
minus(count: number) {
return count - 1;
},
},
effects: {
*doSomething(action, sagaHelperObj) {
// 做非同步或者其他處理
}
}
};
/* dva中action的type被劃分為幾個部分:namespace、reducers或者effects對象的key值。
如果要調用某個reducer,需要知道reducer所在的namespace以及reducer對應的key值,
比如 dispatch({ type: count/add }),就會調用到reducers中的add函數,進而修改數據。
dva集成了redux-saga進行非同步處理,在model中體現為effects
如果想要調起effects中的doSomething,需要這樣 dispatch({type: count/doSomething, payload: anyting})
*/
main.js
import dva from dva;
import model from ./model;
const app = dva();
app.model(model);
app.start(#bd);
到dva這裡,switch...case...的情況沒有了,字元串也減少了許多。雖然還是要寫action type,但是比較能接受了,畢竟減少了一半的使用量:D
歷程
從原生redux,到簡單的非同步中間件redux-thunk,再到非同步流程式控制制中間件redux-saga,再到按數據模塊劃分非同步和reducer。整個過程是逐步地演進。到dva這裡已經是一個完整的數據管理方案了。使用dva已經完全能滿足需求了,那是否意味著在開發中就沒有痛點了呢?
我們可以仔細看一下上面的發展路徑,有些東西是沒有變的,一直延續下來的。
- action的type始終存在於代碼中,串聯起整個數據流程
- 非同步流程始終是通過redux的middleware機制處理的
這兩個點本質上可以歸為一個:action type直白地充當了指令。
action type作為指令沒有問題,問題在於不應該由開發者來裸寫字元串。裸寫字元串會帶來管理上的問題,另外開發工具也無法通過字元串關聯到對應的處理邏輯,完全憑開發者人肉搜索。開發者對action type的熟悉程度將會較大地影響開發效率。
相信這點也是使用過redux的開發者經常吐槽的。
目的
我們需要尋找一種結構來代替開發者裸寫字元串,由這種結構來推導出action type。
action type 具有唯一性,並且action type需要與處理邏輯關聯,也就是reducer關聯。如果能滿足這兩點,基本就是符合要求的結構。
javascript中能滿足這兩點要求的結構,我能想到的就是對象,其他還有的話還請提醒。
對象的屬性路徑滿足唯一性要求,對象的屬性節點也能作為關聯reducer的地方。
過程
將數據進行模塊劃分,每個模塊是一個對象,對象的屬性節點將作為數據節點
user.js
import { gluer } from glue-redux;
// gluer 接收連個參數:第一個是數據處理函數,入參是傳入的數據和當前的state,返回值將作為新的state;第二個是節點初始值
// 定義數據節點
const name = gluer((data, state) => {
return data;
}, 張三);
// 定義數據節點
const age = gluer((data, state) => {
return data;
}, 18);
const user = {
name,
age
};
export default user;
store.js
import { createStore, combineReducers } from redux;
import { destruct } from glue-redux;
import user from ./user;
// 先創建一個空store
const store = createStore(() => ({}));
// 整合model
const model = {
user
};
// 生成最終的reducers對象
const { reducers, referToState } = destruct(store)(model);
// 將store中的reducer替換為最新的
store.replaceReducer(combineReducers(reducers));
export {
model,
referToState
};
export default store;
index.js
import { model, referToState } from ./store;
console.log(referToState(model)); // { user: { name: 張三, age: 18 } }
model.user.age(22);
model.user.name(李四)
console.log(referToState(model)); // { user: { name: 李四, age: 22 } }
console.log(referToState(model.user)); // { name: 李四, age: 22 }
console.log(referToState(model.user.age)); // 22
console.log(referToState(modle.user.name)); // 李四
在線代碼:codesandbox
所有代碼中都沒有再出現action type,要修改哪裡的數據直接調用哪裡的函數並傳入參數就行了。至此之前提到的問題已經較好地解決了:
1.action的type始終存在於代碼中,串聯起整個數據流程
2.非同步流程始終是通過redux的middleware機制處理的這兩個點本質上可以歸為一個:action type直白地充當了指令。
action type的信息已經隱藏在model對象中,經過destruct被提取出來。
那非同步流程呢?這裡非同步流程已經不在redux中處理了,而是開發者將這塊邏輯自定義在別處,根據情況選擇非同步技術,需要在更新數據的時候調用下對應的model方法。
glue-redux:https://www.npmjs.com/package/glue-redux
連接庫:https://www.npmjs.com/package/react-glue-redux-hook
參考
- redux
- dva
- redux-thunk
- redux-saga
- glue-redux
推薦閱讀: