- 使用 MobX 来管理小程序的跨页面数据
在小程序中,常常有些数据需要在几个页面或组件中共享。对于这样的数据,在 web 开发中,有些朋友使用过 redux 、 vuex 之类的 状态管理 框架。在小程序开发中,也有不少朋友喜欢用 MobX ,说明这类框架在实际开发中非常实用。 小程序团队近期也开源了 MobX 的辅助模块,使用 MobX 也更加方便。那么,在这篇文章中就来介绍一下 MobX 在小程序中的一个简单用例! 在小程序中引入 MobX 在小程序项目中,可以通过 npm 的方式引入 MobX 。如果你还没有在小程序中使用过 npm ,那先在小程序目录中执行命令: [代码]npm init -y [代码] 引入 MobX : [代码]npm install --save mobx-miniprogram mobx-miniprogram-bindings [代码] (这里用到了 mobx-miniprogram-bindings 模块,模块说明在这里: https://developers.weixin.qq.com/miniprogram/dev/extended/functional/mobx.html 。) npm 命令执行完后,记得在开发者工具的项目中点一下菜单栏中的 [代码]工具[代码] - [代码]构建 npm[代码] 。 MobX 有什么用呢? 试想这样一个场景:制作一个天气预报资讯小程序,首页是列表,点击列表中的项目可以进入到详情页。 首页如下: [图片] 详情页如下: [图片] 每次进入首页时,需要使用 [代码]wx.request[代码] 获取天气列表数据,之后将数据使用 setData 应用到界面上。进入详情页之后,再次获取指定日期的天气详情数据,展示在详情页中。 这样做的坏处是,进入了详情页之后需要再次通过网络获取一次数据,等待网络返回后才能将数据展示出来。 事实上,可以在首页获取天气列表数据时,就一并将所有的天气详情数据一同获取回来,存放在一个 数据仓库 中,需要的时候从仓库中取出来就可以了。这样,只需要进入首页时获取一次网络数据就可以了。 MobX 可以帮助我们很方便地建立数据仓库。接下来就讲解一下具体怎么建立和使用 MobX 数据仓库。 建立数据仓库 数据仓库通常专门写在一个独立的 js 文件中。 [代码]import { observable, action } from 'mobx-miniprogram' // 数据仓库 export const store = observable({ list: [], // 天气数据(包含列表和详情) // 设置天气列表,从网络上获取到数据之后调用 setList: action(function (list) { this.list = list }), }) [代码] 在上面数据仓库中,包含有数据 [代码]list[代码] (即天气数据),还包括了一个名为 [代码]setList[代码] 的 action ,用于更改数据仓库中的数据。 在首页中使用数据仓库 如果需要在页面中使用数据仓库里的数据,需要调用 [代码]createStoreBindings[代码] 来将仓库中的数据绑定到页面数据中,然后就可以在页面中直接使用仓库数据了。 [代码]import { createStoreBindings } from 'mobx-miniprogram-bindings' import { store } from './store' Page({ onLoad() { // 绑定 MobX store this.storeBindings = createStoreBindings(this, { store, // 需要绑定的数据仓库 fields: ['list'], // 将 this.data.list 绑定为仓库中的 list ,即天气数据 actions: ['setList'], // 将 this.setList 绑定为仓库中的 setList action }) // 从服务器端读取数据 wx.showLoading() wx.request({ // 请求网络数据 // ... success: (data) => { wx.hideLoading() // 调用 setList action ,将数据写入 store this.setList(data) } }) }, onUnload() { // 解绑 this.storeBindings.destroyStoreBindings() }, }) [代码] 这样,可以在 wxml 中直接使用 list : [代码]<view class="item" wx:for="{{list}}" wx:key="date" data-index="{{index}}"> <!-- 这里可以使用 list 中的数据了! --> <view class="title">{{item.date}} {{item.summary}}</view> <view class="abstract">{{item.temperature}}</view> </view> [代码] 在详情页中使用数据仓库 在详情页中,同样可以使用 [代码]createStoreBindings[代码] 来将仓库中的数据绑定到页面数据中: [代码]import { createStoreBindings } from 'mobx-miniprogram-bindings' import { store } from './store' Page({ onLoad(args) { // 绑定 MobX store this.storeBindings = createStoreBindings(this, { store, // 需要绑定的数据仓库 fields: ['list'], // 将 this.data.list 绑定为仓库中的 list ,即天气数据 }) // 页面参数 `index` 表示要展示哪一条天气详情数据,将它用 setData 设置到界面上 this.setData({ index: args.index }) }, onUnload() { // 解绑 this.storeBindings.destroyStoreBindings() }, }) [代码] 这样,这个页面 wxml 中也可以直接使用 list : [代码]<view class="title">{{list[index].date}}</view> <view class="content">温度 {{list[index].temperature}}</view> <view class="content">天气 {{list[index].weather}}</view> <view class="content">空气质量 {{list[index].airQuality}}</view> <view class="content">{{list[index].details}}</view> [代码] 完整示例 完整例子可以在这个代码片段中体验: https://developers.weixin.qq.com/s/YhfvpxmN7HcV 这个就是 MobX 在小程序中最基础的玩法了。相关的 npm 模块文档可参考 mobx-miniprogram-bindings 和 mobx-miniprogram 。 MobX 在实际使用时还有很多好的实践经验,感兴趣的话,可以阅读一些其他相关的文章。
2019-11-01 - 微信小程序数据管理方案:mobx-miniprogram-lite
GitHub: https://github.com/XHFkindergarten/mobx-miniprogram-lite Example: https://developers.weixin.qq.com/s/GNZlomm87hM3 为什么推荐使用 mobx-miniprogram-lite 基于 mobx 实现数据的深度监听,响应式更新视图。 支持 computed 派生计算。 公共状态管理。 基于原生 JS 编写小程序逻辑层,获得更好的代码组织结构。 安装 [代码]npm install mobx-miniprogram-lite [代码] 快速开始 1.定义响应式数据 [代码]import { observable } from 'mobx-miniprogram-lite' export const countStore = observable({ // 被观测的数据 value: 0, // 计算属性 get double() { return this.value * 2 }, // 修改数据的方法 addValue() { this.value++ } }) [代码] 2.将数据与 [代码]Page/Component[代码] 绑定 Page 页面实例 [代码]import { connectPage } from 'mobx-miniprogram-lite' import { countStore } from './count' connectPage({ store: { count: countStore } }) [代码] Component 组件实例 [代码]import { connectComponent } from 'mobx-miniprogram-lite' import { countStore } from './count' connectComponent({ store: { count: countStore } }) [代码] 3.渲染和交互 在组件方法中调用数据方法 [代码]import { connectPage } from 'mobx-miniprogram-lite' import { countStore } from './count' connectPage({ store: { count: countStore }, bindTapAdd() { this.store.count.addValue() } }) [代码] 在 WXML 中绑定数据和交互事件 [代码]<view> value: {{count.value}} </view> <view> value * 2: {{count.doubleValue}} </view> <view bindtap="bindTapAdd"> click me to add value </view> [代码] 这样就完成了一个数字累加器的简单实现。 灵感来源 Westore - 小程序项目分层架构 代码案例 https://developers.weixin.qq.com/s/7U4VcFmN7pKx 核心概念 具体请参考 mobx 文档 observable 被观测的数据,只有修改的数据是可观测状态时,mobx 才能触发自动响应。 action 数据的修改动作需要在 action 函数中完成。 computed 从被观测的数据中派生计算出来的数据,会随着被观测数据的改变而自动更新。 文档 创建可观测的数据 observable:使一个数据结构变为可观测的。 其中的数据及其深层数据均会变为可观测的,其中的 getter 函数将会变为 computed 数据,其中的函数属性将会变为 action 函数。 [代码]import { observable } from 'mobx-miniprogram-lite' // 可观测数组 const todos = observable([ { title: 'Spoil tea', completed: true }, { title: 'Make coffee', completed: false } ]) // 可观测对象 const counter = observable({ value: 0, get double() { return this.value * 2 }, addValue() { this.value++ } }) [代码] makeObservable 和 observable 的作用一致,但是作用于 class 数据。第一个参数为类的实例,第二个参数用于对类的属性进行类型断言。 [代码]import { makeObservable, observable, computed, action } from 'mobx-miniprogram-lite' class Doubler { value constructor(value) { makeObservable(this, { value: observable, double: computed, increment: action }) this.value = value } get double() { return this.value * 2 } increment() { this.value++ } } // 对于继承场景,只能使用 makeObservable API class SubClass extends Doubler { constructor(value) { super(value) makeObservable(this, { triple: computed }) } // 使用父类中的值 get triple() { return this.value * 3 } } [代码] makeAutoObservable makeAutoObservable 是 makeObservable 的自动推导模式,根据属性的类型自动推断为 [代码]observable[代码]、[代码]action[代码]、[代码]computed[代码] 等类型。但是相对来说有一定限制,例如不能用于子类。 [代码]import { makeAutoObservable } from 'mobx-miniprogram-lite' class Doubler { value readonly description = '静态字段' constructor(value) { makeAutoObservable( this, // 和 makeObservable 一样,第二个参数 override 支持指定字段的类型 { // 将 description 字段标记为无需观测 description: false } ) this.value = value } get double() { return this.value * 2 } increment() { this.value++ } async fetch() { const response = await fetch('/api/value') this.value = response.json() } } [代码] 更新数据 Updating state using actions 当我们希望更新一个被 mobx 观测的数据时,应该通过 [代码]action[代码] 来完成这一操作(对于 mobx 来说,这不是必须的,但是强烈推荐这样做)。当你使用任意一个状态管理库时都能够看到类似的概念,这样做的好处是: action 动作会在 mobx 的 事务 中执行,多个 action 动作同时触发时它们将会被批处理,中间态不会对外暴露,避免导致一些 bug。 通过显式地声明 action 能够帮助开发者更好的组织代码,在定位问题时明确数据更新是如何触发的。 相比其他状态管理库,mobx 有其特殊性——不依赖特定的 API 语法,通过原生 JS 赋值即可触发状态的更新。这在给开发者带来极大便利的同时,也带来了一定的风险,试想在一个大型项目中存在 N 处对数据的直接更改,开发者在定位问题时很难定位到哪一处更改导致了最终的结果。 mobx 默认开启了 [代码]enforceActions: true[代码],运行时如果不通过 action 而是直接修改数据,控制台将会抛出 warning:[MobX] Since strict-mode is enabled, changing (observed) observable values without using an action is not allowed. Tried to modify: XXStore@XXProperty. observable [代码]import { observable, action } from 'mobx-miniprogram-lite' // ✅ const counter = observable({ value: 0, // 被 observable 包裹的【非异步】函数自动成为 action updateValue() { this.value++ } }) // ❌ const counter = observable({ value: 0 }) const updateValue = () => { // 非 action counter.value++ } // ✅ const counter = observable({ value: 0 }) // 使用 action 包裹,使函数成为 action 函数 const updateValue = action(() => { counter.value++ }) [代码] makeAutoObservable [代码]import { makeAutoObservable } from 'mobx-miniprogram-lite' class Store { constructor() { makeAutoObservable(this) } value = 0 // 和 observable 相同,makeAutoObservable 会自动识别对象中的函数并包装为 action。 updateValue() { this.value++ } } [代码] makeObservable [代码]import { makeObservable, observable, action } from 'mobx-miniprogram-lite' class Store { constructor() { makeObservable(this, { value: observable, // 手动声明 updateValue 是一个 action updateValue: action }) } value = 0 updateValue() { this.value++ } } [代码] 异步更新 由于 mobx 的事务是同步的,因此异步数据更新无法被事务捕获,异步函数也无法被包装成 action。 [代码]import { observable, runInAction } from 'mobx-miniprogram-lite' // ❌ const counter = observable({ value: 0, async updateValueAsync() { // do something await fetchData() // update this.value++ // 仍会抛出 warning } }) // ✅ const counter = observable({ value: 0, updateValue() { this.value++ } async updateValueAsync() { // do something await fetchData() // 由于 updateValue 是一个 action,因此数据是在事务中完成的更新 this.updateValue() } }) // ✅ const counter = observable({ value: 0, async updateValueAsync() { // do something await fetchData() // runInAction 是 action 函数的立即执行版本,被 runInAction 包裹的函数会立即在事务中执行 runInAction(() => { this.value++ }) } }) [代码] 绑定到小程序组件 由于 Page 和 Component 的生命周期存在差异,因此 mobx-miniprogram-lite 通过两个不同的 API 来绑定数据和组件。它们的区别仅仅是函数名称的不同,函数参数和类型是完全一致的。 开发者要做的就是将小程序原生 API 替换为 mobx-miniprogram-lite 导出的 API: [代码]Page({})[代码] → [代码]connectPage({})[代码] [代码]Component({})[代码] → [代码]connectComponent({})[代码] 并且通过 store 属性完成数据源的注入即可。 Page [代码]import { connectPage } from 'mobx-miniprogram-lite' import counterStore from './stores/counter' import todoStore from './stores/todo' connectPage({ // 通过 store 属性注入观测数据 store: { counter: counterStore, todo: todoStore }, onLoad() {}, onUnload() {}, data: {} }) [代码] Component [代码]import { connectComponent } from 'mobx-miniprogram-lite' import counterStore from './stores/counter' import todoStore from './stores/todo' connectComponent({ // 通过 store 属性注入观测数据 store: { counter: counterStore, todo: todoStore }, lifetimes: { attached() {}, detached() {} }, data: {} }) [代码] 模板渲染 store 中的数据源,会被 mobx-miniprogram-lite 自动映射到 data 中,例如: [代码]this.store.todo.length[代码] → [代码]this.data.todo.length[代码] 因此,可以直接在 wxml 中通过 store 属性中的属性名访问到对应数据: [代码]<view>length: {{todo.length}}</view> [代码] 调用数据层方法 mobx-miniprogram-lite 并没有将 store 中的函数自动绑定到小程序组件中,而是推荐开发者手动进行调用: [代码]connectPage({ store: { todo: todoStore }, toggleTodo(e: WechatMiniprogram.TouchEvent) { const index = e.target.dataset.index this.store.todo.toggleTodo(index) } }) [代码] 在小程序事件处理函数中往往需要处理交互事件,这部分逻辑属于视图层,如果和数据层逻辑写在一起会造成耦合与污染,因此更推荐开发者将视图层逻辑和数据层逻辑拆分。 性能 由于 mobx-miniprogram-lite 的底层对可观测对象进行了深度遍历,随着数据量的增大,性能损耗也将会线性增长,如果你的业务数据量十分庞大,请谨慎使用。 以下为数据条数大约带来的额外计算时长(相比原生 setData): 数据条数 耗时(毫秒) 100 1 1000 3 10000 30 优化手段 mobx-miniprogram-lite 为每一个 store 的子属性单独创建观测实例,因此不要将多个数据字段存放在一起,以此达到最优的性能。 [代码]import { connectPage, observable } from 'mobx-miniprogram-lite' // ❌ const listStore = observable({ // states-observer // loading 更新时,也会触发 list 的重新计算 states = { list: [...], // 长达 5000 条数据 loading: false, } }) // ✅ const listStore = observable({ // list-observer list: [...], // 长达 5000 条数据 // loading-observer // 当 loading 的值发生改变时,仅重新计算 loading loading: false, }) connectPage({ store: { list: listStore } }) [代码]
2023-10-31