为 dva 提供更好的类型支持

3,755 阅读4分钟

背景

为了使 dva 搭配 react hooks 带来更好的 typescript 支持,提高代码的可维护性,对dva的写法进行了改造,使用 class 的写法带来更好的类型支持。 同时结合 umi 3.x 带来更好的开发体验。

思路

  1. 设计 dvaModel(类装饰器),reducer(方法装饰器),effect(方法装饰器),state(属性装饰器),subscription(方法装饰器)等来改变 class 的行为。
  2. 通过装饰器,在 class 原型链 上,根据不同的装饰器类型,将方法转化成 dva model 相对应的结构。
  3. 根据不同的装饰器,改变方法,保持方法的传入参数不变,改变方法的返回值为 redux action。
  4. 兼容原生的 dispatch 方法。
  5. 依赖 umi 3 的插件机制,自动收集 state 的类型定义 和 自动实例化 class, 加载到全局容器中。

使用

npm install dva-model-enhance

umi 3.x

结合 umi 3.x 脚手架,自动加载 class 形式的 model,同时能够自动实例化 class 同时加载到 全局容器 modelsContainer 中。并且按照一定的约定,自动收集 class model 导出的 State 类型。参考例子 umi-example

需要搭配 umi-plugin-dva-enhance 使用

在 umi 项目中

npm install dva-model-enhance
npm install umi-plugin-dva-enhance
修改 umi 配置
// /.umirc.ts
export default {
    dva: {
        hmr: true,
        // 默认为 false,且必须 设置 false,否则 plugin-dva 会重复加载 model
        skipModelValidate: false
    },
    "dva-enhance": {
        // class model 中对外导出的 state 名称, 生成的 StoreState.ts 中需要引用
        // renderStateName?: (namespace: string, path: string) => string;
        // 是否跳过 class model 验证, 跳过后生成的文件 namespace 默认取 文件名称, 默认值 false;
        // skipClassModelValidate?: boolean;
    },
    // 如果提示 plugin umi-plugin-dva-enhance is already registered, 就不用显示添加插件
    plugins: ["umi-plugin-dva-enhance"]
};
在 src/models 下任意添加一个 原生的 model, 来强制启用 @umijs/plugin-dva 插件相关功能,举例如下
/**
 * 添加了一个 dva model, 用于开启 @umijs/plugin-dva 插件
 * 插件内部判断 没有 符合条件的 model 时 不启用 dva 相关功能
 */
export default {
    namespace: "__enableDva",
    state: {}
};
修改写法
export default {
    namespace: 'test',
    state: { count: 0  },
    effects: {
        *initCount({ payload }, { put, call }) {
            const result = yield call(() => Promise.resolve(100));
            yield put(
                this.setState({
                    count: result
                })
            );
        }
    },
    reducers: {
        setState(state, { payload }) {
            return {
                ...state,
                ...payload,
            }
        },
        addCount(state) {
            return {
                ...state,
                count: state.count + 1,
            };
        }
    }
}
修改为 class 写法
import { effect, reducer, dvaModel, BaseModel } from "dva-model-enhance";
import StoreState from 'umi';

interface TestLocalState {
    count: number;
}

export interface TestState extends TestLocalState {}

@dvaModel<TestLocalState>({
    namespace: "test",
    state: {
        count: 0
    }
})
class Test extends BaseModel<TestState, StoreState> {
    @effect()
    *initCount() {
        const result = yield this.effects.call(() => Promise.resolve(100));
        yield this.effects.put(
            this.setState({
                count: result
            })
        );
    }

    @reducer
    addCount() {
        return {
            ...this.state,
            count: this.state.count + 1
        };
    }
}
export default Test;
如下 可以在 umi 中直接导出 actions 和 StoreState
  • actions 是所有 class model 实例
  • StoreState 是所有 class model 文件下 导出的类型集合
  • 使用 dva-model-enhance 中的 useDispatch 传入 actions 可以提供 dva 方法的类型提示
import React, { useEffect } from "react";
import { useSelector, actions, StoreState } from "umi";
import { useDispatch } from "dva-model-enhance";

export default () => {
    const dispatch = useDispatch(actions);
    const state = useSelector((state: StoreState) => state);

    useEffect(() => {
        console.log(state.test, "state");
    }, []);
    return (
        <div>
            {state.test.count}
            <button
                onClick={() => {
                    dispatch.test.addCount();
                }}
            >
                click
            </button>
        </div>
    );
};

和 dva 一起使用

  1. 编写 model
import { effect, reducer, dvaModel, BaseModel } from "dva-model-enhance";

export interface TestState {
  count: number;
}

// 全部的 redux 节点状态,需要在外面统一维护
interface StoreState {
    test: TestState
}

@dvaModel<TestState>({
  namespace: "test",
  state: {
    count: 0
  }
})
class Test extends BaseModel<TestState, StoreState> {
  @effect({ type: 'takeLatest' })
  *initCount() {
    const result: number = yield this.effects.call(() => Promise.resolve(100));
    yield this.effects.put(this.setCount(result));
  }

  @reducer
  setCount(count: number) {
    return {
      ...this.state,
      count,
    };
  }
}

  1. dva 加载 mdel
import dva from 'dva';
import { getModel } from 'dva-model-enhance';

const app = dva({
    namespacePrefixWarning: false, // 取消 dva 的警告
});
app.model(getModel(Test)) // dva 加载 class 形式的 mdel
  1. 在组件中使用
import React from 'react';
import { useDispatch } from 'dva';
import { modelsContainer } from 'dva-model-enhance';
// 引入编写的 class 形式的 model
import Test from './models/test';

const test = new Test();
/**
 * 向 models 容器中添加 以 namespace 为 key 的实例
 * 确保所有model对象是单例,这是为了让
 * dispatch({
 *     type: 'test/setCount',
 *     payload: [50],
 * })
 * 等价于
 * dispatch(test.setCount(50))
 *
 * 这种形式的写法 能正常访问 class model 中的方法
 */
modelsContainer.set('test', test);

interface Props {}

const App: React.FC<Props> = ({ }) => {
    const dispatch = useDispatch();
    const handleClick = () => {
        dispatch(
            test.setCount(50)
        )
    }
    return (
        <div>
            count: {count}
            <button onClick={handleClick}>add</button>
        </div>
    );
};

export default App;
  1. 优化 dispatch 的使用

将 class model 统一管理,希望通过 dispatch 能直接访问到 model 的方法,并具有类型提示

// ./actions.ts
import { modelsContainer } from 'dva-model-enhance';
// 引入编写的 class 形式的 model
import Test from './models/test';

const actions = {
    test: new Test(),
}

modelsContainer.put(actions);
export default actions;
import React from 'react';
import { useDispatch } from 'dva-model-enhance';
import actions from './actions'
interface Props {}

const App: React.FC<Props> = ({ }) => {
    const dispatch = useDispatch<typeof actions>(actions);
    const handleClick = () => {
        dispatch.test.setCount(50)
    }
    return (
        <div>
            count: {count}
            <button onClick={handleClick}>add</button>
        </div>
    );
};