用mobx构建大型项目的最佳实践
栏目: JavaScript · 发布时间: 5年前
内容简介:有一种观点认为本文不会讨论
mobx
是一款基于观察者模式的响应式数据管理框架,相对于 redux
来说是后起之秀。
有一种观点认为 mobx
不适合构建大型项目,这源于 mobx
过于灵活的特点。灵活即意味着随意,这在开发日益复杂的大型项目是致命的弱点。 redux
则不然,它的唯一数据源、 reducer
纯函数、只能通过 dispatch
修改状态等几个特性保证了代码书写格式的高度统一。
本文不会讨论 mobx
的使用细节,只会在充分利用 mobx
优势的基础上,对开发格式进行统一,保证开发大型项目的可维护性。
mobx
的优势极其优秀,面向对象编程、响应式编程、 mutable
的数据处理方式、精准更新组件的能力,这里不过多讨论。
mobx劣势
-
0、数据可随处定义。可以定义在组件内,来替代
state
的作用;也可以定义在单独的store
内 -
1、用户交互逻辑可以写在组件声明的方法内,也可以写在
store
声明的方法内。 -
2、用户交互往往涉及多个
store
的数据处理,store
间可能形成交叉引用的网状结构。 -
3、
store
往往按页面和模块划分,散落在各处,不好统一管理。 -
4、
store
实例化的时机和方式不可控。 -
5、当单例
store
因为业务变更需要支持多实例时,改造难度极大 -
6、对服务端渲染不友好。
node
端在读取数据填充页面时,还需要把数据存储到页面,供前端加载时从数据恢复到store
(redxu
的createStore
天然支持从initialState
恢复数据的能力)
面对以上的种种问题,大部分人都会持有 mobx
不适合大型项目的观点。
解决方案
在笔者用 mobx
+ react
做了诸多中大型的前端项目之后,对这些劣势深恶痛绝,也逐渐摸索出了一些方案来解决上述的问题。
1、分层
为了解决数据定义,数据共享以及逻辑代码如何防止等问题,首先对项目结构进行分层。
- 项目按照页面进行分割
-
页面按照
stores
、actions
、views
分为三层 -
stores
定义页面内各个数据模型及数据的操作方法,各个store之间互相独立 -
views
层作为视图层,接收stores
注入的数据负责渲染 -
actions
层处理交互逻辑,引用各个store
方法调用更新数据,又mobx
自动触发视图刷新
以上是一个典型的 mvc
分层结构,这种方式很大程度上解决了问题点0、1、2。
2、唯一数据源
通过第一步的改造,项目的可维护性可谓上升一个台阶。
但是页面的 store
和 action
需要手动实例化并手动注入到每个页面组件,着实是一个负担。并且 store
实例化自由,管理起来较为混乱。并未解决3、4、5的问题。
所以需要开发一个状态管理库,主要实现如下功能
-
store
和action
的自动查找加载。store
和action
分页面放置,通过某种机制进行查找 -
查找到的所有
store
和action
自动实例化,并形成全局唯一数据源 -
store
提供配置单例或多实例的配置项,减少因需求变更导致的代码改造工作量 -
按需实例化
store
。比如访问页面A
,只需实例化A
页面依赖的store
查找机制
store
和 action
的查找方式简单介绍两种,一种是通过 webpack
提供的 require.context
动态的引入特定目录下的 store
和 action
模块,第二种是通过装饰器模式进行加载。
伪代码如下
//webpack require.context('./',true,/^(.+\/)*stores\/(.+)\.(t|j)sx?$/i) //装饰器 @store({ path:'pageA.storeA', //在全局store中的访问路径 type:'singleton'|'multi' // 声明单例还是多实例 }) class StoreA{ } // store装饰器的实现 let store = (config) => target => { target['__storeType'] = config.type //保存 App['__stores'] = App['__stores'] || [] //App为状态管理类 App['__stores'].push({ target, path: config.path}) return target; } 复制代码
拿到所有 store
的信息之后,就可以在管理类里对 stores
和 actions
进行处理,组装全局唯一的 rootStore
了, action
处理也是一样。
按需实例化
如果为了追求性能,可以考虑实现这么一个特性。实现方式可以用访问器属性,在访问到 store
属性时,再进行动态的实例化。伪代码如下
Object.defineProperty(rootAction, 'storeA', { configurable: true, enumerable: true, get() { StoreA['__instance'] = StoreA['__instance'] || new StoreA() return StoreA['__instance'] }, set() { throw Error("can not set store") } }) 复制代码
通过这么一个状态管理库,我们解决了3、4、5,对于问题6 服务端渲染,也可以通过简单的处理对 rootStore
进行恢复。
3、开发体验优化
(1)path自动声明
上面的装饰器 @store
需要手动指定 store
在 rootStore
中所处的节点,能不能通过 store
文件所在的目录名、文件名、 store
类名等信息直接映射到对应的结构呢?
答案是可以的,只需要编写一个 babel
转换插件,在编译时对文件的抽象语法树进行分析替换,自动填充 @store
的 path
属性就好了。(笔者项目用的是 ts
,提供了一个 ts transformer
完成同样的功能)
(2)脚手架
-
由于页面结构保持了高度统一,无论是
store
文件、action
文件,或是jsx
、css
文件,都有或多或少的样板代码。为了开发流程的自动化,可以开发脚手架工具,自动生成页面骨架。一是为了提升开发效率,二可以规范开发流程。 -
如果项目中用到
ts
的话,这种全局自动加载形成的store
会丢失类型信息。所以需要自动的生成一份类型声明文件(.d.ts
)帮助有更好的开发体验。
4、开发规范限制
最后一个话题,如何更严格的规范代码的书写方式。
即使我们限定了业务逻辑只能在 action
内处理,但终归是口头约定。老成员总有图便利把逻辑写到 view
层的时候,新成员刚加入时的代码更可能如此。
所以我们需要提供一种机制来保证只能在 action
内调用 store
的方法进行逻辑处理,而在 action
外的 store
调用都无效,并在开发环境给以警告。
这个问题如果你认为很简单,可能是因为你还没理解到这个的关键点在哪。下面通过例子来讨论解决方案。
//声明一个store class StoreA{ age = null; setAge(age){ this.age = age; } } //声明一个action class ActionA{ //调用store方法 setAge(age){ this.storeA.setAge(age); //有效 } } //组件内 storeA.setAge(age) //无效 复制代码
对于上述场景,处理方法比较简单。只需要
-
声明一个变量
flag
-
在实例化
store
和action
时对实例的方法分别进行包装 -
action
的方法调用前设置flag
为true
,执行action
的方法,然后设置flag
为false
。 -
这样
store
的方法如果在action
内调用时访问到的flag
为true
,在其他地方访问到的flag
为false
。 -
对
store
方法的包装比较简单,判断flag
,为true
执行数据操作,为false
进行友好提示
经过上述几步,就完成了同步场景的限制处理。
但实际的项目中大量的存在异步操作,如果 action
如下所示,会如何呢?
class ActionA{ //调用store方法 async setAge(age){ await saveAge(url); //接口调用 this.storeA.setAge(age); //有效 } } 复制代码
这时 storeA.setAge
虽然处于 action
内,但访问到的 flag
却是 false
,方案失效了。
对同步操作的处理如此简单,异步操作却是一个巨大的难题。现在的课题可以抽象为如下描述
如何实现在同一个方法内的调用(包括同步操作, setTimeout、promise、rAF、各种事件等异步操作的回调内...)都能访问到同一个上下文(true),而在这个方法外访问到的是另一个(false) 复制代码
内心隐隐约约有一个答案,如果在 action
调用时保存这个上下文,并在各种异步的回调里再取出这个上下文即可实现功能。但这是一个可怕的事情,意味着需要我们去代理所有的异步调用,换句话说我们需要覆盖原生的方法来做这么一件事情!
这似乎是很难去实现的,直到我发现了 zone.js
。
zone.js
简单介绍一下, zone.js
是 angular
框架的核心组件, angular
利用 zone.js
监听所有(可能导致数据变化)的异步事件。
这跨度有点大,怎么又扯到了 angular
。
没关系,重新介绍一下。 zone.js
描述了 JavaScript
执行过程的上下文,可以在异步任务之间进行持久性传递。
重点就是这句话,我翻译一下, zonejs
能保持同一个方法内的调用(无论同步还是异步的)都能访问到同一个上下文对象。这不正好解决了我们的问题吗?
现在利用 zonejs
来解决我们之前的问题。代码如下
//这里并没有阐述zone.js如何使用,如果看过zonejs文档应该很容易理解下面的代码所做的事情 const zone = Zone.root.fork({ name: '__mobx__zone' }); //包装action的setAge方法,使得action内的方法调用访问到Zone.current都为zone let oldFn = ActionA.setAge ActionA.setAge = (...args) => { return zone.run(oldFn, context, args) } //包装store的方法,判断Zone.current是否为zone,如果在action之外调用则为Zone.root let oldFn = StoreA.setAge StoreA.setAge = (...args) => { if(Zone.current === zone){ return oldFn.apply(context,args) }else{ //在action外调用store方法触发警告 console.error('invalid call') } } //以上的包装方法均在内部处理,不暴露在业务代码中 复制代码
利用 zone.js
可以很容易的实现我们想要的功能,通过粗略的源码浏览发现 zone.js
正是暴力的代理了原生的 api
。
通过上述几步处理,我们就可以愉快的拿 mobx
进行大型项目的构建和持续迭代了。
结尾
本文并未涉及过多的代码细节,对于 mobx
如何使用也并未阐述。本文着重去解决在使用 mobx
过程中可能引发的问题,并且在规范成员的代码风格方面做了尝试,使得在用 mobx
进行项目的开发时能最大限度的保证代码格式的统一,降低项目的维护成本。
关于如何开发和维护一个大型项目是一个很大的话题,应该在约定或者强制某些规范的基础上,再根据所处的业务场景进行特定的设计才可能做好。
以上所述就是小编给大家介绍的《用mobx构建大型项目的最佳实践》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Processing编程学习指南(原书第2版)
[美]丹尼尔希夫曼(Daniel Shiffman) / 李存 / 机械工业出版社 / 2017-3-1 / 99.00元
在视觉化界面中学习电脑编程的基本原理! 本书介绍了编程的基本原理,涵盖了创建最前沿的图形应用程序(例如互动艺术、实时视频处理和数据可视化)所需要的基础知识。作为一本实验风格的手册,本书精心挑选了部分高级技术进行详尽解释,可以让图形和网页设计师、艺术家及平面设计师快速熟悉Processing编程环境。 从算法设计到数据可视化,从计算机视觉到3D图形,在有趣的互动视觉媒体和创意编程的背景之......一起来看看 《Processing编程学习指南(原书第2版)》 这本书的介绍吧!
Base64 编码/解码
Base64 编码/解码
html转js在线工具
html转js在线工具