前言
年中,公司启动新项目,需要搭建微前端架构,经过多番调研,确定了乾坤、umi、dva
的技术方案,开始一个月就遇到了大的难题。第一,dva是约定式,不能灵活的配置;第二,乾坤并不能完全满足业务需求,需要更改很多源码,比如主子通信,兄弟通信等。经过一番取舍,放弃了这个方案。后基于single-spa,搭建一套微前端架构,同时通过命令生成模板,类似create-react-app,使用技术栈react、redux。
之前习惯了dva的操作方法,使用redux比较繁琐,因新项目比较庞大,不建议使用mobx。调研了多种方案,最终选择redux作者Dan Abramov今年三月份出的工具库@reduxjs/tooltik(以下简称RTK)。
简介
RTK旨在帮助解决关于Redux的三个问题:
- 配置Redux存储太复杂;
- 必须添加很多包才能让Redux做预期的事情;
- Redux需要太多样板代码;
简单讲配置Redux存储的流程太复杂,完整需要actionTypes、actions、reducer、store、通过connect连接。使用RTK
,只需一个reducer即可,前提是组件必须是hooks的方式。
目录
- configureStore
- createAction
- createReducer
- createSlice
- createAsyncThunk
- createEntityAdapter
- 部分难点代码的unit test
configureStore
configureStore是对标准的Redux的createStore函数的抽象封装,添加了默认值,方便用户获得更好的开发体验。
传统的Redux,需要配置reducer、middleware、devTools、enhancers等,使用configureStore直接封装了这些默认值。代码如下:
import { configureStore } from '@reduxjs/toolkit'
import rootReducer from './reducers'
// 这个store已经集成了redux-thunk和Redux DevTools
const store = configureStore({ reducer: rootReducer })
相较于原生的Redux简化了很多,具体的Redux配置方法就不在这儿赘述了。
createAction、createReducer
createAction语法: 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简化了Redux reducer函数创建程序,在内部集成了immer,通过在reducer中编写可变
代码,简化了不可变的更新逻辑,并支持特定的操作类型直接映射到case reducer函数,这些操作将调度更新状态。
不同于Redux reducer使用switch case的方式,createReducer
简化了这种方式,它支持两种不同的形式:
- builder callback
- map object
第一种方式如下:
import { createAction, createReducer } from '@reduxjs/toolkit'
interface CounterState {
value: number
}
// 创建actions
const increment = createAction('counter/increment')
const decrement = createAction('counter/decrement')
const incrementByAmount = createAction('counter/incrementByAmount')
const initialState: CounterState = { value: 0 }
// 创建reducer
const counterReducer = createReducer(initialState, (builder) => {
builder
.addCase(increment, (state, action) => {
// 使用了immer, 所以不需要使用原来的方式: return {...state, value: state.value + 1}
state.value++
})
.addCase(decrement, (state, action) => {
state.value--
})
.addCase(incrementByAmount, (state, action) => {
state.value += action.payload
})
})
看起来比Redux的actions和reducer要好一些,这儿先不讲第二种方式map object
,后面讲到createSlice和createAsyncThunk结合使用时再讲解。
Builder提供了三个方法
- addCase: 根据action添加一个reducer case的操作。
- addMatcher: 在调用actions前,使用matcher function过滤
- addDefaultCase: 默认值,等价于switch的default case;
createSlice
createSlice对actions、Reducer的一个封装,咋一看比较像dva的方式,是一个函数,接收initial state、reducer、action creator和action types,这是使用RTK的标准写法,它内部使用了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: {},
};
// 创建一个slice
const customerSlice = createSlice({
name: namespaces, // 命名空间
initialState, // 初始值
// reducers中每一个方法都是action和reducer的结合,并集成了immer
reducers: {
changeLoading: (state: InitialStateTypes, action: PayloadAction) => {
state.loading = action.payload;
},
changeCustomerModel: (state: InitialStateTypes, action: PayloadAction) => {
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) => {
builder.addCase(fetchCustomer.fulfilled, (state: InitialStateTypes, { payload }) => {
const { content, pageInfo } = payload;
state.customerList = content;
state.fetchParams.pageInfo = pageInfo;
});
},
});
页面传值取值方式,前提必须是hooks的方式,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的连接,代码优雅不少。
createAsyncThunk
这儿讲RTK本身集成的thunk,想使用redux-saga的自己配置,方式相同。
createAsyncThunk接受Redux action type字符串,返回一个promise callback。它根据传入的操作类型前缀生成Promise的操作类型生命周期,并返回一个thunk action creator。它不跟踪状态或如何处理返回函数,这些操作应该放在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可接受三个参数
- typePrefix: action types
- payloadCreator: { dispatch, getState, extra, requestId ...}, 平常开发只需要了解dispatch和getState就够了,注:这儿的getState能拿到整个store里面的state
- options: 可选,{ condition, dispatchConditionRejection}, condition:可在payload创建成功之前取消执行,return false表示取消执行。
讲createReducer时,有两种表示方法,一种是builder callback
,即build.addCase(),一种是map object
。下面以这种方式讲解。
createAsyncThunk创建成功后,return出去的值,会在extraReducers中接收,有三种状态:
- pending: 'fetchCustomer/requestStatus/pending',运行中;
- fulfilled: 'fetchCustomer/requestStatus/fulfilled',完成;
- rejected: 'fetchCustomer/requestStatus/rejected',拒绝;
代码如下:
const customerSlice = createSlice({
name: namespaces, // 命名空间
initialState, // 初始值
// reducers中每一个方法都是action和reducer的结合,并集成了immer
reducers: {
changeLoading: (state: InitialStateTypes, action: PayloadAction) => {
state.loading = action.payload;
},
changeCustomerModel: (state: InitialStateTypes, action: PayloadAction) => {
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) => {},
// fulfilled
[fetchCustomer.fulfilled]: (state: InitialStateTypes, action: PayloadAction) => {},
// rejected
[fetchCustomer.rejected]: (state: InitialStateTypes, action: PayloadAction) => {},
}
});
对应的builder.addCase的方式:
extraReducers: (builder: ActionReducerMapBuilder) => {
builder.addCase(fetchCustomer.padding, (state: InitialStateTypes, { payload }) => {});
builder.addCase(fetchCustomer.fulfilled, (state: InitialStateTypes, { payload }) => {});
builder.addCase(fetchCustomer.rejected, (state: InitialStateTypes, { payload }) => {});
},
createEntityAdapter
字面意思是创建实体适配器
,目的为了生成一组预建的缩减器和选择器函数,对包含特定类型的对象进行CRUD操作,可以作为case reducers 传递给createReducer和createSlice,也可以作为辅助函数。createEntityAdapter是根据@ngrx/entity移植过来进行大量修改。其作用就是实现state范式化的思想。
Entity
用于表示数据对象的唯一性,一般以id作为key值。
由createEntityAdapter方法生成的entity state
结构如下:
{
// 每个对象唯一的id,必须是string或number
ids: []
// 范式化的对象,实体id映射到相应实体对象的查找表,即key为id,value为id所在对象的值,
entities: {}
}
创建一个createEntityAdapter:
type Book = {
bookId: string;
title: string;
};
export const booksAdapter = createEntityAdapter({
selectId: (book) => book.bookId,
sortComparer: (a, b) => a.title.localeCompare(b.title),
});
const bookSlice = createSlice({
name: 'books',
initialState: booksAdapter.getInitialState(),
reducers: {
// 添加一个book实体
bookAdd: booksAdapter.addOne,
// 接受所有books实体
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' }));
}, []);
从提供的方法中,可以获取到原始的数组值,范式化后的key-value方式,可以获取以存储key的数组ids,就是state范式化。
unit test
公共部分:
const dispatch = jest.fn();
const getState = jest.fn(() => ({
dispatch: jest.fn(),
}));
const condition = jest.fn(() => false);
- reducers中方法,actions单元测试:
const action = changeCustomerModel({
isOpen: true,
value,
});
expect(action.payload).toEqual({
isOpen: true,
value,
});
- thunk actions(createAsyncThunk)单元测试
const mockData = {
status: 0,
data: {
content: [
{
id: '001',
code: 'table001',
name: '张三',
phoneNumber: '15928797333',
address: '成都市天府新区',
},
],
},
}
// server.fetchCustomer方法mock数据
server.fetchCustomer.mockResolvedValue(mockData);
// 执行thunk action异步方法
const result = await fetchCustomer(params)(dispatch, getState, { condition });
// 请求接口数据,断言是否是mock的数据
expect(await server.fetchCustomer(params)).toEqual(mockData);
// dispatch设置loading状态为true
dispatch(changeLoading(true));
// 断言thunk action执行成功
expect(fetchCustomer.fulfilled.match(result)).toBe(true);
// 执行extraReducers的fetchCustomer.fulfilled
customerReducer(
initState,
fetchCustomer.fulfilled(
{
payload: {
content: [value],
pageInfo: initState.fetchParams.pageInfo,
},
},
'',
initState.fetchParams,
),
);
// 断言第一次dispatch设置loading为true
expect(dispatch.mock.calls[1][0]).toEqual({
payload: true,
type: 'customer/changeLoading',
});
// 请求成功,第二次dispatch设置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);
后记
写的有点凌乱,就是当做笔记来记录的,有写的不对的地方不吝赐教。
参考文献
- https://redux-toolkit.js.org/introduction/quick-start
- https://redux.js.org/recipes/structuring-reducers/normalizing-state-shape