Extjsアプリケーションでwebsocketを使用する

Websocketは、おそらく90年代初頭のHTTPプロトコルの最も重要で有用な拡張機能です。 Websocketを使用してサーバーとデータを交換することは、通常のAJAXよりもはるかに有益です。 特にクライアントとサーバーの小さなメッセージを積極的に交換する場合、標準アプリケーションでトラフィックを節約することは重要です。 また、データ要求の応答時間を大幅に短縮しました。 この技術の長期にわたる普及の主な障害は、多くのプロキシがhttpプロトコルの拡張バージョンを不正にサポートしていたことです。 最悪の場合、セキュリティの問題( 証拠 )につながりました。 過去数年間で、Webソケットのサポートが必要な状況は改善し始めており、今では私の意見では、その時が来ました。



この記事では、標準のExtjsコンポーネント(gridpanel、treepanel、combobox)でWebソケットを使用するためのレシピについて説明します。 また、Ext.Ajaxの代替として。



免責事項



当初、この記事はJanusjsシステムに関する前回の投稿の続きとして計画されました。 しかし、このトピック自体は興味深いものであるように思えました。 したがって、投稿の最初の部分はwebsocket、extjs、nodejsについてのもので、2番目の部分ではJanusjsシステムでwebsocketを使用することのニュアンスについて説明します。



この記事のサンプルコード全体はGithubにあります。



準備完了



ExtjsとWebsocketを友達にするというアイデアは、長い間頭に浮かびました。 そのようなコンポーネントがあります: Ext.ux.data.proxy.WebSocket 。 このコンポーネントは、Ext.data.proxy.Ajaxに似ています。 つまり Webソケットは標準のAJAXスキームに従って使用されます。クライアントは特定のURLにリクエストを送信し、レスポンスを読み取ります。 ここから、この実装の主な欠点は、サーバーを読み取る各コンポーネントに個別のソケットが必要になることです。 したがって、Webソケットの利点の多くは失われます。 アプリケーションにテーブルが1つしかない場合、この実装は完全に機能します。 より複雑なタスクには、他に何かが必要です。



この記事を書いたとき、別の同様のライブラリーjWebSocketに出会いました 。 ドキュメントから判断すると、これは注目に値する深刻な開発です。 ただし、ここではサーバーはJavaで構築されています。



Node.jsにjWebSocketを適合させる複雑さを検討した結果、 Ext.ux.data.proxy.WebSocketをファイナライズする方が簡単だという結論に達しました



仕組み(理論)



Webソケットは、全二重接続を提供します;クライアントとサーバーのペアでは、両側が等しくなります。 クライアントからサーバーにメッセージを送信する場合、サーバーから送信される最初のパケットがリクエストに対する回答になるという保証はありません。 したがって、何らかの方法で、必要なものを検索するために、要求にマークを付けたり、応答のストリーム内で必要になります。



AJAXとの2番目の違いは、必要なデータをエンコードできるURLがないことです。



これらすべてを考慮して、データを交換するための簡単なプロトコルの概要を説明します。 クライアントのリクエストでは、サーバーで実行する必要がある関数の名前とそのパラメーターを転送します。

リクエスト形式:

{ "event": "", "opid": "", "data": { <data object> } }
      
      





イベント-アクション識別子(作成/読み取り/更新/削除);

opid-操作識別子、乱数。 このコードにより、目的の回答を検索します。

データ-追加のリクエストパラメーター。 たとえば、これがdata.proxyからのリクエストである場合、フィルタリング、ページング、ソートのオプションがあります。



応答形式は要求とまったく同じです。



サーバー



サーバー側から始めましょう。 必要なNode.jsモジュールを追加します。

 npm i node-static websocket
      
      







クロスドメインリクエストの問題を回避するために、1つのポートで通常のhttpリクエストとWSを処理する複合サーバーを作成します。

Webサーバーの調達:

 var http = require("http") ,port = 8008 //       ./static ,StaticServer = new(require('node-static').Server)(__dirname + '/static') ,WebSocketServer = require('websocket').server; //   http- var server = http.createServer(function(req, res) { //   http-    StaticServer.serv(req, res) }) //   server.listen(8008, function() { console.log((new Date()) + ' Server is listening on port 8008'); }); //  websocket  var wsServer = new WebSocketServer({ //    http- httpServer: server, //      , //       -  autoAcceptConnections: false }); //       - wsServer.on('request', function(request) { wsRequestListener(request) }); //     var wsRequestListener = function(request) { var conn; try { //   conn = request.accept('ws-my-protocol', request.origin); } catch(e) { /*  */} //    conn.on('message', function(message) { wsOnMessage(conn, message); }); //    conn.on('close', function(reasonCode, description) { wsOnClose(conn, reasonCode, description); }); } //   var wsOnMessage = function(conn, message) { } //    var wsOnClose = function(conn, reasonCode, description) { }
      
      







クライアントからのリクエストは、JSONを含む文字列の形式で送信されます。 オブジェクトに変換し、モデルの対応するメソッドを呼び出す必要があります。



 ... //   var wsOnMessage = function(conn, message) { var request; //       try { request = JSON.parse(message.utf8Data); } catch(e) { console.log('Error') return; } if(request && request.data) { //             if(!!this[request.data.model] && !!this[request.data.model][request.data.action]) { //          this[request.data.model][request.data.action](request.data, function(responseData) { //      , //       //    // scope -  -    (store) // opid -   responseData.scope = request.data.scope; if(request.opid) responseData.opid = request.opid //    conn.sendUTF(JSON.stringify({event: request.event, data: responseData})) }) } } } ...
      
      







簡単にするために、サーバーと同じファイル内のモデルとその唯一の関数は、静的データの配列を返します。



 ... //      gridDataModel = { //   // data -   (, ,   ..) // cb -  ,      read: function(data, cb) { cb({ list: [{ Author: 'Author1', Title: 'Title1', Manufacturer: 'Manufacturer1', ProductGroup: 'ProductGroup1', DetailPageURL: 'DetailPageURL1' },{ Author: 'Author2', Title: 'Title2', Manufacturer: 'Manufacturer2', ProductGroup: 'ProductGroup2', DetailPageURL: 'DetailPageURL2' }], total: 2 }) } } ...
      
      







お客様



extjsサンプルの標準セットから、単純なテーブルの例を取り上げ、その中のAJAXプロキシを変更されたWSプロキシに置き換えます。

 Ext.Loader.setConfig({ enabled: true, paths: { 'Ext.ux': 'src/ux' } }); Ext.onReady(function(){ //    var protocol = location.protocol == 'https:'? 'wss':'ws'; //  - var WS = Ext.create('Ext.ux.WebSocket', { url: protocol + "://" + location.host + "/" , protocol: "ws-my-protocol", communicationType: 'event' }); var proxy = Ext.create('Ext.ux.data.proxy.WebSocket',{ //    store storeId: 'stor-1', //            websocket: WS, //       params: { model: 'gridDataModel', scope: 'stor-1' }, //    reader: { type: 'json', rootProperty: 'list', totalProperty: 'total', successProperty: 'success' }, simpleSortMode: true, filterParam: 'query', remoteFilter: true }); //  Ext.define('Book',{ extend: 'Ext.data.Model', fields: [ 'Author', 'Title', 'Manufacturer', 'ProductGroup', 'DetailPageURL' ] }); //  data store var store = Ext.create('Ext.data.Store', { id: 'stor-1', model: 'Book', proxy: proxy }); //  gridpanel Ext.create('Ext.grid.Panel', { title: 'Book List', renderTo: 'binding-example', store: store, bufferedRenderer: false, width: 580, height: 400, columns: [ {text: "Author", width: 120, dataIndex: 'Author', sortable: true}, {text: "Title", flex: 1, dataIndex: 'Title', sortable: true}, {text: "Manufacturer", width: 125, dataIndex: 'Manufacturer', sortable: true}, {text: "Product Group", width: 125, dataIndex: 'ProductGroup', sortable: true} ], forceFit: true, height:210, split: true, region: 'north' }); //       . //      0.1 var loadData = function() { if(WS.ws.readyState) { store.load(); } else { setTimeout(function() {loadData()}, 100) } } loadData() });
      
      







上記の簡単な例では、クライアントスクリプトはリクエストに応じてサーバーからデータを受け取ります。 つまり 標準のAJAXクライアント/サーバー相互作用スキームがありますが、唯一の違いはデータの受信方法です。 しかし、実際のアプリケーションでは、おなじみのXHRを最新のWSに変更したら、もっと何かを手に入れたいです。 たとえば、クライアントの1つがサーバー上のデータを変更した場合、残りはこれらの変更について知る必要があります。 この目的のために、data.Story識別子はWSプロキシ設定で送信されます。データが変更されたというサーバーからの信号を受信すると、WSプロキシは適切なアクションを開始してこれらの変更をUIに表示する必要があります。



WSの最も完全なクライアント/サーバー相互作用アルゴリズムは、 Janusjsシステムに実装されてます。 以下では、このシステムでWebソケットを使用する機能について説明します(サンプルコードはGithubで入手可能です)。



例として、 前の記事で説明したニュースモジュールの例を取り上げます 。 システムの公開部分でニュースにコメントを投稿する機能をユーザーに追加しましょう。 新しいコメントが到着すると、管理パネルでこのコメントのモデレートフォームに移動するボタンを備えた「新しいコメントを受け取りました」というメッセージが表示されます。



まず、ニュースページテンプレート(保護された/サイト/ニュース/ビュー/ one.tpl)を展開します。

 <h4> {name} <i class="date">: {[Ext.Date.format(new Date(values.date_start),'dmY')]}</i> </h4> {text} <tpl if="isCommentCreated">  . </tpl> <h4></h4> <tpl for="comments"> <p>{text}</p> </tpl> <form method="post"> <textarea rows="5" cols="50" name="comment"></textarea><br> <button type="submit"></button> </form>
      
      







ニュースモジュールのパブリックコントローラー(保護された/サイト/ニュース/コントローラー/ News.js)で、ニュースカードを表示するメソッドを追加します。

 Ext.define('Crm.site.news.controller.News',{ extend: "Core.Controller" ... ,showOne: function(params, cb) { var me = this ,out = {} //     ,commentsModel = Ext.create('Crm.modules.comments.model.CommentsModel', { scope: me }); [ function(next) { //      "comment" //    if(params.gpc.comment) { commentsModel.write({ pid: params.pageData.page, //   text: params.gpc.comment //   }, function() { next(true) //    }, {add: true}); //   --   } else next(false) //    } ,function(isCommentCreated, next) { out.isCommentCreated = isCommentCreated; //   //      commentsModel.getData({ filters: [{property: 'pid', value: params.pageData.page}] }, function(data) { out.comments = data.list; next() }) } ,function(next) { //    Ext.create('Crm.modules.news.model.NewsModel', { scope: me }).getData({ filters: [{property: '_id', value: params.pageData.page}] }, function(data) { if(data && data.list && data.list[0]) out = Ext.merge(out, data.list[0]) me.tplApply('.one', out, cb) }); } ].runEach() } ...
      
      







ビデオでは、その仕組み:





新しいコメントについて管理者に速やかに通知するために、メッセージを表示します。 このメッセージのボタンをクリックすると、コメント編集カードを開く必要があります。



コメントモジュールディレクトリで、新しいコントローラーを作成します(static / admin / modules / comments / controller / Eventer.js):

 Ext.define('Crm.modules.comments.controller.Eventer', { extend: 'Core.controller.Controller', autorun: function() { //       // 1  --   ( ) // 2  --   ,    // 3 --   Core.ws.subscribe('eventer', 'Crm.modules.comments.model.CommentsModel', function(eventName, data) { // eventName --   (ins, upd, del  ..) if(eventName == 'ins' && confirm(' .   ?')) location = '#!Crm-modules-comments-controller-Comments_' + data._id }) } });
      
      







原則として、これは必要な機能を実装するのに十分ですが、Janusではすべてのモジュールに独自のモデルが必要です(これはアクセス権配布サブシステムに必要です)。 したがって、空のモデル(static / admin / modules / comments / model / EventerModel.js)を作成します。

 Ext.define('Crm.modules.comments.model.EventerModel', { extend: "Core.data.DataModel" })
      
      







コメントを管理するユーザーグループのコントローラーをスタートアップリストに登録する必要があります。 ビデオの実行方法と作業結果:





WSを使用するもう1つの利点は、「重い」機能の一部をクライアントに転送して、サーバーをオフロードできることです。 たとえば、ローカルファイルからクライアントにデータをインポートする場合、解析およびファイルからのデータの準備を転送し、既製のJSONオブジェクトの小さな部分をサーバーに送信できます。



結論



今日、私の意見では、websocketsは広範囲の使用に非常に適した技術です。 WSが使用されるシステムは、AJAXの対応物よりも視覚的に著しく応答します。 さらに、ソケットは新しい機能を追加します。特に、リアルタイムで動作するシステムを簡単に作成できます。



All Articles