说明
由于我是一个有着一颗玻璃心的博主,导致在2018年后博客很少更新。原因是由于我的分享并没有解决到部分人的问题,而导致被骂了。当时这颗玻璃心就碎了,所以这两年以来很是消极,博客很少更新。这里给那些关注我,支持我的朋友说声【对不起】!前段时间,看了一个工作两年时间博主的 2021 年 flag,突然回首,还有很多记忆。所以,我决定以后每周最少一篇博客,记录我的学习和成长。谢谢!
需求场景
小程序开发完成,接到需求:需要对小程序的所有页面【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. 方案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,减少性能消耗!