前言
年中,公司啟動(dòng)新項(xiàng)目,需要搭建微前端架構(gòu),經(jīng)過(guò)多番調(diào)研,確定了乾坤、umi、dva的技術(shù)方案,開始一個(gè)月就遇到了大的難題。第一,dva是約定式,不能靈活的配置;第二,乾坤并不能完全滿足業(yè)務(wù)需求,需要更改很多源碼,比如主子通信,兄弟通信等。經(jīng)過(guò)一番取舍,放棄了這個(gè)方案。后基于single-spa,搭建一套微前端架構(gòu),同時(shí)通過(guò)命令生成模板,類似create-react-app,使用技術(shù)棧react、redux。
之前習(xí)慣了dva的操作方法,使用redux比較繁瑣,因新項(xiàng)目比較龐大,不建議使用mobx。調(diào)研了多種方案,最終選擇redux作者Dan Abramov今年三月份出的工具庫(kù)@reduxjs/tooltik(以下簡(jiǎn)稱RTK)。
簡(jiǎn)介
RTK旨在幫助解決關(guān)于Redux的三個(gè)問(wèn)題:
- 配置Redux存儲(chǔ)太復(fù)雜;
- 必須添加很多包才能讓Redux做預(yù)期的事情;
- Redux需要太多樣板代碼;
簡(jiǎn)單講配置Redux存儲(chǔ)的流程太復(fù)雜,完整需要actionTypes、actions、reducer、store、通過(guò)connect連接。使用RTK,只需一個(gè)reducer即可,前提是組件必須是hooks的方式。
目錄
- configureStore
- createAction
- createReducer
- createSlice
- createAsyncThunk
- createEntityAdapter
- 部分難點(diǎn)代碼的unit test
configureStore
configureStore是對(duì)標(biāo)準(zhǔn)的Redux的createStore函數(shù)的抽象封裝,添加了默認(rèn)值,方便用戶獲得更好的開發(fā)體驗(yàn)。
傳統(tǒng)的Redux,需要配置reducer、middleware、devTools、enhancers等,使用configureStore直接封裝了這些默認(rèn)值。代碼如下:
import { configureStore } from '@reduxjs/toolkit'
import rootReducer from './reducers'
// 這個(gè)store已經(jīng)集成了redux-thunk和Redux DevTools
const store = configureStore({ reducer: rootReducer })
相較于原生的Redux簡(jiǎn)化了很多,具體的Redux配置方法就不在這兒贅述了。
createAction、createReducer
createAction語(yǔ)法: function createAction(type, prepareAction?)
- type:Redux中的actionTypes
- prepareAction:Redux中的actions
如下:
const INCREMENT = 'counter/increment'
function increment(amount: number) {
return {
type: INCREMENT,
payload: amount,
}
}
const action = increment(3) // { type: 'counter/increment', payload: 3 }
createReducer簡(jiǎn)化了Redux reducer函數(shù)創(chuàng)建程序,在內(nèi)部集成了immer,通過(guò)在reducer中編寫可變代碼,簡(jiǎn)化了不可變的更新邏輯,并支持特定的操作類型直接映射到case reducer函數(shù),這些操作將調(diào)度更新狀態(tài)。
不同于Redux reducer使用switch case的方式,createReducer簡(jiǎn)化了這種方式,它支持兩種不同的形式:
- builder callback
- map object
第一種方式如下:
import { createAction, createReducer } from '@reduxjs/toolkit'
interface CounterState {
value: number
}
// 創(chuàng)建actions
const increment = createAction('counter/increment')
const decrement = createAction('counter/decrement')
const incrementByAmount = createAction<number>('counter/incrementByAmount')
const initialState: CounterState = { value: 0 }
// 創(chuàng)建reducer
const counterReducer = createReducer(initialState, (builder) => {
builder
.addCase(increment, (state, action) => {
// 使用了immer, 所以不需要使用原來(lái)的方式: return {...state, value: state.value + 1}
state.value++
})
.addCase(decrement, (state, action) => {
state.value--
})
.addCase(incrementByAmount, (state, action) => {
state.value += action.payload
})
})
看起來(lái)比Redux的actions和reducer要好一些,這兒先不講第二種方式map object,后面講到createSlice和createAsyncThunk結(jié)合使用時(shí)再講解。
Builder提供了三個(gè)方法
- addCase: 根據(jù)action添加一個(gè)reducer case的操作。
- addMatcher: 在調(diào)用actions前,使用matcher function過(guò)濾
- addDefaultCase: 默認(rèn)值,等價(jià)于switch的default case;
createSlice
createSlice對(duì)actions、Reducer的一個(gè)封裝,咋一看比較像dva的方式,是一個(gè)函數(shù),接收initial state、reducer、action creator和action types,這是使用RTK的標(biāo)準(zhǔn)寫法,它內(nèi)部使用了createAction和createReducer,并集成了immer,完成寫法如下:
// initial state interface
export interface InitialStateTypes {
loading: boolean;
visible: boolean;
isEditMode: boolean;
formValue: CustomerTypes;
customerList: CustomerTypes[];
fetchParams: ParamsTypes;
}
// initial state
const initialState: InitialStateTypes = {
loading: false,
visible: false,
isEditMode: false,
formValue: {},
customerList: [],
fetchParams: {},
};
// 創(chuàng)建一個(gè)slice
const customerSlice = createSlice({
name: namespaces, // 命名空間
initialState, // 初始值
// reducers中每一個(gè)方法都是action和reducer的結(jié)合,并集成了immer
reducers: {
changeLoading: (state: InitialStateTypes, action: PayloadAction<boolean>) => {
state.loading = action.payload;
},
changeCustomerModel: (state: InitialStateTypes, action: PayloadAction<IndexProps>) => {
const { isOpen, value } = action.payload;
state.visible = isOpen;
if (value) {
state.isEditMode = true;
state.formValue = value;
} else {
state.isEditMode = false;
}
},
},
// 額外的reducer,處理異步action的reducer
extraReducers: (builder: ActionReducerMapBuilder<InitialStateTypes>) => {
builder.addCase(fetchCustomer.fulfilled, (state: InitialStateTypes, { payload }) => {
const { content, pageInfo } = payload;
state.customerList = content;
state.fetchParams.pageInfo = pageInfo;
});
},
});
頁(yè)面?zhèn)髦等≈捣绞剑疤岜仨毷莌ooks的方式,class方式不支持:
import { useDispatch, useSelector } from 'react-redux';
import {
fetchCustomer,
changeCustomerModel,
saveCustomer,
delCustomer,
} from '@root/store/reducer/customer';
export default () => {
const dispatch = useDispatch();
// 取值
const { loading, visible, isEditMode, formValue, customerList, fetchParams } = useSelector(
(state: ReducerTypes) => state.customer,
);
useEffect(() => {
// dispatch
dispatch(fetchCustomer(fetchParams));
}, [dispatch, fetchParams]);
}
少了connect的連接,代碼優(yōu)雅不少。
createAsyncThunk
這兒講RTK本身集成的thunk,想使用redux-saga的自己配置,方式相同。
createAsyncThunk接受Redux action type字符串,返回一個(gè)promise callback。它根據(jù)傳入的操作類型前綴生成Promise的操作類型生命周期,并返回一個(gè)thunk action creator。它不跟蹤狀態(tài)或如何處理返回函數(shù),這些操作應(yīng)該放在reducer中處理。
用法:
export const fetchCustomer = createAsyncThunk(
`${namespaces}/fetchCustomer`,
async (params: ParamsTypes, { dispatch }) => {
const { changeLoading } = customerSlice.actions;
dispatch(changeLoading(true));
const res = await server.fetchCustomer(params);
dispatch(changeLoading(false));
if (res.status === 0) {
return res.data;
} else {
message.error(res.message);
}
},
);
createAsyncThunk可接受三個(gè)參數(shù)
- typePrefix: action types
- payloadCreator: { dispatch, getState, extra, requestId ...}, 平常開發(fā)只需要了解dispatch和getState就夠了,注:這兒的getState能拿到整個(gè)store里面的state
- options: 可選,{ condition, dispatchConditionRejection}, condition:可在payload創(chuàng)建成功之前取消執(zhí)行,return false表示取消執(zhí)行。
講createReducer時(shí),有兩種表示方法,一種是builder callback,即build.addCase(),一種是map object。下面以這種方式講解。
createAsyncThunk創(chuàng)建成功后,return出去的值,會(huì)在extraReducers中接收,有三種狀態(tài):
- pending: 'fetchCustomer/requestStatus/pending',運(yùn)行中;
- fulfilled: 'fetchCustomer/requestStatus/fulfilled',完成;
- rejected: 'fetchCustomer/requestStatus/rejected',拒絕;
代碼如下:
const customerSlice = createSlice({
name: namespaces, // 命名空間
initialState, // 初始值
// reducers中每一個(gè)方法都是action和reducer的結(jié)合,并集成了immer
reducers: {
changeLoading: (state: InitialStateTypes, action: PayloadAction<boolean>) => {
state.loading = action.payload;
},
changeCustomerModel: (state: InitialStateTypes, action: PayloadAction<IndexProps>) => {
const { isOpen, value } = action.payload;
state.visible = isOpen;
if (value) {
state.isEditMode = true;
state.formValue = value;
} else {
state.isEditMode = false;
}
},
},
// 額外的reducer,處理異步action的reducer
extraReducers: {
// padding
[fetchCustomer.padding]: (state: InitialStateTypes, action: PayloadAction<IndexProps>) => {},
// fulfilled
[fetchCustomer.fulfilled]: (state: InitialStateTypes, action: PayloadAction<IndexProps>) => {},
// rejected
[fetchCustomer.rejected]: (state: InitialStateTypes, action: PayloadAction<IndexProps>) => {},
}
});
對(duì)應(yīng)的builder.addCase的方式:
extraReducers: (builder: ActionReducerMapBuilder<InitialStateTypes>) => {
builder.addCase(fetchCustomer.padding, (state: InitialStateTypes, { payload }) => {});
builder.addCase(fetchCustomer.fulfilled, (state: InitialStateTypes, { payload }) => {});
builder.addCase(fetchCustomer.rejected, (state: InitialStateTypes, { payload }) => {});
},
createEntityAdapter
字面意思是創(chuàng)建實(shí)體適配器,目的為了生成一組預(yù)建的縮減器和選擇器函數(shù),對(duì)包含特定類型的對(duì)象進(jìn)行CRUD操作,可以作為case reducers 傳遞給createReducer和createSlice,也可以作為輔助函數(shù)。createEntityAdapter是根據(jù)@ngrx/entity移植過(guò)來(lái)進(jìn)行大量修改。其作用就是實(shí)現(xiàn)state范式化的思想。
Entity用于表示數(shù)據(jù)對(duì)象的唯一性,一般以id作為key值。
由createEntityAdapter方法生成的entity state結(jié)構(gòu)如下:
{
// 每個(gè)對(duì)象唯一的id,必須是string或number
ids: []
// 范式化的對(duì)象,實(shí)體id映射到相應(yīng)實(shí)體對(duì)象的查找表,即key為id,value為id所在對(duì)象的值,
entities: {}
}
創(chuàng)建一個(gè)createEntityAdapter:
type Book = {
bookId: string;
title: string;
};
export const booksAdapter = createEntityAdapter<Book>({
selectId: (book) => book.bookId,
sortComparer: (a, b) => a.title.localeCompare(b.title),
});
const bookSlice = createSlice({
name: 'books',
initialState: booksAdapter.getInitialState(),
reducers: {
// 添加一個(gè)book實(shí)體
bookAdd: booksAdapter.addOne,
// 接受所有books實(shí)體
booksReceived(state, action) {
booksAdapter.setAll(state, action.payload.books);
},
},
});
export const { bookAdd, booksReceived } = bookSlice.actions;
export default bookSlice.reducer;
組件中取值:
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
const dispatch = useDispatch();
const entityAdapter = useSelector((state: ReducerTypes) => state);
const books = booksAdapter.getSelectors((state: ReducerTypes) => state.entityAdapter);
console.log(entityAdapter);
// { ids: ['a001', 'a002'], entities: { a001: { bookId: 'a001', title: 'book1' }, a002: { bookId: 'a002', title: 'book2' } } }
console.log(books.selectById(entityAdapter, 'a001'));
// { bookId: 'a001', title: 'book1' }
console.log(books.selectIds(entityAdapter));
// ['a001', 'a002']
console.log(books.selectAll(entityAdapter));
// [{ bookId: 'a001', title: 'book1' }, { bookId: 'a002', title: 'book2' }]
useEffect(() => {
dispatch(bookAdd({ bookId: 'a001', title: 'book1' }));
dispatch(bookAdd({ bookId: 'a002', title: 'book2' }));
}, []);
從提供的方法中,可以獲取到原始的數(shù)組值,范式化后的key-value方式,可以獲取以存儲(chǔ)key的數(shù)組ids,就是state范式化。
unit test
公共部分:
const dispatch = jest.fn();
const getState = jest.fn(() => ({
dispatch: jest.fn(),
}));
const condition = jest.fn(() => false);
- reducers中方法,actions單元測(cè)試:
const action = changeCustomerModel({
isOpen: true,
value,
});
expect(action.payload).toEqual({
isOpen: true,
value,
});
- thunk actions(createAsyncThunk)單元測(cè)試
const mockData = {
status: 0,
data: {
content: [
{
id: '001',
code: 'table001',
name: '張三',
phoneNumber: '15928797333',
address: '成都市天府新區(qū)',
},
],
},
}
// server.fetchCustomer方法mock數(shù)據(jù)
server.fetchCustomer.mockResolvedValue(mockData);
// 執(zhí)行thunk action異步方法
const result = await fetchCustomer(params)(dispatch, getState, { condition });
// 請(qǐng)求接口數(shù)據(jù),斷言是否是mock的數(shù)據(jù)
expect(await server.fetchCustomer(params)).toEqual(mockData);
// dispatch設(shè)置loading狀態(tài)為true
dispatch(changeLoading(true));
// 斷言thunk action執(zhí)行成功
expect(fetchCustomer.fulfilled.match(result)).toBe(true);
// 執(zhí)行extraReducers的fetchCustomer.fulfilled
customerReducer(
initState,
fetchCustomer.fulfilled(
{
payload: {
content: [value],
pageInfo: initState.fetchParams.pageInfo,
},
},
'',
initState.fetchParams,
),
);
// 斷言第一次dispatch設(shè)置loading為true
expect(dispatch.mock.calls[1][0]).toEqual({
payload: true,
type: 'customer/changeLoading',
});
// 請(qǐng)求成功,第二次dispatch設(shè)置loading為false
expect(dispatch.mock.calls[2][0]).toEqual({
payload: false,
type: 'customer/changeLoading',
});
// thunk action return 到extraReducers的值
expect(dispatch.mock.calls[3][0].payload).toEqual(mockData.data);
后記
寫的有點(diǎn)凌亂,就是當(dāng)做筆記來(lái)記錄的,有寫的不對(duì)的地方不吝賜教。