客户端脚本与事件处理
本教程共 56 篇 · 第 10 篇 · 更新于 2026-08-07 · 约 12 分钟阅读
本节目标:学会在
.astro组件里用<script>标签加交互,理解 Astro 对脚本的自动处理,掌握用addEventListener处理点击等事件,以及把脚本区变量安全送到浏览器的方法。
前面几章的组件,输出后都是静静的 HTML,默认不带任何 JavaScript。可网站总要有点“活气”:点按钮放个彩带、点爱心记个数、提交表单弹提示。这些都要在浏览器里跑 JavaScript。Astro 的做法很朴素——直接在组件模板里写标准 HTML 的 <script> 标签就行。
一个最朴素的脚本
想加点交互,在模板里写 <script>,里面写普通 JavaScript:
<button data-confetti-button>庆祝一下!</button>
<script>
// 从 npm 包引入
import confetti from 'canvas-confetti';
// 在页面上找到我们的按钮
const buttons = document.querySelectorAll('[data-confetti-button]');
// 给每个按钮加点击事件,点一下就放彩带
buttons.forEach((button) => {
button.addEventListener('click', () => confetti());
});
</script>
这段不需要任何框架,纯原生 JS 就能让按钮“活”起来。它避免了为这点小事引入 React、Vue 之类框架的开销,也不要求你额外学框架。
Note这种“不靠框架、只靠原生
<script>做交互”的方式,和 Astro 的 群岛架构 思路一致:能用少量原生 JS 解决,就不整页加载框架运行时。
Astro 怎么处理你的脚本
默认情况下,只要 <script> 是“无属性”(或只有 src)的,Astro 就会替你加工它:
- 支持 TypeScript:你写的脚本默认就是 TypeScript,能直接用类型;
- 导入会打包:无论是本地文件还是 npm 包,都会被一起打包;
- 变成 module:处理后的脚本自动带
type="module"; - 自动去重:同一个含脚本的组件在一页用了多次,脚本只打包进页面一次;
- 自动内联:脚本够小的话,Astro 直接把它塞进 HTML,少一次请求。
也就是说,你写的是源码,Astro 负责把它变成浏览器能高效运行的样子。
<script>
// 会被处理!会打包!支持 TypeScript!
// 引入本地脚本和 npm 包都没问题
</script>
不想被处理?用 is:inline
有时你就是想让脚本“原样”进 HTML,不被 Astro 改动(比如一小段第三方统计代码、或依赖页面全局变量的代码)。给它加 is:inline 指令即可:
<script is:inline>
// 会原封不动渲染进 HTML!
// 不被转换:不处理 TypeScript,也不解析 import
// 在组件里用时,每用一次就复制一份
</script>
Tip绝大多数时候你用默认的、不带属性的
<script>就好,让 Astro 帮你打包优化。只有明确需要“原样输出”时才加is:inline。
引入本地或外部脚本文件
脚本也能单独写成 .js / .ts 文件再引入,分两种情况。
脚本在 src/ 里:用 src 指向它,Astro 会按上面的规则处理(打包、TypeScript 都生效):
<!-- 指向 src/scripts/local.js 的相对路径 -->
<script src="../scripts/local.js"></script>
<!-- 本地 TypeScript 文件也行 -->
<script src="./script-with-types.ts"></script>
脚本在 public/ 或 CDN 上:这类在项目 src/ 之外的文件,要加 is:inline,跳过 Astro 的处理:
<!-- 指向 public/my-script.js 的绝对路径 -->
<script is:inline src="/my-script.js"></script>
<!-- 远程服务器上的脚本 -->
<script is:inline src="https://my-analytics.com/script.js"></script>
处理 onClick 等事件
有些框架用特殊语法绑事件,比如 React 的 onClick={...}、Vue 的 @click="..."。Astro 更贴近标准 HTML,不发明新语法,而是用浏览器原生的 addEventListener。
<button class="alert">点我!</button>
<script>
// 找到页面上所有带 alert 类的按钮
const buttons = document.querySelectorAll('button.alert');
// 给每个按钮绑点击
buttons.forEach((button) => {
button.addEventListener('click', () => {
alert('按钮被点了!');
});
});
</script>
为什么用 querySelectorAll 找“所有”按钮?因为即便页面上有多个 <AlertButton /> 组件,Astro 也只把脚本打包进页面一次,不会重复运行。用 querySelectorAll 能保证每个按钮都绑上监听。
用自定义元素做可复用交互
想做更复杂、能多次复用的交互组件,推荐用 Web Components(浏览器原生的“自定义元素”标准)。它让你定义一种新 HTML 标签,自带行为。
下面定义一个 <astro-heart> 标签,点一下爱心就给计数加一:
<!-- 把组件内容包进自定义元素 astro-heart -->
<astro-heart>
<button aria-label="点赞">💜</button> × <span>0</span>
</astro-heart>
<script>
// 定义这种新元素的行为
class AstroHeart extends HTMLElement {
connectedCallback() {
let count = 0;
const heartButton = this.querySelector("button");
const countSpan = this.querySelector("span");
// 每点一次,更新计数
if (heartButton && countSpan) {
heartButton.addEventListener("click", () => {
count++;
countSpan.textContent = count.toString();
});
}
}
}
// 告诉浏览器:<astro-heart> 用 AstroHeart 这个类
customElements.define("astro-heart", AstroHeart);
</script>
用自定义元素有两个好处:
- 用
this.querySelector()只在“当前这个元素实例”内部找子节点,不用满页面document.querySelector(),更聚焦; - 虽然
<script>只跑一次,但浏览器每遇到一个<astro-heart>都会调用它的connectedCallback()。所以即便组件用了很多次,你也能针对“单个实例”写逻辑。
Tip如果组件里混用了框架组件(React/Vue 等),它们渲染可能比
<script>执行晚。这时也推荐用自定义元素,避免脚本找不到还没生成的节点。
把 frontmatter 变量传给脚本
要记住:前面 frontmatter(围栏里)的代码跑在服务器,不会进浏览器。想在脚本里用那些值,得用 data-* 属性当中转站——把值先写到 HTML 元素的 data- 属性上,脚本再用 dataset 读出来。
---
const { message = '欢迎,世界!' } = Astro.props;
---
<!-- 把 message 存进 data-message 属性 -->
<astro-greet data-message={message}>
<button>打招呼</button>
</astro-greet>
<script>
class AstroGreet extends HTMLElement {
connectedCallback() {
// 从 data 属性读回 message
const message = this.dataset.message;
const button = this.querySelector('button');
button?.addEventListener('click', () => {
alert(message);
});
}
}
customElements.define('astro-greet', AstroGreet);
</script>
这样同一个组件传不同 message,每个实例点按钮都会弹出各自的话:
<AstroGreet />
<AstroGreet message="今天适合写组件!" />
<AstroGreet message="很高兴你来了 👋" />
另一种传值方式:define:vars
第 172 行那招「用 data-* 属性过桥」不是唯一办法。Astro 还给了个更省事的指令 define:vars:它能把 frontmatter 里的变量直接「塞」进脚本的作用域。
---
const message = "欢迎,世界!";
const count = 3;
---
<script define:vars={{ message, count }}>
console.log(message); // "欢迎,世界!"
console.log(count); // 3
</script>
define:vars 要求变量能转成 JSON(字符串、数字、数组、普通对象都行)。Astro 会在构建时把它们序列化、注入脚本。它比 data-* 写法短,但有个限制:变量是「只读快照」,脚本里改了它不会影响服务器那边的原值。
两种传值方式怎么选?值很简单、又刚好要挂在元素上,data-* 顺手;要一次性把好几个变量带进脚本,define:vars 更清爽。
脚本里也能用 TypeScript 类型
前面提过,Astro 默认把 <script> 当 TypeScript 处理。这意味着你能在脚本里写类型注解,比如给函数参数标类型、给 DOM 元素标类型,编辑器照样给提示:
<script>
function greet(name: string): void {
console.log("你好," + name);
}
const btn = document.querySelector<HTMLButtonElement>("button.alert");
btn?.addEventListener("click", () => greet("访客"));
</script>
写交互逻辑时顺手加类型,能在写代码阶段就拦住拼写错、类型错,和前面 Props 用 TypeScript 是同一个思路。
记住:脚本只打包进页面一次
前面提过 Astro 会给脚本“去重”:同一个含 <script> 的组件在一页用了十次,脚本也只进页面一次,不会重复运行。这对你写事件监听很重要——正因如此,才要用 querySelectorAll 找到“所有”目标元素,而不是假设脚本会随组件重复执行。
也要留意脚本的执行时机:默认脚本是 type="module",浏览器会等 HTML 解析后再跑,所以直接用 document.querySelectorAll 找元素通常是安全的。但如果你组件里混用了框架组件(React/Vue 等),它们渲染可能比脚本晚,这时用自定义元素(本章前半讲过的 Web Components)更稳,能避开“脚本跑了,节点还没生成”的尴尬。
小结
给 Astro 组件加交互,主线就是写 <script>:默认脚本会被 Astro 自动处理(TypeScript、打包、去重、内联);要原样输出就加 is:inline;用 addEventListener 处理事件,贴近原生 HTML;想做可复用交互就用 Web Components 自定义元素;服务器侧的值借 data-* 属性过桥到浏览器。下一章讲组件的样式——作用域样式与全局样式。