Gatsby Script API 深度指南:用内置 `<Script>` 组件高效管理第三方脚本
2026/9/19 16:24:13 网站建设 项目流程

Gatsby Script API 深度指南:用内置<Script>组件高效管理第三方脚本

【免费下载链接】gatsbyReact-based framework with performance, scalability, and security built in.项目地址: https://gitcode.com/gh_mirrors/ga/gatsby

本文基于 Gatsby 官方文档 Gatsby Script API(自gatsby@4.15.0起内置)并结合仓库源码编写。Gatsby 内置的<Script>组件用于以高性能方式加载第三方脚本,它为开发者提供了声明式加载策略(Loading Strategies)的能力,并内置了开箱即用的默认策略,同时支持带src的远程脚本与内联脚本。阅读本文后,你将掌握<Script>组件的全部用法、三种加载策略的适用场景、基于 Partytown 的实验性off-main-thread策略的完整配置流程,以及如何利用onLoad/onError回调实现脚本的依赖加载——从而在不牺牲核心 Web 指标(如 Total Blocking Time)的前提下,为站点安全地接入分析、标签管理等第三方脚本。

为什么需要 Gatsby Script API

传统做法中,开发者通常直接用原生<script>标签(配合asyncdefer)在页面中引入第三方脚本。Gatsby 文档明确指出:这样做存在一个隐患——脚本很可能与负责页面水合(hydration)的框架 JavaScript并行加载,从而干扰页面进入可交互状态,对 Total Blocking Time(TBT) 等关键 Web 指标产生负面影响。

Gatsby 内置的<Script>组件(源码位于 packages/gatsby-script/src/gatsby-script.tsx)正是为解决这类问题而生:

  • 提供声明式的加载策略(post-hydrateidleoff-main-thread);
  • 默认的post-hydrate策略让站点在“零配置”下也能获得良好的加载性能;
  • 同时支持带src的远程脚本与内联脚本;
  • 内置去重、回调、代理等能力,把管理脚本的“重活”交给 Gatsby。

快速上手:在页面中使用<Script>

在站点的 JSX 或 TSX 源文件中,从gatsby导入Script组件即可:

import React from "react" import { Script } from "gatsby" function MyPage() { return <Script src="https://my-example-script" /> } export default MyPage

如果你已经有使用原生<script>标签的存量代码,迁移成本极低:只需导入Script,并把小写的script标签名改成大写的Script

import React from "react" +import { Script } from "gatsby" function MyPage() { return ( - <script src="https://my-example-script" /> + <Script src="https://my-example-script" /> ) } export default MyPage

默认情况下,<Script>组件会在页面水合(hydration)完成之后再加载脚本。关于加载策略的详细说明,见下文 加载策略 一节。

补充说明:你无需单独安装gatsby-script包,它自gatsby@4.15.0起已作为 Gatsby 主包的一部分对外可用(见 packages/gatsby-script/README.md)。

两种脚本形态:带 src 的脚本与内联脚本

<Script>组件支持两种类型的脚本。

带 src 的脚本(Scripts with sources)

通过src属性指定脚本地址即可:

<Script src="https://my-example-script" />

<Script>组件会src的值作为去重依据:如果在同一页面上引入了两个src相同的脚本,只会有一个被加载。这一行为在源码中有明确实现——gatsby-script.tsx 中通过scriptCache(一个Set<string>)记录已经注入过 DOM 的脚本键,injectScript在注入前会先检查scriptCache.has(scriptKey),命中则直接返回null,避免重复注入。

如果出于某种原因,你确实需要在同一页面上加载两个相同src的脚本,可以为它们分别提供唯一的id属性,组件就会尝试加载两份:

<Script id="first-unique-id" src="https://my-example-script" /> <Script id="second-unique-id" src="https://my-example-script" />

从源码看,scriptKey = id || src(gatsby-script.tsx),因此指定了不同的id后,两个脚本会被视为不同的键而同时加载。

内联脚本(Inline scripts)

内联脚本必须携带唯一的id属性,且可以通过以下两种方式定义:

  • 通过 React 特殊的dangerouslySetInnerHTML属性;
  • 通过模板字符串(children)。

两种写法如下:

<Script id="first-unique-id" dangerouslySetInnerHTML={{ __html: `alert('Hello world')` }} /> <Script id="second-unique-id">{`alert('Hello world')`}</Script>

从功能上讲,这两种定义内联脚本的方式是等价的。源码中的resolveInlineScript也印证了这一点:它优先取dangerouslySetInnerHTML.__html,否则取children

function resolveInlineScript(props: ScriptProps): string { const { dangerouslySetInnerHTML, children = `` } = props || {} const { __html: dangerousHTML = `` } = dangerouslySetInnerHTML || {} return (dangerousHTML as string) || children }

加载策略(Strategies)

通过strategy属性声明加载策略。可用的加载策略共有三种:

策略说明
post-hydrate(默认)页面水合完成后加载
idle页面进入空闲状态后加载
off-main-thread(实验性)通过 Partytown 在 Web Worker 中、主线程之外加载

对应的 JSX 写法:

<Script src="https://my-example-script" strategy="post-hydrate" /> <Script src="https://my-example-script" strategy="idle" /> <Script src="https://my-example-script" strategy="off-main-thread" />

在 TSX 文件中,也可以使用 Gatsby 导出的ScriptStrategy枚举:

import { Script, ScriptStrategy } from "gatsby" <Script src="https://my-example-script" strategy={ScriptStrategy.postHydrate} /> <Script src="https://my-example-script" strategy={ScriptStrategy.idle} /> <Script src="https://my-example-script" strategy={ScriptStrategy.offMainThread} />

在源码中,ScriptStrategy枚举定义于 gatsby-script.tsx:

export enum ScriptStrategy { postHydrate = `post-hydrate`, idle = `idle`, offMainThread = `off-main-thread`, }

组件内部通过switch (strategy)分发不同的注入逻辑(gatsby-script.tsx):post-hydrate直接调用injectScriptidle将注入逻辑包装在requestIdleCallback中;off-main-thread则将脚本属性收集进按页面维护的映射表中,由 Gatsby 在服务端/构建期统一处理。

Post hydrate 策略(默认)

post-hydrate默认加载策略,当你不指定strategy属性时即采用此策略。

该策略的优势在于:你可以声明脚本在水合(hydration)之后才开始加载。水合是页面变得可交互的关键阶段;如果使用普通<script>标签(即使加了asyncdefer),脚本仍可能与负责水合的框架 JavaScript 并行加载,从而影响 Total Blocking Time 等关键 Web 指标。而使用post-hydrate策略可以确保脚本不干扰页面达到可交互状态,为用户带来更好的体验。

post-hydrate适合需要让脚本尽早加载、但又不想影响站点“可交互时间”的场景。

Idle 策略

idle策略与post-hydrate类似,同样在水合之后加载,区别在于:idle会告诉浏览器在主线程空闲时才加载脚本。

也就是说,如果页面正在执行其他关键任务(例如 DOM 操作或其他占用主线程的计算),脚本会等这些工作完成后再开始加载。

idle策略适合希望脚本以“不与其他主线程工作竞争”的方式加载的场景。

从实现上看,Gatsby 在浏览器环境优先使用原生的requestIdleCallback,并在不支持的环境下回退到基于setTimeout的 shim(见 packages/gatsby-script/src/request-idle-callback-shim.ts):

export const requestIdleCallback = (typeof self !== `undefined` && self.requestIdleCallback && self.requestIdleCallback.bind(window)) || function (cb: IdleRequestCallback): number { const start = Date.now() return setTimeout(function () { cb({ didTimeout: false, timeRemaining: function () { return Math.max(0, 50 - (Date.now() - start)) }, }) }, 1) as unknown as number }

Off main thread 策略(实验性)

与前两种策略不同,off-main-thread通过 Partytown 在 Web Worker 中加载脚本。这意味着脚本求值的负担不再由主线程承担,主线程得以腾出来处理其他关键任务。

注意:由于 Partytown 目前仍处于beta阶段,off-main-thread策略被视为实验性功能。它受某些限制约束,并且根据你的使用场景,可能比其他加载策略需要更多的配置。

以下示例使用off-main-thread策略加载 Google Analytics 4(process.env.GTAG是你定义在.env.production.env.development文件中的 GA4 标识符):

import { Script } from "gatsby" <Script src={`https://www.googletagmanager.com/gtag/js?id=${process.env.GTAG}`} strategy="off-main-thread" /> <Script id="gtag-config" strategy="off-main-thread" forward={[`gtag`]}> {` window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments)}; gtag('js', new Date()); gtag('config', ${process.env.GTAG}, { page_path: location ? location.pathname + location.search + location.hash : undefined }) `} </Script>
事件转发(Forward collection)

Gatsby 会收集页面上的所有off-main-thread脚本,并自动将各脚本通过forward属性声明的 Partytown 转发事件 合并为每个页面的一份统一配置:

<Script src={`https://www.googletagmanager.com/gtag/js?id=${process.env.GTAG}`} strategy="off-main-thread" forward={[`dataLayer.push`]} />

forward<Script>组件中唯一由组件本身处理的 Partytown 专属属性。在源码中,ScriptProps接口为forward?: Array<string>(gatsby-script.tsx),且forward不在handledProps集合中,因此会被resolveAttributes透传到最终的<script type="text/partytown">元素上。

代理配置(Proxy configuration)

所有提供给off-main-thread策略的 URL,都会被 Gatsby 代理到/__third-party-proxy?url=${YOUR_URL}。原因在于许多第三方脚本需要代理才能在 Partytown 中正常工作,Gatsby 因此内置了代理功能以简化这一过程。

为保证代理安全,你必须在 Gatsby 配置中通过partytownProxiedURLs键声明允许代理的绝对 URL。如果不这样做,请求将返回 404。

针对上面的 Google Analytics 示例,配置如下:

import dotenv from "dotenv" dotenv.config({ path: `.env.${process.env.NODE_ENV}`, }) module.exports = { siteMetadata: { title: `Gatsby`, }, partytownProxiedURLs: [ `https://www.googletagmanager.com/gtag/js?id=${process.env.GTAG}` ], }

这部分能力在gatsby developgatsby serve以及 Gatsby Cloud 上开箱即用。其底层实现位于 packages/gatsby/src/internal-plugins/partytown/:

  • 代理路径常量定义于 proxy.ts(export const thirdPartyProxyPath = '/__third-party-proxy'),代理中间件通过filter校验请求的url查询参数是否命中partytownProxiedURLs白名单,未命中则直接拒绝转发;
  • 在 gatsby-node.ts 中,Gatsby 会针对partytownProxiedURLs中的每个 URL,通过createRedirect动作创建从/__third-party-proxy?url=...到目标 URL、状态码为 200 的重定向;同时在onCreateDevServer中把代理挂载到开发服务器;
  • 配置项的校验与类型声明分别见 joi-schemas/joi.ts(partytownProxiedURLs: Joi.array().items(Joi.string()))与 packages/gatsby/index.d.ts。

其他托管平台需要支持 Gatsby 的createRedirect动作,才能把/__third-party-proxy?url=${YOUR_URL}的请求以 200 状态码重写到YOUR_URL。你需要与托管服务商确认其是否支持该能力。

自定义 URL 解析(Resolving URLs)

你可以利用 Partytown 的 vanilla config 来处理off-main-thread脚本中 Partytown 专属的行为。其中resolveUrl选项允许你修改由 Partytown 处理的 URL。

resolveUrl的典型使用场景是标签管理器脚本,例如 Google Tag Manager。这类脚本与 Partytown 配合较为困难,因为它们内部包含的其他脚本会发起其他请求,这些请求是否需要被代理取决于 CORS 设置。此时可以用resolveUrl处理这些子脚本的 URL。

以下示例使用 Google Tag Manager(GTM)加载 Google Analytics(Universal Analytics):

注意:此示例假设你已在 Google Tag Manager 后台配置好使用 Universal Analytics。

首先加载 GTM 脚本并发送初始化事件(process.env.GTM是你的 GTM 标识符,定义在.env.production.env.development文件中):

import { Script } from "gatsby" <Script src={`https://www.googletagmanager.com/gtm.js?id=${process.env.GTM}`} strategy="off-main-thread" forward={[`dataLayer.push`]} /> <Script id="gtm-init" strategy="off-main-thread"> {` window.dataLayer = window.dataLayer || [] window.dataLayer.push({ 'gtm.start': new Date().getTime(), 'event': 'gtm.js' }) `} </Script>

然后在 Partytown 的 vanilla config 中定义resolveUrl,处理由 GTM 加载的 Google Analytics 脚本:

import React from "react" export const onRenderBody = ({ setHeadComponents }) => { setHeadComponents([ <script key="partytown-vanilla-config" dangerouslySetInnerHTML={{ __html: `partytown = { resolveUrl(url, location) { if (url.hostname.includes('google-analytics')) { // Use a secure connection if (url?.protocol === 'http:') { url = new URL(url.href.replace('http', 'https')) } // Point to our proxied URL const proxyUrl = new URL(location.origin + '/__third-party-proxy') proxyUrl.searchParams.append('url', url) return proxyUrl } return url } }`, }} />, ]) }

最后,把 Google Analytics 的 URL 加入partytownProxiedURLs,让 Gatsby 知道该 URL 是允许代理的安全地址:

import dotenv from "dotenv" dotenv.config({ path: `.env.${process.env.NODE_ENV}`, }) module.exports = { siteMetadata: { title: `Gatsby`, }, partytownProxiedURLs: [ `https://www.googletagmanager.com/gtm.js?id=${process.env.GTM}`, `https://www.google-analytics.com/analytics.js`, ] }

至此,Google Tag Manager 与 Google Analytics 脚本都应能在你的站点中成功加载。

调试(Debugging)

同样借助 Partytown 的 vanilla config,你可以为off-main-thread脚本开启调试模式:

import React from "react" export const onRenderBody = ({ setHeadComponents }) => { setHeadComponents([ <script key="partytown-vanilla-config" dangerouslySetInnerHTML={{ __html: `partytown = { debug: true }`, }} />, ]) }

你可能需要把开发者工具的日志级别调到 verbose,才能在控制台看到额外的日志输出。

限制(Limitations)

由于依赖 Partytown,使用off-main-thread策略的脚本还必须了解 Partytown 文档中列出的权衡与限制。该策略虽然强大,但未必是所有场景的最佳方案。

以下限制需要 Partytown 上游做出变更才能解除:

  • onLoadonError回调不受支持
  • 脚本仅在 SSR(服务端渲染)导航时加载(例如普通<a>标签跳转),而不会在 CSR(客户端渲染)导航时加载(例如 Gatsby<Link>跳转)。

此外,off-main-thread策略不能在wrapRootElementAPI 中使用——因为脚本收集依赖 location provider。请改用wrapPageElementAPI。

这一限制与源码实现密切相关:GatsbyScript组件内部通过useLocation()获取当前pathname,并把off-main-thread脚本的属性写入 collected-scripts-by-page.tsx 中按pathname组织的 Map(collectedScriptsByPage.set(pathname, attributes),见 gatsby-script.tsx),供 Gatsby 在渲染页面 HTML 时取出并输出。

在 Gatsby SSR 与 Browser API 中使用

<Script>组件还可以用于以下 Gatsby SSR 与 Gatsby Browser API:

  • wrapPageElement
  • wrapRootElement

注意:如果你使用了这些 API,建议同时在 Gatsby SSR 与 Gatsby Browser 中实现。常见的模式是定义一个单一函数,在两个文件中分别导入使用。

以下示例在 Gatsby SSR 和 Browser 中通过wrapPageElement使用<Script>,且不重复代码:

import React from "react" import { Script } from "gatsby" export const wrapPageElement = ({ element }) => { return ( <> {element} <Script src="https://my-example-script" /> </> ) }
export { wrapPageElement } from "./gatsby-shared"
export { wrapPageElement } from "./gatsby-shared"

onLoadonError回调

使用post-hydrateidle策略加载的带 src 的脚本支持两个回调:

  • onLoad— 脚本加载完成后调用;
  • onError— 脚本加载失败时调用。

注意:内联脚本以及使用off-main-thread策略的脚本不支持onLoadonError回调。

使用示例:

<Script src="https://my-example-script" onLoad={() => console.log("success")} onError={() => console.log("sadness")} />

重复脚本(即idsrc属性相同的脚本)即使没有被注入 DOM,也会执行其onLoadonError回调。这一行为在源码中通过scriptCallbackCache(一个Map<string, { load?, error? }>)实现(gatsby-script.tsx):回调注册时会查询缓存,若对应事件已发生过(缓存了event),则立即用缓存的事件重放回调;injectScript则为真实注入的脚本挂载事件监听,并在事件触发时通过onEventCallback把事件写入缓存,供后续重复脚本的回调使用。

依赖加载(Loading scripts dependently)

onLoadonError回调还支持实现脚本的依赖加载。以下示例展示了如何先加载第一个脚本、再加载第二个:

import React, { useState } from "react" import { Script } from "gatsby" function MyPage() { const [loaded, setLoaded] = useState(false) return ( <> <Script src="https://my-example-script" onLoad={() => setLoaded(true)} /> {loaded && <Script src="https://my-other-example-script" />} </> ) } export default Page

实战参考:官方示例与组件属性速查

官方示例站点

仓库中的 examples/using-gatsby-script/ 提供了一个完整的实战参考,值得对照研读:

  • 在 src/pages/index.tsx 中,同时使用了字符串字面量与ScriptStrategy枚举两种方式声明策略,并覆盖了post-hydrateidleoff-main-thread三种策略以及内联脚本的两种写法;
  • 在 gatsby-config.ts 中,把marked模块的 CDN 地址加入partytownProxiedURLs白名单,注释明确说明:这是加载off-main-thread脚本所必需的,否则请求会 404
import type { GatsbyConfig } from "gatsby" const config: GatsbyConfig = { siteMetadata: { title: `using-gatsby-script`, siteUrl: `https://www.yourdomain.tld`, }, plugins: [], /** * Add the CDN URL for the `marked` module to the Partytown proxy allowlist * so we can load it with the `off-main-thread` strategy. * * This is required, otherwise the request will 404. */ partytownProxiedURLs: [`https://cdn.jsdelivr.net/npm/marked/marked.min.js`], } export default config

组件属性速查

根据源码中 ScriptProps 接口的定义,<Script>组件支持以下关键属性:

属性类型说明
srcstring远程脚本地址;同时作为去重键(与id二选一)
idstring唯一标识;内联脚本必须提供;也用于区分同src的重复脚本
strategyScriptStrategy或对应字符串字面量加载策略,默认post-hydrate
childrenstring内联脚本的模板字符串写法
dangerouslySetInnerHTML{ __html: string }内联脚本的 React 写法
onLoad/onError(event) => void加载成功/失败回调(仅带src且策略为post-hydrate/idle时支持)
forwardArray<string>Partytown 事件转发配置(仅off-main-thread
其他属性透传srcstrategydangerouslySetInnerHTMLchildrenonLoadonError之外的所有属性(如crossOrigindata-*等)都会透传到最终渲染出的<script>元素上(见handledPropsresolveAttributes

总结与选型建议

Gatsby<Script>组件为第三方脚本管理提供了从“零配置高性能”到“最大灵活度”的完整梯度:

  • 默认场景:直接用<Script src="..." />post-hydrate策略保证脚本不干扰水合与可交互时间;
  • 低优先级脚本:对分析、埋点等可延后的脚本,使用idle策略在空闲时加载;
  • 重型第三方脚本:对标签管理器、分析类脚本,可尝试实验性的off-main-thread策略,把求值负担移出主线程——但务必按本文步骤配置partytownProxiedURLs白名单与必要的代理/URL 解析,并留意其在 SSR 导航与回调支持上的限制;
  • 脚本依赖管理:利用onLoad/onError回调实现脚本间的依赖加载。

对于其他托管平台,若需要使用off-main-thread策略,请务必确认平台支持 Gatsby 的createRedirect动作(参考 packages/gatsby/src/internal-plugins/partytown/gatsby-node.ts 中的重定向实现),否则代理请求将无法正确转发。结合本文给出的源码证据与官方示例,你可以在生产环境中安全、高效地接入各类第三方脚本。

【免费下载链接】gatsbyReact-based framework with performance, scalability, and security built in.项目地址: https://gitcode.com/gh_mirrors/ga/gatsby

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询