---
title: "介绍 Nuxt Scripts"
description: "Nuxt Scripts 为第三方脚本提供更好的性能、隐私、安全性和开发者体验。"
canonical_url: "https://nuxt.zhcndoc.com/blog/nuxt-scripts"
---
# 介绍 Nuxt Scripts

> Nuxt Scripts 为第三方脚本提供更好的性能、隐私、安全性和开发者体验。

Nuxt 团队与 [Chrome Aurora](https://developer.chrome.com/aurora) 团队在 Google 的合作下，兴奋地宣布 [Nuxt Scripts](https://scripts.nuxt.com) 的公开测试版发布。

Nuxt Scripts 是处理第三方脚本的更好方式，提供了更好的性能、隐私、安全性和开发者体验。

![Nuxt Scripts Banner](https://nuxt.zhcndoc.com/assets/blog/nuxt-scripts/banner.png){.border.border-gray-200.dark:border-gray-700.rounded-lg}

## 开始使用 Nuxt Scripts

在一年前，Daniel 发布了初步的 [Nuxt Scripts RFC](https://github.com/nuxt/nuxt/discussions/22016)。该 RFC 提出了一个模块，旨在“允许管理和优化第三方脚本，遵循性能和合规网站的最佳实践”。

拥有 [个人经验](https://github.com/harlan-zw/nuxt-delay-hydration) 解决与第三方脚本相关的性能问题，我深知这些性能优化会多么困难。尽管如此，我仍然渴望迎接这个挑战，并接手了项目。

以 RFC 为理念的种子，我开始原型设计其可能的 [样子](https://github.com/nuxt/nuxt/discussions/22016#discussioncomment-6527001)，并使用 [Unhead](https://unhead.unjs.io/) 进行开发。

在思考我希望确切构建的内容时，我发现真正的问题不仅是如何加载“优化过的”第三方脚本，而是如何整体上改善与第三方脚本的合作体验。

## 为什么要构建第三方脚本模块？

[94%的网站至少使用一个第三方提供商](https://almanac.httparchive.org/en/2022/third-parties#prevalence)，平均每个网站有 [五个第三方提供商](https://docs.google.com/spreadsheets/d/1YqoRRsyiNsrEabVLu2nRU98JIG_0zLLuoQhC2nX8xbM/edit?gid=1428106498#gid=1428106498)。

我们知道第三方脚本并不完美；它们 [拖慢网页速度](https://web.dev/articles/optimizing-content-efficiency-loading-third-party-javascript#)，造成隐私和安全问题，并且与之合作非常麻烦。

然而，它们在根本上是有用的，短期内不会消失。

通过探索第三方脚本的问题，我们可以看到可以进行改进的地方。

### 😒 开发者体验：全栈的头痛

让我们通过向您的 Nuxt 应用添加一个虚构的 `tracker.js` 脚本，该脚本向窗口添加 `track` 函数，来了解一下。

我们开始使用 `useHead` 加载脚本。

```ts
useHead({ script: [{ src: '/tracker.js', defer: true }] })
```

但是，现在我们尝试在应用中使脚本功能正常工作。

在 Nuxt 中处理第三方脚本时，以下步骤是常见的：

- 一切必须包装以确保 SSR 安全。
- 不可靠的检查以确认脚本是否已加载。
- 为类型增强窗口对象。

::code-group
```vue [1: SSR 安全]
<script setup>
// ❌ 哦不，window 未定义！ 
// 💡 如果我们在 Nuxt 中使用 SSR，窗口无法直接访问。
// 👉 我们需要确保这个 SSR 是安全的
window.track('page_view', useRoute().path)
</script>
```

```vue [2: 脚本时机]
<script setup>
if (import.meta.client) {
  // ❌ 哦不，脚本尚未加载完成！
  // 💡 `defer` 脚本可能在我们的 Nuxt 应用水合期间不可用。
  // 👉 我们需要等待脚本加载完成
  window.track('page_view', useRoute().path)
}
</script>
```

```vue [3: 类型破损]
<script lang="ts" setup>
if (import.meta.client) {
  useTimeoutFn(() => {
    // ✅ 正在工作！
    // ❌ 哦不，类型破损。
    // 💡 `window` 具有严格类型，并且尚未定义任何内容。
    // 👉 我们需要手动增强窗口
    window.track('page_view', useRoute().path)
  }, 1000 /* 应该在 1 秒内加载！？ */)
}
</script>
```

```vue [4: 它工作吗？]
<script lang="ts" setup>
declare global {
  interface Window {
    track: (e: string, p: string) => void
  }
}
if (import.meta.client) {
  useTimeoutFn(() => {
    // ✅ 它正在工作且类型有效！ 
    // ❌ 哦不，广告拦截器、GDPR 和重复脚本
    // 💡 第三方脚本中有许多隐藏的复杂性
    // 👉 我们需要一个更好的 API
    window.track('page_view', useRoute().path)
  }, 1000)
}
</script>
```
::

### 🐌 性能：“为什么我无法在 Lighthouse 上获得 100？”

为了使访问者能够开始与您的 Nuxt 网站互动，应用程序包需要被下载，Vue 需要对应用实例进行水合。

加载第三方脚本可能会干扰此水合过程，即使使用 `async` 或 `defer`。这会减慢网络速度并阻塞主线程，从而导致用户体验下降和糟糕的 [核心网络指标](https://web.dev/vitals/)。

[Chrome 用户体验报告](https://developer.chrome.com/docs/crux) 显示，使用许多第三方资源的 Nuxt 网站通常在 [交互到下一个绘制 (INP)](https://web.dev/articles/inp) 和 [最大内容绘制 (LCP)](https://web.dev/articles/lcp) 分数上较低。

为了查看第三方脚本如何降低性能，我们可以查看 [Web Almanac 2022](https://almanac.httparchive.org/en/2022/third-parties#impact-on-performance)。报告显示，前10个第三方脚本 **平均中断时间为 1.4 秒**。

### 🛡️ 隐私与安全：不做坏事？

在前 10,000 个网站中，有58%网站存在 [在外部 cookie 中交换跟踪 ID 的第三方脚本](https://www3.cs.stonybrook.edu/~mikepo/papers/firstparty.www21.pdf)，这意味着它们可以在用户禁用第三方 cookie 的情况下跨网站跟踪用户。

虽然在许多情况下，我们与使用的提供商的选择受到限制，但我们应该尽量减少泄露终端用户数据的数量。

当我们涉及隐私问题时，准确传达这些信息也可能很困难，并且需要构建所需的同意管理，以遵守 GDPR 等法规。

使用第三方脚本时安全性也是一个关注点。第三方脚本是恶意行为者常用的攻击向量，大多数并未为其脚本提供 `integrity` 哈希，这意味着它们可以随时被破坏，并将恶意代码注入到您的应用程序中。

## Nuxt Scripts 如何解决这些问题？

### 可组合的: useScript

这个可组合函数位于 `<script>` 标签和添加到 `window.{thirdPartyKey}` 的功能之间。

对于 `<script>` 标签，可组合函数:

- 完全显示脚本的加载和错误状态
- 默认情况下，在 Nuxt 水合应用时加载脚本，以获得稍微更好的性能。
- 限制 `crossorigin` 和 `referrerpolicy` 以提高隐私和安全性。
- 提供方法以 [延迟加载脚本](https://scripts.nuxt.com/docs/guides/script-triggers)，直到需要为止。

对于脚本 API，它：

- 提供完全类型安全的脚本功能
- 添加一个代理层，允许你的应用在脚本功能处于不安全的上下文中运行（SSR、脚本未加载前、脚本被阻止）

::code-group
```ts [hello.ts]
const { proxy, onLoaded } = useScript('/hello.js', {
  trigger: 'onNuxtReady',
  use() {
    return window.helloWorld
  }
})

onLoaded(({ greeting }) => {
  // ✅ 脚本已加载！挂钩到 Vue 生命周期
})

// ✅ 或者使用代理 API - 友好 SSR，在脚本加载时调用
proxy.greeting() // Hello, World!

declare global {
  interface Window {
    helloWorld: {
      greeting: () => 'Hello World!'
    }
  }
}
```

```ts [hello.js]
window.helloWorld = {
  greeting() {
    console.log('Hello, World!')
  }
}
```
::

### 脚本注册表

[脚本注册表](https://scripts.nuxt.com/scripts) 是一个常见第三方脚本的一系列第一方集成。发布时，我们支持 21 个脚本，未来还会有更多。

![Nuxt Scripts Registry](https://nuxt.zhcndoc.com/assets/blog/nuxt-scripts/registry.png){.border.border-gray-200.dark:border-gray-700.rounded-lg}

这些注册表脚本是围绕 `useScript` 进行微调的包装，具有完整的类型安全性，运行时验证脚本选项（仅限开发）和环境变量支持。

例如，我们可以查看 [Fathom Analytics](https://scripts.nuxt.com/scripts/analytics/fathom-analytics) 脚本。

```ts
const { proxy } = useScriptFathomAnalytics({
  // ✅ 选项在运行时被验证
  site: undefined
})
// ✅ 类型安全
proxy.trackPageview()
```

### 外观组件

注册表包含几个 [外观组件](https://scripts.nuxt.com/docs/guides/facade-components)，如
[Google Maps](https://scripts.nuxt.com/scripts/content/google-maps)、[YouTube](https://scripts.nuxt.com/scripts/content/youtube-player) 和 [Intercom](https://scripts.nuxt.com/scripts/support/intercom)。

外观组件是“虚假”组件，当第三方脚本加载时进行水合。
外观组件有权衡，但可以大幅提高您的性能。请参阅 [什么是外观组件？](https://scripts.nuxt.com/docs/guides/facade-components#what-are-facade-components)，
了解有关更多信息，。

Nuxt Scripts 提供外观组件作为可访问但无头的组件，意味着它们默认不带样式，
但添加必要的可访问性数据。

::tabs
  :::tabs-item{label="输出"}
    ::::youtube-demo
    ::::
  :::

  :::tabs-item{label="输入"}
  ```vue
  <script setup lang="ts">
  const isLoaded = ref(false)
  const isPlaying = ref(false)
  const video = ref()
  function play() {
    video.value?.player.playVideo()
  }
  function stateChange(state) {
    isPlaying.value = state.data === 1
  }
  </script>

  <template>
    <ScriptYouTubePlayer ref="video" video-id="d_IFKP1Ofq0" @ready="isLoaded = true" @state-change="stateChange">
      <template #awaitingLoad>
        <div class="absolute left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2 h-[48px] w-[68px]">
          <svg height="100%" version="1.1" viewBox="0 0 68 48" width="100%"><path d="M66.52,7.74c-0.78-2.93-2.49-5.41-5.42-6.19C55.79,.13,34,0,34,0S12.21,.13,6.9,1.55 C3.97,2.33,2.27,4.81,1.48,7.74C0.06,13.05,0,24,0,24s0.06,10.95,1.48,16.26c0.78,2.93,2.49,5.41,5.42,6.19 C12.21,47.87,34,48,34,48s21.79-0.13,27.1-1.55c2.93-0.78,4.64-3.26,5.42-6.19C67.94,34.95,68,24,68,24S67.94,13.05,66.52,7.74z" fill="#f00" /><path d="M 45,24 27,14 27,34" fill="#fff" /></svg>
        </div>
      </template>
    </ScriptYouTubePlayer>
  </template>
  ```
  :::
::

### 同意管理与元素事件触发器

`useScript` 可组合函数使您完全控制脚本的加载方式和时机，可以通过提供自定义 `trigger` 或手动调用 `load()` 函数。

在此基础上，Nuxt Scripts 提供了高级触发器，以使其更容易。

- [同意管理](https://scripts.nuxt.com/docs/guides/consent) - 仅在用户给予同意后加载脚本，例如通过 cookie 横幅。
- [元素事件触发器](https://scripts.nuxt.com/docs/guides/script-triggers#element-event-triggers) - 根据用户交互（如滚动、点击或表单提交）加载脚本。

```ts
const cookieConsentTrigger = useScriptTriggerConsent()
const { proxy } = useScript<{ greeting: () => void }>('/hello.js', {
  // 仅在同意被接受后加载脚本
  trigger: cookieConsentTrigger
})
// ...
function acceptCookies() {
  cookieConsentTrigger.accept()
}
// greeting() 在用户接受 cookies 之前会被排队
proxy.greeting()
```

### 脚本打包

在许多情况下，我们从一个我们无法控制的域加载第三方脚本。这可能导致许多问题：

- 隐私：第三方脚本可以跨站点跟踪用户。
- 安全：第三方脚本可能被破坏并注入恶意代码。
- 性能：额外的 DNS 查询会减慢页面加载。
- 开发者体验：经过同意的脚本可能会被广告拦截器阻止。

为此，Nuxt Scripts 提供了一种将第三方脚本打包到您的公共目录而无需额外工作的方法。

```ts
useScript('https://cdn.jsdelivr.net/npm/js-confetti@latest/dist/js-confetti.browser.js', {
  bundle: true,
})
```

现在，这个脚本将从 `/_scripts/{hash}` 以您的自己域名提供。

## 待续

正如我们所看到的，还有许多机会可以改进开发者和最终用户的第三方脚本。

Nuxt Scripts 的初始发布解决了 *一些* 这些问题，但我们还有很多工作要做。

下一步的规划项目包括：

- [添加 web worker 支持 (Partytown)](https://github.com/nuxt/scripts/issues/182)
- [更多实时聊天外观组件](https://github.com/nuxt/scripts/issues/44)
- [将脚本卸载到 Nuxt 服务器代理](https://github.com/nuxt/scripts/issues/87)
- [Iframe 脚本沙箱](https://github.com/nuxt/scripts/issues/131)

我们欢迎您的贡献和支持。

## 开始使用

要开始使用 Nuxt Scripts，我们创建了一份 [教程](https://scripts.nuxt.com/docs/getting-started/confetti-tutorial) 来帮助您快速上手。

## 感谢

- [Harlan Wilton - Nuxt](https://github.com/harlan-zw) (作者)
- [Julien Huang - Nuxt](https://github.com/huang-julien) (贡献者)
- [Daniel Roe - Nuxt](https://github.com/danielroe) (贡献者)
- [Chrome Aurora - Google](https://developer.chrome.com/aurora) (贡献者)

并感谢早期的贡献者们。

![Nuxt Scripts Contributors](https://nuxt.zhcndoc.com/assets/blog/nuxt-scripts/contributors.png){.border.border-gray-200.dark:border-gray-700.rounded-lg}


## Sitemap

See the full [sitemap](https://nuxt.zhcndoc.com/sitemap.md) for all pages.
