這篇文章主要是分享一個想法(文章最後有落地的實現),如標題所指:不要在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已經完全能滿足需求了,那是否意味著在開發中就沒有痛點了呢?

我們可以仔細看一下上面的發展路徑,有些東西是沒有變的,一直延續下來的。

  1. action的type始終存在於代碼中,串聯起整個數據流程
  2. 非同步流程始終是通過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:npmjs.com/package/glue-

連接庫:npmjs.com/package/react

參考

  • redux
  • dva
  • redux-thunk
  • redux-saga
  • glue-redux

推薦閱讀:

相關文章