惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

S
Schneier on Security
The GitHub Blog
The GitHub Blog
Martin Fowler
Martin Fowler
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
H
Help Net Security
Microsoft Security Blog
Microsoft Security Blog
有赞技术团队
有赞技术团队
GbyAI
GbyAI
F
Fortinet All Blogs
The Cloudflare Blog
爱范儿
爱范儿
IT之家
IT之家
博客园 - 叶小钗
阮一峰的网络日志
阮一峰的网络日志
博客园 - 【当耐特】
博客园_首页
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
量子位
C
Cisco Blogs
Latest news
Latest news
Hugging Face - Blog
Hugging Face - Blog
S
Securelist
Stack Overflow Blog
Stack Overflow Blog
K
Kaspersky official blog
Spread Privacy
Spread Privacy
B
Blog
L
Lohrmann on Cybersecurity
Simon Willison's Weblog
Simon Willison's Weblog
I
Intezer
P
Privacy International News Feed
T
Tor Project blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
小众软件
小众软件
P
Proofpoint News Feed
T
Tailwind CSS Blog
C
Cyber Attacks, Cyber Crime and Cyber Security
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Recorded Future
Recorded Future
S
Secure Thoughts
人人都是产品经理
人人都是产品经理
云风的 BLOG
云风的 BLOG
N
News and Events Feed by Topic
Last Week in AI
Last Week in AI
W
WeLiveSecurity
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
S
Security Affairs
宝玉的分享
宝玉的分享
D
Docker
Blog — PlanetScale
Blog — PlanetScale
雷峰网
雷峰网

Kevin Blog

产品随想 2 产品力与人才密度 产品随想 1 做营销还是做产品 Play PyTorch Stable Diffusion and ONNX, Ollama on Intel Core Ultra 5 225H Ubuntu 25.04 Play with ROCm, PyTorch, Ollama on Ubuntu 24.04 and 780m 写在新的旅程开始前 想念自然与青春 懒猫微服体验——自由协作的神器 没有光纤的日子怎么上网?自制 Home WI-FI! Swift on Server Tour 6 关联 User 和 Post Swift on Server Tour 5 创建 Users Swift on Server Tour 4 构建 Post Controller 将不懂的日语一拍扫尽,介绍捧读全新的「OCR 工作台」功能 Swift on Server Tour 2 连通你的数据库与服务器 Swift on Server Tour 1 你的第一个 Server App 以及它背后的故事 Swift on Server Tour 0: 为什么这可能是你的好选择 纪念左耳朵耗子 How to learn Japanese by reading Novels and News with the help of Oyomi. Write WebAssembly in Swift and use it in Swift App BenQ WiT ScreenBar Halo 体验报告 记录 2021 年考驾照的体验 捧读的 EPUB 日语轻小说阅读器来了 使用 Go Mobile 开发跨平台 Library 使用 Kotlin Native 开发跨平台 Library 小番茄 - 一个只有陪伴的自习室 保友金豪电脑椅异响维修记 青岛度假指南 2020 「捧读」更新,支持针对任意文本进行语法讲解 一次百元耳机与千元耳机的横评 高刷墨水屏的海信 A5 Pro 体验报告 「捧读」更新 Safari 扩展,轻松阅读日语网页 新 App 「捧读:日语语法学习与分析」的开发幕后思考 明基 WiT ScreenBar Plus 体验 Switch游戏推荐:「健身环大冒险」和「太鼓达人」 妥协是否是一种艺术? 「50 音起源」1.2 更新说明 作客 UX Coffee 分享了自己最近的独立开发者生活 青岛度假指南 2019 我有「快乐自由」 关于小电台这款产品 聊一聊 50 音起源的再设计 极端精英主义的崩塌 Xcode Error: CFBundleSupportedPlatforms or Mach-O LC_VERSION_MIN 自由职业一周年我收获了什么 「50音起源」从起源学习 50 音的 App iOS/Android 青岛疗养(度假)指南 关于独处的一些经验 跨省携猫🐱搬家记 关于 FilmMentor 这款小众产品的想法 选择离开北京,以及为什么可以
Swift on Server Tour 3 构建 Post 的 API
Kevin Zhow · 2023-07-23 · via Kevin Blog

在本章中,我们将为 Post 配置 API,使得我们可以通过 HTTP 请求来完成 Post 的创建。同时,我们将引入 Environment 来为我们的服务器区分 Development 和 Testing 环境。

.. toc::

API 的基础理念

在我们编程的时候,经常会使用第三方提供的接口来完成一个特定任务,使得原本复杂的任务,只需要调用一个 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.

创建 Post 的 HTTP API

在第一章中,我们曾提供了一个非常基础的 "It works!" 的 API,了解了一个 HTTP 请求所需的基本参数。

现在,我们希望有一个可以创建 Post 的 API,需求如下

  • 使用 POST 请求
  • 路径为 /posts
  • 请求内容为 JSON,内容包含一个 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
    }
}
  • req.content.decode(Post.CreateDTO.self) 表示将请求中 body 的内容解码为 Post.CreateDTO

修改 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 增加 Content 协议,使之具备 Codable 的特性,能够被编码成 JSON 返回给用户
  • 增加了一个Post.CreateDTO 的结构体,同样遵循了 Content 协议,使之可以用来解码用户发来的 JSON

测试 API

现在,我们可以修改 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)
        })
    }
}
  • 使用 app.test(.POST, "posts") 模拟外部 HTTP 请求
  • beforeRequest 可以对请求数据进行修改,我们通过 req.content.encode(postDTO) 将 postDTO 编码到 request 的 body 中,在默认情况下,请求时数据会采用 JSON 编码
  • afterResponse 是请求后从服务器获得的响应,我们首先判断 res.status 是否是 HTTPStatus.ok 即 200
  • XCTAssertEqual(postDTO.content, post.content) 判断服务器上创建的数据是否和发送的一致

运行这段测试,不出意外,将可以看到测试通过的信息

Test Case '-[AppTests.PostTests testCreatePost]' passed (0.269 seconds).

恭喜你!第一个 API 调通了!

使用 cURL 命令进行测试

除了使用 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 进行测试

Postman 是一款非常流行的 API 测试软件,不仅可以方便的进行参数调试,也可以将编辑好的请求转换成其他软件的请求,比如下图右边转换为 cURL

image.png

列出所有 Post

创建 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
    }
}
  • Post.query(on: req.db).all() 将获取数据库中所有的 Post.

现在,如果你使用浏览器访问 http://127.0.0.1:8080/posts 将可以看到所有存储在数据库里的 posts

image.png

使用 Postman 来查看会更方便一些

image.png

使用 Environment 区分服务器环境

到目前为止,我们并没有区分服务器的开发环境和测试环境,这导致每次运行单元测试的时候,都会将我们之前在开发环境里创建的数据清除掉,这显然会给我们带来很多麻烦。

我们的数据库连接信息定义在 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()
  • Application() 在启动时,接受 Environment 变量,并读取与 Environment 对应的 .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 的查看与删除。