Vapor 大型教程
API 模板与约定
本文拆解 Vapor 模板工程,弄清楚 Package.swift、Sources、Tests 各自代表什么,以及程序到底从哪里启动。
目录总体结构
用 vapor new 生成的模板工程,顶层目录大致如下:
$ tree -L 1 .
.
├── Dockerfile
├── Package.resolved
├── Package.swift
├── Public
├── Sources
├── Tests
└── docker-compose.yml
3 directories, 4 files
Package.resolved:解析Package.swift时自动生成,开发者无需修改。Package.swift:描述项目的依赖与产物信息。Public:公共资源目录,存放图片、CSS、JS 以及 Leaf 模板等。Sources:整个工程的主体源代码。Tests:针对工程功能编写的测试用例。Dockerfile与docker-compose.yml:部署到 Docker 容器时使用的配置。
Sources 子目录
$ tree Sources/
Sources
├── App
│ ├── Controllers
│ ├── configure.swift
│ └── routes.swift
└── Run
└── main.swift
3 directories, 3 files
Sources 下的每个子目录都是一个模块。App 是功能模块,Run 是可执行模块——编译后由操作系统调起运行;App 不能单独运行,它被 Run 依赖。整个程序的入口是 main.swift。
Package.swift 项目描述
Package.swift 描述了整个工程如何构成:
// swift-tools-version:5.6
import PackageDescription
let package = Package(
name: "HelloVapor",
platforms: [
.macOS(.v12)
],
dependencies: [
// 💧 A server-side Swift web framework.
.package(url: "https://github.com/vapor/vapor.git", from: "4.0.0"),
],
targets: [
.target(
name: "App",
dependencies: [
.product(name: "Vapor", package: "vapor")
],
swiftSettings: [
.unsafeFlags(["-cross-module-optimization"], .when(configuration: .release))
]
),
.executableTarget(name: "Run", dependencies: [.target(name: "App")]),
.testTarget(name: "AppTests", dependencies: [
.target(name: "App"),
.product(name: "XCTVapor", package: "vapor"),
])
]
)
从描述文件可以看出:每个 Target 是一个模块。App 依赖了 vapor 包里的 Vapor 模块,依赖信息在 dependencies 数组中声明,SPM 会解析并拉取相关文件参与编译。Run 可执行模块依赖 App 功能模块;AppTests 模块依赖 App,因为它是专门给 App 写的测试。
示例工程的代码逻辑
程序以 main.swift 为入口,读取命令行参数与环境变量,据此创建 app,并在运行前用 configure.swift 里的 configure 函数完成配置:
import App
import Vapor
var env = try Environment.detect()
try LoggingSystem.bootstrap(from: &env)
let app = Application(env)
defer { app.shutdown() }
try configure(app)
try app.run()
import Vapor
// configures your application
public func configure(_ app: Application) throws {
// uncomment to serve files from /Public folder
// app.middleware.use(FileMiddleware(publicDirectory: app.directory.publicDirectory))
// register routes
try routes(app)
}
配置过程中会调用 routes.swift 来注册路由:
import Vapor
func routes(_ app: Application) throws {
app.get { req async in
"It works!"
}
app.get("hello") { req async -> String in
"Hello, world!"
}
}
在 HelloVapor 根目录用 vapor run 或 swift run 编译运行后,就能在浏览器里测试 / 与 /hello 这两个 GET 路由。
测试子模块
Tests 目录结构:
$ tree Tests
Tests
└── AppTests
└── AppTests.swift
1 directory, 1 file
在 AppTests.swift 里编写测试 App 模块的用例:
@testable import App
import XCTVapor
final class AppTests: XCTestCase {
func testHelloWorld() throws {
let app = Application(.testing)
defer { app.shutdown() }
try configure(app)
try app.test(.GET, "hello", afterResponse: { res in
XCTAssertEqual(res.status, .ok)
XCTAssertEqual(res.body.string, "Hello, world!")
})
}
}
在工程根目录用 swift test 运行测试。在 Mac 上也可以直接用 Xcode 开发:
$ vapor xcode
Cmd+R:运行程序Cmd+U:执行测试