Jii - JavaScript framework with architecture from Yii 2


    Introduction


    Hello to all the Khabrovites, fans of Yii and Node.js.
    I’m selling a series of articles about the Jii framework and its parts. In the previous parts, we looked at parts of the framework that can be used without initializing the application, namely Query Builder and Active Record . From the vote (as well as letters and comments) it became clear that it was worth continuing. And this time we will talk about the architecture and structural components of the Jii framework.

    Ideology



    Porting the functionality of the framework from PHP to JavaScript is not a trivial task and cannot be performed by a robot. Languages ​​have very different capabilities and differences, the main one being JavaScript asynchrony.
    Below is how these problems were solved and what practices I followed. Other developers should follow the same practices when developing Jii and its extensions.

    Promise


    Each asynchronous operation returns a Promise object implementing the ES6 standard. To do this, use the when library .
    Methods starting with the prefix load (loadData ()) return Promise, methods with the prefix get (getUser (), get ('id')) return data synchronously.

    Saving API Yii 2


    Jii's original idea is to port a beautiful and rich PHP framework to JavaScript. So that php developers who want to try / switch to node.js can quickly and easily learn the framework in
    another programming language, even without reading the reference class!

    Embeddability


    Jii is designed taking into account that it should work wherever possible - browser, server, phonegap, node-webkit, etc.
    Despite the fact that Yii generates quite a few global constants ( YII_ * ), the class Yii and the namespace yii , Jii does not clog the
    global space. Browsers have access to a single Jii object that can be hidden by calling Jii.noConflict () . In the server side, nothing is written in global, and Jii is returned as the result of
    calling require ('jii') .

    Npm packages


    Jii is distributed as a set of jii- * packages and does not have its own package managers (hello, Meteor). This means that with Jii you can plug in any other npm packages.
    Jii is broken into several packages, so it can be used in parts. For example, if you want to start using only ActiveRecord, then you install jii-ar-sql and do not have controllers, views, http server and other code that you do not need.
    If you read the previous articles , then you’ve already seen this.
    The main packages of Jii at the moment are:

    Getters and Setters


    One of the main features of Yii is the ease of accessing the properties and methods of objects through getters and setters, for example, accessing via $ model-> user can either obtain a model property, or call the getUser () method , or even get the user relation, where the call to DB is complete magic, but everyone likes it.
    In JavaScript, not all browsers support getters and setters, and in many projects you still need to support IE8, so in Jii they are implemented as get ('user') and set ('user', 'Ivan') methods . This approach can be found in many libraries, for example in Backbone .
    In the future, when the need for old browsers disappears, real getters and setters will be added in parallel with get / set methods that will work through them.

    Classes and Namespaces


    As they say, every self-respecting programmer should write his own implementation of classes in JavaScript. So here, to implement the classes, my Neatness library is used , which has already shown itself well in other internal projects.

    Why not ES6 classes (and not CoffeeScript / TypeScript)?



    In the comments and on the github there is already a small holivar about this.
    I will also voice here the reasons for the lack of ES6 in Jii:
    1. Node preprocessing is needed for node.js (io.js supports)
    2. There are no namespaces in es6, but they are needed to repeat the features of Yii. They can be implemented through modules, but it will be a crutch. Change one crutch for another - it makes no sense.
    3. ES6 will want to use getters and setters, but they are not supported by older browsers, as mentioned above
    4. Many developers do not know the es6 / coffeescript / typescript format and this will be an additional entry threshold
    5. The complexity of working with code in different formats (ES5 and ES6). It is not always possible in an existing project to change all the code to es6, but you still need to inherit es6 classes, and you still have to use Jii.defineClass () to create classes - the crutch does not go away.

    Below I added a poll on this topic, vote, comment!

    Middlewares


    In the Yii framework, components are accessible globally via Yii :: $ app-> ... However, in Jii, not all components can be located globally, because some of them (Request, Response, WebUser, Session, ...) are tied to the context (request).
    Such components will be created in the context ( Jii.base.Context ) and passed as a parameter to action - similar to passing request and response to express .

    /**
     * @class app.controllers.SiteController
     * @extends Jii.base.Controller
     */
    Jii.defineClass('app.controllers.SiteController', /** @lends app.controllers.SiteController.prototype */{
        __extends: Jii.base.Controller,
        /**
         *
         * @param {Jii.base.Context} context
         * @param {Jii.httpServer.Request} context.request
         * @param {Jii.httpServer.Response} context.response
         */
        actionIndex: function(context) {
            context.response.data = this.render('index');
            context.response.send();
        }
    });
    

    Entities



    Jii applications are organized according to the model-view-behavior (MVC) design pattern and have the following entities:
    • applications : these are globally accessible objects that perform the correct operation of various application components and their coordination for processing a request;
    • application components : these are objects registered in the application and providing various capabilities;
    • request components : these are objects registered in the context of the request and providing various possibilities for processing the current request;
    • modules : these are self-contained packages that include all the tools for MVC. An application can be organized using several modules;

    Examples of their use can be seen in the demo .

    Applications


    Applications are objects that control the entire structure and life cycle of the Jii application system. Usually one instance of the Jii application comes to one worker (process) Node.js, which is available through Jii.app.
    When the input script creates the application, it will load the configuration and apply it to the application, for example:

    var Jii = require('jii');
    // загрузка конфигурации приложения
    var config = {
        application: {
            basePath: __dirname,
            components: {
                http: {
                    className: 'Jii.httpServer.HttpServer'
                }
            }
        },
        context: {
            components: {
                request: {
                    baseUrl: '/myapp'
                }
            }
        }
    };
    // создание объекта приложения и его конфигурирование
    Jii.createWebApplication(config);
    

    An important difference from Yii is that the configuration is divided into two parts - the application configuration (application section) and the context (request) configuration (context section). Application configuration creates and configures components, modules and configures the application itself (Jii.app) - all this happens when the worker starts. In turn, the context configuration creates and configures the components with every action call - an http request, a console command call, etc. Created components are passed as the first argument to the action method.

    Due to the fact that the configuration of an application is often very complicated, it is transferred to files and divided into several configuration files.

    Application components


    Applications store many application components that provide various means for the application to work. For example, urlManager components are responsible for routing web requests to the right controller; the db component provides tools for working with the database; etc.

    Each application component has its own unique ID, which allows it to be identified among other various components in the same application. You can access the component as follows:

    Jii.app.ComponentID
    

    Embedded Application Components


    Jii has several built-in application components, with fixed IDs and default configurations.

    The following is a list of built-in application components. You can configure them as well as other application components. When you configure the built-in component of the application and do not specify the class of this component, the default value will be used.
    • db , Jii.sql.BaseConnection : is a database connection through which you can execute queries. Please note that when you configure this component, you must specify the component class as well as the other necessary parameters.
    • urlManager , Jii.urlManager.UrlManager : used to parse and create URLs;
    • assetManager , Jii.assets.AssetManager : used to manage and publish application resources;
    • view , Jii.view.View : used to display views.

    Context Components


    The set of context components depends on where it is applied. The most common option is an HTTP request from the user, for which we will consider the built-in set of components.

    Like application components, each application component has its own unique ID, which allows it to be identified among various other components in the same context. You can access the component as follows:

        actionIndex: function(context) {
            context.ComponentID
        }
    

    Inline query components


    The following is a list of built-in query components. You can configure them as well as other components in the context section in the configuration file.

    • response , Jii.base.Response : represents the data from the server that will be sent to the user;
    • request , Jii.base.Request : represents a request received from end users;
    • user , Jii.user.WebUser : represents authenticated user information;

    Controllers


    Controllers are part of the MVC architecture. These are objects of classes inherited from Jii.base.Controller and are responsible for processing the request and generating the response. In essence, when processing an HTTP request by the server ( Jii.httpServer.HttpServer ), the controllers will analyze the input data, transfer it
    to the models, insert the model results in [views], and ultimately generate outgoing responses.

    Actions


    Controllers consist of actions that are the main blocks that the end user can access and request the execution of a particular
    functional. A controller may have one or more actions.

    The following example shows a post controller with two actions: view and create :

    /**
     * @class app.controllers.PostController
     * @extends Jii.base.Controller
     */
    Jii.defineClass('app.controllers.PostController', /** @lends app.controllers.PostController.prototype */{
        __extends: Jii.base.Controller,
        actionView: function(context) {
            var id = context.request.get('id');
            return app.models.Post.findOne(id).then(function(model) {
                if (model === null) {
                    context.response.setStatusCode(404);
                    context.response.send();
                    return;
                }
                context.response.data = this.render('view', {
                    model: model
                });
                context.response.send();
            });
        },
        actionCreate: function(context) {
            var model = new app.models.Post();
    		Jii.when.resolve().then(function() {
    			// Save user
    			if (context.request.isPost()) {
    				model.setAttributes(context.request.post());
    				return model.save();
    			}
    			return false;
    		}).then(function(isSuccess) {
                if (isSuccess) {
                    context.request.redirect(['view', {id: model.get('id')}])
                } else {
                    context.response.data = this.render('create', {
                        model: model
                    });
                    context.response.send();
                }
    		});
        }
    });
    

    In the view action (defined by the actionView () method ), the code first loads the model according to the requested model ID; If the model is successfully loaded, the code will display it using a view called view .

    In the create action (defined by the actionCreate () method ), the code is similar. He first tries to load the model using the data from the query and save the model. If everything went well, the code redirects the browser to the view action with the ID of the newly created model. Otherwise, it will display the create view through which the user can fill in the required data.

    Routes


    End users access actions through so-called * routes *. A route is a line consisting of the following parts:
    • Module ID: it exists only if the controller does not belong to the application, but to the [module] (structure-modules);
    • Controller ID: a string that uniquely identifies the controller among all other controllers of the same application (or of the same module, if the controller belongs to the module);
    • Action ID: A string that uniquely identifies an action among all other actions of the same controller.

    Routes can have the following format:

    ControllerID/ActionID
    

    or the following format if the controller belongs to the module:

    ModuleID/ControllerID/ActionID
    

    Action creation


    Creating actions is not difficult as well as declaring the so-called * action methods * in the controller class. An action method is a method whose name begins with the word action . The return value of the action method is the response data that will be sent to the end user. The code below defines two actions index and hello-world :

    /**
     * @class app.controllers.SiteController
     * @extends Jii.base.Controller
     */
    Jii.defineClass('app.controllers.SiteController', /** @lends app.controllers.SiteController.prototype */{
        __extends: Jii.base.Controller,
        actionIndex: function(context) {
            context.response.data = this.render('index');
            context.response.send();
        },
        actionHelloWorld: function(context) {
            context.response.data = 'Hello World';
            context.response.send();
        }
    });
    

    Class Actions


    Actions can be defined as classes inherited from Jii.base.Action or its descendants.

    To use such an action, you must specify it by overriding the Jii.base.Controller.actions () method in your controller class, as follows:

    actions: function() {
        return {
            // объявляет "error" действие с помощью названия класса
            error: 'app.actions.ErrorAction',
            // объявляет "view" действие с помощью конфигурационного объекта
            view: {
                className: 'app.actions.ViewAction',
                viewPrefix: ''
            }
        };
    }
    

    As you can see, the actions () method should return an object whose keys are the action IDs and the values ​​are the corresponding names of the action class or [configuration] (concept-configurations). Unlike built-in actions, individual action IDs can contain arbitrary characters as long as they are defined in the actions () method .

    To create a separate action, you must inherit from the Jii.base.Action class or its descendants, and implement the public run () method . The role of the run () method is similar to other action methods. For instance,

    /**
     * @class app.components.HelloWorldAction
     * @extends Jii.base.Action
     */
    Jii.defineClass('app.components.HelloWorldAction', /** @lends app.components.HelloWorldAction.prototype */{
        __extends: Jii.base.Action,
        run: function(context) {
            context.response.data = 'Hello World';
            context.response.send();
        }
    });
    

    Modules


    Modules are self-contained software blocks consisting of models, views, controllers, and other auxiliary components. When installing modules in the application, the end user gets access to their
    controllers. For this reason, modules are often regarded as miniature applications. Unlike applications, modules cannot be created separately; they must be inside applications.

    Creating Modules


    The module is placed in a directory called the base module path ( Jii.base.Module.basePath ). As in the application directory, in this directory there are subdirectories of controllers , models , views and others, which host controllers, models, views and other elements. The following example shows the sample module contents:

    modules/
        forum/
            Module.js                   файл класса модуля
            controllers/                содержит файлы классов контроллеров
                DefaultController.js    файл класса контроллера по умолчанию
            models/                     содержит файлы классов моделей
            views/                      содержит файлы представлений контроллеров и шаблонов
                layouts/                содержит файлы представлений шаблонов
                default/                содержит файлы представления контроллера DefaultController
                    index.ejs           файл основного представления
    

    Module classes


    Each module is declared using a unique class that inherits from Jii.base.Module . This class should be placed in the root of the base module path . When the application (worker) is
    launched, one instance of the corresponding module class will be created. Like application instances, module instances are needed so that module code can share data and components.

    Here is an example of what a module class might look like:

    /**
     * @class app.modules.forum
     * @extends Jii.base.Module
     */
    Jii.defineClass('app.modules.forum.Module', /** @lends app.modules.forum.Module.prototype */{
        __extends: Jii.base.Module,
        init: function(context) {
            this.params.foo = 'bar';
            return this.__super();
        }
    });
    

    Controllers in modules


    When you create a controller module controllers decided to put classes into the subspace controllers module class namespace. This also implies that the controller class files must be located in the controllers directory of the base module path . For example, to describe the post controller in the forum module from the previous example, the controller class is declared as follows:

    var Jii = require('jii');
    /**
     * @class app.modules.forum.controllers.PostController
     * @extends Jii.base.Controller
     */
    Jii.defineClass('app.modules.forum.controllers.PostController', /** @lends app.modules.forum.controllers.PostController.prototype */{
    	__extends: Jii.base.Controller,
        // ...
    });
    

    You can change the namespace of controller classes by setting the Jii.base.Module.controllerNamespace property . If any controllers fall out of this namespace, you can access them by setting the Jii.base.Module.controllerMap property , similar to how it is done in the application.

    Using modules


    To use the module in the application, just include it in the Jii.base.Application.modules property in the application configuration. The following code uses the forum module in the application configuration :

    var config = {
        application: {
            // ...
            modules: {
                forum: {
                    className: 'app.modules.forum.Module',
                    // ... другие настройки модуля ...
                }
            }
        },
        context: {
            // ...
        }
    };
    

    The Jii.base.Application.modules property is assigned an object containing the module configuration. Each object key is a * module identifier *, which uniquely identifies the module among other application modules, and the corresponding object is the configuration for creating the module.

    Access to modules


    Access to the module instance can be obtained in the following way:

    var module = Jii.app.getModule('forum');
    

    Having an instance of the module, you can access the parameters and components registered in the module. For instance,

    var maxPostCount = module.params.maxPostCount;
    

    In custody



    Below I suggest a survey about the Jii code format. If you choose a non-JavaScript format, indicate in the comments how you would solve the problems described in the section “Why not ES6 classes (and not CoffeeScript / TypeScript)?” In this article.
    Let me remind you that Jii is an open source project, so I will be very happy if someone joins its development. Write to affka@affka.ru.

    Website framework - jiiframework.ru
    GitHub - https://github.com/jiisoft
    Discussion of features goes on githabe

    Like it? Put a star on the github!

    Only registered users can participate in the survey. Please come in.

    What format for creating classes does Jii need?

    • 34.6% Everything is fine, Jii.defineClass () is quite satisfied 62
    • 32.9% ES6, in the comments I will describe how to solve problems 59
    • 11.7% CoffeScript 21
    • 15.6% TypeScript 28
    • 5% other, I will describe in the comments 9

    Also popular now: