<h1 align="center"> <a href="https://redux-model.github.io/redux-model"> Redux Model </a> </h1>


Keywords
vue, redux-model, redux, model, typescript, web, h5, html5, browser, react-redux, vue-redux, taro-redux, redux-typescript, react-model, redux-taro, typescript-redux
License
MIT
Install
npm install @redux-model/vue@9.0.6

Documentation

English Document | 简体中文文档

Redux模型是对原生redux的一次面向对象封装,OOP方案可以实现隐藏重复代码、提高工作效率以及减少开发时间的效果。你只需要花半个小时,就能完全了解模型的用法,并从中受益。

特性

  • 纯面向对象
  • 支持mvvm操作
  • 代码量比函数式redux写法少一半
  • action与reducer合体
  • 无需定义types
  • 完美支持typescript,拥有100%无死角的代码类型提示
  • 请求操作内置loading状态

安装

H5 / Web

# 使用 npm 或者 yarn
npm install @redux-model/web
npm install redux redux-thunk react-redux

redux-thunk并不是必须的,除非你想使用thunk的特性

如果你想使用react的hooks特性,请保持react的版本在16.8.3+以及react-redux的版本在7.1.0+

React-Native

# 使用 npm 或者 yarn
npm install @redux-model/react-native
npm install redux redux-thunk react-redux

Taro

# 使用 npm 或者 yarn
npm install @redux-model/taro
npm install redux redux-thunk @tarojs/redux @tarojs/redux-h5

运行案例(Demo)

请查看项目:redux-model-ts-demo

代码片段

请在vscode的扩展中搜索插件 bluewaitor.tsreact

用上代码片段之后,你基本上不用写一行redux代码。

使用

定义Model

想要定义reducer,就必须先定义一个模型类,因为一个模型可以包含一个或不带reducer。我们需要为reducer定义一个接口,并注入到模型中,这样我们就可以在整个项目中得到数据类型的提示。

// test.ts
import { Model } from '@redux-model/*';

interface Data {
  foo: string;
}

class Test extends Model<Data> {
  protected initReducer(): Data {
    return {
      foo: 'init',
    };
  }
}

export const test = new Test();

如果你不想使用reducer,那么你可以在initReducer()方法中返回null并移除注入的泛型Data

注册Reducer

我们都知道,reducer是要挂载到store中的,所以我们为实例化后的模型提供了一个register()方法。

// reducers.ts
import { combineReducers } from 'redux';

const reducers = {
  ...test.register(),
};

export const rootReducers = combineReducers(reducers);

定义Action

在模型中,我们只需要3种类型的action,而且一个模型支持写入无限个action。

  • 普通action
  • 异步请求action
  • thunk action

定义普通action

普通的action是最基础的action,它的作用就是同步发送一次消息

// test.ts
class Test extends Model<Data> {
  myFirstAction = this.actionNormal({
    action: (name: string) => {
      return this.emit({
        name,
      });
    },
    onSuccess: (state, action) => {
      state.foo = 'new name: ' + action.payload.name;
    },
  });
}

export const test = new Test();

onSuccess()的作用是改变当前模型的reducer值,但它不是必须定义的,你可以删除它,意味着执行这个action不会影响这个模型的reducer。

待会我会告诉你如何用这个action去影响其他模型的reducer数据。


先让我们看看普通action如何使用在React组件中。我们可以通过connect()方法注入action

// By Connect
import React, { FunctionComponent } from 'react';
import { connect } from 'react-redux';
import { test } from './Test';

type Props = ReturnType<typeof mapStateToProps>;

const App: FunctionComponent<Props> = (props) => {
  const { runAction, name } = props;

  return (
    <button onClick={() => test.myFirstAction.action('New Name')}>
      Click me: {name}
    </button>
  );
};

const mapStateToProps = () => {
  return {
    name: test.connectData().foo,
  };
};

export default connect(mapStateToProps)(App);

因为我们在action中定义了onSuccess()方法,所以一旦你点击了按钮,执行runAction会立马变更test模型中reducer数据


如果你的React版本>=16.8.3,而且react-redux的版本>=7.1.0,那么你可以用hooks实现数据的注入,这样做可以让你的代码看起来更清晰

// By React Hooks
import React, { FunctionComponent } from 'react';
import { test } from './Test';

const App: FunctionComponent = () => {
  const name = test.useData((item) => item.foo);

  return (
    <button onClick={() => test.myFirstAction.action('New Name')}>
      Click me: {name}
    </button>
  );
};

export default App;

定义异步请求Action

我们总是需要请求后端接口以展示动态的内容,其中包含很多细节需要处理。在数据成功返回之前,我们可能需要确保界面上有loading加载条。在数据返回失败时,我们需要展示错误的弹窗信息。在数据更新成功的时候,我们需要展示成功的弹窗信息。

别紧张,这一切我都替你想好了。

为了确保action写起来足够简单,我们把诸多细节隐藏到middleware中,所以在开始使用异步请求action之前,我们需要先定义一个自己的中间件

// apiMiddleware.ts
import { createRequestMiddleware, Model } from '@redux-model/*';

export const apiMiddleware = createRequestMiddleware({
  // action和中间件的对应关系
  id: Model.middlewareName,
  // 请求的通用地址前缀
  baseUrl: 'http://api.xxx.com',
  // 请求头信息
  getHeaders: ({ getState }) => {
    // header一般要带token等信息做权限校验,如果token存在reducer中,那么可以直接获取:
    // const token = tokenModel.connectData().access_token;
    return {
      Authorization: `Bearer token`,
      Accept: 'application/json',
     'Content-Type': 'application/json',
   };
  },
  // 定位业务场景下的错误码等信息,会自动存入meta中
  onFail: (error: HttpError<{}>, transform) => {
    const { data } = error.response;

    transform.businessCode = data ? data.code : undefined;
    transform.errorMessage = (data && data.message) || error.message;
  },
  // 可以做一些弹窗操作。
  // 只有当模型提供了successText属性才会触发。
  onShowSuccess: (successText) => {
    alert(successText);
  },
  // 可以做一些弹窗操作。
  // 只有当请求异常或者失败时才会触发。
  // 模型中提供了 hideError 属性时,不再触发。
  onShowError: (errorMessage) => {
    alert(errorMessage);
  },
});

接着注入到store中

// middlewares.ts
import { createReduxStore } from '@redux-model/*';
import { compose, applyMiddleware } from 'redux';
import { apiMiddleware } from './apiMiddleware.ts';
import { rootReducers } from './reducers.ts';

const store = createReduxStore(
  rootReducers,
  {},
  compose(applyMiddleware(apiMiddleware)),
);

好了,准备就绪,开始写第一个异步action

// profile.ts
interface Data {
  id: number;
  name: string;
}

class ProfileModel extends Model<Data> {
  manage = this.actionRequest({
    action: (id: number) => {
      return this.get({
        uri: '/test/api',
        query: {
          id: page,
        },
      });
    },
    onSuccess: (state, action) => {
      return action.response;
    },
  });

  edit = this.actionRequest({
    action: (id: number, name: string) => {
      return this.put({
        uri: `/test/api/${id}`,
        body: {
          name: name,
        },
        payload: {
          name: name,
        },
        successText: '信息更新成功',
      });
    },
    onSuccess: (state, action) => {
      state.name = action.payload.name;
    },
  });

  protected initReducer(): Data {
    return {
      id: 0,
      name: '',
    };
  }
}

export const profileModel = new ProfileModel();

我们有更多请求的参数还没有列出来:

uri  [string] required
请求的相对路径

query  [object]
查询字符串

body  [object]
请求实体,仅在post put patch delete中有效

payload  [object]
额外数据,在改变reducer时使用

hideError  [boolean | (response) => boolean]
请求出错时是否隐藏错误

successText  [string]
请求成功时要展示的成功文字

定义Thunk Action

假设你已经知道什么是 Redux Thunk,并且已经把thunk middleware放进了store中。那么我们来看看怎么定义

// test.ts
import { profileModel } from './ProfileModel.ts';

class Test extends Model {
  myFirstAction = this.actionNormal(...);

  /////////////////////////////////
  /// 使用方法:test.myThunk();  ///
  ////////////////////////////////
  myThunk = this.actionThunk((/* 在这里定义action传入的参数 */) => {
      this.myFirstAction.action();
      profileModel.manage.action();
      ...
  });
}

export const test = new Test();

模型交叉

有时候,执行某个模型下的action可能需要变更其它模型的reducer数据,这是很常见的操作方式,我们提供了一个保护方法effects()来做这个事情。

class Other extends Model {
  reset = this.action.actionNormal(...);
  request = this.action.actionRequest(...);
}

const other = new Other();

// --------

import { Effects, Model } from '@redux-model/*';
interface Data {
  foo: string;
}

class Test extends Model<Data> {
  protected effects(): Effects<Data> {
    return [
      other.reset.onSuccess((state, action) => {
        return {
          foo: 'Oops, reset',
        };
      }),
      other.request.onSuccess((state, action) => {
        return {
          foo: action.response.name,
        };
      }),
      other.request.onFail((state, action) => {
        return {
          foo: 'reset again',
        };
      }),
    ];
  }

  protected initReducer(): Data {
    return {
      foo: 'init',
    };
  }
}

对于普通的action,我们使用model.action.onSuccess(fn)来监听数据的变化。如果是异步请求action,我们总共有onPrepare(fn) onSuccess(fn) onFail(fn) 3个监听事件

异步请求Promise

对于异步请求的action,我们可以在React组件中使用Promise方法,并获得请求的数据。注意,这里也有100%的response代码提示

// By React Hooks
import React, { FunctionComponent } from 'react';
import { profileModel } from './ProfileModel.ts';

const App: FunctionComponent = (props) => {
  const name = profileModel.useData((item) => item.name);
  const handleClick = () => {
    profileModel.manage.action(1)
      .then(({ response }) => {
        console.log('Hello, ' + response.name);
      })
      .catch(() => {
        console.warn('What is wrong?');
      })
      .finally(() => {
        console.log('Wow, cool bro.');
      });
  };

  return (
    <button onClick={this.handleClick}>
      Click me: {name}
    </button>
  );
};

export default App;

异步请求Loading

每个异步请求action都带有loading状态,只要你愿意,你可以随时使用它。

// By React Hooks
import React, { FunctionComponent } from 'react';
import { profileModel } from './ProfileModel.ts';

const App: FunctionComponent = (props) => {
  const name = profileModel.useData((item) => item.name);
  // 这是个布尔值
  const loading = profileModel.manage.useLoading();

  return (
    <button onClick={() => profileModel.manage.action(1)}>
      Click me: {name} {loading ? 'Waiting...' : ''}
    </button>
  );
};

export default App;

如果你是用不用hooks,我们可以用connect()方法注入到props中:

const mapStateToProps = () => {
  loading: profileModel.manage.connectLoading(),
};

export default(mapStateToProps)(App);

有时候,请求粒度会细到某条数据上,也就是说,你想在一个屏幕上同时使用多个loading状态,这时候我们就需要精确知道loading的作用范围。这个其实很简单就实现了,我们利用meta属性:

class Profile extends Model {
  someAction = this.actionRequest({
    action: (id: number, data: any) => {
      return this.post({
        uri: '/profile/api',
        body: data,
        payload: {
          idKey: id,
        },
      });
    },
    meta: 'idKey',
  });
}

必须确保meta的value在payload中能找到相应的key。否则将会产生bug。

接着我们看看如何在React中使用它

// By React Hooks
import React, { FunctionComponent } from 'react';
import { profileModel } from './ProfileModel.ts';

const App: FunctionComponent = (props) => {
  const name = profileModel.useData((item) => item.name);
  const userId = 1;
  const secondUserId = 2;
  const loading = profileModel.manage.useLoading(userId);
  const secondLoading = profileModel.manage.useLoading(secondUserId);

  return (
    <button onClick={() => profileModel.manage.action(userId)}>
      Click me: {name}
      {loading ? 'Waiting...' : ''}
      {secondLoading ? 'Second waiting...' : ''}
    </button>
  );
};

export default App;

泛型

在异步请求action中,你可以加入Response和Payload泛型。你只需要注入一次,就可以在项目的任何地方享受到关于这个action的静态检查

import { Model } from '@redux-model/*';

type Data = Array<{
  id: number;
  name: string;
}>;

interface Response {
  id: number;
  name: string;
}

interface Payload {
  id: number;
}

class Profile extends Model<Data> {
  getProfile = this.actionRequest({
    action: (id: number) => {
      // 这里注入
      return this.get<Response, Payload>({
        uri: `/profile/api/${id}`,
        payload: {
          id: id,
        },
      });
    },
    onSuccess: (state, action) => {
      state[action.payload.id] = action.response;
    },
  });
}

欢迎您自由使用并随时创建issue和PR。