跳转至

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 /projectsGET /projectsGET /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的表,该表具有idnamedueDate列。

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);
  }  
}

用于交换用户凭证以获得访问令牌的内置身份验证控制器称为AuthControllerAuthCodeControllerAuthorizer是中间件,需要有效的访问令牌才能访问其链接的控制器。

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管理应用程序的启动和停止,并公开用于执行请求的默认AgentAgent可以被配置为具有默认的标头,并且在同一个测试中可以使用多个代理。

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命令可以创建一个网页,用于配置特定于你的应用程序的问题请求。