用户
 找回密码
 立即注册

QQ登录

只需一步,快速开始

扫一扫,登录网站

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

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

Rolan 2021-2-23 18:00

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

需求场景

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

需求分析

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

需求解决

  1. 解决多页面生命周期埋点----重写页面生命周期:
    1.1 重写 Page 对象的传入对象,也就是【微信小程序之页面拦截器】的方法;
    1.2 重写 Page 对象本身,就是【 微信小程序–页面劫持】的方法;
  2. 解决多页面引入重写文件的方法:
    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
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46

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

1.2.1 全局引入 app.js
// 引入页面传入对象处理方法
const hijack_page_object = require('./utils/hijack_page_object')
// App 中注册为全局方法
App({
	hijack_page_object 
})
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
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
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
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
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
1.2.4 引入当前代码的输出(index.js)
当前页面生命周期!
全局页面生命周期!
页面生命周期埋点
  • 1
  • 2
  • 3
1.2.5 总结

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

2. 方案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
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42

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
})
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
2.2.2 页面使用 hijack_page (index.js)
// 引入 Page 
const app = getApp();
const { Page } = app.hijack_page;
// 使用 Page 
Page({
	onLoad(){
	    console.log('当前页面生命周期!')
	}
})
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
2.2.3 当前方案代码输出(index.js)
当前页面生命周期!
全局页面生命周期!
页面生命周期埋点
  • 1
  • 2
  • 3
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;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199

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;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

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 
})
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
3.3.2 index.js 使用
Page({
	onLoad(){
	    console.log('当前页面生命周期!')
	}
})
  • 1
  • 2
  • 3
  • 4
  • 5
3.3.3 index.js页面输出
全局
当前页面生命周期!
  • 1
  • 2

4. 总结

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

5. 注意

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

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

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

// 正确示范
let $state = getApp().store.getState()
  • 1
  • 2
  • 3
  • 4
  • 5

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

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

// 正确示范
getApp().store.setState({msg: 'Hello Index!'})
  • 1
  • 2
  • 3
  • 4
  • 5

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})
		}
	}
})
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

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

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