SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

DeveloperZine(デベロッパージン)- エンジニアの意思決定を支える技術情報メディア ProductZine

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

近未来の技術トレンドを先取り! 「Tech-Sketch」出張所

Node.jsのWebフレームワーク「Sails」を使ってpub/subアプリを作ってみる

近未来の技術トレンドを先取り! 「Tech-Sketch」出張所 第11回


チャットルームの作成

 早速ですが、まずは単にメッセージのやり取りができるだけの状態を作ってみたいと思います。sailsコマンドにgenerateオプションを付けて、以下のようにChatRoomControllerを生成します。実行時の引数はコントローラーの名称、初期作成時に追加しておきたいアクション名です。ここではroomというアクションを追加しています。

$ sails generate controller chatRoom room

 以下のようなメッセージが表示されれば、api/controllersにChatRoomController.jsが生成されているはずです。

info: Generated controller for chatRoom!

 生成されたChatRoomController.jsを開いてみます。

/**
 * ChatRoomController
 *
 * @module      :: Controller
 * @description :: A set of functions called `actions`.
 *
 *                 Actions contain code telling Sails how to respond to a certain type of request.
 *                 (i.e. do stuff, then send some JSON, show an HTML page, or redirect to another URL)
 *
 *                 You can configure the blueprint URLs which trigger these actions     (`config/controllers.js`)
 *                 and/or override them with custom routes (`config/routes.js`)
 *
 *                 NOTE: The code you write here supports both HTTP and Socket.io automatically.
 *
 * @docs        :: http://sailsjs.org/#!documentation/controllers
 */
 module.exports = {
 /**
  * Action blueprints:
  *    `/chatroom/room`
  */
 room: function (req, res) {

  // Send a JSON response
  return res.json({
    hello: 'world'
  });
},
/**
 * Overrides for the settings in `config/controllers.js`
 * (specific to ChatRoomController)
 */
   _config: {}
};

 generateの際に指定したアクションのroomが追加された状態で生成されています。デフォルトでは、単にjsonで{hello:'world'}を返すだけになっていますから、こちらをチャット用の画面を返すように修正します。ここでは単純にアクション名と同名のビューを用意しますので、修正は以下のとおりです。roomの実装を修正するのに合わせて、viewsディレクトリ以下にchatroomディレクトリを作成し、room.ejsを配置します。

//ChatRoomController.jsの修正

room: function (req, res) {
  rew.view();
},
// views/chatroomにroom.ejsを追加
<h1>ChatRoom</h1>
<div id='messages'>
  <ul id='message_list'>
  </ul>
</div>
<div>
  <input type='text' id='message'></input>
  <input type='button' id='send' value='送信'></input>
</div>

 room.ejsはとりあえずテキスト入力欄とメッセージの表示領域だけある状態です。この状態でsails liftでアプリを起動し/chatRoom/roomにアクセスしてみます。ChatRoomの表示がされればアクションの修正とビューの追加が行えていますので、チャットのメッセージを送れるように追加・修正していきます。

 メッセージを送信するために、MessageControllerを追加で生成します。コントローラーの追加は先程のChatRoomControllerを作成した時と同様です。加えてmessageのモデルも作成します。モデルを作成する場合は、generateにmodelオプションとモデル名を与えて実行します。

$ sails generate model message
warn: For the record :: to serve the blueprint API for this model,
warn: you'll also need to have an empty controller.
info: Generated model for chat!

 無事実行が終了すると、api/modelsディレクトリ下にMessage.jsが作成されているはずです。生成時は何も記述されていない状態なので、まずはメッセージを保持する属性を追加します。

/**
 * Message
 *
 * @module      :: Model
 * @description :: A short summary of how this model works and what it represents.
 * @docs        :: http://sailsjs.org/#!documentation/models
*/

module.exports = {

  attributes: {

    /* e.g.
    nickname: 'string'
    */
    message:'string'
  }

};

 attributes:にString型のmessageを追加しています。attributesで指定できる型については、公式ドキュメントを参照してください。SailsではAPI blueprintsという仕組みにより、デフォルトでRESTによるCRUDに対応しています。モデルと対応したコントローラーがある場合、postでcontrollerにアクセスすれば、自動的にモデルのcreateの処理が実行されます。また冒頭でも述べましたが、実際のメッセージ送信はsocket.ioを利用してpub/sub方式で行われ、モデルの生成や更新を自動で通知する仕組みがあらかじめ組み込まれています。実際のMessageControllerの中身を見てみましょう。

/**
 * MessageController
 *
 * @module      :: Controller
 * @description :: A set of functions called `actions`.
 *
 *                 Actions contain code telling Sails how to respond to a certain type of request.
 *                 (i.e. do stuff, then send some JSON, show an HTML page, or redirect to another URL)
 *
 *                 You can configure the blueprint URLs which trigger these actions (`config/controllers.js`)
 *                 and/or override them with custom routes (`config/routes.js`)
 *
 *                 NOTE: The code you write here supports both HTTP and Socket.io automatically.
 *
 * @docs        :: http://sailsjs.org/#!documentation/controllers
 */

module.exports = {

/**
 * Overrides for the settings in `config/controllers.js`
 * (specific to MessageController)
 */
 _config: {}
};

 MessageControllerには何も実装を追加していません。この状態でblueprintsの機能によって、RESTによるCRUDとpub/subのサブスクライブ、パブリッシュ双方に対応しています。

 Sailsでは、モデルクラス全体への通知(class room)とモデルクラスの特定のインスタンスについての通知(instance room)の双方をサポートしています。ここでの通知はクラス全体への通知になります。createのタイミングでパブリッシュを行いたい場合、publishCreateメソッドを実行しますが、blueprintsによるpostによるcreateの実行でも同様の処理が実行されます。サブスクライブの処理については、MessageControllerにgetでこれまでのメッセージの一覧を取得することで行います。

 クライアントからの呼び出しは、assets/linker/jsにあるapp.jsに追加していきます。

/**
 * app.js
 *
 * This file contains some conventional defaults for working with Socket.io + Sails.
 * It is designed to get you up and running fast, but is by no means anything special.
 *
 * Feel free to change none, some, or ALL of this file to fit your needs!
 */
(function (io) {
  // as soon as this file is loaded, connect automatically, 
  var socket = io.connect();
  if (typeof console !== 'undefined') {
    log('Connecting to Sails.js...');
  }

  socket.on('connect', function socketConnected() {

    // Listen for Comet messages from Sails
    socket.on('message', function messageReceived(message) {

      //messageを受け取ったらビューのdiv(message_list)にメッセージの
      //リストを追加
      addMessage(message.data.message);
      ///////////////////////////////////////////////////////////
      // Replace the following with your own custom logic
      // to run when a new message arrives from the Sails.js
      // server.
      ///////////////////////////////////////////////////////////
      log('New comet message received :: ', message);
      //////////////////////////////////////////////////////

    });
    ///////////////////////////////////////////////////////////
    // Here's where you'll want to add any custom logic for
    // when the browser establishes its socket connection to 
    // the Sails.js server.
    ///////////////////////////////////////////////////////////
    log(
        'Socket is now connected and globally accessible as `socket`.\n' + 
        'e.g. to send a GET request to Sails, try \n' + 
        '`socket.get("/", function (response) ' +
        '{ console.log(response); })`'
     );
    ///////////////////////////////////////////////////////////

    //メッセージ送信時のアクション呼出し
    $('#send').click(function(){
          socket.post('/message', {'message':$('#message').val()},function(res){});
          $('#message').val('');
    });
  });

  //getでメッセージの一覧を取得して表示
  socket.get('/message',{},function(res){
    $.each(res,function(){addMessage(this.message);});
  });

  // Expose connected `socket` instance globally so that it's easy
  // to experiment with from the browser console while prototyping.
  window.socket = socket;
  // Simple log function to keep the example simple
  function log () {
    if (typeof console !== 'undefined') {
      console.log.apply(console, arguments);
    }
  }

  function addMessage(message){
     var chatMsg = '<li>'+escapeMessage(message)+'</li>';
     $('#message_list').append(chatMsg);
  }

  function escapeMessage(msg){
    return $('<div/>').text(msg).html();
  }
})(
  // In case you're wrapping socket.io to prevent pollution of the global namespace,
  // you can replace `window.io` with your own `io` here:
  window.io
);

 追加している処理は、roomを開いた際のgetによる既存メッセージの取得、メッセージをPostメソッドで送る処理、socketのmessageイベントを拾ったときにメッセージの中身を取り出して画面のメッセージリストに追加するという処理です。先ほど説明した通り、API blueprintsによって、RESTでのCRUDに対応していますので、メッセージの一覧取得とメッセージの投入は、それぞれ/messageにgetとPostをするだけです。追加のパス等は必要ありません。

 今回はあまり多くの処理を記述しませんので、app.jsにすべてまとめて書いてしまっています。必要な場合は適宜ファイルを分けて記述するとよいと思います。

 ところでapp.jsがassets/linker/jsにあると説明しましたので、ここでアセットのインジェクションについて説明を加えておきます。viewsにあるlayout.ejsを開いてみると、以下のようなコメント記述がされている場所があります。

<!--STYLES-->

<!--STYLES END-->
       ・
       ・
       ・
<!--TEMPLATES-->

<!--TEMPLATES END-->
       ・
       ・
       ・
<!--SCRIPTS-->

<!--SCRIPTS END-->

 これらのコメントがインジェクション先です。もしすでに一度アプリを起動したあとであれば、それぞれのコメントの間にlinker以下の各ディレクトリに配置したファイルのパスが埋め込まれていると思います。STYLESにはstylesディレクトリに置かれた.cssや.lessのファイルのパスが、TEMPLATESにはtemplatesディレクトリに置かれた、例えばBackboneやAngularといったクライアントサイドのフレームワークのテンプレートのパスが、SCRIPTSにはjs以下に置かれた各jsファイルのパスがそれぞれ自動的に取り込まれます。

 ファイルの読込順の制御が必要な場合は、アプリのトップディレクトリ直下にあるGruntfile.jsの記述で制御できます。Gruntはご存知の方も多いと思いますが、JavaScriptのタスクランナーです。実際の記述としては、例えばjsの読込順は以下のようになっています。

var jsFilesToInject = [

  // Below, as a demonstration, you'll see the built-in dependencies 
  // linked in the proper order order

  // Bring in the socket.io client
  'linker/js/socket.io.js',

  // then beef it up with some convenience logic for talking to Sails.js
  'linker/js/sails.io.js',

  // A simpler boilerplate library for getting you up and running w/ an
  // automatic listener for incoming messages from Socket.io.
  'linker/js/app.js',

  // *->    put other dependencies here   <-*

  // All of the rest of your app scripts imported here
  'linker/**/*.js'
];

 記述の上から順にパスが追加されていきます。ここでは特に読込順の追加はしていません。先ほどのapp.jsに追加した処理の記述を見ると分かる通り、このアプリではjQueryを利用しています。ローカルにjQueryのファイルを持つ形にしていますので、そちらもjsディレクトリに配置してあります。上記の記述通りの読込順だと、'linker/**/*.js'の指定に従ってapp.jsの下にパスが追加されます。

 ここまでの追加をしたところで、もう一度アプリを起動し、ブラウザを2つ立ち上げるかもしくはそれぞれ違うタブでメッセージを送信してみます。無事それぞれの画面にメッセージが配信されていれば単にメッセージのやり取りをするという状態が作れています。

次のページ
もう少しそれらしく

この記事は参考になりましたか?

近未来の技術トレンドを先取り! 「Tech-Sketch」出張所連載記事一覧

もっと読む

この記事の著者

佐伯 純(TIS株式会社)(サエキ ジュン)

TIS株式会社 AIサービス事業部AIサービス企画開発部所属入社後、アジャイルで商品先物取引のシステム開発という当時としてはかなり尖ったチームに放り込まれ、以来SIerとは思えない辺境を歩き続けている。現在の部門に異動してからはRubyOnRailsを利用した社内向けサービスの開発に参加後、Andr...

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

CodeZine(コードジン)
https://codezine.jp/article/detail/7696 2014/04/02 13:24

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー