📜 ⬆️ ⬇️

Ext JS on server

Photos from here https://github.com/tj/palette When it comes to the Ext JS library, there is quite a lot of negativity from the experts: hard, expensive, buggy. As a rule, most of the problems associated with the inability to cook it. A project correctly assembled using Sencha Cmd with all css, pictures weighs in production in the region of 1Mb, which is comparable to the same Angular. And the glitches are not much more ...

It is possible to relate differently to this offspring of the company Sencha, but even its principled opponents recognize that it is difficult to find the best solution for building serious intranet projects.

In my opinion, the most valuable thing in Ext JS is not a collection of UI components, but a rather successful OOP architecture. Even taking into account the rapid development of JS in recent years, many of the necessary things that were implemented in Ext JS 7 years ago, are still missing in native classes (namespaces, mixins, static properties, convenient calling of parent methods). This is what prompted me a few years ago to experiment with the launch of the Ext JS classes in the bend. About the first such experiments I have already done posts on Habré. This article describes a new implementation of old ideas and a number of fresh ones.
')
Before we begin, attention to the question: what do you think, where is the execution and what does the below code fragment do?

Ext.define('Module.message.model.Message', { .... /* scope:server */ ,async newMessage() { ......... this.fireEvent('newmessage', data); ...... } ... }) 

This code is executed on the server and raises the "newmessage" event in all instances of the class "Module.message.model.Message" on all client machines connected to the server.

To illustrate the possibilities of using Ext JS server, let's analyze a simple chat project. Login will not do anything, just when entering the user enters a nickname. You can post general or private messages. Chat should work in real time. Those interested can immediately try all this stuff in business.

Installation


To run, we need nodejs 9+ and redis-server (assuming they are already installed).

 git clone https://github.com/Kolbaskin/extjs-backend-example cd extjs-backend-example npm i 

We get the server:

 node server 

Open localhost : 3000 / www / auth / in the browser
Enter some nickname and click "enter".

The project is demo, so there is no support for old browsers (there are ES8 constructions), use the new Chrome or FF.

Server


Let's go in order.

Server code (server.js)

 //   http-  express //   Ext JS     express const express = require('express'); const staticSrv = require('extjs-express-static'); const app = express(); const bodyParser = require('body-parser'); //    global = { config: require('config') } //     Ext JS require('extjs-on-backend')({ //     express app, //         wsClient: 'Base.wsClient' }); //    Ext.Loader.setPath('Api', 'protected/rest'); Ext.Loader.setPath('Base', 'protected/base'); Ext.Loader.setPath('Www', 'protected/www'); //   http   app.use( bodyParser.json() ); app.use(bodyParser.urlencoded({ extended: true })); //     Ext JS  app.use('/api/auth', Ext.create('Api.auth.Main')); app.use('/www/auth', Ext.create('Www.login.controller.Login')); //    app.use(staticSrv(__dirname + '/static')); //   const server = app.listen(3000, () => { console.log('server is running at %s', server.address().port); }); 

As you can see, everything is more or less standard for the server on express. Of interest is the connection of classes Ext JS for the maintenance of the corresponding routes:

 app.use('/api/auth', Ext.create('Api.auth.Main')); app.use('/www/auth', Ext.create('Www.login.controller.Login')); 

REST API implementation


The class Api.auth.Main serves requests to the REST API (protected / rest / auth / Main.js).

 Ext.define('Api.auth.Main', { extend: 'Api.Base', //   //     routes: [ { path: '/', get: 'login'}, { path: '/restore', post: 'restoreLogin' }, { path: '/registration', post: 'newuser'}, { path: '/users', get: 'allUsers'} ] //     : // {query: <...>, params: <...>, body: <...>} ,async login(data) { return {data:[{ id:1, subject: 111, sender:222, }]} } ,async restoreLogin() { ... } ,async newuser() { ... } ,async allUsers() { .... } }) 

Generate HTML pages, use XTemplate on server


The second class Www.login.controller.Login builds a regular html page with a login form (protected / www / login / controller / Login.js).

 Ext.define('Www.login.controller.Login', { //      "" : // ,    .. extend: 'Www.Base' //    //   ,   .. ,baseTpl: 'view/inner' //     // ,   ,loginFormTpl: 'login/view/login' //  ,routes: [ { path: '/', get: 'loginForm', post: 'doLogin'} ] //  html   //       ,async loginForm () { return await this.tpl(this.loginFormTpl, { pageTitle: 'Login page', date: new Date() }); } ,async doLogin (params, res) { if(params.body.name && /^[a-z0-9]{2,10}$/i.test(params.body.name)) { this.redirect(`/index.html?name=${params.body.name}`, res); return; } return await this.tpl(this.loginFormTpl, { pageTitle: 'Login page', date: new Date() }); } }) 

The templates use the standard XTemplate (protected / www / login / view / login.tpl)

 <h2>{pageTitle} (date: {[Ext.Date.format(values.date,'dmY')]})</h2> <form method="post"> <input name="name" placeholder="name"> <button type="submit">enter</button> </form> 

Everything described above is quite a standard set, the meticulous reader will say, and for this there was no need to fence this garden with the transfer of Ext JS to the server. Therefore, we turn to the second part of the article, where it will be shown what all this was intended for.

Customer


Create a normal client Ext JS application in the static directory. In this example, I deliberately do not consider the use of cmd, took the already assembled ext-all and the standard theme. Build issues is a separate topic, which may devote a separate post.

It all starts with app.js

 //   Ext.Loader.setConfig({ enabled: true, paths: { "Core": "app/core", "Admin": "app/admin", "Module": "app/admin/modules", "Ext.ux": "ext/ux" } }); //    this.token = Ext.data.identifier.Uuid.createRandom()(); //      //    () //    (   ) Ext.WS = Ext.create('Core.WSocket', { token: this.token, user: new URLSearchParams(document.location.search).get("name") }); //   Ext.application({ name: 'Example', extend: 'Ext.app.Application', requires: ['Admin.*'], autoCreateViewport: 'Admin.view.Viewport' }) 

The presence of a web socket is a crucially important point, it allows him to realize all the magic described below.

The layout of the elements on the page is contained in the class Admin.view.Viewport (static / app / view / Viewport.js). There is nothing interesting.

The main functional elements (list of users, message panel and form of sending) are implemented as separate modules.

a list of users


The simple algorithm of this list operation is as follows: at the moment of opening the page, current users are loaded from the server. When new users are connected, the server generates an “add” event in the class “Module.users.model.UserModel”, when disconnected, the “remove” event is triggered in the same class. The whole thing is that the event is initiated on the server side, and you can track it on the client.

Now, first things first. On the client side, data juggles Store (static / app / modules / users / store / UsersStore.js)

 Ext.define('Module.users.store.UsersStore', { extend: 'Ext.data.Store' ,autoLoad: true ,total: 0 ,constructor() { //         this.dataModel = Ext.create('Module.users.model.UserModel'); //      this.dataModel.on({ add: (records) => { this.onDataAdd(records) }, remove: (records) => { this.onDataRemove(records) } }) this.callParent(arguments) } //   load ,async load() { //      const data = await this.dataModel.$read(); //   this.total = data.total; //    UI this.loadData(data.data); } ,getTotalCount() { return this.total; } //          ,onDataAdd(records) { this.add(records[0]); } //   --  ,onDataRemove(records) { this.remove(this.getById (records[0].id)) } }); 

There are 2 interesting points. First, in the string “const data = await this.dataModel. $ Read ();” the server method of the model is called. Now you do not need to use Ajax, support protocols, etc., just call the server method as local. At the same time, security is not sacrificed (more on this below).

Secondly, the standard construction of this.dataModel.on (...) allows you to track events that will be generated by the server.

The model is a bridge between the client and server part of the application. It is like a dualism of light - it realizes the properties of both the frontend and bekend. Let's look at the model carefully.

 Ext.define('Module.users.model.UserModel', { extend: 'Core.data.DataModel' /* scope:client */ ,testClientMethod() { ... } ,testGlobalMethod() { ... } /* scope:server */ ,privateServerMethod() { .... } /* scope:server */ ,async $read(params) { //      redis const keys = await this.getMemKeys('client:*'); let data = [], name; for(let i = 0;i<keys.length;i++) { //         name = await this.getMemKey(keys[i]); if(name) { data.push({ id: keys[i].substr(7), name }) } } //    return { total: data.length, data } } }) 

Pay attention to the comments / * scope: server * / and / * scope: client * / - these constructions are labels for the server by which it determines the type of method.

testClientMethod - this method runs exclusively on the client and is available only on the client side.
testGlobalMethod - this method runs on the client and on the server and is available for use for the client and server side.
privateServerMethod - the method runs on the server and is available for calling only on the server.
$ read is the most interesting type of method that runs only on the server side, but you can call it both on the client and on the server. The prefix "$" makes any server method available on the client side.

You can monitor the connection and disconnection of a client using a web socket. For each user connection an instance of the class “Base.wsClient” is created (protected / base / wsClient.js)

 Ext.define('Base.wsClient', { extend: 'Core.WsClient' //      ,usersModel: Ext.create('Module.users.model.UserModel') //       ,async onStart() { //   "add"    this.usersModel.fireEvent('add', 'all', [{id: this.token, name: this.req.query.user}]); //     redis await this.setMemKey(`client:${this.token}`, this.req.query.user || ''); //   ""      , //     await this.queueProcess(`client:${this.token}`, async (data, done) => { const res = await this.prepareClientEvents(data); done(res); }) } //      ,onClose() { //   "remove"    this.usersModel.fireEvent('remove', 'all', [{id: this.token, name: this.req.query.user}]) this.callParent(arguments); } }) 

The “fireEvent” method, in contrast to the standard one, has an additional parameter, where it is transmitted on which client the event should be triggered. It is possible to pass a single client identifier, an array of identifiers, or the string “all”. In the latter case, the event will be triggered on all connected clients. Otherwise, this is a standard fireEvent.

Sending and receiving messages


The form controller is responsible for sending messages (static / app / admin / modules / messages / view / FormController.js).

 Ext.define('Module.messages.view.FormController', { extend: 'Ext.app.ViewController' ,init(view) { this.view = view; //     this.model = Ext.create('Module.messages.model.Model'); //      this.msgEl = this.view.down('[name=message]'); //     this.usersGrid = Ext.getCmp('users-grid') //    "" this.control({ '[action=submit]' : {click: () => {this.newMessage() }} }) } //     ,newMessage() { let users = []; //     const sel = this.usersGrid.getSelection(); if(sel && sel.length) { sel.forEach((s) => { users.push(s.data.id) }) } //        if(users.length && users.indexOf(Ext.WS.token) == -1) users.push(Ext.WS.token); //       this.model.$newmessage({ to: users, user: Ext.WS.user, message: this.msgEl.getValue() }) //    this.msgEl.setValue(''); } }); 

On the server, the message is not stored anywhere, just the event "newmessage" is triggered. Of interest is the call “this.fireEvent ('newmessage', data.to, msg);”, where clients' identifiers are sent as message recipients. Thus, the delivery of private messages is implemented (static / app / admin / modules / messages / model / Model.js).

 Ext.define('Module.messages.model.Model', { extend: 'Core.data.DataModel' /* scope:server */ ,async $newmessage(data) { const msg = { user: data.user, message: data.message } if(data.to && Ext.isArray(data.to) && data.to.length) { this.fireEvent('newmessage', data.to, msg); } else { this.fireEvent('newmessage', 'all', msg); } return true; } }) 

As in the case of users, the data for the message list drives Store (static / app / admin / modules / messages / store / MessagesStore.js)

 Ext.define('Module.messages.store.MessagesStore', { extend: 'Ext.data.Store', fields: ['user', 'message'], constructor() { //       Ext.create('Module.messages.model.Model', { listeners: { newmessage: (mess) => { this.add(mess) } } }) this.callParent(arguments); } }); 

In general, this is all that is interesting in this example.

Possible questions


Availability of server methods on the client is, of course, good, but what about security? It turns out that an evil hacker can see the server code and try to hack the back-end?

No, it will not work. First, all server methods are removed from the class code when sent to the client browser. For this purpose, comments are intended directives / * scope: ... * /. Secondly, the code of the most public server method is replaced by an intermediate structure that implements the remote call mechanism on the client side.

Again about security. If server methods can be called on the client, it turns out, can I call any such method? And if this is a database cleanup method?

From the client, you can only call methods that have the $ prefix in their name. For such methods, you yourself determine the logic of checks and accesses. The external user does not have any access to server methods without $, he will not even see them (see previous answer)

It looks like you got a monolithic system in which the client and server are inextricably linked. Is horizontal scaling possible?

The system really looks monolithic, but it is not. The client and the server can live on different machines. The client can be run on any third-party web server (Nginx, Apache, etc.). The question of separating the client and the server is very simply solved by the automatic project builder (I can write a separate post about this). The system uses queues to implement the internal service message exchange mechanism (namely, Redis is required for this). Thus, the server part can be easily scaled horizontally by simply adding new machines.

In the usual approach to development, as a rule, the backend provides a certain set of APIs to which you can connect diverse client applications (website, mobile application). In your case, it turns out that only a client written on Ext JS can work with a backend?

On the server, in particular in the models of modules, some business logic is implemented. In order to provide access to it through the REST API, a rather small “wrapper” is required. The corresponding example is presented in the first part of this article.

findings


As you can see, for comfortable coding of quite complex applications, it is quite possible to do with one library at the frontend and backend. This provides significant benefits.

Accelerating the development process. Each of the team members can work on the back-end and the frontend. Downtime due to “I am waiting for this API to appear on the server” becomes irrelevant.

Less code. The same parts of the code can be used on the client and on the server (verification, verification, etc.).

Maintaining such a system is much easier and cheaper. Instead of two diverse programmers, the system will be able to support one (or the same two but interchangeable). For the same reason, the risks associated with running a team are also lower.

The ability out of the box to create real-time systems.

Using a unified testing system for backends and fronts.

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


All Articles