@blueking/open-telemetry 0.0.1
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 +0 -0
- package/dist/bk-rum.global.js +11 -0
- package/dist/bk-rum.global.js.map +1 -0
- package/dist/index.js +1186 -0
- package/dist/index.js.map +1 -0
- package/package.json +49 -0
- package/playground/App.vue +148 -0
- package/playground/bk-ot.ts +110 -0
- package/playground/components/DemoLogPanel.vue +89 -0
- package/playground/composables/use-demo-log.ts +39 -0
- package/playground/index.html +13 -0
- package/playground/main.ts +36 -0
- package/playground/mock/index.ts +43 -0
- package/playground/pages/BlankScreenDemo.vue +97 -0
- package/playground/pages/BusinessDemo.vue +74 -0
- package/playground/pages/CustomPluginDemo.vue +104 -0
- package/playground/pages/ErrorDemo.vue +75 -0
- package/playground/pages/Home.vue +133 -0
- package/playground/pages/HttpDemo.vue +120 -0
- package/playground/pages/LongTaskDemo.vue +66 -0
- package/playground/pages/WebSocketDemo.vue +100 -0
- package/playground/router/index.ts +92 -0
- package/playground/style.css +180 -0
- package/src/browser.ts +44 -0
- package/src/core/config.ts +327 -0
- package/src/core/plugin.ts +97 -0
- package/src/core/processor.ts +74 -0
- package/src/core/route-observer.ts +128 -0
- package/src/core/sampling.ts +64 -0
- package/src/core/sdk.ts +327 -0
- package/src/core/url.ts +66 -0
- package/src/index.ts +55 -0
- package/src/plugins/blank-screen.ts +170 -0
- package/src/plugins/common.ts +131 -0
- package/src/plugins/csp-violation.ts +74 -0
- package/src/plugins/device.ts +91 -0
- package/src/plugins/error.ts +211 -0
- package/src/plugins/http-body.ts +437 -0
- package/src/plugins/long-task.ts +99 -0
- package/src/plugins/page-view.ts +83 -0
- package/src/plugins/route-timing.ts +89 -0
- package/src/plugins/session.ts +127 -0
- package/src/plugins/web-vitals.ts +159 -0
- package/src/plugins/websocket.ts +179 -0
- package/tsconfig.app.json +27 -0
- package/tsconfig.dts.json +13 -0
- package/tsconfig.json +7 -0
- package/tsconfig.node.json +26 -0
- package/vite.config.ts +70 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<section class="demo-page">
|
|
3
|
+
<header>
|
|
4
|
+
<h2 class="demo-page__title">@blueking/open-telemetry · Playground</h2>
|
|
5
|
+
<p class="demo-page__desc">
|
|
6
|
+
本 playground 演示 SDK 全部内置插件与扩展点的实际用法。打开浏览器控制台即可看到 trace / log / metric
|
|
7
|
+
三类信号的完整产物。
|
|
8
|
+
</p>
|
|
9
|
+
</header>
|
|
10
|
+
|
|
11
|
+
<div class="demo-card">
|
|
12
|
+
<h3 class="demo-card__title">SDK 运行时状态</h3>
|
|
13
|
+
<p class="demo-card__hint">
|
|
14
|
+
通过 <code>BkOTInstance.getRuntimeAttributes()</code> 获取当前所有 span / log 都会携带的公共属性。
|
|
15
|
+
</p>
|
|
16
|
+
<pre class="demo-snippet">{{ runtimeAttrsText }}</pre>
|
|
17
|
+
<div class="demo-actions">
|
|
18
|
+
<button
|
|
19
|
+
class="demo-btn"
|
|
20
|
+
type="button"
|
|
21
|
+
@click="refresh"
|
|
22
|
+
>
|
|
23
|
+
刷新运行时属性
|
|
24
|
+
</button>
|
|
25
|
+
<button
|
|
26
|
+
class="demo-btn demo-btn--ghost"
|
|
27
|
+
type="button"
|
|
28
|
+
@click="forceFlush"
|
|
29
|
+
>
|
|
30
|
+
手动 flush 信号
|
|
31
|
+
</button>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
<div class="demo-card">
|
|
36
|
+
<h3 class="demo-card__title">默认开启的内置插件</h3>
|
|
37
|
+
<ul class="plugin-list">
|
|
38
|
+
<li
|
|
39
|
+
v-for="plugin in plugins"
|
|
40
|
+
:key="plugin.name"
|
|
41
|
+
>
|
|
42
|
+
<strong>{{ plugin.name }}</strong>
|
|
43
|
+
<span>{{ plugin.desc }}</span>
|
|
44
|
+
</li>
|
|
45
|
+
</ul>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<div class="demo-card">
|
|
49
|
+
<h3 class="demo-card__title">最小初始化代码</h3>
|
|
50
|
+
<pre class="demo-snippet">{{ snippet }}</pre>
|
|
51
|
+
</div>
|
|
52
|
+
</section>
|
|
53
|
+
</template>
|
|
54
|
+
|
|
55
|
+
<script setup lang="ts">
|
|
56
|
+
import { shallowRef } from 'vue';
|
|
57
|
+
|
|
58
|
+
import { bkot } from '../bk-ot';
|
|
59
|
+
|
|
60
|
+
const runtimeAttrsText = shallowRef('');
|
|
61
|
+
const refresh = () => {
|
|
62
|
+
runtimeAttrsText.value = JSON.stringify(bkot.getRuntimeAttributes(), null, 2);
|
|
63
|
+
};
|
|
64
|
+
refresh();
|
|
65
|
+
|
|
66
|
+
const forceFlush = async () => {
|
|
67
|
+
await bkot.flush();
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const plugins = [
|
|
71
|
+
{
|
|
72
|
+
name: 'common',
|
|
73
|
+
desc: '基于 OpenTelemetry 官方 instrumentation 自动埋点 fetch/xhr/document-load/user-interaction',
|
|
74
|
+
},
|
|
75
|
+
{ name: 'device', desc: '采集设备 / 浏览器 / 网络环境,作为持久 runtime attributes' },
|
|
76
|
+
{ name: 'session', desc: '维护匿名 session id,闲置 30 分钟后自动滚动' },
|
|
77
|
+
{ name: 'page-view', desc: '监听 SPA 路由变化(pushState / popstate / hashchange)上报 PV' },
|
|
78
|
+
{ name: 'error', desc: '捕获 window.error / unhandledrejection 并按 hash 节流上报' },
|
|
79
|
+
{ name: 'web-vitals', desc: '上报 LCP / CLS / INP / FCP / TTFB 等核心 Web Vitals 指标' },
|
|
80
|
+
{ name: 'blank-screen', desc: '在挂载延时后采样根节点像素分布,识别白屏' },
|
|
81
|
+
{ name: 'http-body', desc: '可选地抓取请求 / 响应体,支持 redact 脱敏' },
|
|
82
|
+
{ name: 'long-task', desc: '监听 PerformanceObserver longtask,超过阈值上报' },
|
|
83
|
+
{ name: 'route-timing', desc: '估算 SPA 路由切换耗时(双 raf + 一个宏任务)' },
|
|
84
|
+
{ name: 'csp-violation', desc: '监听 securitypolicyviolation 事件并上报' },
|
|
85
|
+
{ name: 'websocket', desc: '在 WebSocket 生命周期上挂钩,上报连接 / 消息事件' },
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
const snippet = `import { initBkOT } from '@blueking/open-telemetry';
|
|
89
|
+
|
|
90
|
+
initBkOT({
|
|
91
|
+
endpoint: 'https://your-otlp-collector',
|
|
92
|
+
token: 'your-report-token',
|
|
93
|
+
debug: true,
|
|
94
|
+
rum: {
|
|
95
|
+
fetch: true,
|
|
96
|
+
xhr: true,
|
|
97
|
+
documentLoad: true,
|
|
98
|
+
userInteraction: true,
|
|
99
|
+
longTask: true,
|
|
100
|
+
routeTiming: true,
|
|
101
|
+
},
|
|
102
|
+
});`;
|
|
103
|
+
</script>
|
|
104
|
+
|
|
105
|
+
<style scoped>
|
|
106
|
+
.plugin-list {
|
|
107
|
+
display: grid;
|
|
108
|
+
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
|
109
|
+
gap: var(--space-sm);
|
|
110
|
+
padding: 0;
|
|
111
|
+
margin: 0;
|
|
112
|
+
list-style: none;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.plugin-list li {
|
|
116
|
+
display: flex;
|
|
117
|
+
flex-direction: column;
|
|
118
|
+
gap: 2px;
|
|
119
|
+
padding: var(--space-sm) var(--space-md);
|
|
120
|
+
background: var(--color-bg);
|
|
121
|
+
border-radius: var(--radius-sm);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.plugin-list strong {
|
|
125
|
+
font-family: var(--font-mono);
|
|
126
|
+
color: var(--color-primary);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.plugin-list span {
|
|
130
|
+
font-size: 12px;
|
|
131
|
+
color: var(--color-text-secondary);
|
|
132
|
+
}
|
|
133
|
+
</style>
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<section class="demo-page">
|
|
3
|
+
<header>
|
|
4
|
+
<h2 class="demo-page__title">HTTP 请求 / http-body 插件</h2>
|
|
5
|
+
<p class="demo-page__desc">
|
|
6
|
+
common 插件会为 fetch / xhr 自动产生 client span;http-body 插件会按配置抓取请求与响应体(演示中
|
|
7
|
+
<code>password</code> 字段会被 redact 替换为 <code>***</code>)。
|
|
8
|
+
</p>
|
|
9
|
+
</header>
|
|
10
|
+
|
|
11
|
+
<div class="demo-card">
|
|
12
|
+
<h3 class="demo-card__title">触发请求</h3>
|
|
13
|
+
<p class="demo-card__hint">
|
|
14
|
+
点击按钮后查看控制台 <code>[bk-ot][traces]</code> 中携带 <code>http.*</code> 的 span。
|
|
15
|
+
</p>
|
|
16
|
+
<div class="demo-actions">
|
|
17
|
+
<button
|
|
18
|
+
class="demo-btn"
|
|
19
|
+
:disabled="pending"
|
|
20
|
+
type="button"
|
|
21
|
+
@click="callFetchGet"
|
|
22
|
+
>
|
|
23
|
+
fetch GET
|
|
24
|
+
</button>
|
|
25
|
+
<button
|
|
26
|
+
class="demo-btn"
|
|
27
|
+
:disabled="pending"
|
|
28
|
+
type="button"
|
|
29
|
+
@click="callFetchPost"
|
|
30
|
+
>
|
|
31
|
+
fetch POST(带敏感字段)
|
|
32
|
+
</button>
|
|
33
|
+
<button
|
|
34
|
+
class="demo-btn demo-btn--ghost"
|
|
35
|
+
:disabled="pending"
|
|
36
|
+
type="button"
|
|
37
|
+
@click="callXhr"
|
|
38
|
+
>
|
|
39
|
+
XHR GET
|
|
40
|
+
</button>
|
|
41
|
+
<button
|
|
42
|
+
class="demo-btn demo-btn--warning"
|
|
43
|
+
:disabled="pending"
|
|
44
|
+
type="button"
|
|
45
|
+
@click="callFetchError"
|
|
46
|
+
>
|
|
47
|
+
fetch 404
|
|
48
|
+
</button>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<div class="demo-card">
|
|
53
|
+
<DemoLogPanel
|
|
54
|
+
:entries="entries"
|
|
55
|
+
@clear="clear"
|
|
56
|
+
/>
|
|
57
|
+
</div>
|
|
58
|
+
</section>
|
|
59
|
+
</template>
|
|
60
|
+
|
|
61
|
+
<script setup lang="ts">
|
|
62
|
+
import { shallowRef } from 'vue';
|
|
63
|
+
|
|
64
|
+
import DemoLogPanel from '../components/DemoLogPanel.vue';
|
|
65
|
+
import { useDemoLog } from '../composables/use-demo-log';
|
|
66
|
+
import { httpEndpoints, samplePostBody } from '../mock';
|
|
67
|
+
|
|
68
|
+
const { entries, push, clear } = useDemoLog();
|
|
69
|
+
const pending = shallowRef(false);
|
|
70
|
+
|
|
71
|
+
const wrap = async (label: string, runner: () => Promise<unknown>) => {
|
|
72
|
+
pending.value = true;
|
|
73
|
+
try {
|
|
74
|
+
const result = await runner();
|
|
75
|
+
push(`${label} 成功:${JSON.stringify(result).slice(0, 80)}…`);
|
|
76
|
+
} catch (error) {
|
|
77
|
+
push(`${label} 失败:${(error as Error).message}`, 'error');
|
|
78
|
+
} finally {
|
|
79
|
+
pending.value = false;
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const callFetchGet = () =>
|
|
84
|
+
wrap('fetch GET', async () => {
|
|
85
|
+
const response = await fetch(httpEndpoints.echoGet);
|
|
86
|
+
return response.json();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const callFetchPost = () =>
|
|
90
|
+
wrap('fetch POST', async () => {
|
|
91
|
+
const response = await fetch(httpEndpoints.echoPost, {
|
|
92
|
+
method: 'POST',
|
|
93
|
+
headers: { 'content-type': 'application/json' },
|
|
94
|
+
body: JSON.stringify(samplePostBody),
|
|
95
|
+
});
|
|
96
|
+
return response.json();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const callXhr = () =>
|
|
100
|
+
wrap(
|
|
101
|
+
'XHR GET',
|
|
102
|
+
() =>
|
|
103
|
+
new Promise((resolve, reject) => {
|
|
104
|
+
const xhr = new XMLHttpRequest();
|
|
105
|
+
xhr.open('GET', httpEndpoints.echoGet);
|
|
106
|
+
xhr.onload = () => resolve(xhr.status);
|
|
107
|
+
xhr.onerror = () => reject(new Error('xhr error'));
|
|
108
|
+
xhr.send();
|
|
109
|
+
}),
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
const callFetchError = () =>
|
|
113
|
+
wrap('fetch 404', async () => {
|
|
114
|
+
const response = await fetch(httpEndpoints.notFound);
|
|
115
|
+
if (!response.ok) {
|
|
116
|
+
throw new Error(`status ${response.status}`);
|
|
117
|
+
}
|
|
118
|
+
return response.text();
|
|
119
|
+
});
|
|
120
|
+
</script>
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<section class="demo-page">
|
|
3
|
+
<header>
|
|
4
|
+
<h2 class="demo-page__title">长任务 · long-task 插件</h2>
|
|
5
|
+
<p class="demo-page__desc">
|
|
6
|
+
监听 PerformanceObserver 的 <code>longtask</code> 条目,超过 <code>threshold</code>(默认 50ms,演示中为
|
|
7
|
+
50ms)的任务会上报。
|
|
8
|
+
</p>
|
|
9
|
+
</header>
|
|
10
|
+
|
|
11
|
+
<div class="demo-card">
|
|
12
|
+
<h3 class="demo-card__title">在主线程造一个长任务</h3>
|
|
13
|
+
<p class="demo-card__hint">
|
|
14
|
+
点击按钮后会用同步 while 循环阻塞主线程指定时长,可对比 <code>< 50ms</code> 时不上报、<code>> 50ms</code>
|
|
15
|
+
时上报的差异。
|
|
16
|
+
</p>
|
|
17
|
+
<div class="demo-actions">
|
|
18
|
+
<button
|
|
19
|
+
class="demo-btn"
|
|
20
|
+
type="button"
|
|
21
|
+
@click="block(30)"
|
|
22
|
+
>
|
|
23
|
+
阻塞 30ms(不上报)
|
|
24
|
+
</button>
|
|
25
|
+
<button
|
|
26
|
+
class="demo-btn demo-btn--warning"
|
|
27
|
+
type="button"
|
|
28
|
+
@click="block(120)"
|
|
29
|
+
>
|
|
30
|
+
阻塞 120ms
|
|
31
|
+
</button>
|
|
32
|
+
<button
|
|
33
|
+
class="demo-btn demo-btn--danger"
|
|
34
|
+
type="button"
|
|
35
|
+
@click="block(450)"
|
|
36
|
+
>
|
|
37
|
+
阻塞 450ms
|
|
38
|
+
</button>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<div class="demo-card">
|
|
43
|
+
<DemoLogPanel
|
|
44
|
+
:entries="entries"
|
|
45
|
+
@clear="clear"
|
|
46
|
+
/>
|
|
47
|
+
</div>
|
|
48
|
+
</section>
|
|
49
|
+
</template>
|
|
50
|
+
|
|
51
|
+
<script setup lang="ts">
|
|
52
|
+
import DemoLogPanel from '../components/DemoLogPanel.vue';
|
|
53
|
+
import { useDemoLog } from '../composables/use-demo-log';
|
|
54
|
+
|
|
55
|
+
const { entries, push, clear } = useDemoLog();
|
|
56
|
+
|
|
57
|
+
// 用 while 循环刻意阻塞主线程,触发 PerformanceObserver longtask 条目
|
|
58
|
+
const block = (durationMs: number) => {
|
|
59
|
+
push(`即将阻塞主线程 ${durationMs}ms`, 'warn');
|
|
60
|
+
const start = performance.now();
|
|
61
|
+
while (performance.now() - start < durationMs) {
|
|
62
|
+
// intentionally busy-wait
|
|
63
|
+
}
|
|
64
|
+
push(`阻塞结束(实际耗时 ${(performance.now() - start).toFixed(1)}ms)`);
|
|
65
|
+
};
|
|
66
|
+
</script>
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<section class="demo-page">
|
|
3
|
+
<header>
|
|
4
|
+
<h2 class="demo-page__title">WebSocket · websocket 插件</h2>
|
|
5
|
+
<p class="demo-page__desc">
|
|
6
|
+
websocket 插件会在 WebSocket 生命周期(open / message / close / error)上挂钩,自动上报关键事件。
|
|
7
|
+
</p>
|
|
8
|
+
</header>
|
|
9
|
+
|
|
10
|
+
<div class="demo-card">
|
|
11
|
+
<h3 class="demo-card__title">演示连接</h3>
|
|
12
|
+
<p class="demo-card__hint">
|
|
13
|
+
默认连接 <code>{{ websocketEndpoint }}</code
|
|
14
|
+
>(一个公共 echo server)。点击连接后再点"发送消息"即可看到完整链路。
|
|
15
|
+
</p>
|
|
16
|
+
<div class="demo-actions">
|
|
17
|
+
<button
|
|
18
|
+
class="demo-btn"
|
|
19
|
+
:disabled="connected"
|
|
20
|
+
type="button"
|
|
21
|
+
@click="connect"
|
|
22
|
+
>
|
|
23
|
+
连接
|
|
24
|
+
</button>
|
|
25
|
+
<button
|
|
26
|
+
class="demo-btn demo-btn--success"
|
|
27
|
+
:disabled="!connected"
|
|
28
|
+
type="button"
|
|
29
|
+
@click="send"
|
|
30
|
+
>
|
|
31
|
+
发送消息
|
|
32
|
+
</button>
|
|
33
|
+
<button
|
|
34
|
+
class="demo-btn demo-btn--ghost"
|
|
35
|
+
:disabled="!connected"
|
|
36
|
+
type="button"
|
|
37
|
+
@click="disconnect"
|
|
38
|
+
>
|
|
39
|
+
断开
|
|
40
|
+
</button>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<div class="demo-card">
|
|
45
|
+
<DemoLogPanel
|
|
46
|
+
:entries="entries"
|
|
47
|
+
@clear="clear"
|
|
48
|
+
/>
|
|
49
|
+
</div>
|
|
50
|
+
</section>
|
|
51
|
+
</template>
|
|
52
|
+
|
|
53
|
+
<script setup lang="ts">
|
|
54
|
+
import { onBeforeUnmount, shallowRef } from 'vue';
|
|
55
|
+
|
|
56
|
+
import DemoLogPanel from '../components/DemoLogPanel.vue';
|
|
57
|
+
import { useDemoLog } from '../composables/use-demo-log';
|
|
58
|
+
import { websocketEndpoint } from '../mock';
|
|
59
|
+
|
|
60
|
+
const { entries, push, clear } = useDemoLog();
|
|
61
|
+
const socket = shallowRef<null | WebSocket>(null);
|
|
62
|
+
const connected = shallowRef(false);
|
|
63
|
+
|
|
64
|
+
const connect = () => {
|
|
65
|
+
if (socket.value) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const ws = new WebSocket(websocketEndpoint);
|
|
69
|
+
socket.value = ws;
|
|
70
|
+
push(`正在连接 ${websocketEndpoint}`);
|
|
71
|
+
|
|
72
|
+
ws.addEventListener('open', () => {
|
|
73
|
+
connected.value = true;
|
|
74
|
+
push('WebSocket 已连接');
|
|
75
|
+
});
|
|
76
|
+
ws.addEventListener('message', event => {
|
|
77
|
+
push(`收到消息:${typeof event.data === 'string' ? event.data : '[binary]'}`);
|
|
78
|
+
});
|
|
79
|
+
ws.addEventListener('close', event => {
|
|
80
|
+
connected.value = false;
|
|
81
|
+
socket.value = null;
|
|
82
|
+
push(`已断开(code=${event.code})`, 'warn');
|
|
83
|
+
});
|
|
84
|
+
ws.addEventListener('error', () => {
|
|
85
|
+
push('WebSocket 出错', 'error');
|
|
86
|
+
});
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const send = () => {
|
|
90
|
+
socket.value?.send(`hello from playground @ ${Date.now()}`);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const disconnect = () => {
|
|
94
|
+
socket.value?.close();
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
onBeforeUnmount(() => {
|
|
98
|
+
socket.value?.close();
|
|
99
|
+
});
|
|
100
|
+
</script>
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Tencent is pleased to support the open source community by making
|
|
3
|
+
* 蓝鲸智云PaaS平台 (BlueKing PaaS) available.
|
|
4
|
+
*
|
|
5
|
+
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
|
|
6
|
+
*
|
|
7
|
+
* 蓝鲸智云PaaS平台 (BlueKing PaaS) is licensed under the MIT License.
|
|
8
|
+
*
|
|
9
|
+
* License for 蓝鲸智云PaaS平台 (BlueKing PaaS):
|
|
10
|
+
*
|
|
11
|
+
* ---------------------------------------------------
|
|
12
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
|
|
13
|
+
* documentation files (the "Software"), to deal in the Software without restriction, including without limitation
|
|
14
|
+
* the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and
|
|
15
|
+
* to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
16
|
+
*
|
|
17
|
+
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of
|
|
18
|
+
* the Software.
|
|
19
|
+
*
|
|
20
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO
|
|
21
|
+
* THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
22
|
+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
|
|
23
|
+
* CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
|
24
|
+
* IN THE SOFTWARE.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { type RouteRecordRaw, createRouter, createWebHashHistory } from 'vue-router';
|
|
28
|
+
|
|
29
|
+
// hash 路由便于单页静态部署,同时方便演示 hashchange / popstate 路由观察
|
|
30
|
+
const routes: RouteRecordRaw[] = [
|
|
31
|
+
{
|
|
32
|
+
path: '/',
|
|
33
|
+
name: 'home',
|
|
34
|
+
component: () => import('../pages/Home.vue'),
|
|
35
|
+
meta: { title: '总览' },
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
path: '/http',
|
|
39
|
+
name: 'http',
|
|
40
|
+
component: () => import('../pages/HttpDemo.vue'),
|
|
41
|
+
meta: { title: 'HTTP / http-body' },
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
path: '/error',
|
|
45
|
+
name: 'error',
|
|
46
|
+
component: () => import('../pages/ErrorDemo.vue'),
|
|
47
|
+
meta: { title: '错误捕获' },
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
path: '/business',
|
|
51
|
+
name: 'business',
|
|
52
|
+
component: () => import('../pages/BusinessDemo.vue'),
|
|
53
|
+
meta: { title: '业务事件' },
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
path: '/long-task',
|
|
57
|
+
name: 'long-task',
|
|
58
|
+
component: () => import('../pages/LongTaskDemo.vue'),
|
|
59
|
+
meta: { title: '长任务' },
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
path: '/websocket',
|
|
63
|
+
name: 'websocket',
|
|
64
|
+
component: () => import('../pages/WebSocketDemo.vue'),
|
|
65
|
+
meta: { title: 'WebSocket' },
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
path: '/blank-screen',
|
|
69
|
+
name: 'blank-screen',
|
|
70
|
+
component: () => import('../pages/BlankScreenDemo.vue'),
|
|
71
|
+
meta: { title: '白屏检测' },
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
path: '/custom-plugin',
|
|
75
|
+
name: 'custom-plugin',
|
|
76
|
+
component: () => import('../pages/CustomPluginDemo.vue'),
|
|
77
|
+
meta: { title: '自定义插件' },
|
|
78
|
+
},
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
export const router = createRouter({
|
|
82
|
+
history: createWebHashHistory(),
|
|
83
|
+
routes,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
export const navItems = routes
|
|
87
|
+
.filter(route => typeof route.name === 'string')
|
|
88
|
+
.map(route => ({
|
|
89
|
+
name: route.name as string,
|
|
90
|
+
path: route.path,
|
|
91
|
+
title: (route.meta?.title as string) ?? (route.name as string),
|
|
92
|
+
}));
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 全局基础样式:仅约束布局与排版,组件内细节样式靠 scoped 文件管理
|
|
3
|
+
* 主题色统一通过 CSS 变量驱动,避免重复硬编码
|
|
4
|
+
*/
|
|
5
|
+
:root {
|
|
6
|
+
--color-bg: #f5f7fa;
|
|
7
|
+
--color-surface: #fff;
|
|
8
|
+
--color-border: #e2e8f0;
|
|
9
|
+
--color-text-primary: #1f2937;
|
|
10
|
+
--color-text-secondary: #4b5563;
|
|
11
|
+
--color-text-muted: #94a3b8;
|
|
12
|
+
--color-primary: #3b82f6;
|
|
13
|
+
--color-primary-hover: #2563eb;
|
|
14
|
+
--color-success: #10b981;
|
|
15
|
+
--color-warning: #f59e0b;
|
|
16
|
+
--color-danger: #ef4444;
|
|
17
|
+
--color-code-bg: #0f172a;
|
|
18
|
+
--color-code-text: #e2e8f0;
|
|
19
|
+
--space-xs: 4px;
|
|
20
|
+
--space-sm: 8px;
|
|
21
|
+
--space-md: 16px;
|
|
22
|
+
--space-lg: 24px;
|
|
23
|
+
--space-xl: 32px;
|
|
24
|
+
--radius-sm: 4px;
|
|
25
|
+
--radius-md: 8px;
|
|
26
|
+
--radius-lg: 12px;
|
|
27
|
+
--shadow-sm: 0 1px 2px rgb(15 23 42 / 6%);
|
|
28
|
+
--shadow-md: 0 4px 12px rgb(15 23 42 / 8%);
|
|
29
|
+
--font-sans:
|
|
30
|
+
-apple-system, blinkmacsystemfont, 'Segoe UI', roboto, 'Helvetica Neue', arial, 'PingFang SC', 'Hiragino Sans GB',
|
|
31
|
+
'Microsoft YaHei', sans-serif;
|
|
32
|
+
--font-mono: 'SFMono-Regular', menlo, monaco, consolas, 'Liberation Mono', 'Courier New', monospace;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
* {
|
|
36
|
+
box-sizing: border-box;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
html,
|
|
40
|
+
body,
|
|
41
|
+
#app {
|
|
42
|
+
height: 100%;
|
|
43
|
+
padding: 0;
|
|
44
|
+
margin: 0;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
body {
|
|
48
|
+
font-family: var(--font-sans);
|
|
49
|
+
font-size: 14px;
|
|
50
|
+
line-height: 1.6;
|
|
51
|
+
color: var(--color-text-primary);
|
|
52
|
+
background: var(--color-bg);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
a {
|
|
56
|
+
color: var(--color-primary);
|
|
57
|
+
text-decoration: none;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
a:hover {
|
|
61
|
+
color: var(--color-primary-hover);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
button {
|
|
65
|
+
font-family: inherit;
|
|
66
|
+
font-size: 14px;
|
|
67
|
+
cursor: pointer;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
code,
|
|
71
|
+
pre {
|
|
72
|
+
font-family: var(--font-mono);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/* 通用内容块:每个 demo 页面共用 */
|
|
76
|
+
.demo-page {
|
|
77
|
+
display: flex;
|
|
78
|
+
flex-direction: column;
|
|
79
|
+
gap: var(--space-lg);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.demo-page__title {
|
|
83
|
+
margin: 0;
|
|
84
|
+
font-size: 22px;
|
|
85
|
+
font-weight: 600;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.demo-page__desc {
|
|
89
|
+
margin: 0;
|
|
90
|
+
color: var(--color-text-secondary);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.demo-card {
|
|
94
|
+
padding: var(--space-lg);
|
|
95
|
+
background: var(--color-surface);
|
|
96
|
+
border: 1px solid var(--color-border);
|
|
97
|
+
border-radius: var(--radius-md);
|
|
98
|
+
box-shadow: var(--shadow-sm);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.demo-card__title {
|
|
102
|
+
margin: 0 0 var(--space-sm);
|
|
103
|
+
font-size: 16px;
|
|
104
|
+
font-weight: 600;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.demo-card__hint {
|
|
108
|
+
margin: 0 0 var(--space-md);
|
|
109
|
+
color: var(--color-text-secondary);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.demo-actions {
|
|
113
|
+
display: flex;
|
|
114
|
+
flex-wrap: wrap;
|
|
115
|
+
gap: var(--space-sm);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.demo-btn {
|
|
119
|
+
padding: 8px 16px;
|
|
120
|
+
color: #fff;
|
|
121
|
+
background: var(--color-primary);
|
|
122
|
+
border: none;
|
|
123
|
+
border-radius: var(--radius-sm);
|
|
124
|
+
transition: background-color 0.15s ease-in-out;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.demo-btn:hover {
|
|
128
|
+
background: var(--color-primary-hover);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.demo-btn--success {
|
|
132
|
+
background: var(--color-success);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.demo-btn--warning {
|
|
136
|
+
background: var(--color-warning);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.demo-btn--danger {
|
|
140
|
+
background: var(--color-danger);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.demo-btn--ghost {
|
|
144
|
+
color: var(--color-text-primary);
|
|
145
|
+
background: transparent;
|
|
146
|
+
border: 1px solid var(--color-border);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.demo-btn--ghost:hover {
|
|
150
|
+
background: var(--color-bg);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.demo-log {
|
|
154
|
+
max-height: 240px;
|
|
155
|
+
padding: var(--space-md);
|
|
156
|
+
overflow: auto;
|
|
157
|
+
font-size: 12px;
|
|
158
|
+
color: var(--color-code-text);
|
|
159
|
+
background: var(--color-code-bg);
|
|
160
|
+
border-radius: var(--radius-sm);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.demo-log__line {
|
|
164
|
+
margin: 0;
|
|
165
|
+
word-break: break-all;
|
|
166
|
+
white-space: pre-wrap;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.demo-log__line + .demo-log__line {
|
|
170
|
+
margin-top: 4px;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.demo-snippet {
|
|
174
|
+
padding: var(--space-md);
|
|
175
|
+
overflow: auto;
|
|
176
|
+
font-size: 12px;
|
|
177
|
+
color: var(--color-code-text);
|
|
178
|
+
background: var(--color-code-bg);
|
|
179
|
+
border-radius: var(--radius-sm);
|
|
180
|
+
}
|