はじめに
AngularJS は、使いやすく高速な双方向データ バインディングを提供する優れた JavaScript フレームワークです。再利用可能なカスタム コンポーネントの作成など、多くの機能を使用できる強力なディレクティブ システムです。Socket.IO は、リアルタイム アプリケーションの開発を容易にする、WebSocket 用のクロスブラウザ ラッパーおよびポリフィルです。ちなみに、この 2 つはうまく連携しています。
以前に Express で AngularJS アプリケーションを作成する方法について書きましたが、今回は Socket.IO を統合して AngularJS アプリケーションにリアルタイム機能を追加する方法について説明します。このチュートリアルでは、インスタント メッセージ アプリの作成手順を説明します。これは、以前のチュートリアル(サーバー上の同様の node.js スタックを使用)に基づいて構築されているため、Node.js または Express になじみがない場合は、まず確認することをおすすめします。
通常どおり、GitHub で完成品を入手できます。
前提条件
Socket.IO を設定して Express と統合するためのボイラープレートが少しあるため、Angular Socket.IO シードを作成しました。
まず、GitHub から angular-node-seed リポジトリのクローンを作成します。
git clone git://github.com/btford/angular-socket-io-seed my-project
または zip 形式でダウンロードします。
シードを取得したら、npm でいくつかの依存関係を取得する必要があります。ターミナルを開いてシードを含むディレクトリを表示し、次のコマンドを実行します。
npm install
これらの依存関係をインストールしたら、スケルトン アプリを実行できます。
node app.js
ブラウザで http://localhost:3000
にアクセスして、シードが想定どおりに機能していることを確認します。
アプリの機能を決定する
チャット アプリケーションを作成する方法はいくつかありますが、ここでは最小限の機能について説明します。すべてのユーザーが参加するチャットルームは 1 つだけです。ユーザーは自分の名前を選択して変更できますが、名前は一意である必要があります。サーバーはこの一意性を適用し、ユーザーが名前を変更すると通知します。クライアントは、メッセージのリストと、現在チャットルームにいるユーザーのリストを表示する必要があります。
シンプルなフロントエンド
この仕様により、必要な UI 要素を提供する Jade を使用してシンプルなフロントエンドを作成できます。views/index.jade
を開き、block body
内に以下を追加します。
div(ng-controller='AppCtrl')
.col
h3 Messages
.overflowable
p(ng-repeat='message in messages') :
.col
h3 Users
.overflowable
p(ng-repeat='user in users')
.clr
form(ng-submit='sendMessage()')
| Message:
input(size='60', ng-model='message')
input(type='submit', value='Send')
.clr
h3 Change your name
p Your current user name is
form(ng-submit='changeName()')
input(ng-model='newName')
input(type='submit', value='Change Name')
public/css/app.css
を開き、列とオーバーフローを提供する CSS を追加します。
/* app css stylesheet */
.overflowable {
height: 240px;
overflow-y: auto;
border: 1px solid #000;
}
.overflowable p {
margin: 0;
}
/* poor man's grid system */
.col {
float: left;
width: 350px;
}
.clr {
clear: both;
}
Socket.IO を操作する
Socket.IO は window
で io
変数を公開していますが、AngularJS の依存関係インジェクション システムにカプセル化することをおすすめします。まず、Socket.IO から返される socket
オブジェクトをラップするサービスを作成します。後でコントローラのテストがはるかに容易になるので、便利です。public/js/services.js
を開き、内容を次のように置き換えます。
app.factory('socket', function ($rootScope) {
var socket = io.connect();
return {
on: function (eventName, callback) {
socket.on(eventName, function () {
var args = arguments;
$rootScope.$apply(function () {
callback.apply(socket, args);
});
});
},
emit: function (eventName, data, callback) {
socket.emit(eventName, data, function () {
var args = arguments;
$rootScope.$apply(function () {
if (callback) {
callback.apply(socket, args);
}
});
})
}
};
});
各ソケット コールバックを $scope.$apply
でラップしていることに注目してください。これにより、アプリケーションの状態を確認し、渡されたコールバックの実行後に変更があった場合はテンプレートを更新する必要があることを AngularJS に伝えます。内部的には、$http
も同じように機能します。XHR がリターンすると、$scope.$apply
が呼び出されます。これにより、AngularJS はそれに応じてビューを更新できます。
このサービスは Socket.IO API 全体をラップしていないことに注意してください(これは読者向けの演習として残します)。ただし、このチュートリアルで使用するメソッドはカバーされているため、拡張する場合は正しい方向に案内する必要があります。完全なラッパーを記述することについて再度検討するかもしれませんが、このチュートリアルでは説明しません。
これで、$http
の場合と同様に、コントローラ内で socket
オブジェクトをリクエストできます。
function AppCtrl($scope, socket) {
/* Controller logic */
}
コントローラ内に、メッセージを送受信するためのロジックを追加しましょう。js/public/controllers.js
を開き、次のように置き換えます。
function AppCtrl($scope, socket) {
// Socket listeners
// ================
socket.on('init', function (data) {
$scope.name = data.name;
$scope.users = data.users;
});
socket.on('send:message', function (message) {
$scope.messages.push(message);
});
socket.on('change:name', function (data) {
changeName(data.oldName, data.newName);
});
socket.on('user:join', function (data) {
$scope.messages.push({
user: 'chatroom',
text: 'User ' + data.name + ' has joined.'
});
$scope.users.push(data.name);
});
// add a message to the conversation when a user disconnects or leaves the room
socket.on('user:left', function (data) {
$scope.messages.push({
user: 'chatroom',
text: 'User ' + data.name + ' has left.'
});
var i, user;
for (i = 0; i < $scope.users.length; i++) {
user = $scope.users[i];
if (user === data.name) {
$scope.users.splice(i, 1);
break;
}
}
});
// Private helpers
// ===============
var changeName = function (oldName, newName) {
// rename user in list of users
var i;
for (i = 0; i < $scope.users.length; i++) {
if ($scope.users[i] === oldName) {
$scope.users[i] = newName;
}
}
$scope.messages.push({
user: 'chatroom',
text: 'User ' + oldName + ' is now known as ' + newName + '.'
});
}
// Methods published to the scope
// ==============================
$scope.changeName = function () {
socket.emit('change:name', {
name: $scope.newName
}, function (result) {
if (!result) {
alert('There was an error changing your name');
} else {
changeName($scope.name, $scope.newName);
$scope.name = $scope.newName;
$scope.newName = '';
}
});
};
$scope.sendMessage = function () {
socket.emit('send:message', {
message: $scope.message
});
// add the message to our model locally
$scope.messages.push({
user: $scope.name,
text: $scope.message
});
// clear message box
$scope.message = '';
};
}
このアプリケーションにはビューが 1 つしかないため、public/js/app.js
からルーティングを削除して、次のように簡素化できます。
// Declare app level module which depends on filters, and services
var app = angular.module('myApp', ['myApp.filters', 'myApp.directives']);
サーバーの作成
routes/socket.js
を開きます。ユーザー名が一意になるように、サーバーの状態を維持するためのオブジェクトを定義する必要があります。
// Keep track of which names are used so that there are no duplicates
var userNames = (function () {
var names = {};
var claim = function (name) {
if (!name || userNames[name]) {
return false;
} else {
userNames[name] = true;
return true;
}
};
// find the lowest unused "guest" name and claim it
var getGuestName = function () {
var name,
nextUserId = 1;
do {
name = 'Guest ' + nextUserId;
nextUserId += 1;
} while (!claim(name));
return name;
};
// serialize claimed names as an array
var get = function () {
var res = [];
for (user in userNames) {
res.push(user);
}
return res;
};
var free = function (name) {
if (userNames[name]) {
delete userNames[name];
}
};
return {
claim: claim,
free: free,
get: get,
getGuestName: getGuestName
};
}());
基本的には一連の名前を定義しますが、チャット サーバーのドメインに対応する API を使用します。これをサーバーのソケットに接続して、クライアントが行う呼び出しに応答しましょう。
// export function for listening to the socket
module.exports = function (socket) {
var name = userNames.getGuestName();
// send the new user their name and a list of users
socket.emit('init', {
name: name,
users: userNames.get()
});
// notify other clients that a new user has joined
socket.broadcast.emit('user:join', {
name: name
});
// broadcast a user's message to other users
socket.on('send:message', function (data) {
socket.broadcast.emit('send:message', {
user: name,
text: data.message
});
});
// validate a user's name change, and broadcast it on success
socket.on('change:name', function (data, fn) {
if (userNames.claim(data.name)) {
var oldName = name;
userNames.free(oldName);
name = data.name;
socket.broadcast.emit('change:name', {
oldName: oldName,
newName: name
});
fn(true);
} else {
fn(false);
}
});
// clean up when a user leaves, and broadcast it to other users
socket.on('disconnect', function () {
socket.broadcast.emit('user:left', {
name: name
});
userNames.free(name);
});
};
以上で申し込みは完了です。node app.js
を実行して、試してみましょう。Socket.IO により、アプリケーションがリアルタイムで更新されるはずです。
まとめ
このインスタント メッセージ アプリには他にも多くの機能を追加できます。たとえば、空のメッセージを送信できます。ng-valid
を使用すると、クライアントサイドでこれを防止し、サーバーでチェックできます。新規ユーザーがアプリに参加できるように、サーバーでメッセージの最近の履歴を保持しているかもしれません。
他のライブラリを利用する AngularJS アプリを、サービスにラップする方法を理解し、モデルが変更されたことを Angular に通知すれば、簡単に作成できるようになります。次に、一般的な可視化ライブラリである D3.js で AngularJS を使用する方法について説明します。
参照
Angular Socket.IO Seed 完成した Instant Messaging アプリ AngularJS Express Socket.IO