微信小程序的自动埋点
发表时间:2021-1-5
发布人:葵宇科技
浏览次数:60
在做各种各样的业务时,我们不可避免的需要在业务中进行埋点,这些埋点通常包含但不限于曝光、点击、停留时长、离开页面等场景,而在小程序中因为其和浏览器不同的架构,导致了监听页面变的更加困难,通常我们都会通过重写 Page
方法来达到对小程序原生生命周期的拦截代理,从而进行业务埋点,但是在 Taro
中这一切变得不同了。
现状
在多端统一的 Taro
中,我们不再能看到显式的 Page
调用,甚至 Taro
打包之后的代码里也不再存在任何 Page
的迹象,取而代之的则是小程序原生的 Component
(这一点大家通过观察打包后的内容可以得知),所以为了实现微信小程序在 Taro
中的自动埋点,我们需要换一个策略:重写 Component
。
基本的重写
在微信小程序中,其暴露的 Component
和 Page
能够直接被重写并进行赋值:
const _originalComponent = Component;
const wrappedComponent = function (options) {
...do something before real Component
return _originalComponent(options);
}
复制代码
这样可以很快的解决问题,但是当我们在另一个小程序做这件事情的时候,我们就又需要手动做一次这些处理,难免有些麻烦,为什么不找一个更通用的方案,我们只用关注我们需要关注的业务(埋点)就行了呢?
解决方案
重中之重,从零开始思考,掌握真正问题,接近问题本质
根问题
在解决问题之前,不如让我们先看看这个问题的本质是什么。想在小程序中进行自动的埋点,其实要做的就是在小程序指定的生命周期里做一些固定的处理,所以我们自动埋点的问题实际上是如何劫持小程序的生命周期,而要劫持小程序的生命周期,我们需要做的就是去重写 options
。
如何解决
在解决这个问题之前,我们要把自己需要解决的问题拆分出来:
options
options
我们在上面的基础解决办法对如何重写 options
就已经有了答案,我们只需要在原小程序提供的方法外再包裹一层即可解决,同时为了保证我们的解决方案能适用于原生小程序和 Taro
这种多端统一的小程序方案,我们应该同时支持重写 Component
和 Page
,而对于最后一个问题,我们可以思考一下 js
中的事件系统,相似的我们也可以实现一套发布订阅的逻辑,只需要定制触发事件(生命周期)和 listeners
,再针对生命周期原有逻辑进行包装即可;
step 1
首先我们在重写 Component
和 Page
之前应当保存原始的方法,避免原始方法被污染我们无法回退,这之后再去将小程序中的所有生命周期进行枚举生成一个默认的事件对象中,保证我们在注册了对应生命周期的 listeners
后能通过寻址找到并对原生命周期方法进行重写。
export const ProxyLifecycle = {
ON_READY: 'onReady',
ON_SHOW: 'onShow',
ON_HIDE: 'onHide',
ON_LOAD: 'onLoad',
ON_UNLOAD: 'onUnload',
CREATED: 'created',
ATTACHED: 'attached',
READY: 'ready',
MOVED: 'moved',
DETACHED: 'detached',
SHOW: 'show',
HIDE: 'hide',
RESIZE: 'resize',
};
public constructor() {
this.initLifecycleHooks();
this.wechatOriginalPage = getWxPage();
this.wechatOriginalComponent = getWxComponent();
}
// 初始化所有生命周期的钩子函数
private initLifecycleHooks(): void {
this.lifecycleHooks = Object.keys(ProxyLifecycle).reduce((res, cur: keyof typeof ProxyLifecycle) => {
res[ProxyLifecycle[cur]] = [] as WeappLifecycleHook[];
return res;
}, {} as Record);
}
复制代码
step 2
在这一步我们只需要将监听函数放到我们第一步中声明的事件对象中,然后执行重写流程即可:
public addLifecycleListener(lifeTimeOrLifecycle: string, listener: WeappLifecycleHook): OverrideWechatPage {
// 针对指定周期定义Hooks
this.lifecycleHooks[lifeTimeOrLifecycle].push(listener);
const _Page = this.wechatOriginalPage;
const _Component = this.wechatOriginalComponent;
const self = this;
const wrapMode = this.checkMode(lifeTimeOrLifecycle);
const componentNeedWrap = ['component', 'pageLifetimes'].includes(wrapMode);
const wrapper = function wrapFunc(options: IOverrideWechatPageInitOptions): string | void {
const optionsKey = wrapMode === 'pageLifetimes' ? 'pageLifetimes' : '';
options = self.findHooksAndWrap(lifeTimeOrLifecycle, optionsKey, options);
const res = componentNeedWrap ? _Component(options) : _Page(options);
options.__router__ = (wrapper as any).__route__ = res;
return res;
};
(wrapper as any).__route__ = '';
if (componentNeedWrap) {
overrideWxComponent(wrapper);
} else {
overrideWxPage(wrapper);
}
return this;
}
/**
* 为对应的生命周期重写options
* @param proxyLifecycleOrTime 需要拦截的生命周期
* @param optionsKey 需要重写的 optionsKey,此处用于 lifetime 模式
* @param options 需要被重写的 options
* @returns {IOverrideWechatPageInitOptions} 被重写的options
*/
private findHooksAndWrap = (
proxyLifecycleOrTime: string,
optionsKey = '',
options: IOverrideWechatPageInitOptions,
): IOverrideWechatPageInitOptions => {
let processedOptions = { ...options };
const hooks = this.lifecycleHooks[proxyLifecycleOrTime];
processedOptions = OverrideWechatPage.wrapLifecycleOptions(proxyLifecycleOrTime, hooks, optionsKey, options);
return processedOptions;
};
/**
* 重写options
* @param lifecycle 需要被重写的生命周期
* @param hooks 为生命周期添加的钩子函数
* @param optionsKey 需要被重写的optionsKey,仅用于 lifetime 模式
* @param options 需要被重写的配置项
* @returns {IOverrideWechatPageInitOptions} 被重写的options
*/
private static wrapLifecycleOptions = (
lifecycle: string,
hooks: WeappLifecycleHook[],
optionsKey = '',
options: IOverrideWechatPageInitOptions,
): IOverrideWechatPageInitOptions => {
let currentOptions = { ...options };
const originalMethod = optionsKey ? (currentOptions[optionsKey] || {})[lifecycle] : currentOptions[lifecycle];
const runLifecycleHooks = (): void => {
hooks.forEach((hook) => {
if (currentOptions.__isPage__) {
hook(currentOptions);
}
});
};
const warpMethod = runFunctionWithAop([runLifecycleHooks], originalMethod);
currentOptions = optionsKey
? {
...currentOptions,
[optionsKey]: {
...options[optionsKey],
...(currentOptions[optionsKey] || {}),
[lifecycle]: warpMethod,
},
}
: {
...currentOptions,
[lifecycle]: warpMethod,
};
return currentOptions;
};
复制代码
经过如上两步,我们就能对指定的生命周期进行劫持并注入我们自己的 listeners
,使用被重写过 Component
或者 Page
就会自动触发这些 listeners
。
weapp-lifecycle-hook-plugin
为了方便直接对微信小程序原生环境和 Taro
等多端统一方案进行这一套通用的解决方案,我实现了一个插件来解决这个问题(私心安利)
安装
npm install weapp-lifecycle-hook-plugin
或者
yarn add weapp-lifecycle-hook-plugin
复制代码
使用
import OverrideWechatPage, { setupLifecycleListeners, ProxyLifecycle } from 'weapp-lifecycle-hook-plugin';
// 供 setupLifecycleListeners 使用的 hook 函数,接受一个参数,为当前组件/页面的options
function simpleReportGoPage(options: any): void {
console.log('goPage', options);
}
// setupListeners
class App extends Component {
constructor(props) {
super(props);
}
componentWillMount() {
// ...
// 手动创建的实例和使用 setupLifecycleListeners 创建的实例不是同一个,所以需要销毁时需要单独对其进行销毁
// 直接调用实例方式
const instance = new OverrideWechatPage(this.config.pages);
// 直接调用实例上的 addListener 方法在全局增加监听函数,可链式调用
instance.addLifecycleListener(ProxyLifecycle.SHOW, simpleReportGoPage);
// setupListeners 的使用
setupLifecycleListeners(ProxyLifecycle.SHOW, [simpleReportGoPage], this.config.pages);
// ...
}
// ...
}
复制代码
只需要通过简单地 setup
就能解决以前需要手动书写一大堆的重写逻辑,何乐而不为呢 :stuck_out_tongue_closed_eyes: