KAIROS.WORKSPACE
技术宇宙 / ONLINE
返回文章列表
Astro 实战intermediate8 分钟

一份配置,两个世界:服务端模块如何喂给内联脚本

用 JSON script 载荷把服务端模块里的配置、规则表传给浏览器内联脚本,避免在 Astro 项目中把同一份数据手抄两遍。

#Astro#前端工程#数据流#设计模式
排版
字号
行宽
目录 · 5 节

一个反复出现的漂移现场

Astro 页面有两种脚本:打包模块脚本(可 import)与 is:inline 脚本(原样输出、不打包)。后者常见于必须在首屏前执行、或刻意避免 hydration 时序问题的场景——主题应用、防闪烁脚本、页面级增强。

麻烦从这里开始:很多运行参数其实定义在 TypeScript 模块里(动效配置、成就规则表、工具示例数据),而 is:inline 脚本碰不到模块系统。于是同一份数据出现两种实现:

// src/lib/achievements.ts —— 事实 A
{ id: 'streak-3', label: '三日之约', condition: { kind: 'streakDays', atLeast: 3 } }
// 某页面 is:inline 脚本 —— 事实 B(手抄)
{ id: 'streak-3', label: '三日之约', test: (s) => s.streakDays >= 3 }

两处手抄迟早漂移:改了 lib 忘了脚本,或反过来。更隐蔽的是语义漂移——字段从 threshold: 3 改成 atLeast: 3,另一处没跟上,构建照样通过。

模式:结构化数据进 payload,函数留在消费端

服务端 setup 里 import 配置模块,把可序列化部分写进 JSON script 标签;浏览器端从 DOM 读取并解释。以成就规则为例,规则表在 lib 里定义成纯数据:

// src/lib/achievements.ts
export type AchievementCondition =
  | { kind: 'completedPosts'; atLeast: number }
  | { kind: 'totalSeconds'; atLeast: number }
  | { kind: 'streakDays'; atLeast: number }
  | { kind: 'flag'; flag: string };

export const achievementBadges = [
  {
    id: 'first-read',
    label: '初次点亮',
    description: '完成 1 篇文章阅读',
    condition: { kind: 'completedPosts', atLeast: 1 },
  },
  // …
];

页面模板注入(set:html 对 script 内容是安全的,JSON 中已无用户输入):

<script
  type="application/json"
  id="achievement-badge-data"
  is:inline
  set:html={JSON.stringify(achievementBadges)}
></script>

内联脚本负责解释——函数不能进 JSON,但解释器只写一份:

const badgeRules = JSON.parse(
  document.querySelector('#achievement-badge-data')?.textContent || '[]',
).map((badge) => ({
  ...badge,
  test: (state) => evaluateCondition(badge.condition, state),
}));

关键约束:payload 里是 kind/atLeast 这样的判别式数据,switch 解释器只有内联脚本一处。lib 修改规则表(阈值、文案、新增徽章)无需动任何页面;只有新增条件种类时才需要在解释器里加一个 case——这正是一个显式、可测试的扩展点,而不是隐式的手抄同步。

define:vars 的区别

define:vars 是变量插值:把值展开成源码文本塞进脚本。它在模板阶段生成形如 const cfg = {...} 的顶层声明,在客户端路由(多次执行同一脚本体)下容易触发重复声明错误,调试时看到的产物也充满插值痕迹。JSON script 方案则把数据与代码物理分离:

  • 脚本体是稳定的 IIFE,数据永远来自 textContent,客户端路由下重新初始化只是重新读一次 DOM;
  • 数据可以先在浏览器 DevTools 里直接查看,不需要在编译产物里找插值;
  • 类型检查发生在服务端(setup 的 TS),JSON 结构错误在构建期就暴露。

两者都合理,但“传数据”这件事上,payload 方案在动态化页面里更稳。

工程细节清单

  1. fallback 要有,且要小。 payload 节点丢失或被篡改时,脚本应回退到“最保守可用”的内置子集,而不是整段功能死亡。
  2. 给 JSON script 固定 id 便于 DevTools 检查,也便于测试里断言注入成功。
  3. 解释器函数保持纯函数。 上面 evaluateCondition(condition, state) => boolean 是纯函数,可直接在 Node 层为 lib 的数据驱动版本写单测。
  4. 别把秘密放进 payload。 JSON script 会原样出现在 HTML 源码里,只放本就公开的数据;页面内联脚本读的是 DOM,不是运行时权限。

小结

“服务端知道,浏览器也要知道”的数据,优先设计成可序列化的判别式数据 + 单一解释器。lib 是事实源,HTML 是事实快照,页面脚本只是快照的阅读者。手抄一遍看似省五分钟,漂移一次赔一整个重构。

Conversation

评论与互动

正在加载评论…

提交后需审核,不会立即公开。

Keep exploring

继续探索