mtor 是什么?
mtor 是一个基于react 类响应式数据流状态管理库, 本身是基于原生react hooks 进行了二次封装,对比原生react hooks:
问题 | react hooks | mtor |
属性保存与修改 | 一个useState 只能管理一个属性 | 是用setData方法可以同时对多个属性进行修改 |
可读性 | 随着组件规模增大,展示与业务逻辑混在一起,可读性直线降低, 也更容易出错 | ui展示与业务了逻辑分离,结构更清晰,可读性更好 |
复用性 | 组件里面的业务逻辑不可复用 | 整个模块都是可复用 |
数据共享 | 必须使用useContext,或属性传值,增加大量非业务代码 | 使用依赖注入, 多模块轻松实现共享/内部通信 |
开发体验 | 热更新,所有state丢失,useMemo,useCallback,等依赖项很容易出错 | 热更新数据保留,不用关心依赖项更新问题 |
从一个简单demo开始
实现一个简单小需求, 从后端接口获取一个随机数,展示在页面中, 页面有一个按钮,点击给获取的随机数+1
1. 定义模块类
import {service, Model} from 'mtor';
function ajax() { // 模拟ajax请求
return new Promise((resolve) => {
setTimeout(() => {
resolve(parseInt(Math.random() * 10, 10));
}, 16.7);
});
}
@service('home')
class HomeModel extends Model {
num = 0;
async init() {
this.num = await ajax();
}
add() { // 普通方法
this.num ++;
}
}
export default HomeModel;
-说明
- @service('home') 定义一个模块, 每个模块必须添加此注解, 其中home 是自己给模块取的名称, 如果不想取名,也可直接用module.id, 比如@service(module.id);
- mtor 大量依赖最新注解语法, 需要配置相应babel插件(@babel/plugin-proposal-decorators);
- Model 是个类接口, 主要是给model实例和类提供接口api和属性;
- init() 是一个异步方法, 调用接口返回给num属性;
- add() 是定义的普通类方法, 此方法给类属性num 加1;
- num 是一个类属性, 页面中可以直接使用;
2. 在页面中使用 model (useModel)
页面引入model目前仅支持方法组件(使用hooks语法),使用方法如下:
import React, {useEffect} from 'react';
import {useModel} from 'mtor';
import style from './style.less';
import HomeModel from '../../models/HomeModel';
export default () => {
const model = useModel(HomeModel);
const {
num,
} = model;
useEffect(() => {
model.init();
}, []);
return (
<div className={style.container}>
<div className={style.content}>
<div className={style.addOne} onClick={model.add}>
+1
</div>
<div className={style.txt}>
{num}
</div>
</div>
</div>
);
};
- 说明
- model 是HomeModel的一个实力, 通过useModel 方法获取, 传一个类型类;
- 页面加载后调用model中定义的init方法,获取随机数;
- (小技巧) model中所有方法已经绑定过this了, 可以单独拿出来直接使用;
3. 在页面中使用 model (useModel 二次封装版 useInitModel)
在上面案例中使用useModel写法 每次页面加载都会重新执行init方法, 如果开发情况下, 修改了页面, 会导致页面热跟新,其实并不需要再次执行init方法, 可以用useInitModel 取代 useModel, 方法如下:
export default () => {
const model = useInitModel(HomeModel, () => model.init(), true);
const {
num,
} = model;
return (
<div>
{/* // 业务代码 */}
</div>
);
};
- 说明
- useInitModel 底层基于useModel, useEffect 的二次封装方法;
- 第二个参数是第一次加载初始化方法,第三个参数是退出是否调用reset方法;
- useInitModel开发模式做了优化, 页面热更新的时候不会再次调用reset 与初始化方法;
三、深入理解 mtor
1. 依赖注入(DI)
以上案例基本上可以满足绝大部分业务需求, 但是有时候我们定义了多个model, model之间需要有数据共享, 在mtor 引入了依赖注入(Dependency Inject), 模块之间可以相互依赖, 框架会根据配置类型自动注入进来。举个例子,还是在上面的案例中, HomeModel 依赖另外 一个UserModel, UserModel 中定义了name 属性, HomeModel 初始化后拿到UserModel中的name,并展示在页面中;
1. 定义需要依赖的model
import {Model, service} from 'mtor';
@service('usermodel')
class UserModel extends Model {
name = 'hello user';
}
export default UserModel;
- 此处定义了UserModel, 里面有name 属性
2. 定义准备依赖的model
import UserModel from './UserModel';
@service(module.id) // 也可以直接使用模块标识
class HomeModel extends Model {
num = 0;
username;
/**
* 声明user类型
* @type {UserModel}
*/
@inject(UserModel)
user;
async init() {
this.num = await ajax();
this.username = this.user.name; // 可以在model方法中直接使用UserModel 中name 属性
// this.user.name = 'sampsonli' // ***不可以***直接修改被注入属性中的值, 应该调用被注入属性中的方法修改其值
// this.user.setData({name: 'sampsonli'} // 可以调用UserModel 中setData 为name 赋值
}
add() {
this.num ++;
}
}
export default HomeModel;
- 说明
- @inject(UserModel),给属性注入UserModel 的实例;
- 注入的实例,类方法中可以获取实例属性, 也可以调用注入实例的方法, 但是不能直接修改实例的属性, 只能通过setData方法或者UserModel 中单独定义方法去设置;
- 如果使用es语法,被注入的属性前面建议加上jsDoc注释,表明属性类型,方便后续使用实例属性和方法;
3. 最后在页面中展示数据
import React, {useEffect} from 'react';
import {useModel} from 'mtor';
import style from './style.less';
import HomeModel from '../../models/HomeModel';
export default () => {
const model = useModel(HomeModel);
const {
num,username, user,
} = model;
useEffect(() => {
model.init();
}, []);
return (
<div className={style.container}>
<div className={style.content}>
<div className={style.addOne} onClick={model.add}>
+1
</div>
<div className={style.txt}>
{num}
</div>
<div className={style.txt}>
{username}-{user.name}
</div>
</div>
</div>
);
};
- 说明
- 也可以直接使用被注入属性中的属性值, 当user数据有更新时,会同步到页面中。
2. 定义model注意事项
- 不管是普通方法,还是异步方法, 尽量不要定义为箭头方法, 箭头方法在类中是以普通属性存在, 不能使用类中定义的属性;
- 保留字 setData, reset, ns,不能用于自定义方法、属性名; 自定义onCreated, onBeforeClean 会根据满足某些条件自动调用;
- 当修改模块中对象类型的属性时, 需要同时更新对象引用值。例如:@service(module.id) class DemoModel extends Model { obj = {a: 1}; updateObj() { this.obj.a = 2; // 错误的做法 } updateObj2() { this.obj = {...this.obj, a: 2}; // 正确的做法 } } export default DemoModel;
- 使用属性前,必须先在类中声明, 否则动态添加的属性不生效@service(module.id) class DemoModel extends Model { setA() { this.a = 111; // 此处修改不会反馈到页面中 } } export default DemoModel;
3. 初始化方法
有时候会遇到这种场景, 模块加载的时候进行一些初始化操作(注意不是初始化值), 初始化操作可以定义onCreated方法来实现
@service(module.id)
class CreatedModel extends Model {
num = 0;
constructor() { // 构造方法只能初始化变量
this.num = 1;
// this.ajaxGet()// 不能直接调用模块中的方法
}
ajaxGet() {
// 方法逻辑
}
onCreated() { // 如果定义了onCreated方法,此方法在模块加载的时候会自动执行(注意,热跟新的时候也不会执行哦。。。)
thia.ajaxGet() // 此方法中可以调用模块中的方法进行初始化
}
}
export default CreatedModel;
- 最佳实践, 尽量减少onCreated方法使用, 在模块类中定义init方法,然后放入组件的 React.useEffect方法中调用。
4. 模块生命周期方法
目前只提供了onCreated, onBeforeClean 钩子方法, onCreated 前面已经介绍过了, onBeforeClean 在调用 reset方法前自动调用, 可以用来进行一些数据清理工作, 比如取消事件注册,定时器任务等等。
4. setData 妙用
1. 便捷地操作model中的数据
有时候页面中需要修改model中的数据, 如果只是修改少量数据,新定义一个方法会大大增加业务代码量, 可以使用 model.setData(params)方法 params是一个普通对象, key是要修改的属性名, value是修改后的值。
export default () => {
const model = useModel(HomeModel);
const {
num,username
} = model;
useEffect(() => {
model.init();
}, []);
return (
<div className={style.container}>
<div className={style.content}>
<div className={style.addOne} onClick={() => model.setData({num: num + 1})}>
+1
</div>
<div className={style.txt}>
{num}
</div>
<div className={style.txt}>
{username}
</div>
</div>
</div>
);
};
- 用 model.setData({num: num + 1}) 取代 model.add 方法, 可以减少代码量, 但是缺点是每次页面渲染都会生成一个新方法, 可能对性能优化不是很友好, 具体取舍看业务场景吧! setData 所设置的属性名尽量是模块类中存在的属性, 比如上例 setData({num2: 33}) 设置一个新属性num2, 虽然运行没问题, 但是不提倡这样写。 ####2. 给model动态添加新属性
@service(module.id)
class SetDataModel extends Model {
num = 0;
addNum2() {
this.num2 = 100; // 此方法不会在页面中获取num2
this.setData({num3: 200}); // 可以在页面中获取num3
}
}
export default SetDataModel;
-说明
- 正常情况下, 属性应该先声明再使用,尽量减少添加没有在类中声明的属性
- 也可以在页面中调用setData 动态添加属性
5. 重置model中的所有数据到初始值
组件销毁的时候, 我们要清空现有的数据, 我们可以调用 model.reset;
export default () => {
const model = useModel(HomeModel);
const {
num,username
} = model;
useEffect(() => {
model.init();
return model.reset; // 当前组件销毁的时候会调用 model.reset() 方法
}, []);
return (
<div className={style.container}>
<div className={style.content}>
<div className={style.addOne} onClick={() => model.setData({num: num + 1})}>
+1
</div>
<div className={style.txt}>
{num}
</div>
<div className={style.txt}>
{username}
</div>
</div>
</div>
);
};
-说明
- 可以用 useInitModel 简化以上逻辑。
总结:
以上包含mtor的基本用法。 欢迎大家在评论区讨论, 以及在github 上star,
详情可以查看https://github.com/sampsonli/mtor。您的意见是我持续不断优化的动力。
本文暂时没有评论,来添加一个吧(●'◡'●)