为什么我选择MST承载业务逻辑

在开始阅读之前,建议没看过《后面再考虑用户界面》的读者先去看看那篇文章,了解业务逻辑优先的前端设计思想。本文更偏重实用和我的一些个人喜好,介绍了为什么选择Mobx State Tree(以下简称MST)实现这种解耦UI的前端应用开发方式,当然,其实也没完全解耦......

TL;DR:MST为完全承载业务逻辑设计,并且可以拉齐团队产出质量,让前端应用分离UI和业务逻辑,对于提高可维护性有本质帮助。

为什么

在几年前,数据管理还是前端领域的热门话题之一,涌现出了无数轮子,React官方也提供了Context和useReducer hook。但是,似乎没什么人讨论过数据管理应该怎么用,各种轮子提供的样例也集中在彰显数据共享的能力,似乎数据共享就是数据管理的目的。

数据管理其实有两种用法------"能用多少用多少",关键分别在"多"和"少"上。虽然是一句话,开发思路却完全不同。往多了用的方式会尽可能用数据管理承载业务逻辑,UI只是对业务逻辑的展示和调用,即之前文章中提到的UI afterthought的开发思路。往少了用的方式则相反,会尽可能少的使用数据管理去共享数据,业务逻辑还是在UI组件中,常见的是共享用户信息、前端主题、环境信息等全局信息。

大多数的数据管理方案,尤其是轻量级的方案(Recoil,Atom,Jotai之类的),更适合往少了用的场景。MST则相反,同为Mobx作者的项目,它被创造出来就是为了全面承载业务逻辑,也是Mobx作者本人认为比较好的工程化实践方案。通过领域划分等方式,将业务逻辑尽可能的放到MST中,让业务逻辑和UI分离。

Mobx VS Mobx State Tree

简单说,MST就是明确使用方法的Mobx工程化解决方案。作者也是这么认为的,"opinioned"一词明确表明了作者的态度。

Mobx是一个灵活的面向对象的框架,通常使用Class的语法,提供了声明包含Observable和action对象的方法,以及一些配套的API。mobx-react和mobx-react-lite这两个包提供了从React组件中收集Observable依赖的能力,并且让observable的改动可以驱动React组件的更新,两者配合实现了Mobx在React应用中作为数据管理的能力。

然而,关于如何设计数据管理的结构,Mobx没有任何的意见或是建议。不论是声明一整个应用的数据为一个对象,或是对每一个React组件的数据声明一个对象,Mobx都支持。这导致了使用Mobx的时候除了一些开发约定,几乎没有任何标准。在工程领域,没有标准是一个很可怕的事情,很可能会导致飞速的代码腐化。此外,团队人员的能力也很难达到统一,大家很难设计出达到统一标准的合理的数据对象。

MST便是作者给出针对此问题的解决方案。通过固定的props,views,actions的写法,MST弱化了面向对象的概念,我个人认为在前端领域这其实是件好事。MST明确的指出了数据管理的设计方式------将整个应用的状态设计成一颗结构固定但值可变的树,并通过类型区分了不同的Observable,基本明确了什么情况下应该声明新数据对象的问题。此外,MST还有一些更高级的概念,比如snapshot和middleware,可以帮助开发者应对更复杂的场景。

MST的优势

使用数据管理完全承载业务逻辑可以解耦UI和业务逻辑,这对于项目的可维护性提高有本质的帮助。MST由于设计之初的思路就是完全承载业务逻辑,所以它对比其他的数据管理方案来说,非常适合业务复杂度只增不减的产品(绝大多数的业务形态),这也是我这两年始终应用这个方案最主要的原因。此外,MST相比其他的项目管理主要有以下几点优势:

  1. 写法不复杂且固定。是的,在业务复杂度只增不减的业务系统中,这是一个巨大的优势。这保证了每个人写出来的东西都差不多,让团队的成员能有差不多的产出。不过,这也带来了一个问题,由于MST的写法不标准,大家都会有一些上手成本,另外还需要搞明白一些概念。但是总体来说,一旦完成了最初的上手,整个团队的产出就是基本可控的。

  2. 运行时类型检查。虽然在Typescript逐渐成为标准的今天,这个功能已经不是很重要了,但是在Javascript的年代,这还是很有用的,可以保证在你的业务逻辑中流转的数据类型都是有保证的,防止由输入数据的格式改动可能造成的type error。

  3. 结构化程度高。因为特殊的写法,对于单层的数据对象来说,用MST写出来的东西其实比较固定。数据放在props里,声明类型和默认值;计算值放在views里,用get声明,return一个值;方法放在action中,异步方法用flow,内部数据用闭包变量。需要推敲的就是什么时候需要再新建一颗子树,即新的types.model,这里和面向对象的思考方式其实高度一致。在明确了什么东西需要组成新的对象(也就是MST中的子树)后,MST就会在建立树的实例时,根据初始数据同时建立子树的实例。

  4. 不用考虑太多React的渲染优化。这主要是和Redux对比。Mobx采用收集依赖触发渲染,再加上MST承载所有业务逻辑的方式,让组件之间几乎不需要传递props,所以组件的渲染绝大多数情况下只有Observable的改变触发,这样就不容易出现性能问题。当然,这还需要在React的层面尽可能的根据数据做好组件化,提高组件的颗粒度。

怎么用

最后简单的说说怎么用,这里注重的是工程化设计,而非API介绍,对MST基本概念不理解的读者,可以去看我之前写的简单上手指南。代码可以看npx create-react-app my-app --template cra-template-jw-mst生成的React应用。

types是MST的重要概念,有2类,一类是复杂数据类型modelmodel的集合,另一类是简单类型,如stringnumber等。使用MST最复杂的地方,就是各级model(即数据对象)的设计,代表对业务模型的抽象。

对于简单类型,MST是可以通过默认值推断。绝大多数不需要再有自己功能的对象,都是简单类型的值,在MST中被称为叶子节点,值本身不再是Observable的。大部分的简单类型对应着JS中的基本类型,但有一种是例外------types.frozen,这个值可以用来存不可观测的arrayobjecttypes.frozen存的arrayobject可以看作一个整体,如果array中的元素或object中的key发生变化,MST是不会观测到变化的,在arrayobject的引用地址改变后,MST会观测到,并整体改变这个值,有点类似于Redux需要返回immutable的修改方式。

由于types.frozen的存在,何时使用frozen,何时创建新的model就成为了一个问题。对于这个问题,我感觉没有固定答案,我个人的做法是按照两条路区分。一是如果这个数据是一起来一起改,比如一个列表数据,那么其实frozen就够了。二是看这个对象还需要不需要自己的方法。还是这个列表,如果这个列表是每行同时可编辑的,并且编辑完成后就会触发针对这一行的action,那么这个列表的每一行数据作为子树就比较合适了。不然就得从frozen的数据中找到要修改的元素,修改完成后再替换整个frozen的值,那么实际上是对MST的一种降级。

在建立MST树的实例时,最好采用在根节点create一次的方法,不要手动的建立各个子树。MST会根据初始值,自动按需要建立子树的实例。如果没有相应的初始值,可能需要types.optional赋予相应的数据默认值,保证类型的正确。

总结

本文更多的是聊了一下我个人在数据和UI解耦方面的一些实践和喜好,不一定对别人适用。比如,有很多人就觉得MST "opinioned"的写法不好,也有很多人觉得MST太重,还有一些人觉得MST和Typescript的配合问题很恶心,等等......但是,我个人认为,用一个知名大佬专为解决一个问题设计的工具来解决这个问题至少应该是比较上乘的方案。工具本身不重要,用什么都不会差太多,各有千秋。解耦UI和业务的思想,才是最有价值的,也是希望大家有机会可以在工作中尝试的方法。

相关推荐
万叶学编程3 小时前
Day02-JavaScript-Vue
前端·javascript·vue.js
前端李易安5 小时前
Web常见的攻击方式及防御方法
前端
PythonFun5 小时前
Python技巧:如何避免数据输入类型错误
前端·python
知否技术5 小时前
为什么nodejs成为后端开发者的新宠?
前端·后端·node.js
hakesashou5 小时前
python交互式命令时如何清除
java·前端·python
天涯学馆5 小时前
Next.js与NextAuth:身份验证实践
前端·javascript·next.js
HEX9CF5 小时前
【CTF Web】Pikachu xss之href输出 Writeup(GET请求+反射型XSS+javascript:伪协议绕过)
开发语言·前端·javascript·安全·网络安全·ecmascript·xss
ConardLi5 小时前
Chrome:新的滚动捕捉事件助你实现更丝滑的动画效果!
前端·javascript·浏览器
ConardLi6 小时前
安全赋值运算符,新的 JavaScript 提案让你告别 trycatch !
前端·javascript