фото от сюда https://github.com/tj/palette Когда речь заходит о библиотеке Ext JS, от знатоков приходится слышать довольно много негатива: тяжелая, дорогая, глючная. Как правило, большинство проблем связано с неумением ее готовить. Правильно собранный с использованием Sencha Cmd проект со всеми css, картинками весит в продакшне в районе 1Мб, что сопоставимо с тем же Angular. Да и глюков не сильно больше…

Можно по-разному относится к этому детищу компании Sencha, но даже принципиальные ее противники признают — лучшего решения для построения серьезных интранет проектов найти сложно.

На мой взгляд, самое ценное в Ext JS не коллекция UI компонент, а довольно удачная архитектура ООП. Даже с учетом бурного развития JS в последние годы, многие нужные вещи которые были реализованы в Ext JS еще 7 лет назад, отсутствуют в нативных классах до сих пор (нэймспэйсы, mixins, статические свойства, удобный вызов родительских методов). Именно это побудило меня несколько лет назад поэкспериментировать с запуском Ext JS классов в бакэнде. Про первые подобные опыты я уже делал посты на Хабре. В этой статье описана новая реализация старых идей и ряд свежих.

Перед тем как начнем, внимание вопрос: как вы думаете, где выполняется и что делает приведенный ниже фрагмент кода?

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

Этот код выполняется на сервере и вызывает событие «newmessage» во всех инстансах класса «Module.message.model.Message» на всех подключенных к серверу клиентских машинах.

Для иллюстрации возможностей использования серверного 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), пользуйтесь новым Хромом или ФФ.

Сервер


Пойдем по порядку.

Код сервера (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);
});

Как видим, тут все более-менее стандартно для сервера на express. Интерес представляет подключение классов 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 (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() {
       ....
    }
})

Генерация HTML-страниц, использование XTemplate на сервере


Второй класс Www.login.controller.Login строит обычную html-страницу с формой логина (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()
        });
    }
})

В шаблонах используется стандартный XTemplate (protected/www/login/view/login.tpl)

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

Все описанное выше является вполне стандартным набором, скажет дотошный читатель, и для этого незачем было городить этот огород с переносом Ext JS на сервер. Поэтому, переходим ко второй части статьи, где будет показано то, ради чего все это задумывалось.

Клиент


Создадим обычное клиентское Ext JS приложение в каталоге static. В этом примере я умышлено не рассматриваю использование cmd, взял уже собранный ext-all и стандартную тему оформления. Вопросы сборки это отдельная тема, которой, возможно, посвящу отдельный пост.

Все начинается с 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'    
})

Наличие веб-сокета принципиально важный момент, именно он позволяет реализовать всю магию описанную ниже.

Компоновка элементов на странице содержится в классе Admin.view.Viewport (static/app/view/Viewport.js). Там ничего интересного.

Основные функциональные элементы (список пользователей, панель сообщений и форма отправки) реализованы в виде отдельных модулей.

Список пользователей


Незамысловатый алгоритм работы этого списка такой: в момент открытия страницы с сервера загружаются текущие пользователи. При подключении новых пользователей сервер генерирует событие «add» в классе «Module.users.model.UserModel», при отключении, в том же классе, вызывается событие «remove». Вся штука в том, что событие инициируется на стороне сервера, а отследить его можно на клиенте.

Теперь, обо всем по порядку. С клиентской стороны данными жонглирует 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))
    }

});

Тут 2 интересных момента. Во-первых, в строке «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
        }
    }  

    
})

Обратите внимание на комментарии /* scope:server */ и /* scope:client */ — эти конструкции являются метками для сервера, по которым он определяет тип метода.

testClientMethod — этот метод выполняется исключительно на клиенте и доступен только на клиентской стороне.
testGlobalMethod — этот метод выполняется на клиенте и на сервере и доступен для использования для клиентской и серверной части.
privateServerMethod — метод выполняется на сервере и доступен для вызова только на сервере.
$read — самый интересный тип метода, который выполняется только на серверной стороне, но вызвать его можно как на клиенте, так и на сервере. Префикс "$" превращает любой серверный метод в доступный на клиентской стороне.

Отследить подключение и отключение клиента можно по веб-сокету. Для каждого пользовательского подключения создается экземпляр класса «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.

Отправка и прием сообщений


За отправку сообщений отвечает контроллер формы (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('');        
    }    
});

На сервере сообщение нигде не сохраняется, просто вызывается событие «newmessage». Интерес представляет вызов «this.fireEvent('newmessage', data.to, msg);», где в качестве адресатов сообщений передаются идентификаторы клиентов. Таким образом, реализуется рассылка приватных сообщений (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;        
    }
})

Как и в случае с пользователями данными для списка сообщений рулит 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);
    }    
});

В целом, это все что есть интересного в этом примере.

Возможные вопросы


Доступность серверных методов на клиенте это, конечно, хорошо, но как быть с безопасностью? Получается, что злобный хакер может увидеть серверный код и попытаться взломать бакенд?

Нет, это у него не получится. Во-первых, все серверные методы удаляются из кода класса при отправке в клиентский броузер. Именно для этого, предназначены комментарии-директивы /* scope:… */. Во-вторых, код самого публичного серверного метода подменяется на промежуточную конструкцию, реализующую механизм удаленного вызова на клиентской стороне.

Опять про безопасность. Если серверные методы можно вызывать на клиенте, получается, я могу вызвать любой такой метод? А если это метод очистки базы данных?

Из клиента вы можете вызвать только методы, имеющие в своем названии префикс $. Для таких методов вы сами определяете логику проверок и доступов. К серверным методам без $ у внешнего пользователя нет никакого доступа, он их даже не увидит (смотри предыдущий ответ)

С виду у вас получилась монолитная система в которой клиент и сервер неразрывно связаны. Возможно ли горизонтальное масштабирование?

Система, действительно, выглядит монолитно, но это не так. Клиент и сервер могут «жить» на разных машинах. Клиент может быть запущен на любом стороннем веб-сервере (Nginx, Apache и т.п.). Вопрос разделения клиента и сервера очень просто решается автоматическим сборщиком проекта (об этом могу написать отдельный пост). Для реализации механизма внутреннего обмена служебными сообщениями система использует очереди (именно, для этого требуется Redis). Таким образом серверную часть можно легко горизонтально масштабировать простым добавлением новых машин.

При обычном подходе в разработке, как правило, бакэнд предоставляет некий набор API, к которым можно подключиться разноплановыми клиентскими приложениями (сайт, мобильное приложение). В вашем случае получается, что с бакендом может работать только клиент написанный на Ext JS?

На сервере, в частности в моделях модулей, реализуется некая бизнес-логика. Для того, что бы предоставить доступ к ней через REST API достаточно небольшой «обертки». Соответствующий пример представлен в первой части этой статьи.

Выводы


Как видим, для комфортного кодирования достаточно сложных приложений вполне можно обойтись одной библиотекой на фронтенде и бакенде. Это дает серьезные преимущества.

Ускорение процесса разработки. Каждый из участников команды может работать над бакэндом и фронтендом. Простои по причине «я жду когда этот АПИ появится на сервере» становятся не актуальными.

Меньше кода. Одни и те-же участки кода могут использоваться на клиенте и на сервере (проверки, верификации и т.п.).

Поддерживать такую систему гораздо проще и дешевле. Вместо двух разноплановых программистов, систему сможет поддерживать один (или те же двое но взаимозаменяемые). По той же причине, риски связанные с текучкой в команде тоже ниже.

Возможность «из коробки» создавать системы реального времени.

Использование единой системы тестирования для бакенда и фронтента.

Комментарии (11)


  1. Aries_ua
    28.11.2018 11:24

    В чем преимущества перед Express/Koa/Restify/Fastify + handlebars/mustache/nunjucks?

    PS Несколько лет использовал ExtJS на проекте. Пока не отходишь от дефолтного пути, все более менее нормально. Тормознуто, но работает. Но если надо что-то, что выходит за рамки, начинается боль и страдания. Потому у меня немного предвзятое отношение к ExtJS. Поэтому хотелось бы услышать ответы. Спасибо.


    1. kolbaskinmax Автор
      28.11.2018 11:55

      Не хотелось бы заводить здесь холивар. Тем не менее, любая технология имеет свои плюсы и минусы. Насчет тормознутости я бы не был так категоричен. Работаю с 6й версией довольно давно. При сборке в cmd, не самый простой проект в продакшне со всеми стилями, картинками весит 968Кб (из них js ~600кб) и работает никак не медленнее аналога на Vue. Для меня основной плюс Ext JS как раз то, за что ее многие ругают — тут не предполагается работа непосредственно с html (хотя, такая возможность существует). При этом, для всякого рода интранет-лайк систем (для чего extjs и предназначен) возможностей кастомизации хватает с головой.


    1. kosmonaFFFt
      28.11.2018 15:12

      Зато таких гридов, как в ExtJS, я еще нигде не видел.
      Ну и дефолтная стилизация довольно симпатичная — можно брать и пилить приложение не написав ни строчки CSS.


      1. dmitriym09
        28.11.2018 17:48

        В webix очень неплохие на мой вкус.


      1. berez
        28.11.2018 20:38

        О-о-о да… Помню, приходилось под эти гриды рожать скрипты, дабы можно было полноценно редактировать таблицы на сервере. И вроде бы ничего сложного, но даже для простых таблиц это выливалось в несколько килобайт джаваскриптового кода. А таблиц было много, а еще они были взаимосвязанные…
        В общем, в итоге обошлись вообще без вебморды. :)


    1. kolbaskinmax Автор
      28.11.2018 18:05

      В чем преимущества перед Express/Koa/Restify/Fastify + handlebars/mustache/nunjucks?

      Утром было маловато времени для развернутого ответа. Думаю, вы не совсем поняли о чем пост. Возможно, я сумбурно написал. Там не идет речь о генерации html-страниц с использованием какого-то шаблонизатора. Просто отмечается, что такая возможность есть. Основная затея была сделать нечто, что позволяет упростить создание и поддержку RIA приложений. Проще показать на примере.
      Предположим, по нажатию на красной кнопке вам нужно у всех он-лайн пользователей вашего приложения показать сообщение «Пользователь Вася нажал красную кнопку». В этом случае при стандартном подходе по событию нажатия на кнопку вы отправите сообщение на сервер, на сервере нужно добавить роут с обработчиком, далее, необходимо остальных пользователей известить что кнопка нажата. Все это куча кода в клиентской и серверной частях. Я предлагаю более простой способ (код рабочий если его подставить в пример из статьи):
      Контроллер на клиенте:
      Ext.define('Module.messages.view.FormController', {
         .....
          ,init(view) {
           ....
               this.model = Ext.create('Module.messages.model.Model',{
                   listeners: {
                       onredbutton: (user) => {
                             // событие redbutton будет вызвано сервером у всех клиентов
                             alert(`Пользователь ${user} нажал кнопку`)
                       }
                   }  
               });
              this.control({
                  ...
                  '[action=redbutton]'    : {click: () => {
                        // вызовем метод на сервере, передадим ему имя юзера
                        this.model.$onredbutton(Ext.WS.user) 
                  }}            
              })         
      

      Модель:
      Ext.define('Module.messages.model.Model', {
          extend: 'Core.data.DataModel'
          /* scope:server */
          ,async $onredbutton(user) {
              this.fireEvent('onredbutton', 'all', user);              
          }
      })
      


  1. nohuhu
    30.11.2018 04:15

    Интересный эксперимент, но я бы вежливо посомневался на предмет практической пользы от Ext JS на сервере как вообще, так и в данной конкретной реализации:


    • Классовая система имеет смысл в браузерах, которые ничего кроме ванильного ES5 не знают; Node умеет в ES8, которое хоть и не так наворочено, но вполне работоспособно.
    • Загрузчик и отслеживание зависимостей заточено на Cmd и вне своих рамок выпьет крови по полной программе. То, что вы грузите ext-all-debug.js уже как бы намекает; при этом загружается весь фреймворк, а используется может быть 1% от его возможностей. Понятно, что 30-40 мб памяти для сервера ни о чём, но сам факт.
    • Система конфигов гибка и приятна, но платить за неё придётся производительностью, и платить дорого. А отказываться от неё больно.
    • Система событий абсолютно полностью заточена под браузеры. Я не до конца понимаю, как вам вообще удалось завести всю эту дребедень под Нодой, но есть все шансы, что выстрел в ногу из ядерной пушки на каком-то этапе случится. А если нет, то и толку от неё мало: событийная система в Ext синхронна по определению, что для браузера ещё как-то в общем ничего, но для Ноды смерти подобно.
    • Абсолютно не очевидный при разработке нюанс, который впоследствии практически гарантированно поставит колом систему в боевой среде: Ext JS полностью завязан на глобальные объекты. Ваша память ещё не течёт, как дырявое решето? Будет. Опять же для браузеров терпимо, а вот для сервера каюк.

    И т.д., и т.п. Реализация серверной логики на Ext.app.Controller улыбнула, сделали приятно старому шарлатану. :) А что касается приёма и отправки сообщений, то всё уже украдено до нас. Ext Direct вам в помощь.


    В общем, не нужно оно на сервере, совсем. Это я вам как доктор говорю.


    1. kolbaskinmax Автор
      30.11.2018 09:24

      Все написанное Вами правильно, но не совсем. Если понимать, что ExtJS все-таки больше приедназначен для интранета и всяких там админок, то:

      Классовая система имеет смысл в браузерах, которые ничего кроме ванильного ES5 не знают; Node умеет в ES8, которое хоть и не так наворочено, но вполне работоспособно.

      Современные версии броузеров прекрасно умеют ES8 (всякое старье поддерживать в подобных системах нет нужды)

      Загрузчик и отслеживание зависимостей заточено на Cmd и вне своих рамок выпьет крови по полной программе. То, что вы грузите ext-all-debug.js уже как бы намекает; при этом загружается весь фреймворк, а используется может быть 1% от его возможностей. Понятно, что 30-40 мб памяти для сервера ни о чём, но сам факт.

      Сборка сервера через Cmd занятие бессмысленное, классы подгружаются и остаются в памяти по мере необходимости стандартным нодовским require (там разный набор файлов библиотеки для клиента и сервера). А клиент, да, собирается через cmd и превращается в 1мб. То, что в примере ext-all-debug.js для упрощения этого самого примера. Вообще, вопрос сборки такого проекта интересен сам по себе, рамок одной статьи маловато для этого. Могу описать, если хотите)

      Система конфигов гибка и приятна, но платить за неё придётся производительностью, и платить дорого. А отказываться от неё больно.

      Не совсем понятно как это может повлиять на производительность. Максимум, вы теряете немного в момент инициализации объекта, что для сервера не сильно критично (объекты долгоживущие), а для клиента не сильно заметно.

      Система событий абсолютно полностью заточена под браузеры. Я не до конца понимаю, как вам вообще удалось завести всю эту дребедень под Нодой, но есть все шансы, что выстрел в ногу из ядерной пушки на каком-то этапе случится. А если нет, то и толку от неё мало: событийная система в Ext синхронна по определению, что для браузера ещё как-то в общем ничего, но для Ноды смерти подобно.

      Хм, никакой синхронности от сервера не требуется: сервер просто кидает в сокет нужного клиента сообщение «парень, тебе тут событие» и забывает про него. Остальное дело клиента… Механизм совершенно другой на серверной стороне, синтаксис только похожий, что бы было единообразие в коде.

      Абсолютно не очевидный при разработке нюанс, который впоследствии практически гарантированно поставит колом систему в боевой среде: Ext JS полностью завязан на глобальные объекты. Ваша память ещё не течёт, как дырявое решето? Будет. Опять же для браузеров терпимо, а вот для сервера каюк.

      Глобальные объекты вы имеете ввиду DOM? На сервере их нет в принципе, соответственно, они не используются. Вы видите ExtJS как сборник UI-компонент, но по факту там 2 части — Core (движок классов) и UI (компоненты и все с ними связанное). Мы делали на этой штуке систему лояльности, при нагрузке в районе 150 запросов/сек в течении суток я не заметил каких-то утечек на сервере…

      А что касается приёма и отправки сообщений, то всё уже украдено до нас. Ext Direct вам в помощь.

      Вопрос не стоит в отправке сообщений. Очень хочется иметь под рукой всю картину движения данных от формы в UI до базы данных на сервере, а не прыгать между бакэндом и фронтом, которые, к тому же, при одинаковом языке программирования (в случае с нодой на сервере) построены на совершенно разных паттернах. Целью этого эксперимента было привести все к одному знаменателю. Что бы у меня в каталоге users в папке view лежали формы и таблицы для списка пользователей, а в model все что касается данных…

      Вот как-то так)


      1. nohuhu
        01.12.2018 06:53

        Если понимать, что ExtJS все-таки больше приедназначен для интранета и всяких там админок

        Ну вот если вы это понимаете, то также должны понимать, что для интранета и всяких там админок очень часто используется всякое старьё (конкретно, IE11). В котором не то, что ES8, а даже и ES6 едва присутствует.


        Сборка сервера через Cmd занятие бессмысленное, классы подгружаются и остаются в памяти по мере необходимости стандартным нодовским require

        Допускаю, хотя в исходниках ваших модулей я этого не увидел. Но в общем смотрел по диагонали, мог и пропустить.


        А клиент, да, собирается через cmd и превращается в 1мб.

        Вот тут будут возникать отдельные вопросы, и даже целая пачка. Емнип, Cmd совершенно не в курсе ваших тегов scope:server и если исходники используются там и сям, то перед сборкой клиентской части нужно выкусывать серверную. А это дополнительный инструментарий, шаг сборки, да и просто геморрой.


        Вообще, вопрос сборки такого проекта интересен сам по себе, рамок одной статьи маловато для этого. Могу описать, если хотите)

        Спасибо, с процессом сборки Cmd я, можно сказать, интимно знаком. :) Не настолько этот процесс интересен, как вам кажется, а вот сложен намного более, чем вам представляется.


        Не совсем понятно как это может повлиять на производительность. Максимум, вы теряете немного в момент инициализации объекта, что для сервера не сильно критично (объекты долгоживущие), а для клиента не сильно заметно.

        Я в своё время потратил много, много дней на профилирование Ext JS и выводы были очень интересные. В большей части приложений порядка 20-30% времени до первой отрисовки съедало именно создание объектов и в частности, создание конфигов для них. Вы в курсе, как эта штука внутри устроена? Посмотрите, механизм весьма интересный и гибкий, но и тормозной при этом — очень много накладных расходов на создание объекта с заданным прототипом.


        Хм, никакой синхронности от сервера не требуется: сервер просто кидает в сокет нужного клиента сообщение «парень, тебе тут событие» и забывает про него.

        От сервера требуется асинхронность, иначе масштабирование системы будет под очень большим вопросом. Поскольку всё происходит в одном event loop, каждый кусок кода должен быть очень быстрым и возвращать управление планировщику как можно быстрее. А в Ext на каждое событие можно повесить обработчик, который может тоже стрелять события, на которые могут быть повешены обработчики, и так далее ad nauseam. Всё это происходит синхронно, и пока весь код не отработает, управление к планировщику не вернётся и никакие другие куски кода работать не будут.


        В браузере это не так критично, браузер по определению работает для одного пользователя. Пользователь подождёт и секунду, и две, а какие-нибудь 200 мс и не заметит. А вот на сервере такой подход недопустим, т.к. очень легко попасть на случай, когда залётный таймаут из базы данных или ещё откуда просто положит систему.


        В вашем случае всё это не очевидно, потому что серверная сторона маленькая и простенькая, и на грабли вы ещё не наступали. Может и не наступите. Но подход не масштабируется в принципе.


        Глобальные объекты вы имеете ввиду DOM?

        Нет, при чём тут DOM. В браузере верхний объект это window, в Node это global, но суть не меняется: Ext JS создаёт иерархию классов в виде вложенных объектов со ссылкой на верхний объект. Когда вы пишете что-нибудь в стиле:


        var foo = new Ext.bar.Foo();

        То вот этот Ext, технически, это переменная в глобальном контексте, а по факту свойство window.Ext (или global.Ext). Всё, что находится в самом объекте Ext, живёт в памяти всегда.


        Большая часть этих вложенных объектов — это классы Ext. Некоторые объекты это singletons, а ещё некоторые просто объекты JavaScript, безо всякой специальной магии. Проблема в том, что и singletons, и некоторые классы, и даже просто объекты, делают очень много всяких разных штук, очень не полезных для расхода памяти. Держат ссылки на экземпляры своего типа (StoreManager держит ссылки на все Store, ComponentManager на все компоненты, etc), размещают всякие кеши на прототипе, и протчая, и протчая. И всё это растёт и пенится, от чего Garbage Collector рано или поздно становится плохо. Для браузера это опять же не очень критично — стал тормозить, F5 и понеслось. А вот на сервере будет больно.


        Вы видите ExtJS как сборник UI-компонент, но по факту там 2 части — Core (движок классов) и UI (компоненты и все с ними связанное).

        Я вижу Ext JS как кусок говна софта, на который я потратил 6 лет жизни. ;) Частей там существенно больше двух.


        Мы делали на этой штуке систему лояльности, при нагрузке в районе 150 запросов/сек в течении суток я не заметил каких-то утечек на сервере…

        6.2 у вас? Это приятно слышать, потому что я потратил несколько месяцев на отлов всех утечек, до которых мог дотянуться. Похоже, что выпилил все крупные, раз вы проблем до сих пор не заметили. :)


        Но вот до конца проблему решить не удалось и не удастся. Я могу много рассказать о том, как оно течёт, куда, и почему, но если вкратце: текло, течёт, и течь будет. Основная больная проблема это глобальные ссылки, и от них никуда не денешься. Архитектура такая.


        Что бы у меня в каталоге users в папке view лежали формы и таблицы для списка пользователей, а в model все что касается данных…

        Если вы немного копнёте в Direct, то увидите: это как раз средство свести серверную сторону к абсолютному минимуму. По факту к надстройке над базой данных, которая делает проверку прав доступа и может быть чуть-чуть ещё.


        А всё, что вам нужно, можно сделать на клиентской стороне. Благо, Ext JS как раз для таких вещей и создан.


        1. kolbaskinmax Автор
          01.12.2018 11:36

          Ух, мы с вами, чувствую, погрузимся в такие дебри, от куда будет сложно выбраться. Я с Ext'ом со 2й версии, т.е. года, этак с 2008, если мой склероз мне не изменяет:) Но многие вещи о которых вы пишете довольно спорные, на мой взгляд.

          Ну вот если вы это понимаете, то также должны понимать, что для интранета и всяких там админок очень часто используется всякое старьё

          Эта практика, когда СБ запрещали все кроме ИЕ6 прошла много лет назад. Я еще не разу за последние лет 5 не встречал клиента (а их, поверьте, было довольно много), который сказал бы, что его црмка должна работать на ИЕ8. Все прекрасно понимают, что стоимость поддержки легаси-броузеров в проекте стоит очень дорого.

          Вот тут будут возникать отдельные вопросы, и даже целая пачка. Емнип, Cmd совершенно не в курсе

          Про сборку. Не cmd единым жив интернет) cmd такой же инструмент как оные другие. Никто не мешает включить его в цепочку других инструментов. У меня для сборки простенький баш-скрипт, можно пользоваться гулпом или кучей других сборщиков. Порядок, примерно такой: разделяет общие файлы, очищает от «чужих методов» -> запускает cmd для клиента -> пакует в контейнер. В зависимости от проекта, можно собрать отдельно сервер, отдельно клиент или 2-в-одном.

          В большей части приложений порядка 20-30% времени до первой отрисовки съедало именно создание объектов и в частности, создание конфигов для них.

          Да объясните, наконец, зачем на сервере что-то отрисовывать? На сервере это работает так: Ext.create -> <Трансляция имени класса в путь к файлу> -> requere(<файл>). Еще немного времени возьмет всякого рода фокусы с наследованием, но это копейки. После этого класс у вас в памяти и все последующие Ext.create этого класса мгновенные. Кроме того, большинство классов на сервере живут пока жив текущий процесс.

          От сервера требуется асинхронность, иначе масштабирование системы будет под очень большим вопросом. Поскольку всё происходит в одном event loop, каждый кусок кода должен быть очень быстрым и возвращать управление планировщику как можно быстрее. А в Ext на каждое событие можно повесить обработчик, который может тоже стрелять события, на которые могут быть повешены обработчики, и так далее ad nauseam.

          Спасибо, что просветили насчет event loop, я то, грешным делом, думал как же эта хренотень работает) Если серьезно, то, внимание, вопрос что такое this.fireEvent и чем оно отличается от this.myCoolMethod? Ответ: ровным счетом ничем, это самый обычный метод класса на сервере, который по названию совпадает с методом fireEvent на клиенте, который делает все то, что вы там понаписали. Серверный this.fireEvent сделает асинхронно ровно то, что написал я, а именно, отправит в клиентские сокеты сообщения «чувак, тебе событие». Клиент получит это сообщение, прочитает, что там написано и вызовет уже клиентский fireEvent для нужного объекта. Но кодируя проект если мне нужно в реальном времени о чем-то сообщить клиенту я просто напишу fireEvent на сервере вместо тонны кода, который остается под капотом.

          Я вижу Ext JS как кусок говна софта, на который я потратил 6 лет жизни. ;) Частей там существенно больше двух.

          Сочувствую) Я после 6лет с ПХП долго восстанавливал нервную систему. С Ext JS у меня более счастливый брак))

          6.2 у вас? Это приятно слышать, потому что я потратил несколько месяцев на отлов всех утечек, до которых мог дотянуться. Похоже, что выпилил все крупные, раз вы проблем до сих пор не заметили. :)

          В броузере утечки неизбежны, слишком много всего там живет разношерстого. Сервер в этом плане на порядок проще, и отловить утечки там реально.

          Если вы немного копнёте в Direct, то увидите: это как раз средство свести серверную сторону к абсолютному минимуму. По факту к надстройке над базой данных, которая делает проверку прав доступа и может быть чуть-чуть ещё.

          Да, я директором пользуюсь если сервер не нодовский. Но нафига мне лишняя прослойка, когда у меня один язык на клиенте и сервере? Еще раз: основная задача в этом проекте максимально стереть границу между клиентом и сервером при сохранении возможностей масштабирования.

          В начале времен, когда трава была зеленее и столы выше, программы на клиенте и сервере были написаны одинаково, и могли поддерживаться программистами с одинаковыми компетенциями. Потом настали трудные времена и клиент ушел от сервера. И превратились программисты во фронтендеров и бакэндеров. Я просто пытаюсь снова воссоединить клиент и сервер:) (это юмор, в субботу можно. Только, ради Бога, не пишите про всякие там соапы, терминалы и прочее, что было в те далекие времена)


  1. kolbaskinmax Автор
    01.12.2018 11:28

    Ошибся веткой(