Janusjs: концепт системы, где клиент и сервер — сиамские близнецы

    image По роду своей деятельности мне часто приходится заниматься разработкой разнообразных crm-систем. Клиентскую часть уже очень давно собираю на Extjs (начинал еще со 2-й версии). На сервере пару лет назад прочно обосновался Nodejs, заменив привычный PHP.

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

    Несколько причин для выбора библиотеки от Sencha в качестве базы:

    1. Продукты компании Sencha (Extjs в частности) известны в среде разработчиков, соответственно, есть достаточное количество специалистов в этой теме. По той же причине, нет проблем с документацией, примерами и сообществом.
    2. Extjs — один из немногих js-фреймворков с логичной продуманной архитектурой, которая одинаково хорошо применима как на клиентской так и на серверной стороне приложения.
    3. Единая кодовая база позволяет в одном файле описывать и клиентскую и серверную логику. Это сокращает количество строк кода и позволяет избежать дублирования.


    Установка


    Нам понадобится Nodejs, Mongodb, Memcached. Nodejs и Mongodb, желательно, свежих версий (особенно, Nodejs). Не вижу смысла описывать в статье процесс установки этих программ, в сети достаточно инструкций на любой вкус и ОС.

    Перед началом установки обязательно проверьте, запущены ли процессы mongodb и memcached. Установочный скрипт проверяет возможность коннекта. Если соединения нет, установка завершится с ошибкой.

    Устанавливаем фреймворк:
    npm i janusjs
    


    В конце инсталляции установщик задаст несколько наводящих вопросов для генерации проекта-примера (параметры подключения к БД, пользователь по-умолчанию и т.п.).

    После всех манипуляций, в текущем каталоге у вас будет такое содержимое:
    node_modules
    projects
    cluster.config.js
    cluster.js
    daemon.js
    server.js
    

    projects — каталог с проектами
    из остальных файлов, нас, пока что, интересует server.js, его и запустим:
    node server
    

    Если все в порядке, в консоли будет написано: “Server localhost is listening on port 8008”
    В случае ошибки проверьте, не занят ли порт и запущены ли Mongodb и Memcached.
    Веб-интерфейс системы доступен по адресу localhost:8008/admin/ (если при установке вы не меняли пользователя, то admin:admin). Параметры доступа можно проверить в настоечном файле проекта
    projects/crm/config.json
    

    Пользовательский интерфейс вполне стандартный. Система позволяет для разных групп пользователей создать разнотипные интерфейсы.

    На видео можно посмотреть пример работы crm агентства недвижимости:



    Создание модулей


    При установке Януса должен был создаться пустой проект в каталоге projects/crm. Структура каталогов проекта:

    protected
    static 
        admin
    		css
    		extended
    		locale
    		modules
    			news
    				controller
    				model
    				view
    		config.json
    config.json
    


    static/admin/modules/news — это пример модуля CRM. Там простой список новостей. Посмотрим как это сделано.

    Модули Janusjs строятся на базе шаблона MVP. Подробнее рассмотрим каждую часть модуля:

    controller/News.js — контроллер (Presenter). Модуль реализует стандартное поведение (список записей — карточка записи), поэтому вся функциональность «живет» в родительском классе. Код контроллера:

    Ext.define('Crm.modules.news.controller.News', {
        extend: 'Core.controller.Controller', 
        launcher: {
            text: 'News',  // название модуля
            iconCls:'fa fa-newspaper-o'  //  иконка  модуля, поддержка  Font Awesome  
        }    
    });
    

    У модуля новостей 2 стандартных представления — список новостей и карточка отдельной новости. Важно представлениям давать названия, соответствующие контроллеру:

    Представление для списка (view/NewsList.js)
    Ext.define('Crm.modules.news.view.NewsList', {
        extend: 'Core.grid.GridWindow',    
        sortManually: true,  // Ручная сортировка новостей в списке    
        filterbar: true,   // включить возможность фильтрации 
        /* перечисляем колонки таблицы */
        buildColumns: function() { 
            return [{
                text: 'Title',
                flex: 1,
                sortable: true,
                dataIndex: 'name',
                filter: true
            },{
                text: 'Date start',
                flex: 1,
                sortable: true,
                xtype: 'datecolumn',
                dataIndex: 'date_start',
                filter: true
            },{
                text: 'Date finish',
                flex: 1,
                sortable: true,
                xtype: 'datecolumn',
                dataIndex: 'date_end',
                filter: true
            }]        
        }   
    })
    

    Представление для карточки новости (view/NewsForm.js)
    Ext.define('Crm.modules.news.view.NewsForm', {
        extend: 'Core.form.DetailForm'    
        ,titleIndex: 'name' // имя поля, данные из которого будут выведены в заголовок окна формы    
        ,layout: 'border'    
        ,border: false
        ,bodyBorder: false    
        ,height: 450
        ,width: 750    
        ,buildItems: function() {
            return [{
                xtype: 'panel',
                region: 'north',
                border: false,
                bodyBorder: false,
                layout: 'anchor',
                bodyStyle: 'padding: 5px;',
                items: [{
                    name: 'name',
                    anchor: '100%',
                    xtype: 'textfield',
                    fieldLabel: 'Title'
                },{
                    xtype: 'fieldcontainer',
                    layout: 'hbox',
                    anchor: '100%',
                    items: [{
                        xtype: 'datefield',
                        fieldLabel: 'Date start',
                        name: 'date_start',
                        flex: 1,
                        margin: '0 10 0 0'
                    },{
                        xtype: 'datefield',
                        fieldLabel: 'Date finish',
                        name: 'date_end',
                        flex: 1
                    }]
                },{
                    xtype: 'textarea',
                    anchor: '100%',
                    height: 60,
                    name: 'stext',
                    emptyText: 'Announce'
                }]  
            },
                this.fullText()
            ]
        }    
        ,fullText: function() {
            return Ext.create('Desktop.modules.pages.view.HtmlEditor', {
                hideLabel: true,
                region: 'center',
                name: 'text'
            })
        }
    })
    

    Представления не должны вызывать сложности — это стандартные компоненты Extjs. Контроллеры разных модулей могут использовать одни и те же представления.

    Теперь самое интересное — модель. В Janusjs код модели используется и на клиентской стороне и на серверной. Рассмотрим модель модуля новости:

    Модель новостей (model/NewsModel.js)
    Ext.define('Crm.modules.news.model.NewsModel', {    
        extend: "Core.data.DataModel"
        ,collection: 'news' // имя коллекции/таблицы в БД
        ,removeAction: 'remove' // что делать с записями при удалении
        /* список полей записи */
        ,fields: [{
            name: '_id',   // имя поля
            type: 'ObjectID',  // тип данных
            visible: true // отдавать данные при запросе
        },{
            name: 'name',
            type: 'string',
            filterable: true,  // допустим поиск по этому полю
            editable: true,  // данные можно изменять
            visible: true
        },{
            name: 'date_start',
            type: 'date',
            filterable: true,
            editable: true,
            visible: true
        },{
            name: 'date_end',
            type: 'date',
            filterable: true,
            editable: true,
            visible: true
        },{
            name: 'stext',
            type: 'string',
            filterable: false,
            editable: true,
            visible: true
        },{
            name: 'text',
            type: 'string',
            filterable: false,
            editable: true,
            visible: true
        }]
    })
    

    Название файла модели должно, так же, соответствовать названию контроллера. В противном случае, в контроллере нужно вручную указать с какой моделью он должен работать (параметр ‘modelName’).

    В корне каталога модуля новостей находится файл “manifest.json”. Этот файл нужен для того, что бы модуль появился в главном меню пользовательского интерфейса. В сложных случаях модуль может состоять из нескольких контроллеров и система должна знать какой из них главный, для этого и нужен манифест. Если файл манифеста отсутствует в каталоге модуля, модуль не будет виден в главном меню.

    Важное замечание: при любых изменениях в серверной части системы следует перезапустить сервер Janusjs!

    Один код на клиенте и сервере


    Чтобы проиллюстрировать архитектурные особенности Janusjs, немного доработаем модуль новостей. Добавим кнопку, при клике по которой, все выделенные в списке новости будут опубликованы на неделю (дата начала = текущая дата, дата окончания = +7 дней).

    В представление списка добавим кнопку:

    Ext.define('Crm.modules.news.view.NewsList', {
    …
        // добавляем кнопку в стандартный Tbar
        ,buildTbar: function() {
            // получим массив со стандартными кнопками
            // из родительского класса
            var items = this.callParent();
            // добавим новую кнопку
            items.splice(2,0, {
                text: 'Publish selected',
                action: 'publish'
            })
            return items;
        }
    …
    })
    


    Добавим в контроллер обработчик для новой кнопки:

    Ext.define('Crm.modules.news.controller.News', {
        extend: 'Core.controller.Controller'
        
        ,addControls: function(win) {
            var me = this
            me.control(win,{
                "[action=publish]": {click: function() {me.publish(win)}}
            })
            me.callParent(arguments)
        }
    
       ,publish: function(win) {
            var grid = win.down('grid')
                // получим выделенные строки
                ,selected = grid.getSelectionModel().selected             
                // тут сохраним идентификаторы отмеченных новостей
                ,ids = [];             
            if(selected && selected.items) {
                selected.items.forEach(function(item) {
                    ids.push(item.data._id)    
                })            
                if(ids.length) {
                    // Вызываем клиентский метод модели
                    // передаем ему идентификаторы выделенных новостей
                    this.model.publish(ids, function() {
                        grid.getStore().reload();        
                    })
                }
            }    
        }
    })
    

    Доработаем модель новостей:

    Ext.define('Crm.modules.news.model.NewsModel', {    
        extend: "Core.data.DataModel"
        ,collection: 'news'
        ,removeAction: 'remove'
        ,fields: [
            .......
        ]
        
        ,publish: function(ids, cb) {
            // Передадим идентификаторы новостей на сервер
            this.runOnServer('publish', {ids: ids}, cb)
        }
        
        // со стороны клиента можно вызвать на сервере только методы
        // имя которых начинается с $, в вызове серверного метода
        // на клиенте символ $ можно опустить (см. 6 строк выше)
        ,$publish: function(data, cb) {
          
            var me = this
                ,date_start = new Date() // текущая дата
                ,date_end = new Date(date_start.getTime() + 86400000 * 7) // + 7 дней
                ,ids = data.ids || null;
            
            if(!ids) {
                cb({ok: false})
                return;
            }
            
            // преобразуем идентификаторы новостей 
            // из строки в монговский ObjectId
            ids.each(function(id) {
                return  me.src.db.fieldTypes.ObjectID.getValueToSave(null, id)
            }, true)
            
            // изменим даты в БД
            // me.dbCollection - ссылка на коллекцию текущего модуля
            me.dbCollection.update({
                _id:{$in: ids}    
            }, {
                $set: {
                   date_start: date_start,
                   date_end: date_end
                }
            }, {
                multi: true    
            }, function() {
                cb({ok: true})    
            })        
        }
    })
    

    Таким образом, метод «publish» отработает на клиенте, а метод "$publish" на сервере.

    Вопрос безопасности


    С нашей моделью осталась одна существенная проблема: т.к. код модели доступен на клиенте и на сервере, можно снаружи увидеть, что происходит внутри. Показывать методы серверной логики наружу не кашерно, поэтому, спрячем их. Делается это с помощью специальных серверных директив, которые помещаются в комментарии. Предусмотрено 2 директивы: scope:server и scope:client

    /* scope:server */ — убирает следующий за этим комментарием метод из кода при отдаче клиенту
    // scope:server — убирает всю строку из кода при отдаче клиенту
    /* scope:client */ — убирает следующий за этим комментарием метод из кода перед выполнением кода на сервере
    // scope:client — убирает всю строку из кода перед выполнением кода на сервере

    Используя эти знания, сделаем нашу модель безопасной:

    Ext.define('Crm.modules.news.model.NewsModel', {    
        extend: "Core.data.DataModel"
        ,collection: 'news' // scope:server
        ,removeAction: 'remove' // scope:server
        ,fields: [{
            name: '_id',
            type: 'ObjectID', // scope:server
            visible: true
        },{
            name: 'name',
            type: 'string', // scope:server
            filterable: true, 
            editable: true, 
            visible: true 
        },{
            name: 'date_start',
            type: 'date', // scope:server
            filterable: true,
            editable: true,
            visible: true
        },{
            name: 'date_end',
            type: 'date', // scope:server
            filterable: true,
            editable: true,
            visible: true
        },{
            name: 'stext',
            type: 'string', // scope:server
            filterable: false,
            editable: true,
            visible: true
        },{
            name: 'text',
            type: 'string', // scope:server
            filterable: false,
            editable: true,
            visible: true
        }]
        
        /* scope:client */
        ,publish: function(ids, cb) {
            this.runOnServer('publish', {ids: ids}, cb)
        }
        
        /* scope:server */
        ,$publish: function(data, cb) {
            var me = this
                ,date_start = new Date() // текущая дата
                ,date_end = new Date(date_start.getTime() + 86400000 * 7) // + 7 дней
                ,ids = data.ids || null;
            
            if(!ids) {
                cb({ok: false})
                return;
            }
            ids.each(function(id) {
                return  me.src.db.fieldTypes.ObjectID.getValueToSave(null, id)
            }, true)
            me.dbCollection.update({
                _id:{$in: ids}    
            }, {
                $set: {
                   date_start: date_start,
                   date_end: date_end
                }
            }, {
                multi: true    
            }, function() {
                cb({ok: true})    
            })
        }
    })
    

    Теперь, можно спать спокойно, серверные методы снаружи не видны. К слову, используя директивы «scope» можно давать клиентским и серверным методам и свойствам одной модели одинаковые имена.

    Связанные модули


    Janusjs позволяет легко комбинировать несколько связанных модулей в один. На видео в начале статьи в карточку объекта недвижимости подтягивается список заинтересованных клиентов, комментарии агентов, связанные с объектом документы.

    Добавим в карточку новости вкладку с комментариями. Для начала, создадим каталоги и файлы для модуля комментариев (все пути ниже даны относительно каталога проекта projects/crm/):

    static
        admin
            modules 
                comments
                    controller
                        Comments.js
                    model
                        CommentsModel.js
                    view
                        CommentsList.js
                        CommentsForm.js
    

    Код контроллера (Comments.js):
    Ext.define('Crm.modules.comments.controller.Comments', {
        extend: 'Core.controller.Controller', 
        launcher: {
            text: 'Comments',  // название модуля
            iconCls:'fa fa-comment-o'  //  иконка  модуля  
        }    
    });
    

    Представление списка комментариев (CommentsList.js):
    Ext.define('Crm.modules.comments.view.CommentsList', {
        extend: 'Core.grid.GridWindow',    
        // в таблице списка будет только одна колонка с текстами комментариев
        buildColumns: function() { 
            return [{
                text: 'Comment',
                flex: 1,
                sortable: true,
                dataIndex: 'text',
                filter: true
            }]        
        }   
    })
    


    Форма добавления и редактирования комментария (CommentsForm.js):
    Ext.define('Crm.modules.comments.view.CommentsForm', {
        extend: 'Core.form.DetailForm' 
        ,titleIndex: 'text' // имя поля, данные из которого будут выведены в заголовок окна формы    
        ,buildItems: function() {
            return [
            // поле для ввода текста комментария
            {
                 fieldLabel: 'Comment text',
                 name: 'text',
                 xtype: 'textarea',
                 anchor: '100%',
                 height: 150
            }, 
            // идентификатор записи к которой относится редактируемый комментарий
            // заполняется автоматически
            {
                 name: 'pid',
                 hidden: true
            }]
        }
    })
    

    И, наконец, клиент-серверная модель (CommentsModel.js):

    Ext.define('Crm.modules.comments.model.CommentsModel', {    
        extend: "Core.data.DataModel"
        ,collection: 'comments' // scope:server
        ,removeAction: 'remove' // scope:server
        ,fields: [{
            name: '_id',
            type: 'ObjectID', // scope:server
            visible: true
        },{
            name: 'pid',
            type: 'ObjectID', // scope:server
            visible: true,
            filterable: true, 
            editable: true
        },{
            name: 'text',
            type: 'string', // scope:server
            filterable: true, 
            editable: true, 
            visible: true 
        }]
    })
    

    Как видно, в подчиненном модуле достаточно объявить поле, где будет храниться внешний ключ. Далее, добавим вкладку комментариев на форму редактирования новости (static/admin/modules/news/view/NewsForm.js):

    Ext.define('Crm.modules.news.view.NewsForm', {
        extend: 'Core.form.DetailForm'    
        ,titleIndex: 'name' // имя поля, данные из которого будут выведены в заголовок окна формы    
        ,layout: 'border'    
        ,border: false
        ,bodyBorder: false    
        ,height: 450
        ,width: 750    
        // добавим tabpanel в качестве основного елемента
        ,buildItems: function() {
            return [{
                xtype: 'tabpanel',
                region: 'center',
                items: [
                    this.buildMainFormTab(),
                    this.buildCommentsTab()
                ]
            }]
        }
        // панель с формой новости
        ,buildMainFormTab: function() {
            return {
                xtype: 'panel',
                title: 'Новость',
                layout: 'border',
                items: this.buildMainFormTabItems()
            }
        }
        // поля формы новости
        ,buildMainFormTabItems: function() {
            return [{
                xtype: 'panel',
                region: 'north',
                border: false,
                bodyBorder: false,
                layout: 'anchor',
                bodyStyle: 'padding: 5px;',
                items: [{
                    name: 'name',
                    anchor: '100%',
                    xtype: 'textfield',
                    fieldLabel: 'Title'
                },{
                    xtype: 'fieldcontainer',
                    layout: 'hbox',
                    anchor: '100%',
                    items: [{
                        xtype: 'datefield',
                        fieldLabel: 'Date start',
                        name: 'date_start',
                        flex: 1,
                        margin: '0 10 0 0'
                    },{
                        xtype: 'datefield',
                        fieldLabel: 'Date finish',
                        name: 'date_end',
                        flex: 1
                    }]
                },{
                    xtype: 'textarea',
                    anchor: '100%',
                    height: 60,
                    name: 'stext',
                    emptyText: 'Announce'
                }]  
            },
                this.fullText()
            ]
        }    
        ,fullText: function() {
            return Ext.create('Desktop.modules.pages.view.HtmlEditor', {
                hideLabel: true,
                region: 'center',
                name: 'text'
            })
        }
        ,buildCommentsTab: function() {
            return { 
                xtype: 'panel',
                title: 'Comments',
                layout: 'fit',
                // параметр, указывающий, что в данной панели нужно
                // показать связанный модуль
                childModule: {
                    // контроллер модуля
                    controller: 'Crm.modules.comments.controller.Comments',
                    // название поля ключа родительской записи (_id новости)
                    outKey: '_id',
                    // название поля ключа в дочерней записи (pid в комментариях)
                    inKey: 'pid'
    	    }
            }
        }
    })
    

    Таким образом, достаточно указать параметр childModule у одной из панелей окна с карточкой новости.

    Websocket вместо AJAX


    В Янусе я отказался от использования привычного AJAX для обмена данными между клиентом и сервером в пользу веб-сокетов. Такое решение позволяет создавать системы работающие в реальном времени. Например, при создании новости, она моментально появляется в списках новостей у других пользователей. Немного удивило то, что в стандартном комплекте Extjs (даже последних версий) не нашлось прокси на веб-сокетах и пришлось попотеть, что бы заставить extjs общаться с сервером через них. Вообще, тема использования веб-сокетов в приложениях extjs интересна сама по себе, думаю, написать про это отдельно.

    Создание сайтов


    Janusjs можно использовать и для построения обычных сайтов. Для примера, давайте выведем список наших новостей на отдельной странице. Для начала, создадим простой html-шаблон и разместим его в файле protected/view/index.tpl

    Код шаблона:

    <!DOCTYPE HTML>
    <html>
        <head>
        <title>{[values.metatitle? values.metatitle:values.name]}</title>
        </head>
    <body>
        <tpl if="blocks && blocks[1]">
            <tpl for="blocks[1]">{.}</tpl>
        </tpl>  
    </body>
    </html>
    


    В качестве шаблонизатора используется немного доработанный XTemplate из стандартного пакета Extjs (http://docs.sencha.com/extjs/4.2.2/#!/api/Ext.XTemplate). Тут я не буду рассматривать вопросы, как сделать навигацию, это тема для отдельной статьи. В массиве blocks передается контент. Количество блоков не ограничено и они могут располагаться в разных местах кода шаблона.

    Далее, создадим модуль новостей, он будет состоять из 3-х файлов: контроллера и 2-х шаблонов. Начнем с шаблона списка новостей:

    <tpl for="list">
        <h4>
            <a href="/news/{_id}">{name}</a> 
            <i class="date">Дата: {[Ext.Date.format(new Date(values.date_start),'d.m.Y')]}</i>
        </h4>
        <p>{stext}</p>
    </tpl>
    

    Файл сохраним в protected/site/news/view/list.tpl

    Шаблон новости:

    <h4>
        {name} 
        <i class="date">Дата: {[Ext.Date.format(new Date(values.date_start),'d.m.Y')]}</i>
    </h4>
    {text}
    <a href="./">К списку</a>
    

    Файл сохраним в protected/site/news/view/one.tpl

    Контроллер использует модель модуля из CRM. Для наглядности, реализуем простейшую функциональность без пейджинга, сортировок и т.п.

    Ext.define('Crm.site.news.controller.News',{
        extend: "Core.Controller"
        
        ,show: function(params, cb) {
            // если в url есть идентификатор новости, покажем страницу с полным текстом
            if(params.pageData.page) 
                this.showOne(params, cb)
            else
            // в противном случае, выводится список новостей
                this.showList(params, cb)
        }    
        ,showOne: function(params, cb) {
            var me = this;
            Ext.create('Crm.modules.news.model.NewsModel', {
                scope: me
            }).getData({
                filters: [{property: '_id', value: params.pageData.page}]
            }, function(data) {
                me.tplApply('.one', data.list[0] || {}, cb)            
            }); 
        }    
        ,showList: function(params, cb) {
            var me = this;
            Ext.create('Crm.modules.news.model.NewsModel', {
                scope: me
            }).getData({
                filters: []    
            }, function(data) {
                me.tplApply('.list', data, cb)            
            }); 
        }
    });
    


    Контроллер сохраним в protected/site/news/controller/News.js

    В Janusjs можно реализовать любые подходы для организации роутинга. Все зависит от того, какой контроллер путей подключен к серверу. По-умолчанию, подключен контроллер, который реализует следующий алгоритм:
    • Пути вида <domain.name>/Crm.model.moduleName.methodName/ зарезервированы для вызова публичных методов моделей (это нужно для построения, всякого рода, API)
    • Пути вида <domain.name>/page1/page2/ предназначены для доступа к виртуальным страницам публичного сайта. Модули для управления виртуальными страницами находятся в админке в разделе меню Пуск->Панель управления.


    Итак, для вывода списка новостей на публичной стороне нужно создать виртуальную страницу и к одному из блоков контента привязать публичный метод контроллера модуля новостей. Видео как это сделать:



    Страница со списком новостей будет доступна по адресу:

    localhost:8008/news/

    Фрагментация и работа оффлайн


    Рассмотрим типичный кейс. Предположим, мы разрабатываем систему для управления сетью небольших отелей. Менеджер при создании брони должен иметь доступ к номерному фонду всех отелей. При этом, каждый отель должен уметь работать изолированно в случае обрыва связи. В Янусе реализован экспериментальный подход, позволяющий запускать отдельные копии сервера от имени отдельных пользователей. Такие сервера содержат только тот набор данных, доступ к которым есть у пользователя от имени которого запущен сервер. Другие пользователи в локальной сети отеля могут подключаться к такому серверу под своими учетными записями. Кроме того, такие сервера синхронизируются в режиме реального времени с центральным сервером. В случае отключения интернет в отеле, система продолжает работать с локальным набором данных накапливая изменения. При восстановлении подключения данные синхронизируются с центральным сервером.

    Заключение


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


    PS Это статья обзорная и многие вопросы остались за кадром. По каждому из них можно написать отдельную статью. Вот примерный список не раскрытых тем:

    • Типы данных, добавление кастомизированных типов, связанные поля.
    • Создание пользовательского интерфейса: связанные модули, нестандартные элементы UI, работа с изображениями и файлами.
    • Использование веб-сокетов, системы реального времени на базе Janusjs.
    • Изменение внешнего вида рабочего стола для разных групп пользователей.
    • Система распределения прав доступа, кастомизация набора прав для модулей Janusjs.
    • Создание CMS и сайтов на базе Janusjs.
    • Интеграция с поисковой системой Elasticsearch.
    • Дополнительные возможности: выполнение скриптов по расписанию, почтовые функции.
    • Использование реляционных баз данных, использование нескольких СУБД в одном проекте.
    • Мультиязычные проекты.
    • Настройка продакшн сервера: включение логов, использование всех ядер процессора, распределение нагрузки.
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 17

      0
      А вы классовую систему из extjs перенесли на nodejs?
        0
        Да, на сервере все работает также как на клиенте, нет только визуальных компонент. Не визуальные компоненты типа Ext.Date, Ext.XTemplate и т.п. доступны и на сервере.
          0
          Я правильно понимаю, что он оформлен в отдельный модуль, а именно: github.com/Kolbaskin/janusjs/tree/master/node_modules/extjs-node
            0
            Да, все верно. Это немного доработанный https://github.com/agebrock/extjs-node.
              0
              а шестая версия будет?
                0
                Не думаю, что на серверной стороне нужна 6я версия. В клиентской части сейчас используется 5й extjs, не вижу проблем собрать на 6й. Не знаю, на сколько 6й стабильный, думаю, нужно подождать немного.
        0
        Какой смысл скрывать на клиенте тип поля?
        type: 'date', // scope:server
        
          0
          Как будет работать модель на клиенте, если информация о типе поля убрана (scope:server)???
            0
            Это тип поля как оно сохраняется в БД. В принципе, можно и не скрывать, но береженого…
              0
              Если на клиенте тип поля отличается от серверного, можно написать так:
              type: '<client type>', // scope: client
              type: '<server type>', // scope: server
              
                0
                Меня интересует вопрос, если тип поля скрыт, то как на клиенте такой код работает? Будет ошибка же.
                  0
                  Нет, ошибки не будет.
              0
              Планируете ли Вы использовать Sencha Cmd для серверной части?
                0
                Пока нет планов. В моей реализации серверной части можно использовать модули от Express. Я не копался глубоко в Sencha Cmd, если там есть такая возможность, то можно подумать на эту тему.
                  0
                  Хм, никогда не задумывался на эту тему, но самому стало интересно как можно Sencha Cmd использовать на сервере. Судя по описанию, это инструмент для сборки приложений, к тому же он доступен только по коммерческой лицензии (видимо, поэтому я никогда им не интересовался).
                  0
                  Каким образом реализована автозагрузка классов?
                    0
                    Через require. Там модифицирован Ext.Loader

                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                  Самое читаемое