├── .github
└── workflows
│ ├── ci.yml
│ ├── deploy.yml
│ └── projectboard.yml
├── .gitignore
├── 4.0
├── docs
│ ├── advanced
│ │ ├── commands.md
│ │ ├── middleware.md
│ │ ├── queues.md
│ │ ├── server.md
│ │ ├── services.md
│ │ ├── sessions.md
│ │ ├── testing.md
│ │ └── websockets.md
│ ├── assets
│ │ ├── favicon.png
│ │ └── logo.png
│ ├── basics
│ │ ├── async.md
│ │ ├── client.md
│ │ ├── content.md
│ │ ├── controllers.md
│ │ ├── environment.md
│ │ ├── errors.md
│ │ ├── logging.md
│ │ ├── routing.md
│ │ └── validation.md
│ ├── deploy
│ │ ├── digital-ocean.md
│ │ ├── docker.md
│ │ ├── heroku.md
│ │ ├── nginx.md
│ │ └── supervisor.md
│ ├── fluent
│ │ ├── advanced.md
│ │ ├── migration.md
│ │ ├── model.md
│ │ ├── overview.md
│ │ ├── query.md
│ │ ├── relations.md
│ │ └── schema.md
│ ├── images
│ │ ├── digital-ocean-create-droplet.png
│ │ ├── digital-ocean-distributions-ubuntu-18.png
│ │ ├── digital-ocean-droplet-list.png
│ │ └── swift-download-ubuntu-18-copy-link.png
│ ├── index.md
│ ├── install
│ │ ├── linux.md
│ │ └── macos.md
│ ├── security
│ │ ├── authentication.md
│ │ ├── crypto.md
│ │ └── passwords.md
│ ├── start
│ │ ├── folder-structure.md
│ │ ├── hello-world.md
│ │ ├── spm.md
│ │ └── xcode.md
│ └── version
│ │ ├── 4_0.md
│ │ └── upgrading.md
├── mkdocs.yml
└── theme
│ ├── 404.html
│ ├── main.html
│ ├── partials
│ └── language
│ │ └── zh.html
│ ├── scripts
│ └── carbon.js
│ └── styles
│ └── carbon.css
├── CONTRIBUTING.md
├── LICENSE
├── README.md
└── stack.yaml
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: Check cloudformation linting and build docs
2 |
3 | on:
4 | pull_request:
5 | branches:
6 | - main
7 |
8 | jobs:
9 | build:
10 | name: Build
11 | runs-on: ubuntu-latest
12 | steps:
13 | - name: Install mkdocs
14 | run: |
15 | pip3 install mkdocs mkdocs-material
16 | - name: Checkout
17 | uses: actions/checkout@v2
18 | - name: Setup cloudformation linter
19 | uses: ScottBrenner/cfn-lint-action@v2.2.1
20 | - name: Run cloudformation lint
21 | run: cfn-lint -t stack.yaml
22 | - name: Build docs
23 | run: |
24 | cd 4.0;
25 | mkdocs build;
26 | cd ..;
27 | rm -rf site
28 | mkdir -p site;
29 | mv 4.0/site site/4.0;
30 | echo "" > site/index.html;
--------------------------------------------------------------------------------
/.github/workflows/deploy.yml:
--------------------------------------------------------------------------------
1 | name: Build and deploy blog.vapor.codes
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | jobs:
9 | build:
10 | name: Build and deploy
11 | runs-on: ubuntu-latest
12 | steps:
13 | - name: Install mkdocs
14 | run: |
15 | pip3 install mkdocs mkdocs-material
16 | - name: Checkout
17 | uses: actions/checkout@v2
18 | - name: Build Site
19 | run: |
20 | cd 4.0;
21 | mkdocs build;
22 | cd ..;
23 | rm -rf site
24 | mkdir -p site;
25 | mv 4.0/site site/4.0;
26 | echo "" > site/index.html;
27 | - name: Configure AWS credentials
28 | id: cred
29 | uses: aws-actions/configure-aws-credentials@v1
30 | with:
31 | aws-access-key-id: ${{ secrets.DOCS_DEPLOYER_AWS_ACCESS_KEY_ID }}
32 | aws-secret-access-key: ${{ secrets.DOCS_DEPLOYER_AWS_SECRET_ACCESS_KEY }}
33 | aws-region: 'eu-west-2'
34 | - name: Deploy to AWS Cloudformation
35 | id: clouddeploy
36 | uses: aws-actions/aws-cloudformation-github-deploy@v1.0.3
37 | with:
38 | name: vapor-docs-cn-stack
39 | template: stack.yaml
40 | no-fail-on-empty-changeset: "1"
41 | parameter-overrides: >-
42 | DomainName=cn.docs.vapor.codes,
43 | S3BucketName=vapor-docs-cn-site,
44 | AcmCertificateArn=${{ secrets.CERTIFICATE_ARN }}
45 | if: steps.cred.outcome == 'success'
46 | - name: Deploy to S3
47 | id: s3deploy
48 | uses: jakejarvis/s3-sync-action@master
49 | with:
50 | args: --acl public-read --follow-symlinks --delete
51 | env:
52 | AWS_S3_BUCKET: 'vapor-docs-cn-site'
53 | AWS_ACCESS_KEY_ID: ${{ secrets.DOCS_DEPLOYER_AWS_ACCESS_KEY_ID }}
54 | AWS_SECRET_ACCESS_KEY: ${{ secrets.DOCS_DEPLOYER_AWS_SECRET_ACCESS_KEY }}
55 | AWS_REGION: 'eu-west-2'
56 | SOURCE_DIR: 'site'
57 | if: steps.clouddeploy.outcome == 'success'
58 | - name: Invalidate CloudFront
59 | uses: awact/cloudfront-action@master
60 | env:
61 | SOURCE_PATH: '/*'
62 | AWS_REGION: 'eu-west-2'
63 | AWS_ACCESS_KEY_ID: ${{ secrets.DOCS_DEPLOYER_AWS_ACCESS_KEY_ID }}
64 | AWS_SECRET_ACCESS_KEY: ${{ secrets.DOCS_DEPLOYER_AWS_SECRET_ACCESS_KEY }}
65 | DISTRIBUTION_ID: ${{ secrets.CN_DOCS_DISTRIBUTION_ID }}
66 |
67 |
--------------------------------------------------------------------------------
/.github/workflows/projectboard.yml:
--------------------------------------------------------------------------------
1 | name: issue-to-project-board-workflow
2 | on:
3 | # Trigger when an issue gets labeled or deleted
4 | issues:
5 | types: [reopened, closed, labeled, unlabeled, assigned, unassigned]
6 |
7 | jobs:
8 | setup_matrix_input:
9 | runs-on: ubuntu-latest
10 |
11 | steps:
12 | - id: set-matrix
13 | run: |
14 | output=$(curl ${{ github.event.issue.url }}/labels | jq '.[] | .name') || output=""
15 |
16 | echo '======================'
17 | echo 'Process incoming data'
18 | echo '======================'
19 | json=$(echo $output | sed 's/"\s"/","/g')
20 | echo $json
21 | echo "::set-output name=matrix::$(echo $json)"
22 | outputs:
23 | issueTags: ${{ steps.set-matrix.outputs.matrix }}
24 |
25 | Manage_project_issues:
26 | needs: setup_matrix_input
27 | uses: vapor/ci/.github/workflows/issues-to-project-board.yml@main
28 | with:
29 | labelsJson: ${{ needs.setup_matrix_input.outputs.issueTags }}
30 | secrets:
31 | PROJECT_BOARD_AUTOMATION_PAT: "${{ secrets.PROJECT_BOARD_AUTOMATION_PAT }}"
32 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | site
2 | .DS_Store
3 | .idea
4 |
5 |
--------------------------------------------------------------------------------
/4.0/docs/advanced/commands.md:
--------------------------------------------------------------------------------
1 | # 指令
2 |
3 | Vapor 的 Command API 允许你打造自定义命令行函数并且与终端进行交互。Vapor的默认指令,例如 `serve`, `routes` 和 `migrate`, 都是通过这个 Api 实现的。
4 |
5 | ## 默认指令
6 |
7 | 通过 `--help` 选项你可以了解更多 Vapor 的默认指令。
8 |
9 | ```sh
10 | vapor run --help
11 | ```
12 |
13 | 你同样可以使用 `--help` 在特定的指令上以查看这个指令接受的参数和选项。
14 |
15 | ```sh
16 | vapor run serve --help
17 | ```
18 |
19 | ### Xcode
20 |
21 | 你可以通过加入参数到 Xcode 的 `Run` scheme 以运行指令。通过一下三步做到这点:
22 |
23 | - 选择 `Run` scheme (在 运行/停止 按钮的右边)
24 | - 选择 "Edit Scheme"
25 | - 选择 "Run"
26 | - 选择 "Arguments" 这一栏
27 | - 将指令的名词添加到 "Arguments Passed On Launch" (例如, `serve`)
28 |
29 | ## 自定义指令
30 |
31 | 你可以通过一个符合 `Command` 协议的类型创建你自己的命令
32 |
33 | ```swift
34 | import Vapor
35 |
36 | struct HelloCommand: Command {
37 | ...
38 | }
39 | ```
40 |
41 | 将自定义指令加入到 `app.commands` 将允许你使用这个指令通过 `vapor run`。
42 |
43 | ```swift
44 | app.commands.use(HelloCommand(), as: "hello")
45 | ```
46 |
47 | 为了符合 `Command` ,你必须实现 `run` 方法。这个方法需要你定义一个 `Signature` 。你还需要提供一个默认的帮助文本。
48 |
49 | ```swift
50 | import Vapor
51 |
52 | struct HelloCommand: Command {
53 | struct Signature: CommandSignature { }
54 |
55 | var help: String {
56 | "Says hello"
57 | }
58 |
59 | func run(using context: CommandContext, signature: Signature) throws {
60 | context.console.print("Hello, world!")
61 | }
62 | }
63 | ```
64 |
65 | 这个简单的指令例子没有参数或者选项,所以让 signature 为空。
66 |
67 | 你可以通过 context 访问当前的 console(控制台)。console 有许多有帮助的方法来提示用户输入,格式化输出,还有更多。
68 |
69 | ```swift
70 | let name = context.console.ask("What is your \("name", color: .blue)?")
71 | context.console.print("Hello, \(name) 👋")
72 | ```
73 |
74 | 通过运行你的命令来测试:
75 |
76 | ```sh
77 | vapor run hello
78 | ```
79 |
80 | ### Cowsay
81 |
82 | 看一下这个著名的 [`cowsay`](https://en.wikipedia.org/wiki/Cowsay) 指令的重制版。它将作为 `@Argument` 和 `@Option` 使用的一个例子。
83 |
84 | ```swift
85 | import Vapor
86 |
87 | struct Cowsay: Command {
88 | struct Signature: CommandSignature {
89 | @Argument(name: "message")
90 | var message: String
91 |
92 | @Option(name: "eyes", short: "e")
93 | var eyes: String?
94 |
95 | @Option(name: "tongue", short: "t")
96 | var tongue: String?
97 | }
98 |
99 | var help: String {
100 | "Generates ASCII picture of a cow with a message."
101 | }
102 |
103 | func run(using context: CommandContext, signature: Signature) throws {
104 | let eyes = signature.eyes ?? "oo"
105 | let tongue = signature.tongue ?? " "
106 | let cow = #"""
107 | < $M >
108 | \ ^__^
109 | \ ($E)\_______
110 | (__)\ )\/\
111 | $T ||----w |
112 | || ||
113 | """#.replacingOccurrences(of: "$M", with: signature.message)
114 | .replacingOccurrences(of: "$E", with: eyes)
115 | .replacingOccurrences(of: "$T", with: tongue)
116 | context.console.print(cow)
117 | }
118 | }
119 | ```
120 |
121 | 尝试将这个指令加入到程序然后运行它。
122 |
123 | ```swift
124 | app.commands.use(Cowsay(), as: "cowsay")
125 | ```
126 |
127 | ```sh
128 | vapor run cowsay sup --eyes ^^ --tongue "U "
129 | ```
130 |
--------------------------------------------------------------------------------
/4.0/docs/advanced/middleware.md:
--------------------------------------------------------------------------------
1 | # Middleware
2 |
3 | Middleware 是 client 和路由处理程序间的一个逻辑链。它允许你在传入请求到达路由处理程序之前对传入请求执行操作,并且在输出响应到达 client 之前对传出响应执行操作。
4 |
5 | ## Configuration
6 |
7 | 可以使用 `app.middleware` 在 `configure(_:)` 中全局(针对每条路由)注册 Middleware。
8 |
9 | ```swift
10 | app.middleware.use(MyMiddleware())
11 | ```
12 |
13 | 你也可以通过路由组的方式给单个路由添加 Middleware。
14 |
15 | ```swift
16 | let group = app.grouped(MyMiddleware())
17 | group.get("foo") { req in
18 | // 该请求通过 MyMiddleware 传递。
19 | }
20 | ```
21 |
22 | ### Order
23 |
24 | Middleware 的添加顺序非常重要。进入应用程序的请求将按照在 middleware 添加的顺序依次执行。
25 | 离开应用程序的响应将以相反的顺序通过 Middleware 返回。特定的路由 Middleware 始终在应用程序 Middleware 之后运行。
26 |
27 | 请看以下示例:
28 |
29 | ```swift
30 | app.middleware.use(MiddlewareA())
31 | app.middleware.use(MiddlewareB())
32 |
33 | app.group(MiddlewareC()) {
34 | $0.get("hello") { req in
35 | "Hello, middleware."
36 | }
37 | }
38 | ```
39 |
40 | `GET /hello` 这个请求将按照以下顺序访问 Middleware:
41 |
42 | ```
43 | Request → A → B → C → Handler → C → B → A → Response
44 | ```
45 |
46 | ## File Middleware
47 |
48 | `FileMiddleware` 允许从项目的 Public 文件夹向 client 提供资源。你可以在这里存放 css 或者位图图片等静态文件。
49 |
50 | ```swif
51 | let file = FileMiddleware(publicDirectory: app.directory.publicDirectory)
52 | app.middleware.use(file)
53 | ```
54 |
55 | 一旦注册 `FileMiddleware`,比如 `Public/images/logo.png` 的文件可以在 Leaf 模板通过 `
` 方式引用。
56 |
57 |
58 | ## CORS Middleware
59 |
60 | 跨域资源共享(Cross-origin resource sharing,缩写:CORS),用于让网页的受限资源能够被其他域名的页面访问的一种机制。通过该机制,页面能够自由地使用不同源(英語:cross-origin)的图片、样式、脚本、iframes 以及视频。Vapor 内置的 REST API 需要 CORS 策略,以便将请求安全地返回到 Web 浏览器。
61 |
62 | 配置示例如下所示:
63 |
64 | ```swift
65 | let corsConfiguration = CORSMiddleware.Configuration(
66 | allowedOrigin: .all,
67 | allowedMethods: [.GET, .POST, .PUT, .OPTIONS, .DELETE, .PATCH],
68 | allowedHeaders: [.accept, .authorization, .contentType, .origin, .xRequestedWith, .userAgent, .accessControlAllowOrigin]
69 | )
70 | let cors = CORSMiddleware(configuration: corsConfiguration)
71 | let error = ErrorMiddleware.default(environment: app.environment)
72 | // 清除现有的 middleware。
73 | app.middleware = .init()
74 | app.middleware.use(cors)
75 | app.middleware.use(error)
76 | ```
77 |
78 | 由于抛出的错误会立即返回给客户端,因此必须在 `ErrorMiddleware` 之前注册 `CORSMiddleware`。否则,将返回不带 CORS 标头的 HTTP 错误响应,且浏览器无法读取该错误响应。
79 |
--------------------------------------------------------------------------------
/4.0/docs/advanced/queues.md:
--------------------------------------------------------------------------------
1 | # Queues
2 |
3 | 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.
4 |
5 | Some of the tasks this package works well for:
6 |
7 | - Sending emails outside of the main request thread
8 | - Performing complex or long-running database operations
9 | - Ensuring job integrity and resilience
10 | - Speeding up response time by delaying non-critical processing
11 | - Scheduling jobs to occur at a specific time
12 |
13 | This package is similar to [Ruby Sidekiq](https://github.com/mperham/sidekiq). It provides the following features:
14 |
15 | - Safe handling of `SIGTERM` and `SIGINT` signals sent by hosting providers to indicate a shutdown, restart, or new deploy.
16 | - 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.
17 | - Implements the reliable queue process to help with unexpected failures.
18 | - Includes a `maxRetryCount` feature that will repeat the job until it succeeds up until a specified count.
19 | - Uses NIO to utilize all available cores and EventLoops for jobs.
20 | - Allows users to schedule repeating tasks
21 |
22 | Queues currently has one officially supported driver which interfaces with the main protocol:
23 |
24 | - [QueuesRedisDriver](https://github.com/vapor/queues-redis-driver)
25 |
26 | Queues also has community-based drivers:
27 | - [QueuesMongoDriver](https://github.com/vapor-community/queues-mongo-driver)
28 | - [QueuesFluentDriver](https://github.com/m-barthelemy/vapor-queues-fluent-driver)
29 |
30 | !!! tip
31 | You should not install the `vapor/queues` package directly unless you are building a new driver. Install one of the driver packages instead.
32 |
33 | ## Getting Started
34 |
35 | Let's take a look at how you can get started using Queues.
36 |
37 | ### Package
38 |
39 | 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.
40 |
41 | ```swift
42 | // swift-tools-version:5.2
43 | import PackageDescription
44 |
45 | let package = Package(
46 | name: "MyApp",
47 | dependencies: [
48 | /// Any other dependencies ...
49 | .package(url: "https://github.com/vapor/queues-redis-driver.git", from: "1.0.0"),
50 | ],
51 | targets: [
52 | .target(name: "App", dependencies: [
53 | .product(name: "QueuesRedisDriver", package: "queues-redis-driver")
54 | ]),
55 | .target(name: "Run", dependencies: [.target(name: "App")]),
56 | .testTarget(name: "AppTests", dependencies: [.target(name: "App")]),
57 | ]
58 | )
59 | ```
60 |
61 | 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.
62 |
63 | ### Config
64 |
65 | The next step is to configure Queues in `configure.swift`. We'll use the Redis library as an example:
66 |
67 | ```swift
68 | try app.queues.use(.redis(url: "redis://127.0.0.1:6379"))
69 | ```
70 |
71 | ### Registering a `Job`
72 |
73 | After modeling a job you must add it to your configuration section like this:
74 |
75 | ```swift
76 | //Register jobs
77 | let emailJob = EmailJob()
78 | app.queues.add(emailJob)
79 | ```
80 |
81 | ### Running Workers as Processes
82 |
83 | 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`.
84 |
85 | !!! tip
86 | 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`
87 |
88 | ### Running Workers in-process
89 |
90 | 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`:
91 |
92 | ```swift
93 | try app.queues.startInProcessJobs(on: .default)
94 | ```
95 |
96 | To run scheduled jobs in process, call the following method:
97 |
98 | ```swift
99 | try app.queues.startScheduledJobs()
100 | ```
101 |
102 | !!! warning
103 | If you don't start the queue worker either via command line or the in-process worker the jobs will not dispatch.
104 |
105 | ## The `Job` Protocol
106 |
107 | Jobs are defined by the `Job` protocol.
108 |
109 | ### Modeling a `Job` object:
110 | ```swift
111 | import Vapor
112 | import Foundation
113 | import Queues
114 |
115 | struct Email: Codable {
116 | let to: String
117 | let message: String
118 | }
119 |
120 | struct EmailJob: Job {
121 | typealias Payload = Email
122 |
123 | func dequeue(_ context: QueueContext, _ payload: Email) -> EventLoopFuture {
124 | // This is where you would send the email
125 | return context.eventLoop.future()
126 | }
127 |
128 | func error(_ context: QueueContext, _ error: Error, _ payload: Email) -> EventLoopFuture {
129 | // If you don't want to handle errors you can simply return a future. You can also omit this function entirely.
130 | return context.eventLoop.future()
131 | }
132 | }
133 | ```
134 |
135 | !!! tip
136 | Don't forget to follow the instructions in **Getting Started** to add this job to your configuration file.
137 |
138 | ## Dispatching Jobs
139 |
140 | 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:
141 |
142 | ```swift
143 | app.get("email") { req -> EventLoopFuture in
144 | return req
145 | .queue
146 | .dispatch(
147 | EmailJob.self,
148 | .init(to: "email@email.com", message: "message")
149 | ).map { "done" }
150 | }
151 | ```
152 |
153 | ### Setting `maxRetryCount`
154 |
155 | Jobs will automatically retry themselves upon error if you specify a `maxRetryCount`. For example:
156 |
157 | ```swift
158 | app.get("email") { req -> EventLoopFuture in
159 | return req
160 | .queue
161 | .dispatch(
162 | EmailJob.self,
163 | .init(to: "email@email.com", message: "message"),
164 | maxRetryCount: 3
165 | ).map { "done" }
166 | }
167 | ```
168 |
169 | ### Specifying a delay
170 |
171 | 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`:
172 |
173 | ```swift
174 | app.get("email") { req -> EventLoopFuture in
175 | let futureDate = Date(timeIntervalSinceNow: 60 * 60 * 24) // One day
176 | return req
177 | .queue
178 | .dispatch(
179 | EmailJob.self,
180 | .init(to: "email@email.com", message: "message"),
181 | maxRetryCount: 3,
182 | delayUntil: futureDate
183 | ).map { "done" }
184 | }
185 | ```
186 |
187 | If a job is dequeued before its delay parameter, the job will be re-queued by the driver.
188 |
189 | ### Specify a priority
190 |
191 | 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.
192 |
193 | Start by extending `QueueName `:
194 |
195 | ```swift
196 | extension QueueName {
197 | static let emails = QueueName(string: "emails")
198 | }
199 | ```
200 |
201 | Then, specify the queue type when you retrieve the `jobs` object:
202 |
203 | ```swift
204 | app.get("email") { req -> EventLoopFuture in
205 | let futureDate = Date(timeIntervalSinceNow: 60 * 60 * 24) // One day
206 | return req
207 | .queues(.emails)
208 | .dispatch(
209 | EmailJob.self,
210 | .init(to: "email@email.com", message: "message"),
211 | maxRetryCount: 3,
212 | delayUntil: futureDate
213 | ).map { "done" }
214 | }
215 | ```
216 |
217 | 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.
218 |
219 | ## Scheduling Jobs
220 |
221 | The Queues package also allows you to schedule jobs to occur at certain points in time.
222 |
223 | ### Starting the scheduler worker
224 | The scheduler requires a separate worker process to be running, similar to the queue worker. You can start the worker by running this command:
225 |
226 | ```sh
227 | swift run Run queues --scheduled
228 | ```
229 |
230 | !!! tip
231 | 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`
232 |
233 | ### Creating a `ScheduledJob`
234 | To being, start by creating a new `ScheduledJob`:
235 |
236 | ```swift
237 | import Vapor
238 | import Jobs
239 |
240 | struct CleanupJob: ScheduledJob {
241 | // Add extra services here via dependency injection, if you need them.
242 |
243 | func run(context: QueueContext) -> EventLoopFuture {
244 | // Do some work here, perhaps queue up another job.
245 | return context.eventLoop.makeSucceededFuture(())
246 | }
247 | }
248 | ```
249 |
250 | Then, in your configure code, register the scheduled job:
251 |
252 | ```swift
253 | app.queues.schedule(CleanupJob())
254 | .yearly()
255 | .in(.may)
256 | .on(23)
257 | .at(.noon)
258 | ```
259 |
260 | The job in the example above will be run every year on May 23rd at 12:00 PM.
261 |
262 | !!! tip
263 | The Scheduler takes the timezone of your server.
264 |
265 | ### Available builder methods
266 | 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:
267 |
268 | | Helper Function | Available Modifiers | Description |
269 | |-----------------|---------------------------------------|--------------------------------------------------------------------------------|
270 | | `yearly()` | `in(_ month: Month) -> Monthly` | The month to run the job in. Returns a `Monthly` object for further building. |
271 | | `monthly()` | `on(_ day: Day) -> Daily` | The day to run the job in. Returns a `Daily` object for further building. |
272 | | `weekly()` | `on(_ weekday: Weekday) -> Daily` | The day of the week to run the job on. Returns a `Daily` object. |
273 | | `daily()` | `at(_ time: Time)` | The time to run the job on. Final method in the chain. |
274 | | | `at(_ hour: Hour24, _ minute: Minute)`| The hour and minute to run the job on. Final method in the chain. |
275 | | | `at(_ hour: Hour12, _ minute: Minute, _ period: HourPeriod)` | The hour, minute, and period to run the job on. Final method of the chain |
276 | | `hourly()` | `at(_ minute: Minute)` | The minute to run the job at. Final method of the chain. |
277 |
278 | ### Available helpers
279 | Queues ships with some helpers enums to make scheduling easier:
280 |
281 | | Helper Function | Available Helper Enum |
282 | |-----------------|---------------------------------------|
283 | | `yearly()` | `.january`, `.february`, `.march`, ...|
284 | | `monthly()` | `.first`, `.last`, `.exact(1)` |
285 | | `weekly()` | `.sunday`, `.monday`, `.tuesday`, ... |
286 | | `daily()` | `.midnight`, `.noon` |
287 |
288 | To use the helper enum, call in to the appropriate modifier on the helper function and pass the value. For example:
289 |
290 | ```swift
291 | // Every year in January
292 | .yearly().in(.january)
293 |
294 | // Every month on the first day
295 | .monthly().on(.first)
296 |
297 | // Every week on Sunday
298 | .weekly().on(.sunday)
299 |
300 | // Every day at midnight
301 | .daily().at(.midnight)
302 | ```
303 |
--------------------------------------------------------------------------------
/4.0/docs/advanced/server.md:
--------------------------------------------------------------------------------
1 | # Server
2 |
3 | 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).
4 |
5 | ## Configuration
6 |
7 | Vapor's default HTTP server can be configured via `app.http.server`.
8 |
9 | ```swift
10 | // Only support HTTP/2
11 | app.http.server.configuration.supportVersions = [.two]
12 | ```
13 |
14 | The HTTP server supports several configuration options.
15 |
16 | ### Hostname
17 |
18 | The hostname controls which address the server will accept new connections on. The default is `127.0.0.1`.
19 |
20 | ```swift
21 | // Configure custom hostname.
22 | app.http.server.configuration.hostname = "dev.local"
23 | ```
24 |
25 | 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(...)`.
26 |
27 | ```sh
28 | # Override configured hostname.
29 | vapor run serve --hostname dev.local
30 | ```
31 |
32 | ### Port
33 |
34 | The port option controls which port at the specified address the server will accept new connections on. The default is `8080`.
35 |
36 | ```swift
37 | // Configure custom port.
38 | app.http.server.configuration.port = 1337
39 | ```
40 |
41 | !!! info
42 | `sudo` may be required for binding to ports less than `1024`. Ports greater than `65535` are not supported.
43 |
44 |
45 | 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(...)`.
46 |
47 | ```sh
48 | # Override configured port.
49 | vapor run serve --port 1337
50 | ```
51 |
52 | ### Backlog
53 |
54 | The `backlog` parameter defines the maximum length for the queue of pending connections. The default is `256`.
55 |
56 | ```swift
57 | // Configure custom backlog.
58 | app.http.server.configuration.backlog = 128
59 | ```
60 |
61 | ### Reuse Address
62 |
63 | The `reuseAddress` parameter allows for reuse of local addresses. Defaults to `true`.
64 |
65 | ```swift
66 | // Disable address reuse.
67 | app.http.server.configuration.reuseAddress = false
68 | ```
69 |
70 | ### TCP No Delay
71 |
72 | Enabling the `tcpNoDelay` parameter will attempt to minimize TCP packet delay. Defaults to `true`.
73 |
74 | ```swift
75 | // Minimize packet delay.
76 | app.http.server.configuration.tcpNoDelay = true
77 | ```
78 |
79 | ### Response Compression
80 |
81 | The `responseCompression` parameter controls HTTP response compression using gzip. The default is `.disabled`.
82 |
83 | ```swift
84 | // Enable HTTP response compression.
85 | app.http.server.configuration.responseCompression = .enabled
86 | ```
87 |
88 | To specify an initial buffer capacity, use the `initialByteBufferCapacity` parameter.
89 |
90 | ```swift
91 | .enabled(initialByteBufferCapacity: 1024)
92 | ```
93 |
94 | ### Request Decompression
95 |
96 | The `requestDecompression` parameter controls HTTP request decompression using gzip. The default is `.disabled`.
97 |
98 | ```swift
99 | // Enable HTTP request decompression.
100 | app.http.server.configuration.requestDecompression = .enabled
101 | ```
102 |
103 | To specify a decompression limit, use the `limit` parameter. The default is `.ratio(10)`.
104 |
105 | ```swift
106 | // No decompression size limit
107 | .enabled(limit: .none)
108 | ```
109 |
110 | Available options are:
111 |
112 | - `size`: Maximum decompressed size in bytes.
113 | - `ratio`: Maximum decompressed size as ratio of compressed bytes.
114 | - `none`: No size limits.
115 |
116 | Setting decompression size limits can help prevent maliciously compressed HTTP requests from using large amounts of memory.
117 |
118 | ### Pipelining
119 |
120 | The `supportPipelining` parameter enables support for HTTP request and response pipelining. The default is `false`.
121 |
122 | ```swift
123 | // Support HTTP pipelining.
124 | app.http.server.configuration.supportPipelining = true
125 | ```
126 |
127 | ### Versions
128 |
129 | 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.
130 |
131 | ```swift
132 | // Disable HTTP/1 support.
133 | app.http.server.configuration.supportVersions = [.two]
134 | ```
135 |
136 | ### TLS
137 |
138 | The `tlsConfiguration` parameter controls whether TLS (SSL) is enabled on the server. The default is `nil`.
139 |
140 | ```swift
141 | // Enable TLS.
142 | try app.http.server.configuration.tlsConfiguration = .forServer(
143 | certificateChain: [
144 | .certificate(.init(
145 | file: "/path/to/cert.pem",
146 | format: .pem
147 | ))
148 | ],
149 | privateKey: .file("/path/to/key.pem")
150 | )
151 | ```
152 |
153 | ### Name
154 |
155 | The `serverName` parameter controls the `Server` header on outgoing HTTP responses. The default is `nil`.
156 |
157 | ```swift
158 | // Add 'Server: vapor' header to responses.
159 | app.http.server.configuration.serverName = "vapor"
160 | ```
161 |
162 | ## Serve Command
163 |
164 | To start up Vapor's server, use the `serve` command. This command will run by default if no other commands are specified.
165 |
166 | ```swift
167 | vapor run serve
168 | ```
169 |
170 | The `serve` command accepts the following parameters:
171 |
172 | - `hostname` (`-H`): Overrides configured hostname.
173 | - `port` (`-p`): Overrides configured port.
174 | - `bind` (`-b`): Overrides configured hostname and port joined by `:`.
175 |
176 | An example using the `--bind` (`-b`) flag:
177 |
178 | ```swift
179 | vapor run serve -b 0.0.0.0:80
180 | ```
181 |
182 | Use `vapor run serve --help` for more information.
183 |
184 | 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.
185 |
186 | ## Manual Start
187 |
188 | Vapor's server can be started manually using `app.server`.
189 |
190 | ```swift
191 | // Start Vapor's server.
192 | try app.server.start()
193 | // Request server shutdown.
194 | app.server.shutdown()
195 | // Wait for the server to shutdown.
196 | try app.server.onShutdown.wait()
197 | ```
198 |
199 | ## Servers
200 |
201 | The server Vapor uses is configurable. By default, the built in HTTP server is used.
202 |
203 | ```swift
204 | app.servers.use(.http)
205 | ```
206 |
207 | ### Custom Server
208 |
209 | Vapor's default HTTP server can be replaced by any type conforming to `Server`.
210 |
211 | ```swift
212 | import Vapor
213 |
214 | final class MyServer: Server {
215 | ...
216 | }
217 |
218 | app.servers.use { app in
219 | MyServer()
220 | }
221 | ```
222 |
223 | Custom servers can extend `Application.Servers.Provider` for leading-dot syntax.
224 |
225 | ```swift
226 | extension Application.Servers.Provider {
227 | static var myServer: Self {
228 | .init {
229 | $0.servers.use { app in
230 | MyServer()
231 | }
232 | }
233 | }
234 | }
235 |
236 | app.servers.use(.myServer)
237 | ```
238 |
--------------------------------------------------------------------------------
/4.0/docs/advanced/services.md:
--------------------------------------------------------------------------------
1 | # Services
2 |
3 | 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.
4 |
5 | ## Read Only
6 |
7 | The simplest type of service is read-only. These services consist of computed variables or methods added to either application or request.
8 |
9 | ```swift
10 | import Vapor
11 |
12 | struct MyAPI {
13 | let client: Client
14 |
15 | func foos() -> EventLoopFuture<[String]> { ... }
16 | }
17 |
18 | extension Request {
19 | var myAPI: MyAPI {
20 | .init(client: self.client)
21 | }
22 | }
23 | ```
24 |
25 | 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.
26 |
27 | ```swift
28 | req.myAPI.foos()
29 | ```
30 |
31 | ## Writable
32 |
33 | 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.
34 |
35 | ```swift
36 | struct MyConfiguration {
37 | var apiKey: String
38 | }
39 | ```
40 |
41 | To use storage, you must declare a `StorageKey`.
42 |
43 | ```swift
44 | struct MyConfigurationKey: StorageKey {
45 | typealias Value = MyConfiguration
46 | }
47 | ```
48 |
49 | 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.
50 |
51 | Finally, add an extension to `Application` for getting and setting the `MyConfiguration` struct.
52 |
53 | ```swift
54 | extension Application {
55 | var myConfiguration: MyConfiguration? {
56 | get {
57 | self.storage[MyConfigurationKey.self]
58 | }
59 | set {
60 | self.storage[MyConfigurationKey.self] = newValue
61 | }
62 | }
63 | }
64 | ```
65 |
66 | Once the extension is added, you can use `myConfiguration` like a normal property on `Application`.
67 |
68 |
69 | ```swift
70 | app.myConfiguration = .init(apiKey: ...)
71 | print(app.myConfiguration?.apiKey)
72 | ```
73 |
74 | ## Lifecycle
75 |
76 | Vapor's `Application` allows you to register lifecycle handlers. These let you hook into events such as boot and shutdown.
77 |
78 | ```swift
79 | // Prints hello during boot.
80 | struct Hello: LifecycleHandler {
81 | // Called before application boots.
82 | func willBoot(_ app: Application) throws {
83 | app.logger.info("Hello!")
84 | }
85 | }
86 |
87 | // Add lifecycle handler.
88 | app.lifecycle.use(Hello())
89 | ```
90 |
91 | ## Locks
92 |
93 | 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.
94 |
95 | ```swift
96 | struct TestKey: LockKey { }
97 |
98 | let test = app.locks.lock(for: TestKey.self)
99 | test.withLock {
100 | // Do something.
101 | }
102 | ```
103 |
104 | Each call to `lock(for:)` with the same `LockKey` will return the same lock. This method is thread-safe.
105 |
106 | For an application-wide lock, you can use `app.sync`.
107 |
108 | ```swift
109 | app.sync.withLock {
110 | // Do something.
111 | }
112 | ```
113 |
114 | ## Request
115 |
116 | 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.
117 |
118 | 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`.
119 |
120 | 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.
--------------------------------------------------------------------------------
/4.0/docs/advanced/sessions.md:
--------------------------------------------------------------------------------
1 | # Sessions
2 |
3 | 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.
4 |
5 | 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.
6 |
7 | ## Configuration
8 |
9 | To use sessions in a route, the request must pass through `SessionsMiddleware`. The easiest way to achieve this is by adding this middleware globally.
10 |
11 | ```swift
12 | app.middleware.use(app.sessions.middleware)
13 | ```
14 |
15 | If only a subset of your routes utilize sessions, you can instead add `SessionsMiddleware` to a route group.
16 |
17 | ```swift
18 | let sessions = app.grouped(app.sessions.middleware)
19 | ```
20 |
21 | 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.
22 |
23 | ```swift
24 | // Change the cookie name to "foo".
25 | app.sessions.configuration.cookieName = "foo"
26 |
27 | // Configures cookie value creation.
28 | app.sessions.configuration.cookieFactory = { sessionID in
29 | .init(string: sessionID.string, isSecure: true)
30 | }
31 | ```
32 |
33 | By default, Vapor will use `vapor_session` as the cookie name.
34 |
35 | ## Drivers
36 |
37 | Session drivers are responsible for storing and retrieving session data by identifier. You can create custom drivers by conforming to the `SessionDriver` protocol.
38 |
39 | !!! warning
40 | The session driver should be configured _before_ adding `app.sessions.middleware` to your application.
41 |
42 |
43 | ### In-Memory
44 |
45 | 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`:
46 |
47 | ```swift
48 | app.sessions.use(.memory)
49 | ```
50 |
51 | 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.
52 |
53 | ### Fluent
54 |
55 | 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.
56 |
57 | ```swift
58 | import Fluent
59 |
60 | app.sessions.use(.fluent)
61 | ```
62 |
63 | This will configure sessions to use the application's default database. To specify a specific database, pass the database's identifier.
64 |
65 | ```swift
66 | app.sessions.use(.fluent(.sqlite))
67 | ```
68 |
69 | Finally, add `SessionRecord`'s migration to your database's migrations. This will prepare your database for storing session data in the `_fluent_sessions` schema.
70 |
71 | ```swift
72 | app.migrations.add(SessionRecord.migration)
73 | ```
74 |
75 | 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.
76 |
77 | ## Session Data
78 |
79 | 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`.
80 |
81 | ```swift
82 | app.get("set", ":value") { req -> HTTPStatus in
83 | req.session.data["name"] = req.parameters.get("value")
84 | return .ok
85 | }
86 | ```
87 |
88 | Use the following request to initialize a session with the name Vapor.
89 |
90 | ```http
91 | GET /set/vapor HTTP/1.1
92 | content-length: 0
93 | ```
94 |
95 | You should receive a response similar to the following:
96 |
97 | ```http
98 | HTTP/1.1 200 OK
99 | content-length: 0
100 | set-cookie: vapor-session=123; Expires=Fri, 10 Apr 2020 21:08:09 GMT; Path=/
101 | ```
102 |
103 | 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.
104 |
105 | Add the following route handler for accessing the name value from the session.
106 |
107 | ```swift
108 | app.get("get") { req -> String in
109 | req.session.data["name"] ?? "n/a"
110 | }
111 | ```
112 |
113 | Use the following request to access this route while making sure to pass the cookie value from the previous response.
114 |
115 | ```http
116 | GET /get HTTP/1.1
117 | cookie: vapor-session=123
118 | ```
119 |
120 | 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.
121 |
122 | To end a session, use `req.session.destroy`. This will delete the data from the session driver and invalidate the session cookie.
123 |
124 | ```swift
125 | app.get("del") { req -> HTTPStatus in
126 | req.session.destroy()
127 | return .ok
128 | }
129 | ```
130 |
--------------------------------------------------------------------------------
/4.0/docs/advanced/testing.md:
--------------------------------------------------------------------------------
1 | # 测试
2 |
3 | Vapor 包含一个名为 `XCTVapor` 的模块,它提供了基于 `XCTest` 的测试帮助程序。这些测试辅助程序允许你以编程方式或通过 HTTP 服务器将测试请求发送至 Vapor 应用程序。
4 |
5 | ## 入门
6 |
7 | 要使用 `XCTVapor` 模块,请确保在你的项目 `Package.swift` 文件已添加了对应的 **testTarget**。
8 |
9 | ```swift
10 | let package = Package(
11 | ...
12 | dependencies: [
13 | .package(url: "https://github.com/vapor/vapor.git", from: "4.0.0")
14 | ],
15 | targets: [
16 | ...
17 | .testTarget(name: "AppTests", dependencies: [
18 | .target(name: "App"),
19 | .product(name: "XCTVapor", package: "vapor"),
20 | ])
21 | ]
22 | )
23 | ```
24 |
25 | 然后,在测试文件的顶部添加 `import XCTVapor`,创建继承于 `XCTestCase` 的子类来编写测试用例。
26 |
27 | ```swift
28 | import XCTVapor
29 |
30 | final class MyTests: XCTestCase {
31 | func testStub() throws {
32 | // 在这里测试。
33 | }
34 | }
35 | ```
36 |
37 | 当你的应用程序执行测试时,每个以 `test` 开头的函数都会自动运行。
38 |
39 | ### 运行测试
40 |
41 | 在使用 `Package` 方案的情况下,使用 `cmd+u` 在 Xcode 中运行测试用例。
42 | 或使用 `swift test --enable-test-discovery` 通过 CLI 进行测试。
43 |
44 | ## 可测试的应用程序
45 |
46 | 使用 `.testing` 环境初始化一个 `Application` 实例。你必须在此应用程序初始化之前,调用 `app.shutdown()`。
47 |
48 | ```swift
49 | let app = Application(.testing)
50 | defer { app.shutdown() }
51 | try configure(app)
52 | ```
53 |
54 | 将 `Application` 实例对象作为入参传到 `configure(_:)` 方法来应用你的配置,之后可以应用到任何仅测试的配置。
55 |
56 | ### 发送请求
57 |
58 | 要向你的应用程序发送一个测试请求,请使用 `test` 方法。
59 |
60 | ```swift
61 | try app.test(.GET, "hello") { res in
62 | XCTAssertEqual(res.status, .ok)
63 | XCTAssertEqual(res.body.string, "Hello, world!")
64 | }
65 | ```
66 |
67 | 前两个参数是 HTTP 方法和请求的 URL。后面的尾随闭包接受 HTTP 响应,你可以使用 `XCTAssert` 方法进行验证。
68 |
69 | 对于更复杂的请求,你可以提供一个 `beforeRequest` 闭包来修改请求头或编码内容。Vapor 的 [Content API](../basics/content.md) 可以在测试请求和响应中使用。
70 |
71 | ```swift
72 | try app.test(.POST, "todos", beforeRequest: { req in
73 | try req.content.encode(["title": "Test"])
74 | }, afterResponse: { res in
75 | XCTAssertEqual(res.status, .created)
76 | let todo = try res.content.decode(Todo.self)
77 | XCTAssertEqual(todo.title, "Test")
78 | })
79 | ```
80 |
81 | ### 可测试的方法
82 |
83 | Vapor 的测试 API 支持以编程方式并通过实时 HTTP 服务器发送测试请求。
84 | 你可以通过使用 `testable` 方法来指定你想要使用的方法。
85 |
86 | ```swift
87 | // 使用程序化测试。
88 | app.testable(method: .inMemory).test(...)
89 |
90 | // 通过一个实时的 HTTP 服务器运行测试。
91 | app.testable(method: .running).test(...)
92 | ```
93 |
94 | 默认情况下使用 `inMemory` 选项。
95 |
96 | `running` 选项支持传递一个特定的端口来使用。默认情况下使用的是 `8080`。
97 |
98 | ```swift
99 | .running(port: 8123)
100 | ```
101 |
102 | 当然,你也可以修改为其他端口进行测试。
103 |
--------------------------------------------------------------------------------
/4.0/docs/advanced/websockets.md:
--------------------------------------------------------------------------------
1 | # WebSockets
2 |
3 | [WebSockets](https://zh.wikipedia.org/wiki/WebSocket) 允许客户端和服务器之间进行双向通信。与 HTTP 的请求和响应模式不同,WebSocket 可以在两端之间发送任意数量的消息。Vapor的WebSocket API允许你创建异步处理消息的客户端和服务器。
4 |
5 | ## 服务器
6 |
7 | 你可以使用 [Routing API](../basics/routing.md) 将 WebSocket 端点添加到现有的 Vapor 应用程序中。使用 `webSocket` 的方法就像使用 `get` 或 `post` 一样。
8 |
9 | ```swift
10 | app.webSocket("echo") { req, ws in
11 | // Connected WebSocket.
12 | print(ws)
13 | }
14 | ```
15 |
16 | WebSocket 路由可以像普通路由一样由中间件进行分组和保护。
17 |
18 | 除了接受传入的 HTTP 请求之外,WebSocket 处理程序还可以接受新建立的 WebSocket 连接。有关使用此 WebSocket 发送和阅读消息的更多信息,请参考下文。
19 |
20 | ## 客户端
21 |
22 | 要连接到远程 WebSocket 端口,请使用 `WebSocket.connect` 。
23 |
24 | ```swift
25 | WebSocket.connect(to: "ws://echo.websocket.org", on: eventLoop) { ws in
26 | // Connected WebSocket.
27 | print(ws)
28 | }
29 | ```
30 |
31 | `connect` 方法返回建立连接后完成的 future。 连接后将使用新连接的 WebSocket 调用提供的闭包。有关使用 WebSocket 发送和阅读消息的更多信息,请参见下文。
32 |
33 | ## 消息
34 |
35 | `WebSocket` 类具有发送和接收消息以及侦听诸如关闭之类的方法。WebSocket 可以通过两种协议传输数据:文本以及二进制数据。文本消息为 UTF-8 字符串,而二进制数据为字节数组。
36 |
37 | ### 发送
38 |
39 | 可以使用 WebSocket 的 `send` 方法来发送消息。
40 |
41 | ```swift
42 | ws.send("Hello, world")
43 | ```
44 |
45 | 将 `String` 传递给此方法即可发送文本消息。二进制消息可以通过如下传递 `[UInt8]` 数据来发送:
46 |
47 | ```swift
48 | ws.send([1, 2, 3])
49 | ```
50 |
51 | 发送消息是异步处理,你可以向 send 方法提供一个 `EventLoopPromise`,以便在消息发送完成或发送失败时得到通知。
52 |
53 | ```swift
54 | let promise = eventLoop.makePromise(of: Void.self)
55 | ws.send(..., promise: promise)
56 | promise.futureResult.whenComplete { result in
57 | // 发送成功或失败。
58 | }
59 | ```
60 |
61 | ### 接收
62 |
63 | 接收的消息通过 `onText` 和 `onBinary` 回调进行处理。
64 |
65 | ```swift
66 | ws.onText { ws, text in
67 | // 这个方法接收的是字符串。
68 | print(text)
69 | }
70 |
71 | ws.onBinary { ws, binary in
72 | // 这个方法接收二进制数组。
73 | print(binary)
74 | }
75 | ```
76 |
77 | WebSocket 对象本身作为这些回调的第一个参数提供,以防止循环引用。接收数据后,使用此引用对 WebSocket 采取对应操作。例如,发送回复信息:
78 |
79 | ```swift
80 | // Echoes received messages.
81 | ws.onText { ws, text in
82 | ws.send(text)
83 | }
84 | ```
85 |
86 | ## 关闭
87 |
88 | 如果要关闭 WebSocket,请调用 `close` 方法。
89 |
90 | ```swift
91 | ws.close()
92 | ```
93 |
94 | 该方法返回的 future 将在 WebSocket 关闭时完成。你也可以像 `send` 方法一样,向该方法传递一个 promise。
95 |
96 | ```swift
97 | ws.close(promise: nil)
98 | ```
99 |
100 | 要在对方关闭连接时收到通知,请使用 `onClose`。这样当客户端或服务器关闭 WebSocket 时,将会触发此 future 方法。
101 |
102 | ```swift
103 | ws.onClose.whenComplete { result in
104 | // 关闭成功或失败。
105 | }
106 | ```
107 |
108 | 当 WebSocket 关闭时会返回 `closeCode` 属性,可用于确定对方关闭连接的原因。
109 |
110 | ## Ping / Pong
111 |
112 | 客户端和服务器会自动发送 ping 和 pong 心跳消息,来保持 WebSocket 的连接。你的程序可以使用 `onPing` 和 `onPong` 回调监听这些事件。
113 |
114 | ```swift
115 | ws.onPing { ws in
116 | // 接收到了 Ping 消息。
117 | }
118 |
119 | ws.onPong { ws in
120 | // 接收到了 Pong 消息。
121 | }
122 | ```
123 |
124 |
125 |
--------------------------------------------------------------------------------
/4.0/docs/assets/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vapor/docs-cn/e627fce27c8eaaefb0ec51c3644f22ce23581bc3/4.0/docs/assets/favicon.png
--------------------------------------------------------------------------------
/4.0/docs/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vapor/docs-cn/e627fce27c8eaaefb0ec51c3644f22ce23581bc3/4.0/docs/assets/logo.png
--------------------------------------------------------------------------------
/4.0/docs/basics/async.md:
--------------------------------------------------------------------------------
1 | # Async
2 |
3 | ## Async Await
4 |
5 | Swift 5.5 在语言层面上以 `async`/`await` 的形式引进了并发性。它提供了优秀的方式去处理异步在 Swift 以及 Vapor 应用中。
6 |
7 | Vapor 是在 [SwiftNIO](https://github.com/apple/swift-nio.git) 的基础上构建的, SwiftNIO 为低层面的异步编程提供了基本类型。这些类型曾经是(现在依然是)贯穿整个 Vapor 在 `async`/`await` 到来之前。现在大部分代码可以用 `async`/`await` 编写来代替 `EventLoopFuture`。这将简化您的代码,使其更容易推理。
8 |
9 | 现在大部分的 Vapor 的 APIs 同时提供 `EventLoopFuture` and `async`/`await` 两个版本供你选择。通常,你应该只选择一种编程方式在单个路由 handler 中,而不应该混用。对于应该显示控制 event loops,或者非常需要高性能的应用,应该继续使用 `EventLoopFuture` 在自定义运行器被实现之前(until custom executors are implemented)。 对于其他应用,你应该使用 `async`/`await` 因为它的好处、可读性和可维护性远远超过了任何小的性能损失。
10 |
11 | ### 迁徙到 async/await
12 |
13 | 为了适配 async/await 这里有几个步骤需要做。第一步,如果你使用 macOS 你必须使用 macOS 12 Monterey 或者更高以及 Xcode13.1 或者更高。 对于其他平台你需要运行 Swift5.5 或者更高,然后情确认你已经更新了所有依赖。
14 |
15 | 在你的 Package.swift, 在第一行把 swift-tools-version 设置为 5.5:
16 |
17 | ```swift
18 | // swift-tools-version:5.5
19 | import PackageDescription
20 |
21 | // ...
22 | ```
23 |
24 | 接下来,设置 platform version 为 macOS 12:
25 |
26 | ```swift
27 | platforms: [
28 | .macOS(.v12)
29 | ],
30 | ```
31 |
32 | 最后 更新 `Run` 目标让它变成一个可运行的目标:
33 |
34 | ```swift
35 | .executableTarget(name: "Run", dependencies: [.target(name: "App")]),
36 | ```
37 |
38 | 注意:如果你部署在Linux环境请确保你更新到了最新的Swift版本。比如在 Heroku 或者在你的 Dockerfile。举个例子你的 Dockerfile 应该变为:
39 |
40 | ```diff
41 | -FROM swift:5.2-focal as build
42 | +FROM swift:5.5-focal as build
43 | ...
44 | -FROM swift:5.2-focal-slim
45 | +FROM swift:5.5-focal-slim
46 | ```
47 |
48 | 现在你可以迁徙现存的代码。通常返回 `EventLoopFuture` 的方法现在变为返回 `async`。比如:
49 |
50 | ```swift
51 | routes.get("firstUser") { req -> EventLoopFuture in
52 | User.query(on: req.db).first().unwrap(or: Abort(.notFound)).flatMap { user in
53 | user.lastAccessed = Date()
54 | return user.update(on: req.db).map {
55 | return user.name
56 | }
57 | }
58 | }
59 | ```
60 |
61 | 现在变为:
62 |
63 | ```swift
64 | routes.get("firstUser") { req async throws -> String in
65 | guard let user = try await User.query(on: req.db).first() else {
66 | throw Abort(.notFound)
67 | }
68 | user.lastAccessed = Date()
69 | try await user.update(on: req.db)
70 | return user.name
71 | }
72 | ```
73 |
74 | ### 使用新旧api
75 |
76 | 如果你遇到还未支持 `async`/`await` 的API,你可以调用 `.get()` 方法来返回一个 `EventLoopFuture`。
77 |
78 | 比如
79 |
80 | ```swift
81 | return someMethodCallThatReturnsAFuture().flatMap { futureResult in
82 | // use futureResult
83 | }
84 | ```
85 |
86 | 可以变为
87 |
88 | ```swift
89 | let futureResult = try await someMethodThatReturnsAFuture().get()
90 | ```
91 |
92 | 如果你需要反过来,你可以把
93 |
94 | ```swift
95 | let myString = try await someAsyncFunctionThatGetsAString()
96 | ```
97 |
98 | 变为
99 |
100 | ```swift
101 | let promise = request.eventLoop.makePromise(of: String.self)
102 | promise.completeWithTask {
103 | try await someAsyncFunctionThatGetsAString()
104 | }
105 | let futureString: EventLoopFuture = promise.futureResult
106 | ```
107 |
108 | ## `EventLoopFuture`
109 |
110 | 你可能已经注意到在 Vapor 中一些API返回一个 `EventLoopFuture` 的泛型。如果这是你第一次听到这个特性,它们一开始可能看起来有点令人困惑。但是别担心这个手册会教你怎么利用这些强大的API。
111 |
112 | Promises 和 futures 是相关的, 但是截然不同的类型。
113 |
114 | |类型|描述|是否可修改|
115 | |-|-|-|
116 | |`EventLoopFuture`|代表一个现在还不可用的值|read-only|
117 | |`EventLoopPromise`|一个可以异步提供值的promise|read/write|
118 |
119 |
120 | Futures 是基于回调的异步api的替代方案。可以以简单的闭包所不能的方式进行链接和转换。
121 |
122 | ## 转换
123 |
124 | 就像Swift中的可选选项和数组一样,futures 可以被映射和平映射。这些是你在 futures 中最基本的操作。
125 |
126 | |method|argument|description|
127 | |-|-|-|
128 | |[`map`](#map)|`(T) -> U`|Maps a future value to a different value.|
129 | |[`flatMapThrowing`](#flatmapthrowing)|`(T) throws -> U`|Maps a future value to a different value or an error.|
130 | |[`flatMap`](#flatmap)|`(T) -> EventLoopFuture`|Maps a future value to different _future_ value.|
131 | |[`transform`](#transform)|`U`|Maps a future to an already available value.|
132 |
133 | 如果你看一下 `map` 和 `flatMap` 在 `Optional` 和 `Array` 中的方法签名(method signatures)。你会看到他们和在 `EventLoopFuture` 中的方法非常相似。
134 |
135 | ### map
136 |
137 | `map` 方法允许你把一个未来值转换成另外一个值。 因为这个未来的值可能现在还不可用,我们必须提供一个闭包来接受它的值。
138 |
139 | ```swift
140 | /// 假设我们将来从某些API得到一个字符串。
141 | let futureString: EventLoopFuture = ...
142 |
143 | /// 把这个字符串转换成整形
144 | let futureInt = futureString.map { string in
145 | print(string) // The actual String
146 | return Int(string) ?? 0
147 | }
148 |
149 | /// We now have a future integer
150 | print(futureInt) // EventLoopFuture
151 | ```
152 |
153 | ### flatMapThrowing
154 |
155 | `flatMapThrowing` 方法允许你把一个未来值转换成另一个值或者抛出一个错误。
156 |
157 | !!! 信息
158 | 因为抛出错误必须在内部创建一个新的future,所以这个方法前缀为 `flatMap`,即使闭包不接受future返回。
159 |
160 | ```swift
161 | /// 假设我们将来从某些API得到一个字符串。
162 | let futureString: EventLoopFuture = ...
163 |
164 | /// 把这个字符串转换成整形
165 | let futureInt = futureString.flatMapThrowing { string in
166 | print(string) // The actual String
167 | // 将字符串转换为整数或抛出错误
168 | guard let int = Int(string) else {
169 | throw Abort(...)
170 | }
171 | return int
172 | }
173 |
174 | /// We now have a future integer
175 | print(futureInt) // EventLoopFuture
176 | ```
177 |
178 | ### flatMap
179 |
180 | flatMap方法允许你将未来值转换为另一个未来值。它得到的名称“扁平”映射,因为它允许你避免创建嵌套的未来(例如,`EventLoopFuture>`)。换句话说,它帮助您保持泛型平坦。
181 |
182 | ```swift
183 | /// Assume we get a future string back from some API
184 | let futureString: EventLoopFuture = ...
185 |
186 | /// Assume we have created an HTTP client
187 | let client: Client = ...
188 |
189 | /// flatMap the future string to a future response
190 | let futureResponse = futureString.flatMap { string in
191 | client.get(string) // EventLoopFuture
192 | }
193 |
194 | /// We now have a future response
195 | print(futureResponse) // EventLoopFuture
196 | ```
197 |
198 | !!! 信息
199 | 如果我们在上面的例子中使用 `map`,我们将会得到: `EventLoopFuture>`。
200 |
201 | 要在 `flatMap` 中调用一个抛出方法,使用Swift的 `do` / `catch` 关键字并创建一个[completed future](#makefuture)。
202 | To call a throwing method inside of a `flatMap`, use Swift's `do` / `catch` keywords and create a [completed future](#makefuture).
203 |
204 | ```swift
205 | /// Assume future string and client from previous example.
206 | let futureResponse = futureString.flatMap { string in
207 | let url: URL
208 | do {
209 | // Some synchronous throwing method.
210 | url = try convertToURL(string)
211 | } catch {
212 | // Use event loop to make pre-completed future.
213 | return eventLoop.makeFailedFuture(error)
214 | }
215 | return client.get(url) // EventLoopFuture
216 | }
217 | ```
218 |
219 | ### transform
220 | `transform` 方法允许您修改 future 的值,而忽略现有值。这对于转换 `EventLoopFuture` 的结果特别有用,在这种情况下未来的实际值并不重要。
221 |
222 | !!! 提示
223 | `EventLoopFuture`, 有时也被称为信号,它的唯一目的是通知您某些异步操作的完成或失败。
224 |
225 | ```swift
226 | /// Assume we get a void future back from some API
227 | let userDidSave: EventLoopFuture = ...
228 |
229 | /// Transform the void future to an HTTP status
230 | let futureStatus = userDidSave.transform(to: HTTPStatus.ok)
231 | print(futureStatus) // EventLoopFuture
232 | ```
233 |
234 | 即使我们提供了一个已经可用的值为 `transform`,它仍然是一个 __transformation__ 。直到所有先前的 future 都完成(或失败),future 才会完成。
235 |
236 | ### 链接(Chaining)
237 |
238 | 关于 transformations,最重要的一点是它们可以被链接起来。这允许您轻松地表示许多转换和子任务。
239 |
240 | 让我们修改上面的示例,看看如何利用链接。
241 |
242 | ```swift
243 | /// Assume we get a future string back from some API
244 | let futureString: EventLoopFuture = ...
245 |
246 | /// Assume we have created an HTTP client
247 | let client: Client = ...
248 |
249 | /// Transform the string to a url, then to a response
250 | let futureResponse = futureString.flatMapThrowing { string in
251 | guard let url = URL(string: string) else {
252 | throw Abort(.badRequest, reason: "Invalid URL string: \(string)")
253 | }
254 | return url
255 | }.flatMap { url in
256 | client.get(url)
257 | }
258 |
259 | print(futureResponse) // EventLoopFuture
260 | ```
261 |
262 | 在初始调用 map 之后,创建了一个临时的 `EventLoopFuture`。然后,这个future立即平映射(flat-mapped)到 `EventLoopFuture`
263 |
264 | ## Future
265 |
266 | 让我们看看使用 `EventLoopFuture` 的一些其他方法。
267 |
268 | ### makeFuture
269 |
270 | You can use an event loop to create pre-completed future with either the value or an error.
271 |
272 | ```swift
273 | // Create a pre-succeeded future.
274 | let futureString: EventLoopFuture = eventLoop.makeSucceededFuture("hello")
275 |
276 | // Create a pre-failed future.
277 | let futureString: EventLoopFuture = eventLoop.makeFailedFuture(error)
278 | ```
279 |
280 | ### whenComplete
281 |
282 |
283 | 你可以使用 `whenComplete` 来添加一个回调函数,它将在未来的成功或失败时执行。
284 |
285 | ```swift
286 | /// Assume we get a future string back from some API
287 | let futureString: EventLoopFuture = ...
288 |
289 | futureString.whenComplete { result in
290 | switch result {
291 | case .success(let string):
292 | print(string) // The actual String
293 | case .failure(let error):
294 | print(error) // A Swift Error
295 | }
296 | }
297 | ```
298 |
299 | !!! note
300 | 您可以向 future 添加任意数量的回调。
301 |
302 | ### Wait
303 |
304 | 您可以使用 `.wait()` 来同步等待future完成。由于future可能会失败,这个调用是可抛出错误的。
305 |
306 | ```swift
307 | /// Assume we get a future string back from some API
308 | let futureString: EventLoopFuture = ...
309 |
310 | /// Block until the string is ready
311 | let string = try futureString.wait()
312 | print(string) /// String
313 | ```
314 |
315 | `wait()` 只能在后台线程或主线程中使用,也就是在 `configure.swift` 中。它不能在事件循环线程(event loop)上使用,也就是在路由闭包中。
316 |
317 | !!! 警告
318 | 试图在事件循环线程上调用 `wait()` 将导致断言失败。
319 |
320 |
321 | ## Promise
322 |
323 | 大多数时候,您将转换 Vapor 的 api 返回的 futures。然而,在某些情况下,你可能需要创造自己的 promise。
324 |
325 | 要创建一个 promise,你需要访问一个 `EventLoop`。你可以根据上下文(context)从 `Application` 或 `Request` 获得一个 event loop。
326 |
327 | ```swift
328 | let eventLoop: EventLoop
329 |
330 | // Create a new promise for some string.
331 | let promiseString = eventLoop.makePromise(of: String.self)
332 | print(promiseString) // EventLoopPromise
333 | print(promiseString.futureResult) // EventLoopFuture
334 |
335 | // Completes the associated future.
336 | promiseString.succeed("Hello")
337 |
338 | // Fails the associated future.
339 | promiseString.fail(...)
340 | ```
341 |
342 | !!! info
343 | 一个 promise 只能 completed 一次。任何后续的 completions 都将被忽略。
344 |
345 | promises 可以从任何线程 completed(`succeed` / `fail`)。这就是为什么 promises 需要初始化一个 event loop。promises 确保完成操作(completion action)返回到其 event loop 中执行。
346 |
347 | ## Event Loop
348 |
349 | 当应用程序启动时,它通常会为运行它的CPU中的每个核心创建一个 event loop。每个 event loop 只有一个线程。如果您熟悉 Node.js 中的 event loops,那么 Vapor 中的 event loop也是类似的。主要的区别是 Vapor 可以在一个进程(process)中运行多个 event loop,因为 Swift 支持多线程。
350 |
351 | 每次客户端连接到服务器时,它将被分配给一个event loops。从这时候开始,服务器和客户端之间的所有通信都将发生在同一个 event loop 上(通过关联,该 event loop 的线程)。
352 |
353 | event loop 负责跟踪每个连接的客户机的状态。如果客户端有一个等待读取的请求,event loop 触发一个读取通知,然后数据被读取。一旦读取了整个请求,等待该请求数据的任何 futures 都将完成。
354 |
355 | 在路由闭包中,你可以通过 `Request` 访问当前事件循环。
356 |
357 | ```swift
358 | req.eventLoop.makePromise(of: ...)
359 | ```
360 |
361 | !!! warning
362 | Vapor 预期路由闭包(route closures)将保持在 `req.eventLoop` 上。如果您跳转线程,您必须确保对`Request`的访问和最终的响应都发生在请求的 event loop 中。
363 |
364 | 在路由闭包(route closures)之外,你可以通过 `Application` 获得一个可用的event loops。
365 | Outside of route closures, you can get one of the available event loops via `Application`.
366 |
367 | ```swift
368 | app.eventLoopGroup.next().makePromise(of: ...)
369 | ```
370 |
371 | ### hop
372 |
373 | 你可以通过 `hop` 来改变一个 future 的 event loop。
374 |
375 | ```swift
376 | futureString.hop(to: otherEventLoop)
377 | ```
378 |
379 | ## Blocking
380 |
381 | 在 event loop 线程上调用阻塞代码会阻止应用程序及时响应传入请求。阻塞调用的一个例子是' libc.sleep(_:) '。
382 |
383 | ```swift
384 | app.get("hello") { req in
385 | /// Puts the event loop's thread to sleep.
386 | sleep(5)
387 |
388 | /// Returns a simple string once the thread re-awakens.
389 | return "Hello, world!"
390 | }
391 | ```
392 |
393 | `sleep(_:)` 是一个命令,用于阻塞当前线程的秒数。如果您直接在 event loop 上执行这样的阻塞工作,event loop 将无法在阻塞工作期间响应分配给它的任何其他客户端。换句话说,如果你在一个 event loop 上调用 `sleep(5)`,所有连接到该 event loop 的其他客户端(可能是数百或数千)将延迟至少5秒。
394 |
395 | 确保在后台运行任何阻塞工作。当这项工作以非阻塞方式完成时,使用 promises 来通知 event loop。
396 |
397 | ```swift
398 | app.get("hello") { req -> EventLoopFuture in
399 | /// Dispatch some work to happen on a background thread
400 | return req.application.threadPool.runIfActive(eventLoop: req.eventLoop) {
401 | /// Puts the background thread to sleep
402 | /// This will not affect any of the event loops
403 | sleep(5)
404 |
405 | /// When the "blocking work" has completed,
406 | /// return the result.
407 | return "Hello world!"
408 | }
409 | }
410 | ```
411 |
412 | 并不是所有的阻塞调用都像 `sleep(_:)` 那样明显。如果你怀疑你正在使用的调用可能是阻塞的,研究方法本身或询问别人。下面的部分将更详细地讨论方法如何阻塞。
413 |
414 | ### I/O 约束
415 |
416 | I/O 约束阻塞意味着等待较慢的资源,如网络或硬盘,这些资源可能比 CPU 慢几个数量级。在等待这些资源时阻塞 CPU 会导致时间的浪费。
417 |
418 | !!! danger
419 | 不要在事件循环中直接进行阻塞I/O约束调用.
420 |
421 | 所有的 Vapor 包都构建在 SwiftNIO 上,并使用非阻塞 I/O。然而,现在有很多 Swift 包和 C 库使用了阻塞 I/O。如果一个函数正在进行磁盘或网络 IO 并使用同步 API (没有使用 callbacks 或 future),那么它很有可能是阻塞的。
422 |
423 | ### CPU 约束
424 |
425 | 请求期间的大部分时间都花在等待数据库查询和网络请求等外部资源加载上。因为 Vapor 和 SwiftNIO 是非阻塞的,所以这种停机时间可以用于满足其他传入请求。然而,应用程序中的一些路由可能需要执行大量 CPU 约束的工作。
426 |
427 | 当 event loop 处理CPU约束的工作时,它将无法响应其他传入请求。这通常是没问题的,因为CPU是快速的,大多数CPU工作是轻量级的web应用程序。但是,如果需要大量CPU资源的路由阻止了对更快路由的请求的快速响应,这就会成为一个问题。
428 |
429 | 识别应用程序中长时间运行的CPU工作,并将其转移到后台线程,可以帮助提高服务的可靠性和响应能力。与I/O约束的工作相比,CPU约束的工作更多的是一个灰色区域,最终由您决定在哪里划定界限。
430 |
431 | 大量CPU约束工作的一个常见示例是用户注册和登录期间的Bcrypt哈希。出于安全原因,Bcrypt被故意设置为非常慢和CPU密集型。这可能是一个简单的web应用程序所做的最耗费CPU的工作。将哈希移到后台线程可以允许CPU在计算哈希时交错事件循环工作,从而获得更高的并发性。
432 |
--------------------------------------------------------------------------------
/4.0/docs/basics/client.md:
--------------------------------------------------------------------------------
1 | # Client
2 |
3 | Vapor的 `Client` API 允许您使用 HTTP 调用外部资源,它基于 [async-http-client](https://github.com/swift-server/async-http-client) 构建,并集成了 [Content](./content.md) API。
4 |
5 |
6 | ## 概述
7 |
8 | 你可以通过 `Application` 或通过 `Request` 在路由处理回调中访问默认 `Client`。
9 |
10 | ```swift
11 | app.client // Client
12 |
13 | app.get("test") { req in
14 | req.client // Client
15 | }
16 | ```
17 |
18 |
19 | `Application` 的 `client` 对于在配置期间发起 HTTP 请求非常有用,如果要在路由处理程序中发起 HTTP 请求,请使用 `req.client`。
20 |
21 |
22 | ### 方法
23 |
24 | 如果你要发起一个 GET 请求,请将所需的 URL 地址传给 `client` 的 `get` 方法,如下所示:
25 |
26 | ```swift
27 | let response = try await req.client.get("https://httpbin.org/status/200")
28 | ```
29 |
30 | HTTP 的常用方法(例如 `get`, `post`, `delete`)都有便捷的调用方式,`client` 的响应会以一个 future 的形式返回,它包含了 HTTP 返回的状态、头部信息和内容。
31 |
32 |
33 | ### Content
34 |
35 | Vapor 的 [Content](./content.md) API 可用于处理客户请求和响应中的数据,如果要在请求体中添加参数或编码,请在 `beforeSend` 闭包中进行。
36 |
37 | ```swift
38 | let response = try await req.client.post("https://httpbin.org/status/200") { req in
39 | // Encode query string to the request URL.
40 | try req.query.encode(["q": "test"])
41 |
42 | // Encode JSON to the request body.
43 | try req.content.encode(["hello": "world"])
44 |
45 | // Add auth header to the request
46 | let auth = BasicAuthorization(username: "something", password: "somethingelse")
47 | req.headers.basicAuthorization = auth
48 | }
49 | // Handle the response.
50 | ```
51 |
52 | 你可以用 `Content` 对 response body 解码采用熟悉的方式:
53 | ```swift
54 | let response = try await req.client.get("https://httpbin.org/json")
55 | let json = try response.content.decode(MyJSONResponse.self)
56 | ```
57 |
58 | 如果要解码响应的数据,请在 `flatMapThrowing` 回调中处理。
59 |
60 | ```swift
61 | req.client.get("https://httpbin.org/json").flatMapThrowing { res in
62 | try res.content.decode(MyJSONResponse.self)
63 | }.map { json in
64 | // 处理返回的JSON信息
65 | }
66 | ```
67 |
68 | ## 配置
69 |
70 | 你可以通过 `application` 来配置 HTTP `client` 的基础参数。
71 |
72 | ```swift
73 | // 禁止自动跳转
74 | app.http.client.configuration.redirectConfiguration = .disallow
75 | ```
76 |
77 | 请注意,你必须在首次使用默认的 `client` 之前对其进行配置。
78 |
79 |
--------------------------------------------------------------------------------
/4.0/docs/basics/content.md:
--------------------------------------------------------------------------------
1 | # 内容
2 |
3 | 基于 Vapor 的 content API,你可以轻松地对 HTTP 消息中的可编码结构进行编码/解码。默认使用[JSON](https://tools.ietf.org/html/rfc7159)编码,并支持[URL-Encoded Form](https://en.wikipedia.org/wiki/Percent-encoding#The_application/x-www-form-urlencoded_type)和[Multipart](https://tools.ietf.org/html/rfc2388)。content API 可以灵活配置,允许你为某些 HTTP 请求类型添加、修改或替换编码策略。
4 |
5 |
6 | ## 总览
7 |
8 | 要了解 Vapor 的 content API 是如何工作的,你应该先了解一些关于 HTTP 的基础知识。
9 | 看看下面这个请求的示例:
10 |
11 | ```http
12 | POST /greeting HTTP/1.1
13 | content-type: application/json
14 | content-length: 18
15 |
16 | {"hello": "world"}
17 | ```
18 |
19 | 该请求表明,它包含使用 `content-type` 标头和 `application/json` 媒体类型的JSON编码数据。如前所述,JSON 数据在正文中的标头之后。
20 |
21 | ### 内容结构
22 |
23 | 解码此HTTP消息的第一步是创建匹配预期结构的可编码类型。
24 |
25 | ```swift
26 | struct Greeting: Content {
27 | var hello: String
28 | }
29 | ```
30 |
31 | 使上面的 `Greeting` 数据类型遵循 `Content` 协议,将同时支持 `Codable` 协议规则,符合 Content API 的其他程序代码。
32 |
33 | 然后就可以使用 `req.content` 从传入的请求中对数据进行解码,如下所示:
34 |
35 | ```swift
36 | app.post("greeting") { req in
37 | let greeting = try req.content.decode(Greeting.self)
38 | print(greeting.hello) // "world"
39 | return HTTPStatus.ok
40 | }
41 | ```
42 |
43 | 解码方法使用请求的 content 类型来寻找合适的解码器,如果没有找到解码器,或者请求中不包含 content 类型标头,将抛出 `415` 错误。
44 |
45 | 这意味着该路由自动接受所有其他支持的内容类型,如url编码形式:
46 |
47 | ```http
48 | POST /greeting HTTP/1.1
49 | content-type: application/x-www-form-urlencoded
50 | content-length: 11
51 |
52 | hello=world
53 | ```
54 |
55 | ### 支持的媒体类型
56 |
57 | 以下是 content API 默认支持的媒体类型:
58 |
59 | |name|header value|media type|
60 | |-|-|-|
61 | |JSON|application/json|`.json`|
62 | |Multipart|multipart/form-data|`.formData`|
63 | |URL-Encoded Form|application/x-www-form-urlencoded|`.urlEncodedForm`|
64 | |Plaintext|text/plain|`.plainText`|
65 | |HTML|text/html|`.html`|
66 |
67 | 不是所有的媒体类型都支持所有的 Codable 协议。例如,JSON 不支持顶层片段,Plaintext 不支持嵌套数据。
68 |
69 | ## 查询
70 |
71 | Vapor的 Content API 支持处理 URL 查询字符串中的 URL 编码数据。
72 |
73 | ### 解码
74 |
75 | 要了解 URL 查询字符串的解码是如何工作的,请看下面的示例请求:
76 |
77 | ```http
78 | GET /hello?name=Vapor HTTP/1.1
79 | content-length: 0
80 | ```
81 |
82 | 就像处理 HTTP 消息正文内容的 API 一样,解析 URL 查询字符串的第一步是创建一个与预期结构相匹配的 `struct` 。
83 |
84 | ```swift
85 | struct Hello: Content {
86 | var name: String?
87 | }
88 | ```
89 |
90 | 注意:`name` 是一个可选的 `String`,因为 URL 查询字符串应该是可选的。如果你需要一个参数,请用路由参数代替。
91 |
92 | 现在,你已经为该路由的预期查询字符串提供了 `Content` 结构,可以对其进行解码了。
93 |
94 | ```swift
95 | app.get("hello") { req -> String in
96 | let hello = try req.query.decode(Hello.self)
97 | return "Hello, \(hello.name ?? "Anonymous")"
98 | }
99 | ```
100 |
101 | 给定上面的请求,此路由将触发以下响应:
102 |
103 | ```http
104 | HTTP/1.1 200 OK
105 | content-length: 12
106 |
107 | Hello, Vapor
108 | ```
109 |
110 | 如果省略了查询字符串,如以下请求中所示,将使用"匿名"来代替。
111 |
112 | ```http
113 | GET /hello HTTP/1.1
114 | content-length: 0
115 | ```
116 |
117 | ### 单值
118 |
119 | 除了对 `Content` 结构进行解码外,Vapor 还支持使用下标从查询字符串中获取单个参数值。
120 |
121 | ```swift
122 | let name: String? = req.query["name"]
123 | ```
124 |
125 | ## 钩子
126 |
127 | Vapor 会自动调用 `Content` 类型的 `beforeDecode` 和 `afterDecode`。提供了默认的实现,但你可以使用这些方法来自定义逻辑实现:
128 |
129 | ```swift
130 | // 在此内容被解码后运行。
131 | // 此内容解码后运行。只有 Struct 才需要 'mutating',而 Class 则不需要。
132 | mutating func afterDecode() throws {
133 | // 名称可能没有传入,但如果传入了,那就不能是空字符串。
134 | self.name = self.name?.trimmingCharacters(in: .whitespacesAndNewlines)
135 | if let name = self.name, name.isEmpty {
136 | throw Abort(.badRequest, reason: "Name must not be empty.")
137 | }
138 | }
139 |
140 | // 在对该内容进行编码之前运行。只有 Struct 才需要 'mutating',而 Class 则不需要。
141 | mutating func beforeEncode() throws {
142 | // 必须*总是*传递一个名称回来,它不能是一个空字符串。
143 | guard
144 | let name = self.name?.trimmingCharacters(in: .whitespacesAndNewlines),
145 | !name.isEmpty
146 | else {
147 | throw Abort(.badRequest, reason: "Name must not be empty.")
148 | }
149 | self.name = name
150 | }
151 | ```
152 |
153 | ## 覆盖默认值
154 |
155 | 可以配置 Vapor 的 Content API 所使用的默认编码器和解码器。
156 |
157 | ### 全局
158 |
159 | `ContentConfiguration.global`允许你修改 Vapor 默认使用的编码器和解码器。这对于改变整个应用程序的数据解析和序列化方式非常有用。
160 |
161 | ```swift
162 | // 创建一个新的 JSON 编码器,使用 unix-timestamp 日期编码
163 | let encoder = JSONEncoder()
164 | encoder.dateEncodingStrategy = .secondsSince1970
165 |
166 | // 覆盖用于媒体类型 `.json` 的全局编码器。
167 | ContentConfiguration.global.use(encoder: encoder, for: .json)
168 | ```
169 |
170 | 通常是在 `configure.swift` 文件中修改 `ContentConfiguration`。
171 |
172 | ### 单次生效
173 |
174 | 对编码和解码方法的调用,如 `req.content.decode` ,支持为单次使用配置自定义编码器。
175 |
176 | ```swift
177 | // 创建一个新的 JSON 解码器,使用 unix-timestamp 日期的时间戳
178 | let decoder = JSONDecoder()
179 | decoder.dateDecodingStrategy = .secondsSince1970
180 |
181 | // 使用自定义解码器对 `Hello` 结构进行解码
182 | let hello = try req.content.decode(Hello.self, using: decoder)
183 | ```
184 |
185 | ## 定制编码器
186 |
187 | 应用程序和第三方软件包可以通过创建自定义编码器,对 Vapor 默认不支持的媒体类型进行扩展支持。
188 |
189 | ### 内容
190 |
191 | Vapor 为能够处理 HTTP 消息体中内容的编码器指定了两种协议:`ContentDecoder` 和 `ContentEncoder`。
192 |
193 | ```swift
194 | public protocol ContentEncoder {
195 | func encode(_ encodable: E, to body: inout ByteBuffer, headers: inout HTTPHeaders) throws
196 | where E: Encodable
197 | }
198 |
199 | public protocol ContentDecoder {
200 | func decode(_ decodable: D.Type, from body: ByteBuffer, headers: HTTPHeaders) throws -> D
201 | where D: Decodable
202 | }
203 | ```
204 |
205 | 遵循这些协议,允许你的自定义编码器注册到上面指定的 `ContentConfiguration`。
206 |
207 | ### URL 查询
208 |
209 | Vapor 为能够处理 URL 查询字符串中的内容的编码器指定了两个协议: `URLQueryDecoder` 和 `URLQueryEncoder`。
210 |
211 | ```swift
212 | public protocol URLQueryDecoder {
213 | func decode(_ decodable: D.Type, from url: URI) throws -> D
214 | where D: Decodable
215 | }
216 |
217 | public protocol URLQueryEncoder {
218 | func encode(_ encodable: E, to url: inout URI) throws
219 | where E: Encodable
220 | }
221 | ```
222 |
223 | 遵循这些协议,可以将你的自定义编码器注册到 `ContentConfiguration` 中,以使用 `use(urlEncoder:)` 和 `use(urlDecoder:)` 方法处理 URL 查询字符串。
224 |
225 | ### Custom `ResponseEncodable`
226 |
227 | 另一种方法涉及到在你的类型上实现 `ResponseEncodable`,请看下面这个 `HTML` 包装类型。
228 |
229 | ```swift
230 | struct HTML {
231 | let value: String
232 | }
233 | ```
234 |
235 | 它的 `ResponseEncodable` 实现看起来像这样:
236 |
237 | ```swift
238 | extension HTML: ResponseEncodable {
239 | public func encodeResponse(for request: Request) -> EventLoopFuture {
240 | var headers = HTTPHeaders()
241 | headers.add(name: .contentType, value: "text/html")
242 | return request.eventLoop.makeSucceededFuture(.init(
243 | status: .ok, headers: headers, body: .init(string: value)
244 | ))
245 | }
246 | }
247 | ```
248 |
249 | 如果你正在使用 `async`/`await` 你可以使用 `AsyncResponseEncodable`:
250 |
251 | ```swift
252 | extension HTML: AsyncResponseEncodable {
253 | public func encodeResponse(for request: Request) async throws -> Response {
254 | var headers = HTTPHeaders()
255 | headers.add(name: .contentType, value: "text/html")
256 | return .init(status: .ok, headers: headers, body: .init(string: value))
257 | }
258 | }
259 | ```
260 | 注意,它允许自定义“Content-Type”头,查看更多请查阅 [`HTTPHeaders` reference](https://api.vapor.codes/vapor/master/Vapor/)
261 |
262 | 接下来,你可以在你的路由中使用 `HTML` 作为 response:
263 |
264 | ```swift
265 | app.get { _ in
266 | HTML(value: """
267 |
268 |
269 | Hello, World!
270 |
271 |
272 | """)
273 | }
274 | ```
--------------------------------------------------------------------------------
/4.0/docs/basics/controllers.md:
--------------------------------------------------------------------------------
1 | # Controllers
2 |
3 | `Controller` 是将应用程序的不同逻辑进行分组的优秀方案,大多数 Controller 都具备接受多种请求的功能,并根据需要进行响应。
4 |
5 | 建议将其放在 [Controllers](../start/folder-structure.md#controllers) 文件夹下,具体情况可以根据需求划分模块。
6 |
7 |
8 | ## 概述
9 |
10 | 让我们看一个示例 Controller:
11 |
12 | ```swift
13 | import Vapor
14 |
15 | struct TodosController: RouteCollection {
16 | func boot(routes: RoutesBuilder) throws {
17 | let todos = routes.grouped("todos")
18 | todos.get(use: index)
19 | todos.post(use: create)
20 |
21 | todos.group(":id") { todo in
22 | todo.get(use: show)
23 | todo.put(use: update)
24 | todo.delete(use: delete)
25 | }
26 | }
27 |
28 | func index(req: Request) async throws -> String {
29 | // ...
30 | }
31 |
32 | func create(req: Request) throws -> EventLoopFuture {
33 | // ...
34 | }
35 |
36 | func show(req: Request) throws -> String {
37 | guard let id = req.parameters.get("id") else {
38 | throw Abort(.internalServerError)
39 | }
40 | // ...
41 | }
42 |
43 | func update(req: Request) throws -> String {
44 | guard let id = req.parameters.get("id") else {
45 | throw Abort(.internalServerError)
46 | }
47 | // ...
48 | }
49 |
50 | func delete(req: Request) throws -> String {
51 | guard let id = req.parameters.get("id") else {
52 | throw Abort(.internalServerError)
53 | }
54 | // ...
55 | }
56 | }
57 | ```
58 |
59 | `Controller` 的方法接受 `Request` 参数,并返回 `ResponseEncodable` 对象。该方法可以是异步或者同步(或者返回一个 `EventLoopFuture`)
60 |
61 | !!! 注意
62 | [EventLoopFuture](async.md) 期望返回值为 `ResponseEncodable` (i.e, `EventLoopFuture`) 或 `ResponseEncodable`.
63 |
64 | 最后,你需要在 `routes.swift` 中注册 Controller:
65 |
66 | ```swift
67 | try app.register(collection: TodosController())
68 | ```
69 |
--------------------------------------------------------------------------------
/4.0/docs/basics/environment.md:
--------------------------------------------------------------------------------
1 | # 环境
2 |
3 | Vapor 的环境API帮助您动态配置您的应用程序。默认情况下,你的应用程序将使用 `development` 环境。你可以定义其他有用的环境,如 `production` 或 `staging`,并在每种情况下改变你的应用是如何配置的。您还可以从进程的环境或 `.Env` (dotenv)文件读取配置取决于您的需要。
4 |
5 | 要访问当前环境,请使用 `app.environment`。你可以在 `configure(_:)` 中通过这个属性来执行不同的配置逻辑。
6 | To access the current environment, use `app.environment`. You can switch on this property in `configure(_:)` to execute different configuration logic.
7 |
8 | ```swift
9 | switch app.environment {
10 | case .production:
11 | app.databases.use(....)
12 | default:
13 | app.databases.use(...)
14 | }
15 | ```
16 |
17 | ## 改变环境
18 |
19 | 默认情况下,你的应用程序将在 `development` 环境中运行。你可以通过在应用程序引导期间传递`--env` (`-e`)标志来改变这一点。
20 |
21 | ```swift
22 | vapor run serve --env production
23 | ```
24 |
25 | Vapor 包含下列环境:
26 |
27 | |name|short|description|
28 | |-|-|-|
29 | |production|prod|Deployed to your users.|
30 | |development|dev|Local development.|
31 | |testing|test|For unit testing.|
32 |
33 | !!! info
34 | `production` 环境将默认为 `notice` 级别的日志记录,除非另有说明。所有其他环境默认为 `info`。
35 |
36 | 您可以将全名或短名传递给`--env` (`-e`)标志。
37 |
38 | ```swift
39 | vapor run serve -e prod
40 | ```
41 |
42 | ## 进程变量
43 |
44 | `Environment` 提供了一个简单的、基于字符串的API来访问进程的环境变量。
45 |
46 | ```swift
47 | let foo = Environment.get("FOO")
48 | print(foo) // String?
49 | ```
50 |
51 | 除了 `get` 之外,`Environment` 还通过 `process` 提供了一个动态成员查找API。
52 |
53 | ```swift
54 | let foo = Environment.process.FOO
55 | print(foo) // String?
56 | ```
57 |
58 | 当在终端运行应用程序时,你可以使用 `export` 设置环境变量。
59 |
60 | ```sh
61 | export FOO=BAR
62 | vapor run serve
63 | ```
64 |
65 | 当在Xcode中运行应用程序时,你可以通过编辑 `Run` scheme来设置环境变量。
66 |
67 | ## .env (dotenv)
68 |
69 | Dotenv文件包含一个键值对列表,这些键值对将自动加载到环境中。这些文件使配置环境变量变得很容易,而不需要手动设置它们。
70 |
71 | Vapor 将在当前工作目录中查找dotenv文件。如果你使用Xcode,确保通过编辑 `Run` scheme 设置工作目录。
72 |
73 | Assume the following `.env` file placed in your projects root folder:
74 | 假设以下 `.env` 文件放在你的项目根文件夹中:
75 |
76 | ```sh
77 | FOO=BAR
78 | ```
79 |
80 | 当您的应用程序启动时,您将能够像访问其他进程环境变量一样访问该文件的内容。
81 |
82 | ```swift
83 | let foo = Environment.get("FOO")
84 | print(foo) // String?
85 | ```
86 |
87 | !!! info
88 | 在 `.env` 文件中指定的变量不会覆盖进程环境中已经存在的变量。
89 |
90 | 在`.env`旁边,Vapor 还将尝试为当前环境加载一个dotenv文件。例如,在 `development` 环境中,蒸汽将加载 `.env.development`。特定环境文件中的任何值都将优先于 `.env` 文件内的值。
91 |
92 | 一个典型的模式是项目包含一个 `.env` 文件作为带有默认值的模板。在 `.gitignore` 中使用以下模式忽略特定的环境文件
93 |
94 | ```gitignore
95 | .env.*
96 | ```
97 |
98 | 当项目被 cloned 到新计算机时,已经带有正确的值的`.env`模板可以被复制。
99 |
100 | ```sh
101 | cp .env .env.development
102 | vim .env.development
103 | ```
104 |
105 | !!! warning
106 | 带有敏感信息(如密码)的Dotenv文件不应提交给版本控制。
107 |
108 | 如果你在加载dotenv文件时遇到了困难,尝试使用 `--log debug` 来启用调试日志以获取更多信息。
109 |
110 | ## 自定义环境
111 |
112 | 要定义自定义的环境名称,请扩展 `Environment`。
113 |
114 | ```swift
115 | extension Environment {
116 | static var staging: Environment {
117 | .custom(name: "staging")
118 | }
119 | }
120 | ```
121 |
122 | 应用程序的环境通常使用 `main.swift` 中的 `environment .detect()` 来设置。
123 |
124 | ```swift
125 | import Vapor
126 |
127 | var env = try Environment.detect()
128 | try LoggingSystem.bootstrap(from: &env)
129 |
130 | let app = Application(env)
131 | defer { app.shutdown() }
132 | ```
133 |
134 | `detect` 方法使用进程的命令行参数并自动解析 `--env`标志。您可以通过初始化自定义的 `Environment` 结构来覆盖此行为。
135 |
136 | ```swift
137 | let env = Environment(name: "testing", arguments: ["vapor"])
138 | ```
139 |
140 | 参数数组必须包含至少一个表示可执行名称的参数。可以提供进一步的参数来模拟通过命令行传递参数。这对于测试特别有用。
141 |
--------------------------------------------------------------------------------
/4.0/docs/basics/errors.md:
--------------------------------------------------------------------------------
1 | # Errors
2 |
3 | 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.
4 |
5 | ## Abort
6 |
7 | 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.
8 |
9 | ```swift
10 | // 404 error, default "Not Found" reason used.
11 | throw Abort(.notFound)
12 |
13 | // 401 error, custom reason used.
14 | throw Abort(.unauthorized, reason: "Invalid Credentials")
15 | ```
16 |
17 | 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.
18 |
19 | ```swift
20 | guard let user = user else {
21 | req.eventLoop.makeFailedFuture(Abort(.notFound))
22 | }
23 | return user.save()
24 | ```
25 |
26 | Vapor includes a helper extension for unwrapping futures with optional values: `unwrap(or:)`.
27 |
28 | ```swift
29 | User.find(id, on: db)
30 | .unwrap(or: Abort(.notFound))
31 | .flatMap
32 | { user in
33 | // Non-optional User supplied to closure.
34 | }
35 | ```
36 |
37 | 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:
38 |
39 | ```swift
40 | guard let user = try await User.find(id, on: db) {
41 | throw Abort(.notFound)
42 | }
43 | ```
44 |
45 |
46 | ## Abort Error
47 |
48 | 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.
49 |
50 | To configure the resulting HTTP response status or reason for a particular error, conform it to `AbortError`.
51 |
52 | ```swift
53 | import Vapor
54 |
55 | enum MyError {
56 | case userNotLoggedIn
57 | case invalidEmail(String)
58 | }
59 |
60 | extension MyError: AbortError {
61 | var reason: String {
62 | switch self {
63 | case .userNotLoggedIn:
64 | return "User is not logged in."
65 | case .invalidEmail(let email):
66 | return "Email address is not valid: \(email)."
67 | }
68 | }
69 |
70 | var status: HTTPStatus {
71 | switch self {
72 | case .userNotLoggedIn:
73 | return .unauthorized
74 | case .invalidEmail:
75 | return .badRequest
76 | }
77 | }
78 | }
79 | ```
80 |
81 | ## Debuggable Error
82 |
83 | `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.
84 |
85 | 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.
86 |
87 | 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.
88 |
89 | ```swift
90 | import Vapor
91 |
92 | struct MyError: DebuggableError {
93 | enum Value {
94 | case userNotLoggedIn
95 | case invalidEmail(String)
96 | }
97 |
98 | var identifier: String {
99 | switch self.value {
100 | case .userNotLoggedIn:
101 | return "userNotLoggedIn"
102 | case .invalidEmail:
103 | return "invalidEmail"
104 | }
105 | }
106 |
107 | var reason: String {
108 | switch self.value {
109 | case .userNotLoggedIn:
110 | return "User is not logged in."
111 | case .invalidEmail(let email):
112 | return "Email address is not valid: \(email)."
113 | }
114 | }
115 |
116 | var value: Value
117 | var source: ErrorSource?
118 |
119 | init(
120 | _ value: Value,
121 | file: String = #file,
122 | function: String = #function,
123 | line: UInt = #line,
124 | column: UInt = #column
125 | ) {
126 | self.value = value
127 | self.source = .init(
128 | file: file,
129 | function: function,
130 | line: line,
131 | column: column
132 | )
133 | }
134 | }
135 | ```
136 |
137 | `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.
138 |
139 | ## Stack Traces
140 |
141 | Vapor includes support for viewing stack traces for both normal Swift errors and crashes.
142 |
143 | ### Swift Backtrace
144 |
145 | 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.
146 |
147 | ```sh
148 | swift build -c release -Xswiftc -g
149 | ```
150 |
151 | ### Error Traces
152 |
153 | 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()`.
154 |
155 | ```swift
156 | import Vapor
157 |
158 | struct MyError: DebuggableError {
159 | var identifier: String
160 | var reason: String
161 | var stackTrace: StackTrace?
162 |
163 | init(
164 | identifier: String,
165 | reason: String,
166 | stackTrace: StackTrace? = .capture()
167 | ) {
168 | self.identifier = identifier
169 | self.reason = reason
170 | self.stackTrace = stackTrace
171 | }
172 | }
173 | ```
174 |
175 | When your application's [log level](logging.md#level) is set to `.debug` or lower, error stack traces will be included in log output.
176 |
177 | Stack traces will not be captured when the log level is greater than `.debug`. To override this behavior, set `StackTrace.isCaptureEnabled` manually in `configure`.
178 |
179 | ```swift
180 | // Always capture stack traces, regardless of log level.
181 | StackTrace.isCaptureEnabled = true
182 | ```
183 |
184 | ## Error Middleware
185 |
186 | `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.
187 |
188 | 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.
189 |
190 | ```swift
191 | // Remove all existing middleware.
192 | app.middleware = .init()
193 | // Add custom error handling middleware first.
194 | app.middleware.use(MyErrorMiddleware())
195 | ```
196 |
197 | Very few middleware should go _before_ the error handling middleware. A notable exception to this rule is `CORSMiddleware`.
198 |
--------------------------------------------------------------------------------
/4.0/docs/basics/logging.md:
--------------------------------------------------------------------------------
1 | # Logging
2 |
3 | Vapor 的 `Logging` API 是基于 Apple 的 [SwiftLog](https://github.com/apple/swift-log) 而构建。意味着 Vapor 兼容所有基于 `SwiftLog` 实现的[后端框架](https://github.com/apple/swift-log#backends)。
4 |
5 | ## Logger
6 |
7 | `Logger` 的实例用于输出日志消息,Vapor 提供了一些便捷的方法使用日志记录器。
8 |
9 | ### Request
10 |
11 | 每个传入 `Request` 都有一个单独的日志记录器,你可以在该请求中使用任何类型日志。
12 |
13 | ```swift
14 | app.get("hello") { req -> String in
15 | req.logger.info("Hello, logs!")
16 | return "Hello, world!"
17 | }
18 | ```
19 |
20 | 请求的日志记录器都有一个单独的`UUID`用于标识该请求,便于追踪该日志。
21 |
22 | ```
23 | [ INFO ] Hello, logs! [request-id: C637065A-8CB0-4502-91DC-9B8615C5D315] (App/routes.swift:10)
24 | ```
25 |
26 | !!! info
27 | 日志记录器的元数据仅在调试日志级别或者更低级别显示。
28 |
29 |
30 | ### 应用
31 |
32 | 关于应用程序启动和配置过程中的日志消息,可以使用 `Application` 的日志记录器:
33 |
34 | ```swift
35 | app.logger.info("Setting up migrations...")
36 | app.migrations.use(...)
37 | ```
38 |
39 | ### 自定义日志记录器
40 |
41 | 在无法访问 `Application` 或者 `Request` 情况下,你可以初始化一个新的 `Logger`。
42 |
43 | ```swift
44 | let logger = Logger(label: "dev.logger.my")
45 | logger.info(...)
46 | ```
47 |
48 | 尽管自定义的日志记录器仍将输出你配置的后端日志记录,但是他们没有附带重要的元数据,比如 `request` 的 `UUID`。所以尽量使用 `application` 或者 `request` 的日志记录器。
49 |
50 | ## 日志级别
51 |
52 | `SwiftLog` 支持多种日志级别。
53 |
54 |
55 | |名称|说明|
56 | |-|-|
57 | |trace|用户级基本输出信息|
58 | |debug|用户级调试信息|
59 | |info|用户级重要信息|
60 | |notice|表明会出现非错误的情形,需要关注处理|
61 | |warning|表明会出现潜在错误的情形,比 `notice` 的消息严重|
62 | |error|指出发生错误事件,但不影响系统的继续运行|
63 | |critical|系统级危险,需要立即关注错误信息并处理|
64 |
65 | 出现 `critical` 消息时,日志框架可以自由的执行权限更重的操作来捕获系统状态(比如捕获跟踪堆栈)以方便调试。
66 |
67 | 默认情况下,Vapor 使用 `info` 级别日志。当运行在 `production` 环境时,将使用 `notice` 提高性能。
68 |
69 | ### 修改日志级别
70 |
71 | 不管环境模式如何,你都可以通过修改日志级别来增加或减少生成的日志数量。
72 |
73 | 第一种方法,在启动应用程序时传递可选参数 `--log` 标志:
74 |
75 | ```sh
76 | vapor run serve --log debug
77 | ```
78 |
79 | 第二种方法,通过设置 `LOG_LEVEL` 环境变量:
80 |
81 | ```sh
82 | export LOG_LEVEL=debug
83 | vapor run serve
84 | ```
85 |
86 | 这两种方法可以在 Xcode 中编辑 `Run` (scheme)模式进行修改。
87 |
88 | ## 配置
89 |
90 | `SwiftLog` 可以通过每次进程启动 `LoggingSystem` 时进行配置。Vapor 项目通常在 `main.swift` 执行操作。
91 |
92 | ```swift
93 | import Vapor
94 |
95 | var env = try Environment.detect()
96 | try LoggingSystem.bootstrap(from: &env)
97 | ```
98 |
99 | `bootstrap(from:)` 是 Vapor 提供的调用方法,它将基于命令行参数和环境变量来配置默认日志处理操作。默认的日志处理操作支持使用 ANSI 颜色将消息输出到终端。
100 |
101 | ### 自定义操作
102 |
103 | 你可以覆盖 Vapor 的默认日志处理并注册自己的日志处理操作。
104 |
105 | ```swift
106 | import Logging
107 |
108 | LoggingSystem.bootstrap { label in
109 | StreamLogHandler.standardOutput(label: label)
110 | }
111 | ```
112 |
113 | 所有 SwiftLog 支持的后端框架均可与 Vapor 一起工作。但是,使用命令行参数和环境变量更改日志级别只支持 Vapor 的默认日志处理操作。
114 |
--------------------------------------------------------------------------------
/4.0/docs/basics/routing.md:
--------------------------------------------------------------------------------
1 | # 路由
2 |
3 | 路由是为到来的请求(incoming requetst)找到合适的请求处理程序(request handler)的过程。Vapor 路由的核心是基于 [RoutingKit](https://github.com/vapor/routing-kit) 的高性能 trie-node 路由器。
4 |
5 | ## 概述
6 |
7 | 要了解路由在 Vapor 中的工作方式,你首先应该了解有关 HTTP 请求的一些基础知识。
8 | 看一下以下示例请求:
9 |
10 | ```http
11 | GET /hello/vapor HTTP/1.1
12 | host: vapor.codes
13 | content-length: 0
14 | ```
15 |
16 | 这是对 URL `/hello/vapor` 的一个简单的 HTTP 请求。 如果你将其指向以下 URL,则浏览器将发出这样的 HTTP 请求:
17 |
18 | ```
19 | http://vapor.codes/hello/vapor
20 | ```
21 |
22 | ### HTTP 方法
23 |
24 | 请求的第一部分是 HTTP 方法。其中 GET 是最常见的 HTTP 方法,以下这些是经常会使用几种方法,这些 HTTP 方法通常与 [CRUD](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete) 语义相关联。
25 |
26 |
27 | |Method|CURD|
28 | |:--|:--|
29 | |`GET`|Read|
30 | |`POST`|Create|
31 | |`PUT`|Replace|
32 | |`PATCH`|Update|
33 | |`DELETE`|Delete|
34 |
35 |
36 | ### 请求路径
37 |
38 | 在 HTTP 方法之后是请求的 URI。它由以 `/` 开头的路径和在 `?` 之后的可选查询字符串组成。HTTP 方法和路径是 Vapor 用于路由请求的方法。
39 |
40 | URI 之后是 HTTP 版本,后跟零个或多个标头,最后是正文。由于这是一个 `GET` 请求,因此没有主体(body)。
41 |
42 |
43 | ### 路由方法
44 |
45 | 让我们看一下如何在 Vapor 中处理此请求。
46 |
47 | ```swift
48 | app.get("hello", "vapor") { req in
49 | return "Hello, vapor!"
50 | }
51 | ```
52 |
53 |
54 | 所有常见的 HTTP 方法都可以作为 `Application` 的方法使用。它们接受一个或多个字符串参数,这些字符串参数表示请求路径,以 `/` 分隔。
55 |
56 | 请注意,你也可以在方法之后使用 `on` 编写此代码。
57 |
58 |
59 | ```swift
60 | app.on(.GET, "hello", "vapor") { ... }
61 | ```
62 |
63 | 注册此路由后,上面的示例 HTTP 请求将导致以下 HTTP 响应。
64 |
65 | ```http
66 | HTTP/1.1 200 OK
67 | content-length: 13
68 | content-type: text/plain; charset=utf-8
69 |
70 | Hello, vapor!
71 | ```
72 |
73 | ### 路由参数
74 |
75 | 现在,我们已经成功地基于 HTTP 方法和路径路由了请求,让我们尝试使路径动态化。注意,名称 “vapor” 在路径和响应中都是硬编码的。让我们对它进行动态化,以便你可以访问 `/hello/` 并获得响应。
76 |
77 |
78 | ```swift
79 | app.get("hello", ":name") { req -> String in
80 | let name = req.parameters.get("name")!
81 | return "Hello, \(name)!"
82 | }
83 | ```
84 |
85 | 通过使用前缀为 `:` 的路径组件,我们向路由器指示这是动态组件。现在,此处提供的任何字符串都将与此路由匹配。 然后,我们可以使用 `req.parameters` 访问字符串的值。
86 |
87 | 如果再次运行示例请求,你仍然会收到一条响应,向 vapor 打招呼。 但是,你现在可以在 `/hello/` 之后添加任何名称,并在响应中看到它。 让我们尝试 `/hello/swift`。
88 |
89 |
90 | ```http
91 | GET /hello/swift HTTP/1.1
92 | content-length: 0
93 | ```
94 | ```http
95 | HTTP/1.1 200 OK
96 | content-length: 13
97 | content-type: text/plain; charset=utf-8
98 |
99 | Hello, swift!
100 | ```
101 |
102 | 现在你已经了解了基础知识,请查看每个部分以了解有关参数,分组等的更多信息。
103 |
104 | ## 路径
105 |
106 | 路由为给定的 HTTP 方法和 URI 路径指定请求处理程序(request handler)。它还可以存储其他元数据。
107 |
108 | ### 方法
109 |
110 | 可以使用多种 HTTP 方法帮助程序将路由直接注册到你的 `Application` 。
111 |
112 | ```swift
113 | // responds to GET /foo/bar/baz
114 | app.get("foo", "bar", "baz") { req in
115 | ...
116 | }
117 | ```
118 |
119 | 路由处理程序支持返回 `ResponseEncodable` 的任何内容。这包括 `Content`,一个 `async` 闭包,以及未来值为 `ResponseEncodable` 的 `EventLoopFuture`。
120 |
121 | 你可以在 `in` 之前使用 `-> T` 来指定路线的返回类型。这在编译器无法确定返回类型的情况下很有用。
122 |
123 | ```swift
124 | app.get("foo") { req -> String in
125 | return "bar"
126 | }
127 | ```
128 |
129 | 这些是受支持的路由器方法:
130 |
131 | - `get`
132 | - `post`
133 | - `patch`
134 | - `put`
135 | - `delete`
136 |
137 | 除了 HTTP 方法协助程序外,还有一个 `on` 函数可以接受 HTTP 方法作为输入参数。
138 |
139 | ```swift
140 | // responds to OPTIONS /foo/bar/baz
141 | app.on(.OPTIONS, "foo", "bar", "baz") { req in
142 | ...
143 | }
144 | ```
145 |
146 | ### 路径组件
147 |
148 | 每种路由注册方法都接受 `PathComponent` 的可变列表。此类型可以用字符串文字表示,并且有四种情况:
149 |
150 |
151 | - 常量 (`foo`)
152 | - 参数路径 (`:foo`)
153 | - 任何路径 (`*`)
154 | - 通配路径 (`**`)
155 |
156 | #### 常量
157 |
158 | 这是静态路由组件。仅允许在此位置具有完全匹配的字符串的请求。
159 |
160 | ```swift
161 | // responds to GET /foo/bar/baz
162 | app.get("foo", "bar", "baz") { req in
163 | ...
164 | }
165 | ```
166 |
167 | #### 参数路径
168 |
169 | 这是一个动态路由组件。此位置的任何字符串都将被允许。参数路径组件以 `:` 前缀指定。`:` 后面的字符串将用作参数名称。你可以使用该名称稍后从请求中获取参数值。
170 |
171 | ```swift
172 | // responds to GET /foo/bar/baz
173 | // responds to GET /foo/qux/baz
174 | // ...
175 | app.get("foo", ":bar", "baz") { req in
176 | ...
177 | }
178 | ```
179 |
180 | #### 任何路径
181 |
182 | 除了丢弃值之外,这与参数路径非常相似。此路径组件仅需指定为 `*` 。
183 |
184 | ```swift
185 | // responds to GET /foo/bar/baz
186 | // responds to GET /foo/qux/baz
187 | // ...
188 | app.get("foo", "*", "baz") { req in
189 | ...
190 | }
191 | ```
192 |
193 | #### 通配路径
194 |
195 | 这是与一个或多个组件匹配的动态路由组件,仅使用 `**` 指定。请求中将允许匹配此位置或更高位置的任何字符串。
196 |
197 | ```swift
198 | // responds to GET /foo/bar
199 | // responds to GET /foo/bar/baz
200 | // ...
201 | app.get("foo", "**") { req in
202 | ...
203 | }
204 | ```
205 |
206 | ### 参数
207 |
208 | 使用参数路径组件(以 `:` 前缀)时,该位置的 URI 值将存储在 `req.parameters` 中。 你可以使用路径组件中的名称来访问。
209 |
210 |
211 | ```swift
212 | // responds to GET /hello/foo
213 | // responds to GET /hello/bar
214 | // ...
215 | app.get("hello", ":name") { req -> String in
216 | let name = req.parameters.get("name")!
217 | return "Hello, \(name)!"
218 | }
219 | ```
220 |
221 | !!! 提示
222 | 我们可以确定 `req.parameters.get` 在这里绝不会返回 `nil` ,因为我们的路径包含 `:name`。 但是,如果要访问中间件中的路由参数或由多个路由触发的代码中的路由参数,则需要处理 `nil` 的可能性。
223 |
224 | `req.parameters.get` 还支持将参数自动转换为 `LosslessStringConvertible` 类型。
225 |
226 |
227 | ```swift
228 | // responds to GET /number/42
229 | // responds to GET /number/1337
230 | // ...
231 | app.get("number", ":x") { req -> String in
232 | guard let int = req.parameters.get("x", as: Int.self) else {
233 | throw Abort(.badRequest)
234 | }
235 | return "\(int) is a great number"
236 | }
237 | ```
238 |
239 | ### Body 数据流
240 |
241 | 当使用 `on` 方法注册一个路由时,你可以设置 request body 应该如何被处理。默认情况下,request bodies 被收集到内存中在调用你的 handler 之前。这很有用,因为它允许同步解码请求内容,即使您的应用程序异步读取传入请求。
242 |
243 | 默认情况下,Vapor 将会限制 streaming body collection 的大小为16KB,你可以使用 `app.routes` 来配置它。
244 |
245 | ```swift
246 | // Increases the streaming body collection limit to 500kb
247 | app.routes.defaultMaxBodySize = "500kb"
248 | ```
249 | 如果收集到的 streaming body 大小超过了配置的限制,`413 Payload Too Large` 错误将会被抛出。
250 |
251 | 使用 `body` 参数来为一个单独的路由设置 request body 收集策略。
252 |
253 | ```swift
254 | // Collects streaming bodies (up to 1mb in size) before calling this route.
255 | app.on(.POST, "listings", body: .collect(maxSize: "1mb")) { req in
256 | // Handle request.
257 | }
258 | ```
259 | 如果一个 `maxSize` 被传到 `collect`,它将会覆盖应用的默认配置对这个路由。如果要使用应用的默认配置,请忽略 `maxSize` 参数.
260 |
261 | 对于像文件上传这样的大请求,在缓冲区中收集 request body 可能会占用系统内存。为了防止 request body 收集,使用 `stream` 策略。
262 |
263 | ```swift
264 | // Request body will not be collected into a buffer.
265 | app.on(.POST, "upload", body: .stream) { req in
266 | ...
267 | }
268 | ```
269 | 当请 request body 被流处理时,`req.body.data` 会是 `nil`, 你必须使用 `req.body.drain` 来处理每个被发送到你的路由数据块。
270 |
271 | ### 大小写敏感
272 |
273 | 路由的默认行为是区分大小写和保留大小写的。
274 | 若想不区分大小写方式处理`常量`路径组件;启用此行为,请在应用程序启动之前进行配置:
275 | ```swift
276 | app.routes.caseInsensitive = true
277 | ```
278 | 不需要对原始请求未做任何更改,路由处理程序将接收未经修改的请求路由。
279 |
280 |
281 | ### 查看路由
282 |
283 | 你可以通过 making `Routes` 服务或使用 `app.routes` 来访问应用程序的路由。
284 |
285 | ```swift
286 | print(app.routes.all) // [Route]
287 | ```
288 |
289 | Vapor 还附带了一个 `routes` 命令,该命令以 ASCII 格式的表格打印所有可用的路由。
290 |
291 | ```sh
292 | $ swift run Run routes
293 | +--------+----------------+
294 | | GET | / |
295 | +--------+----------------+
296 | | GET | /hello |
297 | +--------+----------------+
298 | | GET | /todos |
299 | +--------+----------------+
300 | | POST | /todos |
301 | +--------+----------------+
302 | | DELETE | /todos/:todoID |
303 | +--------+----------------+
304 | ```
305 |
306 | ### Metadata
307 |
308 | 所有路线注册方法都会返回创建的 `Route`。 这使你可以将元数据添加到路由的 `userInfo` 字典中。有一些默认方法可用,例如添加描述。
309 |
310 |
311 | ```swift
312 | app.get("hello", ":name") { req in
313 | ...
314 | }.description("says hello")
315 | ```
316 |
317 | ## 路由组
318 |
319 | 通过路由分组,你可以创建带有路径前缀或特定中间件的一组路由。分组支持基于构建器和闭包的语法。
320 |
321 | 所有分组方法都返回一个 `RouteBuilder` ,这意味着你可以将组与其他路由构建方法无限地混合、匹配和嵌套。
322 |
323 | ### 路径前缀
324 |
325 | 路径前缀路由组允许你在一个路由组之前添加一个或多个路径组件。
326 |
327 | ```swift
328 | let users = app.grouped("users")
329 | // GET /users
330 | users.get { req in
331 | ...
332 | }
333 | // POST /users
334 | users.post { req in
335 | ...
336 | }
337 | // GET /users/:id
338 | users.get(":id") { req in
339 | let id = req.parameters.get("id")!
340 | ...
341 | }
342 | ```
343 |
344 | 你可以传递给诸如 `get` 或 `post` 这样的方法的任何路径成分都可以传递给 `grouped`。 还有另一种基于闭包的语法。
345 |
346 | ```swift
347 | app.group("users") { users in
348 | // GET /users
349 | users.get { req in
350 | ...
351 | }
352 | // POST /users
353 | users.post { req in
354 | ...
355 | }
356 | // GET /users/:id
357 | users.get(":id") { req in
358 | let id = req.parameters.get("id")!
359 | ...
360 | }
361 | }
362 | ```
363 |
364 | 嵌套路径前缀路由组使你可以简洁地定义 CRUD API。
365 |
366 | ```swift
367 | app.group("users") { users in
368 | // GET /users
369 | users.get { ... }
370 | // POST /users
371 | users.post { ... }
372 |
373 | users.group(":id") { user in
374 | // GET /users/:id
375 | user.get { ... }
376 | // PATCH /users/:id
377 | user.patch { ... }
378 | // PUT /users/:id
379 | user.put { ... }
380 | }
381 | }
382 | ```
383 |
384 | ### 中间件
385 |
386 | 除了为路径组件添加前缀之外,你还可以将中间件添加到路由组。
387 |
388 | ```swift
389 | app.get("fast-thing") { req in
390 | ...
391 | }
392 | app.group(RateLimitMiddleware(requestsPerMinute: 5)) { rateLimited in
393 | rateLimited.get("slow-thing") { req in
394 | ...
395 | }
396 | }
397 | ```
398 |
399 | 这对于使用不同的身份验证中间件保护路由的子集特别有用。
400 |
401 | ```swift
402 | app.post("login") { ... }
403 | let auth = app.grouped(AuthMiddleware())
404 | auth.get("dashboard") { ... }
405 | auth.get("logout") { ... }
406 | ```
407 |
408 | ## 重定向
409 |
410 | 重定向在很多场景中很有用,像转发旧页面到新页面为了 SEO,把未认证的用户转发到登录页面或保持与API的新版本的向后兼容性。
411 |
412 | 要转发一个请求,请用:
413 |
414 | ```swift
415 | req.redirect(to: "/some/new/path")
416 | ```
417 |
418 | 你可以设置重定向的类型,比如说永久的重定向一个页面(来使你的 SEO 正确的更新),请使用:
419 |
420 | ```swift
421 | req.redirect(to: "/some/new/path", type: .permanent)
422 | ```
423 |
424 | 不同的 `RedirectType` 有:
425 |
426 | * `.permanent` - 返回一个 **301 Permanent** 重定向。
427 | * `.normal` - 返回一个 **303 see other** 重定向。这是 Vapor 的默认行为,来告诉客户端去使用一个 **GET** 请求来重定向。
428 | * `.temporary` - 返回一个 **307 Temporary** 重定向. 这告诉客户端保留请求中使用的HTTP方法。
429 |
430 | > 要选择正确的重定向状态码,请参考 [the full list](https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#3xx_redirection)
--------------------------------------------------------------------------------
/4.0/docs/basics/validation.md:
--------------------------------------------------------------------------------
1 | # Validation API
2 |
3 | Vapor 的 **Validation API** 可帮助你在使用 [Content](content.md) API 解码数据之前,对传入的请求进行验证。
4 |
5 | ## 介绍
6 |
7 | Vapor 对 Swift 的类型安全的`可编码`协议进行了深度集成,这意味着与动态类型的语言相比,你无需担心数据验证。但是,出于某些原因,你可能想选择使用 **Validation API** 进行显式验证。
8 |
9 |
10 | ### 语义可读错误
11 |
12 | 如果取得的数据无效,使用 [Content](content.md) API 对其解码将产生错误。但是,这些错误消息有时可能缺乏可读性。例如,采用以下字符串支持的枚举:
13 |
14 | ```swift
15 | enum Color: String, Codable {
16 | case red, blue, green
17 | }
18 | ```
19 |
20 | 如果用户尝试将字符串“purple”传递给“Color”类型的属性,则将收到类似于以下内容的错误:
21 |
22 | ```
23 | Cannot initialize Color from invalid String value purple for key favoriteColor
24 | ```
25 |
26 | 尽管此错误在技术上是正确的,并且可以成功地保护端点免受无效值的影响,但它可以更好地通知用户该错误以及可用的选项。通过使用 **Validation API**,你可以生成类似以下的错误:
27 |
28 | ```
29 | favoriteColor is not red, blue, or green
30 | ```
31 |
32 | 此外,一旦遇到第一个错误,`Codable` 将停止尝试解码。这意味着即使请求中有许多无效属性,用户也只会看到第一个错误。 **Validation API** 将在单个请求中抛出所有的验证失败信息。
33 |
34 | ### 特殊验证
35 |
36 | `Codable` 可以很好地处理类型验证,但是有时候你还想要更多的验证方式。例如,验证字符串的内容或验证整数的大小。**Validation API** 具有此类验证器,可帮助验证电子邮件、字符集、整数范围等数据。
37 |
38 | ## 验证
39 |
40 | 为了验证请求,你需要生成一个 `Validations` 集合。最常见的做法是使现有类型继承 **Validatable**。
41 |
42 | 让我们看一下如何向这个简单的 `POST/users` 请求添加验证。本指南假定你已经熟悉 [Content](content.md) API。
43 |
44 |
45 | ```swift
46 | enum Color: String, Codable {
47 | case red, blue, green
48 | }
49 |
50 | struct CreateUser: Content {
51 | var name: String
52 | var username: String
53 | var age: Int
54 | var email: String
55 | var favoriteColor: Color?
56 | }
57 |
58 | app.post("users") { req -> CreateUser in
59 | let user = try req.content.decode(CreateUser.self)
60 | // Do something with user.
61 | return user
62 | }
63 | ```
64 |
65 | ### 添加验证
66 |
67 | 第一步是在你要解码的类型(在本例中为 CreateUser)继承 **Validatable** 协议并实现 `validations` 静态方法,可在 `extension` 中完成。
68 |
69 | ```swift
70 | extension CreateUser: Validatable {
71 | static func validations(_ validations: inout Validations) {
72 | // Validations go here.
73 | }
74 | }
75 | ```
76 |
77 | 验证`CreateUser`后,将调用静态方法 `validations(_ :)`。你要执行的所有验证都应添加到 **Validations** 集合中。让我们添加一个简单的验证,以验证用户的电子邮件是否有效。
78 |
79 | ```swift
80 | validations.add("email", as: String.self, is: .email)
81 | ```
82 |
83 | 第一个参数是参数值的预期键,在本例中为`email`。这应与正在验证的类型上的属性名称匹配。第二个参数`as`是预期的类型,在这种情况下为`String`。该类型通常与属性的类型匹配。最后,可以在第三个参数`is`之后添加一个或多个验证器。在这种情况下,我们添加一个验证器,以检查该值是否为电子邮件地址。
84 |
85 |
86 | ### 验证请求的 `Content`
87 |
88 | 当你的数据类型继承了 **Validatable**,就可以使用 `validate(content:)` 静态方法来验证请求的 `content`。在路由处理程序中 `req.content.decode(CreateUser.self)` 之前添加以下行:
89 |
90 | ```swift
91 | try CreateUser.validate(content: req)
92 | ```
93 |
94 | 现在,尝试发送以下包含无效电子邮件的请求:
95 |
96 | ```http
97 | POST /users HTTP/1.1
98 | Content-Length: 67
99 | Content-Type: application/json
100 |
101 | {
102 | "age": 4,
103 | "email": "foo",
104 | "favoriteColor": "green",
105 | "name": "Foo",
106 | "username": "foo"
107 | }
108 | ```
109 |
110 | 你应该能看到返回以下错误:
111 |
112 | ```
113 | email is not a valid email address
114 | ```
115 |
116 | ### 验证请求的 `Query`
117 |
118 | 当你的数据类型继承了 **Validatable**,就可以使用 `validate(query:)` 静态方法来验证请求的 `query`。在路由处理程序中添加以下行:
119 |
120 | ```swift
121 | try CreateUser.validate(query: req)
122 | req.query.decode(CreateUser.self)
123 | ```
124 |
125 | 现在,尝试发送一下包含错误的 email 在 query 的请求。
126 |
127 | ```http
128 | GET /users?age=4&email=foo&favoriteColor=green&name=Foo&username=foo HTTP/1.1
129 |
130 | ```
131 |
132 | 你将会看到下面的错误:
133 |
134 | ```
135 | email is not a valid email address
136 | ```
137 | ### 整数验证
138 |
139 | 现在让我们尝试添加一个针对整数年龄的验证:
140 |
141 | ```swift
142 | validations.add("age", as: Int.self, is: .range(13...))
143 | ```
144 |
145 | 年龄验证要求年龄大于或等于`13`。如果你尝试发送一个和上面相同的请求,现在应该会看到一个新错误:
146 |
147 | ```
148 | age is less than minimum of 13, email is not a valid email address
149 | ```
150 |
151 | ### 字符串验证
152 |
153 | 接下来,让我们添加对“名称”和“用户名”的验证。
154 |
155 | ```swift
156 | validations.add("name", as: String.self, is: !.empty)
157 | validations.add("username", as: String.self, is: .count(3...) && .alphanumeric)
158 | ```
159 |
160 |
161 | 名称验证使用 `!` 运算符将 `.empty` 验证反转。这要求该字符串不为空。
162 | 用户名验证使用`&&`组合了两个验证器。这将要求该字符串的长度至少为3个字符,并且使用 && 来包含字母数字字符。
163 |
164 |
165 | ### 枚举验证
166 |
167 | 最后,让我们看一下更高级的验证,以检查提供的`favoriteColor`是否有效:
168 |
169 | ```swift
170 | validations.add("favoriteColor", as: String.self,is: .in("red", "blue","green"),required: false)
171 |
172 | ```
173 |
174 | 由于无法从无效值中解码“颜色”,因此此验证将“字符串”用作基本类型。它使用 .in 验证器来验证该值是有效的选项:红色、蓝色或绿色。由于该值是可选的,因此将`required`设置为 false 表示如果请求数据中缺少此字段,则验证不会失败。
175 |
176 | 请注意,如果缺少此字段,则收藏夹颜色验证将通过,但如果提供 `null`,则不会通过。 如果要支持`null`,请将验证类型更改为`String?`,并使用 `.nil ||`。
177 |
178 | ```swift
179 | validations.add("favoriteColor", as: String?.self,is: .nil || .in("red", "blue", "green"),required: false)
180 | ```
181 |
182 |
183 | ## 验证器
184 |
185 | 以下是当前支持的验证器的列表,并简要说明了它们的作用:
186 |
187 | |验证方式|描述|
188 | |:--|:--|
189 | |`.ascii`|仅包含ASCII字符|
190 | |`.alphanumeric`|仅包含字母数字字符|
191 | |`.characterSet(_:)`|仅包含提供的 `CharacterSet` 中的字符|
192 | |`.count(_:)`|在提供范围内的集合计数|
193 | |`.email`|包含有效的电子邮件|
194 | |`.empty`|集合为空|
195 | |`.in(_:)`|值在提供的“集合”中|
196 | |`.nil`|值为`null`|
197 | |`.range(_:)`|值在提供的范围内|
198 | |`.url`|包含有效的URL|
199 |
200 | 验证器也可以使用运算符组合起来以构建复杂的验证:
201 |
202 | |操作符|位置|描述|
203 | |:--|:--|:--|
204 | |`!`|前面|反转验证器,要求相反|
205 | |`&&`|中间|组合两个验证器,需要同时满足|
206 | |`||`|中间|组合两个验证器,至少满足一个|
--------------------------------------------------------------------------------
/4.0/docs/deploy/digital-ocean.md:
--------------------------------------------------------------------------------
1 | # 部署到 DigitalOcean
2 |
3 | 本指南将引导你将一个简单的 Hello, world Vapor 应用程序部署到 [Droplet](https://www.digitalocean.com/products/droplets/)。要遵循本指南,你需要有一个付费的 [DigitalOcean](https://www.digitalocean.com) 帐户。
4 |
5 | ## 创建服务器
6 |
7 | 让我们从在 Linux 服务器上安装 Swift 开始。 使用创建菜单创建一个新的 Droplet。
8 |
9 | 
10 |
11 | 在发行版下,选择 Ubuntu 18.04 LTS。以下指南将以此版本为例。
12 |
13 | 
14 |
15 | !!! 注意
16 | 你也可以选择 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) 页面上查看官方支持哪些操作系统。
17 |
18 | 选择完发行版后,选择你喜欢的套餐和数据中心所在区域。然后设置一个 SSH 密钥以在创建服务器后访问它。最后, 点击创建 Droplet 并等待新服务器启动。
19 |
20 | 新服务器准备完毕后,鼠标悬停在 Droplet 的 IP 地址上,然后单击复制。
21 |
22 | 
23 |
24 | ## 初始化设置
25 |
26 | 打开你的终端,使用 SSH 通过 root 身份登录到服务器。
27 |
28 | ```sh
29 | ssh root@your_server_ip
30 | ```
31 |
32 | 在 [Ubuntu 18.04](https://www.digitalocean.com/community/tutorials/initial-server-setup-with-ubuntu-18-04) 上初始化服务器设置,DigitalOcean 提供了深入指南。 本指南将快速介绍一些基础知识。
33 |
34 | ### 配置防火墙
35 |
36 | 允许 OpenSSH 通过防火墙并且启用它。
37 |
38 | ```sh
39 | ufw allow OpenSSH
40 | ufw enable
41 | ```
42 |
43 | ### 添加用户
44 |
45 | 除了 `root` 用户在创建一个新用户。本指南创建了一个 `vapor` 用户。
46 |
47 | ```sh
48 | adduser vapor
49 | ```
50 |
51 | 允许新创建的用户使用 `sudo`。
52 |
53 | ```sh
54 | usermod -aG sudo vapor
55 | ```
56 |
57 | 复制 root 用户的 SSH 密钥到新创建的用户。允许新用户通过 SSH 登录。
58 |
59 | ```sh
60 | rsync --archive --chown=vapor:vapor ~/.ssh /home/vapor
61 | ```
62 |
63 | 最后,退出当前 SSH 会话,用新创建的用户进行登录。
64 |
65 | ```sh
66 | exit
67 | ssh vapor@your_server_ip
68 | ```
69 |
70 | ## 安装 Swift
71 |
72 | 现在你已经创建了一个新的 Ubuntu 服务器并且通过非 root 身份登录到服务器,你可以安装 Swift。
73 |
74 | ### Swift 依赖项
75 |
76 | 安装 Swift 所需要的依赖项。
77 |
78 | ```sh
79 | sudo apt-get update
80 | sudo apt-get install clang libicu-dev libatomic1 build-essential pkg-config
81 | ```
82 |
83 | ### 下载 Toolchain
84 |
85 | 本指南将安装 Swift 5.2.4。访问 [Swift Releases](https://swift.org/download/#releases) 页面获取最新版本的链接。复制 Ubuntu 18.04 的下载链接。
86 |
87 | 
88 |
89 | 下载并解压 Swift toolchain。
90 |
91 | ```sh
92 | 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
93 | tar xzf swift-5.2.4-RELEASE-ubuntu18.04.tar.gz
94 | ```
95 |
96 | !!! 注意
97 | Swift 的[使用下载指南](https://swift.org/download/#using-downloads)包含有关如何使用 PGP 签名验证下载的信息。
98 |
99 | ### 安装 Toolchain
100 |
101 | 将 Swift 移到易于访问的地方。本指南将 `/swift` 与子文件夹中的每个编译器版本一起使用。
102 |
103 | ```sh
104 | sudo mkdir /swift
105 | sudo mv swift-5.2.4-RELEASE-ubuntu18.04 /swift/5.2.4
106 | ```
107 |
108 | 将 Swift 添加到 `/usr/bin` 以便 `vapor` 和 `root` 用户可以执行。
109 |
110 | ```sh
111 | sudo ln -s /swift/5.2.4/usr/bin/swift /usr/bin/swift
112 | ```
113 |
114 | 验证 Swift 是否正确安装。
115 |
116 | ```sh
117 | swift --version
118 | ```
119 |
120 | ## 设置项目
121 |
122 | 现在已经安装了 Swift,让我们克隆并编译项目。本示例,我们使用 Vapor 的 [API 模板](https://github.com/vapor/api-template/)。
123 |
124 | 首先安装 Vapor 的系统依赖项。
125 |
126 | ```sh
127 | sudo apt-get install openssl libssl-dev zlib1g-dev libsqlite3-dev
128 | ```
129 |
130 | 允许 HTTP 通过防火墙。
131 |
132 | ```sh
133 | sudo ufw allow http
134 | ```
135 |
136 | ### 克隆和构建
137 |
138 | 现在克隆项目并构建它。
139 |
140 | ```sh
141 | git clone https://github.com/vapor/api-template.git
142 | cd api-template
143 | swift build --enable-test-discovery
144 | ```
145 |
146 | !!! 建议
147 | 如果生产环境进行构建, 请使用 `swift build -c release --enable-test-discovery`
148 |
149 | ### 运行
150 |
151 | 项目编译完成后,在服务器的 IP 端口80上运行它。本示例的 IP 地址为 `157.245.244.228`。
152 |
153 | ```sh
154 | sudo .build/debug/Run serve -b 157.245.244.228:80
155 | ```
156 |
157 | 如果你使用 `swift build -c release --enable-test-discovery`进行构建, 然后你需要运行:
158 | ```sh
159 | sudo .build/release/Run serve -b 157.245.244.228:80
160 | ```
161 |
162 | 通过浏览器或者本地终端访问服务器的 IP, 你应该会看到 “It works!”。
163 |
164 | ```
165 | $ curl http://157.245.244.228
166 | It works!
167 | ```
168 |
169 | 回到服务器上,你应该会看到测试请求的日志。
170 |
171 | ```
172 | [ NOTICE ] Server starting on http://157.245.244.228:80
173 | [ INFO ] GET /
174 | ```
175 |
176 | 使用 `CTRL+C` 退出服务器。可能需要一秒钟才能关闭。
177 |
178 | 恭喜你的 Vapor 应用程序运行在 DigitalOcean Droplet 上了!
179 |
180 | ## 下一步
181 |
182 | 本指南的其余部分指向的资源用于改进你的部署。
183 |
184 | ### Supervisor
185 |
186 | Supervisor 是一个进程控制系统,可以运行和监控你的 Vapor 可执行文件。通过设置 supervisor, 服务器启动时应用程序自动启动,并在崩溃是重新启动。了解有关 [Supervisor](../deploy/supervisor.md) 的更多信息。
187 |
188 | ### Nginx
189 |
190 | Nginx 是一个速度极快、经过实战考验并且易于配置的 HTTP 服务器和代理。虽然 Vapor 支持直接的 HTTP 请求,但 Nginx 背后的代理可以提供更高的性能、安全性和易用性。了解有关 [Nginx](../deploy/nginx.md) 的更多信息。
191 |
--------------------------------------------------------------------------------
/4.0/docs/deploy/docker.md:
--------------------------------------------------------------------------------
1 | # Docker Deploys
2 |
3 | Using Docker to deploy your Vapor app has several benefits:
4 |
5 | 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.
6 | 2. You can use docker-compose or Kubernetes manifests to orchestrate multiple services needed for a full deployment (e.g. Redis, Postgres, nginx, etc.).
7 | 3. It is easy to test your app's ability to scale horizontally, even locally on your development machine.
8 |
9 | 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.
10 |
11 | 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.
12 |
13 | 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.
14 |
15 | ## Set Up
16 |
17 | 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.
18 |
19 | ### Install Docker
20 |
21 | 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.
22 |
23 | ### Generate Template
24 |
25 | 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.
26 |
27 | 1. Install or build the Vapor Toolbox ([macOS](../install/macos.md#install-toolbox), [Ubuntu](../install/ubuntu.md#install-toolbox)).
28 | 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.
29 |
30 | ## Docker Resources
31 |
32 | 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.
33 |
34 | The template Vapor App has two key Docker-specific resources: A **Dockerfile** and a **docker-compose** file.
35 |
36 | ### Dockerfile
37 |
38 | 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.
39 |
40 | The Dockerfile generated for your Vapor App has two stages.
41 |
42 | 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.
43 | ```docker
44 | # ================================
45 | # Build image
46 | # ================================
47 | FROM vapor/swift:5.2 as build
48 | WORKDIR /build
49 |
50 | # Copy entire repo into container
51 | COPY . .
52 |
53 | # Compile with optimizations
54 | RUN swift build \
55 | --enable-test-discovery \
56 | -c release \
57 | -Xswiftc -g
58 | ```
59 |
60 | 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.
61 | ```docker
62 | # ================================
63 | # Run image
64 | # ================================
65 | FROM vapor/ubuntu:18.04
66 | WORKDIR /run
67 |
68 | # Copy build artifacts
69 | COPY --from=build /build/.build/release /run
70 | # Copy Swift runtime libraries
71 | COPY --from=build /usr/lib/swift/ /usr/lib/swift/
72 | # Uncomment the next line if you need to load resources from the `Public` directory
73 | #COPY --from=build /build/Public /run/Public
74 | ```
75 |
76 | Finally, the Dockerfile defines an entrypoint and a default command to run.
77 | ```docker
78 | ENTRYPOINT ["./Run"]
79 | CMD ["serve", "--env", "production", "--hostname", "0.0.0.0"]
80 | ```
81 |
82 | 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`.
83 |
84 | ### Docker Compose File
85 |
86 | 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.
87 |
88 | !!! note
89 | 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.
90 |
91 | 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`.
92 |
93 | Note that your Docker Compose file has some shared environment variables near the top.
94 | ```docker
95 | x-shared_environment: &shared_environment
96 | LOG_LEVEL: ${LOG_LEVEL:-debug}
97 | DATABASE_HOST: db
98 | DATABASE_NAME: vapor_database
99 | DATABASE_USERNAME: vapor_username
100 | DATABASE_PASSWORD: vapor_password
101 | ```
102 |
103 | You will see these pulled into multiple services below with the `<<: *shared_environment` YAML reference syntax.
104 |
105 | 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.
106 |
107 | !!! note
108 | 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:
109 |
110 | ```
111 | DATABASE_USERNAME: ${DATABASE_USERNAME}
112 | ```
113 |
114 | This passes the environment variable through to the containers as-defined by the host.
115 |
116 | Other things to take note of:
117 |
118 | - Service dependencies are defined by `depends_on` arrays.
119 | - Service ports are exposed to the system running the services with `ports` arrays (formatted as `:`).
120 | - 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'`.
121 | - 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.
122 | - 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.
123 |
124 | ## Building
125 |
126 | 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.
127 |
128 | To build a Docker image for your app, run
129 | ```shell
130 | docker-compose build
131 | ```
132 | from the root directory of your app's project (the folder containing `docker-compose.yml`).
133 |
134 | 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.
135 |
136 | When it is done, you will find your app's image when running
137 | ```shell
138 | docker image ls
139 | ```
140 |
141 | ## Running
142 |
143 | 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.
144 |
145 | ### Standalone
146 |
147 | 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.
148 |
149 | First, execute
150 | ```shell
151 | docker-compose up app
152 | ```
153 | and notice that both the `app` and `db` services are started.
154 |
155 | 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**.
156 |
157 | 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.
158 |
159 | Visit `http://localhost:8080` and you will see `It works!` but visit `http://localhost:8080/todos` and you will get:
160 | ```
161 | {"error":true,"reason":"Something went wrong."}
162 | ```
163 |
164 | Take a peak at the logs output in the terminal where you ran `docker-compose up app` and you will see
165 | ```
166 | [ ERROR ] relation "todos" does not exist
167 | ```
168 |
169 | 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
170 | ```shell
171 | docker-compose up --detach app
172 | ```
173 |
174 | Now your app is going to start up "detached" (in the background). You can verify this by running
175 | ```shell
176 | docker container ls
177 | ```
178 | where you will see both the database and your app running in containers. You can even check on the logs by running
179 | ```shell
180 | docker logs
181 | ```
182 |
183 | To run migrations, execute
184 | ```shell
185 | docker-compose up migrate
186 | ```
187 |
188 | 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.
189 |
190 | #### Log Levels
191 |
192 | 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.
193 |
194 | You can bring your services up with
195 | ```shell
196 | LOG_LEVEL=trace docker-compose up app
197 | ```
198 | 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).
199 |
200 | #### All Service Logs
201 |
202 | If you explicitly specify your database service when you bring containers up then you will see logs for both your database and your app.
203 | ```shell
204 | docker-compose up app db
205 | ```
206 |
207 | #### Bringing Standalone Containers Down
208 |
209 | 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
210 | ```shell
211 | docker container stop
212 | ```
213 | but the easiest way to bring these particular containers down is
214 | ```shell
215 | docker-compose down
216 | ```
217 |
218 | #### Wiping The Database
219 |
220 | 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.
221 |
222 | You can remove the `db_data` volume at the same time as bringing your containers down with
223 | ```shell
224 | docker-compose down --volumes
225 | ```
226 |
227 | 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.
228 |
229 | You can remove these volumes one at a time with e.g.
230 | ```shell
231 | docker volume rm my-dockerized-app_db_data
232 | ```
233 |
234 | You can also clean up all volumes with
235 | ```shell
236 | docker volume prune
237 | ```
238 |
239 | Just be careful you don't accidentally prune a volume with data you wanted to keep around!
240 |
241 | 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`.
242 |
243 | ### Swarm Mode
244 |
245 | 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/).
246 |
247 | The first thing we need is a manager node for our Swarm. Run
248 | ```shell
249 | docker swarm init
250 | ```
251 |
252 | Next we will use our Docker Compose file to bring up a stack named `'test'` containing our services
253 | ```shell
254 | docker stack deploy -c docker-compose.yml test
255 | ```
256 |
257 | We can see how our services are doing with
258 | ```shell
259 | docker service ls
260 | ```
261 |
262 | You should expect to see `1/1` replicas for your `app` and `db` services and `0/0` replicas for your `migrate` and `revert` services.
263 |
264 | We need to use a different command to run migrations in Swarm mode.
265 | ```shell
266 | docker service scale --detach test_migrate=1
267 | ```
268 |
269 | !!! note
270 | 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`.
271 |
272 | 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.
273 |
274 | If you want to run 5 instances of your app concurrently, execute
275 | ```shell
276 | docker service scale test_app=5
277 | ```
278 |
279 | In addition to watching docker scale your app up, you can see that 5 replicas are indeed running by again checking `docker service ls`.
280 |
281 | You can view (and follow) the logs for your app with
282 | ```shell
283 | docker service logs -f test_app
284 | ```
285 |
286 | #### Bringing Swarm Services Down
287 |
288 | When you want to bring your services down in Swarm Mode, you do so by removing the stack you created earlier.
289 | ```shell
290 | docker stack rm test
291 | ```
292 |
293 | ## Production Deploys
294 |
295 | 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.).
296 |
297 | 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.
298 |
299 | 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.
300 |
301 | 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.
302 |
--------------------------------------------------------------------------------
/4.0/docs/deploy/heroku.md:
--------------------------------------------------------------------------------
1 | # Heroku 是什么
2 |
3 | Heroku 是一个一站式程序托管平台,你可以通过[heroku.com](https://www.heroku.com)获取更多信息
4 |
5 | ## 注册
6 |
7 | 你需要一个 heroku 帐户,如果你还没有,请通过此链接注册:[https://signup.heroku.com/](https://signup.heroku.com/)
8 |
9 | ## 安装命令行应用
10 |
11 | 请确保你已安装 heroku 命令行工具
12 |
13 | ### HomeBrew
14 |
15 | ```bash
16 | brew install heroku/brew/heroku
17 | ```
18 |
19 | ### 其他安装方式
20 |
21 | 在此处查看其他安装选项: [https://devcenter.heroku.com/articles/heroku-cli#download-and-install](https://devcenter.heroku.com/articles/heroku-cli#download-and-install).
22 |
23 | ### 登录
24 |
25 | 安装命令行工具后,使用以下命令登录:
26 |
27 | ```bash
28 | heroku login
29 | ```
30 |
31 | 查看当前登录的 heroku 电子邮件账户:
32 |
33 | ```bash
34 | heroku auth:whoami
35 | ```
36 |
37 | ### 创建一个应用
38 |
39 | 通过访问 heroku.com 来访问你的帐户,然后从右上角的下拉菜单中创建一个新应用程序。Heroku 会问一些问题,例如区域和应用程序名称,只需按照提示操作即可。
40 |
41 | ### Git
42 |
43 | Heroku 使用 Git 来部署你的应用程序,因此你需要将你的项目放入 Git 存储库(如果还没有的话)。
44 |
45 | #### 初始化 Git
46 |
47 | 如果你需要将 Git 添加到你的项目中,在终端中输入以下命令:
48 |
49 | ```bash
50 | git init
51 | ```
52 |
53 | #### Master
54 |
55 | 默认情况下,Heroku 部署 **master** 分支。 确保在推送之前将所有更改都加入此分支。
56 |
57 | 通过以下命令检查你当前的分支:
58 |
59 | ```bash
60 | git branch
61 | ```
62 |
63 | 星号表示当前分支。
64 |
65 | ```bash
66 | * master
67 | commander
68 | other-branches
69 | ```
70 |
71 | > **提示**:如果你没有看到任何输出并且你刚刚执行了 `git init`。 你需要先提交(commit)你的代码,然后你会看到 `git branch` 命令的输出。
72 |
73 |
74 | 如果你当前 _不在_ **master** 上,请输入以下命令来切换:
75 |
76 | ```bash
77 | git checkout master
78 | ```
79 |
80 | #### 提交更改
81 |
82 | 如果此命令有输出,那么你有未提交的改动。
83 |
84 | ```bash
85 | git status --porcelain
86 | ```
87 |
88 | 通过以下命令来提交
89 |
90 | ```bash
91 | git add .
92 | git commit -m "a description of the changes I made"
93 | ```
94 |
95 | #### 与 Heroku 进行连接
96 |
97 | 将你的应用与 heroku 连接(替换为你的应用名称)。
98 |
99 | ```bash
100 | $ heroku git:remote -a your-apps-name-here
101 | ```
102 |
103 | ### 设置运行包(Buildpack)
104 |
105 | 设置运行包来告知 heroku 如何处理 Vapor。
106 |
107 | ```bash
108 | heroku buildpacks:set vapor/vapor
109 | ```
110 |
111 | ### Swift 版本文件
112 |
113 | 我们添加的运行包会查找 **.swift-version** 文件以了解要使用的 swift 版本。 (将 5.2.1 替换为你的项目需要的任何版本。)
114 |
115 | ```bash
116 | echo "5.2.1" > .swift-version
117 | ```
118 |
119 | 这将创建 **.swift-version** ,内容为 `5.2.1`。
120 |
121 |
122 | ### Procfile
123 |
124 | Heroku 使用 **Procfile** 来知道如何运行你的应用程序,在我们的示例中它需要这样配置:
125 |
126 | ```
127 | web: Run serve --env production --hostname 0.0.0.0 --port $PORT
128 | ```
129 |
130 | 我们可以使用以下终端命令来创建它
131 |
132 | ```bash
133 | echo "web: Run serve --env production" \
134 | "--hostname 0.0.0.0 --port \$PORT" > Procfile
135 | ```
136 |
137 | ### 提交更改
138 |
139 | 我们刚刚只是更改了这些文件,但它们没有被提交。 如果我们推送(push),heroku 将无法看到这些更改。
140 |
141 | 使用以下命令提交它们。
142 |
143 | ```bash
144 | git add .
145 | git commit -m "adding heroku build files"
146 | ```
147 |
148 | ### 部署到 Heroku
149 |
150 | 你已准备好开始部署,从终端运行以下命令。 构建过程可能会需要一些时间,不必担心。
151 |
152 | ```none
153 | git push heroku master
154 | ```
155 |
156 | ### 扩展
157 |
158 | 成功构建后,你需要添加至少一台服务器,一个网站服务是免费的,你可以通过以下方式获得它:
159 |
160 | ```bash
161 | heroku ps:scale web=1
162 | ```
163 |
164 | ### 继续部署
165 |
166 | 当你想更新时只需将最新的更改推入 master 分支并推送到 heroku,它就会重新部署。
167 |
168 | ## Postgres
169 |
170 | ### 添加 PostgreSQL 数据库
171 |
172 | 在 dashboard.heroku.com 上访问你的应用程序,然后转到 **Add-ons** 部分。
173 |
174 | 从这里输入`postgress`,你会看到`Heroku Postgres`的选项。 选择它。
175 |
176 | 选择爱好开发免费计划(hobby dev free plan)。 Heroku 将自动完成剩下的工作。
177 |
178 | 完成后,你会看到数据库出现在 **Resources** 选项卡下。
179 |
180 | ### 配置数据库
181 |
182 | 我们现在必须告诉我们的应用程序如何访问数据库。 在 app 目录中运行。
183 |
184 | ```bash
185 | heroku config
186 | ```
187 |
188 | 这会输出类似以下内容的内容:
189 |
190 | ```none
191 | === today-i-learned-vapor Config Vars
192 | DATABASE_URL: postgres://cybntsgadydqzm:2d9dc7f6d964f4750da1518ad71hag2ba729cd4527d4a18c70e024b11cfa8f4b@ec2-54-221-192-231.compute-1.amazonaws.com:5432/dfr89mvoo550b4
193 | ```
194 |
195 | **DATABASE_URL** 这里将代表 postgres 数据库。 请**从不** 硬编码静态 url,heroku 会变更这个 url,并破坏你的应用程序。
196 |
197 | 以下是一个示例数据库配置
198 |
199 | ```swift
200 | if let databaseURL = Environment.get("DATABASE_URL") {
201 | app.databases.use(try .postgres(
202 | url: databaseURL
203 | ), as: .psql)
204 | } else {
205 | // ...
206 | }
207 | ```
208 |
209 | 如果你使用 Heroku Postgres 的标准计划,则需要开始未验证的 TLS。
210 |
211 | 不要忘记提交这些更改
212 |
213 | ```none
214 | git add .
215 | git commit -m "configured heroku database"
216 | ```
217 |
218 | ### 重置你的数据库
219 |
220 | 你可以使用 `run` 命令在 heroku 上恢复或运行其他命令。 Vapor 的项目默认也被命名为 `Run`,所以读起来有点怪。
221 | 要重置你的数据库请运行:
222 |
223 | ```bash
224 | heroku run Run -- revert --all --yes --env production
225 | ```
226 |
227 | 如要迁移请运行以下命令:
228 |
229 | ```bash
230 | heroku run Run -- migrate --env production
231 | ```
232 |
--------------------------------------------------------------------------------
/4.0/docs/deploy/nginx.md:
--------------------------------------------------------------------------------
1 | # 使用 Nginx 部署
2 |
3 | Nginx 是一款高性能、高可靠性、易于配置的 HTTP 服务器和 HTTP 反向代理服务器。
4 | 尽管 Vapor 可以直接处理 HTTP 请求,并且支持 TLS。但将 Vapor 应用置于 Nginx 反向代理之后,可以提高性能、安全性、以及易用性。
5 |
6 | !!! note
7 | 我们推荐你将 Vapor 应用配置在 Nginx 的反向代理之后。
8 |
9 | ## 概述
10 |
11 | HTTP 反向代理是什么意思?简而言之,反向代理服务器就是外部网络和你的真实的 HTTP 服务器之间的一个中间人,反向代理服务器处理所有进入的 HTTP 请求,并将它们转发给 Vapor 服务器。
12 |
13 | 反向代理的一个重要特性就是,它可以修改用户的请求,以及对其进行重定向。通过这个特性,反向代理服务器可以配置 TLS (https)、限制请求速率、甚至越过你的 Vapor 应用直接管理 Vapor 应用中的静态文件。
14 |
15 | 
16 |
17 | ### 更多细节
18 |
19 | 默认的接收 HTTP 请求的端口是 `80` (HTTPS 是 `443`)。如果你将 Vapor 服务器绑定到 `80` 端口,它就可以直接处理和响应 HTTP 请求。如果你想要使用反向代理 (比如 Nginx),你就需要将 Vapor 服务器绑定到一个内部端口上,比如 `8080`。
20 |
21 | !!! note
22 | 绑定到大于 1024 的端口号无需使用 `sudo` 命令。
23 |
24 | 一旦你的 Vapor 应用被绑定到 `80` 或 `443` 以外的端口,那么外部网络将无法直接访问它 (没有配置防火墙的情况下,带上端口号仍然可以访问)。然后将 Nginx 服务器绑定到 `80` 端口上,并配置它转发请求到 `8080` 端口上的 Vapor 应用。
25 |
26 | 就这样,如果你正确配置了 Nginx,你可以看到你的 Vapor 应用已经可以响应 `80` 端口上的请求了,而外部网络和你的 Vapor 应用都不会感知到 Nginx 的存在。
27 |
28 | ## 安装 Nginx
29 |
30 | 首先是安装 Nginx。网络上有着大量资源和文档来描述如何安装 Nginx,因此在这里不再赘述。不论你使用哪个平台、操作系统、或服务供应商,你都能找到相应的文档或教程。
31 |
32 | 教程:
33 |
34 | - [如何在 Ubuntu 14.04 LTS 上安装 Nginx?](https://www.digitalocean.com/community/tutorials/how-to-install-nginx-on-ubuntu-14-04-lts) (英文)
35 | - [如何在 Ubuntu 16.04 上安装 Nginx?](https://www.digitalocean.com/community/tutorials/how-to-install-nginx-on-ubuntu-16-04) (英文)
36 | - [如何在 Heroku 上部署 Nginx?](https://blog.codeship.com/how-to-deploy-nginx-on-heroku/) (英文)
37 | - [如何在 Ubuntu 14.04 上用 Docker 容器运行 Nginx?](https://www.digitalocean.com/community/tutorials/how-to-run-nginx-in-a-docker-container-on-ubuntu-14-04) (英文)
38 |
39 |
40 | ### APT
41 |
42 | 可以通过 APT 工具安装 Nginx
43 |
44 | ```sh
45 | sudo apt-get update
46 | sudo apt-get install nginx
47 | ```
48 |
49 | 你可以在浏览器中访问你的服务器的 IP 地址,来检查你的 Nginx 是否被正确安装.
50 |
51 |
52 | ```sh
53 | http://server_domain_name_or_IP
54 | ```
55 |
56 | ### Service
57 |
58 | 如何停止/启动/重启 Nginx 服务 (service)
59 |
60 | ```sh
61 | sudo service nginx stop
62 | sudo service nginx start
63 | sudo service nginx restart
64 | ```
65 |
66 | ## 启动 Vapor
67 |
68 | Nginx 可以通过 `sudo service nginx ...` 命令来启动或停止。同样的,你也需要一些类似的操作来启动或停止你的 Vapor 服务器。
69 |
70 | 有许多方法可以做到这一点,这通常取决于你使用的是哪个平台或系统。Supervisor 是其中一个较为通用的方式,你可以查看 [Supervisor](supervisor.md) 的配置方法,来配置启动或停止你的 Vapor 应用的命令。
71 |
72 | ## 配置 Nginx
73 |
74 | 要启用的站点的配置需要放在 `/etc/nginx/sites-enabled/` 目录下。
75 |
76 | 创建一个新的文件或者从 `/etc/nginx/sites-available/` 目录下的模版文件中拷贝一份配置,然后你就可以开始配置 Nginx 了。
77 |
78 | 这是一份配置文件的样例,它为一个 Vapor 项目进行了配置,这个项目位于 Home 目录下的一个名为 `Hello` 目录中。
79 |
80 | ```sh
81 | server {
82 | server_name hello.com;
83 | listen 80;
84 |
85 | root /home/vapor/Hello/Public/;
86 |
87 | location @proxy {
88 | proxy_pass http://127.0.0.1:8080;
89 | proxy_pass_header Server;
90 | proxy_set_header Host $host;
91 | proxy_set_header X-Real-IP $remote_addr;
92 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
93 | proxy_pass_header Server;
94 | proxy_connect_timeout 3s;
95 | proxy_read_timeout 10s;
96 | }
97 | }
98 | ```
99 |
100 | 这份配置假定你的 `Hello` 程序绑定到了 `8080` 端口上,并启用了生产模式 (production mode)。
101 |
102 | ### 管理文件
103 |
104 | Nginx 可以越过你的 Vapor 应用,直接管理静态资源文件。这样可以为你的 Vapor 进程减轻一些不必要的压力,以提高一些性能。
105 |
106 | ```sh
107 | server {
108 | ...
109 |
110 | # nginx 直接处理所有静态资源文件的请求,其余请求则回落 (fallback) 到 Vapor 应用
111 | location / {
112 | try_files $uri @proxy;
113 | }
114 |
115 | location @proxy {
116 | ...
117 | }
118 | }
119 | ```
120 |
121 | ### TLS
122 |
123 | 如果你已经获取了 TLS 证书 (certification),那么配置 TLS 相对来说是比较简单的。如果想要获取免费的 TLS 证书,可以看看 [Let's Encrypt](https://letsencrypt.org/getting-started/)。
124 |
125 | ```sh
126 | server {
127 | ...
128 |
129 | listen 443 ssl;
130 |
131 | ssl_certificate /etc/letsencrypt/live/hello.com/fullchain.pem;
132 | ssl_certificate_key /etc/letsencrypt/live/hello.com/privkey.pem;
133 |
134 | ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
135 | ssl_prefer_server_ciphers on;
136 | ssl_dhparam /etc/ssl/certs/dhparam.pem;
137 | ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA';
138 | ssl_session_timeout 1d;
139 | ssl_session_cache shared:SSL:50m;
140 | ssl_stapling on;
141 | ssl_stapling_verify on;
142 | add_header Strict-Transport-Security max-age=15768000;
143 |
144 | ...
145 |
146 | location @proxy {
147 | ...
148 | }
149 | }
150 | ```
151 |
152 | 上面这份 Nginx 的 TLS 配置是相对比较严格的。其中一些配置不是必须的,但能提高安全性。
153 |
--------------------------------------------------------------------------------
/4.0/docs/deploy/supervisor.md:
--------------------------------------------------------------------------------
1 | # Supervisor
2 |
3 | [Supervisor](http://supervisord.org) 是一个进程控制系统,可让你轻松启动、停止和重启你的 Vapor 应用程序。
4 |
5 | ## 安装
6 |
7 | Supervisor 可以通过 Linux 上的包管理器安装。
8 |
9 | ### Ubuntu
10 |
11 | ```sh
12 | sudo apt-get update
13 | sudo apt-get install supervisor
14 | ```
15 |
16 | ### CentOS and Amazon Linux
17 |
18 | ```sh
19 | sudo yum install supervisor
20 | ```
21 |
22 | ### Fedora
23 |
24 | ```sh
25 | sudo dnf install supervisor
26 | ```
27 |
28 | ## 配置
29 |
30 | 服务器上的每个 Vapor 应用程序都应该有自己的配置文件。例如 `Hello` 项目,配置文件位于 `/etc/supervisor/conf.d/hello.conf`
31 |
32 | ```sh
33 | [program:hello]
34 | command=/home/vapor/hello/.build/release/Run serve --env production
35 | directory=/home/vapor/hello/
36 | user=vapor
37 | stdout_logfile=/var/log/supervisor/%(program_name)-stdout.log
38 | stderr_logfile=/var/log/supervisor/%(program_name)-stderr.log
39 | ```
40 |
41 | 正如我们的配置文件中所指定的, `Hello` 项目位于用户 `vapor` 的主文件夹中。确保 `directory` 指向 `Package.swift` 文件所在项目的根目录。
42 |
43 | `--env production` 标志会禁用详细日志记录。
44 |
45 | ### 环境
46 |
47 | 你可以使用 supervisor 将变量导出到你的 Vapor 应用程序。要导出多个环境值,请将它们全部放在一行上。根据 [Supervisor 文档](http://supervisord.org/configuration.html#program-x-section-values):
48 |
49 | > 包含非字母数字字符的值应该用引号括起来(e.g. KEY="val:123",KEY2="val,456")。否则,引用值是可选的,但是推荐使用。
50 |
51 | ```sh
52 | environment=PORT=8123,ANOTHERVALUE="/something/else"
53 | ```
54 |
55 | 可以在 Vapor 中使用 `Environment.get` 导出变量
56 |
57 | ```swift
58 | let port = Environment.get("PORT")
59 | ```
60 |
61 | ## 开始
62 |
63 | 你现在可以加载并启动你的应用程序。
64 |
65 | ```sh
66 | supervisorctl reread
67 | supervisorctl add hello
68 | supervisorctl start hello
69 | ```
70 |
71 | !!! 注意
72 | `add` 命令可能已经启动了你的应用程序。
73 |
--------------------------------------------------------------------------------
/4.0/docs/fluent/advanced.md:
--------------------------------------------------------------------------------
1 | # Advanced
2 |
3 | ## MongoDB
4 |
5 | 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.
6 |
7 | The most common identifier in MongoDB is ObjectId. You can use this for your project using `@ID(custom: .id)`.
8 | If you need to use the same models with SQL, do not use `ObjectId`. Use `UUID` instead.
9 |
10 | ```swift
11 | final class User: Model {
12 | // Name of the table or collection.
13 | static let schema = "users"
14 |
15 | // Unique identifier for this User.
16 | // In this case, ObjectId is used
17 | // Fluent recommends using UUID by default, however ObjectId is also supported
18 | @ID(custom: .id)
19 | var id: ObjectId?
20 |
21 | // The User's email address
22 | @Field(key: "email")
23 | var email: String
24 |
25 | // The User's password stores as a BCrypt hash
26 | @Field(key: "password")
27 | var passwordHash: String
28 |
29 | // Creates a new, empty User instance, for use by Fluent
30 | init() { }
31 |
32 | // Creates a new User with all properties set.
33 | init(id: ObjectId? = nil, email: String, passwordHash: String, profile: Profile) {
34 | self.id = id
35 | self.email = email
36 | self.passwordHash = passwordHash
37 | self.profile = profile
38 | }
39 | }
40 | ```
41 |
42 | ### Data Modelling
43 |
44 | 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.
45 |
46 | 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.
47 |
48 | ### Flexible Data
49 |
50 | You can add flexible data in MongoDB, but this code will not work in SQL environments.
51 | To create grouped arbitrary data storage you can use `Document`.
52 |
53 | ```swift
54 | @Field(key: "document")
55 | var document: Document
56 | ```
57 |
58 | Fluent cannot support strictly types queries on these values. You can use a dot notated key path in your query for querying.
59 | This is accepted in MongoDB to access nested values.
60 |
61 | ```swift
62 | Something.query(on: db).filter("document.key", .equal, 5).first()
63 | ```
64 |
65 | ### Flexible Data
66 |
67 | You can add flexible data in MongoDB, but this code will not work in SQL environments.
68 | To create grouped arbitrary data storage you can use `Document`.
69 |
70 | ```swift
71 | @Field(key: "document")
72 | var document: Document
73 | ```
74 |
75 | Fluent cannot support strictly types queries on these values. You can use a dot notated key path in your query for querying.
76 | This is accepted in MongoDB to access nested values.
77 |
78 | ```swift
79 | Something.query(on: db).filter("document.key", .equal, 5).first()
80 | ```
81 |
82 | ### Raw Access
83 |
84 | To access the raw `MongoDatabase` instance, cast the database instance to `MongoDatabaseRepresentable` as such:
85 |
86 | ```swift
87 | guard let db = req.db as? MongoDatabaseRepresentable else {
88 | throw Abort(.internalServerError)
89 | }
90 |
91 | let mongodb = db.raw
92 | ```
93 |
94 | From here you can use all of the MongoKitten APIs.
95 |
--------------------------------------------------------------------------------
/4.0/docs/fluent/migration.md:
--------------------------------------------------------------------------------
1 | # Migrations
2 |
3 | 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.
4 |
5 | ```swift
6 | // An example migration.
7 | struct MyMigration: Migration {
8 | func prepare(on database: Database) -> EventLoopFuture {
9 | // Make a change to the database.
10 | }
11 |
12 | func revert(on database: Database) -> EventLoopFuture {
13 | // Undo the change made in `prepare`, if possible.
14 | }
15 | }
16 | ```
17 |
18 | 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.
19 |
20 | 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.
21 |
22 | ## Register
23 |
24 | Migrations are registered to your application using `app.migrations`.
25 |
26 | ```swift
27 | import Fluent
28 | import Vapor
29 |
30 | app.migrations.add(MyMigration())
31 | ```
32 |
33 | You can add a migration to a specific database using the `to` parameter, otherwise the default database will be used.
34 |
35 | ```swift
36 | app.migrations.add(MyMigration(), to: .myDatabase)
37 | ```
38 |
39 | Migrations should be listed in order of dependency. For example, if `MigrationB` depends on `MigrationA`, it should be added to `app.migrations` second.
40 |
41 | ## Migrate
42 |
43 | To migrate your database, run the `migrate` command.
44 |
45 | ```sh
46 | vapor run migrate
47 | ```
48 |
49 | 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.
50 |
51 | ### Revert
52 |
53 | To undo a migration on your database, run `migrate` with the `--revert` flag.
54 |
55 | ```sh
56 | vapor run migrate --revert
57 | ```
58 |
59 | The command will check the database to see which batch of migrations was last run and ask for a confirmation before reverting them.
60 |
61 | ### Auto Migrate
62 |
63 | If you would like migrations to run automatically before running other commands, you can pass the `--auto-migrate` flag.
64 |
65 | ```sh
66 | vapor run serve --auto-migrate
67 | ```
68 |
69 | You can also do this programatically.
70 |
71 | ```swift
72 | try app.autoMigrate().wait()
73 | ```
74 |
75 | Both of these options exist for reverting as well: `--auto-revert` and `app.autoRevert()`.
76 |
77 | ## Next Steps
78 |
79 | 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.
80 |
--------------------------------------------------------------------------------
/4.0/docs/fluent/query.md:
--------------------------------------------------------------------------------
1 | # Query
2 |
3 | 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.
4 |
5 | ```swift
6 | // An example of Fluent's query API.
7 | let planets = Planet.query(on: database)
8 | .filter(\.$type == .gasGiant)
9 | .sort(by: \.$name)
10 | .with(\.$star)
11 | .all()
12 | ```
13 |
14 | 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.
15 |
16 | ```swift
17 | // Also creates a query builder.
18 | database.query(Planet.self)
19 | ```
20 |
21 | ## All
22 |
23 | The `all()` method returns an array of models.
24 |
25 | ```swift
26 | // Fetches all planets.
27 | let planets = Planet.query(on: database).all()
28 | ```
29 |
30 | The `all` method also supports fetching only a single field from the result set.
31 |
32 | ```swift
33 | // Fetches all planet names.
34 | let names = Planet.query(on: database).all(\.$name)
35 | ```
36 |
37 | ### First
38 |
39 | 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.
40 |
41 | ```swift
42 | // Fetches the first planet named Earth.
43 | let earth = Planet.query(on: database)
44 | .filter(\.$name == "Earth")
45 | .first()
46 | ```
47 |
48 | !!! tip
49 | This method can be combined with [`unwrap(or:)`](../basics/errors.md#abort) to return a non-optional model or throw an error.
50 |
51 | ## Filter
52 |
53 | The `filter` method allows you to constrain the models included in the result set. There are several overloads for this method.
54 |
55 | ### Value Filter
56 |
57 | The most commonly used `filter` method accept an operator expression with a value.
58 |
59 | ```swift
60 | // An example of field value filtering.
61 | Planet.query(on: database).filter(\.$type == .gasGiant)
62 | ```
63 |
64 | 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.
65 |
66 | Below is a list of all supported value operators.
67 |
68 | |Operator|Description|
69 | |-|-|
70 | |`==`|Equal to.|
71 | |`!=`|Not equal to.|
72 | |`>=`|Greater than or equal to.|
73 | |`>`|Greater than.|
74 | |`<`|Less than.|
75 | |`<=`|Less than or equal to.|
76 |
77 | ### Field Filter
78 |
79 | The `filter` method supports comparing two fields.
80 |
81 | ```swift
82 | // All users with same first and last name.
83 | User.query(on: database)
84 | .filter(\.$firstName == \.$lastName)
85 | ```
86 |
87 | Field filters support the same operators as [value filters](#value-filter).
88 |
89 | ### Subset Filter
90 |
91 | The `filter` method supports checking whether a field's value exists in a given set of values.
92 |
93 | ```swift
94 | // All planets with either gas giant or small rocky type.
95 | Planet.query(on: database)
96 | .filter(\.$type ~~ [.gasGiant, .smallRocky])
97 | ```
98 |
99 | The supplied set of values can be any Swift `Collection` whose `Element` type matches the field's value type.
100 |
101 | Below is a list of all supported subset operators.
102 |
103 | |Operator|Description|
104 | |-|-|
105 | |`~~`|Value in set.|
106 | |`!~`|Value not in set.|
107 |
108 | ### Contains Filter
109 |
110 | The `filter` method supports checking whether a string field's value contains a given substring.
111 |
112 | ```swift
113 | // All planets whose name starts with the letter M
114 | Planet.query(on: database)
115 | .filter(\.$name =~ "M")
116 | ```
117 |
118 | These operators are only available on fields with string values.
119 |
120 | Below is a list of all supported contains operators.
121 |
122 | |Operator|Description|
123 | |-|-|
124 | |`~~`|Contains substring.|
125 | |`!~`|Does not contain substring.|
126 | |`=~`|Matches prefix.|
127 | |`!=~`|Does not match prefix.|
128 | |`~=`|Matches suffix.|
129 | |`!~=`|Does not match suffix.|
130 |
131 | ### Group
132 |
133 | 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.
134 |
135 | ```swift
136 | // All planets whose name is either Earth or Mars
137 | Planet.query(on: database).group(.or) { group in
138 | group.filter(\.$name == "Earth").filter(\.$name == "Mars")
139 | }
140 | ```
141 |
142 | 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.
143 |
144 | ## Aggregate
145 |
146 | Query builder supports several methods for performing calculations on a set of values like counting or averaging.
147 |
148 | ```swift
149 | // Number of planets in database.
150 | Planet.query(on: database).count()
151 | ```
152 |
153 | All aggregate methods besides `count` require a key path to a field to be passed.
154 |
155 | ```swift
156 | // Lowest name sorted alphabetically.
157 | Planet.query(on: database).min(\.$name)
158 | ```
159 |
160 | Below is a list of all available aggregate methods.
161 |
162 | |Aggregate|Description|
163 | |-|-|
164 | |`count`|Number of results.|
165 | |`sum`|Sum of result values.|
166 | |`average`|Average of result values.|
167 | |`min`|Minimum result value.|
168 | |`max`|Maximum result value.|
169 |
170 | All aggregate methods except `count` return the field's value type as a result. `count` always returns an integer.
171 |
172 | ## Chunk
173 |
174 | Query builder supports returning a result set as separate chunks. This helps you to control memory usage when handling large database reads.
175 |
176 | ```swift
177 | // Fetches all planets in chunks of at most 64 at a time.
178 | Planet.query(on: self.database).chunk(max: 64) { planets in
179 | // Handle chunk of planets.
180 | }
181 | ```
182 |
183 | 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.
184 |
185 | ## Field
186 |
187 | 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.
188 |
189 | ```swift
190 | // Select only the planet's id and name field
191 | Planet.query(on: database)
192 | .field(\.$id).field(\.$name)
193 | .all()
194 | ```
195 |
196 | 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.
197 |
198 | ```swift
199 | if let name = planet.$name.value {
200 | // Name was fetched.
201 | } else {
202 | // Name was not fetched.
203 | // Accessing `planet.name` will fail.
204 | }
205 | ```
206 |
207 | ## Unique
208 |
209 | Query builder's `unique` method causes only distinct results (no duplicates) to be returned.
210 |
211 | ```swift
212 | // Returns all unique user first names.
213 | User.query(on: database).unique().all(\.$firstName)
214 | ```
215 |
216 | `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`.
217 |
218 | ## Range
219 |
220 | Query builder's `range` methods allow you to choose a subset of the results using Swift ranges.
221 |
222 | ```swift
223 | // Fetch the first 5 planets.
224 | Planet.query(on: self.database)
225 | .range(..<5)
226 | ```
227 |
228 | Range values are unsigned integers starting at zero. Learn more about [Swift ranges](https://developer.apple.com/documentation/swift/range).
229 |
230 | ```swift
231 | // Skip the first 2 results.
232 | .range(2...)
233 | ```
234 |
235 | ## Join
236 |
237 | 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.
238 |
239 | ```swift
240 | // Fetches all planets with a star named Sun.
241 | Planet.query(on: database)
242 | .join(Star.self, on: \Planet.$star.$id == \Star.$id)
243 | .filter(Star.self, \.$name == "Sun")
244 | .all()
245 | ```
246 |
247 | 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.
248 |
249 | 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.
250 |
251 | ```swift
252 | // Sort by joined field "name" on Star model.
253 | .sort(Star.self, \.$name)
254 | ```
255 |
256 | Queries that use joins will still return an array of the base model. To access the joined model, use the `joined` method.
257 |
258 | ```swift
259 | // Accessing joined model from query result.
260 | let planet: Planet = ...
261 | let star = try planet.joined(Star.self)
262 | ```
263 |
264 | ### Model Alias
265 |
266 | 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`.
267 |
268 | ```swift
269 | // Example of model aliases.
270 | final class HomeTeam: ModelAlias {
271 | static let name = "home_teams"
272 | let model = Team()
273 | }
274 | final class AwayTeam: ModelAlias {
275 | static let name = "away_teams"
276 | let model = Team()
277 | }
278 | ```
279 |
280 | 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.
281 |
282 | ```swift
283 | // Fetch all matches where the home team's name is Vapor
284 | // and sort by the away team's name.
285 | let matches = try Match.query(on: self.database)
286 | .join(HomeTeam.self, on: \Match.$homeTeam.$id == \HomeTeam.$id)
287 | .join(AwayTeam.self, on: \Match.$awayTeam.$id == \AwayTeam.$id)
288 | .filter(HomeTeam.self, \.$name == "Vapor")
289 | .sort(AwayTeam.self, \.$name)
290 | .all().wait()
291 | ```
292 |
293 | All model fields are accessible through the model alias type via `@dynamicMemberLookup`.
294 |
295 | ```swift
296 | // Access joined model from result.
297 | let home = try match.joined(HomeTeam.self)
298 | print(home.name)
299 | ```
300 |
301 | ## Update
302 |
303 | Query builder supports updating more than one model at a time using the `update` method.
304 |
305 | ```swift
306 | // Update all planets named "Earth"
307 | Planet.query(on: database)
308 | .set(\.$type, to: .dwarf)
309 | .filter(\.$name == "Pluto")
310 | .update()
311 | ```
312 |
313 | `update` supports the `set`, `filter`, and `range` methods.
314 |
315 | ## Delete
316 |
317 | Query builder supports deleting more than one model at a time using the `delete` method.
318 |
319 | ```swift
320 | // Delete all planets named "Vulcan"
321 | Planet.query(on: database)
322 | .filter(\.$name == "Vulcan")
323 | .delete()
324 | ```
325 |
326 | `delete` supports the `filter` method.
327 |
328 | ## Paginate
329 |
330 | Fluent's query API supports automatic result pagination using the `paginate` method.
331 |
332 | ```swift
333 | // Example of request-based pagination.
334 | app.get("planets") { req in
335 | Planet.query(on: req.db).paginate(for: req)
336 | }
337 | ```
338 |
339 | 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.
340 |
341 | ```http
342 | GET /planets?page=2&per=5 HTTP/1.1
343 | ```
344 |
345 | The above request would yield a response structured like the following.
346 |
347 | ```json
348 | {
349 | "items": [...],
350 | "metadata": {
351 | "page": 2,
352 | "per": 5,
353 | "total": 8
354 | }
355 | }
356 | ```
357 |
358 | Page numbers start at `1`. You can also make a manual page request.
359 |
360 | ```swift
361 | // Example of manual pagination.
362 | .paginate(PageRequest(page: 1, per: 2))
363 | ```
364 |
365 | ## Sort
366 |
367 | Query results can be sorted by field values using `sort` method.
368 |
369 | ```swift
370 | // Fetch planets sorted by name.
371 | Planet.query(on: database).sort(\.$name)
372 | ```
373 |
374 | 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.
375 |
376 | ```swift
377 | // Fetch users sorted by name. If two users have the same name, sort them by age.
378 | User.query(on: database).sort(\.$name).sort(\.$age)
379 | ```
380 |
--------------------------------------------------------------------------------
/4.0/docs/fluent/relations.md:
--------------------------------------------------------------------------------
1 | # Relations
2 |
3 | Fluent's [model API](model.md) helps you create and maintain references between your models through relations. Two types of relations are supported:
4 |
5 | - [Parent](#parent) / [Child](#child) (One-to-many)
6 | - [Siblings](#siblings) (Many-to-many)
7 |
8 | ## Parent
9 |
10 | The `@Parent` relation stores a reference to another model's `@ID` property.
11 |
12 | ```swift
13 | final class Planet: Model {
14 | // Example of a parent relation.
15 | @Parent(key: "star_id")
16 | var star: Star
17 | }
18 | ```
19 |
20 | `@Parent` contains a `@Field` named `id` which is used for setting and updating the relation.
21 |
22 | ```swift
23 | // Set parent relation id
24 | earth.$star.id = sun.id
25 | ```
26 |
27 | 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).
28 |
29 | ```swift
30 | .field("star_id", .uuid, .required, .references("star", "id"))
31 | ```
32 |
33 | Note that the [`.references`](schema.md#field-constraint) constraint is optional. See [schema](schema.md) for more information.
34 |
35 | ### Optional Parent
36 |
37 | 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`.
38 |
39 | ```swift
40 | final class Planet: Model {
41 | // Example of an optional parent relation.
42 | @OptionalParent(key: "star_id")
43 | var star: Star?
44 | }
45 | ```
46 |
47 | The field definition is similar to `@Parent`'s except that the `.required` constraint should be omitted.
48 |
49 | ```swift
50 | .field("star_id", .uuid, .references("star", "id"))
51 | ```
52 |
53 | ## Children
54 |
55 | The `@Children` property creates a one-to-many relation between two models. It does not store any values on the root model.
56 |
57 | ```swift
58 | final class Star: Model {
59 | // Example of a children relation.
60 | @Children(for: \.$star)
61 | var planets: [Planet]
62 | }
63 | ```
64 |
65 | 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).
66 |
67 | New models can be added to this relation using the `create` method.
68 |
69 | ```swift
70 | // Example of adding a new model to a relation.
71 | let earth = Planet(name: "Earth")
72 | sun.$planets.create(earth, on: database)
73 | ```
74 |
75 | This will set the parent id on the child model automatically.
76 |
77 | Since this relation does not store any values, no database schema entry is required.
78 |
79 | ## Siblings
80 |
81 | The `@Siblings` property creates a many-to-many relation between two models. It does this through a tertiary model called a pivot.
82 |
83 | Let's take a look at an example of a many-to-many relation between a `Planet` and a `Tag`.
84 |
85 | ```swift
86 | // Example of a pivot model.
87 | final class PlanetTag: Model {
88 | static let schema = "planet+tag"
89 |
90 | @ID(key: .id)
91 | var id: UUID?
92 |
93 | @Parent(key: "planet_id")
94 | var planet: Planet
95 |
96 | @Parent(key: "tag_id")
97 | var tag: Tag
98 |
99 | init() { }
100 |
101 | init(id: UUID? = nil, planet: Planet, tag: Tag) throws {
102 | self.id = id
103 | self.$planet.id = try planet.requireID()
104 | self.$tag.id = try tag.requireID()
105 | }
106 | }
107 | ```
108 |
109 | 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.
110 |
111 | Adding a [unique](schema.md#unique) constraint to the pivot model can help prevent redundant entries. See [schema](schema.md) for more information.
112 |
113 | ```swift
114 | // Disallows duplicate relations.
115 | .unique(on: "planet_id", "tag_id")
116 | ```
117 |
118 | Once the pivot is created, use the `@Siblings` property to create the relation.
119 |
120 | ```swift
121 | final class Planet: Model {
122 | // Example of a siblings relation.
123 | @Siblings(through: PlanetTag.self, from: \.$planet, to: \.$tag)
124 | public var tags: [Tag]
125 | }
126 | ```
127 |
128 | The `@Siblings` property requires three parameters:
129 |
130 | - `through`: The pivot model's type.
131 | - `from`: Key path from the pivot to the parent relation referencing the root model.
132 | - `to`: Key path from the pivot to the parent relation referencing the related model.
133 |
134 | The inverse `@Siblings` property on the related model completes the relation.
135 |
136 | ```swift
137 | final class Tag: Model {
138 | // Example of a siblings relation.
139 | @Siblings(through: PlanetTag.self, from: \.$tag, to: \.$planet)
140 | public var planets: [Planet]
141 | }
142 | ```
143 |
144 | ### Siblings Attach
145 |
146 | The `@Siblings` property has methods adding and removing models from the relation.
147 |
148 | Use the `attach` method to add a model to the relation. This creates and saves the pivot model automatically.
149 |
150 | ```swift
151 | let earth: Planet = ...
152 | let inhabited: Tag = ...
153 | // Adds the model to the relation.
154 | earth.$tags.attach(inhabited, on: database)
155 | ```
156 |
157 | When attaching a single model, you can use the `method` parameter to choose whether or not the relation should be checked before saving.
158 |
159 | ```swift
160 | // Only attaches if the relation doesn't already exist.
161 | earth.$tags.attach(inhabited, method: .ifNotExists, on: database)
162 | ```
163 |
164 | Use the `detach` method to remove a model from the relation. This deletes the corresponding pivot model.
165 |
166 | ```swift
167 | // Removes the model from the relation.
168 | earth.$tags.detach(inhabited)
169 | ```
170 |
171 | You can check if a model is related or not using the `isAttached` method.
172 |
173 | ```swift
174 | // Checks if the models are related.
175 | earth.$tags.isAttached(to: inhabited)
176 | ```
177 |
178 | ## Get
179 |
180 | Use the `get(on:)` method to fetch a relation's value.
181 |
182 | ```swift
183 | // Fetches all of the sun's planets.
184 | sun.$planets.get(on: database).map { planets in
185 | print(planets)
186 | }
187 | ```
188 |
189 | 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.
190 |
191 | ```swift
192 | sun.$planets.get(reload: true, on: database)
193 | ```
194 |
195 | ## Query
196 |
197 | Use the `query(on:)` method on a relation to create a query builder for the related models.
198 |
199 | ```swift
200 | // Fetch all of the sun's planets that have a naming starting with M.
201 | sun.$planets.query(on: database).filter(\.$name =~ "M").all()
202 | ```
203 |
204 | See [query](query.md) for more information.
205 |
206 | ## Load
207 |
208 | Use the `load(on:)` method to load a relation. This allows the related model to be accessed as a local property.
209 |
210 | ```swift
211 | // Example of loading a relation.
212 | planet.$star.load(on: database).map {
213 | print(planet.star.name)
214 | }
215 | ```
216 |
217 | To check whether or not a relation has been loaded, use the `value` property.
218 |
219 | ```swift
220 | if planet.$star.value != nil {
221 | // Relation has been loaded.
222 | print(planet.star.name)
223 | } else {
224 | // Relation has not been loaded.
225 | // Attempting to access planet.star will fail.
226 | }
227 | ```
228 |
229 | You can set the `value` property manually if needed.
230 |
231 | ## Eager Load
232 |
233 | 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.
234 |
235 | To eager load a relation, pass a key path to the relation to the `with` method on query builder.
236 |
237 | ```swift
238 | // Example of eager loading.
239 | Planet.query(on: database).with(\.$star).all().map { planets in
240 | for planet in planets {
241 | // `star` is accessible synchronously here
242 | // since it has been eager loaded.
243 | print(planet.star.name)
244 | }
245 | }
246 | ```
247 |
248 | 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.
249 |
250 | 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.
251 |
252 |
253 | ### Nested Eager Load
254 |
255 | 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.
256 |
257 | ```swift
258 | Planet.query(on: database).with(\.$star) { star in
259 | star.with(\.$galaxy)
260 | }.all().map { planets in
261 | for planet in planets {
262 | // `star.galaxy` is accessible synchronously here
263 | // since it has been eager loaded.
264 | print(planet.star.galaxy.name)
265 | }
266 | }
267 | ```
268 |
269 | 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.
270 |
--------------------------------------------------------------------------------
/4.0/docs/fluent/schema.md:
--------------------------------------------------------------------------------
1 | # Schema
2 |
3 | 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).
4 |
5 | ```swift
6 | // An example of Fluent's schema API
7 | database.schema("planets")
8 | .id()
9 | .field("name", .string, .required)
10 | .field("star_id", .uuid, .required, .references("stars", "id"))
11 | .create()
12 | ```
13 |
14 | 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).
15 |
16 | ## Actions
17 |
18 | The schema API supports creating, updating, and deleting schemas. Each action supports a subset of the API's available methods.
19 |
20 | ### Create
21 |
22 | 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.
23 |
24 | ```swift
25 | // An example schema creation.
26 | database.schema("planets")
27 | .id()
28 | .field("name", .string, .required)
29 | .create()
30 | ```
31 |
32 | If a table or collection with the chosen name already exists, an error will be thrown. To ignore this, use `.ignoreExisting()`.
33 |
34 | ### Update
35 |
36 | Calling `update()` updates an existing table or collection in the database. All methods for creating, updating, and deleting fields and constraints are supported.
37 |
38 | ```swift
39 | // An example schema update.
40 | database.schema("planets")
41 | .unique(on: "name")
42 | .deleteField("star_id")
43 | .update()
44 | ```
45 |
46 | ### Delete
47 |
48 | Calling `delete()` deletes an existing table or collection from the database. No additional methods are supported.
49 |
50 | ```swift
51 | // An example schema deletion.
52 | database.schema("planets").delete()
53 | ```
54 |
55 | ## Field
56 |
57 | Fields can be added when creating or updating a schema.
58 |
59 | ```swift
60 | // Adds a new field
61 | .field("name", .string, .required)
62 | ```
63 |
64 | 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.
65 |
66 | ### Data Type
67 |
68 | Supported field data types are listed below.
69 |
70 | |DataType|Swift Type|
71 | |-|-|
72 | |`.string`|`String`|
73 | |`.int{8,16,32,64}`|`Int{8,16,32,64}`|
74 | |`.uint{8,16,32,64}`|`UInt{8,16,32,64}`|
75 | |`.bool`|`Bool`|
76 | |`.datetime`|`Date` (recommended)|
77 | |`.time`|`Date` (omitting day, month, and year)|
78 | |`.date`|`Date` (omitting time of day)|
79 | |`.float`|`Float`|
80 | |`.double`|`Double`|
81 | |`.data`|`Data`|
82 | |`.uuid`|`UUID`|
83 | |`.dictionary`|See [dictionary](#dictionary)|
84 | |`.array`|See [array](#array)|
85 | |`.enum`|See [enum](#enum)|
86 |
87 | ### Field Constraint
88 |
89 | Supported field constraints are listed below.
90 |
91 | |FieldConstraint|Description|
92 | |-|-|
93 | |`.required`|Disallows `nil` values.|
94 | |`.references`|Requires that this field's value match a value in the referenced schema. See [foreign key](#foreign-key)|
95 | |`.identifier`|Denotes the primary key. See [identifier](#identifier)|
96 |
97 | ### Identifier
98 |
99 | 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.
100 |
101 | ```swift
102 | // Adds field for default identifier.
103 | .id()
104 | ```
105 |
106 | For custom identifier types, you will need to specify the field manually.
107 |
108 | ```swift
109 | // Adds field for custom identifier.
110 | .field("id", .int, .identifier(auto: true))
111 | ```
112 |
113 | 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.
114 |
115 | ### Update Field
116 |
117 | You can update a field's data type using `updateField`.
118 |
119 | ```swift
120 | // Updates the field to `double` data type.
121 | .updateField("age", .double)
122 | ```
123 |
124 | See [advanced](advanced.md#sql) for more information on advanced schema updates.
125 |
126 | ### Delete Field
127 |
128 | You can remove a field from a schema using `deleteField`.
129 |
130 | ```swift
131 | // Deletes the field "age".
132 | .deleteField("age")
133 | ```
134 |
135 | ## Constraint
136 |
137 | Constraints can be added when creating or updating a schema. Unlike [field constraints](#field-constraint), top-level constraints can affect multiple fields.
138 |
139 | ### Unique
140 |
141 | A unique constraint requires that there are no duplicate values in one or more fields.
142 |
143 | ```swift
144 | // Disallow duplicate email addresses.
145 | .unique(on: "email")
146 | ```
147 |
148 | If multiple field are constrained, the specific combination of each field's value must be unique.
149 |
150 | ```swift
151 | // Disallow users with the same full name.
152 | .unique(on: "first_name", "last_name")
153 | ```
154 |
155 | To delete a unique constraint, use `deleteUnique`.
156 |
157 | ```swift
158 | // Removes duplicate email constraint.
159 | .deleteUnique(on: "email")
160 | ```
161 |
162 | ### Constraint Name
163 |
164 | 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.
165 |
166 | ```swift
167 | // Disallow duplicate email addresses.
168 | .unique(on: "email", name: "no_duplicate_emails")
169 | ```
170 |
171 | To delete a named constraint, you must use `deleteConstraint(name:)`.
172 |
173 | ```swift
174 | // Removes duplicate email constraint.
175 | .deleteConstraint(name: "no_duplicate_emails")
176 | ```
177 |
178 | ## Foreign Key
179 |
180 | 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.
181 |
182 | To add a foreign key constraint to a field, use `.references`.
183 |
184 | ```swift
185 | // Example of adding a field foreign key constraint.
186 | .field("star_id", .uuid, .required, .references("stars", "id"))
187 | ```
188 |
189 | The above constraint requires that all values in the "star_id" field must match one of the values in Star's "id" field.
190 |
191 | This same constraint could be added as a top-level constraint using `foreignKey`.
192 |
193 | ```swift
194 | // Example of adding a top-level foreign key constraint.
195 | .foreignKey("star_id", references: "star", "id")
196 | ```
197 |
198 | Unlike field constraints, top-level constraints can be added in a schema update. They can also be [named](#constraint-name).
199 |
200 | Foreign key constraints support optional `onDelete` and `onUpdate` actions.
201 |
202 | |ForeignKeyAction|Description|
203 | |-|-|
204 | |`.noAction`|Prevents foreign key violations (default).|
205 | |`.restrict`|Same as `.noAction`.|
206 | |`.cascade`|Propogates deletes through foreign keys.|
207 | |`.setNull`|Sets field to null if reference is broken.|
208 | |`.setDefault`|Sets field to default if reference is broken.|
209 |
210 | Below is an example using foreign key actions.
211 |
212 | ```swift
213 | // Example of adding a top-level foreign key constraint.
214 | .foreignKey("star_id", references: "star", "id", onDelete: .cascade)
215 | ```
216 |
217 | ## Dictionary
218 |
219 | 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.
220 |
221 | !!! note
222 | Fluent's SQL database drivers store nested dictionaries in JSON columns.
223 |
224 | Take the following `Codable` struct.
225 |
226 | ```swift
227 | struct Pet: Codable {
228 | var name: String
229 | var age: Int
230 | }
231 | ```
232 |
233 | Since this `Pet` struct is `Codable`, it can be stored in a `@Field`.
234 |
235 | ```swift
236 | @Field(key: "pet")
237 | var pet: Pet
238 | ```
239 |
240 | This field can be stored using the `.dictionary(of:)` data type.
241 |
242 | ```swift
243 | .field("pet", .dictionary, .required)
244 | ```
245 |
246 | Since `Codable` types are heterogenous dictionaries, we do not specify the `of` parameter.
247 |
248 | If the dictionary values were homogenous, for example `[String: Int]`, the `of` parameter would specify the value type.
249 |
250 | ```swift
251 | .field("numbers", .dictionary(of: .int), .required)
252 | ```
253 |
254 | Dictionary keys must always be strings.
255 |
256 | ## Array
257 |
258 | 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.
259 |
260 | Take the following `@Field` that stores an array of strings.
261 |
262 | ```swift
263 | @Field(key: "tags")
264 | var tags: [String]
265 | ```
266 |
267 | This field can be stored using the `.array(of:)` data type.
268 |
269 | ```swift
270 | .field("tags", .array(of: .string), .required)
271 | ```
272 |
273 | Since the array is homogenous, we specify the `of` parameter.
274 |
275 | 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.
276 |
277 | ## Enum
278 |
279 | 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.
280 |
281 | To define a native database enum, use the `enum` method on `Database`. Use `case` to define each case of the enum.
282 |
283 | ```swift
284 | // An example of enum creation.
285 | database.enum("planet_type")
286 | .case("smallRocky")
287 | .case("gasGiant")
288 | .case("dwarf")
289 | .create()
290 | ```
291 |
292 | Once an enum has been created, you can use the `read()` method to generate a data type for your schema field.
293 |
294 | ```swift
295 | // An example of reading an enum and using it to define a new field.
296 | database.enum("planet_type").read().flatMap { planetType in
297 | database.schema("planets")
298 | .field("type", planetType, .required)
299 | .update()
300 | }
301 | ```
302 |
303 | To update an enum, call `update()`. Cases can be deleted from existing enums.
304 |
305 | ```swift
306 | // An example of enum update.
307 | database.enum("planet_type")
308 | .deleteCase("gasGiant")
309 | .update()
310 | ```
311 |
312 | To delete an enum, call `delete()`.
313 |
314 | ```swift
315 | // An example of enum deletion.
316 | database.enum("planet_type").delete()
317 | ```
318 |
319 | ## Model Coupling
320 |
321 | 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.
322 |
323 | To better understand this, take a look at the following example migration.
324 |
325 | ```swift
326 | struct UserMigration: Migration {
327 | func prepare(on database: Database) -> EventLoopFuture {
328 | database.schema("users")
329 | .field("id", .uuid, .identifier(auto: false))
330 | .field("name", .string, .required)
331 | .create()
332 | }
333 |
334 | func revert(on database: Database) -> EventLoopFuture {
335 | database.schema("users").delete()
336 | }
337 | }
338 | ```
339 |
340 | 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.
341 |
342 | ```diff
343 | - @Field(key: "name")
344 | - var name: String
345 | + @Field(key: "first_name")
346 | + var firstName: String
347 | +
348 | + @Field(key: "last_name")
349 | + var lastName: String
350 | ```
351 |
352 | We can make the necessary database schema adjustments with the following migration.
353 |
354 | ```swift
355 | struct UserNameMigration: Migration {
356 | func prepare(on database: Database) -> EventLoopFuture {
357 | database.schema("users")
358 | .deleteField("name")
359 | .field("first_name", .string)
360 | .field("last_name", .string)
361 | .create()
362 | }
363 |
364 | func revert(on database: Database) -> EventLoopFuture {
365 | database.schema("users").delete()
366 | }
367 | }
368 | ```
369 |
370 | 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.
371 |
--------------------------------------------------------------------------------
/4.0/docs/images/digital-ocean-create-droplet.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vapor/docs-cn/e627fce27c8eaaefb0ec51c3644f22ce23581bc3/4.0/docs/images/digital-ocean-create-droplet.png
--------------------------------------------------------------------------------
/4.0/docs/images/digital-ocean-distributions-ubuntu-18.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vapor/docs-cn/e627fce27c8eaaefb0ec51c3644f22ce23581bc3/4.0/docs/images/digital-ocean-distributions-ubuntu-18.png
--------------------------------------------------------------------------------
/4.0/docs/images/digital-ocean-droplet-list.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vapor/docs-cn/e627fce27c8eaaefb0ec51c3644f22ce23581bc3/4.0/docs/images/digital-ocean-droplet-list.png
--------------------------------------------------------------------------------
/4.0/docs/images/swift-download-ubuntu-18-copy-link.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vapor/docs-cn/e627fce27c8eaaefb0ec51c3644f22ce23581bc3/4.0/docs/images/swift-download-ubuntu-18-copy-link.png
--------------------------------------------------------------------------------
/4.0/docs/index.md:
--------------------------------------------------------------------------------
1 |
2 | Vapor 是 Swift 最流行的 Web 网络框架。它可以为你的网站或 API 提供精美的页面展示和简易的使用方式。
3 |
4 |
5 | ## 语言
6 |
7 | - English [(docs.vapor.codes)](https://docs.vapor.codes)
8 | - 简体中文 [(cn.docs.vapor.codes)](https://cn.docs.vapor.codes)
9 |
10 | !!! 招募-翻译爱好者
11 | Vapor 中文翻译小组现招募翻译爱好者参与文档的翻译和校对,你可以在 [这里](https://github.com/vapor/docs-cn/blob/master/CONTRIBUTING.md) 查看尚未完成的部分,然后选一篇未完成的进行翻译并提 PR,我们将在校对无误后更新在这个文档。
12 |
13 |
14 | ## 开始
15 |
16 | 如果你是第一次使用 Vapor,请前往 [安装 → macOS](install/macos.md) 安装 Swift 和 Vapor 开发环境。
17 |
18 | Vapor 安装完成后,请查看 [开始 → 你好,世界](start/hello-world.md) 示例,以创建你的第一个 Vapor 应用程序!
19 |
20 |
21 | ## 其他资源
22 |
23 | 这里是其他一些有关 Vapor 框架的内容,以供学习与交流。
24 |
25 | | 名称 | 描述 | 链接 |
26 | |----------------|--------------------------------------------------|-----------------------------------------------------------------|
27 | | Vapor Discord | 与数千名 Vapor 开发人员交流。 | [访问 →](http://vapor.team) |
28 | | API docs | 通过代码注释自动生成的文档。 | [访问 →](http://api.vapor.codes) |
29 | | Stack Overflow | 使用 `Vapor` 标签提问和回答相关问题。 | [访问 →](http://stackoverflow.com/questions/tagged/vapor) |
30 | | Swift Forums |在 Swift.org 论坛的 Vapor 专题发布。 | [访问 →](https://forums.swift.org/c/related-projects/vapor) |
31 | | Source Code | 了解 Vapor 的工作原理。 | [访问 →](https://github.com/vapor/vapor) |
32 | | GitHub Issues | 在 GitHub 上报告错误或提交功能。 | [访问 →](https://github.com/vapor/vapor/issues) |
33 |
34 |
35 | ## 作者
36 |
37 | [晋先森](https://github.com/Jinxiansen),[OHeroJ](https://github.com/OHeroJ) 以及 Vapor 社区的数百名成员。
38 |
--------------------------------------------------------------------------------
/4.0/docs/install/linux.md:
--------------------------------------------------------------------------------
1 | --
2 | ***校对日期 2021-12-10***
3 | --
4 |
5 | # 在 Linux 上面安装
6 |
7 | 你需要 Swift 5.2 或更高版本来使用Vapor。 Swift5.2可以通过[Swift.org](https://swift.org/download/)上面的工具链来安装。
8 |
9 | ## 支持的发行版和版本
10 |
11 | Vapor 与 Swift 5.2 或者更高的版本对 Linux 的版本支持保持一致。
12 |
13 | !!! 提示
14 | 下面列出的版本可能会随时过期。你可以到 [Swift Releases](https://swift.org/download/#releases) 官方网站去确认官方支持的操作系统。
15 |
16 | |Distribution|Version|Swift Version|
17 | |-|-|-|
18 | |Ubuntu|16.04, 18.04|>= 5.2|
19 | |Ubuntu|20.04|>= 5.2.4|
20 | |Fedora|>= 30|>= 5.2|
21 | |CentOS|8|>= 5.2.4|
22 | |Amazon Linux|2|>= 5.2.4|
23 |
24 | 官方不支持的 Linux 发行版可能可以通过编译源码来运行 Swift,但是 Vapor 不能保证其稳定性。可以在 [Swift repo](https://github.com/apple/swift#getting-started) 学习更多关于编译 Swift。
25 |
26 | ## 安装 Swift
27 |
28 | 访问 Swift.org's [Using Downloads](https://swift.org/download/#using-downloads) 手册来学习如何在 Linux 安装 Swift。
29 |
30 | ### Fedora
31 |
32 | Fedora 用户可以简单的通过下面的命令来安装 Swift:
33 |
34 | ```sh
35 | sudo dnf install swift-lang
36 | ```
37 |
38 | 如果你正在使用 Fedora 30,你需要添加添加 EPEL 8 来获取 Swift 5.2 或更新的版本。
39 |
40 |
41 | ## Docker
42 |
43 | 你也可以使用预装了编译器的 Swift 官方 Docker 镜像,可以在[Swift's Docker Hub](https://hub.docker.com/_/swift)了解更多。
44 |
45 | ## 安装 Toolbox
46 |
47 | 现在你已经安装了Swift,让我们安装 [Vapor Toolbox](https://github.com/vapor/toolbox)。使用 Vapor 不是必须要使用此 CLI 工具,但它包含有用的实用程序。
48 |
49 | 在 Linux 系统上,你需要通过源码来编译toolbox,访问 toolbox 在Github上的 releases 来获取最新版本
50 |
51 | ```sh
52 | git clone https://github.com/vapor/toolbox.git
53 | cd toolbox
54 | git checkout
55 | make install
56 | ```
57 |
58 | 通过打印信息来再次确认是否已经安装成功。
59 |
60 | ```sh
61 | vapor --help
62 | ```
63 |
64 | 你应该能看见可用命令的列表。
65 |
66 | ## 下一步
67 |
68 | 在你安装完 Swift 之后,通过 [开始 → Hello, world](../start/hello-world.md) 来学习创建你的第一个应用。
69 |
--------------------------------------------------------------------------------
/4.0/docs/install/macos.md:
--------------------------------------------------------------------------------
1 | # 在 macOS 上安装
2 |
3 | 要在 macOS 上使用 Vapor,你将需要 Swift 5.2 或更高版本。 Swift 及其所有依赖项都与 Xcode 捆绑。
4 |
5 | ## 安装 Xcode
6 |
7 | 从 [Mac App Store](https://itunes.apple.com/us/app/xcode/id497799835?mt=12) 安装 Xcode 11.4 或更高版本。
8 |
9 | 
10 |
11 | 下载 Xcode 之后,必须将其打开以完成安装。可能还需要耐心等待一会儿。
12 |
13 | 安装后,打开 Terminal 输入以下命令打印 Swift 的版本,检查版本号以确保安装成功。
14 |
15 | ```sh
16 | swift --version
17 | ```
18 |
19 | 你应该能够看到 Swift 的版本信息已打印。
20 |
21 | ```sh
22 | Apple Swift version 5.2 (swiftlang-1100.0.270.13 clang-1100.0.33.7)
23 | Target: x86_64-apple-darwin19.0.0
24 | ```
25 |
26 | Vapor 4 需要 Swift 5.2 或更高版本。
27 |
28 | ## 安装 Toolbox
29 |
30 | 现在你已经安装了 Swift,让我们安装 [Vapor Toolbox](https://github.com/vapor/toolbox)。 使用 Vapor 不需要此 CLI 工具,但是它包含一些实用的程序,例如新项目创建。
31 |
32 | Toolbox 通过 Homebrew 分发。如果你还没有安装 Homebrew,请访问 brew.sh 查看安装说明。
33 |
34 | ```sh
35 | brew install vapor
36 | ```
37 |
38 | 通过输出帮助内容以确保安装成功。
39 |
40 | ```sh
41 | vapor --help
42 | ```
43 |
44 | 你应该可以看到 Vapor 包含的可用命令列表。
45 |
46 | ## 下一步
47 |
48 | 现在你已经安装了 Swift and Vapor Toolbox,在 [开始 → 你好,世界](../start/hello-world.md) 中创建你的第一个 Vapor 应用程序。
49 |
--------------------------------------------------------------------------------
/4.0/docs/security/crypto.md:
--------------------------------------------------------------------------------
1 | # Crypto
2 |
3 | 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).
4 |
5 | ## SwiftCrypto
6 |
7 | 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.
8 |
9 | These APIs will be available automatically when you import Vapor.
10 |
11 | ```swift
12 | import Vapor
13 |
14 | let digest = SHA256.hash(data: Data("hello".utf8))
15 | print(digest)
16 | ```
17 |
18 | CryptoKit includes support for:
19 |
20 | - Hashing: `SHA512`, `SHA384`, `SHA256`
21 | - Message Authentication Codes: `HMAC`
22 | - Ciphers: `AES`, `ChaChaPoly`
23 | - Public-Key Cryptography: `Curve25519`, `P521`, `P384`, `P256`
24 | - Insecure hashing: `SHA1`, `MD5`
25 |
26 | ## Bcrypt
27 |
28 | 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.
29 |
30 | Vapor provides a `Bcrypt` type for hashing and comparing passwords.
31 |
32 | ```swift
33 | import Vapor
34 |
35 | let digest = try Bcrypt.hash("test")
36 | ```
37 |
38 | Because Bcrypt uses a salt, password hashes cannot be compared directly. Both the plaintext password and the existing digest must be verified together.
39 |
40 | ```swift
41 | import Vapor
42 |
43 | let pass = try Bcrypt.verify("test", created: digest)
44 | if pass {
45 | // Password and digest match.
46 | } else {
47 | // Wrong password.
48 | }
49 | ```
50 |
51 | 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.
52 |
53 | ## TOTP
54 |
55 | Coming soon.
56 |
57 |
--------------------------------------------------------------------------------
/4.0/docs/security/passwords.md:
--------------------------------------------------------------------------------
1 | # Passwords
2 |
3 | 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.
4 |
5 | ## Configuration
6 |
7 | To configure the Application's password hasher, use `app.passwords`.
8 |
9 | ```swift
10 | import Vapor
11 |
12 | app.passwords.use(...)
13 | ```
14 |
15 | ### Bcrypt
16 |
17 | To use Vapor's [Bcrypt API](crypto.md#bcrypt) for password hashing, specify `.bcrypt`. This is the default.
18 |
19 | ```swift
20 | app.passwords.use(.bcrypt)
21 | ```
22 |
23 | Bcrypt will use a cost of 12 unless otherwise specified. You can configure this by passing the `cost` parameter.
24 |
25 | ```swift
26 | app.passwords.use(.bcrypt(cost: 8))
27 | ```
28 |
29 | ### Plaintext
30 |
31 | 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.
32 |
33 | ```swift
34 | switch app.environment {
35 | case .testing:
36 | app.passwords.use(.plaintext)
37 | default: break
38 | }
39 | ```
40 |
41 | ## Hashing
42 |
43 | To hash passwords, use the `password` helper available on `Request`.
44 |
45 | ```swift
46 | let digest = try req.password.hash("vapor")
47 | ```
48 |
49 | Password digests can be verified against the plaintext password using the `verify` method.
50 |
51 | ```swift
52 | let bool = try req.password.verify("vapor", created: digest)
53 | ```
54 |
55 | The same API is available on `Application` for use during boot.
56 |
57 | ```swift
58 | let digest = try app.password.hash("vapor")
59 | ```
60 |
61 | ### Async
62 |
63 | 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.
64 |
65 | ```swift
66 | req.password.async.hash("vapor").map { digest in
67 | // Handle digest.
68 | }
69 | ```
70 |
71 | Verifying digests works similarly:
72 |
73 | ```swift
74 | req.password.async.verify("vapor", created: digest).map { bool in
75 | // Handle result.
76 | }
77 | ```
78 |
79 | Calculating hashes on background threads can free your application's event loops up to handle more incoming requests.
80 |
81 |
--------------------------------------------------------------------------------
/4.0/docs/start/folder-structure.md:
--------------------------------------------------------------------------------
1 | # 项目结构
2 |
3 | 现在,你已经创建并运行了第一个 Vapor 应用程序,让我们稍微花点时间熟悉一下 Vapor 的项目结构。
4 |
5 | 该结构是在 [SPM](spm.md) 的基础上演化而来;因此,如果你曾经使用过 SPM,应该会很熟悉。
6 |
7 |
8 | ```
9 | .
10 | ├── Public
11 | ├── Sources
12 | │ ├── App
13 | │ │ ├── Controllers
14 | │ │ ├── Migrations
15 | │ │ ├── Models
16 | │ │ ├── app.swift
17 | │ │ ├── configure.swift
18 | │ │ └── routes.swift
19 | │ └── Run
20 | │ └── main.swift
21 | ├── Tests
22 | │ └── AppTests
23 | └── Package.swift
24 | ```
25 |
26 | 下面将详细地解释每个文件夹的作用。
27 |
28 | ## Public
29 |
30 | 如果你使用了 `FileMiddleware` 中间件,那么此文件夹包含你的应用程序提供的所有公共文件,通常是图片、`.css`样式和浏览器脚本等。
31 |
32 | 例如,对 `localhost:8080/favicon.ico` 发起的请求将检查是否存在 `Public/favicon.ico` 图片并回应。
33 |
34 | 在 Vapor 可以提供公共文件之前,你需要在 `configure.swift` 文件中启用`FileMiddleware`,参考如下所示:
35 |
36 | ```swift
37 | // 从 'Public/' 目录提供文件
38 | let fileMiddleware = FileMiddleware(
39 | publicDirectory: app.directory.publicDirectory
40 | )
41 | app.middleware.use(fileMiddleware)
42 | ```
43 |
44 | ## Sources
45 |
46 | 该文件夹包含项目的所有 Swift 代码源文件。文件夹 `App`和 `Run`反应软件包的模块,例如这篇 [SPM](spm.md) 文章中所述。
47 |
48 | ### App
49 |
50 | 应用程序的所有核心代码都包含在这里。
51 |
52 | #### Controllers
53 |
54 | 控制器是将应用程序的不同逻辑进行分组的优秀方案,大多数控制器都具备接受多种请求的功能,并根据需要进行响应。
55 |
56 | #### Migrations
57 |
58 | 如果你使用 Fluent,则可以在 Migrations 文件夹中进行数据库迁移。
59 |
60 | #### Models
61 |
62 | models 文件夹常用于存放 `Content` 和 Fluent `Model` 的类或结构体。
63 |
64 | #### configure.swift
65 |
66 | 这个文件包含 `configure(_:)` 函数,`main.swift` 调用这个方法用以配置新创建的 `Application` 实例。你可以在这里注册诸如路由、数据库、providers 等服务。
67 |
68 | #### routes.swift
69 |
70 | 这个文件包含 `routes(_:)` 方法,它会在 `configure(_:)` 结尾处被调用,用以将路由注册到你的`Application`。
71 |
72 | ### Run
73 |
74 | 这是主要的可执行目标,只包含启动和运行应用程序所需的代码。
75 |
76 | ## Tests
77 |
78 | `Sources` 文件夹中的每个不可运行的模块在 `Tests` 中都可以创建一个对应的文件夹,包含 `XCTest` 模块上构建的用例,用来测试你的代码。
79 |
80 | 可以在命令行使用 `swift test`或在 Xcode 中按 `⌘+U` 来进行测试。
81 |
82 |
83 | ### AppTests
84 |
85 | 此文件夹包含 `App` 模块中代码的单元测试。
86 |
87 | ## Package.swift
88 |
89 | 最后,是这个项目运行所依赖的第三方库配置。
90 |
91 |
--------------------------------------------------------------------------------
/4.0/docs/start/hello-world.md:
--------------------------------------------------------------------------------
1 | --
2 | ***校对日期 2021-12-10***
3 | --
4 | # 你好,世界
5 |
6 | 本文将指引你逐步创建、编译并运行 Vapor 的项目。
7 |
8 | 如果尚未安装 Swift 和 Vapor Toolbox,请查看安装部分。
9 |
10 | - [安装 → macOS](../install/macos.md)
11 | - [安装 → Linux](../install/linux.md)
12 |
13 | ## 创建
14 |
15 | 首先,在电脑上创建 Vapor 项目。
16 |
17 | 打开终端并使用以下 Toolbox 的命令行,这将会在当前目录创建一个包含 Vapor 项目的文件夹。
18 |
19 | ```sh
20 | vapor new hello -n
21 | ```
22 |
23 | !!! tip
24 | 使用 `-n` 为所有的问题自动选择 no 来为您提供一个基本的模板。
25 |
26 |
27 | 命令完成后,切换到新创建的文件夹
28 |
29 | ```sh
30 | cd hello
31 | ```
32 |
33 | ## 编译 & 运行
34 |
35 | ### Xcode
36 |
37 | 首先,在Xcode打开项目:
38 |
39 | ```sh
40 | open Package.swift
41 | ```
42 |
43 |
44 | Xcode 将自动开始下载Swift包管理器依赖,在第一次打开一个项目时,这可能需要一些时间,当依赖下载后,Xcode将显示可以用的 Scheme。
45 |
46 | 在窗口的顶部,在Play和Stop按钮的右侧,单击项目名称以选择项目的Scheme,并选择一个适当的target——大概率是“My Mac”。单击play按钮编译并运行项目。
47 |
48 | 你应该会在Xcode窗口的底部看到控制台弹出。
49 |
50 | ```sh
51 | [ INFO ] Server starting on http://127.0.0.1:8080
52 | ```
53 |
54 | ### Linux
55 |
56 | 在 Linux 和其他操作系统上(甚至在 macOS 上如果你不想使用 Xcode ),你可以在你喜欢的编辑器中编辑项目,比如 Vim 或 VSCode 。关于设置其他ide的最新细节,请参阅 [Swift Server Guides](https://github.com/swift-server/guides/blob/main/docs/setup-and-ide-alternatives.md)。
57 |
58 | 在终端运行以下命令来编译和运行你的项目。
59 |
60 | ```sh
61 | swift run
62 | ```
63 | 它将构建并运行项目。第一次运行时,需要花费一些时间来获取和下载依赖项。一旦运行,你应该在你的控制台中看到以下内容:
64 |
65 | ```sh
66 | [ INFO ] Server starting on http://127.0.0.1:8080
67 | ```
68 |
69 | ## Visit Localhost
70 |
71 | 打开你的浏览器,然后访问 localhost:8080/hello 或者 http://127.0.0.1:8080
72 |
73 | 你将看见以下页面
74 |
75 | ```html
76 | Hello, world!
77 | ```
78 |
79 | 恭喜你创建,构建,运行了你的第一个 Vapor 应用!🎉
--------------------------------------------------------------------------------
/4.0/docs/start/spm.md:
--------------------------------------------------------------------------------
1 | # Swift Package Manager
2 |
3 | [Swift Package Manager](https://swift.org/package-manager/)(SPM)用于构建项目的源代码和依赖项。由于 Vapor 严重依赖 SPM,因此最好了解其工作原理。
4 |
5 | SPM 与 Cocoapods,Ruby gems 和 NPM 相似。您可以在命令行中将 SPM 与 `swift build`、`swift test` 等命令或兼容的 IDE 结合使用。但是,与其他软件包管理器不同,SPM 软件包没有中央软件包索引。SPM 使用 [Git 标签](https://git-scm.com/book/en/v2/Git-Basics-Tagging) 和 URL 来获取 Git 存储库和依赖版本。
6 |
7 | ## Package Manifest
8 |
9 | SPM 在项目中查找的第一项是 package 清单。它应始终位于项目的根目录中,并命名为 `Package.swift`。
10 |
11 | 看一下这个示例:
12 |
13 | ```swift
14 | // swift-tools-version:5.2
15 | import PackageDescription
16 |
17 | let package = Package(
18 | name: "app",
19 | platforms: [
20 | .macOS(.v10_15)
21 | ],
22 | products: [
23 | .executable(name: "Run", targets: ["Run"]),
24 | .library(name: "App", targets: ["App"]),
25 | ],
26 | dependencies: [
27 | .package(url: "https://github.com/vapor/vapor.git", from: "4.0.0"),
28 | ],
29 | targets: [
30 | .target(name: "App", dependencies: [.product(name: "Vapor", package: "vapor")]),
31 | .target(name: "Run", dependencies: ["App"]),
32 | .testTarget(name: "AppTests", dependencies: ["App"])
33 | ]
34 | )
35 |
36 | ```
37 |
38 | 下面将对这段代码的各部分进行说明。
39 |
40 | ### Tools Version
41 |
42 | 第一行表示需要使用的 Swift tools 版本号,它指明了 Swift 的最低可用版本。Package 描述 API 可能随着 Swift 版本而改变,所以这一行将让 Swift 确认怎么取解析你的配置文件。
43 |
44 | ### Package Name
45 |
46 | `Package` 的第一个参数代表当前 package 的名字。如果软件包是公共的,你应该使用 Git 存储库的 URL 的最后一段作为名称
47 |
48 | ### Platforms
49 |
50 | `platforms` 数组指定此程序包支持的平台和版本。通过指定 `.macOS(.v10_14)`,说明此软件包需要 macOS Mojave 或更高版本。 Xcode 加载该项目时,它将最低部署版本设置为 10.14,以便您可以使用所有可用的 API。
51 |
52 | ### Products
53 |
54 | products 字段代表 package 构建的时候要生成的 targets。示例中,有两个 target,一个是 `library`,另一个是 `executable`。
55 |
56 | ### Dependencies
57 |
58 | dependencies 字段代表项目需要依赖的 package。所有 Vapor 应用都依赖 Vapor package ,但是你也可以添加其它想要的 dependency。
59 |
60 | 如上面这个示例,[vapor/vapor](https://github.com/vapor/vapor) 4.0 或以上版本是这个 package 的 dependency。当在 package 中添加了 dependency 后,接下来你必须设置是哪个 targets 依赖了新的可用模块。
61 |
62 | ### Targets
63 |
64 | Targets 是你的 package 里包含 modules、executables 以及 tests 总和。虽然可以添加任意多的 targets 来组织代码,但大部分 Vapor 应用有 3 个 target 就足够了。每个 target 声明了它依赖的 module。为了在代码中可以 import 这些 modules ,你必须在这里添加 module 名字。一个 target 可以依赖于工程中其它的 target 或者任意你添加在 [dependencies](#dependencies) 数组中且暴露出来的 modules。
65 |
66 | !!! tip
67 | 可运行 targets (包含 `main.swift` 文件的 target) 不能被其它 modules 导入。这就是为什么 Vapor 会有 `App` 和 `Run` 两种 target。任何包含在 App 中的代码都可以在 `AppTests` 中被测试验证。
68 |
69 | ## Folder Structure
70 |
71 | 以下是典型的 SPM package 目录结构。
72 |
73 | ```
74 | .
75 | ├── Sources
76 | │ ├── App
77 | │ │ └── (Source code)
78 | │ └── Run
79 | │ └── main.swift
80 | ├── Tests
81 | │ └── AppTests
82 | └── Package.swift
83 | ```
84 |
85 | 每个 `.target` 对应 `Sources` 中的一个文件夹。
86 | 每个 `.testTarget` 对应 `Tests` 中的一个文件夹。
87 |
88 | ## Package.resolved
89 |
90 | 第一次构建成功后,SPM 将会自动创建一个 `Package.resolved` 文件。`Package.resolved` 保存了当前项目所有用到的 `dependency` 版本。下一次当你构建你的项目时将会同样的版本,甚至是这些依赖有更新的版本也不会也使用更新的版本。
91 |
92 | 更新依赖, 运行 `swift package update`.
93 |
94 | ## Xcode
95 |
96 | 如果使用 Xcode 11 或更高版本,则在修改 `Package.swift` 文件时,将自动更改 dependencies、targets、products 等。
97 |
98 | 如果要更新到最新的依赖项,请使用 File → Swift Packages → 更新到最新的 Swift Package 版本。
99 |
100 | 您可能还想将 `.swiftpm` 文件添加到您的 `.gitignore` 文件中(Xcode 在此处存储 Xcode 项目配置)。
101 |
--------------------------------------------------------------------------------
/4.0/docs/start/xcode.md:
--------------------------------------------------------------------------------
1 | # Xcode
2 |
3 | 这页将显示一下使用Xcode的提示和技巧。如果你使用不同的开发环境,你可以跳过此页。
4 |
5 | ## 自定义工作目录(Working directory)
6 |
7 | Xcode 将默认在 _DerivedData_ 目录运行项目。这与项目的根目录(你的 _Package.swift_ 文件所在的目录)不在同一个目录,这意味着 Vapor 将找不到像 _.env_ 或者 _Public_ 等一些文件和目录。
8 |
9 | 如果在运行应用程序时看到以下警告,您就可以知道这正在发生。
10 |
11 | ```fish
12 | [ WARNING ] No custom working directory set for this scheme, using /path/to/DerivedData/project-abcdef/Build/
13 | ```
14 |
15 | 要解决这个问题,你可以在 Xcode schem 中为你的项目设置一个自定义的工作目录。
16 |
17 | 首先,编辑项目的 scheme。
18 |
19 | 
20 |
21 | 在下拉框中选择 _Edit Scheme..._
22 |
23 | 
24 |
25 | 在 scheme 编辑器中,选择 _Run_ action 以及 _Options_ tab页。选中 _Use custom working directory_ 然后输入你项目根目录。
26 |
27 | 
28 |
29 | 你可以在终端中运行 `pwd` 来获取你项目根目录的绝对目录
30 |
31 | ```fish
32 | # 确认我们在 vapor 项目目录
33 | vapor --version
34 | # get path to this folder
35 | pwd
36 | ```
37 |
38 | 你应该能看见类似下面的输出。
39 |
40 | ```
41 | framework: 4.x.x
42 | toolbox: 18.x.x
43 | /path/to/project
44 | ```
45 |
--------------------------------------------------------------------------------
/4.0/docs/version/4_0.md:
--------------------------------------------------------------------------------
1 | # Redirecting...
2 |
3 |
4 |
--------------------------------------------------------------------------------
/4.0/docs/version/upgrading.md:
--------------------------------------------------------------------------------
1 | # Upgrading to 4.0
2 |
3 | This guide shows you how to upgrade an existing Vapor 3.x project to 4.0. This guide attempts to cover all of Vapor's official packages as well as some commonly used providers. If you notice anything missing, [Vapor's team chat](https://discord.gg/vapor) is a great place to ask for help. Issues and pull requests are also appreciated.
4 |
5 | ## Dependencies
6 |
7 | To use Vapor 4, you will need Xcode 11.4 and macOS 10.15 or greater.
8 |
9 | The Install section of the docs goes over installing dependencies.
10 |
11 | ## Package.swift
12 |
13 | The first step to upgrading to Vapor 4 is to update your package's dependencies. Below is an example of an upgraded Package.swift file. You can also check out the updated [template Package.swift](https://github.com/vapor/template/blob/master/Package.swift).
14 |
15 | ```diff
16 | -// swift-tools-version:4.0
17 | +// swift-tools-version:5.2
18 | import PackageDescription
19 |
20 | let package = Package(
21 | name: "api",
22 | + platforms: [
23 | + .macOS(.v10_15),
24 | + ],
25 | dependencies: [
26 | - .package(url: "https://github.com/vapor/fluent-postgresql.git", from: "1.0.0"),
27 | + .package(url: "https://github.com/vapor/fluent.git", from: "4.0.0-rc"),
28 | + .package(url: "https://github.com/vapor/fluent-postgres-driver.git", from: "2.0.0-rc"),
29 | - .package(url: "https://github.com/vapor/jwt.git", from: "3.0.0"),
30 | + .package(url: "https://github.com/vapor/jwt.git", from: "4.0.0-rc"),
31 | - .package(url: "https://github.com/vapor/vapor.git", from: "3.0.0"),
32 | + .package(url: "https://github.com/vapor/vapor.git", from: "4.0.0-rc"),
33 | ],
34 | targets: [
35 | .target(name: "App", dependencies: [
36 | - "FluentPostgreSQL",
37 | + .product(name: "Fluent", package: "fluent"),
38 | + .product(name: "FluentPostgresDriver", package: "fluent-postgres-driver"),
39 | - "Vapor",
40 | + .product(name: "Vapor", package: "vapor"),
41 | - "JWT",
42 | + .product(name: "JWT", package: "jwt"),
43 | ]),
44 | - .target(name: "Run", dependencies: ["App"]),
45 | - .testTarget(name: "AppTests", dependencies: ["App"])
46 | + .target(name: "Run", dependencies: [
47 | + .target(name: "App"),
48 | + ]),
49 | + .testTarget(name: "AppTests", dependencies: [
50 | + .target(name: "App"),
51 | + ])
52 | ]
53 | )
54 | ```
55 |
56 | All packages that have been upgraded for Vapor 4 will have their major version number incremented by one.
57 |
58 | !!! warning
59 | The `-rc` pre-release identifier is used since Vapor 4 has not been officially released yet.
60 |
61 | ### Old Packages
62 |
63 | Some packages may not be upgraded yet. If you encounter any, file an issue to let the author know.
64 |
65 | Some Vapor 3 packages have been deprecated, such as:
66 |
67 | - `vapor/auth`: Now included in Vapor.
68 | - `vapor/core`: Absorbed into several modules.
69 | - `vapor/crypto`: Replaced by SwiftCrypto.
70 | - `vapor/multipart`: Now included in Vapor.
71 | - `vapor/url-encoded-form`: Now included in Vapor.
72 | - `vapor-community/vapor-ext`: Now included in Vapor.
73 | - `vapor-community/pagination`: Now part of Fluent.
74 | - `IBM-Swift/LoggerAPI`: Replaced by SwiftLogging.
75 |
76 | ### Fluent
77 |
78 | `vapor/fluent` must now be added as a separate dependency to your dependencies list and targets. All database-specific packages have been suffixed with `-driver` to make the requirement on `vapor/fluent` clear.
79 |
80 | ```diff
81 | - .package(url: "https://github.com/vapor/fluent-postgresql.git", from: "1.0.0"),
82 | + .package(url: "https://github.com/vapor/fluent.git", from: "4.0.0-rc"),
83 | + .package(url: "https://github.com/vapor/fluent-postgres-driver.git", from: "2.0.0-rc"),
84 | ```
85 |
86 | ### Platforms
87 |
88 | Vapor's package manifests now explicitly support macOS 10.15 and greater. This means your package will also need to specify platform support.
89 |
90 | ```diff
91 | + platforms: [
92 | + .macOS(.v10_15),
93 | + ],
94 | ```
95 |
96 | Vapor may add additional supported platforms in the future. Your package may support any subset of these platforms as long as the version number is equal or greater to Vapor's minimum version requirements.
97 |
98 | ### Xcode
99 |
100 | Vapor 4 utilizies Xcode 11's native SPM support. This means you will no longer need to generate `.xcodeproj` files. Opening your project's folder in Xcode will automatically recognize SPM and pull in dependencies.
101 |
102 | You can open your project natively in Xcode using `vapor xcode` or `open Package.swift`.
103 |
104 | Once you've updated Package.swift, you may need to close Xcode and clear the following folders from the root directory:
105 |
106 | - `Package.resolved`
107 | - `.build`
108 | - `.swiftpm`
109 | - `*.xcodeproj`
110 |
111 | Once your updated packages have resolved successfully you should see compiler errors--probably quite a few. Don't worry! We'll show you how to fix them.
112 |
113 | ## Run
114 |
115 | The first order of business is to update your Run module's `main.swift` file to the new format.
116 |
117 | ```swift
118 | import App
119 | import Vapor
120 |
121 | var env = try Environment.detect()
122 | try LoggingSystem.bootstrap(from: &env)
123 | let app = Application(env)
124 | defer { app.shutdown() }
125 | try configure(app)
126 | try app.run()
127 | ```
128 |
129 | The `main.swift` file's contents replace the App module's `app.swift`, so you can delete that file.
130 |
131 | ## App
132 |
133 | Let's take a look at how to update the basic App module structure.
134 |
135 | ### configure.swift
136 |
137 | The `configure` method should be changed to accept an instance of `Application`.
138 |
139 | ```diff
140 | - public func configure(_ config: inout Config, _ env: inout Environment, _ services: inout Services) throws
141 | + public func configure(_ app: Application) throws
142 | ```
143 |
144 | Below is an example of an updated configure method.
145 |
146 | ```swift
147 | import Fluent
148 | import FluentSQLiteDriver
149 | import Vapor
150 |
151 | // Called before your application initializes.
152 | public func configure(_ app: Application) throws {
153 | // Serves files from `Public/` directory
154 | // app.middleware.use(FileMiddleware(publicDirectory: app.directory.publicDirectory))
155 | // Configure SQLite database
156 | app.databases.use(.sqlite(.file("db.sqlite")), as: .sqlite)
157 |
158 | // Configure migrations
159 | app.migrations.add(CreateTodo())
160 |
161 | try routes(app)
162 | }
163 | ```
164 |
165 | Syntax changes for configuring things like routing, middleware, fluent, and more are mentioned below.
166 |
167 | ### boot.swift
168 |
169 | `boot`'s contents can be placed in the `configure` method since it now accepts the application instance.
170 |
171 | ### routes.swift
172 |
173 | The `routes` method should be changed to accept an instance of `Application`.
174 |
175 | ```diff
176 | - public func routes(_ router: Router, _ container: Container) throws
177 | + public func routes(_ app: Application) throws
178 | ```
179 |
180 | More information on changes to routing syntax are mentioned below.
181 |
182 | ## Services
183 |
184 | Vapor 4's services APIs have been simplified to make it easier for you to discover and use services. Services are now exposed as methods and properties on `Application` and `Request` which allows the compiler to help you use them.
185 |
186 | To understand this better, let's take a look at a few examples.
187 |
188 | ```diff
189 | // Change the server's default port to 8281
190 | - services.register { container -> NIOServerConfig in
191 | - return .default(port: 8281)
192 | - }
193 | + app.server.configuration.port = 8281
194 | ```
195 |
196 | Instead of registering a `NIOServerConfig` to services, server configuration is now exposed as simple properties on Application that can be overridden.
197 |
198 | ```diff
199 | // Register cors middleware
200 | let corsConfiguration = CORSMiddleware.Configuration(
201 | allowedOrigin: .all,
202 | allowedMethods: [.POST, .GET, .PATCH, .PUT, .DELETE, .OPTIONS]
203 | )
204 | let corsMiddleware = CORSMiddleware(configuration: corsConfiguration)
205 | - var middlewares = MiddlewareConfig() // Create _empty_ middleware config
206 | - middlewares.use(corsMiddleware)
207 | - services.register(middlewares)
208 | + app.middleware.use(corsMiddleware)
209 | ```
210 |
211 | Instead of creating and registering a `MiddlewareConfig` to services, middleware are now exposed as a property on Application that can be added to.
212 |
213 | ```diff
214 | // Make a request in a route handler.
215 | - try req.make(Client.self).get("https://vapor.codes")
216 | + req.client.get("https://vapor.codes")
217 | ```
218 |
219 | Like Application, Request also exposes services as simple properties and methods. Request-specific services should always be used when inside a route closure.
220 |
221 | This new service pattern replaces the `Container`, `Service`, and `Config` types from Vapor 3.
222 |
223 | ### Providers
224 |
225 | Providers are no longer required to configure third party packages. Each package instead extends Application and Request with new properties and methods for configuration.
226 |
227 | Take a look at how Leaf is configured in Vapor 4.
228 |
229 | ```diff
230 | // Use Leaf for view rendering.
231 | - try services.register(LeafProvider())
232 | - config.prefer(LeafRenderer.self, for: ViewRenderer.self)
233 | + app.views.use(.leaf)
234 | ```
235 |
236 | To configure Leaf, use the `app.leaf` property.
237 |
238 | ```diff
239 | // Disable Leaf view caching.
240 | - services.register { container -> LeafConfig in
241 | - return LeafConfig(tags: ..., viewsDir: ..., shouldCache: false)
242 | - }
243 | + app.leaf.cache.isEnabled = false
244 | ```
245 |
246 | ### Environment
247 |
248 | The current environment (production, development, etc) can be accessed via `app.environment`.
249 |
250 | ### Custom Services
251 |
252 | Custom services conforming to the `Service` protocol and registered to the container in Vapor 3 can be now be expressed as extensions to either Application or Request.
253 |
254 | ```diff
255 | struct MyAPI {
256 | let client: Client
257 | func foo() { ... }
258 | }
259 | - extension MyAPI: Service { }
260 | - services.register { container -> MyAPI in
261 | - return try MyAPI(client: container.make())
262 | - }
263 | + extension Request {
264 | + var myAPI: MyAPI {
265 | + .init(client: self.client)
266 | + }
267 | + }
268 | ```
269 |
270 | This service can then be accessed using the extension instead of `make`.
271 |
272 | ```diff
273 | - try req.make(MyAPI.self).foo()
274 | + req.myAPI.foo()
275 | ```
276 |
277 | ### Custom Providers
278 |
279 | Most custom services can be implemented using extensions as shown in the previous section. However, some advanced providers may need to hook into the application lifecycle or use stored properties.
280 |
281 | Application's new `Lifecycle` helper can be used to register lifecycle handlers.
282 |
283 | ```swift
284 | struct PrintHello: LifecycleHandler {
285 | func willBoot(_ app: Application) throws {
286 | print("Hello!")
287 | }
288 | }
289 |
290 | app.lifecycle.use(PrintHello())
291 | ```
292 |
293 | To store values on Application, you case use the new `Storage` helper.
294 |
295 | ```swift
296 | struct MyNumber: StorageKey {
297 | typealias Value = Int
298 | }
299 | app.storage[MyNumber.self] = 5
300 | print(app.storage[MyNumber.self]) // 5
301 | ```
302 |
303 | Accessing `app.storage` can be wrapped in a settable computed property to create a concise API.
304 |
305 | ```swift
306 | extension Application {
307 | var myNumber: Int? {
308 | get { self.storage[MyNumber.self] }
309 | set { self.storage[MyNumber.self] = newValue }
310 | }
311 | }
312 |
313 | app.myNumber = 42
314 | print(app.myNumber) // 42
315 | ```
316 |
317 | ## NIO
318 |
319 | Vapor 4 now exposes SwiftNIO's async APIs directly and does not attempt to overload methods like `map` and `flatMap` or alias types like `EventLoopFuture`. Vapor 3 provided overloads and aliases for backward compatibility with early beta versions that were released before SwiftNIO existed. These have been removed to reduce confusion with other SwiftNIO compatible packages and better follow SwiftNIO's best practice recommendations.
320 |
321 | ### Async naming changes
322 |
323 | The most obvious change is that the `Future` typealias for `EventLoopFuture` has been removed. This can be fixed fairly easily with a find and replace.
324 |
325 | Furthermore, NIO does not support the `to:` labels that Vapor 3 added. Given Swift 5.2's improved type inference, `to:` is less necessary now anyway.
326 |
327 | ```diff
328 | - futureA.map(to: String.self) { ... }
329 | + futureA.map { ... }
330 | ```
331 |
332 | Methods prefixed with `new`, like `newPromise` have been changed to `make` to better suit Swift style.
333 |
334 | ```diff
335 | - let promise = eventLoop.newPromise(String.self)
336 | + let promise = eventLoop.makePromise(of: String.self)
337 | ```
338 |
339 | `catchMap` is no longer available, but NIO's methods like `mapError` and `flatMapErrorThrowing` will work instead.
340 |
341 | Vapor 3's global `flatMap` method for combining multiple futures is no longer available. This can be replaced by using NIO's `and` method to combine many futures together.
342 |
343 | ```diff
344 | - flatMap(futureA, futureB) { a, b in
345 | + futureA.and(futureB).flatMap { (a, b) in
346 | // Do something with a and b.
347 | }
348 | ```
349 |
350 | ### ByteBuffer
351 |
352 | Many methods and properties that previously used `Data` now use NIO's `ByteBuffer`. This type is a more powerful and performant byte storage type. You can read more about its API in [SwiftNIO's ByteBuffer docs](https://apple.github.io/swift-nio/docs/current/NIO/Structs/ByteBuffer.html).
353 |
354 | To convert a `ByteBuffer` back to `Data`, use:
355 |
356 | ```swift
357 | Data(buffer.readableBytesView)
358 | ```
359 |
360 | ### Throwing map / flatMap
361 |
362 | The most difficult change is that `map` and `flatMap` can no longer throw. `map` has a throwing version named (somewhat confusingly) `flatMapThrowing`. `flatMap` however has no throwing counterpart. This may require you to restructure some asynchronous code.
363 |
364 | Maps that do _not_ throw should continue to work fine.
365 |
366 | ```swift
367 | // Non-throwing map.
368 | futureA.map { a in
369 | return b
370 | }
371 | ```
372 |
373 | Maps that _do_ throw must be renamed to `flatMapThrowing`.
374 |
375 | ```diff
376 | - futureA.map { a in
377 | + futureA.flatMapThrowing { a in
378 | if ... {
379 | throw SomeError()
380 | } else {
381 | return b
382 | }
383 | }
384 | ```
385 |
386 | Flat-maps that do _not_ throw should continue to work fine.
387 |
388 | ```swift
389 | // Non-throwing flatMap.
390 | futureA.flatMap { a in
391 | return futureB
392 | }
393 | ```
394 |
395 | Flat-maps that _do_ throw must return a future error.
396 |
397 | ```swift
398 | // Returning a future error.
399 | futureA.flatMap { a in
400 | if ... {
401 | return eventLoop.makeFailedFuture(SomeError())
402 | } else {
403 | return futureB
404 | }
405 | }
406 | ```
407 |
408 | When calling methods that throw, the error can be caught in a do / catch and returned as a future.
409 |
410 | ```swift
411 | // Returning a caught error as a future.
412 | futureA.flatMap { a in
413 | do {
414 | try doSomething()
415 | return futureB
416 | } catch {
417 | return eventLoop.makeFailedFuture(error)
418 | }
419 | }
420 | ```
421 |
422 | Throwing method calls can also be refactored into a `flatMapThrowing` and chained using tuples.
423 |
424 | ```swift
425 | // Refactored throwing method into flatMapThrowing with tuple-chaining.
426 | futureA.flatMapThrowing { a in
427 | try (a, doSomeThing())
428 | }.flatMap { (a, result) in
429 | // result is the value of doSomething.
430 | return futureB
431 | }
432 | ```
433 |
434 | ## Routing
435 |
436 | Routes are now registered directly to Application.
437 |
438 | ```swift
439 | app.get("hello") { req in
440 | return "Hello, world"
441 | }
442 | ```
443 |
444 | This means you no longer need to register a router to services. Simply pass the application to your `routes` method and start adding routes. All of the methods available on `RoutesBuilder` are available on `Application`.
445 |
446 | ### Synchronous Content
447 |
448 | Decoding request content is now synchronous.
449 |
450 | ```swift
451 | let payload = try req.content.decode(MyPayload.self)
452 | print(payload) // MyPayload
453 | ```
454 |
455 | This behavior can be overridden by register routes using the `.stream` body collection strategy.
456 |
457 | ```swift
458 | app.on(.POST, "streaming", body: .stream) { req in
459 | // Request body is now asynchronous.
460 | req.body.collect().map { buffer in
461 | HTTPStatus.ok
462 | }
463 | }
464 | ```
465 |
466 | ### Comma-separated paths
467 |
468 | Paths must now be comma separated and not contain `/` for consistency.
469 |
470 | ```diff
471 | - router.get("v1/users/", "posts", "/comments") { req in
472 | + app.get("v1", "users", "posts", "comments") { req in
473 | // Handle request.
474 | }
475 | ```
476 |
477 | ### Route parameters
478 |
479 | The `Parameter` protocol has been removed in favor of explicitly named parameters. This prevents issues with duplicate parameters and un-ordered fetching of parameters in middleware and route handlers.
480 |
481 | ```diff
482 | - router.get("planets", String.parameter) { req in
483 | - let id = req.parameters.next(String.self)
484 | + app.get("planets", ":id") { req in
485 | + let id = req.parameters.get("id")
486 | return "Planet id: \(id)"
487 | }
488 | ```
489 |
490 | Route parameter usage with models is mentioned in the Fluent section.
491 |
492 | ## Middleware
493 |
494 | `MiddlewareConfig` has been renamed to `MiddlewareConfiguration` and is now a property on Application. You can add middleware to your app using `app.middleware`.
495 |
496 | ```diff
497 | let corsMiddleware = CORSMiddleware(configuration: ...)
498 | - var middleware = MiddlewareConfig()
499 | - middleware.use(corsMiddleware)
500 | + app.middleware.use(corsMiddleware)
501 | - services.register(middlewares)
502 | ```
503 |
504 | Middleware can no longer be registered by type name. Initialize the middleware first before registering.
505 |
506 | ```diff
507 | - middleware.use(ErrorMiddleware.self)
508 | + app.middleware.use(ErrorMiddleware.default(environment: app.environment))
509 | ```
510 |
511 | To remove all default middleware, set `app.middleware` to an empty config using:
512 |
513 | ```swift
514 | app.middleware = .init()
515 | ```
516 |
517 | ## HTTP
518 |
519 | Coming soon.
520 |
521 | ## WebSocket
522 |
523 | Coming soon.
524 |
525 | ## Fluent
526 |
527 | Coming soon.
528 |
529 | ## Crypto
530 |
531 | Coming soon.
532 |
533 | ## Queues
534 |
535 | Coming soon.
536 |
537 | ## Validation
538 |
539 | Coming soon.
540 |
541 | ## Auth
542 |
543 | Coming soon.
544 |
545 | ## Stripe
546 |
547 | Coming soon.
548 |
549 | ## Mailgun
550 |
551 | Coming soon.
552 |
553 | ## Leaf
554 |
555 | Coming soon.
556 |
--------------------------------------------------------------------------------
/4.0/mkdocs.yml:
--------------------------------------------------------------------------------
1 | site_name: "Vapor 中文文档"
2 | site_url: https://cn.docs.vapor.codes/4.0/
3 | repo_url: https://github.com/vapor/docs-cn
4 |
5 | # Configuration
6 | theme:
7 | name: material
8 | custom_dir: "theme/"
9 |
10 | # Language
11 | language: zh
12 | palette:
13 | primary: black
14 | accent: blue
15 |
16 | logo: assets/logo.png
17 | favicon: assets/favicon.png
18 |
19 | # Customization
20 | extra:
21 | # Font
22 | font:
23 | text: Roboto Slab
24 | code: Source Code Pro
25 |
26 | # Social
27 | social:
28 | - icon: fontawesome/brands/twitter
29 | link: https://twitter.com/@codevapor
30 | - icon: fontawesome/brands/discord
31 | link: http://vapor.team/
32 | - icon: fontawesome/brands/github
33 | link: https://github.com/vapor
34 |
35 | #Extensions
36 | markdown_extensions:
37 | - admonition
38 | - codehilite:
39 | guess_lang: false
40 | - footnotes
41 | - meta
42 | - toc:
43 | permalink: true
44 |
45 |
46 |
47 | nav:
48 | - "序言": "index.md"
49 | - "安装":
50 | - "macOS": "install/macos.md"
51 | - "Linux": "install/linux.md"
52 | - "开始":
53 | - "你好,世界": "start/hello-world.md"
54 | - "项目结构": "start/folder-structure.md"
55 | - "SPM": "start/spm.md"
56 | - "Xcode": "start/xcode.md"
57 | - "入门":
58 | - "路由": "basics/routing.md"
59 | - "控制器": "basics/controllers.md"
60 | - "内容": "basics/content.md"
61 | - "客户端": "basics/client.md"
62 | - "验证": "basics/validation.md"
63 | - "异步": "basics/async.md"
64 | - "日志": "basics/logging.md"
65 | - "环境": "basics/environment.md"
66 | - "错误": "basics/errors.md"
67 | - "Fluent":
68 | - "概述": "fluent/overview.md"
69 | - "模型": "fluent/model.md"
70 | - "关联": "fluent/relations.md"
71 | - "迁移": "fluent/migration.md"
72 | - "查询": "fluent/query.md"
73 | - "模式": "fluent/schema.md"
74 | - "高级": "fluent/advanced.md"
75 | - "进阶":
76 | - "中间件": "advanced/middleware.md"
77 | - "测试": "advanced/testing.md"
78 | - "服务": "advanced/server.md"
79 | - "命令": "advanced/commands.md"
80 | - "队列": "advanced/queues.md"
81 | - "WebSockets": "advanced/websockets.md"
82 | - "Sessions": "advanced/sessions.md"
83 | - "Services": "advanced/services.md"
84 | - "安全":
85 | - "认证": "security/authentication.md"
86 | - "加密": "security/crypto.md"
87 | - "密码": "security/passwords.md"
88 | - "部署":
89 | - "DigitalOcean": "deploy/digital-ocean.md"
90 | - "Heroku": "deploy/heroku.md"
91 | - "Supervisor": "deploy/supervisor.md"
92 | - "Nginx": "deploy/nginx.md"
93 | - "Docker": "deploy/docker.md"
94 | - "版本 (4.0)":
95 | - "4.0": "version/4_0.md"
96 | - "更新指南": "version/upgrading.md"
97 |
--------------------------------------------------------------------------------
/4.0/theme/main.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block htmltitle %}
4 | {% if page and page.meta and page.meta.title %}
5 | {{ page.meta.title }} - {{ config.site_name }}
6 | {% elif page and page.title and not page.is_homepage %}
7 | Vapor: {%
8 | if page.parent.title
9 | %} {{ page.parent.title}} → {%
10 | endif
11 | %} {{ page.title }}
12 | {% else %}
13 | {{ config.site_name }}
14 | {% endif %}
15 | {% endblock %}
16 |
17 | {% block styles %}
18 | {{ super() }}
19 |
20 | {% endblock %}
21 |
22 | {% block libs %}
23 | {{ super() }}
24 |
25 | {% endblock %}
--------------------------------------------------------------------------------
/4.0/theme/partials/language/zh.html:
--------------------------------------------------------------------------------
1 | {#-
2 | This file was automatically generated - do not edit
3 | -#}
4 | {% macro t(key) %}{{ {
5 | "language": "zh",
6 | "clipboard.copy": "复制",
7 | "clipboard.copied": "已复制",
8 | "edit.link.title": "编辑此页",
9 | "footer.previous": "上一页",
10 | "footer.next": "下一页",
11 | "meta.comments": "评论",
12 | "meta.source": "来源",
13 | "search.config.lang": "ja",
14 | "search.config.separator": "[\,\。]+",
15 | "search.placeholder": "搜索",
16 | "search.result.placeholder": "键入以开始搜索",
17 | "search.result.none": "没有找到符合条件的结果",
18 | "search.result.one": "找到 1 个符合条件的结果",
19 | "search.result.other": "# 个符合条件的结果",
20 | "skip.link.title": "跳转至",
21 | "source.link.title": "前往 GitHub 仓库",
22 | "source.revision.date": "最后更新",
23 | "toc.title": "目录"
24 | }[key] }}{% endmacro %}
25 |
--------------------------------------------------------------------------------
/4.0/theme/scripts/carbon.js:
--------------------------------------------------------------------------------
1 | // data-md-component="toc"
2 | document.addEventListener("DOMContentLoaded", function(event) {
3 | var toc_inner = document.querySelectorAll('[data-md-component=toc] .md-sidebar__inner')[0];
4 | var script = document.createElement("script");
5 | script.src = '//cdn.carbonads.com/carbon.js?serve=CK7DT2QW&placement=vaporcodes';
6 | script.type = 'text/javascript';
7 | script.id = '_carbonads_js';
8 | toc_inner.appendChild(script);
9 | });
--------------------------------------------------------------------------------
/4.0/theme/styles/carbon.css:
--------------------------------------------------------------------------------
1 | #carbonads {
2 | margin-left: 10px;
3 | margin-top: 12px;
4 | display: block;
5 | overflow: hidden;
6 | max-width: 160px;
7 | border: solid 1px hsla(0, 0%, 0%, .1);
8 | border-radius: 4px;
9 | background-color: hsl(0, 0%, 98%);
10 | text-align: center;
11 | font-size: 12px;
12 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu,
13 | Cantarell, "Helvetica Neue", Helvetica, Arial, sans-serif;
14 | line-height: 1.5;
15 | }
16 |
17 | #carbonads a {
18 | color: inherit;
19 | text-decoration: none;
20 | }
21 |
22 | #carbonads a:hover {
23 | color: inherit;
24 | }
25 |
26 | #carbonads span {
27 | position: relative;
28 | display: block;
29 | overflow: hidden;
30 | }
31 |
32 | .carbon-img {
33 | display: block;
34 | margin-bottom: 8px;
35 | max-width: 160px;
36 | line-height: 1;
37 | }
38 |
39 | .carbon-img img {
40 | display: block;
41 | margin: 0 auto;
42 | max-width: 160px !important;
43 | width: 160px;
44 | height: auto;
45 | }
46 |
47 | .carbon-text {
48 | display: block;
49 | padding: 0 1em 8px;
50 | }
51 |
52 | .carbon-poweredby {
53 | display: block;
54 | padding: 10px 12px;
55 | background: repeating-linear-gradient(-45deg, transparent, transparent 5px, hsla(0, 0%, 0%, .025) 5px, hsla(0, 0%, 0%, .025) 10px) hsla(203, 11%, 95%, .4);
56 | text-transform: uppercase;
57 | letter-spacing: .5px;
58 | font-weight: 600;
59 | font-size: 9px;
60 | line-height: 0;
61 | }
62 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # 致谢
2 |
3 | 这个页面集中记录了大家这段时期的努力和付出,以表谢意!
4 |
5 | 感谢所有参与翻译的朋友们!
6 |
7 | ## 序言
8 |
9 | |序号| 文件名| 中文标题 | 译者 | 校对
10 | |:--:|:--:|:--:|:--:|:--:|
11 | | 1 | index.md | 序言 | [Jinxiansen](https://github.com/jinxiansen) | [Jinxiansen](https://github.com/jinxiansen) |
12 |
13 | ## 安装
14 |
15 | |序号| 文件名| 中文标题 | 译者 | 校对
16 | |:--:|:--:|:--:|:--:|:--:|
17 | | 1 | macos.md | macOS 安装 | [Jinxiansen](https://github.com/jinxiansen) | [Jinxiansen](https://github.com/jinxiansen) |
18 | | 2 | ubuntu.md | Ubuntu 安装 | [Jinxiansen](https://github.com/jinxiansen) | [Jinxiansen](https://github.com/jinxiansen) |
19 |
20 |
21 | ## 开始
22 | |序号| 文件名| 中文标题 | 译者 | 校对
23 | |:--:|:--:|:--:|:--:|:--:|
24 | | 1 | hello-world.md | 你好,世界 | [Jinxiansen](https://github.com/jinxiansen) | [Jinxiansen](https://github.com/jinxiansen) |
25 | | 2 | folder-structure.md | 项目结构 | [Jinxiansen](https://github.com/jinxiansen) | [Jinxiansen](https://github.com/jinxiansen) |
26 | | 3 | spm.md| SPM | [OHeroJ](https://github.com/OHeroJ) | [Jinxiansen](https://github.com/jinxiansen) |
27 |
28 | ## 入门
29 |
30 | |序号| 文件名| 中文标题 | 译者 | 校对
31 | |:--:|:--:|:--:|:--:|:--:|
32 | | 1 | routing.md | 路由 | [Jinxiansen](https://github.com/jinxiansen) | [Jinxiansen](https://github.com/jinxiansen) |
33 | | 2 | controllers.md | 控制器 | [Jinxiansen](https://github.com/jinxiansen) | [Jinxiansen](https://github.com/jinxiansen) |
34 | | 3 | content.md | 内容 | [DmC-l](https://github.com/DmC-l) | [Jinxiansen](https://github.com/jinxiansen) |
35 | | 4 | client.md | 客户端 | [TomShen1234](https://github.com/TomShen1234) | [Jinxiansen](https://github.com/jinxiansen) |
36 | | 5 | validation.md | 验证 | [Jinxiansen](https://github.com/jinxiansen) | [Jinxiansen](https://github.com/jinxiansen) |
37 | | 6 | async.md | 异步 | - | - |
38 | | 7 | logging.md | 日志 | [MaxsLin](https://github.com/MaxsLin) | [Jinxiansen](https://github.com/jinxiansen) |
39 | | 8 | environment.md | 环境 | - | - |
40 | | 9 | errors.md | 错误 | - | - |
41 |
42 | ## Fluent
43 |
44 | |序号| 文件名| 中文标题 | 译者 | 校对
45 | |:---:|:--:|:--:|:--:|:--:|
46 | | 1 | overview.md | 概述 | - | - |
47 | | 2 | model.md | 模型 | - | - |
48 | | 3 | relations.md | 关联 | - | - |
49 | | 4 | migration.md | 迁移 | - | - |
50 | | 5 | query.md | 查询 | - | - |
51 | | 6 | schema.md | 模式 | - | - |
52 | | 7 | advanced.md | 高级 | - | - |
53 |
54 |
55 | ## 进阶
56 |
57 | |序号| 文件名| 中文标题 | 译者 | 校对
58 | |:--:|:--:|:--:|:--:|:--:|
59 | | 1 | middleware.md | 中间件 | [OHeroJ](https://github.com/OHeroJ) | [Jinxiansen](https://github.com/jinxiansen) |
60 | | 2 | testing.md | 测试 | [DmC-l](https://github.com/DmC-l) | [Jinxiansen](https://github.com/jinxiansen) |
61 | | 3 | server.md | 服务 | - | - |
62 | | 4 | commands.md | 命令 | - | - |
63 | | 5 | queues.md | 队列 | - | - |
64 | | 6 | websockets.md | Websockets | [Howerchen666](https://github.com/Howerchen666) | [Jinxiansen](https://github.com/jinxiansen) |
65 | | 7 | sessions.md | Sessions | - | - |
66 | | 8 | services.md | Services | - | - |
67 |
68 |
69 | ## 安全
70 |
71 | |序号| 文件名| 中文标题 | 译者 | 校对
72 | |:--:|:--:|:--:|:--:|:--:|
73 | | 1 | authentication.md | 认证 | - | - |
74 | | 2 | crypto.md | 加密 | - | - |
75 | | 3 | passwords.md | 密码 | - | - |
76 |
77 |
78 | ## 部署
79 |
80 | |序号| 文件名| 中文标题 | 译者 | 校对
81 | |:--:|:--:|:--:|:--:|:--:|
82 | | 1 | digital-ocean.md | DigitalOcean | - | - |
83 | | 2 | supervisor.md | Supervisor | - | - |
84 | | 3 | nginx.md | Nginx | [Zengxs](https://github.com/zengxs) | [Jinxiansen](https://github.com/jinxiansen) |
85 | | 4 | docker.md | Docker | - | - |
86 |
87 |
88 | ## 版本
89 |
90 | |序号| 文件名| 中文标题 | 译者 | 校对
91 | |:--:|:--:|:--:|:--:|:--:|
92 | | 1 | upgrading.md | 更新指南 | - | - |
93 |
94 |
95 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 VAPOR
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ⚠️ **NOTE**: This repo has now been archived as the main [Docs](https://github.com/vapor/docs) repo now supports localisation so the Chinese translations live alongside the other languages.
2 |
3 | # Vapor 中文文档
4 |
5 |
6 | 该站点基于 [mkdocs](https://github.com/mkdocs/mkdocs/) 构建而成。网站内容在 `4.0` 文件夹内,格式为 Markdown。欢迎提 [Issue](https://github.com/vapor/docs-cn/issues/new) 或 [Pull request](https://github.com/vapor/docs-cn/compare)。
7 |
8 | 英文原版仓库:https://github.com/vapor/docs
9 |
10 | ## 安装
11 |
12 | 安装 **mkdocs**:
13 |
14 | ```
15 | $ brew install python3
16 | $ pip3 install mkdocs mkdocs-material
17 | ```
18 |
19 | 然后执行:
20 |
21 | ```
22 | $ mkdocs serve
23 | ```
24 |
25 | > `Running at: http://127.0.0.1:8000`
26 |
27 |
28 | ## 如何参与贡献
29 |
30 | 目前本仓库及文档处于维护状态,我们会定期同步 [英文版](https://github.com/vapor/docs) 的更新。欢迎大家:
31 |
32 | * 同步英文站点最新的改动到这里;
33 | * 修复错别字或错误的书写格式;
34 | * 发 [Issue](https://github.com/vapor/docs-cn/issues/new) 讨论译法或书写格式;
35 | * 发 [Issue](https://github.com/vapor/docs-cn/issues/new) 讨论部署或协作流程上的问题。
36 |
37 | 有劳在翻译之前移步 [Wiki](https://github.com/vapor/docs-cn/wiki) 了解相关注意事项。
38 |
39 | **注意:**
40 |
41 | 1. 原则上这里只进行英文版对应的翻译工作,如果觉得原文有改进之处,或任何不仅针对中文版,而受益所有语言版本的想法,建议直接在英文版仓库讨论。
42 | 2. 原则上这里不适合讨论 Vapor 的使用问题,建议相关问题在 Vapor 的[官方论坛](https://github.com/vapor/vapor) (英文)、[聊天室](http://vapor.team/) (英文) 或各大主流技术社区讨论,以便得到更多人的帮助和更充分的讨论。
43 |
44 |
45 | ## 致谢
46 |
47 | [这个页面](CONTRIBUTING.md) 集中记录了大家辛勤的努力和付出,以表谢意!
48 |
49 | 感谢所有参与翻译的朋友们!
50 |
--------------------------------------------------------------------------------
/stack.yaml:
--------------------------------------------------------------------------------
1 | AWSTemplateFormatVersion: '2010-09-09'
2 | Description: 'The chinese documentation for Vapor docs'
3 | Parameters:
4 | DomainName:
5 | Type: String
6 | Description: The domain name for the site
7 | AllowedPattern: (?!-)[a-zA-Z0-9-.]{1,63}(?