Swift:AjaxとVaporの連携(Ubuntu)

最近、Swift の Web Framework の一つである Vapor を始めました。今回、Ajax と Vapor を連携させるプログラムを作成したので紹介します。
どんなプログラムかというと、HTML のinput要素に二つの数値を入力し、それらを Ajax の post でサーバに送信し、Vapor で受け取って、それらの加算を行い、その結果を返すという単純なプログラムです。 単純ですが、これができると色んなことに応用できるんです。
尚、表題には Ajax が入っていますが、今回のサンプルプログラムでは、Ajax(非同期)のメリットはありません。^^

 

Swift と Vapor Toolbox のインストール

OS:Ubuntu 18.04


$ sudo wget -q https://repo.vapor.codes/apt/keyring.gpg -O- | sudo apt-key add -
$ echo "deb https://repo.vapor.codes/apt $(lsb_release -sc) main" | sudo tee /etc/apt/sources.list.d/vapor.list
$ sudo apt update
$ sudo apt install swift vapor

$ swift --version
$ vapor --version

Swift 4.2 と Vapor Toolbox 3.1.10 がインストールされます。

 

Vapor アプリケーションの作成

今回は、Vapor Toolbox を使わずに、Swift に標準搭載されている Swift Package Manager だけで作成します。

〈実行形式のパッケージの作成〉

$ mkdir webapp
$ cd webapp
$ mkdir Public
$ mkdir Resources
$ cd Resources
$ mkdir Views
$ cd ..
$ swift package init --type executable


webapp(パッケージ名)
 ├── Package.swift
 ├── Public
 ├── README.md
 ├── Resources
 │   └── Views
 │       └── ajax.html
 ├── Sources
 │   └── webapp(ターゲット名)
 │       └── main.swift
 └── Tests
     ├── LinuxMain.swift
     └── webappTests
         ├── webappTests.swift
         └── XCTestManifests.swift

緑色のディレクトリやファイル以外は自動で生成されます。今回、Public ディレクトリは使いません。

生成された main.swift と Package.swift を下記のプログラムに書き換えます。

main.swift

import Vapor

var services = Services.default()
services.register(NIOServerConfig.default(hostname:"127.0.0.1", port:8080))
let application = try Application(config:Config.default(), environment:Environment.detect(), services:services)

struct RequestContent:Content {
  var operation:String!
  var input1:Double!
  var input2:Double!
}

struct ResponseContent:Content {
  var output:Double!
}

let router = try application.make(Router.self)

router.get(String.parameter) { request -> Future<View> in
  let requestParameter = try request.parameters.next(String.self)
  return try request.view().render(requestParameter)
}

router.post(RequestContent.self, at:"calculate") { request, requestContent -> ResponseContent in
  var output:Double!
  if requestContent.operation == "add" {
    output = requestContent.input1 + requestContent.input2
  }
  return ResponseContent(output:output)
}

/*
router.post("calculate") { request -> Future<ResponseContent> in
  let futureResponseContent = try request.content.decode(RequestContent.self).map(to:ResponseContent.self) { requestContent -> ResponseContent in
    var output:Double!
    if requestContent.operation == "add" {
      output = requestContent.input1 + requestContent.input2
    }
    return ResponseContent(output:output)
  }
  return futureResponseContent
}
*/

try application.run()
 

上記の router.post に関してですが、コメント文の方は Future パターンで、非同期で処理を実行するので全体として処理が速くなると思います。 今回の場合は大した違いはないですが、重い処理の場合は Future パターンを使ったほうがいいと思います。

 

Package.swift(UTF-8で保存してビルドするとエラーになりました???)

import PackageDescription

let package = Package(
  name:"webapp",
  dependencies:[
    .package(url:"https://github.com/vapor/vapor.git", from:"3.1.0"),
  ],
  targets:[
    .target(name:"webapp", dependencies:["Vapor"]),
  ]
)
 

注)Vapor Framework の最新バージョンは、Vapor releases で確認してください。

 

次に、ajax.html を Resources/Views の直下に置いてください。

   request.view().render("ajax.html")

とすると、Vapor は Resources/Views から ajax.html を読み込もうとするからです。
もちろん、

   request.view().render("/home/tomato/webapp/Resources/Views/ajax.html")

のように絶対パスで指定することもできます。この作業は、ビルドの後でも構いません。

ajax.html(UTF-8で保存してください)

<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>Ajax</title>
</head>
<body>

<div>
  <input id="Input1" type="text"> + <input id="Input2" type="text">
  <input type="button" value="=" onclick="calculate('add', 'Input1', 'Input2', 'Output');">
  <output id="Output"></output>
</div>

<script src="http://code.jquery.com/jquery-3.3.1.min.js"></script>

<script>
function calculate(operation, input1, input2, output) {
  const url = "calculate";
  const request = {};
  request["operation"] = operation;
  request["input1"] = document.getElementById(input1).value;
  request["input2"] = document.getElementById(input2).value;
  const callback = function (response) { document.getElementById(output).innerHTML = response["output"]; };
/* ChromeとFirefoxではアロー関数(arrow function)を使って下記のように書くこともできます。IE11はダメ!
  const callback = response => document.getElementById(output).innerHTML = response["output"];
*/
  $.post(url, request, callback);
}
</script>

</body>
</html>
 

注)jQuery の最新バージョンは、Downloading jQuery で確認してください。
注)$.postAjax の関数(非同期関数)です。 $.postを実行すると、リクエストの送信とコールバック関数の登録を行い、それらが終了すると、コールバック関数の終了を待たずに次の処理に移行します。

 

ファイルの配置は以下の通りです。


  /home/tomato/webapp/Sources/webapp/main.swift
  /home/tomato/webapp/Package.swift
  /home/tomato/webapp/Resources/Views/ajax.html

 

Vapor アプリケーションのビルドと起動

Package.swift のあるディレクトリで以下のコマンドを実行してください。 ビルドコマンドを実行すると、Package.swift に記述されている url から必要なファイルをダウンロードし、ビルドを開始します。

デバッグビルドと起動〉

$ swift package tools-version --set-current
$ swift build
$ .build/debug/webapp

 

〈リリースビルドと起動〉

$ swift package tools-version --set-current
$ swift build -c release
$ .build/release/webapp

 

注意1

上記のコマンド swift package tools-version --set-current は、swift build を実行したとき、


   error: manifest parse error(s):
   error: argument 'targets' must precede argument 'dependencies'
    targets:[

が出た場合の対処方法です。これは、swift package tools のバージョンが現行のものと一致していないことによるエラーなので、swift build を実行して、エラーが出なければ必要ありません。

 

注意2

main.swift があるディレクトリ(/home/tomato/webapp/Sources/webapp)に Package.swift を置くと次のようなエラーが出ます。


   error: no such module 'PackageDescription'
   import PackageDescription

そのときは Package.swift をそのディレクトリから削除してください。

 

クライアント側(ブラウザ)での実行

サーバ側で Vapor アプリケーションを起動したら、ブラウザのアドレスバーに次のように入力してください。


http://127.0.0.1:8080/ajax.html

すると、次のような画面が表示されます。

+

数値を入力し、=ボタンをクリックして結果が表示されたら成功です。

以上は Vapor アプリケーションがローカルにある場合の設定です。リモートサーバにある場合は、main.swift の中の "127.0.0.1" をリモートサーバのIPアドレスに変更してください。

 

補足

ブラウザのアドレスバーに次のように入力した時、


http://127.0.0.1:8080/aaa/bbb/ccc

このリクエストメッセージのリクエストラインは、次のようになります。


GET /aaa/bbb/ccc HTTP/1.1

注)/aaa/bbb/ccc の部分は、RFC 7230 によると、リクエストターゲット(request-target)と呼ぶそうです。以前は、リクエスURIと呼ばれていたものです。

このリクエストターゲット /aaa/bbb/ccc の ccc は、a.html であったり、b.html であったりするので、変数(パラメータ)として受け取れるようにしておくと便利です。 Vapor では次のようにします。


router.get("aaa/bbb", String.parameter) { request -> Future<View> in
  let ccc = try request.parameters.next(String.self)
  ・・・・・・・・・・
}
 

或いは、次のようにしても構いません。


router.get("aaa", "bbb", String.parameter) { request -> Future<View> in
  let ccc = try request.parameters.next(String.self)
  ・・・・・・・・・・
}
 

 

また、次のようなリクエストラインを持つリクエストメッセージを送信するには、


POST /xxx HTTP/1.1

ajax.html の url に、このリクエストターゲットをセットします。これが、$.post の引数(url)になります。


var url = "xxx";
 

このリクエストを Vapor で受け取るには、main.swift の router.post の引数に、このリクエストターゲットを設定してください。


router.post("xxx") { request in
  ··········
}
 

或いは、次のようにしても構いません。


router.post(Abcde.self, at:"xxx") { request, abcde in
  ··········
}
 

 

感想

今時のWebフレームワークを使うと、簡単にWebサーバの機能やアプリケーションサーバの機能を作り込めることがわかりました。驚きです!
これからサーバサイドで Swift の時代がやってきそうですね。

それから、当初は Kitura を試そうと思っていたのですが、うまくいきませんでした。まだ、Ubuntu 18.04 には対応していないようです。 興味のある方は、Server-Side Swift: Kitura vs Vapor をお読みください。 Kitura と Vapor の比較をしています。

 


参考サイト