API 模板与约定 封面
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

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 runswift 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

本系列其他文章