redux黑魔法@reduxjs/tootik

前言

年中,公司啟動(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的方式。

目錄

  1. configureStore
  2. createAction
  3. createReducer
  4. createSlice
  5. createAsyncThunk
  6. createEntityAdapter
  7. 部分難點(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?)

  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簡(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)化了這種方式,它支持兩種不同的形式:

  1. builder callback
  2. 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è)方法

  1. addCase: 根據(jù)action添加一個(gè)reducer case的操作。
  2. addMatcher: 在調(diào)用actions前,使用matcher function過(guò)濾
  3. 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ù)

  1. typePrefix: action types
  2. payloadCreator: { dispatch, getState, extra, requestId ...}, 平常開發(fā)只需要了解dispatch和getState就夠了,注:這兒的getState能拿到整個(gè)store里面的state
  3. 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):

  1. pending: 'fetchCustomer/requestStatus/pending',運(yùn)行中;
  2. fulfilled: 'fetchCustomer/requestStatus/fulfilled',完成;
  3. 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);
  1. reducers中方法,actions單元測(cè)試:
const action = changeCustomerModel({
      isOpen: true,
      value,
    });
    expect(action.payload).toEqual({
      isOpen: true,
      value,
    });
  1. 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ì)的地方不吝賜教。

參考文獻(xiàn)

  1. https://redux-toolkit.js.org/introduction/quick-start
  2. https://redux.js.org/recipes/structuring-reducers/normalizing-state-shape
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容