はじめに
この記事はGenie.jlというWebアプリケーションフレームワークとHerokuというクラウドサービスを使って、シンプルなWebアプリを簡単に無料で公開してみようという試みの記事である。
もともとは Julia言語で入門するプログラミング でCUIアプリとして作成していたゲームプログラムについて、GUIに凝りたくなった結果、Webアプリにしようと思い立って書き始めたものである。タイトルも「Julia言語で入門するプログラミング Webアプリ編(その1)」にする予定だった。しかし、ゲームの要素を入れる以前にWebアプリケーションを作成・公開する内容を説明するだけで単独の記事になるほどのボリュームとなったので、表題のようなタイトルで公開することにしたのだ。そのため、特に Julia言語で入門するプログラミング の知識を想定してはない。この記事単独で読み進めていただけると思う。
なぜGenie.jlか
それはJuliaだからだ。Juliaの何がいいかについてはこのサイトで何度も語っているが、あえてもう一度書いておくことにしよう。(以下の内容は、Julia言語で入門するプログラミング(その1) から丸ごと引用してきている)
Juliaはインストールが簡単だ
何と言ってもこれだ。独学者にとって、これはとても大事なことだ。入門者であれば尚更だ。環境構築でつまづくと何もかも嫌になってしまうものだ。Juliaはインストールが簡単なので、間違えようがないのだ。
Juliaは親しみやすい言語だ
Juliaは書いていて楽しい。とても楽しい。最小限の指示で、期待通りのことをこなしてくれる。快適さはあらゆる言語の中でも最高レベルだ。
Juliaは強力な言語だ
親しみやすいだけでなく、Juliaは非常に強力だ。贔屓目なしに、現存するプログラミングでも屈指の強力な機能を備えている。初心者向けのオモチャの言語ではないのだ。
Juliaはシンプルな言語だ
親しみやすく強力な言語でありながら、Juliaの提供している機能は驚くほどシンプルだ。余計なものが何一つなく、研ぎ澄まされている。
Juliaは高速な言語だ
ここまでのメリットがありながら、そのうえJuliaは高速に動作する。その速度はあらゆる言語の中でもトップクラスに位置する。親しみやすい言語は、しばしば速度が遅いのが欠点だ。これらは従来はトレードオフだと考えられてきたが、Juliaはそれを覆した。
要するに、Juliaにはデメリットらしきものが何もないのである。唯一の心配は、Juliaが快適すぎて他の言語を学ぼうという意欲が失せないかどうかということくらいだ。
(引用ここまで)
もちろん、単にJulia製だからGenie.jlを使うというわけではない。Genie.jl自体、とても使いやすいフレームワークだ。私はそう信じたからこそ、何十時間もかけてこの文章を書いたのだ。
なぜWebか
GUIにこだわるにしろ、Webでなければならないと言う理由はない。JuliaにはいくつかGUIのツールキットがある。それら使っても素敵なゲームアプリを作ることができただだろう。しかし、あえてWebアプリにしたのには理由がある。
Webアプリはとってもメジャーだ
なんと言ってもこれだ。今の世の中、何でもかんでもWebだ。一昔前はそうでもなかった。アプリはインストールするのが当たり前だった。しかし、ネットワークの発達により全てが変わってしまった。ユーザーはアプリを使うときに、もはやCDを買ってきたり、重たいインストーラをダウンロードに半日を費やす必要はない。僕のパソコンはマッキントッシュだから、と涙を飲む必要もない。ただWebブラウザでページにアクセスするだけで、アプリを利用することができるのだ。この流れは止まりそうもない。Webアプリが提供するビジネス上の価値は計り知れないのだ。
かくして、企業で何かユーザー向けのアプリ開発するときには、Webアプリで提供するというのは、ほとんどデフォルトの選択肢になりつつある。今や最も汎用的なGUIフレームワーク、それがHTMLなのだ。
Webアプリにデメリットはないの?
もちろんある。Webアプリはビジネス上の価値は凄いが、エンジニアリングとしては面倒な事だらけだ。ユーザーにアプリケーションをインストールしてもらっていた頃には考える必要のなかった面倒事に左右される。当たり前といえば当たり前だが、Webアプリを公開するためのサーバーを用意する必要があり、それらは通常24時間の稼働が求められる。ユーザーがF5アタックを仕掛けてきてサービスダウンするかもしれないし、DNSサーバーの不調でサービスにアクセスできなくなるかもしれない。私のように個人的な趣味のアプリケーションで、本質的にはネットワーク通信が不要なのにWebアプリにするのは通常は愚かな選択だ。だがしかし、最近は良いサービスができてアプリの公開が簡単になったし、どうせ趣味のサイトなので、仮に夜中にサービスダウンしていたところで飛び起きて対処しなければならないようなものでもない。そんなわけで前述のメリットと天秤にかけた結果、Webアプリにすることにした。
アーキテクチャ
さて、前置きはこのくらいにして、そろそろ先に進もう。まず今回想定しているアプリケーションの全体構成を提示しておきたい。
フロントエンド
ユーザーが対面するのはWebブラウザだ。ユーザーは一心不乱に、HTMLとCSSでWebブラウザに描画された画面をクリックしたりタップしたりする。そうするとJavaScriptが動いたり動かなかったりして、バックエンドのサーバーにリクエストが渡される。サーバー側で然るべき処理が流れたのちに、答えが返ってくるので描画する。これがフロントエンドの全てだ。
バックエンド
フロントエンドからのリクエストを処理するのは、我らがJulia謹製のWebフレームワークのGenie.jl君だ。Genie.jl君は、Webサーバーを構築するためのツールだ。Webサーバーは、HTTPリクエストを受け取り、リクエストに応じた何らかのJuliaコードを実行して、結果をフロントエンドに戻す。ここで言う「何らかのJuliaコード」と言うのがこれまで我々が手塩にかけて育ててきたJuliaプログラムである。これがバックエンドの全てだ。
その他
バックエンドのプログラムを用意しただけではダメだ。それを動かすサーバー[1] … Continue readingが必要だし、それだけでフロントエンドからアクセスできるわけではない。フロントエンドのリクエストをバックエンドのサーバーに届けるための機構が必要になる。例えばURLだ。あなたがサーバーを購入してWebアプリを動かしたとして、クライアントはどうやってそこにアクセスすればいいのか?URLってどうやって取得すればいいんだっけ?そのURLに届いたリクエストを、どうしたら自分のサーバーに誘導できるのだっけ?そもそもサーバー買わなきゃいけないのかな?お金ないんだけど。このような面倒なことの一手に引き受けてくれるのが、「Heroku」というサービスだ。これがその他の全てだ。
どうだっただろうか?あまりに難解な専門用語の羅列にクラクラしてしまったかもしれない。雲を掴むような話で不安になったかもしれないが、これから具体例を示していくので安心してほしい。
フロントエンドにフレームワークは使わないのか?という声が聞こえてきそうである。例えば、Reactなどである。当面はその予定はない。私が作るHTMLやCSSやJavaScriptはとてもシンプルなので、フレームワークの導入や習得のコストの方が高くつく。それに、フレームワークを導入すると、どうしてもそのお作法に則る必要がある。縛られると言ってもいいかもしれない。そんなのは嫌だ。私はいつだって自由な小鳥でありたいのだ。
構築前のお勉強
いきなり実際のものづくりに入る前に、HTML、CSS、JavaScript、Genie、Herokuの簡単な勉強をしておこう。とはいえ、各セクションで細かい話を始めると相当なボリュームになってしまう。ここでは最低限の説明に留めておく。
HTML+CSS
HTMLというのはWebページを表現するためのドキュメント形式である。HTMLの文書の特徴は、文章が「タグ」で囲まれていることである。HTMLではタグで構造を表現する。"<>"
と"</>"
で囲むのだ。例えば、タイトルが「特売商品のお知らせ」、本文が「バナナ一本2500円!!」という文書は次のようになる。
<title>特売商品のお知らせ</title>
<body>バナナ一本2500円!!</body>
このHTMLは甚だ不完全で、場合によっては文字化けする可能性もあるが、適当な名前で拡張子を.htmlにして保存し、Webブラウザで開くと、おそらく正しく開くことができる。
HTMLのタグはいくつか種類がある。通常の文書を書くのであれば、見出しを意味する<h>タグや、改行を意味する<br>タグ、段落を意味する<p>タグ、表を意味する<table>タグなどが重要になってくるだろう。しかし、今回のコースでは、それらはあまり登場しない。活躍するのは、まとまりを表現する<div>タグや画像を表示する<img>タグなどである。
<div>タグというのは、それ自体では特に意味を持たないグルーピングのためのタグである。では何に使うのかというと、CSSと組み合わせられることで威力を発揮するのだ。CSSというのはスタイルシートと呼ばれるもので、HTMLで指定されたタグの見栄えを変更することができる。例えば、HTMLではコンテンツを縦に並べていくことしかできないのだが、CSSを使えば横に並べることができる。
早速やってみよう。
例えば、1行目に1つ、2行目に2つ、3行目に3つのブロックが表示されるようにしたいと思うとする。cssなしのHTMLでまずはこう書いてみる。これは”sample1.html”という名前で保存することにしよう。
<html>
<head>
</head>
<body>
<div class="row">
<div class="cell">
1行目の1
</div>
</div>
<div class="row">
<div class="cell">
2行目の1
</div>
<div class="cell">
2行目の2
</div>
</div>
<div class="row">
<div class="cell">
3行目の1
</div>
<div class="cell">
3行目の2
</div>
<div class="cell">
3行目の3
</div>
</div>
</body>
</html>
<div class="row">
と<div class="cell">
というものがあある。classというのはHTMLタグに付与できる属性で、この属性を指定してCSSにレイアウトの指示を与えることになる。”row”とか”cell”とかいう名前は、私が勝手につけたもので、基本的には好きな名前をつけることができる。今のHTMLにはCSSの指定がないので、この状態のHTMLではdivタグは無意味だ。開いてみると、次のように単に縦に並んでいるだけだ。
ここからがCSSの出番だ。CSSで、「”row”という名前のクラスのタグは横並びにするようにせよ」と命じるには、次のようにする。
まず、下記の内容を、”sample1.css”という名前のファイルに保存する。
.row{
display: flex;
}
次に、このCSSファイルを、先ほどのHTMLファイルが参照するように指定する。
<head>
<link rel="stylesheet" type="text/css" href="sample1.css"> <!--追加-->
</head>
こうすると、ブラウザ上の表示が次のようになる。
横並びになったのはいいが、隙間がなくて見づらいので、隙間を定義しよう。次のような内容を、”sample1.css”に追記すると、”cell”に対して、上下左右に5ピクセルの隙間を持つようになる。
.cell{
margin: 5px;
}
もう少し続けよう。”row”とか”cell”と名付けた領域自体を可視化してみよう。まず、”cell”と名付けた領域には枠線を追加してみよう。枠線の追加は、”border”という指定で可能だ。
.cell{
margin: 5px;
border: solid; /*追加*/
}
まあそんなものかな、と言った感じだ。次に”row”と名付けた領域に背景色をつけてみよう。背景色の追加は、”background-color”という指定で可能だ。
.row{
display: flex;
background-color: lightblue; /*追加*/
}
うーん?全ての”row”が同じ色になるのでわかりづらい。各”row”で色を変えてみよう。とはいえ、クラスを全て別々にしてしまうのは勿体無い。HTMLタグにはclassとは別にidという属性を与えることができる。
<body>
<div class="row" id="No1"> <!--変更-->
...
</div>
<div class="row" id="No2"> <!--変更-->
...
</div>
<div class="row" id="No3"> <!--変更-->
...
</div>
</body>
そのうえで、CSSで次のようにすることで、”row”クラスの”id”別の指定を行うことができる。
.row{
display: flex; /*背景色の指定なし*/
}
.row#No1{
background-color: lightblue;
}
.row#No2{
background-color: lightgreen;
}
.row#No3{
background-color: lightpink;
}
こうすると、次のようになる。
なるほど、rowは横幅いっぱいに広がっていることが確認できた。rowの横幅を500ピクセルに設定してみよう。
.row{
display: flex;
width: 500px; /*追加*/
}
もちろんid別に横幅を指定することもできる。
なんとなく要領がつかめてきたと思うので、ひとまずの説明はこのくらいに留めておこう。覚えておいてほしいのは、HTMLでは基本的にボックスと呼ばれる長方形の領域で確保されると言うことである。まずは実現したい画面をボックスに分解して考えてみること。ボックスの位置や大きさなどを決めるのはCSSであると言うことである。ボックスは重ね合わせたりすることもできるので、相当複雑なレイアウトでなければ実現できるはずだ。
もう少しきちんとHTMLとCSSについて学びたい方におすすめなのはProgateというサイトだ。それなりに複雑な画面を段階的に構築していくのだが、都度都度で学んだ内容を実際に手で入力していく演習課題があるのがいいところだ。サラッとよんでわかったつもりのところが即座にエラーとして表示されるため、間違いにすぐに気づくことができるのだ。こんなことを書くと私がProgateの回し者のようであるが、誓ってそのようなことはない。どのくらい回しものではないかというと、無料で試せる初級編だけでとりあえずは十分だと思いますよ、といってしまうくらいには回し者ではない。
なお、HTMLやCSSでどのタグを指定すれば何ができるか、と言う情報は調べればすぐに出てくるのだが、何かをしたい時にどのようなHTMLやCSSを指定すれば良いのか、と言う情報は少し調べづらい。このような情報を集めた本が、逆引き集というようなタイトルで売られていたりするので、1冊くらいは持っていてもいいと思う。私は 「今すぐ使えるかんたんEx HTML&CSS 逆引き事典」という本をを持っている。これ一冊しか持っていないので他の本と比べて良いのか悪いのか相対評価はできないが、絶対評価としてはわかりやすくて便利だと思う。
Genie.jl
HTML+CSSときたらお次はJavaScript、と言いたいところだが、JavaScriptの前に、Genie.jlについて説明しよう。最小構成のWebアプリケーションを作るのに、JavaScriptは必要ないからだ。
Genie.jlというのはJulia言語製のWebフレームワークだ。このフレームワークを使うことで、とっても簡単にWebアプリをつくることができるのだ。
早速Genie.jlを試してみよう。Juliaはインストールしているだろうか?まだの人は、Julia言語で入門するプログラミング(その1) を読んで、Juliaをインストールしておこう。
Genie.jlはJuliaのパッケージとして提供されている。JuliaのREPLを起動し、”]”キーを押してみよう。すると、"(@v1.7) pkg>"
というような表記に変わり("@v1.7"
という部分はJuliaのバージョンによって変わる)、Juliaの「パッケージモード」に入ることができる。
(@v1.7) pkg>
ここで、”add Genie”と入力すると、Genie.jlがインストールされ、Genie.jlパッケージを利用できるようになる。
(@v1.7) pkg> add Genie
Genieをインストールできたら、アプリケーションを作ってみよう。今回は”genieexample”という名前で作ってみることにする。
適当なフォルダに移動し、Julia REPLに次のように打ち込もう。
julia> using Genie
julia> Genie.newapp("genieexample")
しばらく待つと、REPL上に、次のような通知が現れる。
“Web Server starting at http://127.0.0.1:8000″と出ている。試しにこのURLにアクセスしてみよう。ブラウザに、”http://127.0.0.1:8000″入力すると、見事次のような可愛らしいキャラクターが歓迎してくれる。つまりアプリケーションの作成に成功したということだ。
ちなみに、REPLを終了するとWebサーバーも終了するためアクセスできなくなってしまう。再度立ち上げるには、binフォルダにあるserverやserver.batを実行すると、再度Webサーバーが立ち上がる。あるいは、replやrepl.batというファイルを実行すると、Webサーバーが初期化された状態でreplが立ち上がるので、up()
という関数を呼び出してもWebサーバーが起動する。
IPアドレスとポート番号
IPアドレス
先に進む前に、一度立ち止まって何が起きたかを確認しておこう。しばらくアプリケーションを作る上では直接は知る必要のない基礎知識の話が続くので退屈だったら飛ばしても良いが、できれば知っておいた方がいい。
まず、127.0.0.1というのは自分自身のことを指し示す特別なIPアドレスである。IPアドレスというのは、ネットワークの世界における住所のようなものだ。IPアドレスを指定することで、ネットワーク上の機器を特定することができる。当然、複数の機器が重複したIPアドレスを持たないように、どこかの誰かが責任を持って管理してくれている。その信頼できるIPアドレスを頼りにネットワーク機器は通信を行なっているのだ。
あなたはパソコンかスマートフォンかわからないが、とにかく何らかの機器を用いてネットワーク経由でこのサイトの文章を読んでいるはずだ。(念力を使っているという人がいたら会ってみたいのでお問合せページから連絡してほしい。) このサイトの文章を手元の機器で読めているということは、次の2つのことが成り立っている。
- このページを表示してほしいと私のサイトの文章が置いてあるサーバーにリクエストをしていること
- この素敵なコンテンツを読みたまへ、とそのサーバーからのレスポンスを受け取っていること
この双方向通信を成り立たせるために、互いのIPアドレスを知っている必要があるのだ。
1についてはDNSという仕組みが働いている。DNSというのはドメインネットワークシステムの略称である。あなたがWebブラウザのURL欄に”https://muuumin.net”と書いたとする。”http(s)://”の部分は、「HTTP(S)通信を行いますよ」という意味だ。次の”muuumin.net”というのが、通信先の「ドメイン名」と言われる部分だ。詳細は割愛するが、DNSサーバーというものがネットワークのどこかにあり、そこに「”muuumin.net”というサイトのIPアドレスはここですよ」という対応表が置かれている。ブラウザはURLからドメインの情報を抜き出し、DNSサーバーに問い合わせてIPアドレスの情報を得る。私はこのサイトを構築する時、レンタルサーバーの業者とホスティング契約を行い、同時に”muuumin.net”というドメイン名取得した。これにより幾許かの金銭と引き換えに、私にドメイン名とIPアドレスが付与され、同時にDNSにそのような情報が書き込まれたのだ。[2] … Continue reading
2については簡単だ。リクエストの際に、差出人のIPアドレスはこれこれでござると書いているので、応答の際にはそれを読み取って使えばいい。
ポート番号
次に”http://127.0.0.1:8000″の末尾の”:8000″に移ろう。この”:8000″はポート番号と呼ばれる。ポート番号とは何に必要なのかと言うと、そのマシンで動いているアプリケーションのうち、どのアプリケーションと通信をしたいかを指定するために使われることが多い。先ほどGenieで作ったアプリケーションを手元のマシンで起動したと思うが、そのアプリケーションと通信するためには、当然マシンの情報だけでは足りない。そのマシンでは他にも無数のアプリケーションが動いるからだ。
IPアドレスは住所に例えられることが多いが、その例えで言うとポート番号は家の各部屋についた窓に相当する。先ほどGenieでつくったアプリケーションは、起動時に8000番の番号が振られた窓の部屋に入っていったというイメージだ。そして、外部からそのアプリと通信するには、8000番の窓に声をかけてあげれば良い。
ポート番号は通常はあまり意識しないことが多いが、これはよく使われる通信に使われるポートが決められているからだ。例えばHTTP通信なら80番のポート、HTTPS通信なら443番のポート、SMTPサーバーなら25番のポート、というようにだ。”https://muuumin.net”とアクセスした際、自動的に”https://muuumin.net:443″と変換され、サーバー側の443番ポートでは、該当するアプリケーション(おそらくWebサーバー)が待ち構えていると言うわけだ。試しに“https://muuumin.net:443″とアドレスバーに打ち込むと、ポート番号なしでアクセスしたのと同じページが表示されるはずだ。
サーバー側にポート番号が必要なら、クライアント側にもポート番号が必要だ。サーバーはクライアントに向けてレスポンスを変えすが、その時にどのアプリケーションに返すかと言う情報がやはり必要なためだ。ブラウザは複数起動されている可能性があるし、HTTP(S)通信を行なっているのがブラウザだけとも限らない。いや、アプリケーションどころか、今時のブラウザは複数のタブを起動できるのが普通なので、それぞれのタブに応じてポート番号が割り振られる必要がある。タブを開くたびに新しくポート番号が割り当てられ、そのタブにどこかのサイト宛てのURLが入力される。そのサイトにクライアント側のIPアドレスとポート番号を付与したリクエストが送られ、そのIPアドレスとポート番号宛てにレスポンスが返され、無事そのタブの情報が更新される。
このようにIPアドレスとポート番号の組み合わせで通信が成り立っているのだ。
ここまで見てわかるように、ポート番号には2つの種類のものがある。1つはWebサーバーやメールサーバーなどリクエストの受け側で持つ必要があるポート番号で、これは皆で認識合わせをして同じ番号を使った方が良い。私のサイトは100番、僕のサイトは200番、拙者のサイトは末広がりの8888番で候、などとやっていては大変だ。ここは皆で同じ番号を使うべきだ。もう1つはクライアント側が使う使い捨てのポート番号で、これらはクライアントの内部で重複しなければそれでいい。ということで、ポート番号のうち、1から1023は Well known ポートと呼ばれる番号で、あらかじめ役割がきちんと定められているもので、これはWebサーバーやSMTPサーバーに使うとよろしい。49152から65535は動的ポート、非公式ポートと呼ばれるもので、これは特に役割が決められていないものでクライアントで適当に使うことができる。その間の1024から49151は登録済みポートと呼ばれ、今回のGenieアプリのように、野良サーバーアプリがとりあえず使ってみて良い番号だ。データベースサーバーなんかもこの範囲の番号を使う。問題は、一つのマシンに複数のサーバーアプリを複数入れた時には、たまたま重複する可能性があると言うことだ。その場合は先に起動した方が勝つ。ちゃんとしたアプリであれば普通はどのポート番号を使うかは設定で決めることができるので、ポート番号がどうちゃらと言われて起動に失敗したら、変更してみるのがいいだろう。自分で入れたサーバーアプリと通信するのは普通自分で配布するクライアントアプリくらいなので、ポート番号の認識さえ一致していれば大丈夫だ。
グローバルIPアドレスとプライベートIPアドレス
さてここまでの話の中で疑問が生じた人もいるかもしれない。私のサイトのサーバーがどこかしらにあり、そのサーバーはIPアドレスを持っている。これはいい。私が契約した業者がIPアドレスを管理する団体に申請したのだろう。しかし、あなたの機器はどうだろうか?あなたはパソコンやスマートフォンを購入して自宅のネットワークに繋ぐ時に、IPアドレスの申請をしただろうか?普通はしていないはずだ。私もしていない。しかし、現にIPアドレスが割り振られているから通信できるのだ。では私の機器のIPアドレスは誰が管理しているのだろうか?
まず、自宅のネットワークを開設するときには、NTTとかケーブルテレビなどのプロバイダと契約するはずだ。契約すると、プロバイダはIPアドレスをユーザーに付与してくれる。このIPアドレスは外の世界と通信できるもので、グローバルIPアドレスと呼ばれる。しかしこのIPアドレスを、例えばユーザーのiPhoneに直接打ち込んだりはしない。プロバイダと契約すると、ルーターという機器が貸し出されると思う。このルーターがグローバルIPアドレスの情報を知っているのだ。我々は自宅のパソコンやスマートフォンやプリンターをルーターに接続する。接続には有線のLANケーブルが使われたり、無線LANが使われたりするが、どちらであれこのようにして、ルーターを中心とした局所的なネットワークが構築される。LANとはまさにこの、ローカルエリアネットワークの略称である。
LAN内の機器は、ルーターを経由して外部と通信したくなるだろう。あなたはきっと、愛用のパソコンを https://muuumin.net に繋ぎたくて繋ぎたくてしょうがなくなるはずだ。それにはまず、そのパソコンとルーターが通信できる必要がある。LAN内の他の機器と通信したくなることもあるだろう。パソコンで閲覧した https://muuumin.net の文章を、無線LAN経由で接続したプリンターで印刷したくなるはずだ。印刷した100ページに及ぶ怪文書を、二宮金次郎のごとく読み歩いたり、最寄駅で配布したくなるはずだ。そうなると当然、LAN内でもIPアドレスが必要になる。しかし、IPアドレスを管理する団体がいちいちそんな事にまで関与していたら体がいくらあっても足りないので、一部の範囲のIPアドレスを「プライベートIPアドレス」として定義していて、それらはLAN内で好き勝手に使っていいことになっている。10.1.1.1とか、172.16.1.1とか、192.168.1.1のようなIPアドレスを見たことがあるかもしれない。これらの番号がプライベートIPアドレスであり、さまざまなLANの中で同じようなIPアドレスが使われるが、どのみち外部との通信には使われないIPアドレスなので問題にはならない。
もちろんLAN内の機器でプライベートIPアドレスが重複してしまうとやはり困ったことになるが、そこはそれぞれのネットワーク管理者が上手いことやってね、と言うことになっている。ちなみに家庭内のLANに関しては、ルーターが上手いこと管理してくれている。
さて当初の疑問に答えよう。我々はいかにして手元の機器で外部とグローバルIPを使って通信しているのか、という疑問である。答えは、ルーターのようなゲートウェイ機器(外部のネットワークとの出入り口になる機器)がネットワークアドレス変換という技術によって、プライベートIPアドレスをグローバルIPアドレスに変換してくれるのだ。
単純な例としてはNAT変換というものがある。これはプライベートIPアドレスとグローバルIPアドレスを一対一で変換する方式だ。これはわかりやすい。変換表があるようなイメージだ。欠点は同時にネットワークに接続する機器の数だけグローバルIPが必要になる点だ。これまで主流だったIPv4という方式の場合、グローバルIPアドレスは約40億個程度しか存在できない。世界の人口が70億人とか80億人と言われているから、1人あたり1つのグローバルIPアドレスすら割り当てることができない。このためより桁数の多いIPv6という方式が提案されているが、IPv4と互換性がないため、移行は道半ばである。グローバルIPアドレスは枯渇が危ぶまれている貴重な資源なのだ。
グローバルIPを極力消費せずに多数のプライベート機器をネットワークに接続するために考案されたのが、NAPTという方式だ。これはゲートウェイ機器とプライベート機器の対応のために、ゲートウェイ機器のグローバルIPアドレスに加えてポート番号も組み合わせて利用する方式だ。ポート番号はたくさんあるので、これで多数のプライベート機器の通信を1つのグローバルIPアドレスでこなすことができるのだ。
Genie.jl再開
夢中でIPアドレスについて話してしまったので、もしかすると私がIPアドレスの妖精なのでは、という誤解を与えてしまったかもしれないが、そんなことはない。私がIPアドレスについて話したいのはこれきりだ。そろそろ本題に戻ろう。
では、実際にどのようにして先ほどの愛くるしいキャラクターが表示されているかを確認しておこう。
作成した”genieexample”フォルダの直下に、”routes.jl”というファイルがあるので、テキストエディタで開いてみよう。するとこのような記述がある。
using Genie.Router
route("/") do
serve_static_file("welcome.html")
end
これは、メインのURLの後に続くパスが”/”(すなわちメインのURLそのもの)であるとき、"welcome.html"
というファイルを返すということを表現している。"public"
フォルダの中に、"welcome.html"
があることを確認しよう。
これで次の方針が見えてきた。出来合いの"welcome.html"
ではなく、自作の何か別のHTMLファイルを変えることができれば、目標にグッと前進する感じが出てくるだろう。早速やってみよう。
まず、publicフォルダ内に、"helloworld.html"
というファイルを作る。中身は次のようにする。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>Hello World</title>
</head>
<body>
<form>
<button type="submit" formaction="helloworld">ハローワールド</button>
</form>
</body>
</html>
これは次のようなボタン1つだけの画面となる。
formaction="helloworld"
となっているのは、"helloworld"
というURLのデータを取得せよ、という意味である。URLには絶対パスも相対パスも指定できるが、今回は相対パスで指定しているので、自身のWebサイトの指定のパス、即ち"http://127.0.0.1:8000/helloworld"
のというURLのデータを取得せよと言う意味になる。
もちろんそのようなURLに対する処理は自分で書く必要がある。さらに、route("/")
の処理の中で返されるHTMLファイルも、今作ったものに変えておく。
using Genie.Router
route("/") do
serve_static_file("helloworld.html") #変更
end
#追加
route("/helloworld") do
"Hello World!!"
end
こうしておくと、"http://127.0.0.1:8000/"
にアクセスしたら、まず返されるのは"helloworld.html"
となり、「ハローワールド」というボタンが1つ表示されている。
このボタンを押すと、Hello World!!
という文字だけのページが返ってくる。
また、今回はボタンを押すことで、"http://127.0.0.1:8000/helloworld"
のURLにアクセスしたが、直接ブラウザのアドレスバーにこのURLを入れた場合も同じ結果が返ってくるだろう。
Heroku
※自分のWebサービスを後悔することに一切興味がなければ、このセクションは飛ばしても良い。次のJavaScriptまでジャンプだ。
さて最低限のWebアプリの体裁が整ったところで、このアプリを公開してみよう。「ええっ!?いきなり世に問うのかって?誰が使ってくれるんだよ!」そう言いたくなる気持ちはわかる。勘違いしないでほしいのだが、私も現段階のアプリケーションがユーザーを獲得できるほどに魅力的なものだとは思っていない。
ここで未完成のWebアプリをあえて公開するのは、サービスを公開することで一人前の開発者としての責任と自覚を得るため…というのは嘘だ。そんな精神論ではない。ハッキリ言うと、このステップが一番の難関なのだ。オリジナルの部分が入るほど、うまくいかない時に疑うべきポイントが増えてしまう。極力無駄なものが入らないうちにやっておきたいのだ。
さて、今回利用するのはHerokuというサービスである。これはWebアプリケーションをホスティングしてくれるサービスである。通常Webアプリケーションを公開するには自前のサーバーが必要だが、Herokuというサービスを利用すると、Herokuが提供しているサーバー上でWebアプリを動かし、公開することができるのだ。しかも小規模なアプリケーションであれば(一部の制約事項はあるものの)無料で利用することができる。
ちなみにWebサービスとして世の中に公開する以上、セキュリティリスクというものはやはり存在してくる。Herokuを使えば、OSの最新のセキュリティパッチをあてておくとか、余計なポートを閉じておくとかいう、「戸締まりがしっかりできているか?」というような話からは解放されるのだが、あなたが玄関から招き入れた客が悪さをするかどうか関しては、依然としてあなたの責任で予防しなければならない。その辺の話は追々してくことになるだろうが、今すぐ知っておきたいという慎重派のあなたにはIPA(情報処理推進機構)の安全なウェブサイトの作り方を紹介しておこう。
さて、Herokuというサービスを使うにあたり、ざっくりとした流れを紹介しておこう。
- まずは、Herokuにアカウントを登録し、Heroku CLIのツールを自分のマシンにインストールする。
- このとき、Gitをインストールしていなければ、それもする必要がある。
- 次に、公開したいアプリをGitにコミットする。この時にHerokuに動かしてもらえるようにちょっとした設定ファイルを追加しておく。
- あとは、普段のリポジトリにプッシュするついでにHerokuのリポジトリにもプッシュすれば、Webアプリが公開されると言う寸法だ。これだけである。早速やってみよう。
なお、以降の手順は基本的にGenie.jl公式サイト(の翻訳サイト)の HerokuビルドパックによるGenieアプリのデプロイ を参考にしている。念の為、最新版である英語版も参照したが、特に気になるズレはなかった。
Herokuへのアカウント登録〜GitやHeroku CLIのインストール
まあこのあたりについては、私が細々説明するよりも公式サイトの説明を読んでもらった方がいいだろう。公式サイトが改定された時に、このサイトが追随できる可能性は低い。ということで読んでアカウント作成とHeroku CLIのインストールを実行してほしい。
さて、公式サイトの説明の中で、Gitをインストールしていない人はGitをインストールするように言われたと思う。Gitというのはオープンソースでの分散バージョン管理システムである。分散バージョン管理システムとは何かということを理解しておかないとこの後のHerokuの操作がわかりづらくなるので、最低限の部分だけ説明しておこう。
1つのアプリケーションのソースコードを複数人で修正する時のことを考えよう。昔は共有フォルダのようなところにソースコードがあり、各々がそれを取得して開発し、その後に共有フォルダのファイルを更新する、というような方法しかなかった。2〜3人で運用するのであればそれでもギリギリ成り立つかもしれないが、もっと大人数になると、このような運用ではそのうち事故が発生するのは明らかだ。それを解決するためのツールが、バージョン管理システムだ。
バージョン管理システムにはさまざまなものが存在するが、ソースコードの原本を管理するリポジトリというものがどこかにあるという点は共通している。各開発者はリポジトリから各開発用フォルダにソースコードを取得して開発し、開発が完了したら原本に反映する。
共有フォルダの運用との重大な違いは、ソースコードの取得、反映が全てバージョン管理システムを通じて行われることだ。これにより、いつ誰がどのファイルをどのように変更したかと言う差分情報が記録される。この各変更の「差分管理」こそがバージョン管理システムの真髄である。各変更点での断面の状態と、それに付随する差分情報を管理することで、何か問題が発生したときのトラブルシュートが非常に容易になる。差分の情報は非常に重要なので、一度公開された差分情報は原則として破棄することはできず、変更を取り消したければ、それを打ち消すような反対の差分を入れることになる。
初期のバージョン管理システムは集中バージョン管理システムと呼ばれるもので、CVSとかSVNとかVSSという名前のツールが有名である。これはここまでしゃべってきたような、リポジトリがどこかに1つだけあり、各開発者が自分の開発マシンにソースを取得すると言う方式だ。
この方式と比べてGitは分散バージョン管理システムと呼ばれるものだ。分散とは何が分散しているのかというと、リポジトリの場所が分散しているのだ。分散バージョン管理システムの場合、どこかのサーバーに本当の原本となるリポジトリはあるのだが、その上で、各開発者がそれぞれの開発マシンにリポジトリが存在するのだ。親玉となるリポジトリのことをリモートリポジトリ、各開発者が持つリポジトリのことをローカルリポジトリという。
この方式のメリットは、各開発者のマシン内でも厳格な差分管理ができるという点だ。そのためGitでの変更の反映は2段階に分かれる。1段階目がローカルリポジトリへの反映で、これをコミット(commit)という。2段階目はリモートリポジトリへの反映で、これをプッシュ(push)という。コミットするだけであれば、あくまで自分のマシン内での変更だけだ。
さて、ローカルリポジトリに限らず、リモートリポジトリだって実は複数持つことができる。ここまで聖典となるただ一つのリモートリポジトリがあり、ローカルのリポジトリはそのコピーであるという流れで話をしたが、実はリモートリポジトリとローカルリポジトリというのは全く同じ物なので、逆にまずローカルリポジトリを作ってからプッシュしてリモートリポジトリを作ることもできる。そして、その後に別のサーバーにプッシュしてリモートリポジトリ2を作ることもできる。
Herokuにアプリを公開するには、Herokuが管理するリモートリポジトリに自分のソースコードをプッシュする必要がある。普通、自分の通常開発しているリモートリポジトリにプッシュしているだけだと複数のリモートリポジトリにプッシュする経験はあまりない。HerokuではHerokuのリモートリポジトリにプッシュする必要があり、混乱するかもしれないので少し長くなったが補足しておいた。
今回はブランチについてなど全く話さなかったが、Gitについて詳しく学んでおきたければ Pro Git という本が無料で公開されているのでおすすめだ。ほとんど公式マニュアルのような位置付けの本だが、そうとは思えないくらい読みやすい。
GitHubアカウントの作成
ところでGitHubという名前を聞いたことがあるかもしれないが、GitHubというのはGitのリモートリポジトリを管理してくれるサービスのことである。Gitのリモートリポジトリは、自分でサーバーを立ててそこに作ることもできるが、GitHubやBitbucketのようなホスティングサービスを使うとより簡単だ。これらのサービスはどこかにサーバーを構えてくれていて、ユーザーのリポジトリを管理してくれるのだ。これらのサービスは運営している会社や提供しているサービス内容が違うというだけで、本質的にそう大きな違いがあるわけではない。
自分のマシン内にリモートリポジトリを作ることもできるが、いつマシンがクラッシュしたり盗まれたり水没したり火事で消失したりするかわからない。昔のGitHubはプライベートリポジトリを作るのが有料だったが、今ではそれも無料でできるので、バックアップも兼ねてGitHubを使っておくといいだろう。BitBucketでもいいが、プライベートリポジトリが5つまでという制限があるので、GitHubの方がおすすめだ。(昔は逆に5つだけとはいえ無料でプライベートでリポジトリがあるのでBitbucketの方が良かったのだが)
GitHubへのアカウント作成等は特に問題になるポイントもないのでここではフォローしない。探せばいくらでも情報は出てくると思う。GitHub Desktopというクライアントツールもダウンロードしておくと便利だ。
細かい補足
Gitをインストールした上で、GitHub Desktopをインストールしたことで、これらの関係がよくわからなくなっているかもしれない。まず、これらは完全に独立したソフトウェアだ。GitHub DesktopはGitを内蔵しているが、それは個別にインストールしたGitとは別のものだ。Herokuを使う上でコマンドを使って操作する必要が出てくるが、インストールしたGitを直接使ってもいいし、GitHub Desktop内蔵のGitを使ってもいい。GitHub Desktop内蔵のGitは、GitHub Desktopのメニューから、「Open in Terminal」のような操作で使うことができる。
Gitへのコミット + 設定ファイルの追加
さて、GitやHeroku CLIのインストールが終わっていると信じて、次に進もう。
まず、先ほど作った"genieexample"
のフォルダの中身は次のようになっているはずだ。
Manifest.toml bin config routes.jl test
Project.toml bootstrap.jl public src
Herokuを利用するにはまず、これらのファイルやフォルダをGitの管理対象にする必要がある。方法は2つあり、1つはこのフォルダをローカルリポジトリとして直接Gitの管理対象にする方法、もう1つはGitHubやBitBucketなどのホスティングサービスでリモートリポジトリを作り、そこにこれらのフォルダを加えるという方法だ。
このフォルダをローカルリポジトリとして直接Gitの管理対象にする方法
GitHubなどのホスティングサービスと切り離された世界で生きていきたいのであれば、こちらの方法である。私は後述するGitHubにリポジトリを作ってから進める方法の方がやりやすいと思うが、一応説明はしておこう。
macやLinuxならターミナルを、WindowsならGit Bashというアプリが入っているのでそれを開き、"cd"
コマンドで該当のフォルダのパスまで移動して、"git init"
というコマンドを入力する。
そうすると、このフォルダがGitの管理対象となり、.git、.gitattributes、.gitignoreなどのフォルダができる。これでHerokuのCLIツールを動かすことができる。
しかし、今の段階ではローカルリポジトリにコミットをしていないのでHerokuにプッシュしたくても対象のファイルがないと言われてしまう。現在のファイルをローカルリポジトリにコミットしよう。コミットするには、まずaddする必要がある。
git add .
これは何かというと、今編集しているファイルを全てコミットしているわけではないので、どのファイルをコミット対象にするかの前準備だ。"add"
の後ろのピリオドは、現在のディレクトリのすべてのファイルということを意味している。続いてコミットしよう。"-m"
というのは"-message"
の略で、後に続く文言がコミットコメントを意味している。
git commit -m "初期ファイル追加"
これで、とりあえずコミットはできたのでHerokuにプッシュできるのだが、これではまだ必要な設定が足りていない。これは後ほど説明しよう。
GitHubでリモートリポジトリを作り、そこにこれらのフォルダを加えるという方法
先ほどとは逆に、GitHub上でリモートリポジトリを作ってからローカルのフォルダにファイルを落としてくる方法だ。この場合、そのフォルダは勝手にGitの管理対象になる。
まず、GitHubでリポジトリを作る。名前はなんでもいいが、とりあえず"genieexample"
としておくといいだろう。プライベートリポジトリでOKだ。こんな感じになる。
このような状態でページ下部にあるCreate Repositoryというようなボタンを押すと、リモートリポジトリを作ることができる。成功するとページが遷移するので、これをローカルリポジトリとして取り込むために、Set up in Desktopというボタンを押す。
すると、Github Desktopを開くことができ、次のような画面が表示される。Local Pathで好きなフォルダを指定し、Cloneボタンを押す。Local Pathはデフォルトのままであれば特に問題はないが、自分で指定する場合には空のフォルダを指定すること。
そうするとそのパスに新しい空のフォルダができるので、そのフォルダにファイルをコピーしてくる。
こうすると、GitHub Desktop上で変更が検知される。これらのファイルをリポジトリに追加するために、コミットコメントを添えて「Commit to main」というボタンを押そう。
これで、とりあえずコミットはできたのでHerokuにプッシュできるのだが、これではまだ必要な設定が足りていない。次にそれを説明しよう。
Procfile
これからアプリケーションをHerokuに投入するわけだが、1つだけ設定が必要になる。それが"Procfile"
というものだ。これはHerokuに、アプリケーションをどうやって起動するかの指示をするものだ。Herokuはソースファイルが放り込まれても、それをどうやって起動すればいいかわからないからだ。
これまで見てきたように、今回作っている"genieexample"
は、"bin"
フォルダの中の"server"
だったり"server.bat"
というファイルを実行することで起動する。Heroku上で動作させる時に指定したいのは"bin/server"
である。
さらにもう1つ指定しておくべきなのが、"GENIE_ENV=prod"
という指定である。これは、本番環境向けに起動してほしいというメッセージだ。
"genieexample"
のフォルダに"config"
というフォルダがある。これは各種環境で動く際の設定値を決める際のもので、デフォルトでは"dev.jl"
のものが採用されるようになっている。例えば、アプリの起動時にlocalhost(127.0.0.1)のポート8000で動いていた、というのがこれによるものだ。
#dev.jl
using Genie.Configuration, Logging
const config = Settings(
server_port = 8000,
server_host = "127.0.0.1",
log_level = Logging.Info,
log_to_file = false,
server_handle_static_files = true,
path_build = "build",
format_julia_builds = true,
format_html_output = true
)
ENV["JULIA_REVISE"] = "auto"
一方、Heroku上では当然別の設定で動いてもらう必要がある。下記の内容は、”prod.jl”のデフォルトの設定値だ。
#prod.jl
using Genie.Configuration, Logging
const config = Settings(
server_port = 8000,
server_host = "0.0.0.0",
log_level = Logging.Error,
log_to_file = false,
server_handle_static_files = true, # for best performance set up Nginx or Apache web proxies and set this to false
path_build = "build",
format_julia_builds = false,
format_html_output = false
)
if config.server_handle_static_files
@warn("For performance reasons Genie should not serve static files (.css, .js, .jpg, .png, etc) in production.
It is recommended to set up Apache or Nginx as a reverse proxy and cache to serve static assets.")
end
ENV["JULIA_REVISE"] = "off"
実際にIPアドレスが何番になるのかはHerokuに委ねることになるので、今の0.0.0.0というのは都合がいい。これはIPアドレスとしては無効な値で、「なんでもいい」というくらいの意味で使われることが多い。さらにポート番号もHerokuに委ねることになる。ここでは8000が指定されているが、ポート番号の指定は別途Procfileで付与する。
最終的に設定すべきProcfileの中身は次のようになる。
web: GENIE_ENV=prod bin/server $PORT
Herokuはポートを動的に変えるので、"$PORT"
という指定でHerokuの環境変数を取得し、これをGenieに渡している。Genieはこれを受けてうまいことやってくれるようだ。
Procfileをフォルダ直下(routes.jlなどがあるのと同じフォルダ)に追加して、コミットしよう。
Herokuアプリケーション作成
いよいよ、Herokuにアプリを投入する最終段階だ。ここで行う作業は2つ。アプリケーション名の決定と、Herokuビルドパックの指定だ。
コマンドラインで、genieexampleフォルダ直下に移動する。Procfileが追加されていることを確認しておこう。
genieexample % ls
Manifest.toml Project.toml bootstrap.jl public src
Procfile bin config routes.jl test
まず、アプリケーション名を決める。他の人のアプリケーションと重複しているとこの後のプロセスで怒られるので、自分の名前を入れるなどして重複しないような名前にしておこう。後から変更できるので気軽に決めれば良い。今回は、"muuuminsoft-genieexample"
という名前にすることにした。名前を決めたら、"heroku create"
のコマンドで、アプリケーション作成を行う。(ちなみにこの時に、"heroku create"
の後にアプリケーション名を指定しなければランダムな名前になるので、とりあえず指定しないというのもありだ。)
genieexample % heroku create muuuminsoft-genieexample --buildpack https://github.com/Optomatica/heroku-buildpack-julia.git
"--buildpack"
が「Herokuビルドパック」を指定している箇所だ。この後ソースを投入するのだが、その際Herokuはソースを実行するために色々なことをする必要がある。例えばJuliaの実行環境を用意しなければならないので、Juliaの公式サイトから指定されたバージョンのJuliaの圧縮ファイルを落としてきて解凍する、というような作業だ。そういった内容をまとめてくれているものが「Herokuビルドパック」なのだ。
ここまでの作業は各アプリケーションに対して最初の一度きり必要なだけで、以降はherokuにプッシュするだけでアプリケーションがHerokuサイトにリリースされることになる。
Juliaバージョン1.6.2以外の方向けの補足
2022年1月1日現在、https://github.com/Optomatica/heroku-buildpack-julia.gitは、動作させるJuliaのデフォルトバージョンを1.6.2としてビルドしている。もしも手元のJulia環境が1.6.2以外のバージョンの場合、Juliaバージョンを指定しない場合、バージョンの差違によってサーバー側での動きが変わる恐れがある。Genie.newappでアプリケーションを作った場合、デフォルトではバージョン指定がつかないので気をつけた方が良い。
実際、私の手元で1.7.1で作成したGenieアプリケーションの場合、Herokuにプッシュする時に”remote: ERROR: TypeError: in typeassert, expected VersionNumber, got a value of type Pkg.Types.VersionSpec”というようなエラーメッセージが表示され、失敗した。バージョン1.7付近でManifest.tomlファイルのフォーマットに変更が入っているためか、バージョン1.6.2のバージョンではうまく解釈できないために、発生しているようである。
これを防ぐためには、次のようにProject.tomlファイルで明示的にJuliaバージョンを指定する必要がある。
#Project.toml
name = "Genieexample"
...
[deps]
...
#追加
[compat]
julia = "1.7.1"
これでHerokuビルドパックはバージョン1.7.1のJuliaを取得して展開してくれるため、そのような懸念はなくなる。Juliaのバージョンを指定すること自体は手放しでいいこととも言えず、将来Julia 1.8や1.9が出た時にも変わらず1.7で動くため、時代に取り残される懸念もあるのだが、現状こうしないと動かないのでやむなく実施している。
デプロイ
ついにデプロイの時を迎えた。次のようなコマンドを入力しよう。
genieexample % git push heroku main
これはmainブランチの変更をherokuという名前のリモートリポジトリにプッシュしてくれという意味だ。herokuというのは具体的にどこかというと、下記のようなコマンドで調べることができる。
genieexample % git remote -v
heroku https://git.heroku.com/muuuminsoft-genieexample.git (fetch)
heroku https://git.heroku.com/muuuminsoft-genieexample.git (push)
origin https://github.com/muuumin-soft/genieexample.git (fetch)
origin https://github.com/muuumin-soft/genieexample.git (push)
この設定については、herokuのCLIツールが"heroku create"
で実施してくれる。
色々なログが流れるが、これが「Herokuビルドパック」がしてくれている環境構築だ。一通りログが流れ終わると、デプロイ完了だ。
ちなみに、エラーが出ているとこんな画面になる。
こうなると何かがおかしいので、ログを確認してみよう。
genieexample % heroku logs -tail -a muuuminsoft-genieexample
成功していたらこのような感じの表示になる。
アプリを開いてみよう。コマンドラインで次のように打ち込んでもいいし、
genieexample % heroku open -a muuuminsoft-genieexample
ブラウザに直接次のURLを打ち込んでもいい。
https://muuuminsoft-genieexample.herokuapp.com/
成功だ!ついにアプリをリリースすることができた!
Herokuのパフォーマンスについて
実はHerokuは応答速度の点ではデフォルトのままでは苦しい部分がある。
30分でスリープする
上記URLにアクセスした時、なかなか応答しなかったという人がいると思うのだが、無料版のherokuでは30分アクセスがないとスリープ状態になるという仕様がある。これは無料枠ではアプリの起動時間に制限がある(550時間/月)ための措置で、こうしておくと必要な時以外は無料枠の制限時間を消費しないので月末付近でアプリが起動できなくなる事態を防ぐことができる、という親心のようなのだが、如何せん応答速度が悪すぎる。
そのため、これを避けるために一定時間おきに自動でURLにアクセスしてくれるアドオンがある。私は面倒なので入れていないが調べたらすぐに出てくると思う。ちなみに550時間の制限時間はクレジットカード情報を登録すれば1000時間に伸ばすことができて、それだけあれば一ヶ月間まるまる24時間起動していても消費し尽くしてしまうことはない。
アプリがUSリージョンに配置されている
Herokuの無償プランでは、アプリの配置リージョンがUSリージョンで固定されている。そのため日本からアクセスするとどうしても時間がかかる。2022年1月1日現在、日本リージョンに配置したければ、エンタープライズプランにしてHeroku Private Spacesというサービスを利用するしかなさそうが、エンタープライズプランのお値段は「お問合せください」である。これに関しては企業がスポンサーにでもついてくれない限りは諦めるしかないだろう。
画像ファイル等の表示に時間がかかる
これはHeroku固有の問題というわけではないが、ついでに紹介しておこう。
"Prod.jl"
に次のような記載があるのに気づいた人もいると思う。
if config.server_handle_static_files
@warn("For performance reasons Genie should not serve static files (.css, .js, .jpg, .png, etc) in production.
It is recommended to set up Apache or Nginx as a reverse proxy and cache to serve static assets.")
end
DeepL翻訳にかけると、「パフォーマンス上の理由から、Genie は静的ファイル (.css, .js, .jpg, .png, etc) を実運用環境で提供するべきではありません。静的なアセットを提供するには、Apache または Nginx をリバースプロキシおよびキャッシュとしてセットアップすることをお勧めします。」となっている。
Genieに限らず一般的な話として、Webサーバーとアプリケーションサーバーは分けた方がいいという話がある。ここでいうWebサーバーというのは、指定されたURLに対してなんの処理を行うかを決めるもので、アプリケーションサーバーとは具体的な処理の実行を担うものである。これまでGenieをWebサーバーと呼んできたが、実際にはWebサーバー機能をもつアプリケーションサーバーといったところだ。
で、なぜそのような構成にするのがいいかというと、ユーザーからのリクエストには事前に用意しておいたHTMLや画像ファイルのようなものをサッと返せばいいだけのものと、もっと頑張って色々な計算をしたりデータベースとやりとりをしたり、という重たい処理をしなければならないものがあるからだ。
前者の仕事はマシン自体はあまり仕事をしないが、画像ファイルなどはデータのサイズが大きいので、ネットワークがボトルネックになりがちだ。一方で後者の仕事は、入力も出力も数値データや文字列データのようなものがメインになるので、データのサイズよりは計算能力の方がボトルネックになりがちだ。
そうなると、物理的にユーザーの近くにある数多くのWebサーバー(質より量)がまず軽い処理を担い、一部の重たい処理は少数のアプリケーションサーバー(量より質)に処理を依頼する、というようにするのが合理的だ。
それを踏まえて、もう一度先ほどのメッセージを見てみよう。
「パフォーマンス上の理由から、Genie は静的ファイル (.css, .js, .jpg, .png, etc) を実運用環境で提供するべきではありません。静的なアセットを提供するには、Apache または Nginx をリバースプロキシおよびキャッシュとしてセットアップすることをお勧めします。」
「Apache または Nginx をリバースプロキシおよびキャッシュとしてセットアップする」というのが、今説明した、Webサーバーに軽い処理を任せよう、という話だ。ApacheやNginxというのは有名なWebサーバーだ。リバースプロキシというのは、システムが外部からのリクエストを最初に受けつける役割のサーバーだ。普通プロキシサーバーというと、システムの内部から外部に出るために必ず通過するという役割のサーバーである。今回は逆に外部から内部なのでリバースという名前がついている。ApacheやNginxというのはキャッシュ機能も持っているので、一度欲しいと言われた静的ファイルは次に欲しいと言われたらすぐに返せるようになっている。例の警告メッセージは、「Genieはアプリケーションを作成するためのツールですので、Webサーバーとしての機能はそんなには頑張りません。本格的なWebサーバーの機能が使いたければ、既存のツールを使ってください」というGenieからの通達である(と思う)。
ただ、ではWebサーバーを導入しましょうか、という話にはしない。なぜならそれをしようと思うと、結局何かのサーバーを入手して、そこにApacheやNginxを立てましょうかという話になるからだ。それはまた骨の折れる作業である。それよりはHerokuが公式に推奨している、AWS CloudFrontを使った方法の方が良さそうな気はする。が、それも今試すのはやめておこう。本当に困ったら検討するかもしれないが、今は放っておくことにする。
JavaScript + Ajax
長くなったがこれが最後のセクションだ。これで当面必要な技術的要素は網羅することになる。
JavaScript
JavaScriptはWebブラウザの中で動くスクリプト言語だ。Webブラウザの中で計算をしたり、画面に動的なアニメーションをつけたり、という時に活躍するものだ。JavaScript自体について語りたいことはいろいろあるが、ここでの本題からはずれるので、それはまたの機会にしよう。
JavaScriptはHTML中に"<script>"
というタグ中に記載する。普通は"<body>"
タグの末尾に書くことが多い。これはHTMLは上から順に読み込まれて描画されるため、JavaScriptが上の方にあると、重い処理の時に画面の途中から描画されないようになってしまうからだ。
当面のところ、JavaScriptにはそんなに深入りしないので特に注意点はない。動的な言語でコンパイラでのチェックはかからないし、コーディングにミスがあっても例外でブラウザが落ちました、というようなこともない。それはそれでユーザー的にはメリットなのだが、開発時には画面を動かしているだけではエラーに気づきづらいので、動作テスト時にはF12キーで立ち上がる開発者ツールを見る癖はつけておいた方がいいだろう。例外が発生したらコンソールに出てくるはずだ。
Ajax
今のアプリはボタンを押したら画面全体がぱちっと切り替わって”Hello World!!”と表示される。これはこれでいいのだが、今後ボタン操作などでサーバーからデータを取得するとき、実際に変更したいのは画面要素のほんの一部で、画面全体の描画を行う必要はないケースがよくある。このような時にまで画面全体の描画を毎回行うと画面がちらついてしまうため、初回のみHTMLを受け取って画面全体を描画し、その後はデータのみをやりとりして、画面の一部のみを更新するようにしたい。
これのために、Ajaxと呼ばれている技術を使うことになる。ライブラリやフレームワークを使うのか?おまえは自由な小鳥でありたかったんじゃないのか?と詰問されるかもしれないが、実際にはピュアなJavaScriptしか使わない。そもそも特別な名前がつくほど大層なものではないように思う。
まずはちょっと用語の整理からしておこう。
XMLHttpRequest
Ajax方式の基本になるのが、XMLHttpRequestという関数である。この関数はJavaScriptの標準関数だ。XMLという名前がついているが、実際にはさまざまな形式のフォーマットで通信ができる。この関数を使うと、ページ全体を更新することなくサーバーからの値を取得することができる。
DOM
DOMというのはDocument Object Modelの略で、HTMLデータをJavaScriptから取り扱うためのデータである。JavaScriptがHTMLを書き換える時、文字列として読み込んで、1文字目がタグの開き文字で、みたいなことをやる必要はない。DOMを操作することで、もっと便利にHTMLを書き換えることができる。例えば、次のように書くと、「HTMLから”hello”というID値のついた要素を取得し、その内部に”Hello World!”という文字列を改行付きで追記する」という意味になる。
let e = document.getElementById("hello");
e.innerHTML += "Hello World!<br>";
JSON
JSONというのはJavaScript Object Notationの略である。{"id":100, "name":"太郎", "parents":[10, 20]}
のように、key-value型のデータ構造である。もともとシステム間のデータ交換にはXMLが使われることが多かったが、Webの世界でデータ交換する際にはこの形式が標準的に用いられる。データの表現力として本質的な違いがあるわけではない(と思う)のだが、JSONは名前から分かる通り、JavaScriptのオブジェクトとすこぶる相性がいいのだ。JavaScript以外の言語としても、XMLよりもJSONで作る方がちょっと簡単だ。そんなわけで、JSONフォーマットは最近はWeb以外のところでも使われたりしている。
Ajaxサンプル
それではXMLHttpRequest関数の使い方を見てみよう。
例えば画面上に「太郎」というボタンがあり、そのボタンを押すとサーバーに通信し、サーバーは「太郎です」という応答を返してくるので、それを画面に描画するという流れを考える。
まず、サーバー側はこのようになる。
#routes.jl
...
#追加
route("/ajax") do
serve_static_file("ajax.html")
end
#追加
route("/getTaroMessageJson") do
"{\"message\":\"太郎です\"}"
end
新しい"ajax.html"
というページを変えるためのURLと、"getTaroMessageJson"
というAjax通信用のURLを用意している。"getTaroMessageJson"
が返しているのはJSONデータ(と解釈できる文字列)である。
そしてHTML側はこのようになる。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>Hello World</title>
</head>
<body>
<form>
<button type="button" onclick="getTaroMessage();">太郎</button>
</form>
<div id="saymessage"></div>
<script>
function getTaroMessage() {
var xhr = new XMLHttpRequest();
//通信の状態が変わった時、responseに対して何をするかを登録
xhr.onreadystatechange = function () {
//readyState=4は通信完了、status=200は成功を意味する
if (this.readyState == 4 && this.status == 200) {
let json = JSON.parse(JSON.stringify(this.response));
let e = document.getElementById("saymessage");
e.innerHTML += json.message + "<br>";
}
};
xhr.responseType = 'json';
xhr.open('GET', "getTaroMessageJson", true);
xhr.send();
}
</script>
</body>
</html>
「太郎」という表示のボタンがクリックされると、"getTaroMessage();"
というJavaScriptが実行されるようにしている。その下の"<div id="saymessage"></div>"
というのは、この後サーバーからの応答結果を書き込むために用意している領域だ。その次の"getTaroMessage()"
関数がAjax通信の本体だ。ごちゃごちゃしているように見えるが、処理の内容は単純なので、コメントを追って貰えばわかると思う。
もう少しAjax部分と通信成功後の処理を分けておくと次のようになる。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>Hello World</title>
</head>
<body>
<form>
<button type="button" onclick="getTaroMessage();">太郎</button>
</form>
<div id="saymessage"></div>
<script>
function requestAjax(url, callback) {
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function () {
if (this.readyState == 4 && this.status == 200) {
callback(this.response);
}
};
xhr.responseType = 'json';
xhr.open('GET', url, true);
xhr.send();
};
function getTaroMessage() {
requestAjax("getTaroMessageJson", function (response) {
let json = JSON.parse(JSON.stringify(response));
let e = document.getElementById("saymessage");
e.innerHTML += json.message + "<br>";
});
}
</script>
</body>
</html>
JavaScriptとAjaxに関する説明は以上だ。動作するサンプルは下記になる。
https://muuuminsoft-genieexample.herokuapp.com/ajax
JuliaデータのJSONへの変換
今の例ではサーバー側でJSONを手書きしたが、実際にはJuliaのデータを変換する必要がある。この方法も知っておこう。
まず、JuliaではJSONというパッケージが存在する。基本的な型については、このパッケージのjson関数がサポートしてくれている。(JuliaのREPLを開いて"using JSON"
とするとパッケージをインストールしていなければするように促されるので、インストールしておこう。)
julia> using JSON
julia> json(1)
"1"
julia> json("hello")
"\"hello\""
julia> json([1, "2", true, "true"])
"[1,\"2\",true,\"true\"]"
julia> json(Dict(:a => 1, "b" => 2, :c => [3, 4, [5, 6]]))
"{\"a\":1,\"b\":2,\"c\":[3,4,[5,6]]}"
JavaScriptのオブジェクトのように、key-value型を取り扱いたければ、Dict型にすると良いことが分かる。自作の構造体をJSON化したいというときも、次のように一度Dictに変換するとやりやすいと思う。
julia> struct Person
id
name
parents
end
julia> function dict(p::Person)
d = Dict()
for prop in propertynames(p)
d[prop] = getproperty(p, prop)
end
return d
end
julia> function JSON.json(p::Person)
JSON.json(dict(p))
end
julia> person = Person(1, "太郎", [2, 3])
Person(1, "太郎", [2, 3])
julia> json(person)
"{\"id\":1,\"parents\":[2,3],\"name\":\"太郎\"}"
パッケージの依存関係の登録
自分で追加したパッケージをGenieプロジェクト内で使う場合、Project.tomlファイルに明記しておかなければ困ったことになる。例えば、今回のJSONパッケージは標準で入っているものではないので、他所の環境(例えばHeroku)に持って行った時に、コードが動かないという事態になりかねない。そのため、それぞれのプロジェクト内で、どのパッケージに依存するか、という情報を明記する場所があり、それがProject.tomlファイルなのだ。
とはいえ、これを手動でメンテナンスしていく必要はない。Juliaのパッケージマネージャーの機能で、自動で追記してくれる。
まず、”genieexample”のフォルダでJuliaのREPLを起動する。次に、”]”キーを押して、パッケージモードに入る。すると、"julia>"
というプロンプトが、"(@v1.7) pkg>"
というような表記に変わる。ここで、"add JSON"
などとやると、そのマシンの標準Julia環境には入るのだが、”genieexample”が必要とするパッケージということは認識されない。
“genieexample”が必要なのだとJuliaに伝えるには次のようにする。
まず、"activate ."
と入力する。すると、"(Genieexample) pkg>"
という表記に変わる。これは”genieexample”に対するパッケージモードで、"add JSON"
とすると、”genieexample”が必要しているのだと認識され、Package.tomlが書き変わるのだ。
(@v1.7) pkg> activate .
Activating project at `~/Documents/GitHub/genieexample`
(Genieexample) pkg> add JSON
Updating registry at `~/.julia/registries/General.toml`
Resolving package versions...
Updating `~/Documents/GitHub/genieexample/Project.toml`
[682c06a0] + JSON v0.21.2
No Changes to `~/Documents/GitHub/genieexample/Manifest.toml`
これを忘れると、ローカルでは動いていたのに、Herokuにプッシュしたら動かない、というようなことになってしまうので気をつけよう。
自作コードの追加
ちょうどいい例なので、今まで手元で温めてきたコードがあり、それをWebアプリとして公開するというシナリオを考えよう。
Genieプロジェクトに自作のコードを追加する際には、プロジェクトフォルダ直下の”lib”というフォルダに追加するというのがお作法になっている。”lib”というフォルダはGenieが作ってくれるわけではないので、自分で手で追加する必要があるが、このフォルダ以下のファイルは、勝手にLoadPathに加えてくれるという。これが何を意味するかというと、これまで自分でモジュールを作っていた場合には、何も考えずに"using"
できるということだ。
例えば、次のような"PersonData.jl"
というファイルを元々持っていて、これをそのまま使いたいとしよう。
#PersonData.jl
module PersonData
using JSON
export Person, json
struct Person
id
name
parents
end
function dict(p::Person)
d = Dict()
for prop in propertynames(p)
d[prop] = getproperty(p, prop)
end
return d
end
function JSON.json(p::Person)
JSON.json(dict(p))
end
end
このファイルをlibフォルダに置くと、次のように、Genieプロジェクト内で"using PersonData"
できる。
#routes.jl
using Genie.Router
using PersonData #using可能
...
usingしたPersonData内部のデータを使って、クライアントへjsonオブジェクトを返すインターフェースを作ろう。
#routes.jl
...
route("/ajax2") do
serve_static_file("ajax2.html")
end
route("/getTaroMessageJson2") do
person = Person(1, "太郎", [2, 3])
json(person)
end
ajax.htmlとほとんど同じだが、ajax2.htmlをいうものを次のように作る。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>Hello World</title>
</head>
<body>
<form>
<button type="button" onclick="getTaroMessage();">太郎</button>
</form>
<div id="saymessage"></div>
<script>
function requestAjax(url, callback) {
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function () {
if (this.readyState == 4 && this.status == 200) {
callback(this.response);
}
};
xhr.responseType = 'json';
xhr.open('GET', url, true);
xhr.send();
};
function getTaroMessage() {
requestAjax("getTaroMessageJson2", function (response) {
let json = JSON.parse(JSON.stringify(response));
let e = document.getElementById("saymessage");
e.innerHTML += `ID${json.id}番の${json.name}です。親のIDは${json.parents}番です.<br>`;
});
}
</script>
</body>
</html>
ボタンを押した時に追記される文言を少し変えているので見分けはつくと思う。
これで既存ソースの自作の構造体のデータをHTML側に反映させる手順を示すことができた。
動くサンプルは下記になる。
https://muuuminsoft-genieexample.herokuapp.com/ajax2
終わりに
Genie.jlを使ってWebアプリケーションを作る方法をサラッと紹介するつもりだったが、予想外にボリュームが増えてしまった。長い説明となったが、今回の内容を応用して、ちょっとしたアプリならGenieとHerokuで簡単に公開できそうだな、というイメージが湧いていれば今回の記事は成功といえる。
コード
今回のソースコードのリポジトリはこちらとなる。
https://github.com/muuumin-soft/genieexample
参考文献
ピュアなJSでAPIリクエストをするメモ(jsonとjsonp)
References
↑1 | ここでいうサーバーとはハードウェアの事を指す。Webサーバーというと、Webサーバー機能をもつアプリケーションを指すこともあれば、機体そのものを指すこともある。紛らわしいが普通は文脈で判断できる。 |
---|---|
↑2 | というのが簡単な理解だが、実際には私の固定のIPアドレスが付与されたとは限らない。DNSに登録されたのは、”muuumin.net”と”レンタルサーバーのIPアドレス”であり、リクエストを受け取ったレンタルサーバー側で「”muuumin.net”というのは、ははーん、このサイトだな」と振り分けている可能性もある。他にも、登録されているIPアドレスがロードバランサーと呼ばれる負荷分散機器のアドレスであり、そこから実際のサーバーに振り分けられている可能性もある。このようにネットワークは多くの機器がバケツリレーのように情報を受け渡していることが多い。 |