performance-helper 1.0.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/README.md +349 -0
- package/dist/core/error.d.ts +35 -0
- package/dist/core/error.js +102 -0
- package/dist/core/performance.d.ts +67 -0
- package/dist/core/performance.js +377 -0
- package/dist/core/reporter.d.ts +38 -0
- package/dist/core/reporter.js +105 -0
- package/dist/core/resource.d.ts +35 -0
- package/dist/core/resource.js +107 -0
- package/dist/index.d.ts +67 -0
- package/dist/index.js +192 -0
- package/dist/types/index.d.ts +134 -0
- package/dist/types/index.js +1 -0
- package/dist/utils/index.d.ts +52 -0
- package/dist/utils/index.js +197 -0
- package/package.json +30 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { PerformanceHelperOptions, PerformanceMetrics, ResourceInfo, ErrorInfo, ReportData, LongTaskInfo, LongTaskAttribution } from './types';
|
|
2
|
+
import { HighlightOptions } from './utils/index';
|
|
3
|
+
/**
|
|
4
|
+
* 性能监控 SDK
|
|
5
|
+
*/
|
|
6
|
+
export declare class PerformanceHelper {
|
|
7
|
+
private options;
|
|
8
|
+
private reporter;
|
|
9
|
+
private performanceCollector;
|
|
10
|
+
private resourceMonitor;
|
|
11
|
+
private errorMonitor;
|
|
12
|
+
private initialized;
|
|
13
|
+
constructor(options: PerformanceHelperOptions);
|
|
14
|
+
/**
|
|
15
|
+
* 初始化 SDK
|
|
16
|
+
*/
|
|
17
|
+
init(): void;
|
|
18
|
+
/**
|
|
19
|
+
* 采集并上报性能指标
|
|
20
|
+
*/
|
|
21
|
+
private collectAndReportPerformance;
|
|
22
|
+
/**
|
|
23
|
+
* 手动上报性能指标
|
|
24
|
+
*/
|
|
25
|
+
reportPerformance(): void;
|
|
26
|
+
/**
|
|
27
|
+
* 获取性能指标
|
|
28
|
+
*/
|
|
29
|
+
getPerformanceMetrics(): PerformanceMetrics;
|
|
30
|
+
/**
|
|
31
|
+
* 框出 LCP 元素(用于调试)
|
|
32
|
+
* 会在 LCP 元素周围添加高亮边框
|
|
33
|
+
* @param options 高亮选项
|
|
34
|
+
* @returns 是否成功框出元素
|
|
35
|
+
*/
|
|
36
|
+
highlightLCPElement(options?: HighlightOptions): boolean;
|
|
37
|
+
/**
|
|
38
|
+
* 移除 LCP 元素的高亮
|
|
39
|
+
*/
|
|
40
|
+
removeLCPHighlight(): void;
|
|
41
|
+
/**
|
|
42
|
+
* 获取资源信息
|
|
43
|
+
*/
|
|
44
|
+
getResources(): ResourceInfo[];
|
|
45
|
+
/**
|
|
46
|
+
* 获取慢资源
|
|
47
|
+
*/
|
|
48
|
+
getSlowResources(threshold?: number): ResourceInfo[];
|
|
49
|
+
/**
|
|
50
|
+
* 获取错误信息
|
|
51
|
+
*/
|
|
52
|
+
getErrors(): ErrorInfo[];
|
|
53
|
+
/**
|
|
54
|
+
* 手动上报错误
|
|
55
|
+
*/
|
|
56
|
+
reportError(error: Error, context?: Record<string, any>): void;
|
|
57
|
+
/**
|
|
58
|
+
* 上报自定义数据
|
|
59
|
+
*/
|
|
60
|
+
reportCustom(type: string, data: any): void;
|
|
61
|
+
/**
|
|
62
|
+
* 销毁 SDK
|
|
63
|
+
*/
|
|
64
|
+
destroy(): void;
|
|
65
|
+
}
|
|
66
|
+
export type { PerformanceHelperOptions, PerformanceMetrics, ResourceInfo, ErrorInfo, ReportData, LongTaskInfo, LongTaskAttribution };
|
|
67
|
+
export default PerformanceHelper;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { PerformanceCollector } from './core/performance';
|
|
2
|
+
import { ResourceMonitor } from './core/resource';
|
|
3
|
+
import { ErrorMonitor } from './core/error';
|
|
4
|
+
import { Reporter } from './core/reporter';
|
|
5
|
+
import { querySelectorByPath, highlightElement, removeElementHighlight } from './utils/index';
|
|
6
|
+
/**
|
|
7
|
+
* 性能监控 SDK
|
|
8
|
+
*/
|
|
9
|
+
export class PerformanceHelper {
|
|
10
|
+
constructor(options) {
|
|
11
|
+
this.initialized = false;
|
|
12
|
+
if (!options.reportUrl) {
|
|
13
|
+
throw new Error('reportUrl is required');
|
|
14
|
+
}
|
|
15
|
+
this.options = {
|
|
16
|
+
immediate: false,
|
|
17
|
+
monitorResources: true,
|
|
18
|
+
monitorErrors: false,
|
|
19
|
+
monitorPerformance: true,
|
|
20
|
+
sampleRate: 1,
|
|
21
|
+
...options,
|
|
22
|
+
};
|
|
23
|
+
this.reporter = new Reporter(this.options);
|
|
24
|
+
this.performanceCollector = new PerformanceCollector();
|
|
25
|
+
this.resourceMonitor = new ResourceMonitor();
|
|
26
|
+
this.errorMonitor = new ErrorMonitor();
|
|
27
|
+
// 立即初始化性能观察器(需要在页面加载前设置)
|
|
28
|
+
if (this.options.monitorPerformance) {
|
|
29
|
+
this.performanceCollector.init();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* 初始化 SDK
|
|
34
|
+
*/
|
|
35
|
+
init() {
|
|
36
|
+
if (this.initialized) {
|
|
37
|
+
console.warn('PerformanceHelper has already been initialized');
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
// 采样率检查
|
|
41
|
+
if (Math.random() > (this.options.sampleRate || 1)) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
// 启动监控
|
|
45
|
+
if (this.options.monitorResources) {
|
|
46
|
+
this.resourceMonitor.start();
|
|
47
|
+
}
|
|
48
|
+
if (this.options.monitorErrors) {
|
|
49
|
+
this.errorMonitor.start();
|
|
50
|
+
}
|
|
51
|
+
if (this.options.monitorPerformance) {
|
|
52
|
+
// 等待页面加载完成后采集性能指标
|
|
53
|
+
if (document.readyState === 'complete') {
|
|
54
|
+
this.collectAndReportPerformance();
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
window.addEventListener('load', () => {
|
|
58
|
+
// 延迟采集,确保所有指标都已计算完成
|
|
59
|
+
setTimeout(() => {
|
|
60
|
+
this.collectAndReportPerformance();
|
|
61
|
+
}, 2000);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
this.initialized = true;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* 采集并上报性能指标
|
|
69
|
+
*/
|
|
70
|
+
collectAndReportPerformance() {
|
|
71
|
+
const metrics = this.performanceCollector.collect();
|
|
72
|
+
this.reporter.report({
|
|
73
|
+
type: 'performance',
|
|
74
|
+
data: metrics,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* 手动上报性能指标
|
|
79
|
+
*/
|
|
80
|
+
reportPerformance() {
|
|
81
|
+
const metrics = this.performanceCollector.collect();
|
|
82
|
+
this.reporter.report({
|
|
83
|
+
type: 'performance',
|
|
84
|
+
data: metrics,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* 获取性能指标
|
|
89
|
+
*/
|
|
90
|
+
getPerformanceMetrics() {
|
|
91
|
+
return this.performanceCollector.collect();
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* 框出 LCP 元素(用于调试)
|
|
95
|
+
* 会在 LCP 元素周围添加高亮边框
|
|
96
|
+
* @param options 高亮选项
|
|
97
|
+
* @returns 是否成功框出元素
|
|
98
|
+
*/
|
|
99
|
+
highlightLCPElement(options) {
|
|
100
|
+
const metrics = this.getPerformanceMetrics();
|
|
101
|
+
const lcpElementPath = metrics.lcpElement;
|
|
102
|
+
if (!lcpElementPath) {
|
|
103
|
+
console.warn('LCP element path not found');
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
// 根据路径查找元素
|
|
107
|
+
const element = querySelectorByPath(lcpElementPath);
|
|
108
|
+
if (!element) {
|
|
109
|
+
console.warn(`LCP element not found: ${lcpElementPath}`);
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
// 使用工具函数高亮元素
|
|
113
|
+
const removeHighlight = highlightElement(element, options);
|
|
114
|
+
if (!removeHighlight) {
|
|
115
|
+
console.warn(`LCP element is not an HTMLElement: ${lcpElementPath}`);
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
// 保存移除函数到元素上(方便调试)
|
|
119
|
+
element.__removeLCPHighlight = removeHighlight;
|
|
120
|
+
console.log('LCP element highlighted:', element, lcpElementPath);
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* 移除 LCP 元素的高亮
|
|
125
|
+
*/
|
|
126
|
+
removeLCPHighlight() {
|
|
127
|
+
const metrics = this.getPerformanceMetrics();
|
|
128
|
+
const lcpElementPath = metrics.lcpElement;
|
|
129
|
+
if (!lcpElementPath) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const element = querySelectorByPath(lcpElementPath);
|
|
133
|
+
if (element) {
|
|
134
|
+
removeElementHighlight(element);
|
|
135
|
+
// 同时清理 LCP 特定的引用
|
|
136
|
+
if (element.__removeLCPHighlight) {
|
|
137
|
+
delete element.__removeLCPHighlight;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* 获取资源信息
|
|
143
|
+
*/
|
|
144
|
+
getResources() {
|
|
145
|
+
return this.resourceMonitor.getResources();
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* 获取慢资源
|
|
149
|
+
*/
|
|
150
|
+
getSlowResources(threshold) {
|
|
151
|
+
return this.resourceMonitor.getSlowResources(threshold);
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* 获取错误信息
|
|
155
|
+
*/
|
|
156
|
+
getErrors() {
|
|
157
|
+
return this.errorMonitor.getErrors();
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* 手动上报错误
|
|
161
|
+
*/
|
|
162
|
+
reportError(error, context) {
|
|
163
|
+
this.errorMonitor.reportError(error, context);
|
|
164
|
+
const errors = this.errorMonitor.getErrors();
|
|
165
|
+
const lastError = errors[errors.length - 1];
|
|
166
|
+
if (lastError) {
|
|
167
|
+
this.reporter.report({
|
|
168
|
+
type: 'error',
|
|
169
|
+
data: lastError,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* 上报自定义数据
|
|
175
|
+
*/
|
|
176
|
+
reportCustom(type, data) {
|
|
177
|
+
this.reporter.report({
|
|
178
|
+
type: type,
|
|
179
|
+
data: data,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* 销毁 SDK
|
|
184
|
+
*/
|
|
185
|
+
destroy() {
|
|
186
|
+
this.reporter.destroy();
|
|
187
|
+
this.performanceCollector.destroy();
|
|
188
|
+
this.initialized = false;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
// 默认导出
|
|
192
|
+
export default PerformanceHelper;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SDK 配置选项
|
|
3
|
+
*/
|
|
4
|
+
export interface PerformanceHelperOptions {
|
|
5
|
+
/** 上报地址 */
|
|
6
|
+
reportUrl: string;
|
|
7
|
+
/** 应用ID */
|
|
8
|
+
appId?: string;
|
|
9
|
+
/** 用户ID */
|
|
10
|
+
userId?: string;
|
|
11
|
+
/** 是否立即上报,默认false(批量上报) */
|
|
12
|
+
immediate?: boolean;
|
|
13
|
+
/** 批量上报间隔(毫秒),默认5000 */
|
|
14
|
+
batchInterval?: number;
|
|
15
|
+
/** 是否监控资源加载,默认true */
|
|
16
|
+
monitorResources?: boolean;
|
|
17
|
+
/** 是否监控错误,默认true */
|
|
18
|
+
monitorErrors?: boolean;
|
|
19
|
+
/** 是否监控性能指标,默认true */
|
|
20
|
+
monitorPerformance?: boolean;
|
|
21
|
+
/** 采样率 0-1,默认1 */
|
|
22
|
+
sampleRate?: number;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* 长任务归因信息
|
|
26
|
+
*/
|
|
27
|
+
export interface LongTaskAttribution {
|
|
28
|
+
/** 任务类型:script, layout, style, paint, composite, other */
|
|
29
|
+
taskType: string;
|
|
30
|
+
/** 容器类型:window, iframe, embed, object */
|
|
31
|
+
containerType?: string;
|
|
32
|
+
/** 容器名称 */
|
|
33
|
+
containerName?: string;
|
|
34
|
+
/** 容器ID */
|
|
35
|
+
containerId?: string;
|
|
36
|
+
/** 容器源(URL) */
|
|
37
|
+
containerSrc?: string;
|
|
38
|
+
/** 任务名称 */
|
|
39
|
+
name?: string;
|
|
40
|
+
/** 脚本URL(如果是脚本执行导致的长任务) */
|
|
41
|
+
scriptURL?: string;
|
|
42
|
+
/** 相关DOM元素(如果是DOM操作导致的长任务) */
|
|
43
|
+
element?: Element;
|
|
44
|
+
/** 元素路径(CSS选择器路径) */
|
|
45
|
+
elementPath?: string;
|
|
46
|
+
/** 元素标签名 */
|
|
47
|
+
elementTag?: string;
|
|
48
|
+
/** 元素ID */
|
|
49
|
+
elementId?: string;
|
|
50
|
+
/** 元素类名 */
|
|
51
|
+
elementClass?: string;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* 长任务详细信息
|
|
55
|
+
*/
|
|
56
|
+
export interface LongTaskInfo {
|
|
57
|
+
/** 开始时间(相对于页面加载) */
|
|
58
|
+
startTime: number;
|
|
59
|
+
/** 持续时间(毫秒) */
|
|
60
|
+
duration: number;
|
|
61
|
+
/** 归因信息数组 */
|
|
62
|
+
attribution: LongTaskAttribution[];
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* 性能指标数据
|
|
66
|
+
*/
|
|
67
|
+
export interface PerformanceMetrics {
|
|
68
|
+
/** First Contentful Paint */
|
|
69
|
+
fcp?: number;
|
|
70
|
+
/** Largest Contentful Paint */
|
|
71
|
+
lcp?: number;
|
|
72
|
+
/** LCP 元素路径 */
|
|
73
|
+
lcpElement?: string;
|
|
74
|
+
/** First Input Delay */
|
|
75
|
+
fid?: number;
|
|
76
|
+
/** Cumulative Layout Shift */
|
|
77
|
+
cls?: number;
|
|
78
|
+
/** Total Blocking Time */
|
|
79
|
+
tbt?: number;
|
|
80
|
+
/** Speed Index */
|
|
81
|
+
speedIndex?: number;
|
|
82
|
+
/** 长任务列表 */
|
|
83
|
+
longTasks?: LongTaskInfo[];
|
|
84
|
+
/** Time to First Byte */
|
|
85
|
+
ttfb?: number;
|
|
86
|
+
/** DOM Content Loaded */
|
|
87
|
+
domContentLoaded?: number;
|
|
88
|
+
/** Load 完成时间 */
|
|
89
|
+
load?: number;
|
|
90
|
+
/** DNS 查询时间 */
|
|
91
|
+
dns?: number;
|
|
92
|
+
/** TCP 连接时间 */
|
|
93
|
+
tcp?: number;
|
|
94
|
+
/** 请求响应时间 */
|
|
95
|
+
request?: number;
|
|
96
|
+
/** 页面解析时间 */
|
|
97
|
+
parse?: number;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* 资源加载信息
|
|
101
|
+
*/
|
|
102
|
+
export interface ResourceInfo {
|
|
103
|
+
name: string;
|
|
104
|
+
type: string;
|
|
105
|
+
duration: number;
|
|
106
|
+
size: number;
|
|
107
|
+
startTime: number;
|
|
108
|
+
url: string;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* 错误信息
|
|
112
|
+
*/
|
|
113
|
+
export interface ErrorInfo {
|
|
114
|
+
message: string;
|
|
115
|
+
source?: string;
|
|
116
|
+
lineno?: number;
|
|
117
|
+
colno?: number;
|
|
118
|
+
stack?: string;
|
|
119
|
+
timestamp: number;
|
|
120
|
+
url: string;
|
|
121
|
+
userAgent: string;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* 上报数据
|
|
125
|
+
*/
|
|
126
|
+
export interface ReportData {
|
|
127
|
+
type: 'performance' | 'resource' | 'error';
|
|
128
|
+
data: PerformanceMetrics | ResourceInfo | ErrorInfo;
|
|
129
|
+
timestamp: number;
|
|
130
|
+
url: string;
|
|
131
|
+
userAgent: string;
|
|
132
|
+
appId?: string;
|
|
133
|
+
userId?: string;
|
|
134
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 工具函数集合
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* 获取元素的DOM路径
|
|
6
|
+
* 生成类似 "html > body > div#app > div.container > img" 的路径字符串
|
|
7
|
+
*
|
|
8
|
+
* @param element - DOM元素
|
|
9
|
+
* @returns 元素的完整DOM路径字符串
|
|
10
|
+
*/
|
|
11
|
+
export declare function getElementPath(element: Element | null): string;
|
|
12
|
+
/**
|
|
13
|
+
* 根据 CSS 选择器路径查找元素
|
|
14
|
+
* @param path CSS 选择器路径,例如 "body > div#app > img"
|
|
15
|
+
* @returns 找到的元素,如果未找到返回 null
|
|
16
|
+
*/
|
|
17
|
+
export declare function querySelectorByPath(path: string): Element | null;
|
|
18
|
+
/**
|
|
19
|
+
* 检查元素是否匹配选择器
|
|
20
|
+
*/
|
|
21
|
+
export declare function matchesSelector(element: Element, selector: string): boolean;
|
|
22
|
+
/**
|
|
23
|
+
* 简单的选择器匹配(降级方案)
|
|
24
|
+
*/
|
|
25
|
+
export declare function simpleMatches(element: Element, selector: string): boolean;
|
|
26
|
+
/**
|
|
27
|
+
* 元素高亮选项
|
|
28
|
+
*/
|
|
29
|
+
export interface HighlightOptions {
|
|
30
|
+
/** 边框颜色,默认红色 */
|
|
31
|
+
borderColor?: string;
|
|
32
|
+
/** 边框宽度,默认 3px */
|
|
33
|
+
borderWidth?: string;
|
|
34
|
+
/** 背景颜色,默认透明黄色 */
|
|
35
|
+
backgroundColor?: string;
|
|
36
|
+
/** 是否滚动到元素位置,默认 true */
|
|
37
|
+
scrollIntoView?: boolean;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* 高亮元素
|
|
41
|
+
* 在元素周围添加高亮边框和背景色
|
|
42
|
+
* @param element - 要高亮的 HTML 元素
|
|
43
|
+
* @param options - 高亮选项
|
|
44
|
+
* @returns 移除高亮的函数,如果元素不是 HTMLElement 则返回 null
|
|
45
|
+
*/
|
|
46
|
+
export declare function highlightElement(element: Element, options?: HighlightOptions): (() => void) | null;
|
|
47
|
+
/**
|
|
48
|
+
* 移除元素高亮
|
|
49
|
+
* 如果元素上有保存的移除函数,则调用它
|
|
50
|
+
* @param element - 要移除高亮的元素
|
|
51
|
+
*/
|
|
52
|
+
export declare function removeElementHighlight(element: Element): void;
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 工具函数集合
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* 获取元素的DOM路径
|
|
6
|
+
* 生成类似 "html > body > div#app > div.container > img" 的路径字符串
|
|
7
|
+
*
|
|
8
|
+
* @param element - DOM元素
|
|
9
|
+
* @returns 元素的完整DOM路径字符串
|
|
10
|
+
*/
|
|
11
|
+
export function getElementPath(element) {
|
|
12
|
+
if (!element) {
|
|
13
|
+
return '';
|
|
14
|
+
}
|
|
15
|
+
const path = [];
|
|
16
|
+
let current = element;
|
|
17
|
+
while (current && current.nodeType === Node.ELEMENT_NODE) {
|
|
18
|
+
let selector = current.tagName.toLowerCase();
|
|
19
|
+
// 添加ID
|
|
20
|
+
if (current.id) {
|
|
21
|
+
selector += `#${current.id}`;
|
|
22
|
+
}
|
|
23
|
+
// 添加类名(最多取前3个,避免路径过长)
|
|
24
|
+
if (current.className && typeof current.className === 'string') {
|
|
25
|
+
const classes = current.className
|
|
26
|
+
.trim()
|
|
27
|
+
.split(/\s+/)
|
|
28
|
+
.filter((cls) => cls)
|
|
29
|
+
.slice(0, 3);
|
|
30
|
+
if (classes.length > 0) {
|
|
31
|
+
selector += `.${classes.join('.')}`;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
// 如果没有ID和类名,添加索引
|
|
35
|
+
if (!current.id && (!current.className || current.className.trim() === '')) {
|
|
36
|
+
const parent = current.parentElement;
|
|
37
|
+
if (parent) {
|
|
38
|
+
const siblings = Array.from(parent.children);
|
|
39
|
+
const index = siblings.indexOf(current);
|
|
40
|
+
if (index >= 0) {
|
|
41
|
+
selector += `:nth-child(${index + 1})`;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
path.unshift(selector);
|
|
46
|
+
// 向上遍历到html为止(包括body和html)
|
|
47
|
+
current = current.parentElement;
|
|
48
|
+
if (current && current.tagName === 'HTML') {
|
|
49
|
+
// 如果当前是html,已经添加到路径,停止遍历
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return path.join(' > ');
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* 根据 CSS 选择器路径查找元素
|
|
57
|
+
* @param path CSS 选择器路径,例如 "body > div#app > img"
|
|
58
|
+
* @returns 找到的元素,如果未找到返回 null
|
|
59
|
+
*/
|
|
60
|
+
export function querySelectorByPath(path) {
|
|
61
|
+
if (!path) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
// 尝试直接使用 querySelector
|
|
66
|
+
const element = document.querySelector(path);
|
|
67
|
+
if (element) {
|
|
68
|
+
return element;
|
|
69
|
+
}
|
|
70
|
+
// 如果直接查询失败,尝试分段查找
|
|
71
|
+
// 路径格式通常是 "html > body > div#id.class > img"
|
|
72
|
+
const parts = path.split(' > ');
|
|
73
|
+
let current = document.documentElement;
|
|
74
|
+
for (const part of parts) {
|
|
75
|
+
if (!current) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
// 尝试在当前元素下查找
|
|
79
|
+
const found = current.querySelector(part);
|
|
80
|
+
if (found && current.contains(found)) {
|
|
81
|
+
current = found;
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
// 如果查询失败,尝试直接匹配当前元素
|
|
85
|
+
if (matchesSelector(current, part)) {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return current;
|
|
92
|
+
}
|
|
93
|
+
catch (e) {
|
|
94
|
+
console.error('Error querying element by path:', path, e);
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* 检查元素是否匹配选择器
|
|
100
|
+
*/
|
|
101
|
+
export function matchesSelector(element, selector) {
|
|
102
|
+
try {
|
|
103
|
+
return element.matches(selector);
|
|
104
|
+
}
|
|
105
|
+
catch (e) {
|
|
106
|
+
// 降级方案:手动解析简单的选择器
|
|
107
|
+
return simpleMatches(element, selector);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* 简单的选择器匹配(降级方案)
|
|
112
|
+
*/
|
|
113
|
+
export function simpleMatches(element, selector) {
|
|
114
|
+
// 移除空格
|
|
115
|
+
selector = selector.trim();
|
|
116
|
+
// 检查标签名
|
|
117
|
+
if (selector.includes('#')) {
|
|
118
|
+
const [tag, id] = selector.split('#');
|
|
119
|
+
if (tag && element.tagName.toLowerCase() !== tag.toLowerCase()) {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
if (id && element.id !== id.split('.')[0]) {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
else if (selector.includes('.')) {
|
|
127
|
+
const [tag, classes] = selector.split('.');
|
|
128
|
+
if (tag && element.tagName.toLowerCase() !== tag.toLowerCase()) {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
if (classes) {
|
|
132
|
+
const classList = classes.split('.');
|
|
133
|
+
for (const cls of classList) {
|
|
134
|
+
if (cls && !element.classList.contains(cls)) {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
if (element.tagName.toLowerCase() !== selector.toLowerCase()) {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* 高亮元素
|
|
149
|
+
* 在元素周围添加高亮边框和背景色
|
|
150
|
+
* @param element - 要高亮的 HTML 元素
|
|
151
|
+
* @param options - 高亮选项
|
|
152
|
+
* @returns 移除高亮的函数,如果元素不是 HTMLElement 则返回 null
|
|
153
|
+
*/
|
|
154
|
+
export function highlightElement(element, options) {
|
|
155
|
+
if (!(element instanceof HTMLElement)) {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
// 设置高亮样式
|
|
159
|
+
const { borderColor = '#ff0000', borderWidth = '3px', backgroundColor = 'rgba(255, 255, 0, 0.2)', scrollIntoView = true, } = options || {};
|
|
160
|
+
// 保存原始样式
|
|
161
|
+
const originalOutline = element.style.outline;
|
|
162
|
+
const originalBoxShadow = element.style.boxShadow;
|
|
163
|
+
const originalBackgroundColor = element.style.backgroundColor;
|
|
164
|
+
const originalZIndex = element.style.zIndex;
|
|
165
|
+
const originalPosition = element.style.position;
|
|
166
|
+
// 应用高亮样式
|
|
167
|
+
element.style.outline = `${borderWidth} solid ${borderColor}`;
|
|
168
|
+
element.style.boxShadow = `0 0 0 ${borderWidth} ${borderColor}`;
|
|
169
|
+
element.style.backgroundColor = backgroundColor;
|
|
170
|
+
element.style.zIndex = '9999';
|
|
171
|
+
if (getComputedStyle(element).position === 'static') {
|
|
172
|
+
element.style.position = 'relative';
|
|
173
|
+
}
|
|
174
|
+
// 滚动到元素位置
|
|
175
|
+
if (scrollIntoView) {
|
|
176
|
+
element.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
177
|
+
}
|
|
178
|
+
// 返回移除高亮的函数
|
|
179
|
+
return () => {
|
|
180
|
+
element.style.outline = originalOutline;
|
|
181
|
+
element.style.boxShadow = originalBoxShadow;
|
|
182
|
+
element.style.backgroundColor = originalBackgroundColor;
|
|
183
|
+
element.style.zIndex = originalZIndex;
|
|
184
|
+
element.style.position = originalPosition;
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* 移除元素高亮
|
|
189
|
+
* 如果元素上有保存的移除函数,则调用它
|
|
190
|
+
* @param element - 要移除高亮的元素
|
|
191
|
+
*/
|
|
192
|
+
export function removeElementHighlight(element) {
|
|
193
|
+
if (element instanceof HTMLElement && element.__removeHighlight) {
|
|
194
|
+
element.__removeHighlight();
|
|
195
|
+
delete element.__removeHighlight;
|
|
196
|
+
}
|
|
197
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "performance-helper",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "前端性能监控 SDK",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"dev": "tsc --watch",
|
|
10
|
+
"prepublishOnly": "npm run build",
|
|
11
|
+
"serve": "node examples/server.js",
|
|
12
|
+
"start": "npm run build && npm run serve"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"performance",
|
|
16
|
+
"monitoring",
|
|
17
|
+
"sdk",
|
|
18
|
+
"frontend"
|
|
19
|
+
],
|
|
20
|
+
"author": "",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/node": "^20.10.0",
|
|
24
|
+
"typescript": "^5.3.3"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist"
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
|