最近接到需求,领导希望使用微信开放平台上免费的We分析进行数据埋点,但又不希望在现有uniapp开发的微信小程序代码上做侵入式修改,笔者奉命进行了技术调研,考虑通过劫持事件的方式来实现捕获特定事件并上传分析平台的功能。
需要特别注意的是,微信小程序是不能得到document对象的,$el上挂载的也是undefined,自然也就不能通过全局addEventListener的方式来监听特定事件。在调研中想到可以通过劫持小程序的自定义组件构造器Component()来实现事件的监听。
为了便于理解,部分数据结构通过TypeScript接口形式进行描述。
部分知识via掘金:https://juejin.cn/post/6968438754180595742#heading-20
uniapp使用了uni-app runtime这个运行时将小程序发行代码进行打包,实现了Vue与小程序之间的数据及事件同步。
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是一个二维数组,每个子数组代表一个事件类型。事件类型有两个值,第一个表示事件类型名称,第二个表示触发事件函数的个数。事件函数又是一个数组,第一个值表述事件函数名称,第二个是参数表。下面用TypeScript的类型声明方式进行简单描述:
//data-event-opts是一个二维数组,每个子数组代表一个事件类型EventTypes
const dataEventOpts: EventTypes;
interface EventTypes {
[index:number]: EventType;
}
//事件类型的描述为EventType。EventType只有两个元素,也就是说EventType.length===2
interface EventType {
//EventType的第一个元素是事件类型名称
//第二个元素是事件函数的数组EventFuncList,数组内元素为被触发的事件函数
[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,触发函数有两个,一个为add函数且无参数,一个为subtract且参数为2。 ['touchstart',[['mixin',['$event']]]]表示事件类型为touchstart,触发函数有一个为mixin,参数为$event对象。
不难看出,我们在进行事件捕捉时,只需要读取到data-event-opts[i][0]就可以得到每个事件的类型。
所有的事件都会调用__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 抽象节点模拟 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]
}
}
mp-wx中的Component文档:https://developers.weixin.qq.com/miniprogram/dev/reference/api/Component.html
在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的构造器,在每个组件的__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,我们可以实现触发事件时执行我们想要的逻辑了。
微信小程序事件对象描述文档: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模板编译一节中可知,我们应该关注currentTarget.dataset.eventOpts这个属性,这里记载了事件被触发时的一些信息。
interface currentTarget {
id: string, //当前元素的id
dataset: Object //当前元素上由data-开头的自定义属性组成的集合
}
mark:可以使用 mark 来识别具体触发事件的 target 节点。此外, mark 还可以用于承载一些自定义数据(类似于 dataset )。
当事件触发时,事件冒泡路径上所有的 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)即为触发事件的节点经过的所有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, //触发事件时光标所在的位置
keyCode: number //触发事件时用户输入的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) {
//记录触发页面信息
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事件记录。实际上笔者通过扩展配置读取的方式来完成更加便捷的埋点操作,后续只需产品给出希望收集的事件名,开发在固定的配置文件中写好代码中事件触发的函数名即可实现tap白名单记录功能。更加详细的埋点功能可以通过阅读分析事件对象小节来扩展,在此仅做抛砖引玉。
flymyd@foxmail.com
2022年06月17日,重庆
我有一个用户工厂。我希望默认情况下确认用户。但是鉴于unconfirmed特征,我不希望它们被确认。虽然我有一个基于实现细节而不是抽象的工作实现,但我想知道如何正确地做到这一点。factory:userdoafter(:create)do|user,evaluator|#unwantedimplementationdetailshereunlessFactoryGirl.factories[:user].defined_traits.map(&:name).include?(:unconfirmed)user.confirm!endendtrait:unconfirmeddoenden
是否有简单的方法来更改默认ISO格式(yyyy-mm-dd)的ActiveAdmin日期过滤器显示格式? 最佳答案 您可以像这样为日期选择器提供额外的选项,而不是覆盖js:=f.input:my_date,as::datepicker,datepicker_options:{dateFormat:"mm/dd/yy"} 关于ruby-on-rails-事件管理员日期过滤器日期格式自定义,我们在StackOverflow上找到一个类似的问题: https://s
华为OD机试题本篇题目:明明的随机数题目输入描述输出描述:示例1输入输出说明代码编写思路最近更新的博客华为od2023|什么是华为od,od薪资待遇,od机试题清单华为OD机试真题大全,用Python解华为机试题|机试宝典【华为OD机试】全流程解析+经验分享,题型分享,防作弊指南华为o
C#实现简易绘图工具一.引言实验目的:通过制作窗体应用程序(C#画图软件),熟悉基本的窗体设计过程以及控件设计,事件处理等,熟悉使用C#的winform窗体进行绘图的基本步骤,对于面向对象编程有更加深刻的体会.Tutorial任务设计一个具有基本功能的画图软件**·包括简单的新建文件,保存,重新绘图等功能**·实现一些基本图形的绘制,包括铅笔和基本形状等,学习橡皮工具的创建**·设计一个合理舒适的UI界面**注明:你可能需要先了解一些关于winform窗体应用程序绘图的基本知识,以及关于GDI+类和结构的知识二.实验环境Windows系统下的visualstudio2017C#窗体应用程序三.
MIMO技术的优缺点优点通过下面三个增益来总体概括:阵列增益。阵列增益是指由于接收机通过对接收信号的相干合并而活得的平均SNR的提高。在发射机不知道信道信息的情况下,MIMO系统可以获得的阵列增益与接收天线数成正比复用增益。在采用空间复用方案的MIMO系统中,可以获得复用增益,即信道容量成倍增加。信道容量的增加与min(Nt,Nr)成正比分集增益。在采用空间分集方案的MIMO系统中,可以获得分集增益,即可靠性性能的改善。分集增益用独立衰落支路数来描述,即分集指数。在使用了空时编码的MIMO系统中,由于接收天线或发射天线之间的间距较远,可认为它们各自的大尺度衰落是相互独立的,因此分布式MIMO
前言一般来说,前端根据后台返回code码展示对应内容只需要在前台判断code值展示对应的内容即可,但要是匹配的code码比较多或者多个页面用到时,为了便于后期维护,后台就会使用字典表让前端匹配,下面我将在微信小程序中通过wxs的方法实现这个操作。为什么要使用wxs?{{method(a,b)}}可以看到,上述代码是一个调用方法传值的操作,在vue中很常见,多用于数据之间的转换,但由于微信小程序诸多限制的原因,你并不能优雅的这样操作,可能有人会说,为什么不用if判断实现呢?但是if判断的局限性在于如果存在数据量过大时,大量重复性操作和if判断会让你的代码显得异常冗余。wxswxs相当于是一个独立
项目介绍随着我国经济迅速发展,人们对手机的需求越来越大,各种手机软件也都在被广泛应用,但是对于手机进行数据信息管理,对于手机的各种软件也是备受用户的喜爱小学生兴趣延时班预约小程序的设计与开发被用户普遍使用,为方便用户能够可以随时进行小学生兴趣延时班预约小程序的设计与开发的数据信息管理,特开发了小程序的设计与开发的管理系统。小学生兴趣延时班预约小程序的设计与开发的开发利用现有的成熟技术参考,以源代码为模板,分析功能调整与小学生兴趣延时班预约小程序的设计与开发的实际需求相结合,讨论了小学生兴趣延时班预约小程序的设计与开发的使用。开发环境开发说明:前端使用微信微信小程序开发工具:后端使用ssm:VU
@作者:SYFStrive @博客首页:HomePage📜:微信小程序📌:个人社区(欢迎大佬们加入)👉:社区链接🔗📌:觉得文章不错可以点点关注👉:专栏连接🔗💃:感谢支持,学累了可以先看小段由小胖给大家带来的街舞👉微信小程序(🔥)目录自定义组件-behaviors 1、什么是behaviors 2、behaviors的工作方式 3、创建behavior 4、导入并使用behavior 5、behavior中所有可用的节点 6、同名字段的覆盖和组合规则总结最后自定义组件-behaviors 1、什么是behaviorsbehaviors是小程序中,用于实现
遍历文件夹我们通常是使用递归进行操作,这种方式比较简单,也比较容易理解。本文为大家介绍另一种不使用递归的方式,由于没有使用递归,只用到了循环和集合,所以效率更高一些!一、使用递归遍历文件夹整体思路1、使用File封装初始目录,2、打印这个目录3、获取这个目录下所有的子文件和子目录的数组。4、遍历这个数组,取出每个File对象4-1、如果File是否是一个文件,打印4-2、否则就是一个目录,递归调用代码实现publicclassSearchFile{publicstaticvoidmain(String[]args){//初始目录Filedir=newFile("d:/Dev");Datebeg
我正在尝试将以下SQL查询转换为ActiveRecord,它正在融化我的大脑。deletefromtablewhereid有什么想法吗?我想做的是限制表中的行数。所以,我想删除少于最近10个条目的所有内容。编辑:通过结合以下几个答案找到了解决方案。Temperature.where('id这给我留下了最新的10个条目。 最佳答案 从您的SQL来看,您似乎想要从表中删除前10条记录。我相信到目前为止的大多数答案都会如此。这里有两个额外的选择:基于MurifoX的版本:Table.where(:id=>Table.order(:id).