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承载业务逻辑,欢迎关注。

相关推荐
FinGet11 小时前
那总结下来,react就是落后了
前端·react.js
王解14 小时前
Jest项目实战(2): 项目开发与测试
前端·javascript·react.js·arcgis·typescript·单元测试
AIoT科技物语1 天前
免费,基于React + ECharts 国产开源 IoT 物联网 Web 可视化数据大屏
前端·物联网·react.js·开源·echarts
初遇你时动了情1 天前
react 18 react-router-dom V6 路由传参的几种方式
react.js·typescript·react-router
番茄小酱0011 天前
ReactNative中实现图片保存到手机相册
react native·react.js·智能手机
王解1 天前
Jest进阶知识:深入测试 React Hooks-确保自定义逻辑的可靠性
前端·javascript·react.js·typescript·单元测试·前端框架
小牛itbull1 天前
ReactPress—基于React的免费开源博客&CMS内容管理系统
前端·react.js·开源·reactpress
~甲壳虫2 天前
react中得类组件和函数组件有啥区别,怎么理解这两个函数
前端·react.js·前端框架
用户8185216881172 天前
react项目搭建create-router-dom,redux详细解说
react.js
new Vue()2 天前
Vue vs React:两大前端框架的区别解析
vue.js·react.js·前端框架