要点
本篇把开山篇的定位落到手上:从空目录到”一个跑得动、看得见、点得动”的 Astro 小站。做完你会有一个两页的个人名片站,页面上带一个真的可交互组件(岛),并且能说清它的 JS 是在什么时候、被谁下载的。框架定位与 Islands 架构在《Astro 入门》讲过,本篇不重复;内容集合(content collections)的体系化用法留给《用 Astro 做一个极速文档站》——这里连 markdown 都不放,纯手写页面,把最基本的面走一遍。
要做什么与验收标准
做一个两页小站:首页是名片(自我介绍加一个技能列表),about 页是一段静态时间线。首页放一个”点赞”按钮,作为第一个岛(island)——整页唯一需要浏览器执行 JavaScript 的地方。指标开工前定死:
| 验收项 | 目标 | 怎么测 |
|---|---|---|
| 两页可达 | / 与 /about/ 都返回 200 |
npx astro preview 起本地预览后访问,或 curl -I 逐页看状态码 |
| about 页零 JS | 没有交互就没有脚本传输 | Network 面板按 JS 类型过滤,访问 about 页传输量应为 0 |
| 岛按需加载 | 点赞按钮滚入视口前不加载它的 JS | 把按钮放在要滚一段才看到的位置;Network 面板观察:滚动到之前无对应请求,滚到之后才出现 |
| 构建通过 | npx astro build 零报错 |
产物在 dist/,抽查 HTML 里是渲染好的完整正文,不是空的挂载点 |
创建工程
Node 用 20 或更新版本(Astro 5 的基线,官方文档口径)。创建命令与开山篇一致,这次模板选 Empty,从白纸开始:
npm create astro@latest my-card
1. 模板选 "Empty";TypeScript 选 strict——frontmatter 里的类型错误构建期就会报
cd my-card
npx astro dev # 默认 http://localhost:4321,改文件即时热更新
目录里真正常用的就五个位置,各司其职:
| 路径 | 放什么 |
|---|---|
src/pages/ |
路由本体:文件路径就是 URL,一个文件一个页面 |
src/layouts/ |
布局组件——没有强制约定,它就是一个普通组件,只是习惯上单放 |
src/components/ |
可复用组件,.astro 或 React/Vue 等框架组件都行 |
src/content/ |
内容集合的文件(本篇不用,实战篇的主角) |
public/ |
原样拷进产物的静态资源:favicon、字体、robots.txt |
第一个页面:.astro 文件解剖
把 src/pages/index.astro 换成下面的内容。一个 .astro 文件只有两段:上面一对三横线夹住的是 frontmatter(构建期在 Node 里执行的代码,不会进浏览器),下面是从顶写起的标签,长得和 HTML 几乎一样:
---
// src/pages/index.astro
// frontmatter:构建期执行,每个页面构建时跑一遍,产物里没有这段代码
const skills = ['Astro', 'Markdown', '一点点 React'];
const year = new Date().getFullYear(); // 注意:是"构建那年",不是访客看到的实时年份
---
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>x-rush 的名片</title>
</head>
<body>
<h1>你好,我是 x-rush</h1>
<p>写博客的人,偶尔写代码。</p>
<ul>
{skills.map((s) => <li>{s}</li>)} <!-- 花括号里是 JS 表达式,输出为 HTML -->
</ul>
<p>© {year}</p>
</body>
</html>
三个心智点:{} 里的表达式在构建期求值,结果直接写死进 HTML;frontmatter 能写 TypeScript、能 import npm 包,因为它跑在 Node 里;正因如此,在 frontmatter 里碰 window/document 是必然报错的——构建环境没有浏览器,这不是 bug,是设计。
第二页与布局
布局(layout)不是框架功能,就是一个把 <html> 外壳包起来的组件,靠 slot 留出内容注入口。建 src/layouts/BaseLayout.astro:
---
// src/layouts/BaseLayout.astro
const { title } = Astro.props; // 组件入参从 Astro.props 拿,与页面约定好字段名
---
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<title>{title}</title>
</head>
<body>
<nav><a href="/">首页</a> | <a href="/about/">关于</a></nav>
<slot /> <!-- 使用者的子内容从这里注入 -->
<footer>© {new Date().getFullYear()}</footer>
</body>
</html>
首页和 about 页都套上它。about 页用 src/pages/about.astro 建,文件名就是路由(/about/):
---
// src/pages/about.astro
import BaseLayout from '../layouts/BaseLayout.astro';
const milestones = [
{ year: '2024', text: '开始写博客' },
{ year: '2025', text: '第一篇被搜索引擎收录' },
{ year: '2025', text: '换成静态站,加载快了' },
];
---
<BaseLayout title="关于 x-rush">
<h1>关于</h1>
<ul>
{milestones.map((m) => <li>{m.year}:{m.text}</li>)}
</ul>
</BaseLayout>
首页同理改为套 <BaseLayout title="...">,把原来手写的 <html> 外壳删掉。动态路由(一篇文档一个 URL 的那种)不在本篇范围,那是实战篇 collections 的主场。
第一个岛
现在给首页加一个真的交互。先装 React 集成(会自动改配置、装依赖):
npx astro add react
写一个最普通的 React 组件——它不知道自己在 Astro 里,就是普通组件:
// src/components/LikeButton.jsx
import { useState } from 'react';
export default function LikeButton({ start = 0 }) {
const [likes, setLikes] = useState(start);
return <button onClick={() => setLikes(likes + 1)}>赞 {likes}</button>;
}
在首页 frontmatter 里 import 它,然后用一条指令把它标记为岛:
---
// src/pages/index.astro 的 frontmatter 追加一行
import LikeButton from '../components/LikeButton';
---
<!-- 放在页面下方,需要滚一段才能看到 -->
<LikeButton client:visible start={0} />
跑 npx astro dev,打开首页验证那三件事:按钮渲染出来了(这是构建期输出的一份静态 HTML,不依赖 JS);按钮滚入视口之前,Network 面板里没有它的 JS 请求;滚入视口后请求出现,点击数字正常增长——client:visible 的语义就是”滚到了才注水(hydration,浏览器执行 JS 把静态 HTML 激活成可交互组件)”。
反模式一:在 .astro 模板里写 onClick={...}。从 React 来的人最容易犯。.astro 模板不是 JSX:事件属性没有意义,构建期渲染成静态 HTML 后没有任何东西处理点击。想交互只有两条正路——要么用框架组件加指令做成岛,要么用 <script> 自己写(取舍与写法是下一篇《Astro 语法要点》</a的主题)。
反模式二:为了”统一”给布局挂 client:load。导航、页脚不需要状态,挂了指令就是白送一整份运行时,两页小站当场被拖回”整页注水”的老路——开山篇强调过的账,动手时最容易破功。
构建与预览
npx astro build # 产物在 dist/:纯 HTML/CSS/少量 JS,无服务器要求
npx astro preview # 本地起静态服务,检查构建产物(dev 跑的是源码,两者要分开验)
dist/ 里每个路由一个 HTML 文件,打开看会发现正文都在——这是静态站和”客户端渲染空壳”的本质区别,SEO 与首屏都吃这个红利。部署就是把这个目录交给任意静态托管(Cloudflare Pages、Netlify、Vercel、GitHub Pages 均可,流程与 CI 串联见实战篇)。dev、preview、部署后是三个环境,验收一律以构建产物为准,别拿 dev 环境的表现当上线结论。
常见问题
Q1: 改了代码页面没变
先分清跑的是哪个环境:dev 热更新针对源码;如果你在验证 dist/,改完源码必须重新 build。frontmatter 里的 new Date() 这类构建期求值也一样,产物定稿后不会自己变。
Q2: 只想加个小交互,也要装 React 吗
不必。主题切换、折叠菜单这类无状态小交互,用 <script> 直接操作 DOM 就够,产物里零框架运行时。什么时候值得动用岛、两条路怎么选,下一篇语法要点有完整的判断标准。
Q3: 什么时候该跳出本篇的写法
一旦主体变成”一批批内容文件”而不是”一个个手写页面”,就该切到内容集合:schema 校验、动态路由、自动导航,都在实战篇一条链走完。本篇的价值是把”页面、组件、岛”三件套吃透,它们是后面一切的地基。
练习与实践
练习一:把 LikeButton 的指令从 client:visible 依次换成 client:load、去掉指令,各跑一次 dev,用 Network 面板记录三种情况下 JS 请求的出现时机(去掉指令后按钮在 HTML 里依然可见,只是点了没反应——亲手体会”渲染”与”交互”是两件事)。练习二:给 about 页加一个完全静态的”回到顶部”链接(<a href="#">),确认整页 JS 传输依然是 0。验收标准:能对着自己的站说出每一块 JS”为什么存在、什么时候被下载”。
延伸阅读
组件语法、模板指令、脚本处理与注水机制的深入拆解,在下一篇《Astro 语法要点:组件、指令与客户端脚本》;要一步到位做真实项目,直接进《用 Astro 做一个极速文档站》;定位与选型没把牢的,回《Astro 入门》补认知。
版本核实(2025-02-08 撰写时检索):Astro 稳定线为 5.2.x(5.0 发布于 2024-12;GitHub releases 口径),本文用到的 .astro frontmatter、slot、Astro.props、npx astro add、client:visible 均为官方文档现行接口;Node 20+ 为 Astro 5 的基线。安装命令一律不钉版本号,以 docs.astro.build 为准。