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?
- in
2018 @Swift
Shrimp gods at the conventionSwift Serve Side
Made a lightning Talk, rightVapor
Speak highly of; - I read some information on the Internet and found that we are right
Vapor
The attention is a little bit higher; Vapor
In grammar and correlationAPI
Design will be moreSwifty
Some;- Everything on Github
Swift Sever Side
It’s in the framestar
Is 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=api
To 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.
fromSQLite
到 MySQL
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