Aqueduct: 游览
此游览展示了Aqueduct的许多功能。
命令行界面 (CLI)
aqueduct
命令行工具创建、运行和记录Aqueduct应用程序;管理数据库迁移;管理OAuth客户端标识符。在安装了Dart的机器上运行pub global activate aqueduct
进行安装。
创建并运行一个应用程序:
aqueduct create my_app
cd my_app/
aqueduct serve
初始化
一个Aqueduct应用始于一个ApplicationChannel。每个应用都要对它进行一次子类化,以处理初始化任务,如设置路由和数据库连接。一个应用程序的例子看起来像这样。
import 'package:aqueduct/aqueduct.dart';
class TodoApp extends ApplicationChannel {
ManagedContext context;
@override
Future prepare() async {
context = ManagedContext(...);
}
@override
Controller get entryPoint {
final router = Router();
router
.route("/projects/[:id]")
.link(() => ProjectController(context));
return router;
}
}
路由
router 确定哪个控制器对象应处理请求。 路由规范语法是一种简洁的语法,用于在单个语句中构造具有变量和可选段的路由。
@override
Controller get entryPoint {
final router = Router();
// Handles /users, /users/1, /users/2, etc.
router
.route("/projects/[:id]")
.link(() => ProjectController());
// Handles any route that starts with /file/
router
.route("/file/*")
.link(() => FileController());
// Handles the specific route /health
router
.route("/health")
.linkFunction((req) async => Response.ok(null));
return router;
}
控制器
Controllers 处理请求。 控制器通过覆盖其处理方法处理请求。 此方法返回响应或请求。 如果返回响应,则将该响应发送到客户端。 如果返回了请求,则链接的控制器将处理该请求。
class SecretKeyAuthorizer extends Controller {
@override
Future<RequestOrResponse> handle(Request request) async {
if (request.raw.headers.value("x-secret-key") == "secret!") {
return request;
}
return Response.badRequest();
}
}
这种行为允许将中间件控制器连接在一起,这样一个请求在最终处理之前会经过许多步骤。
所有的控制器都会在异常处理程序中执行它们的代码。如果在你的控制器代码中抛出了一个异常,就会返回一个带有适当错误代码的响应。你可以将HandlerException
子类化,为特定于应用程序的异常提供自定义的错误响应。
ResourceControllers
ResourceControllers 是最常用的控制器。每个操作,例如 POST /projects
、GET /projects
和GET /projects/1
被映射到子类中的方法。这些方法的参数被注释为当方法被调用时绑定请求的值。
import 'package:aqueduct/aqueduct.dart'
class ProjectController extends ResourceController {
@Operation.get('id')
Future<Response> getProjectById(@Bind.path("id") int id) async {
// GET /projects/:id
return Response.ok(...);
}
@Operation.post()
Future<Response> createProject(@Bind.body() Project project) async {
// POST /project
final inserted = await insertProject(project);
return Response.ok(inserted);
}
@Operation.get()
Future<Response> getAllProjects(
@Bind.header("x-client-id") String clientId,
{@Bind.query("limit") int limit: 10}) async {
// GET /projects
return Response.ok(...);
}
}
ManagedObjectControllers
ManagedObjectController <T>
也是ResourceController
,它会自动将REST接口映射到数据库查询; 例如 POST
插入一行,GET
获取类型的所有行。 它们不需要子类化,但是可以提供定制。
router
.route("/users/[:id]")
.link(() => ManagedObjectController<Project>(context));
配置 (Configuration)
应用程序的配置写在YAML文件中。你的应用程序在每个环境中运行(例如,本地,测试中,生产,开发)都有不同的值,例如端口监听和数据库连接证书。配置文件的格式是由你的应用程序定义的。一个例子看起来像这样。
// config.yaml
database:
host: api.projects.com
port: 5432
databaseName: project
port: 8000
子类化Configuration
并为配置文件中的每个键声明一个属性:
class TodoConfig extends Configuration {
TodoConfig(String path) : super.fromFile(File(path));
DatabaseConfiguration database;
int port;
}
配置文件的默认名称是 config.yaml
,但可以在命令行更改。你可以从应用程序选项中的配置文件路径创建一个配置实例。
import 'package:aqueduct/aqueduct.dart';
class TodoApp extends ApplicationChannel {
@override
Future prepare() async {
var options = TodoConfig(options.configurationFilePath);
...
}
}
运行与并发
Aqueduct应用程序是通过aqueduct serve
命令行工具运行的。你可以附加调试和检测工具,并指定应用程序应该在多少个线程上运行:
aqueduct serve --observe --isolates 5 --port 8888
Aqueduct的应用是多隔离(多线程)的。每个隔离体都运行在同一个web服务器的副本上,有自己的一套服务,比如数据库连接。这使得数据库连接池等行为变得隐式。
PostgreSQL ORM
Query<T>
类配置和执行数据库查询。它的类型参数决定了要查询的表和你将在代码中使用的对象类型。
import 'package:aqueduct/aqueduct.dart'
class ProjectController extends ResourceController {
ProjectController(this.context);
final ManagedContext context;
@Operation.get()
Future<Response> getAllProjects() async {
final query = Query<Project>(context);
final results = await query.fetch();
return Response.ok(results);
}
}
查询的配置(如其WHERE
子句)是通过流畅的类型安全的语法配置的。 属性选择器确定了表的哪一列要应用表达式。下面的查询通过加入相关的表来获取所有在下周到期的项目,并包括它们的任务。
final nextWeek = DateTime.now().add(Duration(days: 7));
final query = Query<Project>(context)
..where((project) => project.dueDate).isLessThan(nextWeek)
..join(set: (project) => project.tasks);
final projects = await query.fetch();
通过设置查询的静态类型值来插入或更新行。
final insertQuery = Query<Project>(context)
..values.name = "Build an aqueduct"
..values.dueDate = DateTime(year, month);
var newProject = await insertQuery.insert();
final updateQuery = Query<Project>(context)
..where((project) => project.id).equalTo(newProject.id)
..values.name = "Build a miniature aqueduct";
newProject = await updateQuery.updateOne();
Query<T>
可以进行排序、加入和分页查询。
final overdueQuery = Query<Project>(context)
..where((project) => project.dueDate).lessThan(DateTime().now())
..sortBy((project) => project.dueDate, QuerySortOrder.ascending)
..join(object: (project) => project.owner);
final overdueProjectsAndTheirOwners = await query.fetch();
控制器将解释查询抛出的异常,向客户端返回适当的错误响应。例如,唯一约束冲突返回409,缺少所需属性返回400,数据库连接失败返回503。
定义数据模型
要使用ORM,您需要将表声明为Dart类型并创建ManagedObject <T>
的子类。 子类映射到数据库中的表,每个实例映射到一行,每个属性是一列。 以下声明将映射到名为_project
的表,该表具有id
,name
和dueDate
列。
class Project extends ManagedObject<_Project> implements _Project {
bool get isPastDue => dueDate.difference(DateTime.now()).inSeconds < 0;
}
class _Project {
@primaryKey
int id;
@Column(indexed: true)
String name;
DateTime dueDate;
}
托管对象与其他托管对象有关系。 关系可以是一对多,一对多和多对多。 关系始终是双向的-相关类型必须声明一个相互引用的属性。
class Project extends ManagedObject<_Project> implements _Project {}
class _Project {
...
// Project has-many Tasks
ManagedSet<Task> tasks;
}
class Task extends ManagedObject<_Task> implements _Task {}
class _Task {
...
// Task belongs to a project, maps to 'project_id' foreign key column
@Relate(#tasks)
Project project;
}
ManagedObject<T>
是可序列化的,可以直接从请求体中读取,或作为响应体编码。
class ProjectController extends ResourceController {
@Operation.put('id')
Future<Response> updateProject(@Bind.path('id') int projectId, @Bind.body() Project project) async {
final query = Query<Project>(context)
..where((project) => project.id).equalTo(projectId)
..values = project;
return Response.ok(await query.updateOne());
}
}
数据库迁移
CLI将通过检测您的管理对象的变化自动生成数据库迁移脚本。当在项目目录下运行以下内容时,将生成并执行数据库迁移。
aqueduct db generate
aqueduct db upgrade --connect postgres://user:password@host:5432/database
你可以手工编辑迁移文件,以改变任何假设或输入所需的值,并运行aqueduct db validate
以确保更改后的模式仍然相同。请确保将生成的文件保存在版本控制中。
OAuth 2.0
OAuth 2.0服务器实现可处理Aqueduct应用程序的身份验证和授权。 您可以在应用程序中创建一个AuthServer
及其委托作为服务。 委托是可配置的,并管理令牌的生成和存储方式。 默认情况下,访问令牌是一个随机的32字节字符串,客户端标识符,令牌和访问代码使用ORM存储在数据库中。
import 'package:aqueduct/aqueduct.dart';
import 'package:aqueduct/managed_auth.dart';
class AppApplicationChannel extends ApplicationChannel {
AuthServer authServer;
ManagedContext context;
@override
Future prepare() async {
context = ManagedContext(...);
final delegate = ManagedAuthDelegate<User>(context);
authServer = AuthServer(delegate);
}
}
用于交换用户凭证以获得访问令牌的内置身份验证控制器称为AuthController
和AuthCodeController
。Authorizer
是中间件,需要有效的访问令牌才能访问其链接的控制器。
Controller get entryPoint {
final router = Router();
// POST /auth/token with username and password (or access code) to get access token
router
.route("/auth/token")
.link(() => AuthController(authServer));
// GET /auth/code returns login form, POST /auth/code grants access code
router
.route("/auth/code")
.link(() => AuthCodeController(authServer));
// ProjectController requires request to include access token
router
.route("/projects/[:id]")
.link(() => Authorizer.bearer(authServer))
.link(() => ProjectController(context));
return router;
}
CLI是具有管理OAuth 2.0客户端标识符和访问范围的工具。
aqueduct auth add-client \
--id com.app.mobile \
--secret foobar \
--redirect-uri https://somewhereoutthere.com \
--allowed-scopes "users projects admin.readonly"
日志
所有的请求都会被记录到整个应用程序的记录器中。在 ApplicationChannel
中为日志器设置一个监听器,将日志信息写入控制台或其他媒介。
class WildfireChannel extends ApplicationChannel {
@override
Future prepare() async {
logger.onRecord.listen((record) {
print("$record");
});
}
}
测试
Aqueduct测试启动您的应用程序的本地版本并执行请求。你对响应写出期望值。TestHarness管理应用程序的启动和停止,并公开用于执行请求的默认Agent
。Agent
可以被配置为具有默认的标头,并且在同一个测试中可以使用多个代理。
import 'harness/app.dart';
void main() {
final harness = TestHarness<TodoApp>()..install();
test("GET /projects returns all projects" , () async {
var response = await harness.agent.get("/projects");
expectResponse(response, 200, body: every(partial({
"id": greaterThan(0),
"name": isNotNull,
"dueDate": isNotNull
})));
});
}
用数据库测试
Aqueduct的ORM使用PostgreSQL作为其数据库。在您的测试运行之前,Aqueduct将在本地PostgreSQL数据库中创建您的应用程序的数据库表。测试完成后,它将删除这些表。这使得您可以为每个测试套件创建一个空数据库,并在测试时精确控制数据库中的记录,但无需管理数据库模式或使用模拟实现(如SQLite)。
这种行为,以及使用OAuth 2.0提供者管理应用程序的行为,都可以通过harness mixins获得。
文档
OpenAPI文档描述了你的应用程序的接口。这些文档可以用来生成文档和客户端代码。只要运行aqueduct document
命令,就可以通过反映你的应用程序的代码库来生成文档。
aqueduct document client
命令可以创建一个网页,用于配置特定于你的应用程序的问题请求。