最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • @reduxjs/tooltik黑魔法

    正文概述 掘金(chicABoo)   2020-12-15   1421

    前言

    年中,公司启动新项目,需要搭建微前端架构,经过多番调研,确定了乾坤、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的方式。

    目录

    1. configureStore
    2. createAction
    3. createReducer
    4. createSlice
    5. createAsyncThunk
    6. createEntityAdapter
    7. 部分难点代码的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?)

    1. type:Redux中的actionTypes
    2. 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简化了这种方式,它支持两种不同的形式:

    1. builder callback
    2. 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<number>('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提供了三个方法

    1. addCase: 根据action添加一个reducer case的操作。
    2. addMatcher: 在调用actions前,使用matcher function过滤
    3. 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<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;
        });
      },
    });
    

    页面传值取值方式,前提必须是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可接受三个参数

    1. typePrefix: action types
    2. payloadCreator: { dispatch, getState, extra, requestId ...}, 平常开发只需要了解dispatch和getState就够了,注:这儿的getState能拿到整个store里面的state
    3. options: 可选,{ condition, dispatchConditionRejection}, condition:可在payload创建成功之前取消执行,return false表示取消执行。

    讲createReducer时,有两种表示方法,一种是builder callback,即build.addCase(),一种是map object。下面以这种方式讲解。 createAsyncThunk创建成功后,return出去的值,会在extraReducers中接收,有三种状态:

    1. pending: 'fetchCustomer/requestStatus/pending',运行中;
    2. fulfilled: 'fetchCustomer/requestStatus/fulfilled',完成;
    3. rejected: 'fetchCustomer/requestStatus/rejected',拒绝;

    代码如下:

    const customerSlice = createSlice({
      name: namespaces, // 命名空间
      initialState, // 初始值
      // reducers中每一个方法都是action和reducer的结合,并集成了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>) => {},
      }
    });
    

    对应的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

    字面意思是创建实体适配器,目的为了生成一组预建的缩减器和选择器函数,对包含特定类型的对象进行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<Book>({
      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);
    
    1. reducers中方法,actions单元测试:
    const action = changeCustomerModel({
          isOpen: true,
          value,
        });
        expect(action.payload).toEqual({
          isOpen: true,
          value,
        });
    
    1. 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);
    

    后记

    写的有点凌乱,就是当做笔记来记录的,有写的不对的地方不吝赐教。

    参考文献

    1. redux-toolkit.js.org/introductio…
    2. redux.js.org/recipes/str…

    起源地下载网 » @reduxjs/tooltik黑魔法

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元