rematch實現了一個插件系統,內置了dispatch和effects兩個插件,分別用來加強dispatch和處理異步操做。rematch的插件,要符合rematch的要求,每一個插件返回一個對象,這個對象能夠包含幾個屬性,用來在不一樣的生命週期中對store進行操做。react
對於每個插件對象,提供了以下幾個屬性進行配置。git
plugin包含exposed、onStoreCreated和onModel三個屬性github
const dispatchPlugin: R.Plugin = {
exposed: {
storeDispatch(action: R.Action, state: any) {
console.warn('Warning: store not yet loaded')
},
storeGetState() {
console.warn('Warning: store not yet loaded')
},
dispatch(action: R.Action) {
return this.storeDispatch(action)
},
createDispatcher(modelName: string, reducerName: string) {
return async (payload?: any, meta?: any): Promise<any> => {
const action: R.Action = { type: `${modelName}/${reducerName}` }
if (typeof payload !== 'undefined') {
action.payload = payload
}
if (typeof meta !== 'undefined') {
action.meta = meta
}
return this.dispatch(action)
}
},
},
// 在store建立完成的時候調用,將加強後的dispatch方法拋出
onStoreCreated(store: any) {
this.storeDispatch = store.dispatch
this.storeGetState = store.getState
return { dispatch: this.dispatch }
},
// 對model上的每一個reducer建立action createor,並掛載到dispatch對象上
onModel(model: R.Model) {
this.dispatch[model.name] = {}
if (!model.reducers) {
return
}
for (const reducerName of Object.keys(model.reducers)) {
this.validate([
[
!!reducerName.match(/\/.+\//),
`Invalid reducer name (${model.name}/${reducerName})`,
],
[
typeof model.reducers[reducerName] !== 'function',
`Invalid reducer (${model.name}/${reducerName}). Must be a function`,
],
])
this.dispatch[model.name][reducerName] = this.createDispatcher.apply(
this,
[model.name, reducerName]
)
}
},
}
複製代碼
這個插件用來處理model的reducers屬性,若是model沒有reducers,就直接退出;不然,遍歷reducers,若是鍵名包含是/符號開頭結尾的或者值不是一個函數,就報錯退出。typescript
經過createDispatcher函數,對每一個reducer進行處理,包裝成一個異步的action creator,action的type由model.name和reducerName組成。redux
onStoreCreated屬性,會返回加強後的dispatch方法去重置redux默認的dispatch方法。bash
effects plugin包含exposed、onModel和middleware三個屬性。app
const effectsPlugin: R.Plugin = {
exposed: {
effects: {},
},
// 將每一個model上的effects添加到dispatch上,這樣能夠經過dispatch[modelName][effectName]來調用effect方法
onModel(model: R.Model): void {
if (!model.effects) {
return
}
// model的effects能夠是一個對象,或者是一個返回對象的函數,這個函數的參數是全局的dispatch方法
const effects =
typeof model.effects === 'function'
? model.effects(this.dispatch)
: model.effects
for (const effectName of Object.keys(effects)) {
this.validate([
[
!!effectName.match(/\//),
`Invalid effect name (${model.name}/${effectName})`,
],
[
typeof effects[effectName] !== 'function',
`Invalid effect (${model.name}/${effectName}). Must be a function`,
],
])
this.effects[`${model.name}/${effectName}`] = effects[effectName].bind(
this.dispatch[model.name]
)
this.dispatch[model.name][effectName] = this.createDispatcher.apply(
this,
[model.name, effectName]
)
// isEffect用來區分是普通的action,仍是異步的,後面的loading插件就是經過這個字段來判斷是否是異步操做
this.dispatch[model.name][effectName].isEffect = true
}
},
// 用來處理 async/await actions的redux中間鍵
middleware(store) {
return next => async (action: R.Action) => {
if (action.type in this.effects) {
await next(action)
// 會把全局的state做爲effect方法的第二個參數傳入
return this.effects[action.type](
action.payload,
store.getState(),
action.meta
)
}
return next(action)
}
},
}
複製代碼
經過exposed將effects掛載到rematch對象上,用來保存全部model中的effects方法。異步
middleware屬性用來定義中間鍵,處理async/await 異步函數。async
onModel用來將全部model的effects方法存儲在dispatch上,並使用'modelName/effectName'的key將對應的effect方法存儲在全局的effects對象上。ide
對每一個model中的effects自動添加loading狀態。
export default (config: LoadingConfig = {}): Plugin => {
validateConfig(config)
const loadingModelName = config.name || 'loading'
const converter =
config.asNumber === true ? (cnt: number) => cnt : (cnt: number) => cnt > 0
const loading: Model = {
name: loadingModelName,
reducers: {
hide: createLoadingAction(converter, -1),
show: createLoadingAction(converter, 1),
},
state: {
...cntState,
},
}
cntState.global = 0
loading.state.global = converter(cntState.global)
return {
config: {
// 增長一個loading model,用來管理全部的loading狀態
models: {
loading,
},
},
onModel({ name }: Model) {
// 用戶定義的model若是和注入的loadingmodel重名,則退出這個model
if (name === loadingModelName) {
return
}
cntState.models[name] = 0
loading.state.models[name] = converter(cntState.models[name])
loading.state.effects[name] = {}
const modelActions = this.dispatch[name]
// 收集每一個model上的effect方法
Object.keys(modelActions).forEach((action: string) => {
// 經過isEffect來判斷是不是異步方法
if (this.dispatch[name][action].isEffect !== true) {
return
}
cntState.effects[name][action] = 0
loading.state.effects[name][action] = converter(
cntState.effects[name][action]
)
const actionType = `${name}/${action}`
// 忽略不在白名單中的action
if (config.whitelist && !config.whitelist.includes(actionType)) {
return
}
// 忽略在黑名單中的action
if (config.blacklist && config.blacklist.includes(actionType)) {
return
}
// 指向原來的effect方法
const origEffect = this.dispatch[name][action]
// 對每一個effect方法進行包裹,在異步方法調用先後進行狀態的處理
const effectWrapper = async (...props) => {
try {
// 異步方法請求前,同步狀態
this.dispatch.loading.show({ name, action })
const effectResult = await origEffect(...props)
// 異步請求成功後,同步狀態
this.dispatch.loading.hide({ name, action })
return effectResult
} catch (error) {
// 異步請求失敗後,同步狀態
this.dispatch.loading.hide({ name, action })
throw error
}
}
// 使用包裹後的函數替代原來的effect方法
this.dispatch[name][action] = effectWrapper
})
},
}
}複製代碼
這個插件會在store上增長一個名爲loading的model,這個model的state有三個屬性,分別爲global、models和effects。
在默認狀況下,會遍歷全部model的effects,對每一個isEffect爲true的action,用一個異步函數進行包裹,使用try catch捕獲錯誤。
{ whitelist: ['count/addOne'] })
{ blacklist: ['count/addOne'] })
store.js
import { init } from '@rematch/core'
import createLoadingPlugin from '@rematch/loading'
// 初始化配置
const loading = createLoadingPlugin({})
init({
plugins: [loading]
})
複製代碼
exmaple modle.js
const asyncDelay = ms => new Promise(r => setTimeout(r, ms));
export default {
state: 0,
reducers: {
addOne(s) {
return s + 1
}
},
effects: {
async submit() {
// mocking the delay of an effect
await asyncDelay(3000)
this.addOne()
},
}
}
複製代碼
app.js
const LoginButton = (props) => (
<AwesomeLoadingButton onClick={props.submit} loading={props.loading}>
Login
</AwesomeLoadingButton>
)
const mapState = state => ({
count: state.example,
loading: {
global: state.loading.global, // true when ANY effect is running
model: state.loading.models.example, // true when ANY effect on the `login` model is running
effect: state.loading.effects.example.submit, // true when the `login/submit` effect is running
},
})
const mapDispatch = (dispatch) => ({
submit: () => dispatch.login.submit()
})
export default connect(mapState, mapDispatch)(LoginButton)
複製代碼
immer 是Mobx做者寫的一個immutable庫,利用ES6的proxy和defineProperty實現js的不可變數據。相比於ImmutableJS,操做更簡單,也不用學習特有的API。
對於一個複雜的對象,immer會複用沒有改變的部分,僅僅替換修改了的部分,相比於深拷貝,能夠大大的減小開銷。
對於react和redux,immer能夠大大減小setState和reducer的代碼量,並提供更好的可讀性。
舉個栗子,想要修改todoList中某項的狀態,常規的寫法
todos = [
{todo: 'bbbb', done: true},
{todo: 'aaa', done: false}
];
state的狀況
this.setState({
todos: [
...todos.slice(0, index),
{
...todos[index],
done: !todos[index].done
},
...todos.slice(index + 1)
]
})
reducer的狀況
const reducer = (state, action) => {
switch (action.type) {
case 'TRGGIER_TODO':
const { members } = state;
return {
...state,
todos: [
...todos.slice(0, index),
{
...todos[index],
done: !todos[index].done
},
...todos.slice(index + 1)
]
}
default:
return state
}
}
複製代碼
若是使用immer,代碼能夠簡化爲
import produce from 'immer'
state的狀況
this.setState(produce(draft => {
draft.todos[index].done = !draft.todos[index].done;
))
reducer的狀況
const reducer = produce((draft, action) => {
switch (action.type) {
case 'TRGGIER_TODO':
draft.todos[index].done = !draft.todos[index].done;
}
})
複製代碼
這個插件重寫了redux的combineReducers方法,從而支持了immer。
import { Models, Plugin } from '@rematch/core'
import produce from 'immer'
import { combineReducers, ReducersMapObject } from 'redux'
function combineReducersWithImmer(reducers: ReducersMapObject) {
const reducersWithImmer = {}
// model的reducer函數必須有返回,由於immer只支持對象類型
for (const [key, reducerFn] of Object.entries(reducers)) {
reducersWithImmer[key] = (state, payload) => {
// 若是state不是對象,則直接返回reducer的計算值
if (typeof state === 'object') {
return produce(state, (draft: Models) => {
const next = reducerFn(draft, payload)
if (typeof next === 'object') {
return next
}
})
} else {
return reducerFn(state, payload)
}
}
}
return combineReducers(reducersWithImmer)
}
const immerPlugin = (): Plugin => ({
config: {
redux: {
combineReducers: combineReducersWithImmer,
},
},
})
export default immerPlugin
複製代碼
使用插件
store.js
import { init } from '@rematch/core'
import immerPlugin from '@rematch/immer';
const immer = immerPlugin();
init({
plugins: [immer]
})
複製代碼
model.js
const immerTodos = {
state: [{
todo: 'Learn typescript',
done: true,
}, {
todo: 'Try immer',
done: false,
}],
reducers: {
addTodo(state, payload) {
state.push(payload);
return state;
},
triggerTodo(state, payload) {
const { index } = payload;
state[index].done = !state[index].done;
return state;
},
},
};
export default immerTodos;
複製代碼
對於通常的model而言,reducer的做用就是根據state和action來生成新的state,相似於(state, action) => state
,這個plugin的做用是爲每一個model生成一個名爲setValues的reducer。若是是state是一個對象,會對action.payload和state使用Object.assign去進行屬性的合併;若是state是基礎類型,會使用action.payload去覆蓋state。
const DefaultReducerPlugin = {
onModel(model) {
const { reducers = {} } = model;
if (Object.keys(reducers).includes('setValues')) {
return false;
}
reducers.setValues = (state, payload) => {
if (isObject(payload) && isObject(state)) {
return Object.assign({}, state, payload);
}
return payload;
};
this.dispatch[model.name].setValues = this.createDispatcher.apply(
this,
[model.name, 'setValues'],
);
},
};
複製代碼
const countModel = {
state: {
configList: [],
},
effects: {
async fetchConfigList() {
const res = await fetch({url});
res && res.list && this.setValues({
configList: res.list,
});
},
},
};
init
import defaultReducerPlugin from 'rematch-plugin-default-reducer';
const store = init({
...
models,
plugins: [defaultReducerPlugin],
...
});複製代碼