VueUse useScriptTag 深度指南:在 Vue 3 中优雅地动态加载与卸载第三方脚本

原创2026-09-30 23:37:37344 阅读
文章标签:前端

VueUse useScriptTag 深度指南:在 Vue 3 中优雅地动态加载与卸载第三方脚本

在 Vue 3 应用中集成 Twitch、YouTube 等第三方 SDK,或在运行时按需加载一段远程 JavaScript,是常见的真实需求。VueUse 提供的 useScriptTag 组合式函数正是为此而生:它负责在组件挂载时创建并注入 <script> 标签、在组件卸载时自动清理,并且能智能复用相同 URL 的脚本,避免重复请求。读完本文,你将掌握 useScriptTag 的全部配置项、手动加载模式,以及它内部基于 DOM 查询、事件监听与生命周期钩子的完整实现原理,能够直接在自己的项目中正确使用并排查问题。

功能概述

useScriptTag 的核心职责可以概括为三点:

  1. 按需注入:给定一个脚本 URL,在组件挂载时自动创建 <script> 标签并追加到 document.head;
  2. 自动清理:组件卸载时自动把该脚本标签从 DOM 中移除,防止脚本与事件监听器长期驻留;
  3. URL 去重:如果页面中已经存在相同 src 的脚本标签,不会重复创建,而是直接复用它。

这一行为与其文档描述完全一致:脚本在组件挂载时自动加载、卸载时自动删除;同时需要注意,如果同页面上此前某次调用已经加载过同一个 URL 并随后卸载了该脚本,那么再次调用时脚本会重新加载,因为旧的标签已被移除(见 index.md)。

基础用法:自动加载模式

默认情况下,useScriptTag 只接受一个脚本 URL 和一个加载完成的回调,组件挂载后立即加载脚本:

import { useScriptTag } from '@vueuse/core'

useScriptTag(
  'https://player.twitch.tv/js/embed/v1.js',
  // 脚本加载完成后触发,回调参数为已就绪的 script 元素
  (el: HTMLScriptElement) => {
    // do something
  },
)

回调中的 el 就是真实存在 DOM 中的 HTMLScriptElement,你可以在这里初始化第三方 SDK、读取脚本暴露的全局对象,或执行后续业务逻辑。

useScriptTag 从 packages/core/index.ts 中统一导出(export * from './useScriptTag'),因此它和其他所有核心工具一样,可以直接从 @vueuse/core 包中按需引入。该包要求 Vue ^3.5.0 作为 peerDependency(见 packages/core/package.json)。

手动模式:精确控制加载与卸载时机

如果不想在挂载时就加载脚本,而是希望在某个用户操作、路由跳转或业务条件成立时才加载,可以把 manual: true 传给配置项。此时 useScriptTag 返回 load 与 unload 两个控制函数,供你自行决定时机:

import { useScriptTag } from '@vueuse/core'

const { scriptTag, load, unload } = useScriptTag(
  'https://player.twitch.tv/js/embed/v1.js',
  () => {
    // do something
  },
  { manual: true },
)

// 手动控制
await load()
await unload()

load 返回一个 Promise,因此可以配合 await 在脚本真正就绪后再继续后续逻辑;unload 则同步移除脚本标签。关于两者的返回值与具体行为,在后面的源码剖析小节中会详细展开。

完整配置项一览

useScriptTag 的第二个参数是 onLoaded 回调(可省略),第三个参数是配置对象 UseScriptTagOptions。除文档明确提到的 manual 外,它还支持一大批实用选项,全部定义在 packages/core/useScriptTag/index.ts 的 UseScriptTagOptions 接口中:

配置项 类型 默认值 说明
immediate boolean true 是否在组件挂载后立即加载脚本。设为 false 时即使不开启 manual,也需要手动调用 load() 才会加载
async boolean true 是否给 <script> 添加 async 属性
type string 'text/javascript' 脚本的 type 属性
manual boolean false 是否完全手动控制加载与卸载时机
crossOrigin 'anonymous' | 'use-credentials' — 设置脚本的 crossorigin 属性,用于 CORS 场景
referrerPolicy 'no-referrer' | 'no-referrer-when-downgrade' | 'origin' | 'origin-when-cross-origin' | 'same-origin' | 'strict-origin' | 'strict-origin-when-cross-origin' | 'unsafe-url' — 设置脚本的 referrerpolicy 属性,控制请求携带的 Referrer 信息
noModule boolean — 是否添加 nomodule 属性,常用于为不支持模块的旧浏览器加载降级脚本
defer boolean — 是否添加 defer 属性,让脚本在文档解析完成后执行
attrs Record<string, string> {} 以对象形式追加任意自定义属性(如 id、data-* 等)
nonce string undefined CSP(内容安全策略)所需的 nonce 值
document Document 默认 document 自定义 document 实例,例如处理 iframe 或测试环境

其中 document 选项继承自 packages/core/_configurable.ts 中定义的 ConfigurableDocument 接口,默认值 defaultDocument 仅在客户端(isClient)环境下指向 window.document,这在服务端渲染时会自动退化为 undefined,从而保证 SSR 安全。

结合配置项,一个带自定义属性和 CSP nonce 的完整示例大致如下:

const { load } = useScriptTag(
  'https://example.com/sdk.js',
  () => console.log('SDK ready'),
  {
    attrs: { id: 'my-sdk', 'data-env': 'production' },
    nonce: 'RANDOM_NONCE',
    defer: true,
  },
)

这些选项在源码中会逐一映射到创建的 <script> 元素上,具体映射逻辑见下文剖析。

源码剖析:useScriptTag 是如何工作的

理解了配置项,再来看看 packages/core/useScriptTag/index.ts 内部的核心实现,这能帮你理解它为什么具备"自动加载、自动卸载、URL 去重"三大行为。

返回值结构

useScriptTag 返回 UseScriptTagReturn:

export interface UseScriptTagReturn {
  scriptTag: ShallowRef<HTMLScriptElement | null>
  load: (waitForScriptLoad?: boolean) => Promise<HTMLScriptElement | boolean>
  unload: () => void
}
  • scriptTag:一个 ShallowRef,指向当前实际存在的 <script> 元素,卸载后变为 null,可响应式地驱动 UI 状态;
  • load:加载函数,返回 Promise,可通过参数控制等待策略(下文说明);
  • unload:卸载函数,同步移除脚本标签。

loadScript:查询、创建与事件绑定

loadScript 是内部核心函数(源码第 98 行起),其流程为:

  1. SSR 保护:如果 document 不存在(如服务端环境),直接 resolve(false),不进行任何 DOM 操作;
  2. URL 查询去重:通过 document.querySelector('script[src="..."]') 查找是否已存在相同 src 的脚本。若不存在,则 document.createElement('script') 创建新元素,并依次设置 type、async、src,再按配置可选地设置 defer、crossOrigin、noModule、referrerPolicy、nonce,最后通过 el.setAttribute 把 attrs 中的自定义属性全部写入;若已存在且带有 data-loaded 标记,则直接复用并 resolve;
  3. 事件监听:用 useEventListener 为脚本元素绑定 error、abort(两者都 reject)与 load(设置 data-loaded 标记、调用 onLoaded 回调并 resolve)事件,监听选项为 { passive: true };
  4. 追加到 head:document.head.appendChild(el) 注入页面。

其中"查找已有脚本并复用"正是文档中"相同 URL 不重复创建"承诺的实现依据;而 data-loaded 属性标记则保证了脚本加载完成后,后续复用同一 URL 的调用能立即拿到已就绪的元素。

load:单例 Promise 防重复调用

load 是对 loadScript 的单例包装:内部缓存 _promise,如果已有进行中的加载请求,直接返回同一个 Promise,避免对同一脚本重复触发加载。load(waitForScriptLoad = true) 的布尔参数含义是:true 时 Promise 等到脚本触发 load 事件后才 resolve;false 时脚本追加到 DOM 后立即 resolve,不等待真实加载完成。

unload:移除脚本并重置状态

unload 会先将 _promise 置空、把 scriptTag.value 设为 null,再通过 querySelector 找到对应 src 的脚本元素并从 document.head 移除。这也解释了文档中的提醒:如果你在卸载后又用同一 URL 调用 useScriptTag,由于旧标签已被删除,脚本会重新加载一次。

生命周期绑定

源码末尾通过 tryOnMounted 与 tryOnUnmounted(来自 @vueuse/shared,见 packages/shared/tryOnMounted/index.ts)把加载与清理挂到组件生命周期上:

  • immediate && !manual 时,挂载后调用 load;
  • !manual 时,卸载时调用 unload。

值得注意的是 tryOnMounted 的语义:如果在组件生命周期内调用则挂载到 onMounted,否则(如非组件上下文)直接同步执行,这让 useScriptTag 在普通模块或测试环境中也能工作。

测试验证:行为都有据可查

仓库为 useScriptTag 提供了完整的浏览器环境测试 packages/core/useScriptTag/index.browser.test.ts,可以作为理解其行为的"行为规范":

  • 自动加载:immediate: true 时,组件挂载后 document.head.appendChild 被调用,且 document.head 中出现对应 src 的 HTMLScriptElement;
  • URL 复用:同一 URL 分别创建两个 manual: true 的实例并各自 load,appendChild 只被调用一次,两个实例共享同一个脚本元素;
  • 自定义属性:传入 attrs: { 'id': ..., 'data-test': ... } 后,创建的元素上能读取到对应属性;
  • 卸载清理:组件 unmount 后 removeChild 被调用、脚本从 DOM 消失、scriptTag 变为 null;
  • 手动 unload:调用 unload 同样能移除脚本;
  • 多实例卸载:两个共享同一 URL 的实例分别 unload 时,removeChild 也只被调用一次(幂等清理)。

这些测试用例直接印证了本文前文所述的"自动加载 / URL 去重 / 自动卸载"三大行为,也为你排查"脚本没加载"、"脚本没移除"等问题提供了参照。

最佳实践与注意事项

  1. 重复加载提醒:useScriptTag 只保证"同一时刻"不重复创建同一 URL 的脚本。如果某次调用加载后又卸载了该脚本,下次调用会重新发起加载——这正是文档强调的注意事项,适合用 manual 模式配合全局状态统一管理;
  2. 等待语义:默认 load() 会等待脚本真正加载完成(load 事件),适合"先加载 SDK 再初始化"的时序依赖场景;若只需"尽快把脚本放进 DOM",可传 load(false) 立即返回;
  3. SSR 安全:服务端没有 document,此时 loadScript 直接 resolve false 而不会抛错,配合 immediate 也不会在服务端注入脚本,可安全用于 Nuxt 等 SSR 框架;
  4. 自定义 document:在 iframe 或特殊测试环境中,可通过 document 选项注入自定义 Document,避免影响主文档;
  5. CSP 环境:受内容安全策略约束的站点,应使用 nonce 选项传入与 CSP 策略匹配的 nonce 值;
  6. 响应式 src:src 参数的类型是 MaybeRefOrGetter<string>,即支持传入 ref 或 getter,让脚本地址本身也可以是响应式的。

总而言之,useScriptTag 用极小的 API 面(一个 URL、一个回调、一组配置项)封装了"动态脚本注入 + 生命周期管理 + 去重复用"这一完整能力,其文档、源码与测试三者在仓库中相互印证,是理解 VueUse"小而美"设计哲学的典型示例。

登录后查看全文
vueuse