第一方模式

通过您的域名路由第三方脚本流量,以提升隐私和可靠性。

背景

当第三方脚本直接从外部服务器加载时,会暴露您的用户数据:

  • IP 地址暴露 — 每个请求都会向第三方泄露用户的 IP 地址
  • 第三方 Cookie — 外部脚本可设置跨站追踪的 Cookie
  • 广告拦截器干扰 — 隐私工具会阻止已知追踪域的请求
  • 连接开销 — 额外的 DNS 查询和 TLS 握手会降低页面加载速度

第一方模式如何帮助

第一方模式通过您的域名路由所有脚本流量:

  • 用户 IP 匿名化 — 在转发前,IP 地址被匿名到子网级别,第三方无法识别单个用户
  • 减少设备指纹识别 — 屏幕分辨率、User-Agent 和硬件信息被泛化到常见类别
  • 无第三方 Cookie — 请求为同源,消除跨站追踪
  • 兼容广告拦截器 — 请求看起来像第一方请求
  • 加载更快 — 无需针对外部域的额外 DNS 查询

工作原理

启用第一方模式时:

  1. 构建时:脚本被下载且 URL 重写为本地路径(例如:https://www.google-analytics.com/g/collect/_scripts/c/ga/g/collect
  2. 运行时:Nitro 路由规则将来自本地路径的请求代理回原始端点
用户浏览器 → 您的服务器 (/_scripts/c/ga/...) → Google Analytics

您的用户永远不会直接连接第三方服务器。

使用方法

全局启用

为所有支持的脚本启用第一方模式:

nuxt.config.ts
export default defineNuxtConfig({
  scripts: {
    firstParty: true,
    registry: {
      googleAnalytics: { id: 'G-XXXXXX' },
      metaPixel: { id: '123456' },
    }
  }
})

Privacy Controls

每个注册的脚本根据其所需数据声明默认隐私设置。隐私通过以下六个标识控制:

标识功能说明
ip在请求头和数据参数中将 IP 地址匿名到子网级别
userAgent将 User-Agent 归一化为浏览器类别 + 主版本(例如 Mozilla/5.0 (compatible; Chrome/131.0)
language将 Accept-Language 归一化为主语言标签
screen将屏幕分辨率、视口、硬件并发性和设备内存泛化到通用类别
timezone泛化时区偏移和 IANA 时区名称
hardware匿名化 canvas/webgl/audio 指纹、插件/字体列表、浏览器版本和设备信息

无论隐私设置如何,敏感请求头(如 cookieauthorization)都会始终被移除。

单个脚本默认隐私设置示例

脚本ipuserAgentlanguagescreentimezonehardware理由
Google Analytics---设备、时间和操作系统相关报告需要 UA/屏幕/时区信息
Google Tag Manager------容器脚本加载,无用户数据传输
Meta Pixel不可信广告网络,需完全匿名
TikTok Pixel不可信广告网络,需完全匿名
X/Twitter Pixel不可信广告网络,需完全匿名
Snapchat Pixel不可信广告网络,需完全匿名
Reddit Pixel不可信广告网络,需完全匿名
Segment------可信的数据通路,要求完整数据
PostHog------可信、开源,要求完整数据
Microsoft Clarity---热图和设备筛选需要 UA/屏幕/时区信息
Hotjar---热图和设备筛选需要 UA/屏幕/时区信息

✓ 表示匿名化处理,- 表示通过原始数据。

全局覆盖

一次性覆盖所有脚本默认隐私设置:

nuxt.config.ts
export default defineNuxtConfig({
  scripts: {
    firstParty: {
      privacy: true, // 对所有脚本全面匿名化
    }
  }
})

或者选择性覆盖特定标识:

nuxt.config.ts
export default defineNuxtConfig({
  scripts: {
    firstParty: {
      privacy: { ip: true }, // 仅对所有脚本匿名化 IP,其他使用脚本默认设置
    }
  }
})
启用某个标识时,数据会被泛化(降低精度)或脱敏(清空/归零)——分析端点仍接收有效数据。例如,屏幕分辨率 1440x900 会变为 1920x1080(桌面类别),User-Agent 会归一化为 Mozilla/5.0 (compatible; Chrome/131.0),而如 canvas、WebGL、插件、字体等硬件指纹则被清空或重置。

自定义路径

可自定义代理收集端点路径:

nuxt.config.ts
export default defineNuxtConfig({
  scripts: {
    firstParty: {
      collectPrefix: '/_analytics', // 默认为 /_scripts/c
    }
  }
})

按脚本选择退出

禁用某脚本的第一方路由:

useScriptGoogleAnalytics({
  id: 'G-XXXXXX',
  scriptOptions: {
    firstParty: false, // 直接从 Google 加载
  }
})

支持的脚本

第一方模式支持以下脚本:

脚本代理端点
Google Analyticsgoogle-analytics.com, analytics.google.com, stats.g.doubleclick.net, pagead2.googlesyndication.com
Google Tag Managerwww.googletagmanager.com
Meta Pixelconnect.facebook.net, www.facebook.com/tr, pixel.facebook.com
TikTok Pixelanalytics.tiktok.com
Segmentapi.segment.io, cdn.segment.com
PostHogus.i.posthog.com, eu.i.posthog.com, us-assets.i.posthog.com, eu-assets.i.posthog.com
Microsoft Claritywww.clarity.ms, scripts.clarity.ms, d.clarity.ms, e.clarity.ms
Hotjarstatic.hotjar.com, script.hotjar.com, vars.hotjar.com, in.hotjar.com
X/Twitter Pixelanalytics.twitter.com, t.co
Snapchat Pixeltr.snapchat.com
Reddit Pixelalb.reddit.com

要求

第一方模式需要服务器运行时环境。无法用于纯静态托管(如 nuxt generate 部署到 GitHub Pages),因为代理端点需要服务器转发请求。

静态部署仍可启用第一方模式,脚本中包含重写 URL,但您需手动配置托管平台的重写规则。

静态托管重写规则示例

若部署为静态站点,请配置平台代理请求:

/_scripts/c/ga/* → https://www.google.com/*
/_scripts/c/gtm/* → https://www.googletagmanager.com/*
/_scripts/c/meta/* → https://connect.facebook.net/*

第一方模式 vs bundle 选项

第一方模式替代了旧的 bundle 选项:

功能bundle: truefirstParty: true
构建时下载脚本
从您域名提供脚本
重写收集 URL
代理 API 请求
隐藏用户 IP
阻止第三方 Cookie

bundle 仅自托管脚本文件。第一方模式还重写并代理所有收集/追踪端点,实现完整第一方路由。

bundle 选项已废弃。新项目请使用 firstParty: true

默认行为

第一方模式默认启用。对于静态托管(如 GitHub Pages),脚本仍被打包,但您需配置平台重写规则以使代理端点生效。

关闭第一方模式:

nuxt.config.ts
export default defineNuxtConfig({
  scripts: {
    firstParty: false
  }
})

平台重写配置

部署到静态托管或边缘平台时,请配置如下重写规则以代理收集端点。

Vercel

vercel.json
{
  "rewrites": [
    { "source": "/_scripts/c/ga/:path*", "destination": "https://www.google.com/:path*" },
    { "source": "/_scripts/c/ga-legacy/:path*", "destination": "https://www.google-analytics.com/:path*" },
    { "source": "/_scripts/c/gtm/:path*", "destination": "https://www.googletagmanager.com/:path*" },
    { "source": "/_scripts/c/meta/:path*", "destination": "https://connect.facebook.net/:path*" },
    { "source": "/_scripts/c/tiktok/:path*", "destination": "https://analytics.tiktok.com/:path*" },
    { "source": "/_scripts/c/segment/:path*", "destination": "https://api.segment.io/:path*" },
    { "source": "/_scripts/c/clarity/:path*", "destination": "https://www.clarity.ms/:path*" },
    { "source": "/_scripts/c/hotjar/:path*", "destination": "https://static.hotjar.com/:path*" },
    { "source": "/_scripts/c/x/:path*", "destination": "https://analytics.twitter.com/:path*" },
    { "source": "/_scripts/c/snap/:path*", "destination": "https://tr.snapchat.com/:path*" },
    { "source": "/_scripts/c/reddit/:path*", "destination": "https://alb.reddit.com/:path*" }
  ]
}

Netlify

netlify.toml
[[redirects]]
  from = "/_scripts/c/ga/*"
  to = "https://www.google.com/:splat"
  status = 200

[[redirects]]
  from = "/_scripts/c/ga-legacy/*"
  to = "https://www.google-analytics.com/:splat"
  status = 200

[[redirects]]
  from = "/_scripts/c/gtm/*"
  to = "https://www.googletagmanager.com/:splat"
  status = 200

[[redirects]]
  from = "/_scripts/c/meta/*"
  to = "https://connect.facebook.net/:splat"
  status = 200

# 根据需要添加更多...

Cloudflare Pages

在公共目录创建 _redirects 文件:

public/_redirects
/_scripts/c/ga/* https://www.google.com/:splat 200
/_scripts/c/ga-legacy/* https://www.google-analytics.com/:splat 200
/_scripts/c/gtm/* https://www.googletagmanager.com/:splat 200
/_scripts/c/meta/* https://connect.facebook.net/:splat 200

或者使用 Cloudflare Workers 获取更灵活的控制:

functions/_scripts/c/[[path]].ts
export const onRequest: PagesFunction = async (context) => {
  const url = new URL(context.request.url)
  const path = url.pathname.replace('/_scripts/c/', '')

  // 根据前缀路由
  const routes: Record<string, string> = {
    'ga/': 'https://www.google.com/',
    'gtm/': 'https://www.googletagmanager.com/',
    'meta/': 'https://connect.facebook.net/',
  }

  for (const [prefix, target] of Object.entries(routes)) {
    if (path.startsWith(prefix)) {
      const targetUrl = target + path.slice(prefix.length) + url.search
      return fetch(targetUrl, context.request)
    }
  }

  return new Response('Not found', { status: 404 })
}

架构示意图

┌─────────────────────────────────────────────────────────────────────┐
│                          构建阶段                                   │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  1. 从第三方下载脚本                                                │
│     https://www.googletagmanager.com/gtag/js?id=G-XXX               │
│                           ↓                                         │
│  2. 重写脚本内容中的 URL                                            │
│     "www.google.com/g/collect" → "/_scripts/c/ga/g/collect"         │
│                           ↓                                         │
│  3. 将重写的脚本保存到构建输出                                     │
│     .output/public/_scripts/abc123.js                               │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘
                                ↓
┌─────────────────────────────────────────────────────────────────────┐
│                            运行阶段                                 │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  用户浏览器                                                        │
│       │                                                             │
│       │ 1. 请求脚本                                                │
│       │    GET /_scripts/abc123.js                                 │
│       ↓                                                             │
│  您的服务器(Nitro)                                               │
│       │                                                             │
│       │ 2. 返回打包的脚本(已重写 URL)                            │
│       ↓                                                             │
│  用户浏览器                                                        │
│       │                                                             │
│       │ 3. 脚本发送分析数据                                        │
│       │    POST /_scripts/c/ga/g/collect                           │
│       ↓                                                             │
│  您的服务器(Nitro 路由规则)                                     │
│       │                                                             │
│       │ 4. 代理请求到第三方                                       │
│       │    POST https://www.google.com/g/collect                   │
│       ↓                                                             │
│  第三方服务器                                                     │
│       │                                                             │
│       │ 5. 看到的是您的服务器 IP,而非用户 IP                      │
│       ↓                                                             │
│  响应被代理回用户                                                 │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

故障排查

分析无数据追踪

症状:统计分析面板没有事件数据。

检查项

  1. 验证代理路由是否生效 — 在开发模式下检查 /_scripts/status.json 或通过 DevTools 的 Scripts 面板
  2. 查看浏览器网络面板 — 确认是否有对 /_scripts/c/ 路径的请求且返回状态码为 200
  3. 检查服务器日志 — 查找 Nitro 代理相关错误
  4. 验证路由规则 — 运行 npx nuxi build 并检查 .output/server/chunks/routes/rules.mjs
// 调试:服务器中间件中打印所有代理请求
export default defineEventHandler((event) => {
  if (event.path.startsWith('/_scripts/c/')) {
    console.log('代理请求:', event.path)
  }
})

跨域请求错误(CORS)

症状:浏览器控制台出现分析请求的 CORS 错误。

解决方案

  1. 确保 nuxt.config.ts 中正确配置了第一方路由规则
  2. 静态托管环境确认重写规则正确生效
  3. 检查重写目标 URL 是否完全匹配

缓存旧脚本

症状:脚本内容过时或未重写 URL。

解决方案

  1. 清理构建缓存:
    rm -rf .nuxt/cache/scripts
    npx nuxi build
    
  2. 开发模式下强制重新下载:
    useScriptGoogleAnalytics({
      id: 'G-XXX',
      scriptOptions: { bundle: 'force' }
    })
    

静态构建不代理

症状:脚本加载正常,但静态托管的分析无数据。

说明:这是预期表现——静态构建无法代理请求。请配置托管平台重写(参见“平台重写配置”章节),或使用服务端渲染模式。

脚本下载失败

症状:构建时遇到网络超时或下载错误。

解决方案

  1. 启用回退模式:
    nuxt.config.ts
    export default defineNuxtConfig({
      scripts: {
        firstParty: true,
        assets: {
          fallbackOnSrcOnBundleFail: true
        }
      }
    })
    
  2. 增加超时配置:
    nuxt.config.ts
    export default defineNuxtConfig({
      scripts: {
        assets: {
          fetchOptions: {
            timeout: 30000 // 30 秒
          }
        }
      }
    })
    

常见问题

第一方模式会绕过 GDPR 同意要求吗?

不会。 第一方模式仅变更请求路由,不影响是否进行跟踪。您仍须在加载跟踪脚本前获得用户同意。使用同意触发器实现控制:

const { accept } = useScriptTriggerConsent()

// 仅在用户同意后加载脚本
function onConsentGiven() {
  accept()
}

第三方修改了脚本,分析还能正常吗?

能,且支持自动更新。 脚本默认缓存 7 天,缓存过期后会自动重新下载并重写 URL。您可自定义缓存时长:

nuxt.config.ts
export default defineNuxtConfig({
  scripts: {
    assets: {
      cacheMaxAge: 86400000 // 1 天,单位毫秒
    }
  }
})

可以自定义代理路径吗?

可以。 通过 collectPrefix 配置修改默认的收集端点地址:

nuxt.config.ts
export default defineNuxtConfig({
  scripts: {
    firstParty: {
      collectPrefix: '/_tracking' // 代替默认的 /_scripts/c
    }
  }
})

支持服务器端追踪吗?

第一方模式主要针对客户端脚本。服务器端追踪(如 Measurement Protocol)建议直接从服务器端发送请求,无需代理层。

如何调试被代理的请求?

  1. DevTools:打开 Nuxt DevTools → Scripts → 第一方标签页
  2. 状态端点:开发时访问 /_scripts/status.json 检查状态
  3. 日志输出:启用调试模式打印日志
    nuxt.config.ts
    export default defineNuxtConfig({
      scripts: {
        debug: true
      }
    })
    

会影响性能吗?

影响极小。 代理带来约 10–50 毫秒的延迟,但通常被以下因素抵消:

  • 免去对第三方域名的 DNS 查询
  • 绕过可能阻止请求的广告拦截器
  • 重用连接减少 TLS 握手时间

哪些脚本支持第一方模式?

当前支持文档「支持的脚本」中列出的 11 个脚本。对其他脚本,您可以:

  1. 通过 Issue 请求支持
  2. 使用(已废弃的)bundle 选项自托管脚本
  3. 手动配置自定义路由规则

混合渲染

第一方模式兼容 Nuxt 的混合渲染架构:

路由级 SSR

禁用特定路由的 SSR 时,第一方模式依旧有效,因为:

  • 脚本在构建阶段打包,不依赖运行时 SSR
  • 代理路由由 Nitro 全局配置管理
  • 收集请求由服务器转发代理

示例:

nuxt.config.ts
export default defineNuxtConfig({
  routeRules: {
    '/dashboard/**': { ssr: false }, // 仪表盘 SPA 模式
  },
  scripts: {
    firstParty: true, // 各路由均启用第一方
  }
})

ISR(增量静态再生成)

ISR 页面同样支持第一方模式。打包的脚本作为静态资源提供,收集请求由 Nitro 在运行时代理。

Edge 渲染

支持部署到边缘平台(如 Cloudflare Workers、Vercel Edge)。代理请求通过 Nitro 内置代理功能支持多种目标环境。

部署到边缘时,请确保运行时允许跨域向分析端点发起外部 fetch 请求。

同意集成

第一方模式作为隐私保护方案与同意管理配合增强:

  • 第一方模式 控制请求发送的位置(通过您的服务器代理)
  • 同意触发器 控制脚本加载的时机(同意前不注入脚本)

联合使用示例

<script setup>
// 第一方模式在 nuxt.config.ts 中全局启用
// 通过同意触发器控制脚本加载

const { status, accept } = useScriptTriggerConsent()

const { gtag } = useScriptGoogleAnalytics({
  id: 'G-XXXXXX',
  scriptOptions: {
    trigger: status, // 未同意前不加载
  }
})

function onConsentGiven() {
  accept() // 一旦同意,脚本通过第一方代理加载
}
</script>

同意横幅示例

<template>
  <div v-if="!hasConsent" class="cookie-banner">
    <p>我们使用分析数据来提升您的体验。</p>
    <button @click="acceptAll">接受</button>
    <button @click="rejectAll">拒绝</button>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const hasConsent = ref(false)

const { accept: acceptGA } = useScriptTriggerConsent()
const { accept: acceptMeta } = useScriptTriggerConsent()

useScriptGoogleAnalytics({
  id: 'G-XXXXXX',
  scriptOptions: { trigger: useScriptTriggerConsent().status }
})

useScriptMetaPixel({
  id: '123456',
  scriptOptions: { trigger: useScriptTriggerConsent().status }
})

function acceptAll() {
  hasConsent.value = true
  acceptGA()
  acceptMeta()
  // 脚本通过第一方代理加载
}

function rejectAll() {
  hasConsent.value = true
  // 不加载脚本
}
</script>

隐私流程示意

用户访问网站
       │
       ↓
脚本注册但未加载(等待同意)
       │
       ↓
用户同意 Cookie
       │
       ↓
脚本通过您的服务器加载(/_scripts/...)
       │
       ↓
分析请求通过您的服务器发送(/_scripts/c/...)
       │
       ↓
第三方看到的是服务器 IP,而非用户 IP

此流程确保 GDPR 合规(先同意后追踪)并增强隐私保护(同意后第一方路由)。

健康检查

验证第一方模式是否正常运行:

1. 检查 DevTools

打开 Nuxt DevTools → Scripts → 第一方标签页,查看是否启用第一方模式,已配置的脚本及活跃代理路由。

2. 查询状态端点

开发时访问 /_scripts/status.json,示例输出:

{
  "enabled": true,
  "scripts": ["googleAnalytics", "metaPixel"],
  "routes": {
    "/_scripts/c/ga/**": "https://www.google.com/**",
    "/_scripts/c/meta/**": "https://connect.facebook.net/**"
  },
  "collectPrefix": "/_scripts/c"
}

3. 浏览器中验证

  • 打开浏览器开发者工具 → 网络面板
  • 过滤路径 /_scripts
  • 触发分析事件
  • 确认请求目标为 /_scripts/c/...,非直接第三方域
  • 检查响应状态码为 200

4. CLI 状态检查

运行命令查看缓存及状态:

npx nuxt-scripts status

显示已缓存的脚本文件及大小。