现象
站点启用 View Transitions 客户端路由后,实验室工具页首次访问一切正常;但从首页点进任一工具,或工具 A 切到工具 B 之后,页面渲染完整、样式正常,唯独交互全部失灵——按钮无响应、输入无反馈,控制台里躺着一条刺眼的 SyntaxError: Identifier 'config' has already been declared。
更迷惑的是复现条件:手动刷新(F5)立刻恢复;站内导航过去才复发。
排查
问题脚本都长这样:
<script is:inline define:vars={{ toolConfig }}>
const config = toolConfig;
// 后续几百行都依赖 config
</script>
define:vars 的展开方式是把变量以顶层 const 声明插进脚本头部。View Transitions 的客户端换页不重建 document,脚本体会在新页面里重新执行——第二次执行时,上一个页面留下的同名顶层绑定仍在同一全局作用域,重复 const 直接抛 SyntaxError,整个脚本块从第一行起死亡,后面的逻辑一行都没跑。
这解释了全部症状:首次直访 = 首次声明,正常;站内导航 = 重复声明,抛错后整段脚本被跳过。
根因与修复
根因是把脚本当模块用,却忽略了内联脚本共享同一个全局作用域。修复分两步:
- 数据与代码分离:配置改为 JSON script 标签注入,脚本体内不再有任何顶层
const依赖外部插值:
<script
type="application/json"
id="lab-config"
set:html={JSON.stringify(toolConfig)}
></script>
<script is:inline>
(() => {
const config = JSON.parse(
document.querySelector('#lab-config')?.textContent || '{}',
);
// …
})();
</script>
- IIFE 包裹一切:顶层作用域不再产生绑定,重复执行天然安全。
教训
- 客户端路由会把“页面脚本只会执行一次”的假设全部作废。 引入
<ClientRouter>时,应把所有内联脚本当作“可能执行 N 次”来审计。 define:vars适合读一次就用的场景(内联样式变量、单一字符串),一旦脚本体在 SPA 导航中重复执行,它生成的顶层声明就是地雷。- 验证手段:浏览器里点击站内导航切换同类页面,观察控制台是否出现
already been declared;CI 中可用 Playwright 做跨导航冒烟测试——本项目后来把“lab 跨工具导航无控制台错误”固化为常规回归项。