用mobx构建大型项目的最佳实践

栏目: JavaScript · 发布时间: 6年前

内容简介:有一种观点认为本文不会讨论

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构建大型项目的最佳实践》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Introduction to Algorithms, 3rd Edition

Introduction to Algorithms, 3rd Edition

Thomas H. Cormen、Charles E. Leiserson、Ronald L. Rivest、Clifford Stein / The MIT Press / 2009-7-31 / USD 94.00

Some books on algorithms are rigorous but incomplete; others cover masses of material but lack rigor. Introduction to Algorithms uniquely combines rigor and comprehensiveness. The book covers a broad ......一起来看看 《Introduction to Algorithms, 3rd Edition》 这本书的介绍吧!

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具