Ext JS sur le serveur

photo d'ici https://github.com/tj/palette En ce qui concerne la bibliothèque Ext JS, les experts ont entendu beaucoup de négativité: lourd, cher, buggy. En règle générale, la plupart des problèmes sont liés à l'incapacité de le faire cuire. Un projet correctement assemblé utilisant Sencha Cmd avec tous les css, les images pèsent en production dans la région 1Mb, ce qui est comparable au même Angular. Oui, et les pépins ne sont pas beaucoup plus ...

L'idée originale de Sencha peut être traitée différemment, mais même ses adversaires de principe admettent qu'il est difficile de trouver la meilleure solution pour construire de sérieux projets intranet.

À mon avis, la chose la plus précieuse dans Ext JS n'est pas une collection de composants d'interface utilisateur, mais plutôt une bonne architecture OOP. Même en tenant compte du développement rapide de JS ces dernières années, bon nombre des choses nécessaires qui ont été implémentées dans Ext JS il y a 7 ans manquent toujours dans les classes natives (espaces de noms, mixins, propriétés statiques, appel commode des méthodes parentes). C'est ce qui m'a poussé il y a quelques années à expérimenter le lancement de classes Ext JS dans le backend. À propos des premières expériences similaires, j'ai déjà fait des publications sur Habré. Cet article décrit une nouvelle implémentation d'anciennes idées et un certain nombre de nouvelles.

Avant de commencer, attention à la question: qu'en pensez-vous, où est-elle exécutée et que fait l'extrait de code ci-dessous?

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

Ce code est exécuté sur le serveur et déclenche l'événement «newmessage» dans toutes les instances de la classe «Module.message.model.Message» sur toutes les machines clientes connectées au serveur.

Pour illustrer les possibilités d'utilisation d'Ext JS côté serveur, nous analyserons un projet de chat simple. Nous ne ferons aucune connexion, juste lorsque vous entrez, l'utilisateur entre un pseudo. Vous pouvez publier des messages généraux ou privés. Le chat devrait fonctionner en temps réel. Ceux qui le souhaitent peuvent immédiatement essayer toute cette économie en affaires.

L'installation


Pour commencer, nous avons besoin de nodejs 9+ et de redis-server (on suppose qu'ils sont déjà installés).

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

Nous démarrons le serveur:

 node server 

Dans le navigateur, ouvrez la page localhost : 3000 / www / auth /
Saisissez un surnom et appuyez sur Entrée.

Le projet est une démo, donc il n'y a pas de support pour les anciens navigateurs (il existe des designs ES8), utilisez le nouveau Chrome ou FF.

Serveur


Allons dans l'ordre.

Code serveur (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); }); 

Comme vous pouvez le constater, ici tout est plus ou moins standard pour le serveur sur express. L'intérêt est l'inclusion de classes Ext JS pour desservir les routes correspondantes:

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

Implémentation de l'API REST


La classe Api.auth.Main sert les requĂŞtes Ă  l'API REST (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() { .... } }) 

Génération de page HTML, en utilisant XTemplate sur le serveur


La deuxième classe, Www.login.controller.Login, crée une page html standard avec un formulaire de connexion (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() }); } }) 

Les modèles utilisent le XTemplate standard (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> 

Tout ce qui est décrit ci-dessus est un ensemble complètement standard, dira un lecteur méticuleux, et pour cela, il n'était pas nécessaire de clôturer ce jardin avec le transfert d'Ext JS au serveur. Par conséquent, nous passons à la deuxième partie de l'article, qui montrera à quoi tout cela était destiné.

Client


Créons l'application cliente Ext JS habituelle dans le répertoire statique. Dans cet exemple, je ne considère pas délibérément l'utilisation de cmd, j'ai pris le thème standard et ext-all déjà construit. Les questions de l'Assemblée est un sujet distinct, qui, peut-être, consacrera un poste séparé.

Tout commence avec 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' }) 

La présence d'une socket web est un point crucial, c'est elle qui vous permet d'implémenter toute la magie décrite ci-dessous.

La disposition des éléments sur la page est contenue dans la classe Admin.view.Viewport (statique / app / view / Viewport.js). Rien d'intéressant là-bas.

Les principaux éléments fonctionnels (liste d'utilisateurs, barre de messages et formulaire d'envoi) sont implémentés en modules séparés.

Liste d'utilisateurs


L'algorithme simple de cette liste est le suivant: au moment de l'ouverture de la page, les utilisateurs actuels sont chargés depuis le serveur. Lorsque de nouveaux utilisateurs se connectent, le serveur génère un événement «add» dans la classe «Module.users.model.UserModel», lorsqu'il est déconnecté, dans la même classe, l'événement «remove» est déclenché. Le fait est que l'événement est déclenché côté serveur et que vous pouvez le suivre sur le client.

Maintenant, tout d'abord. Côté client, Store jongle avec les données (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)) } }); 

Il y a 2 points intéressants. Tout d'abord, dans la ligne "const data = attendez this.dataModel. $ Read ();" La méthode serveur du modèle est appelée. Maintenant, vous n'avez plus besoin d'utiliser Ajax, les protocoles de support, etc., il suffit d'appeler la méthode serveur comme locale. Dans le même temps, la sécurité n'est pas sacrifiée (plus d'informations ci-dessous).

Deuxièmement, la construction standard de this.dataModel.on (...) vous permet de suivre les événements qui seront générés par le serveur.

Le modèle est un pont entre les parties client et serveur de l'application. C'est comme le dualisme de la lumière - il met en œuvre les propriétés du frontend et du backend. Examinons attentivement le modèle.

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

Faites attention aux commentaires / * portée: serveur * / et / * portée: client * / - ces constructions sont des étiquettes pour le serveur par lesquelles elle détermine le type de méthode.

testClientMethod - cette méthode s'exécute exclusivement sur le client et n'est disponible que du côté client.
testGlobalMethod - cette méthode s'exécute sur le client et sur le serveur et est disponible pour une utilisation côté client et serveur.
privateServerMethod - la méthode est exécutée sur le serveur et est disponible pour appeler uniquement sur le serveur.
$ read est le type de méthode le plus intéressant qui ne s'exécute que côté serveur, mais vous pouvez l'appeler à la fois sur le client et sur le serveur. Le préfixe $ rend toute méthode côté serveur disponible côté client.

Vous pouvez suivre la connexion et la déconnexion du client à l'aide d'une prise Web. Une instance de la classe Base.wsClient est créée pour chaque connexion utilisateur (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); } }) 

La méthode fireEvent, contrairement à la méthode standard, a un paramètre supplémentaire, où il est transmis sur quel client l'événement doit être déclenché. Il est acceptable de passer un seul identifiant client, un tableau d'identifiants ou la chaîne «tous». Dans ce dernier cas, l'événement sera déclenché sur tous les clients connectés. Sinon, il s'agit d'un FireEvent standard.

Envoi et réception de messages


Le contrĂ´leur de formulaire (statique / app / admin / modules / messages / view / FormController.js) est responsable de l'envoi des messages.

 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(''); } }); 

Le message n'est enregistré nulle part sur le serveur, l'événement «newmessage» est simplement déclenché. L'intérêt est l'appel "this.fireEvent ('newmessage', data.to, msg);", où les identifiants client sont passés en tant que destinataires du message. Ainsi, la distribution des messages privés est implémentée (statique / 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; } }) 

Comme pour les utilisateurs, les données de la liste des messages sont gérées par le magasin (statique / 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); } }); 

En général, c'est tout ce qui est intéressant dans cet exemple.

Questions possibles


La disponibilité des méthodes serveur sur le client est bien sûr bonne, mais qu'en est-il de la sécurité? Il s'avère qu'un pirate maléfique peut voir le code du serveur et essayer de casser le backend?

Non, il ne réussira pas. Tout d'abord, toutes les méthodes serveur sont supprimées du code de classe lorsqu'elles sont envoyées au navigateur client. A cet effet, les commentaires / directives / * portée sont destinés: ... * /. Deuxièmement, le code de la méthode côté serveur la plus publique est remplacé par une construction intermédiaire qui implémente le mécanisme d'appel distant côté client.

Encore une fois sur la sécurité. Si des méthodes serveur peuvent être appelées sur le client, il s'avère que je peux appeler une telle méthode? Et s'il s'agit d'une méthode de nettoyage de base de données?

Depuis le client, vous ne pouvez appeler que des méthodes qui ont le préfixe $ dans leur nom. Pour de telles méthodes, vous déterminez vous-même la logique des contrôles et des accès. Un utilisateur externe n'a pas accès aux méthodes serveur sans $, il ne les verra même pas (voir réponse précédente)

Il semble que vous ayez un système monolithique dans lequel le client et le serveur sont inextricablement liés. Une mise à l'échelle horizontale est-elle possible?

Le système semble en effet monolithique, mais ce n'est pas le cas. Le client et le serveur peuvent "vivre" sur différentes machines. Le client peut être exécuté sur n'importe quel serveur Web tiers (Nginx, Apache, etc.). Le problème de la séparation du client et du serveur est très facilement résolu par le constructeur de projet automatique (je peux écrire un article séparé à ce sujet). Pour implémenter le mécanisme de messagerie de service interne, le système utilise des files d'attente (à savoir, Redis est requis pour cela). Ainsi, la partie serveur peut être facilement mise à l'échelle horizontalement en ajoutant simplement de nouvelles machines.

Avec l'approche de développement habituelle, le backend fournit généralement un certain ensemble d'API auxquelles vous pouvez vous connecter avec diverses applications clientes (site Web, application mobile). Dans votre cas, il s'avère que seul un client écrit en Ext JS peut fonctionner avec un backend?

Sur le serveur, notamment dans les modèles de modules, une certaine logique métier est implémentée. Pour y accéder via l'API REST, un petit «wrapper» suffit. Un exemple correspondant est présenté dans la première partie de cet article.

Conclusions


Comme vous pouvez le voir, pour un codage confortable d'applications assez complexes, il est tout à fait possible de le faire avec une bibliothèque sur le frontal et le backend. Cela présente des avantages importants.

Accélérer le processus de développement. Chacun des membres de l'équipe peut travailler sur le backend et le frontend. Le temps d'arrêt pour la raison «j'attends que cette API apparaisse sur le serveur» n'est plus pertinent.

Moins de code. Les mêmes sections de code peuvent être utilisées sur le client et sur le serveur (contrôles, vérification, etc.).

La maintenance d'un tel système est beaucoup plus simple et moins chère. Au lieu de deux programmeurs différents, le système pourra en prendre en charge un (ou les deux mêmes mais interchangeables). Pour la même raison, les risques liés au roulement des équipes sont également plus faibles.

La possibilité de créer des systèmes en temps réel prêts à l'emploi.

Utilisation d'un système de test unique pour le backend et le frontent.

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


All Articles