服务器上的Ext JS

照片从这里https://github.com/tj/palette 当涉及到Ext JS库时,专家们听到了很多消极的东西:笨重,昂贵,越野车。 通常,大多数问题与无法烹饪有关。 使用Sencha Cmd和所有css正确组装的项目,图像在1Mb区域中的生产权重,与同一个Angular相当。 是的,而且故障不多...

Sencha的想法很不同,但是即使是有原则的反对者也承认,很难找到构建严肃的Intranet项目的最佳解决方案。

我认为,Ext JS中最有价值的东西不是UI组件的集合,而是一个好的OOP体系结构。 即使考虑到近年来JS的快速发展,本机类中仍缺少7年前在Ext JS中实现的许多必需的东西(命名空间,mixins,静态属性,方便的父方法调用)。 这就是几年前促使我尝试在后端启动Ext JS类的原因。 关于最初的类似实验,我已经在Habré上发表过文章。 本文介绍了旧思想的新实现和许多新思想。

在开始之前,请注意以下问题:您认为如何,它在哪里执行,下面的代码段是做什么的?

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

此代码在服务器上执行,并在连接到服务器的所有客户端计算机上的“ Module.message.model.Message”类的所有实例中导致“ newmessage”事件。

为了说明使用服务器端Ext JS的可能性,我们将分析一个简单的聊天项目。 仅当您输入用户输入昵称时,我们才会进行任何登录。 您可以发布常规或私人消息。 聊天应该实时进行。 那些希望的人可以立即尝试所有这种经济活动。

安装方式


首先,我们需要nodejs 9+和redis-server(假定它们已经安装)。

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

我们启动服务器:

 node server 

在浏览器中,打开localhost页面:3000 / www / auth /
输入一些昵称,然后按Enter。

该项目是一个演示,因此不支持旧的浏览器(有ES8设计),请使用新的Chrome或FF。

伺服器


让我们去吧。

服务器代码(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); }); 

如您所见,这里的所有内容或多或少都是服务器上的标准。 有趣的是包含Ext JS类以服务相应的路由:

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

REST API实施


Api.auth.Main类将请求提供给REST API(受保护的/ 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() { .... } }) 

使用服务器上的XTemplate生成HTML页面


第二类是Www.login.controller.Login,它使用登录表单(受保护的/www/login/controller/Login.js)构建一个常规的html页面。

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

模板使用标准的XTemplate(受保护的/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> 

一丝不苟的读者会说,上述所有内容都是一个完全标准的集合,因此,无需通过将Ext JS传输到服务器来围堵这个花园。 因此,我们继续进行本文的第二部分,它将显示其全部意图。

顾客


让我们在静态目录中创建普通的客户端Ext JS应用程序。 在此示例中,我故意不考虑使用cmd,而是采用了已经构建的ext-all和standard主题。 大会问题是一个单独的主题,也许会另辟一个职位。

一切都始于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' }) 

Web套接字的存在是至关重要的一点,正是它可以让您实现下面描述的所有魔术。

页面上元素的布局包含在Admin.view.Viewport类中(静态/app/view/Viewport.js)。 那里没什么有趣的。

主要功能元素(用户列表,消息栏和发送表单)被实现为单独的模块。

用户清单


该列表的简单算法如下:打开页面时,当前用户已从服务器加载。 当新用户连接时,服务器在“ Module.users.model.UserModel”类中生成一个“ add”事件,当断开连接时,在同一类中,将引发“ remove”事件。 事实是该事件是在服务器端触发的,您可以在客户端上对其进行跟踪。

现在,首先是第一件事。 在客户端,存储杂项数据(静态/应用程序/模块/用户/商店/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)) } }); 

有两个有趣的观点。 首先,在“ const data = await this.dataModel。$ Read();”行中 该模型的服务器方法被调用。 现在,您无需使用Ajax,支持协议等,只需将服务器方法称为本地方法即可。 同时,安全性也没有牺牲(更多内容见下文)。

其次,this.dataModel.on(...)的标准构造使您可以跟踪服务器将生成的事件。

该模型是应用程序的客户端和服务器部分之间的桥梁。 就像光的二元论一样-它实现了前端和后端的属性。 让我们仔细看一下模型。

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

请注意以下注释:/ *作用域:服务器* /和/ *作用域:客户端* /-这些构造是服务器的标签,通过它确定方法的类型。

testClientMethod-此方法专门在客户端上运行,并且仅在客户端可用。
testGlobalMethod-此方法在客户端和服务器上运行,并且可以在客户端和服务器端使用。
privateServerMethod-该方法在服务器上执行,仅可用于在服务器上调用。
$ read是仅在服务器端运行的最有趣的方法类型,但是您可以在客户端和服务器上调用它。 $前缀使客户端可以使用任何服务器端方法。

您可以使用Web套接字跟踪客户端连接和断开连接。 为每个用户连接创建一个Base.wsClient类的实例(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); } }) 

与标准方法不同,fireEvent方法具有一个附加参数,在该参数上传递事件应在哪个客户端上触发。 传递单个客户端标识符,标识符数组或字符串“ all”是可以接受的。 在后一种情况下,事件将在所有连接的客户端上触发。 否则,这是标准的fireEvent。

发送和接收消息


表单控制器(静态/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(''); } }); 

该消息未保存在服务器上的任何位置,只需触发“ newmessage”事件。 有趣的是调用“ this.fireEvent('newmessage',data.to,msg);”,其中客户端标识符作为消息接收者传递。 因此,实现了私人消息的分发(静态/ app / admin /模块/消息/模型/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; } }) 

与用户一样,消息列表的数据由商店驱动(静态/ app / admin /模块/消息/商店/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); } }); 

通常,在此示例中,这就是所有有趣的事情。

可能的问题


客户端上服务器方法的可用性当然很好,但是安全性又如何呢? 事实证明,邪恶的黑客可以看到服务器代码并尝试破解后端?

不,他不会成功。 首先,所有服务器方法在发送到客户端浏览器时都将从类代码中删除。 为此,打算使用注释/指令/ *作用域:... * /。 其次,最公共的服务器端方法的代码被中间结构替代,该中间结构在客户端实现了远程调用机制。

再次谈到安全性。 事实证明,如果可以在客户端上调用服务器方法,我可以调用任何此类方法吗? 如果这是数据库清理方法?

从客户端,您只能调用名称中带有$前缀的方法。 对于这种方法,您自己确定检查和访问的逻辑。 如果没有$,外部用户将无法访问服务器方法,他甚至都不会看到它们(请参阅前面的答案)

看起来您拥有一个完整的系统,其中客户端和服务器之间有着千丝万缕的联系。 水平缩放是否可能?

实际上,该系统看起来是整体的,但事实并非如此。 客户端和服务器可以“驻留”在不同的计算机上。 客户端可以在任何第三方Web服务器(Nginx,Apache等)上运行。 自动项目构建器很容易解决客户端与服务器分离的问题(我可以为此撰写单独的文章)。 为了实现内部服务消息传递机制,系统使用队列(即,为此需要Redis)。 因此,只需添加新机器即可轻松地水平扩展服务器部分。

通常,使用通常的开发方法,后端提供了一组API,您可以将它们与各种客户端应用程序(网站,移动应用程序)连接。 在您的情况下,事实证明只有用Ext JS编写的客户端可以与后端一起使用吗?

在服务器上,尤其是在模块模型中,实现了某种业务逻辑。 为了通过REST API提供对它的访问,一个小的“包装器”就足够了。 本文的第一部分提供了一个相应的示例。

结论


如您所见,为了对相当复杂的应用程序进行舒适的编码,很可能在前端和后端使用一个库。 这具有明显的好处。

加快开发过程。 每个团队成员都可以在后端和前端工作。 由于“我正在等待此API出现在服务器上”原因而导致的停机时间不再相关。

更少的代码。 可以在客户端和服务器上使用相同的代码部分(检查,验证等)。

维护这样的系统更加简单和便宜。 该系统将能够支持一个(或两个相同但可互换),而不是两个不同的程序员。 出于同样的原因,与团队更替相关的风险也较低。

从包装盒中创建实时系统的能力。

对后端和前端使用单个测试系统。

Source: https://habr.com/ru/post/zh-CN431180/


All Articles