最近使用MST比之前又深入一些,发现很多高级功能伴随着很多坑。如果不用这些高级功能,又总感觉没又把MST用到位,先记录下来,后面再琢磨。
序
我接触到MST后做的第一个应用是用它来替换掉一个基于Mobx的框架,当时没有用纯Mobx替换的原因是感觉MST的Opinioned更符合我的观点(有人教你写好过绝大部分人自由发挥),mutable和immutable的结合是吸引我的另一个点,还有类型系统,这些都是用之前那个框架时遇到的痛点。
原方案
在第一个项目替换中,先用MST实现了原有框架的功能,又做了一些整理,结合原有框架的实践,总结出了第一版用法。项目全部是JS文件,主要点有以下几个:
- 全局单一Store实例,通过Context和mobx-react的Provider直接注入到webapp的最外层,内部组件可以通过Context和inject来获取全部store
- Store使用单一出口文件,export一个对象,对象包含各个Model已经生成的tree,以及与Mobx相连的Router
- Model初始化采用默认值直接指定MST model的type,只有MST的Runtime类型检查,基本没遇到类型问题
- 各个Model内直接create实例,不额外传入初始值。在Model内直接unprotect(tree),然后export出实例
这种使用方式已经可以完全替换之前用的Mobx框架,并且通过Context增加了Hooks的支持,还通过默认值的方式设定了大部分store变量的类型,除了写法上一开始有点不习惯,其他都是优点。
后续又用这种方案做了几个规模小一些的项目,用起来也没有什么大问题,只不过要单给array写Model有一些烦(这是一个比较大的坑)。最近因为搭新架子,想把之前JS的MST给转成TS,本以为MST已经有类型了,应该是一个很简单的活,结果发现之前使用的方式有很大的缺陷,主要是两点:
- 虽然是单一全局Store,但并不是一整棵tree,而是很多棵独立的小tree,导致如果要跨模块访问store,只能从顶层出发,而且还有初始化相关的很多问题
- 没有清晰区分node和leaf,node是observable对象,而leaf只是observable的值,里面的信息不会被observe。之前的方案设置变量的时候没有经过仔细的思考,有一定问题
新方案
新方案改进了原方案Store的结构,更好的应用MST snapshot的特性,并且在Model上可以采用TDD的开发模式,从一定程度上解决了前端测试不好下手的问题(这块会单写一篇)。此外,新方案更好的区分了node和leaf,并通过clone等高级功能,解决了一些业务上可能遇到的问题,当然,伴随而来的还有新坑……主要的改进如下:
- 建立RootStore model将各模块连接成一棵完整的树,在model/index.ts中单次建立全局Store。好处是可以通过snapshot设定整个webapp的状态,配合onSnapshotChange,可以自动将程序的最新状态存入localStorage,然后建立Model instance时读取序列化的snapshot就可以恢复关闭页面前的状态。配合module.hot可以在热更新时不丢失页面状态。使用getRoot()还可以在store中获得所有其他的store。
- 使用types.frozen更好的区分node和leaf,这主要是针对对象和数组。界定的方法有一定心智负担,原则是如无必要,勿增实体。如果array和object内部的东西不需要进行单独的观察和修改,就是使用types.frozen让他们成为leaf。如果内部的东西需要被继续修改,则将数组元素和对象扩展成新的model,即node,再将修改的方法放在新的model中。React组件中将array中的子model实例传给对应组件,组件调用子model实例方法即可。
- 更好的分离node和leaf,这样可以将更多的东西塞到MST的tree里,把UI组件中的功能转移到MST中,测试也会从复杂的UI测试,变为简单的function测试,从而实现功能的TDD开发。
- 在function组件中,调用MST action的函数全部可以使用useCallback持久化而不用传递任何依赖,不过React官方说这个方式优化的性能可以忽略不计……
虽然新方案在增加了一些心智负担,但是实现了更完整的MST功能,并且增加了一种可行的TDD开发方式,总的来说还是不错的。不过该方案还有一些坑:
- clone出的model instance没有parent。这个问题我觉得是个巨坑,如果编辑model instance采用clone出instance操作,再applySnapshot回去办法,意味着在model中任何getParent的方法都会报错,这直接导致types.reference基本不可用。目前我没有发现什么好办法,可能通过参数把原node的parent传进去,或者想办法把RootStore.create()生成的instance作为env注入到RootStore.create()中(先有蛋还是先有鸡……),如果有大神知道解决办法,欢迎在评论区指出。
- 心智负担增加。因为我使用MST的一个原因就是opinioned,意味这一些经验欠缺的人或者专注业务的外包开发看一个MST的文档以后就可以照着写了,在加上runtime类型检查,能很大程度上减少功能复杂应用出错的几率(基本类型定死)。然而区分node和leaf显然增加了整个方案的复杂性,可能背离最初的目标。
- 依赖MST的helper function在model中进行跨model访问。这个坑主要是不够优雅,其实用起来还凑合,希望有更好的方案。
- React Router官方不再推荐任何将Router和Store结合的方法,google到的文档链接都404了……直接使用BrowserRouter就可以,不要在用mobx-react-router或者其他的包了。
TypeScript
除此之外,将TS应用到MST的时候也踩了不少坑,主要是以下的几点:
- 首先是MST的类型,MST分为tree model和tree instance,tree instance又可以生成可序列化的snapshot。这些就涉及到了3种不同的类型。在TS中,不能把snapshot推给接受instance的数组,需要先使用model.create(snapshot)生成新的instance才可以。移除instance数组中元素的代理函数也需要先使用cast()转换self model的类型到instance。这些操作中需要用到很多MST的类型转换函数,心智负担比较大。
- 因为TS是静态类型分析,MST是动态类型分析,所以在TS编译时,一些MST中的方法或derived value还不存在导致的问题,如action中调用本model其他action,views中调用其他views都会导致TS编译报错。比较好的解决方案是通过this调用,但这实际上只是因为this的类型是any……
- transverse tree的方法必须要指定类型才不报错,这基本需要export出所有model的类型,也是一块比较大而且意义不大的负担
总结
总结一下,现在的方案更成熟,更充分的利用了MST,但是同时增加了复杂度。下一步的工作方向是如何通过封装,降低复杂度,搞出功能强还简单易用的方案。
参考资料:
作者录的教程:Describe Your Application Domain Using mobx-state-tree(MST) Models