2024-10-10 18:47:53
这次搬家是彻底失算了,没想到日本 2022 年的新筑 Terrace (日语叫 メゾンネット,中文可以理解为联排)竟然没有光纤,这就导致在各家装宽带的网页上,公寓和一户建都不太能准确描述这个地方。
各种波折导致我没能顺利搬迁之前的宽带,也没能很快装上新的宽带。这也是为什么我会写这篇文章,主要是抒发下没有宽带的苦闷。
日本很多运营商都提供了一种叫做 Home Wi-Fi 的家庭热点,原理和手机热点一样,都是通过一张 SIM 卡连接到移动网络,然后再分享出来。
但与手机不同的是,Home WI-FI 这种设备把技能点都点在了下载上。
比如在我这里通过 5G 连接 Au 的线路,下载可以到 200M,但上传可能只有 10M - 20M,同样的卡切换成我的 Pixel 7a 之后,下载是 120M 左右,上传是 60 - 80M. 下图可以看到 Pixel 7a 的上传性能要好很多。
有了这个数据的对比后,我决定把 Android 手机的网通过 RJ45 网线提供给路由器上网。
在日本 Android 手机只能分享 2.4G 的 WI-FI,这导致 WI-FI 的速度和稳定性,带机量都相当有限。
具备下面 3 个物品就可以方便的自制一个 Home WI-FI 了
在日本的话 Povo 和乐天这两个放题卡都是不错的选择,需要注意的是自己所在的区域是否有对应的 5G 信号支持,5G 通常可以提供一个 20ms 左右的低延迟体验,即使人多的时候速度不会降的离谱。
虽然 小米,三星,Google Pixel 的手机大都支持以太网网络共享,但这一点还是需要自己先确认下。
你可以选择一个带有 Type C 供电的转接头,比如我购买的支持 PD 电源输入的这款
除了专门买个转接头外,也可以用自己手头带有 RJ45 接口的扩展坞,但扩展坞和手机的兼容性可能不太好,比如我手里的 Anker 扩展坞可以和小米手机一起用,但 Pixel 使用则会一直掉线。
在购买前需要确认转接设备和 Android 手机的兼容性,一般如果商家写了支持 Android 设备那么通常没问题。
正常的家用 WI-FI 路由器即可 :)
好了,可以享受网络了!我用了一个星期后感觉还是挺稳定的,不过还是祝你早日装上宽带!
2023-10-30 13:28:30
在上一章中我们创建了 User
的 Model
,并构建了 UserController
,但尚未构建起 User
和 Post
之间的关系。在这一章中,我们将在 Vapor 中完成一对多的关系构建。
本章代码可以在 Github 中找到。
要修 Post
表结构,我们首先需要创建一个新的 Migration 文件。
创建 Sources/App/Migrations/3_AddUserIDToPost.swift
文件,添加如下代码:
import Fluent
struct AddUserIDToPost: AsyncMigration {
func prepare(on database: Database) async throws {
try await database.schema(Post.schema)
.field("user_id", .uuid, .references(User.schema, "id"))
.update()
}
func revert(on database: Database) async throws {
try await database.schema(Post.schema)
.deleteField("user_id")
.update()
}
}
这种方式将会给 Post 表添加一个可为空的 user_id
字段,虽然这种方式简单直接,但同时也意味着,我们容忍了 Post 可能不属于任何用户的情况。
在 prepare
方法中,我们使用 references
方法来创建了一个外键约束,这个外键将会引用 User
表中的 id
字段,确保了 user_id
在数据库层面的正确性,避免我们意外插入一个不存在的 user_id
。
在 revert
方法中,我们使用 deleteField
方法来删除 user_id
字段,这个方法会同时删除它的外键约束。
随后,我们应当修改 configure.swift
将这条 Migration 添加到其中
app.migrations.add([CreatePost(), CreateUser(), AddUserIDToPost()])
现在,运行 vapor run migrate
即可进行数据库的迁移。
完成了数据库 Schema 的修改后,我们需要进一步修改 Post Model
编辑 Sources/App/Models/Post.swift
文件如下:
final class Post: Model, Content {
static let schema = "posts"
@ID(key: .id)
var id: UUID?
@OptionalParent(key: "user_id")
var user: User?
@Field(key: "content")
var content: String
@Timestamp(key: "created_at", on: .create)
var createdAt: Date?
init() { }
init(id: UUID? = nil, content: String) {
self.id = id
self.content = content
}
}
因为我们在给 Post 的 Schema 添加 user_id
字段时允许其为空,因此在 Post Model 中,我们使用 @OptionalParent
来表明 user
是不一定存在的。
与此同时,@OptionalParent(key: "user_id")
表明了 user_id
和 User Model @ID 之间的对应关系,这与 Schema 中的 .field("user_id", .uuid, .references(User.schema, "id"))
形成对应关系。
import Vapor
import Fluent
final class User: Model, Content {
static let schema = "users"
@ID(key: .id)
var id: UUID?
@Field(key: "username")
var username: String
@Field(key: "password_hash")
var passwordHash: String
@Children(for: \.$user)
var posts: [Post]
@Timestamp(key: "created_at", on: .create)
var createdAt: Date?
init() {}
init(id: UUID? = nil, username: String, passwordHash: String) {
self.id = id
self.username = username
self.passwordHash = passwordHash
}
}
在 User Model 中,@Children(for: \.$user)
表明了 User 和 Post 之间的一对多关系,同时也说明了,这种关系是通过 Post Model 中的 user
属性来进行关联的。
现在,我们有两种方式可以创建 Post 了,一种是通过 Post Model 的 user
属性:
let user = User(username: "hellouser", passwordHash: try await app.password.async.hash("123456"))
try await user.save(on: app.db)
let post = Post(content: "Hello, World!")
post.$user.id = try user.requiredID()
另一种是通过 User Model 的 posts
属性
let user = User(username: "hellouser", passwordHash: try await app.password.async.hash("123456"))
try await user.save(on: app.db)
let post = Post(content: "Hello, World!")
user.$posts.create(post, on: app.db)
不管采用哪一种,我们都需要在保存 Post 前知道 user_id
。
在 PostController 中,我们需要修改 create
方法,使其能够接收 user_id
参数,并将其与 Post 关联起来。
那么如何将 user_id
传入到 create
方法中呢?
直接让客户端提供 user_id
参数肯定是不可以的,因为这样的话,客户端只需要伪造 user_id
就可以假装成别的用户发布,这显然是不安全的。
我们暂且通过最简单的 BasicAuthorization
来解决这个问题,BasicAuthorization 需要客户端提供 username 和 password 两个字段,并将其进行 Base64 编码后放在 HTTP Header 中:
计算格式如下
base64(username:password)
以上面的用户信息 happyuser:123456
为例,计算后的 Header 结果如下:
Authorization: Basic aGFwcHl1c2VyOjEyMzQ1Ng==
服务器端验证用户名和密码后,才会允许客户端创建对应用户的 Post。
BasicAuthorization 作为一种业界通用实践,Vapor 已经提供了 Model Authenticatable 来帮助我们完成这个功能
修改 Sources/App/Models/User.swift
在底部增加如下代码:
extension User: ModelAuthenticatable {
static let usernameKey = \User.$username
static let passwordHashKey = \User.$passwordHash
func verify(password: String) throws -> Bool {
try Bcrypt.verify(password, created: self.passwordHash)
}
}
usernameKey
和 passwordHashKey
分别表明了 User Model 中的哪两个属性用于存储用户名和密码。当客户端提供用户名和密码时,Vapor 会自动将其与 User Model 中的 usernameKey
和 passwordHashKey
进行对应,从而完成用户验证。
接下来我们需要确保 Post 在创建前,用户已经通过了验证,这可以通过在 PostController 的路由中,加入权限验证相关的 Middleware 来完成,Vapor 已经提供了 User.authenticator()
来帮助我们完成 BasicAuthorization 验证的功能。
修改 Sources/App/Controllers/PostController.swift
中 boot 代码如下
func boot(routes: RoutesBuilder) throws {
routes.group("posts") { posts in
posts.get(use: index)
posts.grouped(User.authenticator()).post(use: create)
}
}
在 posts
路由组中,当客户端请求 posts
路由组中的 post
方法前,User.authenticator()
会进行 BasicAuthorization 验证,如果没有通过验证,Vapor 会返回 401 错误。
修改 Sources/App/Controllers/PostController.swift
中的 create
方法如下:
func create(req: Request) async throws -> Post {
let user = try req.auth.require(User.self)
let postData = try req.content.decode(Post.CreateDTO.self)
let post = Post(content: postData.content)
post.$user.id = try user.requireID()
try await post.create(on: req.db)
return post
}
在 create
方法中,我们首先通过 req.auth.require(User.self)
获取到了已经通过验证的用户,然后通过 req.content.decode(Post.CreateDTO.self)
获取到了客户端传入的 Post 数据,最后通过 post.$user.id = try user.requireID()
将 Post 和 User 关联起来。
修改 Tests/AppTests/PostTests.swift
如下:
@testable import App
import XCTVapor
final class PostTests: XCTestCase {
var app: Application!
override func setUp() async throws {
app = Application(.testing)
try configure(app)
try await app.autoRevert()
try await app.autoMigrate()
}
override func tearDown() async throws {
app.shutdown()
}
func testCreatePost() async throws {
let user = User(username: "hellouser", passwordHash: try await app.password.async.hash("123456"))
try await user.save(on: app.db)
let postDTO = Post.CreateDTO(content: "Post created from test")
try app.test(.POST, "posts", beforeRequest: { req in
try req.content.encode(postDTO)
req.headers.basicAuthorization = BasicAuthorization(username: user.username, password: "123456")
}, afterResponse: { res in
XCTAssertEqual(res.status, .ok)
let post = try res.content.decode(Post.self)
XCTAssertEqual(postDTO.content, post.content)
XCTAssertEqual(try user.requireID(), post.$user.id)
})
}
}
在 testCreatePost
中,我们首先创建了一个用户,然后创建了一个 Post DTO,接着通过 app.test
方法,模拟了客户端的请求,最后验证了 Post 的 User 来判断是否创建成功。
在这一章中,我们学习了如何在 Vapor 中构建一对多的关系,同时也学习了如何使用 BasicAuthorization 来进行用户验证。但这种方式虽然简单,但也有很多缺点,在下一章中,我们会学习使用 BearerAuthentication 来对用户进行验证,并进一步修复 User Model 的安全隐患。
本章代码可以在 Github 中找到。
2023-10-21 13:51:11
在本章中我们将在数据库中创建 User
表,用于存储用户信息,并理解如何将 Post
表和 User
关联起来,以便我们可以知道每篇微博是由哪个用户编写的。
本章代码可以在 Github 中找到。
一个基础的 User
表应该包含以下字段
id
:主键,用于唯一标识一个用户username
:用户名,用于登录password_hash
:密码,用于登录,存储的应该是加密后的密码createdAt
:创建时间,用于记录用户创建时间如果我们插入了一个假数据 happyuser
,那么我们的 User
表应该是这样的:
id | username | password_hash | createdAt |
---|---|---|---|
0 | happyuser | encrypted text | 2023-1-1 |
那么如何关联 Post
表呢?
在关系性数据库中,表关系被总结为三种:
我们的 User
表和 Post
表的关系是一对多,即一个用户可以有多篇微博,而一篇微博只能属于一个用户。
在数据库中,我们可以通过在 Post
表中添加一个 userId
字段来表示这种关系,这个字段用于存储 User
表中的 id
,即 Post
表中的每一行都会有一个 userId
字段,用于表示这篇微博是由哪个用户编写的。
修改之前的 Post
表,添加 userId
字段,表示如下:
id | userId | content | createdAt |
---|---|---|---|
0 | 0 | 这是第一篇博文 | 2023-1-1 |
1 | 0 | 在 LONCAFE 写代码感觉不错呢! | 2023/7/9 14:44 |
当我们需要查询 happyuser 的所有微博时,只需要在 Post
表中查询 userId = 0
的所有行即可。
在 Sources/App/Models
目录下创建 User.swift
文件,添加如下代码:
import Vapor
import Fluent
final class User: Model, Content {
static let schema = "users"
@ID(key: .id)
var id: UUID?
@Field(key: "username")
var username: String
@Field(key: "password_hash")
var passwordHash: String
@Timestamp(key: "created_at", on: .create)
var createdAt: Date?
init() {}
init(id: UUID? = nil, username: String, passwordHash: String) {
self.id = id
self.username = username
self.passwordHash = passwordHash
}
}
extension User {
struct CreateDTO: Content {
let username: String
let password: String
}
}
接下来,我们需要添加 User
表的 Migration 文件,用于在数据库中创建 User
表。
在 Sources/App/Migrations
目录下创建 2_CreateUser.swift
文件,添加如下代码:
import Fluent
struct CreateUser: AsyncMigration {
func prepare(on database: Database) async throws {
try await database.schema(User.schema)
.id()
.field("username", .string, .required)
.field("password_hash", .string, .required)
.field("created_at", .datetime)
.create()
}
func revert(on database: Database) async throws {
try await database.schema(User.schema).delete()
}
}
在 configure.swift
文件中修改 app.migrations.add
,用于注册 User
Migration:
app.passwords.use(.bcrypt) // Bcrypt 是一种密码加密算法,可以确保多次加密后的密码都是不同的,但是可以通过原始密码验证
app.migrations.add([CreatePost(), CreateUser()])
在 Sources/App/Controllers
目录下创建 UserController.swift
文件,添加如下代码:
import Fluent
import Vapor
struct UserController: RouteCollection {
func boot(routes: RoutesBuilder) throws {
routes.group("users") { posts in
posts.get(use: index)
posts.post(use: create)
}
}
func create(req: Request) async throws -> User {
let postData = try req.content.decode(User.CreateDTO.self)
let user = User(username: postData.username, passwordHash: try await req.password.async.hash(postData.password))
try await user.create(on: req.db)
return user
}
func index(req: Request) async throws -> [User] {
let users = try await User.query(on: req.db).all()
return users
}
}
在 configure.swift
文件中注册 UserController
:
try app.register(collection: UserController())
在 Tests/AppTests
目录下创建 UserTests.swift
文件,添加如下代码:
@testable import App
import XCTVapor
final class UserTests: XCTestCase {
var app: Application!
override func setUp() async throws {
app = Application(.testing)
try configure(app)
try await app.autoRevert()
try await app.autoMigrate()
}
override func tearDown() async throws {
app.shutdown()
}
func testCreateUser() async throws {
let userData = User.CreateDTO(username: "happyuser", password: "123456")
try app.test(.POST, "users", beforeRequest: { req in
try req.content.encode(userData)
}, afterResponse: { res in
XCTAssertEqual(res.status, .ok)
let user = try res.content.decode(User.self)
XCTAssertEqual(user.username, userData.username)
XCTAssertTrue(try app.password.verify("123456", created: user.passwordHash))
})
}
func testGetUsers() async throws {
let user = User(username: "hellouser", passwordHash: try await app.password.async.hash("123456"))
try await user.save(on: app.db)
try app.test(.GET, "users", afterResponse: { res in
XCTAssertEqual(res.status, .ok)
let users = try res.content.decode([User].self)
XCTAssertEqual(users.count, 1)
XCTAssertEqual(users[0].username, user.username)
})
}
}
在终端中运行 swift test
命令,顺利的话,可以看到测试用例全部通过。
在本章中,我们学习了如何在数据库中创建 User
表,以及如何将 Post
表和 User
表关联起来,但我们尚未真正在数据库中关联这两个表,我们将在下一章中完成这个功能。
本章代码可以在 Github 中找到。
2023-08-06 15:57:46
在本章中,我们将为 Post 创建一个 Controller,将 Route 和 Route Handler 都移动到这里,保持项目代码的整洁
在上一章中,下面两个和 Post 相关的 Route 和 Handler 都写在了 Sources/App/configure.swift
中
现在我们创建 Sources/App/Controllers/PostController.swift
文件,并将代码移动到这里
import Fluent
import Vapor
struct PostController: RouteCollection {
func boot(routes: RoutesBuilder) throws {
routes.group("posts") { posts in
posts.get(use: index)
posts.post(use: create)
}
}
func create(req: Request) async throws -> Post {
let postData = try req.content.decode(Post.CreateDTO.self)
let post = Post(content: postData.content)
try await post.create(on: req.db)
return post
}
func index(req: Request) async throws -> [Post] {
let posts = try await Post.query(on: req.db).all()
return posts
}
}
在 boot 函数中,我们通过 RoutesBuilder 定义了 route 和 handler 之间的关系。
现在我们需要将 Controller 注册到 Application 中,让 Application 知道有哪些路由需要被处理
修改 Sources/App/configure.swift
import Fluent
import FluentPostgresDriver
import Vapor
public func configure(_ app: Application) throws {
app.databases.use(.postgres(configuration: SQLPostgresConfiguration(
hostname: Environment.get("DATABASE_HOST") ?? "localhost",
port: Environment.get("DATABASE_PORT").flatMap(Int.init(_:)) ?? SQLPostgresConfiguration.ianaPortNumber,
username: Environment.get("DATABASE_USERNAME") ?? "vapor_username",
password: Environment.get("DATABASE_PASSWORD") ?? "vapor_password",
database: Environment.get("DATABASE_NAME") ?? "vapor_database",
tls: .prefer(try .init(configuration: .clientDefault)))
), as: .psql)
app.migrations.add([CreatePost()])
try app.register(collection: PostController())
}
完成添加之后,可以使用 swift run App routes
来打印 App 内生效的 routes
+------+--------+
| GET | / |
+------+--------+
| GET | /posts |
+------+--------+
| POST | /posts |
+------+--------+
我们的项目经过了一遍较大的结构改动,但如果逻辑没有错误,我们之前写的 PostTests 应该仍然是可以通过的,换句话说,只要 PostTests 通过了, 我们就无需过于担心功能是否可以正常运行。
运行测试
swift test
测试结果通过
Test Case '-[AppTests.PostTests testCreatePost]' passed (0.230 seconds).
Unit Test 又一次验证了它的必要性。
在下一章中,我们将会添加 User 并关联 User 和 Post
2023-07-23 22:21:58
在本章中,我们将为 Post 配置 API,使得我们可以通过 HTTP 请求来完成 Post 的创建。同时,我们将引入 Environment 来为我们的服务器区分 Development 和 Testing 环境。
在我们编程的时候,经常会使用第三方提供的接口来完成一个特定任务,使得原本复杂的任务,只需要调用一个 API 就可以完成,这便是 API 最基础的理念,将特定的任务进行封装。
API 几乎无处不在,以用户使用 App 发微博为例,可以表示为下面的流程
User -> UI -> Client -> HTTP -> Server -> Fluent -> Database
用户操作 UI 编写内容,内容被记录在客户端 App,客户端通过 HTTP 请求发送到服务器,服务器接收到请求后,这些请求通过 Fluent 转化成 SQL 语句,最终在数据库中完成数据更新。
UI 提供了「用户」和「客户端」之间的 API
HTTP 接口提供了「客户端」和「服务器」之间的 API
Fluent 提供了「服务器」和「数据库」之间的 API
在本章中,我们将要设计的就是 Client -> Server 之间的 HTTP API.
在第一章中,我们曾提供了一个非常基础的 "It works!"
的 API,了解了一个 HTTP 请求所需的基本参数。
现在,我们希望有一个可以创建 Post 的 API,需求如下
/posts
content
字段来表示 Post 的内容如果客户端按照这个标准发来一个 HTTP 请求,那么内容看起来就是这样的
POST /posts HTTP/1.1
Host: 127.0.0.1:8080
Content-Type: application/json
Content-Length: 49
{
"content": "First post from HTTP request"
}
Hint
需要客户端填写的是 POST /posts
和 JSON 部分的内容,其他如Host, Content-Type, Content-Length 均会在请求时自动生成。
首先,定义一个叫做 Post.CreateDTO
的 DTO struct 来表示客户端发来的 JSON 内容格式
extension Post {
struct CreateDTO {
let content: String
}
}
Hint
DTO,全称 Data Transfer Object,是一种常见的设计模式,用于在服务之间传输数据,可以灵活定义字段,便于封装和安全性验证,亦可减少流量的消耗。
接下来编辑 Sources/App/configure.swift
加入对 POST /posts
的处理
import Fluent
import FluentPostgresDriver
import Vapor
public func configure(_ app: Application) throws {
app.databases.use(.postgres(configuration: SQLPostgresConfiguration(
hostname: "localhost",
port: 5432,
username: "vapor_username",
password: "vapor_password",
database: "vapor_database",
tls: .prefer(try .init(configuration: .clientDefault)))
), as: .psql)
app.migrations.add([CreatePost()])
app.post("posts") { req async throws -> Post in
let postData = try req.content.decode(Post.CreateDTO.self)
let post = Post(content: postData.content)
try await post.create(on: req.db)
return post
}
}
修改 Sources/App/Models/Post.swift
使 Post 支持被编码为 JSON
import Vapor
import Fluent
final class Post: Model, Content {
// 数据库中的表名
static let schema = "posts"
// 唯一性标识符
@ID(key: .id)
var id: UUID?
// 内容
@Field(key: "content")
var content: String
// 创建时间
@Timestamp(key: "created_at", on: .create)
var createdAt: Date?
init() { }
init(id: UUID? = nil, content: String) {
self.id = id
self.content = content
}
}
extension Post {
struct CreateDTO: Content {
let content: String
}
}
Post.CreateDTO
的结构体,同样遵循了 Content 协议,使之可以用来解码用户发来的 JSON现在,我们可以修改 AppTests/PostTests.swift
来测试这个 API 是否能够跑通了
@testable import App
import XCTVapor
final class PostTests: XCTestCase {
func testCreatePost() async throws {
let app = Application(.testing)
defer { app.shutdown() }
try configure(app)
try await app.autoRevert()
try await app.autoMigrate()
let postDTO = Post.CreateDTO(content: "Post created from test")
try app.test(.POST, "posts", beforeRequest: { req in
try req.content.encode(postDTO)
}, afterResponse: { res in
XCTAssertEqual(res.status, .ok)
let post = try res.content.decode(Post.self)
XCTAssertEqual(postDTO.content, post.content)
})
}
}
req.content.encode(postDTO)
将 postDTO 编码到 request 的 body 中,在默认情况下,请求时数据会采用 JSON 编码HTTPStatus.ok
即 200运行这段测试,不出意外,将可以看到测试通过的信息
Test Case '-[AppTests.PostTests testCreatePost]' passed (0.269 seconds).
恭喜你!第一个 API 调通了!
除了使用 Unit Test 进行 API 测试以外,我们也可以使用 cURL 命令进行测试,以上面创建 Post 的请求为例,我们可以在终端中输入以下命令
curl -i --location 'http://127.0.0.1:8080/posts' \
--header 'Content-Type: application/json' \
--data '{
"content": "First post from HTTP request"
}'
Hint
通过 -i 参数,我们要求 curl 打印出完整的 HTTP 响应信息
回车后,正常情况下会响应如下内容
HTTP/1.1 200 OK
content-type: application/json; charset=utf-8
content-length: 121
connection: keep-alive
date: Sun, 23 Jul 2023 13:22:20 GMT
{"content":"First post from HTTP request","createdAt":"2023-07-23T13:22:20Z","id":"1C9A6A1D-98B8-4871-8DE0-BDF011845ADA"}%
通过 cURL 我们可以很方便的观察 HTTP 信息的构成,通过这则信息,我们也可以更好的理解 Unit Test 中 XCTAssertEqual(res.status, .ok)
和 200 OK
的对应关系。
Postman 是一款非常流行的 API 测试软件,不仅可以方便的进行参数调试,也可以将编辑好的请求转换成其他软件的请求,比如下图右边转换为 cURL
创建 Post 之后,最迫切的期望就是列出所有的 Post,现在我们可以编辑 Sources/App/configure.swift
来加入 GET /posts
的支持
import Fluent
import FluentPostgresDriver
import Vapor
public func configure(_ app: Application) throws {
...
app.get("posts") { req async throws -> [Post] in
let posts = try await Post.query(on: req.db).all()
return posts
}
}
现在,如果你使用浏览器访问 http://127.0.0.1:8080/posts
将可以看到所有存储在数据库里的 posts
使用 Postman 来查看会更方便一些
到目前为止,我们并没有区分服务器的开发环境和测试环境,这导致每次运行单元测试的时候,都会将我们之前在开发环境里创建的数据清除掉,这显然会给我们带来很多麻烦。
我们的数据库连接信息定义在 Sources/App/configure.swift
之中,这意味着如果我们可以让 App 启动时动态的配置数据库的连接信息,分别连接「开发环境数据库」和「测试环境数据库」,就可以解决我们的问题。
在 Vapor 中,通过 Environment 我们可以轻松的将他们分开。
回顾 Sources/App/main.swift
,我们使用 let app = Application()
启动我们的 App,但实际上这段代码隐藏了一些细节,它包含了 Environment 的默认值 Environment.development
因此更详尽的代码应该是 let app = Application(.development)
import Vapor
import Fluent
import FluentPostgresDriver
let app = Application(.development)
app.http.server.configuration.port = 8080
defer { app.shutdown() }
app.get { req async in
"It works!"
}
try configure(app)
try app.run()
.env
文件Application(.development)
代码中,Application 会读取 .env.developemnt
Hint
Vapor 的服务器启动时,默认是 development 环境,因此会从服务器启动目录的 .env.development 中读取环境变量,如果你使用的 Xcode,请记得修改 Scheme 中的 Working Directory 为项目根目录
修改编辑项目根目录中的.env.development
配置开发环境变量
DATABASE_HOST=localhost
DATABASE_PORT=5432
DATABASE_USERNAME=vapor_username
DATABASE_PASSWORD=vapor_password
DATABASE_NAME=vapor_database
修改 Sources/App/configure.swift
改变数据库参数的获取方式
app.databases.use(.postgres(configuration: SQLPostgresConfiguration(
hostname: Environment.get("DATABASE_HOST") ?? "localhost",
port: Environment.get("DATABASE_PORT").flatMap(Int.init(_:)) ?? SQLPostgresConfiguration.ianaPortNumber,
username: Environment.get("DATABASE_USERNAME") ?? "vapor_username",
password: Environment.get("DATABASE_PASSWORD") ?? "vapor_password",
database: Environment.get("DATABASE_NAME") ?? "vapor_database",
tls: .prefer(try .init(configuration: .clientDefault)))
), as: .psql)
现在,我们只解决了 develpment 环境的问题,testing 环境需要一个新的数据库,以及一份对应的 .env.testing
修改 docker-compose.yml
增加 db_test 服务
值得注意的是我们在 volumes 增加了 db_data_test,并将 db_test 的端口设置为了 5442
version: '3.7' # 定义 Docker Compose 文件的版本,此处使用的是版本 3.7
volumes: # 定义卷部分
db_data: # docker 会使用这个键作为名字,自动创建 db_data 卷来存储数据
db_data_test:
services: # 定义服务部分
db: # db 服务配置
image: 'postgres:15-alpine' # 使用 PostgreSQL 15 Alpine 版本的镜像
volumes: # 定义挂载卷
- 'db_data:/var/lib/postgresql/data/pgdata' # 将 db_data 卷挂载到容器的 /var/lib/postgresql/data/pgdata 目录
environment: # 定义环境变量
PGDATA: '/var/lib/postgresql/data/pgdata' # 设置 PGDATA 环境变量为 /var/lib/postgresql/data/pgdata
POSTGRES_USER: 'vapor_username' # 设置 POSTGRES_USER 环境变量为 vapor_username
POSTGRES_PASSWORD: 'vapor_password' # 设置 POSTGRES_PASSWORD 环境变量为 vapor_password
POSTGRES_DB: 'vapor_database' # 设置 POSTGRES_DB 环境变量为 vapor_database
ports: # 定义端口映射,将主机的 5432 端口映射到容器的 5432 端口
- '5432:5432'
db_test:
image: 'postgres:15-alpine'
volumes:
- 'db_data_test:/var/lib/postgresql/data/pgdata'
environment: # 定义环境变量
PGDATA: '/var/lib/postgresql/data/pgdata'
POSTGRES_USER: 'vapor_username'
POSTGRES_PASSWORD: 'vapor_password'
POSTGRES_DB: 'vapor_database'
ports:
- '5442:5432'
创建 .env.testing
写入以下内容
DATABASE_HOST=localhost
DATABASE_PORT=5442
DATABASE_USERNAME=vapor_username
DATABASE_PASSWORD=vapor_password
DATABASE_NAME=vapor_database
启动 db_test
docker-compose up db_test -d
现在,服务器的 development 和 testing 环境的数据库就不会再相互干扰了。
本章节的代码可以在 https://github.com/kevinzhow/swift-on-server-tour/tree/main/3 中找到
目前我们 Post 相关的操作都堆在了 configure.swift
中,在下一章中,我们将学习如何将他们移动到 Controller 中,并继续完善我们 Post 的查看与删除。