找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2528

积分

0

好友

364

主题
发表于 昨天 09:53 | 查看: 7| 回复: 0

看似冷门的 document.currentScript,其实能优雅地解决脚本配置、组件通信和加载控制等问题,让你的前端代码更简洁、更“原生”。

我偶然发现了这个 API,起初并不确定它的用途。后来我才意识到,它在为 <script> 元素暴露配置属性时,其实非常方便。

有时候,我会遇到一些在浏览器中存在已久的 JavaScript API——其实我早该知道它们。比如 window.screen 属性和 CSS.supports() 方法。让我稍感安慰的是,我发现自己并不是唯一一个不知道的人。我记得我曾发帖提到 window.screen,结果收到了一堆评论,很多人也不知道它的存在。

一个 API 的知名度,更多取决于它在我们解决问题时的适用性,而不是它存在了多久。如果 window.screen 这种 API 没有太多实际应用场景,人们自然容易忘记它。

不过,偶尔也会有一些机会,让这些不太被注意的特性派上用场。而我最近就发现了这样一个例子 —— document.currentScript,而且我打算好好利用它。

它有什么作用?

从定义上看,document.currentScript 的作用很简单:它能返回当前正在执行的 <script> 元素本身的引用。作为一个基础的 DOM API,它的浏览器兼容性非常好,在所有主流浏览器中已经存在十多年了。

<script>
   console.log("tag name:", document.currentScript.tagName);
   console.log(
     "script element?",
     document.currentScript instanceof HTMLScriptElement
   );

   // tag name: SCRIPT
   // script element? true
</script>

既然拿到了这个元素本身,你就能像操作任何 DOM 节点一样访问它的属性。

<script data-external-key="123urmom" defer>
   console.log("external key:", document.currentScript.dataset.externalKey);

   if (document.currentScript.defer) {
     console.log("script is deferred!");
   }
</script>

// external key: 123urmom
// script is deferred!

模块脚本的限制

document.currentScript 有一个重要的限制:它在模块脚本中不可用。不过奇怪的是,如果你在模块中访问它,不会得到 undefined,而是 null

<script type="module">
   console.log(document.currentScript);
   console.log(document.doesNotExist);

   // null
   // undefined
</script>

这其实是标准中明确规定的行为。文档创建时,currentScript 会被初始化为 null。此外,当脚本执行完毕后,它也会恢复为 null。所以如果你在异步代码中访问它,同样会得到 null

<script>
   console.log(document.currentScript);
   // <script> 标签

   setTimeout(() => {
     console.log(document.currentScript);
     // null
   }, 1000);
</script>

也就是说,在 <script type="module"> 内部,是没有办法访问当前脚本标签的。如果你只是想知道代码是否在模块中同步运行,可以简单地通过判断是否为 null 来实现。

function isInModule() {
   return document.currentScript === null;
}

顺带一提,不要尝试用 import.meta 来检测,即使放在 try/catch 里也不行。因为只要它出现在普通 <script> 标签中,浏览器在解析时就会直接抛出 SyntaxError,甚至不需要执行代码。

<script>
   // 还没执行就会抛出 SyntaxError!
   function isInModule() {
     try {
       return !!import.meta;
     } catch (e) {
       return false;
     }
   };

   // 这也会报错:
   console.log(typeof import?.meta);
</script>

由于模块脚本目前还不支持类似 document.currentScript 的机制,未来如何解决这个问题还在讨论中。在那之前,最简单的替代方案就是——直接通过 DOM 查询脚本元素:

<script type="module" id="moduleScript">
   const scriptTag = document.getElementById("moduleScript");

   // 在这里操作 scriptTag。
</script>

实际需求:传递配置属性

我在一个网站上使用了 Stripe 的定价表,它是一个可以直接嵌入的原生 Web 组件。使用方式很简单:加载一个脚本,在 HTML 中放入组件标签,并设置几个属性:

<script
   async
   src="https://js.stripe.com/v3/pricing-table.js">
</script>

<stripe-pricing-table
   pricing-table-id="prctbl_blahblahblah"
   publishable-key="pk_test_blahblahblah"
>
</stripe-pricing-table>

如果 HTML 渲染时就能取到一些环境变量,这种方式完全没问题。但我想把这个表格嵌入到 Markdown 文件里。Markdown 虽然支持原生 HTML,但在里面拿到这些属性值可不像使用 import.meta.envprocess.env 那么容易。因此,我需要一种方法,在页面渲染后再动态注入这些值。

不幸的是,Stripe 的定价表组件在初始化时就需要这些属性值,无法将渲染和配置分开。所以我不得不通过客户端脚本,连同属性值一起,把整个元素动态插入页面。做法是先在 Markdown 里放一个占位符元素,然后在脚本中填充它的内容。

我的做法如下:

<div data-pricing-table></div>
<script>
   document.querySelectorAll('[data-pricing-table]').forEach(table => {
     table.innerHTML = `
       <stripe-pricing-table
         pricing-table-id="STAY_TUNED"
         publishable-key="STAY_TUNED"
         client-reference-id="picperf"
       ></stripe-pricing-table>
     `;
   })
</script>

到这一步,我唯一缺少的就是属性值本身。我可以在服务器端把这些值写入全局对象(如 window),但那种方式让我感觉不太舒服——我不喜欢随意往全局作用域里塞数据。

一个比想象中更常见的问题

很多内容管理系统出于安全或架构考虑,往往会严格限制编辑器能控制的内容。编辑者可能能修改页面的一些结构或内容,但几乎无法改动 <script> 标签里的内容——这是有道理的,毕竟那会引入很多潜在的安全风险。

更复杂的是,这些脚本往往引用外部团队维护的共享包,但又需要某些配置参数。在这种情况下,就算想在服务器端把变量渲染到脚本里,也做不到。

<!-- 共享库,但仍需要配置! -->
<script src="path/to/shared/signup-form.js"></script>

在类似的场景中,一种常用做法是通过服务器渲染的数据属性传递配置值。服务端定义这些属性,脚本在客户端读取即可。这种模式在单页应用中尤其常见,比如配置写在根节点上:

<div
   id="app"
   data-recaptcha-site-key="{{ siteKey }}"
></div>
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';

const appNode = document.getElementById('app');
const root = ReactDOM.createRoot(appNode);

root.render(
   // 从根节点读取配置值
   <App recaptchaSiteKey={appNode.dataset.recaptchaSiteKey} />
);

应该已经很明显了:使用 data 属性,是从服务器传递特定值到客户端的一种简洁方式。

在上面的单页应用例子里,唯一略显麻烦的地方是——在访问属性前,你得先查询对应的元素。但在我的场景中,既然是 <script> 标签本身在执行代码,那连这一步都可以省掉。因为 document.currentScript 能直接提供当前脚本的引用。

<script
   data-stripe-pricing-table="{{pricingTableId}}"
   data-stripe-publishable-key="{{publishableKey}}"
>
   const scriptData = document.currentScript.dataset;

   document.querySelectorAll('[data-pricing-table]').forEach(table => {
     table.innerHTML = `
       <stripe-pricing-table
         pricing-table-id="${scriptData.stripePricingTable}"
         publishable-key="${scriptData.stripePublishableKey}"
         client-reference-id="picperf"
       ></stripe-pricing-table>
     `;
   })
</script>

这感觉就很完美了。既没有依赖什么神秘的框架特性,也没污染全局作用域,而且完全利用了平台本身的能力。

其他应用场景

在研究 document.currentScript 的过程中,我又想到了一些潜在的用法。

安装提示

假设你维护着一个必须异步加载的 JavaScript 库。你可以利用 document.currentScript 为开发者提供清晰直接的加载反馈:

<script defer src="./script.js"></script>
// script.js
if (!document.currentScript.async) {
   throw new Error("这个脚本必须以异步方式加载!!!");
}

// 你的库的其他代码……

你甚至还能强制要求脚本加载在页面的某个特定位置。比如要求它必须紧跟在 <body> 标签的开始处加载:

const isFirstBodyChild =
   document.body.firstElementChild === document.currentScript;

if (!isFirstBodyChild) {
   throw new Error(
     "这个脚本必须紧贴在 <body> 标签的开头加载。"
   );
}

这样的报错几乎没有歧义,能给出友好、直观的加载指导,算是良好文档的有力补充。

行为局部性

“行为局部性原则”认为:你应该能够仅通过查看某段代码本身,就理解它的行为。与 document.currentScript 结合时,这个原则意味着:你可以仅凭相邻元素的存在,就构建出可移植的小型交互功能。

例如,下面的例子可以让任意表单在提交时自动通过 AJAX 异步提交。只需把脚本标签放在表单后面即可。脚本会自动找到它前面的那个元素:

// form-submitter.js
const form = document.currentScript.previousElementSibling;

form.addEventListener("submit", async (e) => {
   e.preventDefault();

   const formData = new FormData(form);
   const method = form.method || "POST";

   const submitGet = () =>
     fetch(`${form.action}?${params}`, { method: "GET" });

   const submitPost = () =>
     fetch(form.action, { method, body: formData });

   const submit = method === "GET" ? submitGet : submitPost;
   const response = await submit();

   form.reset();

   alert(response.ok ? "提交成功!" : "发生错误!");
});

然后,只需在表单后面引入这个脚本即可:

<form action="/endpoint-one" method="POST">
   <input type="text" name="firstName"/>
   <input type="text" name="lastName"/>
   <input type="submit" value="提交" />
</form>
<script src="form-submitter.js"></script>

<form action="/endpoint-two" method="POST">
   <input type="email" name="emailAddress" />
   <input type="submit" value="提交" />
</form>
<script src="form-submitter.js"></script>

总结

搞清楚这些“老旧但被忽视”的 Web 特性到底有多实用,这种感觉非常棒。document.currentScript 为我们提供了一种在脚本标签内部访问自身属性的原生方式,特别适合用于从服务端向客户端脚本传递配置,避免了全局变量的污染,实现了代码的优雅和解耦。虽然它在模块脚本中不可用,但在传统的脚本加载场景下,依然是一个强大且兼容性极佳的工具。

希望这个技巧能为你带来启发。如果你对这类原生 API 的巧妙用法感兴趣,欢迎到 云栈社区 与其他开发者一起交流探讨。

原文:https://macarthur.me/posts/current-script/




上一篇:磁盘空间不足怎么办?开源工具BleachBit清理指南与隐私保护技巧
下一篇:Claude Agent Skills开发指南:从理解到实践的最佳工作流
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-18 13:11 , Processed in 0.306414 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表