译:Astro 是什么

原文:https://www.eddyvinck.com/blog/what-is-astro/
作者:Eddy Vinck
译者:ChatGPT 4 Turbo

我一用 Astro 就爱上了它,此后我在一次聚会上和四次会议上讲过关于 Astro 的内容。两次是在克拉科夫和华沙的 Code Europe,一次是在柏林的 WeAreDevelopers 世界会议,最后一次是在美国德克萨斯州的 Round Rock 做的扩展版演讲。在这篇文章中,我们将介绍会议演讲的最后一个版本的内容。另外:我和 Astro 没有关联。我只是一个超级粉丝。

📽️ 我还在编辑这篇文章的会议演讲录像。稍后请回访这篇文章,或者在 Twitter 上关注我 @EddyVinckk,这样你就不会错过了。

Astro 是什么,它有什么不同之处?

Astro 是一个框架。但 Astro 并不是另一个类似于 React、Vue、Angular、Svelte、Solid.js 等以及其他面向构建网络应用程序的 JavaScript UI 框架。

Astro 是一个专注于构建网站的框架,它非常专注于这一点。

内容聚焦

Astro 知道它想要做什么。Astro 专注于内容丰富的网站。

内容为中心的网站可以包括博客,甚至是电子商务,但我认为电子商务可能是你应该划界限的地方。任何比这更动态的内容,你可能应该使用其他东西。

但这并不意味着 Astro 不好。它在其旨在做的事情上做得非常好。而我相信,这种专注是 Astro 与 JavaScript 生态系统中试图做所有事情的一些其他框架区别开来的地方。

专注是一件好事。这意味着 Astro 能够提供更加流畅的开发体验来构建以内容为中心的网站,并默认提供更好的结果。

如果你在考虑构建一个网络应用程序,那么 Astro 可能不是正确的选择。如果你在构建任何以内容为中心的东西,那么 Astro 可能是你项目的一个很好的选择。

Astro 用于单页应用程序 (SPAs)

有些人实际上正在使用 Astro 作为他们的 React 应用程序的起点。

他们启动了一个新的 Astro 项目,并且页面中只有一个大的 React 组件,这个组件就是他们完整的 React.js 应用程序。

所以 Astro 可以用于多种类型的应用程序。但从框架的角度来看,它们更专注于多页面应用程序(MPAs,或者就是典型的网站)。

灵活

Astro 是一个非常灵活的框架。

你可以使用自己的 UI 框架。你可以选择在你的 Astro 网站的特定部分使用 React 或 Vue。但它并不局限于特定的前端库,如 React 或 Vue。使用你想要的前端框架。

带上你自己的框架。带上你现有的组件。非常酷。

你甚至可以使用多于一个框架,如果你愿意的话,尽管你可能不应该这么做,因为这意味着你也会随着应用一起打包多个框架的源代码。

Astro 中的内容

为您的 Astro 网站创建内容,您可以使用 CMS,或者使用内置的 Markdown。

有许多可用的 CMS 选项,包括:

  • Storyblok
  • Contentful
  • 甚至 Headless WordPress

快速

Astro 项目构建速度快,用户使用起来也很快。

使用 Astro 构建我的个人网站是一次简单直接的经历。我采用了一个现有模板,并开始根据我在模板中看到的现有代码添加新功能。

我不记得需要多参考文档。一切都非常直观。

我很快就完成了新网站的核心部分。我大部分时间都花在了自定义我的 CSS 上。

但正如我所说,Astro 网站也是快速的网站。Astro 网站默认情况下不包含任何 JavaScript。

Zero JavaScript 并非目标

Zero JavaScript 不是目标。Zero JavaScript 是基线。
— Fred K. Schott,Astro 的联合创始人

不在页面上使用任何 JavaScript 并不是目标。提供良好的用户体验才是。而 JavaScript 可以在这方面提供帮助。

使用 JavaScript 提升网站用户体验的一些例子包括:

  • 客户端表单验证
  • 无需全页刷新的搜索功能
  • 改善焦点管理以帮助提高可访问性
  • 实时数据

Astro 的岛屿架构

Astro 的一个优点是它的 Islands 架构。

Islands Architecture 允许你仅在页面的特定部分使用 JavaScript,并且只用于页面中需要它的部分。

在这个示例页面布局中,有两部分将从 JavaScript 中受益:页眉和图片轮播。其他内容,如侧边栏、主要内容和页脚,大概不需要任何 JavaScript。

但默认情况下,当你使用 Astro 时,你在这个组件中包含的任何 JavaScript 都不会被放到页面上。

客户端指令

为了在你的 Header 和 Image Carousel 组件中包含 JavaScript,Astro 为我们提供了这些方便的客户端指令。

客户端指令就像是你的组件的 HTML 属性:

  • client:load 是用于立即可见的 UI 元素,需要尽快实现交互性。
  • client:idle 是用于优先级较低的 UI 元素,在它们不需要立即交互时这样做是可以的
  • client:visible 这对于那些在打开页面时看不到的 UI 元素非常有用。所以,如果你从不滚动到它们那里,你也就无需加载代码。
  • client:media 适用于只能在特定屏幕尺寸上显示的元素
  • client:only 是为了完全跳过 HTML 渲染,只在客户端渲染代码

示例:

<MyReactComponent client:load />
<MyReactComponent client:idle />
<MyReactComponent client:visible />
<MyReactComponent client:media="(max-width: 50em)" />
<MyReactComponent client:only="solid-js" />

当然,Astro 需要一点点 JavaScript 来使这些工作,但与立即加载前端库和所有组件代码相比,这是一个不错的权衡。

在包含 Header 和 Carousel 的示例中,Header 将使用 client:load,因为你希望它尽可能快地变得可交互。Astro 默认会包含 JavaScript。

但是对于大多数用户来说,尤其是手机用户,图片轮播可能甚至在页面上都不可见。图片轮播的 JavaScript 代码只有在组件对用户可见时才会加载。所以如果他们从不向下滚动,就永远不需要下载这段代码。

但为什么是 Astro?

为什么人们喜欢 Astro?你为什么可能想要使用它?

我之前的网站

我们真的需要另一个 JavaScript 框架吗?

我们长时间以来一直能够创建网站,所以也许不会。

但我记得我在做我的个人网站。

第一版是用 PHP 编写的。它完全是定制的,我个人发现更新起来不太理想,因为内容只是硬编码到网站中的。当然,这是我的第一个网站,所以如果它做得不好也没关系,但对于一个很少更新且不是很动态的网站来说,拥有 PHP 服务器有点大材小用。

在我的第二次迭代中,我使用一个名为 Gatsby.js 的静态网站生成器制作了一个网站。能够在 markdown 文件中编写博客文章和其他内容真是太好了。这是我第一次尝试这个框架,所以也许我没有以最佳方式使用它,但它有一个完整的 GraphQL 层,如果你想做自定义的东西,就需要接入这个层。个人而言,我发现在一段时间不更新那些自定义代码后,很难再次进入状态。而且最后我认为对于一个静态网站来说,GraphQL 层完全是过度设计。

这就引出了我的网站的第三个版本。这一次,我使用了 Astro。这个版本同样是一个静态生成的网站,但是为了构建我想要的内容,所需的复杂性似乎更加合理。当然,到了这个时候我已经是一个更出色的开发者了,但我能够很快完成网站的搭建,性能也很好,而且这还是我第一次使用它。Astro 给我的感觉就是对的。

集成

Astro 的一个巨大优势是可用的集成。尽管 Astro 相对较新,但它已经拥有许多集成。

Astro 网站上有一个专门的页面,你可以在那里找到各种集成,包括官方的和社区创建的集成,适用于各种用例。

  • 框架
  • 特定托管平台的适配器
  • SEO
  • 分析
  • 以及许多其他类型的集成

对于我自己的网站,我正在使用几个集成:

  • Tailwind CSS
  • React.js
  • 站点地图
  • MDX(这让我在 markdown 文件中导入和使用 UI 库组件

为类似 Solid-Js 的东西添加一个集成是相当容易的:

npx astro add react
npx astro add solid
npx astro add vue
npx astro add [integration]

你只需运行命令 npx astro add solid,就会有一个脚本为你完成必要的配置更改。

主题

Astro 有许多现成的主题可供选择。你可以快速地采用别人的主题并使用它。你可以在几分钟内建立好你的网站。

有以下主题:

  • blogs 博客
  • portfolios 作品集
  • e-commerce 电子商务
  • documentation websites 文档网站
  • 和许多其他类型的主题

我使用 AstroPaper 主题作为起点,成功快速启动了自己的网站。

社区

Astro 的另一个优点是社区。

你可以加入一个 Discord 服务器,在那里你可以讨论所有关于 Astro 的事情,看看人们都在构建什么,以及在遇到困难时提问。

我之前在那里提过问题,人们总是非常友好和乐于助人。

Astro in the wild

在现实世界中有许多 Astro 的例子。以下是我找到的一些简短列表:

  • Google Firebase 的工程博客
  • 《卫报》的工程网站
  • Microsoft 使用 Astro 为他们的设计系统(Fluent 2)编写文档
  • NordVPN 在他们的网站上有一个关于社会责任的部分
  • Trivago 的工程博客
  • 即使 Limp Bizkit 也使用 Astro(而且他们有一个深色模式开关!)

你如何使用 Astro 来构建网站?

你可以通过在终端运行 npm create astro@latest 来开始你的新应用。

这将从 npm 下载一个安装脚本来启动您的项目。

它会问你这样的问题。

  • 它应该将项目文件安装在哪里
  • 您想使用哪个入门模板?
  • 如果你想使用 TypeScript ,以及你希望它的严格程度如何

然后它会告诉你需要运行哪些命令来开始你的项目。

当你在代码编辑器中打开项目时,你会看到一些核心目录:

  • 有一个 pages/ 目录,其中包含了页面的文件,这可以包括输出多个页面的动态模板。
  • 和一个 layouts/ 目录,基本上就是用于项目中各种页面布局的组件。
  • 和一个 components/ 目录,你可以在其中放置 .astro 组件以及使用类似 React 的 UI 库构建的组件。

你完全可以像那样从零开始你的 Astro 项目。我的建议是不要从头开始。我推荐使用现有的主题来加快你的项目进度,并根据你的喜好来定制这个主题。

大多数模板在其文档中应该包含这一内容,但你可以使用 CLI 从任何仓库创建一个 Astro 项目:

npm create astro@latest -- --template <github-repo>

创建你自己的 Astro 主题

你也可以很容易地创建自己的模板。

Astro 中的模板只是另一个 Astro 项目,人们可以复制并根据自己的需求进行编辑。

要创建一个模板,你不需要将你的代码作为包之类的放到 NPM 上。

你需要做的就是将你的 GitHub 仓库设为开源,并为人们提供使用你的模板所需的指南。

你也可以将其提交到 Astro 网站的主题部分。

Astro 组件

你可以使用 .astro 文件来创建 Astro 组件、布局或页面。

在左边,你可以看到一个页面的 Astro 文件。在右边,你可以看到一个作为 Astro 文件的 Card 组件:

在顶部是一组虚线之间的 Frontmatter。那是你可以导入组件的地方。既可以使用 .astro 文件,也可以使用像 React 或 Vue 这样的 UI 框架组件。

在左边你可以看到一个布局和一个正在被导入的 Card 组件。在右边你可以看到正在定义的 props,这些是 Card 组件接受作为传入数据的属性。

你也可以在 Frontmatter 中编写代码,创建变量以在你的标记中使用,或者获取组件、布局或页面所需的任何数据。

那就是 HTML。关于这个不需要多说,只是你可以使用大括号来输出变量,像这样: {myVariable}

在底部你会找到 CSS。这个 CSS 可以是 scoped 或者是 global。Astro 文件中的 CSS 默认是 scoped 的,这意味着这里的 h1 样式只会应用到这个页面,而不会影响其他页面。有多种方法可以实现 global CSS,比如在每个页面上导入一个 global CSS 文件,或者在 style 标签上使用 is:global 指令。

你也可以在 .astro 文件中添加一个 script 标签,如果你不一定需要一个 UI 库的话。

内容集合

有一个功能叫做“内容集合”,它通过允许你在 markdown 文件的 frontmatter 部分定义你需要的数据类型,基本上使你的 markdown 和 frontmatter 类型安全。

这可以用来设置某些字段为必填项,比如文章描述或发布日期。Astro 在编译网站时可以检查这些必填字段。

当您使用内容集合时,您会在 src/content 目录中添加网站的内容组。这个内容目录有子目录,对应于您定义的每一种内容集合类型。

这里我有一个内容集合,包括作者、博客和主题,每个集合对应一个目录,这些目录都包含了 markdown 文件。

这是在使用内容集合时出现的一个编译错误示例:

我收到一个错误,说这种类型的内容集合需要 lastname

这些编译错误在你尝试添加新功能时特别有用,如果你忘记更新一个 markdown 文件以包含一个新属性,它们会提醒你。

结合内容集合与 UI 库组件

你可以这样将 UI 库组件(如 Solid.js)与你的内容集合结合起来。

这个例子是我自己的 Astro 主题 “Astro Engineering Blog” 中的搜索栏。

在你的 Astro 文件中,你可以导入 Solid.js 搜索栏,并在 HTML 中使用它。不要忘记添加正确的客户端指令,以便实际加载 JavaScript。在这里,我使用 getCollection 获取我搜索栏所需的所有数据,它从博客内容集合中获取我所有的帖子。然后,我只是将这些数据作为 search-list 属性传递给我的组件。就是这样。

对于 searchbar Solid.js 组件,Astro 创建一个 Astro Island 组件,并将所有 search-list 数据内联为 JSON。当 Solid.js searchbar 组件被渲染时,它会将 search-list 数据传递给该组件。

有其他方法可以做到这一点,比如从端点获取数据,但这是最直接的方法,而且它与静态生成的网站很好地配合。

服务器端渲染和 Astro 中的混合渲染

Astro 不仅适用于静态网站。你可以使用 Astro 来做服务器端渲染的网站。你甚至可以将 SSR 与静态页面结合,这称为混合渲染。

在你的 astro 配置文件中,你可以选择一个输出模式。在这个配置中,我选择了 "hybrid"

export default defineConfig({
  output: "hybrid",
  adapter: nodejs({
    mode: "middleware",
  }),
  site: SITE_URL,
  integrations: [
    mdx(),
    sitemap(),
    solidJs({
      include: "**.tsx",
    }),
    tailwind(),
  ],
});

您也可以选择 "static""server" 输出模式。

"hybrid" 选项允许您将静态和服务器输出模式混合用于特定页面。当您使用 hybrid 模式时,可以导出一个名为 prerender 的变量,来告诉 Astro 是否应该对页面进行静态预渲染。

export const prerender = true;

如果 prerender 为假,则您的页面可以利用服务器功能使其更具动态性。

保持你的 Astro 项目为最新状态

我已经维护了我的 Astro 网站一段时间了,同时我也保持我的 Astro 工程博客模板更新至最新。

在我看来,保持你的 Astro 项目更新一直都很简单。

在升级到 Astro 的一个新的主要版本时,他们甚至会编写一个升级指南,概述你可能需要在代码库中进行的所有更改。

当我这样做时,我可以逐项检查那个列表,并在不到一个小时内将我的网站升级到最新的 Astro 版本,保持其更新。

您的 Astro 项目很可能会有几个包用于您的集成。

Astro 现在提供了一个方便的 CLI 工具来升级所有项目,这样你就不必手动一个个检查了。你可以通过运行以下命令来使用它:

npx @astrojs/upgrade

我在我的项目上运行了这个,它向我展示了哪些是最新的,哪些不是,以及哪些有重大变更,并附上了我需要检查的变更日志的链接:

Astro 中的服务器端点(API 路由)

Astro 允许你为任何你需要的数据类型创建端点,比如 JSON、图片、RSS 源或其他你需要的内容。它可以在构建时做到这一点,但你也可以拥有支持 GETPOST 请求的实时服务器端点。

第一个选项是创建在构建时转换为静态文件的静态文件端点。这样,例如,你甚至可以为静态构建的网站拥有一个 RSS 源。

第二个选项是创建服务器端点。如果你听说过其他框架中的 API 路由,那就是同一回事。

当然,这为你的 Astro 项目打开了一个全新的可能性世界,并使你能够创建更加动态的网站。

要告诉 Astro 一个端点应该是静态的还是应该在服务器上动态运行,你需要做的和页面一样:导出一个 prerender 变量。如果 prerender 等于 false,你的端点将是一个动态服务器端点(API 路由)。

这是一个基本的 GET 端点示例。首先你会做一些典型的服务器操作,比如与数据库通信,然后你返回一个 Response

你也可以在这里有 POSTDELETE 导出,当然,当这些动词用于传入请求方法时,会调用它们。

export const prerender = false;

export const GET: APIRoute = async (): Promise<Response> => {
  // magic server stuff

  return new Response(JSON.stringify(postReactionsRanked), {
    status: 200,
    headers: {
      "Content-Type": "application/json",
    },
  });
};

还有一个 ALL 动词可以导出,它会匹配任何比 GETPOSTDELETE 更不具体的动词。它看起来是这样的:

export const ALL: APIRoute = ({ request }) => {
  return new Response(
    JSON.stringify({
      message: `This was a ${request.method}!`,
    })
  );
};

使用一个后端即服务平台

由于 Astro 基本上可以运行 NodeJS 端点,您也可以使用它们来轻松地与 Backend-as-a-Service 平台集成,以便为您的网站添加更多动态功能。

Astro 文档甚至集成了一些与各种 Backend-as-a-Service 提供商合作的指南。

他们中的每一个主要都将以相同的方式被整合:

  • 步骤1:添加您的 API 密钥
  • 步骤2:安装他们的库
  • 步骤 3:去构建东西

让我们来看看我是如何使用 Appwrite 为我的 Astro Engineering Blog 模板添加一些酷炫的动态功能的。

我为 Astro Engineering Blog 主题的每篇博客文章实现了这些表情反应按钮。你可能以前见过这些:你可以点击博客文章下方的一个按钮,让人们知道你对它的看法。

那么我是如何实现它们的呢?

它都是从 getStaticPaths 函数开始的,该函数运行并创建了我所有的博客文章页面:

// pages/blog/[...page].astro
export const getStaticPaths: GetStaticPaths = async ({ paginate }) => {
  const posts = (await getCollection("blog"))
    .filter(post => post.data.draft === false)
    .sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());

  if (import.meta.env.SECRET_APPWRITE_API_KEY) {
    try {
      await tryInitNewBlogPostsReactionsInDatabaseCollection(posts);
    } catch (error: unknown) {
      if (error instanceof AppwriteException) {
        console.log(`⚠️  There was a problem...`);
      }
    }
  }

  return paginate(posts, {
    pageSize: PAGINATION_POSTS_PER_PAGE,
  });
};

如果你不知道 getStaticPaths 的作用,你可以用它来为特定路由生成所有页面的 URL,在这种情况下,它将生成紧随 /blog 之后的页面路径。

首先,它使用 "blog" 内容集合来获取所有帖子。然后,我检查主题的用户是否有 Appwrite 秘密 API 密钥。

如果存在 Appwrite API 密钥,那么我会调用一个拥有世界上最长名称之一的函数: try Init New BlogPosts Reactions In Database Collection

我们稍后会详细讨论那个函数,但首先让我们看看数据是如何流向前端的。

然后,在每个博客文章页面我有一个 Astro 组件 EmojiReactions 。在这个组件中我会检查是否存在 Appwrite API 密钥,如果没有 API 密钥,那么该组件就只是页面上的一个空 div。

这样,我让 emoji 反应与 Appwrite 的集成完全变成了可选项。如果模板用户没有包含 Appwrite API 密钥,那么这个功能就会优雅地消失。

Astro 组件渲染一个 Solid.js 组件 EmojiReactionButtons

// EmojiReactions.astro
<EmojiReactionsButtons
  client:idle
  articleId={Astro.props.id}
  initialEmojiReactions={emojiReactions}
/>

最初我只在一个 Astro 组件中包含了所有内容,但是你不能在 Astro 组件上使用客户端指令。这只能在 JavaScript 库组件中实现,所以我才改用 Solid.js。否则即使你没有使用 Appwrite,它也会包含按钮的 JavaScript。

这是一个简化版的 Solid.js 组件。当它首次挂载时,它会发起一个请求,用存储在 Appwrite 中的最新数据更新显示的表情反应。

然后,当有人点击其中一个按钮时,它会发起一个 API 调用,然后它将再次更新为最新的反应数据。

// EmojiReactionsButtons.tsx
export const EmojiReactionsButtons = ({ articleId, initialEmojiReactions }) => {
  const [buttonsDisabled, setButtonsDisabled] = createSignal(false);
  const [emojiReactions, setEmojiReactions] = createSignal<PostReactions>(
    initialEmojiReactions
  );

  onMount(async function getCurrentVotes() {
    // fetch data ✨
    setEmojiReactions(data);
  });

  async function handleVote(type: PostReactionOption) {
    if (!articleId || buttonsDisabled()) return;
    // send data ✨
    setEmojiReactions(data);
  }

  return (
    <>
      {initialEmojiReactions !== null && (
        <aside>
          <p>Rate this article</p>
          <div>
            <button onClick={() => handleVote("likes")}>
              👍 {emojiReactions().likes}
            </button>
          </div>
          <p>
            <a href="/blog-ranking">Blog post leaderboard</a>
          </p>
        </aside>
      )}
    </>
  );
};

更新 emoji 反应的 POST 端点可能看起来代码很多,但大部分只是错误处理。如果你至少对 Node.js 有些熟悉,这应该不会很新鲜。它使用了浏览器原生的 RequestResponse 对象。

// pages/api/post-reactions/[id].ts
export const POST: APIRoute = async ({ request, params, clientAddress }) => {
  const id = params.id;
  const userIP = clientAddress;

  if (!import.meta.env.SECRET_APPWRITE_API_KEY) {
    return new Response(JSON.stringify({ error: "internal server error" }), {
      status: 500,
    });
  }

  if (!id) {
    return new Response(JSON.stringify({ error: "not found" }), {
      status: 404,
      statusText: "Not found",
      headers: headers,
    });
  }

  if (request.headers.get("Content-Type") === "application/json") {
    const body = await request.json();
    const reactionType: PostReactionOption = body.type;

    try {
      const result = await incrementEmojiReactionCount(id, reactionType);
      if (!result) throw new Error();
      let newReactions: PostReactions = {
        id: result.id,
        likes: result.likes,
        hearts: result.hearts,
        parties: result.parties,
        poops: result.poops,
      };

      return new Response(JSON.stringify(newReactions), {
        status: 200,
        headers: headers,
      });
    } catch (error) {
      console.log(`🚨 err when reacting to ID "${id}"!`, error);
    }
  }
  return new Response(null, { status: 400 });
};

此端点调用 incrementEmojiReactionsCount 函数,该函数执行对 Appwrite 的所有调用。

让我们也来看看 GET 方法。这个方法同样会检查是否存在 Appwrite 密钥,一个作为 GET 参数提供的 ID,然后返回最新的帖子反应数据:

// pages/api/post-reactions/[id].ts
export const GET: APIRoute = async ({
  params,
  clientAddress,
}): Promise<Response> => {
  const id = params.id;

  if (!import.meta.env.SECRET_APPWRITE_API_KEY) {
    return new Response(JSON.stringify({ error: "internal server error" }), {
      status: 500,
    });
  }
  if (!id) {
    return new Response(JSON.stringify({ error: "not found" }));
  }

  let postReactions = null;
  postReactions = await getPostReactionsById(id);
  if (!postReactions) {
    return new Response(JSON.stringify({ error: "not found" }));
  }

  const postReactionData: PostReactions = {
    id: postReactions.id,
    hearts: postReactions.hearts,
    likes: postReactions.likes,
    parties: postReactions.parties,
    poops: postReactions.poops,
  };

  headers.append("Content-Type", "application/json");
  return new Response(JSON.stringify(postReactionData), {
    status: 200,
    headers: headers,
  });
};

让我们看看与 Appwrite 的通信是如何工作的。这里有一个函数,负责确保你拥有 emoji 反应功能所需的正确数据库和数据库集合。它会检查你是否已经拥有它们,如果没有,它会通过 Appwrite 的 SDK 为你创建它们。

// appwrite.server.ts
import { Client, Databases } from "node-appwrite";

/** Setup */
export const appwriteServerClient = new Client()
  .setEndpoint(PUBLIC_APPWRITE_ENDPOINT)
  .setProject(PUBLIC_APPWRITE_PROJECT_ID)
  .setKey(env.SECRET_APPWRITE_API_KEY);
const appwriteDatabases = new Databases(appwriteServerClient);

const initializeEmojiReactionsCollection = async () => {
  try {
    await appwriteDatabases.get(PUBLIC_APPWRITE_DATABASE_ID);
    console.log("✅ Database found!");
  } catch (error) {
    await appwriteDatabases.create(
      PUBLIC_APPWRITE_DATABASE_ID,
      PUBLIC_APPWRITE_DATABASE_ID,
      true
    );
    console.log(
      `Created new Appwrite database: ${PUBLIC_APPWRITE_DATABASE_ID}`
    );
  }

  try {
    await appwriteDatabases.getCollection(
      PUBLIC_APPWRITE_DATABASE_ID,
      PUBLIC_APPWRITE_EMOJI_REACTIONS_COLLECTION_ID
    );
    console.log("✅ Emoji reactions collection found!");
  } catch (error) {
    await appwriteDatabases.createCollection(
      PUBLIC_APPWRITE_DATABASE_ID,
      PUBLIC_APPWRITE_EMOJI_REACTIONS_COLLECTION_ID,
      PUBLIC_APPWRITE_EMOJI_REACTIONS_COLLECTION_ID
    );
    console.log(
      `Created new Appwrite collection: ${PUBLIC_APPWRITE_EMOJI_REACTIONS_COLLECTION_ID}`
    );
  }
};

然后有一个函数运行前一个函数,以确保你拥有数据库和集合。

它检查已经添加到 Appwrite 的博客帖子,并将这些与你的 Astro 内容集合中的所有 markdown 文件进行比较。

任何尚未在 Appwrite 中的帖子都将以 0 个反应作为初始数据被添加。

而且,这会在你的 getStaticPaths 函数中运行,所以无论何时你在本地运行或者在管道中构建你的网站时,你的新帖子都会被添加到 Appwrite,因此你基本上可以忘记它,你不必手动管理它。

// appwrite.server.ts
export const tryInitNewBlogPostsReactionsInDatabaseCollection = async (
  posts: Array<CollectionEntry<"blog">>
) => {
  await initializeEmojiReactionsCollection();
  const dbPosts = (
    await appwriteDatabases.listDocuments<PostReactionsDocument>(
      PUBLIC_APPWRITE_DATABASE_ID,
      PUBLIC_APPWRITE_EMOJI_REACTIONS_COLLECTION_ID
    )
  ).documents;

  const newPosts: PostReactions[] = [];
  for (let i = 0; i < posts.length; i++) {
    const post = posts[i];
    if (!dbPosts.find(({ id }) => id === post.data.id)) {
      newPosts.push(getInitialPostData(post.data.id));
    }
  }

  if (newPosts.length > 0) {
    const promises = newPosts.map(newPost => {
      return appwriteDatabases.createDocument(
        PUBLIC_APPWRITE_DATABASE_ID,
        PUBLIC_APPWRITE_EMOJI_REACTIONS_COLLECTION_ID,
        ID.unique(),
        newPost
      );
    });

    Promise.all(promises).then(results => {
      console.log(`All new ${results.length} posts have been initialized`);
    });
  }
};

要获取特定帖子 ID 的帖子反应,我们需要在 Appwrite 中搜索带有该 ID 的文档列表:

// appwrite.server.ts
export const getPostReactionsById = async (id: string) => {
  try {
    const list = await appwriteDatabases.listDocuments<PostReactionsDocument>(
      PUBLIC_APPWRITE_DATABASE_ID,
      PUBLIC_APPWRITE_EMOJI_REACTIONS_COLLECTION_ID,
      [Query.equal("id", [id])]
    );
    const document = list.documents[0];
    return document;
  } catch (error) {
    if (error instanceof Error) {
      console.log(
        `Could not get post reaction data for "${id}"`,
        error.message
      );
    }
    return null;
  }
};

当你按下其中一个反应按钮时,为了更新帖子的反应,我们需要再次在 Appwrite 中获取最新数据。

那么你只需在刚刚点击的那个点赞按钮的最后计数上加 1,然后使用这个数据来更新 Appwrite 中的文档,以便数据被保存。

最终,返回的结果是 Appwrite 实际更新的数据,然后用这些数据来更新反应按钮中的数字。

export const incrementEmojiReactionCount = async (
  articleId: string,
  emojiType: PostReactionOption
) => {
  const list = await appwriteDatabases.listDocuments<PostReactionsDocument>(
    PUBLIC_APPWRITE_DATABASE_ID,
    PUBLIC_APPWRITE_EMOJI_REACTIONS_COLLECTION_ID,
    [Query.equal("id", [articleId])]
  );

  const document = list.documents[0];
  const prevCount = document[emojiType];
  const newCount = prevCount + 1;

  const result = await appwriteDatabases.updateDocument<PostReactionsDocument>(
    PUBLIC_APPWRITE_DATABASE_ID,
    PUBLIC_APPWRITE_EMOJI_REACTIONS_COLLECTION_ID,
    document.$id,
    {
      [emojiType]: newCount,
    }
  );

  return result;
};

然后还有一个功能。这个功能再次从 Appwrite 获取文章列表,但它还传递了一些排序标准,并将列表限制为仅 20 篇帖子。这个功能用于创建最受欢迎帖子的排行榜:

export const getPostReactionsRanked = async (): Promise<
  PostReactions[] | null
> => {
  try {
    const list = await appwriteDatabases.listDocuments<PostReactionsDocument>(
      PUBLIC_APPWRITE_DATABASE_ID,
      PUBLIC_APPWRITE_EMOJI_REACTIONS_COLLECTION_ID,
      [
        Query.orderDesc("likes"),
        Query.orderDesc("hearts"),
        Query.orderDesc("parties"),
        Query.orderDesc("poops"),
        Query.limit(20),
      ]
    );

    return list.documents.sort((postA, postB) => {
      // Array.sort() magic ✨
    });
  } catch (error) {
    if (error instanceof Error) {
      console.log(
        `Could not get post reaction data for ranking`,
        error.message
      );
    }
    return null;
  }
};

我简化了本文的一些代码。要获取代码的完整和最新版本,您可以在 GitHub 上查看 Engineering Blog 主题的代码

视图转换

让我们回到一个 Astro 特定功能:视图转换。它们是那些在页面之间转换时实现动画效果的精致动画。这可以是简单的淡入和淡出,或者是更复杂的效果。

默认情况下,Astro 中没有启用视图转换,但启用它们非常容易。

您需要做的,就是在 HTML 的 head 元素中添加 Astro 的标签。

然后你添加 transition:animate 到你想要动画化的元素上,再加上一个可选的过渡名称,这将帮助 Astro 理解在过渡之间应该替换哪些元素。

如果你不包括这个过渡名称,那么 Astro 将通过检查 DOM 中的位置来尝试自己弄清楚。

如果你不希望某些元素进行动画效果,你可以添加 transition:persist

这是一个如何启用视图转换的例子:

---
// layouts/your-layout.astro
import BaseHead from "../components/BaseHead.astro";
import Header from "../components/Header.astro";
import Footer from "../components/Footer.astro";
import { ViewTransitions } from "astro:transitions";
---

<html lang="en">
  <head>
    <BaseHead title={title} description={description} />
    <ViewTransitions />
  </head>

  <body>
    <Header transition:persist />
    <main transition:name="transition-content" transition:animate="slide">
      <slot />
    </main>
    <Footer />
  </body>
</html>

结论:你应该使用 Astro 吗?

我也这么认为。

Astro 是一个很棒的框架,正如我们讨论的,使用 Astro 有很多好处,所以如果你正在构建一个以内容为中心的网站,至少我认为它非常适合你的项目,你应该尝试一下。

有空去 astro.build 看看,试试看用它来做你的下一个项目。顺便也可以看看我做的工程博客模板

那篇文章很长。我希望它能给你一个关于 Astro 的好概览,我也希望你能在你的某个项目中尝试使用它。它已经成为我工具箱中的常用工具之一。