Doing things series of articles is mainly to continue to continue their “T” strategy, but also represents the learning summary of the relevant content of bi design. This paper is the first part of Vapor, mainly recording the problems, thinking and summary of the most popular server framework Vapor of Swift for the first time.

preface

After open source from SwiftNIO, I couldn’t bear to press the Swift Server Side, which I didn’t care about at all before! In particular, I also saw this article, I believe that most of the students must have browsed this article, after reading I am also very excited, is it possible to use Swift unified front-end development day is coming? Only recently, under the pressure of Biesch, did I seriously learn how to use Swift to develop servers. At present, Vapor has the largest number of stars on Github, followed by Perfect.

Why did you choose Vapor?

  • in2018 @SwiftShrimp gods at the conventionSwift Serve SideMade a lightning Talk, rightVaporSpeak highly of;
  • I read some information on the Internet and found that we are rightVaporThe attention is a little bit higher;
  • VaporIn grammar and correlationAPIDesign will be moreSwiftySome;
  • Everything on GithubSwift Sever SideIt’s in the framestarIs the most.

However, at the beginning, it was probably because of the broken network of the school that the generation of Xcode template files was really slow!! Once it took twenty minutes and failed! In the middle, it cuts back to Perfect, and then Perfect also has some other problems, so it changes back.

start

downloadvapor

See the official website.

runHello, world!

  • vapor new yourProjectName. Create template project, of course you can add--template=apiTo create a template project that provides the corresponding service, but I tested it and it looked like any other template project.
  • vapor xcode. Creating Xcode projects is very, very slow, and there is a certain chance that they will fail. I guess the school’s Internet is broken

MVC – M

Vapor is the SQLite in-memory database by default. I wanted to look at the tables in the Vapor SQLite database, but I didn’t. Finally, I thought, it is an in-memory database, that is to say, the data will be cleared every time I Run. As can be seen from config.swift:

// ...
let sqlite = try SQLiteDatabase(storage: .memory)
// ...
Copy the code

It was written in the Vapor document that it was recommended to use Fluent ORM framework to manage the database table structure. At the beginning, I did not know anything about Fluent, so I could check todo. swift in the template file:

import FluentSQLite
import Vapor


final class Todo: SQLiteModel {
    /// unique identifier
    var id: Int?
    var title: String

    init(id: Int? = nil, title: String) {
        self.id = id
        self.title = title
    }
}

/// implement database operations. For example, add a table field and update the table structure
extension Todo: Migration {}/// allows the encoding and decoding of data from HTTP messages
extension Todo: Content {}/// Allows dynamic use of parameters defined in the route
extension Todo: Parameter {}Copy the code

As you can see from the Model in the template file, creating a table structure is like describing a class. Having used Django before, Swifty was surprised to see Vapor ORM like this. Vapor can also be built in accordance with MVC design pattern, and it is based on MVC indeed in the generated template file.

MVC – C

If we only use Vapor to do API service, we can leave V layer alone. In the “View” part of Vapor, we will use Leaf library to do rendering, and the specific details will not be expanded because we have not learned them.

For C, the overall idea is roughly the same as when writing App in the past, which is to deal with the relationship between data and view in C layer, but here only need to deal with the relationship between data and data.

import Vapor

/// Controls basic CRUD operations on `Todo`s.
final class TodoController {
    /// Returns a list of all `Todo`s.
    func index(_ req: Request) throws -> Future"[Todo] > {return Todo.query(on: req).all()
    }

    /// Saves a decoded `Todo` to the database.
    func create(_ req: Request) throws -> Future<Todo> {
        return try req.content.decode(Todo.self).flatMap { todo in
            return todo.save(on: req)
        }
    }

    /// Deletes a parameterized `Todo`.
    func delete(_ req: Request) throws -> Future<HTTPStatus> {
        return try req.parameters.next(Todo.self).flatMap { todo in
            return todo.delete(on: req)
        }.transform(to: .ok)
    }
}
Copy the code

As can be seen from the above template file generated by TodoController, a lot of Future asynchronous features, the first contact will be a little confused, some students recommended that the combination of PromiseKit is actually more pleasant.

fromSQLiteMySQL

The reason for switching is simple, it’s not that SQLite is bad, it’s just that it’s unused. The official document of Vapor was not systematic enough. Although all points were reached, it was too scattered. Besides, I felt that the document of Vapor had learned something from Apple, and the details were not expanded.

Package.swift

Write the corresponding library dependencies in package. swift,

import PackageDescription

let package = Package(
    name: "Unicorn-Server",
    products: [
        .library(name: "Unicorn-Server", targets: ["App"]),
    ],
    dependencies: [
        .package(url: "https://github.com/vapor/vapor.git", from: "3.0.0"),
        // here
        .package(url: "https://github.com/vapor/fluent-mysql.git", from: "3.0.0"),
    ],
    targets: [
        .target(name: "App",
                dependencies: [
                    "Vapor"."FluentMySQL"
            ]),
        .target(name: "Run", dependencies: ["App"]),
        .testTarget(name: "AppTests", dependencies: ["App"]])Copy the code

Triggered update

vapor xcode
Copy the code

Vapor has troubled me for several times, so it was very slow to update the dependency and the update failed. As a result, I have to confirm whether the dependency has been updated successfully every time I update it.

Update the ORM

After the update is successful, we can change the MySQL version of ORM according to the style of the template file todo. swift:

import FluentMySQL
import Vapor

/// A simple user.
final class User: MySQLModel {
    /// The unique identifier for this user.
    var id: Int?
    
    /// The user's full name.
    var name: String
    
    /// The user's current age in years.
    var age: Int
    
    /// Creates a new user.
    init(id: Int? = nil, name: String, age: Int) {
        self.id = id
        self.name = name
        self.age = age
    }
}

/// Allows `User` to be used as a dynamic migration.
extension User: Migration {}/// Allows `User` to be encoded to and decoded from HTTP messages.
extension User: Content {}/// Allows `User` to be used as a dynamic parameter in route definitions.
extension User: Parameter {}Copy the code

The above is my newly created User Model, and it is the same when I change it to Todo Model. There are only two changes: import FluentMySQL and inherit from MySQLModel. Fluent smoothed the use of all kinds of databases. No matter what database you have underneath, you only need to import and then switch inheritance.

Modify theconfig.swift

import FluentMySQL
import Vapor

/// the application will be called after initialization
public func configure(_ config: inout Config, _ env: inout Environment, _ services: inout Services) throws {
    // === mysql ===
    // First register the database
    try services.register(FluentMySQLProvider())

    // Register routes to the router for management
    let router = EngineRouter.default(a)try routes(router)
    services.register(router, as: Router.self)

    // Register middleware
    // Create a middleware configuration file
    var middlewares = MiddlewareConfig(a)// Error middleware. Catch the error and convert it to the HTTP return body
    middlewares.use(ErrorMiddleware.self)
    services.register(middlewares)
    
    // === mysql ===
    // Configure the MySQL database
    let mysql = MySQLDatabase(config: MySQLDatabaseConfig(hostname: "", port: 3306, username: "", password: "", database: "", capabilities: .default, characterSet: .utf8mb4_unicode_ci, transport: .unverifiedTLS))

    // Register the SQLite database configuration file to the database configuration center
    var databases = DatabasesConfig(a)// === mysql ===
    databases.add(database: mysql, as: .mysql)
    services.register(databases)

    // Configure the migration file. Equivalent to a registry
    var migrations = MigrationConfig(a)// === mysql ===
    migrations.add(model: User.self, database: .mysql)
    services.register(migrations)
}
Copy the code

Notice the configuration information for MySQLDatabaseConfig. If you are running MySQL version 8 or older, you can currently only select unverifiedTLS for the secure connection option used to verify connections to MySQL containers, namely the transport field. The blocks of code marked with // === mysql === in the code are different from the ones in the template files that use SQLite.

run

Run the project and enter MySQL to check.

mysql> show tables;+----------------------+ | Tables_in_unicorn_db | +----------------------+ | fluent | | Sticker | | User | +----------------------+ 3 rows in set (0.01sec)
mysql> desc User;+-------+--------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +-------+--------------+------+-----+---------+----------------+ | id | bigint(20) | NO | PRI | NULL | auto_increment | | name | varchar(255) | NO | | NULL | | | age | bigint(20) | NO | | NULL | | + -- -- -- -- -- - + -- -- -- -- -- -- -- -- -- -- -- -- -- - + -- -- -- -- -- - + + -- -- -- -- -- -- -- -- -- -- -- -- -- - + -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - + 3 rows in the set (0.01 SEC)Copy the code

I like the fact that Vapor is not prefixed to the generated table like Django, but that the name of your ORM class is the name of the generated table.

Add a field

Vapor also does not have as powerful workflow as Django. Many people say that Perfect is like Django, while I think Vapor is like Flask.

Changing Vapor table fields is not just a matter of changing Model properties, nor is it a matter of changing Model properties as Django does. Hoisting Python Manage. py Makemigrations and Python Manage. py Migrate We need to create our own migration files and figure out what has happened to the table structure.

In this article by Boxue, it is recommended to create a Migrations group in the App directory for easy operation. However, when I think about it, this will inevitably cause the Model and the corresponding migration files to be split, and then have to split the different migration files in another parent folder, which is obviously a bit of a problem. Finally, a horrible thought came to my mind: “Django is a very powerful, well-constructed framework!” .

My final catalog looks like this:

Model exercises ── heavy exercises ── heavy exercises ── heavy exercises ── heavy exercises ── heavy exercises ── heavy exercises ── UserController. Swift └ ─ ─ User. SwiftCopy the code

Here is an app file tree in Django:

User_avatar ├ ─ ─ just set py ├ ─ ─ admin. Py ├ ─ ─ apps. Py ├ ─ ─ migrations │ ├ ─ ─ 0001 _initial. Py │ ├ ─ ─ Py │ ├─ 0002_Auto_20190303154.py │ ├─ 0002_AUTO_20190303154.py │ ├─ 0002_Auto_20190303154.py │ ├─ 0002_Auto_20190303154.py │ ├─ 0002_Auto_20190303154.py │ ├─ 0003 _auto_20190322_1638. Py │ ├ ─ ─ 0004 _merge_20190408_2131. Py │ └ ─ ─ just set py ├ ─ ─ models. Py ├ ─ ─ tests. Py ├ ─ ─ urls. Py └ ─ ─ views. PyCopy the code

Some non-essential information has been deleted. As you can see, Django’s APP folder structure is pretty good! Note the migration file names under the Migrations folder. If we have good development ability, we can release non-business app for others to import directly into the project.

For me personally, I prefer Vapor/Flask system, because I can add whatever I need, and the whole design mode can be done according to my preference.

Add a createdTime field to the User Model.

import FluentMySQL

struct AddUserCreatedTime: MySQLMigration {
    static func prepare(on conn: MySQLConnection) -> EventLoopFuture<Void> {
        return MySQLDatabase.update(User.self, on: conn, closure: {
            $0.field(for: \User.fluentCreatedAt)
        })
    }
    
    static func revert(on conn: MySQLConnection) -> EventLoopFuture<Void> {
        // Return directly
        return conn.future()
    }
}
Copy the code

Delete a field

The development server using Swift is vulnerable to other development using Swift. In the beginning I did think that deleting the fields needed to be deleted in the Model would be enough, but running the project and checking the database proved that this was not the case.

First, we need to create a file to write the migration code for the Model, but this is not necessary. You can put all the curds that the Model needs to perform subsequent table fields in the same file, since each migration is a struct. What I did was create a new file for each migration as described above, and write “when” and “what was done” for each migration file.

The create method of DatabaseKit is called in the prepare method. Fluent supports most of the databases and encapsulates most of them based on DatabaseKit.

Delete a column from a table by Fluent. If you do not add a column to a table, you need to write a new migration file. For example, you can change the revert method in the above code to:

static func revert(on conn: MySQLConnection) -> EventLoopFuture<Void> {
    return MySQLDatabase.update(User.self, on: conn, closure: {
        $0.deleteField(for: \User.fluentCreatedAt)
    })
}
Copy the code

If we run the project directly, there will be no effect, because running the project directly will not trigger the revert method. We need to activate the Vapor command in config.swift:

var commands = CommandConfig.default()
commands.useFluentCommands()
services.register(commands)
Copy the code

Next, enter vapor Build && Vapor Run Revert in the terminal to undo the last added field. Vapor Build && Vapor Run REVERt-all allows you to undo all generated tables.

Here’s the problem! When my revert method says that when a migration is revoked, I delete the table and everything works fine.

return MySQLDatabase.delete(User.self, on: conn)
Copy the code

But if I try to delete the fluentCreatedAt field from the table when undo migration fails!! Did N long also did not succeed, almost turned over all the content on the Internet, also can not solve, almost all write so and then execute the withdrawal migration command took effect. We’ll see later.

Modify a table field

Persistence.

Auth

There are two methods of user authentication in Vapor. Stateless mode for API services and Web Sessions,

Add the dependent

/ / swift - tools - version: 4.0
import PackageDescription

let package = Package(
    name: "Unicorn-Server",
    products: [
        .library(name: "Unicorn-Server", targets: ["App"]),
    ],
    dependencies: [
        .package(url: "https://github.com/vapor/vapor.git", from: "3.0.0"),
        .package(url: "https://github.com/SwiftyJSON/SwiftyJSON.git", from: "4.0.0"),
        .package(url: "https://github.com/vapor/fluent-mysql.git", from: "3.0.0"),
        / / add the auth
        .package(url: "https://github.com/vapor/auth.git", from: "2.0.0"),
    ],
    targets: [
        .target(name: "App",
                dependencies: [
                    "Vapor"."SwiftyJSON"."FluentMySQL"./ / add the auth
                    "Authentication"
            ]),
        .target(name: "Run", dependencies: ["App"]),
        .testTarget(name: "AppTests", dependencies: ["App"]])Copy the code

Execute vapor Xcode pull dependency and rebuild xcode project.

registered

Add in config.swift:

public func configure(_ config: inout Config, _ env: inout Environment, _ services: inout Services) throws {
    // ...

    try services.register(AuthenticationProvider())
    
    // ...
}
Copy the code

Basic Authorization

In simple terms, this method is authentication password. We need to maintain a Path set for Basic Authorization. When requesting a Path that belongs to this collection, use the username and password as: For example, when username is pjhubs and password is pjhubs123, the result after splice is pjhubs:pjhubs123, The result of encryption is cGpodWJzOnBqaHViczEyMw==. Add it to the header of each HTTP request in the following format:

Authorization: Basic cGpodWJzOnBqaHViczEyMw==
Copy the code

Bearer Authorization

After the user has logged in successfully, we should return a complete token to indicate that the user has logged in and authenticated successfully in our system, and associate the token with the user. In terms of Bearer Authorization verification, we need to generate tokens by ourselves and we can use any method to do so. Vapor official did not provide the corresponding generation tool as long as it can maintain global uniqueness. Each time an HTTP request is made, add the token to the HTTP request in the following format. Assume that the requested token is pxoGJUtBVn7MXWoajWH+iw==, then the complete HTTP header is:

Authorization: Bearer pxoGJUtBVn7MXWoajWH+iw==
Copy the code

createToken Model

import Foundation
import Vapor
import FluentMySQL
import Authentication


final class Token: MySQLModel {
    var id: Int?
    var userId: User.ID
    var token: String
    var fluentCreatedAt: Date?
    
    init(token: String, userId: User.ID) {
        self.token = token
        self.userId = userId
    }
}

extension Token {
    var user: Parent<Token.User> {
        return parent(\.userId)
    }
}

// Implement the 'BearerAuthenticatable' protocol and return the bound 'tokenKey' to tell which attribute of the 'Token' Model to use as the real 'Token'
extension Token: BearerAuthenticatable {
    static var tokenKey: WritableKeyPath<Token.String> { return \Token.token }
}

extension Token: Migration {}extension Token: Content {}extension Token: Parameter {}// Implement the 'authentication. Token' protocol so that 'Token' becomes' authentication. Token '
extension Token: Authentication.Token {
    // Specify that 'UserType' in the protocol is a user-defined 'User'
    typealias UserType = User
    // The 'UserIDType' in the top protocol is the user-defined 'user.id'
    typealias UserIDType = User.ID
    
    // 'token' is bound to 'user'
    static var userIDKey: WritableKeyPath<Token.User.ID> {
        return \Token.userId
    }
}

extension Token {
    / / / ` token ` generated
    static func generate(for user: User) throws -> Token {
        let random = try CryptoRandom().generateData(count: 16)
        return try Token(token: random.base64EncodedString(), userId: user.requireID())
    }
}
Copy the code

Add the configuration

Write the Token configuration information in config.swift.

migrations.add(model: Token.self, database: .mysql)
Copy the code

Modify theUser Model

Associate the User with the Token.

import Vapor
import FluentMySQL
import Authentication

final class User: MySQLModel {
    var id: Int?
    var phoneNumber: String
    var nickname: String
    var password: String
    
    init(id: Int? = nil,
         phoneNumber: String,
         password: String,
         nickname: String) {
        self.id = id
        self.nickname = nickname
        self.password = password
        self.phoneNumber = phoneNumber
    }
}

extension User: Migration {}extension User: Content {}extension User: Parameter {}// Implement 'TokenAuthenticatable'. Which Model should be associated when methods in 'User' require 'token' validation
extension User: TokenAuthenticatable {
    typealias TokenType = Token
}

extension User {
    func toPublic(a) -> User.Public {
        return User.Public(id: self.id! , nickname:self.nickname)
    }
}

extension User {
    /// User outputs information because it does not want to expose all attributes of the entire 'User' entity
    struct Public: Content {
        let id: Int
        let nickname: String}}extension Future where T: User {
    func toPublic(a) -> Future<User.Public> {
        return map(to: User.Public.self) { (user) in
            return user.toPublic()
        }
    }
}
Copy the code

Routing method

After Basic Authorization is used for user authentication, we can route the authentication methods and non-authentication methods separately in the UserController.swift file as follows. If you do not have this file, you need to create a new one.

import Vapor
import Authentication

final class UserController: RouteCollection {
    
    // Override the 'boot' method to define a route in the controller
    func boot(router: Router) throws {
        let userRouter = router.grouped("api"."user")
        
        // Normal routing
        let userController = UserController()
        router.post("register", use: userController.register)
        router.post("login", use: userController.login)
        
        // 'tokenAuthMiddleware' can find the value in the current HTTP header Authorization field and retrieve the user corresponding to the token. The result is cached in the request cache for subsequent use by other methods
        // Routes that require token authentication
        let tokenAuthenticationMiddleware = User.tokenAuthMiddleware()
        let authedRoutes = userRouter.grouped(tokenAuthenticationMiddleware)
        authedRoutes.get("profile", use: userController.profile)
        authedRoutes.get("logout", use: userController.logout)
        authedRoutes.get("", use: userController.all)
        authedRoutes.get("delete", use: userController.delete)
        authedRoutes.get("update", use: userController.update)
    }

    func logout(_ req: Request) throws -> Future<HTTPResponse> {
        let user = try req.requireAuthenticated(User.self)
        return try Token
            .query(on: req)
            .filter(\Token.userId, .equal, user.requireID())
            .delete()
            .transform(to: HTTPResponse(status: .ok))
    }
    
    func profile(_ req: Request) throws -> Future<User.Public> {
        let user = try req.requireAuthenticated(User.self)
        return req.future(user.toPublic())
    }
    
    func all(_ req: Request) throws -> Future"[User.Public] > {return User.query(on: req).decode(data: User.Public.self).all()
    }
    
    func register(_ req: Request) throws -> Future<User.Public> {
        return try req.content.decode(User.self).flatMap({
            return $0.save(on: req).toPublic()
        })
    }
    
    func delete(_ req: Request) throws -> Future<HTTPStatus> {
        return try req.parameters.next(User.self).flatMap { todo in
            return todo.delete(on: req)
            }.transform(to: .ok)
    }
    
    func update(_ req: Request) throws -> Future<User.Public> {
        return try flatMap(to: User.Public.self, req.parameters.next(User.self), req.content.decode(User.self)) { (user, updatedUser) in
            user.nickname = updatedUser.nickname
            user.password = updatedUser.password
            return user.save(on: req).toPublic()
        }
    }
}
Copy the code

Note that let user = try req.requireauthenticated (user.self) is only required if a routing method needs to fetch information from the user associated with the token. Otherwise, if we only need to authenticate a routing method, You just need to join the tokenAuthenticationMiddleware routing groups.

And we don’t need to pass in any information about the current logged-in user, just a token.

Modify theconfig.swift

Finally, add userController which implements RouteCollection protocol to config.swift for route registration.

import Vapor

public func routes(_ router: Router) throws {
    // User routing
    let usersController = UserController(a)try router.register(collection: usersController)
}
Copy the code

Afterword.

It feels like Django when a few design patterns of tips are mixed together. However, there are some big differences with Django. There are not enough Vapor processing in some details, and the document is not simple and clear enough.

In the course of this study, I thought many times “Why do I want to use this broken thing?” “But each time the thought occurred to me, I finally resisted, because it was Swift!

Github address: unicorn-server

PJ’s path to iOS development