Ruby on Rails

マークダウンエディタでドラック&ドロップされた画像を保存できるようにしよう

javascriptのajax関数を用いて、非同期通信処理ができるようになります。 railsのconcernsを用いて、モジュール化を学びます。 rubyで画像データをbase64変換方式を用いてデコードします。

事前準備


はじめに


今回は、前回までのエディタに追加機能を実装していきます。


追加機能


  • ドロップした画像をデータベースに保存
  • リッチエディタ風に選択した文字列を装飾する(今回は分量的に扱えなかったので、興味のある方はmasterブランチのソースコードを読んでみてください)

準備


1. markdown-editorリポジトリを持っている人


以下のコマンドを実行


$ cd `find ~/ -name "markdown-editor" -type d`
$ pwd
/path/to/markdown-editor
$ git branch for_third_testing origin/for_third_testing
$ git checkout for_third_testing

2. プロジェクトを持っていない人


$ git clone -b for_third_testing git@github.com:sa-inu/markdown-editor.git
$ cd markdown-editor
$ bundle
$ rake db:create
$ rake db:migrate

実装


手順


  • 前回の実装から変わった点
  • 画像ドロップ時に$.ajax()でapiを叩く
  • imageを保存

前回までの実装


1. フォームに文字列を入力している時に、JavaScriptの関数を呼び出す


_form.html.erb


views/memos/_form.html.erb は、エディタのフォーム部分を作っている部分テンプレートです。
javascriptのイベントハンドラonkeyupを利用して、キーボードを叩いたタイミングでメソッドを呼び出すようにしています。


_form.html.erb
<%= f.text_field :title, id: 'title', autofocus: true, placeholder: 'タイトルを入力してください', onkeyup: "previewMd($('#title'), $('#title-result'))" %>
<%= f.text_area :text, id: 'editor', class: 'form-control', placeholder: 'Markdown記法で入力してね', onkeyup: "previewMd($('#editor'), $('#result'))" %>


呼び出されるメソッドは、app/assets/javascripts/markdown.jspreviewMd()です。


previewMd()


この関数は、フォームプレビュー表示するエリアをhtmlで渡すことで、
フォームに入力されている文字列marked()を通してhtmlに変換し、指定したエリアにhtmlを出力します。


markdown.js
previewMd = function($form, $result) {
  var md = $form.val();
  $result.html(marked(md));
  paintCodeBlock($result);
}

問題: 1


$.ajax()でapiを叩く


今までrailsでapiを叩くときに、remoteオプションを使ってきたと思いますが、ここではjsのajax関数を使います。
ajax関数は、jsでサーバーサイドにリクエストを送ることができます。またそれは非同期通信となるので、ページ全体を再読み込みすることはせず、部分的に変更を加えたい時などに使えます。


使用例


以下のリクエストを送る。


url HTTPmethod response type parameters
/api/likes POST json user_id, article_id

※dataTypeはレスポンスの形を指定するオプションです。


jsonとは


json(またはxml)は、ほとんどの言語で共通して使用することができるデータ型です。
jsonは基本的には、rubyのハッシュのようなキーバリューストアになっています。また、複数データを送りたい場合は配列を使用することもできます。以下が基本形です。


like.json
{
    id: 3,
    user_id: 4,
    article_id: 7
}

リクエストを送るだけであれば、以下のようになります。


sample.js
ajaxCreateLike = function (user_id, article_id) {
    $.ajax({
        url: '/api/likes',
        type: 'POST',
        dataType: 'json',
        data: {
            user_id: user_id,
            article_id: article_id
        }
    });
}

レスポンスに対して処理を行う


非同期通信でリクエストが送れるようになったので、次はそのリクエストによって返ってきたレスポンスに対して何らかの処理を行います。
この場合、done()コールバック関数使用します。(リクエストに失敗した時の処理は、fail()を使用します。)


レスポンスをコンソールに表示する


done()コールバック関数の引数には、レスポンスデータが格納されます。したがって、コールバックの中ではjsonデータを使用して何らかの処理を行うようになります。仮に以下のようなレスポンスが返ってきたとします。


like.json
{
    id: 3,
    user_id: 4,
    user_name: 'saino'
}

その中からuser_nameをコンソールに表示するには以下のように書きます。


image.js
ajaxCreateLike = function (user_id, article_id) {
    $.ajax({
        url: '/api/likes',
        type: 'POST',
        dataType: 'json',
        data: {
            user_id: user_id,
            article_id: article_id
        }
    }).done(function(data){
        console.log(data.user_name)
    });
}

リクエストが失敗した時


基本的にajax関数を使用する時はリクエストが失敗した時のコールバックも行うようにしましょう。fail()コールバック関数を使用して、エラー内容を通知するようにします。
この時の通知方法はなんでも良いです。ここではconsoleに赤いラベルで通知するようにします。(console.error()を使用)
3つの引数の説明についてはこちらを参考


image.js
ajaxCreateLike = function (user_id, article_id) {
    $.ajax({
        url: '/api/likes',
        type: 'POST',
        dataType: 'json',
        data: {
            user_id: user_id,
            article_id: article_id
        }
    }).done(function(data){
        console.log(data.user_name)
    }).fail(function(jqXHR, textStatus, errorThrown){
        console.error(jqXHR + textStatus + errorThrown);
    });
}

問題1


画像がドロップされた時にリクエストを送り、適当なレスポンスを返すようにしなさい。
該当箇所はapp/javascripts/images.jsの関数ajaxCreateImageです。


リクエスト先


url HTTPmethod response type parameters
/api/images POST json url_str, name

※url_strはドロップされた画像のdata-uri、nameはドロップされた画像ファイル名が入ります


url_str


assets/javascripts/editor.jsの32行目


editor.js
reader.onload = function(e) {

この変数eはドロップされたファイルがFileReaderクラスのインスタンスになっています。(前回の内容)
これから、e.target.resultとすることで画像データのdata-uriが取得できます。(パラメータurl_strに相当)


name


assets/javascripts/images.jsの1行目
引数にfile_nameがあるのでそれを利用します。(パラメータnameに相当)


レスポンス


現段階では、以下のjsonデータをレスポンスとなるようにしましょう。


response.json
{
    url: 'hoge'
}

問題: 2


imageを保存


さて、現段階でドロップされた画像をサーバー側に送信して、何らかのレスポンスを受け取るところまで実装できました。
次は64進数で送られてきた画像データをbase64変換方式を利用してcreateアクション内でデコードして画像を保存します。


concerns


concernsは、複数のコントローラーで共通して行う処理をモジュール化して分離しておくことができます。
例えば、viewではロジックの煩雑さを解消するために、部分テンプレート化したりhelperにまとめるといったことをしました。
controllerで共通の処理を扱う場合、以下の2通りによって煩雑さを解消できます。


  1. 親コントローラークラスに定義して継承させる
  2. concernsにモジュール化してまとめる(処理をコントローラーの外に定義して、呼び出したいコントローラーにだけincludeする)

使用例


例えば、ログインセッションを管理するメソッドは通常以下のようにApplicationControllerに記述します。


app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
    protect_from_forgery with: :exception

def session_required unless session[:id] redirect_to logins_path, alert: 'You should login' end end end

このように親クラスに定義することで、全ての子クラスはこのメソッドの恩恵を受けます。しかし、session_requiredメソッドの恩恵を受けたくない場合もあるかと思います。その場合ApplicationControllerとは違う親クラスと用意したりとクラスの継承関係が複雑になってしまいます。そんな時にconcernsを使用します。これを用いれば使いたい時クラスにだけある機能の恩恵を受けさせることができます。
では、session_requriedメソッドをconcernsに分離します。


app/controllers/concerns/session_action.rb
module SessionAction
    extend ActiveSupport::Concern
    def session_required
        unless session[:id]
            redirect_to logins_path, alert: 'You should login'
        end
    end
end

これだけです。
1行目のモジュール名は適当な名前で良いです。
2行目のextend ActiveSupport::Concernはrailsのconcernsの機能を使うために必要な記述です。(railsのActiveRecord::Baseをモデルクラスに適用したいのでクラスの継承をするのと同じようなことです。)


次に使用したいコントローラーでは以下のように呼び出します。


app/controllers/welcome_controller.rb
class WelcomeController < ApplicationController
    include SeessionAction
    before_action :session_required
    def index
    end
end

2行目のincludeで先ほど分離したSessionActionモジュールを読み込んでいます。
3行目のbefore_actionでSessionActionモジュール内の、session_requiredメソッドを呼び出したいます。
こうすることで、application_controller.rbの記述が綺麗になるのと同時に、メソッドの適用範囲などもはっきりするようになりました。


base64


次にbase64について解説します。
JavaScriptで送られてきたdata-uriは以下のようなものです。


data:image/gif;base64,[バイナリデータを64進数で表現]

base64とは、画像データを表示可能な文字列に変換する方法のことです。画像データはバイナリデータになっているので、通常のプロトコル通信では扱うことができないためbase64という変換方法を用いて英数字と記号に変換しています。参考
64進数とは、a~z, A~Z, 0~9, +, /の64文字で表現することです。
cssの色は16進数(0~9, a~f)ですね。日常生活で用いられる数値は、10進数(0~9)で表現されています。


画像データをデコード


しかしdata-uriのままでは保存できないので<input type='file'>で送られてきたデータと同じものに変換する必要があります。それがActionDispatch::Http::UploadedFileクラスのオブジェクトです。必要な属性は、file_name / type / tempfileです。


まずは、data-uriをそれぞれの要素に分解します。(正規表現などを用いると良いです)
その後、64進数をBase64.decode64()を通してバイナリデータに変換します。
一時ファイルの作成方法は、rubyの組み込みクラスのTempfileを使いましょう。


これで、data-uriをActionDispatch::Http::UploadedFileオブジェクトに変換し、carrierwaveで保存することができます。


問題2


concernsにてdata-uriをActionDispatch::Http::UploadedFileオブジェクトに変換してcreateアクション内で画像を保存できるようにしなさい。
ただし、わからなければcarrierwave-base64というgemを用いても良い。


まとめ


今回は、javascriptのajax関数を用いてapiを叩くことと、railsのconcernsという機能について学習しました。base64変換方式については、実装だけであればgemを用いれば良いですが、このようにあえて車輪の再発明をすることも大切です。


余談


今回は分量的に扱えませんでしたが、マークダウンエディタにリッチエディタの機能をつけたものもmasterブランチにあるのでそちらも時間があれば読んでみてください。