Skip to content

Latest commit

 

History

History
2322 lines (1911 loc) · 128 KB

mvc-app.md

File metadata and controls

2322 lines (1911 loc) · 128 KB

Используем MVC-концепцию в BEViS-проекте

Мы любим MVC за то, что эта концепция наводит порядок в проекте, сортирует компоненты кода по трём признакам — Модель, Представление и Контроллер. Отличную статью можно прочитать на Вики.

Важно понимать, что в этом документе мы будем говорить о внедрении MVC в клиентские модули. Не в bt-шаблоны и не в серверный js из папки server. Мы внедрим MVC в код, который работает в браузере.

Когда мы говорим "внедрим MVC", это не означает, что мы станем внедрять какие-то одни фрагменты кода внутрь каких-то других фрагментов кода. Нет, конечно. MVC - лишь способ абстрактного отношения к своему коду и способ разделить ответственность, распределить роли между фрагментами кода ради того, чтобы уменьшить связность компонентов кода друг с другом, то есть чтобы изменение одного компонента не приводило к необходимости вносить изменения в работу другого. Для этого все компоненты кода разделяются на три условные группы, на три роли — модель, представление и контроллер. Модель отвечает за данные. Представление отвечает за отображение данных. А контроллер — это мозговой центр программы, связывает модели и представления в единую систему.

Строго говоря, согласиться с предыдущими словами можно только при условии, что мы описываем так называемую пассивную версию MVC, потому что в активной MVC мозговым центром программы является именно модель. Но, как интересно подметили в статье на Вики, веб-разработчики пользуются пассивной MVC, потому что опыт программирования скриптов на PHP испортил нас. Мы будем строить приложение на основе пассивной MVC.

За основу для работы на этом практическом занятии давайте возьмём текущее состояние ветки input-and-form в репозитории bevis-stub и шаг за шагом, с подробными объяснениями, вместе — вы и я — внедрим MVC-концепцию в BEViS-приложение. Это будет не быстрое чтение, поэтому, если вам хочется без объяснений, только голую суть, пожалуйста, она здесь — в "Справочник. MVC в BEViS". А если вам захочется разобраться, что-как-и-зачем, возвращайтесь сюда, здесь интересно.

Что интересного? Я подметил, что концепция MVC, то есть распределение ролей, можно увидеть во многих аспектах обычной жизни.

Возьмём большой магазин, например, с бытовой техникой. Он тоже работает по MVC :)

MVC в магазине :)

Где-то на хозяйственном дворе за магазином есть складские помещения, и там работает складской рабочий. Его задача - получить новый товар от поставщиков. Те на фурах привозят товар, кладовщик его принимает и раскладывает на полках склада. Это одна его функция. Вторая функция кладовщика - отдать товар в магазин, когда из магазина придёт заявка с текстом: "Выдать столько-то микроволновок LG и столько-то телевизиров SONY Bravia."

Кладовщик находит на полках склада нужные микроволновки и телевизиры и передаёт в магазин. Он не знает, кому он отдаёт товар — ему это знать не нужно. Не его это забота.

В то же время в самом магазине работает консультант. Его задача — принять откуда-то взявшиеся микроволновки и телевизоры и выставить их на полки магазина. Выставить аккуратно, развесить ценники, наклеить рекламки про скидки (консультант всё это знает, у него такая ответственность). Это одна из его функций. Вторая функция консультанта — общаться с покупателями. Слушать и отвечать на вопросы, взаимодействовать любыми способами, лишь бы товар купили.

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

Кладовщик ведает только задачами склада. Консультант занимается только представлением товара на витринах и общением с покупателями. Эти два специалиста вообще не пересекаются друг с другом и друг от друга не зависят. Так эффективнее.

И тут возникает ещё один сотрудник магазина, который должен организовать работу кладовщика и консультанта. Назовём его менеджером. Именно он опрашивает консультантов: "Сколько каких товаров нужно выставить на витрины", и пишет заявку кладовщику: "Выдать столько-то микроволновок LG и столько-то телевизиров SONY Bravia." Именно он переправляет этот товар со склада в помещение магазина (не сам, конечно, но процесс организовывает именно он).

Менеджер — мозговой центр, который организует потоки товара и связывает все другие роли в магазине в единое целое.

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

Аналогично магазину, в программирование привнесли те же три роли.

Кладовщик - это Модель (Model)

  • Кладовщик получает данные от поставщиков, а Модель получает данные из бекенда — из базы данных или из других сервисов, обращаясь к ним по HTTP.

  • Кладовщик расставляет товар по полкам склада, а Модель сохраняет данные в каких-то внутренних объектах или массивах.

  • Кладовщик по заявке отпускает товар, а Модель отдаёт данные из своего хранилища. Как кладовщику, так и модели без разницы, кто запросил данные — отдал и всё.

Консультант - это Представление (View)

  • Консультант расставляет товар в витринах, чтобы товар было хорошо видно. Представление отображает данные на веб-странице, навешивает стили, анимирует — отображает данные привлекательно. Представление не должно знать, откуда
    пришли данные. Ему это знать не за чем.

  • Консультант слоняется между витринами в магазине и ждёт, пока потенциальный покупатель задаст ему вопрос о микроволновке. Тогда Консультант покажет микроволновку со всех сторон, расскажет о ней. Представление точно так же слушает пользовательские события на веб-странице (клики там, или чейнджи в инпутах) и как-то реагирует на эти события. Например, по клику в инпут в обработчике клика View инпута сделает вокруг текстового поля жёлтую фокусную рамку — поменяет своё же представление.

  • Когда Консультанта просят продать эту микроволновку с витрины, он относит её на кассу (менеджеру) и сообщает там, что товар хотят купить. Когда пользователь ввёл логин и пароль в форму авторизации и нажал Enter, Представление формы читает тексты из полей логина и пароля и передаёт их Контроллеру. И больше вью формы ничего не делает — только собирает данные от пользователя и отдаёт контроллеру.

Менеджер - это Контроллер (Controller)

  • Как Менеджер организует работу в магазине, так Контроллер организует работу web-приложения. Именно в файле Контроллера начинается работа приложения. Имено здесь мы управляем, из каких Моделей нужно запросить данные. Именно здесь мы полученные данные перенаправляем в Представления. Именно здесь мы получаем сообщения от View, что пользователь ввёл "такой-то текст" в "таком-то" View и нажал сабмит. И делаем то, что нужно — отправляем этот текст в модель, чтобы передать данные в бекенд или ещё что-то, не знаю что.

Так устроено javascript-приложение на MVC. Всё три роли общаются друг с другом с помощью кастомных событий, мы о них немного уже говорили в yblock.md.

Ещё мы с вами создавали три компонента — Input, SuperInput и Form. Если посмотреть на них через призму MVC, окажется, что они и есть Представления. То есть, до сих пор в практикуме мы создавали только модули-представления и как-то их друг с другом связывали. Всё это было вью. Конечно, мы сейчас создадим и контроллеры и модели, но сначала нужно подготовить файловую систему — чтобы модели, контроллеры и вью жили каждый в своем доме. Кладовщик на склад, а консультант в салон магазина. Поехали.

Файловая организация для MVC

До сих пор мы разрабатывали проект, файловая структура которого выглядела так:

/blocks
/configs
/core
/pages
/server

Я опустил служебные папки, оставил только те, которые нужны для объяснения.

Это странная файловая структура — на одном уровне мы сложили совершенно разноуровневные директории. Например, server и blocks — почему они рядом? Первая содержит код для создания Node.js-приложения, а вторая содержит наши формочки и инпуты. Совершенно разный масштаб, разная важность, разная предметная область. Непонятно. Неправильно как-то. Будем исправлять :)

MVC - это же javascript-приложение? Оно имеет и серверный код и клиентский код? Кажется логичным в корне проекта сделать две схожие по назначению директории: server и client. В первой будет всё, что относится к Node.js (к серверной части приложения), во второй - к клиентской. Например, blocks, core, pages — это же всё имеет отношение к фронтенду — блоки, страницы. Пусть живут в client:

/client
    /blocks
    /core
    /pages
/configs
/server

Вы можете переделывать файловую структуру со мной вместе. А можете посмотреть как это реализовано в отдельной ветке, где я переформатировал bevis-stub под MVC.

Пути к директориям с исходниками

Так как мы перенесли директорию pages в другое место, сборщику проекта нужно об этом сообщить. Он-то поди ещё не знает, чтобы папка уехала и будет пытаться найти её по прежнему адресу. Настройка сборки проекта находится в файле .enb/make.js

var fs = require('fs');
var path = require('path');

module.exports = function (config) {

    config.setLanguages(['ru', 'en']);

    config.includeConfig('enb-bevis-helper');

    var bevisHelper = config.module('enb-bevis-helper')
        .browserSupport([
            'IE >= 9',
            'Safari >= 5',
            'Chrome >= 33',
            'Opera >= 12.16',
            'Firefox >= 28'
        ])
        .useAutopolyfiller();

    fs.readdirSync('pages').forEach(function(pageName) {
    //                ^------------------------------------------------------------ Здесь! 
        var nodeName = pageName.replace(/(.*?)\-page/, path.join('build', '$1'));

        config.node(nodeName, function (nodeConfig) {

            bevisHelper
                .sourceDeps(pageName)
                .forServerPage()
                .configureNode(nodeConfig);

            nodeConfig.addTech(require('./techs/page'));
            nodeConfig.addTarget('?.page.js');
        });

    });

};

Эту строку нужно заменить на:

    fs.readdirSync('client/pages').forEach(function(pageName) {
    //                ^------------------------------------------------------------ Добавили client/ 

В этом файле мы сделали всё, что нужно. Если код файла вам не очень понятен, да и бог с ним, сейчас это не имеет значения. Главное — теперь сборщик будет искать страницы в папке client/pages.

А как сборщик понимает, откуда подтягивать ресурсы на эти страницы — все эти блоки, формочки и инпуты, все эти i18n и y-block?

Вот, в самом деле. Мы на прошлых занятиях активно писали файлы с зависимостями — всякие там *.deps.yaml, и говорили, что, мол, сборщик читает в этих файлах имена блоков и подтягивает их с файловой системы. Но мы не обсуждали, отуда сборщик знает, в каких именно директориях на файловой системе искать те блоки? Ведь, не обходит же сборщик все директории проекта в поисках файлика form.js, если в *.deps.yaml описана такая зависимость?

- block: form

Нет, конечно же нет. Сборщик не обходит всё дерево. Информацию о директориях с исходниками предоставляем сборщику мы - разработчики проекта. Мы это делаем в файле package.json. Вот эта информация:

"enb": {
    "sources": [
        "blocks",
        "core",
        "pages"
    ]
}

Я привёл не весь файл, а только фрагмент (по ссылке можно увидеть всеь). Но в этом коротком фрагменте описана вся инструкция сборщику, где он должен искать исходники — только в папках blocks, core, pages.

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

"enb": {
    "sources": [
        "client/blocks",
        "client/core",
        "client/pages"
    ]
}

Перезапускаем проект командой make. Если видим что-то подобное, значит всё стартовало успешно:

~/bevis-stub/node_modules/.bin/enb make
17:40:45.968 - build started
17:40:47.267 - [rebuild] [build/index/index.sources] sources
17:40:47.269 - [rebuild] [build/test/test.sources] sources
17:40:47.275 - [isValid] [build/index/index.dest-deps.js] deps
17:40:47.276 - [isValid] [build/test/test.dest-deps.js] deps
17:40:47.278 - [rebuild] [build/index/index.files] files
17:40:47.278 - [rebuild] [build/index/index.dirs] files
17:40:47.278 - [rebuild] [build/test/test.files] files
17:40:47.279 - [rebuild] [build/test/test.dirs] files
17:40:47.280 - [isValid] [build/index/index.bt.js] bt-server
17:40:47.281 - [isValid] [build/index/index.page.js] page-js
17:40:47.281 - [isValid] [build/index/index.css] css-stylus-with-autoprefixer
17:40:47.282 - [isValid] [build/index/index.lang.ru.js] y-i18n-lang-js
17:40:47.283 - [isValid] [build/index/index.lang.en.js] y-i18n-lang-js
17:40:47.284 - [isValid] [build/test/test.bt.js] bt-server
17:40:47.284 - [isValid] [build/test/test.page.js] page-js
17:40:47.285 - [isValid] [build/test/test.css] css-stylus-with-autoprefixer
17:40:47.285 - [isValid] [build/test/test.lang.ru.js] y-i18n-lang-js
17:40:47.285 - [isValid] [build/test/test.lang.en.js] y-i18n-lang-js
17:40:47.286 - [isValid] [build/index/index.bt.client.js] bt-client-module
17:40:47.286 - [isValid] [build/test/test.bt.client.js] bt-client-module
17:40:47.293 - [isValid] [build/index/index.source.ru.js] js
17:40:47.294 - [isValid] [build/index/index.source.en.js] js
17:40:47.295 - [isValid] [build/test/test.source.ru.js] js
17:40:47.296 - [isValid] [build/test/test.source.en.js] js
17:40:47.332 - [rebuild] [build/index/_index.css] file-copy
17:40:47.332 - [rebuild] [build/index/_index.lang.ru.js] file-copy
17:40:47.332 - [rebuild] [build/index/_index.lang.en.js] file-copy
17:40:47.332 - [rebuild] [build/test/_test.css] file-copy
17:40:47.332 - [rebuild] [build/test/_test.lang.ru.js] file-copy
17:40:47.333 - [rebuild] [build/test/_test.lang.en.js] file-copy
17:40:47.333 - [isValid] [build/index/index.modernizr.en.js] modernizr-js
17:40:47.333 - [isValid] [build/index/index.modernizr.ru.js] modernizr-js
17:40:47.334 - [isValid] [build/test/test.modernizr.ru.js] modernizr-js
17:40:47.334 - [isValid] [build/test/test.modernizr.en.js] modernizr-js
17:40:47.372 - [isValid] [build/index/index.en.js] autopolyfiller
17:40:47.373 - [isValid] [build/index/index.ru.js] autopolyfiller
17:40:47.375 - [isValid] [build/test/test.ru.js] autopolyfiller
17:40:47.375 - [isValid] [build/test/test.en.js] autopolyfiller
17:40:47.403 - [rebuild] [build/index/_index.ru.js] file-copy
17:40:47.403 - [rebuild] [build/index/_index.en.js] file-copy
17:40:47.404 - [rebuild] [build/test/_test.en.js] file-copy
17:40:47.404 - [rebuild] [build/test/_test.ru.js] file-copy
17:40:47.405 - build finished - 1437ms

DEBUG: Running node-supervisor with
DEBUG:   program 'server/boot.js'
DEBUG:   --watch 'server,configs'
DEBUG:   --ignore 'undefined'
DEBUG:   --extensions 'node|js'
DEBUG:   --exec 'node'

DEBUG: Starting child process with 'node server/boot.js'
DEBUG: Watching directory '~/bevis-stub/server' for changes.
DEBUG: Watching directory '~/bevis-stub/configs' for changes.
17:40:48 - info: worker 15078 started
17:40:48 - info: worker 15079 started
17:40:48 - info: app started on 8080
17:40:48 - info: app started on 8080

Проверяем в браузере:

localhost:8080 - работает.

localhost:8080/test - работает.

Отлично, не сломалось. Поехали дальше.

View

Выше мы уже говорили, что блоки, которые лежат в папке blocks выполняют роль представления. Поэтому переименуем директорию и дадим ей имя views.

/client
    /core
    /pages
    /views   <----- теперь здесь хранятся все представления проекта
/configs
/server

Не забываем заменить путь к ней в package.json

"enb": {
    "sources": [
        "client/views", // <------------ Было client/blocks
        "client/core",
        "client/pages"
    ]
}

Всё, вьюхи готовы. Мы их давно написали. Они всё умеют — умеют принимать данные неведомо откуда. Умеют отображать их в виде HTML-тегов. Умеют слушать на себе клики и всякие другие действия пользователя. Самые настоящие вью. А больше тут делать и нечего.

Давайте займёмся главной частью приложения — контроллером.

Application Controller

Контроллер - это логика приложения. В нём начинается сеанс работы приложения, в нём раздаются указания вьюхам, моделям (и другим контроллерам по-меньше) — указания о том, как-кому работать, какими данными обмениваться и вообще всё-всё. Это мозг приложения. Создадим самый главный контроллер. Сделаем так, чтобы именно с него начиналась работа приложения.

Ведь сейчас как стартует приложение? Помните этот файл core/block/block__auto-init.js?

modules.require(['block'], function (Block) {
    Block.initDomTree(window.document).done();
});

Помните, что это такое? Это js-модуль, который не объявляется, нет. Он исполняется! Сразу, как только модульная система его увидела. Исполняется потому, что в нём написано не modules.define(), как в других модулях, а именно modules.require().

То есть он "реквайрит" (запрашивает, требует, ждёт) массив из двух модулей — ['jquery', 'block'] и как только те загрузились, исполняет функцию, переданную вторым аргументом. А там сказано — проинициализируй все блоки, которые есть в window.document.

Это и есть контроллер! Он, конечно, какой-то хилый и беспомощный, ничего не делает, да и не знает он ничего про наше приложение. Но это же точка входа в программу? Это то самое место, где начинается работа клиентского приложения? Значит можно относиться к нему, как к контроллеру.

Напомню вам и себе, чтобы он попал в приложение, мы когда-то объявляли зависимость от него. В файле client/pages/test-page/test-page.deps.yaml:

- page
- block: block
  elem: auto-init # <------------- Вот как мы позвали этот модуль.
- input
- super-input
- form

По факту, это главный контроллер приложения. А если это так, почему бы не дать ему соответствующее имя app-controller.js и не положить его в соответсвующую директорию client/controllers/app-controller? Создадим эти папки и переместим туда файлы под новыми именами. А заодно удалим ставшую теперь ненужной директорию client/core/block/__auto-init:

mkdir -p client/controllers/app-controller
mv client/core/block/__auto-init/block__auto-init.js client/controllers/app-controller.js 
mv client/core/block/__auto-init/block__auto-init.deps.yaml client/controllers/app-controller.deps.yaml
rm -rf client/core/block/__auto-init 

Обратите внимание, удалить нужно именно client/core/block/__auto-init, а не папку client/core/block. Не ошибитесь случайно ;)

Теперь наша файловая система обогатилась новой директорий:

/client
    /controllers
        /app-controller  <----- Мы создали здесь контроллер приложения
    /core
    /pages
    /views   
/configs
/server

Сразу сообщим сборщику, что наши ресурсы теперь будут жить и в этой папке тоже. Открываем package.json и дописываем:

"enb": {
    "sources": [
        "client/controllers", // <------ Здесь!
        "client/views", 
        "client/core",
        "client/pages"
    ]
}

А в файле client/pages/test-page/test-page.deps.js укажем, что страница зависит от нашего нового контроллера (вместо старого).

Было:

- page
- block: block
  elem: auto-init # <----------- Добавили
- input
- super-input
- form

Стало

- page
- app-controller # <----------- Добавили
- input
- super-input
- form

Если теперь обновить в браузере localhost:8080/test, ничего не должно измениться - страница должна работать как и
прежде. У меня работает, а у вас?

Отлично. Теперь посмотрим на контроллер приложения внимательно:

modules.require(['block'], function (Block) {
    Block.initDomTree(window.document).done();
});

От контроллера приложения мы предлагаем многого не требовать. Он стартует приложение и хватит с него. Дальше он должен передать управление программой кому-то другому - более специализированному контроллеру. Какому?

Мы с вами делаем сайты. Сайты, как ни крути, состоят из web-страниц. Какой-то сайт состоит из множества страниц, а какой-то из возможного минимума, т.е. из одной (взять то же SPA-приложение — оно обязательно имеет одну страницу . В противном случае вряд ли оно называлось бы Single Page Application). То есть главной верховной сущностью в приложении всё равно, как ни крути, будет страница:

Приложение
    ^
    |
    |
Страница
    ^
    |
    |
Компоненты 
на странице

Или несколько страниц:

                    Приложение
                        ^
                        |
                        |
Страница 1          Страница 2          Страница 3
    ^                   ^                   ^
    |                   |                   |
    |                   |                   |
Компоненты          Компоненты          Компоненты 
на странице1        на странице2        на странице3

Если думать о контроллере приложения и разглядывать эти схемы, может прийти мысль, что неправильно было бы писать логику работы страниц в контроллере приложения. Это что же, нам логику работы всех трёх страниц описать в одном app-controller.js что ли!? На что станет похож этот контроллер - на кучу условных операторов?

if (page === 'страница1') { 
    // Здесь логика работы страницы 1
} else if (page === 'страница2') {
    // Здесь логика работы страницы 2
} else {
    // ...
}

Нет-нет, вместо ожидаемого от MVC порядка мы получим новый ужас в виде спагетти-кода, который ни поддерживать, ни расширять. Даже открыть почитать его будет страшно. Нет-нет. Глядя на эти схемы приходит мысль, что у каждой страницы должен быть свой собственный контроллер, в которм будет описана функциональность только для этой конкретной страницы.

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

  1. Стартовать приложение. И в коде выше он уже успешно с ней справляется.

  2. Передать управление в страничный контроллер.

И именно второй задачей мы сейчас займёмся.

Вызов Page Controller

Мы ещё не создавали страничный контроллер, но представим, что он уже есть. Как мы передадим ему управление из контроллера приложения? Вот так:

/**
 * Application start
 */
modules.require(['block', 'page-controller'], function (Block, PageController) {
    Block.initDomTree(document.body).done(function () {
        var pageController = new PageController();
        pageController.start();
    });
});

Я добавил зависимость от модуля page-controller, потом описал его имя PageController в параметрах анонимной функции. Такого модуля мы ещё нет, пока всё только понарошку. А вот дальше интересно.

Внутри функции done() я описал анонимную функцию (вы знаете, такую ещё называют коллбеком), которая вызовется, когда предыдущий метод в цепочке вызовов разрезолвит промис.

"Ну, полная бессмыслица", — подумал бы я, прочитав последнее предложение, если бы не знал, что такое промисы.

По-простому это объяснить можно так. У объекта Block мы вызываем метод initDomTree. Когда этот метод проинициализирует все блоки в DOM-дереве, вызовется следующий метод в цепочке, то есть метод done, который в свою очередь вызовет ту самую анонимную функцию-коллбек. А та функция выполнит операторы, которые в ней описаны. Так, кажется, понятнее :)

Главное, что после старта приложения, контроллер приложения выполнит эти две строки:

        var pageController = new PageController();
        pageController.start();

В первой строке мы создаём экземпляр контроллера страницы PageController. Его ещё нет в природе, но мы же проектируем, как нам хотелось бы, чтобы было.

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

Всё, на этом работу контроллера приложения app-controller.js можно читать выполненной. Он стартовал и передал бразды правления в страничный контроллер page-controller.js.

Осталось только указать, что app-controller зависит от page-controller. Редактируем
client/controllers/app-controller/app-controller.deps.yaml:

- block
- page-controller # <---- Добавили

И теперь займёмся PageController — создадим его.

Page Controller

У нас в проекте bevis-stub прямо сейчас есть две страницы:

  • Первая — client/pages/index-page. Отвечает в браузере на запрос localhost:8080
  • Вторая — client/pages/test-page. Отвечает на localhost:8080/test

Выше мы поняли, что у каждой страницы должен быть свой собственный PageController. Где их создать, чтобы они друг другу не мешали? Нам кажется, что так будет удобно:

/client
    /controllers
        /app-controller <------------------ Контроллер приложения
    /core
    /pages
        /index-page
            /controllers
                page-controller.js <------- Контроллер страницы index
        /test-page
            /controllers
                page-controller.js <------- Контроллер страницы test
    /views   
/configs
/server

То есть, в директории каждой страницы мы создадим поддиректорию controllers и сложим в неё только те контроллеры, которые будут работать на этой странице.

Я не приготовил никакого удобного инструмента для быстрого создания контроллеров. Я предлагаю вам сейчас создать папку controllers и вложенный в неё файл page-controller.js самостоятельно.

# Создаём директории для страничных контроллеров
mkdir client/pages/index-page/controllers
mkdir client/pages/test-page/controllers

# Создаём пустые файлы страничных контроллеров
echo '' > client/pages/index-page/controllers/page-controller.js
echo '' > client/pages/test-page/controllers/page-controller.js

Теперь надо сделать так, чтобы при сборке страницы index-page сборщик искал page-controller именно в папке pages/index-page/controller, а при сборке test-page — именно в папке pages/test-page/controller. То есть, указать сборщику, где искать ресурсы.

Мы такое уже делали в package.json, только для проекта целиком:

"enb": {
    "sources": [
        "client/controllers",
        "client/views",
        "client/core",
        "client/pages"
    ]
}

Укажем теперь ресурсы для отдельных страниц. И обязательно удалить путь к client/pages:

"enb": {
    "sources": [
        "client/controllers",
        "client/views",
        "client/core",
        // "client/pages" // <-------- Это путь удаляем. Ниже описываем для каждой страницы свой собственный путь.   
    ],
    "profiles": {                     // <-------- Секция с профилями отдельных страниц
    
        "index-page": {               // <-------- Для страницы index 
            "sources": [
                "client/pages/index-page"
            ]
        },
        
        "test-page": {                // <-------- Для страницы test
            "sources": [
                "client/pages/test-page"
            ]
        }
        
    }
}

Вы, конечно, уже обратили внимание, что я указал путь к директории client/pages/test-page, а не к конкретной
директории с контроллерами client/pages/test-page/controllers. Зачем так? А чтобы больше не лазить в этот файл. Надоело :)

Например, когда настанет день, и мы поймём, что некоторый блок, например та же форма Form, используется только на странице test-page, мы можем захотеть перенести её в директорию client/pages/test-page/views/, предварительно такую создав, конечно. То есть, настанет день, когда мы не только контроллеры разделим между страницами, а ещё и представления — те представления, что используются на разных страницах, останутся в client/views, а те, что используются только на конкретной странице, мы перенесем их в client/pages/<page-name>/views.

И когда этот день настанет, нам уже не придётся снова редактировать package.json, потому что мы только что указали в нём, что для страницы test-page все ресурсы нужно искать в директории client/pages/test-page и вложенных в неё поддиректориях:

"test-page": {                
    "sources": [
        "client/pages/test-page"  // <-------- Здесь!
    ]
}

И ещё один важный шаг.

Мы с вами сообщили сборщику, где искать ресурсы для страниц. Но ещё не сказали, что он вообще должен искать их. :)

Откройте файл .enb/make.js - главный конфиг сборщика, и добвьте одну строку:

var fs = require('fs');
var path = require('path');

module.exports = function (config) {

    config.setLanguages(['ru', 'en']);

    config.includeConfig('enb-bevis-helper');

    var bevisHelper = config.module('enb-bevis-helper')
        .browserSupport([
            'IE >= 9',
            'Safari >= 5',
            'Chrome >= 33',
            'Opera >= 12.16',
            'Firefox >= 28'
        ])
        .useAutopolyfiller();

    fs.readdirSync('client/pages').forEach(function(pageName) {
        var nodeName = pageName.replace(/(.*?)\-page/, path.join('build', '$1'));

        config.node(nodeName, function (nodeConfig) {

            bevisHelper
                .sourceDeps(pageName)
                .sources({profile: pageName}) // <------------------ Эту строку!
                .forServerPage()
                .configureNode(nodeConfig);

            nodeConfig.addTech(require('./techs/page'));
            nodeConfig.addTarget('?.page.js');
        });

    });

};

Именно с помощью этой строки мы говорим сборщику, что для каждой страницы ты обязан искать ресурсы ещё и в профиле с именем страницы. В это можно не сильно вдумываться. У нас есть отдельное занятие про настройку сборки проекта, где вы всё-всё поймете, там несложно. А пока думать об этом не надо, достаточно добавить эту строку.

Теперь, наконец, напишем сам контроллер.

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

Пишем контроллер для index-страницы: client/pages/index-page/controllers/page-controller.js

modules.define(
    'page-controller',
    [
        'inherit'
    ],
    function (
        provide,
        inherit
    ) {

    var PageController = inherit({
        __constructor: function () {
            console.log('index: PageController constructor');
            //            ^-------------------------------------- index-page
        },

        start: function () {
            console.log('index: PageController started');
            //            ^-------------------------------------- index-page!
        }
    });

    provide(PageController);
});

Пишем контроллер для test-страницы: client/pages/test-page/controllers/page-controller.js

modules.define(
    'page-controller',
    [
        'inherit'
    ],
    function (
        provide,
        inherit
    ) {

    var PageController = inherit({
        __constructor: function () {
            console.log('test: PageController constructor');
            //            ^-------------------------------------- test-page!
        },

        start: function () {
            console.log('test: PageController started');
            //            ^-------------------------------------- test-page!
        }
    });

    provide(PageController);
});

Они совершенно идентичны. Отличия лишь в комментариях. Мы добавил коментарии, чтобы убедиться, что смогли подключить контроллеры каждый на свою страницу.

По идее, сейчас всё должно получиться так:

  • Когда я обновлю в браузере страницу localhost:8080, отобразится страница index-page.
  • На ней сначала стартует контроллер app-controller и передаст управление в page-controller.
  • А так как в package.json написано "для index-page ищи ресурсы в client/pages/index-page", то модуль page-controller будет искаться именно там.
  • А значит в консоли браузера мы увидим два сообщения (из конструктора PageController и его метода start):
index: PageController constructor
index: PageController started

А когда я обновлю страницу localhost:8080/test в консоли я должен увидеть другие два сообщения - из другого PageController:

test: PageController constructor
test: PageController started

Проверяем... У меня успех наполовину :)

На странице localhost:8080/test всё, как ожидалось - сообщения вижу. А на localhost:8080 сообщений нет. Думаем, почему так. Думаем-думаем-думаем. Зависимость? Ну, конечно!

Смотрите в client/pages/index-page/index-page.deps.yaml. В этом файле описаны все компоненты, которые участвуют в создании это страницы. Здесь же нет ничего про app-controller. Добавляем:

- page
- app-controller # <---------- Добавили
- block
- sidebar
- layout

Обновлям страницу localhost:8080, ура! Я вижу сообщения

index: PageController constructor
index: PageController started

Всё, мы создали страничные контроллеры.

Это было скучновато. Не печальтесь, делать всё это из проекта в проект не надо, есть готовая болванка для создания проекта на BEViS-MVC, только бери и создавай проект. И есть "Справочник. MVC в BEViS" на тему, как устроено BEViS-MVC-приложение, где всё то же самое, что здесь, только сухо, быстро, без объяснений, одни факты. А здесь так подробно и обстоятельно для тех, кто хочет понять почему в BEViS-MVC что-то сделано так, а не иначе.

Связываем Page Controller и Views

Пора написать пусть маленькое, но приложение. Нафантазируем, как оно могло бы работать.

  1. Во-первых, мы хотим, чтобы приложение отвечало по адресу localhost:8080. Не будем больше пользоваться тестовой страницей /test, она своё отслужила. Хотим Single Page Application

  2. Пусть при загрузке страницы приложение проверяет, залогинен ли пользователь — читает эти данные из бекенда.

  3. Если залогинен - пусть сразу покажет основной контентный блок информации — список ссылок на репозиторий Бивиса.

  4. Если не залогинен - пусть покажет форму авторизации и ждёт, пока пользователь залогинится.

  5. Когда пользователь введет данные и пароль, передать их в бекенд и ждать ответ от бекенда.

  6. Если бекенд ответил, что сумел сохранить данные, перейти к п.№3 и выполнить его без перезагрузки страницы.

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

  1. Когда контроллер стартует, он спрашивает у Модели, авторизован ли пользователь.
  2. Модель читает в бекенде такую информацию (пока не важно где) и отдаёт её контроллеру.
  3. Если пользователь авторизован, контроллер ренедрит вью с контентным блоком.
  4. Если пользователь не авторизован:
  • Контроллер ренедрит вью с формой авторизации
  • Контроллер подписывается на событие от формы типа "пользователь попытался авторизоваться с помощью формы"
  • Когда событие произошло, контроллер передаёт в Модель данные пользователя, чтобы ты сохранила их в бекенде
  • Если данные сохранены в бекенде успешно, Модель сообщает в контроллер об этом
  • Контроллер рестартует сам себя, чтобы попасть на пункт №1

Может показаться, что задача перед нами сложная. Это не так. Задача простая, проще некуда. Сейчас увидите.

Шаг 1.

Начнём с поверхностого анализа алгоритма. Если алгоритм окинуть взглядом, не заостряя внимание на деталях, можно выделить такую крупноузловую работу:

  1. Когда контроллер стартует, он должен узнать, авторизован ли пользователь.
  2. Если авторизован, контроллер ренедрит вью с контентным блоком.
  3. Если не авторизован, контроллер ренедрит вью с формой авторизации.

Я предлагаю начать набрасывать программу от крупных фрагментов, постепенно переходя к мелких уточнениям. Общая картина прямо укладывается в простой if-else оператор. Напишем?

client/pages/index-page/controllers/page-controller.js

modules.define(
    'page-controller',
    [
        'inherit'
    ],
    function (
        provide,
        inherit
    ) {

    var PageController = inherit({
        __constructor: function () {
            console.log('index: PageController constructor');
        },

        start: function () {
            // 1. Когда контроллер стартовал, он узнаёт, авторизован ли пользователь.
            var isAuthorized = true;
            
            if (isAuthorized) {
                // 2. Если авторизован, контроллер ренедрит вью с контентным блоком.
            } else {
                // 3. Если не авторизован, контроллер ренедрит вью с формой авторизации.
            }
        }
    });

    provide(PageController);
});

Шаг 2.

Я бы пока не писал функциональность для пункта №1. Временно захардкодим в переменной isAuthorized значение true. Это та самая деталь, на которую внимание заострять пока не будем. Другими словами, рассмотрим случай, когда пользователь уже авторизован. Как в этом случае работает программа?

Так как isAuthorized я установил в true, то в if-else выполнится ветка if. Нужно инициализировать вью с контентным блоком. Контентным блоком я называю блок sidebar, его файлы находятся в client/views/sidebar. Мы давным-давно учились подключать его на страницу прямо из страничного btjson, помните?

client/pages/index-page/index-page.page.js

module.exports = function (pages) {
    pages.declare('index-page', function (params) {
        var options = params.options;
        return {
            block: 'page',
            title: 'Index page',
            styles: [
                {url: options.assetsPath + '.css'}
            ],
            scripts: [
                {url: options.assetsPath + '.' + params.lang + '.js'}
            ],
            body: [
                {
                    block: 'layout',
                    aside: {
                        block: 'sidebar',   // <--- Вот это view

                        title: 'Привет, BEViS!',
                        resources: [
                            {
                                text: 'Репозиторий',
                                url: 'https://github.com/bevis-ui/'
                            },
                            {
                                text: 'Учебник для новичков',
                                url: 'https://github.com/bevis-ui/docs/blob/master/manual-for-beginner.md'
                            },
                            {
                                text: 'Учебник для старичков',
                                url: 'https://github.com/bevis-ui/docs/blob/master/manual-for-master.md'
                            }
                        ]
                    }
                }
            ]
        };
    });
};

Благодаря тому, что блок sidebar описан здесь, в страничном btjson, он будет превращён в HTML ещё на сервере. И в браузер пользователя прилетит уже в виде готового HTML.

Но мы хотим не этого. Мы хотим строить весь HTML для страницы уже в браузере! Чтобы оперативно рендерить то один блок, то другой, в зависимости от программной логики контроллера. Чтобы всё происходило в браузере!

Для этого нам достачтно, чтобы когда пользователь запросил сайт по адресу localhost:8080, сервер сгенерил только пустую HTML-страницу с тегами head и body и передал в браузер. Там же будут указаны, какие скрипты и стили грузить на страницу. А когда файл скрипта (который будет содержать код всех вьюх, моделей и страничного контроллера) подгрузится, в нём запустится страничный контроллер, который и создаст нужный блок и внедрит его в дерево документа.

Это же называется Single Page Application, верно? Мы именно этого и хотим, не так ли?

Поэтому уберем из страничного btjson декларации всех блоков, оставим только минимальный костяк страницы: client/pages/index-page/index-page.page.js

module.exports = function (pages) {
    pages.declare('index-page', function (params) {
        var options = params.options;
        return {
            block: 'page',
            title: 'Index page',
            styles: [
                {url: options.assetsPath + '.css'}
            ],
            scripts: [
                {url: options.assetsPath + '.' + params.lang + '.js'}
            ],
            body: []
        };
    });
};

Если сейчас обновить страницу localhost:8080 в окне браузера мы не увидим ничего, кроме серого фона. Но если посмотреть файрбагом в HTML, увидим костяк страницы и загруженные стилевой и javascript-файл. То, что нужно. Вернемся к контроллеру. Отредерим sidebar из контроллера:

client/pages/index-page/controllers/page-controller.js

modules.define(
    'page-controller',
    [
        'inherit',
        'jquery',
        'sidebar'   // <-- Позвали блок в зависимостях
    ],
    function (
        provide,
        inherit,
        $,
        SidebarView // <-- Приняли его в качестве аргумента
    ) {

    var PageController = inherit({
        __constructor: function () {
            console.log('index: PageController constructor');
        },

        start: function () {
            // 1. Когда контроллер стартовал, он узнаёт, авторизован ли пользователь.
            var isAuthorized = true;

            if (isAuthorized) {
                // 2. Если авторизован, контроллер ренедрит вью с контентным блоком.
                var sidebarView = new SidebarView({   // <----------------- Создали блок на лету
                    title: 'Привет, BEViS!',          // <----------------- Передали в него btjson-опции 
                    resources: [
                        {
                            text: 'Репозиторий',
                            url: 'https://github.com/bevis-ui/'
                        },
                        {
                            text: 'Учебник для новичков',
                            url: 'https://github.com/bevis-ui/docs/blob/master/manual-for-beginner.md'
                        },
                        {
                            text: 'Учебник для старичков',
                            url: 'https://github.com/bevis-ui/docs/blob/master/manual-for-master.md'
                        }
                    ]
                });
                sidebarView.getDomNode().appendTo($('body')); // <--------- Внедрили в DOM-дерево
            } else {
                // 3. Если не авторизован, контроллер ренедрит вью с формой авторизации.
            }
        }
    });

    provide(PageController);
});

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

Мы на лету (в памяти браузера) генерим блок SidebarView, а потом внедряем его внутрь <body> с помощью jQuery-метода appendTo.

Напомню вам, все визуальные блоки в проекте выполняют роль Представления, то есть в терминах MVC они играют роль View . Именно поэтому мы модуль сайдбара принимаем из зависимостей под именем SidebarView. Для удобства только, вы можете так не делать :)

modules.define(
    'page-controller',
    [
        'inherit',
        'jquery',
        'sidebar'   // <-- Позвали блок
    ],
    function (
        provide,
        inherit,
        $,
        SidebarView // <-- Приняли его под удобным именем
    ) {
...

Если сейчас обновить страницу, всё сломается, потому что у блока sidebar нет клиентского js-модуля. А в консоли появилось сообщение об ошибке:

Uncaught Error: Module "page-controller": can't resolve dependence "sidebar"

Всё верно, мы запросили в зависимостях модуль sidebar, но его нет в природе. Нет же файла client/views/sidebar/sidebar.js? Его не существует, мы не создавали его на предыдущих уроках, он не был нужен. А раз нет файла client/views/sidebar/sidebar.js, значит нет модуля sidebar, значит он не придёт к нам под именем SidebarView, следовательно, оператор var sidebarView = new SidebarView() не имеет смысла — нам не от чего делать экземпляр класса SidebarView.

Создадим модуль. Выполните команду в терминале, она создаст модуль сама.

echo "modules.define(
    'sidebar',
    ['inherit', 'block'],
    function (provide, inherit, YBlock) {
        var Sidebar = inherit(YBlock, {
            __constructor: function () {
                this.__base.apply(this, arguments);
            }
        }, {
            getBlockName: function () {
                return 'sidebar';
            }
        });

        provide(Sidebar);
});
" > client/views/sidebar/sidebar.js

Теперь контрукция var sidebarView = new SidebarView() обретает смысл. Как вы помните, в тот момент, когда мы сказали new SidebarView() и передали внутрь опции, запускается конструктор модуля в файле client/views/sidebar/sidebar.js, который запускает генерацию HTML-тегов с помощью шаблонов, описанных в файле client/views/sidebar/sidebar.bt.js

Да, что я объясняю, вы это это знаете. Знаете ведь? Если не очень, а перечитайте раздел про генерацию блока на клиенте

Теперь, обновив страницу в браузере, мы увидим список ссылок про Бивис. Я вижу, а вы?

Отлично, ветка if в контроллере сработала. И этот user case мы реализовали. Переходим к следующему.

Шаг 3.

Теперь нужно представить, что пользователь не авторизован. Для этого переменную isAuthorized установим в false. Теперь в операторе if-else выполнится ветка else. Напишем код для неё. Если пользователь не авторизован, нужно отобразить форму авторизации: client/pages/index-page/controllers/page-controller.js

modules.define(
    'page-controller',
    [
        'inherit',
        'jquery',
        'sidebar',
        'form',     // <-- Позвали форму в зависимостях
        'y-i18n'    // <-- Позвали блок для работы с переводами
    ],
    function (
        provide,
        inherit,
        $,
        SidebarView,
        FormView,   // <-- Приняли форму в качестве аргумента
        i18n        // <-- Приняли переводы в качестве аргумента
    ) {

    var PageController = inherit({
        __constructor: function () {
            console.log('index: PageController constructor');
        },

        start: function () {
            // 1. Когда контроллер стартовал, он узнаёт, авторизован ли пользователь.
            var isAuthorized = false;  // <-------------------------------- Как будто не авторизован

            if (isAuthorized) {
                // 2. Если авторизован, контроллер ренедрит вью с контентным блоком.
                var sidebarView = new SidebarView({
                    title: 'Привет, BEViS!',
                    resources: [
                        {
                            text: 'Репозиторий',
                            url: 'https://github.com/bevis-ui/'
                        },
                        {
                            text: 'Учебник для новичков',
                            url: 'https://github.com/bevis-ui/docs/blob/master/manual-for-beginner.md'
                        },
                        {
                            text: 'Учебник для старичков',
                            url: 'https://github.com/bevis-ui/docs/blob/master/manual-for-master.md'
                        }
                    ]
                });
                sidebarView.getDomNode().appendTo($('body')) ;
            } else {
                // 3. Если не авторизован, контроллер 
                // ренедрит вью с формой авторизации.
                var formAuthView = new FormView({         // <------------ Создали блок на лету
                    titleText: i18n('form', 'title-text') // <------------ Передали в него текст из перевода
                });
                formAuthView.getDomNode().appendTo($('body')) ;
            }
        }
    });

    provide(PageController);
});

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

Шаг 4.

Крупные куски контроллера работают отлично.

  1. Когда контроллер стартовал, он узнаёт, авторизован ли пользователь.

  2. Если авторизован, контроллер ренедрит вью с контентным блоком.

  3. Если не авторизован, контроллер ренедрит вью с формой авторизации.

Пришло время слушать события от формы авторизации. Напомню, как выше выглядит этот фрагмент в алгоритме:

  • Контроллер ренедрит вью с формой авторизации
  • Контроллер подписывается на событие от формы типа "пользователь попытался авторизоваться с помощью формы"

Сделаем второй пункт — подпишем контроллер на событие от формы типа "пользователь попытался авторизоваться с помощью формы". То есть форма должна такое событие выбрасывать, а контроллер ловить. Делаем точно так же, как форма слушала событие от инпута.

Форма выбрасывает кастомное событие

Откроем файл client/views/form/form.js и заменим содержимое метода _onInputSubmitted.

Было:

/**
 * Реагирует на нажатие клавиши Enter в `Input`
 * @param {YEventEmitter} e
 */
_onInputSubmitted: function (e) {
    console.log('Форма поймала событие на Input = ', e);
}

Стало:

/**
 * Реагирует на нажатие клавиши Enter в `Input`
 * @param {YEventEmitter} e
 */
_onInputSubmitted: function (e) {
    this.emit('form-submitted', e.data);
}

Метод emit позволяет "выбросить вверх" произвольное событие. Произвольное событие, которое контроллер будет слушать на форме, я назвал form-submitted.

О методе emit можно освежить знания в разделах Эмитирование произвольного события и Обработка произвольного события и получение данных из эмиттера.

Целиком код формы выглядит так. Он прекрасен, не так ли?

modules.define(
    'form',
    [
        'inherit',
        'block',
        'input',
        'super-input',
        'y-i18n'
    ],
    function (
        provide,
        inherit,
        YBlock,
        Input,
        SuperInput,
        i18n
    ) {
        var form = inherit(YBlock, {
            __constructor: function () {
                this.__base.apply(this, arguments);

                var formDomNode = this.getDomNode();

                // Создаём инпут
                this._greetingInput = new Input({
                    value: 'Привет, Бивис',
                    name: 'loginField',
                    placeholder: 'Инпут на сайте',

                    parentNode: formDomNode
                });
                this._greetingInput.on('input-submitted', this._onInputSubmitted, this);

                // Создаём инпут для пароля
                this._passwordInput = new SuperInput({
                    name: 'passwordField',
                    type: 'password',
                    placeholder: i18n('any-other', 'my-key'),

                    parentNode: formDomNode
                });
                this._passwordInput.on('input-submitted', this._onInputSubmitted, this);// <-- Слушаем событие на Инпуте
                                                                 // ^------------------------- Запускаем обработчик
            },

            /**
             * Реагирует на нажатие клавиши Enter в `Input`
             * @param {YEventEmitter} e
             */
            _onInputSubmitted: function (e) {
                this.emit('form-submitted', e.data); // <---- В обработчике генерим новое событие 
                                           // ^-------------  Передаём данные из инпута 
            }
        }, {
            getBlockName: function () {
                return 'form';
            }
        });

        provide(form);
});

В нём нет ничего лишнего. Форма занимается только тремя вещами:

  1. Создаёт внутри себя инпуты (создаёт своё собственное представление)

  2. Слушает событие от инпутов.

  3. Когда услышит, выкинет своё собственное событие и передаст данные.

Мы почти закончили программировать форму. Форма не знает, кто подписан на её событие. Форма не знает, кому она передаст данные. Ей безралично. Она занята только организацией работы своего собственного участка работы. Именно этого мы и добиваемся.

Глядя в её код, я вижу один недочёт, который не замечал раньше. Мы к нему вернёмся позже и доделаем.

Контроллер ловит кастомное событие формы

Теперь сделаем так, чтобы контроллер ловил событие form-submitted.

client/pages/index-page/controllers/page-controller.js

modules.define(
    'page-controller',
    [
        'inherit',
        'jquery',
        'sidebar',
        'form',
        'y-i18n'
    ],
    function (
        provide,
        inherit,
        $,
        SidebarView,
        FormView,
        i18n
    ) {

    var PageController = inherit({
        __constructor: function () {
            console.log('index: PageController constructor');
        },

        start: function () {
            var isAuthorized = false;

            if (isAuthorized) {
                var sidebarView = new SidebarView({
                    title: 'Привет, BEViS!',
                    resources: [
                        {
                            text: 'Репозиторий',
                            url: 'https://github.com/bevis-ui/'
                        },
                        {
                            text: 'Учебник для новичков',
                            url: 'https://github.com/bevis-ui/docs/blob/master/manual-for-beginner.md'
                        },
                        {
                            text: 'Учебник для старичков',
                            url: 'https://github.com/bevis-ui/docs/blob/master/manual-for-master.md'
                        }
                    ]
                });
                sidebarView.getDomNode().appendTo($('body')) ;
            } else {
                var formAuthView = new FormView({
                    titleText: i18n('form', 'title-text')
                });
                formAuthView.getDomNode().appendTo($('body'));
                formAuthView.on('form-submitted', this._onFormAuthSubmitted, this); // <-- Слушаем событие на Форме
                                                             // ^------------------------- Запускаем обработчик
            }
        },

        /**
         * Обработчик сабмита на форме авторизации
         * @param {YEventEmiiter} e
         */
        _onFormAuthSubmitted: function (e) {
            console.log('Форма передала данные = ', e.data);     // <-- Поймали событие на Форме, как-то обработали.
        }
    });

    provide(PageController);
});

Обновим страницу, поставим курсор в текстовое поле с "Привет, Бивис" и нажмём клавишу Enter. В файрбаге мы увидим сообшение:

Форма передала данные =  Object {value: "Привет, Бивис"}

Теперь контроллер слушает события от формы и получает от неё данные. Это значит, что когда пользователь введёт логин или пароль в форме авторизации и, стоя в каком-то из полей, нажмёт клавишу Enter, информация из текстовых полей по цепочке Input -> Form -> PageController попадёт в страничный контроллер. Ни инпут, ни форма сами по себе ничего не делают с этой информацией - не сохраняют в бекенд, не кладут в куку, ни в локалсторадж — вообще ничего, что относилось бы к логике приложения. Инпут и форма только умеют принимать информацию от пользователя и отдавать её через систему событий. При этом ни форма, ни инпут не знают, кому они её отдают — кому-то, кто хочет её принять!

Форма собирает данные из инпутов

Пришло время исправить странность в Form.

Сейчас форма написана так, что он рождает событие form-submitted всякий раз, когда услышала событие input-submitted на одном из инпутов. И это правильно, кстати.

Неправильно то, что когда форма рождает событие form-submitted, она передаёт данные только из того инпута, который сгенерил событие input-submitted. Если пользователь нажал кнопку Enter в поле для пароля - в контроллер придут данные только с паролем. А если пользователь нажал кнопку Enter в поле для логина - в контроллер придут данные только с логином.

А как надо? Опишем алгоритм:

  1. Когда какой-нибудь инпут стриггерил событие input-submitted, вызвать обработчик. (Сделано!)

  2. Собрать данные с обоих полей формы - и из логина, и из пароля.

  3. Эмитировать событие form-submitted и передать в него данные и с логином и с паролем. (Сделано наполовину!)

Отредактируем client/views/form/form.js Было:

/**
 * Реагирует на нажатие клавиши Enter в `Input`
 * @param {YEventEmitter} e
 */
_onInputSubmitted: function (e) {
    this.emit('form-submitted', e.data);
}

Стало:

/**
 * Реагирует на нажатие клавиши Enter в `Input`
 */
_onInputSubmitted: function () {
    var data = {
        login: this._greetingInput.getValue(),
        password: this._passwordInput.getValue()
    };

    this.emit('form-submitted', data);
}

В форме авторизации мы точно знаем, сколько и какие инпуты есть в форме. Ну так просто возьмём и заберем из них данные с помощью их методов getValue.

Обратите, внимание, я удалил из аргументов функции переменную e, потому что мы читаем текст, введенный пользователем, прямо из инпутов с помощью их публичных методов getValue.

Теперь если обновить страницу и нажать Enter, находясь в одном из полей, мы увидим сообщение типа такого:

Форма передала данные =  Object {login: "Привет, Бивис", password: ""}

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

А теперь свяжем контроллер с моделью! :)

Шаг 5.

И пусть никакой модели ещё нет в природе, это даже хорошо. Cначала мы опишем, как нам было бы удобно пользоваться моделью в контроллере, а уже потом создадим саму модель.

А как удобно пользоваться моделью? Перечитаем ту часть алгоритма, в которой упоминается модель, и составим новое ТЗ к этой части:

  1. Когда контроллер стартует, он спрашивает у Модели, авторизован ли пользователь. Сохраняет эту информацию в переменной isAuthorized.
  2. Модель читает в бекенде такую информацию (пока не важно где) и отдаёт её контроллеру.
  3. Если пользователь не авторизован:
  • Контроллер подписывается на событие от формы типа "пользователь попытался авторизоваться с помощью формы"
  • Когда событие произошло, контроллер передаёт в Модель данные пользователя, чтобы та сохранила их в бекенде
  • Если данные сохранены в бекенде успешно, Модель сообщает в контроллер об этом
  • Контроллер рестартует сам себя, чтобы попасть на пункт №1, таким образом приложение начнёт работать как бы сначала. То есть проверит, авторизован ли пользователь. В этот момент он уже будет авторизован, следовательно приложение отрендерит блок SidebarView

Начнём изменять контроллер. При этом будем писать код так, словно Модель у нас уже есть.

Все новые изменения в файле client/pages/index-page/controllers/page-controller.js я пометил комментариями:

modules.define(
    'page-controller',
    [
        'inherit',
        'jquery',
        'sidebar',
        'form',
        'y-i18n',
        'auth-model' // <-- Позвали модуль с моделью авторизации
    ],
    function (
        provide,
        inherit,
        $,
        SidebarView,
        FormView,
        i18n,
        AuthModel    // <-- Получили модель как переменную AuthModel
    ) {

    var PageController = inherit({
        __constructor: function () {
            console.log('index: PageController constructor');

            // Создали экземпляр Модели Авторизации
            this._authModel = new AuthModel();  

            // Слушаем событие на модели
            // Произойдёт, когда модель успешно сохранит данные
            this._authModel.on('saved', this.start, this);
        },

        start: function () {
            // Спрашиваем у Модели, авторизован ли пользователь
            var isAuthorized = this._authModel.isAuthorized();

            if (isAuthorized) {
                var sidebarView = new SidebarView({
                    title: 'Привет, BEViS!',
                    resources: [
                        {
                            text: 'Репозиторий',
                            url: 'https://github.com/bevis-ui/'
                        },
                        {
                            text: 'Учебник для новичков',
                            url: 'https://github.com/bevis-ui/docs/blob/master/manual-for-beginner.md'
                        },
                        {
                            text: 'Учебник для старичков',
                            url: 'https://github.com/bevis-ui/docs/blob/master/manual-for-master.md'
                        }
                    ]
                });
                sidebarView.getDomNode().appendTo($('body')) ;
            } else {
                var formAuthView = new FormView({
                    titleText: i18n('form', 'title-text')
                });
                formAuthView.getDomNode().appendTo($('body'));
                formAuthView.on('form-submitted', this._onFormAuthSubmitted, this);
            }
        },

        /**
         * Обработчик сабмита на форме авторизации
         * @param {YEventEmiiter} e
         */
        _onFormAuthSubmitted: function (e) {
            this._authModel.set(e.data); // <---- Сохраняем данные, пришедшие из формы
        }
    });

    provide(PageController);
});

Всё, мы связали контроллер и модель с помощью событий. Теперь контроллер можно считать законченным.

Напомню вам и себе, как устроена цепочка событий между Input, Form и PageController:

  1. Инпут слушает нажатие кнопки на клавиатуре.

  2. Если пользователь нажал кнопку Enter, Инпут емитирует событие "пользователь попытался авторизоваться" и передаёт данные, которые ввел пользователь.

  3. Это событие получает Форма, которая с помощью своего метода emit выбрасывает уже своё событие (которое может называться так же, как пришло у Инпута) и передаёт те данные, которые забрал у ИНпута.

  4. Это событие от Формы ловит Контроллер страницы и получает данные от формы.

  5. Контроллер передаёт полученные данные в Модель, чтобы та сохранила их в бекенде.

  6. Модель сохраняет данные в бекенед и выбрасывает событие "Пользователь добавлен в базу".

  7. Контроллер подписан на это событие. Когда контроллер услышал такое событие от Модели, он заново запускает start приложения.

Конечно, если прямо сейчас обновить страницу в браузере, приложение упадёт с ошибкой, что не может найти модуль auth-model. Но это ожидаемо, поэтому обновлять страницу нет смысла.

Нам осталось только создать модель и приложение готово. Так давайте скорее создадим :)

Шаг 6.

Прежде чем создать модель, хорошо бы понять, где именно её создавать, в какой директории на файловой системе. Это не сложно, ответим на простой вопрос: "Модель нужна только на этой странице, или она будет использоваться на всех страницах?"

Так как мы будем создавать модель для получения/сохранения авторизационных данных, то скорее всего модель может быть использована на любой странице. Значит модель будет жить в директории client/models/.

Файл для модели, зависимости и сборка

Такой директории ещё нет, создадим в терминале:

mkdir client/models

Как будет называться эта модель? А мы уже знаем ответ на этот вопрос — придумали ещё на предыдущем шаге, когда фантазировали "каким образом, нам хочется, чтобы контроллер взаимодействовал с этой моделью". Мы тогда придумали ей имя auth-model. Скопируйте в терминал и выполните команду:

echo "modules.define(
    'auth-model',
    [
        'inherit', 
        'event-emitter'
    ],
    function (
        provide, 
        inherit, 
        YEventEmitter
    ) {
        var AuthModel = inherit(YEventEmitter, {
            __constructor: function () {
                this.__base.apply(this, arguments);
            }
        });
    
        provide(AuthModel);
});
" > client/models/auth-model.js

Болванка модели готова. Сразу укажем зависимость в этом файле: client/pages/index-page/index-page.deps.yaml. Мол, индексная страница зависит от нашей модели:

- page
- app-controller
- block
- sidebar
- layout
- auth-model  # <--- Добавили

А чтобы сборщик знал, где все модели искать, сообщим ему в точности так, как это неоднократно уже проделывали в package.json — в секции enb.sources добавим новый элемент массива:

    "enb": {
        "sources": [
            "client/controllers",
            "client/views",
            "client/core",
            "client/models"  // <------ Добавили
        ],
        "profiles": {
            "index-page": {
                "sources": [
                    "client/pages/index-page"
                ]
            },
            "test-page": {
                "sources": [
                    "client/pages/test-page"
                ]
            }
        }
    }

Всё, теперь модель есть. Пару слов об её устройстве

Модель наследуется от YEventEmitter

Вы наверняка заметили, что модель — это обычный Javascript-класс, созданный с помощью функции inherit, для вас это не должно быть новостью.

А если вдруг запамятовали, можно освежить знания почитав раздел
Модули или страницу автора: https://github.com/dfilatov/node-inherit.

Непонятно может быть другое. Почему, когда мы создавали Form и Input, мы наследовали их от Yblock?

var Form = inherit(YBlock, {
var Input = inherit(YBlock, {

Почему, когда мы создавали PageController, мы ни от чего его не наследовали?

var PageController = inherit({

А когда мы создаём AuthModel, мы внезапно наследуем её от YEventEmitter?

var AuthModel = inherit(YEventEmitter, {

Почему так? В чём логика?

"Отвечает Александр Друзь" (с)

Form и Input - это визуальные блоки, следовательно в терминологии MVC выполняют роль View. Они наследуются от абстрактного класса YBlock, потому что YBlock - это хелпер, который содержит множество методов для создания и работы визуальных блоков — рендеринг на странице, поиск блоков и элементов на странице, установка state и data-атрибутов в тегах и всякое другое, что нужно для работы с DOM-объектами. Наверное, хорошо бы ему называться YView вместо YBlock, было бы понятнее. Но какое имя есть, такое есть - не обессудьте :)

Когда Form и Input отнаследовались от YBlock, они приобрели все те методы, что есть у YBlock.

Когда Контроллер не отнаследовался ни от чего, это значит, что ему не нужно брать никакие методы ни у какого другого класса. В самом деле, нашему контроллеру ничего не нужно. Он не представлен на странице никаким визуальным блоков, он не работает напрямую с DOM — он работает только в другими BEViS-блоками, раздаёт им команды и принимает от них отчёты о выполненной работе. Командир же :)

Мы не наследуем Модель от YBlock по той же причине - модель не представлена на странице никаким визуальным блоком. Это по сути хранилище данных. Отчего же мы тогда наследуем её от YEventEmitter? Дело в том, что класс YEventEmitter предоставляет методы для "слушания" произвольных событий.

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

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

this._authModel.on('saved', this.start, this);

Чтобы модель умела эмитировать событие, мы отнаследовали её от YEventEmitter.

Кстати, класс YBlock тоже наследуется от YEventEmitter, вот почему любая вьюха (те же Form и Input) тоже умеют эмитировать события.

Методы модели

Теперь самая простая часть в Модели - это создать те публичные методы, которые мы хотим вызывать из PageController. Если мне не изменяет память, мы хотели дергать только два метода: isAuthorized - чтобы получить из него булевское значение и set, чтобы сохранить данные в бекенде.

Ну тогда создадим эти два метода :)

modules.define(
    'auth-model',
    [
        'inherit', 
        'event-emitter'
    ],
    function (
        provide, 
        inherit, 
        YEventEmitter
    ) {
        var AuthModel = inherit(YEventEmitter, {
            __constructor: function () {
                this.__base.apply(this, arguments);

                /**
                 * Текущее состояние авторизации 
                 * @type {Boolean}
                 */
                this._isAuthorized = false;
            },

            /**
             * Проверяет, авторизован ли пользователь
             * @returns {Boolean}
             */
            isAuthorized: function () {
                return this._isAuthorized;
            },

            /**
             * Сохраняет авторизационные данные
             * @param {Object} data Сохраняемые данные
             */
            set: function (data) {
                console.log("data = ", data);
            }
        });
    
        provide(AuthModel);
});

Я создал пустышки, которые ничего не делают. Хотя, неправда, метод isAuthorized работает хорошо, всегда возвращает false :)

Если прямо сейчас обновить страницу localhost:8080 в браузере, то приложение будет работать - мы увидим отрендеренную форму авторизации. Понятно почему - модель возвращает false из метода isAuthorized, следовательно контроллер думает, что пользователь не авторизован. Если из метода isAuthorized вернуть true и обновить страницу, увидим блок sidebar. Но мы этого делать не будем. Нам как раз надо, чтобы пользователь был не авторизован. Зачем?

Когда мы программировали контроллер, мы заложили в него такую функциональность:

  • Контроллер подписывается на событие от Модели "данные сохранились успешно"

  • Когда контроллер услышит событие от модели, он вызовет свой метод start, таким образом приложение начнёт работать как бы сначала.

Мы реализовали оба пункта с помощью одной строки в client/pages/index-page/controllers/page-controller.js:

// Слушаем событие на модели
// Произойдёт, когда модель успешно сохранит данные
this._authModel.on('saved', this.start, this);

Это означает, что когда модель успешно сохранит данные, она должна эмитировать событие saved. Хорошо, через 10 секунд будет эмитировать :)

modules.define(
    'auth-model',
    [
        'inherit', 
        'event-emitter'
    ],
    function (
        provide, 
        inherit, 
        YEventEmitter
    ) {
        var AuthModel = inherit(YEventEmitter, {
            __constructor: function () {
                this.__base.apply(this, arguments);

                /**
                 * Текущее состояние авторизации 
                 * @type {Boolean}
                 */
                this._isAuthorized = false;
            },

            /**
             * Проверяет, авторизован ли пользователь
             * @returns {Boolean}
             */
            isAuthorized: function () {
                return this._isAuthorized;
            },

            /**
             * Сохраняет авторизационные данные
             * @param {Object} data Сохраняемые данные
             */
            set: function (data) {
                console.log("data = ", data);

                // Должны сохранить данные в бекенде, пока эмулируем с помощью флага
                this._isAuthorized = true;

                // Сообщаем контроллеру об успехе
                this.emit('saved');
            }

        });
    
        provide(AuthModel);
});

Теперь метод set как бы сохраняет данные (на самом деле меняет флаг), после чего эмитирует событие saved.

Кажется, что теперь наша страница готова к полноценному циклу работы. Когда мы обновим страницу, сначала пользователь не авторизован, контроллер покажет форму. Мы нажмём в инпуте формы кнопку Enter - данные из формы попадут в модель в метод set, после чего модель выкинет сообщение saved. Контроллер услышит это сообщение и запустит свой метод start. А в нём он опять обратится к модели в метод isAuthorized, который вернёт теперь уже true. Следовательно контроллер отрендерит теперь sidebar.

Проверяем или страшно? :)

Охо-хо, смешно получилось :) Всё шло по плану, но после сабмита формы она не исчезла, а под ней отобразился сайдбар. Ну, это понятно почему - когда контроллер второй раз вызвал свой метод start, он не очистил DOM-дерево, а просто добавил сайдбар. Прям под форму :)

Ну, исправить несложно. Добавим в контроллер одну строку.

        start: function () {
            // Спрашиваем у Модели, авторизован ли пользователь
            var isAuthorized = this._authModel.isAuthorized();

            if (isAuthorized) {
                $('body').empty();   // <----------- Очищаем дерево от всего :)

                var sidebarView = new SidebarView({
                    title: 'Привет, BEViS!',
                    resources: [
                        {
                            text: 'Репозиторий',
                            url: 'https://github.com/bevis-ui/'
                        },
                        {
                            text: 'Учебник для новичков',
                            url: 'https://github.com/bevis-ui/docs/blob/master/manual-for-beginner.md'
                        },
                        {
                            text: 'Учебник для старичков',
                            url: 'https://github.com/bevis-ui/docs/blob/master/manual-for-master.md'
                        }
                    ]
                });
                sidebarView.getDomNode().appendTo($('body')) ;
            } else {
                var formAuthView = new FormView({
                    titleText: i18n('form', 'title-text')
                });
                formAuthView.getDomNode().appendTo($('body'));
                formAuthView.on('form-submitted', this._onFormAuthSubmitted, this);
            }
        },

Теперь хорошо, форма исчезает, сайдбар появляется.

По сути, мы сделали самое главное. Организовали работу всех компонентов. Осталась малость - по настоящему читать авторизационные данные и сохранять их. То есть, осталось дописать модель.

Модель сохраняет и получает данные

Для упрощения иллюстрации мы (пока) не будем реально передавать данные в какой-то бекенд.

Займёмся этим на следующем практическом занятии, посвящённом взаимодействию с бекендом.

А пока вместо передачи данных в бекенд, пусть наша Модель сохраняет логин и пароль в куку. Это небезопасно, но для практики подойдёт. Если сохранение куки произойдёт успешно, — правда, я не знаю, что может пойти не так при сохранении куки, — тогда Модель выкинет сообщение об успехе, а Контроллер его услышит.

Так же не будет Модель пока читать данные из бекенда. Вместо этого Модель будет ходить в куку и читать логин там: если логин в куке есть, Модель вернёт в Контроллер этот логин.

Приступим. У Модели две задачи:

  • Модель сохраняет данные

  • Модель читает данные

С какой начать? А, всё равно! Начнём с первой.

Модель сохраняет данные

Сохранение авторизационных данных происходит в методе set. Сейчас он принимает аргумент типа Object, показывает его в консоли браузера, переключает фейковый флаг this._isAuthorized в значение true и выбрасывает событие, мол, данные сохранились успешно. Здесь почти всё хорошо, кроме неприкрытой лжи — вместо переключения фейкового флага нужно реально сохранять данные. :)

Мне кажется, получится хорошо, если метод set выступит в роли ручки, которую можно дёрнуть "снаружи", но сама по себе делать почти не будет. Пусть вся ответственность, вся черновая работа ляжет на другой метод, приватный, снаружи недоступный.

Было:

set: function (data) {
    console.log("data = ", data);

    // Должны сохранить данные в бекенде, пока эмулируем с помощью флага
    this._isAuthorized = true;

    // Сообщаем контроллеру об успехе
    this.emit('saved');
}

Стало:

set: function (data) {
    // Передаём данные в бекенд или в куку
    this._setUserData(data);

    // Сообщаем контроллеру об успехе
    this.emit('saved');
}

Вся грязная работа по сохранению будет происходить в приватном методе _setUserData, а метод set будет работать как входные ворота. Это позволит нам безболезненно менять код внутри _setUserData хотя бы и по десять раз на дню. Сегодня мы напишем реализацию этого метода с помощью сохранения данных в куке.

В папке client/core/cookie есть модуль для работы с куками.

Мы взяли плагин Клауса Хартла, и завернули в нашу модульную систему. Спасибо вам, Клаус, отличный плагин!

Зовём модуль Cookie и пишем реализацию метода _setUserData:

modules.define(
    'auth-model',
    [
        'inherit', 
        'event-emitter',
        'cookie'            // <---------- Позвали
    ],
    function (
        provide, 
        inherit, 
        YEventEmitter,
        Cookie              // <---------- Получили
    ) {
        var AuthModel = inherit(YEventEmitter, {
            __constructor: function () {
                this.__base.apply(this, arguments);

                /**
                 * Текущее состояние авторизации 
                 * @type {Boolean}
                 */
                this._isAuthorized = false;
            },

            /**
             * Проверяет, авторизован ли пользователь
             * @returns {Boolean}
             */
            isAuthorized: function () {
                return this._isAuthorized;
            },

            /**
             * Сохраняет авторизационные данные
             * @param {Object} data Сохраняемые данные
             */
            set: function (data) {
                // Передаём данные в бекенд или в куку
                this._setUserData(data);

                // Сообщаем контроллеру об успехе
                this.emit('saved');
            },
            
            /**
             * Сохраняет авторизационные данные в куке
             * @param {Object} data Сохраняемые данные
             */
            _setUserData: function (data) {         // <----------------------- Написали приватный метод
                // Сериализуем в строку, чтобы положить в куку
                data = JSON.stringify(data);

                // Сохраняем данные в куке...
                Cookie.set('authorization', data, {
                    path: '/',
                    expires: 365
                });
            }
        });
    
        provide(AuthModel);
});

Не забываем указать зависимость от модуля cookie в client/models/auth-model.deps.yaml:

echo "- cookie
" > client/models/auth-model.deps.yaml

Обновляю в браузере localhost:8080 — работает! Я вижу форму авторизации, пишу что-то в поля и нажимаю Enter. Визуально, правда, ничего не происходит (но это неудивительно, мы же ничего не писали для того, чтобы визуально что-то менялось), но кука authorization создаётся.

Созданную куку я проверяю с помощью браузерного расширения Edit This Cookie. Оно работает под Chrome, новую Opera, Yandex.Browser и все другие браузеры, основанные на движке Blink. Для Firefox тоже есть подобные расширения, — не пользуюсь файрфоксом, не подскажу.

Всё, мы закончили сохранять данные. Переходим к чтению данных.

Модель читает данные

Сейчас метод isAuthorized возвращает фейковый булевский флаг, а надо, чтобы метод взаправду читал данные в куке и сообщал, есть они там или нет. Посмотрите, как я это реализовал. Подумайте, почему именно так, а не иначе.

modules.define(
    'auth-model',
    [
        'inherit', 
        'event-emitter',
        'cookie'
    ],
    function (
        provide, 
        inherit, 
        YEventEmitter,
        Cookie
    ) {
        var AuthModel = inherit(YEventEmitter, {
            __constructor: function () {
                this.__base.apply(this, arguments);

                this._cookieName = 'authorization';
            },

            /**
             * Проверяет, авторизован ли пользователь
             * @returns {Boolean}
             */
            isAuthorized: function () {
                var userData = this.get();

                return Boolean(userData && userData.login && userData.password);
            },

            /**
             * Сохраняет авторизационные данные
             * @param {Object} data Сохраняемые данные
             */
            set: function (data) {
                // Передаём данные в бекенд или в куку
                this._setUserData(data);

                // Сообщаем контроллеру об успехе
                this.emit('saved');
            },

            /**
             * Возвращает авторизационные данные
             * @returns {Object}
             */
            get: function () {
                return this._getUserData();
            },

            /**
             * Возвращает авторизационные данные пользователя
             * Берёт их из куки 'authorization'
             *
             * @returns {Object | null}
             */
            _getUserData: function () {
                var authCookie = Cookie.get(this._cookieName);

                return authCookie? JSON.parse(authCookie) : null;
            },

            /**
             * Сохраняет авторизационные данные в куке
             * @param {Object} data Сохраняемые данные
             */
            _setUserData: function (data) {
                // Сериализуем в строку, чтобы положить в куку
                data = JSON.stringify(data);

                // Сохраняем данные в куке...
                Cookie.set(this._cookieName, data, {
                    path: '/',
                    expires: 365
                });
            }
        });
    
        provide(AuthModel);
});

Чтобы воочию убедиться, что данные из куки теперь читаются, немного изменим код Контроллера. Выведем в заголовок контентного блока логин пользователя - ту самую строку, которую пользователь ввёл в поле логина:

modules.define(
    'page-controller',
    [
        'inherit',
        'jquery',
        'sidebar',
        'form',
        'y-i18n',
        'auth-model'
    ],
    function (
        provide,
        inherit,
        $,
        SidebarView,
        FormView,
        i18n,
        AuthModel
    ) {

    var PageController = inherit({
        __constructor: function () {
            console.log('index: PageController constructor');

            this._authModel = new AuthModel();
            this._authModel.on('saved', this.start, this);
        },

        start: function () {
            $('body').empty();

            var isAuthorized = this._authModel.isAuthorized();

            if (isAuthorized) {
                var authData = this._authModel.get();          // <------- Получаем авторизационные данные

                var sidebarView = new SidebarView({
                    title: 'Привет, ' + authData.login + '!',  // <------- Здесь показываем логин пользователя
                    resources: [
                        {
                            text: 'Репозиторий',
                            url: 'https://github.com/bevis-ui/'
                        },
                        {
                            text: 'Учебник для новичков',
                            url: 'https://github.com/bevis-ui/docs/blob/master/manual-for-beginner.md'
                        },
                        {
                            text: 'Учебник для старичков',
                            url: 'https://github.com/bevis-ui/docs/blob/master/manual-for-master.md'
                        }
                    ]
                });

                sidebarView.getDomNode().appendTo($('body'));

            } else {

                var formAuthView = new FormView({
                    titleText: i18n('form', 'title-text')
                });

                formAuthView.getDomNode().appendTo($('body'));
                formAuthView.on('form-submitted', this._onFormAuthSubmitted, this);
            }
        },

        /**
         * Обработчик сабмита на форме авторизации
         * @param {YEventEmitter} e
         */
        _onFormAuthSubmitted: function (e) {
            this._authModel.set(e.data);
        }
    });

    provide(PageController);
});

Вот теперь всё работает. Сначала мы видим форму авторизации. В поле логина вместо слов "Привет, Бивис!" вводим своё имя (я ввёл "Вадим"), вводим пароль в текстовое поле для пароля (неважно какой) и нажимаем Enter. Без перезагрузки страницы форма исчезает, а контентный блок появляется. А в заголовке видим: "Привет, Вадим!"

Всё ли вам понятно в коде Модели? Давайте я сделаю краткий обзор.

Модель - Javascript-класс, который наследует методы другого Javascript-класса по имени YEventEmitter для того, чтобы модель могла эмитировать кастомные события.

У модели мы создали две группы методов - публичные и приватные. Публичные методы isAuthorized, set и get вызывают приватные методы, которые и делают всю работу — _setUserData и _getUserData. Сейчас приватные методы сохраняют данные в куке и читают их из куки, на следующем занятии мы их перепишем, они будут работать не с кукой, а с реальным бекендом.

Итог

Мы создали приложение с имитацией авторизации, оно состоит из следующих компонентов:

     app-controller
           |
           |
          \/
    page-controller
    /\          /\
    |           |
    |           |
   \/          \/
auth-model    form
             /\ /\
             |  |
             |  |
         input  super-input 

Два контроллера - один для приложения, второй для страницы. Контроллер приложения только стартует контроллер страницы. Вся логика приложения описывается внутри page-controller — он единовластный командир, раздаёт команды вьюхам и моделям, слушает от них отчёты через систему кастомных событий, и снова раздаёт команды. Мы сделали три вью: form, input и super-input. Модель нам понадобилась только одна — auth-model. Вью и модели друг с другом не контактируют, даже не знают про существование друг друга — об этом знает только контроллер. Вся информация о приложении стекается в контроллер. И это удобно.

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

У нас есть болванка для разработки BEViS-проекта в MVC. Эта болванка лежит в основе нескольких продакшн-продуктов, созданных в отделе Яндекс.Карт. То есть, её можно безбоязненно клонировать и создавать свои собственные шедевры.

##Что дальше, друзья?

Дальше нам предстоит самое интересное и, пожалуй, самое трудное, насыщенное, почти гиковское, практическое занятие. Мы будем говорить о том, как по-настоящему общаться с бекендом. В роли бекенда выступит база данных MongoDB и специальные серверные скрипты, которые будут принимать запросы из нашего MVC-приложения и проксировать их в базу данных.

Нам предстоит:

  • научиться создавать MongoDB и пользоваться ей,
  • научиться писать серверные ручки с помощью инструмента BLA, созданного в Яндекс.Картах,
  • понять, что такое Promises, и это, пожалуй, будет самым тяжелым моментом во всем учебнике.
  • научиться пользоваться промисами с помощью библиотеки Vow.

Думаю, сейчас стоит сделать перерыв.

Может быть вам для тренировки стоит создать какое-то своё MVC приложение. Или покопаться в исходниках bevis-todo — оно сделано на приниципах MVC с той лишь разницей, что на файловой системой компоненты не разнесены по папкам client/controllers, client/views, client/models — всё свалено в папку blocks. Но даже там можно без труда увидеть и контроллер и модели и разные вью. И если на этом занятии мы сохраняли данные в куку, то в bevis-todo можно посмотреть, как сохранять данные в Local Storage, оказывается и для этого есть модуль в проекте.

Хорошего настроения и до встречи здесь, я вас жду! :)