Jii — JavaScript фреймворк с архитектурой от Yii 2

991c3cc6a78a434eab47ee7ab141a772.png


Привет всем хабровчанам, любителям Yii и Node.js.
Продалжаю серию статей про фреймворк Jii и его части. В прошлых частях мы рассмотрели части фреймворка, которые можно использовать без инициализации приложения, а именно — Query Builder и Active Record. Из голосования (а так же писем и комментариев) стало понятно, что продолжать стоит. И на этот раз мы будем говорить о архитектуре и структурных составляющих фреймворка Jii.

9e85d5ca6d49412e952ffa3ed85ba4af.jpg


Перенос функционала фреймворка с PHP на JavaScript — не тривиальная задача и не может выполниться роботом. Языки имеют очень разные возможности и отличия, главным из которых является асинхронность JavaScript.
Ниже о том, как решались эти проблемы и каких практик я придерживался. Этих же практик стоит придерживаться других разработчикам при разработке Jii и его разширений.

Promise


Каждая асинхронная операция возвращает Promise объект, имплементирующий стандарт ES6. Для этого используется библиотека when.
Методы, начинающиеся с префикса load (loadData ()) возвращают Promise, методы с приставкой get (getUser (), get ('id')) — возвращают данные синхронно.

Сохранение API Yii 2


Изначальная идея Jii заключается в том, чтобы перенести прекрасный и насыщенный PHP фреймворк на JavaScript. Чтобы php-разработчики, желающие попробовать/перейти на node.js могли быстро и без труда освоить фреймворк на
другом языке программирования, даже без чтения класс референс!

Встраиваемость


Jii проектируется с учётом того, что он должен работать везде где можно — браузер, сервер, phonegap, node-webkit и т.п.
Не смотря на то, что Yii генерирует не мало глобальных констант (YII_*), класс Yii и неймспейс yii, Jii не засоряет
глобальное пространство. Браузерам доступен единственный объект Jii, который можно спрятать, вызвав Jii.noConflict (). В серверной части в global ничего не пишется, а Jii возвращается как результат
вызова require ('jii').

Npm пакеты


Jii распространяется как набор пакетов jii-* и не имеет собственных пакет-менеджеров (привет, Meteor). Это значит, что вместе с Jii вы можете подключить любые другие npm пакеты.
Jii разбит на несколько пакетов, поэтому его можно использовать по частям. Например, если вы хотите начать использовать только ActiveRecord, то вы устанавливаете jii-ar-sql и не имеете контроллеров, вьюшек, http сервера и прочего ненужного вам кода.
Если вы читали предыдущие статьи, то уже убидились в этом.
Основными пакетамы Jii на данный момент являются:

Геттеры и сеттеры


Одной из основных фишек Yii является легкость доступа к свойствам и методам объектов через геттеры и сеттеры, например, обращаясь через $model→user можно получить или свойство модели, или вызвать метод getUser (), или вообще получить отношение user, где произойдет обращение к БД — магия полнейшая, но всем она нравится.
В JavaScript’е не все браузеры поддерживают геттеры и сеттеры, а во многих проектах нужно еще поддерживать IE8, поэтому в Jii они реализованы в виде методов get ('user') и set ('user', 'Ivan'). Такой подход можно встретить во многих библиотеках, например в Backbone.
В будущем, когда необходимость в старых браузерах отпадет — то настоящие геттеры и сеттеры добавятся паралельно с get/set методами, которые будут через них и работать.

Классы и неймспейсы


Как говорится, каждый уважающий себя программист должен написать свою реализацию классов в JavaScript. Так и тут, для реализации классов используется моя библиотека Neatness, которая уже хорошо показала себя в других внутренних проектах.

Почему не ES6 classes (и не CoffeeScript/TypeScript)?


a98bd16264fc4586b2b6ec477675ba21.jpeg


В комментариях и на гитхабе уже есть небольшой холивар по этому поводу.
Озвучу и здесь причины отсутствия ES6 в Jii:

  1. Для node.js нужен препроцессинг (io.js поддерживает)
  2. В es6 нет неймспейсов, а они нужны для повторения фич Yii. Их можно имплементировать через модули, но это будет костыль. Менять один костыль на другой — нет смысла.
  3. В ES6 захочется использовать геттеры и сеттеры, но они не поддерживаются старыми браузерами, как говорилось выше
  4. Многие разработчики не знают формата es6/coffeescript/typescript и это будет дополнительный порог вхождения
  5. Сложность работы с кодом разных форматов (ES5 и ES6). Не всегда есть возможность в существующем проекте поменять весь код на es6, а наследовать es6 классы все равно нужно, и все равно прийдется использовать Jii.defineClass () для создания классов — костыль не уходит.


Ниже я добавил опрос на эту тему, голосуйте, комментируйте!

Middlewares


В Yii фреймворке компоненты доступны глобально через Yii::$app→… Однако в Jii не все компоненты можно расположить глобально, т.к. некоторые из них (Request, Response, WebUser, Session, …) привязаны к контексту (запросу).
Такие компоненты будут создаваться в контексте (Jii.base.Context) и передаваться в качестве параметра в action — аналогия с передачей request и response в 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();
    }

});


650b3811f5ec46389d3e897fd52f098c.jpg


Jii приложения организованы согласно шаблону проектирования модель-представление-поведение (MVC) и имеют следующие сущности:

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


Примеры их использовать можно увидеть в демо.

Приложения


Приложения это объекты, которые управляют всей структурой и жизненным циклом прикладной системы Jii. Обычно на один воркер (процесс) Node.js приходит один экземпляр приложения Jii, который доступен через Jii.app.
Когда входной скрипт создаёт приложение, он загрузит конфигурацию и применит её к приложению, например:

var Jii = require('jii');

// загрузка конфигурации приложения
var config = {
    application: {
        basePath: __dirname,
        components: {
            http: {
                className: 'Jii.httpServer.HttpServer'
            }
        }
    },
    context: {
        components: {
            request: {
                baseUrl: '/myapp'
            }
        }
    }
};

// создание объекта приложения и его конфигурирование
Jii.createWebApplication(config);


Важное отличие от Yii в том, что конфигурация делится на две части — конфигурация приложения (секция application) и конфигурацию контекста (запроса) (секция context). Конфигурация приложения создаёт и настраивает компоненты, модули и конфигурирует само приложение (Jii.app) — всё это происходит при запуске воркера. В свою очередь, конфигурация контекста создаёт и настраивает компоненты при каждом вызове экшена — http запросе, вызове консольной команды и т.п. Созданные компоненты передаются как первый аргумент в метод экшена.

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

Компоненты приложения


Приложения хранят множество компонентов приложения, которые предоставляют различные средства для работы приложения. Например, компоненты urlManager ответственен за маршрутизацию веб запросов к нужному контроллеру; компонент db предоставляет средства для работы с базой данных; и т. д.

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

Jii.app.ComponentID


Встроенные компоненты приложения


В Jii есть несколько встроенных компонентов приложения, с фиксированными ID и конфигурациями по умолчанию.

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

  • db, Jii.sql.BaseConnection: представляет собой соединение с базой данных, через которое вы можете выполнять запросы. Обратите внимание, что когда вы конфигурируете данный компонент, вы должны указать класс компонента также как и остальные необходимые параметры.
  • urlManager, Jii.urlManager.UrlManager: используется для разбора и создания URL;
  • assetManager, Jii.assets.AssetManager: используется для управления и опубликования ресурсов приложения;
  • view, Jii.view.View: используется для отображения представлений.


Компоненты контекста


Набор компонентов контекста зависит от того, где он применяется. Самый распространённый вариант — это HTTP запрос пользователя, для него мы и рассмотрим встроенный набор компонентов.

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

    actionIndex: function(context) {
        context.ComponentID
    }


Встроенные компоненты запроса


Ниже представлен список встроенных компонентов запроса. Вы можете конфигурировать их также как и другие компоненты в секции context в конфигурационном файле.

  • response, Jii.base.Response: представляет собой данные от сервера, которые будет направлены пользователю;
  • request, Jii.base.Request: представляет собой запрос, полученный от конечных пользователей;
  • user, Jii.user.WebUser: представляет собой информацию аутентифицированного пользователя;


Контроллеры


Контроллеры являются частью MVC архитектуры. Это объекты классов, унаследованных от Jii.base.Controller и отвечающие за обработку запроса и генерирование ответа. В сущности, при обработки запроса HTTP сервером (Jii.httpServer.HttpServer), контроллеры проанализируют входные данные, передадут их
в модели, вставят результаты модели в [представления](structure-views), и в конечном итоге сгенерируют исходящие ответы.

Действия (Actions)


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

Следующий пример показывает post контроллер с двумя действиями: view и 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();
            }
                });
    }

});


В действии view (определенном методом actionView ()), код сначала загружает модель согласно запрошенному ID модели; Если модель успешно загружена, то код отобразит ее с помощью представления под названием view.

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

Маршруты (Routes)


Конечные пользователи обращаются к действиям через так называемые *маршруты*. Маршрут — это строка, состоящая из следующих частей:

  • ID модуля: он существует, только если контроллер принадлежит не приложению, а [модулю](structure-modules);
  • ID контроллера: строка, которая уникально идентифицирует контроллер среди всех других контроллеров одного и того же приложения (или одного и того же модуля, если контроллер принадлежит модулю);
  • ID действия: строка, которая уникально идентифицирует действие среди всех других действия одного и того же контроллера.


Маршруты могут иметь следующий формат:

ControllerID/ActionID


или следующий формат, если контроллер принадлежит модулю:

ModuleID/ControllerID/ActionID


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


Создание действий не представляет сложностей также как и объявление так называемых *методов действий* в классе контроллера. Метод действия это метод, имя которого начинается со слова action. Возвращаемое значение метода действия представляет собой ответные данные, которые будут высланы конечному пользователю. Приведенный ниже код определяет два действия index и 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();
    }

});


Действия-классы


Действия могут определятся в качестве классов, унаследованных от Jii.base.Action или его потомков.

Для использования такого действия, вы должны указать его с помощью переопределения метода Jii.base.Controller.actions () в вашем классе контроллера, следующим образом:

actions: function() {
    return {
        // объявляет "error" действие с помощью названия класса
        error: 'app.actions.ErrorAction',

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


Как вы можете видеть, метод actions () должен вернуть объект, ключами которого являются ID действий, а значениями — соответствующие названия класса действия или [конфигурация](concept-configurations). В отличие от встроенных действий, ID отдельных действий могут содержать произвольные символы, до тех пор пока они определены в методе actions ().

Для создания отдельного действия, вы должны наследоваться от класса Jii.base.Action или его потомков, и реализовать публичный метод run (). Роль метода run () аналогична другим методам действий. Например,

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

});


Модули


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

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


Модуль помещается в директорию, которая называется базовым путем модуля (Jii.base.Module.basePath). Так же как и в директории приложения, в этой директории существуют поддиректории controllers, models, views и другие, в которых размещаются контроллеры, модели, представления и другие элементы. В следующем примере показано примерное содержимое модуля:

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


Классы модулей


Каждый модуль объявляется с помощью уникального класса, который наследуется от Jii.base.Module. Этот класс должен быть помещен в корне базового пути модуля. Во время запуска приложения (воркера) будет
создан один экземпляр соответствующего класса модуля. Как и экземпляры приложения, экземпляры модулей нужны, чтобы код модулей мог получить общий доступ к данным и компонентам.

Приведем пример того, как может выглядеть класс модуля:

/**
 * @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 пространства имен класса модуля. Это также подразумевает, что файлы классов контроллеров должны располагаться в директории controllers базового пути модуля. Например, чтобы описать контроллер post в модуле forum из предыдущего примера, класс контроллера объявляется следующим образом:

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,

    // ...

});


Изменить пространство имен классов контроллеров можно задав свойство Jii.base.Module.controllerNamespace. Если какие-либо контроллеры выпадают из этого пространства имен, доступ к ним можно осуществить, настроив свойство Jii.base.Module.controllerMap, аналогично тому, как это делается в приложении.

Использование модулей


Чтобы задействовать модуль в приложении, достаточно включить его в свойство Jii.base.Application.modules в конфигурации приложения. Следующий код в конфигурации приложения задействует модуль forum:

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


Свойству Jii.base.Application.modules присваивается объект, содержащий конфигурацию модуля. Каждый ключ объекта представляет собой *идентификатор модуля*, который однозначно определяет модуль среди других модулей приложения, а соответствующий объект — это конфигурация для создания модуля.

Доступ к моделям


Доступ к экземпляру модуля можно получить следующим способом:

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


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

var maxPostCount = module.params.maxPostCount;


a0726c01a62a4d43bd0fa41603d8ee48.jpg


Ниже я предлагаю опрос о формате кода Jii. Если выбираете не JavaScript-формат, то укажите в комментариях как бы вы решали проблемы, описанные в разделе «Почему не ES6 classes (и не CoffeeScript/TypeScript)?» в этой статье.
Напомню, Jii — опенсорсный проект, поэтому я буду очень рад, если кто-то присоединится к его разработке. Пишите на affka@affka.ru.

Сайт фреймворка — jiiframework.ru
GitHub — https://github.com/jiisoft
Обсуждение фич проходит на гитхабе

Понравилось? Поставь звезду на гитхабе! :)

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

© Habrahabr.ru