はじめに
2016年2月22日(日本時間23日未明)、IBMのクラウドプラットフォームBluemixがSwiftに対応したことを発表しました。加えて、Kituraと呼ばれるSwiftのWebアプリケーションフレームワークがOSSとして公開されました。
IBMによるSwift関連の取り組みはオフィシャルサイトとGithub上のリポジトリで確認することができます。
なぜIBMはSwiftを選ぶのか?
Swift@IBMのQuestions and Answersで、What is Swift?という問いに対して、以下のように答えていることから、Swiftを将来性のあるプログラミング言語と捉えて、自社のプロダクトに採用しているのだと思います。
- Swiftは強力かつ簡潔なプログラミング言語
- Swfitを書くのはインタラクティブだし楽しい
- Swiftの文法は簡潔なのに奥深い
- Swiftで書かれたアプリケーションは速い(lightning-fast)
イントロ
環境はMac OS Xを想定しています。以下の紹介項目は、ゆーすけべーさんのMojolicious最速マスターに影響を受けています。
ここで紹介したKituraのサンプルコードはこちらです。紹介できなかったSwiftRedisのサンプルコードもあります。
なお、Kituraの開発は現在進行中なので、以下の紹介する項目は動かなくなってしまうかもしれません。その場合はKituraのレポジトリをご確認ください。
インストールする
Kituraを使うには、以下の2点が必要になります。
- Kituraに必要なライブラリのインストール
- 最新のSwiftコンパイラのインストール
必要なライブラリのインストール
必要なライブラリのインストールにはHomebrewを使います。Homebrewのインストール方法や使い方は以下の記事がわかりやすいと思います。
brew install
コマンドを使って次のライブラリをインストールします。
- http-parser ― HTTPパーサライブラリ
- pcre2 ― Perl互換の正規表現ライブラリ
- curl ― クライアントサイド通信ライブラリ
- hiredis ― Redisクライアントライブラリ
$ brew install http-parser pcre2 curl hiredis
最新のSwiftコンパイラのインストール
最新のSwiftコンパイラをインストールします。以下のサイトにアクセスし、Xcode Swift Development Snapshotからコンパイラをダウンロードします。そして、ダウンロードしたpkgファイルからコンパイラをインストールします。
パスを通して、コンパイラを使えるようにします。.bash_profile
や.zshrc
などのシェル設定ファイルにパスを記述します。source
コマンドで設定を反映させましょう。
export PATH="/Library/Developer/Toolchains/swift-latest.xctoolchain/usr/bin:$PATH"
更新メモ
最新コンパイラではなく、swift-DEVELOPMENT-SNAPSHOT-2016-02-08-a(以下のリンク)を使ったほうがよいみたいです。最新コンパイラ(swift-DEVELOPMENT-SNAPSHOT-2016-02-25-a)ではKituraをビルドできないみたいです。確かに、02-08版と02-25版ではSwift Package Managerの挙動が変わっていてmakeできませんでした。なので、以下のコンパイラを使ったほうがよいかもしれません。
雛形を作る
Swift Package ManagerというSwiftアプリ作成のツールを使います。詳しくはswift build --help
を確認してください。swift build --init
でスケルトンを作成します。
$ mkdir MyApp
$ cd MyApp
$ swift build --init
ファイル構成は以下のようになります。
$ tree -al MyApp/
MyApp/
├── .gitignore
├── Package.swift
├── Sources
│ └── main.swift
└── Tests
プロジェクト関連のファイルはSources
、テスト関連のファイルはTests
以下に作成します。また、プロジェクトの依存関係の記述はPackage.swift
に行います。Kituraを利用するには、このPackage.swift
に依存関係を記述していきます。
import PackageDescription
let package = Package(
name: "MyApp",
dependencies: [
.Package(url: "https://github.com/IBM-Swift/Kitura-router.git", versions: Version(0,2,0)..<Version(0,3,0)),
]
)
上のようにPackage.swift
を記述したあとに、swift build
により依存するライブラリをインストールします。error: exit(1) ...
と表示されますがライブラリのインストールには成功しているようなので、スルーしてよいことに注意してください。
$ swift build
Cloning https://github.com/IBM-Swift/Kitura-router.git
Using version 0.2.0 of package Kitura-router
Cloning https://github.com/IBM-Swift/Kitura-net.git
Using version 0.2.0 of package Kitura-net
(略)
<unknown>:0: error: link command failed with exit code 1 (use -v to see invocation)
<unknown>:0: error: build had 1 command failures
**error:** exit(1): ["/Library/Developer/Toolchains/swift-DEVELOPMENT-SNAPSHOT-2016-02-08-a.xctoolchain/usr/bin/swift-build-tool", "-f", "/Users/akimacho/MyApp/.build/debug/MyApp.o/llbuild.yaml"]
最後にプロジェクトをビルドするためのMakefileをコピーします。
$ cp Packages/Kitura-net-0.3.0/Makefile-client Makefile
以上でKituraを使う準備が完了しました!
立ち上げてみる
実際にWebアプリを立ち上げてみます。MyApp/Sources/main.swift
に、以下のKituraのサンプルコードを書いてみましょう。
import KituraRouter
import KituraNet
import KituraSys
let router = Router()
router.get("/") { request, response, next in
next()
}
let server = HttpServer.listen(8090, delegate: router)
Server.run()
プロジェクトをビルドします。
$ make
Webアプリを実行します。ビルドした実行ファイルはMyApp/.build/debug/MyApp
にあります。
もしも「アプリケーション"MyApp"へのネットワーク受信接続を許可しますか?」というウィンドウが表示された場合、許可ボタンを押しましょう。
$ .build/debug/MyApp
ブラウザで http://localhost:8090/ にアクセスすると、以下のようなページが表示されます。
Kituraアプリの基本
まず基本的にはKituraRouter
、KituraNet
、KituraSys
をインポートします。これらはURLのルーティングやWebサーバに必要なライブラリです。次に、Routerのインスタンスに対してルーティング情報とそれに対応する処理などを登録します。
import KituraRouter
import KituraNet
import KituraSys
let router = Router()
// ルーティング情報の登録
// 他のルーティング情報を登録する場合、"/"の情報は最後に登録します
router.get("/") { request, response, next in
response.setHeader("Content-Type", value: "text/html; charset=utf-8")
do {
try response
.status(HttpStatusCode.OK) // ステータスコードを設定します
.send("<p>Hello, world!</p>") // 文字列を返します
.end() // レスポンスを終了します
}
catch {
// エラー処理
print("Response Error!")
}
// next()を呼び出すことで、次のURLに対応した処理の準備に移ります
next()
}
// ポート番号とルータを設定し、サーバを起動します
let server = HttpServer.listen(8090, delegate: router)
Server.run()
ルーティング
GET/POSTのメソッドで振り分ける
Routerオブジェクトのget()
とpost()
を使います。他にも、put()
やdelete()
が用意されています。
getメソッド
とpostメソッド
により、URLとURLに対応した処理のコールバック(クロージャ)を登録することができます。
let router = Router()
router.get("/entry") { request, response, next in
// ...
}
// 以下のように書いても同じです
router.get("/entry", handler: { request, response, next in
// ...
})
router.post("/entry") { request, response, next in
// ...
}
URLの一部をキャプチャする
/entry/{数字}
の数字部分のようなURLの一部をキャプチャしたい場合は、辞書型のrequest.params
にアクセスします。
router.get("/entry/:id") { request, response, next in
let id = request.params["id"]!
// ...
}
コントローラ
リクエストパラメータ値を取得する
辞書型のrequest.body!.asUrlEncoded()!
を使います。
// リクエストパラメータを取得するためのmiddlewareを指定
router.use("/*", middleware: BodyParser())
router.post("/entry") { request, response, next in
let queryParams = request.body!.asUrlEncoded()!
let title = queryParams["title"]!
let body = queryParams["body"]
// ...
}
デバグ用のログを出力する
LoggerAPI
とHeliumLogger
を使うことで、ログレベルに合わせたログ出力を行うことができます。
Package.swift
を以下のように編集します。
import PackageDescription
let package = Package(
name: "MyApp",
dependencies: [
.Package(url: "https://github.com/IBM-Swift/Kitura-router.git", versions: Version(0,2,0)..<Version(0,3,0)),
.Package(url: "https://github.com/IBM-Swift/HeliumLogger.git", versions: Version(0,2,0)..<Version(0,3,0)),
.Package(url: "https://github.com/IBM-Swift/LoggerAPI.git", versions: Version(0,2,0)..<Version(0,3,0)),
]
)
サンプルコードです。
import KituraSys
import KituraNet
import KituraRouter
import LoggerAPI
import HeliumLogger
let router = Router()
Log.logger = HeliumLogger()// ログ出力用オブジェクトを移譲します
router.get("/") { request, response, next in
Log.debug("This is debug log!")
Log.verbose("This is verbose log!")
Log.info("This is info log!")
Log.warning("This is warning log!")
Log.info("This is error log!")
next()
}
let server = HttpServer.listen(8090, delegate: router)
Server.run()
以下のように出力されました(実際は色付きです)。Log.debug("This is debug log!")
の出力結果は、WARNING
レベル扱いになっていました。移譲先のHeliumLoggerでそのように実装されているようです。
WARNING: initialize() /Users/akimacho/MyApp/Packages/Kitura-router-0.2.0/Sources/KituraRouter/contentType/ContentType.swift line 72 - Loading embedded MIME types.
VERBOSE: init() /Users/akimacho/MyApp/Packages/Kitura-router-0.2.0/Sources/KituraRouter/Router.swift line 43 - Router initialized
INFO: spiListen(_:port:) /Users/akimacho/MyApp/Packages/Kitura-net-0.2.0/Sources/KituraNet/HttpServerSpi.swift line 46 - Listening on port 8090
INFO: spiListen(_:port:) /Users/akimacho/MyApp/Packages/Kitura-net-0.2.0/Sources/KituraNet/HttpServerSpi.swift line 51 - Accepted connection from: 127.0.0.1on port 36082
VERBOSE: MyApp /Users/akimacho/MyApp/Sources/main.swift line 9 - This is verbose log!
WARNING: MyApp /Users/akimacho/MyApp/Sources/main.swift line 10 - This is debug log!
INFO: MyApp /Users/akimacho/MyApp/Sources/main.swift line 11 - This is info log!
WARNING: MyApp /Users/akimacho/MyApp/Sources/main.swift line 12 - This is warning log!
ERROR: MyApp /Users/akimacho/MyApp/Sources/main.swift line 13 - This is error log!
INFO: spiListen(_:port:) /Users/akimacho/MyApp/Packages/Kitura-net-0.2.0/Sources/
テンプレートをレンダリングする
後ほど説明しますが、Kitura(のサンプルコード)では、Mustache形式のテンプレートエンジンGRMustache.swiftを採用しています。Mustacheをはじめて聞く方もいらっしゃるのではないでしょうか?私はKituraを通してはじめて知りました。一般的なテンプレートエンジン(例えば、erbでしょうか)とは違いMustacheはLogic-less templatesと呼ばれていて一風変わっていますが、慣れると使いやすくなっていくと思います。
さて、GRMustache.swiftを使ったテンプレートのレンダリング手順は以下のようになります。
-
Template(...)
でテンプレート作成 -
Box(...)
でテンプレートに変数の反映(埋め込み) -
Template.render(...)
でHTMLテキスト作成 -
try! response.status(HttpStatusCode.OK).send(...).end()
でHTMLドキュメントを返す
Mustache形式に則った文字列をHTML形式の文字列に変換したあとresponse.status(HttpStatusCode.OK).send()
に渡すという手順を踏むことになります。
最初に、Package.swift
の依存関係にGRMustache.swiftを加えましょう。
import PackageDescription
let package = Package(
name: "MyApp",
dependencies: [
.Package(url: "https://github.com/IBM-Swift/Kitura-router.git", versions: Version(0,2,0)..<Version(0,3,0)),
.Package(url: "https://github.com/IBM-Swift/GRMustache.swift.git", majorVersion: 1),
]
)
以下のサンプルコードは、URLの一部をキャプチャして、"Hello, ○○"と返すプログラムです。例えば、http://localhost:8090/lattner
にアクセスした場合、"Hello, lattner!!"というテキストが返ってきます。
import KituraRouter
import KituraNet
import KituraSys
import Mustache
let router = Router()
router.get("/:name") { request, response, next in
let name = request.params["name"]!
// テンプレート作成
let template = try! Template(string: "Hello, {{name}}!!")
let data = ["name" : name]
// 変数の反映
let boxedData = try! Box(data)
// HTMLテキスト作成
let html = try! template.render(boxedData)
try! response.status(HttpStatusCode.OK).send(html).end()
next()
}
let server = HttpServer.listen(8090, delegate: router)
Server.run()
$ curl http://localhost:8090/rfdickerson
Hello, rfdickerson!!
JSONを出力する
オブジェクトをJSON形式にエンコードするSwiftyJSON
はデフォルトでインストールされているので、これをインポートするだけでSwiftyJSON
を使うことができます。エンコードした得られた文字列をresponse.status(HttpStatusCode.OK).sendJson(json)
で返します。sendJson(...)
を使うことで、自動的にレスポンスヘッダに"Content-Type: application/json; charset=utf-8"が付加されます。
import KituraRouter
import KituraNet
import KituraSys
import SwiftyJSON
let router = Router()
router.get("/") { request, response, next in
let entries = [
["title":"apple", "content":"swift is a programming language"],
["title":"Ringo Starr", "content":"Ringo Starr is a drummer"],
]
// JSON形式にエンコードする
let json = JSON(entries)
// Content-Type: application/json; charset=utf-8として返す
try! response.status(HttpStatusCode.OK).sendJson(json).end()
next()
}
let server = HttpServer.listen(8090, delegate: router)
Server.run()
$ curl -v http://localhost:8090/
(略)
< HTTP/1.1 200 OK
< Content-Type: application/json
< Content-Length: 165
< Date: Tue, 23 Feb 2016 13:23:59 GMT
<
(略)
$ curl http://localhost:8090/
[
{
"title" : "apple",
"content" : "swift is a programming language"
},
{
"title" : "Ringo Starr",
"content" : "Ringo Starr is a drummer"
}
]
リダレイクトさせる
response.redirect()
を使います。パスだけだと同一アプリ内、URLだとそのURLへリダイレクトされます。
router.get("/dummy") { request, response, next in
try! response.redirect("/")
next()
}
router.get("/example") { request, response, next in
try! response.redirect("http://example.com/")
next()
}
テンプレート
ここでは、テンプレートGRMustacheの構文について取り上げます。より詳しく情報はGRMustache.swiftのリポジトリを確認してください。
変数の展開
単純に変数を展開します。{{value}}
の場合はHTMLエスケープ、{{{value}}}
の場合はHTMLエスケープを行いません。
let template = try! Template(string: "{{text}} , {{{text}}}")
let data = ["text" : "<p>Hello</p>"]
let boxedData = Box(data)
let html = try! template.render(boxedData)
// => <p>Hello</p> , <p>Hello</p>
条件分岐
条件に応じてマークアップを分岐させます。以下のテキストの場合value
がtrueならば{{content}}
をマークアップします。
{{#value}}
{{content}}
{{/value}}
また、value
がtrueでない場合も取り扱うには以下のようにします
{{#value}}
{{content}}
{{/value}}
{{^value}}
{{otherwise}}
{{/value}}
let text = "{{title}}" + "{{#showLink}} {{link}} {{/showLink}}"
let template = try! Template(string: text)
let data1 = ["title" : "Hoge", "link" : "http://example.com", "showLink" : true]
let data2 = ["title" : "Foo", "link" : "http://example.jp", "showLink" : false]
let boxedData1 = Box(data1)
let boxedData2 = Box(data2)
let html1 = try! template.render(boxedData1) //=> Hoge http://example.com
let html2 = try! template.render(boxedData2) // => Foo
テンプレートにおいてfalse扱いされるオブジェクトは以下の通りです。
- 値が格納されていない
- false
- 0
- 空文字("")
- 空配列([])、空辞書([:])
- NSNull
辞書の展開
{{#value}} ... {{/value}}
のvalueが辞書に対するキーになっている場合、配列の要素を順番にアクセスしていきます。
let text = "{{#book}}"
+ "{{author}}, {{date}}, {{title}}"
+ "{{/book}}"
let template = try! Template(string: text)
let data = [
"book" : [
"title" : "C Programming Language",
"author" : "Brian W. Ritchie, Dennis Kernighan",
"date" : "1988/3/22",
]
]
let boxedData = Box(data)
let html = try! template.render(boxedData)
//=> Brian W. Ritchie, Dennis Kernighan, 1988/3/22, C Programming Language
配列の展開
また、{{#value}} ... {{/value}}
のvalueが配列に対するキーになっている場合、配列の要素を順番にアクセスしていきます。
let text = "<ul>\n{{#links }}"
+ "<li><a href=\"{{link}}\">{{title}}</a></li>\n"
+ "{{/links }}</ul>\n"
let template = try! Template(string: text)
let data = [
"links" : [
["title": "IBM", "link" : "http://ibm.com" ],
["title": "Github", "link" : "https://github.com"],
["title": "Qiita", "link" : "http://qiita.com"],
]
]
let boxedData = Box(data)
let html = try! template.render(boxedData)
/*=>
<ul>
<li><a href="http://ibm.com">IBM</a></li>
<li><a href="https://github.com">Github</a></li>
<li><a href="http://qiita.com">Qiita</a></li>
</ul>
*/
レイアウトの適応、外部テンプレートファイルの使用
{{> partial }}
は、テンプレート内にテンプレートpartialを格納します。これを使うことで、レイアウトを適用することができます。ついでに、外部テンプレートファイルを使ったレンダリング方法についても取り上げます。外部テンプレートをファイルを使うことで、ビューとロジックを分離することができます。
まずテンプレートファイルを配置するディレクトリStatics
をMyApp
以下に作成します。次に、生成するHTMLのレイアウト用テンプレートのlayout.mustache
とコンテンツ用のテンプレートentry.mustache
をStatics
以下に作成し、以下のコードを記述します。
{{! コメントです。layout.mustache }}
<html>
<head>
<title>{{ title }}</title>
</head>
<body>
{{# content }}
{{> entry }}
{{/ content }}
</body>
</html>
{{! コメントです。entry.mustache }}
<h1>{{{ header }}}</h1>
<p>{{{ text }}}</p>
以上の作業を通して、プロジェクトのファイル構成は以下のようになっていることを想定しています。
MyApp/
├── Makefile
├── Package.swift
├── Packages
│ ├── BlueSocket-0.0.4
│ ├── GRMustache.swift-1.0.1
│ ├── (略)
│ └── SwiftyJSON-2.3.3
├── Sources
│ └── main.swift
├── Statics
│ ├── entry.mustache
│ └── layout.mustache
└── Tests
実際にレンダリングさせてみましょう。以下のサンプルコードでは、テンプレートファイルが配置されているディレクトリのパスをTemplateRepository(...)
に渡し、ファイル名を指定することでテンプレートを取得しています。
import KituraRouter
import KituraNet
import KituraSys
import Mustache
import Foundation // NSFileManager
// Webアプリのカレントディレクトリを取得します
// サンプルコードの場合、basePathは $HOME/MyApp
let basePath= NSFileManager.defaultManager().currentDirectoryPath
// テンプレートファイルが配置されているディレクトリのパスを指定する
let repository = TemplateRepository(directoryPath: basePath + "/Statics")
let router = Router()
router.get("/") { request, response, next in
// Staticsディレクトリ内のlayout.mustacheのテンプレートを取得する
let template = try! repository.template(named: "layout")
let data = [
"title" : "KItura",
"content": [
"header": "Web application framework Kitura",
"text": "Build end-to-end apps using Swift with Kitura",
]
]
let html = try! template.render(Box(data))
response.status(HttpStatusCode.OK).send(html)
next()
}
let server = HttpServer.listen(8090, delegate: router)
Server.run()
http://localhost:8090/
にブラウザでアクセスすると以下のように表示されました。
その他
セッションを使う
セッションに該当する機能は見つけられませんでした。
ステータスコードを設定する
// 201を返す
response.status(HttpStatusCode.OK).send(html).end()
//
try! response.status(HttpStatusCode. NOT_FOUND).send("Not Found").end()
HTTPリクエストヘッダを取得する
HTTPヘッダの情報は、辞書型変数のrequest.headers
内に格納されています。以下のサンプルコードでは、クライアントのUser-Agentを取得するプログラムです。
router.get("/") { request, response, next in
let userAgent = request.headers["User-Agent"]!
print(userAgent)
next()
}
let server = HttpServer.listen(8090, delegate: router)
Server.run()
curl http://localhost:8090/
でアクセスした結果、curl/7.43.0
と出力されました。
HTTPレスポンスヘッダを設定する
// レスポンスヘッダを Content-type: text/html; charset=utf-8 とする
response.setHeader("Content-Type", value: "text/plain; charset=utf-8")
// IE対策
response.setHeader("X-Content-Type-Options", value: "nosniff")
チュートリアル
Kituraを使ったTODOリストアプリ作成
おわりに
Swiftのようなモダンな型システムを持った言語でのWebアプリ作りははじめてだったので、Kituraは楽しいです。みなさんもぜひよかったら使ってみてはいかがでしょうか。今回省略してしまったエラー処理を補完したサンプルコードのレポジトリは以下のページにあります。