用Swift搭建API Server,Vapor + PostgreSQL + Docker + ECS + OSS

目录

一、Vapor代码简介

1. 项目文件结构

2. 文件/文件夹 简介

3. 部分内容详细展开

1> CommonResponse.swift

2> HTTPCode+Message.swift

3> MissionController.swift

4> WebsiteController.swift

5> APIErrorMiddleware.swift

6> LogMiddleware.swift

7> CreateMission.swift

8> Mission.swift

9> configure.swift

10> routes.swift

11> Dockerfile和docker-compose.yml

4. 注意事项

1> 每次打开Package.swift,都需要重新拉取第三方库

2> 项目多次运行后报错,提示端口被占用

3> No custom working directory set for this scheme,找不到工作文件夹

4> Fatal error: Error raised at top level: connection reset (error set): Connection refused (errno: 61): file Swift/ErrorType.swift, line 200

5> 三方库拉取太慢,或者干脆拉不下来

二、线上部署

1.准备域名(非必要)

1> 购买域名

2> 购买阿里云ECS

3> ICP备案

4> 域名解析

5> 项目线上部署

6> 公安联网备案

2. 购买阿里云ECS+OSS

1> ECS

2> OSS

3. ECS上配置开发环境

4. 上传项目代码

5. docker-compose方式启动项目(推荐)但不一定成功

6.手动启动项目


 

阅读前:

  1. 您应该具备Swift、Vapor、Docker、服务器、存储服务器等相关概念的基本知识。

  2. 本文不涉及全面的知识点,只针对开发、部署中的常见问题和注意事项进行说明。

  3. 我同样是初学者,本职工作是iOS开发,之前对后端开发一窍不通,因此下面的内容中,肯定还是有以前端的思路考虑后端的问题。如有遗漏和错误的地方,请以官方文档为准。

 

一、Vapor代码简介

1. 项目文件结构

 

2. 文件/文件夹 简介

  1. Package.swift: Swift Package Manager文件,声明引用的三方库。
  2. Source文件夹:编写代码的主要位置,包含几乎所有自己编写的代码。
  3. Source-APP文件夹:API Server、Web Server代码的主要位置。
  4. Source-Run文件夹:包含main.swift文件,为程序入口。
  5. Source-APP-CommonDefines文件夹:我自己创建的,存放一些全局性的配置、静态变量、结构体、类等。
  6. Source-APP-Controllers文件夹:包含各个业务功能的路由定义和处理程序。
  7. Source-APP-Middleware文件夹:包含自定义的中间件,比如全局性的Error处理、Log处理等。
  8. Source-APP-Migrations文件夹:顾名思义,数据库的Migration操作。创建表和字段,配置外键、父子关系、兄弟关系等。
  9. Source-APP-Models文件夹:数据库数据模型文件定义。
  10. Resources-Views文件夹:Leaf使用的文件夹,包含leaf文件。
  11. Public文件夹:存放网页使用的静态资源的地方。需要在configure.swift中,打开FileMiddleware中间件。
  12. Dockerfile文件:Docker使用的文件,包含Swift Build的一些指令。
  13. 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
  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值