用户
 找回密码
 立即注册

QQ登录

只需一步,快速开始

扫一扫,登录网站

小程序社区 首页 教程 实战教程 查看内容

微信小程序----全局状态管理 (便于全局埋点等操作)

Rolan 2021-2-23 18:12

小程序开发完成,接到需求:需要对小程序的所有页面【onLoad】生命周期进行埋点,对页面中的点击事件进行埋点。

需求场景

小程序开发完成,接到需求:需要对小程序的所有页面【onLoad】生命周期进行埋点,对页面中的点击事件进行埋点。

需求分析

  • 全部页面生命周期和点击事件的埋点,埋点多;
  • 每个页面引入埋点文件,不利于后期维护。

需求解决

解决多页面生命周期埋点----重写页面生命周期:

1.1 重写 Page 对象的传入对象,也就是【微信小程序之页面拦截器】的方法;
1.2 重写 Page 对象本身,就是【 微信小程序–页面劫持】的方法;
解决多页面引入重写文件的方法:

2.1 重写 Page 对象本身,或者重写 App.Page 对象,方案:【 微信小程序全局状态管理库(wxMiniStore)】

1. 方案1:劫持 Page 的传入对象

1.1 hijack_page_object.js 代码

/** * hijack_page_object 页面对象劫持 * options 对象传入参数 */ const hijack_page_object = (options = {}) => { const { onLoad, onUnload } = options; options = { ...options, collectClick(opts){ // 页面点击埋点 console.log('页面点击埋点') // 点击埋点逻辑 }, collectPage(opts){ // 页面生命周期埋点 console.log('页面生命周期埋点') // 生命周期埋点逻辑 }, jumpNextPage(url){ // 全局页面跳转方法 wx.navigateTo({url}) // 埋点跳转点击 this.collectClick({}) }, onLoad(opts){ onLoad && onLoad.call(this, opts) console.log('全局页面生命周期!') // 埋点 this.collectPage({ "lifeCycle": "onLoad", "loadTime": +new Date() }) }, onUnload(){ onUnload && onUnload.call(this) // 埋点 this.collectPage({ "lifeCycle": "onUnload", "loadTime": +new Date() }) } } return options; } module.exports = hijack_page_object;

1.2 全局引入或者单页面引入

1.2.1 全局引入 app.js

// 引入页面传入对象处理方法 const hijack_page_object = require('./utils/hijack_page_object') // App 中注册为全局方法 App({ hijack_page_object })

1.2.2 页面使用 hijack_page_object 方法(index.js)

// 引入 hijack_page_object const app = getApp(); const { hijack_page_object } = app; // 使用 hijack_page_object Page(hijack_page_object({ onLoad(){ console.log('当前页面生命周期!') } }))

1.2.3 单页面对 hijack_page_object.js 的引入和使用(index.js)

// 引入 hijack_page_object.js const hijack_page_object = require('../utils/hijack_page_object') // 使用 hijack_page_object Page(hijack_page_object({ onLoad(){ console.log('当前页面生命周期!') } }))

1.2.4 引入当前代码的输出(index.js)

  • 当前页面生命周期!
  • 全局页面生命周期!
  • 页面生命周期埋点

1.2.5 总结

方案 1 的两种引入方式比较,全局引入比较快捷,一次引入,其他页面直接使用app里的变量访问即可;单页面引入不方便维护,代码冗余!建议多频率使用的方法等直接在app.js中注册!


方案2:重写 Page 对象

2.1 hijack_page.js 代码

let _Page = Page; Page = (options) => { const { onLoad, onUnload } = options; options = { ...options, collectClick(opts){ // 页面点击埋点 console.log('页面点击埋点') // 点击埋点逻辑 }, collectPage(opts){ // 页面生命周期埋点 console.log('页面生命周期埋点') // 生命周期埋点逻辑 }, jumpNextPage(url){ // 全局页面跳转方法 wx.navigateTo({url}) // 埋点跳转点击 this.collectClick({}) }, onLoad(opts){ onLoad && onLoad.call(this, opts); console.log('全局页面生命周期!') this.collectPage({ "lifeCycle": "onLoad", "loadTime": +new Date() }); }, onUnload(){ onUnload && onUnload.call(this); this.collectClick({ "lifeCycle": "onUnload", "stayTime": +new Date() - this._enterTime }); } } _Page(options) } module.exports = { Page }

2.2 hijack_page 的使用

2.2.1 全局引入 hijack_page (app.js)

// 引入 hijack_page const hijack_page = require('./utils/hijack_page') // 注册 hijack_page App({ hijack_page })

2.2.2 页面使用 hijack_page (index.js)

// 引入 Page const app = getApp(); const { Page } = app.hijack_page; // 使用 Page Page({ onLoad(){ console.log('当前页面生命周期!') } })

2.2.3 当前方案代码输出(index.js)

  • 当前页面生命周期!
  • 全局页面生命周期!
  • 页面生命周期埋点


2.2.4总结

对比方案1和方案2,发现直接重写 Page 比 劫持传入 Page 的对象在使用时方便很多!

3. 方案3:重写 App.Page

3.1 proxyStore.js 代码

const { TYPE_OBJECT, _typeOf, _deepClone, _isObjEqual } = require('./util'); let $state = Symbol('$state'), $openPart = Symbol('$openPart'), $behavior = Symbol('$behavior'), $methods = Symbol('$methods'), $pageLife = Symbol('$pageLife'), $pageListener = Symbol('$pageListener'), $nonWritable = Symbol('$nonWritable'), $stack = Symbol('$stack'), $debug = Symbol('$debug'); class ProxyStore{ constructor(opts){ // 初始化数据 this.initData(opts); // 初始化页面周期数组 this.initPageLife(); // 重写 Page 对象 this.rewritePage(); // 重写 Component 对象 this.rewriteComponent(); } initData(opts){ const { openPart = false, behavior, methods = {}, pageLisener = {}, pageListener, nonWritable = false, debug = true, } = opts; if(_typeOf(opts.state) === TYPE_OBJECT){ this[$state] = _deepClone(opts.state); } this[$openPart] = openPart; this[$behavior] = behavior; this[$methods] = methods; this[$pageListener] = pageListener || pageLisener; this[$nonWritable] = nonWritable; this[$debug] = debug; this[$stack] = []; } initPageLife(){ this[$pageLife] = [ "data", "onLoad", "onShow", "onReady", "onHide", "onUnload", "onPullDownRefresh", "onReachBottom", "onShareAppMessage", "onPageScroll", "onTabItemTap", ] } created(page){ !this[$stack].some(cur => cur === page) && this[$stack].push(page); page.watch && this.watch(page) if(!_isObjEqual(page.data.$state, this[$state])){ page.setData({$state: this[$state]}) } } destroy(page){ let index = this[$stack].findIndex(cur => cur === page); ~index && this[$stack].splice(index, 1); } watch(page){ page.data = new Proxy(page.data,{ set(target, key, value, receiver){ page.watch && page.watch[key] && page.watch[key].call(page, value); return Reflect.set(target, key, value, receiver); }, get(target, key, receiver){ return Reflect.get(target, key, receiver); } }) } rewritePage(){ const _Page = Page; const _this = this; App.Page = (options = {}, ...args) => { const { onLoad, onUnload } = options; options = { ...options, data: { ...(options.data || {}), $state: _this[$state] }, ...(_this[$methods] || {}), onLoad(opts){ _this.created(this) onLoad && onLoad.call(this,opts) }, onUnload(){ _this.destroy(this) onUnload && onUnload.call(this) } } Object.keys(_this[$pageListener]).forEach(key => { if(typeof _this[$pageListener][key] === "function" && _this[$pageLife].some((item) => item === key)){ const lifeName = options[key]; options = { ...options, [key](opts){ let globalValue = _this[$pageListener][key].call(this, opts); let pageValue = lifeName && lifeName.call(this, opts); return pageValue || globalValue; } } } }) _Page(options, ...args) } if (!this[$nonWritable]) { try { Page = App.Page; } catch (e) {} } } rewriteComponent(){ const _Component = Component; const _this = this; App.Component = (options = {}, ...args) => { const { lifetimes = {} } = options; let attached = lifetimes.attached || options.attached, detached = lifetimes.detached || options.detached; options = { ...options, data: { ...(options.data || {}), $state: _this[$state] } } Object.keys(_this[$methods]).forEach(key => { if(typeof _this[$methods][key] === "function" && !_this[$pageLife].some((item) => item === key)){ options.methods || (options.methods = {}) const lifeName = options.methods[key]; options.methods[key] = function(opts){ _this[$methods][key].call(this, opts); lifeName && lifeName.call(this,opts); } } }) let attachednew = function(){ _this.created(this) attached && attached.call(this) } let detachednew = function(){ _this.destroy(this) detached && detached.call(this) } if(options.lifetimes && _typeOf(options.lifetimes) === TYPE_OBJECT){ options.lifetimes.attached = attachednew; options.lifetimes.detached = detachednew; } else { options.attached = attachednew; options.detached = detachednew; } _Component(options, ...args) } if (!this[$nonWritable]) { try { Component = App.Component; } catch (e) {} } } getState() { return _deepClone(this[$state]); } setState(obj, fn = () => {}) { if (_typeOf(obj) !== TYPE_OBJECT) throw new Error("setState的第一个参数须为object!"); let prev = this[$state]; let current = { ..._deepClone(prev), ..._deepClone(obj) }; this[$state] = current; if(this[$stack].length){ let props = this[$stack].map(page => { return new Promise((resolve,reject) => { page.setData({$state: current}, resolve) }) }) Promise.all(props).then(fn); }else{ fn(); } } } module.exports = ProxyStore;

3.2 util.js 基础方法js代码

const util = { TYPE_ARRAY: "[object Array]", TYPE_OBJECT: "[object Object]", _typeOf(value){ return Object.prototype.toString.call(value) }, _deepClone(obj){ return JSON.parse(JSON.stringify(obj)) }, _isEmptyObject(obj){ if(util._typeOf(obj) !== util.TYPE_OBJECT) throw new Error(`传入值不是对象!`); for(let key in obj){ return false; } return true }, _isObjEqual(o1,o2){ var props1 = Object.getOwnPropertyNames(o1); var props2 = Object.getOwnPropertyNames(o2); if (props1.length != props2.length) { return false; } for (var i = 0,max = props1.length; i < max; i++) { var propName = props1[i]; if (o1[propName] !== o2[propName]) { return false; } } return true; } } module.exports = util;

3.3 使用 ProxyStore

3.3.1 app.js 注册

// 引入 ProxyStore const ProxyStore = require('./store/proxyStore'); // 声明 let store = new ProxyStore({ state: { msg: 'Hello World!' }, methods: { jumpNextPage(url){ wx.navigateTo({url}) } }, pageListener: { onLoad(){ console.log('全局') } } }) // app.js注册 App({ store })

3.3.2 index.js 使用

Page({ onLoad(){ console.log('当前页面生命周期!') } })

3.3.3 index.js页面输出

全局

当前页面生命周期!

4. 总结

方案3 采用的是【 微信小程序全局状态管理库——wxMiniStore】的方法,方案可以对全局状态进行管理,同时页面可以使用watch 监听变量的修改!对比三种方案,方案三使用最简单,如果不需要那么多功能,可以删除不需要的代码!

5. 注意

方案三基本使用的是【微信小程序全局状态管理库——wxMiniStore】,但是做了自定义调整,调整如下:

5.1 获取全局状态必须使用 getState() 获取 $state 对象;

// 错误示范【这样是获取不到$state对象的】 let $state = getApp().store.$state // 正确示范 let $state = getApp().store.getState()

5.2 设置全局状态必须使用setState(Object);

// 错误示范【这样是更新不到$state对象的】 getApp().store.$state.msg = 'Hello Index!' // 正确示范 getApp().store.setState({msg: 'Hello Index!'})

5.3 watch 监听必须是 this.data 改变的变量;

// 错误示范【使用 this.setData 监听不到修改】 Page({ onLoad(){ this.setData({goodsList: [1,2,3,4,5,6]}) }, watch: { goodsList(val){ console.log(val) this.setData({goodsList: val}) } } }) // 正确示范 Page({ onLoad(){ this.data.goodsList = [1,2,3,4,5,6] }, watch: { goodsList(val){ console.log(val) this.setData({goodsList: val}) } } })

注意: 如果页面没有 watch 对象,页面并不会执行变量的监听,所以在不需要监听时,尽量不要 watch,减少性能消耗!

5.4 未开发的 wxMiniStore 功能:【开启局部模式、设置范围、useProp 】

鲜花
鲜花
鸡蛋
鸡蛋
分享至 : QQ空间
收藏