本篇幅主要记录下近期使用remax+mobx+typescript的过程中,如何在mobx的store中使用class decorator
近期由于新项目的需求,需要重新搭建一套小程序的开发框架,之前用过taro,使用过程中唯一不适的就是,taro并不能完整的支持react的写法,在代码的编写过程中,需要注意taro在react中的一些限制(可以参考https://taro-docs.jd.com/taro/docs/react),所以秉着我还能不能好好搬砖的想法,继续摸索一下其他的框架,其实市面上也就剩下kbone、或者新出的remax可供选择,remax之前就听说,但并未仔细深入的研究,直到我打开remax的官网,发现截图中这些美妙的描述
于是乎,果断的选择了remax
既然框架层面已经选择完成,那就开始上手撸一个ToDoList吧,得益于前端的飞速发展,任何高大上的框架,必然提供了完美的cli,remax也不例外,按着官网的步骤,npm、npm、npm...一切都已完成,最后npm run,完美启动
既然是项目需要,在react开发的基础上,少不了状态管理这一层,既然remax标榜着使用真正的react构建,那必然,市面react的成熟状态管理框架也能很好的支持,其实也就是redux、mobx之类的选择(笔者选择了后者),有人说redux不香吗?我觉得不香,主要原因是 :
- 我不想定义一个model,需要完成action,constant,reducer,项目过大的时候,action,constant就变得稍微难以维护
- 那又有人说了dva可以解决问题,从我的角度理解,dva确实解决了redux编写过程中action,constant的问题(但是我觉得写async/await逼格高点 ,),开个小玩笑,其实不愿意使用dva的原因在于,项目使用的是ts,dva中的type又只能定义成字符串类型,导致代码重构过程中,不按照规范实现的model,无法很好的使用ts提供的便利,快速的找到model定义的位置。
- 由于之前项目的原因,整体上都已经习惯了mobx层使用class decorator的方式进行定义
基于以上的考量,还是选择了mobx,按照remax的定义,似乎可以无缝的迁移mobx,查找了一遍官方提供的demo(https://github.com/remaxjs/examples/tree/master/wechat-mobx)可以很方便快捷的实现mobx的状态管理,至此,我可以很开心的说,一切都是这么的完美...
仔细查阅官方提供的demo,发现了store的写法似乎不符合我的预期。
说好的class ,说好的高大上的decorator呢,在探索欲望的驱使下,我决定改造 demo中 store的写法,换成我心爱的class decorator,官方的写法,store返回的是TodoList的数据对象,在通过入口文件中的useLocalStore, 将其转换成为proxy(有兴趣的同学可以看看Proxy的定义,vue3中的改进点)
那问题就来了,我需要的是一个class,如何将class 的定义转换成为官方demo中提供的方式就成了解决问题的关键(其实并不是,这真是我躺过的时间最长的坑),那实现的关键方式的方法:
- class转换成为名副其实的object
- 是不是可以在定义一个object,将value值设置为class的实例
第二种方案,从当前的选择中,应该是比较简单的实现方式,想法很完美,现实却很骨感,实现的结果,转换后的store,变成了纯粹的object,不在是Proxy,导致的问题就是,无论如何更新model层的数据,始终无法触发界面的rerender,在无法解决的情况下,最好的方式就是查看useLocalStore的实现,发现在useLocalStore的hooks里,非常简单的事情,就是将store序列化后,useState了一丢丢,按照此思路,我似乎也可以实现的所谓的useClassStore,将class完美转换( 确实在去年的mobx-react issues中,发现了此问题的答案https://github.com/mobxjs/mobx-react/issues/722),此方案应该没有任何瑕疵,按照class-transform的思路,确实实现了一般class 的 方式,到此整个方案应该是落地无任何的问题了,所以我很开心的去睡了一觉。
当我造此思路,决定继续完成我的ToDoList demo,在mobx的class decorator中,有个还算实用的computed方法,但是当我在model加入此方法时,小程序开发工具的console端,无情的给了我一个红色的错误,
看提示,凭着直觉,似乎computed decorator并没有按照预期的实现,而是在转换过程出了差错。decorator的转换,
- 在ts情况下,无非是按照ts的实现进行转换
- 第二种,就是babel
凭着良好的嗅觉,我觉得还是重新翻一遍remax的文档吧,终于在remax文档此页,发现了关于babel的描述,https://remaxjs.org/guide/config/babel,所以,修改tsconfig中关于decorator的配置,并不能完全的转换mobx中的decorator(文档中并没有提及mobx,虽然这一页被我看了几遍),还需要加入关于babel的配置,按照官方提供的文档信息,加入了babel的配置,重启,错误奇迹般的消息了,但是对于做class 转换这件事,我一直觉得是个繁琐的过程,既然是真正的react,就不存在mobx的class 需要手动转换的过程,所以google了不同的关键字,终于在medium的这篇文章中,发现了作者关于multi global store 和 function component的描述,https://medium.com/@suraj.kc/mobx-strategies-with-react-hooks-3de23932cb8c,所以如果使用的是class方式定义的store,在入口文件中,完全不需要provider的支持,纯粹的就是使用useContext API就可以完成全局的状态管理,可以在CodeSandBox(https://codesandbox.io/s/react-mobx-global-state-management-lxcp6?file=/src/index.tsx)看下,按照文章意思,remax中实现class 的 store,似乎也可以不按照官方提供的demo样本,使用useLocalStore,这样不仅可以减少了手动实现class transform的过程,还还原了mobx原来的味道,堪称完美,最后附上mobx class完整实现方案
一、项目加入babel的配置
module.exports = {
presets: [
[
"remax",
{
// 是否使用 @babel/preset-typescript 转换TS代码
// 例子:下面的 `decorators` 和 `classProperties` 可以使Mobx的装饰器能正常工作
// @babel/plugin-proposal-decorators 的选项,详见 https://babeljs.io/docs/en/babel-plugin-proposal-decorators
decorators: {
legacy: true,
},
// @babel/plugin-proposal-class-properties 的选项,详见 https://babeljs.io/docs/en/babel-plugin-proposal-class-properties
classProperties: {
loose: true,
},
},
],
],
};
二、store class 写法(还是那个配方和味道)
import { observable, action, computed } from "mobx";
export interface ITodo {
id: number;
text: string;
completed: boolean;
}
export default class TodoStore {
private id = 5;
@observable
todos: ITodo[] = [
{ id: 1, text: "Learning JavaScript", completed: true },
{ id: 2, text: "Learning ES6", completed: true },
{ id: 3, text: "Learning React Native", completed: true },
{ id: 4, text: "Learning Remax", completed: false },
];
@computed
get total() {
return this.todos.filter((item) => item.completed).length;
}
@action
addTodo(text: string) {
this.todos.push({
id: this.id++,
text,
completed: false,
});
}
@action
async toggleTodo(id: number) {
const todo = this.todos.find((todo) => todo.id === id);
if (todo) {
todo.completed = !todo.completed;
}
}
}
三、组装store createStore.ts
import ToDoStore from "./TodoStore";
const createStore = () => ({
TodoStore: new TodoStore(),
});
export default createStore;
四、改写入口文件app.tsx
import React, { useEffect } from "react";
import axios from "@/utils/axios";
const App: React.FC = ({ children }) => {
return children as React.ReactElement;
};
export default App;
page 写法和web上无差异,具体可以参见前面的codeSandBox的demo。