在小程序开发中,数据共享是个绕不开的话题。我见过太多开发者因为数据同步问题加班到深夜,也见过不少项目因为状态管理混乱而难以维护。为什么我们需要全局数据共享?这要从小程序的基本架构说起。
小程序采用多页面架构,每个页面都是独立的WebView实例。这意味着:
典型的业务场景包括:
这是小程序内置的最简单方案。在app.js中定义globalData对象:
javascript复制// app.js
App({
globalData: {
userInfo: null,
theme: 'light',
systemInfo: null
},
onLaunch() {
// 初始化时获取系统信息
wx.getSystemInfo({
success: res => {
this.globalData.systemInfo = res
}
})
}
})
在页面中使用:
javascript复制// pages/index/index.js
const app = getApp()
Page({
onLoad() {
// 读取全局数据
console.log(app.globalData.theme)
// 修改全局数据
app.globalData.userInfo = {name: '张三'}
}
})
注意事项:
- globalData修改不会自动触发页面更新
- 大型项目容易产生命名冲突
- 不适合存储大量数据,会影响小程序启动速度
Behavior是小程序的混入机制,可以实现逻辑复用:
javascript复制// behaviors/theme.js
module.exports = Behavior({
data: {
theme: 'light'
},
methods: {
toggleTheme() {
const newTheme = this.data.theme === 'light' ? 'dark' : 'light'
this.setData({theme: newTheme})
}
}
})
在页面中使用:
javascript复制// pages/settings/settings.js
const themeBehavior = require('../../behaviors/theme')
Page({
behaviors: [themeBehavior],
onLoad() {
console.log(this.data.theme) // 可以访问theme数据
}
})
实际经验:
- 每个页面实例会创建独立的data副本
- 适合UI组件复用,不适合真正的全局状态
- 可以通过事件总线实现跨页面同步
事件总线是解耦组件通信的经典模式。以下是完整实现:
javascript复制// utils/event-bus.js
class EventBus {
constructor() {
this.events = {}
}
on(eventName, callback) {
if (!this.events[eventName]) {
this.events[eventName] = []
}
this.events[eventName].push(callback)
}
emit(eventName, payload) {
const callbacks = this.events[eventName]
if (callbacks) {
callbacks.forEach(cb => {
try {
cb(payload)
} catch (e) {
console.error(`EventBus callback error:`, e)
}
})
}
}
off(eventName, callback) {
const callbacks = this.events[eventName]
if (callbacks) {
if (callback) {
const index = callbacks.indexOf(callback)
if (index > -1) callbacks.splice(index, 1)
} else {
delete this.events[eventName]
}
}
}
}
export default new EventBus()
在app.js中挂载:
javascript复制// app.js
import eventBus from './utils/event-bus'
App({
eventBus,
onLaunch() {
// 全局错误处理
eventBus.on('globalError', this.handleError)
}
})
页面中使用示例:
javascript复制// pages/cart/cart.js
const app = getApp()
Page({
data: {count: 0},
onLoad() {
// 监听购物车变化
this.cartChangeHandler = (count) => {
this.setData({count})
}
app.eventBus.on('cartChange', this.cartChangeHandler)
},
onUnload() {
// 必须移除监听
app.eventBus.off('cartChange', this.cartChangeHandler)
}
})
最佳实践:
- 事件名建议使用常量定义,避免拼写错误
- 在onUnload中必须移除监听
- 复杂数据建议使用深拷贝避免引用问题
对于复杂应用,可以仿照Vuex实现一个状态管理库:
javascript复制// store/index.js
class Store {
constructor(options) {
this.state = options.state || {}
this.mutations = options.mutations || {}
this.actions = options.actions || {}
this.getters = options.getters || {}
this._subscribers = []
// 处理getters
Object.keys(this.getters).forEach(key => {
Object.defineProperty(this.state, key, {
get: () => this.getters[key](this.state),
enumerable: true
})
})
}
commit(type, payload) {
if (this.mutations[type]) {
this.mutations[type](this.state, payload)
this._notify()
}
}
dispatch(type, payload) {
if (this.actions[type]) {
return this.actions[type](this, payload)
}
return Promise.reject(new Error(`Unknown action type: ${type}`))
}
subscribe(fn) {
this._subscribers.push(fn)
return () => {
const index = this._subscribers.indexOf(fn)
if (index > -1) this._subscribers.splice(index, 1)
}
}
_notify() {
this._subscribers.forEach(sub => sub(this.state))
}
}
// 创建store实例
export default new Store({
state: {
cartItems: [],
userInfo: null
},
mutations: {
ADD_TO_CART(state, item) {
const existing = state.cartItems.find(i => i.id === item.id)
if (existing) {
existing.quantity += item.quantity
} else {
state.cartItems.push(item)
}
},
SET_USER(state, user) {
state.userInfo = user
}
},
actions: {
async fetchUser({ commit }) {
const user = await wx.request({url: '/api/user'})
commit('SET_USER', user)
}
},
getters: {
cartTotal: state => state.cartItems.reduce((total, item) => total + item.price * item.quantity, 0)
}
})
在页面中连接store:
javascript复制// pages/product/product.js
import store from '../../store'
Page({
data: {
product: {},
inCart: false
},
onLoad() {
// 初始化订阅
this.unsubscribe = store.subscribe(state => {
this.setData({
inCart: state.cartItems.some(item => item.id === this.data.product.id)
})
})
},
onUnload() {
this.unsubscribe()
},
addToCart() {
store.commit('ADD_TO_CART', {
id: this.data.product.id,
name: this.data.product.name,
price: this.data.product.price,
quantity: 1
})
}
})
架构建议:
- 将store拆分为modules管理大型项目
- mutations应该是同步函数
- 异步操作放在actions中处理
- 使用getters派生计算状态
对于团队项目,使用成熟的三方库能提升效率:
bash复制npm install miniprogram-store --save
javascript复制// store.js
import { createStore } from 'miniprogram-store'
const store = createStore({
state: {
count: 0
},
mutations: {
increment(state) {
state.count++
}
}
})
export default store
页面中使用:
javascript复制import store from '../../store'
Page({
onLoad() {
store.subscribe(() => {
this.setData({
count: store.state.count
})
})
},
addCount() {
store.commit('increment')
}
})
bash复制npm install mobx-miniprogram --save
javascript复制// store.js
import { observable, action } from 'mobx-miniprogram'
export const store = observable({
// 状态
userInfo: null,
cartItems: [],
// 计算属性
get cartTotal() {
return this.cartItems.reduce((total, item) => total + item.price * item.quantity, 0)
},
// 动作
setUserInfo: action(function(user) {
this.userInfo = user
}),
addToCart: action(function(item) {
const existing = this.cartItems.find(i => i.id === item.id)
if (existing) {
existing.quantity += item.quantity
} else {
this.cartItems.push(item)
}
})
})
页面中绑定:
javascript复制import { createStoreBindings } from 'mobx-miniprogram-bindings'
import { store } from '../../store'
Page({
onLoad() {
this.storeBindings = createStoreBindings(this, {
store,
fields: ['userInfo', 'cartItems', 'cartTotal'],
actions: ['setUserInfo', 'addToCart']
})
},
onUnload() {
this.storeBindings.destroy()
}
})
选型建议:
- 小型项目用miniprogram-store足够
- 复杂项目推荐mobx-miniprogram
- 考虑团队技术栈一致性
| 方案 | 响应式 | 跨页面 | 调试支持 | 学习成本 | 代码组织 |
|---|---|---|---|---|---|
| App.globalData | ❌ | ✅ | ❌ | ⭐ | ❌ |
| Behavior | ✅ | ❌ | ❌ | ⭐⭐ | ⭐⭐ |
| Event Bus | ✅ | ✅ | ⭐⭐ | ⭐⭐ | ⭐⭐⭐ |
| 自定义Store | ✅ | ✅ | ⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ |
| 第三方库 | ✅ | ✅ | ⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐ |
现象:修改了全局状态但页面没有更新
解决方案:
javascript复制// 错误示例 - 直接修改引用
const user = app.globalData.userInfo
user.name = '李四' // 不会触发更新
// 正确做法 - 创建新对象
app.globalData.userInfo = {...app.globalData.userInfo, name: '李四'}
现象:页面反复打开关闭后小程序变卡顿
排查步骤:
javascript复制Page({
onLoad() {
// 错误示例 - 缓存页面实例
app.globalData.currentPage = this
// 正确做法 - 使用弱引用
app.globalData.currentPageRef = new WeakRef(this)
},
onUnload() {
// 必须清理
app.globalData.currentPageRef = null
}
})
现象:同一数据在多处修改,难以追踪变化
解决方案:
javascript复制class StrictStore extends Store {
commit(type, payload) {
console.log(`[Store] Commit: ${type}`, payload)
super.commit(type, payload)
}
}
实现全局状态的本地存储:
javascript复制// 增强store类
class PersistentStore extends Store {
constructor(options) {
super(options)
this._storageKey = options.storageKey || 'app-store'
this._load()
}
_load() {
try {
const data = wx.getStorageSync(this._storageKey)
if (data) Object.assign(this.state, data)
} catch (e) {
console.error('Failed to load store state', e)
}
}
_save() {
try {
wx.setStorageSync(this._storageKey, this.state)
} catch (e) {
console.error('Failed to save store state', e)
}
}
commit(type, payload) {
super.commit(type, payload)
this._save()
}
}
开发调试利器:
javascript复制class DebuggableStore extends Store {
constructor(options) {
super(options)
this._history = []
this._maxHistory = options.maxHistory || 20
}
commit(type, payload) {
// 保存当前状态快照
this._history.push(JSON.stringify(this.state))
if (this._history.length > this._maxHistory) {
this._history.shift()
}
super.commit(type, payload)
}
rollback(steps = 1) {
if (steps <= 0 || steps > this._history.length) return
const index = this._history.length - steps
this.state = JSON.parse(this._history[index])
this._history.length = index
this._notify()
}
}
跟踪状态变更性能:
javascript复制class ProfiledStore extends Store {
commit(type, payload) {
const start = Date.now()
super.commit(type, payload)
const duration = Date.now() - start
if (duration > 50) {
console.warn(`Slow mutation: ${type} took ${duration}ms`)
}
// 可以上报到监控系统
this._reportPerformance(type, duration)
}
}
将store拆分为多个模块:
code复制store/
├── index.js # 主入口
├── modules/
│ ├── user.js # 用户模块
│ ├── cart.js # 购物车模块
│ └── product.js # 商品模块
└── plugins/
├── logger.js # 日志插件
└── persist.js # 持久化插件
为store添加类型定义:
typescript复制// types/store.d.ts
interface User {
id: string
name: string
avatar: string
}
interface CartItem {
id: string
name: string
price: number
quantity: number
}
interface StoreState {
user: User | null
cart: CartItem[]
}
interface StoreMutations {
SET_USER: (state: StoreState, user: User) => void
ADD_TO_CART: (state: StoreState, item: CartItem) => void
}
interface StoreActions {
fetchUser: () => Promise<void>
}
为store编写单元测试:
javascript复制describe('Store', () => {
let store
beforeEach(() => {
store = new Store({
state: { count: 0 },
mutations: {
INCREMENT(state) { state.count++ }
}
})
})
test('commit mutation', () => {
store.commit('INCREMENT')
expect(store.state.count).toBe(1)
})
test('subscription', () => {
const mockFn = jest.fn()
store.subscribe(mockFn)
store.commit('INCREMENT')
expect(mockFn).toHaveBeenCalledWith({count: 1})
})
})
随着小程序技术发展,状态管理也在不断进化。一些值得关注的趋势:
在实际项目中,我建议根据团队规模和技术栈选择最适合的方案。小型项目可以从Event Bus开始,随着业务复杂度的提升逐步过渡到更完善的状态管理方案。关键是要保持代码的可维护性和可扩展性,避免过早优化带来的复杂度。