615 字
3 分钟
GTM 注入的 script 为什么读不到 document.currentScript

最近排查一个问题时踩到的坑,背后其实是一个挺基础但容易忽略的浏览器机制。

一个常见模式:用 data-* 属性传配置#

很多第三方 SDK 喜欢这样设计接入方式:

<script defer src="https://cdn.example.com/sdk.js"
data-token="abc123"></script>

SDK 内部通过 document.currentScript 拿到自己这个 <script> 元素,再读上面的 data-* 属性作为配置:

var script = document.currentScript;
var token = script.getAttribute('data-token');

Cloudflare Web Analytics 的 beacon.min.js 就是这种模式,用 data-cf-beacon 传 token。

document.currentScript 的限制#

关键限制是:document.currentScript 只在脚本作为 HTML 的一部分被浏览器原生解析执行时才有值。如果 script 是被 JS 动态创建并 append 到 DOM 的,它就是 null

// 原生 HTML 解析的 script —— currentScript 有值
<script src="a.js"></script>
// JS 动态创建的 script —— currentScript 为 null
const s = document.createElement('script');
s.src = 'a.js';
document.head.appendChild(s);

这就埋下了和动态注入工具的兼容性问题。

GTM 的注入方式正好踩中#

GTM 的 Custom HTML 标签默认采用沙箱化注入:解析你写的 HTML 字符串,把 <script> 标签提取出来,用 createElement + appendChild 重新创建。

属于典型的”动态创建 script”,所以:

  • document.currentScriptnull
  • 依赖它读配置的 SDK 拿不到 token
  • SDK 函数直接 return,不报错也不工作

表现就是脚本加载成功了、看起来一切正常,但功能没生效。

“Support document.write” 为什么能绕过#

GTM 标签设置里有个 “Support document.write” 选项。勾上之后,GTM 改用 document.write() 写入 HTML 字符串,浏览器会用原生 HTML 解析器处理,包括里面的 <script> 标签。这种方式下 script 是被解析器创建的,document.currentScript 正常有值。

document.write 自身有几个老问题,不太适合作为长期方案:

  • DOMContentLoaded 之后调用会清空整个页面
  • Chrome 在慢网络下会拦截通过它注入的跨域 script
  • 阻塞 HTML 解析,影响首屏性能
  • 长期被推动淡化使用

更好的解法:让配置不依赖 document.currentScript#

如果是自己写 SDK,把配置改成从全局变量读,就彻底绕开这个问题:

<script>window.__sdkConfig = { token: "abc123" };</script>
<script defer src="https://cdn.example.com/sdk.js"></script>

如果是用别人的 SDK,看看它是否支持全局变量回退(Cloudflare beacon 支持 window.__cfBeacon),优先用这种方式接入。

一句话#

document.currentScript 不是动态 script 的可靠 API,凡是依赖它读配置的 SDK,遇上 GTM、A/B 测试工具、CMP 这类动态注入场景都会出问题——配置走全局变量更稳。

GTM 注入的 script 为什么读不到 document.currentScript
https://blog.cuixu.cn/posts/frontend/gtm-document-currentscript/
作者
崔旭
发布于
2022-07-15
许可协议
CC BY-NC-SA 4.0