第一方模式
背景
当第三方脚本直接从外部服务器加载时,会暴露您的用户数据:
- IP 地址暴露 — 每个请求都会向第三方泄露用户的 IP 地址
- 第三方 Cookie — 外部脚本可设置跨站追踪的 Cookie
- 广告拦截器干扰 — 隐私工具会阻止已知追踪域的请求
- 连接开销 — 额外的 DNS 查询和 TLS 握手会降低页面加载速度
第一方模式如何帮助
第一方模式通过您的域名路由所有脚本流量:
- 用户 IP 匿名化 — 在转发前,IP 地址被匿名到子网级别,第三方无法识别单个用户
- 减少设备指纹识别 — 屏幕分辨率、User-Agent 和硬件信息被泛化到常见类别
- 无第三方 Cookie — 请求为同源,消除跨站追踪
- 兼容广告拦截器 — 请求看起来像第一方请求
- 加载更快 — 无需针对外部域的额外 DNS 查询
工作原理
启用第一方模式时:
- 构建时:脚本被下载且 URL 重写为本地路径(例如:
https://www.google-analytics.com/g/collect→/_scripts/c/ga/g/collect) - 运行时:Nitro 路由规则将来自本地路径的请求代理回原始端点
用户浏览器 → 您的服务器 (/_scripts/c/ga/...) → Google Analytics
您的用户永远不会直接连接第三方服务器。
使用方法
全局启用
为所有支持的脚本启用第一方模式:
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 指纹、插件/字体列表、浏览器版本和设备信息 |
无论隐私设置如何,敏感请求头(如 cookie、authorization)都会始终被移除。
单个脚本默认隐私设置示例
| 脚本 | ip | userAgent | language | screen | timezone | hardware | 理由 |
|---|---|---|---|---|---|---|---|
| Google Analytics | ✓ | - | ✓ | - | - | ✓ | 设备、时间和操作系统相关报告需要 UA/屏幕/时区信息 |
| Google Tag Manager | - | - | - | - | - | - | 容器脚本加载,无用户数据传输 |
| Meta Pixel | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | 不可信广告网络,需完全匿名 |
| TikTok Pixel | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | 不可信广告网络,需完全匿名 |
| X/Twitter Pixel | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | 不可信广告网络,需完全匿名 |
| Snapchat Pixel | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | 不可信广告网络,需完全匿名 |
| Reddit Pixel | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | 不可信广告网络,需完全匿名 |
| Segment | - | - | - | - | - | - | 可信的数据通路,要求完整数据 |
| PostHog | - | - | - | - | - | - | 可信、开源,要求完整数据 |
| Microsoft Clarity | ✓ | - | ✓ | - | - | ✓ | 热图和设备筛选需要 UA/屏幕/时区信息 |
| Hotjar | ✓ | - | ✓ | - | - | ✓ | 热图和设备筛选需要 UA/屏幕/时区信息 |
✓ 表示匿名化处理,- 表示通过原始数据。
全局覆盖
一次性覆盖所有脚本默认隐私设置:
export default defineNuxtConfig({
scripts: {
firstParty: {
privacy: true, // 对所有脚本全面匿名化
}
}
})
或者选择性覆盖特定标识:
export default defineNuxtConfig({
scripts: {
firstParty: {
privacy: { ip: true }, // 仅对所有脚本匿名化 IP,其他使用脚本默认设置
}
}
})
1440x900 会变为 1920x1080(桌面类别),User-Agent 会归一化为 Mozilla/5.0 (compatible; Chrome/131.0),而如 canvas、WebGL、插件、字体等硬件指纹则被清空或重置。自定义路径
可自定义代理收集端点路径:
export default defineNuxtConfig({
scripts: {
firstParty: {
collectPrefix: '/_analytics', // 默认为 /_scripts/c
}
}
})
按脚本选择退出
禁用某脚本的第一方路由:
useScriptGoogleAnalytics({
id: 'G-XXXXXX',
scriptOptions: {
firstParty: false, // 直接从 Google 加载
}
})
支持的脚本
第一方模式支持以下脚本:
| 脚本 | 代理端点 |
|---|---|
| Google Analytics | google-analytics.com, analytics.google.com, stats.g.doubleclick.net, pagead2.googlesyndication.com |
| Google Tag Manager | www.googletagmanager.com |
| Meta Pixel | connect.facebook.net, www.facebook.com/tr, pixel.facebook.com |
| TikTok Pixel | analytics.tiktok.com |
| Segment | api.segment.io, cdn.segment.com |
| PostHog | us.i.posthog.com, eu.i.posthog.com, us-assets.i.posthog.com, eu-assets.i.posthog.com |
| Microsoft Clarity | www.clarity.ms, scripts.clarity.ms, d.clarity.ms, e.clarity.ms |
| Hotjar | static.hotjar.com, script.hotjar.com, vars.hotjar.com, in.hotjar.com |
| X/Twitter Pixel | analytics.twitter.com, t.co |
| Snapchat Pixel | tr.snapchat.com |
| Reddit Pixel | alb.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: true | firstParty: true |
|---|---|---|
| 构建时下载脚本 | ✅ | ✅ |
| 从您域名提供脚本 | ✅ | ✅ |
| 重写收集 URL | ❌ | ✅ |
| 代理 API 请求 | ❌ | ✅ |
| 隐藏用户 IP | ❌ | ✅ |
| 阻止第三方 Cookie | ❌ | ✅ |
bundle 仅自托管脚本文件。第一方模式还重写并代理所有收集/追踪端点,实现完整第一方路由。
bundle 选项已废弃。新项目请使用 firstParty: true。默认行为
第一方模式默认启用。对于静态托管(如 GitHub Pages),脚本仍被打包,但您需配置平台重写规则以使代理端点生效。
关闭第一方模式:
export default defineNuxtConfig({
scripts: {
firstParty: false
}
})
平台重写配置
部署到静态托管或边缘平台时,请配置如下重写规则以代理收集端点。
Vercel
{
"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
[[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 文件:
/_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 获取更灵活的控制:
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 │
│ ↓ │
│ 响应被代理回用户 │
│ │
└─────────────────────────────────────────────────────────────────────┘
故障排查
分析无数据追踪
症状:统计分析面板没有事件数据。
检查项:
- 验证代理路由是否生效 — 在开发模式下检查
/_scripts/status.json或通过 DevTools 的 Scripts 面板 - 查看浏览器网络面板 — 确认是否有对
/_scripts/c/路径的请求且返回状态码为 200 - 检查服务器日志 — 查找 Nitro 代理相关错误
- 验证路由规则 — 运行
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 错误。
解决方案:
- 确保
nuxt.config.ts中正确配置了第一方路由规则 - 静态托管环境确认重写规则正确生效
- 检查重写目标 URL 是否完全匹配
缓存旧脚本
症状:脚本内容过时或未重写 URL。
解决方案:
- 清理构建缓存:
rm -rf .nuxt/cache/scripts npx nuxi build - 开发模式下强制重新下载:
useScriptGoogleAnalytics({ id: 'G-XXX', scriptOptions: { bundle: 'force' } })
静态构建不代理
症状:脚本加载正常,但静态托管的分析无数据。
说明:这是预期表现——静态构建无法代理请求。请配置托管平台重写(参见“平台重写配置”章节),或使用服务端渲染模式。
脚本下载失败
症状:构建时遇到网络超时或下载错误。
解决方案:
- 启用回退模式:nuxt.config.ts
export default defineNuxtConfig({ scripts: { firstParty: true, assets: { fallbackOnSrcOnBundleFail: true } } }) - 增加超时配置:nuxt.config.ts
export default defineNuxtConfig({ scripts: { assets: { fetchOptions: { timeout: 30000 // 30 秒 } } } })
常见问题
第一方模式会绕过 GDPR 同意要求吗?
不会。 第一方模式仅变更请求路由,不影响是否进行跟踪。您仍须在加载跟踪脚本前获得用户同意。使用同意触发器实现控制:
const { accept } = useScriptTriggerConsent()
// 仅在用户同意后加载脚本
function onConsentGiven() {
accept()
}
第三方修改了脚本,分析还能正常吗?
能,且支持自动更新。 脚本默认缓存 7 天,缓存过期后会自动重新下载并重写 URL。您可自定义缓存时长:
export default defineNuxtConfig({
scripts: {
assets: {
cacheMaxAge: 86400000 // 1 天,单位毫秒
}
}
})
可以自定义代理路径吗?
可以。 通过 collectPrefix 配置修改默认的收集端点地址:
export default defineNuxtConfig({
scripts: {
firstParty: {
collectPrefix: '/_tracking' // 代替默认的 /_scripts/c
}
}
})
支持服务器端追踪吗?
第一方模式主要针对客户端脚本。服务器端追踪(如 Measurement Protocol)建议直接从服务器端发送请求,无需代理层。
如何调试被代理的请求?
- DevTools:打开 Nuxt DevTools → Scripts → 第一方标签页
- 状态端点:开发时访问
/_scripts/status.json检查状态 - 日志输出:启用调试模式打印日志nuxt.config.ts
export default defineNuxtConfig({ scripts: { debug: true } })
会影响性能吗?
影响极小。 代理带来约 10–50 毫秒的延迟,但通常被以下因素抵消:
- 免去对第三方域名的 DNS 查询
- 绕过可能阻止请求的广告拦截器
- 重用连接减少 TLS 握手时间
哪些脚本支持第一方模式?
当前支持文档「支持的脚本」中列出的 11 个脚本。对其他脚本,您可以:
- 通过 Issue 请求支持
- 使用(已废弃的)
bundle选项自托管脚本 - 手动配置自定义路由规则
混合渲染
第一方模式兼容 Nuxt 的混合渲染架构:
路由级 SSR
禁用特定路由的 SSR 时,第一方模式依旧有效,因为:
- 脚本在构建阶段打包,不依赖运行时 SSR
- 代理路由由 Nitro 全局配置管理
- 收集请求由服务器转发代理
示例:
export default defineNuxtConfig({
routeRules: {
'/dashboard/**': { ssr: false }, // 仪表盘 SPA 模式
},
scripts: {
firstParty: true, // 各路由均启用第一方
}
})
ISR(增量静态再生成)
ISR 页面同样支持第一方模式。打包的脚本作为静态资源提供,收集请求由 Nitro 在运行时代理。
Edge 渲染
支持部署到边缘平台(如 Cloudflare Workers、Vercel Edge)。代理请求通过 Nitro 内置代理功能支持多种目标环境。
同意集成
第一方模式作为隐私保护方案与同意管理配合增强:
- 第一方模式 控制请求发送的位置(通过您的服务器代理)
- 同意触发器 控制脚本加载的时机(同意前不注入脚本)
联合使用示例
<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
显示已缓存的脚本文件及大小。