开始使用Astro和Redis
#serverless #redis #cloudflare #astro

开始使用Astro和Redis

Astro是一种针对构建快速内容网站的新工具。尽管它是新的,但该团队已经发布了1.0版,并且API现在稳定。关键的卖点为零JavaScript 默认情况下以及带上自己的框架。我们将在这里使用Svelte来帮助我们开始使用Astro和Redis。那是使用无服务器数据库构建基本笔记应用程序。尽管我们使用Svelte,但您应该能够在没有事先苗条的知识的情况下跟随。也就是说,如果您喜欢的话,您可以稍后将组件换成React, Vue or other supported frameworks

我们将大力依靠浏览器API,实际上仅在单个组件中运送JavaScript。我们将在CloudFlare上托管该应用程序,并使用工人,但稍后会介入。

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

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

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

pnpm astro add svelte

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

Get started with Astro and Redis: Screen capture shows title Upstash Astro Notes, with an empty view below where notes will go

Astro路线

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

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

src/components文件夹中,我们有我们的Svelte组件。我们将能够像使用布局组件一样将它们导入index.astro

开始使用Astro和Redis:Astro文件

Astro文件有两个部分。第一个,脚本,部分是我们可以添加任何JavaScript或Typescript逻辑的地方。我们将此部分包裹在---系数中。第二部分是模板,看起来很像HTML。您已经知道JSX很熟悉。

一些现代的Astro功能是:

  • 开箱即用打字稿支持,
  • partial hydration
  • 顶级等待(在.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组件,传递道具。这包括现代快捷方式,让我们使用速记{editMode},我们本可以写出editMode={editMode}

我们前面提到的是,Astro默认情况下将零JavaScript运送。实际上,我们刚刚添加的HeadingBar组件遵循此默认值。当我们确实希望JavaScript在组件中运行时,我们可以通过在其属性中包含client:load指令(例如在NotesList组件上)来启用它。可见一次检查Astro docs for other directives像水合物一样。接下来,我们将启动REDIS数据库,以便我们可以摆脱静态的手动评论。

开始使用Astro和Redis:无服务器Redis

我们将使用UpStash来提供我们的REDIS数据库。 Create an Upstash account如果您还没有一个,否则只需登录即可。从控制台创建一个新数据库。我们需要从控制台上的Astro项目进行两个环境变量。向下滚动到数据库详细信息页面的 REST API 部分。我们需要:

  • upstash_redis_rest_url
  • upstash_redis_rest_token

Get started with Astro and Redis: Screen capture shows Astro console with .env tab of REST API parameters section. API keys are displayed in this section

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

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

API路线

我们将最大程度地减少Javasript的使用,并严重倾斜HTML。为此,在主要部分中,我们将使用HTML form element actionmethod属性来启动 create update delete 流程。 阅读可用的注释列表,我们使用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并提出请求。这些功能可以访问输入HTTP请求,然后返回响应

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

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

该表单包含上面端点代码中使用的隐藏的action字段。在这里,动作是create,我们将在以后使用updatedelete(以其他形式)。从本质上讲,我们的put功能可以使用标准API进行操作并请求URL。如果动作是create,我们在标签数据结构中提出了新注释。这是一个Redis structure which fits our use case。我们将当前的时间戳(作为number of milliseconds since ECMAScript epoch)用作元素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));
    }
};

因此,我们使用的主要升级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}) });
  • delete await redis.hdel(HASHSET_KEY, id);

访问HTTP标头

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

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.request API,让我们访问搜索参数,还可以帮助我们确保将请求发送到正确的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';
---

searchParamsURLSearchParams object from the Browser API。您可以看到我们还使用获取浏览器API将GET请求发送到我们的端点,并吸入注释列表(当前为空)。 Astro大量使用浏览器标准。

getDomainUrl是实用程序函数。我们在src/utilities/utilities中定义它。现在将返回http://localhost:3000,类似http://localhost:8788,当时我们以后使用Cloudflare Wrangler工具本地运行该站点,最后,https://example.com当我们将网站部署到Web时。

作为最后一步,在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操作调用API put功能。这在Upstash Redis数据库中创建了一个新注释,然后将浏览器重定向到此URL。 index.astro中的逻辑拾取了edit=true查询参数,因此显示了EditNote组件,而不是NoteBody。难以置信的是,我们做了所有这些主要依靠标准API的所有操作吗?

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

一旦保存第一个便条,请尝试创建另一个注释。这次单击Cancel,而不是保存。在这里,我们使用JavaScript的位置,这就是为什么我们在index.astro中将client:load指令添加到EditNote的原因。对于“取消按钮”,我们使用preventDefault并运行此handlecancel函数:

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

将用户推回主页,而不是对Redis进行更改。这表明我们有一个JavaScript“逃生舱口”,每当我们确实需要一些互动性时。选择您刚刚创建的注释而无需编辑并点击[ delete ]按钮。它的形式在HeadingBar组件中。

Get started with Astro and Redis: Screen capture shows title Upstash Astro Notes, with notes list on the left and the selected note from this this displayed in the main view

构建和部署

Astro为主要的云托管提供商建造了适配器。添加CloudFlare工人适配器:

pnpm astro add cloudflare

接受默认值以自动配置。现在我们可以构建网站:

pnpm run build

如果这是您第一次在计算机上使用Wrangler,则需要通过运行pnpm wrangler login命令将本地实例链接到Cloudflare帐户。检查Wrangler get started guide for full detailswrangler CLI工具包含在项目package.json中。

要预览该站点,因为CloudFlare工人与其他环境有所不同,我们会更改默认命令。我将其保存为package.json中的preview:cloudlfare脚本,因此运行pnpm preview:cloudflare。要查看网站,请转到浏览器中的http://127.0.0.1:8788

查看项目的dist文件夹。这是Astro输出您的生产地点的地方。那里会有一个_worker.js。这是Astro自动为您生成的Cloudflare Worker。我们在库中包含的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服务器将在哪里寻找它们。最后,要将远程构建环境设置为使用节点16,请在项目root文件夹中创建一个.nvmrc文件,然后将内容设置为be 16

我们现在准备部署。要使网站进行直播,请提交并将存储库推向GIT服务。下一个log into your Cloudflare account并选择页面,然后创建一个项目 /连接到git < / strong>。链接到您的git服务。选择 Astro 作为框架预设。最后,请记住在单击保存和部署之前添加两个环境变量。

开始使用Astro和Redis:结束

这就是我现在想向您展示的一切。该教程仅展示了我们可以使用Upstash和Astro所能做的一小部分,但我希望您足以让您开始使用Astro和Redis。请按照上述链接探讨我们提到的功能的细节。检查Upstash blog for more tutorials,您可以learn more Astro from Rodney LabLet me know how you found this tutorial以及您的下一个项目将是什么!