LoginSignup
232

More than 5 years have passed since last update.

IBM製 Swift Webアプリケーションフレームワーク&HTTPサーバ Kitura最速マスター

Last updated at Posted at 2016-02-25

はじめに

Kitura

2016年2月22日(日本時間23日未明)、IBMのクラウドプラットフォームBluemixがSwiftに対応したことを発表しました。加えて、Kituraと呼ばれるSwiftのWebアプリケーションフレームワークがOSSとして公開されました。

IBMによるSwift関連の取り組みはオフィシャルサイトとGithub上のリポジトリで確認することができます。

なぜIBMはSwiftを選ぶのか?

Swift@IBMQuestions 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に依存関係を記述していきます。

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のサンプルコードを書いてみましょう。

main.swift
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_Router.png

Kituraアプリの基本

まず基本的にはKituraRouterKituraNetKituraSysをインポートします。これらはURLのルーティングやWebサーバに必要なライブラリです。次に、Routerのインスタンスに対してルーティング情報とそれに対応する処理などを登録します。

main.swift
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"]
    // ...
}

デバグ用のログを出力する

LoggerAPIHeliumLoggerを使うことで、ログレベルに合わせたログ出力を行うことができます。
Package.swiftを以下のように編集します。

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)),
    ]
)

サンプルコードです。

main.swift
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を使ったテンプレートのレンダリング手順は以下のようになります。

  1. Template(...)でテンプレート作成
  2. Box(...)でテンプレートに変数の反映(埋め込み)
  3. Template.render(...)でHTMLテキスト作成
  4. try! response.status(HttpStatusCode.OK).send(...).end()でHTMLドキュメントを返す

Mustache形式に則った文字列をHTML形式の文字列に変換したあとresponse.status(HttpStatusCode.OK).send()に渡すという手順を踏むことになります。
最初に、Package.swiftの依存関係にGRMustache.swiftを加えましょう。

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/GRMustache.swift.git", majorVersion: 1), 
    ]
)

以下のサンプルコードは、URLの一部をキャプチャして、"Hello, ○○"と返すプログラムです。例えば、http://localhost:8090/lattnerにアクセスした場合、"Hello, lattner!!"というテキストが返ってきます。

main.swift
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"が付加されます。

main.swift
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)
// => &lt;p&gt;Hello&lt;/p&gt; , <p>Hello</p>

条件分岐

条件に応じてマークアップを分岐させます。以下のテキストの場合valuetrueならば{{content}}をマークアップします。

{{#value}}
    {{content}}
{{/value}}

また、valuetrueでない場合も取り扱うには以下のようにします

{{#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を格納します。これを使うことで、レイアウトを適用することができます。ついでに、外部テンプレートファイルを使ったレンダリング方法についても取り上げます。外部テンプレートをファイルを使うことで、ビューとロジックを分離することができます。
まずテンプレートファイルを配置するディレクトリStaticsMyApp以下に作成します。次に、生成するHTMLのレイアウト用テンプレートのlayout.mustacheとコンテンツ用のテンプレートentry.mustacheStatics以下に作成し、以下のコードを記述します。

layout.mustache
{{! コメントです。layout.mustache }}
<html>
<head>
    <title>{{ title }}</title>
</head>
<body>
    {{# content }}
        {{> entry }}
    {{/ content }}
</body>
</html>
entry.mustache
{{! コメントです。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(...)に渡し、ファイル名を指定することでテンプレートを取得しています。

main.swift
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は楽しいです。みなさんもぜひよかったら使ってみてはいかがでしょうか。今回省略してしまったエラー処理を補完したサンプルコードのレポジトリは以下のページにあります。

参考

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
232