原創(chuàng)聲明:本文為作者原創(chuàng),未經(jīng)允許不得轉(zhuǎn)載,經(jīng)授權(quán)轉(zhuǎn)載需注明作者和出處
在一個比較成熟微信小程序中,為了統(tǒng)計每個頁面的行為,如統(tǒng)計頁面PV、UV、對頁面元素點擊等事件進(jìn)行監(jiān)聽,并且上報到我們自己的數(shù)據(jù)統(tǒng)計服務(wù)器上,目前網(wǎng)上能找到的大部分方案是通過手動埋點的方式實現(xiàn),這種方式效率較低,來一個頁面就要加一個統(tǒng)計邏輯,對代碼的侵入較多。且網(wǎng)上的方案都是基于原生微信小程序的解決方案,對于使用Taro進(jìn)行開發(fā)的項目來說,有點力不從心,因此,通過一段時間的研究與實驗,整理出這篇文章,用于梳理在使用Taro如何實現(xiàn)無侵入或低侵入(只需要在app.tsx中調(diào)用一個方法即可實現(xiàn)對所有的頁面生命周期進(jìn)行監(jiān)聽)。
taro-track
歡迎star,如有任何疑問,歡迎提ISSUES進(jìn)行討論
? 我們要想要實現(xiàn)無侵入或低侵入的監(jiān)控頁面聲明周期函數(shù),對于原生的微信小程序,我們可以參考網(wǎng)上的一個現(xiàn)有解決方案:小程序從手動埋點到自動埋點。
? 其實現(xiàn)原理主要是:通過代理微信小程序的Page
方法,在用戶傳遞進(jìn)來的生命周期鉤子函數(shù)外層包裝一層wrapper
函數(shù),并在wrapper
函數(shù)中實現(xiàn)統(tǒng)一數(shù)據(jù)上報的邏輯,然后再調(diào)用用戶定義的聲明周期鉤子函數(shù),這樣,使用者便可以在無感知的情況下進(jìn)行編碼,所有的數(shù)據(jù)收集與上報操作都可以在這個wrapper
函數(shù)中執(zhí)行。
? 然而,上述方案僅適用于原生微信小程序,在基于Taro開發(fā)的微信小程序項目中,由于在Taro中所有單元都是組件Component
,而非Page
,經(jīng)過本人的反復(fù)試驗,Taro在運行的過程中,并沒有調(diào)用過Page
方法,因此,通過代理微信原生Page
方法這條路是行不通了。
? 那么,既然在Taro
中一切皆組件,我們能不能通過代理Component
實現(xiàn)類似的邏輯呢?經(jīng)過試驗,這個想法是可行的,不過由于Component
的生命周期鉤子跟Page
的生命周期鉤子不一樣,所以我們需要對其做一定的轉(zhuǎn)化。
//// core/wx-tools.ts
/**
* 獲取微信原生Page
* @returns {WechatMiniprogram.Page.Constructor}
*/
export function getWxPage():WechatMiniprogram.Page.Constructor {
return Page;
}
/**
* 重寫微信原生Page
* @param newPage
*/
export function overrideWxPage(newPage: any):void {
Page = newPage;
}
/**
* 獲取微信原生App
* @returns {WechatMiniprogram.App.Constructor}
*/
export function getWxApp():WechatMiniprogram.App.Constructor {
return App;
}
/**
* 重寫微信原生App
* @param newApp
*/
export function overrideWxApp(newApp: any): void {
App = newApp;
}
/**
* 獲取微信原生Component
* @returns {WechatMiniprogram.Component.Constructor}
*/
export function getWxComponent():WechatMiniprogram.Component.Constructor {
return Component;
}
/**
* 重寫微信原生Component
* @param newComponent
*/
export function overrideWxComponent(newComponent: any): void {
Component = newComponent;
}
//// overrideWxPage.ts
import { getWxComponent, getWxPage, overrideWxComponent, overrideWxPage } from '@kiner/core/es';
// 需要代理的生命周期鉤子,包含Page和Component的鉤子
const proxyMethods = [
"onShow",
"onHide",
"onReady",
"onLoad",
"onUnload",
"created",
"attached",
"ready",
"moved",
"detached",
];
// 觸發(fā)鉤子的回調(diào)函數(shù)中的初始化參數(shù)
export interface OverrideWechatPageInitOptions {
__route__?: string
__isPage__?: boolean
[key:string]: any
}
// 觸發(fā)鉤子是調(diào)用的回調(diào)函數(shù)類型
export type OverrideWechatPageHooksCb = (method: string, options: OverrideWechatPageInitOptions)=>void;
// 用于存儲所有的回調(diào)函數(shù)
const pageHooksCbs: OverrideWechatPageHooksCb[] = [];
export class OverrideWechatPage {
// 微信原生Page方法
private readonly wechatOriginalPage: WechatMiniprogram.Page.Constructor;
// 微信原生Component方法
private readonly wechatOriginalComponent: WechatMiniprogram.Component.Constructor;
// 是否使用taro框架
private readonly isTaro = true;
public constructor(isTaro:boolean=true) {
this.isTaro = true;
// 基于以后可能需要兼容頭條、百度小程序需要,將所有操作原生微信小程序的操都獨立抽離到單獨的模塊中進(jìn)行維護(hù),
// 若以后需要兼容其他小程序,只需要在蓋某塊內(nèi)部進(jìn)行api動態(tài)指定切換即可
// 保存微信原始Page對象,以便我們在銷毀時恢復(fù)原狀
this.wechatOriginalPage = getWxPage();
// 保存微信原始Component對象,以便我們在銷毀時恢復(fù)原狀
this.wechatOriginalComponent = getWxComponent();
}
public initialize(pageHooksCb: OverrideWechatPageHooksCb): void {
const _Page = getWxPage();
const _Component = getWxComponent();
// 將回調(diào)函數(shù)放入隊列中,在觸發(fā)原生生命周期鉤子時依次調(diào)用
pageHooksCbs.push(pageHooksCb);
console.info(`原始Page對象`, pageHooksCbs, this.wechatOriginalPage);
const self = this;
// 根據(jù)是否使用Taro框架篩選需要代理的鉤子函數(shù)
// 若使用Taro則需代理組件的生命周期鉤子,若使用原生小程序則代理Page的生命周期鉤子
const needProxyMethods = proxyMethods.filter(item=>this.isTaro?!item.startsWith('on'):item.startsWith('on'));
/**
* 實現(xiàn)代理Page|Component的邏輯
* @param {OverrideWechatPageInitOptions} options
* @returns {string}
*/
const wrapper = function(options: OverrideWechatPageInitOptions){
needProxyMethods.forEach(methodName=>{
// 緩存用戶定義的生命周期鉤子
const _originalHooks = options[methodName];
const wrapperMethod = function (...args: any[]) {
// 依次觸發(fā)頁面生命周期回調(diào)
pageHooksCbs.forEach((fn: OverrideWechatPageHooksCb)=>fn(methodName, options));
// 若用戶有定義該生命周期鉤子則執(zhí)行這個鉤子函數(shù)
return _originalHooks&&_originalHooks.call(this, ...args);
};
// 重寫options,用新的包裝函數(shù)覆蓋原始鉤子函數(shù)
options = {
...options,
[methodName]:wrapperMethod
};
});
// 使用新的options進(jìn)行初始化操作
let res = "";
if(self.isTaro){
res = _Component(options);
}else{
_Page(options);
}
// 由于在Taro中,一切皆組件,我們需要知道當(dāng)前組件是頁面組件還是普通組件
// 微信小程序原生的Component執(zhí)行構(gòu)造函數(shù)后會直接返回當(dāng)前組件的路徑,如:pages/index/index
// 因此,我們可以將這個路徑保存在我們的wrapper中,方便我們在外部判斷當(dāng)前組件是否是頁面組件
options.__router__ = wrapper.__route__ = res;
options.__isPage__ = res.startsWith('pages/');
console.info(`重寫微信小程序Page對象`, options, res);
return res;
};
wrapper.__route__ = '';
wrapper.__isPage__ = false;
// 重寫微信原生Page|Component
if(this.isTaro){
overrideWxComponent(wrapper);
}else{
overrideWxPage(wrapper);
}
}
/**
* 重置微信原生方法
*/
public destroy(): void {
overrideWxPage(this.wechatOriginalPage);
overrideWxComponent(this.wechatOriginalComponent);
}
}
//// entry.ts
/**
* 初始化微信小程序生命周期監(jiān)聽
* @param {string | undefined} baseUrl 發(fā)送的日志服務(wù)器,默認(rèn)為生產(chǎn)服務(wù)
* @param {TransporterType} transporter 采用的上傳通道方案是elk還是console
* @param {string | undefined} appVersion 當(dāng)前小程序版本
* @param {string | undefined} appName 當(dāng)前小程序的名稱
* @param {boolean} showLog 發(fā)送成功是否打印日志
* @param {number} pstInterval applet-pst事件循環(huán)上報時間間隔,默認(rèn)為:5000
* @param extraData 額外參數(shù),sdk中無法直接獲取的字段,如appid等
* @param {{[p: string]: string}} extraData
*/
export function initAppletLifecycleListener(
{baseUrl,
isTaro,
transporter,
appVersion,
appName,
showLog = false,
pstInterval = 5000
}: InitAppletLifecycleOption,
extraData: { [key: string]: string } = {}
){
const logger = Logger.create('initAppletLifecycleListener');
const logStyle = 'background: green; color: #FFFFFF; padding: 5px 10px;';
const tpr = initTransporter(transporter, {
baseUrl: baseUrl,
query: {
app_name: appName,
app_version: appVersion,
ev_type: 'client_ub'
}
});
let timer = null;
const overrideWechatPage = new OverrideWechatPage(isTaro);
const prevUrl = getWxCurrentHref();
// 對頁面的onLoad和onReady進(jìn)行監(jiān)聽
overrideWechatPage.initialize(async (methodName: string, options) => {
if (!options.__isPage__) {
return;
}
// const hooksName = CompAndPageHookMap[methodName];
console.log(`dolphin-wx/entry:${methodName}-${CompAndPageHookMap[methodName]}`);
const openTime = Date.now();
const baseExtFields = getBaseExtFields(extraData);
const baseFields = await getBaseFields(extraData);
const extraExt = extraData.ext || {};
function sendPv() {
const now = Date.now();
const sendData = {
ev: 'applet-pv',
...baseFields,
...extraData,
time: now,
ext: {
...baseExtFields,
...extraExt,
time: now - openTime
}
};
tpr.send(sendData, () => showLog && logger.info(`%capplet-pv上報成功:`, logStyle, sendData));
}
function sendPst() {
const now = Date.now();
const sendPstData = {
ev: 'applet-pst',
...baseFields,
...extraData,
time: now,
ext: {
...baseExtFields,
...extraExt,
time: now - openTime,
url: getWxCurrentHref()
}
};
tpr.send(sendPstData, () => showLog && logger.info(`%capplet-pst上報成功:`, logStyle, sendPstData));
}
function sendPvOut() {
const now = Date.now();
const sendPvOutData = {
ev: 'applet-pvout',
...baseFields,
...extraData,
time: now,
pl: baseFields.url,
ext: {
...baseExtFields,
...extraExt,
time: now - openTime,
url: baseFields.url
}
};
tpr.send(sendPvOutData, () => showLog && logger.info(`%capplet-pvout上報成功:`, logStyle, sendPvOutData));
}
// console.log(`dolphin-wx/entry[${methodName}]`);
switch (methodName) {
case proxyWxLifeHooks.onReady:
case proxyWxLifeHooks.ready:
// 若觸發(fā)onLoad或attached時當(dāng)前url與緩存的url不一樣,說明發(fā)生頁面跳轉(zhuǎn),觸發(fā)pvout
if(prevUrl&&prevUrl!==getWxCurrentHref()){
sendPvOut();
}
sendPv();
timer = setInterval(() => {
sendPst();
}, pstInterval);
break;
case proxyWxLifeHooks.onUnload:
case proxyWxLifeHooks.detached:
sendPvOut();
break;
}
});
}
近期有一些朋友詢問我這種方法是否真實可行,再次說明一下,經(jīng)過本人在公司項目中實際應(yīng)用,證實是可行的,雖然不算相當(dāng)完善,但是是完全能達(dá)到我們的目的的。
注:由于taro是第三方框架,版本升級不可控,因此,我們只是對特定個版本,如2.1.5進(jìn)行兼容,其他版本尚未做兼容處理
本項目已經(jīng)將核心功能抽離到github中,有興趣的朋友可以去看一下,歡迎star和issue
taro-track