打包远程脚本

通过将第三方脚本与您的应用打包来优化它们。
bundle 选项已弃用,建议使用 First-Party Mode,它提供相同的优势并增加了路由收集端点以提升隐私保护。新项目请使用 firstParty: true

背景

当您在网站上使用来自其他站点的脚本时,会依赖另一个服务器来加载这些脚本。这可能会导致网站变慢,并引发安全和隐私方面的担忧。

常见问题

  • 网站变慢,因为连接其他服务器需要时间。
  • 如果其他服务器被黑,存在安全风险。
  • 访客数据可能被其他服务器不当使用。
  • 广告拦截器或隐私工具可能阻止这些脚本运行。

如何解决

通过打包这些脚本,您可以自行托管,从而避免这些问题,让您的网站运行更加顺畅。

工作原理

在构建过程中,会检查您的代码以查找需要打包的 useScript 实例。

当检测到需要打包的脚本时,它会被下载并保存为公共资源,路径为 /_scripts/[hash].js,其中 [hash] 表示脚本 URL 的哈希值。

关于打包的重要事项:

  1. 您需要为脚本 URL 和打包设置提供静态值。
// 好例子 - 静态值允许打包
useScript('https://example.com/script.js', {
  bundle: true
})
// 坏例子 - 动态值阻止打包
useScript(scriptSrc, {
  bundle: canBundle
})
  1. 如果原始脚本更改但 URL 未变化,浏览器缓存中的打包版本不会更新。为此,请使用带版本号的 URL 或添加防缓存查询参数。

使用方法

脚本可以单独打包,也可以通过特定设置全局打包。

脚本选项

决定是否打包某个脚本,使用 bundle 选项。

// 选择性打包这个脚本
useScript('https://example.com/script.js', {
  bundle: true,
})

// 强制下载绕过缓存
useScript('https://example.com/script.js', {
  bundle: 'force',
})

打包选项

bundle 选项支持以下值:

  • false - 不打包脚本(默认)
  • true - 打包脚本,若有缓存则使用缓存版本
  • 'force' - 打包脚本,并强制下载绕过缓存

注意: 使用 'force' 会在每次构建时重新下载脚本,可能增加构建时间且安全性较低。

全局打包

通过 Nuxt 配置调整所有脚本的默认行为。下例中所有脚本默认启用打包。

nuxt.config.ts
export default defineNuxtConfig({
  scripts: {
    defaultScriptOptions: {
      bundle: true,
    }
  }
})

构建时与运行时行为

理解打包发生时机及其对运行时行为的影响对有效使用至关重要。

构建时处理

打包通过静态代码分析在构建阶段完成:

// ✅ 构建时打包(静态值)
useScript('https://example.com/script.js', { bundle: true })

// ❌ 无法打包(动态值)
const scriptUrl = computed(() => getScriptUrl())
useScript(scriptUrl, { bundle: dynamic.value })

运行时行为

运行时,打包的脚本表现不同:

// 原始代码
useScript('https://example.com/script.js', { bundle: true })

// 构建转化后
useScript('/_scripts/abc123.js', {})

重要:一旦打包,运行时无法访问原始 URL。如果需要原始 URL 用于追踪或分析,请额外保存。

静态 URL 要求

打包转换器要求 完全静态的值

// 静态字符串字面量
useScript('https://cdn.example.com/lib.js', { bundle: true })

// 静态模板字符串(无变量)
useScript(`https://cdn.example.com/lib.js`, { bundle: true })

// 在模块顶层定义的常量
const SCRIPT_URL = 'https://cdn.example.com/lib.js'
useScript(SCRIPT_URL, { bundle: true })

手动注入模式

当自动打包不可用时,可以手动注入打包脚本:

手动打包解决方案
// 1. 使用静态URL构建时打包
const staticScript = useScript('https://cdn.example.com/static.js', {
  bundle: true,
  trigger: 'manual' // 不自动加载
})

// 2. 根据运行时逻辑条件加载
function loadScript() {
  if (shouldLoadScript.value) {
    staticScript.load()
  }
}

// 3. 另一种方案:使用多个静态配置
const scriptVariants = {
  dev: useScript('https://cdn.example.com/dev.js', { bundle: true, trigger: 'manual' }),
  prod: useScript('https://cdn.example.com/prod.js', { bundle: true, trigger: 'manual' })
}

// 加载合适的版本
const currentScript = computed(() =>
  isDev ? scriptVariants.dev : scriptVariants.prod
)

动态 URL 处理

对于真正动态的场景,可考虑以下方案:

动态 URL 策略
// 选项1:预先打包已知变体
const analytics = {
  google: useScript('https://www.googletagmanager.com/gtag/js', { bundle: true }),
  plausible: useScript('https://plausible.io/js/script.js', { bundle: true })
}

// 选项2:回退运行时加载
function loadDynamicScript(url: string) {
  // 该脚本不会被打包,但运行时可用
  return useScript(url, {
    bundle: false, // 显式禁用
    trigger: 'manual'
  })
}

// 选项3:使用服务器端打包
// 将脚本内容存入包内并手动注入
const { $script } = useNuxtApp()
$script.add({
  innerHTML: await $fetch('/api/dynamic-script-content'),
})

资源配置

利用配置中的 assets 选项定制脚本的打包和缓存行为。

nuxt.config.ts
export default defineNuxtConfig({
  scripts: {
    assets: {
      prefix: '/_custom-script-path/',
      cacheMaxAge: 86400000, // 1 天,单位毫秒
      integrity: true, // 启用 SRI 哈希生成
    }
  }
})

可用选项

  • prefix - 打包脚本服务的自定义路径(默认:/_scripts/
  • cacheMaxAge - 打包脚本缓存时长,单位毫秒(默认:7 天)
  • integrity - 启用自动 SRI(子资源完整性)哈希生成(默认:false

缓存行为

打包系统使用两种不同的缓存策略:

  • 构建时缓存:由 cacheMaxAge 控制(默认 7 天)。超过该时长的脚本会在构建期间重新下载,确保新鲜度。
  • 运行时缓存:打包脚本带有 1 年缓存头,因为它们是基于内容哈希命名的。

这种双重策略平衡了构建性能与浏览器缓存的可靠性。

子资源完整性 (SRI)

子资源完整性(SRI)是一项安全特性,确保脚本未被篡改。启用时,会为每个打包脚本计算加密哈希,并添加为 integrity 属性。

启用 SRI

nuxt.config.ts
export default defineNuxtConfig({
  scripts: {
    assets: {
      integrity: true, // 默认使用 sha384
    }
  }
})

哈希算法

您可以指定哈希算法:

nuxt.config.ts
export default defineNuxtConfig({
  scripts: {
    assets: {
      integrity: 'sha384', // 默认,安全性与大小之间的推荐平衡
      // integrity: 'sha256', // 哈希更小
      // integrity: 'sha512', // 最强安全性
    }
  }
})

工作流程

当启用 integrity

  1. 构建时,会对每个打包脚本内容计算哈希
  2. 哈希存储在构建缓存中以便复用
  3. 脚本标签中注入 integrity 属性
  4. 自动添加 crossorigin="anonymous" 属性(浏览器要求)
<!-- 启用 integrity 后的输出示例 -->
<script src="/_scripts/abc123.js"
        integrity="sha384-oqVuAfXRKap..."
        crossorigin="anonymous"></script>

安全优势

  • 篡改检测:若哈希不匹配,浏览器拒绝执行脚本
  • CDN 被攻破保护:即使 CDN 被篡改,修改的脚本也不会被执行
  • 构建时校验:哈希基于实际下载的内容计算,确保准确性