Ext JS en el servidor

foto de aquí https://github.com/tj/palette Cuando se trata de la biblioteca Ext JS, los expertos han escuchado mucha negatividad: pesada, costosa, con errores. Como regla general, la mayoría de los problemas están relacionados con la incapacidad para cocinarlo. Un proyecto correctamente ensamblado que utiliza Sencha Cmd con todos los css, las imágenes pesan en producción en la región de 1Mb, que es comparable al mismo Angular. Sí, y los problemas técnicos no son mucho más ...

La creación de Sencha puede ser tratada de manera diferente, pero incluso sus oponentes de principios admiten que es difícil encontrar la mejor solución para construir proyectos serios de intranet.

En mi opinión, lo más valioso en Ext JS no es una colección de componentes de la interfaz de usuario, sino más bien una buena arquitectura OOP. Incluso teniendo en cuenta el rápido desarrollo de JS en los últimos años, muchas de las cosas necesarias que se implementaron en Ext JS hace 7 años todavía faltan en las clases nativas (espacios de nombres, mixins, propiedades estáticas, llamadas convenientes de métodos principales). Esto es lo que me impulsó hace unos años a experimentar con el lanzamiento de las clases Ext JS en el back-end. Sobre los primeros experimentos similares ya hice publicaciones en Habré. Este artículo describe una nueva implementación de ideas antiguas y varias nuevas.

Antes de comenzar, preste atención a la pregunta: ¿qué piensa, dónde se ejecuta y qué hace el fragmento de código a continuación?

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

Este código se ejecuta en el servidor y provoca el evento "newmessage" en todas las instancias de la clase "Module.message.model.Message" en todas las máquinas cliente conectadas al servidor.

Para ilustrar las posibilidades de usar Ext JS del lado del servidor, analizaremos un proyecto de chat simple. No haremos ningún inicio de sesión, solo cuando ingresa el usuario ingresa un apodo. Puede publicar mensajes generales o privados. El chat debería funcionar en tiempo real. Aquellos que lo deseen pueden probar de inmediato toda esta economía en los negocios.

Instalación


Para comenzar, necesitamos nodejs 9+ y redis-server (se supone que ya están instalados).

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

Iniciamos el servidor:

 node server 

En el navegador, abra la página localhost : 3000 / www / auth /
Ingrese un apodo y presione enter.

El proyecto es una demostración, por lo que no hay soporte para navegadores antiguos (hay diseños ES8), use el nuevo Chrome o FF.

Servidor


Vamos en orden.

Código de servidor (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); }); 

Como puede ver, aquí todo es más o menos estándar para el servidor en express. De interés es la inclusión de clases Ext JS para servir las rutas correspondientes:

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

Implementación de API REST


La clase Api.auth.Main atiende solicitudes a la API REST (protegido / 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() { .... } }) 

Generación de páginas HTML, usando XTemplate en el servidor


La segunda clase, Www.login.controller.Login, crea una página html normal con un formulario de inicio de sesión (protegido / 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() }); } }) 

Las plantillas usan el XTemplate estándar (protegido / 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> 

Todo lo descrito anteriormente es un conjunto completamente estándar, dirá un lector meticuloso, y para esto no hubo necesidad de cercar este jardín con la transferencia de Ext JS al servidor. Por lo tanto, pasamos a la segunda parte del artículo, que mostrará para qué fue todo.

Cliente


Creemos la aplicación Ext JS de cliente habitual en el directorio estático. En este ejemplo, deliberadamente no considero el uso de cmd, tomé el tema estándar y ext-all ya construido. Los problemas de ensamblaje son un tema separado, que, quizás, dedicará una publicación separada.

Todo comienza con 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 presencia de un socket web es un punto crucial, es el que le permite implementar toda la magia que se describe a continuación.

El diseño de los elementos en la página está contenido en la clase Admin.view.Viewport (static / app / view / Viewport.js). Nada interesante allí.

Los principales elementos funcionales (lista de usuarios, barra de mensajes y formulario de envío) se implementan como módulos separados.

Lista de usuarios


El algoritmo simple de esta lista es el siguiente: en el momento de abrir la página, los usuarios actuales se cargan desde el servidor. Cuando se conectan nuevos usuarios, el servidor genera un evento "agregar" en la clase "Module.users.model.UserModel", cuando se desconecta, en la misma clase, se genera el evento "eliminar". La cuestión es que el evento se desencadena en el lado del servidor y puede realizar un seguimiento en el cliente.

Ahora, lo primero es lo primero. En el lado del cliente, Store hace malabares con los datos (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)) } }); 

Hay 2 puntos interesantes. En primer lugar, en la línea "const data = await this.dataModel. $ Read ();" Se llama al método de servidor del modelo. Ahora no necesita usar Ajax, protocolos de soporte, etc., simplemente llame al método del servidor como local. Al mismo tiempo, no se sacrifica la seguridad (más sobre eso a continuación).

En segundo lugar, la construcción estándar de this.dataModel.on (...) le permite realizar un seguimiento de los eventos que generará el servidor.

El modelo es un puente entre las partes cliente y servidor de la aplicación. Es como el dualismo de la luz: implementa las propiedades tanto del frontend como del backend. Miremos el modelo con cuidado.

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

Preste atención a los comentarios / * alcance: servidor * / y / * alcance: cliente * / - estas construcciones son etiquetas para el servidor por las cuales determina el tipo de método.

testClientMethod: este método se ejecuta exclusivamente en el cliente y solo está disponible en el lado del cliente.
testGlobalMethod: este método se ejecuta en el cliente y en el servidor y está disponible para su uso en el lado del cliente y del servidor.
privateServerMethod: el método se ejecuta en el servidor y está disponible para llamar solo en el servidor.
$ read es el tipo de método más interesante que se ejecuta solo en el lado del servidor, pero puede llamarlo tanto en el cliente como en el servidor. El prefijo $ hace que cualquier método del lado del servidor esté disponible en el lado del cliente.

Puede rastrear la conexión y desconexión del cliente utilizando un socket web. Se crea una instancia de la clase Base.wsClient para cada conexión de usuario (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); } }) 

El método fireEvent, a diferencia del estándar, tiene un parámetro adicional, donde se pasa en qué cliente se debe activar el evento. Es aceptable pasar un único identificador de cliente, una matriz de identificadores o la cadena "todos". En el último caso, el evento se activará en todos los clientes conectados. De lo contrario, este es un evento fireEvent estándar.

Enviar y recibir mensajes


El controlador de formulario (static / app / admin / modules / messages / view / FormController.js) es responsable de enviar los mensajes.

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

El mensaje no se guarda en ningún lugar del servidor, simplemente se activa el evento "newmessage". De interés es la llamada "this.fireEvent ('newmessage', data.to, msg);", donde los identificadores del cliente se pasan como destinatarios del mensaje. Por lo tanto, se implementa la distribución de mensajes privados (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; } }) 

Al igual que con los usuarios, los datos para la lista de mensajes son manejados por la Tienda (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); } }); 

En general, esto es todo lo que es interesante en este ejemplo.

Posibles preguntas


La disponibilidad de métodos de servidor en el cliente es, por supuesto, buena, pero ¿qué pasa con la seguridad? ¿Resulta que un hacker malvado puede ver el código del servidor e intentar descifrar el backend?

No, no tendrá éxito. Primero, todos los métodos del servidor se eliminan del código de clase cuando se envían al navegador del cliente. Para este propósito, los comentarios / directivas / * alcance están destinados: ... * /. En segundo lugar, el código del método más público del lado del servidor se reemplaza por una construcción intermedia que implementa el mecanismo de llamada remota en el lado del cliente.

De nuevo sobre seguridad. Si se pueden invocar métodos de servidor en el cliente, ¿puedo llamar a alguno de estos métodos? ¿Y si este es un método de limpieza de base de datos?

Desde el cliente, solo puede llamar a métodos que tienen el prefijo $ en su nombre. Para tales métodos, usted mismo determina la lógica de los controles y accesos. Un usuario externo no tiene acceso a los métodos del servidor sin $, ni siquiera los verá (ver respuesta anterior)

Parece que tienes un sistema monolítico en el que el cliente y el servidor están inextricablemente vinculados. ¿Es posible el escalado horizontal?

El sistema, de hecho, parece monolítico, pero no lo es. El cliente y el servidor pueden "vivir" en diferentes máquinas. El cliente se puede ejecutar en cualquier servidor web de terceros (Nginx, Apache, etc.). El generador de proyectos automático resuelve el problema de la separación del cliente y el servidor (puedo escribir una publicación por separado sobre esto). Para implementar el mecanismo de mensajería de servicio interno, el sistema usa colas (es decir, se requiere Redis para esto). Por lo tanto, la parte del servidor puede escalarse fácilmente horizontalmente simplemente agregando nuevas máquinas.

Con el enfoque de desarrollo habitual, por regla general, el back-end proporciona un cierto conjunto de API a las que puede conectarse con diversas aplicaciones de cliente (sitio web, aplicación móvil). En su caso, ¿resulta que solo un cliente escrito en Ext JS puede trabajar con un back-end?

En el servidor, en particular en los modelos de módulos, se implementa una cierta lógica empresarial. Para proporcionar acceso a través de la API REST, un pequeño "contenedor" es suficiente. Un ejemplo correspondiente se presenta en la primera parte de este artículo.

Conclusiones


Como puede ver, para una codificación cómoda de aplicaciones bastante complejas, es bastante posible sobrevivir con una biblioteca en el front-end y back-end. Esto tiene beneficios significativos.

Acelerando el proceso de desarrollo. Cada uno de los miembros del equipo puede trabajar en el backend y la interfaz. El tiempo de inactividad por la razón "Estoy esperando que esta API aparezca en el servidor" ya no es relevante.

Menos código Las mismas secciones de código se pueden usar en el cliente y en el servidor (verificaciones, verificación, etc.).

Mantener dicho sistema es mucho más simple y económico. En lugar de dos programadores diversos, el sistema podrá soportar uno (o los mismos dos pero intercambiables). Por la misma razón, los riesgos asociados con la rotación del equipo también son menores.

La capacidad de crear sistemas en tiempo real fuera de la caja.

Usando un único sistema de prueba para backend y frontent.

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


All Articles