rematch經常使用插件介紹

插件系統

rematch實現了一個插件系統,內置了dispatch和effects兩個插件,分別用來加強dispatch和處理異步操做。rematch的插件,要符合rematch的要求,每一個插件返回一個對象,這個對象能夠包含幾個屬性,用來在不一樣的生命週期中對store進行操做。react

對於每個插件對象,提供了以下幾個屬性進行配置。git

  • onInit:進行插件的初始化工做
  • config: 對rematch進行配置,會在rematch初始化以前,對插件的config配置進行merge操做
  • exposed:暴露給全局的屬性和方法,能夠理解爲佔位符,用於數據的共享
  • middleware:至關於redux的中間鍵,若是提供了這個屬性,會把它交給redux進行處理
  • onModel:加載model的時候會調用的方法,用來對model進行處理
  • onStoreCreated:當rematch的store對象生成後,調用的方法,生成最終的store對象


DispatchPlugin

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

EffectsPlugin

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

@rematch/loading

對每一個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。

  • global:用來保存全局的狀態,只要任意effect被觸發,就會引發global的更新
  • models:它是一個對象,用來表示每一個model的狀態,只要是這個model下的effect被觸發,就會引發對應model字段的更新
  • effects:它是一個對象,以model爲單位,記錄這個model下每一個effects的狀態,若是某個effect被觸發,會去精確的更新這個effect對應的狀態。

在默認狀況下,會遍歷全部model的effects,對每一個isEffect爲true的action,用一個異步函數進行包裹,使用try catch捕獲錯誤。

經常使用的配置項

  • asNumber:默認是false,若是設置爲true,那麼狀態就是異步被調用的次數
  • name:loading model的name,默認是loading
  • whitelist:白名單,一個 action 列表,只收集在列表中的action的狀態。命名使用「model名稱」 / 「action名稱」,{ whitelist: ['count/addOne'] })
  • blacklist:黑名單一個 action 列表,不使用 loading 指示器。{ 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)
複製代碼

@rematch/immer

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;
複製代碼

rematch-plugin-default-reducer

對於通常的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],
    
    ...
    
});複製代碼
相關文章
相關標籤/搜索