Astro 语法要点:组件、指令与客户端脚本

要点

开山篇讲了定位,实战篇走了完整项目,本篇负责把语法吃透,只回答三个问题:.astro 组件怎么把数据传下去(props、插槽、递归)、模板指令有哪些且各自的坑在哪、以及最重要的一组——客户端指令(client:*)与 <script> 到底怎么决定”浏览器跑多少 JS”。版本基线为 Astro 5.2.x 稳定线;条目式速查见同系列《Astro 基础知识参考字典》,本篇讲”为什么与怎么选”,字典管”忘了来查”。

一、组件:props、插槽与递归

.astro 组件是纯构建期模板。入参走 Astro.props,推荐直接解构并给默认值——类型签名写在 interface Props 里,TypeScript strict 下拼错字段名构建期就报:

---
// src/components/Card.astro
interface Props {
  title: string;
  tag?: string;
}
const { title, tag = '默认' } = Astro.props;
---
<div class="card">
  <h3>{title}<small>{tag}</small></h3>
  <slot />   <!-- 使用者的子内容注入点 -->
</div>

插槽(slot)有默认与命名两种,命名槽靠 Fragment 投递;在构建期如果需要把子内容当字符串处理(比如塞进另一个组件的 props),用 Astro.slots.render():

---
// 命名槽:标题栏与正文分开投
---
<Card title="今日待办">
  <Fragment slot="header"><strong>3 件未完成</strong></Fragment>
  <p>普通子内容进默认槽</p>
</Card>

递归渲染树形数据用 Astro.self——组件在模板里调用自己,这是构建期模板版”递归组件”:

---
// src/components/Tree.astro:渲染任意深度的导航树
const { items } = Astro.props;
---
<ul>
  {items.map((item) => (
    <li>
      <a href={item.href}>{item.label}</a>
      {item.children && <Astro.self items={item.children} />}
    </li>
  ))}
</ul>

⚠️ 常见坑:class 不会自动进 Astro.props。给组件传 class="mt-4",组件里不声明、不解构,这个类就静默丢失——这是从 HTML/JSX 直觉迁移过来最常见的”为什么样式没生效”。要接它,就在 Props 里声明 class?: string 并显式转发给根元素。

二、模板指令:一张表记住

指令(directive)是 xxx: 前缀的模板属性,编译器看到冒号就知道这不是普通 HTML 属性。全部核心指令如下:

指令 作用 有什么坑
class:list 数组/字符串/对象混合拼 class,假值自动剔除 只负责拼接,去重与排序不做;和普通 class 属性同用时两者都会输出
set:html 注入原始 HTML,不经转义 不消毒,内容含用户输入就是 XSS;注入 JSON-LD 这类自己生成的结构化数据是它的正当用途
set:text 注入纯文本(textContent) 想输出用户内容用它,天然安全;需要的是”带格式”时它做不到
is:inline 让 <script>/<style> 原样输出,不打包不处理 行内脚本失去去重与打包,多实例会重复执行
is:global 样式不限定当前组件作用域 全局样式互相覆盖,能用作用域样式就别开
is:raw 子内容当纯文本,编译器不解析标签与表达式 展示代码示例时好用,别拿它包普通正文
define:vars 向脚本或样式传构建期变量 用在 script 上会隐含 is:inline(详见第四节);用在 style 上则相反,要求不 inline
client:* 把框架组件标记为交互岛并指定注水时机 只对框架组件(React/Vue 等)有意义,挂在 .astro 组件上是错误
transition:name 软导航时给配对元素做过渡动画(配合 ClientRouter) name 需页面内唯一,重复会导致动画错乱

两个最高频的展开示例。拼条件 class 用 class:list 而不是手动字符串拼接:

<li class:list={['nav-item', active && 'nav-item--active', isExternal && 'ext']} />
<!-- active 为 false 时输出 class="nav-item",假值整段消失 -->

注入 JSON-LD(结构化数据)是 set:html 最常见的正当场景——直接 {JSON.stringify(...)} 会被 HTML 转义搞坏引号:

<script type="application/ld+json" set:html={JSON.stringify(articleSchema)} />

三、客户端指令与注水机制

先把机制说破:构建时,带 client:* 指令的框架组件会被编译成一个 <astro-island> 自定义元素,里面是构建期渲染好的静态 HTML,外加该组件的 JS 资源声明;浏览器端由这条指令决定什么时候去取 JS 并注水(hydration)。注水成本与岛的体积成正比,与页面大小无关——所以页面再长,只要岛少,JS 依旧接近零。

指令 注水时机 浏览器底层机制 典型场景
client:load 页面加载立即 脚本就绪即注水 首屏关键交互:顶栏搜索、购物车入口
client:idle 浏览器空闲时 requestIdleCallback 不急但要尽快就绪:评论区、视频播放器外壳
client:visible 滚入视口 IntersectionObserver 页面下方的组件:图表、页脚表单
client:media 媒体查询匹配 matchMedia 只在移动端/桌面端出现的交互件
client:only 纯客户端渲染 跳过构建期 HTML 渲染 强依赖浏览器 API、服务端渲染会炸的组件

两个带值的写法。client:media 接媒体查询字符串;client:only 接框架名——当页面同时集成了多个框架(比如 React 加 Vue)时,必须用 client:only="react" 指名,否则编译器不知道用哪个渲染器:

<MobileMenu client:media="(max-width: 48em)" />
<Chart client:only="react" data={chartData} />

另一个隐性约束:传给岛的 props 要能被序列化。props 会在构建期序列化、构建产物里随身携带、注水时还原给框架。常规 JSON 类型(字符串、数字、布尔、null、数组、普通对象)最稳;函数一定传不过去(注水后拿到的不是你写的那个函数),带方法的类实例会丢方法。拿不准就传纯数据,把行为留在岛内部。

选择逻辑一句话:默认不写指令,实在要交互再从最省的指令开始往上加。顺序是”无指令 → client:idle → client:visible → client:load”,client:load 只留给”首屏就能被点到”的组件;client:only 是最后手段,它放弃服务端渲染换兼容性,首屏白屏时间换来了必然。

四、script:三条管线与数据传递

.astro 里的 <script> 默认走”处理管线”:打包、转成 type="module"、去重、提到页面 head;可以直接写 TypeScript、可以 import npm 包。三条规则值得单独记:

<script>
  // 默认管线:这里可以写 TS,import 也可以;整个页面只执行一次
  document.querySelectorAll('[data-copy]').forEach((btn) => {
    btn.addEventListener('click', () => navigator.clipboard.writeText(btn.dataset.copy));
  });
</script>

规则一:默认脚本按页面去重,不是按组件实例。同一个组件在页面里出现三次,它的 <script> 只执行一次——脚本要写成 querySelectorAll 加循环、按 data- 属性分派,别按”每个实例跑一遍”的直觉写,否则后两个实例的事件永远挂不上。

规则二:is:inline 退回原始行为。不打包、不去重、原样输出,适合放第三方给的统计代码片段。去掉打包能力的代价是这类脚本里不能写 TS、不能 import。

规则三:构建期变量传进脚本用 define:vars,它会隐含 is:inline。变量被序列化成 JSON 挂到脚本作用域:

---
const siteTitle = 'x-rush 的名片';
---
<script define:vars={{ siteTitle }}>
  // 注意:因为隐含 is:inline,这里只能写普通 JS
  console.log(`站点标题:${siteTitle}`);
</script>

另一个软导航时代的坑:如果启用了视图过渡(astro:transitions 的 <ClientRouter />,开山篇提过的软导航组件),默认脚本不会在每次导航后重新执行;确实要重跑的脚本加 data-astro-rerun 属性。旧代码迁移到软导航后”交互失灵”,十有八九是这条。

最后回答”什么时候不用岛”:主题切换、复制按钮、折叠菜单这类无状态小交互,默认管线的一个 <script> 就是最优解——零框架运行时、零注水,产物里只有几行你自己的 JS。岛是为”组件自身有状态、有渲染逻辑”准备的,为挂个监听器就拉进整个 React,是把 client:load 的账算错了对象。

反模式点名

  • 给所有框架组件默认 client:load。图的省事换来整页注水,岛架构的收益清零;正确顺序是先问”这块真的需要 JS 吗”,再问”什么时候需要”。
  • 用 set:html 注入未消毒的用户输入。评论、表单回显走 set:html 等于亲手制造存储型 XSS;用户内容用 set:text 或默认插值,非用户内容(自己生成的 JSON-LD)才用 set:html。
  • 把函数当 props 传给岛。回调在序列化时丢掉,岛内拿到的不是原函数,行为静默错乱;正确做法是传数据、让岛内部自己定义行为,或用浏览器事件(CustomEvent)跨边界通信。

练习与实践

练习一:写一个列表组件,用 Astro.self 渲染三层嵌套目录,然后故意在模板里写 onClick={...},观察构建器反应,再改回岛或 <script> 方案。练习二:同一个页面放三个 CopyButton 组件实例(各带 data-copy),先按”每实例跑一遍”的直觉写脚本,验证只有第一个实例工作,再改成 querySelectorAll 版本。验收标准:拿到任何一个交互需求,能不假思索地答出”静态渲染加脚本、还是岛、用哪条 client: 指令、为什么”。

延伸阅读

本篇是语法主线;条目忘了来找《Astro 基础知识参考字典》(指令组、脚本组、环境变量组都有独立条目与坑位标注);动手顺序没跟上的回《第一个 Astro 应用》,完整项目看《用 Astro 做一个极速文档站》。岛的注水成本为什么值得斤斤计较,站内《React 渲染与重渲染》把”白算”这笔账算得很细,两边对读会更有体感。

版本核实(2025-02-15 撰写时检索):截至本文,Astro 稳定线为 5.2.x(5.0 发布于 2024-12;GitHub releases 口径)。本文涉及的 client:load/idle/visible/media/only 五条客户端指令及底层机制(requestIdleCallback、IntersectionObserver、matchMedia)、astro-island 自定义元素、模板指令(class:list、set:html、set:text、is:inline、is:global、is:raw、define:vars)、脚本默认处理管线与 data-astro-rerun、Astro.slots/Astro.self,均为官方模板指令参考与组件文档的现行内容;props 需可序列化、函数不支持为官方明确口径。以 docs.astro.build 为准。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇