Astro 和 Redis 入门

Astro 是一种针对构建快速 内容网站而优化的新工具。虽然它是新的,但团队已经发布了 1.0 版本,并且 API 现在已经稳定。主要卖点是默认情况下零 JavaScript 以及“自带框架”。我们将在这里使用 Svelte 来帮助我们开始使用 Astro 和 Redis。那就是用无服务器数据库构建一个基本的笔记应用程序。尽管我们使用 Svelte,但您应该能够在没有事先 Svelte 知识的情况下进行操作。也就是说,如果您愿意,您可以稍后将组件换成React、Vue 或其他支持的框架。

我们将严重依赖浏览器 API,事实上,仅在单个组件中提供 JavaScript。我们将在 Cloudflare 上托管应用程序并使用 Workers,但稍后会介绍。

要开始使用 Astro 和 Redis,请在本地克隆起始代码模板:

pnpm create astro -- --template rodneylab/upstash-astro

进入全屏模式 退出全屏模式

出现提示时,接受依赖项的安装并初始化 git 存储库。选择推荐的 TypeScript 选项。将目录更改为新项目文件夹。默认情况下没有框架支持,我们添加了一个集成以便能够使用 Svelte:

pnpm astro add svelte

进入全屏模式 退出全屏模式

如果您从提示中选择默认选项,Svelte 设置是自动的。接下来,要启动开发服务器,请运行pnpm dev命令。 CLI 将为您提供 URL(类似于http://localhost:3000)。在浏览器中打开 URL。

开始使用 Astro 和 Redis:屏幕截图显示标题 Upstash Astro 笔记,下方有一个空白视图,笔记将转到

天文路由

我们来看看src/pages/index.astro。这是http://localhost:3000/主页的 Astro 标记。 Astro 使用基于文件的路由。这意味着我们在src/pages目录中创建的 Astro 文件将输出到具有匹配路径的 HTML 页面。我们还将在src/pages/api.tsTypeScript 中编写一个 API 端点。 API 文件也遵循基于文件的路由模式,因此我们将在http://localhost:3000/api访问该端点。

您可能会注意到我们在浏览器中有一个 favicon,但在index.astro标记中没有link标记。那是因为我们使用的是src/layouts/Layout.astro的布局文件。在index.astro中,我们将页面主要内容包装在Layout组件中。这种模式为我们节省了在大型项目中重复的布局代码。

src/components文件夹中,我们有 Svelte 组件。我们将能够将这些导入到index.astro中,就像我们对 Layout 组件所做的那样。

开始使用 Astro 和 Redis:Astro 文件

Astro 文件有两个部分。第一部分,script,是我们可以添加任何 JavaScript 或 TypeScript 逻辑的地方。我们将此部分包装在“---”分隔符中。第二部分是模板,看起来很像 HTML。如果您已经了解 JSX,您会很熟悉。

一些现代 Astro 功能包括:

  • 开箱即用的 TypeScript 支持,

  • 部分补水

  • 顶级等待(在.astro文件中)。

初始注释

让我们在index.astro的脚本部分定义一些注释,然后将它们传递给模板部分的 Svelte 组件。

更新src/pages/index.astro中的代码(您可以将style标记留在底部,因为它在整个教程中都是如此):

---
import HeadingBar from '~components/HeadingBar.svelte';
import NoteBody from '~components/NoteBody.svelte';
import NotesList from '~components/NotesList.svelte';
import Layout from '~layouts/Layout.astro';
import type { Note } from '~types/note';

const editMode = false;

const notes: Note[] = [
    {
        id: '1',
        title: 'Very first note',
        text: 'First note’s text',
        modified: new Date().toISOString(),
    },
    {
        id: '2',
        title: 'Another note',
        text: 'This note’s text',
        modified: new Date().toISOString(),
    },
];
const selectedNote = notes[0];

const title = 'Upstash Astro Notes';
---

<Layout title={title}>
    <main class="wrapper">
        <h1>{title}</h1>
        <div class="container">
            <header class="heading">
                {selectedNote ? <HeadingBar note={selectedNote} {editMode} /> : null}
            </header>
            <aside class="list">
                <NotesList client:load {notes} selectedId={selectedNote?.id} {editMode} />
            </aside>
            <section class="note">
                <NoteBody note={selectedNote} />
            </section>
        </div>
    </main>
</Layout>

进入全屏模式 退出全屏模式

您应该会看到显示的第一个注释。目前还不能做很多其他事情,我们稍后会设置数据库。现在,这是您完成的第一个 Astro 代码!我们看到了一些 JSX 的影响:

{selectedNote ? <HeadingBar note={selectedNote} {editMode} /> : null}

进入全屏模式 退出全屏模式

这里我们使用 JavaScript 三元运算符来检查是否存在selectedNote,如果没有则不渲染。注意我们如何使用HeadingBar组件,传入 props。这包括一个现代快捷方式,让我们可以使用速记{editMode},我们可以在其中写出editMode={editMode}

我们之前提到,Astro 默认发布零 JavaScript。事实上,我们刚刚添加的HeadingBar组件遵循这个默认值。当我们确实希望 JavaScript 在组件中运行时,我们可以通过在其属性中包含client:load指令来启用它(就像在NotesList组件上一样)。检查Astro 文档以获取其他指令,例如水合物一旦可见。接下来我们将启动我们的 Redis 数据库,这样我们就可以摆脱静态的手动注释。

Astro 和 Redis 入门:无服务器 Redis

我们将使用 Upstash 来提供我们的 Redis 数据库。创建一个 Upstash 帐户如果您还没有,则只需登录即可。从控制台创建一个新数据库。我们需要来自控制台的 Astro 项目的两个环境变量。向下滚动到数据库_详细信息_页面的_REST API_部分。我们需要:

  • UPSTASH_REDIS_REST_URL

  • UPSTASH_REDIS_REST_TOKEN

开始使用 Astro 和 Redis:屏幕截图显示了带有 REST API 参数部分的 .env 选项卡的 Astro 控制台。 API 密钥显示在此部分

将项目根目录中的.env.EXAMPLE文件重命名为.env并在其中添加这些凭据。.env包含在项目.gitignore中,以避免意外提交这些值。

现在我们已经设置好了,我们可以创建我们的 API 路由,将我们的应用程序链接到无服务器 Redis 数据库。

API 路由

我们将尽量减少 JavaSript 的使用并严重依赖 HTML。为了实现这一点,在主要部分,我们将使用HTML 表单元素actionmethod属性来启动 createupdatedelete 过程。要阅读可用笔记列表,我们使用 HTTP GET 请求。这些过程都从index.astro或 Svelte 组件文件代码开始。

src/pages/api.ts文件将处理所有这些,最终来自 Cloudflare Worker。从那里我们联系到 Upstash 无服务器数据库以完成 CRUD(创建、读取、更新和删除)操作。让我们仔细看看文件。所以,这个文件处理发送到http://localhost:3000/api的 HTTP 请求。我们正在使用@upstash/redis包来连接我们的远程数据库。我们导入它,然后在顶部配置 Redis 实例:

import { Redis } from "@upstash/redis";

/* TRUNCATED */

const HASHSET_KEY = "notes";
const url = import.meta.env.UPSTASH_REDIS_REST_URL;
const token = import.meta.env.UPSTASH_REDIS_REST_TOKEN;

const redis = new Redis({ url, token });

进入全屏模式 退出全屏模式

要访问 Astro 中的秘密环境变量,我们使用import.meta.env

HTTP 请求

如您所料,getput方法(进一步向下)中的代码响应端点接收的 HTTP GET 和 PUT 请求。这些函数可以访问输入的 HTTP 请求并返回一个响应

我们设置了我们的笔记创建过程,以便新笔记的标题自动设置为Untitled和一个空的正文。然后用户可以从浏览器进行更新。我们将使用查询参数将浏览器重定向到注释编辑表单。那是我们创建了骨架注释之后。这是src/components/NotesList.svelte中的代码,它从浏览器启动注释 create 工作流:

<form action="/api" method="post">
  <input type="hidden" name="action" value="create" />
  <button>[ new ]</button>
</form>

进入全屏模式 退出全屏模式

该表单包含上面端点代码中使用的隐藏action字段。这里的action是create,后面我们会用到updatedelete(其他形式)。本质上,我们的put函数使用标准 API 获取操作和请求 URL。如果动作是create,我们在 Hashset 数据结构 中做一个新的注释。这是一个Redis 结构,非常适合我们的用例。我们使用当前时间戳(作为自 ECMAScript 纪元以来的毫秒数)作为元素id。这适用于我们的简单应用程序。redis.hset调用负责为我们将新便笺放入数据库:

const date = new Date();
const id = date.getTime();

/* TRUNCATED */

await redis.hset(HASHSET_KEY, {
    [id]: JSON.stringify(note),
});
urlParams.append("edit", "true");
urlParams.append("note", id.toString(10));

/* TRUNCATED */

return Response.redirect(`${redirectURL}?${urlParams.toString()}`);

进入全屏模式 退出全屏模式

最后,请注意我们如何在重定向 URL 的末尾添加editnote查询参数。我们将更新前端代码以使用这些。

完整API

目前我们只能创建一个骨架注释,甚至不能编辑它,所以这里是 API 的完整代码。使用完整功能更新src/pages/api.ts:

import { Redis } from "@upstash/redis/cloudflare";
import type { APIRoute } from "astro";
import type { Note } from "../types/note";
import { getDomainUrl } from "../utilities/utilities";

const HASHSET_KEY = "notes";
const url = import.meta.env.UPSTASH_REDIS_REST_URL;
const token = import.meta.env.UPSTASH_REDIS_REST_TOKEN;

const redis = new Redis({ url, token });

export const get: APIRoute = async function get() {
    try {
        const notes: Record<string, Note> | null = await redis.hgetall(HASHSET_KEY);
        /* notes (when not null) has structure:
                {
                    '1660841122914': { // this is the note id
                        title: 'First One',
                        text: 'First text',
                        modified: '2022-08-18T16:45:22.914Z'
                    },
                    '1660843285978': {
                        title: 'Second one',
                        text: 'Hi',
                        modified: '2022-08-18T17:21:25.978Z'
                    }
                }
        */

        if (notes) {
            const sortedNotes: Note[] = Object.entries(notes)
                .map(([id, { title, text, modified }]) => ({
                    id,
                    title,
                    text,
                    modified,
                }))
                .sort((a, b) => Date.parse(b.modified) - Date.parse(a.modified));

            return new Response(JSON.stringify({ notes: sortedNotes }), {
                headers: { "content-type": "application/json" },
                status: 200,
            });
        }

        return new Response(JSON.stringify({ notes: [] }), {
            headers: { "content-type": "application/json" },
            status: 200,
        });
    } catch (error: unknown) {
        console.error(`Error in /api GET method: ${error as string}`);
        return new Response(JSON.stringify({ notes: [] }), {
            headers: { "content-type": "application/json" },
            status: 200,
        });
    }
};

export const post: APIRoute = async function post({ request }) {
    try {
        const form = await request.formData();
        const action = form.get("action");
        const redirectURL: string = getDomainUrl(request);
        const urlParams = new URLSearchParams();

        switch (action) {
            case "create": {
                const date = new Date();
                const id = date.getTime();
                const modified = date.toISOString();
                const note = {
                    title: "Untitled",
                    text: "",
                    modified,
                };
                await redis.hset(HASHSET_KEY, {
                    [id]: JSON.stringify(note),
                });
                urlParams.append("edit", "true");
                urlParams.append("note", id.toString(10));

                break;
            }
            case "update": {
                const id = form.get("id") as string;
                const title = form.get("title");
                const text = form.get("text");
                const modified = new Date().toISOString();

                await redis.hset(HASHSET_KEY, {
                    [id]: JSON.stringify({ title, text, modified }),
                });
                urlParams.append("note", id);
                break;
            }
            case "delete": {
                const id = form.get("id");
                if (typeof id === "string") {
                    await redis.hdel(HASHSET_KEY, id);
                }
                break;
            }
            default:
        }
        return Response.redirect(`${redirectURL}?${urlParams.toString()}`);
    } catch (error: unknown) {
        console.error(`Error in /api PUT method: ${error as string}`);
        return Response.redirect(getDomainUrl(request));
    }
};

进入全屏模式 退出全屏模式

所以我们使用的主要 Upstash Redis 命令是:

  • 创建:await redis.hset(HASHSET_KEY, { [id]: JSON.stringify({ title, text, modified}) });,

  • 阅读:await redis.hgetall(HASHSET_KEY);,

  • 更新:await redis.hset(HASHSET_KEY, { [id]: JSON.stringify({ title, text, modified}) });,

  • 删除:await redis.hdel(HASHSET_KEY, id);

访问 HTTP 头

默认情况下,Astro 是一个静态站点生成器 (SSG),我们希望访问服务器端渲染 (SSR),因此我们始终显示数据库中的最新注释。稍后我们将添加一个 Cloudflare 适配器,这样做会自动将我们从 SSG 切换到 SSR。同时,我们希望访问请求标头,这是我们在下一节中使用的getDomainUrl函数。要切换到 SSR,请更新astro.config.js文件以包含output: 'server',然后重新启动您的开发服务器:

import { defineConfig } from "astro/config";

import svelte from "@astrojs/svelte";

// https://astro.build/config
export default defineConfig({
    output: "server",
    integrations: [svelte()],
});

进入全屏模式 退出全屏模式

前端:创建和更新

好的,我们现在正在取得一些进展。接下来我们要更新index.astro文件以读取 URL 中的查询参数,然后将用户引导到正确的视图。一旦我们把它连接起来,我们将创建和编辑一些新的笔记。

Astro 的Astro.urlAstro.requestAPI 让我们可以访问搜索参数,还可以帮助我们确保将 GET 和 PUT 请求发送到正确的 URL。更新index.astro的脚本部分:

---
import EditNote from '~components/EditNote.svelte';
import HeadingBar from '~components/HeadingBar.svelte';
import NoteBody from '~components/NoteBody.svelte';
import NotesList from '~components/NotesList.svelte';
import Layout from '~layouts/Layout.astro';
import type { Note } from '~types/note';
import { getDomainUrl } from '~utilities/utilities';

const { request, url } = Astro;
const domainUrl = getDomainUrl(request);
const { searchParams } = url;

const selectedId = searchParams.get('note');
const editMode = searchParams.get('edit') === 'true';

const response = await fetch(`${domainUrl}/api`, { method: 'GET' });
const { notes }: { notes: Note[] } = await response.json();
const selectedNote = notes.find(({ id }) => id === selectedId) ?? notes[0];

const title = 'Upstash Astro Notes';
---

进入全屏模式 退出全屏模式

searchParams是来自浏览器 API](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams)的[URLSearchParams 对象。您可以看到我们还使用 fetch Browser API 将 get 请求发送到我们的端点并拉入笔记列表(当前为空)。 Astro 大量使用浏览器标准。

getDomainUrl是一个实用函数。我们在src/utilities/utilities中定义。这将暂时返回http://localhost:3000,类似于http://localhost:8788,当我们稍后使用 Cloudflare Wrangler 工具在本地运行站点时,当我们将站点部署到 Web 时,最后返回https://example.com

作为最后一步,更新index.astro中的模板代码:

<Layout title={title}>
  <main class="wrapper">
    <h1>{title}</h1>
    <div class="container">
      <header class="heading">
        {selectedNote ? <HeadingBar client:load note={selectedNote} {editMode} /> : null}
      </header>
      <aside class="list">
        <NotesList client:load {notes} {selectedId} {editMode} />
      </aside>
      <section class="note">
        {editMode && selectedNote ? <EditNote client:load note={selectedNote} /> : null}
        {!editMode && selectedNote ? <NoteBody note={selectedNote} /> : null}
      </section>
    </div>
  </main>
</Layout>

进入全屏模式 退出全屏模式

在这里,我们使用从 URL 中提取的editMode参数。

开始使用 Astro 和 Redis:测试它

[ new ]按钮创建一个新笔记。检查浏览器地址栏中的 URL。我有http://localhost:3000/?edit=true&note=1661428055833。按下按钮,使用create操作调用 APIput函数。这在 Upstash Redis 数据库中创建了一个新注释,然后将浏览器重定向到该 URL。index.astro中的逻辑提取了edit=true查询参数,因此显示了EditNote组件,而不是NoteBody。我们主要依靠标准 API 完成所有这些工作,这难道不是很不可思议吗?

点击Save Changes。我们在这里使用的表单与创建表单的工作方式类似,不过这次表单代码在src/components/EditNote.svelte中。

保存第一个便笺后尝试创建另一个便笺。这次点击Cancel,而不是保存。这里我们使用了一段 JavaScript,这就是为什么我们在index.astro中的EditNote中添加了client:load指令。对于取消按钮,我们使用preventDefault并运行这个 handleCancel 函数:

function handleCancel() {
    const searchParams = new URLSearchParams({ note: id });
    window.location.replace(`/?${searchParams}`);
}

进入全屏模式 退出全屏模式

这会将用户推回到主页,而不是提交对 Redis 的更改。这表明我们有一个 JavaScript “逃生舱”,可以在我们确实需要一些交互性的时候使用。选择您刚刚创建的未编辑的笔记,然后点击[ delete ]按钮。其形式在HeadingBar组件中。

开始使用 Astro 和 Redis:屏幕截图显示标题 Upstash Astro 笔记,左侧有笔记列表,从中选择的笔记显示在主视图中

构建和部署

Astro 为主要的云托管提供商构建了适配器。添加 Cloudflare Workers 适配器:

pnpm astro add cloudflare

进入全屏模式 退出全屏模式

接受 defaults 以自动配置它。现在我们可以构建站点:

pnpm run build

进入全屏模式 退出全屏模式

如果这是您第一次在您的机器上使用 Wrangler,您需要通过运行pnpm wrangler login命令将本地实例链接到您的 Cloudflare 帐户。查看牧马人入门指南以获取完整的详细信息。wranglerCLI 工具包含在package.json项目中。

为了预览站点,由于 Cloudflare 工作人员与其他环境略有不同,我们更改了默认命令。我把这个作为preview:cloudlfare脚本保存在package.json中,所以运行pnpm preview:cloudflare。要查看该站点,请在浏览器中访问http://127.0.0.1:8788

查看项目的dist文件夹。这是 Astro 输出您的生产站点的地方。里面会有一个_worker.js。这是 Astro 自动为您生成的 Cloudflare Worker。我们在 repo 中包含的wrangler.toml文件中添加了路径。

部署到 Cloudflare

在 Cloudflare Worker 运行时支持读取秘密环境变量有一个小的变化。当 Astro 团队为开箱即用的支持找到永久解决方案时,这是一种解决方法。更新astro.config.js:

/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import svelte from "@astrojs/svelte";
import { defineConfig } from "astro/config";

import cloudflare from "@astrojs/cloudflare";

// https://astro.build/config
export default defineConfig({
    output: "server",
    integrations: [svelte()],
    adapter: cloudflare(),
    vite: {
        define: {
            "process.env.UPSTASH_REDIS_REST_URL": JSON.stringify(
                process.env.UPSTASH_REDIS_REST_URL,
            ),
            "process.env.UPSTASH_REDIS_REST_TOKEN": JSON.stringify(
                process.env.UPSTASH_REDIS_REST_TOKEN,
            ),
        },
    },
});

进入全屏模式 退出全屏模式

这些行使我们的秘密环境变量在 Cloudflare 服务器将要查找它们的位置准确可用。最后,要设置远程构建环境使用 Node 16,在项目根文件夹中创建一个.nvmrc文件,并将内容设置为16

我们现在准备部署。为了让网站上线,提交并将 repo 推送到 git 服务。接下来登录到您的 Cloudflare 帐户并选择 Pages 并 Create a Project / Connect to Git。链接到您的 git 服务。选择 Astro 作为框架预设。最后,记得在点击Save and Deploy之前添加两个环境变量。

Astro 和 Redis 入门:总结

这就是我现在想向您展示的全部内容。本教程仅演示了我们可以使用 Upstash 和 Astro 做的一小部分,但我希望它足以让您开始使用 Astro 和 Redis。按照上面的链接来探索我们提到的功能的详细信息。查看Upstash 博客以获取更多教程并且您可以从 Rodney Lab了解更多 Astro。告诉我您是如何找到本教程的以及您的下一个项目将是什么!

Logo

Redis社区为您提供最前沿的新闻资讯和知识内容

更多推荐