@gctrack/vue 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +69 -26
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +10 -3
- package/dist/index.d.ts +10 -3
- package/dist/index.js +68 -25
- package/dist/index.js.map +1 -1
- package/package.json +12 -4
package/dist/index.cjs
CHANGED
|
@@ -1,44 +1,90 @@
|
|
|
1
|
-
|
|
1
|
+
|
|
2
2
|
//#region src/index.ts
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* 运行时探测 Vue2 vs Vue3。
|
|
5
|
+
* Vue2 通过 `Vue.use(plugin)` 传入「构造函数」(typeof === 'function');
|
|
6
|
+
* Vue3 通过 `app.use(plugin)` 传入「应用对象」(typeof === 'object')。
|
|
7
|
+
*/
|
|
8
|
+
function isVue2(appOrVue) {
|
|
9
|
+
return typeof appOrVue === "function";
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* 兼容取组件名:Vue2 `vm.$options.name` / Vue3 `vm.type.name`(Vue3 的 instance 也暴露 $options)。
|
|
13
|
+
*/
|
|
14
|
+
function getComponentName(vm) {
|
|
15
|
+
if (!vm) return "Anonymous";
|
|
16
|
+
const name = vm.$options && vm.$options.name || vm.type && vm.type.name;
|
|
17
|
+
return name || "Anonymous";
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* GCTrack Vue 插件对象(Vue2 / Vue3 通用)
|
|
5
21
|
*/
|
|
6
|
-
const GCTrackPlugin = { install(
|
|
22
|
+
const GCTrackPlugin = { install(appOrVue, options) {
|
|
7
23
|
const { tracker, router, trackPageView = true, trackErrors = true } = options;
|
|
8
|
-
|
|
24
|
+
const v2 = isVue2(appOrVue);
|
|
25
|
+
const config = appOrVue.config;
|
|
26
|
+
if (v2) appOrVue.prototype.$tracker = tracker;
|
|
27
|
+
else config.globalProperties.$tracker = tracker;
|
|
9
28
|
if (router && trackPageView) router.afterEach((to, from) => {
|
|
10
29
|
tracker.track("page_view", {
|
|
11
30
|
from: from.path,
|
|
12
31
|
to: to.path,
|
|
13
|
-
title: to.meta && to.meta.title || document.title,
|
|
32
|
+
title: to.meta && to.meta.title || (typeof document !== "undefined" ? document.title : ""),
|
|
14
33
|
query: to.query,
|
|
15
34
|
params: to.params
|
|
16
35
|
});
|
|
17
36
|
});
|
|
18
|
-
if (trackErrors)
|
|
37
|
+
if (trackErrors) config.errorHandler = (err, vm, info) => {
|
|
19
38
|
console.error("Vue Error:", err);
|
|
20
39
|
tracker.track("vue_error", {
|
|
21
40
|
message: err.message,
|
|
22
41
|
stack: err.stack,
|
|
23
|
-
componentName: vm
|
|
42
|
+
componentName: getComponentName(vm),
|
|
24
43
|
info,
|
|
25
44
|
timestamp: Date.now()
|
|
26
45
|
});
|
|
27
46
|
};
|
|
28
|
-
registerDirectives(
|
|
47
|
+
registerDirectives(appOrVue, tracker, v2);
|
|
29
48
|
} };
|
|
30
49
|
/**
|
|
50
|
+
* 元素 → 清理函数(移除监听 / 断开 observer)。
|
|
51
|
+
* 修复旧实现只有 bind、无 unbind 导致的监听/observer 泄漏。
|
|
52
|
+
*/
|
|
53
|
+
const cleanupMap = /* @__PURE__ */ new WeakMap();
|
|
54
|
+
/**
|
|
55
|
+
* 按版本返回正确的指令钩子名(V2 `bind`/`unbind`,V3 `mounted`/`unmounted`),
|
|
56
|
+
* 内部 mount/unmount 逻辑两端共用;unmount 统一执行 cleanupMap 中的清理。
|
|
57
|
+
*/
|
|
58
|
+
function versionedDirective(v2, mount) {
|
|
59
|
+
const unmount = (el) => {
|
|
60
|
+
const cleanup = cleanupMap.get(el);
|
|
61
|
+
if (cleanup) {
|
|
62
|
+
cleanup();
|
|
63
|
+
cleanupMap.delete(el);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
return v2 ? {
|
|
67
|
+
bind: mount,
|
|
68
|
+
unbind: unmount
|
|
69
|
+
} : {
|
|
70
|
+
mounted: mount,
|
|
71
|
+
unmounted: unmount
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
31
75
|
* 注册全局指令
|
|
32
76
|
*/
|
|
33
|
-
function registerDirectives(
|
|
34
|
-
|
|
35
|
-
|
|
77
|
+
function registerDirectives(appOrVue, tracker, v2) {
|
|
78
|
+
appOrVue.directive("track", versionedDirective(v2, (el, binding) => {
|
|
79
|
+
const handler = () => {
|
|
36
80
|
const eventName = binding.value;
|
|
37
81
|
const eventData = binding.arg ? { [binding.arg]: binding.modifiers } : {};
|
|
38
82
|
tracker.track(eventName, eventData);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
|
|
83
|
+
};
|
|
84
|
+
el.addEventListener("click", handler);
|
|
85
|
+
cleanupMap.set(el, () => el.removeEventListener("click", handler));
|
|
86
|
+
}));
|
|
87
|
+
appOrVue.directive("track-view", versionedDirective(v2, (el, binding) => {
|
|
42
88
|
if (typeof IntersectionObserver === "undefined") return;
|
|
43
89
|
const observer = new IntersectionObserver((entries) => {
|
|
44
90
|
entries.forEach((entry) => {
|
|
@@ -50,11 +96,12 @@ function registerDirectives(Vue, tracker) {
|
|
|
50
96
|
});
|
|
51
97
|
});
|
|
52
98
|
observer.observe(el);
|
|
53
|
-
|
|
54
|
-
|
|
99
|
+
cleanupMap.set(el, () => observer.disconnect());
|
|
100
|
+
}));
|
|
101
|
+
appOrVue.directive("track-stay", versionedDirective(v2, (el, binding) => {
|
|
55
102
|
if (typeof IntersectionObserver === "undefined") return;
|
|
56
103
|
const startTime = Date.now();
|
|
57
|
-
new IntersectionObserver((entries) => {
|
|
104
|
+
const observer = new IntersectionObserver((entries) => {
|
|
58
105
|
entries.forEach((entry) => {
|
|
59
106
|
if (!entry.isIntersecting) {
|
|
60
107
|
const stayTime = Date.now() - startTime;
|
|
@@ -62,28 +109,24 @@ function registerDirectives(Vue, tracker) {
|
|
|
62
109
|
tracker.track(`${eventName}_stay`, { stayTime });
|
|
63
110
|
}
|
|
64
111
|
});
|
|
65
|
-
})
|
|
66
|
-
|
|
112
|
+
});
|
|
113
|
+
observer.observe(el);
|
|
114
|
+
cleanupMap.set(el, () => observer.disconnect());
|
|
115
|
+
}));
|
|
67
116
|
}
|
|
68
117
|
/**
|
|
69
118
|
* Vue Mixin
|
|
70
119
|
*/
|
|
71
120
|
const GCTrackMixin = { methods: {
|
|
72
|
-
/**
|
|
73
|
-
* 追踪事件
|
|
74
|
-
*/
|
|
75
121
|
$track(eventType, data) {
|
|
76
122
|
if (this.$tracker) this.$tracker.track(eventType, data);
|
|
77
123
|
},
|
|
78
|
-
/**
|
|
79
|
-
* 追踪页面浏览
|
|
80
|
-
*/
|
|
81
124
|
$trackPageView(data) {
|
|
82
125
|
if (this.$tracker) this.$tracker.track("page_view", data);
|
|
83
126
|
}
|
|
84
127
|
} };
|
|
128
|
+
|
|
85
129
|
//#endregion
|
|
86
130
|
exports.GCTrackMixin = GCTrackMixin;
|
|
87
131
|
exports.GCTrackPlugin = GCTrackPlugin;
|
|
88
|
-
|
|
89
132
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["import type { Tracker } from '@gctrack/core'\n\n/**\n * Vue 插件选项\n */\nexport interface VuePluginOptions {\n tracker: Tracker\n router?: any\n trackPageView?: boolean\n trackErrors?: boolean\n}\n\n/**\n * GCTrack Vue
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["appOrVue: any","vm: any","options: VuePluginOptions","to: any","from: any","err: Error","info: string","v2: boolean","mount: (el: HTMLElement, binding: any) => void","el: HTMLElement","tracker: Tracker","binding: any","eventType: string","data?: Record<string, any>"],"sources":["../src/index.ts"],"sourcesContent":["import type { Tracker } from '@gctrack/core'\n\n/**\n * Vue 插件选项\n */\nexport interface VuePluginOptions {\n tracker: Tracker\n router?: any\n trackPageView?: boolean\n trackErrors?: boolean\n}\n\n/**\n * 运行时探测 Vue2 vs Vue3。\n * Vue2 通过 `Vue.use(plugin)` 传入「构造函数」(typeof === 'function');\n * Vue3 通过 `app.use(plugin)` 传入「应用对象」(typeof === 'object')。\n */\nfunction isVue2(appOrVue: any): boolean {\n return typeof appOrVue === 'function'\n}\n\n/**\n * 兼容取组件名:Vue2 `vm.$options.name` / Vue3 `vm.type.name`(Vue3 的 instance 也暴露 $options)。\n */\nfunction getComponentName(vm: any): string {\n if (!vm) return 'Anonymous'\n const name = (vm.$options && vm.$options.name) || (vm.type && vm.type.name)\n return name || 'Anonymous'\n}\n\n/**\n * GCTrack Vue 插件对象(Vue2 / Vue3 通用)\n */\nexport const GCTrackPlugin = {\n install(appOrVue: any, options: VuePluginOptions): void {\n const { tracker, router, trackPageView = true, trackErrors = true } = options\n const v2 = isVue2(appOrVue)\n const config = appOrVue.config\n\n // 全局属性:V2 挂 prototype / V3 挂 globalProperties\n if (v2) {\n appOrVue.prototype.$tracker = tracker\n } else {\n config.globalProperties.$tracker = tracker\n }\n\n // 路由变化时追踪页面浏览(vue-router 3/4 均提供 afterEach)\n if (router && trackPageView) {\n router.afterEach((to: any, from: any) => {\n tracker.track('page_view', {\n from: from.path,\n to: to.path,\n title: (to.meta && to.meta.title) || (typeof document !== 'undefined' ? document.title : ''),\n query: to.query,\n params: to.params,\n })\n })\n }\n\n // 全局错误处理(两端均挂 .config.errorHandler)\n if (trackErrors) {\n config.errorHandler = (err: Error, vm: any, info: string) => {\n console.error('Vue Error:', err)\n\n tracker.track('vue_error', {\n message: err.message,\n stack: err.stack,\n componentName: getComponentName(vm),\n info,\n timestamp: Date.now(),\n })\n }\n }\n\n // 注册全局指令(Vue2/3 都有 .directive,接收者已正确)\n registerDirectives(appOrVue, tracker, v2)\n },\n}\n\n/**\n * 元素 → 清理函数(移除监听 / 断开 observer)。\n * 修复旧实现只有 bind、无 unbind 导致的监听/observer 泄漏。\n */\nconst cleanupMap = new WeakMap<HTMLElement, () => void>()\n\n/**\n * 按版本返回正确的指令钩子名(V2 `bind`/`unbind`,V3 `mounted`/`unmounted`),\n * 内部 mount/unmount 逻辑两端共用;unmount 统一执行 cleanupMap 中的清理。\n */\nfunction versionedDirective(\n v2: boolean,\n mount: (el: HTMLElement, binding: any) => void,\n): any {\n const unmount = (el: HTMLElement) => {\n const cleanup = cleanupMap.get(el)\n if (cleanup) {\n cleanup()\n cleanupMap.delete(el)\n }\n }\n return v2 ? { bind: mount, unbind: unmount } : { mounted: mount, unmounted: unmount }\n}\n\n/**\n * 注册全局指令\n */\nfunction registerDirectives(appOrVue: any, tracker: Tracker, v2: boolean): void {\n // 点击埋点指令\n appOrVue.directive(\n 'track',\n versionedDirective(v2, (el: HTMLElement, binding: any) => {\n const handler = () => {\n const eventName = binding.value\n const eventData = binding.arg ? { [binding.arg]: binding.modifiers } : {}\n tracker.track(eventName, eventData)\n }\n el.addEventListener('click', handler)\n cleanupMap.set(el, () => el.removeEventListener('click', handler))\n }),\n )\n\n // 曝光埋点指令\n appOrVue.directive(\n 'track-view',\n versionedDirective(v2, (el: HTMLElement, binding: any) => {\n if (typeof IntersectionObserver === 'undefined') {\n return\n }\n\n const observer = new IntersectionObserver((entries) => {\n entries.forEach((entry) => {\n if (entry.isIntersecting) {\n const eventName = binding.value\n tracker.track(`${eventName}_view`, {\n element: el.tagName,\n })\n // 只上报一次\n observer.unobserve(el)\n }\n })\n })\n\n observer.observe(el)\n cleanupMap.set(el, () => observer.disconnect())\n }),\n )\n\n // 停留时长埋点指令\n appOrVue.directive(\n 'track-stay',\n versionedDirective(v2, (el: HTMLElement, binding: any) => {\n if (typeof IntersectionObserver === 'undefined') {\n return\n }\n\n const startTime = Date.now()\n\n const observer = new IntersectionObserver((entries) => {\n entries.forEach((entry) => {\n if (!entry.isIntersecting) {\n const stayTime = Date.now() - startTime\n const eventName = binding.value\n\n tracker.track(`${eventName}_stay`, {\n stayTime,\n })\n }\n })\n })\n\n observer.observe(el)\n cleanupMap.set(el, () => observer.disconnect())\n }),\n )\n}\n\n/**\n * Vue Mixin\n */\nexport const GCTrackMixin = {\n methods: {\n /**\n * 追踪事件\n */\n $track(this: any, eventType: string, data?: Record<string, any>): void {\n if (this.$tracker) {\n this.$tracker.track(eventType, data)\n }\n },\n\n /**\n * 追踪页面浏览\n */\n $trackPageView(this: any, data?: Record<string, any>): void {\n if (this.$tracker) {\n this.$tracker.track('page_view', data)\n }\n },\n },\n}\n\n/**\n * 类型声明(Vue2 / Vue3 双端增强)\n */\n// @ts-ignore vue@3 无 vue/types/vue 子路径;此处增强对 Vue2 消费者侧仍生效,vue@3 构建时压制报错\ndeclare module 'vue/types/vue' {\n interface Vue {\n $tracker: Tracker\n $track: (eventType: string, data?: Record<string, any>) => void\n $trackPageView: (data?: Record<string, any>) => void\n }\n}\n\ndeclare module 'vue' {\n interface ComponentCustomProperties {\n $tracker: Tracker\n $track: (eventType: string, data?: Record<string, any>) => void\n $trackPageView: (data?: Record<string, any>) => void\n }\n}\n"],"mappings":"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"}
|
package/dist/index.d.cts
CHANGED
|
@@ -5,10 +5,10 @@ export interface VuePluginOptions {
|
|
|
5
5
|
trackPageView?: boolean;
|
|
6
6
|
trackErrors?: boolean;
|
|
7
7
|
}
|
|
8
|
-
export declare const GCTrackPlugin: {install(
|
|
8
|
+
export declare const GCTrackPlugin: {install(appOrVue: any, options: VuePluginOptions): void};
|
|
9
9
|
export declare const GCTrackMixin: {methods: {
|
|
10
|
-
$track(eventType: string, data?: Record<string, any>): void;
|
|
11
|
-
$trackPageView(data?: Record<string, any>): void;
|
|
10
|
+
$track(this: any, eventType: string, data?: Record<string, any>): void;
|
|
11
|
+
$trackPageView(this: any, data?: Record<string, any>): void;
|
|
12
12
|
}};
|
|
13
13
|
declare module "vue/types/vue" {
|
|
14
14
|
interface Vue {
|
|
@@ -17,3 +17,10 @@ declare module "vue/types/vue" {
|
|
|
17
17
|
$trackPageView: (data?: Record<string, any>) => void;
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
|
+
declare module "vue" {
|
|
21
|
+
interface ComponentCustomProperties {
|
|
22
|
+
$tracker: Tracker;
|
|
23
|
+
$track: (eventType: string, data?: Record<string, any>) => void;
|
|
24
|
+
$trackPageView: (data?: Record<string, any>) => void;
|
|
25
|
+
}
|
|
26
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -5,10 +5,10 @@ export interface VuePluginOptions {
|
|
|
5
5
|
trackPageView?: boolean;
|
|
6
6
|
trackErrors?: boolean;
|
|
7
7
|
}
|
|
8
|
-
export declare const GCTrackPlugin: {install(
|
|
8
|
+
export declare const GCTrackPlugin: {install(appOrVue: any, options: VuePluginOptions): void};
|
|
9
9
|
export declare const GCTrackMixin: {methods: {
|
|
10
|
-
$track(eventType: string, data?: Record<string, any>): void;
|
|
11
|
-
$trackPageView(data?: Record<string, any>): void;
|
|
10
|
+
$track(this: any, eventType: string, data?: Record<string, any>): void;
|
|
11
|
+
$trackPageView(this: any, data?: Record<string, any>): void;
|
|
12
12
|
}};
|
|
13
13
|
declare module "vue/types/vue" {
|
|
14
14
|
interface Vue {
|
|
@@ -17,3 +17,10 @@ declare module "vue/types/vue" {
|
|
|
17
17
|
$trackPageView: (data?: Record<string, any>) => void;
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
|
+
declare module "vue" {
|
|
21
|
+
interface ComponentCustomProperties {
|
|
22
|
+
$tracker: Tracker;
|
|
23
|
+
$track: (eventType: string, data?: Record<string, any>) => void;
|
|
24
|
+
$trackPageView: (data?: Record<string, any>) => void;
|
|
25
|
+
}
|
|
26
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -1,43 +1,89 @@
|
|
|
1
1
|
//#region src/index.ts
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* 运行时探测 Vue2 vs Vue3。
|
|
4
|
+
* Vue2 通过 `Vue.use(plugin)` 传入「构造函数」(typeof === 'function');
|
|
5
|
+
* Vue3 通过 `app.use(plugin)` 传入「应用对象」(typeof === 'object')。
|
|
4
6
|
*/
|
|
5
|
-
|
|
7
|
+
function isVue2(appOrVue) {
|
|
8
|
+
return typeof appOrVue === "function";
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* 兼容取组件名:Vue2 `vm.$options.name` / Vue3 `vm.type.name`(Vue3 的 instance 也暴露 $options)。
|
|
12
|
+
*/
|
|
13
|
+
function getComponentName(vm) {
|
|
14
|
+
if (!vm) return "Anonymous";
|
|
15
|
+
const name = vm.$options && vm.$options.name || vm.type && vm.type.name;
|
|
16
|
+
return name || "Anonymous";
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* GCTrack Vue 插件对象(Vue2 / Vue3 通用)
|
|
20
|
+
*/
|
|
21
|
+
const GCTrackPlugin = { install(appOrVue, options) {
|
|
6
22
|
const { tracker, router, trackPageView = true, trackErrors = true } = options;
|
|
7
|
-
|
|
23
|
+
const v2 = isVue2(appOrVue);
|
|
24
|
+
const config = appOrVue.config;
|
|
25
|
+
if (v2) appOrVue.prototype.$tracker = tracker;
|
|
26
|
+
else config.globalProperties.$tracker = tracker;
|
|
8
27
|
if (router && trackPageView) router.afterEach((to, from) => {
|
|
9
28
|
tracker.track("page_view", {
|
|
10
29
|
from: from.path,
|
|
11
30
|
to: to.path,
|
|
12
|
-
title: to.meta && to.meta.title || document.title,
|
|
31
|
+
title: to.meta && to.meta.title || (typeof document !== "undefined" ? document.title : ""),
|
|
13
32
|
query: to.query,
|
|
14
33
|
params: to.params
|
|
15
34
|
});
|
|
16
35
|
});
|
|
17
|
-
if (trackErrors)
|
|
36
|
+
if (trackErrors) config.errorHandler = (err, vm, info) => {
|
|
18
37
|
console.error("Vue Error:", err);
|
|
19
38
|
tracker.track("vue_error", {
|
|
20
39
|
message: err.message,
|
|
21
40
|
stack: err.stack,
|
|
22
|
-
componentName: vm
|
|
41
|
+
componentName: getComponentName(vm),
|
|
23
42
|
info,
|
|
24
43
|
timestamp: Date.now()
|
|
25
44
|
});
|
|
26
45
|
};
|
|
27
|
-
registerDirectives(
|
|
46
|
+
registerDirectives(appOrVue, tracker, v2);
|
|
28
47
|
} };
|
|
29
48
|
/**
|
|
49
|
+
* 元素 → 清理函数(移除监听 / 断开 observer)。
|
|
50
|
+
* 修复旧实现只有 bind、无 unbind 导致的监听/observer 泄漏。
|
|
51
|
+
*/
|
|
52
|
+
const cleanupMap = /* @__PURE__ */ new WeakMap();
|
|
53
|
+
/**
|
|
54
|
+
* 按版本返回正确的指令钩子名(V2 `bind`/`unbind`,V3 `mounted`/`unmounted`),
|
|
55
|
+
* 内部 mount/unmount 逻辑两端共用;unmount 统一执行 cleanupMap 中的清理。
|
|
56
|
+
*/
|
|
57
|
+
function versionedDirective(v2, mount) {
|
|
58
|
+
const unmount = (el) => {
|
|
59
|
+
const cleanup = cleanupMap.get(el);
|
|
60
|
+
if (cleanup) {
|
|
61
|
+
cleanup();
|
|
62
|
+
cleanupMap.delete(el);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
return v2 ? {
|
|
66
|
+
bind: mount,
|
|
67
|
+
unbind: unmount
|
|
68
|
+
} : {
|
|
69
|
+
mounted: mount,
|
|
70
|
+
unmounted: unmount
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
30
74
|
* 注册全局指令
|
|
31
75
|
*/
|
|
32
|
-
function registerDirectives(
|
|
33
|
-
|
|
34
|
-
|
|
76
|
+
function registerDirectives(appOrVue, tracker, v2) {
|
|
77
|
+
appOrVue.directive("track", versionedDirective(v2, (el, binding) => {
|
|
78
|
+
const handler = () => {
|
|
35
79
|
const eventName = binding.value;
|
|
36
80
|
const eventData = binding.arg ? { [binding.arg]: binding.modifiers } : {};
|
|
37
81
|
tracker.track(eventName, eventData);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
|
|
82
|
+
};
|
|
83
|
+
el.addEventListener("click", handler);
|
|
84
|
+
cleanupMap.set(el, () => el.removeEventListener("click", handler));
|
|
85
|
+
}));
|
|
86
|
+
appOrVue.directive("track-view", versionedDirective(v2, (el, binding) => {
|
|
41
87
|
if (typeof IntersectionObserver === "undefined") return;
|
|
42
88
|
const observer = new IntersectionObserver((entries) => {
|
|
43
89
|
entries.forEach((entry) => {
|
|
@@ -49,11 +95,12 @@ function registerDirectives(Vue, tracker) {
|
|
|
49
95
|
});
|
|
50
96
|
});
|
|
51
97
|
observer.observe(el);
|
|
52
|
-
|
|
53
|
-
|
|
98
|
+
cleanupMap.set(el, () => observer.disconnect());
|
|
99
|
+
}));
|
|
100
|
+
appOrVue.directive("track-stay", versionedDirective(v2, (el, binding) => {
|
|
54
101
|
if (typeof IntersectionObserver === "undefined") return;
|
|
55
102
|
const startTime = Date.now();
|
|
56
|
-
new IntersectionObserver((entries) => {
|
|
103
|
+
const observer = new IntersectionObserver((entries) => {
|
|
57
104
|
entries.forEach((entry) => {
|
|
58
105
|
if (!entry.isIntersecting) {
|
|
59
106
|
const stayTime = Date.now() - startTime;
|
|
@@ -61,27 +108,23 @@ function registerDirectives(Vue, tracker) {
|
|
|
61
108
|
tracker.track(`${eventName}_stay`, { stayTime });
|
|
62
109
|
}
|
|
63
110
|
});
|
|
64
|
-
})
|
|
65
|
-
|
|
111
|
+
});
|
|
112
|
+
observer.observe(el);
|
|
113
|
+
cleanupMap.set(el, () => observer.disconnect());
|
|
114
|
+
}));
|
|
66
115
|
}
|
|
67
116
|
/**
|
|
68
117
|
* Vue Mixin
|
|
69
118
|
*/
|
|
70
119
|
const GCTrackMixin = { methods: {
|
|
71
|
-
/**
|
|
72
|
-
* 追踪事件
|
|
73
|
-
*/
|
|
74
120
|
$track(eventType, data) {
|
|
75
121
|
if (this.$tracker) this.$tracker.track(eventType, data);
|
|
76
122
|
},
|
|
77
|
-
/**
|
|
78
|
-
* 追踪页面浏览
|
|
79
|
-
*/
|
|
80
123
|
$trackPageView(data) {
|
|
81
124
|
if (this.$tracker) this.$tracker.track("page_view", data);
|
|
82
125
|
}
|
|
83
126
|
} };
|
|
127
|
+
|
|
84
128
|
//#endregion
|
|
85
129
|
export { GCTrackMixin, GCTrackPlugin };
|
|
86
|
-
|
|
87
130
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["import type { Tracker } from '@gctrack/core'\n\n/**\n * Vue 插件选项\n */\nexport interface VuePluginOptions {\n tracker: Tracker\n router?: any\n trackPageView?: boolean\n trackErrors?: boolean\n}\n\n/**\n * GCTrack Vue
|
|
1
|
+
{"version":3,"file":"index.js","names":["appOrVue: any","vm: any","options: VuePluginOptions","to: any","from: any","err: Error","info: string","v2: boolean","mount: (el: HTMLElement, binding: any) => void","el: HTMLElement","tracker: Tracker","binding: any","eventType: string","data?: Record<string, any>"],"sources":["../src/index.ts"],"sourcesContent":["import type { Tracker } from '@gctrack/core'\n\n/**\n * Vue 插件选项\n */\nexport interface VuePluginOptions {\n tracker: Tracker\n router?: any\n trackPageView?: boolean\n trackErrors?: boolean\n}\n\n/**\n * 运行时探测 Vue2 vs Vue3。\n * Vue2 通过 `Vue.use(plugin)` 传入「构造函数」(typeof === 'function');\n * Vue3 通过 `app.use(plugin)` 传入「应用对象」(typeof === 'object')。\n */\nfunction isVue2(appOrVue: any): boolean {\n return typeof appOrVue === 'function'\n}\n\n/**\n * 兼容取组件名:Vue2 `vm.$options.name` / Vue3 `vm.type.name`(Vue3 的 instance 也暴露 $options)。\n */\nfunction getComponentName(vm: any): string {\n if (!vm) return 'Anonymous'\n const name = (vm.$options && vm.$options.name) || (vm.type && vm.type.name)\n return name || 'Anonymous'\n}\n\n/**\n * GCTrack Vue 插件对象(Vue2 / Vue3 通用)\n */\nexport const GCTrackPlugin = {\n install(appOrVue: any, options: VuePluginOptions): void {\n const { tracker, router, trackPageView = true, trackErrors = true } = options\n const v2 = isVue2(appOrVue)\n const config = appOrVue.config\n\n // 全局属性:V2 挂 prototype / V3 挂 globalProperties\n if (v2) {\n appOrVue.prototype.$tracker = tracker\n } else {\n config.globalProperties.$tracker = tracker\n }\n\n // 路由变化时追踪页面浏览(vue-router 3/4 均提供 afterEach)\n if (router && trackPageView) {\n router.afterEach((to: any, from: any) => {\n tracker.track('page_view', {\n from: from.path,\n to: to.path,\n title: (to.meta && to.meta.title) || (typeof document !== 'undefined' ? document.title : ''),\n query: to.query,\n params: to.params,\n })\n })\n }\n\n // 全局错误处理(两端均挂 .config.errorHandler)\n if (trackErrors) {\n config.errorHandler = (err: Error, vm: any, info: string) => {\n console.error('Vue Error:', err)\n\n tracker.track('vue_error', {\n message: err.message,\n stack: err.stack,\n componentName: getComponentName(vm),\n info,\n timestamp: Date.now(),\n })\n }\n }\n\n // 注册全局指令(Vue2/3 都有 .directive,接收者已正确)\n registerDirectives(appOrVue, tracker, v2)\n },\n}\n\n/**\n * 元素 → 清理函数(移除监听 / 断开 observer)。\n * 修复旧实现只有 bind、无 unbind 导致的监听/observer 泄漏。\n */\nconst cleanupMap = new WeakMap<HTMLElement, () => void>()\n\n/**\n * 按版本返回正确的指令钩子名(V2 `bind`/`unbind`,V3 `mounted`/`unmounted`),\n * 内部 mount/unmount 逻辑两端共用;unmount 统一执行 cleanupMap 中的清理。\n */\nfunction versionedDirective(\n v2: boolean,\n mount: (el: HTMLElement, binding: any) => void,\n): any {\n const unmount = (el: HTMLElement) => {\n const cleanup = cleanupMap.get(el)\n if (cleanup) {\n cleanup()\n cleanupMap.delete(el)\n }\n }\n return v2 ? { bind: mount, unbind: unmount } : { mounted: mount, unmounted: unmount }\n}\n\n/**\n * 注册全局指令\n */\nfunction registerDirectives(appOrVue: any, tracker: Tracker, v2: boolean): void {\n // 点击埋点指令\n appOrVue.directive(\n 'track',\n versionedDirective(v2, (el: HTMLElement, binding: any) => {\n const handler = () => {\n const eventName = binding.value\n const eventData = binding.arg ? { [binding.arg]: binding.modifiers } : {}\n tracker.track(eventName, eventData)\n }\n el.addEventListener('click', handler)\n cleanupMap.set(el, () => el.removeEventListener('click', handler))\n }),\n )\n\n // 曝光埋点指令\n appOrVue.directive(\n 'track-view',\n versionedDirective(v2, (el: HTMLElement, binding: any) => {\n if (typeof IntersectionObserver === 'undefined') {\n return\n }\n\n const observer = new IntersectionObserver((entries) => {\n entries.forEach((entry) => {\n if (entry.isIntersecting) {\n const eventName = binding.value\n tracker.track(`${eventName}_view`, {\n element: el.tagName,\n })\n // 只上报一次\n observer.unobserve(el)\n }\n })\n })\n\n observer.observe(el)\n cleanupMap.set(el, () => observer.disconnect())\n }),\n )\n\n // 停留时长埋点指令\n appOrVue.directive(\n 'track-stay',\n versionedDirective(v2, (el: HTMLElement, binding: any) => {\n if (typeof IntersectionObserver === 'undefined') {\n return\n }\n\n const startTime = Date.now()\n\n const observer = new IntersectionObserver((entries) => {\n entries.forEach((entry) => {\n if (!entry.isIntersecting) {\n const stayTime = Date.now() - startTime\n const eventName = binding.value\n\n tracker.track(`${eventName}_stay`, {\n stayTime,\n })\n }\n })\n })\n\n observer.observe(el)\n cleanupMap.set(el, () => observer.disconnect())\n }),\n )\n}\n\n/**\n * Vue Mixin\n */\nexport const GCTrackMixin = {\n methods: {\n /**\n * 追踪事件\n */\n $track(this: any, eventType: string, data?: Record<string, any>): void {\n if (this.$tracker) {\n this.$tracker.track(eventType, data)\n }\n },\n\n /**\n * 追踪页面浏览\n */\n $trackPageView(this: any, data?: Record<string, any>): void {\n if (this.$tracker) {\n this.$tracker.track('page_view', data)\n }\n },\n },\n}\n\n/**\n * 类型声明(Vue2 / Vue3 双端增强)\n */\n// @ts-ignore vue@3 无 vue/types/vue 子路径;此处增强对 Vue2 消费者侧仍生效,vue@3 构建时压制报错\ndeclare module 'vue/types/vue' {\n interface Vue {\n $tracker: Tracker\n $track: (eventType: string, data?: Record<string, any>) => void\n $trackPageView: (data?: Record<string, any>) => void\n }\n}\n\ndeclare module 'vue' {\n interface ComponentCustomProperties {\n $tracker: Tracker\n $track: (eventType: string, data?: Record<string, any>) => void\n $trackPageView: (data?: Record<string, any>) => void\n }\n}\n"],"mappings":";;;;;;AAiBA,SAAS,OAAOA,UAAwB;AACtC,eAAc,aAAa;AAC5B;;;;AAKD,SAAS,iBAAiBC,IAAiB;AACzC,MAAK,GAAI,QAAO;CAChB,MAAM,OAAQ,GAAG,YAAY,GAAG,SAAS,QAAU,GAAG,QAAQ,GAAG,KAAK;AACtE,QAAO,QAAQ;AAChB;;;;AAKD,MAAa,gBAAgB,EAC3B,QAAQD,UAAeE,SAAiC;CACtD,MAAM,EAAE,SAAS,QAAQ,gBAAgB,MAAM,cAAc,MAAM,GAAG;CACtE,MAAM,KAAK,OAAO,SAAS;CAC3B,MAAM,SAAS,SAAS;AAGxB,KAAI,GACF,UAAS,UAAU,WAAW;KAE9B,QAAO,iBAAiB,WAAW;AAIrC,KAAI,UAAU,cACZ,QAAO,UAAU,CAACC,IAASC,SAAc;AACvC,UAAQ,MAAM,aAAa;GACzB,MAAM,KAAK;GACX,IAAI,GAAG;GACP,OAAQ,GAAG,QAAQ,GAAG,KAAK,iBAAkB,aAAa,cAAc,SAAS,QAAQ;GACzF,OAAO,GAAG;GACV,QAAQ,GAAG;EACZ,EAAC;CACH,EAAC;AAIJ,KAAI,YACF,QAAO,eAAe,CAACC,KAAYJ,IAASK,SAAiB;AAC3D,UAAQ,MAAM,cAAc,IAAI;AAEhC,UAAQ,MAAM,aAAa;GACzB,SAAS,IAAI;GACb,OAAO,IAAI;GACX,eAAe,iBAAiB,GAAG;GACnC;GACA,WAAW,KAAK,KAAK;EACtB,EAAC;CACH;AAIH,oBAAmB,UAAU,SAAS,GAAG;AAC1C,EACF;;;;;AAMD,MAAM,6BAAa,IAAI;;;;;AAMvB,SAAS,mBACPC,IACAC,OACK;CACL,MAAM,UAAU,CAACC,OAAoB;EACnC,MAAM,UAAU,WAAW,IAAI,GAAG;AAClC,MAAI,SAAS;AACX,YAAS;AACT,cAAW,OAAO,GAAG;EACtB;CACF;AACD,QAAO,KAAK;EAAE,MAAM;EAAO,QAAQ;CAAS,IAAG;EAAE,SAAS;EAAO,WAAW;CAAS;AACtF;;;;AAKD,SAAS,mBAAmBT,UAAeU,SAAkBH,IAAmB;AAE9E,UAAS,UACP,SACA,mBAAmB,IAAI,CAACE,IAAiBE,YAAiB;EACxD,MAAM,UAAU,MAAM;GACpB,MAAM,YAAY,QAAQ;GAC1B,MAAM,YAAY,QAAQ,MAAM,GAAG,QAAQ,MAAM,QAAQ,UAAW,IAAG,CAAE;AACzE,WAAQ,MAAM,WAAW,UAAU;EACpC;AACD,KAAG,iBAAiB,SAAS,QAAQ;AACrC,aAAW,IAAI,IAAI,MAAM,GAAG,oBAAoB,SAAS,QAAQ,CAAC;CACnE,EAAC,CACH;AAGD,UAAS,UACP,cACA,mBAAmB,IAAI,CAACF,IAAiBE,YAAiB;AACxD,aAAW,yBAAyB,YAClC;EAGF,MAAM,WAAW,IAAI,qBAAqB,CAAC,YAAY;AACrD,WAAQ,QAAQ,CAAC,UAAU;AACzB,QAAI,MAAM,gBAAgB;KACxB,MAAM,YAAY,QAAQ;AAC1B,aAAQ,OAAO,EAAE,UAAU,QAAQ,EACjC,SAAS,GAAG,QACb,EAAC;AAEF,cAAS,UAAU,GAAG;IACvB;GACF,EAAC;EACH;AAED,WAAS,QAAQ,GAAG;AACpB,aAAW,IAAI,IAAI,MAAM,SAAS,YAAY,CAAC;CAChD,EAAC,CACH;AAGD,UAAS,UACP,cACA,mBAAmB,IAAI,CAACF,IAAiBE,YAAiB;AACxD,aAAW,yBAAyB,YAClC;EAGF,MAAM,YAAY,KAAK,KAAK;EAE5B,MAAM,WAAW,IAAI,qBAAqB,CAAC,YAAY;AACrD,WAAQ,QAAQ,CAAC,UAAU;AACzB,SAAK,MAAM,gBAAgB;KACzB,MAAM,WAAW,KAAK,KAAK,GAAG;KAC9B,MAAM,YAAY,QAAQ;AAE1B,aAAQ,OAAO,EAAE,UAAU,QAAQ,EACjC,SACD,EAAC;IACH;GACF,EAAC;EACH;AAED,WAAS,QAAQ,GAAG;AACpB,aAAW,IAAI,IAAI,MAAM,SAAS,YAAY,CAAC;CAChD,EAAC,CACH;AACF;;;;AAKD,MAAa,eAAe,EAC1B,SAAS;CAIP,OAAkBC,WAAmBC,MAAkC;AACrE,MAAI,KAAK,SACP,MAAK,SAAS,MAAM,WAAW,KAAK;CAEvC;CAKD,eAA0BA,MAAkC;AAC1D,MAAI,KAAK,SACP,MAAK,SAAS,MAAM,aAAa,KAAK;CAEzC;AACF,EACF"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gctrack/vue",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Vue plugin for GCTrack",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -28,11 +28,19 @@
|
|
|
28
28
|
"access": "public"
|
|
29
29
|
},
|
|
30
30
|
"peerDependencies": {
|
|
31
|
-
"vue": "^2.6.0",
|
|
32
|
-
"@gctrack/core": "0.
|
|
31
|
+
"vue": "^2.6.0 || ^3.0.0",
|
|
32
|
+
"@gctrack/core": "0.2.0"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@vue/test-utils": "^2.4.0",
|
|
36
|
+
"happy-dom": "^15.0.0",
|
|
37
|
+
"vue": "^3.4.0",
|
|
38
|
+
"vitest": "^2.0.0",
|
|
39
|
+
"@gctrack/core": "0.2.0"
|
|
33
40
|
},
|
|
34
41
|
"scripts": {
|
|
35
42
|
"build": "tsdown",
|
|
36
|
-
"dev": "tsdown --watch"
|
|
43
|
+
"dev": "tsdown --watch",
|
|
44
|
+
"test": "vitest run"
|
|
37
45
|
}
|
|
38
46
|
}
|