C4SA へアクセスし、ログイン後にRubyコンテクストを選択してください。
今回はSinatraを使ってウェブアプリを開発します。 そのためにまずSinatraをC4SA上にインストールする必要があります。
gem "sinatra"
バンドルインストールの「ウインドウ」を開き「実行ボタン」からインストールできます。
SinatraとはRubyでウェブアプリを開発するためのフレームワークの1つです。 Rubyで有名なフレームワークといえばRuby on Railsがありますが、 SinatraはRuby on Railsに比べて軽量で、少ない量のコードでウェブアプリを実装できるという特徴があります。 使い分けの例としてはデータベースを使うある程度かっちりとしたアプリではRails、 それ以外の軽めのアプリではSinatraといった感じです。
まずはウェブアプリの基礎としてhello worldを作成します。手順は以下の通りです。
# -*- encoding: utf-8 -*- require "rubygems" require "sinatra" # トップページ向けに / でアクセスされた場合のルートを定義 get "/" do "hello world" end
作成したapp.rbを実行するための設定。元々書かれているのは削除して構いません。
require "./app" run Sinatra::Application
ここまでできたらサーバをリスタートして動作確認をしましょう。
動作確認は上部にあるリンクから行えます。
次にユーザからの入力を受け取るようにしましょう。Sinatraでは params
という変数にパラメータは格納されているのでそこから取得します。例えば以下のように/resultに対して付与された name=foo パラメータを受け取るようにしましょう。
http://localhost/result?name=foo
get "/" do "hello world" end # nameパラメータを受け取るルートを追加 get "/result" do "hello " + params[:name] end
編集したらサーバを再起動して確認してみましょう。
ここまでは単純に文字列を表示するだけでしたが、ウェブアプリなのでHTMLを表示するようにしましょう。
ただHTMLを生で書くのは効率的ではないのでテンプレートエンジンを使ってHTML出力させます。
今回はerbというrubyに標準添付されているテンプレートエンジンを使います。
テンプレートエンジンとは雛形ファイル(テンプレート)とそれに埋め込みたい実データを元に ドキュメントを生成するためのライブラリで、HTML内にプログラムのコードを埋め込むものや、 HTMLに変換するためのDSL(Domain-Specific Language)を用いて より簡潔な記述でHTMLを表現するものなどいろいろな種類があります。
テンプレートエンジンを導入するための手順は以下の通りです。
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title><%= TITLE %></title> </head> <body> <h1><%= TITLE %></h1> <form method="GET" action="/result"> <label>アカウント名: <input type="text" name="name" /></label> <input type="submit" value="診断する" /> </form> </body> </html>
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title><%= TITLE %></title> </head> <body> <h1><%= @name %>さんの診断結果</h1> <p><%= @result %></p> <p><a href="/">トップへもどる</a></p> </body> </html>
# -*- encoding: utf-8 -*- require "rubygems" require "sinatra" require "erb" # erb の require を追加 # 共通で使うタイトルの定義 TITLE = "foo診断" get "/" do # テンプレートを呼び出し # :index が views/index.rb に対応する erb :index end get "/result" do @name = params[:name] @result = "hello " + @name erb :result end
ここまで出来たらサーバを再起動して、トップページで入力フォームが表示されること、 「診断する」ボタンで結果ページが表示されることを確認しましょう。
上記のサンプルでは受け取ったパラメータをそのまま画面に表示しているが、 これだとパラメータに悪意のあるコードを含まれた場合に クロスサイトスクリプティングの攻撃を受けてしまいます。
例えばFirefoxで以下のようなパラメータを付けることで任意のJavaScriptを実行できてしまう(XSS攻撃)ことを確認しましょう。
http://localhost/result?name=%3Cscript%3Ealert%28%27test%27%29%3B%3C%2Fscript%3E
XSS攻撃へ対処するには入力値を画面表示する前にエスケープ処理をしてあげる必要があります。
require "cgi" # CGIライブラリを追加 get "/result" do # HTMLに使われる文字をエスケープすることでscriptタグなどを無効にする @name = CGI.escapeHTML params[:name] @result = "hello " + @name erb :result end
index.erbとresult.erbでは共通する部分がいくつかあります。 今回はこの2ファイルのみですが、テンプレートが増えた場合には 同じものを何度も書くのは非効率的です。 そこでレイアウトを利用しましょう
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title><%= TITLE %></title> </head> <body> <!-- yield部分に子テンプレートの内容が挿入される --> <%= yield %> </body> </html>
<h1><%= TITLE %></h1> <form method="GET" action="/result"> <label>アカウント名: <input type="text" name="name" /></label> <input type="submit" value="診断する" /> </form>
<h1><%= @name %>さんの診断結果</h1> <p><%= @result %></p> <p><a href="/">トップへもどる</a></p>
テンプレートの修正はサーバの再起動なしに反映されるので ここまで出来たら再起動はせずにブラウザで表示に変わりないか、 layout.erbを編集するとトップページ/結果ページ共に変更が反映されているか確認してみましょう。
Sinatraでのルート定義、パラメータの受取り方、erbを用いた画面出力までできるようになりました。
ここからはいよいよ診断アプリのコアロジックの実装に入ります。
手順は以下の通りです。
アカウント名から診断結果を導くために第一段階としてアカウント名を整数に変換します。
ここでは単純にアスキーコードの合計値を用いることとします。
h | e | l | l | o |
---|---|---|---|---|
104 | 101 | 108 | 108 | 111 |
# # アスキーコードの合計値を返す # ex) # puts get_bytes_total "hello" # #=> 532 # def get_bytes_total(s) total = 0 s.each_byte do |i| total += i end total end
第二段階として整数から、用意しておいた診断結果の配列の要素を取得します。
# # 診断結果の配列と整数から1つの診断結果を返す # ex) # # 診断結果 # result = ["hello", "world", "neko", "inu"] # puts get_message(result, 0) # #=> "hello" # def get_message(arr, num) arr[num % arr.size] end
ここまで出来るとget_bytes_total
とget_message
を組み合わせて
例えば以下のようにしてアカウント名から診断結果を取得できます。
################################### # C4SA上には書かないでください ################################### result = ["hello", "world", "neko", "inu"] puts get_message(result, get_bytes_total("foo")) # => "hello" puts get_message(result, get_bytes_total("bar")) # => "world"
ただし今の処理だとアカウント名が同じ場合にはいつも同じ結果しか表示されません。 そこで日毎に結果が変わるように修正しましょう。
name = params[:name] # 今日の日付を文字列で取得 today = Time.now.strftime("%Y%m%d") # 毎秒変えて確認する場合はこちら #today = Time.now.strftime("%Y%m%d%H%M%S") # アスキーコードの合計値を取得する際に(アカウント名+今日の日付)の合計値を取得することで # 日毎の合計値に違いを付ける num = get_bytes_total(name + today) result = get_message(RESULT, num)
診断結果を取得する処理ができたので、これをアプリに反映させましょう。
なお以下の例では1つの診断結果ではなく、3つの診断結果を用意しておいて
それを1つの文字列にまとめるようにしています。
# -*- encoding: utf-8 -*- require "rubygems" require "sinatra" require "erb" require "cgi" # タイトルを変更 TITLE = "リア充診断" # 診断結果を定義 MSG_A = [0, 1, 50, 100, 1000, 10000, 1000000] MSG_B = ["3秒", "10分", "1週間", "半年", "1年", "50年"] MSG_C = [-1000, -100, -50, 0, 1, 50, 100, 1000, 10000, 1000000] FORMAT = "あなたのリア充レベルは%dで、%s後にはレベル%dになるでしょう!" # アスキーコードの合計値を返す def get_bytes_total(s) total = 0 s.each_byte do |i| total += i end total end # 診断結果の配列と整数から1つの診断結果を返す def get_message(arr, num) arr[num % arr.size] end # トップページの処理は変更なし get "/" do erb :index end # 結果ページで診断結果を表示するよう修正 get "/result" do name = params[:name] num = get_bytes_total(name) @name = CGI.escapeHTML(name) @result = sprintf(FORMAT, get_message(MSG_A, num), get_message(MSG_B, num), get_message(MSG_C, num)) erb :result end
編集が完了したらサーバを再起動して確認してみましょう。
結果の表示まで出来たら次はTwitterで共有できるようにしましょう。
ここではOAuth認証連携までは時間がなくてできないので、
単純にリンクでの連携までにします。
http://twitter.com/?status=... でTwitterへの投稿用リンクが作れます。
例) http://twitter.com/?status=投稿リンクテスト
request.env
で環境変数にアクセスでき、request.env['HTTP_HOST']
で
アプリが動いているホスト名を取得することができます。
自アプリへのリンクを貼る場合にはハードコーディングでも良いですが、
環境変数を使って生成するという方法もあります。
<!-- Twitter共有リンク --> <p><a target="_blank" href="http://twitter.com/?status=<%= TITLE %>: <%= @result %> http://<%= request.env['HTTP_HOST'] %>"> » Twitterで共有する </a></p>
テンプレートの修正のみなので再起動は不要です。ブラウザで確認してみましょう。
とりあえず結果が表示できるところまではできました。
が、HTMLは最低限のものなので見た目がかなり貧相です。
デザインが出来る人であればデザインしてもいいのですが、
デザインができない/自身がない場合にはフレームワークを導入すると早いです。
今回はTwitter Bootstrapでデザインを整えてみます。
手順は以下の通りです。
Bootstrapのページへ行き、 Download Bootstrapボタンからダウンロードしてください。
C4SAではアップロードするファイルがZIPの場合には自動で展開してくれる機能があります。 解凍してからアップロードでは面倒なので、今回はその機能を使います。
publicフォルダは静的ファイルを置くためのフォルダで、 public配下に置かれたファイル、例えば public/foo.png は http://.../foo.png でアクセスできます。(publicという文字はURLに含まれません)
Bootstrapが有効になるようテンプレートを編集しましょう。
今回ここで編集する内容はBootstrapのドキュメントにも書いてあることなので
より細かくBootstrapを使いたい場合にはドキュメントを参照して挑戦してみてください。
手順は以下の通りです。
共通のレイアウトファイルではCSS、Javascripを主に読み込みます。
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title><%= TITLE %></title> <!-- Bootstrapのスタイルシートを追加 ここから --> <link rel="stylesheet" type="text/css" href="/bootstrap/css/bootstrap.min.css" /> <!-- /Bootstrapのスタイルシートを追加 ここまで --> </head> <body> <!-- コンテナを追加 ここから --> <div class="container"> <%= yield %> </div> <!-- /コンテナを追加 ここまで --> <!-- BootstrapのJavascriptを追加 ここから --> <script type="text/javascript" src="/bootstrap/js/bootstrap.min.js"></script> <!-- /BootstrapのJavascriptを追加 ここまで --> </body> </html>
トップページではHero unitを使います。
<!-- ドキュメントに従って記述 ここから --> <div class="hero-unit"> <h1><%= TITLE %></h1> <p>Twitterのアカウント名から~を診断します。</p> <!-- form内は今まで通り --> <form method="GET" action="/result" class="well form-inline"> <label>アカウント名: <input type="text" name="name" /></label> <input type="submit" class="btn btn-primary" value="診断する" /> </form> </div> <!-- /ドキュメントに従って記述 ここまで -->
結果ページではPage headerと Wells、 またIcon glyphsを利用します。
<!-- Page headerを適用 ここから --> <div class="page-header"> <h1><%= @name %>さんの診断結果</h1> </div> <!-- /Page headerを適用 ここまで --> <!-- Wellsを適用 ここから --> <div class="well well-large"> <p><%= @result %></p> <p><a target="_blank" href="http://twitter.com/?status=<%= TITLE %>: <%= @result %> http://<%= request.env['HTTP_HOST'] %>"> <!-- Iconglyphsを利用 --> <i class="icon-share"></i>Twitterで共有する </a></p> </div> <!-- /Wellsを適用 ここまで --> <!-- Iconglyphsを利用 --> <p><a href="/"><i class="icon-home"></i>トップへ戻る</a></p>
今回もテンプレートの修正だけだったので、サーバは再起動せずにブラウザで確認してみましょう。 どうでしょう?少しは見れるページになったでしょうか?
Bootstrapを使うと綺麗なページを簡単に作ることができますが、
Bootstrapを使ったサイトが溢れてくるとどれも同じページに見えてしまうようになります。
Bootstrapを使いつつ他サイトとの差別化を図りたい場合には
Bootswatchで配布されているBootstrap向けのテーマファイルを利用したり、
WrapBootstrapで販売しているテーマファイルを利用すると
テンプレートに手を加えることなくデザインを変えることができます。
これ以降はハンズオンでは扱わない内容になります。 時間が余ってしまった人は是非挑戦してみてください!
アカウント名が abc, cba といったアナグラムになっている場合、
get_bytes_total
の計算結果が同じ値になってしまうため
違うアカウント名なのに必ず同じ結果が返ってしまいます。
今回はこれをハッシュ値を使って解決してみましょう。
# 他ライブラリのrequireは省略 # ... # SHA1のライブラリを読み込み require "digest/sha1" # 他処理のコードは省略 # ... get "/result" do name = params[:name] today = Time.now.strftime("%Y%m%d") # (name + today) をハッシュ化してユニークな文字列にする num = get_bytes_total(Digest::SHA1.hexdigest(name + today)) @name = CGI.escapeHTML(name) @result = sprintf(FORMAT, get_message(MSG_A, num), get_message(MSG_B, num), get_message(MSG_C, num)) erb :result end
今回はハッシュ化の例を出しましたが他の解決策がないか考えてみましょう!
現状、診断結果のメッセージはプログラムの中に埋め込んでいますが
それだと変更がある度にサーバの再起動が必要になり面倒です。
そこで外部にメッセージファイルを作成してそれを読み込むようにしてみましょう。
手順は以下の通りです。
まず外部ファイルのフォーマットの仕様を決めましょう。
診断結果の個数, 個数分の診断結果メッセージ, 出力フォーマット の3つの情報がわかれば良いので
それが判別できるよう記述されていれば問題ありません。
ただファイル形式を独自のものにしてしまうと汎用性がなくなってしまうので
ここではYAML(YAML Ain't Markup Language)を使います。
format: "あなたのリア充レベルは%dで、%s後にはレベル%dになるでしょう!" messages: - [0, 1, 50, 100, 1000, 10000, 1000000] - ["3秒", "10分", "1週間", "半年", "1年", "50年"] - [-1000, -100, -50, 0, 1, 50, 100, 1000, 10000, 1000000]
これはrubyのコードでいうと以下と同じになります。
{"format" => "あなたのリア充レベルは%dで、%s後にはレベル%dになるでしょう!" "messages" => [[0, 1, 50, 100, 1000, 10000, 1000000], ["3秒", "10分", "1週間", "半年", "1年", "50年"], [-1000, -100, -50, 0, 1, 50, 100, 1000, 10000, 1000000]]}
では実際にYAMLからデータを読み込んで結果表示させる処理を書きましょう。
# 他ライブラリのrequireは省略 # ... # YAMLライブラリを読み込み require "yaml" # 他処理のコードは省略 # ... get "/result" do name = params[:name] today = Time.now.strftime("%Y%m%d") num = get_bytes_total(Digest::SHA1.hexdigest(name + today)) # messages.yamlをrubyの形式で読み込む data = YAML.load_file("messages.yaml") # messagesには診断結果リストの配列が入っているので # それぞれの配列に対して診断結果を求める # この時点で msgs は以下のような値を持つ # => ["1つ目の診断の結果", "2つ目の診断の結果", "3つ目の診断の結果"] msgs = data["messages"].map do |msg_list| get_message(msg_list, num) end @name = CGI.escapeHTML(name) # sprintf(data["format"], msgs[0], msgs[1], msgs[2]) と同意 @result = sprintf(data["format"], *msgs) erb :result end
アプリのコードを変更したのでサーバを再起動して確認してみましょう。 またmessages.yamlを変更してすぐに結果に反映されることも確認してみましょう。
今のところ「~診断」という1つの診断をアプリ内で作って来ました。
ただ今後診断内容を増やしたいと思った時に毎回別アプリで構築するのは面倒です。
そこで1つのアプリ内で複数の診断ができるよう修正しましょう。
手順は以下の通りです。
前回のmessages.yamlを一段回階層を下げて、その上に診断名のキーとなる文字列を定義します。
また今までは定数として持っていたタイトルも設定ファイル内で定義するように変更します。
default: title: "リア充診断" format: "あなたのリア充レベルは%dで、%s後にはレベル%dになるでしょう!" messages: - [0, 1, 50, 100, 1000, 10000, 1000000] - ["3秒", "10分", "1週間", "半年", "1年", "50年"] - [-1000, -100, -50, 0, 1, 50, 100, 1000, 10000, 1000000] programming: title: "プログラミング言語診断" format: "あなたが使うべき言語は%sで、PaaSは%sがいいでしょう!" messages: - ["Ruby", "Java", "Clojure", "JavaScript", "Perl", "PHP", "Scala", "Python"] - ["Nifty C4SA", "DotCloud", "Windows Azure", "Google Appengine", "Heroku"]
次にapp.rbを新しい設定ファイルに対応させます。
今回は診断毎のタイトルを設定ファイルで持っているのでトップページを表示する際にも
設定ファイルを読み込む必要があり、結果ページでも勿論設定ファイルを読み込みます。
このように複数のルートで共通の処理がある場合にはbefore
, after
ルートが便利です。
その名の通りbefore
は各ルートが実行される前に、after
は実行された後に共通して実行されるルートです。
# -*- encoding: utf-8 -*- require "rubygems" require "sinatra" require "erb" require "cgi" require "digest/sha1" require "yaml" def get_bytes_total(s) total = 0 s.each_byte do |i| total += i end total end def get_message(arr, num) arr[num % arr.size] end # ここまで変更なし before do # kindパラメータ: default以外の診断をする場合に指定 # 未指定の場合には "default" が使われる kind = params[:kind] || "default" # 今回は英字のみに限定して不正な文字列が入りこまないようにする halt 500, 'error' unless /^[a-zA-z]+$/ =~ kind # 設定ファイルを読み込み data = YAML.load_file("messages.yaml") # 設定ファイル内に kind に対応する定義が存在すればそれを使う # 存在しなければ "default" を使う @kind = (data.include? kind) ? kind : "default" @def = data[@kind] end get "/" do # 表示するタイトルを設定 @title = @def["title"] erb :index end get "/result" do name = params[:name] today = Time.now.strftime("%Y%m%d") num = get_bytes_total(Digest::SHA1.hexdigest(name + today)) # 診断結果は @def 内のmessagesから取得するよう修正 msgs = @def["messages"].map do |msg_list| get_message(msg_list, num) end # タイトルを設定 @title = @def["title"] @name = CGI.escapeHTML(name) # formatも @def から取得 @result = sprintf(@def["format"], *msgs) erb :result end
最後にテンプレートを修正しましょう。主に追加になった kind パラメータの扱いに関する修正です。
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <link rel="stylesheet" type="text/css" href="/bootstrap/css/bootstrap.min.css" /> <!-- 定数ではなく @title を出力するよう修正 --> <title><%= @title %></title> </head> <body> <div class="container"> <%= yield %> </div> <script type="text/javascript" src="/bootstrap/js/bootstrap.min.js"></script> </body> </html>
<div class="hero-unit"> <!-- 定数ではなく @title を出力するよう修正 --> <h1><%= @title %></h1> <p>Twitterのアカウント名から~を診断します。</p> <form method="GET" action="/result" class="well form-inline"> <!-- 結果ページへ kind パラメータを引き継ぐよう修正 --> <input type="hidden" name="kind" value="<%= @kind %>" /> <label>アカウント名 <input type="text" name="name" /></label> <button type="submit" class="btn btn-primary">診断する</button> </form> </div>
<div class="page-header"> <h1><%= @name %>さんの診断結果</h1> </div> <div class="well well-large"> <p><%= @result %></p> <!-- サイトへのURLにkindパラメータを追加 --> <p><a target="_blank" href="http://twitter.com/?status=<%= @title %>: <%= @result %> http://<%= request.env['HTTP_HOST'] %>/?kind=<%= @kind %>"> <i class="icon-share"></i>Twitterで共有する </a></p> </div> <!-- トップページのURLにkindパラメータを追加 --> <p><a href="/?kind=<%= @kind %>"><i class="icon-home"></i>トップへ戻る</a></p>
ではサーバを再起動して確認してみましょう。
http://.../ でデフォルトの診断が、http://.../?kind=programming でプログラミング診断が
表示されることが確認できるでしょうか?
今回はハンズオンの時間が90分しかないということで簡単なアプリの開発だけでしたが、
C4SA + Ruby + Sinatraでの開発フローはわかっていただけたと思います。
プログラムの実行環境をすぐに利用できるのがPaaSの強みなので
これを機にウェブアプリ開発に興味を持っていただけたら幸いです!