Serverpod

Serverpod 是一个下一代应用和 Web 服务器,专为 Flutter 和 Dart 生态系统构建。它允许您使用 Dart 编写服务器端代码,自动生成 API,并轻松连接数据库。Serverpod 是开源的,您可以在任何地方托管您的服务器。

这是 Serverpod 的早期版本。API 是稳定的,并且已被多个项目在生产中使用,但未来更新中可能会有一些小的更改。一些功能仍缺失,但将包含在 1.0 版本中。有关正在开发的功能,请参阅下面的 路线图

Serverpod 中的每一个设计决策都旨在最大限度地减少需要编写的代码量,并使其尽可能易于阅读。除了仅仅作为服务器之外,Serverpod 还集成了许多常见的任务,这些任务否则很难实现或需要外部服务。

  • Serverpod 将通过分析您的服务器来自动生成协议和客户端代码。调用远程端点就像进行本地方法调用一样简单——不再需要解析 REST API 响应。
  • 连接数据库从未如此简单。数据库行由类型化的 Dart 类表示,甚至可以序列化并直接发送到客户端。
  • 需要执行一项任务在未来的特定时间?Serverpod 支持未来的调用,即使您需要重新启动服务器,这些调用也是持久的。
  • 缓存和扩展变得简单。常用对象可以本地缓存在单个服务器上,或者使用 Redis 分布在服务器集群中。

安装 Serverpod

Serverpod 在 Mac 和 Linux 上进行了测试(推荐 Mac),Windows 支持正在开发中。在安装 Serverpod 之前,您需要安装以下工具

在安装和配置好 Flutter 和 Docker 后,打开终端并运行以下命令安装 Serverpod

dart pub global activate serverpod_cli

现在通过运行以下命令测试安装

serverpod

如果一切配置正确,现在应该会显示 serverpod 命令的帮助信息。

创建您的第一个项目

要启动并运行本地服务器,您需要创建一个新的 Serverpod 项目。通过运行 serverpod create 来创建新项目。

serverpod create mypod

此命令将创建一个名为 mypod 的新目录,其中包含三个 Dart 包;mypod_servermypod_clientmypod_flutter

  • mypod_server:此包包含您的服务器端代码。修改它以添加新端点或服务器所需的其他功能。
  • mypod_client:这是与服务器通信所需的代码。通常,此包中的所有代码都是自动生成的,您不应编辑此包中的文件。
  • mypod_flutter:这是 Flutter 应用,已预先配置为连接到您的本地服务器。

启动 Postgres 和 Redis

Serverpod 服务器需要访问 Postgres 数据库和 Redis 才能运行。在运行 serverpod create 时,数据库已配置了默认的表集。要启动 Postgres 和 Redis,请 cd 进入 mypod_server,然后运行。

docker-compose up -d --build

这将构建并启动一组 Docker 容器,并提供对 Postgres 和 Redis 的访问。如果您以后需要停止容器,只需运行 docker-compose stop

启动服务器

现在您应该准备好运行您的服务器了。通过进入 mypod_server 目录并输入以下命令来启动它

dart bin/main.dart

如果一切正常,您应该在终端上看到类似以下内容

Mode: development
Config loaded from: config/development.yaml
api port: 8080
service port: 8081
postgres host: localhost
postgres port: 8090
postgres name: serverpod_test
postgres user: postgres
postgres pass: ********
redis host: localhost
redis port: 8091
redis pass: ********
Insights listening on port 8081
Server id 0 listening on port 8080

处理端点

端点是客户端连接到服务器的连接点。使用 Serverpod,您可以向端点添加方法,并且您的客户端代码将自动生成以进行方法调用。为了生成代码,您需要将您的端点放在服务器的端点目录中。您的端点应该继承 Endpoint 类。为了生成方法,它们需要返回一个类型化的 Future,并且其第一个参数应该是 Session 对象。Session 对象包含有关服务器调用信息,并提供数据库访问。

import 'package:serverpod/serverpod.dart';

class ExampleEndpoint extends Endpoint {
  Future<String> hello(Session session, String name) async {
    return 'Hello $name';
  }
}

上面的代码将创建一个名为 example 的端点(将删除 Endpoint 后缀),其中包含单个 hello 方法。要生成服务器端代码,请在服务器的主目录中运行 serverpod generate

在客户端,您现在可以通过调用以下方式调用该方法

var result = await client.example.hello('World');

传递参数

端点方法的实现有一些限制。目前命名参数尚不支持。参数和返回类型可以是 boolintdoubleStringDateTime,或生成的序列化对象(参见下一节)。应始终返回类型化的 Future。支持空安全。传递 DateTime 时,它始终转换为 UTC。

生成可序列化类

Serverpod 可以轻松生成可序列化类,这些类可以在服务器和客户端之间传递,或用于连接数据库。类的结构定义在协议目录的 yaml 文件中。运行 serverpod generate 来构建类的 Dart 代码,使其对服务器和客户端都可用。

这是一个定义可序列化类的 yaml 文件的简单示例

class: Company
fields:
  name: String
  foundedDate: DateTime?
  employees: List<Employee>

支持的类型是 boolintdoubleStringDateTime 和其他可序列化类。您还可以使用对象列表。支持空安全。一旦类生成完毕,您就可以将它们用作端点方法的参数或返回类型。

注意:目前不支持 Map 或列表的列表。这些计划在未来版本中添加。

数据库映射

可以将可序列化类直接映射到数据库中的表。为此,请将 table 键添加到您的 yaml 文件中

class: Company
table: company
fields:
  name: String
  foundedDate: DateTime?
  employees: List<Employee>

运行 serverpod generate 时,数据库架构将保存在 generated/tables.pgsql 文件中。您可以使用此文件创建相应的数据库表。

在某些情况下,您想将字段保存到数据库,但该字段永远不应发送到服务器。您可以通过将 database 标志添加到类型来将其从协议中排除。

class: UserData
fields:
  name: String
  password: String, database

同样,如果您只希望一个字段可用于协议但未存储在服务器中,您可以添加 api 标志。默认情况下,字段对 API 和数据库都是可访问的。

数据库索引

出于性能原因,您可能希望为数据库表添加索引。您可以在定义可序列化对象的 yaml 文件中添加这些索引。

class: Company
table: company
fields:
  name: String
  foundedDate: DateTime?
  employees: List<Employee>
indexes:
  company_name_idx:
    fields: name

fields 键包含一个逗号分隔的列名列表。此外,还可以添加一个类型键(默认为 btree)和一个 unique 键(默认为 false)。

与数据库通信

Serverpod 可以轻松地使用严格类型的对象与数据库进行通信,无需任何 SQL 代码。但是,如果您需要执行更复杂的任务,您可以随时直接执行 SQL 调用。

为了使通信正常工作,您需要已生成具有 table 键的可序列化类,并且相应的表必须已在数据库中创建。

插入表行

通过调用 Session 对象中 db 字段的 insert 方法,在数据库中插入新行。

var myRow = Company(name: 'Serverpod corp.', employees: []);
await Company.insert(session, myRow);

插入对象后,其 id 字段将根据其在数据库中的行进行设置。

查找单行

您可以按 id 或使用表达式查找单行。您需要将表描述的引用传递给调用。表描述可以通过全局变量访问,这些变量以对象类名开头,前面加上 t

var myCompany = await Company.findById(session, companyId);

如果未找到匹配的行,则返回 null。您还可以使用 where 参数通过表达式搜索行。where 参数是一个类型化的表达式构建器。构建器的参数 t 包含表的描述,该描述提供了对表列的访问。

var myCompany = await Company.findSingleRow(
  session,
  where: (t) => t.name.equals('My Company'),
);

查找多行

要查找多行,请使用与查找单行相同的原理。返回的是 TableRowList

var companies = await Company.find(
  tCompany,
  where: (t) => t.id < 100,
  limit 50,
);

更新行

要更新行,请使用 update 方法。您更新的对象必须将其 id 设置为非 null 值。

var myCompany = await session.db.findById(tCompany, companyId) as Company?;
myCompany.name = 'New name';
await session.db.update(myCompany);

删除行

删除单行的方式与 update 方法类似,但您也可以使用 where 参数删除行。

// Delete a single row
await Company.deleteRow(session, myCompany);

// Delete all rows where the company name ends with 'Ltd'
await Company.delete(
  where: (t) => t.name.like('%Ltd'),
);

创建表达式

要查找或删除特定行,通常需要表达式。Serverpod 可以轻松地构建经过静态类型检查的表达式。列使用全局表描述符对象进行引用。表描述符 t 被传递给表达式构建器函数。>>=<<=&| 运算符被重载,以便更容易地处理列值。使用运算符时,最好将其放在括号内,因为其优先级规则并不总是符合预期。以下是一些表达式示例。

// The name column of the Company table equals 'My company')
t.name.equals('My company')

// Companies founded at or after 2020
t.foundedDate >= DateTime.utc(2020)

// Companies with number of employees between 10 and 100
(t.numEmployees > 10) & (t.numEmployees <= 100)

// Companies that has the founded date set
t.foundedDate.notEquals(null)

事务

文档稍后提供。

执行原始查询

有时需要对数据库执行更高级的任务。对于这些情况,可以对数据库运行原始 SQL 查询。使用 query 方法。将返回一个 List<List<dynamic>>,其中包含行和列。

var result = await session.db.query('SELECT * FROM mytable WHERE ...');

缓存

访问数据库有时对于复杂的数据库查询可能成本很高,或者如果您需要为特定任务运行许多不同的查询。Serverpod 可以轻松地将频繁请求的对象缓存在 RAM 中。任何可序列化的对象都可以被缓存。如果您的 Serverpod 是在集群中的多个服务器上托管的,对象将存储在 Redis 缓存中。

缓存可以通过 Session 对象访问。这是一个用于请求用户数据的端点方法的示例

Future<UserData> getUserData(Session session, int userId) async {
  // Define a unique key for the UserData object
  var cacheKey = 'UserData-$userId';

  // Try to retrieve the object from the cache
  var userData = await session.caches.local.get(cacheKey) as UserData?;

  // If the object wasn't found in the cache, load it from the database and
  // save it in the cache. Make it valid for 5 minutes.
  if (userData == null) {
    userData = session.db.findById(tUserData, userId) as UserData?;
    await session.caches.local.put(cacheKey, userData!, lifetime: Duration(minutes: 5));
  }

  // Return the user data to the client
  return userData;
}

总共有三个缓存可以存储您的对象。两个缓存是本地服务器处理当前会话的,一个是通过 Redis 分布在服务器集群中的。本地缓存有两种变体,一种是常规缓存,一种是优先级缓存。将频繁访问的对象放在优先级缓存中。

日志记录

Serverpod 使用数据库来存储日志,这使得搜索错误、慢查询或调试消息变得容易。要在会话执行期间记录自定义消息,请使用 session 对象的 log 方法。当会话关闭时(无论是成功执行还是通过抛出异常失败),消息都会写入日志。默认情况下,每个完成的会话都会写入会话日志条目。

session.log('This is working well');

日志条目存储在数据库的以下表中:serverpod_log 用于文本消息,serverpod_query_log 用于查询,serverpod_session_log 用于完成的会话。可选地,您可以传递一个日志级别来过滤消息,具体取决于服务器的运行时设置。

Serverpod GUI 即将推出,这将使得轻松读取、搜索和配置日志成为可能。

配置文件和部署

Serverpod 有三个主要的配置文件,具体取决于服务器运行的模式;developmentstagingproduction。文件位于 config/ 目录中。默认情况下,服务器将以开发模式启动。要使用其他配置文件,请在启动服务器时使用 --mode 选项。如果您运行的是集群中的多个服务器,请使用 --server-id 选项来指定每个服务器的 ID。默认情况下,服务器将以 ID 0 运行。例如,要以生产模式启动 ID 为 2 的服务器,请运行以下命令

dart bin/main.dart --mode production --server-id 2

根据服务器的内存占用量和高峰时段的服务请求量,您可能希望增加 Dart 可以使用的最大堆大小。您可以通过将 --old_gen_heap_size 选项传递给 dart 来实现。如果您将其设置为 0,它将为 Dart 提供无限的堆空间。Serverpod 可以在大多数可以运行 Dart 的操作系统上运行;不需要 Flutter。

运行服务器集群

要运行服务器集群,您需要将服务器置于负载均衡器后面,以便它们有一个通用的访问点到主 API 端口。如果您想收集服务器的运行时信息,服务端口不仅需要在集群服务器之间可访问,还需要从外部可访问。默认情况下,与服务 API 的通信是加密的,而您很可能希望为负载均衡器添加 HTTPS 证书,以确保所有与客户端的通信都是加密的。

使用 Google 和 Apple 登录

Serverpod 支持通过 auth 模块使用 Google 和 Apple 登录。有关设置身份验证的更多信息,请查看 auth 文档

路线图

Serverpod 处于早期但稳定的版本,并已在多个项目中用于生产。在接下来的几个月里,将会有许多令人兴奋的功能。

  • 改进文档。
  • 改进错误消息。
  • 用于查看和配置日志的图形界面。
  • 用于查看服务器健康状况和指标的图形界面。
  • Google Cloud 文件上传(目前支持 AWS)。
  • 更多身份验证选项(目前支持 Apple、Google 和电子邮件)。
  • 更轻松的部署。

GitHub

查看 Github