3. Chanko、Ridgepoleを試してみる
前回の記事でRails周りの環境が出来たので、いよいよChankoとRidgepoleを試してみる。だが、その前に画面を作らなければ行けないの適当にでっち上げる。erbとslimは試していたがhamlは初めて書く事になった。そしてやっぱりhtmlを作成するのが一番時間がかかった・・・。Emmetとかでサクサクっと作れる人がいるのであればすごく羨ましい能力だと思う。
まずはChankoから。詳しくは本家のドキュメントを見てもらった方がいいが、本番環境でプロトタイピングしちゃおうというフレームワークだ。「Unit」という単位でモジュールを増やしていき、管理を行う。既存のプロダクトソースとは基本別ファイルに作成することになる。そのUnitが有効になる条件等もそのUnitに記載する。プロトタイピング出来る部分は細かく指定することが出来、コントローラのアクション単位や、renderするテンプレートやビューで使えるヘルパーメソッド、モデルに機能を追加することも出来る。そして既存にアクションがまだない場合でも「unit_action」を使って、プロトタイプを追加することも出来る。
やりたい事としてプロトタイピングなのだが、やはり本番想定でデータがあった方がいいという理論でやっている都合上、当然本番環境で動かすことになる。もちろんUnitが有効になる条件をIPアドレスやユーザ単位等で制限は入れることにはなる。その為Unitが有効でないユーザ用のソースを共存させる必要がある。かつ、念の為プロトタイプでエラーが発生した場合もフェイルオーバーして既存のソースが動くようになっている。デフォルトではこの機能はproduction環境だけなので、開発中は通常通りエラーが出る。
そしてルール上はプロトタイプにはテストを書かないで良いようだ。これはプロトタイプというのもあるし、スピード感を出す為とのこと。こういった考えは本当に関心する。 せっかくなのでproduction環境でraiseして本当にフェイルオーバーするか確認したところ、きっちりとフェイルオーバーしていた。(当たり前)
RAILS_ENV=production bundle exec rails s -b 0.0.0.0
そして次はRidgepoleを触ってみる。これもChanko同様にプレゼン資料等で情報だけは知っていた。migrateを差分で記載していくのではなく」あるべき姿」を管理することでRidgepole側が差分を検知し必要なmigrationを行ってくれる。例で乗っているのはarticlesテーブルがあり、そこへ「author」カラムを追加した場合はSchemafileに以下の様に1行追加してapplyするだけでいい。
# before create_table "articles", force: :cascade do |t| t.string "title" t.text "text" t.datetime "created_at" t.datetime "updated_at" end
# after create_table "articles", force: :cascade do |t| t.string "title" t.text "text" t.text "author" t.datetime "created_at" t.datetime "updated_at" end
bundle exec ridgepole -c config/database.yml --apply -f db/Schemafile
仕組み上バージョン番号等は無いので、どこまでmigrateされているのかは確認が面倒そうだ。しかしmigrateが増えまくって行くことや、migration番号の衝突などがなくなるのは良いことだと思う。正しい姿をきちんと管理していれば、適用すればそうなるというの安心感もある。本家のドキュメントには色々なパターンの書き方が乗っており、基本的には機能的問題はなさそう。イレギュラーケースの場合はちょっとどうするのか想像できないパターンもあるが、その場合はその時に対処するのだろう。開発者ブログでも記載されているように、全てに使っているわけではない無いらしい。かつ、「開発者が開発DBに自由にALTER」というのはなかなか斬新な発言だとは感じる。この辺はCI周りとテスト周りがしっかりしているからこその開発手法であり、開発スピードを落とさない結果に繋がっているのだろう。
どちらも素晴らしいフレームワークであり、使えそうなら是非使ってみることをお勧めしたい。時間を作って勉強の為にどちらのソースを読んでみたいと思う。
2. Rails周りの環境構築
今まではPadrinoしか触っていなかったが、今回はRails 4.2でChankoとRidgepoleを試してみる。初Railsでハマる部分も有るとは思うがとりあえず環境構築から。前回のitamaeレシピに必要なpackageを追加。sqliteは多分要らないけど。
%w/epel-release sqlite sqlite-devel httpd-devel curl-devel apr-devel apr-util-devel libffi-devel openssh openssl openssl-devel readline-devel libxml2-devel libxslt-devel mysql mysql-server mysql-devel nodejs npm/.each do |pkg| package pkg end
そしていつものNokogiriを「--use-system-libraries」して入れてからrailsのインストール。
gem install rails --no-ri --no-doc
プロジェクトの作成およびdatabaseの作成。
rails new shugepad -d mysql cd shugepad bundle install --path vendor/bundle bundle exec rake db:create
とりあえず起動の確認。Padrinoではlocalhost以外からも接続出来るようにするには「-h」だったがrailsは「-b」だった。
bundle exec rails s -b 0.0.0.0
Gemfileにhaml用の設定を追加。
vi Gemfiles
gem 'haml-rails' gem 'erb2haml'
現状のerb(と言っても1つ)をhamlに変換。
./bin/rake haml:replace_erbs
とりあえず「bower」を使おうと「package.json」作成してインストール。
npm init npm install bower --save-dev
そして「bower-rails」をgemに追加し、initialize。直接bowerを使わないでもいいようだ。
gem 'bower-rails'
./bin/rails g bower_rails:initialize
bower.jsonはどうするのかと思ったら、「Bowerfile」というRuby DSLで設定するようだ。特に意味はないが「jQuery」と「Moment」を記載。
asset 'jquery' asset 'moment'
デフォルトでは「./vendor/assets/bower_components」に入る。そしてbower.jsonは直下ではなく「vendor/assets/bower.json」に作成されていた。bower.jsonで記載していた設定がRuby DSLになったので記載方法を覚える必要があった。と言ってもそんなに差がないのですんなりいけるが。
後はsemantic-uiも同じく「bower-rails」で入れようと思ったが、検索すると「less-rails-semantic_ui」というのがあるようだ。合わせて「autoprefixer-rails」も追加。これだけだと上手く動かなかったので「less-rails」もさらに追加。semantic-uiがlessに切り替わったのをどう対応するのかと思ったらそういうgemがあった。基本何でも有るのがRailsなのかも。
gem 'less-rails-semantic_ui' gem 'autoprefixer-rails' gem 'less-rails'
とりあえずRails環境構築周りはここまで。余談だが、今回試しでrbenvでsystemにインストールしてみたがやはり権限周りの問題が出るので、ユーザが管理できるところに入れたほうがいいようだ。
1. 環境構築
まずは開発環境の構築。CentOS7には行かずCentOS6.6で一旦Vagrantを使ってセットアップ。
Vagrant init shugepad # Vagrantfile修正 vi Vagrantfile # 起動およびssh接続 vagrant up vagrant ssh
必要最小限のツールをインストール。MacBookProで作業しているが色々汚したくないので今後の作業は全部VM上で行う。
# gitのインストール sudo yum install -y git # rbenvのインストール git clone https://github.com/sstephenson/rbenv.git ~/.rbenv git clone https://github.com/sstephenson/ruby-build.git ~/.rbenv/plugins/ruby-build echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bash_profile echo 'eval "$(rbenv init -)"' >> ~/.bash_profile source ~/.bash_profile # ruby 2.2.3のインストール rbenv install 2.2.3 rbenv global 2.2.3 # bundlerのインストール rbenv exec gem install bundler
itamaeレシピ用のリポジトリをGitHubに作成。リポジトリは基本GitHubの手順通りに作成。originはhttpsで。(キーの発行を横着)
mkdir shugepad-prov cd !$ echo "# shugepad-prov" >> README.md git init git add README.md git commit -m "first commit" git remote add origin https://wata-gh@github.com/wata-gh/shugepad-prov.git git push -u origin master
Gemfile作成して、bundle install。nownabe_nginxを入れるとitamaeが古くなってしまうのでforkして使用。メンテされていないところを見ると恐らく使われていない。
vi Gemfile
source 'https://rubygems.org' gem 'itamae' gem 'itamae-plugin-recipe-selinux' gem 'itamae-plugin-recipe-nownabe_nginx', :git => 'https://github.com/wata-gh/itamae-plugin-recipe-nownabe_nginx.git' gem 'itamae-plugin-resource-ssh_key' gem 'itamae-plugin-recipe-rbenv'
bundle install --path vendor/bundle
itamaeレシピ作成。vagrantユーザで実行していたのでrootになるためにlocalhostにsshでitamae実行。
bundle exec itamae ssh --user root --host localhost roles/web.rb
中途採用で求められること
ここで言っているのは、採用する会社側の話。やはり「即戦力」というキーワードは一番なのかなと。そうなるとその会社の事業や技術を把握していて、ある程度は経験や実績が有ることを示す必要があるのか。また、その企業文化に合う合わないも非常に重要なんだと思う。そういうものに向かって今からでも全力で努力してみよう。
Chrome Extensionを作ってみた
前から1回作ってみようと考えてはいたのだが、優先順位が中々上がらず今まで着手していなかった。そんな中manifestのバージョンが上がり、Content Security Policy (CSP)が今後主流になる(したい)などを聞きいい機会だと作って見ることにした。
まずはどういった作りなのかを把握するために本家のドキュメントへ。非公式の日本語訳があるので、日本語が良い方はこちらも参照してください。
なんとなくChromeのExtensionは使っていて、アドレスバーの中に出るタイプとアイコンとしてアドレスバーの隣に出るのは知っていた。アドレスバーの中のタイプはpage actionsと呼ばれていて、特定のページでアイコンを表示、非表示するのに適しているとのこと。そしてもう1つのアドレスバーの隣に出るアイコンはbrowser actionsと呼ばれている。どちらもツールチップ、バッジやポップアップをつけることが出来る。
作り方としては以下の流れになる。
- マニフェストを作成
- アイコン、HTMLやJavaScriptを作成
- 拡張機能でデベロッパー モードにする
- 「パッケージ化されていない拡張機能を読み込む...」から2で作ったファイルのディレクトリを指定し読み込む
- 修正したらリロードで反映しながら開発していく
必要な技術要素としてはマニフェストファイルを書くためにJSONとHTMLとJavaScriptのみ。イメージとしてはWeb画面を作っている感じなので今までのWebサイトの構築のノウハウをそのまま使うことが出来る。ディレクトリ構成を作って画像やらを入れておけばアイコンとして使えたりもする。
マニフェストファイルは作りたいExtensionの設定を以下の様に書いていく。
{ "name": "test", // 拡張機能名 "version": "0.0.1", // バージョン:公開する時に必ず増やして行かなければ行けない値 "icons": { "48": "img/icon-on-48.png", "128": "img/icon-on-128.png" }, "description": "sample extension", // 拡張機能の説明 "background": { // バックグラウンドで動く処理(Chrome起動時に呼ばれる) "scripts": [ "chrome_ex_oauthsimple.js", "chrome_ex_oauth.js", "jquery.min.js", "background.js" ] }, "browser_action": { // browser actionsを作る場合に必要な指定 "default_title": "", "default_icon": "img/icon-off-19.png", "default_popup": "popup.html" // 押した時にポップアップメニューを表示する場合は指定 }, "manifest_version": 2, // マニフェストのバージョン、そろそろ1はサポートされなくなるので2を指定 "web_accessible_resources": [ // セキュリティの為、Ajax等で外部リソースにアクセス出来るファイルを指定しなければいけない "chrome_ex_oauth.html", "popup.html" ] }
アイコンの画像と指定したHTMLとJavaScriptを書くだけ。JavaScriptにはAPIが提供されていて、ブラウザに対して色々な処理が出来る様になっている。当然HTMLとJavaScriptなのでデバッグもブラウザで出来る。ポップアップ等のHTMLを含む場合は「chrome-extension://[ID]/[デバッグ対象HTML]」でアクセスしてデバッグする ことが出来る。「ID」はExtensionとして追加しないと発番されないので、「拡張機能」でデベロッパー モードにして自分の作成したExtensionを追加する。
バックグラウンドで動く処理もあるのでそれをデバッグするにはchrome://chrome/extensions/からデベロッパーツールを起動する形になる。
APIは基本的には値をreturnするのではなく、結果を非同期でコールバック関数の引数に渡す形式になっている。Cookie情報を取得する関数もコールバックなので記述が若干面倒だと感じた。単純にCookieの値を取得して何か処理を行いたいだけなのに、わざわざコールバック関数を作ってそこで処理しないと行けないためだ。コールバックにしている思想としては重い処理で処理をブロックしないように非同期で行う為だとは思うのだが・・・。
JavaScriptはHTMLの中にscriptタグを作ってインラインに書くことは出来ず、必ず別のjsファイルとして分けることが必須となっている。ちょっと面倒だがセキュリティ観点の為だそうだ。
Extensionを公開するにはChrome ウェブストアのデベロッパー登録($5.00)が必要。そして「デベロッパーダッシュボード」からExtensionを登録する。Extensionに必要なファイルを固めたのをアップロードするのだが、Chromeで「拡張機能のパッケージ化」したファイルでは拡張子がzipではないのでダメだと言われた。なので拡張子をzipに変えてアップロードした。
そして通常のExtension同様にChrome ウェブストアからインストールすることも出来る。一般公開の前にテスターに公開するというモードもあるので、開発時はこちらを利用するのがいいのかもしれない。ただ、テスターのみ公開でも更新する時はバージョン番号はやはり増やさないと行けないので注意。
今回実装してみて、Extensionの実装によってはChromeが遅くなるというのを理解できた。自分でsetTimeout等でぐるぐるバックグラウンドで動かれたり、リクエストを投げまくったりされてもChrome側で防ぎようが無い為だ。今後はBackground PagesではなくEvent Pagesを使うように推奨されている。必要な時にリソースを確保し、不要な場合は破棄するようだ。このあたりの活動も重くなる対策の1つだろう。
基本APIを見ながらJavaScriptを書くだけなので、非常に簡単に作ることは出来る。だがやはりコールバック地獄になるのは何とかしたいところだ。ただ、逆を言えばそれぐらいで通常のWeb開発者が簡単にChrome Extensionを作ることが出来るのは魅力ではないだろうか。
Scalaを使ってみた
最近はPerlを使っていることもあり、スクリプト言語の良さを理解出来るようになってきた。動的な型で得られるメリットとして生産性の向上というか記述が簡潔になることが上げられると思う。duck typingの柔軟性を知ってしまうと、静的な型の言語は面倒に感じてしまう。自分で書くなら動的の方が生産性が高いのではと本気で思う。そんな中でも大規模開発する上ではJavaは非常に良い言語だと思う。静的な型の強みと、言語仕様のシンプルさが開発者の敷居を下げ、出来上がるコードのブレを軽減することが出来ると考えているからだ。そんなジレンマを抱えながら、ふと思い出したのが「Scala」、「Groovy」と「Xtend」の存在だ。Groovyは前に少し触ったことがあったが、現状では何がトレンドなのかと調べてみるとScalaっぽい。気になっている「Play framework」でもScala使えるみたいだしということでScalaを触って見ることにした。
まずは開発環境ということで「ScalaIDE」を入れた。なぜかEclipse4.2系に入らなかったので、3.7.2に入れた。
最初は軽い気持ちで触ってみようと考えていたが、言語仕様がPDFで191ページ。そして歴史もあるっぽい。(歴史があると何かを調べた時に引っかかった情報が過去のバージョンの情報で今風の書き方で無かったり、最悪動かなかったりもする為、学習コストがかさむ)というわけで今後も時間をかけて触っていかないと覚えられないと感じた。その為とりあえずHello World!的な感じでやってみた。さすがにHello World!ではすぐ終わってしまうので、簡単な課題をすることにしてみた。
CVSにコミットした並び順は順不同でモジュール名の重複がある「モジュール名[半角スペース][リビジョン]」のフォーマットでリスト化されているファイルがある。そこからモジュール名としてユニークな一覧を作り、そのリビジョンはファイルの中で一番リビジョンが新しいのをリビジョンとして読み込んだファイルと同じフォーマットで表示する。
CVSのリビジョンなので今回は1.Xを基本とし、2.Xは無い物として扱う。(マイナーバージョンのみで比較する。)ファイルの中身のイメージは以下。
src/wata/Hoge.java 1.5 src/Moge.java 1.1 src/wata/Fuga.java 1.10 src/wata/Hoge.java 1.4
やってみた結果は以下。
package wata import java.lang.{RuntimeException => RE} import scala.collection.mutable.HashMap import scala.io.Source object Release { def main(args: Array[String]) = release() def release(fn: String = "module.txt") { val fmt = """([^ ]+) ([1-9]+)\.([0-9]+)""" val ml = fmt.r val m = new HashMap[String, Int] val src = Source.fromFile(fn) src.getLines.foreach( (v) => { if (!v.matches(fmt)) throw new RE(v) val ml(f, u, l) = v val r = l.toInt if (r > m.getOrElse(f, -1)) m += (f -> r) }) src.close m.keys.toList.sorted.foreach( (v) => printf("%s 1.%d\n", v, m(v))) } }
これがScalaっぽいのかは分からないがとりあえず解説。
まずは「package」や「import」。
package wata import java.lang.{RuntimeException => RE} import scala.collection.mutable.HashMap import scala.io.Source
基本Javaと同じだがセミコロンが省略出来るので基本書いていない。RuntimeExceptionは長いので別名が定義出来るので省略形(RE)にした「{RuntimeException => RE}」。
次は「object」宣言。
object Release {
Scalaでも通常クラスは「class」で宣言するがstaticが存在しないScalaではstaticメソッド(今回ではmainメソッド)を作りたい場合は「object」して宣言する。mainメソッドは特別だとは思うがobject宣言してもstaticになるわけではなくシングルトンになる。
次は「def」宣言。
def main(args: Array[String]) = release()
これはメソッドの宣言で通常は「def メソッド名(引数名: 型, …): 戻り型 { /* 処理 */ }」の様に書く。戻り値がない場合はUnit(Javaで言うvoid)とも書けるが、Unitの場合は省略出来るので省略して書いている。中括弧「{}」もif文等と同じで1行なら省略可能なので書いていない。
メソッドの引数はデフォルト値を指定出来る。
def release(fn: String = "module.txt") {
「String = "module.txt"」が引数が渡されなかった場合の変数「fn」の値となる。
次は文字列定数定義について。
val fmt = """([^ ]+) ([1-9]+)\.([0-9]+)"""
ダブルクォートを3つ「"""」にすることで、中身に円マーク「\」と改行がエスケープせずに書けるようになる。そして型は無いので「val」で受けているが「var」でも受けることは可能。違いとしては「val」は一度代入すると2度と代入出来ない。(Javaのfinal)
次は関数「r」について。
val ml = fmt.r
次は「HashMap」。
val m = new HashMap[String, Int]
ほぼJavaと同じで型も指定する。
次はファイルの解析部分。
valsrc = Source.fromFile(fn) src.getLines.foreach( (v) => { if (!v.matches(fmt)) throw new RE(v) val ml(f, u, l) = v val r = l.toInt if (r > m.getOrElse(f, -1)) m += (f -> r) }) src.close
「Source」でファイルを読み込み「getLines」で1行ずつ読み込み、それを「foreach」で回している。Scalaの構文の特徴としてメソッド呼び出しの書き方として「オブジェクト.メソッド(引数)」を「オブジェクト メソッド 引数」の様に、空白で区切って呼び出すことも出来る。そして引数がない場合はカッコ「()」も省略出来る。なので前の「fmt.r」も「fmt r」と書くことも出来る。このシンタックスシュガーは1から10までループする時に以下のように書きたいから作ったのだと思う。
for (i <- 1 to 10) print(i + "\n")
この「1 to 10」は「1.to(10)」と同義でScalaでは全てがオブジェクトなので1も恐らくRichIntクラスのtoメソッドを呼んでいることになる。これ以外にも同じような感じで使えるので、なかなか面白い考えだ。
src.getLines.foreach( (v) => { if (!v.matches(fmt)) throw new RE(v) val ml(f, u, l) = v val r = l.toInt if (r > m.getOrElse(f, -1)) m += (f -> r) })
foreachにはメソッドを渡していて、引数はvとして1つ受け取ると定義している。まずはフォーマットチェックしてマッチしなければ例外を投げる。その次は正規表現にマッチしたら各グループを順番に変数「f, u, l」に代入する。つまり「f」には「([^ ]+)」でマッチした値が入り、「u」には「([1-9]+)」にマッチした値が入るという具合だ。これは文字列として来るので大小を比較する為に文字列を「toInt」メソッドで数値化する。その後まとめ用のHashMapにモジュールをキーで入れたリビジョンと比較して大きければ上書きしている。なぜかScalaのHashMapは存在しないキーを「get」すると例外が発生してしまう。させたくない場合は「getOrElse」でない場合に返す値を指定しておく。そして「put」する場合は「+=」で出来る。これは演算子のオーバーライドに見えるが、Scala的には「+=」というメソッドの様だ。
そして最後の表示部分。
m.keys.toList.sorted.foreach((v) => printf("%s 1.%d\n", v, m(v)))
HashMapに作ったユニークなモジュールのキー値だけを「keys」メソッドで取り出し、「toList」メソッドでリスト化して「sorted」メソッドでソートして「foreach」メソッドでループする。先ほどと同じ1つ1つの値は「v」変数に入ってくる。HashMapから「get」するには「m(v)」とも書ける。これは実際は「apply」メソッドを呼んでいることになるので「m.apply(v)」と等価だ。Scalaでは「apply」メソッドを呼び出す時はメソッド名を省略可能としているので「m(v)」と書けるわけだ。「m[v]」と書けた方が個人的にはしっくり来るのだが。
というわけで簡単に触ってみた。感触としては非常に面白い。もう少し長く付き合って行きたい言語ではある。ただ、ある意味とても癖があるので変態言語と認識されて万人受けはしないだろう。書き方も色々あったり、性能問題が発生する可能性もあるので業務では使える場所は選びそう。興味がある人は是非趣味として嗜んでみては如何だろうか。
MessagePackを使ってみた
HTML(画面)とサーバサイドでやり取りする形式としては、リクエストなら単純ならパラメータでも良いが、レスポンスにはJSONやXMLを使うことが多い。まあ、HTMLを返すというパターンも場合によってはある。JSONにしろXMLにしろ、データが膨らみがちで、ちょっとぐらいなら良いが、大量データやパフォーマンスを意識しだすと改善したくもなる時がある。Googleが以前Protocol Buffersを出していたが、それとは別のMessagePackを今回使ってみた。
特徴としては効率的なバイナリシリアライゼーションフォーマットであり、JSONの様に様々な言語で、データ交換出来るが、データが小さくて済み、変換も早い。簡単な例だと小さい数値、例えば10ならJSONだと2bytes(2文字)かかってしまうが、バイナリなら1byteで済む。
こういったバイナリデータのやり取りはAjaxなどでは不向きだと思っていて、どちらかと言うと生のソケット通信を行うアプリ(もしくはゲーム)やWebSocketと相性がいいのだろうと思う。だが、使うだけでは面白く無いのでAjax(jQuery)でMessagePackをやり取りするにはどうすればいいのかを試してみようと考えた。
まずは受信から。JavaScript側はmsgpack-javascriptを使い、サーバ側はData::MessagePackを利用した。
$.ajaxSetup({ converters: { 'text msgpack': function(packed) { if(packed != '') return msgpack.unpack(packed); return ''; } } }); $.ajax({ type: "GET", url: "/get_msgpack", dataType: "msgpack", success: function(data) { console.debug(data); } });
いちいちコールバックで変換するのもアレなのでConvertersを利用してmsgpackをunpackするようにしてやるとあまり気にしなくて良い感じになる。サーバサイドとしてContent-typeとしては「application/x-msgpack; charset=x-user-defined」の様にcharsetを指定しないと上手く行かなかった。
データとして以下のJSON的な意味の内容を送ったところ22bytesだった。(以下のJSONは32bytes)
{"hoge":"hoge_value","num":"13"}
次は送信。
msgpack.upload('/send_msgpack', {data: {'k1': 'v1'}}, function(data, option, resp) { console.debug('ok'); });
jQueryでバイナリデータを送信する方法がわからなかったので、結局msgpack.jsのuploadを使った。中身はMessagePackしたデータをBASE64エンコードしてから送信するという仕組み。なのでサーバ側ではBASE64を解除してからunpackする事で対応。
リアルタイムを求められるネットゲームや、大量データを扱う場合には非常に優れたフォーマットであり、ネットワーク越しでは無くファイルとして保存する形式としても使えるだろう。下手にXMLとかで保存するよりよっぽど小さくなると思われる。ただ、やはりバイナリなので転送されているデータを見ても人間には内容がわからない、文字コードの変換も自分でやらないといけない等のデメリットもある。条件や用途によって分けて使い分ければ上手くメリットを享受出来るだろう。