Please enable Javascript to view the contents

Astro: 另一个更好的 “HTML”

 ·  ☕ 8 分钟

前不久,自诞生以来就备受关注的 Astro 发布了 1.0 版本。博主早就开始关注这个项目了,如今是时候好好谈谈了。

首先,Astro 是一个气质独特的前端框架网页生成器。它的主要卖点有:

  1. 零运行时
  2. 支持 React、Svelte、Vue、Solid、Preact、Lit 等流行的 UI 框架的集成
  3. 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 来安装:

1
2
3
4
5
6
# 使用 npm 创建新项目
npm create astro@latest 
# 或 yarn
yarn create astro
# 或 pnpm
pnpm create astro@latest

一个 Astro 的新项目是默认不包括 React、Svelte、Vue、Solid、Preact、Lit 这些额外的 UI 框架的,需要单独安装,但好在有脚手架可以帮助我们配置好一切 (官方称之为安装 Astro 集成):

1
2
3
4
5
6
7
# 安装 React 集成
npx astro add react
# 安装 Tailwind CSS
npx astro add tailwind
# 安装 MDX
npx astro add mdx
# ...

此时你的项目结构可能看起来像这样:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
├── 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 语法)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
---
// 组件 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)
  • 创建你要在模板中引用的变量
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
---
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 指令

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
---
// 你的组件脚本在这!
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) 组件:

1
2
3
4
5
---
// 使用:<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 文件
  • 如果你的组件在页面上多次使用,则脚本标签将只包含一次
1
2
3
<script>
  // 处理!打包!TypeScript 支持!可以使用 ESM 导入,甚至也适用于 npm 包。
</script>

如果想避免打包脚本,你可以使用 is:inline 属性:

1
2
3
4
<script is:inline>
  // 将会完全按照写好的内容呈现在 HTML 中!
  // ESM 导入将不会相对于文件进行解析。
</script>

使用 React 组件

你可以在 Astro 文件中导入并使用 React 组件 (Vue、Svelte… 等 UI 框架也是一样的道理)。就像使用普通的 Astro 组件一样。

1
2
3
4
5
6
7
8
9
---
import MyReactComponent from "../components/MyReactComponent.jsx";
---
<html>
  <body>
    <h1>Use React components directly in Astro!</h1>
    <MyReactComponent />
  </body>
</html>

默认情况下, Astro 会将 React 组件将渲染为静态 HTML。

激活组件

框架组件可以使用 client:* 指令实现激活。它还可以定义 React 组件应该如何被渲染和激活。

1
2
3
4
5
6
7
8
9
---
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 要求一个特殊的 frontmatterlayout;你需要通过这个属性指定包裹内容的 Layout 组件:

1
2
3
4
5
6
7
---
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 目录中的路径和文件名相对应。

1
2
3
4
5
6
# 示例:静态路由
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 文件:

  1. 使用 [bracket] 标记来识别动态参数 (文件名必须是形如 src/pages/[bracket].astro 的形式)
  2. 导出 getStaticPaths() 函数来明确要由 Astro 进行预渲染的路径

简单来讲,Astro 会先运行这个 Astro 文件的 Astro 脚本区导出的 getStaticPaths(),并根据这个函数返回的结果决定生成的 HTML 页面的数量和路径。然后在每个页面中用 Astro.params 接收由 getStaticPaths() 返回的参数 (params)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
---
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>
<!-- ... -->

缺点

目前(2022-08-16) ([email protected]) 而言,Astro 还有一个挺致命的缺点的:Astro 目前还不支持任何一种测试框架 (无论 Vitest,Jest 还是 Cypress,都不支持)!这就注定 Astro 目前还只是个玩具;虽然已经发布正式版 (1.0 版) 了,但离真正的“正经项目”还是差最后一公里。

不过我也很高兴的看到 Astro 已经开始着手解决这一问题了!

结语

以上便是对 Astro 的基本介绍;一个令人眼前一亮又十分传统保守前端框架网页生成器。

说什么取代 XXX 几乎是不可能的。但也不能说 Astro 就一无是处,未来 Astro 必定会凭借其独特的个性在某些细分领域闯出点名堂 (用来写博客就挺不错的)。


Mogeko
作者
Mogeko
I am a student of Debrecen University, Hungary, majoring in Computer Science. I hope to be a Full-stack in the future.