📜 ⬆️ ⬇️

Janusjs: concept of the system, where the client and server are conjoined twins

image By the nature of my work, I often have to develop a variety of crm-systems. The client part I have been collecting for a very long time on Extjs (I started from version 2). A couple of years ago Nodejs firmly settled on the server, replacing the usual PHP.

Last year, the idea of ​​a unified platform for the client and server parts of the web application based on Extjs. After a year of trial and error, the puzzle has more or less taken shape. In this article, I want to share the concept of a framework whose code looks the same on the client and server side.

A few reasons for choosing Sencha as the base library:

  1. Products of the company Sencha (Extjs in particular) are known among the developers, respectively, there are a sufficient number of specialists in this topic. For the same reason, there are no problems with documentation, examples, and the community.
  2. Extjs is one of the few js frameworks with a logical, well-thought-out architecture that is equally well applicable on both the client and server side of the application.
  3. A single code base allows you to describe both client and server logic in a single file. This reduces the number of lines of code and avoids duplication.

')

Installation


We need Nodejs, Mongodb, Memcached. Nodejs and Mongodb, preferably, fresh versions (especially Nodejs). I see no point in describing the installation process of these programs in the article, there are enough instructions for every taste and OS on the network.

Before starting the installation, be sure to check whether the mongodb and memcached processes are running. The installation script checks the connectivity. If there is no connection, the installation will fail.

Install the framework:
npm i janusjs 


At the end of the installation, the installer will ask a few suggestive questions for generating the sample project (database connection parameters, default user, etc.).

After all the manipulations, in the current directory you will have the following content:
 node_modules projects cluster.config.js cluster.js daemon.js server.js 

projects - project directory
from the rest of the files, we are, for now, interested in server.js, and run it:
 node server 

If everything is in order, the console will say: “Server localhost is listening on port 8008”
In case of an error, check if the port is busy and Mongodb and Memcached are running.
The web interface of the system is available at the address localhost : 8008 / admin / (if you did not change the user during the installation, then admin: admin). Access parameters can be checked in the project's project file.
 projects/crm/config.json 

The user interface is quite standard. The system allows for different user groups to create different types of interfaces.

On the video you can see an example of the work of crm real estate agency:



Creating modules


When installing Janus, an empty project was to be created in the projects / crm directory. Project directory structure:

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


static / admin / modules / news is an example of a CRM module. There is a simple list of news. Let's see how this is done.

Janusjs modules are built based on the MVP pattern. In detail we will consider each part of the module:

controller / News.js - controller (Presenter). The module implements the standard behavior (the list of entries is a record card), therefore all the functionality “lives” in the parent class. Controller code:

 Ext.define('Crm.modules.news.controller.News', { extend: 'Core.controller.Controller', launcher: { text: 'News', //   iconCls:'fa fa-newspaper-o' //  ,  Font Awesome } }); 

The news module has 2 standard views - a list of news and a separate news card. It is important for representations to give names corresponding to the controller:

List view (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 }] } }) 

Presentation for the news card (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' }) } }) 

Views should not be difficult - these are standard Extjs components. Controllers of different modules can use the same views.

Now the most interesting is the model. In Janusjs, the model code is used on both the client side and the server side. Consider the news module model:

News model (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 }] }) 

The name of the model file must also match the name of the controller. Otherwise, in the controller, you must manually specify which model it should work with (the 'modelName' parameter).

At the root of the news module directory is the file “manifest.json”. This file is needed for the module to appear in the main menu of the user interface. In complex cases, a module may consist of several controllers and the system must know which of them is the main one, for this, a manifest is needed. If the manifest file is not in the module directory, the module will not be visible in the main menu.

Important note: for any changes in the server part of the system, restart the Janusjs server!

One code on client and server


To illustrate the architectural features of Janusjs, we will slightly modify the news module. Add a button, when clicked, all selected news in the list will be published for a week (start date = current date, end date = +7 days).

Add a button to the list view:

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


Add a handler to the controller for the new button:

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

Let's finalize the news model:

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

Thus, the “publish” method will work on the client, and the $ publish method on the server.

Safety issue


There is one significant problem with our model: the model code is available on the client and on the server; you can see from the outside what is going on inside. Show the server logic methods to the outside is not kosher, so hide them. This is done with the help of special server directives that are placed in the comments. There are 2 directives: scope: server and scope: client

/ * scope: server * / - removes the method following this comment from the code when returning to the client
// scope: server - removes the entire line from the code when returning to the client
/ * scope: client * / - removes the method following this comment from the code before executing the code on the server
// scope: client - removes the entire line from the code before executing the code on the server

Using this knowledge, we will make our model safe:

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

Now you can sleep peacefully, server methods are not visible from the outside. By the way, using the “scope” directives, you can give client and server methods and properties of the same model the same names.

Related Modules


Janusjs makes it easy to combine several related modules into one. On the video at the beginning of the article, the list of interested clients, the comments of the agents, and the documents related to the object are pulled into the real estate card.

Add a tab with comments to the news card. To begin with, create directories and files for the comment module (all the paths below are relative to the project directory projects / crm /):

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

Controller code (Comments.js):
 Ext.define('Crm.modules.comments.controller.Comments', { extend: 'Core.controller.Controller', launcher: { text: 'Comments', //   iconCls:'fa fa-comment-o' //   } }); 

Presentation of the list of comments (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 }] } }) 


The form for adding and editing a comment (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 }] } }) 

And finally, the client-server model (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 }] }) 

As you can see, in the slave module it is enough to declare the field where the foreign key will be stored. Next, add a comments tab to the news editing form (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' } } } }) 

Thus, it is enough to specify the childModule parameter in one of the panels of the window with the news card.

Websocket instead of AJAX


In Janus, I refused to use the usual AJAX to exchange data between the client and the server in favor of web sockets. This solution allows you to create systems that work in real time. For example, when creating a news item, it instantly appears in the news lists of other users. A little surprised that in the standard Extjs bundle (even the latest versions) there was no proxy on the web sockets and had to sweat to get extjs to communicate with the server through them. In general, the topic of using web sockets in extjs applications is interesting in itself, I think, to write about it separately.

Website development


Janusjs can also be used to build regular sites. For example, let's list our news on a separate page. First, create a simple html template and place it in the protected / view / index.tpl file

Template code:

 <!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> 


A slightly modified XTemplate from the standard Extjs package (http://docs.sencha.com/extjs/4.2.2/#!/api/Ext.XTemplate) is used as a template engine. Here I will not consider the issues of how to make navigation, this is a topic for a separate article. Content is transferred in the block array. The number of blocks is not limited and they can be located in different places of the template code.

Next, create a news module, it will consist of 3 files: a controller and 2 templates. Let's start with the news list template:

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

Save the file in protected / site / news / view / list.tpl

News template:

 <h4> {name} <i class="date">: {[Ext.Date.format(new Date(values.date_start),'dmY')]}</i> </h4> {text} <a href="./"> </a> 

Save the file to protected / site / news / view / one.tpl

The controller uses a module model from CRM. For clarity, we implement the simplest functionality without paging, sorting, etc.

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


The controller will save protected / site / news / controller / News.js

In Janusjs, you can implement any approach for organizing routing. It all depends on which path controller is connected to the server. By default, a controller is connected that implements the following algorithm:


So, to display the list of news on the public side, you need to create a virtual page and bind to one of the content blocks the public method of the news module controller. Video how to do it:



A page with a list of news will be available at:

localhost:8008/news/

Fragmentation and work offline


Consider a typical case. Suppose we are developing a system for managing a network of small hotels. Manager when creating a reservation should have access to the number of rooms of all hotels. At the same time, each hotel should be able to work in isolation in case of connection failure. In Janus, an experimental approach is implemented that allows you to run separate copies of the server on behalf of individual users. Such servers contain only the data set that the user has access to on behalf of which the server is running. Other users in the local network of the hotel can connect to such a server using their own accounts. In addition, such servers are synchronized in real time with the central server. In the case of disconnection of the Internet at the hotel, the system continues to work with a local dataset accumulating changes. When the connection is restored, the data is synchronized with the central server.

Conclusion


In conclusion, I will list the points why I needed my own bike. Needed a system:


PS This article is an overview and many questions remained behind the scenes. For each of them, you can write a separate article. Here is a sample list of not disclosed topics:

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


All Articles