📜 ⬆️ ⬇️

Using websocket in Extjs applications

Websocket is probably the most serious and useful extension to the HTTP protocol since it appeared in the early nineties. Using websockets to communicate with the server is much more profitable than the usual AJAX. Saving traffic in standard applications is significant, especially with the active exchange of client and server with small messages. Also, the response time for data requests is significantly reduced. The main obstacle to the widespread adoption of this technology for a long time was the fact that many proxy servers crookedly supported the enhanced version of the http protocol. What led, at worst, to security problems ( proof ). Over the past couple of years, the situation with the support of webboxes has begun to improve and now, in my opinion, their time has come.

This article describes the recipes for using web sockets in standard Extjs components (gridpanel, treepanel, combobox). And, also, as a replacement for Ext.Ajax.

Disclaimer


Initially, the article was planned as a continuation to my previous post about the Janusjs system. But, it seemed to me that this topic could be interesting in itself. Therefore, the first part of the post will be about websockets, extjs and nodejs, in the second part I will describe the nuances of using websockets in the Janusjs system.

The complete example code from this article can be found on Github .
')

What is ready


The idea to make friends Extjs and Websocket came to the bright mind for a long time. There is such a component: Ext.ux.data.proxy.WebSocket . This component is made in the likeness of Ext.data.proxy.Ajax. Those. The web socket is used according to the standard AJAX scheme: the client sent a request to a specific URL, read the answer. From here the main disadvantage of this implementation is that we need a separate socket for each component of the reading server. Thus, many of the benefits of web sockets are lost. If your application has only one table, then this implementation will fit perfectly. For more complex tasks, something else is needed.

When I wrote this article, I came across one more similar library: jWebSocket . Judging by the documentation, this is a serious development that deserves attention. But here the server is built in Java.

After weighing the complexity of adapting jWebSocket for Nodejs, I came to the conclusion that it is easier to modify Ext.ux.data.proxy.WebSocket .

How it works (theory)


The web socket provides a full duplex connection, in a client-server pair, both sides are equal. Sending a message from the client to the server, there is no guarantee that the first packet coming from the server will be the answer to our request. Therefore, it is necessary, somehow, to mark requests and search for the necessary ones in the stream of answers.

The second difference from AJAX is that we do not have a URL in which we can encode what data is needed.

Given all this, we outline a simple protocol for data exchange. In the client's request, we will pass the name of the function to be run on the server and parameters for it.
Request format:
{ "event": "", "opid": "", "data": { <data object> } } 

event - action identifier (create / read / update / delete);
opid - operation identifier, random number. By this code we will look for the right answer;
data - additional request parameters. For example, if this is a request from data.proxy, then there will be filtering, paging, sorting options.

The format of the answer is exactly the same as the request.

Server


Let's start with the server side. Add the required Node.js modules:
 npm i node-static websocket 


In order to avoid problems with cross-domain requests, we will create a combined server that will process the usual http requests and ws on the same port.
Procurement for the web server:
 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) { } 


Requests from the client will come in the form of a string containing JSON. It needs to be translated into the object and call the appropriate method of the model.

 ... //   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})) }) } } } ... 


For simplicity, the model in the same file as the server and its only function will be to give an array of static data:

 ... //      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 }) } } ... 


Customer


From the standard extjs sample set, take the example of a simple table and replace the AJAX proxy in it with a modified WS-proxy:
 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() }); 


In the simple example discussed above, the client script retrieves data from the server upon request. Those. we have a standard AJAX client-server interaction scheme, the only difference is in the method of receiving data. But, in real applications, if we change the familiar XHR to the new-fashioned WS, I want to get something more. For example, if one of the clients changed the data on the server, the rest should know about these changes. For this purpose, the data.Story identifier is transmitted in the WS-proxy settings: when a signal is received from the server that the data has changed, WS-proxy should initiate the appropriate actions to display these changes in the UI.

The most complete algorithms of client and server interaction via WS are implemented in the Janusjs system. The following describes the features of using web sockets in this system (the example code is available on Github ).

I will take as an example the example of the news module discussed in the previous article . Let's add users the ability to leave comments to the news in the public part of the system. When a new comment is received, the message “received a new comment” should fly out in the admin panel with a button to switch to the moderation mode for this comment.

To begin with, let's expand the news page template (protected / site / news / view / 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> 


In the public controller of the news module (protected / site / news / controller / News.js) we add the method of displaying the news card:
 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() } ... 


On the video, how it works:


To promptly notify the administrator of new comments, we will display a message. When clicking on the button in this message, you need to open a comment editing card.

Create a new controller in the comments module directory (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 }) } }); 


In principle, this is enough to implement the function we need, but Janus requires that all modules have their own models (this is necessary for the access rights distribution subsystem). Therefore, create an empty model (static / admin / modules / comments / model / EventerModel.js):
 Ext.define('Crm.modules.comments.model.EventerModel', { extend: "Core.data.DataModel" }) 


It remains to register our controller in the startup list for those user groups who will administer the comments. Video how to do it and the result of the work:


Another advantage of using WS is that now you can transfer some "heavy" functions to the client by unloading the server. For example, when importing data from local files to a client, you can transfer parsing and data preparation from a file and send small portions of ready-made JSON objects to the server.

findings


Today, in my opinion, websockets is quite a suitable technology for widespread use. Systems where WS is used are visually noticeably more responsive than AJAX analogues. In addition, sockets add new features, in particular, make it easy to create systems that work in real time.

Source: https://habr.com/ru/post/270369/


All Articles