Fedify SearchK Main Navigation HomeInstallationCLI Tutorials Quick demo Learning the basics Creating a microblog Manual Federation Context Vocabulary Actor dispatcher Inbox listeners Sending activities Collections Object dispatcher Access control NodeInfo Pragmatics Key–value store Message queue Integration Testing Logging OpenTelemetry API referenceUnstable Appearance Menu On this page Sidebar Navigation What is Fedify? Installation CLI toolchain

Tutorials

Quick demo Learning the basics Creating a microblog

Manual

Federation Context Vocabulary Actor dispatcher Inbox listeners Sending activities Collections Object dispatcher Access control NodeInfo Pragmatics Key–value store Message queue Integration Testing Logging OpenTelemetry Examples Security policy Contribute Sponsors Changelog On this page

使用 Fedify 创建你自己的联邦宇宙微型博客

提示 本教程还提供以下语言版本:한국어 (韩语) 和 日本語 (日语)。

在本教程中,我们将构建一个小的 微型博客,它实现了 ActivityPub 协议,类似于 MastodonMisskey,并使用 Fedify(一个 ActivityPub 服务器框架)。本教程将更多地侧重于如何使用 Fedify,而不是理解其底层操作原理。

如果您有任何问题、建议或反馈,请随时加入我们的 Matrix 聊天空间Discord 服务器GitHub Discussions

目标读者

本教程面向那些想要学习 Fedify 并创建 ActivityPub 服务器软件的人。

我们假设您有使用 HTML 和 HTTP 创建 Web 应用程序的经验,并且您了解命令行界面、SQL、JSON 和基本 JavaScript。但是,您不需要了解 TypeScript、JSX、ActivityPub 或 Fedify,我们将逐步教您您需要了解的内容。

您不需要创建 ActivityPub 软件的经验,但我们确实假设您至少使用过一种 ActivityPub 软件,例如 Mastodon 或 Misskey。这样您就对我们尝试构建的内容有所了解。

目标

在本教程中,我们将使用 Fedify 创建一个单用户微型博客,该博客可以通过 ActivityPub 与其他联邦软件和服务进行通信。该软件将包括以下功能:

为了简化本教程,我们将施加以下功能约束:

当然,完成本教程后,欢迎您添加这些功能,这将是一个很好的练习!

完整的源代码可在 GitHub 存储库 中找到,提交根据每个实现步骤分开,供您参考。

设置开发环境

安装 Node.js

Fedify 支持三种 JavaScript 运行时:DenoBunNode.js。其中,Node.js 是使用最广泛的,因此我们将使用 Node.js 作为本教程的基础。

提示 JavaScript 运行时是一个执行 JavaScript 代码的平台。 Web 浏览器是一种 JavaScript 运行时,对于命令行或服务器使用,Node.js 广泛使用。最近,像 Cloudflare Workers 这样的云边缘功能也作为 JavaScript 运行时而广受欢迎。

要使用 Fedify,您需要 Node.js 20.0.0 或更高版本。有 各种安装方法,请选择最适合您的一种。

安装 Node.js 后,您将可以访问 nodenpm 命令:

node --version
npm --version

安装 fedify 命令

要设置 Fedify 项目,您需要在系统上安装 fedify 命令。有 几种安装方法,但使用 npm 命令是最简单的:

npm install -g @fedify/cli

安装后,检查是否可以使用 fedify 命令。您可以使用此命令检查 fedify 命令的版本:

fedify --version

确保版本号为 1.0.0 或更高版本。如果版本较旧,您将无法正确地遵循本教程。

使用 fedify init 初始化项目

要启动一个新的 Fedify 项目,让我们决定一个要在其中工作的目录路径。在本教程中,我们将它命名为 microblog。运行 fedify init 命令,后跟目录路径(即使目录尚不存在也可以):

fedify init microblog

运行 fedify init 命令时,您会看到一系列提示。依次选择 Node.jsnpmHonoIn-memoryIn-process

       ___   _____    _ _ __
      /'_')  | ___|__ __| (_)/ _|_  _
   .-^^^-/ /   | |_ / _ \/ _` | | |_| | | |
  __/    /    | _| __/ (_| | | _| |_| |
 <__.|_|-|_|    |_| \___|\__,_|_|_| \__, |
                      |___/
? Choose the JavaScript runtime to use
 Deno
 Bun
❯ Node.js
? Choose the package manager to use
❯ npm
 Yarn
 pnpm
? Choose the web framework to integrate Fedify with
 Bare-bones
 Fresh
❯ Hono
 Express
 Nitro
? Choose the key-value store to use for caching
❯ In-memory
 Redis
 PostgreSQL
 Deno KV
? Choose the message queue to use for background jobs
❯ In-process
 Redis
 PostgreSQL
 AMQP (e.g., RabbitMQ)
 Deno KV

注意 Fedify 不是一个全栈框架,而是一个专门用于实现 ActivityPub 服务器的框架。因此,它被设计为与其他 Web 框架一起使用。在本教程中,我们将采用 Hono 作为与 Fedify 一起使用的 Web 框架。

稍等片刻,您将在工作目录中看到创建的文件,其结构如下:

您可能已经猜到,我们使用的是 TypeScript 而不是 JavaScript,这就是为什么我们有 .ts.tsx 文件而不是 .js 文件。

生成的源代码是一个工作演示。让我们首先检查它是否正常运行:

npm run dev

此命令将使服务器保持运行状态,直到您按 Ctrl+C

Server started at http://0.0.0.0:8000

在服务器运行的情况下,打开一个新的终端选项卡并运行以下命令:

fedify lookup http://localhost:8000/users/john

此命令查询我们本地设置的 ActivityPub 服务器上的一个 actor (actor)。在 ActivityPub 中,actor 可以被认为是一个可以在各种 ActivityPub 服务器上访问的帐户。

如果看到这样的输出,则表示它工作正常:

✔ Looking up the object...
Person {
 id: URL "http://localhost:8000/users/john",
 name: "john",
 preferredUsername: "john"
}

此结果告诉我们,有一个 actor 对象位于 /users/john 路径上,它的类型为 Person,它的 ID 是 http://localhost:8000/users/john,它的名称是 john,它的用户名也是 john

提示 fedify lookup 是一个查询 ActivityPub 对象的命令。这相当于在 Mastodon 上使用相应的 URI 进行搜索。(当然,由于您的服务器目前只能在本地访问,因此在 Mastodon 上搜索还不会产生任何结果。)

如果您更喜欢 curl 而不是 fedify lookup 命令,您也可以使用此命令查询 actor(请注意,我们使用 -H 选项发送 Accept 标头):

curl -H"Accept: application/activity+json" http://localhost:8000/users/john

但是,如果像这样查询,结果将采用 JSON 格式,这很难用肉眼阅读。如果您的系统上也安装了 jq 命令,则可以一起使用 curljq

curl -H"Accept: application/activity+json" http://localhost:8000/users/john | jq .

Visual Studio Code

Visual Studio Code 可能不是您最喜欢的编辑器。但是,我们建议您在遵循本教程时使用 Visual Studio Code。这是因为我们需要使用 TypeScript,而 Visual Studio Code 目前是最方便、最出色的 TypeScript IDE。此外,生成的项目设置已经包含 Visual Studio Code 设置,因此您无需与格式化程序或 linter 作斗争。

警告 不要将此与 Visual Studio 混淆。 Visual Studio Code 和 Visual Studio 仅共享一个品牌名称;它们是完全不同的软件。

安装 Visual Studio Code 后,通过从菜单中选择 FileOpen Folder… 打开工作目录。

如果在右下方看到一个弹出窗口,询问“Do you want to install the recommended 'Biome' extension from biomejs for this repository?",单击 Install 按钮安装该扩展。安装此扩展将自动格式化您的 TypeScript 代码,因此您在编写 TypeScript 代码时无需与代码样式(如缩进或间距)作斗争。

提示 如果您是忠实的 Emacs 或 Vim 用户,我们不会阻止您使用您最喜欢的编辑器。但是,我们建议设置 TypeScript LSP。根据是否设置了 TypeScript LSP,生产力的差异非常显着。

前提条件

TypeScript

在开始修改代码之前,让我们简要介绍一下 TypeScript。如果您已经熟悉 TypeScript,则可以跳过本节。

TypeScript 将静态类型检查添加到 JavaScript。 TypeScript 语法与 JavaScript 几乎相同,但主要区别在于您可以为变量和函数指定类型。类型通过在变量或参数后添加冒号 (:) 来指定。

例如,以下代码指示 foo 变量是一个 string

let
foo
: string;

如果您尝试将不同类型的值分配给像这样声明的变量,Visual Studio Code 甚至会在您运行它之前显示红色下划线并显示类型错误:

foo= 123;
Type 'number' is not assignable to type 'string'.

编码时,不要忽略红色下划线。如果您忽略它们并运行程序,很可能会在该部分实际发生错误。

在 TypeScript 中编码时,您会遇到的最常见的错误类型是 null 可能性错误。例如,在以下代码中,bar 变量可以是 stringnull (string | null):

const
bar
: string | null =
someFunction
();

如果您尝试像这样获取此变量内容的第一个字符会发生什么?

const
firstChar
 =bar.
charAt
(0);
'bar' is possibly 'null'.

您会得到像上面这样的类型错误。它表示 bar 有时可能是 null,在这种情况下,调用 null.charAt(0) 会导致错误,因此您需要修复代码。在这种情况下,您需要像这样添加对 null 情况的处理:

const
firstChar
 =
bar
=== null ? "" :
bar
.
charAt
(0);

通过这种方式,TypeScript 通过让您考虑在编码时可能没有考虑到的情况来帮助防止错误。

TypeScript 的另一个附带优势是它可以实现自动完成。例如,如果您键入 foo.,则会显示字符串对象可用的方法列表,允许您从中选择。这允许更快地编码,而无需每次都检查文档。

我们希望您在遵循本教程时会感受到 TypeScript 的魅力。最重要的是,Fedify 在与 TypeScript 一起使用时提供了最佳体验。

提示 如果您想正确且彻底地学习 TypeScript,我们建议您阅读 The TypeScript Handbook。阅读完所有内容大约需要 30 分钟。

JSX

JSX 是 JavaScript 的一种语法扩展,允许您将 XML 或 HTML 插入到 JavaScript 代码中。它也可以在 TypeScript 中使用,在这种情况下,它有时称为 TSX。在本教程中,我们将使用 JSX 语法在 JavaScript 代码中编写所有 HTML。已经熟悉 JSX 的人可以跳过本节。

例如,以下代码将顶部带有 <div> 元素的 HTML 树分配给 html 变量:

const
html
 = <
div
>
 <
p
id
="greet">Hello, <
strong
>JSX</
strong
>!</
p
>
</
div
>;

您还可以使用大括号插入 JavaScript 表达式(当然,以下代码假定存在 getName() 函数):

const
html
 = <
div
title
={"Hello, " +
getName
() + "!"}>
 <
p
id
="greet">Hello, <
strong
>{
getName
()}</
strong
>!</
p
>
</
div
>;

JSX 的一个功能是您可以定义称为组件的自定义标签。组件可以定义为普通的 JavaScript 函数。例如,以下代码定义并使用 <Container> 组件(组件名称通常遵循 PascalCase 样式):

import type { 
Child
, 
FC
 } from "hono/jsx";
function
getName
() {
 return "JSX";
}
interfaceContainerProps {

name
: string;

children
:
Child
;
}
const
Container
:
FC
<ContainerProps> = (
props
) => {
 return <
div
title
={"Hello, " +
props
.
name
+ "!"}>{
props
.
children
}</
div
>;
};
const
html
 = <
Container
name
={
getName
()}>
 <
p
id
="greet">Hello, <
strong
>{
getName
()}</
strong
>!</
p
>
</
Container
>;

在上面的代码中,FC 由我们使用的 Web 框架 Hono 提供,它有助于定义组件的类型。 FC 是一种泛型类型,并且 FC 后的尖括号内的类型是类型参数。在这里,我们将 props 类型指定为类型参数。 Props 指的是传递给组件的参数。在上面的代码中,我们将 ContainerProps 接口声明并用作 <Container> 组件的 props 类型。

提示 泛型类型的类型参数可以有多个,用逗号分隔。例如,Foo<A, B> 将类型参数 AB 应用于泛型类型 Foo

还有泛型函数,写成 someFunction<A, B>(foo, bar)

当只有一个类型参数时,括住类型参数的尖括号可能看起来像 XML/HTML 标签,但它们与 JSX 功能无关。

FC<ContainerProps> 将类型参数 ContainerProps 应用于泛型类型 FC<Container> 打开一个名为 <Container> 的组件标签。必须用 </Container> 关闭。

在作为 props 传递的内容中,children 特别值得注意。这是因为组件的子元素作为 children prop 传递。因此,在上面的代码中,html 变量被分配 HTML 树 <div title="Hello, JSX!"><p id="greet">Hello, <strong>JSX</strong>!</p></div>

提示 JSX 是在 React 项目中发明的,并开始被广泛使用。如果您想了解更多关于 JSX 的信息,请阅读 React 文档的 Writing Markup with JSXJavaScript in JSX with Curly Braces 部分。

帐户创建页面

我们要创建的第一件事是帐户创建页面。我们需要创建一个帐户,然后才能发布或关注其他帐户。让我们从构建可见部分开始。

首先,让我们创建一个名为 src/views.tsx 的文件。在此文件中,我们将使用 JSX 定义一个 <Layout> 组件:

src/views.tsx

import type { 
FC
 } from "hono/jsx";
export const
Layout
:
FC
 = (
props
) => (
 <
html
lang
="en">
  <
head
>
   <
meta
charset
="utf-8" />
   <
meta
name
="viewport"
content
="width=device-width, initial-scale=1" />
   <
meta
name
="color-scheme"
content
="light dark" />
   <
title
>Microblog</
title
>
   <
link


rel
="stylesheet"

href
="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css"
   />
  </
head
>
  <
body
>
   <
main
class
="container">{
props
.children}</
main
>
  </
body
>
 </
html
>
);

为了避免在设计上花费太多时间,我们将使用一个名为 Pico CSS 的 CSS 框架。

提示 当 TypeScript 的类型检查器可以推断出变量或参数的类型时,例如上面的 props,可以省略类型注解。即使在这种情况下省略了类型注解,您也可以通过将鼠标光标悬停在 Visual Studio Code 中的变量名称上来检查变量的类型。

接下来,在同一个文件中,让我们定义一个将放置在布局内的 <SetupForm> 组件:

src/views.tsx

export const
SetupForm
:
FC
 = () => (
 <>
  <
h1
>Set up your microblog</
h1
>
  <
form
method
="post"
action
="/setup">
   <
fieldset
>
    <
label
>
     Username{" "}
     <
input


type
="text"

name
="username"

required


maxlength
={50}

pattern
="^[a-z0-9_\-]+$"
     />
    </
label
>
   </
fieldset
>
   <
input
type
="submit"
value
="Setup" />
  </
form
>
 </>
);

在 JSX 中,您只能有一个顶级元素,但 <SetupForm> 组件有两个顶级元素:<h1><form>。这就是我们用 <></> 包装它们的原因。这称为片段。

现在是时候使用我们定义的组件了。打开 src/app.tsx 文件并 import 我们刚刚定义的两个组件:

src/app.tsx

import { 
Layout
, 
SetupForm
 } from "./views.tsx";

然后,在我们刚刚创建的 /setup 页面上显示帐户创建表单:

src/app.tsx


app
.
get
("/setup", (
c
) =>

c
.
html
(
  <
Layout
>
   <
SetupForm
 />
  </
Layout
>,
 ),
);

现在,让我们在 Web 浏览器中打开 http://localhost:8000/setup 页面。如果您看到这样的屏幕,则表示它工作正常:

帐户创建页面

注意 要使用 JSX,源文件扩展名必须是 .jsx.tsx。请注意,我们在本节中编辑的两个文件都具有 .tsx 扩展名。

数据库设置

现在我们已经实现了可见部分,是时候实现功能了。我们需要一个存储帐户信息的地方,因此让我们使用 SQLite。 SQLite 是一个适用于小型应用程序的关系数据库。

首先,让我们声明一个用于保存帐户信息的表。从现在开始,我们将所有表声明都写在 src/schema.sql 文件中。我们将帐户信息存储在 users 表中:

src/schema.sql

CREATE TABLE IF NOT EXISTS users (
 id    INTEGER NOT NULL PRIMARY KEY CHECK (id = 1),
 username TEXT  NOT NULL UNIQUE   CHECK (trim(lower(username)) = username
                        AND username <> ''
                        AND length(username) <= 50)
);

由于我们创建的微型博客只能有一个帐户,因此我们对 id 列(主键)进行了约束,不允许除 1 以外的值。这确保了 users 表不能包含多个记录。我们还对 username 列进行了约束,不允许空字符串或太长的字符串。

现在我们需要运行 src/schema.sql 文件来创建 users 表。为此,我们需要 sqlite3 命令,您可以 [从 SQLite 网站获取](https://fedify.