Vite插件实现UniApp埋点

前言

最近接到埋点需求,需要对Uni项目的App端和小程序进行埋点。经过研究社区提供的方案后,发现并没有一个完全符合公司业务需求的方案。因此,决定自己动手实现一个定制化的埋点方案。

方案以及实现原理

前提条件是使用Vite脚手架搭建的Uni项目。

解决方案是编写一个Vite插件,在构建过程中预先修改模板字符串。借助Vue自带的vue/compiler-sfc插件,我们可以将Vue代码解析为AST(抽象语法树),通过AST精确修改模板代码,无需使用复杂的正则表达式进行字符串匹配和修改。这种方案适用于大多数Vue项目。需要注意的是,由于这种操作会修改项目代码,建议在完成后编写测试进行验证,推荐使用Vitest进行测试。

代码

typescript 复制代码
由于小程序端以及app端并没有window,所以需要使用混入的方式
// utils.ts
export const 你的埋点函数 = {
  methods: {
    sendMd: (code: string) => {
    // ... 处理你的业务
      uni.$emit("sendMd", code);
    }
  }
};

APP.VUE
export const createApp = () => {
   // ***
  app.mixin('你的埋点函数');
  return { app, Pinia };
};
xml 复制代码
// pages.vue 你的页面
    
<template>
    <view>
        <view @click=test data-md="code1">
                // ....
        </view>
    </view>
</template>
javascript 复制代码
// inject-click-handler.ts vite插件
import { parse } from "vue/compiler-sfc";
import { Plugin } from "vite";

export default (): Plugin => {
  return {
    name: "inject-click-handler",
    transform(code, id) {
      try {
        if (!/.vue$/.test(id)) return null;
        const parseCode = parse(code);
        if (!parseCode) return null;
        if (!parseCode.descriptor?.template?.content) return null;
        const dataMdRegex = /<[^>]*\bdata-md="([^"]*)"[^>]*>/g; // 匹配data-md,这里需要根据业务进行调整。
        // 匹配当前文件是否有埋点标识,在继续往下遍历ast
        const { content, ast } = parseCode.descriptor.template;
        // 返回null的时候表示不修改任何代码
        if (!content.match(dataMdRegex)) {
          return null;
        }
        // 获取template模板
        let $code = parseCode.descriptor.template.content;
        // 需要修改的节点数组
        const nodeArray = [];
        // 递归ast节点
        const handleEachAst = (node) => {
          if (node?.props.length) {
             // 查找我们在页面写的data-md
            const isMd = node?.props?.find(
              (item) =>
                item?.name === "data-md" ||
                (item.name === "bind" && item?.arg?.content === "data-md")
            );
            // 查找当前元素是否有点击函数并追加混入的埋点函数
            if (!!isMd) {
              const findVueClickEvent = node?.props?.find(
                (item) => item.name === "on" && item.type === 7
              );
              let pushFn = "";
              const mdContent =
                isMd.name === "data-md" ? `'${isMd.value.content}'` : isMd.exp.content;
              if (findVueClickEvent) {
                 // 检查是否一个函数   sendMd是混入的埋点函数
                const isFunctionCall = /^\s*[a-zA-Z_$][0-9a-zA-Z_$]*\s*([^)]*)\s*$/;
                if (!isFunctionCall.test(findVueClickEvent.exp.content)) {
                  findVueClickEvent.exp.content += "()";
                }
                pushFn = `@${findVueClickEvent.arg.content}="sendMd('sendMd',${mdContent});${findVueClickEvent.exp.content}"`;
              } else {
                pushFn = `@click="sendMd('sendMd',${mdContent})"`;
              }
              const nodeStr = node.loc.source.replace(findVueClickEvent.loc.source, pushFn);
              nodeArray.push({
                source: node.loc.source,
                replaceSource: nodeStr
              });
            }
          }
          if (Array.isArray(node?.children)) {
            node.children.forEach((item) => {
              item.props && handleEachAst(item);
            });
          }
         
          if (nodeArray.length) {
            nodeArray.forEach((item) => {
              $code = $code.replace(item.source, item.replaceSource);
            });
          }
        };

        handleEachAst(ast);
        return {
          code: code.replace(parseCode.descriptor.template.content, $code)
        };
      } catch (e) {
        return null;
      }
    }
  };
};
arduino 复制代码
vite.config.js
plugins: [
  InjectClickHandler(), // 必须写在uniPlugin前面
  uniPlugin,
  // ...
]
相关推荐
牛马1115 分钟前
Flutter CustomPaint
开发语言·前端·javascript
炽烈小老头15 分钟前
函数式编程范式(三)
前端·typescript
ruoyusixian28 分钟前
chrome二维码识别查插件
前端·chrome
fengfuyao98539 分钟前
一个改进的MATLAB CVA(Change Vector Analysis)变化检测程序
前端·算法·matlab
虚拟世界AI1 小时前
Vue.js安装指南:快速搭建开发环境
vue.js·npm·node.js
yuhaiqiang1 小时前
为什么这道初中数学题击溃了所有 AI
前端·后端·面试
djk88881 小时前
支持手机屏幕的layui后台html模板
前端·html·layui
紫_龙1 小时前
最新版vue3+TypeScript开发入门到实战教程之watch详解
前端·javascript·typescript
默默学前端2 小时前
ES6模板语法与字符串处理详解
前端·ecmascript·es6
lxh01132 小时前
记忆函数 II 题解
前端·javascript