Uniapp微信小程序實現全局事件監(jiān)聽并進行數據埋點的方法
零、前言
最近接到需求,領導希望使用微信開放平臺上免費的We分析進行數據埋點,但又不希望在現有uniapp開發(fā)的微信小程序代碼上做侵入式修改,筆者奉命進行了技術調研,考慮通過劫持事件的方式來實現捕獲特定事件并上傳分析平臺的功能。
需要特別注意的是,微信小程序是不能得到document對象的,$el上掛載的也是undefined,自然也就不能通過全局addEventListener的方式來監(jiān)聽特定事件。在調研中想到可以通過劫持小程序的自定義組件構造器Component()來實現事件的監(jiān)聽。
為了便于理解,部分數據結構通過TypeScript接口形式進行描述。
一、軟件環(huán)境
- HbuilderX 3.4.7.20220422
- 微信開發(fā)者工具 Stable 1.05.2203070
- 小程序基礎庫版本 2.24.4 [749]
二、相關分析及實現
uniapp編譯微信小程序時對于事件的處理分析
部分知識via掘金:http://www.dbjr.com.cn/article/267434.htm
uniapp使用了uni-app runtime這個運行時將小程序發(fā)行代碼進行打包,實現了Vue與小程序之間的數據及事件同步。
源Vue模板及編譯產物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>
編譯結果為:
<view data-event-opts="{{ [ ['tap', [['add'],['subtract',[2]]] ], ['touchstart', [['mixin',['$event']]] ] ] }}" bindtap="__e" bindtouchstart="__e" class="_div"> {{num}} </view>
可以看到,uniapp將tap和touchstart事件綁定到__e函數上,然后將事件對應的動作放到了名為eventOpts的dataset中。
data-event-opts
data-event-opts非常重要。data-event-opts
是一個二維數組,每個子數組代表一個事件類型。事件類型有兩個值,第一個表示事件類型名稱,第二個表示觸發(fā)事件函數的個數。事件函數又是一個數組,第一個值表述事件函數名稱,第二個是參數表。下面用TypeScript的類型聲明方式進行簡單描述:
//data-event-opts是一個二維數組,每個子數組代表一個事件類型EventTypes const dataEventOpts: EventTypes; interface EventTypes { [index:number]: EventType; } //事件類型的描述為EventType。EventType只有兩個元素,也就是說EventType.length===2 interface EventType { //EventType的第一個元素是事件類型名稱 //第二個元素是事件函數的數組EventFuncList,數組內元素為被觸發(fā)的事件函數 [index:number]: string | EventFuncList; } interface EventFuncList { //事件函數依舊是一個數組 [index:number]: EventFunc; } //事件函數的元素為1或2個,分別是事件函數名稱和參數表Array<any> interface EventFunc { [index:number]: string | Array<any>; }
對照模板,就可以得出如下推論:
['tap',[['add'],['subtract',[2]]]]
表示事件類型為tap
,觸發(fā)函數有兩個,一個為add
函數且無參數,一個為subtract
且參數為2。 ['touchstart',[['mixin',['$event']]]]
表示事件類型為touchstart
,觸發(fā)函數有一個為mixin
,參數為$event
對象。
不難看出,我們在進行事件捕捉時,只需要讀取到data-event-opts[i][0]
就可以得到每個事件的類型。
handleEvent事件:__e
所有的事件都會調用__e事件,也就是handleEvent。在上文的模板中,handleEvent做了如下操作:
1、拿到點擊元素上的data-event-opts
屬性:[['tap',[['add'],['subtract',[2]]]]
,['touchstart',[['mixin',['$event']]]]]
2、根據點擊類型獲取相應數組,比如bindTap
就取['tap',[['add'],['subtract',[2]]]]
,bindtouchstart
就取['touchstart',[['mixin',['$event']]]]
3、依次調用相應事件類型的函數,并傳入參數,比如tap
調用this.add();this.subtract(2)
uniapp對mp-wx的相關處理在/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 : []; // 參數尾部增加原始事件對象用于復雜表達式內獲取額外數據 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
構造器Component()
在uniapp-mp-wx中,組件的裝載是通過實例化Component進行的。uniapp會默認裝載如下8個參數:
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中注入如下兩個函數:
methods: { __l: handleLink, //建立組件父子關系 __e: handleEvent //事件處理器 }
劫持自定義組件構造器Component
劫持Component的構造器,在每個組件的__e中注入自定義的事件劫持器eventProxy
// 劫持Component const _componentProto_ = Component; Component = function(options) { //options.methods內有uniapp注入的事件處理器__e及mpHook Object.keys(options.methods).forEach(methodName => { //劫持事件處理器__e if (methodName == "__e") { eventProxy(options.methods, methodName) } }) _componentProto_.apply(this, arguments); }
通過劫持事件處理器__e,我們可以實現觸發(fā)事件時執(zhí)行我們想要的邏輯了。
分析事件對象并編寫事件處理器劫持函數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,那么編寫針對事件處理器的劫持函數吧。
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"]
其中,對于數據埋點尤其有用的是如下四個屬性:
- type:描述事件類型。常見種類有tap(click)、input、blur、focus等
- currentTarget:事件綁定的當前組件
從Vue模板編譯一節(jié)中可知,我們應該關注currentTarget.dataset.eventOpts這個屬性,這里記載了事件被觸發(fā)時的一些信息。
interface currentTarget { id: string, //當前元素的id dataset: Object //當前元素上由data-開頭的自定義屬性組成的集合 }
mark:可以使用 mark
來識別具體觸發(fā)事件的 target 節(jié)點。此外, mark
還可以用于承載一些自定義數據(類似于 dataset
)。
當事件觸發(fā)時,事件冒泡路徑上所有的 mark
會被合并,并返回給事件回調函數。(即使事件不是冒泡事件,也會 mark
。)
如果想要得到一些詳細的錨點數據,可以在代碼中做一些mark標記。
<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é)點經過的所有mark e.mark.myMark === "last" // true e.mark.anotherMark === "leaf" // true } }) </script>
detail:自定義事件所攜帶的數據,如表單組件的提交事件會攜帶用戶的輸入,媒體的錯誤事件會攜帶錯誤信息,詳見組件定義中各個事件的定義。
點擊事件的detail
帶有的 x, y 同 pageX, pageY 代表距離文檔左上角的距離。
這里給出tap及input事件返回的detail結構:
interface tapDetail { x: number, //距離文檔X軸零點的距離,零點為文檔左上角 y: number //距離文檔Y軸零點的距離 } interface inputDetail { value: string, //用戶輸入的值 cursor: number, //觸發(fā)事件時光標所在的位置 keyCode: number //觸發(fā)事件時用戶輸入的keyCode }
結合如上屬性,簡單地完善一下事件劫持器吧:
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參數 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); //在此處上傳記錄的事件數據 } } } }; }
三、完整代碼結構
(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內有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'
四、后記
上述事件劫持器只是一個例子,實現了基本的tap事件記錄。實際上筆者通過擴展配置讀取的方式來完成更加便捷的埋點操作,后續(xù)只需產品給出希望收集的事件名,開發(fā)在固定的配置文件中寫好代碼中事件觸發(fā)的函數名即可實現tap白名單記錄功能。更加詳細的埋點功能可以通過閱讀分析事件對象小節(jié)來擴展,在此僅做拋磚引玉。
到此這篇關于Uniapp微信小程序實現全局事件監(jiān)聽并進行數據埋點的文章就介紹到這了,更多相關Uniapp微信小程序全局事件監(jiān)聽內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
如何在父窗口中得知window.open()出的子窗口關閉事件
在父窗口中得知window.open()出的子窗口關閉事件的方法有很多,在本文將為大家詳細介紹下,感興趣的朋友可以參考下2013-10-10JS中confirm,alert,prompt函數區(qū)別分析
JS中confirm,alert,prompt函數使用區(qū)別有哪些呢?2011-01-01