基於Vue3實現一個前端埋點上報插件並打包發佈到npm

前端埋點對於那些營銷活動的項目是必須的,它可以反應出用戶的喜好與習慣,從而讓項目的運營者們能夠調整策略優化流程提高用戶體驗從而獲取更多的$。這篇文章將實現一個Vue3版本的埋點上報插件,主要功能有

  • 通過Vue自定義指令形式實現點擊事件上報
  • 提供手動調用上報方法
  • 上報每個頁面訪問人數與次數(UV,PV)
  • 上報用戶在每個頁面停留時長

項目環境搭建

本項目採用pnpm進行Monorepo環境搭建,因為未來這個項目可能會加入更多的工具包.

安裝pnpm

npm install pnpm -g

初始化package.json

pnpm init

新建配置文件 .npmrc

shamefully-hoist = true

新建pnpm-workspace.yaml

packages:
  - "packages/**"
  - "play"

此時我們的packages目錄和play目錄便關聯起來的,我們後面就可以愉快的在本地調試了。其中packages是我們各種包存放的地方,具體我們本次開發的埋點插件v-tracking便是其中之一。play則是一個Vue3項目用來測試我們的本地包,它的創建方法這裡就不再詳細說了。最終它的目錄結構如下

image.png

插件開發

終端進入v-tracking,執行pnpm init讓它成為一個包,然後新建index.js作為入口。

在vue3是通過 app.use(plugin)的形式引入插件的,它會直接調用插件的install方法.install會接收到應用實例和傳遞給 app.use() 的額外選項作為參數。所以我們在v-tracking/index.js默認導出一個帶有install函數的對象

export default {
    install: (app, options) => {
        console.log(options)
    }
}

進入paly執行pnpm add v-tracking此時你會發現paly下的package.json多了個這樣的依賴

image.png

這樣就是表示play已經關聯到本地的包[email protected]的包了,然後我們在palymain.js引入我們的插件

import { createApp } from 'vue'
import App from './App.vue'
import router from './router/index'
import vTracking from 'v-tracking'
const app = createApp(App)
app.use(router)
app.use(vTracking, {
    baseParams: {
        uid: 123
    }
})
app.mount('#app')

啟動項目我們會發現install函數被調用了,並且獲取到了傳來的額外參數.
image.png

點擊事件上報

點擊事件的上報我們提供兩種方式,一種是以Vue自定義指令的形式,一種是手動調用上報方法。因為指令形式的點擊上報並不能實現異步上報,所以加入手動調用上報的方法

vue自定義指令

首先我們簡單了解一下什麼是自定義指令。我們都用過Vue的內置的一系列指令 (比如 v-model 或 v-show) 等,而Vue還提供了註冊自定義指令的函數directive用法如下,其中el是我們綁定指令的dom,binding則是指令傳來的一系列參數,比如

<div v-example:foo.bar="baz">

binding則是這樣一個對象

{
  arg: 'foo',
  modifiers: { bar: true },
  value: /* `baz` 的值 */,
  oldValue: /* 上一次更新時 `baz` 的值 */
}

了解完指令我們便可以開始自定義指令click的開發了。其實很簡單,就是監聽el的點擊事件然後獲取到指令的value上報給後端即可

export default {
    install: (app, options) => {     
        app.directive('click', (el, bind) => {
            el.addEventListener('click', () => {
                console.log(bind.value)
            })
        })

    }
}

我們在playpage1.vue種進行綁定指令測試

<template>
    <div v-click="{ eventName: 'test1' }">test1</div>
</template>

我們點擊test1便可以在控制台看到我們需要上報的數據

image.png

手動上報方法

我們可以手動調用上報方法掛載在實例全局即可,在vue3種掛載全局屬性的方法是app.config.globalProperties.xxx,所以我們定義一個全局上報方法$vtrack

export default {
    install: (app, options) => {
        app.directive('click', (el, bind) => {
            el.addEventListener('click', () => {
                console.log(bind.value)
            })
        })
        //掛載全局用於手動上報
        app.config.globalProperties.$vtrack = (params) => {
            console.log(params)
        }

    }
}

然後我們在page1.vue中進行使用

<template>
    <div v-click="{ eventName: 'test1' }">test1</div>
</template>

<script setup>
import { getCurrentInstance } from 'vue';
const { proxy } = getCurrentInstance()
proxy.$vtrack({ eventName: 'test1' })
</script>

同樣的我們可以獲取到我們需要的上報數據。

頁面訪問次數上報(pv,uv)

對於頁面訪問次數或者人數我們可以通過檢測路由的變化從而上報當前頁面事件。比如在page1頁面我們可以以prefix_/page1(這個前綴可以由自己來定義)形式上報。但是在插件中如何檢測路由變化呢?

起初我想通過監聽onhashchange事件來監聽路由變化的,但是經過測試發現Vue中的push事件根本不會觸發onhashchange。所以我便引入了@vue/reactivity,通過它的reactive讓傳入app實例進行一個響應式包裹,再通過effect函數監聽路由變化從而實現統計每個頁面的進入事件,首先安裝

pnpm add @vue/reactivity -w

然後引用

import { reactive,effect } from '@vue/reactivity'
//uv and pv
const getVisitor = (app, prefix) => {
  const globalProperties = reactive(app.config.globalProperties);
  effect(() => {
    const path = globalProperties.$route.path;
    console.log({
      eventName: `${prefix}_${path}`,
    });
  });
};

export default {
  install: (app, options) => {
    stayTime();
    getVisitor(app, "track");
    app.directive("click", (el, bind) => {
      el.addEventListener("click", () => {
        console.log(bind.value);
      });
    });
    //掛載全局用於手動上報
    app.config.globalProperties.$vtrack = (params) => {
      console.log(params);
    };
  },
};

然後在項目中切換路由就會獲取到需要上報的事件

image.png

頁面停留時間(TP)

頁面停留時長同樣藉助effect函數,通過計算頁面變化的時間差從而上報頁面停留時長事件,一般當進入第二個頁面才會統計第一個頁面的TP,進入三個頁面計算第二個頁面的TP。。。所以我們把邏輯寫在getVisitor函數中然後給它改個名

//上報uv&pv&TP
const getVisitorAndTP = (app, prefix) => {
  const globalProperties = reactive(app.config.globalProperties);
  let startTime = new Date().getTime();
  let path = "";
  let lastPath = "";
  effect(() => {
    const endTime = new Date().getTime();
    const TP = endTime - startTime;
    startTime = endTime;
    lastPath = path;
    path = globalProperties.$route.path;
    //間隔為0不上報
    if (!TP) return;
    console.log({
      eventName: `${prefix}_${path}`,
    });
    //頁面停留時長小於0.5s不上報
    if (TP < 500) return;
    console.log({
      eventName: `${prefix}_${TP}_${lastPath}`,
    });
  });
};

export default {
  install: (app, options) => {
    getVisitorAndTP(app, "track");
    app.directive("click", (el, bind) => {
      el.addEventListener("click", () => {
        console.log(bind.value);
      });
    });
    //掛載全局用於手動上報
    app.config.globalProperties.$vtrack = (params) => {
      console.log(params);
    };
  },
};

上傳TP事件的格式為prefix_TP_path,因此我們切換頁面的時候可以看到同時上報的兩個事件

image.png

獲取公共參數

根據用戶傳來的固定參數baseParams和事件前綴prefix調整我們上報事件形式。假設在main.js用戶傳來這些數據

import { createApp } from "vue";
import App from "./App.vue";
import router from "./router/index";
import vTracking from "v-tracking";
const app = createApp(App);
app.use(router);
app.use(vTracking, {
  baseParams: {
    uid: 123,
    userAgent: "Chrome",
  },
  prefix: "app",
});
app.mount("#app");

然後修改一下我們的插件(這裡將uv/pv還有TP作為單獨參數上報,不再使用上面的eventName形式,太懶了,上面的寫法不想改了😑)

import { reactive, effect } from "@vue/reactivity";
//上報uv&pv&TP
const getVisitorAndTP = (app, prefix, baseParams) => {
  const globalProperties = reactive(app.config.globalProperties);
  let startTime = new Date().getTime();
  let path = "";
  let lastPath = "";
  effect(() => {
    const endTime = new Date().getTime();
    const TP = endTime - startTime;
    startTime = endTime;
    lastPath = path;
    path = globalProperties.$route.path;
    //間隔為0不上報
    if (!TP) return;
    console.log({
      ...baseParams,
      UPVEventName: `${prefix}_${path}`,
    });
    //頁面停留時長小於0.5s不上報
    if (TP < 500) return;
    console.log({
      ...baseParams,
      TP: {
        path: lastPath,
        time: TP,
      },
    });
  });
};

export default {
  install: (app, options) => {
    const { prefix, baseParams } = options;

    getVisitorAndTP(app, prefix || "track", baseParams || {});
    app.directive("click", (el, bind) => {
      el.addEventListener("click", () => {
        console.log({ ...bind.value, ...(baseParams || {}) });
      });
    });
    //掛載全局用於手動上報
    app.config.globalProperties.$vtrack = (params) => {
      console.log(params);
    };
  },
};

此時這控制台打印出事件類型上報格式為

image.png

引入axios

最後簡單寫一個axios的請求函數,這裡不考慮請求失敗的情況,此時需要用戶傳入一個baseUrl

import { reactive, effect } from "@vue/reactivity";
import axios from "axios";
axios.defaults.headers["Content-Type"] = "application/json";
const request = (baseUrl, params) => {
  axios({
    url: baseUrl,
    method: "post",
    data: params,
  });
};

//上報uv&pv&TP
const getVisitorAndTP = (app, prefix, baseParams, baseUrl) => {
  const globalProperties = reactive(app.config.globalProperties);
  let startTime = new Date().getTime();
  let path = "";
  let lastPath = "";
  effect(() => {
    const endTime = new Date().getTime();
    const TP = endTime - startTime;
    startTime = endTime;
    lastPath = path;
    path = globalProperties.$route.path;
    //間隔為0不上報
    if (!TP) return;
    request(baseUrl, {
      ...baseParams,
      UPVEventName: `${prefix}_${path}`,
    });
    //頁面停留時長小於0.5s不上報
    if (TP < 500) return;
    request(baseUrl, {
      ...baseParams,
      TP: {
        path: lastPath,
        time: TP,
      },
    });
  });
};

export default {
  install: (app, options) => {
    const { prefix, baseParams, baseUrl } = options;
    getVisitorAndTP(app, prefix || "track", baseParams || {}, baseUrl);
    app.directive("click", (el, bind) => {
      el.addEventListener("click", () => {
        request(baseUrl, { ...bind.value, ...(baseParams || {}) });
      });
    });
    //掛載全局用於手動上報
    app.config.globalProperties.$vtrack = (params) => {
      request(baseUrl, { ...params, ...(baseParams || {}) });
    };
  },
};


此時便可以看到事件的請求了

image.png

image.png

image.png

打包發佈

最後使用vite進行打包發佈,全局安裝vite

pnpm add vite -w -D

然後在v-tracking下新建vite.config.js,配置庫模式打包cjs和es格式

import { defineConfig } from "vite";
import { resolve } from "path";
export default defineConfig({
  build: {
    target: "modules",
    //壓縮
    minify: true,
    rollupOptions: {
      input: ["index.js"],
      //忽略文件
      external: ["@vue/reactivity", "axios"],
      output: [
        {
          format: "es",
          //不用打包成.es.js,這裡我們想把它打包成.js
          entryFileNames: "[name].js",
          //配置打包根目錄
          dir: resolve(__dirname, "./dist/es"),
        },
        {
          format: "cjs",
          //不用打包成.mjs
          entryFileNames: "[name].js",
          //配置打包根目錄
          dir: resolve(__dirname, "./dist/lib"),
        },
      ],
    },
    lib: {
      entry: "./index.js",
      name: "vtrack",
    },
  },
});

然後將v-tracking/package.json入口文件指向打包後路徑,其中module代表如果項目支持es格式的話就會使用dist/es/index.js這個路徑

{
  "name": "v-tracking",
  "version": "1.0.0",
  "main": "dist/lib/index.js",
  "module": "dist/es/index.js",
  "description": "",
  "keywords": [],
  "files": [
    "dist"
  ],
  "dependencies": {
    "@vue/reactivity": "^3.2.37",
    "axios": "^0.27.2"
  },
  "author": "",
  "license": "MIT"
}

最後在v-tracking目錄下執行pnpm publish進行發佈(這裡需要註冊npm賬戶等等)

image.png

使用說明

安裝

npm install v-tracking -S

在 main.js 中引入插件

import { createApp } from "vue";
import App from "./App.vue";
import router from "./router/index";
import vTracking from "v-tracking";
const app = createApp(App);
app.use(router);
app.use(vTracking, Options);
app.mount("#app");

注意

因為涉及到路由檢測,所以必須配合vue-router使用

Options

  • baseParams (string)

公共參數,每次上報都會攜帶的參數,比如用戶的登錄信息 uid 等

  • baseUrl (string)

上報的後台請求地址,後端接口需按照前端請求參數設計

  • prefix (string)

PV&UV&TP 事件前綴,一般用於區分不同項目等(建議和普通事件前綴一致)

  • isVisTP (Boolean)

是否統計頁面 UV&PV&PT

Options 示例

app.use(vTracking, {
  baseParams: {
    uid: 123
  },
  baseUrl: "//example/event",
  prefix: "app",
  isVisTP: false,
});

點擊指令上報

<template>
    <div>page1</div>
    <div v-click="{ eventName: 'test1' }">click</div>
</template>

後台接收數據格式為

{ uid: 123 , eventName: "test1" }

手動上報

<template>
    <div>page1</div>
    <div @click="track">click</div>
</template>
<script setup>
import { getCurrentInstance } from 'vue';
const { proxy } = getCurrentInstance()
//手動上報事件
const track = ()=>{
  proxy.$vtrack({ eventName: 'test1'  })
}

</script>

後台接收數據格式為

{ uid: 123, eventName: "test1" }

UV&PV

isVisTP為 true 時候插件會自動上報每個頁面進入時的數據,其中後台接收數據格式為

{ uid: 123, UPVEventName: `${prefix}_${path}` }

其中path為頁面路由路徑,如/page1

頁面停留時長(TP)

isVisTP為 true 時候插件會自動上報每個頁面用戶停留時長,其中後台接收數據格式為

{
  uid: 123,
  TP: { path: "/page2", time: 1269446 },
}

time 則表示時長(ms)

寫在最後

本篇文章旨在提供一些思路,難免會有不妥或者錯誤之處,也歡迎大家評論區指出不勝感激。倉庫地址vue-utils

都看到這了,點個贊再走吧~😘