フロントエンドがAngularJS上に構築されている1つのサービスで作業する過程で、異なる論理チャネルを介してsocket.ioサーバーと通信する必要が生じました。 同時に、サーバーへの実際の物理接続を1つだけ使用して、アプリケーションコードの各チャネルに個別の接続オブジェクトを作成するという要望がありました。 これの由来は、カットの下にあることがわかります。
socket.io名前空間
判明したように、 socket.ioライブラリは、同じ物理接続内のさまざまなサブシステムからのメッセージを多重化するのに役立つ、いわゆる名前空間を作成する機能を提供します。 この場合のクライアントコードは次のようになります。
var channelMessages = io.connect('http://localhost:3000/messages'), channelMessages.on('message received', function() { /* notify about new message */ }); // ... channelCommands = io.connect('http://localhost:3000/commands'); channelCommands.emit('init'); channelCommands.on('command received', function() { /* process new command */ });
つまり 各
io.connect
呼び出しは、
on
emit
と
emit
を持つ新しい接続オブジェクトを返します。 そのような接続オブジェクトのために、サーバーとの便利な通信のためにそれぞれにAngularJSサービスを持ちたいと思っていました。
AngularJSサービスソケットv.1
いくつかの名前付きソケットを取得したいときに、socket.ioを操作するサービスは、この記事の説明のように見えました。
app.factory('socket', function ($rootScope) { var socket = io.connect(); return { on: function (eventName, callback) { socket.on(eventName, function () { var args = arguments; $rootScope.$apply(function () { 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); } }); }) } }; });
実際、これは
on
および
emit
プリミティブラッパー
on
あり、メッセージの受信時/メッセージの送信の確認時にすべてのスコープの更新につながります。 サーバーへの接続は、サービスの初期化中に1回発生します(AngularJSは
factory
メソッドを1回呼び出して、サービスの「シングルトン」を確保するため)。
免責事項
くしゃみごとにすべてのスコープを更新するのはクールではないため、
更新中の領域を最小限に抑えるようにしてください。 しかし、この記事では、これはオフトピックです。
$apply()
更新中の領域を最小限に抑えるようにしてください。 しかし、この記事では、これはオフトピックです。
名前空間の出現により、ソケットサービスの初期化時にサーバーへの接続を作成する方法が機能しなくなりました。 さらに、サービスの複数のインスタンスが必要であり、各インスタンスは独自のチャネルに接続されていました。 また、別の明らかな要件は、将来名前付き接続を作成するときにコードの重複を避けることです。
AngularJS Service Socket v.2
開発のこの時点で、いくつかの認知的不協和音が生じました。 多くの名前付き接続が必要であり、AngularJSのサービスはシングルトンです。 この問題を解決する最初の試みは、AngularJSがすぐに似たようなことをする方法をすでに知っているという考えでした。 結局のところ、サービスを作成するには少なくとも3つの方法があります。 最も単純なのは
module.service
で、コンストラクタが受け入れます。このオブジェクトを使用して、要求に応じてサービスオブジェクトが作成されます。 やや柔軟な方法は
module.factory
。これにより、コンストラクターで直接操作するよりも、サービスインスタンスを返す前にいくつかの追加アクションを実行する方が便利です。 そして、最も柔軟な方法は
module.provider
です。 名前から判断すると、プロバイダーに対するクライアントモジュールの依存関係を指定し、クライアントコードに
socketsProvider.get('foo')
ようなものを記述して、指定されたconnection
/foo
を取得できると想定できます。 ただし、
module.provider
使用すると、サービスインスタンスを1回だけ構成できます。クライアントコードはプロバイダーに依存するのではなく、直接サービス自体に依存する必要があります。
同僚と問題を話し合った後、
namespace
パラメーターを最初のパラメーターとして追加してソケットサービスの
emit
を展開し、サービス内に遅延接続のプールを保持するというアイデアが生まれました。
on
または
emit
呼び出しごとに、指定された名前空間との接続が既に存在するかどうかを確認し、存在しない場合は新しいものを作成する必要があります。 名前付き接続オブジェクトの実装では、軽量のサービス
socketFoo
、
socketBar
などを作成し、独自の実装
on
および
emit
を
socket.emit
、
socket.on
および
socket.emit
カリー化して、名前空間パラメーターを 'foo'および 'bar'の定数値で修正する必要があります。 実用的なソリューションですが、重大な欠点があります-ソケットメソッドのセットを拡張する場合、
socketFoo
および
socketBar
サービスクライアント
socketBar
、既存の
socketFoo
および
socketBar
サービスコードを変更せずに新しいソケットメソッドを呼び出すことができません。
頭を悩ませた後、関数を含むオブジェクトがAngularJSのサービスとして動作できることをもう少し思い出すことができました! インスタンスとしてサービスを使用する従来のパターンは、次のアプローチに変更できます。
var module = angular.module('myApp.services', []); app.factory('MyService', function() { function MyService(options) { /* */ } MyService.prototype.baz = function() { /* ... */ }; MyService.prototype.qux = function() { /* ... */ }; return MyService; }); // ... module.factory('clientService', function(MyService) { var myService = new MyService({foo: 1, bar: 42}); myService.qux(); // return ... });
サービスを作成する方法だけでなく、命名の方法も変更されました。 従来の
camelCase
(インスタンスを処理することを意味します)の代わりに、
CamelCase
使用して、サービスが実際にコンストラクターであることを示します。 このアプローチを使用して、
Socket
サービスが実装されました。
Socket.js
var services = angular.module('myApp.services', []); services.factory('Socket', ['$rootScope', function($rootScope) { var connections = {}; // , function getConnection(channel) { if (!connections[channel]) { connections[channel] = io.connect('http://localhost:3000/' + channel); } return connections[channel]; } // , namespace- . function Socket(namespace) { this.namespace = namespace; } Socket.prototype.on = function(eventName, callback) { var con = getConnection(this.namespace), self = this; // con.on(eventName, function() { var args = arguments; $rootScope.$apply(function() { callback.apply(con, args); }); }); }; Socket.prototype.emit = function(eventName, data, callback) { var con = getConnection(this.namespace); // . con.emit(eventName, data, function() { var args = arguments; $rootScope.$apply(function() { if (callback) { callback.apply(con, args); } }); }) }; return Socket; }]);
クライアントモジュールが依存する名前付き接続の特定の実装は、次のようになります。
Examples.js
var services = angular.module('myApp.services.channels', []); // . services.factory('channelFoo', function(Socket) { return new Socket('foo'); }); // . services.factory('channelBar', function(Socket) { function ChannelBar() { this.namespace = 'bar'; } ChannelBar.prototype = angular.extend(Socket.prototype, {}); ChannelBar.prototype.start = function() { this.emit('start'); }; ChannelBar.prototype.exit = function() { this.emit('exit'); }; return new ChannelBar(); });
この方法で作成されたチャネルには、基礎となるSocketオブジェクトからすべての機能を自動的に継承するという利点があり、
socket.on
および
socket.emit
場合よりもテンプレートコードが大幅に少なくて
socket.emit
。
おわりに
指定された
Socket
サービスの実装例は概念にすぎません。 これを最大限に活用するには、
io
オブジェクトを挿入する機能、接続文字列と承認の設定、およびサーバーからメッセージを受信するときに更新するスコープを指定する機能を追加する必要があります。 例付きのコードはgithubにあります。