Astro: 另一个更好的 “HTML”
前不久,自诞生以来就备受关注的 Astro 发布了 1.0 版本。博主早就开始关注这个项目了,如今是时候好好谈谈了。
首先,Astro 是一个气质独特的前端框架网页生成器。它的主要卖点有:
- 零运行时
- 支持 React、Svelte、Vue、Solid、Preact、Lit 等流行的 UI 框架的集成
- SSG 以及 SSR
- …
博主第一次看见这些特性时,直接被吓了一跳;且不说零运行时和支持 React 怎么同时存在于一张 Feature 列表的?就单单让 React 和 Vue 相互兼容就非常的不可思议!这到底是什么黑魔法?!
阅读完 Astro Docs 后,Emmm… 它确实是 “零运行时”,也确实可以 “让 React 和 Vue 相互兼容”,不过代价是不加载 JavaScript!相当于将 React 和 Vue 在编译阶段就渲染成 HTML 和 CSS (默认行为),想要拥有状态?也不是不行,不过这样就必须有运行时了 (按需加载)。
这不就是另一个 Next.js 吗?不仅不是,还差的有点远;在 SSG 方面,Astro 比 Next.js 更激进更极端 (默认不加载任何 JavaScript);并且在某些方面,Astro 需要的代码更少,速度反而更快 (零运行时)。但对于动态能力要求比较高的网站,或者 SSR 领域,Astro 目前就完全只能被 Next.js 碾压了。
Astro 究竟是什么?🔗
Astro 独特的个性使其很难被简单的分类为前端框架(类似于 Next.js) 或是网页生成器 (Jekyll、Hugo 之类的)。你说他是前端框架吧?它的运行方式更像是一个网页生成器;你说它是网页生成器吧?用起来的感觉到和 Remix 有几分相似。
思考了一下,我终于悟了。这不就是一个 “更好用的 HTML” 吗?!
首先,虽然使用了类 JSX 的语法来表达结构,但此 JSX 非彼 JSX,React 但 JSX 本质上是 JavaScript 函数,而 Astro 中的 JSX 更像是模版,是为了渲染 HTML 和 CSS 而存在的;在不使用 React 等框架的情况下,使用 <script>
加载 JavaScript;使用 <style>
加载 CSS,像什么 CSS in JS,也不是完全不可能,只是肯定非常的水土不服。就连开发思路也是传统 Web 开发的那一套。
零运行时,组件化… 这不就是 HTML 最想成为的那个样子吗?
扯了这么多,终于要正式进入 Astro 的世界了!
值得一提的是,Astro 的官方文档是支持中文的;我这篇文章只是带大家 “Take a look”,想要系统学习,还是更推荐看官方文档。
安装 Astro🔗
安装 Astro 最简单的方式是通过官方脚手架 create-astro
来安装:
# 使用 npm 创建新项目
npm create astro@latest
# 或 yarn
yarn create astro
# 或 pnpm
pnpm create astro@latest
一个 Astro 的新项目是默认不包括 React、Svelte、Vue、Solid、Preact、Lit 这些额外的 UI 框架的,需要单独安装,但好在有脚手架可以帮助我们配置好一切 (官方称之为安装 Astro 集成):
# 安装 React 集成
npx astro add react
# 安装 Tailwind CSS
npx astro add tailwind
# 安装 MDX
npx astro add mdx
# ...
此时你的项目结构可能看起来像这样:
├── src/
│ ├── components/
│ │ ├── Header.astro
│ │ └-─ Button.jsx
│ ├── layouts/
│ │ └-─ PostLayout.astro
│ └── pages/
│ │ ├── posts/
│ │ │ ├── post1.md
│ │ │ ├── post2.md
│ │ │ └── post3.md
│ │ └── index.astro
│ └── styles/
│ └-─ global.css
├── public/
│ ├── robots.plaintext
│ ├── favicon.svg
│ └-─ social-image.png
├── astro.config.mjs
└── package.json
其中:
src/*
- 项目的源代码 (组件、页面、CSS 等)。src/pages/*
- 页面文件 (必须要有),Astro 将根据它的目录结构构建路由public/*
- 非代码、不需要处理的资源 (字体、图标等)package.json
- 项目元数据列表。astro.config.mjs
- Astro 的配置文件 (可选)
Astro 组件🔗
Astro 组件是 Astro 项目的基础构建块。它们是纯 HTML、无需客户端运行时的模板组件。
Astro 组件由两部分组成:
- 编译时运行 (SSG) 或运行在 Server 上 (SSR) 的 JavaScript 代码
- 组件模板 (类 JSX 语法)
---
// 组件 Script(JavaScript)
import Button from "./Button.astro";
---
<!-- Component Template (HTML + JS Expressions) -->
<div>
<Button title="Button 1" />
<Button title="Button 2" />
<Button title="Button 3" />
</div>
组件 Script🔗
Astro 的组件 Script 需要放在代码栅栏 (---) 里。是不是很像 Markdown 的 frontmatter?Astro 的组件 Script 的灵感就来源于此。
在 组件 Script 中,你可以:
- 导入其他 Astro 组件
- 导入其他框架 (如 React) 编写的组件
- 导入数据,如 JSON 文件
- 从 API 或数据库中获取内容 (使用
fetch
) - 创建你要在模板中引用的变量
---
import SomeAstroComponent from "../components/SomeAstroComponent.astro";
import SomeReactComponent from "../components/SomeReactComponent.jsx";
import someData from "../data/pokemon.json";
// 访问传入的组件参数,如 `<X title="Hello, World"/>`
const {title} = Astro.props;
// 获取外部数据,甚至可以从私有 API 和数据库中获取
const data = await fetch ("SOME_SECRET_API_URL/users").then (r => r.json ());
---
<!-- 你的模板在这! -->
组件 Script 只会在编译时,或服务器上运行。如果你在组件 Script 中使用 console.log()
,它也只将信息打印在运行 Astro 的终端 (Node.js) 上,而不会出现在浏览器的控制台里。
代码围栏的设计是为了保证你在其中编写的 JavaScript 被“围起来”。它不会逃到你的前端应用程序中,或落入你的用户手中。你可以安全地在这里写一些昂贵或敏感的代码(比如调用你的私人数据库),而不用担心它会出现在你的用户的浏览器中。
组件模板🔗
在组件脚本下面的是组件模板。组件模板决定了你的组件的 HTML 输出。
Astro 组件模版跟 JSX 非常像,但 Astro 组件模版拥有一些特殊的 Astro 指令。
---
// 你的组件脚本在这!
import ReactPokemonComponent from "../components/ReactPokemonComponent.jsx";
const myFavoritePokemon = [/* ... */];
---
<!-- 支持 HTML 注释! -->
<h1>你好,世界!</h1>
<!-- 在组件脚本中使用参数或其他变量: -->
<p>我最喜欢的宝可梦是:{Astro.props.title}</p>
<!-- 包括其他带有 `client:` 指令的激活组件: -->
<ReactPokemonComponent client:visible />
<!-- 混合 HTML 和 JavaScript 表达式,类似于 JSX: -->
<ul>
{myFavoritePokemon.map ((data) => <li>{data.name}</li>)}
<ul>
<!-- 使用模板指令并根据字符串或对象来生成 class 名: -->
<p class:list={["add", "dynamic", {classNames: true}]} />
组件参数🔗
Astro 组件可以通过 Astro.props
定义和接受参数。
例如下面这个 GreetingHeadline
(GreetingHeadline.astro
) 组件:
---
// 使用:<GreetingHeadline greeting="Howdy" name="Partner" />
const { greeting, name } = Astro.props;
---
<h2>{greeting}, {name}!</h2>
在不使用别的 UI 框架组件 (React、Svelte、Vue、Preact、SolidJS、AlpineJS、Lit) 的情况下,你可以在 Astro 组件模板中使用 <script>
标签编写运行在客户端 (浏览器) 中的 JavaScript 代码。
默认情况下,<script>
标签由 Astro 处理:
- 任何导入都将被打包,并且允许你导入本地文件或 Node.js 模块
- 处理后的脚本将通过
type="module"
注入你页面的<head>
- 全面支持 TypeScript 包括导入 TypeScript 文件
- 如果你的组件在页面上多次使用,则脚本标签将只包含一次
<script>
// 处理!打包!TypeScript 支持!可以使用 ESM 导入,甚至也适用于 npm 包。
</script>
如果想避免打包脚本,你可以使用 is:inline
属性:
<script is:inline>
// 将会完全按照写好的内容呈现在 HTML 中!
// ESM 导入将不会相对于文件进行解析。
</script>
使用 React 组件🔗
你可以在 Astro 文件中导入并使用 React 组件 (Vue、Svelte… 等 UI 框架也是一样的道理)。就像使用普通的 Astro 组件一样。
---
import MyReactComponent from "../components/MyReactComponent.jsx";
---
<html>
<body>
<h1>Use React components directly in Astro!</h1>
<MyReactComponent />
</body>
</html>
默认情况下, Astro 会将 React 组件将渲染为静态 HTML。
激活组件🔗
框架组件可以使用 client:*
指令实现激活。它还可以定义 React 组件应该如何被渲染和激活。
---
import InteractiveButton from "../components/InteractiveButton.jsx";
import InteractiveCounter from "../components/InteractiveCounter.jsx";
---
<!-- 该组件将在页面加载开始时导入 -->
<InteractiveButton client:load />
<!-- 该组件将不会分发给客户端直到用户滚动到该组件的位置 (使组件在页面上是可见的) -->
<InteractiveCounter client:visible />
框架组件所必须的渲染 JS(如 React、Svelte)都会随着页面一同下载。client:* 指令只决定了何时导入组件 JS,以及何时激活框架。
Astro 页面🔗
src/pages/*
下的所有 Astro 文件 (*.astro
) 或 Markdown (*.md
) 文件都会被渲染成页面。并且 Astro 会根据你的目录结构生成路由。
Astro 文件🔗
Astro 文件生成的 Astro 页面没什么好说的,更 Astro 组件是一回事。
Markdown 文件🔗
对于 Markdown 文件,Astro 要求一个特殊的 frontmatter:layout
;你需要通过这个属性指定包裹内容的 Layout 组件:
---
layout: "../layouts/MySiteLayout.astro"
title: "My Markdown page"
---
# Title
This is my page, written in **Markdown.**
路由🔗
Astro 的路由基于文件,它根据项目的 src/pages
目录中的文件结构来在编译阶段为你生成 HTML 页面。
大体来讲,Astro 路由分为静态路由和动态路由。不过动态路由的本质仍然是静态路由。
静态路由🔗
src/pages
目录中的 Astro 文件 (*.astro
) 和 Markdown 文件 (*.md
) 将自动成为网站页面。每个页面的路由都和其在 src/pages
目录中的路径和文件名相对应。
# 示例:静态路由
src/pages/index.astro -> mysite.com/
src/pages/about.astro -> mysite.com/about
src/pages/about/index.astro -> mysite.com/about
src/pages/about/me.astro -> mysite.com/about/me
src/pages/posts/1.md -> mysite.com/posts/1
动态路由🔗
动态路由就是通过一个 Astro 文件在编译阶段有条件的生成多个 HTML 页面。
这个 Astro 文件:
- 使用
[bracket]
标记来识别动态参数 (文件名必须是形如src/pages/[bracket].astro
的形式) - 导出
getStaticPaths()
函数来明确要由 Astro 进行预渲染的路径
简单来讲,Astro 会先运行这个 Astro 文件的 Astro 脚本区导出的 getStaticPaths()
,并根据这个函数返回的结果决定生成的 HTML 页面的数量和路径。然后在每个页面中用 Astro.params
接收由 getStaticPaths()
返回的参数 (params
)。
---
export async function getStaticPaths() {
const posts = Astro.glob("../post/*.md");
const tags = posts.reduce((acc, post) => {
return acc.concat(post.frontmatter.tags);
}, []);
return [...new Set(tags)].map((tag) => {
return { params: { tag: tag } };
});
}
const { tag } = Astro.params;
---
<h1>{tag}</h1>
<!-- ... -->
缺点🔗
就目前 (astro@1.0.6) 而言,Astro 还有一个挺致命的缺点的:Astro 目前还不支持任何一种测试框架 (无论 Vitest,Jest 还是 Cypress,都不支持)!这就注定 Astro 目前还只是个玩具;虽然已经发布正式版 (1.0 版) 了,但离真正的“正经项目”还是差最后一公里。
不过我也很高兴的看到 Astro 已经开始着手解决这一问题了!
结语🔗
以上便是对 Astro 的基本介绍;一个令人眼前一亮又十分传统保守的前端框架网页生成器。
说什么取代 XXX 几乎是不可能的。但也不能说 Astro 就一无是处,未来 Astro 必定会凭借其独特的个性在某些细分领域闯出点名堂 (用来写博客就挺不错的)。