目录
11> Dockerfile和docker-compose.yml
1> 每次打开Package.swift,都需要重新拉取第三方库
3> No custom working directory set for this scheme,找不到工作文件夹
5. docker-compose方式启动项目(推荐)但不一定成功
阅读前:
您应该具备Swift、Vapor、Docker、服务器、存储服务器等相关概念的基本知识。
本文不涉及全面的知识点,只针对开发、部署中的常见问题和注意事项进行说明。
我同样是初学者,本职工作是iOS开发,之前对后端开发一窍不通,因此下面的内容中,肯定还是有以前端的思路考虑后端的问题。如有遗漏和错误的地方,请以官方文档为准。
一、Vapor代码简介
1. 项目文件结构
2. 文件/文件夹 简介
- Package.swift: Swift Package Manager文件,声明引用的三方库。
- Source文件夹:编写代码的主要位置,包含几乎所有自己编写的代码。
- Source-APP文件夹:API Server、Web Server代码的主要位置。
- Source-Run文件夹:包含main.swift文件,为程序入口。
- Source-APP-CommonDefines文件夹:我自己创建的,存放一些全局性的配置、静态变量、结构体、类等。
- Source-APP-Controllers文件夹:包含各个业务功能的路由定义和处理程序。
- Source-APP-Middleware文件夹:包含自定义的中间件,比如全局性的Error处理、Log处理等。
- Source-APP-Migrations文件夹:顾名思义,数据库的Migration操作。创建表和字段,配置外键、父子关系、兄弟关系等。
- Source-APP-Models文件夹:数据库数据模型文件定义。
- Resources-Views文件夹:Leaf使用的文件夹,包含leaf文件。
- Public文件夹:存放网页使用的静态资源的地方。需要在configure.swift中,打开FileMiddleware中间件。
- Dockerfile文件:Docker使用的文件,包含Swift Build的一些指令。
- docker-compose.yml文件:Docker-Compose的配置文件,包含Docker部署时数据库的配置、API Server和数据库的依赖关系等等。
3. 部分内容详细展开
1> CommonResponse.swift
我在这里定义了BasicResponse,所有的网络请求,无论是正常返回还是报错,都转换为这个Response,方便APP统一处理。
import Foundation
import Vapor
typealias MSStringDictionary = [String: String]
struct BasicResponse<T>: Content where T: Codable {
let code: String
let msg: String
let data: T
}
2> HTTPCode+Message.swift
我将自己控制的报错信息,放到了这里,避免散落在代码各处,不好管理维护。
import Foundation
// 自定义的HTTPStatus
enum MSStatus {
static let success = CodeMsgModel(code: "200", msg: "Success!")
enum Login {
static let unauthorized = CodeMsgModel(code: "401", msg: "用户名或密码错误,请查证后重新输入")
}
enum Step {
static let uploadImageError = CodeMsgModel(code: "500", msg: "图片上传失败,请稍后再试")
static let deleteImageError = CodeMsgModel(code: "500", msg: "图片删除失败,请稍后再试")
static let editNothingChangeError = CodeMsgModel(code: "400", msg: "您没有修改任何内容")
}
}
struct CodeMsgModel {
let code: String
let msg: String
}
3> MissionController.swift
业务详细代码,Token校验、配置路由、路由响应等。
import Foundation
import Vapor
import Fluent
struct MissionController: RouteCollection {
func boot(routes: RoutesBuilder) throws {
// 地址: http://IP/api/v1/mission
let missionRoutes = routes.grouped("api", "v1", "mission")
// Token Authentication
let tokenRoutes = missionRoutes.grouped(Token.authenticator())
tokenRoutes.post("create", use: createV1Handler(_:))
tokenRoutes.get("all", use: allV1Handler(_:))
tokenRoutes.post("delete", ":missionID", use: deleteV1Handler(_:))
tokenRoutes.post("update", ":missionID", use: updateV1Handler(_:))
}
// 创建任务
func createV1Handler(_ req: Request) throws -> EventLoopFuture<BasicResponse<String>> {
let data = try req.content.decode(CreateMissionData.self)
let user = try req.auth.require(User.self)
let mission = try Mission(name: data.name, totalStep: data.totalStep, userID: user.requireID())
return mission.save(on: req.db)
.transform(to: BasicResponse(code: MSStatus.success.code, msg: MSStatus.success.msg, data: ""))
}
// 获取用户全部任务
func allV1Handler(_ req: Request) throws -> EventLoopFuture<BasicResponse<[MissionData]>> {
let user = try req.auth.require(User.self)
return user.$missions.query(on: req.db)
.sort(\.$createdAt, .ascending)
.all()
.map { missions in
let missionDatas = missions.map { mission in
return MissionData(missionID: mission.id,
name: mission.name,
totalStep: mission.totalStep,
currentStep: mission.currentStep == nil ? "0" : mission.currentStep)
}
return BasicResponse(code: MSStatus.success.code,
msg: MSStatus.success.msg,
data: missionDatas)
}
}
// 删除任务
func deleteV1Handler(_ req: Request) throws -> EventLoopFuture<BasicResponse<String>> {
Mission.find(req.parameters.get("missionID"), on: req.db)
.unwrap(or: Abort(.notFound))
.flatMap { mission in
mission.delete(on: req.db)
.transform(to: BasicResponse(code: MSStatus.success.code, msg: MSStatus.success.msg, data: ""))
}
}
// 更新任务
func updateV1Handler(_ req: Request) throws -> EventLoopFuture<BasicResponse<String>> {
let data = try req.content.decode(CreateMissionData.self)
return Mission.find(req.parameters.get("missionID"), on: req.db)
.unwrap(or: Abort(.notFound))
.flatMap { mission in
if mission.name == data.name, mission.totalStep == data.totalStep {
return req.eventLoop.makeSucceededFuture(
BasicResponse(code: MSStatus.Step.editNothingChangeError.code,
msg: MSStatus.Step.editNothingChangeError.msg,
data: "")
)
}
mission.name = data.name
mission.totalStep = data.totalStep
return mission.save(on: req.db)
.transform(to: BasicResponse(code: MSStatus.success.code, msg: MSStatus.success.msg, data: ""))
}
}
struct CreateMissionData: Content {
let name: String
let totalStep: String
}
struct MissionData: Content {
let missionID: UUID?
let name: String
let totalStep: String
let currentStep: String?
}
}
4> WebsiteController.swift
网页请求的处理,包括哪个路由使用哪个页面。req.view.render("index", context)表示使用index.leaf页面显示 http://IP 的get请求。
import Vapor
import Leaf
struct WebsiteController: RouteCollection {
func boot(routes: RoutesBuilder) throws {
routes.get(use: indexHandler(_:))
}
func indexHandler(_ req: Request) throws -> EventLoopFuture<View> {
let context = IndexContext()
return req.view.render("index", context)
}
struct IndexContext: Encodable {
let title = "网站的标题"
}
}
5> APIErrorMiddleware.swift
从ErrorMiddleware中copy的代码,将response部分改为了自己的BasicResponse,这样对于throw出的错误,也可以得到全局中间件的处理。包括对一些错误的特殊处理。
import Foundation
import Vapor
import Fluent
import FluentPos