JS ext no servidor

foto daqui https://github.com/tj/palette Quando se trata da biblioteca Ext JS, os especialistas ouviram bastante negatividade: pesado, caro, com erros. Como regra, a maioria dos problemas está relacionada à incapacidade de cozinhá-lo. Um projeto montado corretamente usando o Sencha Cmd com todos os css, as imagens pesam na produção na região de 1Mb, comparável à mesma Angular. Sim, e falhas não são muito mais ...

A ideia de Sencha pode ser tratada de maneira diferente, mas mesmo seus oponentes de princípios admitem que é difícil encontrar a melhor solução para a criação de projetos sérios na intranet.

Na minha opinião, a coisa mais valiosa no Ext JS não é uma coleção de componentes da interface do usuário, mas uma boa arquitetura OOP. Mesmo levando em consideração o rápido desenvolvimento do JS nos últimos anos, muitas das coisas necessárias que foram implementadas no Ext JS há 7 anos ainda estão ausentes nas classes nativas (namespaces, mixins, propriedades estáticas, chamada conveniente dos métodos pai). Isso foi o que me levou, há alguns anos, a experimentar o lançamento das classes Ext JS no back-end. Sobre os primeiros experimentos semelhantes, eu já fiz posts sobre Habré. Este artigo descreve uma nova implementação de idéias antigas e várias novas.

Antes de começar, atenção à pergunta: o que você acha, onde é executado e o que o trecho de código abaixo faz?

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

Esse código é executado no servidor e gera o evento "newmessage" em todas as instâncias da classe "Module.message.model.Message" em todas as máquinas clientes conectadas ao servidor.

Para ilustrar as possibilidades de usar Ext JS do lado do servidor, analisaremos um projeto de bate-papo simples. Não faremos nenhum login, apenas quando você digitar o usuário digitar um apelido. Você pode postar mensagens gerais ou privadas. O bate-papo deve funcionar em tempo real. Os interessados ​​podem experimentar imediatamente toda essa economia nos negócios.

Instalação


Para começar, precisamos do nodejs 9+ e do redis-server (presume-se que eles já estejam instalados).

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

Iniciamos o servidor:

 node server 

No navegador, abra a página localhost : 3000 / www / auth /
Digite um apelido e pressione enter.

O projeto é uma demonstração, portanto, não há suporte para navegadores antigos (existem modelos ES8), use o novo Chrome ou FF.

Servidor


Vamos em ordem.

Código do 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 você pode ver, aqui tudo é mais ou menos padrão para o servidor no express. É interessante a inclusão de classes Ext JS para servir as rotas correspondentes:

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

Implementação da API REST


A classe Api.auth.Main atende solicitações à 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() { .... } }) 

Geração de página HTML, usando XTemplate no servidor


A segunda classe, Www.login.controller.Login, cria uma página html comum com um formulário de login (protected / www / login / controller / 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() }); } }) 

Os modelos usam o XTemplate padrão (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> 

Tudo o que foi descrito acima é um conjunto completamente padrão, dirá um leitor meticuloso, e para isso não havia necessidade de bloquear esse jardim com a transferência Ext JS para o servidor. Portanto, passamos para a segunda parte do artigo, que mostrará para que tudo foi planejado.

Cliente


Vamos criar o aplicativo Ext JS usual do cliente no diretório estático. Neste exemplo, eu deliberadamente não considero o uso do cmd, usei o ext-all já construído e o tema padrão. Questões de montagem é um tópico separado, que, talvez, dedique um post separado.

Tudo começa com 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' }) 

A presença de um soquete da Web é um ponto crucial, é o que permite implementar toda a mágica descrita abaixo.

O layout dos elementos na página está contido na classe Admin.view.Viewport (static / app / view / Viewport.js). Nada de interessante por lá.

Os principais elementos funcionais (lista de usuários, barra de mensagens e formulário de envio) são implementados como módulos separados.

Lista de Usuários


O algoritmo simples desta lista é o seguinte: no momento da abertura da página, os usuários atuais são carregados do servidor. Quando novos usuários se conectam, o servidor gera um evento "add" na classe "Module.users.model.UserModel", quando desconectado, na mesma classe, o evento "remove" é gerado. A questão é que o evento é disparado no lado do servidor e você pode rastreá-lo no cliente.

Agora, as primeiras coisas primeiro. No lado do cliente, a loja manipula dados (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)) } }); 

Existem 2 pontos interessantes. Primeiramente, na linha “const data = aguardam this.dataModel. $ Read ();” O método do servidor do modelo é chamado. Agora você não precisa usar o Ajax, protocolos de suporte etc., basta chamar o método do servidor como local. Ao mesmo tempo, a segurança não é sacrificada (mais sobre isso abaixo).

Em segundo lugar, a construção padrão de this.dataModel.on (...) permite rastrear eventos que serão gerados pelo servidor.

O modelo é uma ponte entre as partes cliente e servidor do aplicativo. É como o dualismo da luz - implementa as propriedades do front-end e do back-end. Vamos olhar o modelo com 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 atenção aos comentários / * scope: server * / e / * scope: client * / - essas construções são rótulos para o servidor pelo qual ele determina o tipo de método.

testClientMethod - esse método é executado exclusivamente no cliente e está disponível apenas no lado do cliente.
testGlobalMethod - esse método é executado no cliente e no servidor e está disponível para uso no cliente e no servidor.
privateServerMethod - o método é executado no servidor e está disponível para chamar apenas no servidor.
$ read é o tipo de método mais interessante que é executado apenas no lado do servidor, mas você pode chamá-lo no cliente e no servidor. O prefixo $ disponibiliza qualquer método do lado do servidor no lado do cliente.

Você pode rastrear a conexão e desconexão do cliente usando um soquete da web. Uma instância da classe Base.wsClient é criada para cada conexão do usuário (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); } }) 

O método fireEvent, diferente do padrão, possui um parâmetro adicional, onde é passado para qual cliente o evento deve ser acionado. É aceitável passar um único identificador de cliente, uma matriz de identificadores ou a string "all". Neste último caso, o evento será acionado em todos os clientes conectados. Caso contrário, este é um fireEvent padrão.

Enviando e recebendo mensagens


O controlador de formulário (static / app / admin / modules / messages / view / FormController.js) é responsável pelo envio de mensagens.

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

A mensagem não é salva em nenhum lugar do servidor, o evento "newmessage" é simplesmente acionado. De interesse é a chamada “this.fireEvent ('newmessage', data.to, msg);”, onde os identificadores de clientes são enviados como destinatários da mensagem. Assim, a distribuição de mensagens privadas é implementada (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; } }) 

Assim como os usuários, os dados da lista de mensagens são controlados pela loja (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); } }); 

Em geral, isso é tudo o que é interessante neste exemplo.

Possíveis perguntas


A disponibilidade de métodos de servidor no cliente é, obviamente, boa, mas e a segurança? Acontece que um hacker malvado pode ver o código do servidor e tentar quebrar o back-end?

Não, ele não terá sucesso. Primeiro, todos os métodos do servidor são removidos do código da classe quando enviados ao navegador do cliente. Para esse fim, os comentários / diretrizes / * escopo destinam-se: ... * /. Em segundo lugar, o código do método mais público do lado do servidor é substituído por uma construção intermediária que implementa o mecanismo de chamada remota no lado do cliente.

Mais uma vez sobre segurança. Se os métodos do servidor puderem ser chamados no cliente, posso chamar um desses métodos? E se esse for um método de limpeza de banco de dados?

No cliente, você pode chamar apenas métodos que tenham o prefixo $ no nome. Para esses métodos, você mesmo determina a lógica de verificações e acessos. Um usuário externo não tem acesso aos métodos do servidor sem $, ele nem os verá (veja a resposta anterior)

Parece que você possui um sistema monolítico no qual o cliente e o servidor estão inextricavelmente vinculados. A escala horizontal é possível?

O sistema, de fato, parece monolítico, mas não é. O cliente e o servidor podem "viver" em máquinas diferentes. O cliente pode ser executado em qualquer servidor Web de terceiros (Nginx, Apache, etc.). A questão da separação de cliente e servidor é resolvida com muita facilidade pelo criador de projeto automático (posso escrever um post separado sobre isso). Para implementar o mecanismo interno do sistema de mensagens, o sistema utiliza filas (a saber, Redis é necessário para isso). Assim, a parte do servidor pode ser facilmente dimensionada horizontalmente simplesmente adicionando novas máquinas.

Com a abordagem usual de desenvolvimento, como regra, o back-end fornece um determinado conjunto de APIs às quais você pode se conectar com diversos aplicativos clientes (site, aplicativo móvel). No seu caso, verifica-se que apenas um cliente escrito em Ext JS pode trabalhar com um back-end?

No servidor, em particular nos modelos de módulo, uma certa lógica de negócios é implementada. Para fornecer acesso a ele por meio da API REST, basta um pequeno "wrapper". Um exemplo correspondente é apresentado na primeira parte deste artigo.

Conclusões


Como você pode ver, para uma codificação confortável de aplicativos bastante complexos, é bem possível fazer isso com uma biblioteca no front-end e no back-end. Isso tem benefícios significativos.

Acelerando o processo de desenvolvimento. Cada um dos membros da equipe pode trabalhar no backend e no frontend. O tempo de inatividade pelo motivo "Estou aguardando a exibição desta API no servidor" não se torna relevante.

Menos código. As mesmas seções de código podem ser usadas no cliente e no servidor (verificações, verificações etc.).

Manter esse sistema é muito mais simples e mais barato. Em vez de dois programadores diversos, o sistema poderá suportar um (ou os mesmos dois, mas intercambiáveis). Pelo mesmo motivo, os riscos associados à rotatividade de equipes também são mais baixos.

A capacidade de criar sistemas em tempo real prontos para uso.

Usando um único sistema de teste para back-end e frontent.

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


All Articles