软件技术学习笔记

个人博客,记录软件技术与程序员的点点滴滴。

Redux中间件: ChanMiddleware

在开发客户端渲染(CSR)与服务端渲染(SSR)共用的前端代码时,需要使用Redux初始化状态。但是发现Redux-saga的take等函数只能在createStore()之后才能使用,不是很方便。于是,造一个跨组件通信的Redux side-effect中间件: ChanMiddleware 。

ChanMiddleware支持以下3个函数:

interface ChanHandler {
  (state?: any, action?: AnyAction, dispatch?: Dispatch, next?: () => void): void
}

const use = (type: string, handler: ChanHandler) => void;
const useOnce = (type: string, handler: ChanHandler) => void;
const unUse = (type: string, handler: ChanHandler) => void;

type等于'‘时,handler处理所有的Action。

1. ChanMiddleware源码

// chanMiddleware.ts
import {
  AnyAction, Store, Dispatch,
} from 'redux';

export interface ChanHandler {
  (state?: any, action?: AnyAction, dispatch?: Dispatch, next?: () => void): void
}

const chanNextArgsRequired = 4;

function createChanMiddleware(opts = {}) {
  const channels: {
    [type: string]: ChanHandler[];
  } = {};

  const use = (type: string, handler: ChanHandler) => {
    if (typeof handler !== 'function') {
      throw new Error('Channel handler must be a function');
    }

    const handlers = channels[type];

    if (handlers) {
      const index = handlers.indexOf(handler);

      if (index === -1) {
        handlers.push(handler);
      }

      return;
    }

    channels[type] = [handler];
  };

  const unUse = (type: string, handler: ChanHandler) => {
    const handlers = channels[type];

    if (handlers) {
      const index = handlers.indexOf(handler);

      if (index !== -1) {
        handlers.splice(index, 1);
      }
    }
  };

  const useOnce = (type: string, handler: ChanHandler) => {
    const wrapHandler: ChanHandler = (state, action, dispatch, next) => {
      unUse(type, wrapHandler);
      const argLen = handler.length;

      if (argLen < chanNextArgsRequired) {
        handler(state, action, dispatch);
        next();
      } else {
        handler(state, action, dispatch, next);
      }
    };

    use(type, wrapHandler);
  };

  const dispatchChan = (store: Store, action: AnyAction) => {
    if (!action || !action.type) {
      return;
    }

    const state = store.getState();
    const { dispatch } = store;

    const handlers = [...(channels[''] || []), ...(channels[action.type] || [])];

    if (handlers.length === 0) {
      return;
    }

    let i = 0;

    const next = () => {
      if (i >= handlers.length) {
        return;
      }

      const handler = handlers[i];
      i += 1;

      const argLen = handler.length;

      if (argLen < chanNextArgsRequired) {
        handler(state, action, dispatch);
        next();
      } else {
        handler(state, action, dispatch, next);
      }
    };

    next();
  };

  const chanMiddleware = (store: Store) => (next) => (action: AnyAction) => {
    const result = next(action);
    dispatchChan(store, action);
    return result;
  };

  chanMiddleware.use = use;
  chanMiddleware.useOnce = useOnce;
  chanMiddleware.unUse = unUse;

  return chanMiddleware;
}

export default createChanMiddleware;

2. ChanMiddleware的使用

创建Chan中间件:

// store.ts
const chanMiddleware = createChanMiddleware();
const enhancer = composeEnhancers(
  applyMiddleware(thunk, chanMiddleware),
);

使用use():

// booksChan.ts
import { ChanHandler } from '../redux/chanMiddleware';
import { setBooks } from '../redux/actionCreators';
import { RELOAD_BOOKS } from '../redux/actionTypes';
import { chanMiddleware } from '../redux/store';

import fetchBooks from '../service/fetchBooks';

const reloadBooksHandler: ChanHandler = async (state, action, dispatch) => {
  const books = await fetchBooks();
  dispatch(setBooks(books));
};

chanMiddleware.use(RELOAD_BOOKS, reloadBooksHandler);

3. 局限性

在编写客户端渲染与服务端渲染共用的代码时,需要了解chanMiddleware全局实例的局限性。

3.1. 服务端渲染

服务端渲染时,如果使用全局chanMiddleware实例,没有做到每个请求之间隔离,ChanHandler最好是纯函数,避免状态跨越多个请求。这种用法最简单,但是有这一点局限性。

可以在每个请求的处理函数中使用createChanMiddleware()动态创建中间件,然后调用chanMiddleware.use()动态注册ChanHandler,做到请求之间完全隔离。但是,这种用法比较复杂。

3.2. 客户端渲染

客户端渲染时,没有服务端渲染的问题,可以使用全局chanMiddleware实例。