【Vapor】03 Chapter 5: Fluent & Persisting Models

0x00 Chapter 5: Fluent & Persisting Models

1.Fluent is Vapor’s ORM or object relational mapping tool.
It’s an abstraction layer between the Vapor application and the database


2.Models are the Swift representation of your data and are used throughout Fluent


3.创建一个使用 Fluent 的新项目
新建一个文件夹,比如 vapor_learn
打开终端 cd 到此文件夹:
打开终端,输入 cd,按个空格,把文件夹 vapor_learn 拖入终端,最后按 Enter
创建一个名为:TILApp 的项目,(Today I Learn)
vapor new TILApp

是否使用 Fluent?
输入:y
数据库使用 Postgres
不使用 Leaf

等待创建完成

以下是日志:

Cloning template...
name: TILApp
Would you like to use Fluent?
y/n> y
fluent: Yes
db: Postgres (Recommended)
Would you like to use Leaf?
y/n> n
leaf: No
Generating project files
+ Package.swift
+ main.swift
+ configure.swift
+ routes.swift
+ Todo.swift
+ CreateTodo.swift
+ .gitkeep
+ TodoController.swift
+ AppTests.swift
+ index.leaf
+ .gitkeep
+ Dockerfile
+ docker-compose.yml
+ .gitignore
+ .dockerignore
Creating git repository
Adding first commit
                                                        
                                       **               
                                     **~~**             
                                   **~~~~~~**           
                                 **~~~~~~~~~~**         
                               **~~~~~~~~~~~~~~**       
                             **~~~~~~~~~~~~~~~~~~**     
                           **~~~~~~~~~~~~~~~~~~~~~~**   
                          **~~~~~~~~~~~~~~~~~~~~~~~~**  
                         **~~~~~~~~~~~~~~~~~~~~~~~~~~** 
                        **~~~~~~~~~~~~~~~~~~~~~~~~~~~~**
                        **~~~~~~~~~~~~~~~~~~~~~~~~~~~~**
                        **~~~~~~~~~~~~~~~~~~~~~++++~~~**
                         **~~~~~~~~~~~~~~~~~~~++++~~~** 
                          ***~~~~~~~~~~~~~~~++++~~~***  
                            ****~~~~~~~~~~++++~~****    
                               *****~~~~~~~~~*****      
                                  *************         
                                                        
                         _       __    ___   ___   ___  
                        \ \  /  / /\  | |_) / / \ | |_) 
                         \_\/  /_/--\ |_|   \_\_/ |_| \ 
                           a web framework for Swift    
                                                        
                        Project TILApp has been created!
                                        
                 Use cd 'TILApp' to enter the project directory
                  Use vapor xcode to open the project in Xcode

看图片更清晰:

使用 cd TILApp 进入项目
使用 vapor xcode 可以在 Xocde 中打开项目

项目结构如下:

TILApp
--- Package.swift
--- Public
--- Resources
    --- Views
        --- index.leaf
--- Sources
    --- App
        --- Controllers
            --- TodoController.swift        
        --- Migrations
            --- CreateTodo.swift
        --- Models
            --- Todo.swift
        --- configure.swift
        --- routes.swift                
    --- Run
        --- main.swift    
--- Tests
    --- AppTests
        --- AppTests.swift
    --- docker-compose.yml
    --- Dockerfile
        

看图片更清晰:
【Vapor】03 Chapter 5: Fluent & Persisting Models_第1张图片


4.删除模板中自动生成的一些文件:
依次输入以下 3 个命令:

rm -rf Sources/App/Models/*
rm -rf Sources/App/Migrations/*
rm -rf Sources/App/Controllers/*

删除 configure.swift 中的代码:

app.migrations.add(CreateTodo())

删除 routes.swift 中的代码:

try app.register(collection: TodoController())

5.在 Models 文件夹内,新建模型文件:
Acronym.swift,代表要提交的数据

// 1 
final class Acronym: Model {
    // 2 
    static let schema = "acronyms"
    
    // 3 
    @ID
    var id: UUID?
    
    // 4 
    @Field(key: "short")
    var short: String
    
    @Field(key: "long")
    var long: String
    
    // 5 
    init() {}
    
    // 6 
    init(id: UUID? = nil, short: String, long: String) {
        self.id = id
        self.short = short
        self.long = long
    }
}

// 1. Define a class that conforms to Model.

// 2. Specify the schema as required by Model. This is the name of the table in the database.

// 3. Define an optional id property that stores the ID of the model, if one has been set. This is annotated with Fluent’s @ID property wrapper. This tells Fluent what to use to look up the model in the database

// 4. Define two String properties to hold the acronym and its definition. These use the @Field property wrapper to denote a generic database field. The key parameter is the name of the column in the database
数据库表中的字段,列名称

@ID marks a property as the ID for that table
@Field marks the property of a model as a generic column in the database

// 5. Provide an empty initializer as required by Model. Fluent uses this to initialize models returned from database queries.

// 6. Provide an initializer to create the model as required.


6.在 Migrations 文件夹内,新建文件:
CreateAcronym.swift,用于创建对应的数据库表

// 1
struct CreateAcronym: Migration {
    // 2
    func prepare(on database: Database) -> EventLoopFuture {
        database.schema("acronyms") // 3
            .id() // 4
            .field("short", .string, .required) // 5
            .field("long", .string, .required)  // 5
            .create() // 6
    }
    
    // 7
    func revert(on database: Database) -> EventLoopFuture {
        database.schema("acronyms").delete()
    }
}

// 1. Define a new type, CreateAcronym that conforms to Migration.

// 2. Implement prepare(on:) as required by Migration. You call this method when you run your migrations.

// 3. Define the table name for this model. This must match schema from the model.

// 4. Define the ID column in the database.

// 5. Define columns for short and long. Set the column type to string and mark the columns as required. This matches the non-optional String properties in the model. The field names must match the key of the property wrapper, not the name of the property itself.

// 6. Create the table in the database.

// 7. Implement revert(on:) as required by Migration. You call this function when you revert your migrations. This deletes the table referenced with schema(_:).

继承自 Migration
实现方法 prepare(on:)
为模型创建表,表名 必须和 模型schema 一样
定义 id, short, long 这几个列,指定类型,required 对应 non-optional
.field 的列名,跟 @Fieldkey 对应


7.配置 configure.swift
app.databases.use(_:as:) 方法后添加代码:

app.migrations.add(CreateAcronym())
app.logger.logLevel = .debug
try app.autoMigrate().wait()

8.存储数据模型,需要数据库
测试 PostgreSQL:在 Docker container 里运行 Postgres server
依次 下载安装打开 软件: docker
地址:https://www.docker.com/get-docker

使用终端创建数据库:

docker run --name postgres \
 -e POSTGRES_DB=vapor_database \
 -e POSTGRES_USER=vapor_username \
 -e POSTGRES_PASSWORD=vapor_password \
 -p 5432:5432 -d postgres

运行一个名为 postgres 的容器
指定 数据库名用户名密码
指定 端口,默认是 5432

检查数据库是否运行:
docker ps

有以下日志,表示成功:

CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
53dc8048acaf postgres “docker-entrypoint.s…” 8 months ago Up 3 seconds 0.0.0.0:5432->5432/tcp, :::5432->5432/tcp postgres

9.最后,提交模型数据到数据库

9.1.让模型 Acronym 遵守 Content 协议
Acronym.swift 最后添加:

extension Acronym: Content {}

Since Acronym already conforms to Codable via Model, you don’t have to add anything else.


9.2.定义 路由:
接收数据,解析 JSON,转成 Acronym 模型,存入数据库

routes.swift 文件中,在 routes 方法内添加:

    // 1
    app.post("api", "acronyms") { req -> EventLoopFuture in
        // 2 
        let acronym = try req.content.decode(Acronym.self)
        // 3 
        return acronym.save(on: req.db).map {
            // 4
            acronym
        }
    }

// 1. Register a new route at /api/acronyms that accepts a POST request and returns EventLoopFuture. It returns the acronym once it’s saved.

// 2. Decode the request’s JSON into an Acronym model using Codable.

// 3. Save the model using Fluent and the database from Request.

// 4. save(on:) returns EventLoopFuture so use map to return the acronym when the save completes.


10.使用 Rested 提交数据
把代码 运行 起来后,就可以提交数据了

url: http://127.0.0.1:8080/api/acronyms
method: POST
parameters: {"long": "Tomorrow is a good day", "short": "TGD"}

参数提交格式,选择 JSON 形式: JSON-encoded
点击右下角的 Send Request 发起请求
成功后,会返回对应的模型数据,id 字段被赋值了

如下图所示:


11.运行代码
在终端进入到项目的根目录:TILApp
运行命令:vapor run 或者 swift run
会自动下载所有的依赖文件
依赖文件会有很多很多
这一步耗时会 很长很长很长......
你们可能在这 最后一步放弃了

反正我打算新建一个演示项目时
整了一天,都没有搞好

Updating https://github.com/vapor/fluent.git
Updating https://github.com/vapor/fluent-postgres-driver.git
Updating https://github.com/vapor/vapor.git
Updating https://github.com/vapor/fluent-kit.git
Fetching https://github.com/vapor/async-kit.git
Updating https://github.com/vapor/postgres-kit.git
Fetching https://github.com/vapor/postgres-nio.git
Fetching https://github.com/vapor/sql-kit.git
Fetching https://github.com/apple/swift-nio-transport-services.git
error: Failed to clone https://github.com/apple/swift-nio-transport-services.git:
    Cloning into bare repository '/Users/apple/vapor_learn/1/TILApp/.build/repositories/swift-nio-transport-services-91c80623'...
    fatal: unable to access 'https://github.com/apple/swift-nio-transport-services.git/': LibreSSL SSL_connect: SSL_ERROR_SYSCALL in connection to github.com:443 
VaporToolbox/exec.swift:55: Fatal error: result 1
Illegal instruction: 4

拉到某个库的时候,就可能失败了
fatal: unable to access 'https://github.com/apple/swift-nio-transport-services.git/': LibreSSL SSL_connect: SSL_ERROR_SYSCALL in connection to github.com:443

找了好多方案,就是解决不了
这实在是,无话可说了~


0x01 最后

12.如果你们成功了!
终端会输出以下:

[ NOTICE ] Server starting on http://127.0.0.1:8080 (Vapor/HTTP/Server/HTTPServer.swift:270)

表示服务启动了
祝你们好运


0x02 后续

直到今天(2022-03-29 18:48:19)
更新 系统 后:Mac 12.3
更新 Xcode 后:Xcode 13.3

新建项目:vapor new TILApp
终于能够再次跑起来了!
忍不住 地想要记录下来

终端 cdTILApp 目录后
运行:swift run
以下是成功日志:
虽然有警告,但程序员通常对应它视而不见

Updating https://github.com/vapor/fluent-postgres-driver.git
Updating https://github.com/vapor/vapor.git
Updating https://github.com/vapor/fluent.git
Updated https://github.com/vapor/fluent-postgres-driver.git (2.65s)
Updated https://github.com/vapor/fluent.git (2.65s)
Updated https://github.com/vapor/vapor.git (2.65s)
Computing version for https://github.com/vapor/fluent.git
Computed https://github.com/vapor/fluent.git at 4.4.0 (0.02s)
Updating https://github.com/vapor/fluent-kit.git
Updating https://github.com/vapor/fluent-kit.git
Updated https://github.com/vapor/fluent-kit.git (1.29s)
Computing version for https://github.com/vapor/fluent-postgres-driver.git
Computed https://github.com/vapor/fluent-postgres-driver.git at 2.2.6 (0.02s)
Updating https://github.com/vapor/fluent-kit.git
Updating https://github.com/vapor/async-kit.git
Updating https://github.com/vapor/postgres-kit.git
Updated https://github.com/vapor/fluent-kit.git (120.51s)
Updating https://github.com/vapor/async-kit.git
Updated https://github.com/vapor/async-kit.git (20.05s)
Updating https://github.com/vapor/postgres-kit.git
Updated https://github.com/vapor/postgres-kit.git (1.30s)
Computing version for https://github.com/vapor/postgres-kit.git
Computed https://github.com/vapor/postgres-kit.git at 2.6.0 (0.02s)
Updating https://github.com/vapor/async-kit.git
Updating https://github.com/vapor/sql-kit.git
Updating https://github.com/vapor/postgres-nio.git
Updated https://github.com/vapor/postgres-nio.git (1.32s)
Updated https://github.com/vapor/async-kit.git (5.65s)
Updated https://github.com/vapor/sql-kit.git (5.65s)
Computing version for https://github.com/vapor/sql-kit.git
Computed https://github.com/vapor/sql-kit.git at 3.16.0 (0.03s)
Updating https://github.com/apple/swift-log.git
Updating https://github.com/apple/swift-nio.git
Updated https://github.com/apple/swift-log.git (49.37s)
Updated https://github.com/apple/swift-nio.git (49.37s)
Computing version for https://github.com/vapor/postgres-nio.git
Computed https://github.com/vapor/postgres-nio.git at 1.9.0 (0.02s)
Updating https://github.com/apple/swift-crypto.git
Updating https://github.com/apple/swift-nio-ssl.git
Updating https://github.com/apple/swift-metrics.git
Updated https://github.com/apple/swift-crypto.git (1.31s)
Updating https://github.com/apple/swift-nio-transport-services.git
Updated https://github.com/apple/swift-nio-transport-services.git (1.16s)
Updated https://github.com/apple/swift-nio-ssl.git (44.55s)
Updated https://github.com/apple/swift-metrics.git (44.55s)
Computing version for https://github.com/apple/swift-nio-transport-services.git
Computed https://github.com/apple/swift-nio-transport-services.git at 1.11.4 (0.02s)
Computing version for https://github.com/apple/swift-log.git
Computed https://github.com/apple/swift-log.git at 1.4.2 (0.02s)
Computing version for https://github.com/apple/swift-nio.git
Computed https://github.com/apple/swift-nio.git at 2.39.0 (0.02s)
Computing version for https://github.com/apple/swift-metrics.git
Computed https://github.com/apple/swift-metrics.git at 2.3.0 (0.02s)
Computing version for https://github.com/apple/swift-nio-ssl.git
Computed https://github.com/apple/swift-nio-ssl.git at 2.18.0 (0.02s)
Computing version for https://github.com/vapor/fluent-kit.git
Computed https://github.com/vapor/fluent-kit.git at 1.23.2 (0.02s)
Computing version for https://github.com/vapor/async-kit.git
Computed https://github.com/vapor/async-kit.git at 1.11.1 (0.02s)
Computing version for https://github.com/vapor/vapor.git
Computed https://github.com/vapor/vapor.git at 4.55.3 (0.03s)
Updating https://github.com/vapor/multipart-kit.git
Updating https://github.com/apple/swift-nio-extras.git
Updating https://github.com/vapor/websocket-kit.git
Updated https://github.com/apple/swift-nio-extras.git (120.49s)
Updated https://github.com/vapor/websocket-kit.git (120.49s)
Updating https://github.com/apple/swift-nio-http2.git
Updating https://github.com/vapor/console-kit.git
Fetching https://github.com/swift-server/swift-backtrace.git
Updated https://github.com/apple/swift-nio-http2.git (68.32s)
Updated https://github.com/vapor/console-kit.git (68.32s)
Fetching https://github.com/swift-server/async-http-client.git
Fetching https://github.com/vapor/routing-kit.git
[883/20948] Fetching objectsFetched https://github.com/swift-server/swift-backtrace.git (70.93s)
Updating https://github.com/vapor/multipart-kit.git
[3514/30681] Fetching objectsUpdated https://github.com/vapor/multipart-kit.git (1.24s)
[22895/30681] Fetching objectsSkipping cache due to an error: Failed to clone repository https://github.com/swift-server/async-http-client.git:
    Cloning into bare repository '/Users/apple/Library/Caches/org.swift.swiftpm/repositories/async-http-client-0b7b4e02'...
    remote: Enumerating objects: 9733, done.        
remote: Counting objects: 100% (3385/3385), done.        
remote: Compressing objects: 100% (1082/1082), done.        
error: 3672 bytes of body are still expectedKiB | 4.00 KiB/s 
fetch-pack: unexpected disconnect while reading sideband packets
    fatal: early EOF
    fatal: fetch-pack: invalid index-pack output
Fetched https://github.com/vapor/routing-kit.git (236.43s)
[30681/30681] Fetching objects
Fetched https://github.com/swift-server/async-http-client.git (244.35s)
Computing version for https://github.com/apple/swift-nio-http2.git
Computed https://github.com/apple/swift-nio-http2.git at 1.20.1 (1.94s)
Updating https://github.com/vapor/multipart-kit.git
Computing version for https://github.com/swift-server/async-http-client.git
Updated https://github.com/vapor/multipart-kit.git (1.27s)
Computed https://github.com/swift-server/async-http-client.git at 1.9.0 (2.02s)
Computing version for https://github.com/apple/swift-nio-extras.git
Computed https://github.com/apple/swift-nio-extras.git at 1.10.2 (0.74s)
Computing version for https://github.com/vapor/multipart-kit.git
Computed https://github.com/vapor/multipart-kit.git at 4.5.1 (0.71s)
Computing version for https://github.com/swift-server/swift-backtrace.git
Computed https://github.com/swift-server/swift-backtrace.git at 1.3.1 (0.72s)
Computing version for https://github.com/vapor/routing-kit.git
Computed https://github.com/vapor/routing-kit.git at 4.3.1 (0.69s)
Computing version for https://github.com/vapor/websocket-kit.git
Computed https://github.com/vapor/websocket-kit.git at 2.3.1 (0.74s)
Computing version for https://github.com/vapor/console-kit.git
Computed https://github.com/vapor/console-kit.git at 4.2.7 (0.75s)
Computing version for https://github.com/apple/swift-crypto.git
Computed https://github.com/apple/swift-crypto.git at 2.0.5 (0.74s)
Creating working copy for https://github.com/apple/swift-nio-http2.git
Working copy of https://github.com/apple/swift-nio-http2.git resolved at 1.20.1
Creating working copy for https://github.com/apple/swift-nio.git
Working copy of https://github.com/apple/swift-nio.git resolved at 2.39.0
Creating working copy for https://github.com/vapor/vapor.git
Working copy of https://github.com/vapor/vapor.git resolved at 4.55.3
Creating working copy for https://github.com/vapor/routing-kit.git
Working copy of https://github.com/vapor/routing-kit.git resolved at 4.3.1
Creating working copy for https://github.com/apple/swift-log.git
Working copy of https://github.com/apple/swift-log.git resolved at 1.4.2
Creating working copy for https://github.com/apple/swift-nio-ssl.git
Working copy of https://github.com/apple/swift-nio-ssl.git resolved at 2.18.0
Creating working copy for https://github.com/vapor/postgres-kit.git
Working copy of https://github.com/vapor/postgres-kit.git resolved at 2.6.0
Creating working copy for https://github.com/vapor/fluent-kit.git
Working copy of https://github.com/vapor/fluent-kit.git resolved at 1.23.2
Creating working copy for https://github.com/vapor/async-kit.git
Working copy of https://github.com/vapor/async-kit.git resolved at 1.11.1
Creating working copy for https://github.com/vapor/postgres-nio.git
Working copy of https://github.com/vapor/postgres-nio.git resolved at 1.9.0
Creating working copy for https://github.com/swift-server/async-http-client.git
Working copy of https://github.com/swift-server/async-http-client.git resolved at 1.9.0
Creating working copy for https://github.com/apple/swift-nio-extras.git
Working copy of https://github.com/apple/swift-nio-extras.git resolved at 1.10.2
Creating working copy for https://github.com/apple/swift-nio-transport-services.git
Working copy of https://github.com/apple/swift-nio-transport-services.git resolved at 1.11.4
Creating working copy for https://github.com/vapor/fluent.git
Working copy of https://github.com/vapor/fluent.git resolved at 4.4.0
Creating working copy for https://github.com/vapor/sql-kit.git
Working copy of https://github.com/vapor/sql-kit.git resolved at 3.16.0
Creating working copy for https://github.com/apple/swift-metrics.git
Working copy of https://github.com/apple/swift-metrics.git resolved at 2.3.0
Creating working copy for https://github.com/swift-server/swift-backtrace.git
Working copy of https://github.com/swift-server/swift-backtrace.git resolved at 1.3.1
Creating working copy for https://github.com/apple/swift-crypto.git
Working copy of https://github.com/apple/swift-crypto.git resolved at 2.0.5
Creating working copy for https://github.com/vapor/console-kit.git
Working copy of https://github.com/vapor/console-kit.git resolved at 4.2.7
Creating working copy for https://github.com/vapor/multipart-kit.git
Working copy of https://github.com/vapor/multipart-kit.git resolved at 4.5.1
Creating working copy for https://github.com/vapor/fluent-postgres-driver.git
Working copy of https://github.com/vapor/fluent-postgres-driver.git resolved at 2.2.6
Creating working copy for https://github.com/vapor/websocket-kit.git
Working copy of https://github.com/vapor/websocket-kit.git resolved at 2.3.1
Building for debugging...
/Users/apple/vapor_learn/2/TILApp/.build/checkouts/postgres-kit/Sources/PostgresKit/PostgresConnectionSource.swift:53:39: warning: 'connect(to:tlsConfiguration:serverHostname:logger:on:)' is deprecated: Use the new connect method that allows you to connect and authenticate in a single step
            return PostgresConnection.connect(
                                      ^
/Users/apple/vapor_learn/2/TILApp/.build/checkouts/postgres-kit/Sources/PostgresKit/PostgresConnectionSource.swift:53:39: note: use 'connect(on:configuration:id:logger:)' instead
            return PostgresConnection.connect(
                                      ^~~~~~~
                                      connect
/Users/apple/vapor_learn/2/TILApp/.build/checkouts/postgres-kit/Sources/PostgresKit/PostgresConnectionSource.swift:60:29: warning: 'authenticate(username:database:password:logger:)' is deprecated: Use the new connect method that allows you to connect and authenticate in a single step
                return conn.authenticate(
                            ^
/Users/apple/vapor_learn/2/TILApp/.build/checkouts/postgres-kit/Sources/PostgresKit/PostgresConnectionSource.swift:60:29: note: use 'connect(on:configuration:id:logger:)' instead
                return conn.authenticate(
                            ^~~~~~~~~~~~
                            connect
/Users/apple/vapor_learn/2/TILApp/.build/checkouts/postgres-kit/Sources/PostgresKit/PostgresConnectionSource.swift:53:39: warning: 'connect(to:tlsConfiguration:serverHostname:logger:on:)' is deprecated: Use the new connect method that allows you to connect and authenticate in a single step
            return PostgresConnection.connect(
                                      ^
/Users/apple/vapor_learn/2/TILApp/.build/checkouts/postgres-kit/Sources/PostgresKit/PostgresConnectionSource.swift:53:39: note: use 'connect(on:configuration:id:logger:)' instead
            return PostgresConnection.connect(
                                      ^~~~~~~
                                      connect
/Users/apple/vapor_learn/2/TILApp/.build/checkouts/postgres-kit/Sources/PostgresKit/PostgresConnectionSource.swift:60:29: warning: 'authenticate(username:database:password:logger:)' is deprecated: Use the new connect method that allows you to connect and authenticate in a single step
                return conn.authenticate(
                            ^
/Users/apple/vapor_learn/2/TILApp/.build/checkouts/postgres-kit/Sources/PostgresKit/PostgresConnectionSource.swift:60:29: note: use 'connect(on:configuration:id:logger:)' instead
                return conn.authenticate(
                            ^~~~~~~~~~~~
                            connect
/Users/apple/vapor_learn/2/TILApp/.build/checkouts/postgres-kit/Sources/PostgresKit/PostgresConnectionSource.swift:53:39: warning: 'connect(to:tlsConfiguration:serverHostname:logger:on:)' is deprecated: Use the new connect method that allows you to connect and authenticate in a single step
            return PostgresConnection.connect(
                                      ^
/Users/apple/vapor_learn/2/TILApp/.build/checkouts/postgres-kit/Sources/PostgresKit/PostgresConnectionSource.swift:53:39: note: use 'connect(on:configuration:id:logger:)' instead
            return PostgresConnection.connect(
                                      ^~~~~~~
                                      connect
/Users/apple/vapor_learn/2/TILApp/.build/checkouts/postgres-kit/Sources/PostgresKit/PostgresConnectionSource.swift:60:29: warning: 'authenticate(username:database:password:logger:)' is deprecated: Use the new connect method that allows you to connect and authenticate in a single step
                return conn.authenticate(
                            ^
/Users/apple/vapor_learn/2/TILApp/.build/checkouts/postgres-kit/Sources/PostgresKit/PostgresConnectionSource.swift:60:29: note: use 'connect(on:configuration:id:logger:)' instead
                return conn.authenticate(
                            ^~~~~~~~~~~~
                            connect
[1956/1956] Linking Run
Build complete! (109.21s)
[ NOTICE ] Server starting on http://127.0.0.1:8080

0x03 我的作品

欢迎体验我的作品之一:小五笔 86 版
学习五笔的好帮手!
App Store 搜索即可~


你可能感兴趣的:(Swift,Vapor,swift,vapor)