Uniapp微信小程序?qū)崿F(xiàn)全局事件監(jiān)聽并進行數(shù)據(jù)埋點的方法
零、前言
最近接到需求,領(lǐng)導(dǎo)希望使用微信開放平臺上免費的We分析進行數(shù)據(jù)埋點,但又不希望在現(xiàn)有uniapp開發(fā)的微信小程序代碼上做侵入式修改,筆者奉命進行了技術(shù)調(diào)研,考慮通過劫持事件的方式來實現(xiàn)捕獲特定事件并上傳分析平臺的功能。
需要特別注意的是,微信小程序是不能得到document對象的,$el上掛載的也是undefined,自然也就不能通過全局addEventListener的方式來監(jiān)聽特定事件。在調(diào)研中想到可以通過劫持小程序的自定義組件構(gòu)造器Component()來實現(xiàn)事件的監(jiān)聽。
為了便于理解,部分?jǐn)?shù)據(jù)結(jié)構(gòu)通過TypeScript接口形式進行描述。
一、軟件環(huán)境
- HbuilderX 3.4.7.20220422
- 微信開發(fā)者工具 Stable 1.05.2203070
- 小程序基礎(chǔ)庫版本 2.24.4 [749]
二、相關(guān)分析及實現(xiàn)
uniapp編譯微信小程序時對于事件的處理分析
部分知識via掘金:http://www.dbjr.com.cn/article/267434.htm
uniapp使用了uni-app runtime這個運行時將小程序發(fā)行代碼進行打包,實現(xiàn)了Vue與小程序之間的數(shù)據(jù)及事件同步。
源Vue模板及編譯產(chǎn)物wxml對照
uniapp的模板編譯器代碼在/Applications/HBuilderX.app/Contents/HBuilderX/plugins/uniapp-cli/node_modules/@dcloudio/uni-template-complier下。
首先以一個簡單的Vue模板為例,觀察uniapp是如何將Vue template編譯為wxml的:
<template>
<div @click="add();subtract(2)" @touchstart="mixin($event)">{{ num }}</div>
</template>
編譯結(jié)果為:
<view
data-event-opts="{{
[
['tap', [['add'],['subtract',[2]]] ],
['touchstart', [['mixin',['$event']]] ]
]
}}"
bindtap="__e" bindtouchstart="__e"
class="_div">
{{num}}
</view>
可以看到,uniapp將tap和touchstart事件綁定到__e函數(shù)上,然后將事件對應(yīng)的動作放到了名為eventOpts的dataset中。
data-event-opts
data-event-opts非常重要。data-event-opts是一個二維數(shù)組,每個子數(shù)組代表一個事件類型。事件類型有兩個值,第一個表示事件類型名稱,第二個表示觸發(fā)事件函數(shù)的個數(shù)。事件函數(shù)又是一個數(shù)組,第一個值表述事件函數(shù)名稱,第二個是參數(shù)表。下面用TypeScript的類型聲明方式進行簡單描述:
//data-event-opts是一個二維數(shù)組,每個子數(shù)組代表一個事件類型EventTypes
const dataEventOpts: EventTypes;
interface EventTypes {
[index:number]: EventType;
}
//事件類型的描述為EventType。EventType只有兩個元素,也就是說EventType.length===2
interface EventType {
//EventType的第一個元素是事件類型名稱
//第二個元素是事件函數(shù)的數(shù)組EventFuncList,數(shù)組內(nèi)元素為被觸發(fā)的事件函數(shù)
[index:number]: string | EventFuncList;
}
interface EventFuncList {
//事件函數(shù)依舊是一個數(shù)組
[index:number]: EventFunc;
}
//事件函數(shù)的元素為1或2個,分別是事件函數(shù)名稱和參數(shù)表Array<any>
interface EventFunc {
[index:number]: string | Array<any>;
}
對照模板,就可以得出如下推論:
['tap',[['add'],['subtract',[2]]]]表示事件類型為tap,觸發(fā)函數(shù)有兩個,一個為add函數(shù)且無參數(shù),一個為subtract且參數(shù)為2。 ['touchstart',[['mixin',['$event']]]]表示事件類型為touchstart,觸發(fā)函數(shù)有一個為mixin,參數(shù)為$event對象。
不難看出,我們在進行事件捕捉時,只需要讀取到data-event-opts[i][0]就可以得到每個事件的類型。
handleEvent事件:__e
所有的事件都會調(diào)用__e事件,也就是handleEvent。在上文的模板中,handleEvent做了如下操作:
1、拿到點擊元素上的data-event-opts屬性:[['tap',[['add'],['subtract',[2]]]],['touchstart',[['mixin',['$event']]]]]
2、根據(jù)點擊類型獲取相應(yīng)數(shù)組,比如bindTap就取['tap',[['add'],['subtract',[2]]]],bindtouchstart就取['touchstart',[['mixin',['$event']]]]
3、依次調(diào)用相應(yīng)事件類型的函數(shù),并傳入?yún)?shù),比如tap調(diào)用this.add();this.subtract(2)
uniapp對mp-wx的相關(guān)處理在/Applications/HBuilderX.app/Contents/HBuilderX/plugins/uniapp-cli/node_modules/@dcloudio/uni-mp-weixin下。
// @dcloudio/uni-mp-weixin/dist/index.js:1302
function handleEvent (event) {
event = wrapper$1(event);
// [['tap',[['handle',[1,2,a]],['handle1',[1,2,a]]]]]
const dataset = (event.currentTarget || event.target).dataset;
if (!dataset) {
return console.warn('事件信息不存在')
}
const eventOpts = dataset.eventOpts || dataset['event-opts']; // 支付寶 web-view 組件 dataset 非駝峰
if (!eventOpts) {
return console.warn('事件信息不存在')
}
// [['handle',[1,2,a]],['handle1',[1,2,a]]]
const eventType = event.type;
const ret = [];
eventOpts.forEach(eventOpt => {
let type = eventOpt[0];
const eventsArray = eventOpt[1];
const isCustom = type.charAt(0) === CUSTOM;
type = isCustom ? type.slice(1) : type;
const isOnce = type.charAt(0) === ONCE;
type = isOnce ? type.slice(1) : type;
if (eventsArray && isMatchEventType(eventType, type)) {
eventsArray.forEach(eventArray => {
const methodName = eventArray[0];
if (methodName) {
let handlerCtx = this.$vm;
if (handlerCtx.$options.generic) { // mp-weixin,mp-toutiao 抽象節(jié)點模擬 scoped slots
handlerCtx = getContextVm(handlerCtx) || handlerCtx;
}
if (methodName === '$emit') {
handlerCtx.$emit.apply(handlerCtx,
processEventArgs(
this.$vm,
event,
eventArray[1],
eventArray[2],
isCustom,
methodName
));
return
}
const handler = handlerCtx[methodName];
if (!isFn(handler)) {
throw new Error(` _vm.${methodName} is not a function`)
}
if (isOnce) {
if (handler.once) {
return
}
handler.once = true;
}
let params = processEventArgs(
this.$vm,
event,
eventArray[1],
eventArray[2],
isCustom,
methodName
);
params = Array.isArray(params) ? params : [];
// 參數(shù)尾部增加原始事件對象用于復(fù)雜表達(dá)式內(nèi)獲取額外數(shù)據(jù)
if (/=\s*\S+\.eventParams\s*\|\|\s*\S+\[['"]event-params['"]\]/.test(handler.toString())) {
// eslint-disable-next-line no-sparse-arrays
params = params.concat([, , , , , , , , , , event]);
}
ret.push(handler.apply(handlerCtx, params));
}
});
}
});
if (
eventType === 'input' &&
ret.length === 1 &&
typeof ret[0] !== 'undefined'
) {
return ret[0]
}
}
微信小程序自定義組件Component
mp-wx中的Component文檔:https://developers.weixin.qq.com/miniprogram/dev/reference/api/Component.html
構(gòu)造器Component()
在uniapp-mp-wx中,組件的裝載是通過實例化Component進行的。uniapp會默認(rèn)裝載如下8個參數(shù):
interface optionsList {
options: Object | Map<any, any>,
data: Object,
properties: Object | Map<any, any>,
behaviors: string | Array<any>,
lifetimes: Object,
pageLifetimes: Object,
methods: Object,
created: Function
}
并且在methods中注入如下兩個函數(shù):
methods: {
__l: handleLink, //建立組件父子關(guān)系
__e: handleEvent //事件處理器
}
劫持自定義組件構(gòu)造器Component
劫持Component的構(gòu)造器,在每個組件的__e中注入自定義的事件劫持器eventProxy
// 劫持Component
const _componentProto_ = Component;
Component = function(options) {
//options.methods內(nèi)有uniapp注入的事件處理器__e及mpHook
Object.keys(options.methods).forEach(methodName => {
//劫持事件處理器__e
if (methodName == "__e") {
eventProxy(options.methods, methodName)
}
})
_componentProto_.apply(this, arguments);
}
通過劫持事件處理器__e,我們可以實現(xiàn)觸發(fā)事件時執(zhí)行我們想要的邏輯了。
分析事件對象并編寫事件處理器劫持函數(shù)eventProxy
微信小程序事件對象描述文檔:https://developers.weixin.qq.com/miniprogram/dev/framework/view/wxml/event.html#%E4%BA%8B%E4%BB%B6%E5%AF%B9%E8%B1%A1
在上一步里我們劫持了Component,并且成功獲得了事件處理器__e,那么編寫針對事件處理器的劫持函數(shù)吧。
function eventProxy(methodList, methodName) {
const _funcProto_ = methodList[methodName];
methodList[methodName] = function() {
_funcProto_.apply(this, arguments);
let prop = {};
if (isObject(arguments[0])) {
if (Object.keys(arguments[0]).length > 0) {
//arguments[0]即為事件對象的屬性
}
}
}
}
uniapp-mp-wx中,事件對象通常具有如下屬性:
["type", "timeStamp", "target", "currentTarget", "mark", "detail", "touches", "changedTouches", "mut", "_userTap", "mp", "stopPropagation", "preventDefault"]
其中,對于數(shù)據(jù)埋點尤其有用的是如下四個屬性:
- type:描述事件類型。常見種類有tap(click)、input、blur、focus等
- currentTarget:事件綁定的當(dāng)前組件
從Vue模板編譯一節(jié)中可知,我們應(yīng)該關(guān)注currentTarget.dataset.eventOpts這個屬性,這里記載了事件被觸發(fā)時的一些信息。
interface currentTarget {
id: string, //當(dāng)前元素的id
dataset: Object //當(dāng)前元素上由data-開頭的自定義屬性組成的集合
}
mark:可以使用 mark 來識別具體觸發(fā)事件的 target 節(jié)點。此外, mark 還可以用于承載一些自定義數(shù)據(jù)(類似于 dataset )。
當(dāng)事件觸發(fā)時,事件冒泡路徑上所有的 mark 會被合并,并返回給事件回調(diào)函數(shù)。(即使事件不是冒泡事件,也會 mark 。)
如果想要得到一些詳細(xì)的錨點數(shù)據(jù),可以在代碼中做一些mark標(biāo)記。
<view mark:myMark="last" bindtap="bindViewTap">
<button mark:anotherMark="leaf" bindtap="bindButtonTap">按鈕</button>
</view>
<script>
Page({
bindViewTap: function(e) {
//Object.keys(e.mark)即為觸發(fā)事件的節(jié)點經(jīng)過的所有mark
e.mark.myMark === "last" // true
e.mark.anotherMark === "leaf" // true
}
})
</script>
detail:自定義事件所攜帶的數(shù)據(jù),如表單組件的提交事件會攜帶用戶的輸入,媒體的錯誤事件會攜帶錯誤信息,詳見組件定義中各個事件的定義。
點擊事件的detail 帶有的 x, y 同 pageX, pageY 代表距離文檔左上角的距離。
這里給出tap及input事件返回的detail結(jié)構(gòu):
interface tapDetail {
x: number, //距離文檔X軸零點的距離,零點為文檔左上角
y: number //距離文檔Y軸零點的距離
}
interface inputDetail {
value: string, //用戶輸入的值
cursor: number, //觸發(fā)事件時光標(biāo)所在的位置
keyCode: number //觸發(fā)事件時用戶輸入的keyCode
}
結(jié)合如上屬性,簡單地完善一下事件劫持器吧:
function eventProxy(methodList, methodName) {
const _funcProto_ = methodList[methodName];
methodList[methodName] = function() {
_funcProto_.apply(this, arguments);
let prop = {};
if (isObject(arguments[0])) {
if (Object.keys(arguments[0]).length > 0) {
//記錄觸發(fā)頁面信息
const pages = getCurrentPages();
const currentPage = pages[pages.length - 1];
prop["$page_path"] = currentPage.route; //頁面路徑
prop["$page_query"] = currentPage.options || {}; //頁面攜帶的query參數(shù)
const type = arguments[0]["type"];
const current_target = arguments[0].currentTarget || {};
const dataset = current_target.dataset || {};
prop["$event_type"] = type;
prop["$event_timestamp"] = Date.now();
prop["$element_id"] = current_target.id;
const eventDetail = arguments[0].detail;
prop["$event_detail"] = eventDetail;
if (!!dataset.eventOpts && type) {
if (type == "tap") { //只記錄點擊事件
const event_opts = dataset.eventOpts;
if (Array.isArray(event_opts) && event_opts[0].length === 2) {
let eventFunc = [];
event_opts[0][1].forEach(event => {
eventFunc.push({
name: event[0],
params: event[1] || ''
})
})
prop["$event_function"] = eventFunc;
}
}
postWeData(prop); //在此處上傳記錄的事件數(shù)據(jù)
}
}
}
};
}
三、完整代碼結(jié)構(gòu)
(function() {
const isObject = function(obj) {
if (obj === undefined || obj === null) {
return false;
} else {
return toString.call(obj) == "[object Object]";
}
};
// 劫持Component
const _componentProto_ = Component;
Component = function(options) {
//options.methods內(nèi)有uniapp注入的事件處理器__e及mpHook
Object.keys(options.methods).forEach(methodName => {
if (methodName == "__e") {
//劫持事件處理器
eventProxy(options.methods, methodName)
}
})
_componentProto_.apply(this, arguments);
}
function eventProxy(methodList, methodName) {
//事件處理器的劫持
}
const postWeData = function(data) {
//埋點上傳器
console.log(data)
}
})()
使用:在項目的main.js里引入即可
//main.js import './common/WeData/index.js'
四、后記
上述事件劫持器只是一個例子,實現(xiàn)了基本的tap事件記錄。實際上筆者通過擴展配置讀取的方式來完成更加便捷的埋點操作,后續(xù)只需產(chǎn)品給出希望收集的事件名,開發(fā)在固定的配置文件中寫好代碼中事件觸發(fā)的函數(shù)名即可實現(xiàn)tap白名單記錄功能。更加詳細(xì)的埋點功能可以通過閱讀分析事件對象小節(jié)來擴展,在此僅做拋磚引玉。
到此這篇關(guān)于Uniapp微信小程序?qū)崿F(xiàn)全局事件監(jiān)聽并進行數(shù)據(jù)埋點的文章就介紹到這了,更多相關(guān)Uniapp微信小程序全局事件監(jiān)聽內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
如何在父窗口中得知window.open()出的子窗口關(guān)閉事件
在父窗口中得知window.open()出的子窗口關(guān)閉事件的方法有很多,在本文將為大家詳細(xì)介紹下,感興趣的朋友可以參考下2013-10-10
JS中confirm,alert,prompt函數(shù)區(qū)別分析
JS中confirm,alert,prompt函數(shù)使用區(qū)別有哪些呢?2011-01-01
Javascript基于OOP實實現(xiàn)探測器功能代碼實例
這篇文章主要介紹了Javascript基于OOP實實現(xiàn)探測器功能代碼實例,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-08-08

