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ù)雜表達式內(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會默認裝載如下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
。)
如果想要得到一些詳細的錨點數(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白名單記錄功能。更加詳細的埋點功能可以通過閱讀分析事件對象小節(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)閉事件的方法有很多,在本文將為大家詳細介紹下,感興趣的朋友可以參考下2013-10-10JS中confirm,alert,prompt函數(shù)區(qū)別分析
JS中confirm,alert,prompt函數(shù)使用區(qū)別有哪些呢?2011-01-01Javascript基于OOP實實現(xiàn)探測器功能代碼實例
這篇文章主要介紹了Javascript基于OOP實實現(xiàn)探測器功能代碼實例,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-08-08