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>标签(配合async或defer)在页面中引入第三方脚本。Gatsby 文档明确指出:这样做存在一个隐患——脚本很可能与负责页面水合(hydration)的框架 JavaScript并行加载,从而干扰页面进入可交互状态,对 Total Blocking Time(TBT) 等关键 Web 指标产生负面影响。
Gatsby 内置的<Script>组件(源码位于 packages/gatsby-script/src/gatsby-script.tsx)正是为解决这类问题而生:
- 提供声明式的加载策略(
post-hydrate、idle、off-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直接调用injectScript;idle将注入逻辑包装在requestIdleCallback中;off-main-thread则将脚本属性收集进按页面维护的映射表中,由 Gatsby 在服务端/构建期统一处理。
Post hydrate 策略(默认)
post-hydrate是默认加载策略,当你不指定strategy属性时即采用此策略。
该策略的优势在于:你可以声明脚本在水合(hydration)之后才开始加载。水合是页面变得可交互的关键阶段;如果使用普通<script>标签(即使加了async或defer),脚本仍可能与负责水合的框架 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 develop、gatsby 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 上游做出变更才能解除:
onLoad与onError回调不受支持;- 脚本仅在 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:
wrapPageElementwrapRootElement
注意:如果你使用了这些 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"onLoad与onError回调
使用post-hydrate或idle策略加载的带 src 的脚本支持两个回调:
onLoad— 脚本加载完成后调用;onError— 脚本加载失败时调用。
注意:内联脚本以及使用
off-main-thread策略的脚本不支持onLoad与onError回调。
使用示例:
<Script src="https://my-example-script" onLoad={() => console.log("success")} onError={() => console.log("sadness")} />重复脚本(即id或src属性相同的脚本)即使没有被注入 DOM,也会执行其onLoad与onError回调。这一行为在源码中通过scriptCallbackCache(一个Map<string, { load?, error? }>)实现(gatsby-script.tsx):回调注册时会查询缓存,若对应事件已发生过(缓存了event),则立即用缓存的事件重放回调;injectScript则为真实注入的脚本挂载事件监听,并在事件触发时通过onEventCallback把事件写入缓存,供后续重复脚本的回调使用。
依赖加载(Loading scripts dependently)
onLoad与onError回调还支持实现脚本的依赖加载。以下示例展示了如何先加载第一个脚本、再加载第二个:
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-hydrate、idle、off-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>组件支持以下关键属性:
| 属性 | 类型 | 说明 |
|---|---|---|
src | string | 远程脚本地址;同时作为去重键(与id二选一) |
id | string | 唯一标识;内联脚本必须提供;也用于区分同src的重复脚本 |
strategy | ScriptStrategy或对应字符串字面量 | 加载策略,默认post-hydrate |
children | string | 内联脚本的模板字符串写法 |
dangerouslySetInnerHTML | { __html: string } | 内联脚本的 React 写法 |
onLoad/onError | (event) => void | 加载成功/失败回调(仅带src且策略为post-hydrate/idle时支持) |
forward | Array<string> | Partytown 事件转发配置(仅off-main-thread) |
| 其他属性 | 透传 | src、strategy、dangerouslySetInnerHTML、children、onLoad、onError之外的所有属性(如crossOrigin、data-*等)都会透传到最终渲染出的<script>元素上(见handledProps与resolveAttributes) |
总结与选型建议
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),仅供参考