Kitura Swift 后端开发 - 笔记

第一章:引言

Kitura 是什么?

在词源学上,Kitura 这个词松散地源于希伯来语 Keturah,字面意思是“香”。

为什么选择 Swift 写后端?

支持为服务器编写 Swift 的核心论点是在应用程序的前端和后端之间共享代码和知识。如果你是一个团队,这种精简可以说是更加关键和赋权。这包括但不限于模型对象代码。

BFF(Backonnd for Frontend)是一种设计模式,很快就会成为你最好的朋友(永远?)。

这个模式在这个特定用例中起作用的几个原因:

  1. 你可以确定从此 API 发出请求的唯一设备将是 iOS 设备。这意味着你可以更少担心平衡请求负载。

  2. 你可以更好地控制对 iOS 设备的响应中的数据。在此之前,你可能已经等待后端开发人员最终帮助你,并根据 User-Agent 首部有条件地修剪响应数据。现在,你可以让后端开发人员不间断地工作,并通过将响应数据解析为你的 BFF 设备所需的内容来解决你的问题。

  3. 在我们的特定用例中,你可以在发送响应之前保存一个用户可能对数据库进行的查询的响应。这意味着,如果其他人在相同的位置请求餐馆信息(例子是请求包含地理位置的餐馆信息),并且存储的数据足够新以满足你的刷新策略,你甚至不必向主服务器发送查询!你只需发送缓存的数据,并相信它对你的用户来说足够新。

  4. 你可以使用你已经知道如何在 Swift 中编写代码的语言完成 1-3!

第二章:Hello, World!

如何 Kitura 项目

先安装 Kitura macOS App 和 Kitura CLI

brew tap ibm-swift/kitura
brew install kitura
kitura
Usage: kitura [options] [command]

Kitura command-line interface

Options:
-V, --version output the version number
-h, --help output usage information

Commands:
build build the project in a local container
create interactively create a Kitura project
idt install IBM Cloud Developer Tools
init scaffold a bare-bones Kitura project
kit print Cocoapods boilerplate for KituraKit
run run the project in a local container
sdk generate a client SDK from an OpenAPI/Swagger spec
help [cmd] display help for [cmd]

创建的 Kitura 项目提供了一个开箱即用的全功能 Kitura 应用程序。 它提供了在生产环境中运行的任何应用程序可能需要的一些功能,包括:

  • 使用 HeliumLogger 记录信息,警告和错误消息。

  • 使用 CloudEnvironment 进行动态配置查找和设置。

  • 使用 Health 查看和报告应用程序健康状况。

  • 使用 SwiftMetrics 进行 App 和 Kitura 框架指标和监控。

项目使用 SPM 管理依赖

// swift-tools-version:4.0
import PackageDescription

let package = Package(
name: "EmojiJournalServer",
dependencies: [
.package(url: "https://github.com/IBM-Swift/Kitura.git", .upToNextMinor(from: "2.5.0")),
.package(url: "https://github.com/IBM-Swift/HeliumLogger.git", from: "1.7.1"),
.package(url: "https://github.com/IBM-Swift/CloudEnvironment.git", from: "9.0.0"),
.package(url: "https://github.com/RuntimeTools/SwiftMetrics.git", from: "2.0.0"),
.package(url: "https://github.com/IBM-Swift/Health.git", from: "1.0.0"),
],
targets: [
.target(name: "EmojiJournalServer", dependencies: [.target(name: "Application"), "Kitura" , "HeliumLogger"]),
.target(name: "Application", dependencies: ["Kitura", "CloudEnvironment","SwiftMetrics","Health", ]),

.testTarget(name: "ApplicationTests" , dependencies: [.target(name: "Application"), "Kitura","HeliumLogger" ])
]
)

Application.swift 包含应用程序的生命周期处理,提供 emojiJournalServer 目标中 main.swift 使用的核心 init()run()方法来启动应用程序。

import Foundation
import Kitura
import LoggerAPI
import Configuration
import CloudEnvironment
import KituraContracts
import Health

public let projectPath = ConfigurationManager.BasePath.project.path
public let health = Health()

public class App {
let router = Router()
let cloudEnv = CloudEnv()

public init() throws {
// Run the metrics initializer
initializeMetrics(router: router)
}

func postInit() throws {
// Endpoints
initializeHealthRoutes(app: self)
router.get("/", handler: helloWorldHandler)
}

func helloWorldHandler(request: RouterRequest, response: RouterResponse, next: ()->()) {
response.send("Hello, World!")
next()
}

public func run() throws {
try postInit()
Kitura.addHTTPServer(onPort: cloudEnv.port, with: router)
Kitura.run()
}
}

运行后

欢迎页:http://localhost:8090

健康:http://localhost:8090/health

监控仪表板:http://localhost:8090/swiftmetrics-dash

HTTP 吞吐量视图使用每秒请求(RPS)度量标准显示在任何时间点发生的请求量。

http://localhost:8090/metrics

使用 Docker 在 Linux 上运行应用程序

Docker 和 Kubernetes 是 Kitura 部署的核心,它们提供了重要的性能和扩展优势。

kitura build
kitura run

第三章:RESTful APIs

在开始构建这些 RESTful API 之前,将了解 RESTful API 本身,包括它们的架构方法和设计原则。

REpresentational State Transfer (REST)

REpresentational State Transfer(REST)是一种架构风格或设计模式的 API。 如果一个 API 符合 Roy Fielding 博士在他的 2000 年论文“架构风格和基于网络的软件架构设计”中提出的一系列约束,那么它就是 RESTful。

Fielding 提出了以下六个限制:

  1. 客户端 - 服务器

RESTful API 在客户端和服务器之间提供明确的分离,允许客户端和服务器彼此独立地开发。这种分离使得为同一服务器实现多个客户端成为可能,就像你为 EmojiJournal 创建 iOS 和 Web 应用程序客户端时所做的那样。

  1. 无状态

RESTful API 应该是无状态的。每个请求都应该是自包含的,并且不应该依赖于存储在服务器上的先前请求的任何会话上下文。

  1. 可缓存

由于 RESTful API 是无状态和自包含的,因此可以将请求的结果缓存到 RESTful API。这可以由客户端或诸如 web 代理之类的中介来完成。

设计良好的 RESTful API 应该鼓励尽可能存储可缓存的数据,对请求的响应被隐式或显式标记为可缓存,可缓存到特定时间(expires-at)或不可缓存。

  1. 分层系统

RESTful API 可以是分层的,因此客户端不知道它是直接连接到 API 本身还是通过代理或负载均衡器间接连接。另外,API 本身可以由服务器系统本身内的若干层或源组成和构建。

  1. 按需代码(可选)

RESTful API 可以提供可以直接在客户端上运行的功能。实际上,这通常仅限于在浏览器中运行的 JavaScript 或 Java Applet。

  1. 统一界面

RESTful API 最重要的概念之一是资源的使用。资源可以是 API 可以提供有关信息的任何对象,例如你将使用的 JournalEntry 资源。

提供统一接口的 RESTful API 必须满足以下四个约束:

  • 请求必须确定他们采取行动的资源。
  • 请求和响应必须使用资源的表示。
  • 消息必须是自描述的,自包含的,并且只能使用标准操作。
  • 资源必须通过链接连接到其他资源。

另请注意,没有提及 HTTP 和 Web 请求。虽然 RESTful API 最常使用 HTTP 实现,但这不是必需的。也就是说,我们将仅介绍如何使用标准 HTTP 操作构建 RESTful API,我们接下来将简要介绍 HTTP 本身。

HyperText Transfer Protocol (HTTP)

超文本传输协议(HTTP)是一种客户端 - 服务器,基于请求 - 响应的协议,是网站和基于 Web 的流量的基础。 因此,它受到客户端和服务器的普遍支持。

HTTP-based RESTful APIs

URL 作为资源

统一接口的第一个要求是请求标识它们所依赖的资源。在 HTTP 中,这是通过 RESTful API 使用请求的 URL 实现的。

例如:

/entries

特定资源的 URL 编码标识符

将提供这些 URL 以使客户端能够与所有日记帐分录和特定日记帐分录进行交互:

/entries 
/entries/<identifier>

用于资源组的 URL 编码查询参数

你可能还希望提供与资源子集的 API。 这再次通过 URL 完成,这次使用 URL 查询参数。

URL 查询参数以 ? 开头。 他们可以使用 <key> = <value> 格式的键值对传递有关资源的任何其他信息。 可以使用键值对之间的 分隔符聚合和传递多个键值对。

/entries 
/entries?emoji=<emoji>&date=<date>
/entries/<idenfifier>

分层 API 的分层 URL

RESTful URL 也可以是分层的。 在你的 EmojiJournal 应用程序中,你只会支持拥有单个 EmojiJournal 的用户。 但是,假设你希望支持拥有多个期刊的用户,每个期刊都有自己的日记帐分录。

/journals 
/journals/<identifier>
/journals/<identifier>/entries
/journals/<identifier>/entries?emoji=<emoji>&date=<date> /journals/<identifier>/entries/<identifier>

例如,对以下 URL 的客户端请求将与日记 2 中的条目 20 进行交互:

/journals/2/entries/20

资源表示

回想一下,统一接口的第二个概念是请求和响应应该使用资源的表示。 这是通过在 HTTP 正文数据中编码资源的表示,在基于 HTTP 的 RESTful API 中实现的。

HTTP 消息,无论是请求还是响应,基本上由两部分组成:一组标题和正文数据。 为了完整性,响应还包含一个状态代码,用于报告请求的状态。

HTTP 首部和正文数据

HTTP 首部为请求和响应提供了额外的元数据。 首部在请求或响应开始时传输,并存储为键值对,一些首部允许多个值。

你可以使用 HTTP 首部存储你希望的任何其他信息。 你应该了解两个正式定义的标准标题,请参阅https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html(由正式的核心 RFC 规范定义),以及不太正式但广泛使用的非标准的首部,请参阅https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#Common_non-standard_request_fields

标准头用于传递诸如 Cookie,授权,ContentType(描述正文数据的媒体类型)和 Content-Length(以字节为单位描述正文数据长度)等信息。

HTTP 消息正文包含与请求或响应关联的数据。 在响应对网页的请求的情况下,正文数据包含网页本身的来源。 看一下对 Hello,World 的请求生成的 HTTP 响应! 你在上一章中创建的应用程序。

正文数据中的资源表示

如前所述,RESTful API 对返回的 HTTP 正文数据中的资源表示进行编码。 身体通常由三部分组成:

  1. Content-Type 首部

Content-Type 首部表示用于表示正文数据中的资源的编码。最常用的方法是使用 JavaScript Object Notation(JSON)对表示进行编码,并将 Content-Type 首部设置为application/json。有时与 RESTful API 一起使用的身体数据编码的其他示例包括 Protobuf,Thrift 和 Avro。

  1. 接受首部(仅限请求)

接受首部指示客户端将接受响应的编码。这通常是application/json,但它也可以包含编码列表。例如,如果客户端在 JSON 和 Protobuf 中都接受了响应,则此值可能设置为:application/json;application/protobuf

  1. 编码的正文数据

最后,使用 Content-Type 首部中设置的编码将资源本身编码为正文数据。在最常见的 application/json 场景中,这是资源的 JSON 表示。 JSON 的细节取决于特定资源的模型。例如,在本书后面稍后,JournalEntry 的资源将包含 id,emoji 和 date,类似于:

{
"id":"1",
"emoji":"😊",
"date":"2018-09-18T00:07:01Z"
}

自我描述性的信息

你已经了解了资源的识别和表示,但你如何对这些资源进行操作?

统一接口的第三个原则解决了这个问题。 它规定请求和响应都应该是自描述的,并且它们仅使用客户端和服务器都理解的标准操作。 在基于 HTML 的 RESTful API 中,这是通过使用特定的 HTTP 方法实现的。

RFC 2616 概述了八种 HTTP 方法:OPTIONS,GET,HEAD,POST,PUT,DELETE,TRACE 和 CONNECT。

通过链接连接资源

统一接口的第四个也是最后一个概念是资源应该通过链接连接到其他资源。

如你所见,基于 HTTP 的 RESTful API 使用 URL 和基于 URL 的标识符来提供对资源的访问并指定单个资源。 因此,资源之间的链接只是进一步的 RESTful URL 和链接到连接资源的基于 URL 的标识符。

有两种常见的场景需要 URL 或标识资源:

  • 请求的资源引用另一个(子或相关)资源。
  • 你需要知道资源的标识符。

构建 RESTful API

image-20190806103823282

到目前为止,你已经学会了所有基础知识。 当你了解有关设计和实现 RESTful API 的更多信息时,你会发现除了我们在此处介绍的范围之外还有其他最佳实践。 这包括设置正确的 Content-Type 和 Accepts 标头,为成功创建设置 Location 标头,并为请求中的任何故障设置正确的 HTTP 状态代码。

此外,需要对请求和响应主体进行编码和解码,最常见的是来自 JSON。 这曾经是一项艰巨的任务。 由于强大的 Codable 协议,Swift 使 JSON 优雅且无痛。

第四章:Codable 简介

Codable 协议是 Swift 编程语言最强大的功能之一。 在一个句子中,当你扩展对象以符合 Codable 时,你可以使该对象自动序列化为任何外部可读格式,例如 JSON,XML 或协议缓冲区。

The bare necessities

直接看一个例子

import Foundation

// 1 Cadabe 是一个组合的协议:Decodable & Encodable

public struct Song: Codable {
var title: String
var length: Int

enum CodingKeys: String, CodingKey {
case title = "songTitle"
case length = "songLength"
}
}

public struct Animal: Codable {
var name: String
var age: Int?
var isFriendly: Bool
var birthday: Date
var songs: [Song]
}

// 2 嵌套
let baloo = Animal(name: "Baloo",
age: 5,
isFriendly: true,
birthday: Date(),
songs: [Song(title: "The Bare Necessities", length: 180)])
let bagheera = Animal(name: "Bagheera",
age: nil,
isFriendly: true,
birthday: Date(),
songs: [Song(title: "Jungle's No Place For A Boy", length: 95)])

// 3 处理日期格式,驼峰式与蛇式转换
let encoder = JSONEncoder()
encoder.keyEncodingStrategy = .convertToSnakeCase
encoder.dateEncodingStrategy = .iso8601
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
decoder.dateDecodingStrategy = .iso8601

do {
// 4
let encodedBaloo = try encoder.encode(baloo)
if let balooString = String(data: encodedBaloo, encoding: .utf8) {
print(balooString)
}
let encodedBagheera = try encoder.encode(bagheera)
if let bagheeraString = String(data: encodedBagheera, encoding: .utf8) {
print(bagheeraString)
}
// 5
let decodedBaloo = try decoder.decode(Animal.self, from: encodedBaloo)
print(decodedBaloo)
let decodedBagheera = try decoder.decode(Animal.self, from: encodedBagheera)
print(decodedBagheera)
} catch let error {
print("Error occurred: \(error.localizedDescription)")
}

第五章:Codable Routing

Codable Routing 支持以完全类型安全的方式在你的应用程序的结构和类以及 HTTP 请求和响应中使用的正文数据之间自动转换。 它大大减少了构建路由处理程序时需要编写的代码,并使 Kitura 能够代表你执行数据验证和错误处理。

看 Model 和 Route 的实现:

import Foundation

struct JournalEntry: Codable {
var id: String?
var emoji: String
var date: Date
}
import Foundation
import LoggerAPI
import Kitura

var entries: [JournalEntry] = []

func initializeEntryRoutes(app: App) {
app.router.get("/entries", handler: getAllEntries)
app.router.post("/entries", handler: addEntry)
app.router.delete("/entries", handler: deleteEntry)
Log.info("Journal entry routes created")
}

func addEntry(entry: JournalEntry, completion: @escaping (JournalEntry?, RequestError?) -> Void) {
var storedEntry = entry
storedEntry.id = entries.count.value
entries.append(storedEntry)
completion(storedEntry, nil)
}

func getAllEntries(completion: @escaping ([JournalEntry]?, RequestError?) -> Void) -> Void {
completion(entries, nil)
}

func deleteEntry(id: String, completion: @escaping (RequestError?) -> Void) {
guard let index = entries.index(where: { $0.id == id }) else {
return completion(.notFound)
}
entries.remove(at: index)
completion(nil)
}

Codable Routing 的一个独特功能是它只需要你在处理函数中定义你希望从请求中接收的值,以及你将通过完成处理程序添加到响应中的值。

目前,Kitura 仅支持从 application/json 解码。 这意味着,实际上,Content-Type 当前必须始终设置为application/json。 但是,Kitura 团队计划在即将发布的版本中支持其他编码类型。

第六章:OpenAPI 规范

Swagger 的目标

在 Tony Tam 开创性的开源项目的基础上,其他公司开始做出贡献。 最终,Linux 基金会承担了该项目的赞助,并将其名称更改为 OpenAPI Initiative。 这种广泛的支持使 OpenAPI 规范在开源软件开发中占据了非常突出的位置。

当 Tony 开始研究 Swagger API 项目时,他确定了三个关键目标:

  • API 开发
  • API 文档
  • API 交互

生成你的规格

通过添加 Kitura-OpenAPI 依赖

.package(url: "https://github.com/IBM-Swift/Kitura-OpenAPI.git", from: "1.1.1"),

运行后

http://localhost:8080/openapi

{
"schemes" : [
"http"
],
"swagger" : "2.0",
"info" : {
"version" : "1.0",
"title" : "Kitura Project",
"description" : "Generated by Kitura"
},
"paths" : {
"\/entries" : {
"get" : {
"responses" : {
"200" : {
"schema" : {
"type" : "array",
"items" : {
"$ref" : "#\/definitions\/JournalEntry"
}
},
"description" : "successful response"
}
},
"consumes" : [
"application\/json"
],
"produces" : [
"application\/json"
]
},
"post" : {
"consumes" : [
"application\/json"
],
"produces" : [
"application\/json"
],
"responses" : {
"200" : {
"schema" : {
"$ref" : "#\/definitions\/JournalEntry"
},
"description" : "successful response"
}
},
"parameters" : [
{
"in" : "body",
"name" : "input",
"required" : true,
"schema" : {
"$ref" : "#\/definitions\/JournalEntry"
}
}
]
}
},
"\/health" : {
"get" : {
"responses" : {
"200" : {
"schema" : {
"$ref" : "#\/definitions\/Status"
},
"description" : "successful response"
}
},
"consumes" : [
"application\/json"
],
"produces" : [
"application\/json"
]
}
},
"\/entries\/{id}" : {
"delete" : {
"consumes" : [
"application\/json"
],
"produces" : [
"application\/json"
],
"responses" : {
"200" : {
"description" : "successful response"
}
},
"parameters" : [
{
"in" : "path",
"name" : "id",
"required" : true,
"type" : "string"
}
]
}
}
},
"basePath" : "\/",
"definitions": {
"JournalEntry": {
"type": "object",
"required": ["date","emoji"],
"properties": {
"id": {"type":"string"},
"emoji": {"type":"string"},
"date": {"type":"number"}
}
},
"Status": {
"type": "object",
"required": ["status","details","timestamp"],
"properties": {
"status": {"type":"string"},
"details": {"items":{"type":"string"},"type":"array"},
"timestamp": {"type":"string"}
}
}
}
}

此代码段的顶部表示你正在查看路径 /entries 的 GET 路由。 三个子节点描述了这条路线:

  • “responses”提供用户可以接收的一系列响应。

  • “consumes”指定必须给出此方法的数据类型。

  • “produces”描述响应将包含的数据类型。

使用 Kitura OpenAPI UI

同步生成的还有 UI 调试界面

http://localhost:8080/openapi/ui

image-20190807140446505

这对于你需要进行快速而肮脏的测试的情况非常有用 - 或者如果你无法让顽固的队友相信这个模块真的有用!

为你的 iOS 应用生成 SDK

终端运行,即可生成 SDK,一个功能齐全的 Swift SDK,可以处理与此特定服务器的所有网络通信。

docker pull swaggerapi/swagger-codegen-cli
docker run --rm -v ${PWD}:/local swaggerapi/swagger-codegen-cli langs
mkdir GeneratedSDK
touch specification.json
// 复制 http://localhost:8080/openapi 的内容到 specification.json
docker run --rm -v ${PWD}:/local swaggerapi/swagger-codegen-cli generate -i /local/specification.json -l swift -o /local/GeneratedSDK

在实践中,这不仅可以节省时间并让你快速启动和运行,还可以节省错误,并且对于那些没有(喘气)在 Swift 工作的人来说尤其有用!