用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 端在读取数据填充页面时,还需要把数据存储到页面,供前端加载时从数据恢复到 storeredxucreateStore 天然支持从 initialState 恢复数据的能力)

面对以上的种种问题,大部分人都会持有 mobx 不适合大型项目的观点。

解决方案

在笔者用 mobx + react 做了诸多中大型的前端项目之后,对这些劣势深恶痛绝,也逐渐摸索出了一些方案来解决上述的问题。

1、分层

为了解决数据定义,数据共享以及逻辑代码如何防止等问题,首先对项目结构进行分层。

  • 项目按照页面进行分割
  • 页面按照 storesactionsviews 分为三层
  • stores 定义页面内各个数据模型及数据的操作方法,各个store之间互相独立
  • views 层作为视图层,接收 stores 注入的数据负责渲染
  • actions 层处理交互逻辑,引用各个 store 方法调用更新数据,又 mobx 自动触发视图刷新

以上是一个典型的 mvc 分层结构,这种方式很大程度上解决了问题点0、1、2。

2、唯一数据源

通过第一步的改造,项目的可维护性可谓上升一个台阶。

但是页面的 storeaction 需要手动实例化并手动注入到每个页面组件,着实是一个负担。并且 store 实例化自由,管理起来较为混乱。并未解决3、4、5的问题。

所以需要开发一个状态管理库,主要实现如下功能

  • storeaction 的自动查找加载。 storeaction 分页面放置,通过某种机制进行查找
  • 查找到的所有 storeaction 自动实例化,并形成全局唯一数据源
  • store 提供配置单例或多实例的配置项,减少因需求变更导致的代码改造工作量
  • 按需实例化 store 。比如访问页面 A ,只需实例化 A 页面依赖的 store

查找机制

storeaction 的查找方式简单介绍两种,一种是通过 webpack 提供的 require.context 动态的引入特定目录下的 storeaction 模块,第二种是通过装饰器模式进行加载。 伪代码如下

//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 的信息之后,就可以在管理类里对 storesactions 进行处理,组装全局唯一的 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 需要手动指定 storerootStore 中所处的节点,能不能通过 store 文件所在的目录名、文件名、 store 类名等信息直接映射到对应的结构呢?

答案是可以的,只需要编写一个 babel 转换插件,在编译时对文件的抽象语法树进行分析替换,自动填充 @storepath 属性就好了。(笔者项目用的是 ts ,提供了一个 ts transformer 完成同样的功能)

(2)脚手架

  • 由于页面结构保持了高度统一,无论是 store 文件、 action 文件,或是 jsxcss 文件,都有或多或少的样板代码。为了开发流程的自动化,可以开发脚手架工具,自动生成页面骨架。一是为了提升开发效率,二可以规范开发流程。
  • 如果项目中用到 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
  • 在实例化 storeaction 时对实例的方法分别进行包装
  • action 的方法调用前设置 flagtrue ,执行 action 的方法,然后设置 flagfalse
  • 这样 store 的方法如果在 action 内调用时访问到的 flagtrue ,在其他地方访问到的 flagfalse
  • 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.jsangular 框架的核心组件, 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版)

Processing编程学习指南(原书第2版)

[美]丹尼尔希夫曼(Daniel Shiffman) / 李存 / 机械工业出版社 / 2017-3-1 / 99.00元

在视觉化界面中学习电脑编程的基本原理! 本书介绍了编程的基本原理,涵盖了创建最前沿的图形应用程序(例如互动艺术、实时视频处理和数据可视化)所需要的基础知识。作为一本实验风格的手册,本书精心挑选了部分高级技术进行详尽解释,可以让图形和网页设计师、艺术家及平面设计师快速熟悉Processing编程环境。 从算法设计到数据可视化,从计算机视觉到3D图形,在有趣的互动视觉媒体和创意编程的背景之......一起来看看 《Processing编程学习指南(原书第2版)》 这本书的介绍吧!

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

html转js在线工具
html转js在线工具

html转js在线工具