| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329 |
- <script setup lang="ts">
- definePageMeta({
- auth: false,
- })
- type CookieItem = {
- name: string
- value: string
- decodedValue?: string
- valueLength: number
- }
- type CookieSsrSnapshot = {
- serverCollectedAt: string
- url: string
- method: string
- clientIp: string | null
- cookieHeader: string
- cookieHeaderLength: number
- cookies: CookieItem[]
- headers: Record<string, string>
- }
- function safeDecodeURIComponent(value: string): string | undefined {
- try {
- return decodeURIComponent(value)
- }
- catch {
- return undefined
- }
- }
- function parseCookieHeader(cookieHeader: string): CookieItem[] {
- if (!cookieHeader)
- return []
- return cookieHeader
- .split(';')
- .map(part => part.trim())
- .filter(Boolean)
- .map((part) => {
- const idx = part.indexOf('=')
- const name = (idx >= 0 ? part.slice(0, idx) : part).trim()
- const value = (idx >= 0 ? part.slice(idx + 1) : '').trim()
- const decodedValue = value ? safeDecodeURIComponent(value) : undefined
- return {
- name,
- value,
- decodedValue,
- valueLength: value.length,
- }
- })
- }
- function collectSsrSnapshot(): CookieSsrSnapshot {
- const url = useRequestURL()
- const event = useRequestEvent()
- const headersRaw = useRequestHeaders([
- 'cookie',
- 'user-agent',
- 'accept-language',
- 'referer',
- 'host',
- 'x-forwarded-for',
- 'x-real-ip',
- ])
- const headers: Record<string, string> = {}
- for (const [k, v] of Object.entries(headersRaw)) {
- if (typeof v === 'string')
- headers[k] = v
- }
- const cookieHeader = headersRaw.cookie || ''
- const cookies = parseCookieHeader(cookieHeader)
- const xff = headersRaw['x-forwarded-for']
- const clientIp = (xff ? xff.split(',')[0]?.trim() : null)
- || headersRaw['x-real-ip']
- || event?.node?.req?.socket?.remoteAddress
- || null
- return {
- serverCollectedAt: new Date().toISOString(),
- url: url.toString(),
- method: event?.node?.req?.method || 'UNKNOWN',
- clientIp,
- cookieHeader,
- cookieHeaderLength: cookieHeader.length,
- cookies,
- headers,
- }
- }
- const importMetaServer = import.meta.server
- const ssrSnapshot = useState<CookieSsrSnapshot | null>('test-cookie-ssr-snapshot', () => {
- if (!import.meta.server)
- return null
- return collectSsrSnapshot()
- })
- const hydrated = ref(false)
- const clientNow = ref(new Date().toISOString())
- onMounted(() => {
- hydrated.value = true
- clientNow.value = new Date().toISOString()
- })
- </script>
- <template>
- <div class="cookie-page">
- <h1 class="title">
- Cookie SSR 调试
- </h1>
- <div class="meta">
- <div class="meta__row">
- <span class="meta__label">当前环境</span>
- <span class="meta__value">{{ importMetaServer ? 'server' : 'client' }}</span>
- </div>
- <div class="meta__row">
- <span class="meta__label">客户端已挂载</span>
- <span class="meta__value">{{ hydrated ? 'yes' : 'no' }}</span>
- </div>
- <div class="meta__row">
- <span class="meta__label">客户端时间</span>
- <span class="meta__value">{{ clientNow }}</span>
- </div>
- </div>
- <van-divider />
- <template v-if="ssrSnapshot">
- <h2 class="section-title">
- 原始 Cookie Header
- </h2>
- <pre class="code">{{ ssrSnapshot.cookieHeader || '(empty)' }}</pre>
- <van-divider />
- <h2 class="section-title">
- 解析后的 Cookies
- </h2>
- <template v-if="ssrSnapshot.cookies.length">
- <div class="table-wrap">
- <table class="table">
- <thead>
- <tr>
- <th>name</th>
- <th>value</th>
- <th>decodedValue</th>
- <th>length</th>
- </tr>
- </thead>
- <tbody>
- <tr
- v-for="c in ssrSnapshot.cookies"
- :key="c.name"
- >
- <td class="mono">
- {{ c.name }}
- </td>
- <td class="mono">
- {{ c.value }}
- </td>
- <td class="mono">
- {{ c.decodedValue ?? '-' }}
- </td>
- <td class="mono">
- {{ c.valueLength }}
- </td>
- </tr>
- </tbody>
- </table>
- </div>
- </template>
- <template v-else>
- <div class="empty">
- 未收到 Cookie(请求头里没有 cookie 字段或为空)。
- </div>
- </template>
- <van-divider />
- <h2 class="section-title">
- Cookie 相关请求头(SSR 采样)
- </h2>
- <pre class="code">{{ JSON.stringify(ssrSnapshot.headers, null, 2) }}</pre>
- <van-divider />
- <h2 class="section-title">
- SSR 快照(服务端渲染时采集并注入到客户端)
- </h2>
- <van-cell-group inset>
- <van-cell
- title="SSR 采集时间"
- :value="ssrSnapshot.serverCollectedAt"
- />
- <van-cell
- title="请求 URL"
- :value="ssrSnapshot.url"
- />
- <van-cell
- title="请求方法"
- :value="ssrSnapshot.method"
- />
- <van-cell
- title="推测客户端 IP"
- :value="ssrSnapshot.clientIp || '-'"
- />
- <van-cell
- title="Cookie Header 长度"
- :value="String(ssrSnapshot.cookieHeaderLength)"
- />
- <van-cell
- title="Cookie 数量(解析后)"
- :value="String(ssrSnapshot.cookies.length)"
- />
- </van-cell-group>
- </template>
- <template v-else>
- <van-empty description="未获取到 SSR 快照(可能是纯 CSR 导航且未命中 SSR,或当前环境不可用)。" />
- </template>
- </div>
- </template>
- <style scoped>
- .cookie-page {
- padding: 16px;
- }
- .title {
- font-size: 18px;
- font-weight: 700;
- margin: 0 0 12px;
- }
- .section-title {
- font-size: 14px;
- font-weight: 700;
- margin: 12px 0 8px;
- }
- .meta {
- display: grid;
- grid-template-columns: 1fr;
- gap: 6px;
- padding: 12px;
- border-radius: 10px;
- background: rgba(0, 0, 0, 0.03);
- }
- .meta__row {
- display: flex;
- justify-content: space-between;
- gap: 12px;
- font-size: 12px;
- }
- .meta__label {
- opacity: 0.7;
- }
- .meta__value {
- font-weight: 600;
- word-break: break-all;
- text-align: right;
- }
- .code {
- padding: 12px;
- border-radius: 10px;
- background: rgba(0, 0, 0, 0.04);
- overflow: auto;
- font-size: 12px;
- line-height: 1.45;
- margin: 0;
- }
- .table-wrap {
- overflow: auto;
- border-radius: 10px;
- background: rgba(0, 0, 0, 0.02);
- padding: 8px;
- }
- .table {
- width: 100%;
- border-collapse: collapse;
- font-size: 12px;
- }
- .table th,
- .table td {
- padding: 8px;
- border-bottom: 1px solid rgba(0, 0, 0, 0.08);
- text-align: left;
- vertical-align: top;
- white-space: nowrap;
- }
- .mono {
- font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
- }
- .empty {
- padding: 12px;
- border-radius: 10px;
- background: rgba(0, 0, 0, 0.03);
- font-size: 12px;
- opacity: 0.8;
- }
- .notes {
- margin: 0;
- padding-left: 18px;
- font-size: 12px;
- line-height: 1.6;
- }
- </style>
|