bower に table のカラムの表示/非表示を自由に選べるライブラリを追加してみた

皆さん。こんにちは。MUGENUP のosadaです。

電車を降りようとしたら、スマホを線路に落としました! 駅員さんにお願いして取っていただいたところ、 なんと画面が割れることもなく無事でした!ラッキー! そんな運が良い osada がお送りする今回は、bower のお話です。

js のパッケージ管理ツール Bower。 皆さん使っていらっしゃいますか? 弊社では、bowerパッケージを自動でgemにしてくれるRailsAssetsというサービスを使って、 Rails上でbowerを使っています。

今回はそんなbowerのパッケージを作ってたみたので、レポートします。

要点

  • js ライブラリのデモページは、github pages が良かった
  • githubのソースを、<script> として読み込む rawgit
  • RailsAssets に bower を直ちに反映させたければ、Add Componet

表示するカラムを自分で決められるTable 用 jQuery プラグイン

弊社はクリエイティブの幅広い部分をカバーする会社ですので、 多くの案件と複数の職種が入り乱れて、ゴールを目指します。 すると、同じ案件の情報でも、見たいものが異なるのは自然なことです。 さらにいうと、同じ職種の人でも、見たいものが異なるということも、 よくあります。

そこで作ったのが、table_as_u_like です。

  • Table as u like
  • 機能
    • テーブルの th を読み込みトグルボタンにする
    • トグルによって、カラムの表示/非表示が切り替えられる
    • session によって、個人ごとに状態を維持する

coffeescript で 45行の簡単なプラグインですが、便利に使っています。 今回はこれをbowerに登録しました。

bower に登録する前に

Rails用に開発したので、ライブラリとして公開するのに必要なものが2つありました。

  1. Railsの代わりにcoffeescriptコンパイルする環境
  2. デモ用のページ です。

gulp で coffee を コンパイル

coffeescriptコンパイルだと、 Gruntが思いつきますが、 今回は gulp.js を使ってみました。

設定ファイルが書きやすい、速い、など、Grunt を改良したツールということでしたが、 1つの coffeeコンパイルする程度だと、あまり違いが感じられませんでした。 今度は大規模なものに使ってみたいところです。

  • インストール
npm install -g bower
npm install -g gulp
npm install --save-dev coffee-script
npm install --save-dev gulp-coffee
  • 設定ファイル
gulp.task 'coffee', () ->
  gulp.src files.coffee
    .pipe coffee()
    .pipe gulp.dest('lib')

設定ファイル、というより、シェルスクリプト、といった感じでした。 勿論watchもあるので、安心です。

github pages でデモ用のページを作る

動的な要素が不要な場合、github pagesはとても便利です。

githubsettings から github-pages を有効にすると、 gh-pages というブランチが作られます。 以降はそのブランチへpushすることで、反映されるという簡単構造です。 (多少遅延があります)

jekyll が使えるということでしたので、 大きなページを作る場合は、そちらの方が良いですね。 今回は1ページだったので、 ローカルでサーバを起動して、 開発して、push という手順でした。

こんなときはpythonが大活躍。

python -m http.server

小さなものなら、これで十分ですよね。

github に置いたまま <script> で読み込む rawgit

さて、そんな便利なgithub-pagesですが、唯一困ったのか、 ライブラリの読み込みです。

jquery は cdn を使って読み込めます。

<script src="//code.jquery.com/jquery-2.1.1.min.js"></script>

ですが、github においてあるファイルは、そのままでは読み込めません。 例えば、こう書くと

<script src="https://raw.githubusercontent.com/osdakira/table_as_u_like/master/lib/jquery.table_as_u_like.js"></script>"

こう怒られます。

Refused to execute script from 'https://raw.githubusercontent.com/osdakira/table_as_u_like/master/lib/jquery.table_as_u_like.js' because its MIME type ('text/plain') is not executable, and strict MIME type checking is enabled.

github がこれを禁止していて、理由は、遅いから、 *1 ということのようです。

この制限を迂回するサービスがrawgit.comです。 ヘッダを付与することで、githubへのダイレクトなアクセスを行い、 かつ、キャッシュすることでスピードを上げているようです。 デモの用途であれば*2、使用できるようです。

f:id:mgnup:20141202095930p:plain

そうしてできたデモが Table as u like です。 index.html一枚で、js, css は全て外部から読み込んでいます。

bower に登録する

bower の登録については、既に多くの記事があるので(皆さん、ありがとう!)、概要のみ記載します。

  1. bower をインストール
  2. bower init で対話形式でbower.jsonを作成
  3. bower register で登録
npm install -g bower
bower init
bower register table_as_u_like git@github.com:osdakira/table_as_u_like.git`

とても簡単でした。

RailsAssets に Add Componet する

ところが、bowerには登録されていても、RailsAssetsには出てきませんでした。 RailsAssetsへの登録は非同期で行われているようで、遅延があるようです。 この場合、AddComponetを使うことで、 直ちに反映させることができました。

f:id:mgnup:20141202095955p:plain

bower が自動的にgem になる。なんて便利なんでしょう!

まとめ

以上がjsライブラリbowerに登録してみたレポートです。

  • js ライブラリのデモページは、github pages が良かった
  • githubに置いたまま、<script> として読み込む rawgit
  • RailsAssets に bower を直ちに反映させたければ、Add Componet

そうそう、最後にもう一つ。 線路にものを落とした時は、 何番線の何号車の位置に落としたかを伝えましょう。 びっくりしますし、焦ってしまいますが、 それを伝えることができたら、話はスムーズです。 (私は上りと下りを間違えて、ウロウロしてしまいました)

では、良い geek ライフを!

参考

*1:Heads up: nosniff header support coming to Chrome and Firefox

*2:アクセスが少なく、負荷をかけないもの

【Rails】Scaffoldされたコントローラを読んでみる

こんにちは、MUGENUPの倉成です。 僕はRailsを使い始めてあと少しで丸2年になるのですが、初めの頃は覚えることがたくさんあって、とても大変だった記憶があります。

今回はそんな中でも、Railsで初めてWebプログラミングをする人向けに、コントローラのアクションであるnew,create,showについて解説してみたいと思います。

この記事ではソースコードは説明のため、実際にscaffoldされたコードを抜粋、もしくは省略されているコードを挿入して取り上げます。 なお、使用するコードは

rails g scaffold user name:string email:string postal_code:string address:string

で生成されたコードを元にしており、モデル・ビュー・コントローラはこれを使うことにします。

それではnewから見て行きましょう。

def new : 新規登録フォームを作成

def new
  # (1) Userオブジェクトを生成
  @user = User.new
  # (2) フォームを生成(Rails4では省略されている)
  render :new
end

このアクションでは(1)@userを作成し、(2)では(1)で生成したオブジェクト(@user)とテンプレート(new.html.erb)を使ってhtmlを生成しています。

図にすると以下の様なイメージです。

f:id:mgnup:20140828233234p:plain

今回は@userの全ての属性の初期状態がnilなので、空のinputフォームが生成されています。

さて、次はcreateアクションを見て行きましょう。

def create : 登録 or 差戻し

scaffoldされたcontrollerは複雑で悩むポイントはたくさんあると思うのですが、特に難しいのが行数の多いcreateだと思います。 今回は簡単のためformat.htmlformat.jsonそしてnoticeは一旦脇に置いておいて

def create
  # (1) フォームから渡されたパラメータを使用し、Userオブジェクトを生成
  @user = User.new(user_params)
  # (2) DBへ保存(を試みる)
  if @user.save
    # (3) 新規登録されたユーザーページヘリダイレクト
    redirect_to @user
  else
    # (3') @userを使って、もう一度フォームを表示
    render :new
  end
end

のコードを追いましょう。

(1) @userを生成

@userならnewするときに作ったよ」と思うかもしれません。

しかし、ブラウザからリクエストとして送られてくるのは、フォームに入力されたテキストデータのみのため、改めて@userを生成する必要があります。 同じコントローラでも「一回ごとのリクエストでインスタンス変数が共有化されない」というのはWebアプリが初めてだと取っ付きづらく感じる原因かもしれないですね。

f:id:mgnup:20140828234344p:plain

create内で、フォームからの情報を使って、@userを作ります。

f:id:mgnup:20140828234945p:plain

(2) DBへ保存

(1)で生成した@userの値を検証し、DBへの保存が成功した場合には、(3)redirect_to、失敗した時には(3')renderをします。 saveが失敗するケースとしては「ユーザー名が空白です」「郵便番号は7桁で入力して下さい」のように、バリデーションに引っかかることが多いかと思います。

(3)登録に成功: 登録内容を表示する

ユーザーの登録に成功したら、そのユーザーの登録内容を表示します。 ここでは登録に成功した@userのページを表示するため

redirect_to @user
# 以下と同じ意味
# redirect_to user_path(@user)
# redirect_to /users/#{@user.id}

としています。

しかしnewにあったrenderと違い、redirect_toはテンプレートを使用してHTMLを生成するわけではありませんredirect_toはブラウザに「localhost:3000/users/1にアクセスしなおしてね」と教えるだけです(urlはドメインlocalhost:3000@user.id=1の場合)。

ブラウザはサーバーからのredirectの指示に従ってlocalhost:3000/users/1にアクセスすることで、詳細ページが表示されることになります。

f:id:mgnup:20140829012717p:plain

renderじゃないのはなぜ?

さて、ここでredirect_toを使用していて、render :showとしないのはなぜでしょう? 上の図でも真ん中の2本の矢印は不要に思えますし、create内で@userが既に存在しているので、再度DBから読み直すのは無駄な気がします。

実は、saveに成功時にrender :showとしてしまうと、ページをリロードするたび、新規ユーザー登録処理が発生してしまうという問題があります。 Railsの視点から説明すると、ブラウザをリロードした時には、最後にrenderを呼び出したアクション、今回の場合はcreateアクションが呼ばれてしまうことになります。*1

下はcreateアクションでrender :showをした後、ブラウザをリロードさせた図です。多重登録を防ぐため「フォーム再送信の確認」ダイアログが表示されているのが分かるかと思います。

f:id:mgnup:20140829004303p:plain

今回はscaffoldのコードに習いshowにリダイレクトしましたが、もちろん一覧ページであるindexに飛んでもいいですし、ホームのページ(localhost:3000/)に飛ぶことも多いと思います。 DBの更新が発生した後には、ページを表示するだけのアクションにリダイレクトするということを覚えておきましょう。

また、このpost → redirect → getの一連の流れはその頭文字を取ってPRGパターンと呼ばれているようです。この記事の説明でしっくりこないなぁという方は、ぜひPRGパターンで検索してみてください。

(3')登録に失敗: もう一度入力フォームを表示する

入力内容に不備があった場合、もう一度フォームを表示して、再度情報を入力してもらうため、フォームのテンプレートであるnew.html.erbrenderします。createアクションでrender :newをすると、保存に失敗した@userインスタンス変数をそのまま使えるため不正な項目のみを再度入力してもらうことが出来ます。

f:id:mgnup:20140828235346p:plain

redirect_toじゃないのはなぜ?

一方、再度フォーム画面を表示さるためにredirect_to "users/new"を使ってしまうとフォームは表示させることが出来るのですが、ユーザーがこれまでに入力した内容が全て消え、再度全ての項目を入力してもらわなければなりません。

newの処理を思い出すと、@userの生成時に@user = User.newとしているので、白紙のフォームが表示されてしまうのは納得できるかと思います。

ユーザー登録時に「一箇所ミスがあるくらいで、全ての内容を再入力させるなー!」という経験は一度くらいあるはずですが、このようなシステムを作らないためにはredirect_toではなくrenderを使う必要があるのです。

def show : 登録情報を表示

ユーザーの情報を表示するのがshowの役割です。

createからredirectしてくる場合でも、createで使用した@userは使用できないため、改めてDBからデータを取得し、@userを生成する必要があります。

  def show
    # Rails4ではbefore_actionで呼ばれる
    # (1) params[:id]のユーザーを取得
    @user = User.find(params[:id])
    # (2)Viewを生成(Rails4では省略されている)
    render :show
  end

f:id:mgnup:20140829011625p:plain

@userを作って、View(show.html.erb)のhtmlを生成するということで、フォームなのかテキストなのかという違いがあれど、図はnewと似たものになります。

最後に

さて、今回は僕がRails初心者だった頃に「renderredirect_toは何が違うの?」とか「ここの@userって、こっちのアクションで使えないの?」とか、よく混乱していた部分を中心にRailsのコントローラを解説してみました。 この記事を読んで少しでも理解が深まれば嬉しいです。

この記事はDavid Griffiths著『Head First Rails』の3章を参考にさせていただきました。 この本はRails2系が対象と、やや古くRails3/4では、写経したコードが動かないことが多々あるのですが、Railsの仕組みを知る上ではかなり良書だと思います。 リファレンス的には使用できず、回り道に感じるHead First Railsですが、Railsで初めてWebプログラミングをするという方はこの本から始めてみるとよいかもしれません。

*1:HTTP的な視点で見ると、localhost:3000/usersに対し、再度POSTが発行されている状態です

enctype='multipart/form-data'ってなんだ?

こんにちは、MUGENUPアルバイトの倉成です。

今回は僕が前々から気になっていた、フォームからファイルを送信するときのおまじないenctype="multipart/form-data"について調べてみたので、得られた知識をまとめて見ようと思います。

また、マルチパートの情報を検索していると、HTMLのフォームだけではなく、メールのマルチパートの情報に当たることも多くありました。 調べてみると、HTMLの仕様と電子メールの仕様が似ているのは、どうやら歴史的な経緯があるようなので、後半ではインターネット成長の歴史についても少しだけ触れてみようと思います。

multipart/form-data: ファイルを送るおまじない

それでは、フォームでファイルをアップロードするシチュエーションを考えましょう。

ファイルアップロードをする場合input要素は<input type="file" />を使い、その親のform要素には以下のようにenctype="multipart/form-data"と書く必要があります。

f:id:mgnup:20140827094606p:plain

<form action="URI" method="post" enctype="multipart/form-data">
  <input type="file" name="file" />
</form>

さて、このおまじないenctype="multipart/form-data"はなぜ必要なのでしょうか?

Firebugを使ってリクエストを見る

今回はenctypeの有無によりHTTPリクエストのパラメータがどのように変化するかFirefoxプラグインであるFirebugを使って調べてみようと思います。

なお、今回使用するhtmlのソースコード

<html>
  <!-- enctype指定あり -->
  <form action="http://localhost:4567" method="post" enctype="multipart/form-data">
    <input type="file" name="datafile" />
    <input type="submit" name="submit_btn" />
  </form>
  <!-- enctype指定なし -->
  <form action="http://localhost:4567" method="post">
    <input type="file" name="datafile" />
    <input type="submit" name="submit_btn" />
  </form>
</html>

で、送信するファイルは

hello world!

hogehoge
fugafuga

とします(添付ファイル名はsample.txtです)。

さて、それぞれを試したところFirebugは以下の画像のようになりました。

enctype指定あり

f:id:mgnup:20140826195719p:plain

enctype指定なし

f:id:mgnup:20140826195727p:plain

enctype指定ありでは、Content-DispositionContent-Typeなど添付ファイルに関する情報とともに、ファイルの本文の情報が存在することが分かります。今回はテキストですが、画像や音声などバイナリの情報でも、同様にここにデータが格納されます。 また、--------で始まる行があることも特徴的です。

enctype指定なしでは、filedataにファイル名の情報のみが与えられていて、添付ファイルの情報は見当たりません。 どうも、inputエリアにファイル名のみを指定した

f:id:mgnup:20140825232727p:plain

と同様な印象を受けます。 つまり、enctype="multipart/form-data"を指定しない場合、添付ファイルの情報を送信できていないので、サーバー側では添付ファイルを扱えないという事になりそうです。

なお、-----------------------------146617270317...のような区切り文字をboundaryと呼び、この区切りを使って複数のパートに分割することからマルチパートと呼ばれているようです。boundaryを使うと入れ子構造も再現できるようです、興味のある方は以下のリンクが詳しくまとまっているので御覧ください。

フォームよるファイルアップロードの仕様 - Jakarta Commons FileUploadの利用手順

メールの場合は

boundaryを使ってファイルの情報を送信するというのがmultipart/form-dataの仕組みでした。 しかし、冒頭で述べたようにメールにも同様のマルチパートという仕組みがあるようです。 では、これまで見てきたフォームのマルチパートはメールのマルチパートとどのような関係があるのでしょうか?

ここでは「Webを支える技術」の引用をさせていただきます。

HTTPの最初のバージョン0.9にはヘッダがありませんでした。HTTPの仕様策定が進められるに従って、HTTPで転送する本文のメタデータを表現するために電子メールのメッセージ仕様(RFC822)のヘッダ形式を借りてくる形で追加されました。

Webを支える技術 P.126

つまり本記事では、formつまりHTTPのマルチパートを先に紹介していたのですが時系列で言うとこれは逆で、HTTPの仕様策定時に電子メールの仕様を参考にしたというのが歴史的な流れのようです。マルチパートに限らず、HTTPの仕様が電子メールの仕様に似ているのはこのような経緯があるためなのですね。

f:id:mgnup:20140825232655p:plain

なぜ電子メールの仕様にマルチパートが加わったのか(MIMEが策定されたのか)については

MIME の基礎

で詳しく解説されています。

ざっくりというとMIME策定前の仕様ではメールのヘッダ・本文がASCII限定で、日本語など非ASCIIの言語が正しく認識できなかったり、添付ファイルがうまく扱えなかったという問題があったようです。

multipart/alternative:textとhtmlを同時送信

「マルチパート メール」などで検索してヒットする記事にはmultipart/alternativeに関する記事も多いようです。

multipart/alternativeはマルチパートの構造を利用して一つのメールにtext版とhtml版の2つの内容を含めて送信し、メーラーの設定次第でtexthtmlを選択して表示できるものです。

-----------------------------1948084979928559891542425288
はじめに

こんにちはMUGENUPの倉成です。

-----------------------------1948084979928559891542425288 
<h1>はじめに</h1>

<b>こんにちは</b>MUGENUPの倉成です。
-----------------------------1948084979928559891542425288--

こんなメールを送ると、メーラーの設定次第でプレーンテキストでもリッチテキストでも表示できるようになるわけですね。

さいごに

enctypeを見るたびに「これ、なんだろうなぁー」と思っていたことから始まり、調べて行ったらインターネット成長の歴史的な経緯まで知ることができて、楽しい夏休みの自由研究となりました。 RFCの仕様も機会があればもう少し読んでみようと思います。

HTTPの仕様やRFC策定の歴史については山本陽平さんの「Webを支える技術」を参考にさせていただきました。 URI設計やステータスコードJSON、HTTPメソッドの使い分けなど基礎的な情報が網羅されているので、特にWebプログラミングが初めてのエンジニアさんにおすすめの一冊です。

意外と簡単。HTML5のデスクトップ通知を実装してみる

こんにちは、MUGENUPの倉成です。 最近はWebアプリでもデスクトップ通知が出来るものが増えていますよね。 今日はそんなデスクトップ通知の実装を取り上げてみようと思います。

便利なライブラリ

デスクトップ通知はブラウザによって実装が異なり、各ブラウザの対応は手間がかかるので、今回はクロスブラウザ対応を簡単にできるHTML5-Desktop-Notificationsを使います。 他のデスクトップ通知のライブラリにはnotifyもあり、こちらもHTML5-Desktop-Notificationsと同じくらいのStarが付いているようです。

使い方

さて、ここからはHTML5-Desktop-Notificationsの使い方をREADMEにそって

  1. ブラウザ対応状況の確認
  2. ユーザーに通知の許可を求める
  3. 通知を発行

の3段階で説明していきたいと思います。

なお、本記事はこのコミット時のコードを対象にしており、今後の開発により変更が発生する場合があります。

また、HTML5-Desktop-NotificationsのサンプルコードはAngular.jsベースとなっており、馴染みのない方も多いと思うので、gistにサンプルコードを書いてみました、こちらも合わせて見ていただければと思います。

https://gist.github.com/kuranari-tm/e8d8b6411b90da10910e

Step1(ブラウザが通知に対応しているかチェック)

まずは以下のコードでブラウザがデスクトップ通知に対応しているか確認しましょう。 コードは

notify.isSupported // ブラウザが対応していればtrue, そうでなければfalse

です。なおnotifyHTML5-Desktop-Notificationsで定義されているグローバル変数です。

Step2(ユーザーに通知を求める)

デスクトップ通知は、ユーザーから通知の許可をもらわなければ通知が発行できません。 次は、notify.permissionLevel()ドメインに対する通知の許可状況をチェックします。

許可・拒否の状態は

notify.PERMISSION_DEFAULT // 通知が許可されていない
notify.PERMISSION_GRANTED // 通知が許可されている
notify.PERMISSION_DENIED  // 通知が拒否されている

の3状態で、PERMISSION_DEFAULTとなっている場合は

notify.requestPermission()

で、ユーザーから通知の許可をもらいましょう。

f:id:mgnup:20140820000117p:plain

DEFAULTDENIEDの違いですが、notify.requestPermission()を実行時にDEFAULTでは上のような「デスクトップ通知の表示を許可しますか?」のメッセージが表示されますが、DENIEDではこのメッセージは表示されず、ユーザーがブラウザの設定を変更するまでは通知機能を使用することが出来ません。

その場合、例えばChromeでは以下の箇所からドメインに対する通知の許可をユーザーに行ってもらわなければなりません。

f:id:mgnup:20140820000105p:plain

通知の設定はドメイン単位で保存されるため、初回一度だけ許可をもらえば、その後は通知を自由に発行することが出来ます*1

Step3(通知を発行)

さて、Step2までで設定が終了したので、notify.createNotificationで通知を発行します。

notify.createNotification(String title [, Object options])

この関数を実行すると、MacChrome(version.36)では以下のような表示がされます。

f:id:mgnup:20140820000110p:plain

optionsに渡せるパラメータは

body, icon, tag, timeout

の4つです。

第一引数のtitle、そしてオプションのbody,iconは上の通知画像を見れば、大体どこに対応するか分かるかと思います。ただし、iconが必須パラメータになっていることには注意しましょう。

tagはユニークな値を設定することで、複数のタブでページを開いていた場合に、開いているタブと同じ数の通知が発行されることを防ぐことが出来ます。

timeoutはREADMEには通知が閉じるまでの時間を設定できると書いてありますが、README通りに設定を行っても求める挙動が実現できません。 一定時間経過後に自動で閉じる設定をするには

notify.config({autoClose: 1000}); // 1000[ミリ秒後]に通知を閉じる

とする必要があります。issueに上がっているようにみえるのですが、ちょっとハマりどころです。

なお、通知をクリックした時に何らかの処理をしたいといった処理は本家では実装されていませんが、Fork先では実装されているものもあるようなので、必要があればこちらを参考に機能を追加しても良いかもしれません。

終わりに

gistのサンプルコードでもjsは30行程度で、とても簡単にデスクトップ通知(しかもクロスブラウザ対応まで)を実装することが出来ました。

Webサービスにデスクトップ通知があることで使い勝手が格段に良くなるケースも少なく無いと思うので、意外と簡単に実装できてしまうデスクトップ通知機能、ぜひ一度使ってみてください!

おまけ:ローカルで通知が表示されない場合

セキリティ設定の影響でChromeなど幾つかのブラウザではサーバーを通さないローカルのjsが動作しないことがあります。 その場合は以下のサイトを参考に

$ python -m SimpleHTTPServer 8080

コマンド1つで今すぐWebサーバを起動させるためのワンライナー(Ruby or Python) - 元RX-7乗りの適当な日々

とし、ブラウザからlocalhost:8080にアクセスすると、簡単にサーバー経由で動作を確認することが出来ます。

*1:逆に、一度でもDENIEDの状態にされるとブラウザの設定から許可状態にしてもらわなければならないので、やや面倒なことになります。

【Rails】after_createが発動するタイミングはいつでしょう?

MUGENUPの倉成です。 今回はRailsのCallbackであるafter_createafter_commitの処理順番を改めて確認し、処理順番を誤解していた事によって僕が遭遇した問題ついて記事を書こうと思います。

シチュエーション

ブログ記事の新規投稿があった時、購読者に対してメールを送信する。

ブログの投稿時に通知のメールを送信するような場合、記事の投稿に必要な最低限の処理のみを行い、購読者へのメール送信などリアルタイムな処理が必要でないものは非同期で処理することで、レスポンス速度を向上させることが出来ます。

f:id:mgnup:20140804095733j:plain

非同期処理を行うためのライブラリとしてはresquesidekiqdelayed_jobなど幾つかのものがありますが、弊社ではこの用途にresqueを使っているので、この記事では特にresqueを取り上げて説明します*1

実装

さて、上の要件の実装として、僕は以下のようなコードを書きました。

# app/models/article.rb
class Article < ActiveRecord::Base
  # 記事が作成されたらメール送信タスクをqueueに積む
  after_create do
    Resque.enqueue(SendMail, id)
  end
end
# app/workers/send_mail.rb
class SendMail
  @queue = :send_mail
  def self.perform(article_id)
    # 投稿された記事をDBから読み込む
    article = Article.find(article_id)
    # ブログの購読者にメールを送信
    # ....
    # ....
  end
end

self.perform(article_id)の第一引数article_idResque.enqueue(SendMail, id)の第二引数idと対応しており、 新規記事のIDが渡されることになります。

さて、実際にアプリケーションを立ち上げて実行してみるとself.perform内部で

article = Article.find(article_id)で稀にNoRecordFoundが発生する

という現象が起きていました。

after_createという名のとおり記事は既にcreateされていて、レコードは存在しているはずです。その証拠に既にidは与えられていますよね。

なぜでしょう。

after_createはDBへのコミット直前に実行される

不思議に思い、出力されたログを見てみると、ResqueからのSELECTBEGIN ~ COMMITの内部で実行されていました。

DBはINSERTを実行しても、COMMITされるまでは、レコードは永続化されません。 つまりRailsがDBにCOMMITする前に、外部プロセスであるResqueのプロセスがSELECTをしていたため、NoRecordFoundが発生していまうという状態になっていました。

BEGIN
  INSERT INTO `articles` (`col1`, `col2`) VALUES ('..', '..')
  /* COMMIT前にResqueからのSELECTが発行される */
  SELECT `articles`.* FROM `articles` WHERE `articles`.`id` = 1
COMMIT

これは、after_createがDBへのコミット直前に実行されることが原因です*2

after_commitを使う

原因がわかりました。 after_createの代わりに、コミット直後に実行されるafter_commit on: :createを使いましょう。

これでCOMMITのあとにResqueの処理が実行されるため、NoRecordFoundが発生することもなくなりました。

BEGIN
  INSERT INTO `articles` (`col1`, `col2`) VALUES ('..', '..')
COMMIT
/* COMMIT後にResqueからのSELECTが発行される */
SELECT `articles`.* FROM `articles` WHERE `articles`.`id` = 1

BEGIN ~ COMMITの外側でSELECTが実行されていることが、ログでも確認できます。

NoRecordFoundの原因は

稀にNoRecordFoundが発生する状態だったのは「COMMITの前に外部プロセスであるResqueのSELECTが発生するケースがあったため」というのが結論です。なお常に発生するでは無いのは、Resqueが新規ジョブをポーリングする間隔により、Resqueの処理がCOMMIT後に行われることもあったためです。

今回のようにRailscallbackをトリガーにして外部プロセスからDBにアクセスする場合はafter_commitを使うようにしましょう。

なお,同様の注意点は http://apidock.com/rails/ActiveRecord/Callbacks/after_createにも書いてありました*3

おわりに

今回は「after_createはDBにCOMMITされた後に呼び出される」という間違った認識によって遭遇した問題について記事を書かせていただきました。 ResqueなどRails外のプロセスを使う場合にこの記事を思い出していただけたら幸いです。

*1:今回は説明のため使用しませんがメール送信を非同期で行うならresque_mailerが便利です。

*2:コールバックの順番はこちらの記事にとても詳しくまとまっています http://techracho.bpsinc.jp/baba/2013_08_23/12670

*3:Rails 2.3.8のドキュメントですが,この注意点に関しては今のバージョンでも変わらずに使えると思います

初めてのVimプラグイン開発とMUGENUPエンジニアのエディタ事情

初めまして!MUGENUPアルバイトの倉成です。こちらで書く初めての記事として、今回は僕が初めて作ったVimプラグインMUGENUPエンジニアのエディタ事情について紹介しようと思います。

僕自身はMUGENUPでアルバイトを始めてから、Emacs, SublimeText, RubyMineといろいろなエディタに手を出していましたが、現在はVimに落ち着いています。

もちろん、それぞれのエディタには一長一短があり「他で出来たことがこっちでは出来ない」というようなことはよくありますよね。

後置記法を支援するプラグイン

そんな中でも、RubyMineを使っていた時に便利だと思った「bodyが1行のif, unless, while, until文を後置記法に変換する*1」ことがVimではできず、軽く調べてもプラグインが見つからなかったので、自分で作ってみることにしました*2

f:id:mgnup:20140730202756g:plain

Github: https://github.com/kuranari-tm/backend_if

内部の処理は比較的簡単で、カーソル行が一行化・複数行化できるかを判定し、もし変換可能であれば正規表現等を使って置換を行っています。

このプラグインif cond thenのようにthenが入っているとうまく動かなかったり、コードを整形するのにnormal! 2k3==と強引なことをやっていたりと、不十分な部分は多いのですが、普段の開発で使えるプラグインを開発できたのが何よりの成果でした。便利なプラグインを使うのももちろんよいですが、自給自足で必要な物を作る楽しみは何物にも代えがたいですよね。

if文の一行化・複数行化はよく行うと思うので、if文を後置化するのにddkPJ==jddなどしている方、ぜひこのbackend_ifsplitjoinを使ってみてください!

MUGENUPエンジニアのエディタ事情

さてさて、エンジニア同士の会話で「エディタ何使っていますか?」というのは鉄板ネタだと思うので、MUGENUPエンジニアのエディタ事情について少し紹介します。

現在MUGENUPでは8人のエンジニアで開発をしているのですが、使用エディタは

と圧倒的にSublime Textが人気です。

確かに、新人さんやIDEしか使ったことがない人がメンバーに加わる時には「とりあえずSublime使ってみよう!」ってなりますもんね。 学習コストが比較的低い割に、高機能で拡張性もあり、とてもよいエディタだと思います。

ということでSublime勢の勢力に押され、社内でVimを使っているのは少数派となってしまっているのですが、サーバー設定の場面などではVim(Vi)が使えたほうが何かと便利だったりするので、これからも少しずつ良さを広めていけたらなと思います。

おわりに

今回は僕が初めて作ったプラグインの紹介、そしてMUGENUPエンジニア陣のエディタ事情について書かせていただきました。

if文を一行化・複数行化する機能は初めてのプラグイン開発にちょうどいい難易度に感じたのでEmacsSublime textなど他のエディタを使っている方も同様なプラグインを作ってみてはいかがでしょうか。

GithubへのPull Requestもお待ちしています!

*1:RubyMineでは後置記法が可能な文を複数行で書いていると、警告とともに1行化するボタンが表示されていました。警告を出すこと自体はRubyMine以外のエディタでもRubocopを使用することで可能です。

*2:実は後からしっかり調べたらsplitjoinというプラグインが見つかりました、各種言語に対応しているので実際に使うならこちらのほうがオススメです。

Yammer に投稿したLGTMな画像を、GitHub に POST する Chrome 拡張を作ってみた

皆さん、こんにちは。MUGENUP の osada です。

今回のテーマは、

  1. Chrome 拡張を作ってみた
  2. JS の BLOB として、画像をダウンロードする
  3. GitHub 経由で、S3 に POST する

の3点です。

読者ターゲットは、 画像は S3 に置きたいけど、管理はしたい 人や、 <input type="file"> を使わずに画像をPOSTしたい 人です。

注: 一部画像にモザイクを掛けてあります。copyrightを守ります

Yammer から GitHub

開発部では、社内コミニケーションツールとして、Yammer を使っています。 Yammer は社内専用の Facebook のようなSNSです。 そこに LGTM な画像置き場を作っているのですが、Yammer の URL をそのまま GitHub に投稿しても表示できません。 Yammer は会員制でクローズドなので GitHub はアクセスすることができないからです。

そこで今までは、

  1. yammer から ローカルPC にダウンロード
  2. ローカルPC から GitHub にアップロード

という2手順を踏んで、LGTM に使っていました。

f:id:mgnup:20140727210649p:plain

しかしこの手順は意外と面倒くさい!

結局、LGTM.in拡張機能 LGTMでめでたさを伝えるChrome拡張をつくった - Thinking-megane を使ってしまい、 このままでは Yammer は、ただの画像置き場になってしまう! ということで、今回、Chrome 拡張を作ることにしました。

Chrome拡張の動作

動作は大きく3つに分かれます

  1. Yammer API で、投稿を取得(JSON)し、サムネイル画像をレンダリングする
  2. 選択したサムネイル画像を、BLOB形式でダウンロードする
  3. 画像をGitHubを経由してS3にアップロードする

f:id:mgnup:20140727210702p:plain

鍵となるのは、下記の2点です。

  1. 画像を BLOB として扱うこと
  2. GitHub は画像を3段階でアップロードすること

では、1つずつ解説していきます。

Chrome 拡張の構造

と、その前に、Chrome 拡張の構造を解説します。

この Chrome 拡張は、下記の3つから成り立っています。

  • chrome拡張を定義する、manifest.json
  • chrome拡張側の、アイコンをクリックしてポップアップする、popup.htmlyammer.js
  • 表示中のページで読み込まれる content.js

f:id:mgnup:20140727210908p:plain

chrome 拡張と、表示中のタブ間は、完全に独立しているため、 yammer.js と content.js は、メッセージで通信することになります。

Yammer API で、投稿を取得(JSON)し、サムネイル画像をレンダリングする

chrome 拡張側、アイコンをクリックすると、popup.html を開き、yammer.js を読み込んで投稿を取得します。

f:id:mgnup:20140727212549p:plain

function loadAndAppendGroupImages($body, groupID){
    var url = "https://www.yammer.com/api/v1/messages/in_group/"+ groupID + ".json";
    $.getJSON(url)
        .done(function(data, status, xhr){
            var images = [];
            data.messages.forEach(function(message){
                if(message.attachments){
                    message.attachments.forEach(function(attachment){
                        if(image = createImage(attachment)){
                            images.push(image);
                        }
                    });
                }
            });
            var image_tags = [];
            images.forEach(function(image){
                image_tags.push(toHTML(image));
            });
            $body.replaceWith(image_tags);
        })
        .fail(function(xhr, status){
            $body.replaceWith("<div style='width: 200px'>読み取れません。ログインしていないか、ネットワークが違います</div>");
        });
}

この json は、messages の array が返ってきます。 この中で画像の情報は attachments に含まれています。 ここから、サムネイルURLとダウンロードURLを取り出して、popup.html にレンダリングします。

なお、createImagetoHTML は、取り出しと、HTML整形です。

function createImage(attachment){
    return {
        thumbnail_url: attachment.thumbnail_url,
        download_url: attachment.download_url
    };
}

function toHTML(image){
    return "<a href='" + image.download_url +  "'>" +
        "<img src='" + image.thumbnail_url + "'>" +
        "</a>" +
        "<br>";
}
  • yammer api で取得できる json
{
  "threaded_extended": {},
  "messages": [
    {
      "id": ,
      ......
      "attachments": [
        {
          "id": xxxxx,
          "url": "https://www.yammer.com/api/v1/uploaded_files/xxxxx",
          "web_url": "https://www.yammer.com/mugenup.com/uploaded_files/xxxxx",
          "type": "image",
          "name": "xxxx.gif",
          "original_name": "xxxx.gif",
          "full_name": "xxxx",
          "description": "",
          "content_type": "image/gif",
          "small_icon_url": "https://c64.assets-yammer.com/images/file_icons/types/picture_orange_39x50_icon.png",
          "large_icon_url": "https://c64.assets-yammer.com/images/file_icons/types/picture_orange_79x102_icon.png",
          "download_url": "https://www.yammer.com/api/v1/uploaded_files/xxxx/download",
          "thumbnail_url": "https://www.yammer.com/api/v1/uploaded_files/xxxx/version/20643071/thumbnail",
          "preview_url": "https://www.yammer.com/api/v1/uploaded_files/xxxx/preview/xxxx.gif",
          "large_preview_url": "https://www.yammer.com/api/v1/uploaded_files/xxxx/version/20643071/large_preview/xxxx.gif",
          "size": 1014474,
          ......
          "height": 278,
          "width": 500,
          "scaled_url": "https://www.yammer.com/api/v1/uploaded_files/xxxx/version/20643071/scaled/{{width}}x{{height}}",
          "image": {
            "url": "https://www.yammer.com/api/v1/uploaded_files/xxxx/preview/xxxx.gif",
            "size": 1014474,
            "thumbnail_url": "https://www.yammer.com/api/v1/uploaded_files/xxxx/version/20643071/thumbnail"
          },
        }
      ],
    },......

選択したサムネイル画像を、BLOB形式でダウンロードする

popup.html に表示された画像一覧から、画像をクリックしたとき、 download_url を使って、画像をダウンロードします。

このダウンロード動作は拡張側ではなく、ページ側で行うため、download_urltab 側に メッセージとして送信します。

// 画像がクリックされたとき、ダウンロードURLを、コンテンツスクリプトに委譲
    $(document).on("click", "a", function(e){
        $this = $(this);
        $this.replaceWith("<img src='./imgs/loading.gif'>");

        var download_url = $this.attr("href");
        chrome.tabs.getSelected(null, function(tab) {
            chrome.tabs.sendRequest(
                tab.id,
                { download_url: download_url },
                function(response){
                    window.close();
                });
        });
    });

コンテンツスクリプト側では、addListener を使い、 メッセージをキャッチします。

chrome.extension.onRequest.addListener(
    function(request, sender, sendResponse) {
        var download_url = request.download_url;
        xhr = new XMLHttpRequest();
        xhr.open('get', download_url, true);
        xhr.responseType = 'blob';
        xhr.onload = function(){
            if(this.status == 200){
                var blob = this.response;
                var ContentDisposition = xhr.getResponseHeader("Content-Disposition");
                blob.filename = ContentDisposition.match(/filename="([^"]+)"/)[1];
                postGithub(blob);
            }
        };
        xhr.send();
    }
);

ここで大事なのは、画像としてダウンロードすることです。

よって、xhr.responseType = 'blob'; を指定しています。

blob というのは、バイナリ・ラージ・オブジェクトのことであり、コレを使うことで、response をバイナリとして扱うことができます。

(ここだけ、jQuery ではなく、XMLHttpRequest を使っているのは、 jQueryresponseType を指定する方法が不明だったためです)

画像をGitHubを経由してS3にアップロードする

GitHub の画像のアップロードは、大変優れた方法です。 GitHub側では、レコードを管理し、大きなバイナリデータはS3に直接アップロードさせます。

(ご存じの方は3ウェイ・ハンドシェイク を 思い浮かべていただければ、わかりやすいと思います。)

GitHub へのアップロードは下記の動作で実行されます。

  1. GitHub に post する
  2. GitHub は、S3 へのアップロードに必要な情報を返す
  3. その情報を使い、S3 にアップロードする
  4. GitHub に put して、データを更新する(?)

1つずつ解説します

1. GitHub に post する

name, size, content_typeGitHub に post します。

        function postGithub(blob){
            var formData = new FormData();
            formData.append("name", blob.filename);
            formData.append("size", blob.size);
            formData.append("content_type", blob.type);

            var url = "https://github.com/upload/policies/assets";
            $.ajax({
                url: url,
                type: "post",
                headers: {"X-CSRF-Token": csrf},
                data: formData,
                processData: false,
                contentType: false
            }).done(function(data){
                postS3(data, blob);
            });
        }

ここでは、ファイル自体は post していないことに注目してください

2. GitHub は、S3 へのアップロードに必要な情報を返す

すると、GitHub は、S3へのアップロードに必要な情報を返してきます。 (一部の箇所は、xxxxx で隠しています)

{
  "upload_url": "https://s3.amazonaws.com/github-cloud",
  "header": {},
  "asset": {
    "id": xxxxx,
    "name": "xxxxx.jpg",
    "size": 59192,
    "content_type": "image/jpeg",
    "href": "https://cloud.githubusercontent.com/assets/xxxxx/3711249/fd7cc29e-14ca-11e4-9914-0ca5b063b0c5.jpg",
    "original_name": "xxxxx.jpg"
  },
  "form": {
    "key": "assets/xxxxx/3711249/fd7cc29e-14ca-11e4-9914-0ca5b063b0c5.jpg",
    "AWSAccessKeyId": "xxxxx",
    "acl": "public-read",
    "policy": "xxxxx",
    "signature": "xxxxx",
    "Content-Type": "image/jpeg",
    "Cache-Control": "max-age=31557600",
    "x-amz-meta-Surrogate-Control": "max-age=31557600",
    "x-amz-meta-Surrogate-Key": "user-xxxxx"
  },
  "asset_upload_url": "/upload/assets/xxxxx"
}

3. S3 にアップロードする

返された json の、form に含まれるデータと、 実際の画像データのBLOBデータを formData 化して、 S3にアップロードします。

        function postS3(data, blob){
            var formData = new FormData();
            for ( var key in data.form ) {
                formData.append(key, data.form[key]);
            }
            formData.append("file", blob);

            var asset_upload_url = data.asset_upload_url;
            $.ajax({
                url: data.upload_url,
                type: "post",
                headers: {"X-CSRF-Token": csrf},
                data: formData,
                processData: false,
                contentType: false
            }).done(function(data){
                putGitHub(data, asset_upload_url);
            });
        }

4. GitHub に put して、データを更新する(?)

実は、この put がなくても、url にアクセスすれば、画像は見られます。 よって、この put が何のデータを更新しているのか不明なのですが、 何かのデータを更新しているに違いありません(!?)。

(個人的には、S3へのアップロードが完了したフラグを更新しているのではないかと思っています)

        function putGithub(data, asset_upload_url){
            $.ajax({
                url: asset_upload_url,
                type: "put",
                headers: {"X-CSRF-Token": csrf},
                processData: false,
                contentType: false
            }).done(function(data, status, xhr){
                writeLGTM(data);
            });
        }

そして、最後に、アップロードされた画像データのURLを textarea に貼り付けて終了です。

        function writeLGTM(data){
            var lgtm = "![LGTM](" + data.href + ")";
            var oldMessage = $("textarea[name='comment[body]']").val();
            if(oldMessage != ""){
                lgtm = oldMessage + "\n" + lgtm;
            }
            $("textarea[name='comment[body]']").val(lgtm);
            sendResponse({});
        }

画像をアップロード、というと、一回のPOSTで全部処理してしまいそうですが、 RESTful を遵守することで、3アクションにはなっていますが、シンプルな構成になっています。 さすが GitHub といったところでしょうか。

まとめ

さて今回はYammer に投稿したLGTMな画像を、GitHub に POST する Chrome 拡張を作ってみた というテーマでした。

動作は概要図を再掲します。

f:id:mgnup:20140727210702p:plain

今回わかったことは、下記の3点です。

  1. Chrome 拡張 は表示中のページに介入できる
  2. ajax で画像を扱うときは、xhr.responseType = 'blob'; を使うと便利
  3. GitHub の画像アップロードは、3アクションの RESTful で美しい

まず、Chrome 拡張 は表示中のページに介入できる というのは、本当に怖いことです。 yammerや、github へのログイン認証についての、説明が無いことに気づかれたかもしれません。 Chrome拡張はブラウザのセッションを使うことができるので、ログインさえしていれば、拡張側では処理が必要ないのです。 自分がログインしているサービスに対して、バックグラウンドで、Chrome拡張が通信することができてしまいます。 例えば、Facebookにログインしている Chrome に、勝手に投稿させる という javascript は簡単に書くことができそうです。野良拡張には本当に気をつけましょう

気をつけ方としては、manifest.json"permissions": を確認することです。 ここに記載があるサイトに関して、Chrome拡張は権限を持つので、オカシイな?と思ったら、使うのを止めましょう(そもそも使わないですよね)。 (一番安心なのは、ソースコードを全部読んでしまうことです。unzip で解凍することができます)

unzip hoge.crx

次に、responseType ですが、普段ajaxjQuery を使うことが多いため、今回初めて知りました。xhr.responseType を使うと、返り値を制御するとができますが、jQuery ではどうやると良いのでしょうか?

最後に、GitHub の画像の扱いはスマートでした。DBとストレージが異なる、というのは、昨今当たり前なので、大変参考になりました。皆さんもこのように実装されてはいかがでしょうか?(もうやってる?)

ということで、Chrome拡張を作ってみた、というお話でした。

拡張は、html+css+js なので、WEBエンジニアならとても簡単に作ることができます。是非、作ってみることをおすすめします。きっと新しい発見があると思います。