diff --git a/4.0/docs/advanced/apns.md b/4.0/docs/advanced/apns.md new file mode 100644 index 0000000..2c48342 --- /dev/null +++ b/4.0/docs/advanced/apns.md @@ -0,0 +1,148 @@ +# APNS + +Vapor的苹果推送通知服务(APNS)API使认证和发送推送通知到苹果设备变得容易。它建立在[APNSwift](https://github.com/kylebrowning/APNSwift)的基础上。 + +## 开始使用 + +让我们来看看你如何开始使用APNS。 + +### Package + +使用APNS的第一步是将软件包添加到你的依赖项中。 + +```swift +// swift-tools-version:5.2 +import PackageDescription + +let package = Package( + name: "my-app", + dependencies: [ + // 其他的依赖性... + .package(url: "https://github.com/vapor/apns.git", from: "2.0.0"), + ], + targets: [ + .target(name: "App", dependencies: [ + // 其他的依赖性... + .product(name: "APNS", package: "apns") + ]), + // Other targets... + ] +) +``` + +如果您在Xcode中直接编辑清单,它将会自动接收更改并在保存文件时获取新的依赖关系。否则,从终端运行`swift package resolve`来获取新的依赖关系。 + +### 配置 + +APNS模块为`Application`添加了一个新的属性`apns`。为了发送推送通知,你需要用你的证书设置`configuration`属性。 + +```swift +import APNS + +// 使用JWT认证配置APNS。 +app.apns.configuration = try .init( + authenticationMethod: .jwt( + key: .private(filePath: <#path to .p8#>), + keyIdentifier: "<#key identifier#>", + teamIdentifier: "<#team identifier#>" + ), + topic: "<#topic#>", + environment: .sandbox +) +``` + +在占位符中填入你的凭证。上面的例子显示了[基于JWT的认证](https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server/establishing_a_token-based_connection_to_apns),使用你从苹果的开发者门户获得的`.p8`密钥。对于带有证书的[基于TLS的认证](https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server/establishing_a_certificate-based_connection_to_apns),使用`.tls`认证方法。 + +```swift +authenticationMethod: .tls( + privateKeyPath: <#path to private key#>, + pemPath: <#path to pem file#>, + pemPassword: <#optional pem password#> +) +``` + +### 发送 + +一旦配置了APNS,你可以使用`apns.send`方法在`Application`或`Request`上发送推送通知。 + +```swift +// 发送一个推送通知。 +try app.apns.send( + .init(title: "Hello", subtitle: "This is a test from vapor/apns"), + to: "98AAD4A2398DDC58595F02FA307DF9A15C18B6111D1B806949549085A8E6A55D" +).wait() + +// 或 +try await app.apns.send( + .init(title: "Hello", subtitle: "This is a test from vapor/apns"), + to: "98AAD4A2398DDC58595F02FA307DF9A15C18B6111D1B806949549085A8E6A55D" +) +``` + +只要你在路由处理程序中,就使用`req.apns`。 + +```swift +// 发送一个推送通知。 +app.get("test-push") { req -> EventLoopFuture in + req.apns.send(..., to: ...) + .map { .ok } +} + +// 或 +app.get("test-push") { req async throws -> HTTPStatus in + try await req.apns.send(..., to: ...) + return .ok +} +``` + +第一个参数接受推送通知警报,第二个参数是目标设备令牌。 + +## 警报 + +`APNSwiftAlert`是要发送的推送通知警报的实际元数据。关于每个属性的具体细节[这里](https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/PayloadKeyReference.html)。它们遵循苹果文档中列出的一对一的命名方案 + +```swift +let alert = APNSwiftAlert( + title: "Hey There", + subtitle: "Full moon sighting", + body: "There was a full moon last night did you see it" +) +``` + +这种类型可以直接传递给`send`方法,它将被自动包裹在`APNSwiftPayload`中。 + +### Payload + +`APNSwiftPayload`是推送通知的元数据。诸如警报、徽章数量等内容。关于每个属性的具体细节都提供了[这里](https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/PayloadKeyReference.html)。它们遵循苹果文档中列出的一对一的命名方案 + +```swift +let alert = ... +let aps = APNSwiftPayload(alert: alert, badge: 1, sound: .normal("cow.wav")) +``` + +这可以传递给`send`方法。 + +### 自定义通知数据 + +苹果公司为工程师提供了在每个通知中添加自定义有效载荷数据的能力。为了方便这一点,我们有`APNSwiftNotification`。 + +```swift +struct AcmeNotification: APNSwiftNotification { + let acme2: [String] + let aps: APNSwiftPayload + + init(acme2: [String], aps: APNSwiftPayload) { + self.acme2 = acme2 + self.aps = aps + } +} + +let aps: APNSwiftPayload = ... +let notification = AcmeNotification(acme2: ["bang", "whiz"], aps: aps) +``` + +这个自定义的通知类型可以被传递给`send`方法。 + +## 更多信息 + +关于可用方法的更多信息,请参阅[APNSwift的README](https://github.com/kylebrowning/APNSwift)。 diff --git a/4.0/docs/advanced/middleware.md b/4.0/docs/advanced/middleware.md index ceca777..d13243e 100644 --- a/4.0/docs/advanced/middleware.md +++ b/4.0/docs/advanced/middleware.md @@ -1,8 +1,8 @@ -# Middleware +# 中间件 -Middleware 是 client 和路由处理程序间的一个逻辑链。它允许你在传入请求到达路由处理程序之前对传入请求执行操作,并且在输出响应到达 client 之前对传出响应执行操作。 +中间件是客户端和路由处理程序间的一个逻辑链。它允许你在传入请求到达路由处理程序之前对传入请求执行操作,并且在输出响应到达 client 之前对传出响应执行操作。 -## Configuration +## 配置 可以使用 `app.middleware` 在 `configure(_:)` 中全局(针对每条路由)注册 Middleware。 diff --git a/4.0/docs/advanced/queues.md b/4.0/docs/advanced/queues.md index daa803c..7c63eba 100644 --- a/4.0/docs/advanced/queues.md +++ b/4.0/docs/advanced/queues.md @@ -1,42 +1,42 @@ -# Queues +# 队列 -Vapor Queues ([vapor/queues](https://github.com/vapor/queues)) is a pure Swift queuing system that allows you to offload task responsibility to a side worker. +Vapor Queues([vapor/queues](https://github.com/vapor/queues))是一个纯粹的Swift队列系统,允许你将任务责任卸载给一个侧翼工作者。 -Some of the tasks this package works well for: +这个package的一些任务很好用: -- Sending emails outside of the main request thread -- Performing complex or long-running database operations -- Ensuring job integrity and resilience -- Speeding up response time by delaying non-critical processing -- Scheduling jobs to occur at a specific time +- 在主请求线程之外发送电子邮件 +- 执行复杂或长时间运行的数据库操作 +- 确保工作的完整性和复原力 +- 通过延迟非关键性的处理来加快响应时间 +- 将工作安排在特定的时间发生 -This package is similar to [Ruby Sidekiq](https://github.com/mperham/sidekiq). It provides the following features: +这个包类似于[Ruby Sidekiq](https://github.com/mperham/sidekiq)。它提供了以下功能: -- Safe handling of `SIGTERM` and `SIGINT` signals sent by hosting providers to indicate a shutdown, restart, or new deploy. -- Different queue priorities. For example, you can specify a queue job to be run on the email queue and another job to be run on the data-processing queue. -- Implements the reliable queue process to help with unexpected failures. -- Includes a `maxRetryCount` feature that will repeat the job until it succeeds up until a specified count. -- Uses NIO to utilize all available cores and EventLoops for jobs. -- Allows users to schedule repeating tasks +- 安全地处理主机供应商发送的`SIGTERM`和`SIGINT`信号,以表示关闭、重新启动或新的部署。 +- 不同的队列优先级。例如,你可以指定一个队列作业在电子邮件队列中运行,另一个作业在数据处理队列中运行。 +- 实施可靠的队列进程,以帮助处理意外故障。 +- 包括一个`maxRetryCount`功能,它将重复作业,直到它成功到指定的计数。 +- 使用NIO来利用所有可用的核心和作业的EventLoops。 +- 允许用户安排重复的任务 -Queues currently has one officially supported driver which interfaces with the main protocol: +Queues目前有一个官方支持的驱动程序,它与主协议接口: - [QueuesRedisDriver](https://github.com/vapor/queues-redis-driver) -Queues also has community-based drivers: +Queues也有基于社区的驱动程序。 - [QueuesMongoDriver](https://github.com/vapor-community/queues-mongo-driver) - [QueuesFluentDriver](https://github.com/m-barthelemy/vapor-queues-fluent-driver) -!!! tip - You should not install the `vapor/queues` package directly unless you are building a new driver. Install one of the driver packages instead. +!!! Tip + 你不应该直接安装`vapor/queues`包,除非你正在构建一个新的驱动程序。应该安装其中的一个驱动包。 -## Getting Started +## 开始使用 -Let's take a look at how you can get started using Queues. +让我们来看看你如何开始使用队列。 ### Package -The first step to using Queues is adding one of the drivers as a dependency to your project in your SwiftPM package manifest file. In this example, we'll use the Redis driver. +使用 Queues 的第一步是在 SwiftPM 包清单文件中将其中一个驱动程序作为依赖项添加到您的项目中。在本例中,我们将使用 Redis 驱动程序。 ```swift // swift-tools-version:5.2 @@ -45,7 +45,7 @@ import PackageDescription let package = Package( name: "MyApp", dependencies: [ - /// Any other dependencies ... + /// 任何其他的依赖性... .package(url: "https://github.com/vapor/queues-redis-driver.git", from: "1.0.0"), ], targets: [ @@ -58,55 +58,56 @@ let package = Package( ) ``` -If you edit the manifest directly inside Xcode, it will automatically pick up the changes and fetch the new dependency when the file is saved. Otherwise, from Terminal, run `swift package resolve` to fetch the new dependency. +如果您在Xcode中直接编辑清单,它将会自动接收更改并在保存文件时获取新的依赖关系。否则,从终端运行`swift package resolve`来获取新的依赖关系。 -### Config +### 配置 -The next step is to configure Queues in `configure.swift`. We'll use the Redis library as an example: +下一步是在`configure.swift`中配置队列。我们将使用Redis库作为一个例子: ```swift try app.queues.use(.redis(url: "redis://127.0.0.1:6379")) ``` -### Registering a `Job` +### 注册一个Job -After modeling a job you must add it to your configuration section like this: +在建立一个作业模型后,你必须把它添加到你的配置部分,像这样: ```swift -//Register jobs +// 注册工作 let emailJob = EmailJob() app.queues.add(emailJob) ``` -### Running Workers as Processes +### 以进程形式运行Workers -To start a new queue worker, run `vapor run queues`. You can also specify a specific type of worker to run: `vapor run queues --queue emails`. +要启动一个新的队列Worker,运行`vapor run queues`。你也可以指定一个特定类型的Worker来运行:`vapor run queues --queue emails`。 -!!! tip - Workers should stay running in production. Consult your hosting provider to find out how to keep long-running processes alive. Heroku, for example, allows you to specify "worker" dynos like this in your Procfile: `worker: Run run queues` +!!! Tip + Workers应在生产中保持运行。请咨询您的主机提供商,了解如何保持长期运行的进程。例如,Heroku允许你在Procfile中这样指定 "worker"动态。`worker: Run queues`。有了这个,你可以在Dashboard/Resources标签上启动worker,或者用`heroku ps:scale worker=1`(或任何数量的dynos优先)。 -### Running Workers in-process +### 在进程中运行Workers -To run a worker in the same process as your application (as opposed to starting a whole separate server to handle it), call the convenience methods on `Application`: +要在与你的应用程序相同的进程中运行一个worker(而不是启动一个单独的服务器来处理它),请调用`Application`上的便利方法: ```swift try app.queues.startInProcessJobs(on: .default) ``` -To run scheduled jobs in process, call the following method: +要在进程中运行预定的工作,请调用以下方法: ```swift try app.queues.startScheduledJobs() ``` !!! warning - If you don't start the queue worker either via command line or the in-process worker the jobs will not dispatch. + 如果你不通过命令行或进程中的工作者启动队列工作者,作业就不会被派发。 -## The `Job` Protocol +## `Job`协议 -Jobs are defined by the `Job` protocol. +工作是由`Job`或`AsyncJob`协议定义的。 + +### 建立一个`Job`对象的模型: -### Modeling a `Job` object: ```swift import Vapor import Foundation @@ -121,23 +122,39 @@ struct EmailJob: Job { typealias Payload = Email func dequeue(_ context: QueueContext, _ payload: Email) -> EventLoopFuture { - // This is where you would send the email + // 这是你要发送电子邮件的地方 return context.eventLoop.future() } func error(_ context: QueueContext, _ error: Error, _ payload: Email) -> EventLoopFuture { - // If you don't want to handle errors you can simply return a future. You can also omit this function entirely. + // 如果你不想处理错误,你可以简单地返回一个未来。你也可以完全省略这个函数。 return context.eventLoop.future() } } ``` -!!! tip - Don't forget to follow the instructions in **Getting Started** to add this job to your configuration file. +如果使用`async`/`await`,你应该使用`AsyncJob`: -## Dispatching Jobs +```swift +struct EmailJob: AsyncJob { + typealias Payload = Email + + func dequeue(_ context: QueueContext, _ payload: Email) async throws { + // 这是你要发送电子邮件的地方 + } + + func error(_ context: QueueContext, _ error: Error, _ payload: Email) async throws { + // 如果你不想处理错误,你可以直接返回。你也可以完全省略这个函数。 + } +} +``` + +!!! Tip + 不要忘了按照**入门**中的说明,将这项工作添加到你的配置文件中。 -To dispatch a queue job, you need access to an instance of `Application` or `Request`. You will most likely be dispatching jobs inside of a route handler: +## 调度Jobs + +要调度一个队列作业,你需要访问`Application`或`Request`的一个实例。你很可能会在路由处理程序中调度作业。 ```swift app.get("email") { req -> EventLoopFuture in @@ -148,11 +165,20 @@ app.get("email") { req -> EventLoopFuture in .init(to: "email@email.com", message: "message") ).map { "done" } } + +// 或 + +app.get("email") { req async throws -> String in + try await req.queue.dispatch( + EmailJob.self, + .init(to: "email@email.com", message: "message")) + return "done" +} ``` -### Setting `maxRetryCount` +### 设置`maxRetryCount`。 -Jobs will automatically retry themselves upon error if you specify a `maxRetryCount`. For example: +如果你指定了一个`maxRetryCoun`,作业会在出错时自动重试。例如: ```swift app.get("email") { req -> EventLoopFuture in @@ -164,33 +190,41 @@ app.get("email") { req -> EventLoopFuture in maxRetryCount: 3 ).map { "done" } } + +// 或 + +app.get("email") { req async throws -> String in + try await req.queue.dispatch( + EmailJob.self, + .init(to: "email@email.com", message: "message"), + maxRetryCount: 3) + return "done" +} ``` -### Specifying a delay +### 指定一个延迟 -Jobs can also be set to only run after a certain `Date` has passed. To specify a delay, pass a `Date` into the `delayUntil` parameter in `dispatch`: +工作也可以被设置为只在某个`Date`过后运行。要指定一个延迟,在`dispatch`的`delayUntil`参数中传递一个`Date`: ```swift -app.get("email") { req -> EventLoopFuture in +app.get("email") { req async throws -> String in let futureDate = Date(timeIntervalSinceNow: 60 * 60 * 24) // One day - return req - .queue - .dispatch( - EmailJob.self, - .init(to: "email@email.com", message: "message"), - maxRetryCount: 3, - delayUntil: futureDate - ).map { "done" } + try await req.queue.dispatch( + EmailJob.self, + .init(to: "email@email.com", message: "message"), + maxRetryCount: 3, + delayUntil: futureDate) + return "done" } ``` -If a job is dequeued before its delay parameter, the job will be re-queued by the driver. +如果一个作业在其延迟参数之前被取消排队,该作业将被驱动重新排队。 -### Specify a priority +### 指定一个优先级 -Jobs can be sorted into different queue types/priorities depending on your needs. For example, you may want to open an `email` queue and a `background-processing` queue to sort jobs. +作业可以根据你的需要被分到不同的队列类型/优先级。例如,你可能想开一个`email`队列和一个`background-processing`队列来分类作业。 -Start by extending `QueueName `: +通过扩展`QueueName`开始。 ```swift extension QueueName { @@ -198,7 +232,7 @@ extension QueueName { } ``` -Then, specify the queue type when you retrieve the `jobs` object: +然后,在检索`jobs`对象时指定队列类型: ```swift app.get("email") { req -> EventLoopFuture in @@ -212,42 +246,66 @@ app.get("email") { req -> EventLoopFuture in delayUntil: futureDate ).map { "done" } } + +// 或 + +app.get("email") { req async throws -> String in + let futureDate = Date(timeIntervalSinceNow: 60 * 60 * 24) // One day + try await req + .queues(.emails) + .dispatch( + EmailJob.self, + .init(to: "email@email.com", message: "message"), + maxRetryCount: 3, + delayUntil: futureDate + ) + return "done" +} ``` -If you do not specify a queue the job will be run on the `default` queue. Make sure to follow the instructions in **Getting Started** to start workers for each queue type. +如果你不指定队列,作业将在 "默认 "队列上运行。请确保按照**入门**中的说明,为每个队列类型启动工作者。 -## Scheduling Jobs +## 调度Jobs -The Queues package also allows you to schedule jobs to occur at certain points in time. +队列包还允许你将作业安排在某些时间点上发生。 -### Starting the scheduler worker -The scheduler requires a separate worker process to be running, similar to the queue worker. You can start the worker by running this command: +### 启动调度器的工作程序 +调度器需要运行一个单独的工作进程,与队列工作器类似。你可以通过运行这个命令来启动这个工作程序。 ```sh swift run Run queues --scheduled ``` -!!! tip - Workers should stay running in production. Consult your hosting provider to find out how to keep long-running processes alive. Heroku, for example, allows you to specify "worker" dynos like this in your Procfile: `worker: Run run queues --scheduled` +!!! Tip + 工人应该在生产中保持运行。请咨询你的主机提供商,了解如何让长期运行的进程保持活力。例如,Heroku允许你在Procfile中像这样指定"worker"动态:`worker: Run queues --scheduled`。 -### Creating a `ScheduledJob` -To being, start by creating a new `ScheduledJob`: +### 创建一个ScheduledJob + +首先,创建一个新的`ScheduledJob`或`AsyncScheduledJob`: ```swift import Vapor -import Jobs +import Queues struct CleanupJob: ScheduledJob { - // Add extra services here via dependency injection, if you need them. + // 如果你需要的话,在这里通过依赖性注入添加额外的服务。 func run(context: QueueContext) -> EventLoopFuture { - // Do some work here, perhaps queue up another job. + // 在这里做一些工作,或许可以排队做另一份工作。 return context.eventLoop.makeSucceededFuture(()) } } + +struct CleanupJob: AsyncScheduledJob { + // 如果你需要的话,在这里通过依赖性注入添加额外的服务。 + + func run(context: QueueContext) async throws { + // 在这里做一些工作,或许可以排队做另一份工作。 + } +} ``` -Then, in your configure code, register the scheduled job: +然后,在你的配置代码中,注册ScheduledJob: ```swift app.queues.schedule(CleanupJob()) @@ -257,46 +315,86 @@ app.queues.schedule(CleanupJob()) .at(.noon) ``` -The job in the example above will be run every year on May 23rd at 12:00 PM. +上面例子中的工作将在每年的5月23日12:00点运行。 -!!! tip - The Scheduler takes the timezone of your server. +!!! Tip + Scheduler采用你的服务器的时区。 -### Available builder methods -There are five main methods that can be called on a scheduler, each of which creates its respective builder object that contains more helper methods. You should continue building out a scheduler object until the compiler does not give you a warning about an unused result. See below for all available methods: +### 可用的构建器方法 +有五个主要的方法可以在调度器上调用,每个方法都会创建各自的构建器对象,其中包含更多的辅助方法。你应该继续构建一个调度器对象,直到编译器不给你一个关于未使用结果的警告。所有可用的方法见下文。 -| Helper Function | Available Modifiers | Description | +| 帮助函数 | 可用的修改器 | 描述 | |-----------------|---------------------------------------|--------------------------------------------------------------------------------| -| `yearly()` | `in(_ month: Month) -> Monthly` | The month to run the job in. Returns a `Monthly` object for further building. | -| `monthly()` | `on(_ day: Day) -> Daily` | The day to run the job in. Returns a `Daily` object for further building. | -| `weekly()` | `on(_ weekday: Weekday) -> Daily` | The day of the week to run the job on. Returns a `Daily` object. | -| `daily()` | `at(_ time: Time)` | The time to run the job on. Final method in the chain. | -| | `at(_ hour: Hour24, _ minute: Minute)`| The hour and minute to run the job on. Final method in the chain. | -| | `at(_ hour: Hour12, _ minute: Minute, _ period: HourPeriod)` | The hour, minute, and period to run the job on. Final method of the chain | -| `hourly()` | `at(_ minute: Minute)` | The minute to run the job at. Final method of the chain. | - -### Available helpers -Queues ships with some helpers enums to make scheduling easier: - -| Helper Function | Available Helper Enum | +| `yearly()` | `in(_ month: Month) -> Monthly` | 运行该工作的月份。返回一个`Monthly`对象,以便进一步构建。 | +| `monthly()` | `on(_ day: Day) -> Daily` | 运行该工作的日期。返回一个`Daily`的对象,以便进一步构建。 | +| `weekly()` | `on(_ weekday: Weekday) -> Daily` | 在一周中的哪一天运行工作。返回一个`Daily`对象。 | +| `daily()` | `at(_ time: Time)` | 运行该作业的时间。链中的最后一个方法。 | +| | `at(_ hour: Hour24, _ minute: Minute)`| 运行该作业的小时和分钟。链中的最后一个方法。 | +| | `at(_ hour: Hour12, _ minute: Minute, _ period: HourPeriod)` | 运行该工作的小时、分钟和时期。链的最终方法 | +| `hourly()` | `at(_ minute: Minute)` | 运行该工作的分钟数。链的最终方法。 | + +### 可用的帮助器 +队列带有一些帮助器枚举,使调度更容易。 + +| 帮助函数 | 可用的帮助枚举 | |-----------------|---------------------------------------| | `yearly()` | `.january`, `.february`, `.march`, ...| | `monthly()` | `.first`, `.last`, `.exact(1)` | | `weekly()` | `.sunday`, `.monday`, `.tuesday`, ... | | `daily()` | `.midnight`, `.noon` | -To use the helper enum, call in to the appropriate modifier on the helper function and pass the value. For example: +要使用帮助器枚举,请在帮助器函数上调用相应的修改器并传递数值。例如: ```swift -// Every year in January +// 每年的1月 .yearly().in(.january) -// Every month on the first day +// 每个月的第一天 .monthly().on(.first) -// Every week on Sunday +// 每星期的星期天 .weekly().on(.sunday) -// Every day at midnight +// 每天午夜时分 .daily().at(.midnight) ``` + +## 事件代理 +Queues包允许你指定`JobEventDelegate`对象,当工作者对作业采取行动时,这些对象将收到通知。这可用于监控、浮现洞察力或警报目的。 + +要开始使用,请将一个对象与`JobEventDelegate`相符合,并实现任何所需的方法 + +```swift +struct MyEventDelegate: JobEventDelegate { + /// 当作业从一个路由被分派到队列工作者时被调用 + func dispatched(job: JobEventData, eventLoop: EventLoop) -> EventLoopFuture { + eventLoop.future() + } + + /// 当作业被放入处理队列并开始工作时被调用 + func didDequeue(jobId: String, eventLoop: EventLoop) -> EventLoopFuture { + eventLoop.future() + } + + /// 当作业完成处理并从队列中移除时被调用。 + func success(jobId: String, eventLoop: EventLoop) -> EventLoopFuture { + eventLoop.future() + } + + /// 当作业完成处理但有错误时被调用。 + func error(jobId: String, error: Error, eventLoop: EventLoop) -> EventLoopFuture { + eventLoop.future() + } +} +``` + +然后,在你的配置文件中添加它: + +```swift +app.queues.add(MyEventDelegate()) +``` + +有许多第三方软件包使用委托功能来提供对你的队列工作者的额外洞察力: + +- [QueuesDatabaseHooks](https://github.com/vapor-community/queues-database-hooks) +- [QueuesDash](https://github.com/gotranseo/queues-dash) diff --git a/4.0/docs/advanced/server.md b/4.0/docs/advanced/server.md index a27cbeb..d2535f3 100644 --- a/4.0/docs/advanced/server.md +++ b/4.0/docs/advanced/server.md @@ -1,233 +1,230 @@ # Server -Vapor includes a high-performance, asynchronous HTTP server built on [SwiftNIO](https://github.com/apple/swift-nio). This server supports HTTP/1, HTTP/2, and protocol upgrades like [WebSockets](websockets.md). The server also supports enabling TLS (SSL). +Vapor包括一个建立在[SwiftNIO](https://github.com/apple/swift-nio)上的高性能、异步的HTTP服务器。该服务器支持HTTP/1、HTTP/2以及[WebSockets](websockets.md)等协议的升级。该服务器还支持启用TLS(SSL)。 -## Configuration +## 配置 -Vapor's default HTTP server can be configured via `app.http.server`. +Vapor的默认HTTP服务器可以通过`app.http.server`进行配置。 ```swift -// Only support HTTP/2 +// 只支持HTTP/2 app.http.server.configuration.supportVersions = [.two] ``` -The HTTP server supports several configuration options. +HTTP服务器支持几个配置选项。 -### Hostname +### 主机名 -The hostname controls which address the server will accept new connections on. The default is `127.0.0.1`. +主机名控制服务器接受新连接的地址。默认是127.0.0.1。 ```swift -// Configure custom hostname. +// 配置自定义主机名。 app.http.server.configuration.hostname = "dev.local" ``` -The server configuration's hostname can be overridden by passing the `--hostname` (`-H`) flag to the `serve` command or by passing the `hostname` parameter to `app.server.start(...)`. +服务器配置的主机名可以通过向`serve`命令传递`--主机名`(`-H`)标志或向`app.server.start(...)`传递`主机名`参数来覆盖。 ```sh -# Override configured hostname. +# 覆盖配置的主机名。 vapor run serve --hostname dev.local ``` -### Port +### 端口 -The port option controls which port at the specified address the server will accept new connections on. The default is `8080`. +端口选项控制服务器在指定地址的哪个端口接受新的连接。默认是`8080`。 ```swift -// Configure custom port. +// 配置自定义端口。 app.http.server.configuration.port = 1337 ``` -!!! info - `sudo` may be required for binding to ports less than `1024`. Ports greater than `65535` are not supported. +!!! 信息 + 绑定小于`1024`的端口可能需要`sudo`。不支持大于`65535`的端口。 -The server configuration's port can be overridden by passing the `--port` (`-p`) flag to the `serve` command or by passing the `port` parameter to `app.server.start(...)`. +服务器配置的端口可以通过向`serve`命令传递`--port`(`-p`)标志或向`app.server.start(..)`传递`port`参数来覆盖。 ```sh -# Override configured port. +# 覆盖配置的端口。 vapor run serve --port 1337 ``` -### Backlog +### 积压 -The `backlog` parameter defines the maximum length for the queue of pending connections. The default is `256`. +参数`backlog`定义了等待连接队列的最大长度。默认值是`256`。 ```swift -// Configure custom backlog. +// 配置自定义backlog. app.http.server.configuration.backlog = 128 ``` -### Reuse Address +### 重用地址 -The `reuseAddress` parameter allows for reuse of local addresses. Defaults to `true`. +`reuseAddress`参数允许重复使用本地地址。默认为`true`。 ```swift -// Disable address reuse. +// 禁用地址重用。 app.http.server.configuration.reuseAddress = false ``` ### TCP No Delay -Enabling the `tcpNoDelay` parameter will attempt to minimize TCP packet delay. Defaults to `true`. +启用`tcpNoDelay`参数将试图最小化TCP数据包的延迟。默认值为`true`。 ```swift -// Minimize packet delay. +// 尽量减少数据包延迟。 app.http.server.configuration.tcpNoDelay = true ``` -### Response Compression +### 响应压缩 -The `responseCompression` parameter controls HTTP response compression using gzip. The default is `.disabled`. +`responseCompression`参数控制HTTP响应的压缩,使用gzip。默认是`.disabled`。 ```swift -// Enable HTTP response compression. +// 启用HTTP响应压缩. app.http.server.configuration.responseCompression = .enabled ``` -To specify an initial buffer capacity, use the `initialByteBufferCapacity` parameter. +要指定一个初始缓冲区容量,请使用`initialByteBufferCapacity`参数。 ```swift .enabled(initialByteBufferCapacity: 1024) ``` -### Request Decompression +### 请求解压 -The `requestDecompression` parameter controls HTTP request decompression using gzip. The default is `.disabled`. +`requestDecompression`参数控制HTTP请求使用gzip进行解压。默认是`.disabled`。 ```swift -// Enable HTTP request decompression. +// 启用HTTP请求解压。 app.http.server.configuration.requestDecompression = .enabled ``` -To specify a decompression limit, use the `limit` parameter. The default is `.ratio(10)`. +要指定一个解压限制,使用`limit`参数。默认是`.ratio(10)`。 ```swift -// No decompression size limit +// 没有解压大小限制 .enabled(limit: .none) ``` -Available options are: +可用的选项是。 -- `size`: Maximum decompressed size in bytes. -- `ratio`: Maximum decompressed size as ratio of compressed bytes. -- `none`: No size limits. +- `size`:以字节为单位的最大解压尺寸。 +- `ratio`:最大解压大小与压缩字节数的比率。 +- `none`:没有大小限制。 -Setting decompression size limits can help prevent maliciously compressed HTTP requests from using large amounts of memory. +设置解压大小限制可以帮助防止恶意压缩的HTTP请求使用大量的内存。 ### Pipelining -The `supportPipelining` parameter enables support for HTTP request and response pipelining. The default is `false`. +`supportPipelining`参数允许支持HTTP请求和响应的管道化。默认是`false`. ```swift -// Support HTTP pipelining. +// 支持HTTP管道化. app.http.server.configuration.supportPipelining = true ``` -### Versions +### 版本 -The `supportVersions` parameter controls which HTTP versions the server will use. By default, Vapor will support both HTTP/1 and HTTP/2 when TLS is enabled. Only HTTP/1 is supported when TLS is disabled. +`supportVersions`参数控制服务器将使用哪些HTTP版本。默认情况下,当启用TLS时,Vapor将同时支持HTTP/1和HTTP/2。当TLS被禁用时,只支持HTTP/1。 ```swift -// Disable HTTP/1 support. +// 禁用HTTP/1支持。 app.http.server.configuration.supportVersions = [.two] ``` ### TLS -The `tlsConfiguration` parameter controls whether TLS (SSL) is enabled on the server. The default is `nil`. +`tlsConfiguration`参数控制服务器上是否启用TLS(SSL)。默认为`nil`。 ```swift -// Enable TLS. +// 启用TLS。 try app.http.server.configuration.tlsConfiguration = .forServer( - certificateChain: [ - .certificate(.init( - file: "/path/to/cert.pem", - format: .pem - )) - ], + certificateChain: NIOSSLCertificate.fromPEMFile("/path/to/cert.pem").map { .certificate($0) }, privateKey: .file("/path/to/key.pem") ) ``` -### Name +为了使这个配置能够编译,你需要在配置文件的顶部添加`import NIOSSL`。你也可能需要在你的Package.swift文件中把NIOSSL作为一个依赖项。 -The `serverName` parameter controls the `Server` header on outgoing HTTP responses. The default is `nil`. +### 名称 + +`serverName`参数控制HTTP响应中的`Server`头。默认为`nil`。 ```swift -// Add 'Server: vapor' header to responses. +// 在响应中添加'Server: vapor'头。 app.http.server.configuration.serverName = "vapor" ``` -## Serve Command +## Serve命令 -To start up Vapor's server, use the `serve` command. This command will run by default if no other commands are specified. +要启动Vapor的服务器,使用`serve`命令。如果没有指定其他命令,该命令将默认运行。 ```swift vapor run serve ``` -The `serve` command accepts the following parameters: +`serve`命令接受以下参数: -- `hostname` (`-H`): Overrides configured hostname. -- `port` (`-p`): Overrides configured port. -- `bind` (`-b`): Overrides configured hostname and port joined by `:`. +- `hostname` (`-H`):覆盖配置的主机名。 +- `port` (`-p`):覆盖配置的端口。 +- `bind`(`-b`):覆盖配置的主机名和端口用`:`连接。 -An example using the `--bind` (`-b`) flag: +一个使用`-bind`(`-b`)标志的例子: ```swift vapor run serve -b 0.0.0.0:80 ``` -Use `vapor run serve --help` for more information. +使用`vapor run serve --help`获得更多信息。 -The `serve` command will listen for `SIGTERM` and `SIGINT` to gracefully shutdown the server. Use `ctrl+c` (`^c`) to send a `SIGINT` signal. When the log level is set to `debug` or lower, information about the status of graceful shutdown will be logged. +`serve`命令将监听`SIGTERM`和`SIGINT`以优雅地关闭服务器。使用`ctrl+c`(`^c`)来发送`SIGINT`信号。当日志级别被设置为`debug'或更低时,关于优雅关机状态的信息将被记录下来。 -## Manual Start +## 手动启动 -Vapor's server can be started manually using `app.server`. +Vapor的服务器可以使用`app.server`手动启动。 ```swift -// Start Vapor's server. +// 启动Vapor的服务器。 try app.server.start() -// Request server shutdown. +// 要求服务器关闭。 app.server.shutdown() -// Wait for the server to shutdown. +// 等待服务器关机。 try app.server.onShutdown.wait() ``` -## Servers +## 服务器 -The server Vapor uses is configurable. By default, the built in HTTP server is used. +Vapor使用的服务器是可配置的。默认情况下,使用内置的HTTP服务器。 ```swift app.servers.use(.http) ``` -### Custom Server +### 自定义服务器 -Vapor's default HTTP server can be replaced by any type conforming to `Server`. +Vapor的默认HTTP服务器可以被任何符合`Server`的类型所取代。 ```swift import Vapor final class MyServer: Server { - ... + ... } app.servers.use { app in - MyServer() + MyServer() } ``` -Custom servers can extend `Application.Servers.Provider` for leading-dot syntax. +自定义服务器可以扩展`Application.Servers.Provider`,以获得领先的点状语法。 ```swift extension Application.Servers.Provider { static var myServer: Self { .init { $0.servers.use { app in - MyServer() + MyServer() } } } diff --git a/4.0/docs/advanced/services.md b/4.0/docs/advanced/services.md index 3d6bd8e..cb667c1 100644 --- a/4.0/docs/advanced/services.md +++ b/4.0/docs/advanced/services.md @@ -1,10 +1,10 @@ -# Services +# 服务 -Vapor's `Application` and `Request` are built to be extended by your application and third-party packages. New functionality added to these types are often called services. +Vapor的`Application`和`Request`是为你的应用和第三方包的扩展而建立的。添加到这些类型的新功能通常被称为服务。 -## Read Only +## 只读 -The simplest type of service is read-only. These services consist of computed variables or methods added to either application or request. +最简单的服务类型是只读的。这些服务由添加到应用程序或请求中的计算变量或方法组成。 ```swift import Vapor @@ -12,7 +12,7 @@ import Vapor struct MyAPI { let client: Client - func foos() -> EventLoopFuture<[String]> { ... } + func foos() async throws -> [String] { ... } } extension Request { @@ -22,15 +22,15 @@ extension Request { } ``` -Read-only services can depend on any pre-existing services, like `client` in this example. Once the extension has been added, your custom service can be used like any other property on request. +只读服务可以依赖于任何预先存在的服务,比如本例中的`client`。一旦扩展被添加,你的自定义服务可以像其他属性一样按要求使用。 ```swift req.myAPI.foos() ``` -## Writable +## 可写 -Services that need state or configuration can utilize `Application` and `Request` storage for storing data. Let's assume you want to add the following `MyConfiguration` struct to your application. +需要状态或配置的服务可以利用`Application`和`Request`存储来存储数据。让我们假设你想在你的应用程序中添加以下`MyConfiguration`结构。 ```swift struct MyConfiguration { @@ -38,7 +38,7 @@ struct MyConfiguration { } ``` -To use storage, you must declare a `StorageKey`. +要使用存储,你必须声明一个`StorageKey`。 ```swift struct MyConfigurationKey: StorageKey { @@ -46,9 +46,9 @@ struct MyConfigurationKey: StorageKey { } ``` -This is an empty struct with a `Value` typealias specifying which type is being stored. By using an empty type as the key, you can control what code is able to access your storage value. If the type is internal or private, only your code will be able to modify the associated value in storage. +这是一个空结构,有一个`Value`类型的别名,指定被存储的类型。通过使用一个空类型作为键,你可以控制哪些代码能够访问你的存储值。如果该类型是内部或私有的,那么只有你的代码能够修改存储中的相关值。 -Finally, add an extension to `Application` for getting and setting the `MyConfiguration` struct. +最后,给`Application`添加一个扩展,用于获取和设置`MyConfiguration`结构。 ```swift extension Application { @@ -63,7 +63,7 @@ extension Application { } ``` -Once the extension is added, you can use `myConfiguration` like a normal property on `Application`. +一旦扩展被添加,你就可以像使用`Application`的普通属性一样使用`myConfiguration`。 ```swift @@ -71,50 +71,50 @@ app.myConfiguration = .init(apiKey: ...) print(app.myConfiguration?.apiKey) ``` -## Lifecycle +## 生命周期 -Vapor's `Application` allows you to register lifecycle handlers. These let you hook into events such as boot and shutdown. +Vapor的`Application`允许你注册生命周期处理程序。这些处理程序可以让你钩住诸如启动和关机等事件。 ```swift -// Prints hello during boot. +// 在启动过程中打印出Hello。 struct Hello: LifecycleHandler { - // Called before application boots. + // 在应用程序启动前调用。 func willBoot(_ app: Application) throws { app.logger.info("Hello!") } } -// Add lifecycle handler. +// 添加生命周期处理程序。 app.lifecycle.use(Hello()) ``` -## Locks +## 锁定 -Vapor's `Application` includes conveniences for synchronizing code using locks. By declaring a `LockKey`, you can get a unique, shared lock to synchronize access to your code. +Vapor的`Application`包括使用锁来同步代码的便利性。通过声明一个`LockKey`,你可以得到一个唯一的、共享的锁来同步访问你的代码。 ```swift struct TestKey: LockKey { } let test = app.locks.lock(for: TestKey.self) test.withLock { - // Do something. + // 做点什么。 } ``` -Each call to `lock(for:)` with the same `LockKey` will return the same lock. This method is thread-safe. +每次调用`lock(for:)`时,使用相同的`LockKey`将返回同一个锁。这个方法是线程安全的。 -For an application-wide lock, you can use `app.sync`. +对于一个应用程序范围内的锁,你可以使用`app.sync`。 ```swift app.sync.withLock { - // Do something. + // 做点什么。 } ``` -## Request +## 请求 -Services that are intended to be used in route handlers should be added to `Request`. Request services should use the request's logger and event loop. It is important that a request stay on the same event loop or an assertion will be hit when the response is returned to Vapor. +打算在路由处理程序中使用的服务应该被添加到`Request`中。请求服务应该使用请求的记录器和事件循环。重要的是,请求应保持在同一事件循环中,否则当响应返回到Vapor时,会有一个断言被击中。 -If a service must leave the request's event loop to do work, it should make sure to return to the event loop before finishing. This can be done using the `hop(to:)` on `EventLoopFuture`. +如果一个服务必须离开请求的事件循环来进行工作,它应该确保在完成之前返回到事件循环中。这可以使用`EventLoopFuture`上的`hop(to:)`来实现。 -Request services that need access to application services, such as configurations, can use `req.application`. Take care to consider thread-safety when accessing the application from a route handler. Generally, only read operations should be performed by requests. Write operations must be protected by locks. \ No newline at end of file +需要访问应用服务的请求服务,如配置,可以使用`req.application`。当从路由处理程序访问应用程序时,要注意考虑线程安全。一般来说,只有读操作应该由请求执行。写操作必须有锁的保护。 diff --git a/4.0/docs/advanced/sessions.md b/4.0/docs/advanced/sessions.md index 9226aff..f972a95 100644 --- a/4.0/docs/advanced/sessions.md +++ b/4.0/docs/advanced/sessions.md @@ -1,58 +1,57 @@ -# Sessions +# 会话 -Sessions allow you to persist a user's data between multiple requests. Sessions work by creating and returning a unique cookie alongside the HTTP response when a new session is initialized. Browsers will automatically detect this cookie and include it in future requests. This allows Vapor to automatically restore a specific user's session in your request handler. +会话允许你在多个请求之间持续保存用户的数据。会话的工作方式是,当一个新的会话被初始化时,在HTTP响应旁边创建并返回一个独特的cookie。浏览器会自动检测这个cookie,并将其包含在未来的请求中。这允许Vapor在你的请求处理程序中自动恢复一个特定用户的会话。 -Sessions are great for front-end web applications built in Vapor that serve HTML directly to web browsers. For APIs, we recommend using stateless, [token-based authentication](../security/authentication.md) to persist user data between requests. +会话对于在Vapor中构建的直接向Web浏览器提供HTML的前端Web应用是非常好的。对于API,我们建议使用无状态的,[基于令牌的认证](../security/authentication.md)来保持用户数据在两次请求之间。 -## Configuration +## 配置 -To use sessions in a route, the request must pass through `SessionsMiddleware`. The easiest way to achieve this is by adding this middleware globally. +要在路由中使用会话,请求必须通过`SessionsMiddleware`。最简单的方法是在全局范围内添加这个中间件来实现这一点。 ```swift app.middleware.use(app.sessions.middleware) ``` -If only a subset of your routes utilize sessions, you can instead add `SessionsMiddleware` to a route group. +如果你的路由中只有一部分利用会话,你可以把`SessionsMiddleware`添加到路由组。 ```swift let sessions = app.grouped(app.sessions.middleware) ``` -The HTTP cookie generated by sessions can be configured using `app.sessions.configuration`. You can change the cookie name and declare a custom function for generating cookie values. +由会话生成的HTTP cookie可以使用`app.session.configuration`来配置。你可以改变cookie的名称,并声明一个用于生成cookie值的自定义函数。 ```swift -// Change the cookie name to "foo". +// 将cookie的名称改为 "foo"。 app.sessions.configuration.cookieName = "foo" -// Configures cookie value creation. +// 配置cookie的价值创造。 app.sessions.configuration.cookieFactory = { sessionID in .init(string: sessionID.string, isSecure: true) } ``` -By default, Vapor will use `vapor_session` as the cookie name. +默认情况下,Vapor将使用`vapor_session`作为cookie名称。 -## Drivers +## 驱动程序 -Session drivers are responsible for storing and retrieving session data by identifier. You can create custom drivers by conforming to the `SessionDriver` protocol. +会话驱动程序负责按标识符存储和检索会话数据。你可以通过符合`SessionDriver`协议来创建自定义的驱动程序。 !!! warning - The session driver should be configured _before_ adding `app.sessions.middleware` to your application. + 会话驱动应该在添加`app.session.middleware`到你的应用程序之前配置好。 +### 内存中 -### In-Memory - -Vapor utilizes in-memory sessions by default. In-memory sessions require zero configuration and do not persist between application launches which makes them great for testing. To enable in-memory sessions manually, use `.memory`: +Vapor默认使用内存中的会话。内存会话不需要任何配置,也不会在应用程序启动时持续存在,这使得它们非常适用于测试。要手动启用内存会话,请使用`.memory`。 ```swift app.sessions.use(.memory) ``` -For production use cases, take a look at the other session drivers which utilize databases to persist and share sessions across multiple instances of your app. +对于生产用例,可以看看其他的会话驱动,它们利用数据库在你的应用程序的多个实例中坚持和共享会话。 ### Fluent -Fluent includes support for storing session data in your application's database. This section assumes you have [configured Fluent](../fluent/overview.md) and can connect to a database. The first step is to enable the Fluent sessions driver. +Fluent包括支持将会话数据存储在你的应用程序的数据库中。本节假设你已经[配置了Fluent](../fluent/overview.md)并能连接到数据库。第一步是启用Fluent会话驱动。 ```swift import Fluent @@ -60,23 +59,40 @@ import Fluent app.sessions.use(.fluent) ``` -This will configure sessions to use the application's default database. To specify a specific database, pass the database's identifier. +这将把会话配置为使用应用程序的默认数据库。要指定一个特定的数据库,请传递数据库的标识符。 ```swift app.sessions.use(.fluent(.sqlite)) ``` -Finally, add `SessionRecord`'s migration to your database's migrations. This will prepare your database for storing session data in the `_fluent_sessions` schema. +最后,将`SessionRecord`的迁移添加到你的数据库的迁移中。这将为你的数据库在`_fluent_sessions`模式中存储会话数据做好准备。 ```swift app.migrations.add(SessionRecord.migration) ``` -Make sure to run your application's migrations after adding the new migration. Sessions will now be stored in your application's database allowing them to persist between restarts and be shared between multiple instances of your app. +请确保在添加新的迁移后运行你的应用程序的迁移。现在,会话将被存储在你的应用程序的数据库中,允许它们在重新启动时持续存在,并在你的应用程序的多个实例之间共享。 + +### Redis + +Redis提供了对在你配置的Redis实例中存储会话数据的支持。本节假设你已经[配置了Redis](../redis/overview.md),并且可以向Redis实例发送命令。 + +要将 Redis 用于会话,请在配置你的应用程序时选择它。 + +```swift +import Redis + +app.sessions.use(.redis) +``` + +这将配置会话以使用Redis会话驱动程序的默认行为。 + +!!! Seealso + 参考 [Redis → Sessions](../redis/sessions.md) 以了解有关 Redis 和 Sessions 的更多详细信息。 -## Session Data +## 会话数据 -Now that sessions are configured, you are ready to persist data between requests. New sessions are initialized automatically when data is added to `req.session`. The example route handler below accepts a dynamic route parameter and adds the value to `req.session.data`. +现在会话已经配置好了,你已经准备好在请求之间持续保存数据。当数据被添加到`req.session`中时,新的会话会自动被初始化。下面的示例路由处理程序接受一个动态路由参数,并将其值添加到`req.session.data`。 ```swift app.get("set", ":value") { req -> HTTPStatus in @@ -85,14 +101,14 @@ app.get("set", ":value") { req -> HTTPStatus in } ``` -Use the following request to initialize a session with the name Vapor. +使用下面的请求来初始化一个名为Vapor的会话。 ```http GET /set/vapor HTTP/1.1 content-length: 0 ``` -You should receive a response similar to the following: +你应该收到类似于以下的答复: ```http HTTP/1.1 200 OK @@ -100,9 +116,9 @@ content-length: 0 set-cookie: vapor-session=123; Expires=Fri, 10 Apr 2020 21:08:09 GMT; Path=/ ``` -Notice the `set-cookie` header has been added automatically to the response after adding data to `req.session`. Including this cookie in subsequent requests will allow access to the session data. +注意在向`req.session`添加数据后,`set-cookie`头被自动添加到响应中。在随后的请求中包括这个cookie将允许对会话数据的访问。 -Add the following route handler for accessing the name value from the session. +添加以下路由处理程序,用于访问会话中的名称值。 ```swift app.get("get") { req -> String in @@ -110,16 +126,16 @@ app.get("get") { req -> String in } ``` -Use the following request to access this route while making sure to pass the cookie value from the previous response. +使用下面的请求来访问这个路由,同时确保传递先前响应中的cookie值。 ```http GET /get HTTP/1.1 cookie: vapor-session=123 ``` -You should see the name Vapor returned in the response. You can add or remove data from the session as you see fit. Session data will be synchronized with the session driver automatically before returning the HTTP response. +你应该看到响应中返回的名称是Vapor。你可以在你认为合适的时候添加或删除会话中的数据。会话数据将在返回HTTP响应前自动与会话驱动程序同步。 -To end a session, use `req.session.destroy`. This will delete the data from the session driver and invalidate the session cookie. +要结束一个会话,使用`req.session.destroy`。这将从会话驱动中删除数据并使会话cookie无效。 ```swift app.get("del") { req -> HTTPStatus in diff --git a/4.0/docs/basics/async.md b/4.0/docs/basics/async.md index e20babd..db5ed71 100644 --- a/4.0/docs/basics/async.md +++ b/4.0/docs/basics/async.md @@ -1,18 +1,18 @@ -# Async +# 异步 -## Async Await +## 异步等待 -Swift 5.5 在语言层面上以 `async`/`await` 的形式引进了并发性。它提供了优秀的方式去处理异步在 Swift 以及 Vapor 应用中。 +Swift 5.5以`async`/`await`的形式为语言引入了并发性。这为处理Swift和Vapor应用程序中的异步代码提供了一种一流的方式。 -Vapor 是在 [SwiftNIO](https://github.com/apple/swift-nio.git) 的基础上构建的, SwiftNIO 为低层面的异步编程提供了基本类型。这些类型曾经是(现在依然是)贯穿整个 Vapor 在 `async`/`await` 到来之前。现在大部分代码可以用 `async`/`await` 编写来代替 `EventLoopFuture`。这将简化您的代码,使其更容易推理。 +Vapor建立在[SwiftNIO](https://github.com/apple/swift-nio.git)之上,它为低层异步编程提供了原始类型。在`async`/`await `出现之前,这些类型已经(并且仍然)在整个Vapor中使用。然而,大多数应用程序的代码现在可以使用`async`/`await`来编写,而不是使用`EventLoopFuture`。这将简化你的代码,使其更容易推理。 -现在大部分的 Vapor 的 APIs 同时提供 `EventLoopFuture` and `async`/`await` 两个版本供你选择。通常,你应该只选择一种编程方式在单个路由 handler 中,而不应该混用。对于应该显示控制 event loops,或者非常需要高性能的应用,应该继续使用 `EventLoopFuture` 在自定义运行器被实现之前(until custom executors are implemented)。 对于其他应用,你应该使用 `async`/`await` 因为它的好处、可读性和可维护性远远超过了任何小的性能损失。 +现在Vapor的大多数API都提供了`EventLoopFuture`和`async`/`await`两个版本,供你选择哪一个最好。一般来说,你应该在每个路由处理程序中只使用一种编程模型,而不是在你的代码中混合使用。对于需要明确控制事件循环的应用程序,或非常高性能的应用程序,你应该继续使用`EventLoopFuture`,直到自定义执行器被实现。对于其他人,你应该使用`async`/`await`,因为可读性和可维护性的好处远远超过了任何小的性能损失。 -### 迁徙到 async/await +### 迁移到async/await -为了适配 async/await 这里有几个步骤需要做。第一步,如果你使用 macOS 你必须使用 macOS 12 Monterey 或者更高以及 Xcode13.1 或者更高。 对于其他平台你需要运行 Swift5.5 或者更高,然后情确认你已经更新了所有依赖。 +迁移到async/await需要几个步骤。首先,如果使用macOS,你必须在macOS 12 Monterey或更高版本和Xcode 13.1或更高版本。对于其他平台,你需要运行Swift 5.5或更高版本。接下来,确保你已经更新了所有的依赖性。 -在你的 Package.swift, 在第一行把 swift-tools-version 设置为 5.5: +在你的Package.swift中,在文件的顶部将工具版本设置为5.5: ```swift // swift-tools-version:5.5 @@ -21,7 +21,7 @@ import PackageDescription // ... ``` -接下来,设置 platform version 为 macOS 12: +接下来,将平台版本设置为macOS 12: ```swift platforms: [ @@ -29,13 +29,13 @@ import PackageDescription ], ``` -最后 更新 `Run` 目标让它变成一个可运行的目标: +最后更新`Run`目标,将其标记为可执行目标: ```swift .executableTarget(name: "Run", dependencies: [.target(name: "App")]), ``` -注意:如果你部署在Linux环境请确保你更新到了最新的Swift版本。比如在 Heroku 或者在你的 Dockerfile。举个例子你的 Dockerfile 应该变为: +注意:如果你在Linux上部署,请确保你也在那里更新Swift的版本,例如在Heroku或你的Docker文件中。例如,你的Dockerfile应改为: ```diff -FROM swift:5.2-focal as build @@ -45,7 +45,7 @@ import PackageDescription +FROM swift:5.5-focal-slim ``` -现在你可以迁徙现存的代码。通常返回 `EventLoopFuture` 的方法现在变为返回 `async`。比如: +现在你可以迁移现有的代码了。一般来说,返回`EventLoopFuture`的函数现在是`async`。比如说: ```swift routes.get("firstUser") { req -> EventLoopFuture in @@ -58,7 +58,7 @@ routes.get("firstUser") { req -> EventLoopFuture in } ``` -现在变为: +现在变成了: ```swift routes.get("firstUser") { req async throws -> String in @@ -71,11 +71,11 @@ routes.get("firstUser") { req async throws -> String in } ``` -### 使用新旧api +### 与新旧API合作 -如果你遇到还未支持 `async`/`await` 的API,你可以调用 `.get()` 方法来返回一个 `EventLoopFuture`。 +如果你遇到的API还没有提供`async`/`await`版本,你可以在返回`EventLoopFuture`的函数上调用`.get()`来转换。 -比如 +例如。 ```swift return someMethodCallThatReturnsAFuture().flatMap { futureResult in @@ -83,19 +83,19 @@ return someMethodCallThatReturnsAFuture().flatMap { futureResult in } ``` -可以变为 +可以成为 ```swift let futureResult = try await someMethodThatReturnsAFuture().get() ``` -如果你需要反过来,你可以把 +如果你需要反其道而行之,你可以转换 ```swift let myString = try await someAsyncFunctionThatGetsAString() ``` -变为 +为 ```swift let promise = request.eventLoop.makePromise(of: String.self) @@ -105,148 +105,147 @@ promise.completeWithTask { let futureString: EventLoopFuture = promise.futureResult ``` -## `EventLoopFuture` +## 事件循环功能 -你可能已经注意到在 Vapor 中一些API返回一个 `EventLoopFuture` 的泛型。如果这是你第一次听到这个特性,它们一开始可能看起来有点令人困惑。但是别担心这个手册会教你怎么利用这些强大的API。 +你可能已经注意到Vapor的一些API期望或返回一个通用的`EventLoopFuture`类型。如果这是你第一次听到期货,它们一开始可能会有点混乱。但不要担心,本指南将告诉你如何利用它们强大的API。 -Promises 和 futures 是相关的, 但是截然不同的类型。 +许诺和期货是相关的,但又是不同的类型。许诺用于_创建_期货。大多数情况下,你将使用Vapor的API返回的期货,你不需要担心创建诺言。 -|类型|描述|是否可修改| +|类型|描述|可变性| |-|-|-| -|`EventLoopFuture`|代表一个现在还不可用的值|read-only| -|`EventLoopPromise`|一个可以异步提供值的promise|read/write| +|`EventLoopFuture`|对一个可能还不能使用的值的引用。|只读| +|`EventLoopPromise`|一个异步提供一些值的承诺。|读/写| - -Futures 是基于回调的异步api的替代方案。可以以简单的闭包所不能的方式进行链接和转换。 +Futures是基于回调的异步API的替代品。期货可以以简单闭包的方式进行链式和转换。 ## 转换 -就像Swift中的可选选项和数组一样,futures 可以被映射和平映射。这些是你在 futures 中最基本的操作。 +就像Swift中的选项和数组一样,期货可以被map和flatMap。这些是你在期货上最常执行的操作。 -|method|argument|description| +|方法|参数|描述| |-|-|-| -|[`map`](#map)|`(T) -> U`|Maps a future value to a different value.| -|[`flatMapThrowing`](#flatmapthrowing)|`(T) throws -> U`|Maps a future value to a different value or an error.| -|[`flatMap`](#flatmap)|`(T) -> EventLoopFuture`|Maps a future value to different _future_ value.| -|[`transform`](#transform)|`U`|Maps a future to an already available value.| +|[`map`](#map)|`(T) -> U`|将一个未来值映射到一个不同的值。| +|[`flatMapThrowing`](#flatmapthrowing)|`(T) throws -> U`|将一个未来值映射到一个不同的值或一个错误。| +|[`flatMap`](#flatmap)|`(T) -> EventLoopFuture`|将一个未来值映射到不同的未来值。| +|[`transform`](#transform)|`U`|将一个未来映射到一个已经可用的值。| -如果你看一下 `map` 和 `flatMap` 在 `Optional` 和 `Array` 中的方法签名(method signatures)。你会看到他们和在 `EventLoopFuture` 中的方法非常相似。 +如果你看一下`Optional`和`Array`上的`map`和`flatMap`的方法签名,你会发现它们与`EventLoopFuture`上的方法非常相似。 ### map -`map` 方法允许你把一个未来值转换成另外一个值。 因为这个未来的值可能现在还不可用,我们必须提供一个闭包来接受它的值。 +`map`方法允许你将未来的值转换为另一个值。因为未来的值可能还不可用(它可能是一个异步任务的结果),我们必须提供一个闭包来接受这个值。 ```swift -/// 假设我们将来从某些API得到一个字符串。 +/// 假设我们从某个API得到一个未来的字符串 let futureString: EventLoopFuture = ... -/// 把这个字符串转换成整形 +/// 将未来的字符串映射为一个整数 let futureInt = futureString.map { string in print(string) // The actual String return Int(string) ?? 0 } -/// We now have a future integer +/// 我们现在有了一个未来的整数 print(futureInt) // EventLoopFuture ``` ### flatMapThrowing -`flatMapThrowing` 方法允许你把一个未来值转换成另一个值或者抛出一个错误。 +`flatMapThrowing`方法允许你将未来的值转换为另一个值或者抛出一个错误。 -!!! 信息 - 因为抛出错误必须在内部创建一个新的future,所以这个方法前缀为 `flatMap`,即使闭包不接受future返回。 +!!! info + 因为抛出一个错误必须在内部创建一个新的未来值,所以这个方法的前缀是`flatMap`,尽管闭包不接受未来的返回。 ```swift -/// 假设我们将来从某些API得到一个字符串。 +/// 假设我们从某个API得到一个未来的字符串 let futureString: EventLoopFuture = ... -/// 把这个字符串转换成整形 +/// 将未来的字符串映射为一个整数 let futureInt = futureString.flatMapThrowing { string in - print(string) // The actual String - // 将字符串转换为整数或抛出错误 + print(string) // 实际的字符串 + // 将字符串转换为整数,否则抛出一个错误 guard let int = Int(string) else { throw Abort(...) } return int } -/// We now have a future integer +/// 我们现在有了一个未来的整数 print(futureInt) // EventLoopFuture ``` ### flatMap -flatMap方法允许你将未来值转换为另一个未来值。它得到的名称“扁平”映射,因为它允许你避免创建嵌套的未来(例如,`EventLoopFuture>`)。换句话说,它帮助您保持泛型平坦。 +`flatMap`方法允许你将future的值转换为另一个未来的值。它之所以被称为"flat"映射,是因为它可以让你避免创建嵌套的期货(例如,`EventLoopFuture>`)。换句话说,它可以帮助你保持你的泛型的扁平。 ```swift -/// Assume we get a future string back from some API +/// 假设我们从某个API中得到一个未来的字符串回来 let futureString: EventLoopFuture = ... -/// Assume we have created an HTTP client +/// 假设我们已经创建了一个HTTP客户端 let client: Client = ... -/// flatMap the future string to a future response +/// 将未来字符串flatMap到未来响应上 let futureResponse = futureString.flatMap { string in client.get(string) // EventLoopFuture } -/// We now have a future response +/// 我们现在有了一个未来的回应 print(futureResponse) // EventLoopFuture ``` -!!! 信息 - 如果我们在上面的例子中使用 `map`,我们将会得到: `EventLoopFuture>`。 +!!! info + 如果我们在上面的例子中改用`map`,我们就会变成:`EventLoopFuture>`。 -要在 `flatMap` 中调用一个抛出方法,使用Swift的 `do` / `catch` 关键字并创建一个[completed future](#makefuture)。 -To call a throwing method inside of a `flatMap`, use Swift's `do` / `catch` keywords and create a [completed future](#makefuture). +要在`flatMap`中调用一个抛出方法,请使用Swift的`do`/`catch`关键字,并创建一个[已完成的未来](#makefuture)。 ```swift -/// Assume future string and client from previous example. +/// 假设前面的例子中的未来字符串和客户端。 let futureResponse = futureString.flatMap { string in let url: URL do { - // Some synchronous throwing method. + // 一些同步抛出的方法。 url = try convertToURL(string) } catch { - // Use event loop to make pre-completed future. + // 使用事件循环来制作预完成的未来。 return eventLoop.makeFailedFuture(error) } return client.get(url) // EventLoopFuture } ``` -### transform -`transform` 方法允许您修改 future 的值,而忽略现有值。这对于转换 `EventLoopFuture` 的结果特别有用,在这种情况下未来的实际值并不重要。 +### 转换 + +`transform`方法允许你修改一个future的值,忽略现有的值。这对于转换`EventLoopFuture`的结果特别有用,因为未来的实际值并不重要。 -!!! 提示 - `EventLoopFuture`, 有时也被称为信号,它的唯一目的是通知您某些异步操作的完成或失败。 +!!! tip + `EventLoopFuture`,有时也称为信号,是一个未来,其唯一目的是通知你一些异步操作的完成或失败。 ```swift -/// Assume we get a void future back from some API +/// 假设我们从某个API中得到一个无效的未来。 let userDidSave: EventLoopFuture = ... -/// Transform the void future to an HTTP status +/// 将无效的未来转换为HTTP状态 let futureStatus = userDidSave.transform(to: HTTPStatus.ok) print(futureStatus) // EventLoopFuture ``` -即使我们提供了一个已经可用的值为 `transform`,它仍然是一个 __transformation__ 。直到所有先前的 future 都完成(或失败),future 才会完成。 +即使我们为`transform`提供了一个已经可用的值,这仍然是一个转换。在所有先前的期货完成(或失败)之前,该期货不会完成。 -### 链接(Chaining) +### 链式 -关于 transformations,最重要的一点是它们可以被链接起来。这允许您轻松地表示许多转换和子任务。 +期货上的转换最棒的地方是它们可以被链起来。这允许你轻松地表达许多转换和子任务。 -让我们修改上面的示例,看看如何利用链接。 +让我们修改上面的例子,看看我们如何利用链式的优势。 ```swift -/// Assume we get a future string back from some API +/// 假设我们从某个API得到一个未来的字符串 let futureString: EventLoopFuture = ... -/// Assume we have created an HTTP client +/// 假设我们已经创建了一个HTTP客户端 let client: Client = ... -/// Transform the string to a url, then to a response +/// 将字符串转换为一个网址,然后再转换为一个响应 let futureResponse = futureString.flatMapThrowing { string in guard let url = URL(string: string) else { throw Abort(.badRequest, reason: "Invalid URL string: \(string)") @@ -259,31 +258,31 @@ let futureResponse = futureString.flatMapThrowing { string in print(futureResponse) // EventLoopFuture ``` -在初始调用 map 之后,创建了一个临时的 `EventLoopFuture`。然后,这个future立即平映射(flat-mapped)到 `EventLoopFuture` +在初始调用map后,有一个临时的`EventLoopFuture`被创建。然后这个未来会被立即平铺到一个`EventLoopFuture`中。 ## Future -让我们看看使用 `EventLoopFuture` 的一些其他方法。 +让我们来看看其他一些使用`EventLoopFuture`的方法。 ### makeFuture -You can use an event loop to create pre-completed future with either the value or an error. +你可以使用一个事件循环来创建预完成的未来,其中包括值或错误。 ```swift -// Create a pre-succeeded future. +// 创建一个预成功的未来。 let futureString: EventLoopFuture = eventLoop.makeSucceededFuture("hello") -// Create a pre-failed future. +// 创建一个预先失败的未来。 let futureString: EventLoopFuture = eventLoop.makeFailedFuture(error) ``` ### whenComplete -你可以使用 `whenComplete` 来添加一个回调函数,它将在未来的成功或失败时执行。 +你可以使用`whenComplete`来添加一个回调,在未来成功或失败时执行。 ```swift -/// Assume we get a future string back from some API +/// 假设我们从某个API得到一个Future的字符串 let futureString: EventLoopFuture = ... futureString.whenComplete { result in @@ -297,72 +296,71 @@ futureString.whenComplete { result in ``` !!! note - 您可以向 future 添加任意数量的回调。 + 你可以为一个未来添加任意多的回调。 ### Wait -您可以使用 `.wait()` 来同步等待future完成。由于future可能会失败,这个调用是可抛出错误的。 +你可以使用`.wait()`来同步地等待未来的完成。由于未来可能会失败,这个调用是抛出的。 ```swift -/// Assume we get a future string back from some API +/// 假设我们从某个API得到一个Future的字符串 let futureString: EventLoopFuture = ... -/// Block until the string is ready +/// 阻断,直到字符串准备好 let string = try futureString.wait() print(string) /// String ``` -`wait()` 只能在后台线程或主线程中使用,也就是在 `configure.swift` 中。它不能在事件循环线程(event loop)上使用,也就是在路由闭包中。 +`wait()`只能在后台线程或主线程上使用,即在`configure.swift`中。它不能在事件循环线程上使用,即在路由关闭中。 -!!! 警告 - 试图在事件循环线程上调用 `wait()` 将导致断言失败。 +!!! warning + 试图在一个事件循环线程上调用`wait()`将导致断言失败。 ## Promise -大多数时候,您将转换 Vapor 的 api 返回的 futures。然而,在某些情况下,你可能需要创造自己的 promise。 +大多数情况下,你将转换调用Vapor的API所返回的期货。然而,在某些时候,你可能需要创建一个自己的承诺。 -要创建一个 promise,你需要访问一个 `EventLoop`。你可以根据上下文(context)从 `Application` 或 `Request` 获得一个 event loop。 +要创建一个承诺,你需要访问一个`EventLoop`。根据上下文,你可以从`Application`或`Request`获得对事件循环的访问。 ```swift let eventLoop: EventLoop -// Create a new promise for some string. +// 为某个字符串创建一个新的Promise。 let promiseString = eventLoop.makePromise(of: String.self) print(promiseString) // EventLoopPromise print(promiseString.futureResult) // EventLoopFuture -// Completes the associated future. +// 完成相关的未来。 promiseString.succeed("Hello") -// Fails the associated future. +// 失败的相关未来。 promiseString.fail(...) ``` !!! info - 一个 promise 只能 completed 一次。任何后续的 completions 都将被忽略。 + 一个承诺只能被完成一次。任何后续的完成都将被忽略。 -promises 可以从任何线程 completed(`succeed` / `fail`)。这就是为什么 promises 需要初始化一个 event loop。promises 确保完成操作(completion action)返回到其 event loop 中执行。 +承诺可以从任何线程完成(`成功`/`失败`)。这就是为什么承诺需要一个事件循环被初始化。许诺确保完成动作被返回到其事件循环中执行。 -## Event Loop +## 事件循环 -当应用程序启动时,它通常会为运行它的CPU中的每个核心创建一个 event loop。每个 event loop 只有一个线程。如果您熟悉 Node.js 中的 event loops,那么 Vapor 中的 event loop也是类似的。主要的区别是 Vapor 可以在一个进程(process)中运行多个 event loop,因为 Swift 支持多线程。 +当你的应用程序启动时,它通常会为它所运行的CPU的每个核心创建一个事件循环。每个事件循环正好有一个线程。如果你熟悉Node.js中的事件循环,Vapor中的事件循环是类似的。主要区别在于,Vapor可以在一个进程中运行多个事件循环,因为Swift支持多线程。 -每次客户端连接到服务器时,它将被分配给一个event loops。从这时候开始,服务器和客户端之间的所有通信都将发生在同一个 event loop 上(通过关联,该 event loop 的线程)。 +每次客户端连接到你的服务器时,它将被分配到其中一个事件循环。从那时起,服务器和该客户之间的所有通信都将发生在同一个事件循环上(以及通过关联,该事件循环的线程)。 -event loop 负责跟踪每个连接的客户机的状态。如果客户端有一个等待读取的请求,event loop 触发一个读取通知,然后数据被读取。一旦读取了整个请求,等待该请求数据的任何 futures 都将完成。 +事件循环负责跟踪每个连接的客户端的状态。如果有一个来自客户端的请求等待被读取,事件循环会触发一个读取通知,导致数据被读取。一旦整个请求被读取,任何等待该请求数据的期货将被完成。 -在路由闭包中,你可以通过 `Request` 访问当前事件循环。 +在路由闭包中,你可以通过`Request`访问当前的事件循环。 ```swift req.eventLoop.makePromise(of: ...) ``` !!! warning - Vapor 预期路由闭包(route closures)将保持在 `req.eventLoop` 上。如果您跳转线程,您必须确保对`Request`的访问和最终的响应都发生在请求的 event loop 中。 + Vapor期望路由关闭将停留在`req.eventLoop`上。如果你跳线程,你必须确保对`Request`的访问和最终的响应未来都发生在请求的事件循环上。 -在路由闭包(route closures)之外,你可以通过 `Application` 获得一个可用的event loops。 -Outside of route closures, you can get one of the available event loops via `Application`. +在路由关闭之外,你可以通过`Application`获得可用的事件循环之一。 ```swift app.eventLoopGroup.next().makePromise(of: ...) @@ -370,7 +368,7 @@ app.eventLoopGroup.next().makePromise(of: ...) ### hop -你可以通过 `hop` 来改变一个 future 的 event loop。 +你可以使用`hop`来改变一个未来的事件循环。 ```swift futureString.hop(to: otherEventLoop) @@ -378,54 +376,54 @@ futureString.hop(to: otherEventLoop) ## Blocking -在 event loop 线程上调用阻塞代码会阻止应用程序及时响应传入请求。阻塞调用的一个例子是' libc.sleep(_:) '。 +在事件循环线程上调用阻塞代码会使你的应用程序无法及时响应传入的请求。阻塞调用的一个例子是`libc.sleep(_:)`之类的。 ```swift app.get("hello") { req in - /// Puts the event loop's thread to sleep. + /// 让事件循环的线程进入睡眠状态。 sleep(5) - /// Returns a simple string once the thread re-awakens. + /// 一旦线程重新唤醒,返回一个简单的字符串。 return "Hello, world!" } ``` -`sleep(_:)` 是一个命令,用于阻塞当前线程的秒数。如果您直接在 event loop 上执行这样的阻塞工作,event loop 将无法在阻塞工作期间响应分配给它的任何其他客户端。换句话说,如果你在一个 event loop 上调用 `sleep(5)`,所有连接到该 event loop 的其他客户端(可能是数百或数千)将延迟至少5秒。 +`sleep(_:)`是一个命令,它可以阻断当前线程所提供的秒数。如果你直接在事件循环上做这样的阻塞工作,那么在阻塞工作期间,事件循环将无法响应分配给它的任何其他客户端。换句话说,如果你在一个事件循环上做`sleep(5)`,所有连接到该事件循环的其他客户端(可能是成百上千)将被延迟至少5秒。 -确保在后台运行任何阻塞工作。当这项工作以非阻塞方式完成时,使用 promises 来通知 event loop。 +确保在后台运行任何阻塞性工作。当这项工作以非阻塞的方式完成时,使用承诺来通知事件循环。 ```swift app.get("hello") { req -> EventLoopFuture in - /// Dispatch some work to happen on a background thread + /// 派遣一些工作在后台线程上发生 return req.application.threadPool.runIfActive(eventLoop: req.eventLoop) { - /// Puts the background thread to sleep - /// This will not affect any of the event loops + /// 让背景线程进入睡眠状态 + /// 这不会影响任何事件循环。 sleep(5) - /// When the "blocking work" has completed, - /// return the result. + /// 当"阻塞工作"完成后。 + /// 返回结果。 return "Hello world!" } } ``` -并不是所有的阻塞调用都像 `sleep(_:)` 那样明显。如果你怀疑你正在使用的调用可能是阻塞的,研究方法本身或询问别人。下面的部分将更详细地讨论方法如何阻塞。 +并非所有的阻塞调用都会像`sleep(_:)`那样明显。如果你怀疑你正在使用的某个调用可能是阻塞的,请研究该方法本身或询问他人。下面的章节将更详细地介绍方法如何阻塞。 -### I/O 约束 +### I/O绑定 -I/O 约束阻塞意味着等待较慢的资源,如网络或硬盘,这些资源可能比 CPU 慢几个数量级。在等待这些资源时阻塞 CPU 会导致时间的浪费。 +I/O绑定阻塞是指在网络或硬盘等慢速资源上等待,这些资源的速度可能比CPU慢几个数量级。在你等待这些资源的时候阻塞CPU会导致时间的浪费。 !!! danger - 不要在事件循环中直接进行阻塞I/O约束调用. + 永远不要在事件循环上直接进行阻塞式I/O绑定调用。 -所有的 Vapor 包都构建在 SwiftNIO 上,并使用非阻塞 I/O。然而,现在有很多 Swift 包和 C 库使用了阻塞 I/O。如果一个函数正在进行磁盘或网络 IO 并使用同步 API (没有使用 callbacks 或 future),那么它很有可能是阻塞的。 +Vapor的所有软件包都建立在SwiftNIO之上,使用非阻塞式I/O。然而,在野外有许多Swift包和C库都使用阻塞式I/O。如果一个函数正在进行磁盘或网络IO,并且使用同步API(没有回调或期货),它很可能是阻塞的。 -### CPU 约束 +### CPU绑定 -请求期间的大部分时间都花在等待数据库查询和网络请求等外部资源加载上。因为 Vapor 和 SwiftNIO 是非阻塞的,所以这种停机时间可以用于满足其他传入请求。然而,应用程序中的一些路由可能需要执行大量 CPU 约束的工作。 +请求期间的大部分时间都是在等待外部资源,如数据库查询和网络请求的加载。因为Vapor和SwiftNIO是非阻塞的,所以这段停机时间可以用来满足其他传入的请求。然而,你的应用程序中的一些路由可能需要做繁重的CPU绑定工作,作为请求的结果。 -当 event loop 处理CPU约束的工作时,它将无法响应其他传入请求。这通常是没问题的,因为CPU是快速的,大多数CPU工作是轻量级的web应用程序。但是,如果需要大量CPU资源的路由阻止了对更快路由的请求的快速响应,这就会成为一个问题。 +当一个事件循环正在处理CPU约束的工作时,它将无法响应其他传入的请求。这通常是好的,因为CPU是快速的,而且大多数网络应用的CPU工作是轻量级的。但是,如果有长期运行的CPU工作的路由阻碍了对快速路由的请求的快速响应,这就会成为一个问题。 -识别应用程序中长时间运行的CPU工作,并将其转移到后台线程,可以帮助提高服务的可靠性和响应能力。与I/O约束的工作相比,CPU约束的工作更多的是一个灰色区域,最终由您决定在哪里划定界限。 +识别你的应用程序中长期运行的CPU工作,并将其转移到后台线程,可以帮助改善你的服务的可靠性和响应性。与I/O绑定的工作相比,CPU绑定的工作更像是一个灰色地带,最终由你来决定你想在哪里划清界限。 -大量CPU约束工作的一个常见示例是用户注册和登录期间的Bcrypt哈希。出于安全原因,Bcrypt被故意设置为非常慢和CPU密集型。这可能是一个简单的web应用程序所做的最耗费CPU的工作。将哈希移到后台线程可以允许CPU在计算哈希时交错事件循环工作,从而获得更高的并发性。 +在用户注册和登录过程中,Bcrypt的散列工作是一个常见的CPU约束工作的例子。为了安全起见,Bcrypt故意做得很慢,而且CPU密集。这可能是一个简单的Web应用程序实际做的最密集的CPU工作。将哈希运算转移到后台线程,可以让CPU在计算哈希运算时交错进行事件循环工作,从而获得更高的并发性。 diff --git a/4.0/docs/basics/client.md b/4.0/docs/basics/client.md index 3cd5132..e1f2acd 100644 --- a/4.0/docs/basics/client.md +++ b/4.0/docs/basics/client.md @@ -1,6 +1,6 @@ -# Client +# 客户端 -Vapor的 `Client` API 允许您使用 HTTP 调用外部资源,它基于 [async-http-client](https://github.com/swift-server/async-http-client) 构建,并集成了 [Content](./content.md) API。 +Vapor的 `Client` API 允许您使用 HTTP 调用外部资源,它基于 [async-http-client](https://github.com/swift-server/async-http-client) 构建,并集成了 [内容](./content.md) API。 ## 概述 diff --git a/4.0/docs/basics/controllers.md b/4.0/docs/basics/controllers.md index fd20780..4d2dfaa 100644 --- a/4.0/docs/basics/controllers.md +++ b/4.0/docs/basics/controllers.md @@ -59,7 +59,7 @@ struct TodosController: RouteCollection { `Controller` 的方法接受 `Request` 参数,并返回 `ResponseEncodable` 对象。该方法可以是异步或者同步(或者返回一个 `EventLoopFuture`) !!! 注意 - [EventLoopFuture](async.md) 期望返回值为 `ResponseEncodable` (i.e, `EventLoopFuture`) 或 `ResponseEncodable`. + [EventLoopFuture](./async.md) 期望返回值为 `ResponseEncodable` (i.e, `EventLoopFuture`) 或 `ResponseEncodable`. 最后,你需要在 `routes.swift` 中注册 Controller: diff --git a/4.0/docs/basics/errors.md b/4.0/docs/basics/errors.md index 6e9d70c..616381d 100644 --- a/4.0/docs/basics/errors.md +++ b/4.0/docs/basics/errors.md @@ -1,20 +1,20 @@ -# Errors +# 错误 -Vapor builds on Swift's `Error` protocol for error handling. Route handlers can either `throw` an error or return a failed `EventLoopFuture`. Throwing or returning a Swift `Error` will result in a `500` status response and the error will be logged. `AbortError` and `DebuggableError` can be used to change the resulting response and logging respectively. The handling of errors is done by `ErrorMiddleware`. This middleware is added to the application by default and can be replaced with custom logic if desired. +Vapor基于Swift的`Error`协议进行错误处理。路由处理程序可以`throw`一个错误或返回一个失败的`EventLoopFuture`。抛出或返回一个Swift的`Error`将导致一个`500`状态响应,并且错误将被记录下来。`AbortError`和`DebuggableError`可以分别用来改变结果响应和记录。错误的处理是由`ErrorMiddleware`完成的。这个中间件默认被添加到应用程序中,如果需要的话,可以用自定义的逻辑来代替。 -## Abort +## 终止 -Vapor provides a default error struct named `Abort`. This struct conforms to both `AbortError` and `DebuggableError`. You can initialize it with an HTTP status and optional failure reason. +Vapor提供了一个默认的错误结构,名为`Abort`。这个结构同时符合`AbortError`和`DebuggableError`。你可以用一个HTTP状态和可选的失败原因来初始化它。 ```swift -// 404 error, default "Not Found" reason used. +// 404错误, 默认使用`未找到`原因. throw Abort(.notFound) -// 401 error, custom reason used. +// 401错误,使用自定义原因。 throw Abort(.unauthorized, reason: "Invalid Credentials") ``` -In old asynchronous situations where throwing is not supported and you must return an `EventLoopFuture`, like in a `flatMap` closure, you can return a failed future. +在旧的异步情况下,不支持抛出,你必须返回一个`EventLoopFuture`,比如在`flatMap`闭包中,你可以返回一个失败的future。 ```swift guard let user = user else { @@ -23,7 +23,7 @@ guard let user = user else { return user.save() ``` -Vapor includes a helper extension for unwrapping futures with optional values: `unwrap(or:)`. +Vapor包括一个辅助扩展,用于解包带有可选值的值:`unwrap(or:)`。 ```swift User.find(id, on: db) @@ -34,7 +34,7 @@ User.find(id, on: db) } ``` -If `User.find` returns `nil`, the future will be failed with the supplied error. Otherwise, the `flatMap` will be supplied with a non-optional value. If using `async`/`await` then you can handle optionals as normal: +如果`User.find`返回`nil`,未来将以提供的错误而失败。否则,`flatMap`将被提供一个非选择的值。如果使用`async`/`await`,那么你可以像平常一样处理选项。 ```swift guard let user = try await User.find(id, on: db) { @@ -43,11 +43,11 @@ guard let user = try await User.find(id, on: db) { ``` -## Abort Error +## 终止错误 -By default, any Swift `Error` thrown or returned by a route closure will result in a `500 Internal Server Error` response. When built in debug mode, `ErrorMiddleware` will include a description of the error. This is stripped out for security reasons when the project is built in release mode. +默认情况下,任何由路由闭包抛出或返回的Swift`Error`将导致`500 Internal Server Error`响应。当以调试模式构建时,`ErrorMiddleware`将包括错误的描述。当项目在发布模式下构建时,出于安全考虑,这将被剥离出来。 -To configure the resulting HTTP response status or reason for a particular error, conform it to `AbortError`. +要配置一个特定错误的HTTP响应状态或原因,请将其与`AbortError`相符合。 ```swift import Vapor @@ -78,13 +78,13 @@ extension MyError: AbortError { } ``` -## Debuggable Error +## 可调试的错误 -`ErrorMiddleware` uses the `Logger.report(error:)` method for logging errors thrown by your routes. This method will check for conformance to protocols like `CustomStringConvertible` and `LocalizedError` to log readable messages. +`ErrorMiddleware`使用`Logger.report(error:)`方法来记录由你的路由抛出的错误。该方法将检查是否符合`CustomStringConvertible`和`LocalizedError`等协议,以记录可读信息。 -To customize error logging, you can conform your errors to `DebuggableError`. This protocol includes a number of helpful properties like a unique identifier, source location, and stack trace. Most of these properties are optional which makes adopting the conformance easy. +为了定制错误日志,你可以将你的错误与`DebuggableError`相符合。这个协议包括一些有用的属性,如唯一的标识符、源位置和堆栈跟踪。这些属性大多是可选的,这使得采用一致性很容易。 -To best conform to `DebuggableError`, your error should be a struct so that it can store source and stack trace information if needed. Below is an example of the aforementioned `MyError` enum updated to use a `struct` and capture error source information. +为了最好地符合`DebuggableError`,你的错误应该是一个结构,这样它就可以在需要时存储源和堆栈跟踪信息。下面是前面提到的`MyError`枚举的例子,它被更新为使用`struct`并捕获错误源信息。 ```swift import Vapor @@ -134,23 +134,23 @@ struct MyError: DebuggableError { } ``` -`DebuggableError` has several other properties like `possibleCauses` and `suggestedFixes` that you can use to improve the debuggability of your errors. Take a look at the protocol itself for more information. +`DebuggableError`有几个其他属性,如`possibleCauses`和`suggestedFixes`,你可以用它们来提高错误的调试性。看一下协议本身,了解更多信息。 -## Stack Traces +## 堆栈跟踪 -Vapor includes support for viewing stack traces for both normal Swift errors and crashes. +Vapor包括对查看Swift正常错误和崩溃的堆栈跟踪的支持。 ### Swift Backtrace -Vapor uses the [SwiftBacktrace](https://github.com/swift-server/swift-backtrace) library to provide stack traces after a fatal error or assertion on Linux. In order for this to work, your app must include debug symbols during compilation. +Vapor使用[SwiftBacktrace](https://github.com/swift-server/swift-backtrace)库来提供Linux上发生致命错误或断言后的堆栈跟踪。为了使其发挥作用,你的应用程序必须在编译时包含调试符号。 ```sh swift build -c release -Xswiftc -g ``` -### Error Traces +### 错误跟踪 -By default, `Abort` will capture the current stack trace when initialized. Your custom error types can achieve this by conforming to `DebuggableError` and storing `StackTrace.capture()`. +默认情况下, `Abort`在初始化时将捕获当前的堆栈跟踪。你的自定义错误类型可以通过符合`DebuggableError`和存储`StackTrace.capture()`来实现。 ```swift import Vapor @@ -172,26 +172,26 @@ struct MyError: DebuggableError { } ``` -When your application's [log level](logging.md#level) is set to `.debug` or lower, error stack traces will be included in log output. +当你的应用程序的[日志级别](./logging.md#level)被设置为`.debug`或更低,错误的堆栈痕迹将被包含在日志输出中。 -Stack traces will not be captured when the log level is greater than `.debug`. To override this behavior, set `StackTrace.isCaptureEnabled` manually in `configure`. +当日志级别大于`.debug`时,堆栈跟踪将不会被捕获。要覆盖这一行为,请在`configure`中手动设置`StackTrace.isCaptureEnabled`。 ```swift -// Always capture stack traces, regardless of log level. +// 始终捕获堆栈跟踪,无论日志级别如何。 StackTrace.isCaptureEnabled = true ``` -## Error Middleware +## 错误中间件 -`ErrorMiddleware` is the only middleware added to your application by default. This middleware converts Swift errors that have been thrown or returned by your route handlers into HTTP responses. Without this middleware, errors thrown will result in the connection being closed without a response. +`ErrorMiddleware`是默认添加到你的应用程序的唯一中间件。这个中间件将你的路由处理程序抛出或返回的Swift错误转换为HTTP响应。如果没有这个中间件,抛出的错误将导致连接被关闭而没有响应。 -To customize error handling beyond what `AbortError` and `DebuggableError` provide, you can replace `ErrorMiddleware` with your own error handling logic. To do this, first remove the default error middleware by setting `app.middleware` to an empty configuration. Then, add your own error handling middleware as the first middleware to your application. +要想在`AbortError`和`DebuggableError`之外定制错误处理,你可以用你自己的错误处理逻辑替换`ErrorMiddleware`。要做到这一点,首先通过设置`app.middleware`到一个空的配置来删除默认的错误中间件。然后,添加你自己的错误处理中间件作为你的应用程序的第一个中间件。 ```swift -// Remove all existing middleware. +// 删除所有现有的中间件。 app.middleware = .init() -// Add custom error handling middleware first. +// 首先添加自定义错误处理中间件。 app.middleware.use(MyErrorMiddleware()) ``` -Very few middleware should go _before_ the error handling middleware. A notable exception to this rule is `CORSMiddleware`. +很少有中间件应该走在错误处理中间件之前。这个规则的一个明显的例外是`CORSMiddleware`。 diff --git a/4.0/docs/basics/logging.md b/4.0/docs/basics/logging.md index c5a8c1e..cf57922 100644 --- a/4.0/docs/basics/logging.md +++ b/4.0/docs/basics/logging.md @@ -47,7 +47,7 @@ logger.info(...) 尽管自定义的日志记录器仍将输出你配置的后端日志记录,但是他们没有附带重要的元数据,比如 `request` 的 `UUID`。所以尽量使用 `application` 或者 `request` 的日志记录器。 -## 日志级别 +## Level `SwiftLog` 支持多种日志级别。 diff --git a/4.0/docs/basics/routing.md b/4.0/docs/basics/routing.md index d0b3eb2..0611b3b 100644 --- a/4.0/docs/basics/routing.md +++ b/4.0/docs/basics/routing.md @@ -218,9 +218,12 @@ app.get("hello", ":name") { req -> String in } ``` -!!! 提示 +!!! Tip 我们可以确定 `req.parameters.get` 在这里绝不会返回 `nil` ,因为我们的路径包含 `:name`。 但是,如果要访问中间件中的路由参数或由多个路由触发的代码中的路由参数,则需要处理 `nil` 的可能性。 +!!! Tip + 如果你想检索URL查询参数,例如`/hello/?name=foo`,你需要使用Vapor的内容API来处理URL查询字符串中的URL编码数据。更多细节见[`内容`参考](./content.md)。 + `req.parameters.get` 还支持将参数自动转换为 `LosslessStringConvertible` 类型。 @@ -427,4 +430,4 @@ req.redirect(to: "/some/new/path", type: .permanent) * `.normal` - 返回一个 **303 see other** 重定向。这是 Vapor 的默认行为,来告诉客户端去使用一个 **GET** 请求来重定向。 * `.temporary` - 返回一个 **307 Temporary** 重定向. 这告诉客户端保留请求中使用的HTTP方法。 -> 要选择正确的重定向状态码,请参考 [the full list](https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#3xx_redirection) \ No newline at end of file +> 要选择正确的重定向状态码,请参考 [the full list](https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#3xx_redirection) diff --git a/4.0/docs/basics/validation.md b/4.0/docs/basics/validation.md index 9d8aa90..e51b3e9 100644 --- a/4.0/docs/basics/validation.md +++ b/4.0/docs/basics/validation.md @@ -1,6 +1,6 @@ -# Validation API +# 验证API -Vapor 的 **Validation API** 可帮助你在使用 [Content](content.md) API 解码数据之前,对传入的请求进行验证。 +Vapor 的 **验证API** 可帮助你在使用 [内容](./content.md) API 解码数据之前,对传入的请求进行验证。 ## 介绍 @@ -9,7 +9,7 @@ Vapor 对 Swift 的类型安全的`可编码`协议进行了深度集成,这 ### 语义可读错误 -如果取得的数据无效,使用 [Content](content.md) API 对其解码将产生错误。但是,这些错误消息有时可能缺乏可读性。例如,采用以下字符串支持的枚举: +如果取得的数据无效,使用 [内容](./content.md) API 对其解码将产生错误。但是,这些错误消息有时可能缺乏可读性。例如,采用以下字符串支持的枚举: ```swift enum Color: String, Codable { @@ -17,7 +17,7 @@ enum Color: String, Codable { } ``` -如果用户尝试将字符串“purple”传递给“Color”类型的属性,则将收到类似于以下内容的错误: +如果用户尝试将字符串`“purple”`传递给`Color`类型的属性,则将收到类似于以下内容的错误: ``` Cannot initialize Color from invalid String value purple for key favoriteColor @@ -39,7 +39,7 @@ favoriteColor is not red, blue, or green 为了验证请求,你需要生成一个 `Validations` 集合。最常见的做法是使现有类型继承 **Validatable**。 -让我们看一下如何向这个简单的 `POST/users` 请求添加验证。本指南假定你已经熟悉 [Content](content.md) API。 +让我们看一下如何向这个简单的 `POST/users` 请求添加验证。本指南假定你已经熟悉 [内容](./content.md) API。 ```swift @@ -203,4 +203,4 @@ validations.add("favoriteColor", as: String?.self,is: .nil || .in("red", "blue", |:--|:--|:--| |`!`|前面|反转验证器,要求相反| |`&&`|中间|组合两个验证器,需要同时满足| -|`||`|中间|组合两个验证器,至少满足一个| \ No newline at end of file +|`||`|中间|组合两个验证器,至少满足一个| diff --git a/4.0/docs/deploy/digital-ocean.md b/4.0/docs/deploy/digital-ocean.md index bd9c227..98d2c67 100644 --- a/4.0/docs/deploy/digital-ocean.md +++ b/4.0/docs/deploy/digital-ocean.md @@ -1,190 +1,190 @@ -# Deploying to DigitalOcean +# 部署到DigitalOcean -This guide will walk you through deploying a simple Hello, world Vapor application to a [Droplet](https://www.digitalocean.com/products/droplets/). To follow this guide, you will need to have a [DigitalOcean](https://www.digitalocean.com) account with billing configured. +本指南将指导您将一个简单的Hello, world Vapor应用程序部署到[Droplet](https://www.digitalocean.com/products/droplets/)。要遵循本指南,你将需要有一个[DigitalOcean](https://www.digitalocean.com)账户,并配置了计费。 -## Create Server +## 创建服务器 -Let's start by installing Swift on an Ubuntu server. Use the create menu to create a new Droplet. +让我们先在Linux服务器上安装Swift。使用创建菜单来创建一个新的Droplet。 ![Create Droplet](../images/digital-ocean-create-droplet.png) -Under distributions, select Ubuntu 18.04 LTS. +在发行版下,选择Ubuntu 18.04 LTS。下面的指南将以这个版本为例。 ![Ubuntu Distro](../images/digital-ocean-distributions-ubuntu-18.png) !!! note - You may select any version of Ubuntu that Swift supports. At the time of writing, Swift 5.2 supports 16.04 and 18.04. You can check which operating systems are officially supported on the [Swift Releases](https://swift.org/download/#releases) page. + 你可以选择任何有Swift支持的版本的Linux发行版。在撰写本文时,Swift 5.2.4 支持 Ubuntu 16.04、18.04、20.04、CentOS 8 和 Amazon Linux 2。你可以在 [Swift Releases](https://swift.org/download/#releases) 页面上查看官方支持哪些操作系统。 -After selecting the distribution, choose any plan and datacenter region you prefer. Then setup an SSH key to access the server after it is created. Finally, click create Droplet and wait for the new server to spin up. +在选择分布后,选择你喜欢的任何计划和数据中心区域。然后设置一个SSH密钥,在服务器创建后访问它。最后,点击创建Droplet,等待新服务器启动。 -Once the new server is ready, hover over the Droplet's IP address and click copy. +一旦新服务器准备好了,将鼠标悬停在Droplet的IP地址上,点击复制。 ![Droplet List](../images/digital-ocean-droplet-list.png) -## Initial Setup +## 初始设置 -Open your terminal and connect to the server as root using SSH. +打开你的终端,用SSH以root身份连接到服务器。 ```sh ssh root@your_server_ip ``` -DigitalOcean has an in-depth guide for [initial server setup on Ubuntu 18.04](https://www.digitalocean.com/community/tutorials/initial-server-setup-with-ubuntu-18-04). This guide will quickly cover the basics. +DigitalOcean对[Ubuntu 18.04的初始服务器设置](https://www.digitalocean.com/community/tutorials/initial-server-setup-with-ubuntu-18-04)有一个深度指南。本指南将快速涵盖基础知识。 -### Configure Firewall +### 配置防火墙 -Allow OpenSSH through the firewall and enable it. +允许OpenSSH通过防火墙并启用它。 ```sh ufw allow OpenSSH ufw enable ``` -### Add User +### 添加用户 -Create a new user besides `root`. This guide calls the new user `vapor`. +在`root`之外创建一个新的用户。本指南称这个新用户为`vapor`。 ```sh adduser vapor ``` -Allow the newly created user to use `sudo`. +允许新创建的用户使用`sudo`。 ```sh usermod -aG sudo vapor ``` -Copy the root user's authorized SSH keys to the newly created user. This will allow you to SSH in as the new user. +将根用户的授权SSH密钥复制到新创建的用户。这将使你能够以新用户的身份进行SSH登录。 ```sh rsync --archive --chown=vapor:vapor ~/.ssh /home/vapor ``` -Finally, exit the current SSH session and login as the newly created user. +最后,退出当前的SSH会话,以新创建的用户身份登录。 ```sh exit ssh vapor@your_server_ip ``` -## Install Swift +## 安装Swift -Now that you've created a new Ubuntu server and logged in as a non-root user you can install Swift. +现在你已经创建了一个新的Ubuntu服务器并以非root用户身份登录,你可以安装Swift了。 -### Swift Dependencies +### Swift的依赖性 -Install Swift's required dependencies. +安装Swift的必要依赖项。 ```sh sudo apt-get update sudo apt-get install clang libicu-dev libatomic1 build-essential pkg-config ``` -### Download Toolchain +###下载工具链 -This guide will install Swift 5.2.0. Visit the [Swift Downloads](https://swift.org/download/#releases) page for a link to latest release. Copy the download link for Ubuntu 18.04. +本指南将安装 Swift 5.2.4。请访问[Swift Releases](https://swift.org/download/#releases)页面,获取最新版本的链接。复制Ubuntu 18.04的下载链接。 ![Download Swift](../images/swift-download-ubuntu-18-copy-link.png) -Download and decompress the Swift toolchain. +下载并解压Swift工具链。 ```sh -wget https://swift.org/builds/swift-5.2-release/ubuntu1804/swift-5.2-RELEASE/swift-5.2-RELEASE-ubuntu18.04.tar.gz -tar xzf swift-5.2-RELEASE-ubuntu18.04.tar.gz +wget https://swift.org/builds/swift-5.2.4-release/ubuntu1804/swift-5.2.4-RELEASE/swift-5.2.4-RELEASE-ubuntu18.04.tar.gz +tar xzf swift-5.2.4-RELEASE-ubuntu18.04.tar.gz ``` !!! note - Swift's [Using Downloads](https://swift.org/download/#using-downloads) guide includes information on how to verify downloads using PGP signatures. + Swift的[使用下载](https://swift.org/download/#using-downloads)指南包括如何使用PGP签名验证下载的信息。 -### Install Toolchain +### 安装工具链 -Move Swift somewhere easy to acess. This guide will use `/swift` with each compiler version in a subfolder. +将 Swift 移到容易获取的地方。本指南将使用`/swift`,每个编译器版本都在一个子文件夹中。 ```sh sudo mkdir /swift -sudo mv swift-5.2-RELEASE-ubuntu18.04 /swift/5.2.0 +sudo mv swift-5.2.4-RELEASE-ubuntu18.04 /swift/5.2.4 ``` -Add Swift to `/usr/bin` so it can be executed by `vapor` and `root`. +将Swift添加到`/usr/bin`,这样它就可以被`vapor`和`root`执行。 ```sh -sudo ln -s /swift/5.2.0/usr/bin/swift /usr/bin/swift +sudo ln -s /swift/5.2.4/usr/bin/swift /usr/bin/swift ``` -Verify that Swift was installed correctly. +验证Swift的安装是否正确。 ```sh swift --version ``` -## Setup Project +## 设置项目 -Now that Swift is installed, let's clone and compile your project. For this example, we'll be using Vapor's [API template](https://github.com/vapor/api-template/). +现在Swift已经安装完毕,让我们来克隆和编译你的项目。在这个例子中,我们将使用Vapor的[API模板](https://github.com/vapor/api-template/)。 -First let's install Vapor's system dependencies. +首先让我们安装Vapor的系统依赖项。 ```sh sudo apt-get install openssl libssl-dev zlib1g-dev libsqlite3-dev ``` -Allow HTTP through the firewall. +允许HTTP通过防火墙。 ```sh sudo ufw allow http ``` -### Clone & Build +### 克隆和构建 -Now clone the project and build it. +现在克隆项目并构建它。 ```sh git clone https://github.com/vapor/api-template.git cd api-template -swift build +swift build --enable-test-discovery ``` !!! tip - If you are building this project for production, use `swift build -c release` + 如果你要为生产构建这个项目,请使用`swift build -c release --enable-test-discovery`。 -### Run +### 运行 -Once the project has finished compiling, run it on your server's IP at port 80. +一旦项目完成编译,在你的服务器IP上以80端口运行。本例中的IP地址是`157.245.244.228`。 ```sh sudo .build/debug/Run serve -b 157.245.244.228:80 ``` -If you used `swift build -c release`, then you need to run: +如果你使用`swift build -c release --enable-test-discovery`,那么你需要运行。 ```sh sudo .build/release/Run serve -b 157.245.244.228:80 ``` -Visit your server's IP via browser or local terminal and you should see "It works!". +通过浏览器或本地终端访问你的服务器的IP,你应该看到 "It works!"。 ``` $ curl http://157.245.244.228 It works! ``` -Back on your server, you should see logs for the test request. +回到你的服务器上,你应该看到测试请求的日志。 ``` [ NOTICE ] Server starting on http://157.245.244.228:80 [ INFO ] GET / ``` -Use `CTRL+C` to quit the server. It may take a second to shutdown. +使用`CTRL+C`来退出服务器。它可能需要一秒钟的时间来关闭。 -Congratulations on getting your Vapor app running on a DigitalOcean Droplet! +祝贺你在DigitalOcean Droplet上运行你的Vapor应用程序! -## Next Steps +## 接下来的步骤 -The rest of this guide points to additional resources to improve your deployment. +本指南的其余部分指出了其他资源,以改善你的部署。 ### Supervisor -Supervisor is a process control system that can run and monitor your Vapor executable. With supervisor setup, your app can automatically start when the server boots and be restarted in case it crashes. Learn more about [Supervisor](../deploy/supervisor.md). +Supervisor是一个过程控制系统,可以运行和监控你的Vapor可执行文件。通过设置监督器,你的应用程序可以在服务器启动时自动启动,并在服务器崩溃时重新启动。了解更多关于[Supervisor](.../deploy/supervisor.md)。 ### Nginx -Nginx is an extremely fast, battle tested, and easy-to-configure HTTP server and proxy. While Vapor supports directly serving HTTP requests, proxying behind Nginx can provide increased performance, security, and ease-of-use. Learn more about [Nginx](../deploy/nginx.md). +Nginx是一个速度极快、经过战斗检验、易于配置的HTTP服务器和代理。虽然Vapor支持直接提供HTTP请求,但在Nginx后面的代理可以提供更高的性能、安全性和易用性。了解更多关于[Nginx](.../deploy/nginx.md)。 diff --git a/4.0/docs/deploy/docker.md b/4.0/docs/deploy/docker.md index b7e0784..d0d18fa 100644 --- a/4.0/docs/deploy/docker.md +++ b/4.0/docs/deploy/docker.md @@ -1,96 +1,55 @@ -# Docker Deploys +# Docker部署 -Using Docker to deploy your Vapor app has several benefits: +使用Docker来部署你的Vapor应用程序有几个好处。 -1. Your dockerized app can be spun up reliably using the same commands on any platform with a Docker Daemon -- namely, Linux (CentOS, Debian, Fedora, Ubuntu), macOS, and Windows. -2. You can use docker-compose or Kubernetes manifests to orchestrate multiple services needed for a full deployment (e.g. Redis, Postgres, nginx, etc.). -3. It is easy to test your app's ability to scale horizontally, even locally on your development machine. +1. 你的docker化应用可以在任何有Docker Daemon的平台上使用相同的命令可靠地启动,即Linux(CentOS、Debian、Fedora、Ubuntu)、macOS和Windows。 +2. 你可以使用docker-compose或Kubernetes清单来协调全面部署所需的多个服务(如Redis、Postgres、nginx等)。 +3. 3.很容易测试你的应用程序的水平扩展能力,即使是在你的开发机器上也是如此。 -This guide will stop short of explaining how to get your dockerized app onto a server. The simplest deploy would involve installing Docker on your server and running the same commands you would run on your development machine to spin up your application. +本指南将不再解释如何将你的docker化应用放到服务器上。最简单的部署是在服务器上安装Docker,并在开发机上运行相同的命令来启动你的应用程序。 -More complicated and robust deployments are usually different depending on your hosting solution; many popular solutions like AWS have builtin support for Kubernetes and custom database solutions which make it difficult to write best practices in a way that applies to all deployments. +更复杂和强大的部署通常取决于你的托管解决方案;许多流行的解决方案,如AWS,都有对Kubernetes和自定义数据库解决方案的内置支持,这使得我们很难以适用于所有部署的方式来编写最佳实践。 -Nevertheless, using Docker to spin your entire server stack up locally for testing purposes is incredibly valuable for both big and small serverside apps. Additionally, the concepts described in this guide apply in broad strokes to all Docker deployments. +尽管如此,使用Docker将整个服务器堆栈旋转到本地进行测试,对于大型和小型的服务器端应用程序来说都是非常有价值的。此外,本指南中描述的概念大致适用于所有的Docker部署。 -## Set Up +## 设置 -You will need to set your developer environment up to run Docker and gain a basic understanding of the resource files that configure Docker stacks. +你需要设置你的开发环境来运行Docker,并对配置Docker堆栈的资源文件有一个基本的了解。 -### Install Docker +### 安装Docker -You will need to install Docker for your developer environment. You can find information for any platform in the [Supported Platforms](https://docs.docker.com/install/#supported-platforms) section of the Docker Engine Overview. If you are on Mac OS, you can jump straight to the [Docker for Mac](https://docs.docker.com/docker-for-mac/install/) install page. +你将需要为你的开发者环境安装Docker。你可以在Docker引擎概述的[支持平台](https://docs.docker.com/install/#supported-platforms)部分找到任何平台的信息。如果你使用的是Mac OS,你可以直接跳到[Docker for Mac](https://docs.docker.com/docker-for-mac/install/)安装页面。 -### Generate Template +###生成模板 -We suggest using the Vapor template as a starting place. If you already have an App, build the template as described below into a new folder as a point of reference while dockerizing your existing app -- you can copy key resources from the template to your app and tweak them slightly as a jumping off point. +我们建议使用Vapor模板作为一个起点。如果你已经有了一个应用,按照下面的描述将模板建立到一个新的文件夹中,作为对现有应用进行docker化的参考点--你可以将模板中的关键资源复制到你的应用中,并对它们稍作调整作为一个跳板。 -1. Install or build the Vapor Toolbox ([macOS](../install/macos.md#install-toolbox), [Ubuntu](../install/ubuntu.md#install-toolbox)). -2. Create a new Vapor App with `vapor new my-dockerized-app` and walk through the prompts to enable or disable relevant features. Your answers to these prompts will affect how the Docker resource files are generated. +1. 安装或构建Vapor工具箱([macOS](../install/macos.md#安装-Toolbox),[Linux](../install/linux.md#安装-Toolbox))。 +2. 用`vapor new my-dockerized-app`创建一个新的Vapor应用程序,并通过提示来启用或禁用相关功能。你对这些提示的回答将影响Docker资源文件的生成方式。 -## Docker Resources +## Docker资源 -It is worthwhile, whether now or in the near future, to familiarize yourself with the [Docker Overview](https://docs.docker.com/engine/docker-overview/). The overview will explain some key terminology that this guide uses. +无论是现在还是在不久的将来,熟悉一下[Docker概述](https://docs.docker.com/engine/docker-overview/)是值得的。该概述将解释本指南所使用的一些关键术语。 -The template Vapor App has two key Docker-specific resources: A **Dockerfile** and a **docker-compose** file. +模板Vapor App有两个关键的Docker专用资源。一个**Dockerfile**和一个**docker-compose**文件。 ### Dockerfile -A Dockerfile tells Docker how to build an image of your dockerized app. That image contains both your app's executable and all dependencies needed to run it. The [full reference](https://docs.docker.com/engine/reference/builder/) is worth keeping open when you work on customizing your Dockerfile. +Dockerfile告诉Docker如何为你的docker化应用建立一个镜像。该镜像包含你的应用的可执行文件和运行该应用所需的所有依赖项。当你致力于定制你的Dockerfile时,[完整参考](https://docs.docker.com/engine/reference/builder/)值得保持开放。 -The Dockerfile generated for your Vapor App has two stages. +为你的Vapor应用程序生成的Dockerfile有两个阶段。第一阶段构建你的应用程序,并设置一个包含结果的保持区。第二阶段设置安全运行环境的基本要素,将保持区中的所有内容转移到最终镜像中,并设置一个默认的入口和命令,在默认端口(8080)上以生产模式运行你的应用程序。这个配置可以在使用镜像时被重写。 -It starts by pulling in another image to build off of (`vapor/swift:5.2`). This base image makes available any build time dependencies needed by your Vapor App. Then it copies your app into the build environment and builds it. -```docker -# ================================ -# Build image -# ================================ -FROM vapor/swift:5.2 as build -WORKDIR /build - -# Copy entire repo into container -COPY . . - -# Compile with optimizations -RUN swift build \ - --enable-test-discovery \ - -c release \ - -Xswiftc -g -``` - -Next, it pulls in a different image (`vapor/ubuntu:18.04`) as a second stage. The second stage copies what is needed to run your app and omits build-only dependencies. -```docker -# ================================ -# Run image -# ================================ -FROM vapor/ubuntu:18.04 -WORKDIR /run - -# Copy build artifacts -COPY --from=build /build/.build/release /run -# Copy Swift runtime libraries -COPY --from=build /usr/lib/swift/ /usr/lib/swift/ -# Uncomment the next line if you need to load resources from the `Public` directory -#COPY --from=build /build/Public /run/Public -``` +### Docker Compose文件 -Finally, the Dockerfile defines an entrypoint and a default command to run. -```docker -ENTRYPOINT ["./Run"] -CMD ["serve", "--env", "production", "--hostname", "0.0.0.0"] -``` - -Both of these can be overridden when the image is used, but by default running your image will result in calling the `serve` command of your app's `Run` target. The full command on launch will be `./Run serve --env production --hostname 0.0.0.0`. +Docker Compose文件定义了Docker建立多个服务的方式,使其相互关联。Vapor应用程序模板中的Docker Compose文件提供了部署应用程序的必要功能,但如果你想了解更多,你应该查阅[完整参考](https://docs.docker.com/compose/compose-file/),其中有关于所有可用选项的细节。 -### Docker Compose File +!!!注意 + 如果你最终打算使用Kubernetes来协调你的应用,那么Docker Compose文件就不直接相关。然而,Kubernetes清单文件在概念上是相似的,甚至有一些项目旨在将[移植Docker Compose文件](https://kubernetes.io/docs/tasks/configure-pod-container/translate-compose-kubernetes/)移植到Kubernetes清单中。 -A Docker Compose file defines the way Docker should build out multiple services in relation to each other. The Docker Compose file in the Vapor App template provides the necessary functionality to deploy your app, but if you want to learn more you should consult the [full reference](https://docs.docker.com/compose/compose-file/) which has details on all of the available options. - -!!! note - If you ultimately plan to use Kubernetes to orchestrate your app, the Docker Compose file is not directly relevant. However, Kubernetes manifest files are similar conceptually and there are even projects out there aimed at [porting Docker Compose files](https://kubernetes.io/docs/tasks/configure-pod-container/translate-compose-kubernetes/) to Kubernetes manifests. +新的Vapor App中的Docker Compose文件将定义运行App的服务,运行迁移或恢复迁移,以及运行数据库作为App的持久层。确切的定义将取决于你在运行`vapor new`时选择使用的数据库。 -The Docker Compose file in your new Vapor App will define services for running your app, running migrations or reverting them, and running a database as your app's persistence layer. The exact definitions will vary depending on which database you chose to use when you ran `vapor new`. +注意你的Docker Compose文件在顶部附近有一些共享的环境变量(你可能有一组不同的默认变量,这取决于你是否使用Fluent,以及如果你使用Fluent驱动。) -Note that your Docker Compose file has some shared environment variables near the top. ```docker x-shared_environment: &shared_environment LOG_LEVEL: ${LOG_LEVEL:-debug} @@ -100,202 +59,202 @@ x-shared_environment: &shared_environment DATABASE_PASSWORD: vapor_password ``` -You will see these pulled into multiple services below with the `<<: *shared_environment` YAML reference syntax. +你会看到这些被拉到下面的多个服务中,有`<<: *shared_environment`YAML参考语法。 -The `DATABASE_HOST`, `DATABASE_NAME`, `DATABASE_USERNAME`, and `DATABASE_PASSWORD` variables are hard coded in this example whereas the `LOG_LEVEL` will take its value from the environment running the service or fall back to `'debug'` if that variable is unset. +`DATABASE_HOST`, `DATABASE_NAME`, `DATABASE_USERNAME`, 和`DATABASE_PASSWORD`变量在这个例子中是硬编码的,而`LOG_LEVEL`将从运行服务的环境中取值,如果该变量未被设置,则返回到`'debug'`。 !!! note - Hard-coding the username and password is acceptable for local development, but you would want to store these variables in a secrets file for production deploys. One way to handle this in production is to export the secrets file to the environment that is running your deploy and use lines like the following in your Docker Compose file: + 对于本地开发来说,硬编码的用户名和密码是可以接受的,但对于生产部署来说,你应该将这些变量存储在一个秘密文件中。在生产中处理这个问题的一个方法是将秘密文件导出到运行你的部署的环境中,并在你的Docker Compose文件中使用类似下面的行: ``` DATABASE_USERNAME: ${DATABASE_USERNAME} ``` - This passes the environment variable through to the containers as-defined by the host. + 这将把环境变量传递给主机定义的容器。 -Other things to take note of: +其他需要注意的事项。 -- Service dependencies are defined by `depends_on` arrays. -- Service ports are exposed to the system running the services with `ports` arrays (formatted as `:`). -- The `DATABASE_HOST` is defined as `db`. This means your app will access the database at `http://db:5432`. That works because Docker is going to spin up a network in use by your services and the internal DNS on that network will route the name `db` to the service named `'db'`. -- The `CMD` directive in the Dockerfile is overridden in some services with the `command` array. Note that what is specified by `command` is run against the `ENTRYPOINT` in the Dockerfile. -- In Swarm Mode (more on this below) services will by default be given 1 instance, but the `migrate` and `revert` services are defined as having `deploy` `replicas: 0` so they do not start up by default when running a Swarm. +- 服务的依赖性是由`depends_on`数组定义的。 +- 服务端口通过`ports`数组暴露给运行服务的系统(格式为`:`)。 +- `DATABASE_HOST`被定义为`db`。这意味着你的应用程序将在`http://db:5432`访问数据库。这是因为Docker将建立一个服务使用的网络,该网络的内部DNS将把`db`这个名字路由到名为`'db'`的服务。 +- Docker文件中的`CMD`指令在一些服务中被`command`阵列覆盖。请注意,由`command`指定的内容是针对Dockerfile中的`ENTRYPOINT`运行的。 +- 在Swarm模式下(下面会有更多介绍),服务默认为1个实例,但`migrate`和`revert`服务被定义为`deploy` `replicas: 0`,所以它们在运行Swarm时默认不会启动。 -## Building +## 构建 -The Docker Compose file tells Docker how to build your app (by using the Dockerfile in the current directory) and what to name the resulting image (`my-dockerized-app:latest`). The latter is actually the combination of a name (`my-dockerized-app`) and a tag (`latest`) where tags are used to version Docker images. +Docker Compose文件告诉Docker如何构建你的应用程序(通过使用当前目录下的Dockerfile),以及如何命名生成的镜像(`my-dockerized-app:newth`)。后者实际上是一个名字(`my-dockerized-app`)和一个标签(`latest`)的组合,标签用于Docker镜像的版本。 -To build a Docker image for your app, run +要为你的应用程序建立一个Docker镜像,请运行 ```shell -docker-compose build +docker compose build ``` -from the root directory of your app's project (the folder containing `docker-compose.yml`). +从你的应用程序项目的根目录(包含`docker-compose.yml`的文件夹)。 -You'll see that your app and its dependencies must be built again even if you had previously built them on your development machine. They are being built in the Linux build environment Docker is using so the build artifacts from your development machine are not reusable. +你会看到你的应用程序和它的依赖项必须重新构建,即使你之前在你的开发机器上构建过它们。它们是在Docker使用的Linux构建环境中构建的,所以来自你的开发机器的构建工件是不可重复使用的。 -When it is done, you will find your app's image when running +当它完成后,你会发现你的应用程序的图像,当运行 ```shell docker image ls ``` -## Running +## 运行 -Your stack of services can be run directly from the Docker Compose file or you can use an orchestration layer like Swarm Mode or Kubernetes. +你的服务栈可以直接从Docker Compose文件中运行,也可以使用Swarm模式或Kubernetes等协调层。 -### Standalone +### 独立运行 -The simplest way to run your app is to start it as a standalone container. Docker will use the `depends_on` arrays to make sure any dependant services are also started. +运行你的应用程序的最简单方法是将其作为一个独立的容器启动。Docker将使用`depends_on`数组来确保任何依赖的服务也被启动。 -First, execute +首先,执行: ```shell -docker-compose up app +docker compose up app ``` -and notice that both the `app` and `db` services are started. +并注意到`app`和`db`服务都已启动。 -Your app is listening on port 80 _inside the Docker container_ but as defined by the Docker Compose file, it is accessible on your development machine at **http://localhost:8080**. +你的应用程序正在监听8080端口,正如Docker Compose文件所定义的,它可以在你的开发机器上访问**http://localhost:8080**。 -This port mapping distinction is very important because you can run any number of services on the same ports if they are all running in their own containers and they each expose different ports to the host machine. +这个端口映射的区别是非常重要的,因为你可以在相同的端口上运行任何数量的服务,如果它们都在自己的容器中运行,并且它们各自向主机暴露不同的端口。 -Visit `http://localhost:8080` and you will see `It works!` but visit `http://localhost:8080/todos` and you will get: +访问`http://localhost:8080`,你会看到`It works!`但访问`http://localhost:8080/todos`,你会得到: ``` {"error":true,"reason":"Something went wrong."} ``` -Take a peak at the logs output in the terminal where you ran `docker-compose up app` and you will see +看一下你运行`docker compose up app`的终端的日志输出,你会看到: ``` [ ERROR ] relation "todos" does not exist ``` -Of course! We need to run migrations on the database. Press `Ctrl+C` to bring your app down. We are going to start the app up again but this time with +当然了! 我们需要在数据库上运行迁移程序。按`Ctrl+C`关闭你的应用程序。我们将再次启动该应用程序,但这次是用: ```shell -docker-compose up --detach app +docker compose up --detach app ``` -Now your app is going to start up "detached" (in the background). You can verify this by running +现在你的应用程序将"datached"启动(在后台)。你可以通过运行来验证这一点: ```shell docker container ls ``` -where you will see both the database and your app running in containers. You can even check on the logs by running +在那里你会看到数据库和你的应用程序都在容器中运行。你甚至可以通过运行来检查日志: ```shell docker logs ``` -To run migrations, execute +要运行迁移,请执行: ```shell -docker-compose up migrate +docker compose run migrate ``` -After migrations run, you can visit `http://localhost:8080/todos` again and you will get an empty list of todos instead of an error message. +迁移运行后,你可以再次访问`http://localhost:8080/todos`,你会得到一个空的todos列表,而不是错误信息。 #### Log Levels -Recall above that the `LOG_LEVEL` environment variable in the Docker Compose file will be inherited from the environment where the service is started if available. +回顾上文,Docker Compose文件中的`LOG_LEVEL`环境变量将从服务启动的环境中继承(如果有)。 -You can bring your services up with +你可以把你的服务用 ```shell LOG_LEVEL=trace docker-compose up app ``` -to get `trace` level logging (the most granular). You can use this environment variable to set the logging to [any available level](../basics/logging.md#levels). +来获得`trace`级别的日志(最细的)。你可以使用这个环境变量将日志设置为[任何可用级别](.../logging.md#levels)。 -#### All Service Logs +#### 所有服务日志 -If you explicitly specify your database service when you bring containers up then you will see logs for both your database and your app. +如果你在启动容器时明确指定了你的数据库服务,那么你会看到数据库和应用程序的日志。 ```shell docker-compose up app db ``` -#### Bringing Standalone Containers Down +#### 使独立的容器停止运行 -Now that you've got containers running "detached" from your host shell, you need to tell them to shut down somehow. It's worth knowing that any running container can be asked to shut down with +现在你已经让容器从你的主机外壳上"detached"运行,你需要告诉它们以某种方式关闭。值得注意的是,任何正在运行的容器都可以通过以下方式被要求关闭 ```shell docker container stop ``` -but the easiest way to bring these particular containers down is +但要把这些特定的容器降下来,最简单的方法是 ```shell docker-compose down ``` -#### Wiping The Database +#### 擦拭数据库 -The Docker Compose file defines a `db_data` volume to persist your database between runs. There are a couple of ways to reset your database. +Docker Compose文件定义了一个`db_data`卷,用于在运行期间保持你的数据库。有几种方法可以重置你的数据库。 -You can remove the `db_data` volume at the same time as bringing your containers down with +你可以在关闭容器的同时删除`db_data`卷,并使用 ```shell docker-compose down --volumes ``` -You can see any volumes currently persisting data with `docker volume ls`. Note that the volume name will generally have a prefix of `my-dockerized-app_` or `test_` depending on whether you were running in Swarm Mode or not. +你可以用`docker volume ls`查看任何当前持久化数据的卷。请注意,卷的名称通常会有一个`my-dockerized-app_`或`test_`的前缀,取决于你是否在Swarm模式下运行。 -You can remove these volumes one at a time with e.g. +你可以一次删除这些卷,比如说 ```shell docker volume rm my-dockerized-app_db_data ``` -You can also clean up all volumes with +你也可以用以下方法清理所有卷 ```shell docker volume prune ``` -Just be careful you don't accidentally prune a volume with data you wanted to keep around! +只是要注意不要不小心修剪了你想保留的有数据的卷! -Docker will not let you remove volumes that are currently in use by running or stopped containers. You can get a list of running containers with `docker container ls` and you can see stopped containers as well with `docker container ls -a`. +Docker不会让你删除正在运行或停止的容器所使用的卷。你可以用`docker container ls`获得正在运行的容器的列表,你也可以用`docker container ls -a`看到停止的容器。 -### Swarm Mode +### Swarm模式 -Swarm Mode is an easy interface to use when you've got a Docker Compose file handy and you want to test how your app scales horizontally. You can read all about Swarm Mode in the pages rooted at the [overview](https://docs.docker.com/engine/swarm/). +Swarm模式是一个简单的界面,当你有一个Docker Compose文件在手,并且你想测试你的应用程序如何横向扩展时,可以使用它。你可以在扎根于[概述](https://docs.docker.com/engine/swarm/)的页面中阅读关于Swarm模式的所有内容。 -The first thing we need is a manager node for our Swarm. Run +我们需要的第一件事是为我们的Swarm提供一个管理节点。运行 ```shell docker swarm init ``` -Next we will use our Docker Compose file to bring up a stack named `'test'` containing our services +接下来我们将使用我们的Docker Compose文件来建立一个名为`'test'`的堆栈,其中包含我们的服务 ```shell docker stack deploy -c docker-compose.yml test ``` -We can see how our services are doing with +我们可以通过以下方式了解我们的服务情况 ```shell docker service ls ``` -You should expect to see `1/1` replicas for your `app` and `db` services and `0/0` replicas for your `migrate` and `revert` services. +你应该看到`app`和`db`服务有`1/1`个副本,`migrate`和`revert`服务有`0/0`个副本。 -We need to use a different command to run migrations in Swarm mode. +我们需要使用一个不同的命令来运行Swarm模式下的迁移。 ```shell docker service scale --detach test_migrate=1 ``` !!! note - We have just asked a short-lived service to scale to 1 replica. It will successfully scale up, run, and then exit. However, that will leave it with `0/1` replicas running. This is no big deal until we want to run migrations again, but we cannot tell it to "scale up to 1 replica" if that is already where it is at. A quirk of this setup is that the next time we want to run migrations within the same Swarm runtime, we need to first scale the service down to `0` and then back up to `1`. + 我们刚刚要求一个短命的服务扩展到1个副本。它将成功扩大规模,运行,然后退出。然而,这将使它的`0/1`个复制在运行。在我们想再次运行迁移之前,这没什么大不了的,但是如果它已经处于这个状态,我们就不能告诉它"扩展到1个副本"。这种设置的一个怪癖是,当我们下次想在同一个Swarm运行时间内运行迁移时,我们需要首先将服务缩减到`0`,然后再回升到`1`。 -The payoff for our trouble in the context of this short guide is that now we can scale our app to whatever we want in order to test how well it handles database contention, crashes, and more. +在这个简短的指南中,我们的麻烦的回报是,现在我们可以将我们的应用程序扩展到任何我们想要的程度,以测试它对数据库争用、崩溃等的处理情况。 -If you want to run 5 instances of your app concurrently, execute +如果你想同时运行5个应用程序的实例,执行 ```shell docker service scale test_app=5 ``` -In addition to watching docker scale your app up, you can see that 5 replicas are indeed running by again checking `docker service ls`. +除了观察docker扩展你的应用程序,你还可以通过再次检查`docker service ls`看到5个副本确实在运行。 -You can view (and follow) the logs for your app with +你可以通过以下方式查看(和跟踪)你的应用程序的日志 ```shell docker service logs -f test_app ``` -#### Bringing Swarm Services Down +#### 将Swarm服务关闭 -When you want to bring your services down in Swarm Mode, you do so by removing the stack you created earlier. +当你想在Swarm模式下关闭你的服务时,你可以通过移除你先前创建的堆栈来实现。 ```shell docker stack rm test ``` -## Production Deploys +## 生产部署 -As noted at the top, this guide will not go into great detail about deploying your dockerized app to production because the topic is large and varies greatly depending on the hosting service (AWS, Azure, etc.), tooling (Terraform, Ansible, etc.), and orchestration (Docker Swarm, Kubernetes, etc.). +如上所述,本指南不会详细介绍将docker化应用部署到生产环境中的问题,因为这个话题很大,而且根据托管服务(AWS、Azure等)、工具(Terraform、Ansible等)和协调(Docker Swarm、Kubernetes等)的不同而变化很大。 -However, the techniques you learn to run your dockerized app locally on your development machine are largely transferable to production environments. A server instance set up to run the docker daemon will accept all the same commands. +然而,你所学习的在开发机器上本地运行docker化应用的技术在很大程度上可以转移到生产环境。为运行docker守护程序而设置的服务器实例将接受所有相同的命令。 -Copy your project files to your server, SSH into the server, and run a `docker-compose` or `docker stack deploy` command to get things running remotely. +将你的项目文件复制到服务器上,通过SSH进入服务器,并运行`docker-compose`或`docker stack deploy`命令来实现远程运行。 -Alternatively, set your local `DOCKER_HOST` environment variable to point at your server and run the `docker` commands locally on your machine. It is important to note that with this approach, you do not need to copy any of your project files to the server _but_ you do need to host your docker image somewhere your server can pull it from. +或者,设置你的本地`DOCKER_HOST`环境变量指向你的服务器,在你的机器上运行`docker`命令。值得注意的是,使用这种方法,你不需要将任何项目文件复制到服务器上,但你需要将你的docker镜像寄存在服务器可以提取的地方。 diff --git a/4.0/docs/deploy/heroku.md b/4.0/docs/deploy/heroku.md index 429e1a7..c1f9095 100644 --- a/4.0/docs/deploy/heroku.md +++ b/4.0/docs/deploy/heroku.md @@ -1,14 +1,14 @@ -# What is Heroku +# 什么是Heroku -Heroku is a popular all in one hosting solution, you can find more at [heroku.com](https://www.heroku.com) +Heroku是一个流行的一体化托管解决方案,你可以在[heroku.com](https://www.heroku.com)找到更多信息。 -## Signing Up +## 注册 -You'll need a heroku account, if you don't have one, please sign up here: [https://signup.heroku.com/](https://signup.heroku.com/) +你需要一个heroku账户,如果你没有,请在这里注册:[https://signup.heroku.com/](https://signup.heroku.com/) -## Installing CLI +## 安装CLI -Make sure that you've installed the heroku cli tool. +确保你已经安装了heroku cli工具。 ### HomeBrew @@ -16,35 +16,35 @@ Make sure that you've installed the heroku cli tool. brew install heroku/brew/heroku ``` -### Other Install Options +### 其他安装选项 -See alternative install options here: [https://devcenter.heroku.com/articles/heroku-cli#download-and-install](https://devcenter.heroku.com/articles/heroku-cli#download-and-install). +请参阅这里的其他安装选项:[https://devcenter.heroku.com/articles/heroku-cli#download-and-install](https://devcenter.heroku.com/articles/heroku-cli#download-and-install)。 -### Logging in +### 登录 -once you've installed the cli, login with the following: +一旦你安装了cli,用以下方式登录: ```bash heroku login ``` -verify that the correct email is logged in with: +验证正确的电子邮件是否已登录: ```bash heroku auth:whoami ``` -### Create an application +###创建一个应用程序 -Visit dashboard.heroku.com to access your account, and create a new application from the drop down in the upper right hand corner. Heroku will ask a few questions such as region and application name, just follow their prompts. +访问dashboard.heroku.com访问你的账户,从右上角的下拉菜单中创建一个新的应用程序。Heroku会问一些问题,如地区和应用程序的名称,按照他们的提示进行操作即可。 ### Git -Heroku uses Git to deploy your app, so you’ll need to put your project into a Git repository, if it isn’t already. +Heroku使用Git来部署你的应用程序,所以你需要把你的项目放入Git仓库,如果它还没有的话。 -#### Initialize Git +#### 安装Git -If you need to add Git to your project, enter the following command in Terminal: +如果你需要将Git添加到你的项目中,在终端输入以下命令: ```bash git init @@ -52,15 +52,15 @@ git init #### Master -By default, Heroku deploys the **master** branch. Make sure all changes are checked into this branch before pushing. +默认情况下,Heroku部署的是**master**分支。在推送之前,请确保所有的修改都检查到这个分支。 -Check your current branch with +用以下方法检查你的当前分支 ```bash git branch ``` -The asterisk indicates current branch. +`*`号表示当前分支。 ```bash * master @@ -68,133 +68,134 @@ The asterisk indicates current branch. other-branches ``` -> **Note**: If you don’t see any output and you’ve just performed `git init`. You’ll need to commit your code first then you’ll see output from the `git branch` command. +!!! note + 如果你没有看到任何输出,而且你刚刚执行了`git init`。你需要先提交你的代码,然后你会看到`git branch`命令的输出。 -If you’re _not_ currently on **master**, switch there by entering: +如果你目前不在**master**上,可以通过输入来切换到那里: ```bash git checkout master ``` -#### Commit changes +#### 提交更改 -If this command produces output, then you have uncommitted changes. +如果这个命令产生输出,那么你有未提交的修改。 ```bash git status --porcelain ``` -Commit them with the following +用下面的命令提交它们 ```bash git add . git commit -m "a description of the changes I made" ``` -#### Connect with Heroku +#### 与Heroku连接 -Connect your app with heroku (replace with your app's name). +将你的应用程序与heroku连接起来(用你的应用程序的名称代替)。 ```bash $ heroku git:remote -a your-apps-name-here ``` -### Set Buildpack +### 设置Buildpack -Set the buildpack to teach heroku how to deal with vapor. +设置buildpack来教heroku如何处理Vapor。 ```bash heroku buildpacks:set vapor/vapor ``` -### Swift version file +### Swift 版本文件 -The buildpack we added looks for a **.swift-version** file to know which version of swift to use. (replace 5.2.1 with whatever version your project requires.) +我们添加的 buildpack 会寻找一个 **.swift-version** 文件,以了解要使用哪个版本的 swift。(用你的项目需要的任何版本替换5.2.1)。 ```bash echo "5.2.1" > .swift-version ``` -This creates **.swift-version** with `5.2.1` as its contents. +这将创建**.swift-version**为`5.2.1`的内容。 ### Procfile -Heroku uses the **Procfile** to know how to run your app, in our case it needs to look like this: +Heroku使用**Procfile**来知道如何运行你的应用程序,在我们的例子中,它需要看起来像这样: ``` web: Run serve --env production --hostname 0.0.0.0 --port $PORT ``` -we can create this with the following terminal command +我们可以用下面的终端命令来创建它 ```bash echo "web: Run serve --env production" \ "--hostname 0.0.0.0 --port \$PORT" > Procfile ``` -### Commit changes +### 提交修改 -We just added these files, but they're not committed. If we push, heroku will not find them. +我们刚刚添加了这些文件,但它们还没有提交。如果我们推送,heroku将找不到它们。 -Commit them with the following. +用下面的方法提交它们。 ```bash git add . git commit -m "adding heroku build files" ``` -### Deploying to Heroku +### 部署到Heroku -You're ready to deploy, run this from the terminal. It may take a while to build, this is normal. +你已经准备好部署了,从终端运行这个。它可能需要一些时间来构建,这是正常的。 ```none git push heroku master ``` -### Scale Up +### 扩大规模 -Once you've built successfully, you need to add at least one server, one web is free and you can get it with the following: +一旦你建立成功,你需要添加至少一个服务器,一个网络是免费的,你可以通过以下方式获得它。 ```bash heroku ps:scale web=1 ``` -### Continued Deployment +### 继续部署 -Any time you want to update, just get the latest changes into master and push to heroku and it will redeploy +任何时候你想更新,只需将最新的变化放入主目录并推送到heroku,它就会重新部署。 ## Postgres -### Add PostgreSQL database +### 添加PostgreSQL数据库 -Visit your application at dashboard.heroku.com and go to the **Add-ons** section. +访问你在dashboard.heroku.com的应用程序,进入**附加组件**部分。 -From here enter `postgress` and you'll see an option for `Heroku Postgres`. Select it. +在这里输入`postgress`,你会看到一个`Heroku Postgres`的选项。选择它。 -Choose the hobby dev free plan, and provision. Heroku will do the rest. +选择爱好开发的免费计划,然后提供。Heroku会做其他事情。 -Once you finish, you’ll see the database appears under the **Resources** tab. +一旦你完成,你会看到数据库出现在**资源**标签下。 -### Configure the database +### 配置数据库 -We have to now tell our app how to access the database. In our app directory, let's run. +我们现在必须告诉我们的应用程序如何访问数据库。在我们的应用程序目录中,让我们运行。 ```bash heroku config ``` -This will make output somewhat like this +这将使输出有点像这样 ```none === today-i-learned-vapor Config Vars DATABASE_URL: postgres://cybntsgadydqzm:2d9dc7f6d964f4750da1518ad71hag2ba729cd4527d4a18c70e024b11cfa8f4b@ec2-54-221-192-231.compute-1.amazonaws.com:5432/dfr89mvoo550b4 ``` -**DATABASE_URL** here will represent out postgres database. **NEVER** hard code the static url from this, heroku will rotate it and it will break your application. It is also bad practice. +**DATABASE_URL**这里将代表postgres数据库。**千万**不要在这里硬编码静态URL,heroku会旋转它,这将破坏你的应用程序。这也是不好的做法。 -Here is an example databsae configuration +下面是一个数据库配置的例子 ```swift if let databaseURL = Environment.get("DATABASE_URL") { @@ -206,26 +207,38 @@ if let databaseURL = Environment.get("DATABASE_URL") { } ``` -Unverified TLS is required if you are using Heroku Postgres's standard plan. +如果你使用Heroku Postgres的标准计划,则需要未验证的TLS: -Don't forget to commit these changes +```swift +if let databaseURL = Environment.get("DATABASE_URL"), var postgresConfig = PostgresConfiguration(url: databaseURL) { + postgresConfig.tlsConfiguration = .makeClientConfiguration() + postgresConfig.tlsConfiguration?.certificateVerification = .none + app.databases.use(.postgres( + configuration: postgresConfig + ), as: .psql) +} else { + // ... +} +``` + +不要忘记提交这些修改 ```none git add . git commit -m "configured heroku database" ``` -### Reverting your database +### 恢复你的数据库 -You can revert or run other commmands on heroku with the `run` command. Vapor's project is by default also named `Run`, so it reads a little funny. +你可以用`run`命令恢复或运行heroku上的其他命令。Vapor的项目默认也被命名为`Run`,所以它的读法有点奇怪。 -To revert your database: +要恢复你的数据库: ```bash -heroku run Run -- revert --all --yes --env production +heroku run Run -- migrate --revert --all --yes --env production ``` -To migrate +要迁移 ```bash heroku run Run -- migrate --env production diff --git a/4.0/docs/deploy/nginx.md b/4.0/docs/deploy/nginx.md index 52f86ce..c516a0d 100644 --- a/4.0/docs/deploy/nginx.md +++ b/4.0/docs/deploy/nginx.md @@ -1,61 +1,75 @@ -# 使用 Nginx 部署 +# 使用Nginx进行部署 -Nginx 是一款高性能、高可靠性、易于配置的 HTTP 服务器和 HTTP 反向代理服务器。 -尽管 Vapor 可以直接处理 HTTP 请求,并且支持 TLS。但将 Vapor 应用置于 Nginx 反向代理之后,可以提高性能、安全性、以及易用性。 +Nginx是一个非常快的、经过测试的、易于配置的HTTP服务器和代理。虽然Vapor支持直接为带有或不带有TLS的HTTP请求提供服务,但在Nginx后面进行代理可以提供更高的性能、安全性和易用性。 !!! note - 我们推荐你将 Vapor 应用配置在 Nginx 的反向代理之后。 + 我们建议在Nginx后面代理Vapor HTTP服务器。 ## 概述 -HTTP 反向代理是什么意思?简而言之,反向代理服务器就是外部网络和你的真实的 HTTP 服务器之间的一个中间人,反向代理服务器处理所有进入的 HTTP 请求,并将它们转发给 Vapor 服务器。 +代理一个HTTP服务器是什么意思?简而言之,代理在公共互联网和你的HTTP服务器之间充当中间人。请求来到代理,然后将它们发送到Vapor。 -反向代理的一个重要特性就是,它可以修改用户的请求,以及对其进行重定向。通过这个特性,反向代理服务器可以配置 TLS (https)、限制请求速率、甚至越过你的 Vapor 应用直接管理 Vapor 应用中的静态文件。 +这个中间人代理的一个重要特点是,它可以改变甚至重定向请求。例如,代理可以要求客户使用TLS(https),限制请求的速率,甚至可以不与你的Vapor应用程序交谈而提供公共文件。 ![nginx-proxy](https://cloud.githubusercontent.com/assets/1342803/20184965/5d9d588a-a738-11e6-91fe-28c3a4f7e46b.png) ### 更多细节 -默认的接收 HTTP 请求的端口是 `80` (HTTPS 是 `443`)。如果你将 Vapor 服务器绑定到 `80` 端口,它就可以直接处理和响应 HTTP 请求。如果你想要使用反向代理 (比如 Nginx),你就需要将 Vapor 服务器绑定到一个内部端口上,比如 `8080`。 +接收HTTP请求的默认端口是`80`端口(HTTPS为`443`)。当你将Vapor服务器绑定到`80`端口时,它将直接接收并响应到你的服务器上的HTTP请求。当添加像Nginx这样的代理时,你将Vapor绑定到一个内部端口,如端口`8080`。 !!! note - 绑定到大于 1024 的端口号无需使用 `sudo` 命令。 + 大于1024的端口不需要`sudo`来绑定。 -一旦你的 Vapor 应用被绑定到 `80` 或 `443` 以外的端口,那么外部网络将无法直接访问它 (没有配置防火墙的情况下,带上端口号仍然可以访问)。然后将 Nginx 服务器绑定到 `80` 端口上,并配置它转发请求到 `8080` 端口上的 Vapor 应用。 +当Vapor被绑定到`80`或`443`以外的端口时,它将不能被外部互联网访问。然后,将Nginx绑定到`80`端口,并将其配置为将请求路由到Vapor服务器的`8080`端口(或你选择的任何一个端口)。 -就这样,如果你正确配置了 Nginx,你可以看到你的 Vapor 应用已经可以响应 `80` 端口上的请求了,而外部网络和你的 Vapor 应用都不会感知到 Nginx 的存在。 +就这样了。如果Nginx配置正确,你会看到Vapor应用程序对`80`端口的请求进行响应。Nginx代理的请求和响应是不可见的。 -## 安装 Nginx +## 安装Nginx -首先是安装 Nginx。网络上有着大量资源和文档来描述如何安装 Nginx,因此在这里不再赘述。不论你使用哪个平台、操作系统、或服务供应商,你都能找到相应的文档或教程。 +第一步是安装Nginx。Nginx的一个伟大之处在于它有大量的社区资源和文档。正因为如此,我们不会在这里详细介绍Nginx的安装,因为几乎肯定会有针对你的特定平台、操作系统和供应商的教程。 -教程: +教程: -- [如何在 Ubuntu 14.04 LTS 上安装 Nginx?](https://www.digitalocean.com/community/tutorials/how-to-install-nginx-on-ubuntu-14-04-lts) (英文) -- [如何在 Ubuntu 16.04 上安装 Nginx?](https://www.digitalocean.com/community/tutorials/how-to-install-nginx-on-ubuntu-16-04) (英文) -- [如何在 Heroku 上部署 Nginx?](https://blog.codeship.com/how-to-deploy-nginx-on-heroku/) (英文) -- [如何在 Ubuntu 14.04 上用 Docker 容器运行 Nginx?](https://www.digitalocean.com/community/tutorials/how-to-run-nginx-in-a-docker-container-on-ubuntu-14-04) (英文) +- [如何在Ubuntu 20.04上安装Nginx](https://www.digitalocean.com/community/tutorials/how-to-install-nginx-on-ubuntu-20-04) +- [如何在Ubuntu 18.04上安装Nginx](https://www.digitalocean.com/community/tutorials/how-to-install-nginx-on-ubuntu-18-04) +- [如何在CentOS 8上安装Nginx](https://www.digitalocean.com/community/tutorials/how-to-install-nginx-on-centos-8) +- [如何在Ubuntu 16.04上安装Nginx](https://www.digitalocean.com/community/tutorials/how-to-install-nginx-on-ubuntu-16-04) +- [如何在Heroku上部署Nginx](https://blog.codeship.com/how-to-deploy-nginx-on-heroku/) +### 软件包管理器 -### APT +Nginx可以通过Linux上的软件包管理器进行安装。 -可以通过 APT 工具安装 Nginx +#### Ubuntu ```sh sudo apt-get update sudo apt-get install nginx ``` -你可以在浏览器中访问你的服务器的 IP 地址,来检查你的 Nginx 是否被正确安装. +#### CentOS和Amazon Linux +```sh +sudo yum install nginx +``` + +#### Fedora ```sh +sudo dnf install nginx +``` + +### 验证安装 + +通过在浏览器中访问服务器的IP地址,检查Nginx是否被正确安装 + +``` http://server_domain_name_or_IP ``` -### Service +### 服务 -如何停止/启动/重启 Nginx 服务 (service) +该服务可以被启动或停止。 ```sh sudo service nginx stop @@ -63,19 +77,19 @@ sudo service nginx start sudo service nginx restart ``` -## 启动 Vapor +## 启动Vapor -Nginx 可以通过 `sudo service nginx ...` 命令来启动或停止。同样的,你也需要一些类似的操作来启动或停止你的 Vapor 服务器。 +Nginx可以通过`sudo service nginx...`命令来启动和停止。你将需要类似的东西来启动和停止Vapor服务器。 -有许多方法可以做到这一点,这通常取决于你使用的是哪个平台或系统。Supervisor 是其中一个较为通用的方式,你可以查看 [Supervisor](supervisor.md) 的配置方法,来配置启动或停止你的 Vapor 应用的命令。 +有很多方法可以做到这一点,它们取决于你要部署到哪个平台。查看[Supervisor](./supervisor.md)说明,添加启动和停止Vapor应用程序的命令。 -## 配置 Nginx +## 配置代理 -要启用的站点的配置需要放在 `/etc/nginx/sites-enabled/` 目录下。 +启用站点的配置文件可以在`/etc/nginx/sites-enabled/`中找到。 -创建一个新的文件或者从 `/etc/nginx/sites-available/` 目录下的模版文件中拷贝一份配置,然后你就可以开始配置 Nginx 了。 +创建一个新的文件或复制`/etc/nginx/sites-available/`中的例子模板来开始使用。 -这是一份配置文件的样例,它为一个 Vapor 项目进行了配置,这个项目位于 Home 目录下的一个名为 `Hello` 目录中。 +下面是一个在主目录下名为`Hello`的Vapor项目的配置文件例子。 ```sh server { @@ -97,30 +111,30 @@ server { } ``` -这份配置假定你的 `Hello` 程序绑定到了 `8080` 端口上,并启用了生产模式 (production mode)。 +这个配置文件假设`Hello`项目在生产模式下启动时绑定到端口`8080`。 -### 管理文件 +### 服务文件 -Nginx 可以越过你的 Vapor 应用,直接管理静态资源文件。这样可以为你的 Vapor 进程减轻一些不必要的压力,以提高一些性能。 +Nginx也可以在不询问Vapor应用程序的情况下提供公共文件。这可以通过释放Vapor进程来提高性能,使其在重载下执行其他任务。 ```sh server { - ... + ... - # nginx 直接处理所有静态资源文件的请求,其余请求则回落 (fallback) 到 Vapor 应用 - location / { - try_files $uri @proxy; - } + # 通过nginx提供所有的公共/静态文件,其余的退到Vapor。 + location / { + try_files $uri @proxy; + } - location @proxy { - ... - } + location @proxy { + ... + } } ``` ### TLS -如果你已经获取了 TLS 证书 (certification),那么配置 TLS 相对来说是比较简单的。如果想要获取免费的 TLS 证书,可以看看 [Let's Encrypt](https://letsencrypt.org/getting-started/)。 +只要正确地生成了证书,添加TLS是相对简单的。要免费生成TLS证书,请查看[Let's Encrypt](https://letsencrypt.org/getting-started/)。 ```sh server { @@ -149,4 +163,4 @@ server { } ``` -上面这份 Nginx 的 TLS 配置是相对比较严格的。其中一些配置不是必须的,但能提高安全性。 +上面的配置是对Nginx的TLS的相对严格的设置。这里的一些设置不是必须的,但可以增强安全性。 diff --git a/4.0/docs/deploy/supervisor.md b/4.0/docs/deploy/supervisor.md index 0554f4d..e60c236 100644 --- a/4.0/docs/deploy/supervisor.md +++ b/4.0/docs/deploy/supervisor.md @@ -1,17 +1,33 @@ # Supervisor -[Supervisor](http://supervisord.org) is a process control system that makes it easy to start, stop, and restart your Vapor app. +[Supervisor](http://supervisord.org)是一个过程控制系统,可以轻松启动、停止和重新启动Vapor应用程序。 -## Install +## 安装 + +Supervisor可以通过Linux上的软件包管理器安装。 + +### Ubuntu ```sh sudo apt-get update sudo apt-get install supervisor ``` -## Configure +### CentOS和Amazon Linux + +```sh +sudo yum install supervisor +``` -Each Vapor app on your server should have its own configuration file. For an example `Hello` project, the configuration file would be located at `/etc/supervisor/conf.d/hello.conf` +### Fedora + +```sh +sudo dnf install supervisor +``` + +## 配置 + +你服务器上的每个Vapor应用都应该有自己的配置文件。以`Hello`项目为例,该配置文件位于`/etc/supervisor/conf.d/hello.conf`。 ```sh [program:hello] @@ -22,27 +38,29 @@ stdout_logfile=/var/log/supervisor/%(program_name)-stdout.log stderr_logfile=/var/log/supervisor/%(program_name)-stderr.log ``` -As specified in our configuration file the `Hello` project is located in the home folder for the user `vapor`. Make sure `directory` points to the root directory of your project where the `Package.swift` file is. +正如我们的配置文件中所指定的,`Hello`项目位于用户`vapor`的主文件夹中。确保`directory`指向你项目的根目录,即`Package.swift`文件所在的目录。 + +`--env production`标志将禁用粗略的日志记录。 -The `--env production` flag will disable verbose logging. +###环境 -### Environment +你可以用supervisor向你的Vapor应用程序导出变量。如果要导出多个环境值,请把它们都放在一行。根据[Supervisor文档](http://supervisord.org/configuration.html#program-x-section-values)。 -You can export variables to your Vapor app with supervisor. +> 含有非字母数字字符的值应该加引号(例如:KEY="val:123",KEY2="val,456")。否则,值的引号是可选的,但推荐使用。 ```sh -environment=PORT=8123 +environment=PORT=8123,ANOTHERVALUE="/something/else" ``` -Exported variables can be used in Vapor using `Environment.get` +输出的变量可以在Vapor中使用`Environment.get`。 ```swift let port = Environment.get("PORT") ``` -## Start +## 开始 -You can now load and start your app. +现在你可以加载并启动你的应用程序。 ```sh supervisorctl reread @@ -51,4 +69,4 @@ supervisorctl start hello ``` !!! note - The `add` command may have already started your app. + `add`命令可能已经启动了你的应用程序。 diff --git a/4.0/docs/fluent/advanced.md b/4.0/docs/fluent/advanced.md index dee2a89..0a14359 100644 --- a/4.0/docs/fluent/advanced.md +++ b/4.0/docs/fluent/advanced.md @@ -1,35 +1,135 @@ -# Advanced +# 高级 + +Fluent致力于创建一个通用的、与数据库无关的API来处理你的数据。这使得无论你使用哪种数据库驱动,都能更容易地学习 Fluent。创建通用的API也可以让你的数据库工作在Swift中感觉更自如。 + +然而,你可能需要使用你的底层数据库驱动的某个功能,而这个功能还没有通过Fluent支持。本指南涵盖了Fluent中只适用于某些数据库的高级模式和API。 + +## SQL + +所有的Fluent的SQL数据库驱动都是建立在[SQLKit](https://github.com/vapor/sql-kit)之上的。这种通用的SQL实现在Fluent的`FluentSQL`模块中与Fluent一起提供。 + +### SQL数据库 + +任何Fluent的 "数据库 "都可以被转换为 "SQLDatabase"。这包括`req.db`, `app.db`, 传递给`Migration`的`数据库`,等等。 + +```swift +import FluentSQL + +if let sql = req.db as? SQLDatabase { + // 底层数据库驱动是SQL。 + let planets = try await sql.raw("SELECT * FROM planets").all(decoding: Planet.self) +} else { + // 底层数据库驱动是_not_ SQL。 +} +``` + +只有当底层数据库驱动是一个SQL数据库时,这种投射才会起作用。在[SQLKit的README](https://github.com/vapor/sql-kit)中了解更多关于`SQLDatabase`的方法。 + +### 特定的SQL数据库 + +你也可以通过导入驱动来投递到特定的SQL数据库。 + +```swift +import FluentPostgresDriver + +if let postgres = req.db as? PostgresDatabase { + // 底层数据库驱动是PostgreSQL。 + postgres.simpleQuery("SELECT * FROM planets").all() +} else { + // 底层数据库不是PostgreSQL。 +} +``` + +在撰写本文时,支持以下SQL驱动。 + +|数据库|驱动程序|库| +|-|-|-| +|`PostgresDatabase`|[vapor/fluent-postgres-driver](https://github.com/vapor/fluent-postgres-driver)|[vapor/postgres-nio](https://github.com/vapor/postgres-nio)| +|`MySQLDatabase`|[vapor/fluent-mysql-driver](https://github.com/vapor/fluent-mysql-driver)|[vapor/mysql-nio](https://github.com/vapor/mysql-nio)| +|`SQLiteDatabase`|[vapor/fluent-sqlite-driver](https://github.com/vapor/fluent-sqlite-driver)|[vapor/sqlite-nio](https://github.com/vapor/sqlite-nio)| + +请访问该库的README以了解更多关于数据库特定API的信息。 + +### SQL自定义 + +几乎所有的Fluent查询和模式类型都支持`.custom`情况。这可以让你利用Fluent尚不支持的数据库功能。 + +```swift +import FluentPostgresDriver + +let query = Planet.query(on: req.db) +if req.db is PostgresDatabase { + // ILIKE支持。 + query.filter(\.$name, .custom("ILIKE"), "earth") +} else { + // ILIKE不支持。 + query.group(.or) { or in + or.filter(\.$name == "earth").filter(\.$name == "Earth") + } +} +query.all() +``` + +SQL数据库在所有`.custom`情况下都支持`String`和`SQLExpression`。`FluentSQL`模块为常见的使用情况提供方便的方法。 + +```swift +import FluentSQL + +let query = Planet.query(on: req.db) +if req.db is SQLDatabase { + // 底层数据库驱动是SQL。 + query.filter(.sql(raw: "LOWER(name) = 'earth'")) +} else { + // 底层数据库驱动是_not_ SQL。 +} +``` + +下面是一个通过`.sql(raw:)`便利性使用模式生成器的`.custom`的例子。 + +```swift +import FluentSQL + +let builder = database.schema("planets").id() +if database is MySQLDatabase { + // 底层数据库驱动是MySQL。 + builder.field("name", .sql(raw: "VARCHAR(64)"), .required) +} else { + // 底层数据库驱动是_not_ MySQL。 + builder.field("name", .string, .required) +} +builder.create() +``` ## MongoDB -Fluent MongoDB is an integration between [Fluent](../fluent/overview.md) and the [MongoKitten](https://github.com/OpenKitten/MongoKitten/) driver. It leverages Swift's strong type system and Fluent's database agnostic interface using MongoDB. +Fluent MongoDB是[Fluent](../fluent/overview.md)和[MongoKitten](https://github.com/OpenKitten/MongoKitten/)驱动之间的集成。它利用Swift的强类型系统和Fluent的数据库无关的接口,使用MongoDB。 -The most common identifier in MongoDB is ObjectId. You can use this for your project using `@ID(custom: .id)`. -If you need to use the same models with SQL, do not use `ObjectId`. Use `UUID` instead. +MongoDB中最常见的标识符是ObjectId。你可以使用`@ID(custom: .id)`为你的项目使用这个。 +如果你需要用SQL使用相同的模型,不要使用`ObjectId`。使用`UUID`代替。 ```swift final class User: Model { - // Name of the table or collection. + // 表或集合的名称。 static let schema = "users" - // Unique identifier for this User. - // In this case, ObjectId is used - // Fluent recommends using UUID by default, however ObjectId is also supported + // 该用户的唯一标识符。 + // 在这种情况下,使用ObjectId。 + // Fluent推荐默认使用UUID,但也支持ObjectId。 @ID(custom: .id) var id: ObjectId? - // The User's email address + // 用户的电子邮件地址 @Field(key: "email") var email: String - // The User's password stores as a BCrypt hash + // 用户的密码以BCrypt哈希值的形式存储。 @Field(key: "password") var passwordHash: String - // Creates a new, empty User instance, for use by Fluent + // 创建一个新的、空的用户实例,供Fluent使用。 init() { } - // Creates a new User with all properties set. + // 创建一个新的用户,并设置所有属性。 init(id: ObjectId? = nil, email: String, passwordHash: String, profile: Profile) { self.id = id self.email = email @@ -39,49 +139,32 @@ final class User: Model { } ``` -### Data Modelling +### 数据建模 -In MongoDB, Models are defined in the same as in any other Fluent environment. The main difference between SQL databases and MongoDB lies in relationships and architecture. +在MongoDB中,模型的定义与其他Fluent环境相同。SQL数据库和MongoDB的主要区别在于关系和架构。 -In SQL environments, it's very common to create join tables for relationships between two entities. In MongoDB, however, an array can be used to store related identifiers. Due to the design of MongoDB, it's more efficient and practical to design your models with nested data structures. - -### Flexible Data - -You can add flexible data in MongoDB, but this code will not work in SQL environments. -To create grouped arbitrary data storage you can use `Document`. - -```swift -@Field(key: "document") -var document: Document -``` - -Fluent cannot support strictly types queries on these values. You can use a dot notated key path in your query for querying. -This is accepted in MongoDB to access nested values. - -```swift -Something.query(on: db).filter("document.key", .equal, 5).first() -``` +在SQL环境中,为两个实体之间的关系创建连接表是非常常见的。然而,在MongoDB中,可以使用数组来存储相关的标识符。由于MongoDB的设计,用嵌套的数据结构来设计你的模型更加有效和实用。 -### Flexible Data +### 灵活的数据 -You can add flexible data in MongoDB, but this code will not work in SQL environments. -To create grouped arbitrary data storage you can use `Document`. +你可以在MongoDB中添加灵活的数据,但是这段代码在SQL环境中无法工作。 +为了创建分组的任意数据存储,你可以使用`Document`。 ```swift @Field(key: "document") var document: Document ``` -Fluent cannot support strictly types queries on these values. You can use a dot notated key path in your query for querying. -This is accepted in MongoDB to access nested values. +Fluent不能支持对这些值的严格类型查询。你可以在你的查询中使用点符号的关键路径进行查询。 +这在MongoDB中被接受,用于访问嵌套值。 ```swift Something.query(on: db).filter("document.key", .equal, 5).first() ``` -### Raw Access +### 原始访问 -To access the raw `MongoDatabase` instance, cast the database instance to `MongoDatabaseRepresentable` as such: +要访问原始的`MongoDatabase`实例,请将数据库实例投给`MongoDatabaseRepresentable`。 ```swift guard let db = req.db as? MongoDatabaseRepresentable else { @@ -91,4 +174,4 @@ guard let db = req.db as? MongoDatabaseRepresentable else { let mongodb = db.raw ``` -From here you can use all of the MongoKitten APIs. +在这里,你可以使用所有的MongoKitten APIs。 diff --git a/4.0/docs/fluent/migration.md b/4.0/docs/fluent/migration.md index 90571bd..8637f89 100644 --- a/4.0/docs/fluent/migration.md +++ b/4.0/docs/fluent/migration.md @@ -1,27 +1,41 @@ -# Migrations +# 迁移 -Migrations are like a version control system for your database. Each migration defines a change to the database and how to undo it. By modifying your database through migrations, you create a consistent, testable, and shareable way to evolve your databases over time. +迁移就像是你的数据库的一个版本控制系统。每个迁移都定义了对数据库的改变,以及如何撤销它。通过迁移来修改你的数据库,你创建了一个一致的、可测试的、可共享的方式来逐步发展你的数据库。 ```swift -// An example migration. +// 一个迁移的例子。 struct MyMigration: Migration { func prepare(on database: Database) -> EventLoopFuture { - // Make a change to the database. + // 对数据库做一个改变。 } func revert(on database: Database) -> EventLoopFuture { - // Undo the change made in `prepare`, if possible. + // 如果可能的话,撤消在`prepare`中所作的修改。 } } ``` -The `prepare` method is where you make changes to the supplied `Database`. These could be changes to the database schema like adding or removing a table or collection, field, or constraint. They could also modify the database content, like creating new model instances, updating field values, or doing cleanup. +如果你使用`async`/`await`,你应该实现`AsyncMigration`协议。 -The `revert` method is where you undo these changes, if possible. Being able to undo migrations can make prototyping and testing easier. They also give you a backup plan if a deploy to production doesn't go as planned. +```swift +struct MyMigration: AsyncMigration { + func prepare(on database: Database) async throws { + // 对数据库做一个改变。 + } -## Register + func revert(on database: Database) async throws { + // 如果可能的话,撤消在`prepare`中所作的修改。 + } +} +``` -Migrations are registered to your application using `app.migrations`. +`prepare`方法是你对提供的`Database`进行修改的地方。这可能是对数据库模式的改变,比如添加或删除一个表或集合、字段或约束。他们也可以修改数据库内容,比如创建新的模型实例,更新字段值,或者进行清理。 + +如果可能的话,`revert`方法是你撤销这些修改的地方。能够撤销迁移可以使原型设计和测试更加容易。如果部署到生产中的工作没有按计划进行,他们也会给你一个备份计划。 + +## 注册 + +使用`app.migrations`将迁移注册到你的应用程序中。 ```swift import Fluent @@ -30,50 +44,53 @@ import Vapor app.migrations.add(MyMigration()) ``` -You can add a migration to a specific database using the `to` parameter, otherwise the default database will be used. +你可以使用`to`参数将迁移添加到一个特定的数据库,否则将使用默认数据库。 ```swift app.migrations.add(MyMigration(), to: .myDatabase) ``` -Migrations should be listed in order of dependency. For example, if `MigrationB` depends on `MigrationA`, it should be added to `app.migrations` second. +迁移应该按照依赖性的顺序排列。例如,如果`MigrationB`依赖于`MigrationA`,那么它应该被添加到`app.migrations`的第二部分。 -## Migrate +## 迁移 -To migrate your database, run the `migrate` command. +为了迁移数据库,运行`migrate`命令。 ```sh vapor run migrate ``` -You can also run this [command through Xcode](../advanced/commands.md#xcode). The migrate command will check the database to see if any new migrations have been registered since it was last run. If there are new migrations, it will ask for a confirmation before running them. +你也可以通过运行[Xcode命令](../advanced/commands.md#xcode)。migrate命令会检查数据库,看自它上次运行以来是否有新的迁移被注册。如果有新的迁移,它将在运行前要求确认。 -### Revert +### 恢复 -To undo a migration on your database, run `migrate` with the `--revert` flag. +要撤销数据库中的迁移,可以在运行`migrate`时加上`--revert`标志。 ```sh vapor run migrate --revert ``` -The command will check the database to see which batch of migrations was last run and ask for a confirmation before reverting them. +该命令将检查数据库,看最后运行的是哪一批迁移,并在恢复这些迁移之前要求进行确认。 -### Auto Migrate +### 自动迁移 -If you would like migrations to run automatically before running other commands, you can pass the `--auto-migrate` flag. +如果你希望在运行其他命令之前自动运行迁移,你可以通过`--auto-migrate`标志。 ```sh vapor run serve --auto-migrate ``` -You can also do this programatically. +你也可以通过编程来完成这个任务。 ```swift try app.autoMigrate().wait() + +// 或者 +try await app.autoMigrate() ``` -Both of these options exist for reverting as well: `--auto-revert` and `app.autoRevert()`. +这两个选项也都是用于还原的。`--auto-revert`和`app.autoRevert()`。 -## Next Steps +## 接下来的步骤 -Take a look at the [schema builder](schema.md) and [query builder](query.md) guides for more information about what to put inside your migrations. +看看[schema builder](./schema.md)和[query builder](./query.md)指南,了解更多关于在迁移过程中应该放什么的信息。 diff --git a/4.0/docs/fluent/model.md b/4.0/docs/fluent/model.md index fd5d401..cfe7a52 100644 --- a/4.0/docs/fluent/model.md +++ b/4.0/docs/fluent/model.md @@ -1,26 +1,26 @@ -# Models +# 模型 -Models represent data stored in tables or collections in your database. Models have one or more fields that store codable values. All models have a unique identifier. Property wrappers are used to denote identifiers, fields, and relations. +模型代表存储在数据库中的表或集合中的数据。模型有一个或多个字段来存储可编码的值。所有模型都有一个唯一的标识符。属性包装器被用来表示标识符、字段和关系。 -Below is an example of a simple model with one field. Note that models do not describe the entire database schema, such as constraints, indexes, and foreign keys. Schemas are defined in [migrations](migration.md). Models are focused on representing the data stored in your database schemas. +下面是一个有一个字段的简单模型的例子。注意,模型并不描述整个数据库模式,比如约束、索引和外键。模式是在[migrations](./migration.md)中定义的。模型的重点是表示存储在你的数据库模式中的数据。 ```swift final class Planet: Model { - // Name of the table or collection. + // 表或集合的名称。 static let schema = "planets" - // Unique identifier for this Planet. + // 该Planet的唯一标识符。 @ID(key: .id) var id: UUID? - // The Planet's name. + // Planet的名字。 @Field(key: "name") var name: String - // Creates a new, empty Planet. + // 创建一个新的、空的Planet。 init() { } - // Creates a new Planet with all properties set. + // 创建一个新的Planet,并设置所有属性。 init(id: UUID? = nil, name: String) { self.id = id self.name = name @@ -30,100 +30,100 @@ final class Planet: Model { ## Schema -All models require a static, get-only `schema` property. This string references the name of the table or collection this model represents. +所有模型都需要一个静态的、只可获取的`schema`属性。这个字符串引用了这个模型所代表的表或集合的名称。 ```swift final class Planet: Model { - // Name of the table or collection. + // 表或集合的名称。 static let schema = "planets" } ``` -When querying this model, data will be fetched from and stored to the schema named `"planets"`. +当查询这个模型时,数据将被提取并存储到名为`planets`的模式中。 -!!! tip - The schema name is typically the class name pluralized and lowercased. +!!!提示 + 模式名称通常是类名的复数和小写。 -## Identifier +## 标识符 -All models must have an `id` property defined using the `@ID` property wrapper. This field uniquely identifies instances of your model. +所有模型都必须有一个使用`@ID`属性包装器定义的`id`属性。这个字段唯一地标识了你的模型的实例。 ```swift final class Planet: Model { - // Unique identifier for this Planet. + // 该Planet的唯一标识符。 @ID(key: .id) var id: UUID? } ``` -By default, the `@ID` property should use the special `.id` key which resolves to an appropriate key for the underlying database driver. For SQL this is `"id"` and for NoSQL it is `"_id"`. +默认情况下,`@ID`属性应该使用特殊的`.id`键,它可以解析为底层数据库驱动的适当键。对于SQL来说,这是`id`,对于NoSQL来说,这是`_id`。 -The `@ID` should also be of type `UUID`. This is the only identifier value currently supported by all database drivers. Fluent will automatically generate new UUID identifiers when models are created. +`@ID`也应该是`UUID`类型。这是目前所有数据库驱动都支持的唯一标识符值。当模型被创建时,Fluent会自动生成新的UUID标识符。 -`@ID` has an optional value since unsaved models may not have an identifier yet. To get the identifier or throw an error, use `requireID`. +`@ID`有一个可选的值,因为未保存的模型可能还没有一个标识符。要获得标识符或抛出一个错误,请使用`requireID`。 ```swift let id = try planet.requireID() ``` -### Exists +### 存在 -`@ID` has an `exists` property that represents whether the model exists in the database or not. When you initialize a model, the value is `false`. After you save a model or when you fetch a model from the database, the value is `true`. This property is mutable. +`@ID`有一个`exists`属性,表示模型是否存在于数据库中。当你初始化一个模型时,其值是`false`。当你保存一个模型或从数据库中获取一个模型时,其值为`true`。这个属性是可变的。 ```swift if planet.$id.exists { - // This model exists in database. + // 这个模型存在于数据库中。 } ``` -### Custom Identifier +### 自定义标识符 -Fluent supports custom identifier keys and types using the `@ID(custom:)` overload. +Fluent支持使用`@ID(custom:)`重载的自定义标识符键和类型。 ```swift final class Planet: Model { - // Unique identifier for this Planet. + // 该Planet的唯一标识符。 @ID(custom: "foo") var id: Int? } ``` -The above example uses an `@ID` with custom key `"foo"` and identifier type `Int`. This is compatible with SQL databases using auto-incrementing primary keys, but is not compatible with NoSQL. +上面的例子使用了一个`@ID`,有自定义键`"foo"`和标识符类型`Int`。这与使用自动递增主键的SQL数据库兼容,但与NoSQL不兼容。 -Custom `@ID`s allow the user to specify how the identifier should be generated using the `generatedBy` parameter. +自定义`@ID`允许用户使用`generatedBy`参数指定标识符的生成方式。 ```swift @ID(custom: "foo", generatedBy: .user) ``` -The `generatedBy` parameter supports these cases: +`generatedBy`参数支持这些情况: -|Generated By|Description| -|-|-| -|`.user`|`@ID` property is expected to be set before saving a new model.| -|`.random`|`@ID` value type must conform to `RandomGeneratable`.| -|`.database`|Database is expected to generate a value upon save.| +| 生成者 | 描述 | +| ----------- | ----------------------------------------- | +| `.user` | `@ID`属性应该在保存一个新模型之前被设置。 | +| `.random` | `@ID`值类型必须符合`RandomGeneratable`。 | +| `.database` | 预计数据库在保存时将产生一个值。 | -If the `generatedBy` parameter is omitted, Fluent will attempt to infer an appropriate case based on the `@ID` value type. For example, `Int` will default to `.database` generation unless otherwise specified. +如果省略了`generatedBy`参数,Fluent将试图根据`@ID`值类型推断出一个合适的情况。例如,`Int`将默认为`.database`生成,除非另有规定。 -## Initializer +## 初始化器 -Models must have an empty initializer method. +模型必须有一个空的初始化方法。 ```swift final class Planet: Model { - // Creates a new, empty Planet. + // 创建一个新的、空的Planet。 init() { } } ``` -Fluent requires this method internally to initialize models returned by queries. It is also used for reflection. +Fluent内部需要这个方法来初始化查询返回的模型。它也被用于反射。 -You may want to add a convenience initializer to your model that accepts all properties. +你可能想给你的模型添加一个方便的初始化器,接受所有属性。 ```swift final class Planet: Model { - // Creates a new Planet with all properties set. + // 创建一个新的Planet,并设置所有属性。 init(id: UUID? = nil, name: String) { self.id = id self.name = name @@ -131,236 +131,236 @@ final class Planet: Model { } ``` -Using convenience initializers makes it easier to add new properties to the model in the future. +使用方便的初始化器使得将来向模型添加新的属性更加容易。 -## Field +## 字段 -Models can have zero or more `@Field` properties for storing data. +模型可以有零个或多个`@Field`属性用于存储数据。 ```swift final class Planet: Model { - // The Planet's name. + // 该Planet的名字 @Field(key: "name") var name: String } ``` -Fields require the database key to be explicitly defined. This is not required to be the same as the property name. +字段要求明确定义数据库键。这并不要求与属性名称相同。 -!!! tip - Fluent recommends using `snake_case` for database keys and `camelCase` for property names. +!!!提示 + Fluent建议数据库键使用`snake_case`,属性名使用`camelCase`。 -Field values can be any type that conforms to `Codable`. Storing nested structures and arrays in `@Field` is supported, but filtering operations are limited. See [`@Group`](#group) for an alternative. +字段值可以是任何符合`Codable`的类型。支持在`@Field`中存储嵌套结构和数组,但过滤操作受到限制。参见[`@Group`](#group)以获得替代方案。 -For fields that contain an optional value, use `@OptionalField`. +对于包含可选值的字段,使用`@OptionalField`。 ```swift @OptionalField(key: "tag") var tag: String? ``` -## Relations +## 关系 -Models can have zero or more relation properties referencing other models like `@Parent`, `@Children`, and `@Siblings`. Learn more about relations in the [relations](relations.md) section. +模型可以有零个或多个引用其他模型的关系属性,如 `@Parent`, `@Children`, 和 `@Siblings`。在[关系](./relations.md)部分了解更多关于关系的信息。 -## Timestamp +## 时间戳 -`@Timestamp` is a special type of `@Field` that stores a `Foundation.Date`. Timestamps are set automatically by Fluent according to the chosen trigger. +`@Timestamp`是一种特殊的`@Field`类型,用于存储一个`Foundation.Date`。时间戳是由Fluent根据所选择的触发器自动设置的。 ```swift final class Planet: Model { - // When this Planet was created. + // 这个Planet是什么时候创建的。 @Timestamp(key: "created_at", on: .create) var createdAt: Date? - // When this Planet was last updated. + // 这个Planet最后更新的时间。 @Timestamp(key: "updated_at", on: .update) var updatedAt: Date? } ``` -`@Timestamp` supports the following triggers. +`@Timestamp`支持以下触发器。 -|Trigger|Description| -|-|-| -|`.create`|Set when a new model instance is saved to the database.| -|`.update`|Set when an existing model instance is saved to the database.| -|`.delete`|Set when a model is deleted from the database. See [soft delete](#soft-delete).| +| 触发器 | 描述 | +| --------- | ------------------------------------------------------------ | +| `.create` | 当一个新的模型实例被保存到数据库时设置。 | +| `.update` | 当一个现有的模型实例被保存到数据库时设置。 | +| `.delete` | 当一个模型从数据库中被删除时设置。参见[软删除](#软删除)。 | -`@Timestamp`'s date value is optional and should be set to `nil` when initializing a new model. +`@Timestamp`的日期值是可选的,在初始化一个新模型时应设置为`nil`。 -### Timestamp Format +### 时间戳格式 -By default, `@Timestamp` will use an efficient datetime encoding based on your database driver. You can customize how the timestamp is stored in the database using the `format` parameter. +默认情况下,`@Timestamp`将使用基于你的数据库驱动的有效日期编码。你可以使用`format`参数来定制时间戳在数据库中的存储方式。 ```swift -// Stores an ISO 8601 formatted timestamp representing -// when this model was last updated. +// 存储一个ISO 8601格式的时间戳,代表这个模型的最后更新时间。 +// 这个模型最后一次被更新的时间。 @Timestamp(key: "updated_at", on: .update, format: .iso8601) var updatedAt: Date? ``` -Available timestamp formats are listed below. +可用的时间戳格式列举如下。 -|Format|Description|Type| -|-|-|-| -|`.default`|Uses efficient datetime encoding for specific database.|Date| -|`.iso8601`|[ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) string. Supports `withMilliseconds` parameter.|String| -|`.unix`|Seconds since Unix epoch including fraction.|Double| +| 格式 | 描述 | 类型 | +| ---------- | ------------------------------------------------------------ | ------ | +| `.default` | 为特定的数据库使用有效的日期时间编码。 | Date | +| `.iso8601` | [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601)字符串。支持`withMilliseconds`参数。 | String | +| `.unix` | 自Unix epoch以来的秒数,包括分数。 | Double | -You can access the raw timestamp value directly using the `timestamp` property. +你可以使用`timestamp`属性直接访问原始时间戳值。 ```swift -// Manually set the timestamp value on this ISO 8601 -// formatted @Timestamp. +// 在这个ISO 8601上手动设置时间戳值 +// 格式化的@Timestamp。 model.$updatedAt.timestamp = "2020-06-03T16:20:14+00:00" ``` -### Soft Delete +### 软删除 -Adding a `@Timestamp` that uses the `.delete` trigger to your model will enable soft-deletion. +在你的模型中添加一个使用`.delete`触发器的`@Timestamp`将启用软删除。 ```swift final class Planet: Model { - // When this Planet was deleted. + // 这个Planet被删除时间 @Timestamp(key: "deleted_at", on: .delete) var deletedAt: Date? } ``` -Soft-deleted models still exist in the database after deletion, but will not be returned in queries. +软删除的模型在删除后仍然存在于数据库中,但将不会在查询中返回。 -!!! tip - You can manually set an on delete timestamp to a date in the future. This can be used as an expiration date. +!!!提示 + 你可以手动设置一个删除时的时间戳到未来的一个日期。这可以作为一个到期日。 -To force a soft-deletable model to be removed from the database, use the `force` parameter in `delete`. +要强制将一个可软删除的模型从数据库中删除,使用 `delete` 中的 `force` 参数。 ```swift -// Deletes from the database even if the model -// is soft deletable. +// 从数据库中删除,即使该模型 +// 是可以软删除的。 model.delete(force: true, on: database) ``` -To restore a soft-deleted model, use the `restore` method. +要恢复一个软删除的模型,使用`restore`方法。 ```swift -// Clears the on delete timestamp allowing this -// model to be returned in queries. +// 清除删除时的时间戳,允许这个 +// 模型在查询中被返回。 model.restore(on: database) ``` -To include soft-deleted models in a query, use `withDeleted`. +要在查询中包括软删除的模型,使用 `withDeleted`。 ```swift -// Fetches all planets including soft deleted. +// 获取所有的Planet,包括软删除。 Planet.query(on: database).withDeleted().all() ``` ## Enum -`@Enum` is a special type of `@Field` for storing string representable types as native database enums. Native database enums provide an added layer of type safety to your database and may be more performant than raw enums. +`@Enum`是`@Field`的一种特殊类型,用于将字符串可表示的类型存储为本地数据库枚举。本地数据库枚举为你的数据库提供了一个额外的类型安全层,并且可能比原始枚举更有性能。 ```swift -// String representable, Codable enum for animal types. +// 字符串可表示,动物类型的可编码枚举。 enum Animal: String, Codable { case dog, cat } final class Pet: Model { - // Stores type of animal as a native database enum. + // 将动物的类型存储为本地数据库枚举。 @Enum(key: "type") var type: Animal } ``` -Only types conforming to `RawRepresentable` where `RawValue` is `String` are compatible with `@Enum`. `String` backed enums meet this requirement by default. +只有符合`RawRepresentable`的类型,其中`RawValue`是`String`,才与`@Enum`兼容。`String`支持的枚举默认满足这一要求。 -To store an optional enum, use `@OptionalEnum`. +要存储一个可选的枚举,请使用`@OptionalEnum`。 -The database must be prepared to handle enums via a migration. See [enum](schema.md#enum) for more information. +数据库必须准备好通过迁移来处理枚举。更多信息请参见[enum](./schema.md#enum)。 -### Raw Enums +### 原始枚举 -Any enum backed by a `Codable` type, like `String` or `Int`, can be stored in `@Field`. It will be stored in the database as the raw value. +任何由`Codable`类型支持的枚举,如`String`或`Int`,都可以存储在`@Field`中。它将作为原始值存储在数据库中。 -## Group +## 组 -`@Group` allows you to store a nested group of fields as a single property on your model. Unlike Codable structs stored in a `@Field`, the fields in a `@Group` are queryable. Fluent achieves this by storing `@Group` as a flat structure in the database. +`@Group`允许你将一组嵌套的字段作为一个单一的属性存储在你的模型上。与存储在`@Field`中的可编码结构不同,`@Group`中的字段是可查询的。Fluent通过将`@Group`作为一个平面结构存储在数据库中来实现这一点。 -To use a `@Group`, first define the nested structure you would like to store using the `Fields` protocol. This is very similar to `Model` except no identifier or schema name is required. You can store many properties here that `Model` supports like `@Field`, `@Enum`, or even another `@Group`. +要使用`@Group`,首先要使用`Fields`协议定义你想存储的嵌套结构。这与`Model`非常相似,只是不需要标识符或模式名称。你可以在这里存储许多`Model`支持的属性,如`@Field`,`@Enum`,甚至另一个`@Group`。 ```swift -// A pet with name and animal type. +// 一个有名字和动物类型的宠物。 final class Pet: Fields { - // The pet's name. + // 宠物的名字。 @Field(key: "name") var name: String - // The type of pet. + // 宠物的类型。 @Field(key: "type") var type: String - // Creates a new, empty Pet. + // 创建一个新的、空的宠物。 init() { } } ``` -After you've created the fields definition, you can use it as the value of a `@Group` property. +在你创建了字段定义后,你可以把它作为`@Group`属性的值。 ```swift final class User: Model { - // The user's nested pet. + // 用户的嵌套宠物。 @Group(key: "pet") var pet: Pet } ``` -A `@Group`'s fields are accessible via dot-syntax. +一个`@Group`的字段可以通过点语法访问。 ```swift let user: User = ... print(user.pet.name) // String ``` -You can query nested fields like normal using dot-syntax on the property wrappers. +你可以像平常一样使用属性包装器上的点语法查询嵌套字段。 ```swift User.query(on: database).filter(\.$pet.$name == "Zizek").all() ``` -In the database, `@Group` is stored as a flat structure with keys joined by `_`. Below is an example of how `User` would look in the database. +在数据库中,`@Group`被存储为一个平面结构,键由`_`连接。下面是一个例子,说明`User'在数据库中的样子。 -|id|name|pet_name|pet_type| -|-|-|-|-| -|1|Tanner|Zizek|Cat| -|2|Logan|Runa|Dog| +| id | name | pet_name | pet_type | +| ---- | ------ | -------- | -------- | +| 1 | Tanner | Zizek | Cat | +| 2 | Logan | Runa | Dog | ## Codable -Models conform to `Codable` by default. This means you can use your models with Vapor's [content API](../basics/content.md) by adding conformance to the `Content` protocol. +模型默认符合`Codable`。这意味着你可以在Vapor的[内容API](../basics/content.md)中使用你的模型,只要加入对`Content`协议的符合性。 ```swift extension Planet: Content { } app.get("planets") { req in - // Return an array of all planets. + // 返回一个所有Planet的数组。 Planet.query(on: req.db).all() } ``` -When serializing to / from `Codable`, model properties will use their variable names instead of keys. Relations will serialize as nested structures and any eager loaded data will be included. +当从`Codable`序列化时,模型属性将使用它们的变量名而不是键。关系将被序列化为嵌套结构,任何急于加载的数据将被包括在内。 -### Data Transfer Object +### 数据传输对象 -Model's default `Codable` conformance can make simple usage and prototyping easier. However, it is not suitable for every use case. For certain situations you will need to use a data transfer object (DTO). +模型默认的`Codable`一致性可以使简单的使用和原型设计更容易。然而,它并不适合于每一种使用情况。对于某些情况,你需要使用数据传输对象(DTO)。 -!!! tip - A DTO is a separate `Codable` type representing the data structure you would like to encode or decode. +!!! 提示 + DTO是一个单独的`Codable`类型,代表你想编码或解码的数据结构。 -Assume the following `User` model in the upcoming examples. +在接下来的例子中,假设有以下`User`模型。 ```swift -// Abridged user model for reference. +// 简略的用户模型供参考。 final class User: Model { @ID(key: .id) var id: UUID? @@ -373,52 +373,52 @@ final class User: Model { } ``` -One common use case for DTOs is in implementing `PATCH` requests. These requests only include values for fields that should be updated. Attempting to decode a `Model` directly from such a request would fail if any of the required fields were missing. In the example below, you can see a DTO being used to decode request data and update a model. +DTO的一个常见用例是实现`PATCH`请求。这些请求只包括应该被更新的字段的值。如果缺少任何所需的字段,试图直接从这样的请求中解码`Model`将会失败。在下面的例子中,你可以看到一个DTO被用来解码请求数据并更新一个模型。 ```swift -// Structure of PATCH /users/:id request. +// PATCH /users/:id请求的结构。 struct PatchUser: Decodable { var firstName: String? var lastName: String? } app.patch("users", ":id") { req in - // Decode the request data. + // 对请求数据进行解码。 let patch = try req.content.decode(PatchUser.self) - // Fetch the desired user from the database. + // 从数据库中获取所需的用户。 return User.find(req.parameters.get("id"), on: req.db) .unwrap(or: Abort(.notFound)) .flatMap { user in - // If first name was supplied, update it. + // 如果提供了名字,则更新它。 if let firstName = patch.firstName { user.firstName = firstName } - // If new last name was supplied, update it. + // 如果提供了新的姓氏,就更新它。 if let lastName = patch.lastName { user.lastName = lastName } - // Save the user and return it. + // 保存用户并返回。 return user.save(on: req.db) .transform(to: user) } } ``` -Another common use case for DTOs is customizing the format of your API responses. The example below shows how a DTO can be used to add a computed field to a response. +DTO的另一个常见的用例是定制你的API响应的格式。下面的例子显示了如何使用DTO来为响应添加一个计算字段。 ```swift -// Structure of GET /users response. +// GET /users 响应的结构。 struct GetUser: Content { var id: UUID var name: String } app.get("users") { req in - // Fetch all users from the database. + // 从数据库中获取所有用户。 User.query(on: req.db).all().flatMapThrowing { users in try users.map { user in - // Convert each user to GET return type. + // 将每个用户转换为GET返回类型。 try GetUser( id: user.requireID(), name: "\(user.firstName) \(user.lastName)" @@ -428,43 +428,43 @@ app.get("users") { req in } ``` -Even if the DTO's structure is identical to model's `Codable` conformance, having it as a separate type can help keep large projects tidy. If you ever need to make a change to your models properties, you don't have to worry about breaking your app's public API. You may also consider putting your DTOs in a separate package that can be shared with consumers of your API. +即使DTO的结构与model的`Codable`一致性相同,把它作为一个单独的类型可以帮助保持大型项目的整洁。如果你需要改变你的模型属性,你不必担心会破坏你的应用程序的公共API。你也可以考虑把你的DTO放在一个单独的包里,可以与你的API的消费者共享。 -For these reasons, we highly recommend using DTOs wherever possible, especially for large projects. +由于这些原因,我们强烈建议尽可能地使用DTOs,特别是对于大型项目。 -## Alias +## 别名 -The `ModelAlias` protocol lets you uniquely identify a model being joined multiple times in a query. For more information, see [joins](query.md#join). +`ModelAlias`协议可以让你在查询中唯一地识别一个被多次连接的模型。更多信息,请参阅[joins](./query.md#join)。 -## Save +## 保存 -To save a model to the database, use the `save(on:)` method. +要保存一个模型到数据库,请使用`save(on:)`方法。 ```swift planet.save(on: database) ``` -This method will call `create` or `update` internally depending on whether the model already exists in the database. +这个方法将在内部调用`创建`或`更新`,取决于模型是否已经存在于数据库中。 -### Create +### 创建 -You can call the `create` method to save a new model to the database. +你可以调用`create`方法来保存一个新模型到数据库中。 ```swift let planet = Planet(name: "Earth") planet.create(on: database) ``` -`create` is also available on an array of models. This saves all of the models to the database in a single batch / query. +`create`也可用于模型的数组。这在一个批次/查询中把所有的模型保存到数据库中。 ```swift -// Example of batch create. +// 批量创建的例子。 [earth, mars].create(on: database) ``` -### Update +### 更新 -You can call the `update` method to save a model that was fetched from the database. +你可以调用`update`方法来保存一个从数据库中获取的模型。 ```swift Planet.find(..., on: database).flatMap { planet in @@ -473,60 +473,60 @@ Planet.find(..., on: database).flatMap { planet in } ``` -## Query +## 查询 -Models expose a static method `query(on:)` that returns a query builder. +模型暴露了一个静态方法`query(on:)`,返回一个查询生成器。 ```swift Planet.query(on: database).all() ``` -Learn more about querying in the [query](./query.md) section. +在[查询](./query.md)部分了解更多关于查询的信息。 -## Find +## 查找 -Models have a static `find(_:on:)` method for looking up a model instance by identifier. +模型有一个静态的`find(_:on:)`方法,用于通过标识符查找一个模型实例。 ```swift Planet.find(req.parameters.get("id"), on: database) ``` -This method returns `nil` if no model with that identifier was found. +如果没有找到具有该标识符的模型,该方法返回`nil`。 -## Lifecycle +## 生命周期 -Model middleware allow you to hook into your model's lifecycle events. The following lifecycle events are supported. +模型中间件允许你挂入你的模型的生命周期事件。支持以下的生命周期事件。 -|Method|Description| -|-|-| -|`create`|Runs before a model is created.| -|`update`|Runs before a model is updated.| -|`delete(force:)`|Runs before a model is deleted.| -|`softDelete`|Runs before a model is soft deleted.| -|`restore`|Runs before a model is restored (opposite of soft delete).| +| 方法 | 描述 | +| ---------------- | ------------------------------------ | +| `create` | 在创建一个模型之前运行。 | +| `update` | 在模型更新前运行。 | +| `delete(force:)` | 在一个模型被删除之前运行。 | +| `softDelete` | 在一个模型被软删除之前运行。 | +| `restore` | 在恢复模型之前运行(与软删除相反)。 | -Model middleware are declared using the `ModelMiddleware` protocol. All lifecycle methods have a default implementation, so you only need to implement the methods you require. Each method accepts the model in question, a reference to the database, and the next action in the chain. The middleware can choose to return early, throw an error, or call the next action to continue normally. +模型中间件使用`ModelMiddleware`协议来声明。所有的生命周期方法都有一个默认的实现,所以你只需要实现你需要的方法。每个方法都接受有关的模型、对数据库的引用以及链中的下一个动作。中间件可以选择提前返回,抛出一个错误,或者调用下一个动作继续正常进行。 -Using these methods you can perform actions both before and after the specific event completes. Performing actions after the event completes can be done by mapping the future returned from the next responder. +使用这些方法,你可以在特定事件完成之前和之后执行动作。在事件完成后执行动作可以通过映射下一个响应者返回的未来来完成。 ```swift -// Example middleware that capitalizes names. +// 将名字大写的中间件示例。 struct PlanetMiddleware: ModelMiddleware { func create(model: Planet, on db: Database, next: AnyModelResponder) -> EventLoopFuture { - // The model can be altered here before it is created. + // 模型在创建之前可以在这里进行修改。 model.name = model.name.capitalized() return next.create(model, on: db).map { - // Once the planet has been created, the code - // here will be executed. + // 一旦行星被创建,这里的代码 + // 这里将被执行。 print ("Planet \(model.name) was created") } } } ``` -Once you have created your middleware, you can enable it using `app.databases.middleware`. +一旦你创建了你的中间件,你可以使用`app.databases.middleware`来启用它。 ```swift -// Example of configuring model middleware. +// 配置模型中间件的例子。 app.databases.middleware.use(PlanetMiddleware(), on: .psql) ``` diff --git a/4.0/docs/fluent/overview.md b/4.0/docs/fluent/overview.md index b6bf9d7..84ce26e 100644 --- a/4.0/docs/fluent/overview.md +++ b/4.0/docs/fluent/overview.md @@ -1,20 +1,20 @@ # Fluent -Fluent is an [ORM](https://en.wikipedia.org/wiki/Object-relational_mapping) framework for Swift. It takes advantage of Swift's strong type system to provide an easy-to-use interface for your database. Using Fluent centers around the creation of model types which represent data structures in your database. These models are then used to perform create, read, update, and delete operations instead of writing raw queries. +Fluent是Swift的一个[ORM](https://en.wikipedia.org/wiki/Object-relational_mapping)框架。它利用Swift强大的类型系统,为你的数据库提供一个易于使用的接口。使用Fluent的核心是创建模型类型,代表你数据库中的数据结构。这些模型然后被用来执行创建、读取、更新和删除操作,而不是编写原始查询。 -## Configuration +## 配置 -When creating a project using `vapor new`, answer "yes" to including Fluent and choose which database driver you want to use. This will automatically add the dependencies to your new project as well as example configuration code. +当使用`vapor new`创建一个项目时,回答"YES"包括Fluent并选择你想使用的数据库驱动。这将自动为你的新项目添加依赖项,以及配置代码的例子。 -### Existing Project +### 现有项目 -If you have an existing project that you want to add Fluent to, you will need to add two dependencies to your [package](../start/spm.md): +如果你有一个现有的项目想加入Fluent,你需要在你的[package](../start/spm.md)中添加两个依赖项。 - [vapor/fluent](https://github.com/vapor/fluent)@4.0.0 -- One (or more) Fluent driver(s) of your choice +- 你选择的一个(或多个)Fluent驱动程序 ```swift -.package(url: "https://github.com/vapor/fluent.git", from: "4.0.0-beta"), +.package(url: "https://github.com/vapor/fluent.git", from: "4.0.0"), .package(url: "https://github.com/vapor/fluent--driver.git", from: ), ``` @@ -26,7 +26,7 @@ If you have an existing project that you want to add Fluent to, you will need to ]), ``` -Once the packages are added as dependencies, you can configure your databases using `app.databases` in `configure.swift`. +一旦软件包被添加为依赖项,你可以使用`configure.swift`中的`app.databases`来配置你的数据库。 ```swift import Fluent @@ -35,27 +35,27 @@ import FluentDriver app.databases.use(, as: ) ``` -Each of the Fluent drivers below has more specific instructions for configuration. +以下每个Fluent驱动程序都有更具体的配置说明。 -### Drivers +### 驱动程序 -Fluent currently has three officially supported drivers. You can search GitHub for the tag [`fluent-driver`](https://github.com/topics/fluent-database) for a full list of official and third-party Fluent database drivers. +Fluent目前有四个官方支持的驱动程序。你可以在GitHub上搜索标签[`fluent-driver`](https://github.com/topics/fluent-driver),以获得官方和第三方Fluent数据库驱动的完整列表。 #### PostgreSQL -PostgreSQL is an open source, standards compliant SQL database. It is easily configurable on most cloud hosting providers. This is Fluent's **recommended** database driver. +PostgreSQL是一个开源的、符合标准的SQL数据库。它很容易在大多数云主机供应商上配置。这是Fluent公司**推荐的**数据库驱动。 -To use PostgreSQL, add the following dependencies to your package. +要使用PostgreSQL,请在你的软件包中添加以下依赖项。 ```swift -.package(url: "https://github.com/vapor/fluent-postgres-driver.git", from: "2.0.0-beta") +.package(url: "https://github.com/vapor/fluent-postgres-driver.git", from: "2.0.0") ``` ```swift .product(name: "FluentPostgresDriver", package: "fluent-postgres-driver") ``` -Once the dependencies are added, configure the database's credentials with Fluent using `app.databases.use` in `configure.swift`. +一旦添加了依赖关系,使用`configure.swift`中的`app.databases.use`将数据库的凭证配置给Fluent。 ```swift import Fluent @@ -64,7 +64,7 @@ import FluentPostgresDriver app.databases.use(.postgres(hostname: "localhost", username: "vapor", password: "vapor", database: "vapor"), as: .psql) ``` -You can also parse the credentials from a database connection string. +你也可以从数据库连接字符串中解析凭证。 ```swift try app.databases.use(.postgres(url: ""), as: .psql) @@ -72,19 +72,19 @@ try app.databases.use(.postgres(url: ""), as: .psql) #### SQLite -SQLite is an open source, embedded SQL database. Its simplistic nature makes it a great candiate for prototyping and testing. +SQLite是一个开源的、嵌入式的SQL数据库。它的简单性使它成为原型设计和测试的最佳选择。 -To use SQLite, add the following dependencies to your package. +要使用SQLite,请在你的软件包中添加以下依赖项。 ```swift -.package(url: "https://github.com/vapor/fluent-sqlite-driver.git", from: "4.0.0-beta") +.package(url: "https://github.com/vapor/fluent-sqlite-driver.git", from: "4.0.0") ``` ```swift .product(name: "FluentSQLiteDriver", package: "fluent-sqlite-driver") ``` -Once the dependencies are added, configure the database with Fluent using `app.databases.use` in `configure.swift`. +一旦添加了依赖关系,使用`configure.swift`中的`app.databases.use`来配置Fluent的数据库。 ```swift import Fluent @@ -93,24 +93,29 @@ import FluentSQLiteDriver app.databases.use(.sqlite(.file("db.sqlite")), as: .sqlite) ``` -You can also configure SQLite to store the database ephemerally in memory. +你也可以配置SQLite在内存中短暂地存储数据库。 ```swift app.databases.use(.sqlite(.memory), as: .sqlite) ``` -If you use an in-memory database, make sure to set Fluent to migrate automatically using `--auto-migrate` or run `app.autoMigrate()` after adding migrations. +如果你使用的是内存数据库,请确保使用`--auto-migrate`将Fluent设置为自动迁移,或者在添加迁移后运行`app.autoMigrate()`。 ```swift app.migrations.add(CreateTodo()) try app.autoMigrate().wait() +// 或 +try await app.autoMigrate() ``` +!!!提示 + SQLite配置会自动对所有创建的连接启用外键约束,但不会改变数据库本身的外键配置。直接删除数据库中的记录,可能会违反外键约束和触发器。 + #### MySQL -MySQL is a popular open source SQL database. It is available on many cloud hosting providers. This driver also supports MariaDB. +MySQL是一个流行的开源SQL数据库。它在许多云主机供应商上都可以使用。这个驱动也支持MariaDB。 -To use MySQL, add the following dependencies to your package. +要使用MySQL,请在你的软件包中添加以下依赖项。 ```swift .package(url: "https://github.com/vapor/fluent-mysql-driver.git", from: "4.0.0-beta") @@ -120,7 +125,7 @@ To use MySQL, add the following dependencies to your package. .product(name: "FluentMySQLDriver", package: "fluent-mysql-driver") ``` -Once the dependencies are added, configure the database's credentials with Fluent using `app.databases.use` in `configure.swift`. +一旦添加了依赖关系,使用`configure.swift`中的`app.databases.use`将数据库的凭证配置给Fluent。 ```swift import Fluent @@ -129,17 +134,38 @@ import FluentMySQLDriver app.databases.use(.mysql(hostname: "localhost", username: "vapor", password: "vapor", database: "vapor"), as: .mysql) ``` -You can also parse the credentials from a database connection string. +你也可以从数据库连接字符串中解析凭证。 ```swift try app.databases.use(.mysql(url: ""), as: .mysql) ``` +要配置一个不涉及SSL证书的本地连接,你应该禁用证书验证。例如,如果在Docker中连接到MySQL 8数据库,你可能需要这样做。 + +```swift +var tls = TLSConfiguration.makeClientConfiguration() +tls.certificateVerification = .none + +app.databases.use(.mysql( + hostname: "localhost", + username: "vapor", + password: "vapor", + database: "vapor", + tlsConfiguration: tls +), as: .mysql) +``` + +!!! warning + 请不要在生产中禁用证书验证。你应该向`TLSConfiguration`提供一个证书来验证。 + #### MongoDB -MongoDB is a popular schemaless NoSQL database designed for programmers. The driver supports all cloud hosting providers and self-hosted installations from version 3.4 and up. +MongoDB是一个流行的无模式NoSQL数据库,为程序员设计。该驱动支持所有的云主机供应商和3.4以上版本的自我托管安装。 + +!!!注意 + 该驱动由社区创建和维护的MongoDB客户端提供支持,该客户端名为[MongoKitten](https://github.com/OpenKitten/MongoKitten)。MongoDB维护着一个官方客户端,[mongo-swift-driver](https://github.com/mongodb/mongo-swift-driver),以及一个Vapor集成,[mongodb-vapor](https://github.com/mongodb/mongodb-vapor)。 -To use MongoDB, add the following dependencies to your package. +要使用MongoDB,请在你的软件包中添加以下依赖项。 ```swift .package(url: "https://github.com/vapor/fluent-mongo-driver.git", from: "1.0.0"), @@ -149,9 +175,9 @@ To use MongoDB, add the following dependencies to your package. .product(name: "FluentMongoDriver", package: "fluent-mongo-driver") ``` -Once the dependencies are added, configure the database's credentials with Fluent using `app.databases.use` in `configure.swift`. +一旦添加了依赖关系,使用`configure.swift`中的`app.databases.use`将数据库的凭证配置给Fluent。 -To connect, pass a connection string in the standard MongoDB [connection URI format](https://docs.mongodb.com/master/reference/connection-string/index.html). +要进行连接,请传递一个标准的MongoDB[连接URI格式](https://docs.mongodb.com/master/reference/connection-string/index.html)的连接字符串。 ```swift import Fluent @@ -160,27 +186,27 @@ import FluentMongoDriver try app.databases.use(.mongo(connectionString: ""), as: .mongo) ``` -## Models +## 模型 -Models represent fixed data structures in your database, like tables or collections. Models have one or more fields that store codable values. All models also have a unique identifier. Property wrappers are used to denote identifiers and fields as well as more complex mappings mentioned later. Take a look at the following model which represents a galaxy. +模型代表你数据库中的固定数据结构,像表或集合。模型有一个或多个字段来存储可编码的值。所有的模型也有一个唯一的标识符。属性包装器被用来表示标识符和字段,以及后面提到的更复杂的映射关系。看看下面的模型,它表示一个星系。 ```swift final class Galaxy: Model { - // Name of the table or collection. + // 表或集合的名称。 static let schema = "galaxies" - // Unique identifier for this Galaxy. + // 这个Galaxy的唯一标识符。 @ID(key: .id) var id: UUID? - // The Galaxy's name. + // 银河系的名字。 @Field(key: "name") var name: String - // Creates a new, empty Galaxy. + // 创建一个新的、空的Galaxy。 init() { } - // Creates a new Galaxy with all properties set. + // 创建一个新的Galaxy,并设置所有属性。 init(id: UUID? = nil, name: String) { self.id = id self.name = name @@ -188,50 +214,50 @@ final class Galaxy: Model { } ``` -To create a new model, create a new class conforming to `Model`. +要创建一个新的模型,创建一个符合`Model`的新类。 -!!! tip - It's recommended to mark model classes `final` to improve performance and simplify conformance requirements. +!!!提示 + 建议将模型类标记为`final`,以提高性能并简化一致性要求。 -The `Model` protocol's first requirement is the static string `schema`. +`Model`协议的第一个要求是静态字符串`schema`。 ```swift static let schema = "galaxies" ``` -This property tells Fluent which table or collection the model corresponds to. This can be a table that already exists in the database or one that you will create with a [migration](#migration). The schema is usually `snake_case` and plural. +这个属性告诉Fluent这个模型对应于哪个表或集合。这可以是一个已经存在于数据库中的表,也可以是一个你将通过[迁移](#migrations)创建的表。该模式通常是`snake_case`和复数。 -### Identifier +### 标识符 -The next requirement is an identifier field named `id`. +下一个要求是一个名为`id'的标识符字段。 ```swift @ID(key: .id) var id: UUID? ``` -This field must use the `@ID` property wrapper. Fluent recommends using `UUID` and the special `.id` field key since this is compatible with all of Fluent's drivers. +这个字段必须使用`@ID`属性包装器。Fluent推荐使用`UUID`和特殊的`.id`字段键,因为这与Fluent的所有驱动兼容。 -If you want to use a custom ID key or type, use the `@ID(custom:)` overload. +如果你想使用一个自定义的ID键或类型,请使用[`@ID(custom:)`](model.md#custom-identifier) 重载。 -### Fields +### 字段 -After the identifier is added, you can add however many fields you'd like to store additional information. In this example, the only additional field is the galaxy's name. +在标识符被添加后,你可以添加任何你想要的字段来存储额外信息。在这个例子中,唯一的附加字段是星系的名字。 ```swift @Field(key: "name") var name: String ``` -For simple fields, the `@Field` property wrapper is used. Like `@ID`, the `key` parameter specifies the field's name in the database. This is especially useful for cases where database field naming convention may be different than in Swift, e.g., using `snake_case` instead of `camelCase`. +对于简单的字段,使用`@Field`属性包装器。和`@ID`一样,`key`参数指定了数据库中字段的名称。这对于数据库字段命名规则可能与Swift中不同的情况特别有用,例如使用`snake_case`而不是`camelCase`。 -Next, all models require an empty init. This allows Fluent to create new instances of the model. +接下来,所有模型都需要一个空的init。这允许Fluent创建模型的新实例。 ```swift init() { } ``` -Finally, you can add a convenience init for your model that sets all of its properties. +最后,你可以为你的模型添加一个方便的init,设置其所有的属性。 ```swift init(id: UUID? = nil, name: String) { @@ -240,54 +266,54 @@ init(id: UUID? = nil, name: String) { } ``` -Using convenience inits is especially helpful if you add new properties to your model as you can get compile-time errors if the init method changes. +如果你向你的模型添加新的属性,使用方便的inits特别有帮助,因为如果init方法改变了,你会得到编译时错误。 -## Migrations +## 迁移 -If your database uses pre-defined schemas, like SQL databases, you will need a migration to prepare the database for your model. Migrations are also useful for seeding databases with data. To create a migration, define a new type conforming to the `Migration` protocol. Take a look at the following migration for the previously defined `Galaxy` model. +如果你的数据库使用预定义的模式,如SQL数据库,你将需要一个迁移来为你的模型准备数据库。迁移对于用数据播种数据库也很有用。要创建一个迁移,需要定义一个符合`Migration`或`AsyncMigration`协议的新类型。请看下面的迁移,它适用于之前定义的 "Galaxy "模型。 ```swift -struct CreateGalaxy: Migration { - // Prepares the database for storing Galaxy models. - func prepare(on database: Database) -> EventLoopFuture { - database.schema("galaxies") +struct CreateGalaxy: AsyncMigration { + // 准备数据库以存储Galaxy模型。 + func prepare(on database: Database) async throws { + try await database.schema("galaxies") .id() .field("name", .string) .create() } - // Optionally reverts the changes made in the prepare method. - func revert(on database: Database) -> EventLoopFuture { - database.schema("galaxies").delete() + // 可选择恢复prepare方法中的修改。 + func revert(on database: Database) async throws { + try await database.schema("galaxies").delete() } } ``` -The `prepare` method is used for preparing the database to store `Galaxy` models. +`prepare`方法用于准备数据库以存储`Galaxy`模型。 -### Schema +### 模式 -In this method, `database.schema(_:)` is used to create a new `SchemaBuilder`. One or more `field`s are then added to the builder before calling `create()` to create the schema. +在这个方法中,`database.schema(_:)`被用来创建一个新的`SchemaBuilder`。在调用`create()`创建模式之前,一个或多个`字段'被添加到创建器中。 -Each field added to the builder has a name, type, and optional constraints. +每个添加到构建器的字段都有一个名称、类型和可选的约束。 ```swift field(, , ) ``` -There is a convenience `id()` method for adding `@ID` properties using Fluent's recommended defaults. +有一个方便的`id()`方法可以使用Fluent推荐的默认值添加`@ID`属性。 -Reverting the migration undoes any changes made in the prepare method. In this case, that means deleting the Galaxy's schema. +恢复迁移会撤销在prepare方法中做出的任何改变。在这种情况下,这意味着删除Galaxy的模式。 -Once the migration is defined, you must tell Fluent about it by adding it to `app.migrations` in `configure.swift`. +一旦定义了迁移,你必须把它加入到`configure.swift`中的`app.migrations`中,以此来告诉Fluent。 ```swift app.migrations.add(CreateGalaxy()) ``` -### Migrate +### 迁移 -To run migrations, call `vapor run migrate` from the command line or add `migrate` as an argument to Xcode's Run scheme. +要运行迁移,可以在命令行中调用`vapor run migrate`或者在Xcode的Run scheme中添加`migrate`作为参数。 ``` @@ -300,21 +326,21 @@ y/n> y Migration successful ``` -## Querying +## 查询 -Now that you've successfully created a model and migrated your database, you're ready to make your first query. +现在,你已经成功地创建了一个模型并迁移了你的数据库,你已经准备好进行你的第一次查询。 -### All +###所有 -Take a look at the following route which will return an array of all the galaxies in the database. +看看下面的路线,它将返回数据库中所有星系的一个数组。 ```swift -app.get("galaxies") { req in - Galaxy.query(on: req.db).all() +app.get("galaxies") { req async throws in + try await Galaxy.query(on: req.db).all() } ``` -In order to return a Galaxy directly in a route closure, add conformance to `Content`. +为了在路由闭合中直接返回一个Galaxy,请在`内容'中添加一致性。 ```swift final class Galaxy: Model, Content { @@ -322,14 +348,14 @@ final class Galaxy: Model, Content { } ``` -`Galaxy.query` is used to create a new query builder for the model. `req.db` is a reference to the default database for your application. Finally, `all()` returns all of the models stored in the database. +`Galaxy.query`是用来为模型创建一个新的查询构建器。`req.db`是对你应用程序的默认数据库的引用。最后,`all()`返回存储在数据库中的所有模型。 -If you compile and run the project and request `GET /galaxies`, you should see an empty array returned. Let's add a route for creating a new galaxy. +如果你编译并运行项目,请求`GET /galaxies`,你应该看到返回一个空数组。让我们添加一个创建新星系的路由。 -### Create +### 创建 -Following RESTful convention, use the `POST /galaxies` endpoint for creating a new galaxy. Since models are codable, you can decode a galaxy directly from the request body. +按照RESTful惯例,使用`POST /galaxies`端点来创建一个新星系。由于模型是可编码的,你可以直接从请求体中解码一个星系。 ```swift app.post("galaxies") { req -> EventLoopFuture in @@ -339,12 +365,24 @@ app.post("galaxies") { req -> EventLoopFuture in } ``` -!!! seealso - See [Content → Overview](../basics/content.md) for more information about decoding request bodies. +!!! 另见 + 参见 [Content → Overview](../basics/content.md) 了解更多关于解码请求体的信息。 -Once you have an instance of the model, calling `create(on:)` saves the model to the database. This returns an `EventLoopFuture` which signals that the save has completed. Once the save completes, return the newly created model using `map`. +一旦你有了模型的实例,调用`create(on:)`将模型保存到数据库中。这将返回一个`EventLoopFuture`,这表明保存已经完成。一旦保存完成,使用`map`返回新创建的模型。 -Build and run the project and send the following request. +如果你使用`async`/`await`,你可以这样写你的代码。 + +```swift +app.post("galaxies") { req async throws -> Galaxy in + let galaxy = try req.content.decode(Galaxy.self) + try await galaxy.create(on: req.db) + return galaxy +} +``` + +在这种情况下,异步版本不会返回任何东西,但一旦保存完成就会返回。 + +建立并运行该项目,并发送以下请求。 ```http POST /galaxies HTTP/1.1 @@ -356,7 +394,7 @@ content-type: application/json } ``` -You should get the created model back with an identifier as the response. +你应该得到创建的模型和一个标识符作为响应。 ```json { @@ -365,34 +403,34 @@ You should get the created model back with an identifier as the response. } ``` -Now, if you query `GET /galaxies` again, you should see the newly created galaxy returned in the array. +现在,如果你再次查询`GET /galaxies`,你应该看到新创建的星系在数组中返回。 -## Relations +## 关联 -What are galaxies without stars! Let's take a quick look at Fluent's powerful relational features by adding a one-to-many relation between `Galaxy` and a new `Star` model. +没有恒星的星系是什么呢?让我们通过在`Galaxy`和一个新的`Star`模型之间添加一对多的关系,来快速了解一下Fluent强大的关系功能。 ```swift final class Star: Model, Content { - // Name of the table or collection. + // 表或集合的名称。 static let schema = "stars" - // Unique identifier for this Star. + // 该星的唯一标识符。 @ID(key: .id) var id: UUID? - // The Star's name. + // "明星"的名字。 @Field(key: "name") var name: String - // Reference to the Galaxy this Star is in. + // 参考这颗星所处的星系。 @Parent(key: "galaxy_id") var galaxy: Galaxy - // Creates a new, empty Star. + // 创建一个新的、空的Star。 init() { } - // Creates a new Star with all properties set. + // 创建一个新的星,并设置所有的属性。 init(id: UUID? = nil, name: String, galaxyID: UUID) { self.id = id self.name = name @@ -401,77 +439,77 @@ final class Star: Model, Content { } ``` -### Parent +### 父级 -The new `Star` model is very similar to `Galaxy` except for a new field type: `@Parent`. +新的`Star`模型与`Galaxy`非常相似,但有一个新的字段类型。`@Parent`. ```swift @Parent(key: "galaxy_id") var galaxy: Galaxy ``` -The parent property is a field that stores another model's identifier. The model holding the reference is called the "child" and the referenced model is called the "parent". This type of relation is also known as "one-to-many". The `key` parameter to the property specifies the field name that should be used to store the parent's key in the database. +父级属性是一个存储另一个模型的标识符的字段。持有引用的模型被称为"child",被引用的模型被称为"parent"。这种类型的关系也被称为 "一对多"。该属性的`key`参数指定了在数据库中用于存储父代键的字段名。 -In the init method, the parent identifier is set using `$galaxy`. +在init方法中,使用`$galaxy`来设置父标识符。 ```swift self.$galaxy.id = galaxyID ``` - By prefixing the parent property's name with `$`, you access the underlying property wrapper. This is required for getting access to the internal `@Field` that stores the actual identifier value. + 通过在父属性的名字前加上`$`,你可以访问底层的属性包装器。这是访问内部`@Field`的必要条件,它存储了实际的标识符值。 -!!! seealso - Check out the Swift Evolution proposal for property wrappers for more information: [[SE-0258] Property Wrappers](https://github.com/apple/swift-evolution/blob/master/proposals/0258-property-wrappers.md) +!!! 另见 + 请查看 Swift Evolution 中关于属性包装器的建议,了解更多信息。[[SE-0258] Property Wrappers](https://github.com/apple/swift-evolution/blob/master/proposals/0258-property-wrappers.md) -Next, create a migration to prepare the database for handling `Star`. +接下来,创建一个迁移,为处理`Star`的数据库做准备。 ```swift -struct CreateStar: Migration { - // Prepares the database for storing Star models. - func prepare(on database: Database) -> EventLoopFuture { - database.schema("stars") +struct CreateStar: AsyncMigration { + // 为存储Star模型的数据库做准备。 + func prepare(on database: Database) async throws { + try await database.schema("stars") .id() .field("name", .string) .field("galaxy_id", .uuid, .references("galaxies", "id")) .create() } - // Optionally reverts the changes made in the prepare method. - func revert(on database: Database) -> EventLoopFuture { - database.schema("stars").delete() + // 可选择恢复prepare方法中的修改。 + func revert(on database: Database) async throws { + try await database.schema("stars").delete() } } ``` -This is mostly the same as galaxy's migration except for the additional field to store the parent galaxy's identifier. +这与星系的迁移基本相同,只是多了一个字段来存储父级galaxy的标识符。 ```swift field("galaxy_id", .uuid, .references("galaxies", "id")) ``` -This field specifies an optional constraint telling the database that the field's value references the field "id" in the "galaxies" schema. This is also known as a foreign key and helps ensure data integrity. +这个字段指定了一个可选的约束条件,告诉数据库这个字段的值参考了"galaxies"模式中的字段 "id"。这也被称为外键,有助于确保数据的完整性。 -Once the migration is created, add it to `app.migrations` after the `CreateGalaxy` migration. +一旦创建了迁移,就把它添加到`app.migrations`中,放在`CreateGalaxy`迁移之后。 ```swift app.migrations.add(CreateGalaxy()) app.migrations.add(CreateStar()) ``` -Since migrations run in order, and `CreateStar` references the galaxies schema, ordering is important. Finally, [run the migrations](#migrate) to prepare the database. +由于迁移是按顺序进行的,而且`CreateStar`引用的是星系模式,所以排序很重要。最后,[运行迁移](#migrate)来准备数据库。 -Add a route for creating new stars. +添加一个用于创建新star的路由。 ```swift -app.post("stars") { req -> EventLoopFuture in +app.post("stars") { req async throws -> Star in let star = try req.content.decode(Star.self) - return star.create(on: req.db) - .map { star } + try await star.create(on: req.db) + return star } ``` -Create a new star referencing the previously created galaxy using the following HTTP request. +使用下面的HTTP请求创建一个新的star,引用之前创建的galaxy。 ```http POST /stars HTTP/1.1 @@ -486,7 +524,7 @@ content-type: application/json } ``` -You should see the newly created star returned with a unique identifier. +你应该看到新创建的star有一个独特的标识符返回。 ```json { @@ -498,29 +536,29 @@ You should see the newly created star returned with a unique identifier. } ``` -### Children +### 子级 -Now let's take a look at how you can utilize Fluent's eager-loading feature to automatically return a galaxy's stars in the `GET /galaxies` route. Add the following property to the `Galaxy` model. +现在让我们来看看如何利用Fluent的急于加载功能,在`GET /galaxies`路由中自动返回星系的星星。给`Galaxy`模型添加以下属性。 ```swift -// All the Stars in this Galaxy. +// 这个galaxy中的所有星星。 @Children(for: \.$galaxy) var stars: [Star] ``` -The `@Children` property wrapper is the inverse of `@Parent`. It takes a key-path to the child's `@Parent` field as the `for` argument. Its value is an array of children since zero or more child models may exist. No changes to the galaxy's migration are needed since all the information needed for this relation is stored on `Star`. +`@Children`属性包装器是`@Parent`的反面。它需要一个通往孩子的`@Parent`字段的关键路径作为`for`参数。它的值是一个子模型的数组,因为可能存在零个或多个子模型。不需要改变galaxy的迁移,因为这种关系所需的所有信息都存储在`Star`上。 -### Eager Load +### 急于加载 -Now that the relation is complete, you can use the `with` method on the query builder to automatically fetch and serialize the galaxy-star relation. +现在关系已经完成,你可以使用查询生成器上的`with`方法来自动获取并序列化galaxy-star关系。 ```swift app.get("galaxies") { req in - Galaxy.query(on: req.db).with(\.$stars).all() + try await Galaxy.query(on: req.db).with(\.$stars).all() } ``` -A key-path to the `@Children` relation is passed to `with` to tell Fluent to automatically load this relation in all of the resulting models. Build and run and send another request to `GET /galaxies`. You should now see the stars automatically included in the response. +`@Children`关系的关键路径被传递给`with`,告诉Fluent在所有产生的模型中自动加载这个关系。建立并运行另一个请求,向`GET /galaxies`发送。现在你应该看到恒星自动包含在响应中。 ```json [ @@ -540,267 +578,6 @@ A key-path to the `@Children` relation is passed to `with` to tell Fluent to aut ] ``` +## 接下来 -### Siblings - -The last type of relationship is many-to-many, or sibling relationship. Create a `Tag` model with an `id` and `name` field that we'll use to tag stars with certain characteristics. - -```swift -final class Tag: Model, Content { - // Name of the table or collection. - static let schema: String = "tags" - - // Unique identifier for this Tag. - @ID(key: .id) - var id: UUID? - - // The Tag's name. - @Field(key: "name") - var name: String - - // Creates a new, empty Tag. - init() {} - - // Creates a new Tag with all properties set. - init(id: UUID? = nil, name: String) { - self.id = id - self.name = name - } -} -``` - -A tag can have many stars and a star can have many tags making them siblings. A sibling relationship between two models requires a third model (called a pivot) that holds the relationship data. Each of these `StarTag` model objects will represent a single star-to-tag relationship holding the ids of a single `Star` and a single `Tag`: - -```swift -final class StarTag: Model { - // Name of the table or collection. - static let schema: String = "star_tag" - - // Unique identifier for this pivot. - @ID(key: .id) - var id: UUID? - - // Reference to the Tag this pivot relates. - @Parent(key: "tag_id") - var tag: Tag - - // Reference to the Star this pivot relates. - @Parent(key: "star_id") - var star: Star - - // Creates a new, empty pivot. - init() {} - - // Creates a new pivot with all properties set. - init(tagID: UUID, starID: UUID) { - self.$tag.id = tagID - self.$star.id = starID - } - -} -``` - -Now let's update our new `Tag` model to add a `stars` property for all the stars that contain a tag: - -```swift -@Siblings(through: StarTag.self, from: \.$tag, to: \.$star) -var stars: [Star] -``` - -The` @Siblings` property wrapper takes three arguments. The first argument is the pivot model that we created earlier, `StarTag`. The next two arguments are key paths to the pivot model's parent relations. The `from` key path is the pivot's parent relation to the current model, in this case `Tag`. The `to` key path is the pivot's parent relation to the related model, in this case `Star`. These three arguments together create a relation from the current model `Tag`, through the pivot `StarTag`, to the desired model `Star`. Now let's update our `Star` model with its siblings property which is the inverse of the one we just created: - -```swift -@Siblings(through: StarTag.self, from: \.$star, to: \.$tag) -var tags: [Tag] -``` - -These siblings properties rely on `StarTag` for storage so we don't need to update the `Star` migration, but we do need to create migrations for the new `Tag` and `StarTag` models: - -```swift -struct CreateTag: Migration { - func prepare(on database: Database) -> EventLoopFuture { - database.schema("tags") - .id() - .field("name", .string) - .create() - } - - func revert(on database: Database) -> EventLoopFuture { - database.schema("tags").delete() - } - -} - -struct CreateStarTag: Migration { - func prepare(on database: Database) -> EventLoopFuture { - database.schema("star_tag") - .id() - .field("star_id", .uuid, .required, .references("stars", "id")) - .field("tag_id", .uuid, .required, .references("tags", "id")) - .create() - } - - func revert(on database: Database) -> EventLoopFuture { - database.schema("star_tag").delete() - } -} -``` - -And then add the migrations in configure.swift: - -```swift -app.migrations.add(CreateTag()) -app.migrations.add(CreateStarTag()) -``` - -Now we want to add tags to stars. After creating a route to create a new tag, we need to create a route that will add a tag to an existing star. - -```swift -app.post("star", ":starID", "tag", ":tagID") { req -> EventLoopFuture in - let star = Star.find(req.parameters.get("starID"), on: req.db) - .unwrap(or: Abort(.notFound)) - let tag = Tag.find(req.parameters.get("tagID"), on: req.db) - .unwrap(or: Abort(.notFound)) - return star.and(tag).flatMap { (star, tag) in - star.$tags.attach(tag, on: req.db) - }.transform(to: .ok) -} -``` - -This route includes parameter path components for the IDs of star and tag that we want to associate with one another. If we want to create a relationship between a star with an ID of 1 and a tag with an ID of 2, we'd send a **POST** request to `/star/1/tag/2` and we'd receive an HTTP response code in return. First, we lookup the star and tag in the database to ensure these are valid IDs. Then, we create the relationship by attaching the tag to the star's tags. Since the star's `tags` property is a relationship to another model, we need to access it via it's `@Siblings` property wrapper by using the `$` operator. - -Siblings aren't fetched by default so we need to update our get route for stars if we want include them when querying by inserting the `with` method: - -```swift -app.get("stars") { req in - Star.query(on: req.db).with(\.$tags).all() -} -``` - -## Lifecycle - -To create hooks that respond to events on your `Model`, you can create middlewares for your model. Your middleware must conform to `ModelMiddleware`. - -Here is an example of a simple middleware: - -```swift -struct GalaxyMiddleware: ModelMiddleware { - // Runs when a model is created - func create(model: Galaxy, on db: Database, next: AnyModelResponder) -> EventLoopFuture { - return next.create(model, on: db) - } - - // Runs when a model is updated - func update(model: Galaxy, on db: Database, next: AnyModelResponder) -> EventLoopFuture { - return next.update(model, on: db) - } - - // Runs when a model is soft deleted - func softDelete(model: Galaxy, on db: Database, next: AnyModelResponder) -> EventLoopFuture { - return next.softDelete(model, on: db) - } - - // Runs when a soft deleted model is restored - func restore(model: Galaxy, on db: Database, next: AnyModelResponder) -> EventLoopFuture { - return next.restore(model , on: db) - } - - // Runs when a model is deleted - // If the "force" parameter is true, the model will be permanently deleted, - // even when using soft delete timestamps. - func delete(model: Galaxy, force: Bool, on db: Database, next: AnyModelResponder) -> EventLoopFuture { - return next.delete(model, force: force, on: db) - } -} -``` - -Each of these methods has a default implementation, so you only need to include the methods you require. You should return the corresponding method on the next `AnyModelResponder` so Fluent continues processing the event. - -!!! Important - The middleware will only respond to lifecycle events of the `Model` type provided in the functions. In the above example `GalaxyMiddleware` will respond to events on the Galaxy model. - -Using these methods you can perform actions both before, and after the event completes. Performing actions after the event completes can be done using using .flatMap() on the future returned from the next responder. For example: - -```swift -struct GalaxyMiddleware: ModelMiddleware { - func create(model: Galaxy, on db: Database, next: AnyModelResponder) -> EventLoopFuture { - - // The model can be altered here before it is created - model.name = "" - - return next.create(model, on: db).flatMap { - // Once the galaxy has been created, the code here will be executed - print ("Galaxy \(model.name) was created") - } - } -} -``` - -Once you have created your middleware, you must register it with the `Application`'s database middleware configuration so Vapor will use it. In `configure.swift` add: - -```swift -app.databases.middleware.use(GalaxyMiddleware(), on: .psql) -``` - -## Timestamps - -Fluent provides the ability to track creation and update times on models by specifying `Timestamp` fields in your model. Fluent automatically sets the fields when necessary. You can add these like so: - -```swift -@Timestamp(key: "created_at", on: .create) -var createdAt: Date? - -@Timestamp(key: "updated_at", on: .update) -var updatedAt: Date? -``` - -!!! Info - You can use any name/key for these fields. `created_at` / `updated_at`, are only for illustration purposes - -Timestamps are added as fields in a migration using the `.datetime` data type. - -```swift -database.schema(...) - ... - .field("created_at", .datetime) - .field("updated_at", .datetime) - .create() -``` - -### Soft Delete - -Soft deletion marks an item as deleted in the database but doesn't actually remove it. This can be useful when you have data retention requirements, for example. In Fluent, it works by setting a deletion timestamp. By default, soft deleted items won't appear in queries and can be restored at any time. - -Similar to created and deleted timestamps, to enable soft deletion in a model just set a deletion timestamp for `.delete`: - -```swift -@Timestamp(key: "deleted_at", on: .delete) -var deletedAt: Date? -``` - -Calling `Model.delete(on:)` on a model that has a delete timestamp property will automatically soft delete it. - -If you need to perform a query that includes the soft deleted items, you can use `withDeleted()` in your query. - -```swift -// Get all galaxies including soft-deleted ones. -Galaxy.query(on: db).withDeleted().all() -``` - -You can restore a soft deleted model with `restore(on:)`: - -```swift -// Restore galaxy -galaxy.restore(on: db) -``` - -To permanently delete an item with an on-delete timestamp, use the `force` parameter: - -```swift -// Permanently delete -galaxy.delete(force: true, on: db) -``` - -## Next Steps - -Congratulations on creating your first models and migrations and performing basic create and read operations. For more in-depth information on all of these features, check out their respective sections in the Fluent guide. +恭喜你创建了你的第一个模型和迁移,并进行了基本的创建和读取操作。关于所有这些功能的更深入的信息,请查看Fluent指南中各自的章节。 diff --git a/4.0/docs/fluent/query.md b/4.0/docs/fluent/query.md index eb1a821..de24eb4 100644 --- a/4.0/docs/fluent/query.md +++ b/4.0/docs/fluent/query.md @@ -1,9 +1,9 @@ -# Query +# 查询 -Fluent's query API allows you to create, read, update, and delete models from the database. It supports filtering results, joins, chunking, aggregates, and more. +Fluent的查询API允许你从数据库中创建、读取、更新和删除模型。它支持过滤结果、连接、分块、聚合等功能。 ```swift -// An example of Fluent's query API. +// Fluent的查询API的一个例子。 let planets = Planet.query(on: database) .filter(\.$type == .gasGiant) .sort(by: \.$name) @@ -11,262 +11,262 @@ let planets = Planet.query(on: database) .all() ``` -Query builders are tied to a single model type and can be created using the static [`query`](model.md#query) method. They can also be created by passing the model type to the `query` method on a database object. +查询构建器与单一的模型类型相联系,可以使用静态[`query`](model.md#query)方法来创建。它们也可以通过将模型类型传递给数据库对象的`query`方法来创建。 ```swift -// Also creates a query builder. +// 也可以创建一个查询生成器。 database.query(Planet.self) ``` -## All +## 所有 -The `all()` method returns an array of models. +`all()`方法返回一个模型的数组。 ```swift -// Fetches all planets. +// 获取所有行星。 let planets = Planet.query(on: database).all() ``` -The `all` method also supports fetching only a single field from the result set. +`all`方法也支持从结果集中只取一个字段。 ```swift -// Fetches all planet names. +//获取所有的星球名称。 let names = Planet.query(on: database).all(\.$name) ``` -### First +### 第一 -The `first()` method returns a single, optional model. If the query results in more than one model, only the first is returned. If the query has no results, `nil` is returned. +`first()`方法返回一个单一的、可选的模型。如果查询的结果有一个以上的模型,只返回第一个。如果查询没有结果,`nil`将被返回。 ```swift -// Fetches the first planet named Earth. +// 获取第一个名为Earth的行星。 let earth = Planet.query(on: database) .filter(\.$name == "Earth") .first() ``` -!!! tip - This method can be combined with [`unwrap(or:)`](../basics/errors.md#abort) to return a non-optional model or throw an error. +!!!提示 + 这个方法可以和[`unwrap(or:)`](../basics/errors.md#abort)结合起来,返回一个非选择的模型或抛出一个错误。 -## Filter +## 过滤器 -The `filter` method allows you to constrain the models included in the result set. There are several overloads for this method. +`Filter`方法允许你限制包含在结果集中的模型。这个方法有几个重载。 -### Value Filter +### 值过滤器 -The most commonly used `filter` method accept an operator expression with a value. +最常用的`filter`方法接受一个带有数值的操作表达式。 ```swift -// An example of field value filtering. +// 一个字段值过滤的例子。 Planet.query(on: database).filter(\.$type == .gasGiant) ``` -These operator expressions accept a field key path on the left hand side and a value on the right. The supplied value must match the field's expected value type and is bound to the resulting query. Filter expressions are strongly typed allowing for leading-dot syntax to be used. +这些运算符表达式在左边接受一个字段关键路径,在右边接受一个值。提供的值必须与字段的预期值类型相匹配,并被绑定到结果查询中。过滤表达式是强类型的,允许使用前导点语法。 -Below is a list of all supported value operators. +下面是所有支持的值运算符的列表。 -|Operator|Description| +|运算符|描述| |-|-| -|`==`|Equal to.| -|`!=`|Not equal to.| -|`>=`|Greater than or equal to.| -|`>`|Greater than.| -|`<`|Less than.| -|`<=`|Less than or equal to.| +|`==`|等于。| +|`!=`|不等于。| +|`>=`|大于或等于。| +|`>`|大于。| +|`<`|少于。| +|`<=`|小于或等于。| -### Field Filter +### 字段过滤 -The `filter` method supports comparing two fields. +`filter`方法支持比较两个字段。 ```swift -// All users with same first and last name. +// 所有具有相同firstName和lastName的用户。 User.query(on: database) .filter(\.$firstName == \.$lastName) ``` -Field filters support the same operators as [value filters](#value-filter). +字段过滤器支持与[值过滤器](#值过滤器)相同的操作。 -### Subset Filter +### 子集过滤器 -The `filter` method supports checking whether a field's value exists in a given set of values. +`filter`方法支持检查一个字段的值是否存在于一个给定的值集合中。 ```swift -// All planets with either gas giant or small rocky type. +// 所有具有gasGiant或smallRocky的行星。 Planet.query(on: database) .filter(\.$type ~~ [.gasGiant, .smallRocky]) ``` -The supplied set of values can be any Swift `Collection` whose `Element` type matches the field's value type. +提供的值集可以是任何Swift `Collection`,其`Element`类型与字段的值类型相符。 -Below is a list of all supported subset operators. +下面是所有支持的子集运算符的列表。 -|Operator|Description| +|运算符|描述| |-|-| -|`~~`|Value in set.| -|`!~`|Value not in set.| +|`~~`|值在集合中。| +|`!~`|值不在集合中。| -### Contains Filter +### 包含过滤器 -The `filter` method supports checking whether a string field's value contains a given substring. +`filter`方法支持检查一个字符串字段的值是否包含一个给定的子字符串。 ```swift -// All planets whose name starts with the letter M +// 所有名字以字母M开头的行星 Planet.query(on: database) .filter(\.$name =~ "M") ``` -These operators are only available on fields with string values. +这些运算符只适用于有字符串值的字段。 -Below is a list of all supported contains operators. +下面是所有支持的包含运算符的列表。 -|Operator|Description| +|运算符|描述| |-|-| -|`~~`|Contains substring.| -|`!~`|Does not contain substring.| -|`=~`|Matches prefix.| -|`!=~`|Does not match prefix.| -|`~=`|Matches suffix.| -|`!~=`|Does not match suffix.| +|`~~`|包含子字符串。| +|`!~`|不包含子字符串| +|`=~`|与前缀相匹配。| +|`!=~`|与前缀不匹配。| +|`~=`|匹配后缀。| +|`!~=`|与后缀不匹配。| -### Group +### 组 -By default, all filters added to a query will be required to match. Query builder supports creating a group of filters where only one filter must match. +默认情况下,添加到查询中的所有过滤器都需要匹配。查询生成器支持创建一个过滤器组,其中只有一个过滤器必须匹配。 ```swift -// All planets whose name is either Earth or Mars +// 所有名称为Earth或Mars的行星 Planet.query(on: database).group(.or) { group in group.filter(\.$name == "Earth").filter(\.$name == "Mars") } ``` -The `group` method supports combining filters by `and` or `or` logic. These groups can be nested indefinitely. Top-level filters can be thought of as being in an `and` group. +`Group`方法支持通过`and`或`or`逻辑组合过滤器。这些组可以无限制地嵌套。顶层的过滤器可以被认为是在一个`and`组中。 -## Aggregate +## 聚合 -Query builder supports several methods for performing calculations on a set of values like counting or averaging. +查询生成器支持几种对一组数值进行计算的方法,如计数或平均。 ```swift -// Number of planets in database. +// 数据库中行星的数量。 Planet.query(on: database).count() ``` -All aggregate methods besides `count` require a key path to a field to be passed. +除了`count`以外的所有聚合方法都需要传递一个字段的关键路径。 ```swift -// Lowest name sorted alphabetically. +//按字母顺序排序的最低名称。 Planet.query(on: database).min(\.$name) ``` -Below is a list of all available aggregate methods. +下面是所有可用的聚合方法的列表。 -|Aggregate|Description| +|汇总|描述| |-|-| -|`count`|Number of results.| -|`sum`|Sum of result values.| -|`average`|Average of result values.| -|`min`|Minimum result value.| -|`max`|Maximum result value.| +|`count`|结果的数量。| +|`sum`|结果值的总和。| +|`average`|结果值的平均值。| +|`min`|最小结果值。| +|`max`|最大的结果值。| -All aggregate methods except `count` return the field's value type as a result. `count` always returns an integer. +除了`count`之外,所有的聚合方法都将字段的值类型作为结果返回。`count`总是返回一个整数。 ## Chunk -Query builder supports returning a result set as separate chunks. This helps you to control memory usage when handling large database reads. +查询生成器支持将结果集作为独立的块返回。这有助于你在处理大型数据库读取时控制内存的使用。 ```swift -// Fetches all planets in chunks of at most 64 at a time. +// 每次最多提取64个分块的所有计划。 Planet.query(on: self.database).chunk(max: 64) { planets in // Handle chunk of planets. } ``` -The supplied closure will be called zero or more times depending on the total number of results. Each item returned is a `Result` containing either the model or an error returned attempting to decode the database entry. +根据结果的总数,提供的闭包将被调用0次或多次。返回的每一项都是一个`Result`,包含模型或试图解码数据库条目时返回的一个错误。 -## Field +## 字段 -By default, all of a model's fields will be read from the database by a query. You can choose to select only a subset of a model's fields using the `field` method. +默认情况下,一个模型的所有字段都将通过查询从数据库中读取。你可以选择使用`field`方法只选择模型字段的一个子集。 ```swift -// Select only the planet's id and name field +// 只选择星球的id和name字段 Planet.query(on: database) .field(\.$id).field(\.$name) .all() ``` -Any model fields not selected during a query will be in an unitialized state. Attempting to access uninitialized fields directly will result in a fatal error. To check if a model's field value is set, use the `value` property. +任何在查询过程中没有被选中的模型字段都将处于单元化状态。试图直接访问未初始化的字段将导致一个致命的错误。要检查一个模型的字段值是否被设置,使用`value`属性。 ```swift if let name = planet.$name.value { - // Name was fetched. + // 名字被取走了。 } else { - // Name was not fetched. - // Accessing `planet.name` will fail. + // 名字没有被取走。 + // 访问`planet.name`将失败。 } ``` -## Unique +## 独特 -Query builder's `unique` method causes only distinct results (no duplicates) to be returned. +查询生成器的`unique`方法只返回不同的结果(没有重复的)。 ```swift -// Returns all unique user first names. +// 返回所有唯一的用户名字。 User.query(on: database).unique().all(\.$firstName) ``` -`unique` is especially useful when fetching a single field with `all`. However, you can also select multiple fields using the [`field`](#field) method. Since model identifiers are always unique, you should avoid selecting them when using `unique`. +`unique`在用`all`获取单个字段时特别有用。然而,你也可以使用[`field`](#field)方法选择多个字段。由于模型标识符总是唯一的,你应该在使用`unique`时避免选择它们。 -## Range +## 范围 -Query builder's `range` methods allow you to choose a subset of the results using Swift ranges. +查询生成器的`range`方法允许你使用Swift范围来选择结果的一个子集。 ```swift -// Fetch the first 5 planets. +// 取出前5个行星。 Planet.query(on: self.database) .range(..<5) ``` -Range values are unsigned integers starting at zero. Learn more about [Swift ranges](https://developer.apple.com/documentation/swift/range). +范围值是无符号整数,从零开始。了解更多关于[Swift ranges](https://developer.apple.com/documentation/swift/range)。 ```swift -// Skip the first 2 results. +// 跳过前两个结果。 .range(2...) ``` -## Join +## 联合 -Query builder's `join` method allows you to include another model's fields in your result set. More than one model can be joined to your query. +查询生成器的`join`方法允许你在你的结果集中包括另一个模型的字段。多于一个模型可以被加入到你的查询中。 ```swift -// Fetches all planets with a star named Sun. +// 获取所有有太阳系的行星。 Planet.query(on: database) .join(Star.self, on: \Planet.$star.$id == \Star.$id) .filter(Star.self, \.$name == "Sun") .all() ``` -The `on` parameter accepts an equality expression between two fields. One of the fields must already exist in the current result set. The other field must exist on the model being joined. These fields must have the same value type. +参数`on `接受两个字段之间的相等表达式。其中一个字段必须已经存在于当前的结果集中。另一个字段必须存在于被连接的模型中。这些字段必须有相同的值类型。 -Most query builder methods, like `filter` and `sort`, support joined models. If a method supports joined models, it will accept the joined model type as the first parameter. +大多数查询生成器方法,如`filter`和`sort`,支持联合模型。如果一个方法支持联合模型,它将接受联合模型类型作为第一个参数。 ```swift -// Sort by joined field "name" on Star model. +// 在Star模型上按连接字段 "name "排序。 .sort(Star.self, \.$name) ``` -Queries that use joins will still return an array of the base model. To access the joined model, use the `joined` method. +使用连接的查询仍然会返回一个基础模型的数组。要访问连接的模型,请使用`joined`方法。 ```swift -// Accessing joined model from query result. +// 从查询结果中访问连接的模型。 let planet: Planet = ... let star = try planet.joined(Star.self) ``` -### Model Alias +### 模型别名 -Model aliases allow you to join the same model to a query multiple times. To declare a model alias, create one or more types conforming to `ModelAlias`. +模型别名允许你将同一个模型多次加入到一个查询中。要声明一个模型别名,创建一个或多个符合`ModelAlias`的类型。 ```swift -// Example of model aliases. +// 模型别名的例子。 final class HomeTeam: ModelAlias { static let name = "home_teams" let model = Team() @@ -277,11 +277,11 @@ final class AwayTeam: ModelAlias { } ``` -These types reference the model being aliased via the `model` property. Once created, you can use model aliases like normal models in a query builder. +这些类型通过`model`属性引用被别名的模型。一旦创建,你可以在查询生成器中像普通模型一样使用模型别名。 ```swift -// Fetch all matches where the home team's name is Vapor -// and sort by the away team's name. +// 获取所有主队名称为Vapor的比赛 +// 的所有比赛,并按照客队的名字进行排序。 let matches = try Match.query(on: self.database) .join(HomeTeam.self, on: \Match.$homeTeam.$id == \HomeTeam.$id) .join(AwayTeam.self, on: \Match.$awayTeam.$id == \AwayTeam.$id) @@ -290,59 +290,59 @@ let matches = try Match.query(on: self.database) .all().wait() ``` -All model fields are accessible through the model alias type via `@dynamicMemberLookup`. +所有的模型字段都可以通过`@dynamicMemberLookup`的模型别名类型访问。 ```swift -// Access joined model from result. +// 从结果中访问加入的模型。 let home = try match.joined(HomeTeam.self) print(home.name) ``` -## Update +## 更新 -Query builder supports updating more than one model at a time using the `update` method. +查询生成器支持使用`update`方法一次更新多个模型。 ```swift -// Update all planets named "Earth" +// 更新所有名为"Earth"的行星 Planet.query(on: database) .set(\.$type, to: .dwarf) .filter(\.$name == "Pluto") .update() ``` -`update` supports the `set`, `filter`, and `range` methods. +`update`支持`set`, `filter`, 和`range`方法。 -## Delete +## 删除 -Query builder supports deleting more than one model at a time using the `delete` method. +查询生成器支持使用`delete`方法一次删除一个以上的模型. ```swift -// Delete all planets named "Vulcan" +// 删除所有名为"Vulcan"的行星 Planet.query(on: database) .filter(\.$name == "Vulcan") .delete() ``` -`delete` supports the `filter` method. +`delete`支持`filter`方法。 -## Paginate +## 分页 -Fluent's query API supports automatic result pagination using the `paginate` method. +Fluent的查询API支持使用`paginate`方法对结果进行自动分页。 ```swift -// Example of request-based pagination. +// 基于请求的分页的例子. app.get("planets") { req in Planet.query(on: req.db).paginate(for: req) } ``` -The `paginate(for:)` method will use the `page` and `per` parameters available in the request URI to return the desired set of results. Metadata about current page and total number of results is included in the `metadata` key. +`paginate(for:)`方法将使用请求URI中的`page`和`per`参数来返回所需的结果集。关于当前页面和结果总数的元数据被包含在`metadata`键中。 ```http GET /planets?page=2&per=5 HTTP/1.1 ``` -The above request would yield a response structured like the following. +上述请求将产生一个结构如下的响应。 ```json { @@ -355,25 +355,25 @@ The above request would yield a response structured like the following. } ``` -Page numbers start at `1`. You can also make a manual page request. +页码从`1`开始。你也可以进行手动的页面请求。 ```swift -// Example of manual pagination. +// 手动分页的例子。 .paginate(PageRequest(page: 1, per: 2)) ``` -## Sort +## 排序 -Query results can be sorted by field values using `sort` method. +查询结果可以使用`sort`方法按字段值进行排序。 ```swift -// Fetch planets sorted by name. +// 取出按名称排序的行星。 Planet.query(on: database).sort(\.$name) ``` -Additional sorts may be added as fallbacks in case of a tie. Fallbacks will be used in the order they were added to the query builder. +在出现相同的情况下,可以添加额外的排序作为后备排序。回调将按照它们被添加到查询生成器的顺序使用。 ```swift -// Fetch users sorted by name. If two users have the same name, sort them by age. +// 取出按名字排序的用户。如果两个用户有相同的名字,按年龄排序。 User.query(on: database).sort(\.$name).sort(\.$age) ``` diff --git a/4.0/docs/fluent/relations.md b/4.0/docs/fluent/relations.md index eabf95c..a40b321 100644 --- a/4.0/docs/fluent/relations.md +++ b/4.0/docs/fluent/relations.md @@ -1,89 +1,141 @@ -# Relations +# 关系 -Fluent's [model API](model.md) helps you create and maintain references between your models through relations. Two types of relations are supported: +Fluent的[模型API](model.md)帮助你通过关系创建和维护模型之间的引用。支持两种类型的关系。 -- [Parent](#parent) / [Child](#child) (One-to-many) -- [Siblings](#siblings) (Many-to-many) +- [父级](#parent)/[可选子级](#optional-child) (一对一) +- [父级](#parent)/[子级](#children) (一对多) +- [同级](#同级) (多对多) -## Parent +## 父级 -The `@Parent` relation stores a reference to another model's `@ID` property. +`@Parent`关系存储对另一个模型的`@ID`属性的引用。 ```swift final class Planet: Model { - // Example of a parent relation. + // 父级关系的例子。 @Parent(key: "star_id") var star: Star } ``` -`@Parent` contains a `@Field` named `id` which is used for setting and updating the relation. +`@Parent`包含一个名为`id`的`@Field`,用于设置和更新关系。 ```swift -// Set parent relation id +// 设置父级关系ID earth.$star.id = sun.id ``` -The `key` parameter defines the field key to use for storing the parent's identifier. Assuming `Star` has a `UUID` identifier, this `@Parent` relation is compatible with the following [field definition](schema.md#field). +例如,`Planet`的初始化器看起来像: + +```swift +init(name: String, starID: Star.IDValue) { + self.name = name + // ... + self.$star.id = starID +} +``` + +`key`参数定义了用于存储父类标识符的字段键。假设`Star`有一个`UID`标识符,这个`@Parent`关系与下面的[字段定义](schema.md#field)兼容。 ```swift .field("star_id", .uuid, .required, .references("star", "id")) ``` -Note that the [`.references`](schema.md#field-constraint) constraint is optional. See [schema](schema.md) for more information. +注意,[`.references`](schema.md#field-constraint)约束是可选的。更多信息请参见[schema](./schema.md)。 -### Optional Parent +### 可选父级 -The `@OptionalParent` relation stores an optional reference to another model's `@ID` property. It works similarly to `@Parent` but allows for the relation to be `nil`. +`@OptionalParent`关系存储对另一个模型的`@ID`属性的可选引用。它的工作原理与`@Parent`类似,但允许关系为`nil`。 ```swift final class Planet: Model { - // Example of an optional parent relation. + // 可选的父级关系的例子。 @OptionalParent(key: "star_id") var star: Star? } ``` -The field definition is similar to `@Parent`'s except that the `.required` constraint should be omitted. +这个字段的定义与`@Parent`类似,除了`.required`约束应该被省略。 ```swift .field("star_id", .uuid, .references("star", "id")) ``` -## Children +## 可选子级 + +`@OptionalChild`属性在两个模型之间创建一个一对一的关系。它不在根模型上存储任何值。 + +```swift +final class Planet: Model { + // 可选子级关系的例子。 + @OptionalChild(for: \.$planet) + var governor: Governor? +} +``` + +`for`参数接受一个指向引用根模型的`@Parent`或`@OptionalParent`关系的关键路径。 + +一个新的模型可以通过`create'方法被添加到这个关系中。 + +```swift +// 向一个关系添加新模型的例子。 +let jane = Governor(name: "Jane Doe") +try await mars.$governor.create(jane, on: database) +``` + +这将自动在子模型上设置父级ID。 + +由于这个关系不存储任何值,根模型不需要数据库模式条目。 + +关系的一对一性质应该在子模型的模式中使用引用父类模型的列上的`.unique`约束来强制执行。 + +```swift +try await database.schema(Governor.schema) + .id() + .field("name", .string, .required) + .field("planet_id", .uuid, .required, .references("planets", "id")) + // unique约束的例子 + .unique(on: "planet_id") + .create() +``` +!!! warning + 从客户的模式中省略对父级ID字段的唯一性约束会导致不可预测的结果。 + 如果没有唯一性约束,子表可能最终为任何给定的父类表包含一个以上的子行;在这种情况下,`@OptionalChild`属性仍然一次只能访问一个子,没有办法控制哪个子被加载。如果你可能需要为任何给定的父类存储多个子行,请使用`@Children`代替。 + +## 子级 -The `@Children` property creates a one-to-many relation between two models. It does not store any values on the root model. +`@Children`属性在两个模型之间创建一个一对多的关系。它不在根模型上存储任何值。 ```swift final class Star: Model { - // Example of a children relation. + // 子级关系的例子。 @Children(for: \.$star) var planets: [Planet] } ``` -The `for` parameter accepts a key path to a `@Parent` or `@OptionalParent` relation referencing the root model. In this case, we are referencing the `@Parent` relation from the previous [example](#parent). +`for`参数接受一个指向引用根模型的`@Parent`或`@OptionalParent`关系的关键路径。在这种情况下,我们引用的是前面[例子](#父级)中的`@Parent`关系。 -New models can be added to this relation using the `create` method. +新的模型可以使用`create`方法被添加到这个关系中。 ```swift -// Example of adding a new model to a relation. +// 向一个关系添加新模型的例子。 let earth = Planet(name: "Earth") -sun.$planets.create(earth, on: database) +try await sun.$planets.create(earth, on: database) ``` -This will set the parent id on the child model automatically. +这将自动在子模型上设置父类ID。 -Since this relation does not store any values, no database schema entry is required. +因为这种关系不存储任何值,所以不需要数据库模式条目。 -## Siblings +## 同级 -The `@Siblings` property creates a many-to-many relation between two models. It does this through a tertiary model called a pivot. +`@Siblings`属性在两个模型之间创建了一个多对多的关系。它通过一个叫做pivot的三级模型来实现。 -Let's take a look at an example of a many-to-many relation between a `Planet` and a `Tag`. +让我们看一下`Planet`和`Tag`之间的多对多关系的例子。 ```swift -// Example of a pivot model. +// Pivot模型的例子。 final class PlanetTag: Model { static let schema = "planet+tag" @@ -106,164 +158,187 @@ final class PlanetTag: Model { } ``` -Pivots are normal models that contain two `@Parent` relations. One for each of the models to be related. Additional properties can be stored on the pivot if desired. +pivot是包含两个`@Parent`关系的正常模型。每个模型都有一个被关联。如果需要的话,额外的属性可以被存储在pivot上。 -Adding a [unique](schema.md#unique) constraint to the pivot model can help prevent redundant entries. See [schema](schema.md) for more information. +给pivot模型添加一个 [unique](schema.md#unique) 约束可以帮助防止多余的条目。参见[schema](schema.md)以了解更多信息。 ```swift -// Disallows duplicate relations. +// 不允许重复的关系。 .unique(on: "planet_id", "tag_id") ``` -Once the pivot is created, use the `@Siblings` property to create the relation. +一旦pivot被创建,使用`@Siblings`属性来创建关系。 ```swift final class Planet: Model { - // Example of a siblings relation. + // 同级关系的例子。 @Siblings(through: PlanetTag.self, from: \.$planet, to: \.$tag) public var tags: [Tag] } ``` -The `@Siblings` property requires three parameters: +`@Siblings`属性需要三个参数。 -- `through`: The pivot model's type. -- `from`: Key path from the pivot to the parent relation referencing the root model. -- `to`: Key path from the pivot to the parent relation referencing the related model. +- `through`: pivot模型的类型。 +- `from`: 从pivot到引用根模型的父类关系的关键路径。 +- `to`: 从pivot到引用相关模型的父类关系的关键路径。 -The inverse `@Siblings` property on the related model completes the relation. +相关模型上的反向`@Siblings`属性完成了该关系。 ```swift final class Tag: Model { - // Example of a siblings relation. + // 同级关系的例子。 @Siblings(through: PlanetTag.self, from: \.$tag, to: \.$planet) public var planets: [Planet] } ``` -### Siblings Attach +### 同级的附件 -The `@Siblings` property has methods adding and removing models from the relation. +`@Siblings`属性有从关系中添加和删除模型的方法。 -Use the `attach` method to add a model to the relation. This creates and saves the pivot model automatically. +使用`attach`方法来添加一个模型到关系中。这将自动创建并保存pivot模型。 ```swift let earth: Planet = ... let inhabited: Tag = ... -// Adds the model to the relation. -earth.$tags.attach(inhabited, on: database) +// 将模型添加到关系中。 +try await earth.$tags.attach(inhabited, on: database) ``` -When attaching a single model, you can use the `method` parameter to choose whether or not the relation should be checked before saving. +当附加一个单一的模型时,你可以使用`method`参数来选择在保存前是否要检查关系。 ```swift -// Only attaches if the relation doesn't already exist. -earth.$tags.attach(inhabited, method: .ifNotExists, on: database) +// 只在关系不存在的情况下附加。 +try await earth.$tags.attach(inhabited, method: .ifNotExists, on: database) ``` -Use the `detach` method to remove a model from the relation. This deletes the corresponding pivot model. +使用`detach`方法从关系中删除一个模型。这将删除相应的pivot模型。 ```swift -// Removes the model from the relation. -earth.$tags.detach(inhabited) +// 将模型从关系中移除。 +try await earth.$tags.detach(inhabited, on: database) ``` -You can check if a model is related or not using the `isAttached` method. +你可以使用`isAttached`方法检查一个模型是否相关。 ```swift -// Checks if the models are related. +// 检查这些模型是否相关。 earth.$tags.isAttached(to: inhabited) ``` -## Get +## 获取 -Use the `get(on:)` method to fetch a relation's value. +使用`get(on:)`方法来获取一个关系的值。 ```swift -// Fetches all of the sun's planets. +// 获取太阳系的所有行星。 sun.$planets.get(on: database).map { planets in print(planets) } + +// 或者 + +let planets = try await sun.$planets.get(on: database) +print(planets) ``` -Use the `reload` parameter to choose whether or not the relation should be re-fetched from the database if it has already been already loaded. +使用`reload`参数来选择是否应该从数据库中重新获取关系,如果它已经被加载。 ```swift -sun.$planets.get(reload: true, on: database) +try await sun.$planets.get(reload: true, on: database) ``` -## Query +## 查询 -Use the `query(on:)` method on a relation to create a query builder for the related models. +在一个关系上使用`query(on:)`方法,为相关模型创建一个查询生成器。 ```swift -// Fetch all of the sun's planets that have a naming starting with M. -sun.$planets.query(on: database).filter(\.$name =~ "M").all() +// 取出所有命名以M开头的太阳行星。 +try await sun.$planets.query(on: database).filter(\.$name =~ "M").all() ``` -See [query](query.md) for more information. +更多信息见[query](./query.md)。 -## Load +## 急于加载 -Use the `load(on:)` method to load a relation. This allows the related model to be accessed as a local property. +Fluent的查询生成器允许你在从数据库中获取模型的关系时预先加载。这被称为急于加载,允许你同步访问关系,而不需要先调用[`load`](#lazy-eager-loading)或[`get`](#get)。 + +要急于加载一个关系,需要将关系的关键路径传递给查询生成器的`with`方法。 ```swift -// Example of loading a relation. -planet.$star.load(on: database).map { - print(planet.star.name) +// 急于加载的例子。 +Planet.query(on: database).with(\.$star).all().map { planets in + for planet in planets { + // `star`在这里可以同步访问。 + // 因为它已经被急于加载。 + print(planet.star.name) + } } -``` -To check whether or not a relation has been loaded, use the `value` property. +// 或者 -```swift -if planet.$star.value != nil { - // Relation has been loaded. +let planets = try await Planet.query(on: database).with(\.$star).all() +for planet in planets { + // `star`在这里可以同步访问。 + // 因为它已经被急于加载。 print(planet.star.name) -} else { - // Relation has not been loaded. - // Attempting to access planet.star will fail. } ``` -You can set the `value` property manually if needed. +在上面的例子中,一个名为 `star`的[`@Parent`](#父级)关系的关键路径被传递给`with`。这导致查询生成器在所有行星被加载后进行额外的查询,以获取所有相关的恒星。然后,这些星星可以通过`@Parent`属性同步访问。 -## Eager Load +每一个急于加载的关系只需要一个额外的查询,无论返回多少个模型。急切加载只能通过查询生成器的`all`和`first`方法实现。 -Fluent's query builder allows you to preload a model's relations when it is fetched from the database. This is called eager loading and allows you to access relations synchronously without needing to call [`load`](#load) or [`get`](#get) first. -To eager load a relation, pass a key path to the relation to the `with` method on query builder. +### 嵌套急于加载 + +查询生成器的`with`方法允许你在被查询的模型上急于加载关系。然而,你也可以在相关模型上急于加载关系。 ```swift -// Example of eager loading. -Planet.query(on: database).with(\.$star).all().map { planets in - for planet in planets { - // `star` is accessible synchronously here - // since it has been eager loaded. - print(planet.star.name) - } +let planets = try await Planet.query(on: database).with(\.$star) { star in + star.with(\.$galaxy) +}.all() +for planet in planets { + // `star.galaxy`可以在这里同步访问。 + // 因为它已经被急于加载。 + print(planet.star.galaxy.name) } ``` -In the above example, a key path to the [`@Parent`](#parent) relation named `star` is passed to `with`. This causes the query builder to do an additional query after all of the planets are loaded to fetch all of their related stars. The stars are then accessible synchronously via the `@Parent` property. +`with`方法接受一个可选的闭包作为第二个参数。这个闭包接受所选关系的急迫加载构建器。渴望加载的深度没有限制,可以嵌套。 -Each relation eager loaded requires only one additional query, no matter how many models are returned. Eager loading is only possible with the `all` and `first` methods of query builder. +## 懒惰的急于加载 +如果你已经获取了父类模型,你想加载它的一个关系,你可以使用`load(on:)`方法来达到这个目的。这将从数据库中获取相关模型,并允许它作为一个本地属性被访问。 -### Nested Eager Load +```swift +planet.$star.load(on: database).map { + print(planet.star.name) +} + +// 或者 + +try await planet.$star.load(on: database) +print(planet.star.name) +``` -The query builder's `with` method allows you to eager load relations on the model being queried. However, you can also eager load relations on related models. +要检查一个关系是否已经被加载,使用`value`属性。 ```swift -Planet.query(on: database).with(\.$star) { star in - star.with(\.$galaxy) -}.all().map { planets in - for planet in planets { - // `star.galaxy` is accessible synchronously here - // since it has been eager loaded. - print(planet.star.galaxy.name) - } +if planet.$star.value != nil { + // 关系已被加载。 + print(planet.star.name) +} else { + // 关系没有被加载。 + // 试图访问planet.star将失败。 } ``` -The `with` method accepts an optional closure as a second parameter. This closure accepts an eager load builder for the chosen relation. There is no limit to how deeply eager loading can be nested. +如果你在一个变量中已经有了相关的模型,你可以使用上面提到的`value`属性手动设置关系。 + +```swift +planet.$star.value = star +``` + +这将把相关模型附加到父类模型上,就像它被急于加载或懒于加载一样,不需要额外的数据库查询。 diff --git a/4.0/docs/fluent/schema.md b/4.0/docs/fluent/schema.md index 4de6f7d..40c42fe 100644 --- a/4.0/docs/fluent/schema.md +++ b/4.0/docs/fluent/schema.md @@ -1,227 +1,231 @@ -# Schema +# 模式 -Fluent's schema API allows you to create and update your database schema programatically. It is often used in conjunction with [migrations](migration.md) to prepare the database for use with [models](model.md). +Fluent的模式API允许你以编程方式创建和更新你的数据库模式。它通常与[migrate](migration.md)一起使用,以便为[model](model.md)的使用准备数据库。 ```swift -// An example of Fluent's schema API -database.schema("planets") +// Fluent的模式API的一个例子 +try await database.schema("planets") .id() .field("name", .string, .required) .field("star_id", .uuid, .required, .references("stars", "id")) .create() ``` -To create a `SchemaBuilder`, use the `schema` method on database. Pass in the name of the table or collection you want to affect. If you are editing the schema for a model, make sure this name matches the model's [`schema`](model.md#schema). +要创建一个`SchemaBuilder`,使用数据库的`schema`方法。传入你想影响的表或集合的名称。如果你正在编辑一个模型的模式,确保这个名字与模型的[`schema`](model.md#schema)相符。 -## Actions +## 行动 -The schema API supports creating, updating, and deleting schemas. Each action supports a subset of the API's available methods. +模式API支持创建、更新和删除模式。每个动作都支持API的一个可用方法的子集。 -### Create +### 创建 -Calling `create()` creates a new table or collection in the database. All methods for defining new fields and constraints are supported. Methods for updates or deletes are ignored. +调用`create()`在数据库中创建一个新的表或集合。所有用于定义新字段和约束的方法都被支持。更新或删除的方法被忽略。 ```swift -// An example schema creation. -database.schema("planets") +// 一个创建模式的例子。 +try await database.schema("planets") .id() .field("name", .string, .required) .create() ``` -If a table or collection with the chosen name already exists, an error will be thrown. To ignore this, use `.ignoreExisting()`. +如果一个具有所选名称的表或集合已经存在,将抛出一个错误。要忽略这一点,请使用`.ignoreExisting()`。 -### Update +### 更新 -Calling `update()` updates an existing table or collection in the database. All methods for creating, updating, and deleting fields and constraints are supported. +调用`update()`可以更新数据库中现有的表或集合。所有用于创建、更新和删除字段和约束的方法都被支持。 ```swift -// An example schema update. -database.schema("planets") +// 一个模式更新的例子。 +try await database.schema("planets") .unique(on: "name") .deleteField("star_id") .update() ``` -### Delete +### 删除 -Calling `delete()` deletes an existing table or collection from the database. No additional methods are supported. +调用`delete()`可以从数据库中删除一个现有的表或集合。没有额外的方法被支持。 ```swift -// An example schema deletion. +// 一个删除模式的例子. database.schema("planets").delete() ``` -## Field +## 字段 -Fields can be added when creating or updating a schema. +在创建或更新模式时,可以添加字段。 ```swift -// Adds a new field +// 添加一个新的字段 .field("name", .string, .required) ``` -The first parameter is the name of the field. This should match the key used on the associated model property. The second parameter is the field's [data type](#data-type). Finally, zero or more [constraints](#field-constraint) can be added. +第一个参数是字段的名称。这应该与相关模型属性上使用的键相匹配。第二个参数是字段的[数据类型](#data-type)。最后,可以添加零个或多个[约束](#field-constraint)。 -### Data Type +### 数据类型 -Supported field data types are listed below. +支持的字段数据类型列举如下。 -|DataType|Swift Type| +|数据类型|快速类型| |-|-| |`.string`|`String`| |`.int{8,16,32,64}`|`Int{8,16,32,64}`| |`.uint{8,16,32,64}`|`UInt{8,16,32,64}`| |`.bool`|`Bool`| -|`.datetime`|`Date` (recommended)| -|`.time`|`Date` (omitting day, month, and year)| -|`.date`|`Date` (omitting time of day)| +|`.datetime`|`Date` (推荐)| +|`.time`|`Date` (省略日、月、年)| +|`.date`|`Date` (省略一天中的时间)| |`.float`|`Float`| |`.double`|`Double`| |`.data`|`Data`| |`.uuid`|`UUID`| -|`.dictionary`|See [dictionary](#dictionary)| -|`.array`|See [array](#array)| -|`.enum`|See [enum](#enum)| +|`.dictionary`|看 [dictionary](#dictionary)| +|`.array`|看 [array](#array)| +|`.enum`|看 [enum](#enum)| -### Field Constraint +### 字段约束 -Supported field constraints are listed below. +支持的字段约束列举如下。 -|FieldConstraint|Description| +|字段约束|描述| |-|-| -|`.required`|Disallows `nil` values.| -|`.references`|Requires that this field's value match a value in the referenced schema. See [foreign key](#foreign-key)| -|`.identifier`|Denotes the primary key. See [identifier](#identifier)| +|`.required`|不允许使用`nil`值。| +|`.references`|要求这个字段的值与被引用模式中的一个值相匹配。参见[外键](#foreign-key)| +|`.identifier`|表示主键。参见[标识符](#identifier)| -### Identifier +### 标识符 -If your model uses a standard `@ID` property, you can use the `id()` helper to create its field. This uses the special `.id` field key and `UUID` value type. +如果你的模型使用一个标准的`@ID`属性,你可以使用`id()`助手来创建它的字段。这使用了特殊的`.id`字段键和`UUID`值类型。 ```swift -// Adds field for default identifier. +// 添加默认标识符的字段。 .id() ``` -For custom identifier types, you will need to specify the field manually. +对于自定义标识符类型,你将需要手动指定该字段。 ```swift -// Adds field for custom identifier. +// 添加自定义标识符的字段。 .field("id", .int, .identifier(auto: true)) ``` -The `identifier` constraint may be used on a single field and denotes the primary key. The `auto` flag determines whether or not the database should generate this value automatically. +`identifier`约束可以用在一个字段上,表示主键。`auto`标志决定了数据库是否应该自动生成这个值。 -### Update Field +### 更新字段 -You can update a field's data type using `updateField`. +你可以使用`updateField`来更新一个字段的数据类型。 ```swift -// Updates the field to `double` data type. +// 将字段更新为`double`数据类型。 .updateField("age", .double) ``` -See [advanced](advanced.md#sql) for more information on advanced schema updates. +参见[advanced](advanced.md#sql)了解更多关于高级模式更新的信息。 -### Delete Field +### 删除字段 -You can remove a field from a schema using `deleteField`. +你可以使用`deleteField`从模式中删除一个字段。 ```swift -// Deletes the field "age". +// 删除字段"age"。 .deleteField("age") ``` -## Constraint +## 制约因素 -Constraints can be added when creating or updating a schema. Unlike [field constraints](#field-constraint), top-level constraints can affect multiple fields. +在创建或更新模式时,可以添加约束条件。与[字段约束](#field-constraint)不同,顶层约束可以影响多个字段。 -### Unique +### 唯一 -A unique constraint requires that there are no duplicate values in one or more fields. +唯一约束要求在一个或多个字段中不存在重复的值。 ```swift -// Disallow duplicate email addresses. +// 不允许重复的电子邮件地址。 .unique(on: "email") ``` -If multiple field are constrained, the specific combination of each field's value must be unique. +如果多个字段被限制,每个字段的具体组合值必须是唯一的。 ```swift -// Disallow users with the same full name. +// 不允许有相同全名的用户。 .unique(on: "first_name", "last_name") ``` -To delete a unique constraint, use `deleteUnique`. +要删除一个唯一约束,使用`deleteUnique`。 ```swift -// Removes duplicate email constraint. +// 删除重复的电子邮件约束。 .deleteUnique(on: "email") ``` -### Constraint Name +### 约束条件名称 -Fluent will generate unique constraint names by default. However, you may want to pass a custom constraint name. You can do this using the `name` parameter. +Fluent默认会生成唯一的约束名称。然而,你可能想传递一个自定义的约束名称。你可以使用`name`参数来做到这一点。 ```swift -// Disallow duplicate email addresses. +// 不允许重复的电子邮件地址。 .unique(on: "email", name: "no_duplicate_emails") ``` -To delete a named constraint, you must use `deleteConstraint(name:)`. +要删除一个命名的约束,你必须使用`deleteConstraint(name:)`。 ```swift -// Removes duplicate email constraint. +// 删除重复的电子邮件约束。 .deleteConstraint(name: "no_duplicate_emails") ``` -## Foreign Key +## 外键 -Foreign key constraints require that a field's value match ones of the values in the referenced field. This is useful for preventing invalid data from being saved. Foreign key constraints can be added as either a field or top-level constraint. +外键约束要求一个字段的值与被引用字段中的一个值相匹配。这对于防止无效的数据被保存是很有用的。外键约束可以作为字段或顶层约束来添加。 -To add a foreign key constraint to a field, use `.references`. +要给一个字段添加外键约束,使用`.references`。 ```swift -// Example of adding a field foreign key constraint. +// 添加字段外键约束的例子。 .field("star_id", .uuid, .required, .references("stars", "id")) ``` -The above constraint requires that all values in the "star_id" field must match one of the values in Star's "id" field. +上述约束要求`star_id`字段中的所有值必须与Star的`id`字段中的一个值匹配。 -This same constraint could be added as a top-level constraint using `foreignKey`. +同样的约束可以使用`foreignKey`作为顶层约束来添加。 ```swift -// Example of adding a top-level foreign key constraint. -.foreignKey("star_id", references: "star", "id") +// 添加顶层外键约束的例子。 +.foreignKey("star_id", references: "stars", "id") ``` -Unlike field constraints, top-level constraints can be added in a schema update. They can also be [named](#constraint-name). +与字段约束不同,顶层约束可以在模式更新中被添加。它们也可以被[命名](#constraint-name)。 -Foreign key constraints support optional `onDelete` and `onUpdate` actions. +外键约束支持可选的`onDelete`和`onUpdate`动作。 -|ForeignKeyAction|Description| +|外键动作|描述| |-|-| -|`.noAction`|Prevents foreign key violations (default).| -|`.restrict`|Same as `.noAction`.| -|`.cascade`|Propogates deletes through foreign keys.| -|`.setNull`|Sets field to null if reference is broken.| -|`.setDefault`|Sets field to default if reference is broken.| +|`.noAction`|防止违反外键(默认)。| +|`.restrict`|与`.noAction`相同。| +|`.cascade`|通过外键传播删除信息。| +|`.setNull`|如果引用被破坏,则将字段设置为空。| +|`.setDefault`|如果引用被破坏,将字段设置为默认。| -Below is an example using foreign key actions. +下面是一个使用外键操作的例子。 ```swift -// Example of adding a top-level foreign key constraint. -.foreignKey("star_id", references: "star", "id", onDelete: .cascade) +// 添加一个顶层外键约束的例子。 +.foreignKey("star_id", references: "stars", "id", onDelete: .cascade) ``` +!!! warning + 外键操作只发生在数据库中,绕过了Fluent。 + 这意味着像模型中间件和软删除可能无法正常工作。 + ## Dictionary -The dictionary data type is capable of storing nested dictionary values. This includes structs that conform to `Codable` and Swift dictionaries with a `Codable` value. +dictionary数据类型能够存储嵌套的dictionary值。这包括符合`Codable'的结构和具有`Codable'值的Swift字典。 !!! note - Fluent's SQL database drivers store nested dictionaries in JSON columns. + Fluent的SQL数据库驱动在JSON列中存储嵌套字典。 -Take the following `Codable` struct. +以下面这个`Codable`结构为例。 ```swift struct Pet: Codable { @@ -230,58 +234,58 @@ struct Pet: Codable { } ``` -Since this `Pet` struct is `Codable`, it can be stored in a `@Field`. +由于这个`Pet`结构是`Codable`的,它可以被存储在`@Field`中。 ```swift @Field(key: "pet") var pet: Pet ``` -This field can be stored using the `.dictionary(of:)` data type. +这个字段可以使用`.dictionary(of:)`数据类型来存储。 ```swift .field("pet", .dictionary, .required) ``` -Since `Codable` types are heterogenous dictionaries, we do not specify the `of` parameter. +由于`Codable`类型是异质的字典,我们不指定`of`参数。 -If the dictionary values were homogenous, for example `[String: Int]`, the `of` parameter would specify the value type. +如果字典的值是同质的,例如`[String: Int]`,`of`参数将指定值的类型。 ```swift .field("numbers", .dictionary(of: .int), .required) ``` -Dictionary keys must always be strings. +字典的键必须始终是字符串。 -## Array +## 数组 -The array data type is capable of storing nested arrays. This includes Swift arrays that contain `Codable` values and `Codable` types that use an unkeyed container. +数组数据类型能够存储嵌套数组。这包括包含`Codable`值的Swift数组和使用无键容器的`Codable`类型。 -Take the following `@Field` that stores an array of strings. +以下面这个存储字符串数组的`@Field`为例。 ```swift @Field(key: "tags") var tags: [String] ``` -This field can be stored using the `.array(of:)` data type. +这个字段可以使用`.array(of:)`数据类型来存储。 ```swift .field("tags", .array(of: .string), .required) ``` -Since the array is homogenous, we specify the `of` parameter. +由于数组是同质的,我们指定`of`参数。 -Codable Swift `Array`s will always have a homogenous value type. Custom `Codable` types that serialize heterogenous values to unkeyed containers are the exception and should use the `.array` data type. +可编码的Swift`Array`将总是有一个同质的值类型。将异质值序列化为无键容器的自定义`Codable`类型是个例外,应该使用`.array`数据类型。 -## Enum +## 枚举 -The enum data type is capable of storing string backed Swift enums natively. Native database enums provide an added layer of type safety to your database and may be more performant than raw enums. +枚举数据类型能够在本地存储以字符串为基础的Swift枚举。本地数据库枚举为你的数据库提供了一个额外的类型安全层,并且可能比原始枚举更有性能。 -To define a native database enum, use the `enum` method on `Database`. Use `case` to define each case of the enum. +要定义一个本地数据库枚举,请使用`Database`上的`enum`方法。使用`case`来定义枚举的每个情况。 ```swift -// An example of enum creation. +// 一个创建枚举的例子。 database.enum("planet_type") .case("smallRocky") .case("gasGiant") @@ -289,55 +293,62 @@ database.enum("planet_type") .create() ``` -Once an enum has been created, you can use the `read()` method to generate a data type for your schema field. +一旦创建了一个枚举,你可以使用`read()`方法为你的模式字段生成一个数据类型。 ```swift -// An example of reading an enum and using it to define a new field. +// 一个读取枚举并使用它来定义一个新字段的例子。 database.enum("planet_type").read().flatMap { planetType in database.schema("planets") .field("type", planetType, .required) .update() } + +// 或 + +let planetType = try await database.enum("planet_type").read() +try await database.schema("planets") + .field("type", planetType, .required) + .update() ``` -To update an enum, call `update()`. Cases can be deleted from existing enums. +要更新一个枚举,请调用`update()`。可以从现有的枚举中删除案例。 ```swift -// An example of enum update. +// 一个枚举更新的例子。 database.enum("planet_type") .deleteCase("gasGiant") .update() ``` -To delete an enum, call `delete()`. +要删除一个枚举,请调用`delete()`。 ```swift -// An example of enum deletion. +// 一个删除枚举的例子。 database.enum("planet_type").delete() ``` -## Model Coupling +## 模型耦合 -Schema building is purposefully decoupled from models. Unlike query building, schema building does not make use of key paths and is completely stringly typed. This is important since schema definitions, especially those written for migrations, may need to reference model properties that no longer exist. +模式构建是有目的地与模型解耦的。与查询构建不同,模式构建不使用关键路径,并且是完全字符串类型的。这一点很重要,因为模式定义,特别是那些为迁移而写的定义,可能需要引用不再存在的模型属性。 -To better understand this, take a look at the following example migration. +为了更好地理解这一点,请看下面这个迁移示例。 ```swift -struct UserMigration: Migration { - func prepare(on database: Database) -> EventLoopFuture { - database.schema("users") +struct UserMigration: AsyncMigration { + func prepare(on database: Database) async throws { + try await database.schema("users") .field("id", .uuid, .identifier(auto: false)) .field("name", .string, .required) .create() } - func revert(on database: Database) -> EventLoopFuture { - database.schema("users").delete() + func revert(on database: Database) async throws { + try await database.schema("users").delete() } } ``` -Let's assume that this migration has been has already been pushed to production. Now let's assume we need to make the following change to the User model. +让我们假设这次迁移已经被推送到生产中了。现在我们假设我们需要对用户模型做如下改变。 ```diff - @Field(key: "name") @@ -349,22 +360,22 @@ Let's assume that this migration has been has already been pushed to production. + var lastName: String ``` -We can make the necessary database schema adjustments with the following migration. +我们可以通过以下迁移进行必要的数据库模式调整。 ```swift -struct UserNameMigration: Migration { - func prepare(on database: Database) -> EventLoopFuture { - database.schema("users") +struct UserNameMigration: AsyncMigration { + func prepare(on database: Database) async throws { + try await database.schema("users") .deleteField("name") .field("first_name", .string) .field("last_name", .string) - .create() + .update() } - func revert(on database: Database) -> EventLoopFuture { - database.schema("users").delete() + func revert(on database: Database) async throws { + try await database.schema("users").delete() } } ``` -Note that for this migration to work, we need to be able to reference both the removed `name` field and the new `firstName` and `lastName` fields at the same time. Furthermore, the original `UserMigration` should continue to be valid. This would not be possible to do with key paths. +请注意,为了使这个迁移工作,我们需要能够同时引用被删除的`name`字段和新的`firstName`和`lastName`字段。此外,原来的`UserMigration`应该继续有效。这一点用密钥路径是不可能做到的。 diff --git a/4.0/docs/leaf/custom-tags.md b/4.0/docs/leaf/custom-tags.md new file mode 100644 index 0000000..3573703 --- /dev/null +++ b/4.0/docs/leaf/custom-tags.md @@ -0,0 +1,117 @@ +# 自定义标签 + +你可以使用[`LeafTag`](https://api.vapor.codes/leaf-kit/latest/LeafKit/LeafSyntax/LeafTag.html)协议创建自定义Leaf标签。 + +为了证明这一点,让我们来看看创建一个自定义标签`#now`,打印出当前的时间戳。该标签还将支持一个用于指定日期格式的可选参数。 + +## LeafTag + +首先创建一个名为`NowTag`的类,并将其与`LeafTag`相匹配。 + +```swift +struct NowTag: LeafTag { + + func render(_ ctx: LeafContext) throws -> LeafData { + ... + } +} +``` + +现在我们来实现`render(_:)`方法。传递给这个方法的`LeafContext`上下文有我们应该需要的一切。 + +```swift +struct NowTagError: Error {} + +let formatter = DateFormatter() +switch ctx.parameters.count { +case 0: formatter.dateFormat = "yyyy-MM-dd HH:mm:ss" +case 1: + guard let string = ctx.parameters[0].string else { + throw NowTagError() + } + formatter.dateFormat = string +default: + throw NowTagError() +} + +let dateAsString = formatter.string(from: Date()) +return LeafData.string(dateAsString) +``` + +!!! tip + 如果你的自定义标签渲染了HTML,你应该将你的自定义标签符合`UnsafeUnescapedLeafTag`,这样HTML就不会被转义。记住要检查或净化任何用户输入。 + +## 配置标签 + +现在我们已经实现了`NowTag`,我们只需要把它告诉Leaf。你可以像这样添加任何标签--即使它们来自一个单独的包。你通常在`configure.swift`中这样做: + +```swift +app.leaf.tags["now"] = NowTag() +``` + +就这样了! 我们现在可以在Leaf中使用我们的自定义标签。 + +```leaf +The time is #now() +``` + +## 上下文属性 + +`LeafContext`包含两个重要的属性。`parameters`和`data`,它们拥有我们应该需要的一切。 + +- `parameters`: 一个数组,包含标签的参数。 +- `data`: 一个字典,包含传递给`render(_:_:)`的视图的数据,作为上下文。 + +### 示例 Hello 标签 + +为了了解如何使用它,让我们使用这两个属性实现一个简单的Hello标签。 + +#### 使用参数 + +我们可以访问包含名称的第一个参数。 + +```swift +struct HelloTagError: Error {} + +public func render(_ ctx: LeafContext) throws -> LeafData { + + guard let name = ctx.parameters[0].string else { + throw HelloTagError() + } + + return LeafData.string("

Hello \(name)

'") + } +} +``` + +```leaf +#hello("John") +``` + +#### 使用数据 + +我们可以通过使用data属性中的"name"键来访问name值。 + +```swift +struct HelloTagError: Error {} + +public func render(_ ctx: LeafContext) throws -> LeafData { + + guard let name = ctx.data["name"]?.string else { + throw HelloTagError() + } + + return LeafData.string("

Hello \(name)

'") + } +} +``` + +```leaf +#hello() +``` + +控制器: + +```swift +return req.view.render("home", ["name": "John"]) +``` diff --git a/4.0/docs/leaf/getting-started.md b/4.0/docs/leaf/getting-started.md new file mode 100644 index 0000000..ad54e1e --- /dev/null +++ b/4.0/docs/leaf/getting-started.md @@ -0,0 +1,94 @@ +# Leaf + +Leaf是一种强大的模板语言,具有Swift启发的语法。你可以用它来生成前端网站的动态HTML页面,或者生成丰富的电子邮件,从API中发送。 + +## Package + +使用Leaf的第一步是在你的SPM包清单文件中把它作为一个依赖项添加到你的项目中。 + +```swift +// swift-tools-version:5.2 +import PackageDescription + +let package = Package( + name: "MyApp", + platforms: [ + .macOS(.v10_15) + ], + dependencies: [ + /// 任何其他的依赖性... + .package(url: "https://github.com/vapor/leaf.git", from: "4.0.0"), + ], + targets: [ + .target(name: "App", dependencies: [ + .product(name: "Leaf", package: "leaf"), + /// 任何其他的依赖性... + ]), + // 其他目标 + ] +) +``` + +## 配置 + +一旦你将软件包添加到你的项目中,你可以配置Vapor来使用它。这通常是在[`configure.swift`](.../folder-structure.md#configureswift)中完成的。 + +```swift +import Leaf + +app.views.use(.leaf) +``` + +这告诉Vapor在你的代码中调用`req.view`时使用`LeafRenderer`。 + +!!! note + Leaf有一个内部缓存用于渲染页面。当`Application`的环境被设置为`.development`时,这个缓存被禁用,所以对模板的修改会立即生效。在`.production`和所有其他环境下,缓存默认是启用的;任何对模板的修改都将不会生效,直到应用程序重新启动。 + +!!! warning + 为了使Leaf在从Xcode运行时能够找到模板,你必须为你的Xcode工作区设置[自定义工作目录](https://docs.vapor.codes/4.0/xcode/#custom-working-directory)。 + +## 文件夹结构 + +一旦你配置了Leaf,你将需要确保你有一个`Views`文件夹来存储你的`.leaf`文件。默认情况下,Leaf希望视图文件夹是相对于项目根目录的`./Resources/Views`。 + +如果你打算为Javascript和CSS文件提供服务,你可能还想启用Vapor的[`FileMiddleware`](https://api.vapor.codes/vapor/latest/Vapor/Classes/FileMiddleware.html),从`/Public`文件夹中提供文件。 + +``` +VaporApp +├── Package.swift +├── Resources +│   ├── Views +│   │   └── hello.leaf +├── Public +│   ├── images (images resources) +│   ├── styles (css resources) +└── Sources +    └── ... +``` + +## 渲染一个视图 + +现在Leaf已经配置好了,让我们来渲染你的第一个模板。在`Resources/Views`文件夹中,创建一个名为`hello.leaf`的新文件,内容如下。 + +```leaf +Hello, #(name)! +``` + +然后,注册一个路由(通常在`routes.swift`或控制器中完成)来渲染视图。 + +```swift +app.get("hello") { req -> EventLoopFuture in + return req.view.render("hello", ["name": "Leaf"]) +} + +// 或 + +app.get("hello") { req async throws -> View in + return try await req.view.render("hello", ["name": "Leaf"]) +} +``` + +这使用了`Request`上的通用`view`属性,而不是直接调用Leaf。这允许你在测试中切换到一个不同的渲染器。 + + +打开你的浏览器,访问`/hello`。你应该看到`Hello, Leaf!`。恭喜你渲染了你的第一个Leaf视图! diff --git a/4.0/docs/leaf/overview.md b/4.0/docs/leaf/overview.md new file mode 100644 index 0000000..217d7e3 --- /dev/null +++ b/4.0/docs/leaf/overview.md @@ -0,0 +1,276 @@ +# Leaf概述 + +Leaf是一种强大的模板语言,具有Swift启发的语法。你可以用它来为前端网站生成动态HTML页面,或者生成丰富的电子邮件,从API中发送。 + +本指南将给你一个关于Leaf语法和可用标签的概述。 + +## 模板语法 + +下面是一个基本的Leaf标签用法的例子。 + +```leaf +There are #count(users) users. +``` + +叶子标签是由四个元素组成的。 + +- Token `#`:这预示着叶子分析器开始寻找一个标签。 +- 名称`count`: 这标识了标签。 +- 参数列表`(user)`:可以接受零个或多个参数。 +- Body:可以使用分号和结尾标签为某些标签提供一个可选的主体。 + +根据标签的实现,这四个元素可以有许多不同的用法。让我们看看几个例子,看看如何使用Leaf的内置标签: + +```leaf +#(variable) +#extend("template"): I'm added to a base template! #endextend +#export("title"): Welcome to Vapor #endexport +#import("body") +#count(friends) +#for(friend in friends):
  • #(friend.name)
  • #endfor +``` + +Leaf还支持许多你在Swift中熟悉的表达方式。 + +- `+` +- `%` +- `>` +- `==` +- `||` +- 等等.. + +```leaf +#if(1 + 1 == 2): + Hello! +#endif + +#if(index % 2 == 0): + This is even index. +#else: + This is odd index. +#endif +``` + +## 上下文 + +在[入门](./getting-started.md)的例子中,我们用一个`[String: String]`字典来向Leaf传递数据。然而,你可以传递任何符合`Encodable`的东西。实际上,由于不支持`[String: Any]`,所以最好使用`Encodable`结构。这意味着你*不能*传入一个数组,而应该把它包在一个结构中。 + +```swift +struct WelcomeContext: Encodable { + var title: String + var numbers: [Int] +} +return req.view.render("home", WelcomeContext(title: "Hello!", numbers: [42, 9001])) +``` + +这将向我们的Leaf模板暴露`title`和`numbers`,然后可以在标签中使用。比如说: + +```leaf +

    #(title)

    +#for(number in numbers): +

    #(number)

    +#endfor +``` + +## 用法 + +以下是一些常见的叶子的使用例子。 + +### 条件 + +Leaf能够使用它的`#if`标签评估一系列的条件。例如,如果你提供一个变量,它将检查该变量在其上下文中是否存在: + +```leaf +#if(title): + The title is #(title) +#else: + No title was provided. +#endif +``` + +你也可以写比较,比如说: + +```leaf +#if(title == "Welcome"): + This is a friendly web page. +#else: + No strangers allowed! +#endif +``` + +如果你想使用另一个标签作为条件的一部分,你应该省略内部标签的`#`。比如说: + +```leaf +#if(count(users) > 0): + You have users! +#else: + There are no users yet :( +#endif +``` + +你也可以使用`#elseif`语句: + +```leaf +#if(title == "Welcome"): + Hello new user! +#elseif(title == "Welcome back!"): + Hello old user +#else: + Unexpected page! +#endif +``` + +### 循环 + +如果你提供了一个项目数组,Leaf可以在这些项目上循环,让你使用其`#for`标签单独操作每个项目。 + +例如,我们可以更新我们的Swift代码,以提供一个行星的列表: + +```swift +struct SolarSystem: Codable { + let planets = ["Venus", "Earth", "Mars"] +} + +return req.view.render("solarSystem", SolarSystem()) +``` + +然后我们可以像这样在Leaf中对它们进行循环: + +```leaf +Planets: +
      +#for(planet in planets): +
    • #(planet)
    • +#endfor +
    +``` + +这将呈现一个看起来像这样的视图: + +``` +Planets: +- Venus +- Earth +- Mars +``` + +### 扩展模板 + +Leaf的`#extend`标签允许你将一个模板的内容复制到另一个模板中。当使用它时,你应该总是省略模板文件的.leaf扩展名。 + +扩展对于复制一个标准的内容是很有用的,例如一个页面的页脚、广告代码或多个页面共享的表格: + +```leaf +#extend("footer") +``` + +这个标签对于在另一个模板上构建一个模板也很有用。例如,你可能有一个layout.leaf文件,其中包括布置网站所需的所有代码--HTML结构、CSS和JavaScript--有一些空隙,代表页面内容的变化。 + +使用这种方法,你将构建一个子模板,填入其独特的内容,然后扩展父模板,适当地放置内容。要做到这一点,你可以使用`#export`和`#import`标签来存储和以后从上下文中检索内容。 + +例如,你可以创建一个`child.leaf`模板,像这样: + +```leaf +#extend("master"): + #export("body"): +

    Welcome to Vapor!

    + #endexport +#endextend +``` + +我们调用`#export`来存储一些HTML,并将其提供给我们目前正在扩展的模板。然后我们渲染`master.leaf`,并在需要时使用导出的数据,以及从Swift传入的任何其他上下文变量。例如,`master.leaf`可能看起来像这样: + +```leaf + + + #(title) + + #import("body") + +``` + +这里我们使用`#import`来获取传递给`#extend`标签的内容。当从Swift传来`["title": "Hi there!"]`时,`child.leaf`将呈现如下: + +```html + + + Hi there! + +

    Welcome to Vapor!

    + +``` + +###其他标签 + +#### `#count` + +`#count`标签返回一个数组中的个数。比如说: + +```leaf +Your search matched #count(matches) pages. +``` + +#### `#lowercased` + +`#lowercased`标签将一个字符串中的所有字母变小写。 + +```leaf +#lowercased(name) +``` + +#### `#uppercased` + +`#uppercased`标签将一个字符串中的所有字母变大写。 + +```leaf +#uppercased(name) +``` + +#### `#capitalized` + +`#capitalized`标签将字符串中每个词的第一个字母大写,其他字母小写。参见[`String.capitalized`](https://developer.apple.com/documentation/foundation/nsstring/1416784-capitalized)以了解更多信息。 + +```leaf +#capitalized(name) +``` + +#### `#contains` + +`#contains`标签接受一个数组和一个值作为其两个参数,如果参数一中的数组包含参数二中的值,则返回true。 + +```leaf +#if(contains(planets, "Earth")): + Earth is here! +#else: + Earth is not in this array. +#endif +``` + +#### `#date` + +`#date`标签将日期转换成可读的字符串。默认情况下,它使用ISO8601格式。 + +```swift +render(..., ["now": Date()]) +``` + +```leaf +The time is #date(now) +``` + +你可以传递一个自定义的日期格式化字符串作为第二个参数。参见Swift的[`DateFormatter`](https://developer.apple.com/documentation/foundation/dateformatter)以获得更多信息。 + +```leaf +The date is #date(now, "yyyy-MM-dd") +``` + +#### `#unsafeHTML` + +`#unsafeHTML`标签的作用类似于一个变量标签--例如`#(变量)`。然而,它并不转义任何`variable`可能包含的HTML: + +```leaf +The time is #unsafeHTML(styledTitle) +``` + +!!! note + 在使用这个标签时,你应该小心,以确保你提供的变量不会使你的用户受到XSS攻击。 diff --git a/4.0/docs/redis/overview.md b/4.0/docs/redis/overview.md new file mode 100644 index 0000000..fbd873a --- /dev/null +++ b/4.0/docs/redis/overview.md @@ -0,0 +1,169 @@ +# Redis + +[Redis](https://redis.io/)是最流行的内存数据结构存储之一,通常作为缓存或消息代理使用。 + +这个库是Vapor和[**RediStack**](https://gitlab.com/mordil/redistack)的集成,后者是与Redis通信的底层驱动。 + +!!!注意 + Redis的大部分功能是由**RediStack**提供的。 + 我们强烈建议熟悉它的文档。 + + 在适当的地方提供了链接。 + +## Package + +使用Redis的第一步是在你的Swift包清单中把它作为一个依赖项加入你的项目。 + +> 这个例子是针对一个现有的包。关于启动新项目的帮助,请参见[入门](.../hello-world.md)主指南。 + +```swift +dependencies: [ + // ... + .package(url: "https://github.com/vapor/redis.git", from: "4.0.0") +] +// ... +targets: [ + .target(name: "App", dependencies: [ + // ... + .product(name: "Redis", package: "redis") + ]) +] +``` + +## 配置 + +Vapor对[`RedisConnection`](https://docs.redistack.info/Classes/RedisConnection.html)实例采用了池化策略,有几个选项可以配置单个连接和池子本身。 + +配置Redis的最低要求是提供一个连接的URL: + +```swift +let app = Application() + +app.redis.configuration = try RedisConfiguration(hostname: "localhost") +``` + +### Redis配置 + +> API文档。[`RedisConfiguration`](https://api.vapor.codes/redis/main/Redis/RedisConfiguration/) + +#### serverAddresses + +如果你有多个Redis端点,比如一个Redis实例集群,你会想创建一个[`[SocketAddress]`](https://apple.github.io/swift-nio/docs/current/NIOCore/Enums/SocketAddress.html#/s:3NIO13SocketAddressO04makeC13ResolvingHost_4portACSS_SitKFZ)集合来代替传递初始化器。 + +创建`SocketAddress`的最常见方法是使用[`makeAddressResolvingHost(_:port:)`](https://apple.github.io/swift-nio/docs/current/NIOCore/Enums/SocketAddress.html#/s:3NIO13SocketAddressO04makeC13ResolvingHost_4portACSS_SitKFZ)静态方法。 + +```swift +let serverAddresses: [SocketAddress] = [ + try .makeAddressResolvingHost("localhost", port: RedisConnection.Configuration.defaultPort) +] +``` + +对于一个单一的Redis端点,使用方便的初始化器可能更容易,因为它将为你处理创建`SocketAddress`。 + +- [`.init(url:pool)`](https://api.vapor.codes/redis/main/Redis/RedisConfiguration/#redisconfiguration.init(url:pool:)) (with `String` or [`Foundation.URL`](https://developer.apple.com/documentation/foundation/url)) +- [`.init(hostname:port:password:database:pool:)`](https://api.vapor.codes/redis/main/Redis/RedisConfiguration/#redisconfiguration.init(hostname:port:password:database:pool:)) + +#### 密码 + +如果你的Redis实例是由密码保护的,你需要把它作为`password`参数传递。 + +每个连接在创建时,都将使用该密码进行验证。 + +#### 数据库 + +这是你希望在创建每个连接时选择的数据库索引。 + +这使你不必自己向Redis发送`SELECT`命令。 + +!!! warning + 数据库的选择不会被维护。自己发送`SELECT`命令时要小心。 + +### 连接池选项 + +> API文档:[`RedisConfiguration.PoolOptions`](https://api.vapor.codes/redis/main/Redis/RedisConfiguration_PoolOptions/) + +!!! note + 这里只强调了最常改变的选项。对于所有的选项,请参考API文档。 + +#### minimumConnectionCount + +这是设置你希望每个池子在任何时候都保持多少个连接的值。 + +如果你的值是`0`,那么如果连接因任何原因丢失,池将不会重新创建它们,直到需要。 + +这被称为"冷启动"连接,与维持最小连接数相比,确实有一些开销。 + +#### maximumConnectionCount + +这个选项决定了如何维护最大连接数的行为。 + +!!! seealso + 参考[`RedisConnectionPoolSize`](https://docs.redistack.info/Enums/RedisConnectionPoolSize.html) API,熟悉有哪些选项。 + +## 发送命令 + +你可以使用任何[`Application`](https://api.vapor.codes/vapor/main/Vapor/Application/)或[`Request`](https://api.vapor.codes/vapor/main/Vapor/Request/)实例上的`.redis`属性来发送命令,这将使你能够访问一个[`RedisClient`](https://docs.redistack.info/Protocols/RedisClient.html)。 + +任何`RedisClient`都有几个扩展,用于所有各种[Redis命令](https://redis.io/commands)。 + +```swift +let value = try app.redis.get("my_key", as: String.self).wait() +print(value) +// Optional("my_value") + +// or + +let value = try await app.redis.get("my_key", as: String.self) +print(value) +// Optional("my_value") +``` + +### 不支持的命令 + +如果**RediStack**不支持带有扩展方法的命令,你仍然可以手动发送。 + +```swift +// 命令后面的每个值是Redis期望的位置参数 +try app.redis.send(command: "PING", with: ["hello"]) + .map { + print($0) + } + .wait() +// "hello" + +// 或 + +let res = try await app.redis.send(command: "PING", with: ["hello"]) +print(res) +// "hello" +``` + +## Pub/Sub模式 + +Redis支持进入["Pub/Sub "模式](https://redis.io/topics/pubsub)的能力,其中一个连接可以监听特定的"通道",并在订阅的通道发布"消息"(一些数据值)时运行特定的关闭。 + +订阅有一个确定的生命周期。 + +1. **subscribe**:当订阅第一次开始时调用一次。 +1. **message**:当消息被发布到订阅的频道时被调用0次以上。 +1. **unsubscribe**:当订阅结束时调用一次,无论是请求还是连接丢失。 + +当你创建一个订阅时,你必须至少提供一个[`messageReceiver`](https://docs.redistack.info/Typealiases.html#/s:9RediStack32RedisSubscriptionMessageReceiver)来处理所有由订阅频道发布的消息。 + +你可以选择为`onSubscribe`和`onUnsubscribe`提供一个[`RedisSubscriptionChangeHandler`](https://docs.redistack.info/Typealiases.html#/s:9RediStack30RedisSubscriptionChangeHandlera),以处理它们各自的生命周期事件。 + +```swift +// 创建2个订阅,每个给定通道一个 +app.redis.subscribe + to: "channel_1", "channel_2", + messageReceiver: { channel, message in + switch channel { + case "channel_1": // do something with the message + default: break + } + }, + onUnsubscribe: { channel, subscriptionCount in + print("unsubscribed from \(channel)") + print("subscriptions remaining: \(subscriptionCount)") + } +``` diff --git a/4.0/docs/redis/sessions.md b/4.0/docs/redis/sessions.md new file mode 100644 index 0000000..d2261e7 --- /dev/null +++ b/4.0/docs/redis/sessions.md @@ -0,0 +1,78 @@ +# Redis与会话 + +Redis可以作为一个存储提供者来缓存[会话数据](.../sessions.md#session-data),如用户证书。 + +如果没有提供自定义的[`RedisSessionsDelegate`](https://api.vapor.codes/redis/master/Redis/RedisSessionsDelegate/),将使用默认值。 + +## 默认行为 + +### SessionID创建 + +除非你在[你自己的`RedisSessionsDelegate`](#RedisSessionsDelegate)中实现[`makeNewID()`](https://api.vapor.codes/redis/master/Redis/RedisSessionsDelegate/#redissessionsdelegate.makeNewID())方法,否则所有[`SessionID`](https://api.vapor.codes/vapor/master/Vapor/SessionID/)值都将通过以下方式创建: + +1. 生成32字节的随机字符 +1. 对该值进行base64编码 + +例如:`Hbxozx8rTj+XXGWAzOhh1npZFXaGLpTWpWCaXuo44xQ=`。 + +### 会话数据存储 + +`RedisSessionsDelegate`的默认实现将使用`Codable`将[`SessionData`](https://api.vapor.codes/vapor/master/Vapor/SessionData/)存储为一个简单的JSON字符串值。 + +除非你在自己的`RedisSessionsDelegate`中实现了[`makeRedisKey(for:)`](https://api.vapor.codes/redis/master/Redis/RedisSessionsDelegate/#redissessionsdelegate.makeRedisKey(for:))方法,否则`SessionData`将以`vrs-`(**V**apor **R**edis **S**essions)为前缀的钥匙存储在Redis中。 + +例如:`vrs-Hbxozx8rTj+XXGWAzOhh1npZFXaGLpTWpWCaXuo44xQ=` + +## 注册一个自定义代理 + +要定制数据从Redis读取和写入Redis的方式,请注册你自己的`RedisSessionsDelegate`对象,如下所示: + +```swift +import Redis + +struct CustomRedisSessionsDelegate: RedisSessionsDelegate { + // 执行 +} + +app.sessions.use(.redis(delegate: CustomRedisSessionsDelegate())) +``` + +## RedisSessionsDelegate + +> API文档:[`RedisSessionsDelegate`](https://api.vapor.codes/redis/master/Redis/RedisSessionsDelegate/) + +符合该协议的对象可以用来改变Redis中`SessionData`的存储方式。 + +符合该协议的类型只需要实现两个方法。[`redis(_:store:with:)`](https://api.vapor.codes/redis/master/Redis/RedisSessionsDelegate/#redissessionsdelegate.redis(_:store:with:))和[`redis(_:fetchDataFor:)`](https://api.vapor.codes/redis/master/Redis/RedisSessionsDelegate/#redissessionsdelegate.redis(_:fetchDataFor:) )。 + +这两个都是必须的,因为你自定义将会话数据写入Redis的方式与如何从Redis读取数据有内在联系。 + +### RedisSessionsDelegate Hash 示例 + +例如,如果你想把会话数据存储为[**Hash**在Redis中](https://redis.io/topics/data-types-intro#redis-hashes),你可以实现如下内容。 + +```swift +func redis( + _ client: Client, + store data: SessionData, + with key: RedisKey +) -> EventLoopFuture { + // 将每个数据字段存储为一个单独的哈希字段 + return client.hmset(data.snapshot, in: key) +} +func redis( + _ client: Client, + fetchDataFor key: RedisKey +) -> EventLoopFuture { + return client + .hgetall(from: key) + .map { hash in + // 哈希值是[String: RESPValue],所以我们需要尝试将其解包为字符串,并将每个值存储在数据容器中。 + // 变成一个字符串,并将每个值存储在数据容器中。 + return hash.reduce(into: SessionData()) { result, next in + guard let value = next.value.string else { return } + result[next.key] = value + } + } +} +``` diff --git a/4.0/docs/security/authentication.md b/4.0/docs/security/authentication.md index 2758474..53e8107 100644 --- a/4.0/docs/security/authentication.md +++ b/4.0/docs/security/authentication.md @@ -1,26 +1,25 @@ -# Authentication +# 认证 -Authentication is the act of verifying a user's identity. This is done through the verification of credentials like a username and password or unique token. Authentication (sometimes called auth/c) is distinct from authorization (auth/z) which is the act of verifying a previously authenticated user's permissions to perform certain tasks. +认证是验证用户身份的行为。这是通过验证用户名和密码或独特的令牌等凭证来完成的。认证(有时称为auth/c)与授权(auth/z)不同,后者是验证先前认证的用户执行某些任务的权限的行为。 -## Introduction +## 介绍 -Vapor's Authentication API provides support for authenticating a user via the `Authorization` header, using [Basic](https://tools.ietf.org/html/rfc7617) and [Bearer](https://tools.ietf.org/html/rfc6750). It also supports authenticating a user via the data decoded from the [Content](../basics/content.md) API. +Vapor的认证API支持通过`Authorization`头对用户进行认证,使用[基本](https://tools.ietf.org/html/rfc7617)和[承载](https://tools.ietf.org/html/rfc6750)。它还支持通过从[Content](/content.md)API解码的数据来验证用户。 -Authentication is implemented by creating an `Authenticator` which contains the verification logic. An authenticator can be used to protect individual route groups or an entire app. The following authenticator helpers ship with Vapor: +认证是通过创建一个包含验证逻辑的`Authenticator`来实现的。一个认证器可以用来保护单个路由组或整个应用程序。以下是Vapor提供的认证器辅助工具。 -|Protocol|Description| +|协议|描述| |-|-| -|`RequestAuthenticator`|Base authenticator capable of creating middleware.| -|[`BasicAuthenticator`](#basic)|Authenticates Basic authorization header.| -|[`BearerAuthenticator`](#bearer)|Authenticates Bearer authorization header.| -|`UserTokenAuthenticator`|Authenticates a token type with associated user.| -|`CredentialsAuthenticator`|Authenticates a credentials payload from the request body.| +|`RequestAuthenticator`/`AsyncRequestAuthenticator`|能够创建中间件的基础认证器。| +|[`BasicAuthenticator`/`AsyncBasicAuthenticator`](#basic)|验证基本授权头。| +|[`BearerAuthenticator`/`AsyncBearerAuthenticator`](#bearer)|验证承载器授权头。| +|`CredentialsAuthenticator`/`AsyncCredentialsAuthenticator`|从请求体中认证一个证书有效载荷。| -If authentication is successful, the authenticator adds the verified user to `req.auth`. This user can then be accessed using `req.auth.get(_:)` in routes protected by the authenticator. If authentication fails, the user is not added to `req.auth` and any attempts to access it will fail. +如果认证成功,认证器会将经过验证的用户添加到`req.auth`中。然后可以使用`req.auth.get(_:)`在认证器保护的路由中访问这个用户。如果认证失败,该用户不会被添加到`req.auth`中,任何访问该用户的尝试都会失败。 -## Authenticatable +## 可认证的 -To use the Authentication API, you first need a user type that conforms to `Authenticatable`. This can be a `struct`, `class`, or even a Fluent `Model`. The following examples assume this simple `User` struct that has one property: `name`. +要使用认证API,你首先需要一个符合`Authenticatable`的用户类型。这可以是一个`struct`,`class`,甚至是一个Fluent`Model`。下面的例子假设这个简单的`User`结构有一个属性:`name`。 ```swift import Vapor @@ -30,11 +29,11 @@ struct User: Authenticatable { } ``` -Each example below will create an authenticator named `UserAuthenticator`. +下面的每个例子都将使用我们创建的认证器的一个实例。在这些例子中,我们称它为`UserAuthenticator`。 -### Route +### 路线 -Authenticators are middleware and be be used for protecting routes. +认证器是中间件,可用于保护路由。 ```swift let protected = app.grouped(UserAuthenticator()) @@ -43,31 +42,31 @@ protected.get("me") { req -> String in } ``` -`req.auth.require` is used to fetch the authenticated `User`. If authentication failed, this method will throw an error, protecting the route. +`req.auth.require`用于获取认证的`User`。如果认证失败,该方法将抛出一个错误,保护路线。 ### Guard Middleware -You can also use `GuardMiddleware` in your route group to ensure that a user has been authenticated before reaching your route handler. +你也可以在你的路由组中使用`GuardMiddleware`来确保用户在到达你的路由处理程序之前已经被认证了。 ```swift let protected = app.grouped(UserAuthenticator()) .grouped(User.guardMiddleware()) ``` -Requiring authentication is not done by the authenticator middleware to allow for composition of authenticators. Read more about [composition](#composition) below. +要求认证者中间件不做认证,以允许认证者的组成。请阅读下面关于[组合](#composition)的更多信息。 -## Basic +## 基本 -Basic authentication sends a username and password in the `Authorization` header. The username and password are concatenated with a colon (e.g. `test:secret`), base-64 encoded, and prefixed with `"Basic "`. The following example request encodes the username `test` with password `secret`. +基本认证在`Authorization`头中发送一个用户名和密码。用户名和密码用冒号连接(例如`test:secret`), 以base-64编码, 并以`"Basic"`为前缀。下面的请求示例对用户名`test`和密码`secret`进行编码。 ```http GET /me HTTP/1.1 Authorization: Basic dGVzdDpzZWNyZXQ= ``` -Basic authentication is typically used once to log a user in and generate a token. This minimizes how frequently the user's sensitive password must be sent. You should never send Basic authorization over a plaintext or unverified TLS connection. +基本认证通常只用一次,用于登录用户并生成一个令牌。这就最大限度地减少了必须发送用户敏感密码的频率。你不应该通过明文或未经验证的TLS连接发送基本授权。 -To implement Basic authentication in your app, create a new authenticator conforming to `BasicAuthenticator`. Below is an example authenticator hard-coded to verify the request from above. +要在你的应用程序中实现基本认证,创建一个符合`BasicAuthenticator`的新认证器。下面是一个硬编码的认证器例子,用来验证上面的请求。 ```swift @@ -79,41 +78,59 @@ struct UserAuthenticator: BasicAuthenticator { func authenticate( basic: BasicAuthorization, for request: Request - ) -> EventLoopFuture { - guard basic.username == "test" && basic.password == "secret" else { - return request.eventLoop.makeSucceededFuture(nil) - } - let test = User(name: "Vapor") - return request.eventLoop.makeSucceededFuture(test) + ) -> EventLoopFuture { + if basic.username == "test" && basic.password == "secret" { + request.auth.login(User(name: "Vapor")) + } + return request.eventLoop.makeSucceededFuture(()) + } +} +``` + +如果你使用`async`/`await`,你可以使用`AsyncBasicAuthenticator`代替。 + +```swift +import Vapor + +struct UserAuthenticator: AsyncBasicAuthenticator { + typealias User = App.User + + func authenticate( + basic: BasicAuthorization, + for request: Request + ) async throws { + if basic.username == "test" && basic.password == "secret" { + request.auth.login(User(name: "Vapor")) + } } } ``` -This protocol requires you to implement `authenticate(basic:for:)` which will be called when an incoming request contains the `Authorization: Basic ...` header. A `BasicAuthorization` struct containing the username and password is passed to the method. +这个协议要求你实现`authenticate(basic:for:)`,当传入的请求包含`Authorization: Basic... `头。一个包含用户名和密码的`BasicAuthorization`结构被传递给该方法。 -In this test authenticator, the username and password are tested against hard-coded values. In a real authenticator, you might check against a database or external API. This is why the `authenticate` method allows you to return a future. +在这个测试认证器中,用户名和密码与硬编码的值进行测试。在一个真正的认证器中,你可能会针对数据库或外部API进行检查。这就是为什么`authenticate`方法允许你返回一个未来。 -!!! tip - Passwords should never be stored in a database as plaintext. Always use password hashes for comparison. +!!! Tip + 密码不应该以明文形式存储在数据库中。总是使用密码哈希值进行比较。 -If the authentication parameters are correct, in this case matching the hard-coded values, a `User` named Vapor is returned. If the authentication parameters do not match, `nil` is returned, which signifies authentication failed. +如果认证参数是正确的,在这种情况下与硬编码的值相匹配,一个名为Vapor的`用户`被登录。如果认证参数不匹配,没有用户被登录,这表示认证失败。 -If you add this authenticator to your app, and test the route defined above, you should see the name `"Vapor"` returned for a successful login. If the credentials are not correct, you should see a `401 Unauthorized` error. +如果你把这个认证器添加到你的应用程序中,并测试上面定义的路由,你应该看到名字`"Vapor"`返回成功的登录。如果凭证不正确, 你应该看到一个`401 Unauthorized`的错误. ## Bearer -Bearer authentication sends a token in the `Authorization` header. The token is prefixed with `"Bearer "`. The following example request sends the token `foo`. +Bearer认证在`Authorization`头中发送一个令牌。该令牌的前缀是`"Bearer"`。下面的请求示例发送了令牌`foo`。 ```http GET /me HTTP/1.1 Authorization: Bearer foo ``` -Bearer authentication is commonly used for authentication of API endpoints. The user typically requests a Bearer token by sending credentials like a username and password to a login endpoint. This token may last minutes or days depending on the application's needs. +Bearer认证通常用于API端点的认证。用户通常通过向登录端点发送用户名和密码等凭证来请求一个承载器令牌。这个令牌可能持续几分钟或几天,取决于应用程序的需求。 -As long as the token is valid, the user can use it in place of his or her credentials to authenticate against the API. If the token becomes invalid, a new one can be generated using the login endpoint. +只要令牌是有效的,用户就可以用它来代替他或她的凭证,对API进行认证。如果令牌失效了,可以使用登录端点生成一个新的令牌。 -To implement Bearer authentication in your app, create a new authenticator conforming to `BearerAuthenticator`. Below is an example authenticator hard-coded to verify the request from above. +要在你的应用程序中实现Bearer认证,创建一个符合`BearerAuthenticator`的新认证器。下面是一个硬编码的认证器例子,用来验证上面的请求。 ```swift import Vapor @@ -124,35 +141,53 @@ struct UserAuthenticator: BearerAuthenticator { func authenticate( bearer: BearerAuthorization, for request: Request - ) -> EventLoopFuture { - guard bearer.token == "foo" else { - return request.eventLoop.makeSucceededFuture(nil) + ) -> EventLoopFuture { + if bearer.token == "foo" { + request.auth.login(User(name: "Vapor")) } - let test = User(name: "Vapor") - return request.eventLoop.makeSucceededFuture(test) + return request.eventLoop.makeSucceededFuture(()) } } ``` -This protocol requires you to implement `authenticate(bearer:for:)` which will be called when an incoming request contains the `Authorization: Bearer ...` header. A `BearerAuthorization` struct containing the token is passed to the method. +如果你使用`async`/`await`,你可以使用`AsyncBasicAuthenticator`代替: -In this test authenticator, the token is tested against a hard-coded value. In a real authenticator, you might verify the token by checking against a database or using cryptographic measures, like is done with JWT. This is why the `authenticate` method allows you to return a future. +```swift +import Vapor + +struct UserAuthenticator: AsyncBearerAuthenticator { + typealias User = App.User -!!! tip - When implementing token verification, it's important to consider horizontal scalability. If your application needs to handle many users concurrently, authentication can be a potential bottlneck. Consider how your design will scale across multiple instances of your application running at once. + func authenticate( + bearer: BearerAuthorization, + for request: Request + ) async throws { + if bearer.token == "foo" { + request.auth.login(User(name: "Vapor")) + } + } +} +``` -If the authentication parameters are correct, in this case matching the hard-coded value, a `User` named Vapor is returned. If the authentication parameters do not match, `nil` is returned, which signifies authentication failed. +这个协议要求你实现`authenticate(bearer:for:)`,当一个传入的请求包含`Authorization: Bearer ... `头时,将被调用。一个包含令牌的`BearerAuthorization`结构被传递给该方法。 -If you add this authenticator to your app, and test the route defined above, you should see the name `"Vapor"` returned for a successful login. If the credentials are not correct, you should see a `401 Unauthorized` error. +在这个测试认证器中,令牌是针对一个硬编码的值进行测试。在真正的认证器中,你可以通过检查数据库或使用加密措施来验证令牌,就像JWT那样。这就是为什么`authenticate`方法允许你返回一个未来。 -## Composition +!!! Tip + 在实现令牌验证时,考虑横向可扩展性是很重要的。如果你的应用程序需要同时处理许多用户,验证可能是一个潜在的瓶颈。考虑你的设计如何在你的应用程序同时运行的多个实例中扩展。 -Multiple authenticators can be composed (combined together) to create more complex endpoint authentication. Since an authenticator middleware will not reject the request if authentication fails, more than one of these middleware can be chained together. Authenticators can composed in two key ways. +如果认证参数是正确的,在这种情况下与硬编码的值相匹配,一个名为Vapor的`User`被登录。如果认证参数不匹配,则没有用户被登录,这表示认证失败。 -### Composing Methods +如果你把这个认证器添加到你的应用程序中,并测试上面定义的路由,你应该看到名字`"Vapor"`返回成功登录。如果凭证不正确,你应该看到一个`401 Unauthorized`的错误。 +## 组成 -The first method of authentication composition is chaining more than one authenticator for the same user type. Take the following example: +多个认证器可以组成(组合在一起)以创建更复杂的终端认证。由于认证器中间件在认证失败时不会拒绝请求,因此可以将多个这样的中间件串联起来。认证器可以通过两种主要方式组成。 + +### 组成方法 + + +第一种认证组成方法是为同一用户类型链上一个以上的认证器。以下面的例子为例。 ```swift app.grouped(UserPasswordAuthenticator()) @@ -161,17 +196,17 @@ app.grouped(UserPasswordAuthenticator()) .post("login") { req in let user = try req.auth.require(User.self) - // Do something with user. + // 对用户做一些事情。 } ``` -This example assumes two authenticators `UserPasswordAuthenticator` and `UserTokenAuthenticator` that both authenticate `User`. Both of these authenticators are added to the route group. Finally, `GuardMiddleware` is added after the authenticators to require that `User` was successfully authenticated. +这个例子假设有两个认证器`UserPasswordAuthenticator`和`UserTokenAuthenticator`,它们都认证`User`。这两个认证器都被添加到路由组中。最后,`GuardMiddleware`被添加到认证器之后,以要求`User`被成功认证。 -This composition of authenticators results in a route that can be accessed by either password or token. Such a route could allow a user to login and generate a token, then continue to use that token to generate new tokens. +这种认证器的组合产生了一个可以通过密码或令牌访问的路由。这样的路由可以允许用户登录并生成一个令牌,然后继续使用该令牌来生成新的令牌。 -### Composing Users +### 组成用户 -The second method of authentication composition is chaining authenticators for different user types. Take the following example: +认证组合的第二种方法是为不同的用户类型连锁认证器。以下面的例子为例: ```swift app.grouped(AdminAuthenticator()) @@ -181,39 +216,39 @@ app.grouped(AdminAuthenticator()) guard req.auth.has(Admin.self) || req.auth.has(User.self) else { throw Abort(.unauthorized) } - // Do something. + // 做点什么。 } ``` -This example assumes two authenticators `AdminAuthenticator` and `UserAuthenticator` that authenticate `Admin` and `User`, respectively. Both of these authenticators are added to the route group. Instead of using `GuardMiddleware`, a check in the route handler is added to see if either `Admin` or `User` were authenticated. If not, an error is thrown. +这个例子假设有两个认证器`AdminAuthenticator`和`UserAuthenticator`,分别认证`Admin`和`User`。这两个认证器都被添加到路由组中。不使用`GuardMiddleware`,而是在路由处理程序中添加一个检查,看`Admin`或`User`是否已被认证。如果没有,则抛出一个错误。 -This composition of authenticators results in a route that can be accessed by two different types of users with potentially different methods of authentication. Such a route could allow for normal user authentication while still giving access to a super-user. +这种认证器的组合导致一个路由可以被两种不同类型的用户以潜在的不同认证方法访问。这样的路由可以允许正常的用户认证,同时仍然允许超级用户访问。 -## Manual +## 手动 -You can also handle authentication manually using `req.auth`. This is especially useful for testing. +你也可以使用`req.auth`手动处理认证。这对测试特别有用。 -To manually log a user in, use `req.auth.login(_:)`. Any `Authenticatable` user can be passed to this method. +要手动登录一个用户,使用`req.auth.login(_:)`。任何`Authenticatable`的用户都可以被传递给这个方法。 ```swift req.auth.login(User(name: "Vapor")) ``` -To get the authenticated user, use `req.auth.require(_:)` +要获得认证的用户,使用`req.auth.require(_:)`。 ```swift let user: User = try req.auth.require(User.self) print(user.name) // String ``` -You can also use `req.auth.get(_:)` if you don't want to automatically throw an error when authentication fails. +如果你不想在认证失败时自动抛出一个错误,你也可以使用`req.auth.get(_:)`。 ```swift let user = req.auth.get(User.self) print(user?.name) // String? ``` -To unauthenticate a user, pass the user type to `req.auth.logout(_:)`. +要取消一个用户的认证,把用户类型传给`req.auth.logout(_:)`。 ```swift req.auth.logout(User.self) @@ -221,15 +256,15 @@ req.auth.logout(User.self) ## Fluent -Fluent defines two protocols `ModelAuthenticatable` and `ModelTokenAuthenticatable` which can be added to your existing models. Conforming your models to these protocols allows for the creation of authenticators for protecting endpoints. +[Fluent](fluent/overview.md)定义了两个协议`ModelAuthenticatable`和`ModelTokenAuthenticatable`,可以添加到你现有的模型中。使你的模型符合这些协议允许创建保护端点的认证器。 -`ModelTokenAuthenticatable` authenticates with a Bearer token. This is what you use to protect most of your endpoints. `ModelAuthenticatable` authenticates with username and password and is used by a single endpoint for generating tokens. +`ModelTokenAuthenticatable`用Bearer token进行认证。这是你用来保护大多数端点的方法。`ModelAuthenticatable`使用用户名和密码进行认证,由一个端点用于生成令牌。 -This guide assumes you are familiar with Fluent and have successfully configured your app to use a database. If you are new to Fluent, start with the [overview](../fluent/overview.md). +本指南假设你熟悉Fluent,并且已经成功地配置了你的应用程序来使用数据库。如果你是Fluent的新手,请从[概述](fluent/overview.md)开始。 -### User +###用户 -To start, you will need a model representing the user that will be authenticated. For this guide, we'll be using the following model, but you are free to use an existing model. +首先,你需要一个代表将被验证的用户的模型。在本指南中,我们将使用以下模型,但你也可以自由使用现有的模型。 ```swift import Fluent @@ -261,39 +296,40 @@ final class User: Model, Content { } ``` -The model must be able to store a username, in this case an email, and a password hash. The corresponding migration for this example model is here: +该模型必须能够存储一个用户名,在这里是一个电子邮件,以及一个密码哈希值。我们还将`email`设置为唯一的字段,以避免重复的用户。这个例子模型的相应迁移在这里: ```swift import Fluent import Vapor extension User { - struct Migration: Fluent.Migration { + struct Migration: AsyncMigration { var name: String { "CreateUser" } - func prepare(on database: Database) -> EventLoopFuture { - database.schema("users") + func prepare(on database: Database) async throws { + try await database.schema("users") .id() .field("name", .string, .required) .field("email", .string, .required) .field("password_hash", .string, .required) + .unique(on: "email") .create() } - func revert(on database: Database) -> EventLoopFuture { - database.schema("users").delete() + func revert(on database: Database) async throws { + try await database.schema("users").delete() } } } ``` -Don't forget to add the migration to `app.migrations`. +不要忘记将迁移添加到`app.migrations`中。 ```swift app.migrations.add(User.Migration()) ``` -The first thing you will need is an endpoint to create new users. Let's use `POST /users`. Create a [Content](../basics/content.md) struct representing the data this endpoint expects. +你首先需要的是一个创建新用户的端点。让我们使用`POST /users`。创建一个[Content](content.md)结构,代表这个端点所期望的数据。 ```swift import Vapor @@ -308,7 +344,7 @@ extension User { } ``` -If you like, you can conform this struct to [Validatable](../basics/validation.md) to add validation requirements. +如果你愿意,你可以将这个结构与[Validatable](validation.md)相符合,以增加验证要求。 ```swift import Vapor @@ -322,11 +358,11 @@ extension User.Create: Validatable { } ``` -Now you can create the `POST /users` endpoint. +现在你可以创建`POST /users`端点。 ```swift -app.post("users") { req -> EventLoopFuture in - try User.Create.validate(req) +app.post("users") { req async throws -> User in + try User.Create.validate(content: req) let create = try req.content.decode(User.Create.self) guard create.password == create.confirmPassword else { throw Abort(.badRequest, reason: "Passwords did not match") @@ -336,14 +372,14 @@ app.post("users") { req -> EventLoopFuture in email: create.email, passwordHash: Bcrypt.hash(create.password) ) - return user.save(on: req.db) - .map { user } + try await user.save(on: req.db) + return user } ``` -This endpoint validates the incoming request, decodes the `User.Create` struct, and checks that the passwords match. It then uses the decoded data to create a new `User` and saves it to the database. The plaintext password is hashed using `Bcrypt` before saving to the database. +这个端点验证传入的请求,对`User.Create`结构进行解码,并检查密码是否匹配。然后,它使用解码后的数据创建一个新的`User`并将其保存到数据库。在保存到数据库之前,使用`Bcrypt`对明文密码进行散列。 -Build and run the project, making sure to migrate the database first, then use the following request to create a new user. +建立并运行该项目,确保首先迁移数据库,然后使用下面的请求创建一个新的用户。 ```http POST /users HTTP/1.1 @@ -353,14 +389,14 @@ Content-Type: application/json { "name": "Vapor", "email": "test@vapor.codes", - "password": "secret", - "confirmPassword": "secret" + "password": "secret42", + "confirmPassword": "secret42" } ``` #### Model Authenticatable -Now that you have a user model and an endpoint to create new users, let's conform the model to `ModelAuthenticatable`. This will allow for the model to be authenticated using username and password. +现在你有了一个用户模型和一个创建新用户的端点,让我们把这个模型变成`ModelAuthenticatable`。这将允许该模型使用用户名和密码进行认证。 ```swift import Fluent @@ -376,11 +412,11 @@ extension User: ModelAuthenticatable { } ``` -This extension adds `ModelAuthenticatable` conformance to `User`. The first two properties specify which fields should be used for storing the username and password hash respectively. The `\` notation creates a key path to the fields that Fluent can use to access them. +这个扩展为`User`增加了`ModelAuthenticatable`的一致性。前两个属性分别指定了哪些字段应该用来存储用户名和密码哈希值。`\`符号创建了一个通往字段的关键路径,Fluent可以用它来访问这些字段。 -The last requirement is a method for verifying plaintext passwords sent in the Basic authentication header. Since we're using Bcrypt to hash the password during signup, we'll use Bcrypt to verify that the supplied password matches the stored password hash. +最后一个要求是验证Basic认证头中发送的明文密码的方法。由于我们在注册时使用Bcrypt对密码进行散列,我们将使用Bcrypt来验证所提供的密码是否与存储的密码散列相符。 -Now that the `User` conforms to `ModelAuthenticatable`, we can create an authenticator for protecting the login route. +现在`User`符合`ModelAuthenticatable`,我们可以创建一个认证器来保护登录路线。 ```swift let passwordProtected = app.grouped(User.authenticator()) @@ -389,22 +425,22 @@ passwordProtected.post("login") { req -> User in } ``` -`ModelAuthenticatable` adds a static method `authenticator` for creating an authenticator. +`ModelAuthenticatable`增加了一个静态方法`authenticator`用于创建一个认证器。 -Test that this route works by sending the following request. +通过发送以下请求来测试这个路由是否工作。 ```http POST /login HTTP/1.1 -Authorization: Basic dGVzdEB2YXBvci5jb2RlczpzZWNyZXQ= +Authorization: Basic dGVzdEB2YXBvci5jb2RlczpzZWNyZXQ0Mg== ``` -This request passes the username `test@vapor.codes` and password `secret` via the Basic authentication header. You should see the previously created user returned. +这个请求通过基本认证头传递用户名`test@vapor.codes`和密码`secret42`。你应该看到先前创建的用户被返回。 -While you could theoretically use Basic authentication to protect all of your endpoints, it's recommended to use a separate token instead. This minimizes how often you must send the user's sensitive password over the Internet. It also makes authentication much faster since you only need to perform password hashing during login. +虽然理论上你可以使用Basic认证来保护你所有的端点,但建议使用单独的令牌来代替。这可以尽量减少你必须在互联网上发送用户敏感密码的频率。这也使得认证速度大大加快,因为你只需要在登录时进行密码散列。 -### User Token +### 用户令牌 -Create a new model for representing user tokens. +创建一个新的模型来代表用户令牌。 ```swift import Fluent @@ -432,19 +468,19 @@ final class UserToken: Model, Content { } ``` -This model must have a `value` field for storing the token's unique string. It must also have a [parent-relation](../fluent/overview.md#parent) to the user model. You may add additional properties to this token as you see fit, such as an expiration date. +这个模型必须有一个`value`字段,用于存储令牌的唯一字符串。它还必须有一个[parent-relation](fluent/overview.md#parent)到用户模型。你可以在你认为合适的时候为这个令牌添加额外的属性,比如说过期日期。 -Next, create a migration for this model. +接下来,为这个模型创建一个迁移。 ```swift import Fluent extension UserToken { - struct Migration: Fluent.Migration { + struct Migration: AsyncMigration { var name: String { "CreateUserToken" } - func prepare(on database: Database) -> EventLoopFuture { - database.schema("user_tokens") + func prepare(on database: Database) async throws { + try await database.schema("user_tokens") .id() .field("value", .string, .required) .field("user_id", .uuid, .required, .references("users", "id")) @@ -452,22 +488,22 @@ extension UserToken { .create() } - func revert(on database: Database) -> EventLoopFuture { - database.schema("user_tokens").delete() + func revert(on database: Database) async throws { + try await database.schema("user_tokens").delete() } } } ``` -Notice that this migration makes the `value` field unique. It also creates a foreign key reference between the `user_id` field and the users table. +请注意,这个迁移使得`value`字段是唯一的。它还在`user_id`字段和用户表之间创建了一个外键引用。 -Don't forget to add the migration to `app.migrations`. +不要忘记把这个迁移添加到`app.migrations`中。 ```swift app.migrations.add(UserToken.Migration()) ``` -Finally, add a method on `User` for generating a new token. This method will be used during login. +最后,在`User`上添加一个方法,用于生成一个新的令牌。这个方法将在登录时使用。 ```swift extension User { @@ -480,31 +516,31 @@ extension User { } ``` -Here we're using `[UInt8].random(count:)` to generate a random token value. For this example, 16 bytes, or 128 bits, of random data are being used. You can adjust this number as you see fit. The random data is then base-64 encoded to make it easy to transmit in HTTP headers. +这里我们使用`[UInt8].random(count:)`来生成一个随机的token值。在这个例子中,使用了16个字节,或128位的随机数据。你可以根据你的需要调整这个数字。然后,随机数据被base-64编码,以使其易于在HTTP头文件中传输。 -Now that you can generate user tokens, update the `POST /login` route to create and return a token. +现在你可以生成用户令牌了,更新`POST /login`路由以创建并返回一个令牌。 ```swift let passwordProtected = app.grouped(User.authenticator()) -passwordProtected.post("login") { req -> EventLoopFuture in +passwordProtected.post("login") { req async throws -> UserToken in let user = try req.auth.require(User.self) let token = try user.generateToken() - return token.save(on: req.db) - .map { token } + try await token.save(on: req.db) + return token } ``` -Test that this route works by using the same login request from above. You should now get a token upon logging in that looks something like: +通过使用上述相同的登录请求来测试这个路由是否有效。你现在应该在登录时得到一个令牌,看起来像这样。 ``` 8gtg300Jwdhc/Ffw784EXA== ``` -Hold onto the token you get as we'll use it shortly. +请保管好你得到的令牌,因为我们很快就会用到它。 -#### Model Token Authenticatable +#### 可验证的Token模型 -Conform `UserToken` to `ModelTokenAuthenticatable`. This will allow for tokens to authenticate your `User` model. +使`UserToken`符合`ModelTokenAuthenticatable`。这将允许令牌认证你的`User`模型。 ```swift import Vapor @@ -520,13 +556,13 @@ extension UserToken: ModelTokenAuthenticatable { } ``` -The first protocol requirement specifies which field stores the token's unique value. This is the value that will be sent in the Bearer authentication header. The second requirement specifies the parent-relation to the `User` model. This is how Fluent will look up the authenticated user. +第一个协议要求规定了哪个字段存储令牌的唯一值。这是将在承载认证头中发送的值。第二个要求是指定与`User` 模型的父关系。这是Fluent查找认证用户的方式。 -The final requirement is an `isValid` boolean. If this is `false`, the token will be deleted from the database and the user will not be authenticated. For simplicity, we'll make the tokens eternal by hard-coding this to `true`. +最后一个要求是一个`isValid`布尔值。如果是`false`,则令牌将被从数据库中删除,用户将不会被认证。为了简单起见,我们将通过硬编码使令牌变成永恒的`true'。 -Now that the token conforms to `ModelTokenAuthenticatable`, you can create an authenticator for protecting routes. +现在令牌符合`ModelTokenAuthenticatable`,你可以创建一个认证器来保护路由。 -Create a new endpoint `GET /me` for getting the currently authenticated user. +创建一个新的端点`GET /me`来获取当前认证的用户。 ```swift let tokenProtected = app.grouped(UserToken.authenticator()) @@ -535,26 +571,26 @@ tokenProtected.get("me") { req -> User in } ``` -Similar to `User`, `UserToken` now has a static `authenticator()` method that can generate an authenticator. The authenticator will attempt to find a matching `UserToken` using the value provided in the Bearer authentication header. If it finds a match, it will fetch the related `User` and authenticate it. +与`User`类似,`UserToken`现在有一个静态的`authenticator()`方法,可以生成一个认证器。认证器将尝试使用Bearer认证头中提供的值找到一个匹配的`UserToken`。如果它找到一个匹配的,它将获取相关的`User`并对其进行认证。 -Test that this route works by sending the following HTTP request where the token is the value you saved from the `POST /login` request. +通过发送以下HTTP请求来测试这个路由是否有效,其中令牌是你在`POST /login`请求中保存的值。 ```http GET /me HTTP/1.1 Authorization: Bearer ``` -You should see the authenticated `User` returned. +你应该看到认证的`User`返回。 ## Session -Vapor's [Session API](../advanced/sessions.md) can be used to automatically persist user authentication between requests. This works by storing a unique identifier for the user in the request's session data after successful login. On subsequent requests, the user's identifier is fetched from the session and used to authenticate the user before calling your route handler. +Vapor的[Session API](session.md)可以用来在不同的请求之间自动保持用户认证。这通过在成功登录后在请求的会话数据中存储用户的唯一标识符来实现。在随后的请求中,用户的标识符被从会话中获取,并在调用你的路由处理程序之前用于验证用户。 -Sessions are great for front-end web applications built in Vapor that serve HTML directly to web browsers. For APIs, we recommend using stateless, token-based authentication to persist user data between requests. +会话非常适合在Vapor中构建的前端Web应用,它直接向Web浏览器提供HTML。对于API,我们建议使用无状态的、基于令牌的认证,以在请求之间保持用户数据。 -### Session Authenticatable +### 会话可认证 -To use session-based authentication, you will need a type that conforms to `SessionAuthenticatable`. For this example, we'll use a simple struct. +要使用基于会话的认证,你将需要一个符合`SessionAuthenticatable`的类型。对于这个例子,我们将使用一个简单的结构。 ```swift import Vapor @@ -564,7 +600,7 @@ struct User { } ``` -To conform to `SessionAuthenticatable`, you will need to specify a `sessionID`. This is the value that will be stored in the session data and must uniquely identify the user. +为了符合`SessionAuthenticatable`,你需要指定一个`sessionID`。这是将被存储在会话数据中的值,必须唯一地识别用户。 ```swift extension User: SessionAuthenticatable { @@ -574,11 +610,11 @@ extension User: SessionAuthenticatable { } ``` -For our simple `User` type, we'll use the email address as the unique session identifier. +对于我们简单的`User`类型,我们将使用电子邮件地址作为唯一的会话标识符。 -### Session Authenticator +### 会话认证器 -Next, we'll need a `SessionAuthenticator` to handle resolving instances of our User from the persisted session identifier. +接下来,我们需要一个`SessionAuthenticator`来处理从持久化的会话标识符中解析用户的实例。 ```swift @@ -592,28 +628,39 @@ struct UserSessionAuthenticator: SessionAuthenticator { } ``` -Since all the information we need to initialize our example `User` is contained in the session identifier, we can create and login the user synchronously. In a real-world application, you would likely use the session identifier to perform a database lookup or API request to fetch the rest of the user data before authenticating. +如果你使用`async`/`await`,你可以使用`AsyncSessionAuthenticator`: + +```swift +struct UserSessionAuthenticator: AsyncSessionAuthenticator { + typealias User = App.User + func authenticate(sessionID: String, for request: Request) async throws { + let user = User(email: sessionID) + request.auth.login(user) + } +} +``` + +由于我们需要初始化我们的例子`User`的所有信息都包含在会话标识符中,我们可以同步创建和登录用户。在现实世界的应用中,你可能会使用会话标识符来执行数据库查询或API请求,以便在验证之前获取其余的用户数据。 -Next, let's create a simple bearer authenticator to perform the initial authentication. +接下来,让我们创建一个简单的承载认证器来执行初始认证。 ```swift -struct UserBearerAuthenticator: BearerAuthenticator { - func authenticate(bearer: BearerAuthorization, for request: Request) -> EventLoopFuture { +struct UserBearerAuthenticator: AsyncBearerAuthenticator { + func authenticate(bearer: BearerAuthorization, for request: Request) async throws { if bearer.token == "test" { - let user = User(name: "hello@vapor.codes") + let user = User(email: "hello@vapor.codes") request.auth.login(user) } - return request.eventLoop.makeSucceededFuture(()) } } ``` -This authenticator will authenticate a user with the email `hello@vapor.codes` when the bearer token `test` is sent. +当发送不记名令牌`test`时,这个认证器将用电子邮件`hello@vapor.codes`来认证用户。 -Finally, let's combine all these pieces together in your application. +最后,让我们在你的应用程序中把所有这些部分结合起来。 ```swift -// Create protected route group which requires user auth. +// 创建受保护的路由组,需要用户授权。 let protected = app.routes.grouped([ app.sessions.middleware, UserSessionAuthenticator(), @@ -621,61 +668,210 @@ let protected = app.routes.grouped([ User.guardMiddleware(), ]) -// Add GET /me route for reading user's email. +// 添加GET /me路由,用于读取用户的电子邮件。 protected.get("me") { req -> String in try req.auth.require(User.self).email } ``` -`SessionsMiddleware` is added first to enable session support on the application. More information about configuring sessions can be found in the [Session API](../advanced/sessions.md) section. +`SessionsMiddleware`首先被添加到应用程序上,以启用会话支持。关于配置会话的更多信息可以在[会话API](session.md)部分找到。 -Next, the `SessionAuthenticator` is added. This handles authenticating the user if a session is active. +接下来,添加`SessionAuthenticator`。如果会话处于活动状态,这将处理对用户的认证。 -If the authentication has not been persisted in the session yet, the request will be forwarded to the next authenticator. `UserBearerAuthenticator` will check the bearer token and authenticate the user if it equals `"test"`. +如果认证还没有被保存在会话中,请求将被转发到下一个认证器。`UserBearerAuthenticator`将检查承载令牌,如果它等于`"test"`,将对用户进行认证。 -Finally, `User.guardMiddleware()` will ensure that `User` has been authenticated by one of the previous middleware. If the user has not been authenticated, an error will be thrown. +最后,`User.guardMiddleware()`将确保`User`已经被之前的一个中间件认证过。如果用户没有被认证,将抛出一个错误。 -To test this route, first send the following request: +要测试这个路由,首先发送以下请求: ```http GET /me HTTP/1.1 authorization: Bearer test ``` -This will cause `UserBearerAuthenticator` to authenticate the user. Once authenticated, `UserSessionAuthenticator` will persist the user's identifier in session storage and generate a cookie. Use the cookie from the response in a second request to the route. +这将导致`UserBearerAuthenticator`对用户进行认证。一旦通过认证,`UserSessionAuthenticator`将在会话存储中保留用户的标识符,并生成一个cookie。在第二次请求路由时使用响应中的cookie。 ```http GET /me HTTP/1.1 cookie: vapor_session=123 ``` -This time, `UserSessionAuthenticator` will authenticate the user and you should again see the user's email returned. +这一次,`UserSessionAuthenticator`将对用户进行认证,你应该再次看到用户的电子邮件返回。 -### Model Session Authenticatable +### 模型会话可认证 -Fluent models can generate `SessionAuthenticator`s by conforming to `ModelSessionAuthenticatable`. This will use the model's unique identifier as the session identifier and automatically perform a database lookup to restore the model from the session. +Fluent模型可以通过符合`ModelSessionAuthenticatable`来生成`SessionAuthenticator`。这将使用模型的唯一标识符作为会话标识符,并自动执行数据库查询以从会话中恢复模型。 ```swift import Fluent final class User: Model { ... } -// Allow this model to be persisted in sessions. +// 允许这个模型在会话中被持久化。 extension User: ModelSessionAuthenticatable { } ``` -You can add `ModelSessionAuthenticatable` to any existing model as an empty conformance. Once added, a new static method will be available for creating a `SessionAuthenticator` for that model. +你可以把`ModelSessionAuthenticatable`作为一个空的一致性添加到任何现有的模型中。一旦添加,一个新的静态方法将可用于为该模型创建一个`SessionAuthenticator'。 ```swift User.sessionAuthenticator() ``` -This will use the application's default database for resolving the user. To specify a database, pass the identifier. +这将使用应用程序的默认数据库来解决用户的问题。要指定一个数据库,请传递标识符。 ```swift User.sessionAuthenticator(.sqlite) ``` +## 网站认证 + +网站是认证的一个特殊情况,因为使用浏览器限制了你如何将凭证附加到浏览器上。这就导致了两种不同的认证方案。 + +* 通过一个表格进行初始登录 +* 使用会话cookie验证的后续调用 + +Vapor和Fluent提供了几个助手来实现这种无缝连接。 + +### 会话认证 + +会话认证的工作原理如上所述。你需要将会话中间件和会话认证器应用于用户将要访问的所有路由。这包括任何受保护的路由,任何公开的路由,但如果用户登录了,你可能仍然想访问他们(例如显示一个账户按钮)***和***登录路由。 + +你可以在你的应用程序中的**configure.swift**中全局启用这个功能,就像这样。 + +```swift +app.middleware.use(app.sessions.middleware) +app.middleware.use(User.sessionAuthenticator()) +``` + +这些中间件做了以下工作。 + +* 会话中间件接收请求中提供的会话cookie,并将其转换为一个会话。 +* 会话验证器获取会话,并查看该会话是否有一个经过验证的用户。如果有,中间件就对请求进行认证。在响应中,会话认证器查看该请求是否有一个已认证的用户,并将其保存在会话中,以便在下一个请求中对其进行认证。 + +###保护路由 + +在保护API的路由时,如果请求没有被认证,你通常会返回一个状态代码为**401 Unauthorized**的HTTP响应。然而,对于使用浏览器的人来说,这并不是一个很好的用户体验。Vapor为任何`Authenticatable`类型提供了一个 `RedirectMiddleware`,以便在这种情况下使用: + +```swift +let protectedRoutes = app.grouped(User.redirectMiddleware(path: "/login?loginRequired=true")) +``` + +这与`GuardMiddleware`的工作原理类似。任何对注册到`protectedRoutes`的路由的请求,如果没有经过验证,将被重定向到提供的路径。这允许你告诉你的用户登录,而不是仅仅提供一个**401未经授权的**。 + +### 表格登录 + +为了验证用户和未来的请求与会话,你需要将用户登录。Vapor提供了一个`ModelCredentialsAuthenticatable`协议,以符合该协议。这可以处理通过表单登录的问题。首先让你的`User`符合这个协议。 + +```swift +extension User: ModelCredentialsAuthenticatable { + static let usernameKey = \User.$email + static let passwordHashKey = \User.$password + + func verify(password: String) throws -> Bool { + try Bcrypt.verify(password, created: self.password) + } +} +``` + +这和`ModelAuthenticatable`是一样的,如果你已经符合这个要求,那么你就不需要再做什么了。接下来将这个`ModelCredentialsAuthenticator`中间件应用到你的登录表单POST请求: +```swift +let credentialsProtectedRoute = sessionRoutes.grouped(User.credentialsAuthenticator()) +credentialsProtectedRoute.post("login", use: loginPostHandler) +``` + +这使用默认的凭证认证器来保护登录路线。你必须在POST请求中发送`username`和`password`。你可以像这样设置你的表单: + +```html +
    + + + + + +
    +``` + +`CredentialsAuthenticator`从请求体中提取`username`和`password`,从用户名中找到用户并验证密码。如果密码是有效的,中间件就对请求进行认证。然后`SessionAuthenticator`为后续请求验证会话。 + +## JWT +[JWT](jwt.md)提供了一个`JWTAuthenticator`,可用于验证传入请求中的JSON Web令牌。如果你是JWT的新手,请查看[概述](jwt.md)。 +首先,创建一个代表JWT有效载荷的类型。 + +```swift +// JWT有效载荷示例。 +struct SessionToken: Content, Authenticatable, JWTPayload { + + // 常量 + let expirationTime = 60 * 15 + + // Token数据 + var expiration: ExpirationClaim + var userId: UUID + + init(userId: UUID) { + self.userId = userId + self.expiration = ExpirationClaim(value: Date().addingTimeInterval(expirationTime)) + } + + init(user: User) throws { + self.userId = try user.requireID() + self.expiration = ExpirationClaim(value: Date().addingTimeInterval(expirationTime)) + } + + func verify(using signer: JWTSigner) throws { + try expiration.verifyNotExpired() + } +} +``` + +接下来,我们可以定义一个成功的登录响应中所包含的数据表示。目前,该响应将只有一个属性,即代表签名的JWT的字符串。 + +```swift +struct ClientTokenReponse: Content { + var token: String +} +``` + +使用我们的JWT令牌和响应的模型,我们可以使用一个密码保护的登录路线,它返回一个`ClientTokenReponse`,并包括一个签名的`SessionToken`。 + +```swift +let passwordProtected = app.grouped(User.authenticator(), User.guardMiddleware()) +passwordProtected.post("login") { req -> ClientTokenReponse in + let user = try req.auth.require(User.self) + let payload = try SessionToken(with: user) + return ClientTokenReponse(token: try req.jwt.sign(payload)) +} +``` + +另外,如果你不想使用认证器,你可以有一些看起来像以下的东西。 +```swift +app.post("login") { req -> ClientTokenReponse in + // Validate provided credential for user + // Get userId for provided user + let payload = try SessionToken(userId: userId) + return ClientTokenReponse(token: try req.jwt.sign(payload)) +} +``` + +通过使有效载荷符合`Authenticatable`和`JWTPayload`,你可以使用`authenticator()`方法生成一个路由认证器。将其添加到路由组中,在你的路由被调用之前自动获取并验证JWT。 + +```swift +// Create a route group that requires the SessionToken JWT. +let secure = app.grouped(SessionToken.authenticator(), SessionToken.guardMiddleware()) +``` + +添加可选的[防护中间件](#guard-middleware)将要求授权成功。 + +在受保护的路由内部,你可以使用`req.auth`访问经过认证的JWT有效载荷。 + +```swift +// Return ok reponse if the user-provided token is valid. +secure.post("validateLoggedInUser") { req -> HTTPStatus in + let sessionToken = try req.auth.require(SessionToken.self) + print(sessionToken.userId) + return .ok +} +``` diff --git a/4.0/docs/security/crypto.md b/4.0/docs/security/crypto.md index e402e8c..72af437 100644 --- a/4.0/docs/security/crypto.md +++ b/4.0/docs/security/crypto.md @@ -1,12 +1,12 @@ -# Crypto +# 加密 -Vapor includes [SwiftCrypto](https://github.com/apple/swift-crypto/) which is a Linux-compatible port of Apple's CryptoKit library. Some additional crypto APIs are exposed for things SwiftCrypto does not have yet, like [Bcrypt](https://en.wikipedia.org/wiki/Bcrypt) and [TOTP](https://en.wikipedia.org/wiki/Time-based_One-time_Password_algorithm). +Vapor包括[SwiftCrypto](https://github.com/apple/swift-crypto/),它是苹果公司CryptoKit库的一个Linux兼容的移植。一些额外的加密 API 被暴露出来,以满足 SwiftCrypto 还没有的东西,比如 [Bcrypt](https://en.wikipedia.org/wiki/Bcrypt) 和 [TOTP](https://en.wikipedia.org/wiki/Time-based_One-time_Password_algorithm)。 ## SwiftCrypto -Swift's `Crypto` library implements Apple's CryptoKit API. As such, the [CryptoKit documentation](https://developer.apple.com/documentation/cryptokit) and the [WWDC talk](https://developer.apple.com/videos/play/wwdc2019/709) are great resources for learning the API. +Swift的`Crypto`库实现了苹果的CryptoKit API。因此,[CryptoKit 文档](https://developer.apple.com/documentation/cryptokit) 和[WWDC 讲座](https://developer.apple.com/videos/play/wwdc2019/709) 是学习该 API 的绝佳资源。 -These APIs will be available automatically when you import Vapor. +当你导入Vapor时,这些API将自动可用。 ```swift import Vapor @@ -15,19 +15,19 @@ let digest = SHA256.hash(data: Data("hello".utf8)) print(digest) ``` -CryptoKit includes support for: +CryptoKit包括对以下内容的支持。 -- Hashing: `SHA512`, `SHA384`, `SHA256` -- Message Authentication Codes: `HMAC` -- Ciphers: `AES`, `ChaChaPoly` -- Public-Key Cryptography: `Curve25519`, `P521`, `P384`, `P256` -- Insecure hashing: `SHA1`, `MD5` +- 加密:`SHA512`, `SHA384`, `SHA256`。 +- 消息验证码:`HMAC`。 +- 密码器:`AES`, `ChaChaPoly`. +- 公钥加密:`Curve25519`, `P521`, `P384`, `P256`。 +- 不安全的散列:`SHA1`, `MD5`。 ## Bcrypt -Bcrypt is a password hashing algorithm that uses a randomized salt to ensure hashing the same password multiple times doesn't result in the same digest. +Bcrypt是一种密码散列算法,使用随机的盐来确保多次散列同一密码不会产生相同的摘要。 -Vapor provides a `Bcrypt` type for hashing and comparing passwords. +Vapor提供了一个`Bcrypt`类型,用于散列和比较密码。 ```swift import Vapor @@ -35,22 +35,59 @@ import Vapor let digest = try Bcrypt.hash("test") ``` -Because Bcrypt uses a salt, password hashes cannot be compared directly. Both the plaintext password and the existing digest must be verified together. +因为Bcrypt使用盐,所以密码哈希值不能直接比较。明文密码和现有摘要都必须一起验证。 ```swift import Vapor let pass = try Bcrypt.verify("test", created: digest) if pass { - // Password and digest match. + // 密码和摘要相符。 } else { - // Wrong password. + // 错误的密码。 } ``` -Login with Bcrypt passwords can be implemented by first fetching the user's password digest from the database by email or username. The known digest can then be verified against the supplied plaintext password. +使用Bcrypt密码登录可以通过首先从数据库中通过电子邮件或用户名获取用户的密码摘要来实现。然后可以根据提供的明文密码对已知的摘要进行验证。 -## TOTP +## OTP -Coming soon. +Vapor支持HOTP和TOTP两种一次性密码。OTP与SHA-1、SHA-256和SHA-512哈希函数一起工作,可以提供6、7或8位数的输出。OTP通过生成一个一次性的人类可读密码来提供认证。要做到这一点,各方首先要商定一个对称密钥,该密钥必须始终保持私有,以维护所生成密码的安全。 +#### HOTP + +HOTP是一种基于HMAC签名的OTP。除了对称密钥外,双方还商定了一个计数器,它是一个为密码提供唯一性的数字。在每次生成尝试后,计数器都会增加。 +```swift +let key = SymmetricKey(size: .bits128) +let hotp = HOTP(key: key, digest: .sha256, digits: .six) +let code = hotp.generate(counter: 25) + +// 或者使用静态生成函数 +HOTP.generate(key: key, digest: .sha256, digits: .six, counter: 25) +``` + +#### TOTP + +TOTP是HOTP的一个基于时间的变体。它的工作原理基本相同,但不是一个简单的计数器,而是用当前时间来产生唯一性。为了补偿由不同步的时钟、网络延迟、用户延迟和其他干扰因素带来的不可避免的偏差,生成的TOTP代码在指定的时间间隔内(最常见的是30秒)保持有效。 +```swift +let key = SymmetricKey(size: .bits128) +let totp = TOTP(key: key, digest: .sha256, digits: .six, interval: 60) +let code = totp.generate(time: Date()) + +// 或者使用静态生成函数 +TOTP.generate(key: key, digest: .sha256, digits: .six, interval: 60, time: Date()) +``` + +#### 范围 +OTP对于提供验证和不同步计数器的回旋余地非常有用。这两种OTP实现都有能力生成一个有误差范围的OTP。 +```swift +let key = SymmetricKey(size: .bits128) +let hotp = HOTP(key: key, digest: .sha256, digits: .six) + +// 生成一个正确计数器的窗口 +let codes = hotp.generate(counter: 25, range: 2) +``` +上面的例子允许留有2的余地,这意味着HOTP将对计数器的值`23 ... 27`进行计算,并且所有这些代码都将被返回。 + +!!! warning + 注意:使用的误差幅度越大,攻击者的行动时间和自由度就越大,降低了算法的安全性。 diff --git a/4.0/docs/security/jwt.md b/4.0/docs/security/jwt.md new file mode 100644 index 0000000..af35129 --- /dev/null +++ b/4.0/docs/security/jwt.md @@ -0,0 +1,445 @@ +# JWT + + +JSON Web Token(JWT)是一个开放的标准([RFC 7519](https://tools.ietf.org/html/rfc7519)),它定义了一种紧凑和独立的方式,以JSON对象的形式在各方之间安全地传输信息。这种信息可以被验证和信任,因为它是经过数字签名的。JWTs可以使用秘密(使用HMAC算法)或使用RSA或ECDSA的公共/私人密钥对进行签名。 + +## 开始使用 + +使用JWT的第一步是在你的[Package.swift](spm.md#package-manifest)中添加依赖关系。 + +```swift +// swift-tools-version:5.2 +import PackageDescription + +let package = Package( + name: "my-app", + dependencies: [ + // 其他的依赖性... + .package(url: "https://github.com/vapor/jwt.git", from: "4.0.0"), + ], + targets: [ + .target(name: "App", dependencies: [ + // 其他的依赖性... + .product(name: "JWT", package: "jwt") + ]), + // 其他目标... + ] +) +``` + +如果您在Xcode中直接编辑清单,它将会自动接收更改并在保存文件时获取新的依赖关系。否则,运行`swift package resolve`来获取新的依赖关系。 + +### 配置 + +JWT模块为`Application`添加了一个新属性`jwt`,用于配置。为了签署或验证JWT,你需要添加一个签名者。最简单的签名算法是`HS256`或HMAC与SHA-256。 + +```swift +import JWT + +// 添加带有SHA-256签名者的HMAC。 +app.jwt.signers.use(.hs256(key: "secret")) +``` + +`HS256`签名器需要一个密钥来初始化。与其他签名器不同的是,这个单一的密钥既可用于签名_也可用于验证令牌。了解更多关于以下可用的[算法](#algorithms)。 + +### 有效载荷 + +让我们试着验证一下下面这个JWT的例子。 + +```swift +eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ2YXBvciIsImV4cCI6NjQwOTIyMTEyMDAsImFkbWluIjp0cnVlfQ.lS5lpwfRNSZDvpGQk6x5JI1g40gkYCOWqbc3J_ghowo +``` + +你可以通过访问[jwt.io](https://jwt.io)并在调试器中粘贴该令牌来检查该令牌的内容。将 "验证签名 "部分的键设置为`secret`。 + +我们需要创建一个符合`JWTPayload`的结构,代表JWT的结构。我们将使用JWT包含的[claims](#claims)来处理常见的字段,如`sub`和`exp`。 + +```swift +// JWT有效载荷结构。 +struct TestPayload: JWTPayload { + // 将较长的 Swift 属性名称映射为 + // JWT 有效载荷中使用的缩短的键。 + enum CodingKeys: String, CodingKey { + case subject = "sub" + case expiration = "exp" + case isAdmin = "admin" + } + + // "sub"(subject)声明确定了作为JWT主体的委托人。 + // JWT的主体。 + var subject: SubjectClaim + + // "exp" (expiration time) 声称确定了JWT的过期时间。 + // 或之后,该JWT必须不被接受进行处理。 + var expiration: ExpirationClaim + + // 自定义数据。 + // 如果为真,该用户是管理员。 + var isAdmin: Bool + + // 运行除签名验证之外的任何其他验证逻辑。 + // 在这里进行签名验证。 + // 由于我们有一个ExpirationClaim,我们将 + // 调用其验证方法。 + func verify(using signer: JWTSigner) throws { + try self.expiration.verifyNotExpired() + } +} +``` + +###验证 + +现在我们有了一个`JWTPayload`,我们可以将上面的JWT附加到一个请求中,并使用`req.jwt`来获取和验证它。在你的项目中添加以下路由。 + +```swift +// 从传入的请求中获取并验证JWT。 +app.get("me") { req -> HTTPStatus in + let payload = try req.jwt.verify(as: TestPayload.self) + print(payload) + return .ok +} +``` + +`req.jwt.verify`帮助器将检查`Authorization`头是否有承载令牌。如果存在,它将解析JWT并验证其签名和声明。如果这些步骤失败,将抛出一个401 Unauthorized错误。 + +通过发送以下HTTP请求来测试该路由。 + +```http +GET /me HTTP/1.1 +authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ2YXBvciIsImV4cCI6NjQwOTIyMTEyMDAsImFkbWluIjp0cnVlfQ.lS5lpwfRNSZDvpGQk6x5JI1g40gkYCOWqbc3J_ghowo +``` + +如果一切正常,将返回一个200 OK响应,并打印出有效载荷: + +```swift +TestPayload( + subject: "vapor", + expiration: 4001-01-01 00:00:00 +0000, + isAdmin: true +) +``` + +### 签名 + +这个包也可以生成JWTs,也被称为签名。为了证明这一点,让我们使用上一节中的`TestPayload`。在你的项目中添加以下路由。 + +```swift +// 生成并返回一个新的JWT。 +app.post("login") { req -> [String: String] in + // 创建一个新的JWTPayload的实例 + let payload = TestPayload( + subject: "vapor", + expiration: .init(value: .distantFuture), + isAdmin: true + ) + // 返回已签名的JWT + return try [ + "token": req.jwt.sign(payload) + ] +} +``` + +`req.jwt.sign`助手将使用默认配置的签名器对`JWTPayload`进行序列化和签名。编码后的JWT将以`String`形式返回。 + +通过发送以下HTTP请求来测试该路由。 + +```http +POST /login HTTP/1.1 +``` + +你应该看到新生成的令牌在一个_200 OK_的响应中返回。 + +```json +{ + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ2YXBvciIsImV4cCI6NjQwOTIyMTEyMDAsImFkbWluIjp0cnVlfQ.lS5lpwfRNSZDvpGQk6x5JI1g40gkYCOWqbc3J_ghowo" +} +``` + +##认证 + +关于使用Vapor认证API的JWT的更多信息,请访问[Authentication → JWT](authentication.md#jwt)。 + +## 算法 + +Vapor的JWT API支持使用以下算法验证和签署令牌。 + +### HMAC + +HMAC是最简单的JWT签名算法。它使用一个单一的密钥,可以同时签署和验证令牌。该密钥可以是任何长度。 + +- `hs256`: 使用SHA-256的 HMAC +- `hs384`: 使用SHA-384的HMAC +- `hs512`: 使用SHA-512的HMAC + +```swift +// 添加带有SHA-256签名者的HMAC。 +app.jwt.signers.use(.hs256(key: "secret")) +``` + +### RSA + +RSA是最常用的JWT签名算法。它支持不同的公钥和私钥。这意味着公钥可以被分发,用于验证JWT的真实性,而生成它们的私钥是保密的。 + +要创建一个RSA签名器,首先要初始化一个`RSAKey`。这可以通过传入组件来完成。 + +```swift +// 用组件初始化一个RSA密钥。 +let key = RSAKey( + modulus: "...", + exponent: "...", + // 只包括在私人钥匙中。 + privateExponent: "..." +) +``` + +你也可以选择加载一个PEM文件: + +```swift +let rsaPublicKey = """ +-----BEGIN PUBLIC KEY----- +MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC0cOtPjzABybjzm3fCg1aCYwnx +PmjXpbCkecAWLj/CcDWEcuTZkYDiSG0zgglbbbhcV0vJQDWSv60tnlA3cjSYutAv +7FPo5Cq8FkvrdDzeacwRSxYuIq1LtYnd6I30qNaNthntjvbqyMmBulJ1mzLI+Xg/ +aX4rbSL49Z3dAQn8vQIDAQAB +-----END PUBLIC KEY----- +""" + +// 用公共pem初始化一个RSA密钥。 +let key = RSAKey.public(pem: rsaPublicKey) +``` + +使用`.private`来加载私人RSA PEM密钥。这些钥匙的开头是: + +``` +-----BEGIN RSA PRIVATE KEY----- +``` + +一旦你有了RSAKey,你可以用它来创建一个RSA签名器。 + +- `rs256`:使用SHA-256的RSA +- `rs384`:使用SHA-384的RSA +- `rs512`:使用SHA-512的RSA + +```swift +// 添加带有SHA-256的RSA签名者。 +try app.jwt.signers.use(.rs256(key: .public(pem: rsaPublicKey))) +``` + +### ECDSA + +ECDSA是一种更现代的算法,与RSA相似。在给定的密钥长度下,它被认为比RSA[^1]更安全。然而,在决定之前,你应该做你自己的研究。 + +[^1]: [https://sectigostore.com/blog/ecdsa-vs-rsa-everything-you-need-to-know/](https://sectigostore.com/blog/ecdsa-vs-rsa-everything-you-need-to-know/) + +像RSA一样,你可以使用PEM文件加载ECDSA密钥: + +```swift +let ecdsaPublicKey = """ +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2adMrdG7aUfZH57aeKFFM01dPnkx +C18ScRb4Z6poMBgJtYlVtd9ly63URv57ZW0Ncs1LiZB7WATb3svu+1c7HQ== +-----END PUBLIC KEY----- +""" + +// 用公共PEM初始化ECDSA密钥。 +let key = ECDSAKey.public(pem: ecdsaPublicKey) +``` + +使用`.private`来加载ECDSA PEM私钥。这些钥匙的开头是: + +``` +-----BEGIN PRIVATE KEY----- +``` + +你也可以使用`generate()`方法生成随机ECDSA。这对测试是很有用的。 + +```swift +let key = try ECDSAKey.generate() +``` + +一旦你有了ECDSA密钥,你就可以用它来创建ECDSA签名器。 + +- `es256`:使用SHA-256的ECDSA +- `es384`:使用SHA-384的ECDSA +- `es512`:使用SHA-512的ECDSA + +```swift +// 添加带有SHA-256的ECDSA签名者。 +try app.jwt.signers.use(.es256(key: .public(pem: ecdsaPublicKey))) +``` + +### 关键识别符(kid) + +如果你使用多种算法,你可以使用密钥标识符(`kid`s)来区分它们。当配置一个算法时,传递`kid`参数。 + +```swift +// 添加带有SHA-256签名者的HMAC,命名为 "a"。 +app.jwt.signers.use(.hs256(key: "foo"), kid: "a") +// 添加带有SHA-256签名者的HMAC,命名为 "b"。 +app.jwt.signers.use(.hs256(key: "bar"), kid: "b") +``` + +签署JWTs时,要为所需的签名者传递`kid`参数。 + +```swift +// 使用签名者"a"签名 +req.jwt.sign(payload, kid: "a") +``` + +这将自动在JWT头的`"kid"`字段中包括签名者的名字。当验证JWT时,这个字段将被用来查找适当的签名者。 + +```swift +// 使用"kid"头指定的签名者进行验证。 +// 如果没有"kid"头,将使用默认签名人。 +let payload = try req.jwt.verify(as: TestPayload.self) +``` + +由于[JWKs](#jwk)已经包含了`kid`值,你不需要在配置时指定它们。 + +```swift +// JWKs已经包含了 "孩子 "字段。 +let jwk: JWK = ... +app.jwt.signers.use(jwk: jwk) +``` + +## Claims + +Vapor的JWT包包括几个帮助器,用于实现常见的[JWT声明](https://tools.ietf.org/html/rfc7519#section-4.1)。 + +|索赔|类型|验证方法| +|---|---|---| +|`aud`|`AudienceClaim`|`verifyIntendedAudience(includes:)`| +|`exp`|`ExpirationClaim`|`verifyNotExpired(currentDate:)`| +|`jti`|`IDClaim`|n/a| +|`iat`|`IssuedAtClaim`|n/a| +|`iss`|`IssuerClaim`|n/a| +|`locale`|`LocaleClaim`|n/a| +|`nbf`|`NotBeforeClaim`|`verifyNotBefore(currentDate:)`| +|`sub`|`SubjectClaim`|n/a| + +所有的索赔应该在`JWTPayload.verify`方法中进行验证。如果索赔有一个特殊的验证方法,你可以使用该方法。否则,使用`value`访问索赔的值并检查它是否有效。 + +## JWK + +JSON网络密钥(JWK)是一种JavaScript对象符号(JSON)数据结构,代表一个加密密钥([RFC7517](https://tools.ietf.org/html/rfc7517))。这些通常用于为客户提供验证JWT的密钥。 + +例如,苹果公司将他们的Sign in with Apple JWKS托管在以下网址。 + +```http +GET https://appleid.apple.com/auth/keys +``` + +你可以把这个JSON网络密钥集(JWKS)添加到你的`JWTSigners`中。 + +```swift +import JWT +import Vapor + +// 下载JWKS。 +// 如果需要,这可以异步完成。 +let jwksData = try Data( + contentsOf: URL(string: "https://appleid.apple.com/auth/keys")! +) + +// 对下载的JSON进行解码。 +let jwks = try JSONDecoder().decode(JWKS.self, from: jwksData) + +// 创建签名者并添加JWKS。 +try app.jwt.signers.use(jwks: jwks) +``` + +你现在可以将JWTs从Apple传递到`verify`方法。JWT头中的密钥标识符(`kid`)将被用来自动选择正确的密钥进行验证。 + +截至目前,JWK只支持RSA密钥。此外,JWT发行者可能会轮换他们的JWKS,意味着你需要偶尔重新下载。请参阅Vapor支持的JWT [Vendors](#vendors)列表,了解能自动做到这一点的API。 + +## 供应商 + +Vapor提供API来处理以下流行的发行商的JWTs。 + +### 苹果 + +首先,配置你的苹果应用标识符。 + +```swift +// 配置苹果应用程序的标识符。 +app.jwt.apple.applicationIdentifier = "..." +``` + +然后,使用`req.jwt.apple`助手来获取和验证苹果JWT。 + +```swift +//从授权头中获取并验证苹果JWT。 +app.get("apple") { req -> EventLoopFuture in + req.jwt.apple.verify().map { token in + print(token) // AppleIdentityToken + return .ok + } +} + +// 或 + +app.get("apple") { req async throws -> HTTPStatus in + let token = try await req.jwt.apple.verify() + print(token) // AppleIdentityToken + return .ok +} +``` + +### 谷歌 + +首先,配置你的谷歌应用标识符和G套件域名。 + +```swift +// 配置谷歌应用程序标识符和域名。 +app.jwt.google.applicationIdentifier = "..." +app.jwt.google.gSuiteDomainName = "..." +``` + +然后,使用`req.jwt.google`帮助器来获取和验证Google JWT。 + +```swift +// 从授权头中获取并验证Google JWT。 +app.get("google") { req -> EventLoopFuture in + req.jwt.google.verify().map { token in + print(token) // GoogleIdentityToken + return .ok + } +} + +// 或 + +app.get("google") { req async throws -> HTTPStatus in + let token = try await req.jwt.google.verify() + print(token) // GoogleIdentityToken + return .ok +} +``` + +### Microsoft + +首先,配置你的Microsoft应用程序标识符。 + +```swift +// 配置微软应用程序标识符。 +app.jwt.microsoft.applicationIdentifier = "..." +``` + +然后,使用`req.jwt.microsoft`帮助器来获取和验证Microsoft的JWT。 + +```swift +//从授权头中获取并验证微软JWT。 +app.get("microsoft") { req -> EventLoopFuture in + req.jwt.microsoft.verify().map { token in + print(token) // MicrosoftIdentityToken + return .ok + } +} + +// 或 + +app.get("microsoft") { req async throws -> HTTPStatus in + let token = try await req.jwt.microsoft.verify() + print(token) // MicrosoftIdentityToken + return .ok +} +``` diff --git a/4.0/docs/security/passwords.md b/4.0/docs/security/passwords.md index 3b9779d..3b89e2b 100644 --- a/4.0/docs/security/passwords.md +++ b/4.0/docs/security/passwords.md @@ -1,10 +1,10 @@ -# Passwords +# 密码 -Vapor includes a password hashing API to help you store and verify passwords securely. This API is configurable based on environment and supports asynchronous hashing. +Vapor包括一个密码散列API,帮助你安全地存储和验证密码。该API可根据环境进行配置,并支持异步散列。 -## Configuration +## 配置 -To configure the Application's password hasher, use `app.passwords`. +要配置应用程序的密码散列器,请使用`app.passwords`。 ```swift import Vapor @@ -14,21 +14,21 @@ app.passwords.use(...) ### Bcrypt -To use Vapor's [Bcrypt API](crypto.md#bcrypt) for password hashing, specify `.bcrypt`. This is the default. +要使用Vapor的[Bcrypt API](crypto.md#bcrypt)进行密码散列,指定`.bcrypt`。这是默认的。 ```swift app.passwords.use(.bcrypt) ``` -Bcrypt will use a cost of 12 unless otherwise specified. You can configure this by passing the `cost` parameter. +除非另有规定,否则Bcrypt将使用12的成本。你可以通过传递`cost`参数来配置。 ```swift app.passwords.use(.bcrypt(cost: 8)) ``` -### Plaintext +### 明文 -Vapor includes an insecure password hasher that stores and verifies passwords as plaintext. This should not be used in production but can be useful for testing. +Vapor包括一个不安全的密码收集器,它以明文形式存储和验证密码。这不应该在生产中使用,但对测试是有用的。 ```swift switch app.environment { @@ -40,41 +40,49 @@ default: break ## Hashing -To hash passwords, use the `password` helper available on `Request`. +要对密码进行散列,请使用`Request`上的`password`助手。 ```swift let digest = try req.password.hash("vapor") ``` -Password digests can be verified against the plaintext password using the `verify` method. +密码摘要可以用`verify`方法与明文密码进行验证。 ```swift let bool = try req.password.verify("vapor", created: digest) ``` -The same API is available on `Application` for use during boot. +同样的API在`Application`上也可以使用,以便在启动时使用。 ```swift let digest = try app.password.hash("vapor") ``` -### Async +### 异步 -Password hashing algorithms are designed to be slow and CPU intensive. Because of this, you may want to avoid blocking the event loop while hashing passwords. Vapor provides an asynchronous password hashing API that dispatches hashing to a background thread pool. To use the asynchronous API, use the `async` property on a password hasher. +密码散列算法被设计成慢速和CPU密集型。正因为如此,你可能想避免在密码散列时阻塞事件循环。Vapor提供了一个异步密码散列API,将散列分配到后台线程池。要使用异步API,请使用密码散列器的`async`属性。 ```swift req.password.async.hash("vapor").map { digest in - // Handle digest. + // 处理摘要。 } + +// 或 + +let digest = try await req.password.async.hash("vapor") ``` -Verifying digests works similarly: +验证摘要的工作原理与此类似: ```swift req.password.async.verify("vapor", created: digest).map { bool in - // Handle result. + // 处理结果。 } + +// 或 + +let result = try await req.password.async.verify("vapor", created: digest) ``` -Calculating hashes on background threads can free your application's event loops up to handle more incoming requests. +在后台线程上计算哈希值可以释放你的应用程序的事件循环,以处理更多传入的请求。 diff --git a/4.0/mkdocs.yml b/4.0/mkdocs.yml index 9a76b9b..8ca3935 100644 --- a/4.0/mkdocs.yml +++ b/4.0/mkdocs.yml @@ -44,7 +44,9 @@ nav: - "Xcode": "start/xcode.md" - "入门": - "路由": "basics/routing.md" - - "控制器": "basics/controllers.md" +# TODO: 提高质量 +# 多数情况下只是一个代码样本,没有什么解释。 +# - "控制器": "basics/controllers.md" - "内容": "basics/content.md" - "客户端": "basics/client.md" - "验证": "basics/validation.md" @@ -60,19 +62,28 @@ nav: - "查询": "fluent/query.md" - "模式": "fluent/schema.md" - "高级": "fluent/advanced.md" +- "Leaf": + - "入门": "leaf/getting-started.md" + - "概述": "leaf/overview.md" + - "自定义Tags": "leaf/custom-tags.md" +- "Redis": + - "概述": "redis/overview.md" + - "会话": "redis/sessions.md" - "进阶": - "中间件": "advanced/middleware.md" - "测试": "advanced/testing.md" - - "服务": "advanced/server.md" + - "服务器": "advanced/server.md" - "命令": "advanced/commands.md" - "队列": "advanced/queues.md" - "WebSockets": "advanced/websockets.md" - - "Sessions": "advanced/sessions.md" - - "Services": "advanced/services.md" + - "会话": "advanced/sessions.md" + - "服务": "advanced/services.md" + - "APNs": "advanced/apns.md" - "安全": - "认证": "security/authentication.md" - "加密": "security/crypto.md" - "密码": "security/passwords.md" + - "JWT": "security/jwt.md" - "部署": - "DigitalOcean": "deploy/digital-ocean.md" - "Heroku": "deploy/heroku.md"