Привет всем хабровчанам, любителям Yii и Node.js. Продолжаю серию статей про Jii, на сей раз настала очередь рассказать о том, что Jii можно использовать в браузере.


Представьте, уже сейчас все структуры фреймворка, такие как приложения, компоненты, контроллеры, модули, модели, представления доступны в браузере!

Для тех, кто в первый раз слышит об этом фреймворке, рекомендую прочитать предыдущие статьи или посетить сайт. Если коротко, то
Jii — это фреймфорк, архитектура и API которого базируется на PHP фреймворке Yii 2.0, взяв из него лучшие стороны и сохраняя приемущества JavaScript.

Jii на клиенте (в браузере)


Jii изначально пишется так, чтобы его можно было использовать везде, где может выполняться JavaScript код. Если код модуля не завязан на серверной инфраструктуре, то его можно использовать в браузере.

Все это разбито и подключается по частям, в видел npm модулей:

  • jii (или jii/deps) — базовые классы и основа фреймворка;
  • jii-clientrouter — Роутер, запускающий действия при изменении адресной строки; Работает совместно с jii-urlmanager;
  • jii-comet — Комет клиент;
  • jii-model — Модель набором валидаторов (которые указываются в rules());
  • jii-urlmanager — Разбор url, получение route на основе указанных правил;
  • jii-view — Рендер представлений.

Создание приложения на клиенте


Создаваться приложение будет почти также как и серверное:
// Libs
require('jii/deps'); // included underscore and underscore.string libraries
require('jii-urlmanager');
require('jii-clientrouter');

// Application
require('./controllers/SiteController');

Jii.createWebApplication({
    application: {
        basePath: location.href,
        components: {
            urlManager: {
                className: 'Jii.urlManager.UrlManager',
                rules: {
                    '': 'site/index'
                }
            },
            router: {
                className: 'Jii.clientRouter.Router'
            }
        }
    }
}).start();

console.log('Index page url: ' + Jii.app.urlManager.createUrl('site/index'));

Зависимости



Jii имеет зависимости от библиотек Underscore и Underscore.string. Если они уже подключены у вас на странице, то нужно подключать Jii как require('jii'), если зависимости нужны — require('jii/deps').

Компиляция JavaScript кода


Jii рекомендует использовать CommonJS подход для подгрузки зависимостей. Сборку модулей можно делать любыми средствами, например, используя Browserify. Рассмотрим простейший пример сборки.
Установка зависимотей:

npm install --save-dev gulp gulp-easy

Файл gulpfile.js:

require('gulp-easy')(require('gulp'))
    .js('sources/index.js', 'bundle.js')

Роутинг на клиенте



Когда необходимо следить и обрабатывать состояние адресной строки браузера, в бой вступает модуль jii-clientrouter, предназначенный именно для этой цели.
Jii-clientrouter устанавливается как компонент приложения и подписывается на событие popstate (или hashchange для браузеров, не поддерживающих HTML5 History API).

При загрузке страницы или изменении адресной строки запускается обработчик, который парсит адресную строку компонентом Jii.urlManager.UrlManager, получает route с параметрами запроса и запускает действие (action), эквивалентное найденному route. Поэтому для работы Jii-clientrouter необходимо так же подключить jii-urlmanager.

Пример конфигурации приложения:

require('jii/deps');
require('jii-urlmanager');
require('jii-clientrouter');

// Application
window.app = Jii.namespace('app');
require('./controllers/SiteController');

Jii.createWebApplication({
    application: {
        basePath: location.href,
        components: {
            urlManager: {
                className: 'Jii.urlManager.UrlManager',
                rules: {
                    '': 'site/index',
                    'article/<id>': 'site/article',
                }
            },
            router: {
                className: 'Jii.clientRouter.Router'
            },

            // ...
        }
    }
}).start();

В действии будут доступны компоненты request (Jii.clientRouter.Request) и response (Jii.clientRouter.Response), как это было при работе на сервере с HTTP сервером. Используя эти компоненты, можно получить параметры из адресой строки. Рассмотрим небольшой пример такого действия.

Предположим, что у нас адресная строка содержить адрес localhost:3000/article/new-features, тогда при переходе на заданный адрес на клиенте сработает обработчик Jii.clientRouter.Router._onRoute(), который найдет роутер site/article и запустит действие (метод) actionArticle() контроллера app.controllers.SiteController:

/**
 * @class app.controllers.SiteController
 * @extends Jii.base.Controller
 */
Jii.defineClass('app.controllers.SiteController', /** @lends app.controllers.SiteController.prototype */{

	__extends: Jii.base.Controller,

	// ...

	actionArticle: function(context) {
	    console.log(context.request.getQueryParams()); // {id: 'new-features'}
	}

});

Демо


Пример использования Jii в браузере можно посмотреть на Github в исходниках примитивного чата — jiisoft/jii-boilerplate-chat.

Не стоит воспринимать данный пример как «лучшие практики» использования Jii, так как эти практики еще не сформировались. Я буду рад услышать рекомендации по оформлению и структуре кода на клиенте.

В заключении



Напомню, Jii — опенсорсный проект, поэтому я буду очень рад, если кто-то присоединится к его разработке. Пишите на affka@affka.ru.

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

Нравится идея фреймворка? Ставь звезду на гитхабе!


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


  1. SerafimArts
    07.10.2015 00:19
    +5

    Предлагаю постараться и перейти на ES6\7 с бабелом в виде компилятора. Всё же он будет в разы ближе к php, нежели текущий ES4\5, да и удобнее — это уж факт. С другой стороны там выхлоп километровый, но со временем он минимизируется, учитывая то, что код на перспективу, а куски ES6 уже в некоторых браузерах реализованы.


    1. affka
      07.10.2015 10:32

      Много обсуждалось уже, много чего лучше стало с выходом nodejs v4, но еще далеко до идеала. Есть стоперы, из-за которых я не хочу переходить на es6:

      • Нужно компилировать под node.js. Компилированный код очень плохо дебажится, если библиотека подключена как зависимость. А я очень часто смотрю в исходники библиотек, чтоб понять что они делают.
      • Отсутствие неймспейсов. Нет возможности сделать ленивую автоподгрузку классов по мотивам PHP, import просто не умеет динамически подгружать модули. Отсюда и невозможность по строке подгрузить класс (в конфигурации Yii обычно указываются классы для создания компонентов/модулей/..)


      Если вы подскажите как можно решить эти проблемы, но я с удовольствием перейду на es6. Но пока внятных рекомендаций я не услышал.


      1. hell0w0rd
        07.10.2015 11:30

        Хоть я и не одобряю как yii, так и вашу поделку, осмелюсь спросить — а чего сложного в дебаге скомпилированного кода из es6 в es5? Есть сорсмэпы, да и babel генерирует понятный, читабельный код. Это вам не coffee.
        Зачем вам в node ленивая подгрузка модулей? В php она нужна, потому что на каждый запрос новая инициализация, а зачем такое вам?


        1. affka
          07.10.2015 11:50
          -4

          А вы подебажьте сперва такой код, тот же babel добавляет кучу мусора (хоть и необходимого), чтобы сделать правильный mapping es6->5, это все сильно ухудшает чтение кода!


          1. hell0w0rd
            07.10.2015 12:00
            +2

            Я пользуюсь babel еще тогда, когда он был в 3 версии и назывался 6to5. Я отлично знаю, как с ним дебажить.
            Покажите мусор, покажите что не работает. Хватит бросаться словами на ветер.


            1. affka
              08.10.2015 11:00
              +1

              В итоге зря меня заминусовали. Пообщавшись с hell0w0rd, выяснил следующее:
              — отладка по es6 коду все же не возможна (именно в формета es6), потому что node запускает только скомпилированный код. И все что остается — добавить babel-runtime, loose mode, и т.п. чтобы сделать более читаемым. Но это не одно и то же!
              — мусор это код, который получается когда вычитаешь из es5 кода es6 (diff). По ссылочке ниже "Вот так правильно" можно легко увидеть что добавляется. Все что добавилось — мусор/шум/необходимость — называйте как угодно, он ухудшает чтение кода.
              — IDE пока еще плохо понимает es6 код (вылечится со временем), а скомпилированный код вообще не понимает! Возьмем за пример модуль postcss, написанный на es6 и компилируемый при публикации в es5. Т.е. когда я делаю npm install postcss — я получаю es5 код. В данном случае по нему работает документация, потому что там добавлена папка d.ts. Но делать такую папку в проектах — это большой труд, не говоря о том, что его нужно поддерживать. А без нее IDE уже ничего не знает.
              В Jii же, использует jsdoc, который писать и поддерживать проще (т.к. jsdoc непосредственно в коде описывается) и IDE его отлично понимает, прекрасно работает навигация по коду и автоподсказки! И вы мне предлагаете перейти на es6, где этого нет!
              — По неймспейсам — да, в какой-то мере это можно заменить модулями, вложенными по папкам. Но их прийдется подключать в каждом файле. В PHP, например, подобным образом прописывается use ...; в начале файла, но он там прописывается автоматом, как только ты прописал new MyClass и нажал Ctrl+Space. Для JS такого нет (хотя есть тикет с этой фичей в idea, может запилят когда-нить). Мое решение с неймспейсами позволяет не подключать все подряд.

              Если кто знает как можно подключить sourcemap для Php/Webstorm, например, чтобы IDE при переходе к определению метода переходила на код es6 — поделитесь решением. Или ссылку на пример дайте, где это реализовано.


              1. hell0w0rd
                08.10.2015 13:28

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

                1. Попробуйте babel-node, там используется библиотека node-source-map-support и сорсмэпы работают.
                2. Чтение кода ухудшает использование ES5, по сравнению с ES2015. Мне почти не приходилось дебажить свой код, все решалось написанием тестов.

                Я пока отвечал вам на все это, пришел к мнению, что вам нужно перейти на typescript. У него существенно лучше поддержка в IDE и редакторах, тк по языкам с типами и детерминированными конструкции для импортов/экспортов функций/классов гораздо проще сделать все удобные штучки для IDE. Лично для меня в этом языке только async/await не хватает.


                1. affka
                  08.10.2015 14:18

                  Здесь написал для того, чтобы минусовщики тоже увидели. Это не ссор, это обсуждение.
                  1. babel-node пробовал, да. Вот только он бесполезен если подключать модуль как внешнюю библиотеку, т.к. там код уже скомпилированный в es5. Или как-то можно хитро прописать переключатель? Хотя в любом случае костыльно… Это еще каждому разработчику пойди объясни все это.
                  2. А написание тестов чем спасает? Вы же в процессе написания тоже дебажите, когда ошибка возникает. От тестов, конечно, зависит, но далеко не всегда по тексту ошибки понятна ее причина.

                  У TypeScript куча других минусов (помимо плюсов), главный из которых — невозможность подключить любую библиотеку, нужно писать адаптер и расписывать типы.


                  1. hell0w0rd
                    08.10.2015 14:27

                    Перечитайте первый пункт моего ответа. node-source-map-support подключить можно и без babel-node.
                    Тесты спасают 90% случаев. Если, конечно, разбивать все на маленькие модули и тестировать все по отдельности. У меня раз в неделю-две возникает необходимость что-то дебажить, в остальном решается либо тестами, либо console.log банально. Для меня это выбор — писать на приятном глазу языке, или расставлять костыли для IDE. Каждому свое.

                    А вы как будто не расписываете типы, чтобы можно было использовать библиотеку с вашей точки зрения (вы написали что без автокомплита не можете программировать)?.. Ну и есть огромный репозиторий .d.ts файлов почти на все популярные библиотеки. Постоянно обновляется и дополняется. Им, кстати, пользуется js плагин в idea.


        1. SerafimArts
          07.10.2015 13:04

          Babel генерирует понятный код в отличие от coffee? Я не ослышался? Феерическая глупость. Берите любой пример, на кофе он будет в разы лаконичнее и понятнее. https://gist.github.com/SerafimArts/9e24db3ad5e71e0b3431


          1. hell0w0rd
            07.10.2015 13:11
            +1

            Феерическая глупость — использовать инструменты без чтения документации. Советую ознакомиться с loose mode, зачем он нужен и почему по дефолту без него.
            Вот так правильно


            1. SerafimArts
              07.10.2015 13:20

              Согласен, он делает код намного более читаемым, моя вина, не знал об опции. Но всё равно выхлоп у кофе будет лакончинее, хоть и не на много, особенно в таких вещах:
              http://babeljs.io/repl/#?experimental=true&evaluate=true&loose=true&spec=false&code=var%20a%20%3D%20[1%2C%202%2C%203]%3B%0A%0Afor%20%28var%20i%20of%20a%29%20{%0A%20%20console.log%28i%29%3B%0A}

              http://coffeescript.org/#try:a%20%3D%20%5B1%2C%202%2C%203%5D%3B%0A%0Afor%20i%20in%20a%0A%20%20console.log%20i

              P.S. Согласен, что у кофе не учитывается Symbol.iterator, что и даёт излишний код в случае babel. Но это не отменяет того, что кофе всегда будет лаконичнее на выходе.


              1. hell0w0rd
                07.10.2015 13:27
                +2

                Ну конечно лаконичнее. Ведь код babel и код coffee делает разные вещи. for-of — синтаксис для обхода итераторов, а for-in в coffee — для массивов. Подучите матчасть по javascript.
                Вот вам примерчик:

                const numbers = new Set([1, 2, 3]);
                for (const num of numbers) {
                  console.log(num);
                }
                


                А вот вам вдогонку пример, как coffee убивает производительность вашего кода:
                // coffee
                foo = (args...) ->
                  console.log(args)
                
                // js
                var foo,
                  slice = [].slice;
                
                foo = function() {
                  var args;
                  args = 1 <= arguments.length ? slice.call(arguments, 0) : [];
                  return console.log(args);
                };
                


                1. SerafimArts
                  07.10.2015 13:39

                  Я так понимаю по примеру, что есть какая-то проблема в slice.call? Т.к. babel возвращает результат в виде цикла по аргументам:

                  "use strict";
                  
                  var foo = function foo() {
                    for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {
                      args[_key] = arguments[_key];
                    }
                  
                    console.log(args);
                  };
                  


                  1. hell0w0rd
                    07.10.2015 13:42
                    +1

                    1. SerafimArts
                      07.10.2015 13:55

                      Из примера я понял, спасибо, что подобный код был специально реализован в виде цикла ради какой-то оптимизации, но что подразумевается под «утечкой аргументов» (leak arguments)? Не все аргументы возвращаются после slice и некоторые «утекают»? Это всё, что могу предположить.


                      1. hell0w0rd
                        07.10.2015 14:08

                        В кратце — использование arguments вне функции (возвращение из функции, передача в качестве параметра, использование в замыкании), все это не дает v8 (и возможно другим движкам) оптимизировать функцию.


                        1. SerafimArts
                          07.10.2015 15:18

                          Большое спасибо за разъяснения и в первую очередь за ссылку выше, много чего интересного.


      1. SerafimArts
        07.10.2015 11:36

        1) Source maps. Не знаю как в ноде, но в браузере они показывают проблемы в исходном файле, а не скомпилированном. Т.е. выхлоп смотреть не нужно, слава всевышнему.
        2) Автолоада нет, но вполне возможно написать массив из нужных путей и сделать его подгрузку:

        var classes = ['Some/Any'];
        ...
        classes.forEach(name => {
            var cls = require(name); // В случае, когда класс указан как `export default class`
            var instance = new cls;
        });
        


        1. affka
          07.10.2015 11:53

          1. А мне вот как раз для node.js и интересно. Плюс такого точно не будет для отладки в режиме реального времени, когда хочется посмотреть какие данные пришли в точку останова.
          2. Тоже костыль еще тот, некоторые люди заботятся о потребляемой памяти (особенно когда есть только shared хостинг), и такие люди уже обращались с замечаниями, что они хотят подключать только то, что будет использоваться — github.com/jiisoft/jii/issues/16


          1. hell0w0rd
            07.10.2015 12:11
            +1

            Это не забота о потребляемой памяти, это техническая неграмотность. Любой node сервер работает в асинхронном режиме, а require — синхронная функция. То есть так или иначе в какой-то момент весь ваш код будет загружен в память, и именно в момент подгрузки ваш асинхронный код залочится, а для кого-то это может быть критично, а не пара мегабайт байткода.


      1. hell0w0rd
        07.10.2015 11:38
        +2

        Ах да, и неймспейсы не нужны.

        const EventListener = require('react/lib/EventListener');
        

        У вас есть файловая система и common.js/es2015 modules.


    1. SerafimArts
      07.10.2015 13:26

      Добавлю, что ES7 я не просто так упомянул. В нём есть безумно крутая штука, называемая декораторами, что позволит писать ещё более приближённые к исходнику вещи, например определять абстрактные методы: https://gist.github.com/SerafimArts/c418e63a2a5ce0be75e5


  1. NeonXP
    07.10.2015 10:48
    +1

    Было бы интересно посмотреть на Jymfony 2. Есть смелые и отважные? :)


    1. affka
      07.10.2015 11:06
      +6

      //sarcasm// Каждый должен в постах о Jii предложить сделать Jymfony, Jaravel, Jend, jsNuke, Jitrix, Joomla, Jrupal,… %)


      1. PQR
        07.10.2015 11:54
        +4

        Предлагаю двигаться в обратном направлении: PHPExpress, PHPMeteor…


      1. ange007
        07.10.2015 16:17

        Но ведь Joomla уже есть!
        Так что ждёмс всё остальное.