真・入門編に関して
Web技術はハイパーテキストの流通に関わる多数のプロトコル,言語,サーバソフトウェア,データベース,仮想マシン,ブラウザ等の集合として定義されます。また、同様の機能を実現するブラウザ,ライブラリ,フレームワークが乱立して開発競争が起きている分野ゆえ、非常に変化が激しく、纏まった情報も少ない状態です。従って、Webアプリケーション開発の入門に取り掛かる前の既存技術のサーベイにおいて、膨大な時間が要求されてしまいます。また、機能を実装する方法が見つかったとしても、よりシンプルな方法がないか不安に感じる事もあるでしょう。
真・入門編では、実運用環境に向けた詳細な実装を徹底的に削ぎ落としたシンプルな実装を示します。少なくとも、どのWebアプリケーションにおいても、抽象的にはこういう動作をしている、ということが分かれば幸いです。まず動く物を作って、概要を掴みたい人に本シリーズをお薦めします。
まえがき
真・入門編#1_1 → 真・入門編#1_2 の差分
画像共有機能追加
前回の真・入門編#1_1の記事は http://qiita.com/ktjava/items/df9def7274287f282bb6 です。
概要
今回は、前回作成したリアルタイムWebアプリケーションに対して、画像共有機能を追加します。
本記事における、リアルタイムWebアプリケーションのソースコードは、
にて公開しています。
前提とする知識
- HTML
- JavaScript
- CoffeeSctipt
- jQuery
- Ruby
- eRuby(ERB)
- MVC(Model-View-Controller)の関係性
- Rails5のディレクトリの意味
前回と同様です。言語仕様丸暗記は不要です。基本的なHTMLタグ,JavaScript API,各種演算子,条件分岐,関数等を知っている程度で十分です。
今回は軽量化されたJavaScriptであるCoffeeScriptを多用します。CoffeeScriptはJavaScriptと比較して、条件分岐や関数の構文が大きく異なり、インデントによりスコープを区別するため、思わぬバグを発生させやすいので注意して下さい。
開発環境
開発環境は、前回と同じです。
使用するソフトウェア
- Ruby 2.3.1p112 (2016-04-26) [x86_64-linux-gnu]
- Ruby on Rails 5.0.0.1
設計
本章では画像共有機能を設計します。
画面イメージ
ユーザがファイルを指定して投稿した時に、各メッセージの下に画像を貼り付ける形式にします。
内部動作の定義
クライアント側で名前,メッセージ,画像を読み込み、それを接続中のチャネルに向けて送信し、チャネルを保持するサーバにおいて、チャネルに接続中の全てのクライアントに対して名前,メッセージ,画像をブロードキャストします。クライアント側で受信した名前,メッセージ,画像を整形して表示します。
各ユーザが操作しているクライアント上のみでデータの内容に対する処理を行い、サーバはデータの内容には一切関知しないようにして、データ転送のみを行います。これにより、サーバ側で行う1ユーザ当たりの処理が単純になり、サーバのCPU負荷上昇を大幅に抑えることができます。従って、多人数がチャットに参加した場合のレスポンス低下を大幅に抑制できます。
実装
本章では画像共有機能を実装します。
ビューの改変
<h1>Visual WebSocket Chat</h1>
<form>
<label>Name:</label>
<input type="text" id="name_form" value="">
<br>
<label>Message:</label>
<input type="text" id="message_form" value="">
<br>
<input type="file" id="file_form" data-behavior="image_loader" size="30" value="">
<br>
<input type="button" data-behavior="room_speaker" value="Submit">
</form>
<h5>Received Messages</h5>
<div id="messages"></div>
今回着目すべきはこの部分です。
<input type="file" id="file_form" data-behavior="image_loader" size="30" value="">
form要素の子要素として、type属性にfileを指定したinput要素を付加しました。この要素をユーザがクリックした場合には、ファイル選択画面が自動で表示されます。JavaScriptにおける、DOM要素取得用としてid属性にfile_formを指定し、DOM要素のイベントハンドリングに用いる属性として、data-behavior属性にimage_loaderを指定しました。
チャネルの改変
サーバ側のデータ転送処理は、データ形式には一切関知しないため、改変する必要がありません。従って、クライアント側のデータ送受信処理のみを改変します。
App.room = App.cable.subscriptions.create "RoomChannel",
connected: ->
# Called when the subscription is ready for use on the server
window.reader = new FileReader
disconnected: ->
# Called when the subscription has been terminated by the server
received: (data) ->
# Called when there's incoming data on the websocket for this channel
feed = ""
if data["name"] isnt ""
feed = '<p>'+data["name"]+' :'
if data["message"] isnt "" then feed += data["message"]
feed += '</p>'
if data["image"] isnt null then feed += '<a href="'+data["image"]+'" target="blank_"><img width="240px" src="'+data["image"]+'"></a>'
$('#messages').append(feed)
speak: (name, message, reader) ->
@perform 'speak', name: name, message: message, image: reader.result
$(document).on 'click', '[data-behavior~=room_speaker]', (event) ->
nameForm = $('#name_form')
messageForm = $('#message_form')
fileForm = $('#file_form')
App.room.speak nameForm.val(), messageForm.val(), window.reader
messageForm.val('')
fileForm.val('')
$(document).on 'change', '[data-behavior~=image_loader]', (event) ->
file = event.target.files
window.reader.readAsDataURL(file[0])
このコードを見ているだけで嫌になりそうですね。JavaScriptだけでもヘビーですが、その上に独特な言語が覆いかぶさっているという。下記でユーザによる入力から受信データ表示までの一連の動作を追いかけながら説明します。
クライアント側送信処理
connected: ->
# Called when the subscription is ready for use on the server
window.reader = new FileReader
チャネルへの接続開始時にFileReaderのインスタンスを生成し、windowオブジェクトのreaderプロパティとして参照を保持します。
$(document).on 'change', '[data-behavior~=image_loader]', (event) ->
file = event.target.files
window.reader.readAsDataURL(file[0])
ユーザにより画像指定の操作が行われた場合には、data-behavior属性にimage_loaderを指定したinput要素でchangeイベントが発生します。その際に、FileReaderのreadAsDataURLメソッドで画像の中身を読み込みます。
$(document).on 'click', '[data-behavior~=room_speaker]', (event) ->
nameForm = $('#name_form')
messageForm = $('#message_form')
fileForm = $('#file_form')
App.room.speak nameForm.val(), messageForm.val(), window.reader
messageForm.val('')
fileForm.val('')
data-behavior属性にroom_speakerを指定したinput要素(Submitボタン)がクリックされると、クリックイベントが発火します。そのクリックイベントをリッスンし、App.room.speakというメソッドに名前フォームの値とメッセージフォームの値とFileReaderのインスタンスを渡して呼び出します。その後メッセージフォームとファイルフォームを空欄にします。
speak: (name, message, reader) ->
@perform 'speak', name: name, message: message, image: reader.result
App.room.speakは、上記に対応しており、ここで送信処理を実行しています。FileReaderのインスタンスにユーザが指定した画像を保持しているため、reader引数が増えています。reader.resultでFileReaderからWebSocketに対して画像の実体を渡します。
クライアント側受信処理
received: (data) ->
# Called when there's incoming data on the websocket for this channel
feed = ""
if data["name"] isnt ""
feed = '<p>'+data["name"]+' :'
if data["message"] isnt "" then feed += data["message"]
feed += '</p>'
if data["image"] isnt null then feed += '<a href="'+data["image"]+'" target="blank_"><img width="240px" src="'+data["image"]+'"></a>'
$('#messages').append(feed)
受信データのうち、名前とメッセージをp要素に、画像データをa要素とその子要素のimg要素に組み込みます。そして、それらの要素をid属性にmessageを指定したdiv要素の子要素として追加します。
実行結果
本章では、前章で実装した画像共有機能の実行結果を確認します。
以降の作業を行う前に、コンソールから下記のコマンドを入力してHTTPサーバを起動して下さい。
rails server
3人の会話
3つのブラウザ画面を開き、各々から http://localhost:3000 に対してアクセスした後、1つのNameフォームにuser1、もう1つのNameフォームにuser2、最後の1つのNameフォームにuser3と入力します。そして、user1→user2→user3と言う順番で、順番にメッセージを入力し、画像を指定してSubmitボタンを押します。
下記の画面と同様の挙動が現れた場合、実装は成功です。
あとがき
チャットルームで画像共有が出来るようになりました。これで、ユーザ間で見せたい画像を共有して議論することが出来ます。この感覚は、コミュニケーションアプリのLINE等にも似ています。
今回までで、サーバはデータの中継のみを行い、その他の処理は全てクライアントサイドで処理しています。このような分散処理アーキテクチャを採用すると、複数人がWebサイトにアクセスした場合のサーバへの負荷集中を大幅に抑え、Webサイトのレスポンスを向上させることが出来ます。この技法はクライアント側において計算資源が十分に確保できる場合のみ有効ですが、JavaScriptを用いて、クライアントサイドのみでデータ処理を行う技術を磨いておくと、Webサイトのパフォーマンスチューニングにおいて大いに役立つでしょう。
チャット以外においても、クライアント側でユーザの入力を受け付けて逐次データ処理を行い、処理済みのデータをサーバに送り、サーバ側で受信したデータをサービスに接続中の全てのクライアントに対してブロードキャストして、クライアント側で受信したデータに対して再度処理を行って表示する。という考え方自体は広く応用できると思います。ミクロな事象ばかりを気にすると、方向性を見失いがちです。抽象化して考える癖を付けましょう。
参考文献
本章では、本記事におけるリアルタイムWebアプリケーションに対する画像共有機能の追加において参考にした文献を記載します。
https://momdo.github.io/html5/forms.html#file-upload-state-(type=file) 4.10 フォーム — HTML5 日本語訳
W3Cによるform要素のリファレンスです。本記事で使用したinput要素に関しても詳細に説明されています。
https://developer.mozilla.org/ja/docs/Web/API/FileReader FileReader - Web API インターフェイス | MDN
本記事でファイルの読み込みに使用した、FileReaderというJavaScript APIに関するリファレンスです。
http://coffeescript.org/ CoffeeScript
CoffeeScriptの公式リファレンスです。