VueUse useScriptTag 深度指南:在 Vue 3 中优雅地动态加载与卸载第三方脚本
VueUse useScriptTag 深度指南:在 Vue 3 中优雅地动态加载与卸载第三方脚本
在 Vue 3 应用中集成 Twitch、YouTube 等第三方 SDK,或在运行时按需加载一段远程 JavaScript,是常见的真实需求。VueUse 提供的 useScriptTag 组合式函数正是为此而生:它负责在组件挂载时创建并注入 <script> 标签、在组件卸载时自动清理,并且能智能复用相同 URL 的脚本,避免重复请求。读完本文,你将掌握 useScriptTag 的全部配置项、手动加载模式,以及它内部基于 DOM 查询、事件监听与生命周期钩子的完整实现原理,能够直接在自己的项目中正确使用并排查问题。
功能概述
useScriptTag 的核心职责可以概括为三点:
- 按需注入:给定一个脚本 URL,在组件挂载时自动创建
<script>标签并追加到document.head; - 自动清理:组件卸载时自动把该脚本标签从 DOM 中移除,防止脚本与事件监听器长期驻留;
- 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 行起),其流程为:
- SSR 保护:如果
document不存在(如服务端环境),直接resolve(false),不进行任何 DOM 操作; - URL 查询去重:通过
document.querySelector('script[src="..."]')查找是否已存在相同src的脚本。若不存在,则document.createElement('script')创建新元素,并依次设置type、async、src,再按配置可选地设置defer、crossOrigin、noModule、referrerPolicy、nonce,最后通过el.setAttribute把attrs中的自定义属性全部写入;若已存在且带有data-loaded标记,则直接复用并 resolve; - 事件监听:用
useEventListener为脚本元素绑定error、abort(两者都 reject)与load(设置data-loaded标记、调用onLoaded回调并 resolve)事件,监听选项为{ passive: true }; - 追加到 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 去重 / 自动卸载"三大行为,也为你排查"脚本没加载"、"脚本没移除"等问题提供了参照。
最佳实践与注意事项
- 重复加载提醒:
useScriptTag只保证"同一时刻"不重复创建同一 URL 的脚本。如果某次调用加载后又卸载了该脚本,下次调用会重新发起加载——这正是文档强调的注意事项,适合用manual模式配合全局状态统一管理; - 等待语义:默认
load()会等待脚本真正加载完成(load事件),适合"先加载 SDK 再初始化"的时序依赖场景;若只需"尽快把脚本放进 DOM",可传load(false)立即返回; - SSR 安全:服务端没有
document,此时loadScript直接 resolvefalse而不会抛错,配合immediate也不会在服务端注入脚本,可安全用于 Nuxt 等 SSR 框架; - 自定义 document:在 iframe 或特殊测试环境中,可通过
document选项注入自定义 Document,避免影响主文档; - CSP 环境:受内容安全策略约束的站点,应使用
nonce选项传入与 CSP 策略匹配的 nonce 值; - 响应式 src:
src参数的类型是MaybeRefOrGetter<string>,即支持传入 ref 或 getter,让脚本地址本身也可以是响应式的。
总而言之,useScriptTag 用极小的 API 面(一个 URL、一个回调、一组配置项)封装了"动态脚本注入 + 生命周期管理 + 去重复用"这一完整能力,其文档、源码与测试三者在仓库中相互印证,是理解 VueUse"小而美"设计哲学的典型示例。