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

说明

由于我是一个有着一颗玻璃心的博主,导致在2018年后博客很少更新。原因是由于我的分享并没有解决到部分人的问题,而导致被骂了。当时这颗玻璃心就碎了,所以这两年以来很是消极,博客很少更新。这里给那些关注我,支持我的朋友说声【对不起】!前段时间,看了一个工作两年时间博主的 2021 年 flag,突然回首,还有很多记忆。所以,我决定以后每周最少一篇博客,记录我的学习和成长。谢谢!

需求场景

小程序开发完成,接到需求:需要对小程序的所有页面【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 全局引入或者单页面引入
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. 方案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 】

6. 参考

  1. 微信小程序之页面拦截器
  2. 微信小程序–页面劫持
  3. 微信小程序全局状态管理库——wxMiniStore

7. WXRUI体验二维码

WXRUI体验码

如果文章对你有帮助的话,请打开微信扫一下二维码,点击一下广告,支持一下作者!谢谢!

8. 其他

我的博客,欢迎交流!

我的CSDN博客,欢迎交流!

微信小程序专栏

前端笔记专栏

微信小程序实现部分高德地图功能的DEMO下载

微信小程序实现MUI的部分效果的DEMO下载

微信小程序实现MUI的GIT项目地址

微信小程序实例列表

前端笔记列表

游戏列表

  • 8
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
微信小程序是一种基于微信平台的应用的开发模式,可以快速的开发出符合用户需求的小程序。在小程序的开发中,组件是一个非常重要的概念,通过组件可以实现复用性和模块化编程思想。 组件应用是小程序开发的基础。通过组件可以将某一模块化并封装起来,使得组件可以在不同的页面间得到复用,大大提升了开发效率并减少了代码冗余。微信小程序提供了丰富的自带组件,包括文本、图片、按钮、输入框等等,开发者也可以自己开发组件来满足自己的需求。实际开发中,通过组件可以快速搭建页面框架和业务逻辑。 Demo是一个演示小程序的示例程序。在小程序的实际开发过程中,一个好的Demo非常重要。通过Demo,开发人员可以更深入的了解小程序的开发流程、组件的应用和实际的业务开发等等。在Demo中,通常会包括小程序的一些基础操作,如页面跳转、数据绑定、组件的使用等。而在实际开发中,Demo还会包括一些复杂的业务场景,如支付、登录、数据列表展示等等。Demo不仅为开发者提供了学习和实践的机会,也方便了使用者了解该小程序的功能和特点。 总之,微信小程序组件的应用和Demo的开发都是小程序开发过程中非常重要的两个部分。良好的组件应用和精心设计的Demo,可以在极短的时间内实现小程序开发。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Rattenking

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值