Mobx State Tree 简单上手指南

Mobx State Tree 简单上手指南

Mobx State Tree (简称MST)作为Mobx作者的亲儿子和作者指定的"更专业的数据存储方式",在国内却基本没什么人了解。无论是对于造轮子搞KPI,还是降本增效的务实派,MST都是一个很好的基础,比Redux强了不少。鉴于MST的中文资料实在是少的可怜,为了让更多人了解、使用MST,我写一篇简单的上手指南,降低MST的使用门槛。本文旨在让读者了解MST、尝试使用MST解决问题,注重实用,不会涉及原理、源码类的问题。

在当前大环境下,英语显然比之前更值得学习了一些,所以推荐大家去看官方的教程:mobx-state-tree.js.org/intro/welco... 挺简单的。

What

MST是什么?作者的答案是一个建立在Mobx上的状态管理系统。作者将Mobx形容为状态管理的引擎,而MST则是基于这个引擎造的车。和Mobx相同,配合不同的包,MST可以支持所有的现代Web框架,包括但不限于React、Vue、Angular,本文以React为主。作者说:"MST适用于大项目,或者希望快速扩展的项目"。所以在使用MST时,最好尽可能的将业务逻辑放到其中,然后通过在React组件中调用MST中的数据和方法来完成前端功能的开发。这种应用方式,让MST有了第一个特点,虽然很多人诟病,但我认为却是一大优点 ------ Opinioned写法。

Opinioned 写法

Opinioned意思是:"有自己意见的"。这里举一个例子方便理解,Eslint和Prettier都可用作代码格式化。Eslint是Unopinioned,所有的规则都要自己配。Prettier是Opinioned,除了少数的配置项,大部分的格式写法都是Prettier的决定的,没办法修改。Prettier晚于Eslint出现,但现在的项目中,代码格式化的工作已经基本被Prettier统一,Eslint也是引用Prettier的规则,说明大家对Opinioned的工具并不排斥,毕竟学完了就可以省时省力。MST的Opinioned体现在两个方面,一个和本文关系不大,体现在model的设计思想等形而上的方面。另一个是API,下面就看看MST的API有什么不一样。

必须的 API

MST自己定义了一套独特的API写法,与Redux的函数式写法不同(虽然我认为Redux的写法也是Opinioned,只不过通常被叫作boilerplate),也和Mobx的对象式写法不同。这种写法容易让人第一时间蒙圈,但其实大大减少了需要记忆的API。核心的API只有4块(注意不是个,因为每个API都有进一步的API):

  • types.model()
  • types.model.props()
  • types.model.actions()
  • Model.create()

有了这4部分就可以完成定义一颗状态树80%以上的工作。这里我贴一段官方的例子,在代码中用注释介绍:

javascript 复制代码
import { types } from "mobx-state-tree"

// 定义一个Tweet类型
const Tweet = types
    .model("Tweet")
    .props({
        body: types.string,
        read: false // 由默认值推断types
    })
    .actions((tweet) => ({
        toggle() {
            tweet.read = !tweet.read
        }
    }))

// 定义一个TwitterStore类型,包括Tweet的数组
const TwitterStore = types.model("TwitterStore", {
    tweets: types.array(Tweet)
})

// 新建一个TwitterStore实例
const twitterStore = TwitterStore.create({
    tweets: [
        {
            body: "Anyone tried MST?"
        }
    ]
})

// 调用Tweet类型方法
twitterStore.tweets[0].toggle()

这里有两个地方值得一说,一是types.model()types.model.props()的用法,两种方法基本是等价的,其中的不同不影响使用,有兴趣的可以去看官方文档,我会选择types.model.props()的写法,更清晰一些。

二是types,前面的核心API中的方法也是types中的。types是MST另一个褒贬不一的特点,下面单独介绍一下。

types

types是MST的重要概念,可以简单理解成数据类型或数据结构,还用来进行runtime类型检查。数据类型有2类,一类是复杂数据类型modelmodel的集合,另一类是简单类型,如stringnumber等。复杂数据类型可以包括简单数据类型,反之则不行。此外,还有一些帮助类型,这里不一一类举了。因为绝大部分类型不需要记忆,MST是可以通过默认值推断的,比如上面的例子中,你设置body: types.string,也可以直接body: '',这样既设定了body的type是string,还设定了默认值是空字符串。只有两个类型需要在这篇文章里提一下:

  • 一个是帮助类型中的types.optional,这个类型可以避免在初始化model类型时需要传入初始数据,一般的用法是types.optional(Model, {})注意,这个并不是官方用法,是在我开发的过程中发现这么写可以省很多事儿,也没发现什么副作用。如果有问题,欢迎在评论区指正。
  • 另一个是types.frozen,这个类型的作用是存不可观测的arrayobject,是简单类型。这里有点难理解,简单来说,这个arrayobject可以看作一个整体,如果array中的元素或object中的key发生变化,MST是不会观测到变化的,在arrayobject的引用地址改变后,MST会观测到,并整体改变这个值,有点类似于Redux需要返回immutable的修改方式。

类型检查和Typescript的类型检查一样,变量只能被赋予预先定义类型的值。不同的是,Typescript的类型检查是静态的,MST的类型检查时运行中的,即在程序运行的任何时候,给MST的prop传入了不对的数据类型,MST都会报错,但并不会中断JS进程。由于MST开发的时候typescript还没有现在这么流行,所以MST做了自己的类型检查系统,这对于复杂的业务系统是很必要的。在使用typescript的情况下,MST在绝大多数情况下,也可以很好的配合,只不过还有一些小问题需要hack一下,下面会提到。

最好掌握的进阶API

这些进阶的API是我在开发中发现的常用功能,但不是必要的,所以和必须API区分开来。除此之外,MST还有很多高级功能,由于篇幅和定位,感兴趣的可以直接去文档查阅。

  • types.views()

对应Mobx中的计算值,在types.views()中定义,必须是纯函数,如果定义的是getter方法,则以值的形式从外部访问。我认为区分view和action的最简单方法就是view中都定义getter,可以减少心智负担。views最大的用处就是减少业务中通过一个状态推算出的另一个状态,直接看官方示例:

javascript 复制代码
const UserStore = types
    .model({
        users: types.array(User)
    })
    .views(self => ({
        // 对外暴露为值
        get numberOfChildren() {
            return self.users.filter(user => user.age < 18).length
        },
        // 对外暴露为方法
        numberOfPeopleOlderThan(age) {
            return self.users.filter(user => user.age > age).length
        }
    }))
  • self

types.views()types.actions()中回调函数的参数,代表当前树的实例,作者解释这样可以解决this可能带来的问题。通过self,在view和action中可以使用当前树上定义的数据和方法,也可以通过帮助函数在树上遍历。self还带有参数类型,不过由于view和action中的方法通过回调方式增加,这导致Typescript无法推断出self上通过view和action定义的方法和数据。目前我还没有发现优雅的解决方案,网上的方案在适用性上都有一定的问题,只能先让Typescript忽略掉这个错误(// @ts-ignore)。

详见:mobx-state-tree.js.org/tips/typesc...

javascript 复制代码
const Example = types
    .model("Example", {
        prop: types.string
    })
    .views(self => ({
        get upperProp(): string {
            return self.prop.toUpperCase()
        },
        get twiceUpperProp(): string {
            // 这里就是问题
            // @ts-ignore
            return self.upperProp + self.upperProp // Compile error: `self.upperProp` is not yet defined
        }
    }))
  • flow

异步函数处理函数,接受一个generator函数作为参数,然后在该generator函数中对prop的修改就都会被认为是在action中做的。这个方法和Mobx中的flow方法一样,类似于Redux中的异步中间件,都是为了解决异步函数中异步操作不在action context中的问题。所以其实只需要记住用法:flow(function* { ... }),然后把async function() { await ... }换成funciton*() { yield ... },还是看官方示例有个直观印象:

javascript 复制代码
import { types, flow } from "mobx-state-tree"

someModel.actions(self => ({
    // generator函数
    fetchProjects: flow(function*() {
        self.state = "pending"
        try {
            // yield 代替 await
            self.githubProjects = yield fetchGithubProjectsSomehow()
            self.state = "done"
        } catch (error) {
            console.error("Failed to fetch projects", error)
            self.state = "error"
        }
        // flow会返回一个promise,如果返回值,则在promise中resolve该值
        return self.githubProjects.length
    })
}))
  • actionvolatile state

听着高大上,其实就是通过闭包在action中建立变量存放不需要对外暴露的数据。volatile state在MST中有两种用法,一种是types.volatile,一种是闭包中声明变量。比较坑的地方在于两种方式声明的volatile state表现是不同的,最主要的区别在于types.volatile可以被外部访问到,而闭包中变量是绝对无法访问的。具体区别详见:mobx-state-tree.js.org/concepts/vo...

我个人建议只使用闭包的方式定义,这更符合volatile state的使用场景,闭包的变量也不会对model外部造成影响,防止本不该暴露的状态被外部引用。如果这个状态需要向外暴露,那么声明为prop更加合理。

官方示例:

javascript 复制代码
const Store = types
    .model({
        todos: types.array(Todo),
        state: types.enumeration("State", ["loading", "loaded", "error"])
    })
    .actions(self => {
        // volatile state
        let pendingRequest = null 

        function afterCreate() {
            self.state = "loading"
            pendingRequest = someXhrLib.createRequest("someEndpoint")
        }

        function beforeDestroy() {
            // 使用闭包变量
            pendingRequest.abort()
        }

        return {
            afterCreate,
            beforeDestroy
        }
    })

React里怎么用

在本文的What部分已经说了,MST几乎可以和市面上的任何前端框架搭配,所以和React的配合的部分不是MST提供的,而是mobx-reactmobx-react-lite。这两个包会收集React组件中用到的observable,并把组件的渲染注册成这些observablereaction,在observable变化之后,自动触发依赖组件的渲染。这也是Mobx系方案对比Redux系方案的一大优势------几乎不需要手动的优化组件渲染 。对于业务系统来说,这是一个可以降本增效的大优点。两个包的区别在于只有mobx-react支持Class组件,对于新项目来说,建议直接使用function组件和更轻量mobx-react-lite,然后通过hooks创建一个root modelcontext,组件通过context provider获取model实例。看一个具体的例子:

javascript 复制代码
// Model/index.ts
import { createContext, useContext } from 'react'
import { types } from 'mobx-state-tree'

// 定义context
export const store = types.model({ ... }).create({ ... })
export const ModelContext = createContext(store)
export const useStore = () => useContext(ModelContext)

// index.tsx
import ReactDOM from 'react-dom'

const root = ReactDOM.createRoot(document.getElementById('root'))
// 通过context provider注入model
root.render(
    <ModelContext.Provider value={store}>
      <App />
    </ModelContext.Provider>
)

// component.tsx
import { observer } from 'mobx-react-lite'

const Example = () => {
    // 通过自定义hook使用model
    const { MODEL_PROP: model } = useStore()

    const onAction = () => {
        model.action(...)
    }

    return <div>{model.prop}</div>
}
// 使用observer包裹组件
export default observer(Example)

详细内容参考:zh.mobx.js.org/react-integ...

总结

本文基于MST的文档,梳理介绍了使用MST开发常用的API,帮助不熟悉MST的人快速上手。但如果想要用好MST,只看本文是远远不够的,这遍文章连基础都算不上,更类似于cheat sheet,很多开发中涉及的细节官方文档会说的更详细。

本文的示例都很零散,也不一定能跑起来,只是为了提供一个对API用法的直观印象,如果想参考能实际运行的MST的例子,可以通过我的CRA template生成一个MVC的todo list项目:www.npmjs.com/package/cra... ,里面还有一些本文没有涉及到的高级用法,目前看不懂也不影响。

希望本文能帮助更多的人了解、使用MST,通过MST解决业务前端系统开发中的最大痛点,让开发的工作和开发的内容都更加井然有序。另外,也希望大家在使用的过程中暴露遇到的问题,让我有更多的case去思考MST在现实场景中需要解决的问题。之后还会写一篇更接近于方法论的文章,来聊聊为什么选择MST承载业务逻辑,欢迎关注。

相关推荐
凯哥爱吃皮皮虾7 小时前
如何给 react 组件写单测
前端·react.js·jest
每一天,每一步10 小时前
react antd点击table单元格文字下载指定的excel路径
前端·react.js·excel
screct_demo20 小时前
詳細講一下在RN(ReactNative)中,6個比較常用的組件以及詳細的用法
javascript·react native·react.js
光头程序员1 天前
grid 布局react组件可以循数据自定义渲染某个数据 ,或插入某些数据在某个索引下
javascript·react.js·ecmascript
limit for me1 天前
react上增加错误边界 当存在错误时 不会显示白屏
前端·react.js·前端框架
浏览器爱好者1 天前
如何构建一个简单的React应用?
前端·react.js·前端框架
VillanelleS1 天前
React进阶之高阶组件HOC、react hooks、自定义hooks
前端·react.js·前端框架
某哈压力大1 天前
基于react-vant实现弹窗搜索功能
前端·react.js
傻小胖1 天前
React 中hooks之useInsertionEffect用法总结
前端·javascript·react.js
flying robot2 天前
React的响应式
前端·javascript·react.js