全編mac版。Vaporのバージョンは2.0.4
Vaporの始め方やHello World
までの手順は記事がいっぱいあるので割愛します。
サーバーサイドに慣れてないSwift開発者だけど、自分でもできそう!
と思ってもらえるよう記事を書くことにしました。
作ったもの
Qiita Pocketアップデートしました🎉
— hirothings (@hirothings) 2017年10月5日
なんと、月間ランキングが見れるようになりました!
このためにVapor + Herokuで自作APIを作りました😂ぶっちゃけサーバー不安定な気もしますが仏の心で是非使ってみてください!https://t.co/nohLu4qGVc pic.twitter.com/wd0nfqNzly
Qiita Pocketという自作アプリのAPIです。(サーバー安定してます(小声))
仕様は下記です。
- Qiitaの記事ランキングを返す
- periodパラメータ(week/month)で指定した期間ごとのランキングを返す
- tagパラメータ(ex. swift)で指定したタグごとのランキングを返す
- ランキング生成のため、日次バッチ処理でQiita APIから記事を取得する
今までは、QiitaのAPIを使ってデータを表示するだけだったのですが、月間ランキングの生成を公式のAPIだけで賄うのは難しく(検索結果のイイね順ソートがサポートされていないため)
自作のAPIが必要になり、自作しました。
リポジトリはこちら
https://github.com/hirothings/qiita-pocket-API
なぜVapor?
Server-Side-Swiftのフレームワークでは、PerfectとVaporのスター数が拮抗しているのですが、Vaporは
- toolboxというCLIが用意されている
- ドキュメントが読みやすい
- Herokuへのデプロイが可能(環境の構築にお金と時間をかけたくなかった)
- デザインがカッコいい(重要)
ので、Vaporを選択しました。
Xcodeで開発できるのは嬉しい
プロジェクトの生成
vaporプロジェクトの生成は、vaporをインストールして
vapor new <project name> [--template]
で生成できます。
[--template]
で、テンプレートを指定すると、
テンプレートに合わせたルーティング, コントローラ, モデルが揃ったサンプルプロジェクトが生成されるので、それをベースに作ると良いかと思います。
テンプレートにはAPI開発向け、認証向けなど様々あります。
template一覧
今回はAPIなので、
vapor new qiita-pocket-API --template=api
でプロジェクトを生成しました。
Xcode
Xcode projectの生成もコマンド一発です。
これでXcodeでwebアプリケーションの開発ができます。
vapor xcode
なお、パッケージ管理は、Swift Package Managerをサポートしています。
フォルダ構成
vaporがレコメンドするフォルダ構成があります。
https://docs.vapor.codes/2.0/vapor/folder-structure/
前述したtemplateを使ったproject生成している場合は、すでにこのフォルダ構成に準拠しているので、特に気にする必要はありません。
Vaporの主な登場人物
APIの開発で主に使ったパッケージです。
ちなみに全てプリインストールされています。
パッケージ名 | ロール |
---|---|
toolbox | projectの作成やherokuへのビルドなどまとまった便利CLI |
Routing | ルーティング処理を担当 |
Fluent | モデルに関わる処理を担当 (作成、読み取り、更新、削除、クエリ発行) |
HTTP | HTTPリクエスト・レスポンスなど担当 |
Node | JSONデータなどからタイプセーフな型変換を行う |
JSON | JSONのエンコード・デコードを担当。内部で上記のNodeを使っている |
toolbox
CLIです。コマンド一覧は、 vapor help
で見れます。
主に使ったコマンドは下記でした。
build
, run
に関してはXcodeで開発していればcmd + R, cmd + Bでできます。
コマンド | 説明 |
---|---|
new | Vaporアプリの作成時 |
build | アプリのビルド |
run | アプリの起動 |
xcode | Xcode projectの生成・更新 |
heroku | herokuへのデプロイ |
Routing
他のフレームワーク同様、ルーティングファイルにそのまま処理を書くのではなく、
ルーティングごとのアクションをControllerに定義することができます。
ルーティングごとのアクションをまず、Controller側のメソッドで定義します
その際、ResponseRepresentable
プロトコル(文字列やJSON何かしらのレスポンスを返す取り決め)に準拠したメソッドである必要があります。
// ArticleController.swift
func index(_ req: Request) throws -> ResponseRepresentable {
// リクエストパラメータは引数reqから取得できる
print(req.query!["period"]!.string) // "week"
// 中略
return try articles
.sort(Article.likesCount_key, .descending)
.limit(20, offset: 1)
.all()
.makeJSON()
}
ルーティング側は、Controllerのインスタンスを生成し、上記で作成したアクションをハンドラーに渡すだけです。
これだけで、ルーティングが完成します
// Routes.swift
extension Droplet {
func setupRoutes() throws {
let articles = ArticleController(droplet: self)
get("articles", handler: articles.index)
post("articles", handler: articles.create)
}
}
Vaporであらかじめ用意されているアクション名に準拠すれば、Controller側でルーティングを解釈させて、上記のようにルーティング側で1個1個のアクションを指定しない書き方もあります。
詳しくは:
https://docs.vapor.codes/2.0/vapor/controllers/#resources
Fluent
Vaporで開発するにあたって最も重要と言っても過言ではないパッケージです。
Fluentは、モデルの作成、読み取り、更新、削除などサポートしており、
DBの作成やクエリの発行もサポートしています。
ものすごく雑に言うとRealmのように、永続化データへのアクセスを簡単にしてくれるパッケージです。
まず、Modelプロトコルに準拠したModelを定義します。
※Modelが継承しているEntityがClass Only Protocolなので、Structは使えません
final class Article: Model {
var storage: Storage = Storage()
static let idType: IdentifierType = .int
var title: String
var likesCount: Int = 0
init(
title: String,
likesCount: Int
) {
self.title = title
self.likesCount = likesCount
}
テーブルの生成
modelをPreparation protocolに準拠させると、テーブルの作成をアプリをRunしたタイミングで行ってくれます。
extension Article: Preparation {
static func prepare(_ database: Database) throws {
try database.create(self) { (builder: Creator) in
builder.id() // 主キー
builder.string("title") // テーブルのカラム名
builder.int("likes_count") // テーブルのカラム名
}
}
static func revert(_ database: Database) throws {
try database.delete(self)
}
}
テーブル名の指定
デフォルトはmodelの複数形がそのままtable name, collectionの名前になります。
この指定に沿っていない名前の場合、自分で名前をつけることもできます
final class Pet: Model {
static let entity = "pets"
}
ハマったこと
上記でテーブル追加したあとに、Config+Setup.swift
の setupPreparations()
メソッドにModelを追加しないと認識されません
extension Config {
private func setupPreparations() throws {
preparations.append(Article.self)
}
}
modelの保存・更新
保存はRealmのように、Modelのインスタンスを作ってsave
メソッドを呼ぶだけです。
// Qiita APIのJSONデータを元にArticle Modelを生成し、保存
let article = try Article(json: json)
try article.save()
ただ、単純にsaveしていると重複したレコードが生成されるので、
Qiitaの記事のIDをキーに既存レコードを検索して存在していたら、上書きする処理を書きました。
extension Article {
static func save(_ article: Article) throws {
if let existedArticle = try Article.makeQuery().filter(Article.itemID_key == article.itemID).first() {
existedArticle.title = article.title
existedArticle.likesCount = article.likesCount
try existedArticle.save()
}
else {
try article.save()
}
}
}
Relation
Articleには複数のtagが紐づくため1対多のリレーションを実装しました。
手順としては下記です。
- TagのModelを作る
- テーブル生成のprepareメソッド内でTagから親のModelへのリレーションを張る
- 親のIdentify型キーと外部キー制約を張る
extension Tag: Preparation {
static func prepare(_ database: Database) throws {
try database.create(self) { builder in
builder.id()
builder.string(Tag.name_key)
// 親とのリレーション
builder.foreignKey(Tag.articleID_key, references: Article.idKey, on: Article.self)
builder.parent(Article.self)
}
}
}
親側から子のデータの参照は計算型プロパティで定義できます
final class Article: Model {
var tags: Children<Article, Tag> {
return children()
}
}
// let tags = article.tags
参考:
https://docs.vapor.codes/2.0/fluent/relations/
クエリ
DBのクエリ発行はRealmの記法に似てるかなと思いました。
子モデルまで探索してModelのデータを取得するなど色々なことが可能です。
// タグをキーにクエリを発行
private func searchArticles(with tag: String, articles: Query<Article>) throws -> Query<Article> {
return try articles
.makeQuery()
.join(Tag.self) // 子ModelのTagとJoin
.filter(raw: "upper(`tags`.`name`) = upper('\(tag)')")
}
ハマったこと
クエリについては、オペレーターである程度のサポートがされているのですが、
例:try query.filter("age" >= 21)
凝った検索をしたい場合は、生でSQL文を書く必要があります(上記のfilter文など)
SQL力がLV.1くらいしかないので、ここで詰まりました。。
ソート
ソートや検索件数の制限ももちろん可能です。
// イイね順の投稿20件を取得
Article
.makeQuery()
.sort(Article.likesCount_key, .descending)
.limit(20, offset: 1)
.all() // [Article]
参考:
https://docs.vapor.codes/2.0/fluent/query/
SQLite DBを使う
fluentで標準で入っているDBは、
- SQLite in-memory DB (デフォルト)
- SQLite
の2つです。
他にもパッケージを追加することでMySQLなどにも対応可能
デフォルトのin-memory DBは、アプリを止めるとデータが破棄されるので、開発時は便利ですが、ある程度開発が落ち着いたらSQLiteなどに移行します。
SQLiteの移行は下記の作業だけで済みました。
-
fluent.json
で、"driver": "sqlite"
を指定 - sqlite.jsonを作成
- sqlite.jsonにDBのPathを指定
{ "path": "qiita-articleDB.sqlite" }
SQLite in-memory DBとは
プロセスの間だけデータを保持するDB。速度は抜群に速い
http://qiita.com/umisama/items/2014f8f09cee447c313f#sqlite%E3%81%A7in-memory-database
SQLiteは標準サポート
過去記事を見ると、vapor-community/sqlite-providerという追加パッケージを導入していますが、Deprecatedになっています。現在は、プリセットされたパッケージで対応可能です
https://github.com/vapor-community/sqlite-provider
HTTP
シンプルなHTTP Requestなら、HTTPパッケージに用意されているClient
で簡単に実装できます。
下記のコードはQiitaのAPIをコールして、投稿データをArticle型で保存するまでの一連のコードです。
宣言的に書けます。なお、このHTTP通信ですが、内部でリクエストの順序を担保していて直列実行してくれるようです。
// Qiitaの投稿取得APIから100件、投稿データを取得
let response: Response = try drop.client.get("https://qiita.com/api/v2/items", query: [
"page": 1,
"per_page": 100,
"query": "user:hirothings"
], [
"Authorization": "Bearer hogehogehoge"
])
// ルートがarrayのJSONに変換
guard let jsonArray = response.json?.array else {
return
}
// JSONをArticle型に変換し、保存
for json in jsonArray {
let article = try Article(json: json)
try Article.save(article)
try saveEntities(itemID: article.itemID, json: json)
}
参考:
https://docs.vapor.codes/2.0/http/client/
herokuにデプロイ
toolboxにheroku用のコマンドが用意されているので、それを使うだけです
(もちろん、herokuへのユーザー登録は事前に必要です)
vapor heroku init
vapor heroku push
初期化時の設定ですが、自作APIだとこんな感じでした。
Would you like to provide a custom Heroku app name?
y/n> y
Custom app name:
> qiita-pocket-api
Would you like to deploy to a region other than the US?
y/n> n
https://qiita-pocket-api.herokuapp.com/ | https://git.heroku.com/qiita-pocket-api.git
Would you like to provide a custom Heroku buildpack?
y/n> n
Setting buildpack...
Are you using a custom Executable name?
y/n> n
Setting procfile...
Committing procfile...
Would you like to push to Heroku now?
y/n> y
Cron
Vaporには定期タスクを実行するパッケージはないです。
自作APIではHeroku Schedulerを使って、毎日Qiita投稿取得のエンドポイントをコールしてランキング記事を更新しています。
追記:
@mono0926 さんにコメントいただきましたが、VaporにはCommandを追加する機能があるので、それを活用して vapor run COMMAND_NAME
をHeroku Schedulerなどでコールした方がスマートでした。
Vapor Cloud
最近、Version1.0がリリースされたVapor Cloudだと、モニタリングやCronジョブもサポートされているようです。
自分のサイトなりAPIを公開する際、heroku以外の選択肢を公式がサポートするのはVaporを選択する大きなアドバンテージになりそうです。
価格も20000リクエスト/月までなら無料と個人アプリ開発に優しく、導入しやすくなったのではないでしょうか。
実際に使ってみてどうだったか?
タイプセーフ
やはり型があるのは嬉しいです。あと、エラーハンドリングもtry catch文で綺麗に書けるので、堅牢なアプリケーション開発ができるのではないかと思います。
Xcodeで開発できるのもiOS開発者としては嬉しいですね。
やりたいことに対しての敷居は低い
正直SSSは敷居が高いかなと思っていましたが、そんなことはないです。
やりたかったことは一通り標準のパッケージで実現できました。
あと、projectの作成時、あらかじめtemplateが用意されているのが、大きかったです。
慣れない英語でドキュメントを読みながらもtemplateのサンプルコードと照らし合わせて、理解できることが多々ありました。
気になること
と言っても、サーバーサイド言語としてはまだまだ発展途上の状態なので、Railsなどで業務でサーバーサイド開発をしている人の要件を賄えるのか気になります。
Swiftの学習にも向いている
普段rubyなどを書いていて、Swiftを触ってみたいけど、iOSの文脈の勉強が億劫な人にもSSSはオススメです。
herokuないし、Vapor Cloudを使えば無料でwebサイトの公開までできるので、Swiftの初学に使ってみるのはいかがでしょうか
公式ドキュメント
Docs
https://docs.vapor.codes/2.0/
Projects
https://github.com/vapor/vapor/blob/master/Documents/PROJECTS.md
Tutorials
https://github.com/vapor/vapor/blob/master/Documents/TUTORIALS.md