阅读 217

在 Vue3 中进行点击事件埋点

函数埋点是一个常见的需求,之前进行埋点是通过 babel 进行解析插入或者手工进行添加

前几天在某 babel 群里有人提出疑问:如何在 Vue 中对每个点击事件插入一个函数?由于 .vue 文件是将 <template><script><style> 分开进行单独解析,所以不能通过 babel 将监听函数准确解析出来(或许自己没好好看文档,不知道????‍♂️。要补补 webpack 了)。于是想了如下方法:

通过修改源码的方式添加额外函数

如果公司有自己魔改的 Vue 框架,还是挺方便的。源码版本为:3.2.20,位于 runtime-dom/src/modules/events.ts

export function patchEvent(   el: Element & { _vei?: Record<string, Invoker | undefined> },   rawName: string,   prevValue: EventValue | null,   nextValue: EventValue | null,   instance: ComponentInternalInstance | null = null ) {   const invokers = el._vei || (el._vei = {})   const existingInvoker = invokers[rawName]      //TODO: 添加额外函数   if (nextValue && existingInvoker) {     // 更新监听函数     existingInvoker.value = nextValue   } else {     const [name, options] = parseName(rawName)     if (nextValue) {       // 添加监听函数       const invoker = (invokers[rawName] = createInvoker(nextValue, instance))       addEventListener(el, name, invoker, options)     } else if (existingInvoker) {       // 移除监听函数       removeEventListener(el, name, existingInvoker, options)       invokers[rawName] = undefined     }   } } 复制代码

patchEvent 方法中,通过 createInvoker 为监听函数封装了一层用于执行额外任务,随后添加至元素的事件回调队列中:

function createInvoker(   initialValue: EventValue,   instance: ComponentInternalInstance | null ) {   const invoker: Invoker = (e: Event) => {     const timeStamp = e.timeStamp || _getNow()     if (skipTimestampCheck || timeStamp >= invoker.attached - 1) {       callWithAsyncErrorHandling(         patchStopImmediatePropagation(e, invoker.value),         instance,         ErrorCodes.NATIVE_EVENT_HANDLER,         [e]       )     }   }   invoker.value = initialValue   invoker.attached = getNow()   return invoker } 复制代码

patchStopImmediatePropagation 的作用是:在 invoker.value 是数组的情况下生成新的监听函数数组,检查 e._stopped 决定是否执行监听函数

callWithAsyncErrorHandling 的作用是:执行函数队列,函数的参数为传入的 第四个参数的解构

实现功能

function createInvokerValueWithSecretFunction (rawName: string, v: EventValue | null) {   if (!v) return v   const targetName = 'click'   const [name] = parseName(rawName)   const newValue: EventValue = isArray(v) ? v : [v]   if (name === targetName) {     newValue.unshift(insertFunction)   }   return newValue   function insertFunction (e: Event) {     console.log('Hello Click')   } } /**  * 完成之前的TODO  *  - //TODO: 添加额外函数  *  + nextValue = createInvokerValueWithSecretFunction(rawName, nextValue)  */ 复制代码

通过 Vue 插件的方式添加额外函数

目的是在 生在渲染函数 时增加额外的转换,将类似 @click="fn" 修改为 @click="[insertFn, fn]",更多细节待读者完善。由于是在生成渲染函数时转换,所以 自定义的渲染函数组件不支持 添加额外函数

增加额外编译转换函数可以直接修改实例配置,考虑和 babel 插件功能相似还是选择单独写成 Vue 插件

相关源码介绍,源码版本为:3.2.20,位于 runtime-core/src/component.ts

export function finishComponentSetup(   instance: ComponentInternalInstance,   isSSR: boolean,   skipOptions?: boolean ) {   /* ... */   if (!instance.render) {     if (!isSSR && compile && !Component.render) {       /* ... */       if (template) {         /* ... */         const { isCustomElement, compilerOptions } = instance.appContext.config         const { delimiters, compilerOptions: componentCompilerOptions } =           Component         const finalCompilerOptions: CompilerOptions = extend(           extend(             {               isCustomElement,               delimiters             },             compilerOptions           ),           componentCompilerOptions         )         /* ... */         Component.render = compile(template, finalCompilerOptions)       }     }     /* ... */   }      /* ... */ } 复制代码

在生成渲染函数前,会从 instance.appContext.config 即 Vue 实例的全局上下文的配置中获取编译配置对象 compilerOptions,也会从当前组件的配置对象获取编译配置对象 compilerOptions

位于 runtime-core/src/compile.ts

export function baseCompile(   template: string | RootNode,   options: CompilerOptions = {} ): CodegenResult {   /* ... */   const ast = isString(template) ? baseParse(template, options) : template   /* ... */   transform(     ast,     extend({}, options, {       prefixIdentifiers,       nodeTransforms: [         ...nodeTransforms,         ...(options.nodeTransforms || []) // user transforms       ],       directiveTransforms: extend(         {},         directiveTransforms,         options.directiveTransforms || {} // user transforms       )     })   )   return generate(     ast,     extend({}, options, {       prefixIdentifiers     })   ) } 复制代码

在生成完抽象语法树后,通过 transform 进行源码级别的转换,并可以从中看到会合并使用者传入的选项:options.nodeTransformsoptions.directiveTransforms,两者的区别在于前者是针对所有抽象语法树的每个节点,后者是仅针对内置指令

实现功能

// 可自行修改,这里简单描述 type Options = {   event: string   fn?: (e: Event) => void } function nodeTransformPluginForInsertFunction (app:App, options: Options = { event: 'click' }) {   const { event, fn } = options   if (typeof fn !== 'undefined' && typeof fn !== 'function') {     console.warn(/* 警告 */)     return   }   // 全局添加统一函数,用于渲染函数获取   const globalInsertFnName = '$__insertFunction__'   app.config.globalProperties[globalInsertFnName] = fn || defaultInsertFunction   const transformEventOfElement: NodeTransform = (node, context) => {     if (node.type === NodeTypes.ELEMENT /* 1 */ && node.props && node.props.length > 0) {       for (let i = 0; i < node.props.length; i++) {         const prop = node.props[i]         if (           /* 指令 */           prop.type === NodeTypes.DIRECTIVE /* 7 */ && prop.name === 'on'           /* 符合条件的指令 */           && prop.arg && prop.arg.type === NodeTypes.SIMPLE_EXPRESSION /* 4 */ && prop.arg.content === event           /* 确保 exp 存在 */           && prop.exp && prop.exp.type === NodeTypes.SIMPLE_EXPRESSION /* 4 */ && prop.exp.content.trim()         ) {           let trimmedContent = prop.exp.content.trim()           // 将类似 `@click="fn"` 修改为 `@click="[insertFn, fn]"`           // 还需要考虑其他情况,文章这里就简单处理了           if (trimmedContent[0] === '[') {             trimmedContent = `[${globalInsertFnName}, ${trimmedContent.substr(1)}`           } else {             trimmedContent = `[${globalInsertFnName}, ${trimmedContent}]`           }           prop.exp.content = trimmedContent         }       }     }   }   // 增加编译转换函数   const nodeTransforms: NodeTransform[] =     (app.config.compilerOptions as any).nodeTransforms|| ((app.config.compilerOptions as any).nodeTransforms = [])   nodeTransforms.push(transformEventOfElement)   function defaultInsertFunction (e: Event) {} } 复制代码

还有很多细节可以完善,可以参考内置的转换函数让自身的函数更加健壮

手动的方式添加额外函数


作者:渣渣富
链接:https://juejin.cn/post/7033701776419356680

文章分类
代码人生
文章标签
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐