├── .gitignore ├── README.md ├── faq ├── active_round.png ├── bem-vs-bevis.md └── pseudo_shadow.png ├── how-to-make ├── backend-requests.md ├── css.md ├── dependencies.md ├── enb.md ├── i18n.md ├── modules.md ├── mvc-app.md ├── new-block.md ├── new-page.md ├── similar-block.md ├── tests.md └── yblock.md ├── manual-for-beginner.md ├── manual-for-master.md ├── practice.md └── practice ├── backend-requests.md ├── css.md ├── dependencies.md ├── enb.md ├── i18n.md ├── modules.md ├── mvc-app.md ├── new-block.md ├── new-page.md ├── oop.md ├── tests.md └── yblock.md /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Быстрый старт 2 | ``` 3 | git clone git@github.com:bevis-ui/bevis-stub.git your-project 4 | cd your-project 5 | make 6 | ``` 7 | Откройте в браузере ```localhost:8080``` и скажите: «Привет, Бивис!» 8 | 9 | ## BEViS? 10 | Это `javascript` фреймворк для создания веб-сайтов. Не нужно знать ни одного серверного языка 11 | программирования, чтобы сверстать настоящий коммерческий большой сайт. Достаточно лишь небольших знаний 12 | о `HTML`, `CSS`, `Javascript`. Вот это и есть `BEViS`. Заинтересовались? :) 13 | 14 | Мы подготовили для вас 15 | [весёлые учебники](#%D0%92%D0%B5%D1%81%D1%91%D0%BB%D1%8B%D0%B5-%D1%83%D1%87%D0%B5%D0%B1%D0%BD%D0%B8%D0%BA%D0%B8), 16 | от которых (мы смеем надеяться) вы не сможете оторваться и ещё готовим [практические занятия](practice.md), на которых 17 | вместе с вами создадим проект и пощупаем все технологии. 18 | 19 | А для тех из вас, кто не хочет читать, а хочет только глазком заглянуть в референс, скоро напишем пишем серьёзные 20 | [справочные руководства](#%D0%A1%D0%BF%D1%80%D0%B0%D0%B2%D0%BE%D1%87%D0%BD%D1%8B%D0%B5-%D1%80%D1%83%D0%BA%D0%BE%D0%B2%D0%BE%D0%B4%D1%81%D1%82%D0%B2%D0%B0), 21 | в которых нет даже намёка на шутливость. 22 | 23 | Выбирайте, что вам по душе, и добро пожаловать :) 24 | 25 |   26 | 27 | ---- 28 | 29 |   30 | 31 | ### Весёлые учебники 32 | 33 | > ##### «Про сложные темы невозможно говорить серьёзно.» 34 | _Андрей Кармацкий_ 35 | 36 |   37 | 38 | Наши дети в школах страдают от недостатка _интересной_ учебной литературы, а мы страдаем от этого же на работе, 39 | когда руководитель даёт нам задание срочно освоить что-то новое. 40 | 41 | К сожалению, хорошие учебники для взрослых можно пересчитать по пальцам. Я знаю про 42 | [Git Magic](http://www-cs-students.stanford.edu/~blynn/gitmagic/index.html) и про 43 | [Why's Poignant Guide To Ruby](http://mislav.uniqpath.com/poignant-guide/). Я осмелюсь заявить, что наши весёлые 44 | учебники можно поставить с ними на одну полку — они нескучные, написаны простым русским языком, с аналогиями и 45 | понятными метафорами. 46 | 47 | Их два. Один расчитан на специалистов самого широкого профиля, то есть даже вообще неспециалистов, а второй 48 | написан для тех, кто уже понюхал в жизни пороху и знает, что такое MVC, шаблоны представления и какие сервера 49 | кроме Apache бывают на свете. Выбирайте по вкусу :) 50 | 51 | * [Учебник для новичков](manual-for-beginner.md) 52 | * [Учебник для старичков](manual-for-master.md) 53 | 54 | Между делом заведите себе гиковский блог на [Бивис-движке](http://github.com/bevis-ui/bevis-blog). 55 | Это займёт не больше двух минут, включая публикацию блога на бесплатный хостинг. Пишите статьи в маркдауне, 56 | забудьте о вордпрессе ;) 57 | 58 | Или посмотрите на одностраничное приложение, реализующее 59 | функциональность [TODO MVC](http://github.com/bevis-ui/bevis-todo). 60 | 61 | А ещё лучше, посетите наш практикум. Он не скучный, быстрый, понятный. 62 | 63 |   64 | 65 | #### Практикум 66 | 67 | _На вопросы, отмеченные звёздочкой, мы либо ещё не ответили, либо ответили не полностью._ 68 | 69 | 1. [Создаём новую страницу проекта](practice/new-page.md) 70 | 2. [Добавляем на страницу новый блок](practice/new-block.md) 71 | 3. [Описываем зависимости между блоками](practice/dependencies.md) 72 | 4. [Пишем стили для блока](practice/css.md) 73 | 5. [Пишем `js`-поведение для блока](practice/yblock.md) 74 | 6. [Настраиваем локализацию в проекте](practice/i18n.md) 75 | 7. [Используем `MVC`-концепцию в `BEViS`-проекте](practice/mvc-app.md) 76 | 8. [Взаимодействуем с бекендом](practice/backend-requests.md) [*] 77 | 9. [Пишем тесты](practice/tests.md) [*] 78 | 79 |   80 | 81 | ---- 82 | 83 |   84 | 85 | ### Справочные руководства 86 | 87 | > #####«Краткость — сестра таланта.» 88 | _Антон Павлович Чехов_ 89 | 90 |   91 | 92 | #### Решение типовых задач 93 | 94 | _На вопросы, отмеченные звёздочкой, мы либо ещё не ответили, либо ответили не полностью._ 95 | 96 | В этих документах мы кратко пересказываем то, что описано в соответствующих уроках практикума. Только суть без 97 | подробных объяснений. 98 | 99 | 1. [Новая страница](how-to-make/new-page.md) [*] 100 | 2. [Новый блок на странице](how-to-make/new-block.md) [*] 101 | 3. [Зависимости между блоками](how-to-make/dependencies.md) [*] 102 | 4. [`CSS`-стили блока](how-to-make/css.md) [*] 103 | 5. [`JS`-поведение блока](how-to-make/yblock.md) [*] 104 | 6. [Локализация проекта](how-to-make/i18n.md) [*] 105 | 7. [`MVC` в `BEViS`](how-to-make/mvc-app.md) [*] 106 | 8. [Взаимодействие с бекендом](how-to-make/backend-requests.md) [*] 107 | 9. [Тесты](how-to-make/tests.md) [*] 108 | 109 | #### О технологиях 110 | 1. [bt](https://github.com/enb-make/bt) 111 | 2. [ENB](https://github.com/enb-make/enb) 112 | 3. [Ymaps Modules](how-to-make/modules.md) 113 | 4. [Stylus](http://learnboost.github.io/stylus/) 114 | 115 | #### Отвечаем на частые вопросы 116 | 1. [Зачем нужен BEViS, если уже есть BEM?](faq/bem-vs-bevis.md) 117 | 2. [Я сверстал блок. Теперь нужен такой же, только чуть-чуть другой. Как это сделать?](how-to-make/similar-block.md) [*] 118 | 119 | -------------------------------------------------------------------------------- /faq/active_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bevis-ui/docs/8d39012843de0261dbb3861ffa7ff9a5f9e16529/faq/active_round.png -------------------------------------------------------------------------------- /faq/pseudo_shadow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bevis-ui/docs/8d39012843de0261dbb3861ffa7ff9a5f9e16529/faq/pseudo_shadow.png -------------------------------------------------------------------------------- /how-to-make/backend-requests.md: -------------------------------------------------------------------------------- 1 | # Справочник: Взаимодействие с бекендом 2 | 3 | в разработке 4 | -------------------------------------------------------------------------------- /how-to-make/css.md: -------------------------------------------------------------------------------- 1 | # Справочник: `CSS`-стили блока 2 | 3 | в разработке 4 | -------------------------------------------------------------------------------- /how-to-make/dependencies.md: -------------------------------------------------------------------------------- 1 | # Справочник: Зависимости 2 | 3 | в разработке 4 | -------------------------------------------------------------------------------- /how-to-make/enb.md: -------------------------------------------------------------------------------- 1 | # Справочник: Настраиваем сборку проекта 2 | 3 | в разработке 4 | -------------------------------------------------------------------------------- /how-to-make/i18n.md: -------------------------------------------------------------------------------- 1 | # Справочник: Локализация проекта 2 | 3 | в разработке 4 | 5 | 6 | -------------------------------------------------------------------------------- /how-to-make/modules.md: -------------------------------------------------------------------------------- 1 | # Модульная система YMaps Modules 2 | 3 | Что такое Модульная система? Зачем и для кого? Как этим пользоваться? 4 | 5 | На эти вопросы я давно хотел получить ответы, но стеснялся их задать. Если вы такой же робкий, этот документ для вас :) 6 | 7 | Что это? 8 | -------- 9 | Вы web-разработчик, пишете сайты; на страницах верстаете формы с контролами, 10 | различные компоненты, большие и маленькие; для большинства из них 11 | пишете клиентский `js`-код. Вот и продолжайте это делать. Пока у вас нет своего клиентского `js`-кода, 12 | модульная система вам не нужна. Никому не нужен пустой фантик без конфеты внутри. 13 | 14 | Модуль - это фантик, в который вы заворачиваете конфету. На фантике написано имя конфеты и больше ничего. Оберните 15 | код пяти своих контролов каждый в свой отдельный фантик, у вас получится 5 модулей — система из пяти **ваших** 16 | модулей. Конфеты ссыпают в корзинку для сладостей, откуда конфеты удобно таскать детям. Модульная система и есть 17 | такая корзинка. 18 | 19 | Зачем это и для кого? 20 | -------------------- 21 | Как вы связываете `js`-компоненты между собой? Например, вы объявляете на странице блок с формой авторизации, 22 | пишете для неё клиентский `js`-код, в котором описываете поведение формы: если нажали на кнопку сабмита — отправь 23 | авторизационные данные аяксом, а не через стандартный `form.submit()`. 24 | 25 | И предположим, в вашем проекте уже есть специальный компонент "кнопка", в котором реализовано всё поведение кнопки, 26 | например, "если меня нажали, выкинь вверх некое событие, чтобы форма могла это событие услышать и отреагировать нужным 27 | образом". 28 | 29 | То есть, ваша "форма" должна уметь найти кнопку и слушать на ней событие клика. 30 | 31 | Вопрос: а как вы определяете, что кнопка **уже готова** взаимодействовать с вашей формой? Наверное, в `js`-файле вы 32 | сначала описываете функциональность для кнопки, а ниже - для формы? А если js-код кнопки и формы лежат в разных 33 | файлах? Тогда вы так настраиваете сборку финального `js`-файла, чтобы всё равно код формы оказался описан ниже 34 | кода кнопки? 35 | 36 | Модульная система избавляет нас от этих трудностей. Она избавляет нас от головной боли отслеживать зависимости между 37 | компонентами, поддерживать руками эти зависимости в актуальном состоянии. 38 | 39 | Если вы оформляете форму в виде `ymaps-модуля`, вы просто декларируете зависимость от кнопки, и знаете, 40 | что код формы будет активирован _только тогда_, когда активирован код кнопки. Вам не нужно самому отслеживать 41 | готовность ваших компонентов взаимодействовать друг с другом. Это делает за вас модульная система. 42 | 43 | 44 | Как этим пользоваться? 45 | --------------------- 46 | Это болванка `ymaps-модуля`: 47 | 48 | ````javascript 49 | modules.define( 50 | 'A', 51 | ['B', 'C'], 52 | function(provide, b, c) { 53 | var a = {}; 54 | 55 | provide(a); 56 | } 57 | ); 58 | ```` 59 | 60 | В этом примере ваш собственный код - единственная строка: 61 | ````javascript 62 | var a = {}; 63 | ```` 64 | 65 | В реальности, конечно, вместо этой строчки вы напишете намного больше кода, но для знакомства с модульной системой 66 | этого достаточно. 67 | 68 | Всё остальное кроме этой строки и есть модуль. Модуль - это своего рода обёртка, фантик — несколько специальных 69 | инструкций, которые и делают из вашего кода модуль. Внутрь фантика вы складываете нужные ингредиенты (зависимости от 70 | других модулей), перемешиваете, как вам хочется (пишете код своего компонента), фантик скручиваете и втыкаете 71 | в бок зубочистку-флажок с именем модуля. Вы сделали модуль `А`. 72 | 73 | Давайте посмотрим на конфету внимательно. 74 | 75 | Так объявляется модуль. Позовите метод `define()` 76 | ````javascript 77 | modules.define( 78 | ); 79 | ```` 80 | 81 | Скажите модулю: "Тебя зовут `А`" 82 | ````javascript 83 | modules.define( 84 | 'A' 85 | ); 86 | ```` 87 | 88 | Потом добавьте: "Вообще-то ты зависишь от двух модулей, их зовут`В` и `С`" 89 | ````javascript 90 | modules.define( 91 | 'A', 92 | ['B', 'C'], 93 | ); 94 | ```` 95 | 96 | Опишите код модуля в анонимной функции. В него параметрами `b` и `c` придёт код модулей `B` и `С`, и вы сможете 97 | использовать их внутри модуля. 98 | ````javascript 99 | modules.define( 100 | 'A', 101 | ['B', 'C'], 102 | function(provide, b, c) { 103 | 104 | } 105 | ); 106 | ```` 107 | 108 | Чтобы другие модули могли использовать ваш модуль, пусть он громко произнесёт вслух: 109 | "Модуль `А` - это я!". Это делается вызовом функции `provide(а)`, в которую аргументом передаётся ссылка на ваш класс 110 | или объект, то есть на сам программный код вашего модуля. 111 | ````javascript 112 | modules.define( 113 | 'A', 114 | ['B', 'C'], 115 | function(provide, b, c) { 116 | var a = {}; 117 | 118 | provide(a); 119 | } 120 | ); 121 | ```` 122 | 123 | И, наконец, напишите свой компонент. Пусть это будет что-нибудь хорошее ;) 124 | ````javascript 125 | modules.define( 126 | 'A', 127 | ['B', 'C'], 128 | function(provide, b, c) { 129 | var a = 'Yandex is a good company'; 130 | 131 | provide(a); 132 | } 133 | ); 134 | ```` 135 | 136 | Всё. Сложности закончились :) 137 | 138 | А вот так, к примеру, может выглядеть код модуля, отвечающий за форму логина. Создадим фантик, 139 | позовём в него кнопку и что-нибудь запрограммируем внутри: 140 | ````javascript 141 | modules.define( 142 | 'form', 143 | ['button'], 144 | function(provide, button) { 145 | var form = getElementById('my-form'); 146 | form.on('submit', onSubmited); 147 | 148 | function onSubmited() { 149 | if ($(button).css('disabled')) { 150 | return false; 151 | } 152 | form.submit(); 153 | } 154 | 155 | provide(form); 156 | } 157 | ); 158 | ```` 159 | 160 | Это выдуманный пример, скорее всего он нерабочий, но для иллюстрации годится. В теле анонимной функции мы программируем 161 | поведение формы в зависимости от того, активна или неактивна кнопка. А после этого методом `provide(form)` 162 | выбрасываем вверх флаг: "Форма — это я". Теперь и этот модуль может быть вызван другими модулями. Метод `provide` - 163 | это выкинутая вверх рука и всемирно известное: "Свободная касса!" 164 | 165 | Возвращаясь к аналогии с конфетой, фантик — это метод `define()` с анонимной функцией внутри. В фантик воткнута 166 | зубочистка, за которую конфетку удобно взять — это метод `provide()`. Модуль независим и работоспособен, потому что 167 | он будет запущен только после того, как все зависимости будут предоставлены (правильные программисты используют 168 | термины "разрезолвлены" или "разрешены") внутрь модуля. 169 | 170 | Осталось сказать, что зависимости резолвятся асинхронно, дерево зависимостей строится в рантайме, 171 | модули можно переопределять и доопределять. 172 | 173 | Если захотите заглянуть в репозиторий модульной системы, проследуйте сюда: 174 | [https://github.com/ymaps/modules](https://github.com/ymaps/modules/blob/master/README.ru.md), и возвращайтесь к 175 | документации про `BEViS`, я вас там жду. 176 | -------------------------------------------------------------------------------- /how-to-make/mvc-app.md: -------------------------------------------------------------------------------- 1 | # Справочник: `MVC` в `BEViS` 2 | 3 | в разработке 4 | -------------------------------------------------------------------------------- /how-to-make/new-block.md: -------------------------------------------------------------------------------- 1 | # Справочник: Новый блок 2 | 3 | в разработке 4 | -------------------------------------------------------------------------------- /how-to-make/new-page.md: -------------------------------------------------------------------------------- 1 | # Справочник: Новая страница 2 | 3 | в разработке 4 | -------------------------------------------------------------------------------- /how-to-make/similar-block.md: -------------------------------------------------------------------------------- 1 | в разработке [немного информации из faq](https://github.com/bevis-ui/docs/blob/master/faq/bem-vs-bevis.md#%D0%9D%D0%B5-%D1%82%D0%BE%D1%82-%D0%B6%D0%B5-%D1%81%D0%B0%D0%BC%D1%8B%D0%B9-%D0%B1%D0%BB%D0%BE%D0%BA-%D0%B0-%D0%BD%D0%B5%D1%87%D1%82%D0%BE-%D0%B8%D0%BD%D0%BE%D0%B5) 2 | -------------------------------------------------------------------------------- /how-to-make/tests.md: -------------------------------------------------------------------------------- 1 | # Справочник: Тесты 2 | 3 | в разработке 4 | -------------------------------------------------------------------------------- /how-to-make/yblock.md: -------------------------------------------------------------------------------- 1 | # Справочник: `JS`-поведение блока 2 | 3 | в разработке 4 | 5 | -------------------------------------------------------------------------------- /manual-for-master.md: -------------------------------------------------------------------------------- 1 | #### Предисловие 2 | 3 | Методология ли это? Я имею в виду `BEViS`. Методология? Ну, можно и так, если очень хочется. Но мы так не считаем. 4 | «Методология» — затёртое до дыр понятие с десятком различных определений. Попробуй пойми, что имеет в виду автор WEB-фреймворка, 5 | когда называет свой проект методологией: «Учение о методах и средствах познания мира», «Учение об организации абстрактной 6 | деятельности» или вообще «Систему принципов и подходов научной исследовательской деятельности»? 7 | Методология сейчас — модное наукообразное слово, которое ничего не помогает понять, а только вводит в заблуждение. Buzzword. 8 | 9 | _Так что, нет, `BEViS` — не методология._ 10 | 11 | `BEViS` — эволюционная ветвь вёрстки `Абсолютно Независимыми Блоками`. Вы слышали о ней? Виталий Харисов, Яндекс, 2009 год — слышали? 12 | Мы переосмыслили `АНБ` и поняли, как эту концепцию можно улучшить, как можно её упростить. Бивис в первую очередь — это соглашения об именовании тегов 13 | и `CSS`-классов, о выделении логических частей внутри вашего сайта. Это рекомендации о том, 14 | как эффективно управлять кодом вашего проекта. Эти рекомендации призваны упорядочить хаос из тегов, 15 | стилей и скриптов, сделать ваш код стабильным, поддерживаемым и расширяемым. 16 | 17 | `BEViS — это АНБ, но со строгим API` 18 | 19 | Мы, разработчики `BEViS`, предлагаем вам думать о нашем продукте, так же, как вы думаете 20 | о `Webkit`-е, когда изредка вспоминаете, что `Webkit` — это вообще-то форк почившего в бозе `KHTML`, 21 | и даже не столько форк, сколько его эволюционная ветвь, то есть нечто большее, чем был `KHTML` изначально, 22 | нечто лучшее. И смотрите, ещё какой нюанс. Про `Webkit` сейчас знают все. А кто из вас вспоминает про `KHTML`? А про `АНБ`? ;) 23 | 24 | Но Бивис — это не только какие-то бестелесные советы и рекомендации. Это ещё и готовые инструменты, 25 | с которыми сверстать полноценный динамический сайт может даже подросток. Для этого не нужно учить `PHP`, `Ruby` 26 | или `Python`. Всё, что нужно для разработки полноценных сайтов на Бивисе, вы скорее всего знаете и так — `HTML`, `CSS`, 27 | `Javascript`. Скажите, заманчиво? ;) 28 | 29 | В добрый путь? :) 30 | 31 | ## Приложение 32 | 33 | Как работает обычное веб-приложение? Это зависит от языка, но схематично это выглядит примерно у всех одинаково: 34 | 35 | MVC 36 | 37 | Запрос из браузера попадает на некий `HTTP`-сервер. Это может быть `Apache` или `Nginx` или любой другой. 38 | Запрос направляется в контроллер — программу, написанную, например, на `PHP` или на `Python`, которая 39 | получает данные из бекенда (из `MySql` или серванта), и передаёт в шаблоны представления, которые и генерят конечный `HTML`. 40 | 41 | Эта схема известна каждому. Ну так мы её и не меняли. Мы поменяли технологии и шаблонизатор. 42 | 43 | MVC BEViS 44 | 45 | Вместо `Apache` или `Nginx` у нас `Node.js`. Вместо `PHP` мы пишем программу на серверном `JS`, в которой обращаемся в сервант за данными, 46 | накладываем на них шаблоны представления, которые тоже написаны на серверном `JS`, и отдаём пользователю конечный `html`. 47 | 48 | Видите — схема та же. Только всё на яваскрипте, на серверном яваскрипте. 49 | 50 | ---- 51 | 52 | Зачем изобретать новый велосипед, если есть `Smarty`, есть `Django`, есть `XSLT`, в конце концов? 53 | 54 | Две причины: простота и асинхронность. Простота в том, что сверстать приложение может любой школьник, который умеет на `HTML`/`CSS`/`JS` 55 | Ему не нужно учить новый язык программирования — ни `PHP`, ни `Ruby`, ни `Python`. Что ещё учить не надо? `Perl`, `Java`, `C#`, `XSL`, продолжайте. 56 | Я понимаю, что у каждого языка свои преимущества в определённых ситуациях. Но мы и не говорим, что они не нужны. Мы говорим, 57 | что типовые задачи веб-разработки можно успешно делать на серверном `JS`, который мы уже знаем. 58 | 59 | И вторая причина — асинхронность. Для больших проектов важна скорость. Да и просто это модно, а мы ребята модные :) 60 | 61 | Иными словами, мы захотели обрести полный контроль над фронтендом. Самое время! Есть все возможности! 62 | 63 | ---- 64 | 65 | Как выглядит наш контроллер `index.page.js?` 66 | Это набор `JS`-команд. Так как мы пишем на `Node.js`, наш яваскрипт завернут в `Node.js` модули. И выглядит это примерно так: 67 | 68 | ```javascript 69 | module.exports = function (pages) { 70 | 71 | pages.declare('index', function () { 72 | return [ 73 | { block: 'header' }, 74 | { block: 'authorization' } 75 | ]; 76 | }); 77 | 78 | }; 79 | ``` 80 | Если вы на `Node.js` не писали, не пугайтесь, не обращайте внимания на первую строку. Относитесь к ней, как к обвязке, без которой нельзя. 81 | Всё интересное происходит внутри. А внутри обычный `JS`. Видите вызов функции `pages.declare()`? 82 | 83 | В бивис-приложении есть объект `pages`, у которого есть метод `declare`. 84 | В нём мы декларируем, что `index`-страница будет состоять из двух компонентов — из шапки и формы авторизации. 85 | Выражаясь языком юристов, это «декларация о намерениях». 86 | 87 | Это абсолютно валидный `JSON`. Совершенно обычный `JSON`. Бивис создавался, как фреймворк, который использует валидные нативные конструкции. 88 | В этом `JSON` есть только одно служебное поле, оно называется `block`. Остальные поля можно называть как бог на душу положит. 89 | 90 | Почему блок? Зачем какой-то `JSON`? И что оно вообще такое? 91 | 92 | Подойдите вплотную к любой картине. Подойдите так близко, чтобы коснуться её носом. Что-то видно? 93 | Хаос из цветных мазочков. Ни черта не разобрать. 94 | 95 | Утро 96 | 97 | А теперь отойдите на несколько шагов и снова посмотрите на картину. 98 | О! Так это же Шишкин Иван Иванович с его знаменитыми мишками в сосновом бору. 99 | 100 | Утро 101 | 102 | И с вёрсткой так же. Когда мы получаем эскиз сайта от дизайнера, мы смотрим на него и 103 | видим не теги, которые будет в `HTML`-файле. Мы видим какие-то абстракции. Ага — это будет шапка, это подвал, а тут форма авторизации. 104 | 105 | Вы тоже, скорее всего, так делаете. Кто-то из вас называет такие абстракции модулями, кто-то компонентами или контролами. 106 | Мы их называем `блоками`, потому что мы из Яндекса, а там родилась концепция верстки абсолютно независимыми блоками, 107 | в которой впервые прозвучало слово `блок`. Ну и пусть, хороший же термин :) 108 | 109 | А если вы не верстальщик, а настоящий программист, то вам станет яснее, когда я скажу, что блоком мы называем 110 | примерно такую же абстракцию, как паттерн «Модуль» в `JS`. 111 | 112 | Эдди Османи [говорит](http://largescalejs.ru/module-pattern/): 113 | > «Модуль» — это популярная реализация паттерна, инкапсулирующего приватную информацию, состояние и структуру… 114 | 115 | 116 | ## Описание страницы обязано быть простым 117 | 118 | Бивис-блок — это очень простая декларация на `JSON`. Простота — это очень важно. 119 | Чем предмет проще, тем легче им пользоваться. Тем больше удовольствия от использования. 120 | 121 | Бабушке моей жены восемьдесят девять лет. Она не любит покупной хлеб, печёт сама. Мы подарили бабушке хлебопечку, чтобы ей было легче. 122 | 123 | Но она не пользуется! 124 | 125 | Оказалось, пожилому человеку трудно запомнить 126 | в какой последовательности какие кнопки нажимать — из инструкции старому человеку это 127 | непонятно. Она по-прежнему пользуется газовой духовкой — куда проще — открыл дверцу, сунул тесто, закрыл дверцу. 128 | 129 | Какой веб-фреймворк для верстки ни возьми, он в какой-то степени напоминает хлебопечку или пульт от телевизора. 130 | Вы, кстати, обращали внимание, какими кнопками на пульте пользуется ваша бабушка? Только тремя: включить, сделать громче и переключить канал. 131 | 132 | Мы с Маратом люди пожилые. И в отделе у нас работают ребята, которым уже за тридцать, а кому-то и под сорок, как мне :) 133 | Нам это всё надоело. Надоело описывать страницу тегами, размечать их кастомными атрибутами. Это неоправданно сложно. 134 | Слишком много суеты, слишком мало смысла. 135 | 136 | Поэтому вот так, мы решили, должно выглядеть описание страницы. Максимально просто, максимально плоско. 137 | ```javascript 138 | [ 139 | { block: 'header' }, 140 | { block: 'authorization' } 141 | ] 142 | ``` 143 | 144 | Здесь сказано, что на странице будет два блока — шапка и форма авторзации. 145 | Здесь нет ни одного тега, ни одного атрибута. Но такого описания достаточно, чтобы Бивис сгенерил полноценный 146 | развесистый `HTML`, например такой: 147 | 148 | ``` 149 |
150 | 153 |

Демо-страница

154 |

Слоган

155 | 156 | 157 | 158 |
159 |
160 | 161 | 162 | 163 | 165 | 168 |
169 | ``` 170 | 171 | Как мы этого добиваемся? Главное, что мы сделали — приняли закон, главенствующий приницип Бивиса. 172 | 173 | ####Чтобы пользоваться блоком, достаточно одного имени 174 | 175 | Чтобы на странице оказался блок, о нём нужно знать только его имя и больше ничего. Вот… вообще ничего. 176 | 177 | И эта та вещь в Бивисе, которой мы гордимся-гордимся. 178 | 179 | И это очень похоже на реальную жизнь. Ведь, 180 | когда я хочу позвать к себе в гости Марата, я делаю это очень просто — я зову его по имени и говорю: «Марат, 181 | а приходи к нам в гости сегодня, жена яблочный пирог испечёт, посидим». А у Марата жена и двое детей, 182 | к примеру. Я же не зову отдельно Марата, отдельно его жену и каждого ребенка по отдельности. Нет. Только его, 183 | а уж он сам приведёт всё своё семейство, будьте уверены. 184 | ```javascript 185 | { 186 | друг: 'Марат' 187 | } 188 | ``` 189 | 190 | И с блоком так же. Когда я хочу, чтобы на станице появился какой-то блок, Я НЕ ХОЧУ думать, о том, из каких тегов и атрибутов он внутри устроен. 191 | 192 | Ну, в самом деле, я когда в ресторан прихожу, и заказываю солянку, я же не даю указаний официанту: «Так, дорогуша, в солянке должно быть четыре кусочка картошки, 50 граммов салями, 193 | мелко нарезанный солёный огурчик…» Нет. Я просто заказываю солянку. Ну, максимум, могут предложить: «Вам со сметаной?» Да, конечно, мне со сметаной: 194 | 195 | ```javascript 196 | { 197 | суп: 'солянка', 198 | сметанаНужна: 'да' 199 | } 200 | ``` 201 | 202 | То есть могут ещё задать какой-то дополнительный параметр. Но при этом, обратите внимание, и без сметаны суп можно есть. Просто с ней 203 | возникает новый вкус. 204 | 205 | Командир в окопе кричит сержанту: «Сержант Петренко, прикрыть правый фланг!» 206 | 207 | ```javascript 208 | { 209 | сержант: 'Петренко', 210 | прикрыть: 'правый фланг' 211 | } 212 | ``` 213 | 214 | Всё! Командир приказал — Петренко сделал. И пусть Петренко сам решает, как это делать — то ли вести самому автоматный огонь 215 | в указанном направлении, то ли гранатами точку подавлять, то ли других бойцов послать штурмом фланг занять. Командиру 216 | какое дело до этих деталей вообще? 217 | 218 | Ну что, похоже на правду? 219 | 220 | ```javascript 221 | { 222 | block: 'header', 223 | showSearch: true 224 | } 225 | ``` 226 | 227 | Пусть блоки сами решают, в какой `HTML` они должны превратиться, чтобы лучшим образом выполнить свою задачу. 228 | Как они устроены внутри, я как пользователь блока, думать не хочу. 229 | 230 | И за это я люблю Бивис. Создавать страницы из готовых блоков никогда ещё не было так просто. 231 | 232 | ####Блок — структура-представление-поведение 233 | 234 | Да и сами блоки создавать тоже просто, потому что это очень жизненная абстракция. 235 | 236 | Любое изобретение человека существует долго только в том случае, когда оно не противоречит законам природы. 237 | 238 | Квадратное колесо, например, не прижилось, хотя человек пытался наладить его промышленный выпуск. Пытался. А почему не попытаться-то? 239 | Квадратное легче производить, его удобнее транспортировать к точкам продажи. Но квадратное не может катиться. Это закон физической природы. 240 | 241 | Всё, что создано человеком и смогло сохраниться веками — это то, что следует законам природы. 242 | 243 | Блок, как божья тварь, состоит из трёх начал. Чтобы было понятнее, представьте меня в виде модуля, в виде блока: 244 | 245 | 1. У меня есть скелет, мышцы и внутренние органы. Это моя структура. У блока эту роль выполняет `HTML`-разметка. 246 | 247 | 2. У меня есть какой-то рост, вес, цвет кожи и выражение лица. Это мой внешний вид. У блока эту роль выполняют `CSS`-стили. 248 | 249 | 3. Я сейчас размахиваю руками и говорю с вами со сцены. Это моё поведение. Блок тоже умеет как-то взаимодействовать с пользователем на странице. 250 | 251 | Меня можно позвать в гости, меня можно накормить солянкой, можно отослать меня в окоп. 252 | Но при этом внутри себя я очень сложный. Кому-то пришлось потрудиться, чтобы я был таким, как есть. 253 | 254 | И разработчику блока тоже нужно потрудиться. Чтобы мне в ресторане принесли солянку, нужно чтобы кто-то заранее её сварил, правильно? 255 | 256 | Если я приду домой, и как в ресторане заявлю жене, что хочу на ужин борщ с фасолью и сметаной, знаете, что я услышу в ответ? 257 | 258 | — Легко! Картошку почисть! 259 | 260 | И я, кстати, не против. Жене по хозяйству нужно помогать. Я в этом убеждён твёрдо. 261 | 262 | Чтобы в проекте появился блок, который легко позвать на страницу, нужно этот блок создать — его структуру, внешний вид и поведение. В Бивисе это делается легко. 263 | 264 | ### Императивные шаблоны 265 | 266 | Как мы генерим `HTML` в смарти, джанго и других `MVC`? 267 | 268 | Мы пишем шаблоны, которые выглядят, как `HTML`-код, в который точечно добавляются данные. Как будто шприцом обкалываем: 269 | ```html 270 |

Уважаемый {{ person }}

271 |

Ваш заказ от {{ date:"F j, Y" }} принят на обработку.

272 |

Пожалуйста, убедитесь, что всё выбрано верно:

273 | 274 | 279 | 280 | {% if ordered_warranty %} 281 |

Гарантия - 12 месяцев.

282 | {% else %} 283 |

Со всеми неполадками обращайтесь в наш сервисный центр.

284 | {% endif %} 285 | ``` 286 | 287 | Я напомню, шаблоны эти пишутся вот в этом участке схемы. 288 | 289 | Утро 290 | 291 | И в случае с `PHP/Smarty` пишутся в файле с расширением `*.tpl.php` 292 | 293 | Утро 294 | 295 | В бивисе мы шаблоны пишем в этом же месте схемы. Отличия только в том, что мы пишем шаблоны в файлах с расширением `*.bt.js` 296 | и шаблоны пишем не такие, а другие. Декларативные. 297 | 298 | ### Декларативные шаблоны 299 | 300 | На `XSLT` писали? `XML` в `HTML` трансформировали? Тогда вы нас понимаете — шаблоны как в `XSLT` 301 | А кто не писал на `XSL`, сейчас поймёте. Это легко. 302 | 303 | Что это? 304 | 305 | ```css 306 | h1 { 307 | color: red; 308 | } 309 | ``` 310 | 311 | Даже школьник ответит: «Это `CSS`-селектор». Ну тогда поздравляем, вы умеете писать декларативные шаблоны. 312 | 313 | `CSS`-селекторы — это пример самых настоящих декларативных шаблонов. Заголовок станет красным только тогда, когда он есть на странице. Если h1 на 314 | странице нет, то и красить нечего, правда? 315 | 316 | В этом вся соль декларативных языков программирования. Вместо инъекций шприцем (как в смарти-шаблонах) — только матчеры, 317 | которые применяются к данным, если они есть. Это как горчичники — если есть куда их приложить — доктор их обязательно приложит. 318 | Горчичники заматчатся или не заматчатся. 319 | 320 | Предположим, наш контроллер выглядит так — на странице мы хотим видеть одну только шапку. Больше никаких блоков. 321 | 322 | ```javascript 323 | module.exports = function (pages) { 324 | 325 | pages.declare('index', function () { 326 | return { 327 | block: 'header' 328 | } 329 | }); 330 | }; 331 | ``` 332 | 333 | Фактически, мы видим декларацию только одного блока. Не смотрите на всю мишуру вокруг, сконцентрируйтесь только на этом `JSON`: 334 | 335 | ```javascript 336 | { 337 | block: 'header' 338 | } 339 | ``` 340 | 341 | Мы хотим, чтобы из этого `JSON`-а на странице появился такой `HTML`: 342 | ```html 343 |
344 | ``` 345 | 346 | Декларативный шаблон мог бы выглядеть почти, как в `CSS` 347 | 348 | ```javascript 349 | header { 350 | tag: div; 351 | } 352 | ``` 353 | 354 | Этот шаблон бы читался так: если на странице объявлен блок header, вылей для него тег div. 355 | В реальности, мы почти так и пишем, только на чистом валидном `JS`: 356 | 357 | Это шаблон на Бивисе: 358 | ```javascript 359 | bt.match('header', function (ctx) { 360 | ctx.setTag('div'); 361 | }); 362 | ``` 363 | 364 | Похоже на это, правда? 365 | ```javascript 366 | header { 367 | tag: div; 368 | } 369 | ``` 370 | 371 | Шаблон записывается в виде функции `match`, в которую мы первым параметром передаём имя блока. 372 | А вторым аргументом пишем функцию, внутрь которой приходит исходный `btjson` в переменной `ctx` 373 | А в функции сказано — создай тег `div` 374 | 375 | Функция `match` — это метод шаблонизатора `bt`. `bt` — это и есть повар в ресторане или ваша жена на кухне. 376 | У шаблонизатора прекрасная документация, которую поймёт даже школьник, почитайте. 377 | 378 | Так что же такое «матчинг», наложение шаблонов? 379 | 380 | В `CSS` такой шаблон вы знаете под именем «селектор» (от английского to select), потому что селектор как бы 381 | выбирает блоки, к которым нужно применить стилевые свойства, а в других декларативных языках эта же 382 | декларация зовётся «шаблоном» (трудно сказать почему). И ещё говорят, что шаблон матчится на 383 | блок (от английского to match, что переводится, как «подбирать», «приводить в соответствие»). 384 | Кстати to select имеет те же самые значения, что и to match. То есть это равнозначные слова с одинаковым смыслом :) 385 | 386 | Матчинг — это и есть селекция, выбор, проверка совпадения условий в селекторе (матчере). 387 | То же самое, что вы делаете, когда пишете `CSS`-стили. 388 | 389 | Только эти шаблоны вы пишете не в `CSS`-файле, а в `index.bt.js`. 390 | Посмотрите ещё раз на схему, чтобы сориентироваться. 391 | 392 | MVC BEViS 393 | 394 | А теперь вернёмся к шаблону 395 | ```javascript 396 | bt.match('header', function (ctx) { 397 | ctx.setTag('div'); 398 | }); 399 | ``` 400 | 401 | Он довольно бесполезный. Чтобы вылить тег, столько всего пришлось написать. 402 | Давайте усложнять. Я хочу, чтобы внутри шапки появился заголовок. 403 | 404 | Я добавил комментарии в код, чтобы вы всё поняли, а я помолчу. 405 | 406 | ```javascript 407 | bt.match('header', function (ctx) { 408 | ctx.setTag('div'); 409 | 410 | // Генерим внутри шапки новый btjson и устанавливаем его, как содержимое шапки 411 | ctx.setContent({ 412 | elem: 'title', // указываем элемент title в качестве содержимого 413 | text: 'CodeFest 2014' // произвольный текст для заголовка 414 | }); 415 | }); 416 | 417 | // регистрируем матчер для элемента title блока header 418 | bt.match('header__title', function (ctx) { 419 | ctx.setTag('h1'); 420 | 421 | // Получаем параметр text 422 | var text = ctx.getParam('text'); 423 | 424 | // задаём содержимое заголовку 425 | ctx.setContent(text); 426 | }); 427 | ``` 428 | 429 | Результат: 430 | ```html 431 |
432 |

CodeFest 2014

433 |
434 | ``` 435 | 436 | Но это синтетический пример. 437 | 438 | А вот вам живое демо. 439 | 440 | [слайд](http://bevis-ui.github.io/bevis-and-bt-speech/?full#28) 441 | 442 | Поиграйтесь и понаблюдайте, как наличие того или иного параметра влияет на внешний вид блока. 443 | Это возможно благодаря простому вызову блока и декларативным шаблонам. Конечно, этого же можно добиться и с помощью императивных 444 | шаблонов. И тут возникает вопрос: «Императивные шаблоны Смарти и Джанго привычные, понятные, их понимают все. 445 | Почему мы не используем их?» 446 | 447 | #### Чем декларативные шаблоны лучше? 448 | 449 | Императивные шаблоны остаются понятными, пока их немного. 450 | Когда проект разрастается, все больше и больше визуальных фрагментов переиспользуются, тогда шаблоны становятся сложными, запутанными и многоуровневыми. 451 | 452 | В такой ситуации BT-шаблоны позволяют сохранять сложность на одном уровне. 453 | Более того, BT-шаблоны позволяют нам иметь явный и понятный API каждого фрагмента страницы. 454 | 455 | Наш подход хорош тем, что на странице нет описания того, КАК должен выглядеть блок, а только само указание, что «здесь есть такой-то блок и 456 | у него такие-то параметры». Это же прям как в настоящем программировании — вызов класса с передачей в него параметров. 457 | Класс выставляет наружу интерфейсные методы, которые не меняются и поддерживаются пожизненно, а внутрення реализация класса скрыта от глаз, 458 | находится в приватной области. 459 | 460 | Мы создали то же самое для `HTML`-верстки. 461 | 462 | `BTJson` блока — это его внешнее АПИ, а `bt`-шаблоны — это приватная реализация блоков. 463 | 464 | Ощутите мощь этой идеи на примере. 465 | 466 | ```javascript 467 | { 468 | block: "y-header", 469 | 470 | view: "islet-search", 471 | showSearch: true, 472 | showSuggest: true, 473 | } 474 | ``` 475 | 476 | При такой простой декларации шапки, конечная `HTML`-верстка может какой угодно сложной. 477 | 478 | ```html 479 | 512 | ``` 513 | 514 | Но самое ценное в том, что отныне можете менять шаблоны, генерирующие `HTML` как угодно часто, вам не нужно при этом переписывать 515 | все страницы, и бояться, что на какой-то из них что-то пойдёт не так. АПИ блока гарантирует результат. 516 | 517 | Возможно, кто из вас читал про Web Components. 518 | 519 | Бивис реализует именно эту же идею. Но отличие в том, что Web Components пока ещё не 520 | поддерживаются браузерами, а бивис вы можете использовать уже сегодня. 521 | 522 | Кто-то может возразить, что мы пишем больше кода — каждый декларативный шаблон нужно явно описать, там задать какой тег выливать, 523 | какие элементы внутри блока генерить. Ну так вы же на Смарти шаблонах тоже всё это пишете, только в виде чистого `HTML`. А мы пишем это на `JS`. 524 | 525 | Вы скажете, что мы пишем куда больше букв и в доказательство приведёте эти две строки. 526 | ```javascript 527 | ctx.setTag('div'); // 18 символов 528 | ``` 529 | 530 | ```html 531 |
532 | ``` 533 | 534 | Да, наша запись на семь символов длиннее, но только при условии, что вы руками пишете каждый символ, когда пишете теги. 535 | Вы же не в `notepad.exe` код пишете, надеюсь? 536 | 537 | Вы пользуетесь аутокомплитом, то есть написали открывающую скобку, затем букву `d` и нажали хоткей для аутокомплита. 538 | Или если [Дзен-кодингом](http://pepelsbey.net/2008/08/zen-html/) набираете `HTML`, который придумал Вадим Макеев, а разработал Сергей 539 | Чикуёнок, и теперь он называется [Emmet](http://emmet.io/), тогда вы вообще пишете три символа `div` и нажимаете `tab`. 540 | 541 | Ну, а мы пользуемся сниппетами. Нажал хоткей, вписал только три символа `div` и у нас готова строка ```ctx .setTag ('div');``` 542 | 543 | Я веду к тому, что вы пишете императивные шаблоны, а мы декларативные, и прикладываем мы с вами сравнительно одинаковое количество сил для написания шаблонов. 544 | 545 | ## CSS блока 546 | 547 | Тему стилей мы начнём с заявления, с которым многие из вас будут категорически не согласны. Но тем интереснее, правда? 548 | 549 | ### Смешивание блоков 550 | 551 | Мы убеждены, что имя у блока должно быть одно. Мы не согласны с теми из вас, кто использует в верстке такой приём: 552 | 553 | ``` 554 |
555 | ``` 556 | 557 | Что это — шапка и форма авторизации одновременно? Это как? Я и папа, я и мама? 558 | 559 | Вы смотрели фильм Георгия Данелия «Мимино«? 560 | 561 | Мимино, главный герой фильма, грузинский летчик из горного селения. Играет Вахтанг Кикабидзе. 562 | По сценарию Мимино захотел работать в большой авиации, прилетел в Москву, найти работу в Аэрофлоте. Жить ему негде, в гостиницах мест нет. 563 | Это происходило во времена CCCP. Но знакомые знакомых договорились по блату поселить его в гостиницу, в которой как раз проводится всесоюзный слёт врачей-эндокринологов. 564 | И чтобы Мимино заселили, ему нужно было на ресепшине солгать, что он тоже врач-эндокринолог. 565 | 566 | И вот представьте. Мимино приходит в гостиницу. Синяя форма лётчика. Выглаженные стрелки на брюках. Погоны. Фуражка с эмблемой авиации. 567 | И когда его удивлённо спрашивают «Вы, что — лётчик?», знаете, что он отвечает? 568 | 569 | — Да. Иногда... Вообще-то, я эндокринолог. 570 | 571 | Хороший летчик не может быть одновременно хорошим эндокринологом. Попробуй совмещать обе профессии — сплошные проблемы, 572 | такова жизнь, не нам с ней спорить. Я, кстати, пробовал быть врачом и программистом одновременно ;) 573 | 574 | В верстке с множественными именами у блоков сплошные хаос и боль, потому что в `CSS` нет надёжного механизма разруливания весов 575 | у стилевых селекторов с одинаковой специфичностью: какой селектор позже записан, тот и применится к тегу. 576 | 577 | Если для блока с `class="header authorization"` написать селектор `.header {border-color: red}`, а до него другой `.authorization {border-color: green}`, 578 | то блок окажется окружён красным бордером. А если вдруг селекторы придут в обратном порядке — вокруг блока будет зелёный бордер. 579 | А приходить они могут в разной последовательности, когда у вас динамическое построение `CSS`-файла. 580 | 581 | Когда мы смешиваем два блока на одной `HTML`-ноде, нет способа влиять на эту ситуацию, нет возможности контролировать её. 582 | Управлять ей. Точнее, он есть, но он вообще не гарантирует результат — надо повышать специфичность селекторов или загружать стилевые селекторы в «правильном» порядке, 583 | собирать `CSS`-файл в «правильном» порядке. 584 | 585 | А правильный — это какой? Как понять, в каком порядке, если есть возможность смешать два произвольных блока в произвольном порядке? 586 | А если три блока? Или четыре? Количество сочетаний блоков на одной простой странице исчисляется тысячами. 587 | Точно ли разработчик сможет учесть всё многообразие сочетаний? Сможет? 588 | 589 | В `BEViS` мы отказались от смешивания блоков друг с другом. Нет миксов блоков на одной ноде — нет конфликтов. 590 | 591 | Бивис-блок имеет всегда только одно имя. Всегда одно. Это важно. 592 | 593 | ### Имя и фамилия 594 | 595 | Имя у блока может быть простым — просто Марат, или просто Вадим, или просто header. Но бывают случаи, когда оно должно быть более точным. 596 | Например, Марат Дулин, или Вадим Макишвили. Чтобы добавить в имя уникальности. 597 | 598 | Кого из вас зовут Вадим? 599 | 600 | Представьте, вы сидите в кино, и вдруг в дверь вбегает кто-то запыхавшийся и кричит на весь кинотеатр: 601 | «Вадим, у тебя в продакшене 404 на главной странице!» 602 | 603 | Сколько Вадимов встрепенётся и занервничает? Да, все Вадимы. Но если он крикнет «Вадим Макишвили», то 604 | встрепенусь только я, а остальные Вадимы лишь недовольно оглянутся на шум. 605 | 606 | Жизнь доказывает, что имени и фамилии достаточно для идентификации человека. 607 | 608 | Когда мы зовём block: 'header', мы зовём наш блок по имени. Но если мы хотим добавить ещё больше уникальности, мы добавляем здесь 609 | же «фамилию», которую у блока мы именуем словом `view` 610 | 611 | ```javascript 612 | { 613 | block: 'header', 614 | view: 'search' 615 | } 616 | ``` 617 | 618 | Читать это следует так: создай на странице шапку, да не простую, а поисковую. 619 | Получится такой HTML: 620 | 621 | ```html 622 | 625 | ``` 626 | 627 | Понимаете, блок один — шапка, но на одной странице она может быть поисковая, а на другой непоисковая. 628 | Поэтому у неё есть два варианта отображения на странице, два представления, две темы, два скина, две шкурки... 629 | два `View`. Разный вид одного и того же блока мы называем разным представлением. Разным `View`. 630 | 631 | Но имя у блока, как вы видите, у нас по-прежнему одно. 632 | 633 | #### Не смешивай несмешиваемое 634 | Не знаю, как у вас в проектах, в Яндексе довольно часто можно встретить такие записи: 635 | ```html 636 |
кнопка
637 | ``` 638 | 639 | Есть некий базовый класс и несколько модификаторов, каждый добавляет к базовому классу что-то или переопределяет 640 | какие-то стили из базового класса. 641 | 642 | Чем это опасно? 643 | Вы добавляете или изменяете какое-то свойство в одном из этих трёх классов, у вас нет никакой гарантии, что 644 | ваши исправления не придут в конфликт весов со стилями других классов на этой ноде. 645 | 646 | В большом проекте даже после небольших изменений в таких стилях хорошо бы перетестировать весь проект с нуля, 647 | чтобы убедиться, что верстка не сломалась в тех блоках, где есть множественные классы. Проблема же в том, что нет 648 | другого механизма, кроме ручного перебора каждой страницы. А если у вас Single Page Application, часть блоков не видна, 649 | потому что строятся по данным, которые запрашиваются аяксом... В общем, верстка становится нестабильной. 650 | 651 | И это совсем не похоже на независимые блоки. Какая же это независимость, если даже внутри одного блока могут 652 | родиться проблемы отображения? Трудно себе даже представить потенциальные конфликты в стилях, если мы смешаем 653 | два разных блока на одной ноде. 654 | 655 | Что делает Бивис? 656 | Мы в такой ситуации рассуждаем так: если в проекте нужна кнопка с темой normal и тенью, то должен существовать отдельный класс, 657 | в котором сразу будет и тема normal и тень. В одном классе. Поэтому та запись с тремя классами превращается в один класс: 658 | 659 | ```html 660 |
кнопка
661 | ``` 662 | 663 | То есть мы делам блок button с `view: "normal-shadow"`. В `JSON` это записывается так: 664 | 665 | ```javascript 666 | { 667 | block: 'button', 668 | view: 'normal-shadow' 669 | } 670 | ``` 671 | 672 | А уже внутри этого вью мы смешиваем все эти разновидности кнопки и разруливаем все конфликты. 673 | И делаем это с помощью препроцессора. 674 | 675 | Кто сейчас не знает, что такое `CSS`-препроцессор? Все знают. 676 | 677 | Кроме меня. Я относился к `CSS`-препроцессорам с брезгливостью. «Баловство это», — так я считал, — «Тот же `CSS`, только с синтаксическим сахаром.» 678 | А сахар я не люблю, кофе могу пить и несладкий. 679 | 680 | Только оказалось, что сахар в препроцессорах — это не то, ради чего они нужны. 681 | Мы с их помощью делаем верстку гибкой и модульной. 682 | 683 | Как мы решаем задачу? 684 | 685 | ### Публичные селекторы и приватные функции 686 | 687 | ```css 688 | .button { 689 | skin-common() 690 | skin-theme-normal() 691 | } 692 | 693 | .button_normal-shadow { 694 | skin-common(); 695 | skin-theme-normal(); 696 | skin-shadow(); 697 | } 698 | ``` 699 | 700 | Мы создаём два селектора — `.button` и `.button_normal-shadow`. Они не зависят друг от друга. Каждый — самодостаточный. 701 | В каждом из них я зову некоторые функции, как мы это делаем в `JS`. 702 | 703 | Это своего рода геттеры — функции, которые что-то возвращают вместо себя. 704 | В Stylus-препроцессоре они называются миксины. Примеси. Внутри миксинов мы описываем стилевые правила. 705 | 706 | ```css 707 | skin-common() { 708 | /* общие стили для кнопки */ 709 | } 710 | 711 | skin-theme-normal() { 712 | /* цветовая тема */ 713 | } 714 | 715 | skin-shadow() { 716 | /* стили добавляют тень */ 717 | } 718 | 719 | ``` 720 | 721 | Мощь в том, что эти функции приватны. Они не попадают напрямую в скомпилированный `css`-файл. 722 | Пока из конкретного селектора не вызову этот миксин, его содержимое нигде не светится. 723 | 724 | ### Все конфликты под контролем 725 | 726 | И здесь же, на этапе препроцессора мы можем разрулить все конфликты между правилами разных миксинов, если они возникнут. 727 | 728 | ```css 729 | .button_normal-shadow { 730 | skin-common(); 731 | skin-theme-normal(); 732 | skin-shadow(); 733 | 734 | /* а здесь разрулил все конфликты этих миксинов */ 735 | } 736 | ``` 737 | 738 | То есть мы решаем все конфликты на этапе разработки, а не на этапе использования. 739 | В браузер пойдёт для одного блока один css-селектор. Гарантированно работающий. 740 | 741 | Идею так использовать препроцессор мы позаимствовали у Романа Комарова. Он предложил такую же схему для своего `CSS`-фреймворка Stylobat. Рома — это тот парень, 742 | который является сейчас мейнтейнером препроцессора Stylus. 743 | 744 | Рома, спасибо тебе большое! 745 | 746 | Что мы имеем на выходе: один блок имеет один `CSS`-класс. Мы по-прежнему утверждаем, что блок — это просто, одного имени достаточно. 747 | 748 | ## JS-поведение блока 749 | 750 | Как мы пишем `JS` для блоков на странице? 751 | 752 | В маленьких проектах мы делаем так: 753 | 754 | ```javascript 755 | $( document ).ready(function() { 756 | 757 | var form = $('#my-form'); 758 | form.submit(onSubmited); 759 | 760 | function onSubmited() { 761 | if ($('#my-form .button').hasClass('disabled')) { 762 | return false; 763 | } 764 | } 765 | 766 | }); 767 | ``` 768 | 769 | Для каждого блока мы описываем, что он должен делать. 770 | Здесь же описываем реакции на пользовательские события. 771 | 772 | В бивисе мы делаем то же самое, только не внутри ```$( document ).ready()```, а внутри модуля. 773 | 774 | Модульные системы — это и `RequireJS`, и `CommonJS`. 775 | Модуль — это очень простая идея. Это специальный `JS`-объект, внутрь которого мы вкладываем свой программный код. 776 | Ваш код будет работать, как и прежде, в нём ничего не меняется. 777 | 778 | Модуль нужен только для того, чтобы обеспечить взаимодействие с другими фрагментами вашего кода, другими вашими модулями. 779 | Модуль, как кассир в Макдональдсе — когда он готов работать, он вскидывает руку: «Свободная касса!» 780 | 781 | Когда модуль загрузился в память, он сообщает об этом в модульную систему. И другие модули с этого момента могут использовать ваш 782 | код из этого модуля. Прям как по `domReady`. 783 | 784 | У нас есть своя модульная система, она асинхронная на всех этапах своей работы. 785 | Примерно так выглядит описание модуля в терминах модульной системы. 786 | 787 | ```javascript 788 | modules.define( 789 | 'form', 790 | ['jquery'], 791 | function(provide, $) { 792 | 793 | var form = $('#my-form'); 794 | form.submit(onSubmited); 795 | 796 | function onSubmited() { 797 | if ($('#my-form .button').hasClass('disabled')) { 798 | return false; 799 | } 800 | } 801 | 802 | provide(form); 803 | } 804 | ); 805 | ``` 806 | Сам код остался тем же, обвязка немного поменялась. 807 | 808 | Мы объявили модуль в первой строке. 809 | Во второй мы дали модулю имя. 810 | В третьей описали зависимости — в нашем случае это только `JQuery` 811 | В четвёртой описали содержимое модуля. Именно в этой функции и происходит программирование поведения блока. 812 | И пред-предпоследняя строка — это и есть вскинутая рука: «Свободная касса». Модуль form готов к работе. Так другие модули понимают, что могут использовать этот модуль. 813 | 814 | Обратите внимание, мы не используем напрямую `JQuery`. 815 | Мы используем свой базовый класс для написания блоков. Это такой `JS`-класс, 816 | который позволяет оперировать в `JS` именно нашими абстрактными блоками, а не тегами. Если нам надо найти блок, или элемент блока, мы делаем это не через dom-методы, типа `getElementById()`, 817 | а нашими методами. 818 | 819 | ```javascript 820 | modules.define( 821 | 'form', 822 | ['button', 'input', 'y-block'], 823 | function(provide, button, input, YBlock) { 824 | 825 | var Form = inherit(YBlock, { 826 | __constructor: function () { 827 | this._submitButton = YButton.find(this); 828 | this._bindTo(this.getDomNode(), 'submit', this._onSubmited); 829 | 830 | }, 831 | 832 | _onSubmited: function (e) { 833 | if (this._submitButton.isDisabled()) { 834 | e.preventDefault(); 835 | } else { 836 | this.emit('submit'); 837 | } 838 | } 839 | }); 840 | 841 | provide(Form); 842 | } 843 | ); 844 | ``` 845 | 846 | Зачем оно такое надо? Модули и свой `JS`-класс для блоков? Почему не просто `jQuery`? 847 | 848 | Во-первых, мы получаем код, который можно вновь и вновь использовать. 849 | 850 | Во-вторых, мы получаем абстракцию над `HTML`-структурой блока. Нам не важно, как блок представлен на странице. Порой нам даже не важно, где он лежит — в body или в каком-то другом элементе. 851 | Мы переходим от DOM-взгляда на компонент на другой. Мы смотрим на блоки с точки зрения семантики их работы. 852 | 853 | И ещё это нужно, чтобы максимально разделить визуальное отображение и поведение. У Бивиса получилось. 854 | 855 | Когда бивис сгенерит `HTML` для шапки, вы увидите там такое: 856 | ```html 857 |
860 |
861 | ``` 862 | Слово `header`, записанное в атрибуте class участвует исключительно в стилях. 863 | А то, что написано в `data-block` — именно по этому атрибуту Бивис находит шапку и назначает ей `JS`-поведение. 864 | 865 | В бивисе блок может иметь один `CSS`-класс, а дата-атрибут совершенно другой. То есть вы можете спокойно удалить атрибут — 866 | поведение вы потеряете, но внешний вид блока не сломается. 867 | 868 | Состояние блока — это единственное место, где поведение и отображение пересекаются. Этого никак не избежать, потому что и в жизни оно так устроено. 869 | 870 | Когда я бегаю по стадиону, я краснею и потею. Меняется мой внешний вид. Когда я плаваю долго в море, кожа на пальцах сморщивается и бледнеет. 871 | У нас есть знакомые, которые так определяют, можно ли ребенку ещё плавать в бассейне: 872 | 873 | — Сын, у тебя там кожа на ладошках уже состарилась? 874 | 875 | — Нет, пап, не состарилась! 876 | 877 | А сам тем временем прячет за спину помятые ладошки. 878 | 879 | В блоке тоже самое. Возьмём ту же форму авторизации. По нажатию на кнопку сабмита надо проверить поле логина. Если пустое — обвести поле 880 | логина стыдливой красной рамкой. То есть изменить отображение блока, сменить его состояние с обычного на тревожное. 881 | 882 | Из `JS` мы добавляем дополнительный класс, который отвечает за изменение внешнего вида в состоянии «тревога». 883 | 884 | Вот что произойдёт в `HTML`-элементе блока 885 | ```html 886 | 887 | ``` 888 | 889 | а вот что мы заранее напишем в стилях 890 | ```css 891 | /* обычное состояние */ 892 | .login { 893 | ... 894 | } 895 | 896 | /* тревожное состояние */ 897 | .login._unfilled { 898 | border: 1px solid red; 899 | } 900 | ``` 901 | 902 | И когда мы на клиенте проверим, что поле логина пустое, мы добавим в ноду к инпуту класс «_unfilled» 903 | Мы называем такие классы состояниями. State 904 | Имя стейта может быть любым, какое вы выберете. Вам всего лишь нужно в стилях предусмотреть, что 905 | если `JS` добавит такой-то стейт-класс, чтобы в отображении что-то изменилось. 906 | 907 | Почему класс начинается с подчёркивания? Чтобы подчеркнуть, что он приватный для блока и чтобы легче его замечать. 908 | 909 | Теперь понимаете, почему мы назвали наш подход BEViS? 910 | ``` 911 | B — [B]lock 912 | E — [E]lement 913 | Vi— [Vi]ew 914 | S — [S]tate 915 | ``` 916 | 917 | ## Сборка статики без боли 918 | 919 | Мы почти закончили. Осталось сказать, что конечно же мы не пишем все стили в одном файле, все шаблоны в одном файле, 920 | весь клиентский `JS` в одном файле. Каждый блок представлен на файловой системе своей папкой, это удобно и очень по-человечески. 921 | 922 | Ведь у меня, как и у любого человека, есть дом — место, откуда я выхожу утром на работу и куда возвращаюсь каждый вечер. 923 | Ну, хорошо, это не дом. Это квартира в многоэтажке. 924 | 925 | У блока тоже есть свой дом — место, где блок отдыхает и ждёт, когда его «позовут поработать» в веб-странице. 926 | В корне проекта есть директория `/blocks`, в ней живут все блоки. Только в ней. Это дом блоков. Не ищите других мест. 927 | Если блока здесь нет, больше нет нигде. 928 | 929 | ```` 930 | /blocks 931 | /header 932 | header.bt.js 933 | header.styl 934 | header.js 935 | /form 936 | form.bt.js 937 | form.styl 938 | form.js 939 | ```` 940 | 941 | Каждый блок хранится в своей собственной папке, где шаблоны для генерации `HTML` описаны в `*.bt.js`, стили в `*.styl`, клиентский яваскрипт в `*.js`. 942 | Эффективно, хотя и не ново. Когда моя дочка готовится к математике, она на парту выкладывает учебник по математике, тетрадь по математике, 943 | угольник и карандаш — всё, что нужно именно для урока математики. У нас так же — всё, что нужно для блока находится в одной папке. 944 | 945 | А когда учитель хочет проверить у всего класса домашнюю работу по математике, она ходит по рядам и собирает тетради в стопку. 946 | У нас тоже есть такой сборщик. Когда в сервер приходит запрос на страницу, собирается не только `HTML`, но и `CSS`, и `JS`. Это всё делает 947 | сборщик. Почему не Ant, Make, Grunt? 948 | 949 | Сборка часто является узким местом в сложных компонентных системах, но в `BEViS` мы полностью интегрировали сборку в процесс разработки. 950 | Работая над проектом достаточно лишь обновлять страницу в браузере, чтобы всегда видеть актуальные стили, скрипты и верстку. Но не по этой причине мы писали свой сборщик :) 951 | 952 | Наш [ENB](https://github.com/enb-make/enb)-сборщик отличается огромной скоростью сборки для проектов с блочной структурой. 953 | Он собирает проекты со скоростью пули. Вот та самая причина! 954 | 955 | И теперь, когда мы знаем всё это, давайте посмотрим на схему. Давайте свяжем все ниточки в один клубок: 956 | 957 | 1. В адресную строку в браузере пользователь вводит `http://my-server.ru/`. 958 | 959 | 2. Браузер отправляет `HTTP`-запрос к серверу и ждёт. 960 | 961 | 3. ``Node.js``-сервер принимает запрос. Понимает, что нужно создать 962 | страницу `index.html` и открывает на чтение файл `/pages/index/index.page.js` 963 | 964 | 4. Выполняет там `JS`-инструкции, а именно: ходит за данными в бекенд, формирует декларацию страницы из `btjson`, 965 | анализирует каждый элемент `btjson`-массива и запоминает из каких блоков будет построена будущая `HTML`-страница. 966 | 967 | 5. После этого сервер запускает `ENB`-сборщик. На этом этапе, когда сервер 968 | принял запрос, `ENB` начинает с того, что создаёт пустые файлы 969 | * `/pages/index/index.styl`, 970 | * `/pages/index/index.js`, 971 | * `/pages/index/index.bt.js` 972 | 973 | Если такие файлы уже есть, `ENB` пропускает этот шаг и переходит к следующему. 974 | 975 | 6. Дальше `ENB` получает от сервера на вход имя первого блока — "header". `ENB` идёт на файловую систему проекта, находит директорию `/blocks/header` и заходит в неё. 976 | 977 | 7. Ищет файл `header.js`. Если он есть — копирует его содержимое в `/pages/index/index.js`. Делает то же самое для 978 | `bt`-файла, создавая `/pages/index/index.bt.js`. А для стилей делает то же самое, только сначала запускает `Stylus`-компилятор, 979 | который из `header.styl` генерит `CSS`-селекторы, а потом уже их копирует в `/pages/index/index.css`. 980 | 981 | 8. Дальше `ENB` выходит из `/blocks/header` и заходит в следующий блок, объявленный в btjson, например в `/blocks/form` и делает то же самое. И так повторяется для всех блоков. 982 | 983 | 9. В результате работы сборщика файлы `index.bt.js`, `index.css`, `index.js` заполнены `BT`-шаблонами, 984 | `CSS`-селекторами и `JS`-скриптами, необходимыми для генерации `HTML` всех блоков на странице, для их стилизации и 985 | для программирования `JS`-поведения. То есть `ENB` занимается тем, что создаёт страничные файлы. 986 | 987 | 10. Это почти всё, стилевой и яваскриптовый файлы готовы к отправке в браузер, но мы не можем пока отправлять, потому что у нас 988 | не готов конечный `HTML` страницы. Поэтому `ENB` отключается, и контроллер `index.page.js` вызывает `BT`-шаблонизатор. 989 | 990 | 11. `BT` принимает из контроллера `btjson` и накладывает на него только что 991 | сгенерённый `index.bt.js`, в результате чего происходит трансформация из `btjson`-а в 992 | `HTML`-теги. В результате, в памяти сервера 993 | формируется `HTML`-код страницы 994 | 995 | 12. Всё, это финал. `BT` создал `HTML`, `ENB` собрал `CSS` и `JS`. Сервер отправляет всё это назад в браузер по `HTTP`-протоколу. 996 | 997 | 13. И ура! В окне браузера у пользователя отобразилась наша страница. 998 | 999 | Теперь вы точно представляете, как работает Бивис-приложение. 1000 | 1001 | ## Заключение 1002 | 1003 | Начать работать на бивисе очень просто: 1004 | ``` 1005 | git clone git@github.com:bevis-ui/bevis-stub.git your-project 1006 | cd your-project 1007 | make 1008 | ``` 1009 | После этого мы открываем в браузере ```localhost:8080``` и говорим: «Привет, Бивис!» 1010 | 1011 | Пользоваться бивисом нам радостно по нескольким причинам. 1012 | 1013 | 1. Бивис на 99% — это простой `JS`. 1014 | 2. Весь программный код покрыт юнит-тестами. 1015 | 3. Бивис написан в очень понятном кодстайле, которого придерживается команда Яндекс.Карт. 1016 | 1017 | Убедиться в этом можно на двух тестовых проектах. 1018 | 1019 | * Одностраничное приложение, реализующее функциональность [TODO MVC](http://github.com/bevis-ui/bevis-todo) 1020 | 1021 | * Движок для ведения блогов — [BEViS Blog](http://github.com/bevis-ui/bevis-blog) 1022 | 1023 | И самое главное… конечно же мы любим бивис, потому что сами его и придумали ;) 1024 | -------------------------------------------------------------------------------- /practice.md: -------------------------------------------------------------------------------- 1 | # Немного практики 2 | 3 | Хватит теории, давайте попишем код! 4 | 5 | Читайте последовательно, выполняйте задания вместе с нами на вашей локальной машине. 6 | Со всеми вопросы добро пожаловать в Issues :) 7 | 8 | _На вопросы, отмеченные звёздочкой, мы либо ещё не ответили, либо ответили не полностью._ 9 | 10 | 1. [Создаём новую страницу проекта](practice/new-page.md) 11 | 12 | 2. [Добавляем на страницу новый блок](practice/new-block.md) 13 | 14 | 3. [Описываем зависимости между блоками](practice/dependencies.md) 15 | 16 | 4. [Пишем стили для блока](practice/css.md) 17 | 18 | 5. [Пишем `js`-поведение для блока](practice/yblock.md) 19 | 20 | 6. [Настраиваем локализацию в проекте](practice/i18n.md) 21 | 22 | 7. [Используем `MVC`-концепцию в `BEViS`-проекте](practice/mvc-app.md) 23 | 24 | 8. [Взаимодействуем с бекендом](practice/backend-requests.md) [*] 25 | 26 | 9. [Пишем тесты](practice/tests.md) [*] 27 | -------------------------------------------------------------------------------- /practice/backend-requests.md: -------------------------------------------------------------------------------- 1 | # Взаимодействуем с бекендом 2 | 3 | 4 | ---- 5 | 6 | в разработке 7 | 8 | ---- 9 | 10 | План: 11 | 1. Преположим есть путь к ручкам сохранения и чтения данных. Какое-то api 12 | 2. дергаем их $.ajax 13 | 3. Нельзя дернуть то, что лежит на другом домене. Ошибка в браузере, текст. 14 | 4. Нужно сделать прокисирующие ручки на сервере и уже они будут ходить по http на другой домен. 15 | 5. Как бы хотелось, чтобы они работали? В виде цепочки методов, как мы привыкли в jQuery 16 | ``` 17 | do() 18 | .then() 19 | .fail(); 20 | ``` 21 | 22 | В `then` параметром то, что вернул `do()`. Метод `fail` вызовется, если `do` упал с ошибкой. 23 | 24 | Тогда мы написали бы так: 25 | ``` 26 | request('путь к серверным ручкам') 27 | .then(function () { 28 | this.emit('saved') 29 | }) 30 | .fail(function () { 31 | this.emit('not-saved') 32 | }); 33 | ``` 34 | 35 | Цепочка методов, совсем как в jQuery. ТОлько там выполнялись бы все три метода подряд, а здесь выполнится первый, а 36 | потом или второй или третий. 37 | 38 | На деле, мы только что описали Promises, Обещания. На человеческом языке это выглядело бы так: 39 | ``` 40 | обещаю вызвать('путь к серверным ручкам') 41 | .в_случае_успеха_вызову(function () { 42 | this.emit('saved') 43 | }) 44 | .в_случае_провала_вызову(function () { 45 | this.emit('not-saved') 46 | }); 47 | ``` 48 | 49 | 6. перепишем Модель, как нам бы хотелось, чтобы она работала. Пишем на промисах методы `_setUserData` и `_getUserData` 50 | Предположим, что такой модуль есть и он называется просто - `api`. 51 | ``` 52 | api 53 | .exec('путь к серверным ручкам') 54 | .then(function () { 55 | this.emit('saved') 56 | }) 57 | .fail(function () { 58 | this.emit('not-saved') 59 | }); 60 | ``` 61 | 62 | 7. Такой модуль действительно есть, называется BLA 63 | добавляем в package.json 64 | npm update 65 | рассказываю, что он умеет, как работает в общих чертах. 66 | 67 | -------------------------------------------------------------------------------- /practice/css.md: -------------------------------------------------------------------------------- 1 | Первые два шага мы уже сделали — [создали новую страницу](new-page.md), [создали новый блок](new-block.md). 2 | 3 | У нас уже есть страница `/pages/test-page/test-page.page.js`, а в ней единственный блок `input`: 4 | 5 | ```javascript 6 | module.exports = function (pages) { 7 | pages.declare('test-page', function (params) { 8 | var options = params.options; 9 | return { 10 | block: 'page', 11 | title: 'test page', 12 | styles: [ 13 | {url: options.assetsPath + '.css'} 14 | ], 15 | scripts: [ 16 | {url: options.assetsPath + '.' + params.lang + '.js'} 17 | ], 18 | body: [ 19 | { 20 | block: 'input', 21 | value: 'Привет, Бивис!' 22 | } 23 | ] 24 | }; 25 | }); 26 | } 27 | ``` 28 | 29 | Ещё по адресу `/blocks/input/` есть `bt`-шаблоны для `input`, стилевой файл и файл с `JS`-поведением этого блока. 30 | Откроем стилевой файл `/blocks/input/input.styl` и начнём :) 31 | 32 | Если вы выполнили шаги в [предыдущем руководстве](new-block.md), то файл у вас выглядит совершенно бесполезно. 33 | У меня там написано вот что: 34 | 35 | ```css 36 | .input { 37 | border: 1px solid red; 38 | } 39 | ``` 40 | 41 | Откроем в браузере `http://localhost:8080/test`. Блок `input` выглядит непрезентабельно - голое текстовое поле 42 | с красной обводкой. Именно это мы и будем это исправлять. 43 | 44 | ## Дизайнер дал картинку 45 | 46 | Вот реальный эскиз из Яндекса. 47 | 48 | 49 | 50 | Дизайн без особых затей: 51 | 52 | * текстовое поле плоское, белый фон, серая рамка 53 | * жёлтое свечение вокруг поля, когда поле получает фокус 54 | * текст-приглашение, если поле пустое 55 | * крестик для очистки поля 56 | 57 | Ещё дизайнер предусмотрел разные размеры блока. И, конечно, предусмотрел неактивное состояние, 58 | когда поле недоступно для действий пользователя. 59 | 60 | Глядя на эскиз, становится очевидно, что одним DOM-узлом мы не обойдёмся, придётся верстать дополнительный 61 | элемент `крестик`, потому что такого нативного браузерного контрола, к сожалению, не существует. Значит блок `input` 62 | придётся усложнить. Сейчас он выглядит так: 63 | 64 | ```html 65 | 66 | ``` 67 | 68 | А мы сделаем его таким: 69 | 70 | ```html 71 |
72 | 73 |
74 |
75 | ``` 76 | 77 | Текстовое поле, как и элемент "крестик" станут детьми блока, его элементами. И как мы видим, 78 | у текстового поля появится атрибут `placeholder`, в котором и будет храниться тот самый 79 | текст-приглашение. 80 | 81 | Мы не хотим хардкодить текст-приглашение, потому что блок `input` получится универсальным блоком, 82 | и нам захочется использовать его в разных формах. По той же причине мы не будем хардкодить `name` текстового поля. 83 | Вместо этого мы расширим API блока — разрешим при декларации блока указывать значения для этих атрибутов. Для этого в 84 | `/pages/test-page/test-page.page.js` в вызове блока добавим два новых поля - `name` и `placeholder`: 85 | 86 | ```javascript 87 | module.exports = function (pages) { 88 | pages.declare('test-page', function (params) { 89 | var options = params.options; 90 | return { 91 | block: 'page', 92 | title: 'test page', 93 | styles: [ 94 | {url: options.assetsPath + '.css'} 95 | ], 96 | scripts: [ 97 | {url: options.assetsPath + '.' + params.lang + '.js'} 98 | ], 99 | body: [ 100 | { 101 | block: 'input', 102 | value: 'Привет, Бивис!', 103 | name: 'loginField', 104 | placeholder: 'на сайте' 105 | } 106 | ] 107 | }; 108 | }); 109 | } 110 | ``` 111 | 112 | И теперь поддержим все изменения в шаблонах. 113 | 114 | ## Усложним HTML-структуру 115 | 116 | Откроем файл `/blocks/input/input.bt.js`, сейчас он выглядит вот так: 117 | 118 | ```javascript 119 | module.exports = function (bt) { 120 | 121 | bt.match('input', function (ctx) { 122 | ctx.enableAutoInit(); 123 | ctx.setTag('input'); 124 | 125 | var currentValue = ctx.getParam('value'); 126 | ctx.setAttr('value', currentValue); 127 | }); 128 | 129 | }; 130 | ``` 131 | 132 | Пусть блок `input` превратится в небольшой фрагмент DOM-дерева с двумя дочерними элементами - собственно текстовое поле 133 | и крестик для очистки текстового поля: 134 | 135 | ```javascript 136 | module.exports = function (bt) { 137 | 138 | bt.match('input', function (ctx) { 139 | ctx.enableAutoInit(); 140 | 141 | var value = ctx.getParam('value'); 142 | var name = ctx.getParam('name'); 143 | var placeholder = ctx.getParam('placeholder'); 144 | 145 | ctx.setContent([ 146 | { 147 | elem: 'control', 148 | inputValue: value, 149 | inputName: name, 150 | placeholder: placeholder 151 | }, 152 | { 153 | elem: 'clear' 154 | } 155 | ]); 156 | }); 157 | 158 | }; 159 | ``` 160 | 161 | Мы выкинули метод `ctx.setTag('input')`, потому что обертка должна быть тегом `div`, 162 | а такой тег генерится шаблонизатором по умолчанию даже если мы этого не указали явно. 163 | 164 | Мы создали локальные переменные `value`, `name`, `placeholder` и сохранили в них значения параметров из контекста с 165 | помощью метода `ctx.getParam()`. Из какого такого контекста? В контексте чего работает шаблон блока? Шаблон блока 166 | работает в контексте данных, которые описаны в `test-page.page.js`, то есть фактически в контексте этого 167 | фрагмента страницы: 168 | 169 | ```javascript 170 | { 171 | block: 'input', 172 | value: 'Привет, Бивис!', 173 | name: 'loginField', 174 | placeholder: 'на сайте' 175 | } 176 | ``` 177 | 178 | То есть, когда мы в шаблоне сказали, к примеру, `var placeholder = ctx.getParam('placeholder')`, 179 | то в локальной переменной `placeholder` окажется строка "на сайте", которую мы указали в декларации страницы — мы 180 | получили значение параметра из контекста. 181 | 182 | Затем мы вызвали метод `ctx.setContent()`, в который передали массив элементов. Мы решили, 183 | что внутри блока будут жить два элемента - те самые `input__control` и `input__clear`. Элементы описываются так же, 184 | как блок, с той лишь разницей, что вместо слова `block` мы зовём элемент словом `elem`. 185 | 186 | Если сейчас обновить страницу в браузере, текстовое поле со страницы исчезнет, но заглянув в HTML-код страницы, 187 | мы увидим почти то, что нужно: 188 | 189 | ```html 190 |
191 |
192 |
193 |
194 | ``` 195 | 196 | Осталось сгенерить правильный тег для `input__control` и передавать в него значение для атрибутов 197 | `value`, `name` и `placeholder`. Для этого добавим новый шаблон (смотрите ниже, 198 | там появился шаблон для `input__control`): 199 | 200 | ```javascript 201 | module.exports = function (bt) { 202 | 203 | bt.match('input', function (ctx) { 204 | ctx.enableAutoInit(); 205 | 206 | var value = ctx.getParam('value'); 207 | var name = ctx.getParam('name'); 208 | var placeholder = ctx.getParam('placeholder'); 209 | 210 | ctx.setContent([ 211 | { 212 | elem: 'control', 213 | inputValue: value, 214 | inputName: name, 215 | placeholder: placeholder 216 | }, 217 | { 218 | elem: 'clear' 219 | } 220 | ]); 221 | }); 222 | 223 | bt.match('input__control', function (ctx) { 224 | ctx.setTag('input'); 225 | 226 | var currentValue = ctx.getParam('inputValue'); 227 | var currentName = ctx.getParam('inputName'); 228 | var currentPlaceholder = ctx.getParam('placeholder'); 229 | 230 | ctx.setAttr('value', currentValue); 231 | ctx.setAttr('name', currentName); 232 | ctx.setAttr('placeholder', currentPlaceholder); 233 | }); 234 | 235 | }; 236 | ``` 237 | 238 | В шаблоне для `input__control` контекст меняется. Шаблон этого элемента работает в контексте данных, 239 | которые пришли к нему из предыдущего шаблона, то есть в контексте вот этого `btjson`: 240 | 241 | ```javascript 242 | ctx.setContent([ 243 | { 244 | elem: 'control', 245 | inputValue: value, 246 | inputName: name, 247 | placeholder: placeholder 248 | } 249 | ``` 250 | 251 | Именно поэтому в шаблоне элемента `control` мы ожидаем параметры под другими именами. Я специально назвал их иначе, 252 | нежели в родительском шаблоне, чтобы вы ухватили идею. 253 | 254 | Ещё раз о контексте, потому что это важный момент. 255 | 256 | ```javascript 257 | // В шаблоне для `input` я получил параметры из контекста `test-page.page.js` 258 | var value = ctx.getParam('value'); 259 | var name = ctx.getParam('name'); 260 | var placeholder = ctx.getParam('placeholder'); 261 | 262 | // а потом прокинул их значения в элемент `control` 263 | ctx.setContent([ 264 | { 265 | elem: 'control', 266 | inputValue: value, 267 | inputName: name, 268 | placeholder: placeholder 269 | } 270 | ``` 271 | 272 | ```javascript 273 | // В шаблоне дочернего `input__control` я получил ровно эти же значения, 274 | // но уже из параметров самого элемента `control` 275 | var currentValue = ctx.getParam('inputValue'); 276 | var currentName = ctx.getParam('inputName'); 277 | var currentPlaceholder = ctx.getParam('placeholder'); 278 | 279 | // и создал атрибуты для html-тега: 280 | ctx.setAttr('value', currentValue); 281 | ctx.setAttr('name', currentName); 282 | ctx.setAttr('placeholder', currentPlaceholder); 283 | ``` 284 | 285 | Теперь смотрим в браузере. Да, всё отлично, структура приходит. 286 | 287 | ```html 288 |
289 | 290 |
291 |
292 | ``` 293 | 294 | Наконец-то займёмся стилями. 295 | 296 | ## Стили для блока и его элементов 297 | 298 | Возвращаемся в `/blocks/input/input.styl` и начинаем :) 299 | 300 | Если вы выполнили шаги в [предыдущем руководстве](new-block.md), то файл у вас выглядит совершенно бесполезно. 301 | У меня там написано вот что: 302 | 303 | ```css 304 | .input { 305 | border: 1px solid red; 306 | } 307 | ``` 308 | 309 | Напишем стили, реализующие задумку дизайнера. 310 | 311 | ```css 312 | .input { 313 | $height = 26px; 314 | 315 | position: relative; 316 | display: inline-block; 317 | box-sizing: border-box; 318 | 319 | background-color: #fff; 320 | border: 1px solid rgba(0, 0, 0, 0.2); 321 | 322 | /* чтобы под бордером не было белого фона */ 323 | background-clip: padding-box; 324 | 325 | vertical-align: baseline; 326 | line-height: normal; 327 | cursor: text; 328 | 329 | /* анимация желтой фокусной рамки */ 330 | transition:box-shadow .05s ease-out; 331 | 332 | /* */ 333 | &__control { 334 | font-family: Arial, Helvetica, sans-serif; 335 | font-size: 13px; 336 | 337 | position: relative; 338 | display: inline-block; 339 | box-sizing: border-box; 340 | margin: 0; 341 | padding: 0 .6em; 342 | width: 100%; 343 | height: $height; 344 | /* Чтобы не заползал под крестик */ 345 | padding-right: $height; 346 | 347 | border: none; 348 | vertical-align: baseline; 349 | 350 | &:focus { 351 | outline: none; 352 | } 353 | 354 | /* 355 | @see http://alexbaumgertner.ya.ru/replies.xml?item_no=2112 356 | @see http://www.cssnewbie.com/input-button-line-height-bug/ 357 | */ 358 | &::-moz-focus-inner { 359 | padding: 0; 360 | border: none; 361 | } 362 | 363 | 364 | /* прячем стандартный крестик IE10 */ 365 | &::-ms-clear { 366 | display: none; 367 | } 368 | } 369 | 370 | &._focused { 371 | outline: 0; 372 | box-shadow: 0 0 10px #fc0; 373 | 374 | if (ie == 8) { 375 | border-color: #d1bb66; 376 | } else { 377 | border-color: rgba(178, 142, 0, .6); 378 | } 379 | } 380 | 381 | &._disabled { 382 | border-color: transparent !important; 383 | cursor: default; 384 | 385 | if (ie == 8) { 386 | background-color: #ebebeb !important; 387 | } else { 388 | background-color: rgba(0, 0, 0, 0.08) !important; 389 | } 390 | } 391 | 392 | &._disabled &__control { 393 | user-select: none; 394 | background-color: inherit; 395 | } 396 | 397 | &__clear { 398 | position: absolute; 399 | top: 0; 400 | right: 0; 401 | z-index: 2; 402 | 403 | width: $height; 404 | height: $height; 405 | vertical-align: middle; 406 | 407 | background: url('images/clear-14.png') 50% 50% no-repeat; 408 | cursor: pointer; 409 | opacity: (ie != 8) ? 0.3 : 1; 410 | 411 | &:hover { 412 | background-image: url('images/clear-14-hover.png'); 413 | opacity: 1; 414 | } 415 | 416 | &._visible { 417 | display: block; 418 | } 419 | } 420 | } 421 | ``` 422 | 423 | Обычный `CSS`-код с базовыми конструкциями препроцессора: переменные (`$height`), условные операторы (`if`) и ссылка на 424 | родительский селектор `&`. Возможно, вы захотите использовать циклы, перебирающие методы и другие богатства `Stylus`. Мы 425 | сознательно не используем все возможности препроцессора, чтобы код оставался максимально похожим на `Plain CSS` — простым и наглядным. 426 | 427 | Скопировали? Теперь обновите страницу в браузере. Не хватает картинки для крестика. Не хватает жёлтой рамки вокруг блока 428 | при установке фокуса. А в остальном похоже на то, что хочет дизайнер. 429 | 430 | Стили незамысловатые, селекторы понятные, поэтому о них всего по паре слов. 431 | 432 | ```css 433 | background-color: #fff; 434 | border: 1px solid rgba(0, 0, 0, 0.2); 435 | 436 | /* чтобы под бордером не было белого фона */ 437 | background-clip: padding-box; 438 | ``` 439 | 440 | Дизайнер придумал для блока темно-серый полупрозрачный бордер. Страница у нас имеет светло-серый фон, бордеру задаём 441 | `rgba(0, 0, 0, 0.2)` и ждём, что скзвозь бордер мы увидим фон страницы. Но почему-то сквозь бордер мы видим белый 442 | фоновый цвет блока. Почему? 443 | 444 | Бордер, по всем спецификациям, откладывается _вне_ блока. Но фоновый цвет, заданный блоку, почему-то оказывается и 445 | под бордером тоже. Такое поведение фона цвета кажется необычным с точки зрения человеческой логики, даже если оно по 446 | спецификации. Вот для чего нужен `background-clip: padding-box` — этим мы заставляем фоновый цвет ограничиваться зоной 447 | внутренних отступов. Это такая хитрость, вдруг вы раньше не сталкивались, а теперь будете знать. 448 | 449 | Нативный браузерный `` в нашем блоке представлен элементом с классом `.input__control`. В `Stylus` мы описываем его 450 | в селекторе `&__control {}`, где `&` - ссылка на родительский селектор `.input{}`. Нам нужно лишить нативный браузерный 451 | `` всякого нативного оформления, а родительский блок `.input{}` наоборот оформить согласно 452 | дизайну. 453 | 454 | Про фокус. Так как браузерный `` оказался внутри нашего кастомного блока, стал безликим и словно растворился в 455 | дизайне, мы больше не можем разрешать браузеру показывать дефолтный фокус на ``. Отменяем: 456 | 457 | ```css 458 | /* */ 459 | &__control { 460 | 461 | &:focus { 462 | outline: none; 463 | } 464 | 465 | /* 466 | @see http://alexbaumgertner.ya.ru/replies.xml?item_no=2112 467 | @see http://www.cssnewbie.com/input-button-line-height-bug/ 468 | */ 469 | &::-moz-focus-inner { 470 | padding: 0; 471 | border: none; 472 | } 473 | } 474 | ``` 475 | 476 | Но фокус показывать мы должны, поэтому эмулируем его на самом блоке с помощью специального `состояния блока`. Блок 477 | оказался в фокусе. 478 | 479 | ## Стили состояний 480 | 481 | Состояние, оно же `state`, выражается специальным `CSS`-классом, который _немного_ (это важное уточнение) изменяет 482 | внешний вид блока. Чаще всего состояние меняется из `javascript`. 483 | 484 | Например, на блок нажали — у блока появилось состояние `pressed`. Блок неактивный — у него сразу выставлено состояние 485 | `disabled` 486 | 487 | Здесь описано состояние, когда блок `input` оказался в фокусе. 488 | 489 | ```css 490 | &._focused { 491 | outline: 0; 492 | box-shadow: 0 0 10px #fc0; 493 | 494 | if (ie == 8) { 495 | border-color: #d1bb66; 496 | } else { 497 | border-color: rgba(178, 142, 0, .6); 498 | } 499 | } 500 | ``` 501 | 502 | Жёлтое свечение вокруг блока. Однопиксельный бордер, почти невидимый. В IE8 бордер сплошной без прозрачности. 503 | 504 | Кстати, для IE ничего особенного не пишем. 505 | 506 | Простые условия: 507 | 508 | ```css 509 | if (ie == 8) { 510 | border-color: #d1bb66; 511 | } else { 512 | border-color: rgba(178, 142, 0, .6); 513 | } 514 | ``` 515 | 516 | Или тернарные: 517 | 518 | ```css 519 | opacity: (ie != 8) ? 0.3 : 1; 520 | ``` 521 | 522 | Ничего сложнее, кажется, и не нужно. 523 | 524 | Классы состояний мы начинаем с символа подчёркивания. 525 | 526 | В программировании символ подчёркивания в начале имени переменной иллюстрирует, что эта переменная — приватная. 527 | Мы в `BEViS` этим же символом иллюстрируем, что состояние — тоже приватно. Сам символ ни на что не влияет, спокойно 528 | можно и без него жить. Но нам нравится, мы используем. 529 | 530 | Состояние само по себе ничего не делает. Оно лишь _добавляет_ или _переопределяет_ стили, чтобы блок сделать чуть-чуть 531 | иным — раскрасить чуть иначе, в соответствии с текущим состоянием. 532 | 533 | Классам состояний мы даём имена, как если бы они были образованы для формирования страдательного залога английского 534 | языка. Непонятно? Я бы и сам не понял, прочитав такое. А можно проще: 535 | 536 | "Block is focused" — отбрасываем "Block is" — что осталось, то и пишем. 537 | 538 | "Block is disabled", "Block is visible", "Block is active". Мы называем так. Вы можете называть иначе. Главное, чтобы 539 | было удобно. Догмы нет. 540 | 541 | Блок может находиться в разных состояниях одновременно: `_focused` и `_pressed`, `_visible` и `_disabled`. Комбинации 542 | могут быть самые разные, хоть все сразу. 543 | 544 | Так как состояния доопределяют или переопределяют стили блока, то сочетания разных состояний грозит возможными 545 | конфликтами между селекторами. Или скажем иначе: "Не грозит, а грозило бы, если в BEViS не существовало жёсткого 546 | правила — на одной html-ноде может быть только один блок". А если так, тогда всё многообразие состояний блока существует 547 | в пределах одного стилевого файла. И мы способны все сочетания состояний продумывать заранее. 548 | 549 | Что будет, когда блок нажат и стоит в фокусе? Подумаем и напишем: 550 | 551 | ```css 552 | &._focused._pressed { 553 | /* здесь стили */ 554 | } 555 | ``` 556 | 557 | А когда задизейблен и не скрыт? Напишем: 558 | 559 | ```css 560 | &._disabled._visible { 561 | /* здесь стили */ 562 | } 563 | ``` 564 | 565 | Стили готовы, но увидеть их в браузере польностью мы не можем, потому что ещё не написан скрипт, меняющий состояния. 566 | Скрипт мы будем писать позже и в [отдельном документе](yblock.md), а здесь только про стили. 567 | 568 | ## Нужны разные отображения? 569 | 570 | Дизайнер нарисовал текстовые поля в разных размерах — у них совершенно одинаковые стили, кроме размеров, каких-то 571 | отступов, величины гарнитуры шрифта. Нарисовал целых 4 размера и назвал их, как маркируют бирки на одежде - S, M, L, XL. 572 | 573 | В таких случаях верстальщики создают базовый класс `.input`, соответствующий дефолтному размеру, и 3 дополнительных, 574 | модифицирующих базовый класс, определяющих другие размеры и отступы. Мы называли их `.small`, `.large`, `.xlarge` и 575 | всё это выглядело бы примерно так: 576 | 577 | ```css 578 | .input { 579 | /* все базовые стили */ 580 | } 581 | 582 | .input.small { 583 | height: 16px; 584 | font-size: 11px; 585 | padding: 2px; 586 | } 587 | 588 | .input.large { 589 | height: 22px; 590 | font-size: 18px; 591 | padding: 6px; 592 | } 593 | 594 | .input.xlarge { 595 | /* и здесь уточнения для XL */ 596 | } 597 | ``` 598 | 599 | Если внимательно вглядеться в эти составные селекторы, легко заметить, что они читаются как человеческие имена: "Инпут, 600 | Маленький Инпут, Большой Инпут, Очень Большой Инпут". Почему же эти имена выражены через два `CSS`-класса? 601 | 602 | В Бивисе мы создаём четыре отдельных класса - по одному для каждого размера: 603 | 604 | ```css 605 | .input_normal { 606 | /* все базовые стили + стили для размера M */ 607 | } 608 | 609 | .input_small { 610 | /* все базовые стили + стили для размера S */ 611 | } 612 | 613 | .input_large { 614 | /* все базовые стили + стили для размера L */ 615 | } 616 | 617 | .input_xlarge { 618 | /* все базовые стили + стили для размера XL */ 619 | } 620 | ``` 621 | 622 | Каждый такой класс будет в себе содержать все базовые стили (которые раньше хранились в классе `.input`) и нужные 623 | размеры. Если мы так сделаем, код станет простым и очевидным — для каждого размера свой класс, который реализует только 624 | нужное отображение. Представление. 625 | 626 | Эти четыре селектора выше - это `представления` или `отображения` или `view` блока. Инпут может иметь нормальное 627 | представление (`normal`), маленькое (`small`), большое (`large`) или любое другое, которое мы ему создадим. 628 | 629 | Сразу решим ваши сомнения. Дублируется ли код базовых стилей блока в каждом вью? Нет: 630 | 631 | ```css 632 | input_mixin() { 633 | /* все базовые стили */ 634 | } 635 | 636 | .input_normal { 637 | input_mixin(); 638 | /* здесь только стили для размера M */ 639 | } 640 | 641 | .input_small { 642 | input_mixin(); 643 | /* здесь только стили для размера S */ 644 | } 645 | 646 | .input_large { 647 | input_mixin(); 648 | /* здесь только стили для размера L */ 649 | } 650 | 651 | .input_xlarge { 652 | input_mixin(); 653 | /* здесь только стили для размера XL */ 654 | } 655 | ``` 656 | 657 | Миксины - это четвертая и последняя возможность, которую мы используем в `Stylus`. Миксин - молчаливая функция, которая 658 | не попадает в конечный `CSS` сама по себе. Но если её в каком-то селекторе позвать, она вернёт вместо себя стили, 659 | описанные внутри. 660 | 661 | Это мы и делаем, чтобы обеспечить каждое представление инпута базовыми стилями. Именно внутрь `input_mixin()` мы 662 | перенесём все стили, которые сейчас описаны в `input.styl`. Откройте его на редактирование и замените первую строку 663 | файла, чтобы вместо `.input {` получилось `input_mixin() {`. Обратите внимание - нужно удалить точку (это не селектор, 664 | а функция) и дописать круглые скобки (что лишний раз показывает, что это всё-таки функция, а не селектор). 665 | 666 | Если сейчас обновить страницу в браузере, блок потеряет все стили. Ну вот, миксин - не селектор, сам по себе не 667 | применяется. 668 | 669 | Допишем в конец файла селектор для Большого Инпута. У вас должно получиться так: 670 | 671 | ```css 672 | input_mixin() { 673 | $height = 26px; 674 | 675 | /* все остальные базовые стили */ 676 | } 677 | 678 | .input_large { 679 | input_mixin($height = 32px); 680 | 681 | &__control{ 682 | font-size: 16px; 683 | } 684 | } 685 | ``` 686 | 687 | Обратили внимание, что в миксин мы передаём параметр? Поддержим этот параметр в самом миксине и удалим жёстко 688 | зашитую переменную: 689 | 690 | Было: 691 | ```css 692 | input_mixin() { 693 | $height = 26px; 694 | ``` 695 | 696 | 697 | Сделаем так: 698 | ```css 699 | input_mixin($height) { 700 | 701 | ``` 702 | 703 | Теперь мы принимаем значение высоты, как аргумент миксина. 704 | 705 | На всякий случай сверим содержимое файла `input.styl`. У вас там сейчас написано следующее? Если нет, самое время 706 | поправить, чтобы было именно так: 707 | 708 | ```css 709 | input_mixin($height) { 710 | 711 | position: relative; 712 | display: inline-block; 713 | box-sizing: border-box; 714 | 715 | background-color: #fff; 716 | border: 1px solid rgba(0, 0, 0, 0.2); 717 | 718 | // чтобы под бордером не было белого фона 719 | background-clip: padding-box; 720 | 721 | vertical-align: baseline; 722 | line-height: normal; 723 | cursor: text; 724 | 725 | transition:box-shadow .05s ease-out; /* анимация желтой фокусной рамки */ 726 | 727 | &._focused { 728 | outline: 0; 729 | box-shadow: 0 0 10px #fc0; 730 | 731 | if (ie == 8) { 732 | border-color: #d1bb66; 733 | } else { 734 | border-color: rgba(178, 142, 0, .6); 735 | } 736 | } 737 | 738 | /* */ 739 | &__control{ 740 | font-family: Arial, Helvetica, sans-serif; 741 | font-size: 13px; 742 | 743 | position: relative; 744 | display: inline-block; 745 | box-sizing: border-box; 746 | margin: 0; 747 | padding: 0 .6em; 748 | width: 100%; 749 | height: $height; 750 | // Чтобы не заползал под крестик 751 | padding-right: $height; 752 | 753 | border: none; 754 | vertical-align: baseline; 755 | 756 | &:focus{ 757 | outline: none; 758 | } 759 | 760 | /* 761 | @see http://alexbaumgertner.ya.ru/replies.xml?item_no=2112 762 | @see http://www.cssnewbie.com/input-button-line-height-bug/ 763 | */ 764 | &::-moz-focus-inner { 765 | padding: 0; 766 | border: none; 767 | } 768 | 769 | 770 | /* прячем стандартный крестик IE10 */ 771 | &::-ms-clear { 772 | display: none; 773 | } 774 | } 775 | 776 | &._disabled{ 777 | border-color: transparent !important; 778 | cursor: default; 779 | 780 | if (ie == 8) { 781 | background-color: #ebebeb !important; 782 | } else { 783 | background-color: rgba(0, 0, 0, 0.08) !important; 784 | } 785 | } 786 | 787 | &._disabled &__control{ 788 | user-select: none; 789 | background-color: inherit; 790 | } 791 | 792 | &__clear { 793 | position: absolute; 794 | top: 0; 795 | right: 0; 796 | z-index: 2; 797 | 798 | width: $height; 799 | height: $height; 800 | vertical-align: middle; 801 | 802 | background: url('images/clear-14.png') 50% 50% no-repeat; 803 | cursor: pointer; 804 | opacity: (ie != 8) ? 0.3 : 1; 805 | 806 | &:hover { 807 | background-image: url('images/clear-14-hover.png'); 808 | opacity: 1; 809 | } 810 | 811 | &._visible { 812 | display: block; 813 | } 814 | } 815 | } 816 | 817 | .input_large { 818 | input_mixin($height = 32px); 819 | 820 | &__control{ 821 | font-size: 16px; 822 | } 823 | } 824 | ``` 825 | 826 | Обновим страницу в браузере. Ничего не изменилось. Смотрим в html - видим, что блок выглядит так 827 | 828 | ```html 829 |
830 | 831 |
832 |
833 | ``` 834 | 835 | Уверен, вы поняли что пошло не так. Мы написали селектор для блока с вью — `input_large`, а в файле `test-page.page.js` мы декларируем 836 | инпут без всякого вью. Поэтому стили не применяются. 837 | 838 | Поправим декларацию блока. Добавим `view: 'large'` 839 | 840 | ```javascript 841 | { 842 | block: 'input', 843 | view: 'large', 844 | value: 'Привет, Бивис', 845 | name: 'loginField', 846 | placeholder: 'на сайте' 847 | } 848 | ``` 849 | 850 | Обновим страницу в браузере. Блок вообще исчез! Смотрим в файрбаге - видим только обертку блока: 851 | 852 | ```html 853 |
854 | ``` 855 | 856 | Если внутренности не генерятся, это значит, не отрабатывают `bt`-шаблоны. 857 | 858 | И действительно. Шаблоны сейчас матчатся на `input`, а у нас уже не просто `input`, у нас уже `input_large`. 859 | 860 | Решение простое - переписать match-и c `match('input'` на `match('input_large'`. 861 | 862 | Переписали, смотрим в браузере - да! Теперь инпут стал большим. 863 | 864 | Теперь мы можем в `input.styl` написать ещё один селектор для Маленького Инпута? И ещё один для Нормального Инпута? А 865 | для Очень Большого, для Красного Инпута, для Серо-Буро-Малинового? Для любого можем и оно заработает? 866 | 867 | Нет, потому что мы умудрились с вами захардкодить имя вью в шаблонах, когда переписали матчи на на `match('input_large'` 868 | Эти шаблоны применятся _только_ для Большого Инпута. Мы можем в шаблонах использовать "маски". 869 | 870 | К имени блока вместо конкретного вью добавьте маску `*`: 871 | 872 | Было: 873 | ```javascript 874 | module.exports = function (bt) { 875 | bt.match('input_large', function (ctx) { 876 | // ... 877 | }); 878 | }; 879 | ``` 880 | 881 | Стало: 882 | ```javascript 883 | module.exports = function (bt) { 884 | bt.match('input*', function (ctx) { 885 | // ... 886 | }); 887 | }; 888 | ``` 889 | 890 | Вот теперь эти шаблоны будут отрабатывать для инпутов с любым `view`, то есть для любого из нижеописанных вызовов: 891 | 892 | ```javascript 893 | { 894 | block: 'input', 895 | view: 'normal', 896 | value: 'Hello' 897 | } 898 | // Сгенерит
899 | 900 | { 901 | block: 'input', 902 | view: 'active', 903 | value: 'Hello' 904 | } 905 | // Сгенерит
906 | ``` 907 | 908 | Маску нужно добавлять к матчеру каждого элемента. Иначе шаблон не выполнится. 909 | 910 | Было: 911 | ```javascript 912 | bt.match('input_large', function (ctx) { 913 | // ... 914 | } 915 | 916 | bt.match('input_large__control', function (ctx) { 917 | // ... 918 | } 919 | 920 | bt.match('input_large__clear', function (ctx) { 921 | // ... 922 | } 923 | ``` 924 | 925 | Стало 926 | ```javascript 927 | module.exports = function (bt) { 928 | bt.match('input*', function (ctx) { 929 | // ... 930 | } 931 | 932 | bt.match('input*__control', function (ctx) { 933 | // ... 934 | } 935 | 936 | bt.match('input*__clear', function (ctx) { 937 | // ... 938 | } 939 | ``` 940 | 941 | А ещё можно задавать какое-либо `view` как дефолтное. Чтобы даже если вы не задекларировали никакого `view`, оно 942 | сгенерировалось бы само. Это нужно, когда на сайте большинство инпутов имеет, к примеру, размер `large`. Тогда мы везде 943 | декларируем блок без `view`: 944 | 945 | ```javascript 946 | { 947 | block: 'input', 948 | value: 'Hello' 949 | } 950 | ``` 951 | 952 | А чтобы построился такой html `
`, мы в файле с шаблонами объявляем дефолтный `view` с 953 | помощью метода `bt.setDefaultView('input', 'large');` 954 | 955 | Было: 956 | ```javascript 957 | module.exports = function (bt) { 958 | 959 | bt.match('input*', function (ctx) { 960 | // ... 961 | ``` 962 | 963 | Стало 964 | ```javascript 965 | module.exports = function (bt) { 966 | bt.setDefaultView('input', 'large'); 967 | 968 | bt.match('input*', function (ctx) { 969 | // ... 970 | ``` 971 | 972 | И вот теперь наш блок готов работать с любыми `view`. Как только нам понадобится новое представление, мы создаём новый 973 | селектор для представления, звоём в него базовый миксин и доопределяем или переопределяем стили базового миксина. 974 | 975 | И всё. 976 | 977 | Сложное объяснение? Ээх, оно таким сложным выглядит только на бумаге. Если бы я объяснял вам это же голосом, вам бы оно 978 | показалось простым. :( 979 | 980 | ## Файловая система для `view` 981 | 982 | У нас получилось так, что все-все стили оказались в одном файле. Файл рискует стать большим. На наш взгляд это совсем 983 | неудобно. Так можно. Но нам неудобно. Мы стараемся разбивать стили на файлы согласно простому правилу: "Миксин - в 984 | отдельный файл с именем миксина, вью - в отдельный файл с именем вью". 985 | 986 | Работать с таким кодом легче, мы редактируем всегда только то, что нужно в данный момент, 987 | не рискуя задеть код соседних вью или, не дай боже, миксина. Как это организовать? 988 | 989 | Создайте файл `input_large.styl`. Это будет файл для вью "Большой Инпут". В него из файла `input.styl` перенесите 990 | селектор для этого вью: 991 | 992 | ```css 993 | .input_large { 994 | input_mixin($height = 32px); 995 | 996 | /* */ 997 | &__control{ 998 | font-size: 16px; 999 | } 1000 | } 1001 | ``` 1002 | 1003 | А в файле `input.styl` таким образом останется только код миксина `input_mixin($height) {...}`. 1004 | 1005 | Проверяем в браузере, стили исчезли. Почему? Сейчас научу, как понять. Откройте собранный `css`-файл. Он находится по 1006 | адресу `/build/test/test.css`. Там увидите такое: 1007 | 1008 | ```css 1009 | /* /bevis/blocks/page/page.styl:begin */ 1010 | .page { 1011 | background: #eaeaea url(../../blocks/page/page.png); 1012 | } 1013 | /* /bevis/blocks/page/page.styl:end */ 1014 | /* /bevis/blocks/input/input.styl:begin */ 1015 | /* /bevis/blocks/input/input.styl:end */ 1016 | ``` 1017 | 1018 | Видно, что сборщик заглядывает в файл `input.styl`, но оттуда ничего не забирает, потому что там только миксин. А вот 1019 | содержимого файла `input_large.styl` в собранном `css` нет. Как сделать, чтобы подключался и он? 1020 | 1021 | Создайте `input.deps.yaml` и напишите в нём одну строку: 1022 | 1023 | ``` 1024 | - view: "*" 1025 | ``` 1026 | 1027 | Теперь обновите браузер — стили для селектора `.input_large` придут. И если вы рядом положите файлик 1028 | `input_small.styl`, то и его содержимое попадёт в собранный `css`. И для любого другого вью тоже. 1029 | 1030 | Теперь всё почти хорошо. Не нравится только то, что код миксина лежит в файле с именем `input.styl`. Можно так и 1031 | оставить, если вас это устраивает. Но если миксинов будет несколько? Доведём объяснение до логического конца. 1032 | 1033 | Создадим для миксинов своё особенное место, чтобы они не мозолили глаза. 1034 | 1035 | 1. Создайте папку `blocks/input/_skin` (именно так, с подчеркиванием впереди), перенесите в неё файл `input.styl` и 1036 | сразу его переименуйте в `input_skin_base.styl`. Вместо `base` можно написать что вам больше нравится, 1037 | а вот конструкция `input_skin` в имени файла - обязательна и неизменна. 1038 | 1039 | 2. Откройте его на редактирование и замените для консистентности имя миксина с `input_mixin($height)` на 1040 | `input_skin_base($height)`. 1041 | 1042 | 3. Сразу же в файле `input_large.styl` замените вызов миксина с `input_mixin($height = 32px);` на 1043 | `input_skin_base($height = 32px);` 1044 | 1045 | Слово `skin` - служебное. Папка `_skin` - служебная, специально для хранения миксинов. Мы назвали её так, 1046 | потому что такие штуки, как миксины, напоминает нам скины из WinAmp. 1047 | 1048 | Обновите страницу в браузере. Стили исчезли? Исчезли, но не все. Остались только те, чтобы в `input_large.styl`. А код 1049 | миксина не попал внутрь селектора `.input_large {...}`. Почему? Потому что вью "Большой Инпут" не знает, где ему брать 1050 | код миксина. Он не знает, что нужно открыть папку с названием `_skin`, открыть там файл с именем `input_skin_base.styl` 1051 | и в нём искать одноименный миксин. 1052 | 1053 | Обо всём этом сборщику нужно сказать явно. Через зависимость. Нужно сказать, что такие-то вью инпута (а лучше, 1054 | что любые вью инпута) зависят от таких-то миксинов. Создадим файл `input_view.deps.yaml` 1055 | 1056 | ``` 1057 | - skin: "*" 1058 | required: true 1059 | ``` 1060 | 1061 | Этот файл надо понимать так: "Любые вью инпутов (это следует из названия `deps`-файла ) зависят от любых миксинов, 1062 | находящихся в папке `_skin` (это следует из инструкции `- skin: "*"`). При этом миксины должны попасть в сборку раньше, 1063 | чем сами вью (об этом сообщает инструкция `required: true`)" 1064 | 1065 | Обновляем страницу в браузере, стили вернулись. 1066 | 1067 | Теперь в папке `/blocks/input/` мы видим только файлы разных отображений. А миксины спрятаны в папке 1068 | `/blocks/input/_skin`. И если нам понадобится, мы можем сделать ещё скинов, сложить в папку `_skin` и звать их из 1069 | разных вью. Получается, что папка `_skin` стала своеобразной библиотекой скинов. 1070 | 1071 | Уфф. Про вью и миксины это всё. 1072 | 1073 | Итого, миксин `input_skin_base.styl` хранит всё стили текстового поля, общие для любых отображений блока, 1074 | а `input_large.styl` хранит `CSS`-селектор для отображения типа "Большой Инпут". 1075 | 1076 | Нужно другое отображение, например,"Маленький Инпут"? Быстрый путь такой - скопировать и отредатировать: 1077 | 1078 | ``` 1079 | cd blocks/input 1080 | cp input_large.styl input_small.styl 1081 | ``` 1082 | Отредактировать `input_small.styl`: 1083 | 1084 | Было: 1085 | ```css 1086 | .input_large { 1087 | input_skin_base($height = 32px); 1088 | 1089 | /* */ 1090 | &__control{ 1091 | font-size: 16px; 1092 | } 1093 | } 1094 | ``` 1095 | 1096 | Стало: 1097 | ```css 1098 | .input_small { 1099 | input_skin_base($height = 22px); 1100 | 1101 | /* */ 1102 | &__control{ 1103 | font-size: 13px; 1104 | } 1105 | } 1106 | ``` 1107 | 1108 | Теперь вы знаете всё, чтобы создавать отображения блока. 1109 | 1110 | Нам осталось исправить одну недоделку в файле `input_skin_base.styl`. Посмотрим, как сейчас выглядит селектор 1111 | крестика для очищения поля: 1112 | 1113 | ```css 1114 | &__clear { 1115 | position: absolute; 1116 | top: 0; 1117 | right: 0; 1118 | z-index: 2; 1119 | 1120 | width: $height; 1121 | height: $height; 1122 | vertical-align: middle; 1123 | 1124 | background: url('images/clear-14.png') 50% 50% no-repeat; 1125 | cursor: pointer; 1126 | opacity: (ie != 8) ? 0.3 : 1; 1127 | 1128 | &:hover { 1129 | background-image: url('images/clear-14-hover.png'); 1130 | opacity: 1; 1131 | } 1132 | 1133 | &._visible { 1134 | display: block; 1135 | } 1136 | } 1137 | ``` 1138 | 1139 | Во-первых, у вас нет этих картинок. Возьмите их у нас и скопируйте в проект в папку `/blocks/input/images`. 1140 | 1141 | * [крестик 10 px](http://bevis-ui.github.io/docs/images/input__clear/clear-10.png) 1142 | * [крестик 10 px для hover](http://bevis-ui.github.io/docs/images/input__clear/clear-10-hover.png) 1143 | * [крестик 14 px](http://bevis-ui.github.io/docs/images/input__clear/clear-14.png) 1144 | * [крестик 14 px для hover](http://bevis-ui.github.io/docs/images/input__clear/clear-14-hover.png) 1145 | 1146 | Во-вторых, стили сейчас написаны так глупо, что картинка `clear-14.png` будет применяться для обоих вью - для 1147 | "Большого" и "Маленького Инпута". Это место хочется параметризировать: для "Большого Инпута" показывать 1148 | картинку `clear-14.png`, а для "Маленького Инпута" - `clear-10.png`. Что делаем? Конечно, новый миксин! :) 1149 | 1150 | Создаём файл `_skin/input_skin_clear.styl` и выносим в него из файла `input_skin_base.styl` только тот код, который 1151 | можно параметризировать: 1152 | 1153 | ```css 1154 | input_skin_clear($size) { 1155 | background: url('images/clear-' + $size + '.png') 50% 50% no-repeat; 1156 | 1157 | &:hover { 1158 | background-image: url('images/clear-' + $size + '-hover.png'); 1159 | opacity: 1; 1160 | } 1161 | } 1162 | ``` 1163 | 1164 | Мы понимаем, что параметризировать здесь надо свойство `background-image`, а не составное `background`. Но у нас нет 1165 | цели показать вам максимально оптимизированный `CSS`-код. Мы понимаем, что не нам вас учить, как оптимально писать 1166 | CSS-код :) 1167 | 1168 | Осталось только позвать этот миксин в каждом вью. Добавим вызовы, каждый со своим размером иконки 1169 | 1170 | В `input_small.styl` передаём параметр `$size = 10` 1171 | 1172 | ```css 1173 | .input_small { 1174 | input_skin_base($height = 22px); 1175 | input_skin_clear($size = 10); 1176 | 1177 | /* */ 1178 | &__control{ 1179 | font-size: 13px; 1180 | } 1181 | } 1182 | ``` 1183 | 1184 | А в `input_large.styl` передаём `$size = 14`: 1185 | 1186 | ```css 1187 | .input_large { 1188 | input_skin_base($height = 32px); 1189 | input_skin_clear($size = 14); 1190 | 1191 | /* */ 1192 | &__control{ 1193 | font-size: 16px; 1194 | } 1195 | } 1196 | ``` 1197 | 1198 | Вы столкнулись с неконсистентностью передачи параметров. Почему-то один параметр мы передаём с `px`, а другой без. Но это 1199 | не имеет значения для этой страницы документации. `Stylus` позволяет делать так. Как именно будете писать вы - 1200 | выбор за вами. 1201 | 1202 | Всё. На этом пора закругляться о стилях. 1203 | 1204 | Получилось как-то многословно. Этот мануал требовал от вас внимательности и аккуратности. Мы старались описывать всё 1205 | обстоятельно, по шагам, без спешки, чтобы вы успевали редактировать с нами синхронно. 1206 | 1207 | Вот, что должно было [у вас получиться](https://github.com/bevis-ui/bevis-stub/tree/input-and-form/blocks/input). 1208 | 1209 | Сверьте ваш и наш код сейчас, потому что дальше мы будем программировать поведение текстового поля. Если так случилось, 1210 | что ваш код отличается от нашего - возможно, вы что-то не сделали во время этого урока. Или, что весьма вероятно, мы 1211 | что-то написали не очень понятно или, вообще, с явными ошибками. Если вы нашли ошибку, напишите нам скорее :) 1212 | 1213 | А теперь посмотрим, как мы программируем поведение блока. Нам [сюда](yblock.md). 1214 | -------------------------------------------------------------------------------- /practice/dependencies.md: -------------------------------------------------------------------------------- 1 | # Зависимости 2 | 3 | Зависимости - это очень просто. На вашем сайте есть форма, а в форме есть текстовое поле. Форма без инпута - 4 | бессмысленная штука. Форма - это блок, который отвечает за отправку данных, а инпут совершенно другой блок, 5 | который только и умеет, что получать данные от пользователя. Корректная работы формы зависит от работы инпута. Это 6 | и есть зависимость :) 7 | 8 | Блоки могут зависеть друг от друга по поведению и (или) по отображению. 9 | 10 | Например, инпут слушает на себе событие `onchange`, и как только оно случилось - триггерит кастомное событие "Меня 11 | заполнили текстом!", которое слушает форма. Как только форма его услышала, она отправляет данные на сервер. Это 12 | простой пример "зависимости по поведению". Такие зависимости мы резолвим с 13 | помощью [модульной системы](https://github.com/ymaps/modules). Если вы не знакомы с модулями, 14 | прочтите [эту статью](modules.md) и всё поймёте. 15 | 16 | 17 | Но бывает так, что один блок использует другой не из поведения, а из отображения. Возьмём ту же форму, 18 | её стили описаны в одном стилевом файле, а стили инпута в другом. Логично где-то указать, 19 | что форма зависит от инпута. Зачем разработчику, подключающему форму на свою страницу, заботиться о том, 20 | какие блоки понадобятся для корректной работы формы? Пусть сама форма про себя волнуется. 21 | 22 | Зависимость описывается простым текстом, почти человеческим языком. 23 | 24 | Например, блок `header` использует блок `input`. Создаем файл `header.deps.yaml` со следующим содержимым: 25 | 26 | ```yaml 27 | - input 28 | ``` 29 | 30 | Да, мы используем формат `YAML` для описания зависимостей. Он немногословный, простой и понятный. Хотите разобраться 31 | досконально, прочитайте [руководство](http://en.wikipedia.org/wiki/YAML). А в общих чертах мы и сами вам расскажем :) 32 | 33 | ## Структура 34 | 35 | Каждый `deps.yaml`-файл - это массив. Каждый элемент массива начинается со знака минус (`-`). 36 | 37 | Пример зависимостей шапки от двух блоков: 38 | 39 | ```yaml 40 | - input 41 | - button 42 | ``` 43 | Этим кодом мы объявляем: шапка зависит от инпута и кнопки. 44 | 45 | Если вам надо указать не только имя блока, а ещё что-то (например `view` блока), тогда зависимость описываем чуть 46 | детальнее: указываем `block` и `view`, но по-прежнему это делается после служебного знака (`-`). А чтобы парсер 47 | `yaml` смог понять, где заканчивается первый элемент массива и начинается второй, делаем отступ от начала строки, 48 | выравнивая содержимое: 49 | 50 | ```yaml 51 | - block: button 52 | view: active 53 | - block: input 54 | view: icons 55 | ``` 56 | 57 | Вот и всё про `yaml`-синтаксис. Просто и интуитивно. 58 | 59 | ## Обязательные зависимости 60 | 61 | Флагом `required` в значении `yes` обозначаются те зависимости, код которых при сборке должен предшествовать коду 62 | текущего блока. 63 | 64 | Пример использования `required`: 65 | 66 | ```yaml 67 | - block: design 68 | required: yes 69 | ``` 70 | 71 | Если этот кода написан для шапки, в результате сборки код блока `design` окажется в собранном файле раньше, 72 | чем код самой шапки. 73 | -------------------------------------------------------------------------------- /practice/enb.md: -------------------------------------------------------------------------------- 1 | # Настраиваем сборку проекта 2 | 3 | в разработке 4 | -------------------------------------------------------------------------------- /practice/i18n.md: -------------------------------------------------------------------------------- 1 | # Как настроить локализацию? 2 | 3 | Если под локализацией вы подразумеваете то же, что и мы — перевод сайта на другие языки, — тогда вы, как и мы, 4 | заблуждаетесь. 5 | 6 | Читаем Википедию. 7 | 8 | **Локализация** — процесс адаптации веб-приложения к культуре какой-либо страны. 9 | 10 | **Интернационализация** — технологические приёмы разработки, упрощающие адаптацию продукта к языковым и культурным 11 | особенностям регионов, отличных от того, в котором разрабатывался продукт. 12 | 13 | Важное отличие одного от другого можно сформулировать совсем коротко. Интернационализация — это адаптация продукта 14 | для использования _в разных странах_, в то время как локализация — это добавление специальных функций для 15 | использования _в какой-то определённой стране_. 16 | 17 | Мы, разработчики сайтов, говорим "локализация", но обычно подразумеваем только лишь перевод сайта на разные языки. 18 | А так как этот документ именно о переводах, придётся его переозаглавить. 19 | 20 | # Как настроить интернационализацию? 21 | 22 | В английском языке для слова `internationalization` принято сокращение `i18n`. При этом число `18` означает количество 23 | пропущенных между `i` и `n` букв. Для Локализации `Localization` применяют сокращение `L10n`. Заглавная буква `L` 24 | используется чтобы не путать с `i` в `L10n`, число `10` — количество букв между `L` и `n`. 25 | 26 | Неудивительно, что инструменты, которые так или иначе связаны с интернационализацией, называются с использованием 27 | сокращения `i18n`. В `BEViS` мы не стали ломать привычки. Наши инструменты для интернационализации называются так же. 28 | 29 | Освоить их проще простого, если вы вместе с нами освоили задания из последнего занятия про [yblock](yblock.md). 30 | 31 | Теперь у вас есть страница `/pages/test-page/test-page.page.js`, на которой к этому моменту задекларирован один 32 | единственный блок `form`. Добавим в блок новую опцию `titleText`, в которую передадим произвольный текст. Его-то мы и 33 | будем переводить на разные языки. 34 | 35 | У вас должна получиться такая структура: 36 | 37 | ```javascript 38 | module.exports = function (pages) { 39 | pages.declare('test-page', function (params) { 40 | var options = params.options; 41 | return { 42 | block: 'page', 43 | title: 'test page', 44 | styles: [ 45 | {url: options.assetsPath + '.css'} 46 | ], 47 | scripts: [ 48 | {url: options.assetsPath + '.' + params.lang + '.js'} 49 | ], 50 | body: [ 51 | { 52 | block: 'form', 53 | titleText: 'Лучший кофе на дороге' // <---------- Новая опция 54 | } 55 | ] 56 | }; 57 | }); 58 | } 59 | ``` 60 | 61 | Допишем шаблон для формы. Будем принимать опцию `titleText` и создавать элемент блока `head`, который в `DOM`-дереве 62 | будет представлен заголовком `h1` с переданным текстом. 63 | 64 | Было: 65 | ```javascript 66 | module.exports = function (bt) { 67 | 68 | bt.match('form', function (ctx) { 69 | ctx.enableAutoInit(); 70 | 71 | ctx.setTag('span'); 72 | 73 | ctx.setContent('Содержимое блока'); 74 | }); 75 | 76 | }; 77 | ``` 78 | 79 | Стало: 80 | ```javascript 81 | module.exports = function (bt) { 82 | 83 | bt.match('form', function (ctx) { 84 | ctx.enableAutoInit(); 85 | 86 | ctx.setTag('span'); 87 | 88 | var title = ctx.getParam('titleText'); 89 | ctx.setContent( 90 | { 91 | elem: 'head', 92 | text: title 93 | } 94 | ); 95 | }); 96 | 97 | bt.match('form__head', function (ctx) { 98 | ctx.setTag('h1'); 99 | 100 | var headText = ctx.getParam('text'); 101 | ctx.setContent(headText); 102 | }); 103 | 104 | }; 105 | ``` 106 | 107 | _Я нарочно дал совершенно разные имена опциям шаблона и локальным переменным, чтобы вам было легче проследить, 108 | как текст "Лучший кофе на дороге" передаётся от опции `ctx.getParam('titleText')` до установки контента в 109 | `ctx.setContent(headText);`. Проследите-проследите :) Освежите свои знания про генерацию `HTML` из `BT`-шаблонов._ 110 | 111 | Обновим страницу `localhost:8080/test` в браузере и увидим огромный заголовок про кофе. Я вижу, а вы? 112 | 113 | Очевидно, что в опцию `titleText` передавать готовую строку на русском языке нельзя. Надо передавать какой-то ключ, 114 | по которому нужно _где-то_ получать русский текст для интерфейса на русском языке, английский текст для 115 | интерфейса на английском языке, украинский для украинского, турецкий для турецкого и т.д. 116 | 117 | Слово _где-то_ я выделил курсивом, чтобы сразу о нём поговорить. Невозможно локализировать сайт, не имея некоего 118 | хранилища переведённых текстов. В `BEViS` такое хранилище мы организовали в виде файлов. Создадим его вместе с вами. 119 | 120 | Создайте папку `blocks/i18n/_keyset`. 121 | 122 | Это будет специальное место, где будут храниться переводы всех блоков. Почему внутри папки `blocks/i18n` мы создали 123 | подпапку `_keyset` — об этом не задумывайтесь. Это всё обсудим позже. Сейчас переходим к самому интересному. 124 | 125 | Мы готовим переводы для блока `Form`, поэтому создадим набор ключей для этого блока. Набор ключей будем называть 126 | кисетом (если на ваш взгляд слово "кейсет" звучит лучше, называйте так). Давайте создадим кисет для формы. 127 | 128 | Создаём папку `blocks/i18n/_keyset/i18n_keyset_form.i18n`. Опять же, пока не задумываемся, почему такое странное 129 | название у директории. А внутри неё создаём единственный (пока!) языковой-файл `ru.js` и пишем в него единственный 130 | (пока!) ключ с переводом: 131 | 132 | `blocks/i18n/_keyset/i18n_keyset_form.i18n/ru.js`: 133 | ```javascript 134 | module.exports = { 135 | "form": { 136 | "title-text": "Лучший кофе на дороге" 137 | } 138 | }; 139 | ``` 140 | 141 | Остановимся на содержимом подробнее. 142 | 143 | Языковой файл `ru.js` — это обычный `Node.js`-модуль, который экспортирует объект с единственным полем, который мы 144 | назвали `form`. Мы назвали его так, чтобы имя кисета перекликалось с именем блока `Form`, который мы видим на 145 | web-странице. Когда имена перекликаются, так легче. 146 | 147 | В этом файле сейчас мы описали только один ключ `title-text` и его значением сделали тот самый текст про кофе. Для 148 | наглядности добавим ещё один ключ. Сейчас он нам не нужен, а позже пригодится. 149 | 150 | `blocks/i18n/_keyset/i18n_keyset_form.i18n/ru.js`: 151 | ```javascript 152 | module.exports = { 153 | "form": { 154 | "title-text": "Лучший кофе на дороге", 155 | "hint-content": "Иллюзионист иллюстрирует иллюзорно" 156 | } 157 | }; 158 | ``` 159 | 160 | Хранилище готово. Конечно, оно готово только для русского языка. Сразу приготовим тексты для английского 161 | языка. Создадим в той же папке файл с именем `en.js`. 162 | 163 | `blocks/i18n/_keyset/i18n_keyset_form.i18n/en.js`: 164 | ```javascript 165 | module.exports = { 166 | "form": { 167 | "title-text": "The best coffee on the road", 168 | "hint-content": "Illusionist illustrates illusorily" 169 | } 170 | }; 171 | ``` 172 | 173 | А теперь давайте сделаем так, чтобы приложение ходило за текстом про кофе именно в это хранилище. 174 | 175 | ## Локализация внутри декларации страницы 176 | 177 | Первое, что мы делаем всегда после создания нового блока, указываем зависимость — сообщаем, что такой-то 178 | блок будет использоваться на такой-то странице. 179 | 180 | Редактируем `pages/test-page/test-page.deps.yaml`, добавляемость зависимость к блоку `i18n` и указываем, что на этой 181 | странице используется кисет `form`: 182 | ``` 183 | - page 184 | - block: block 185 | elem: auto-init 186 | - input 187 | - super-input 188 | - form 189 | - block: i18n 190 | keyset: form # <----- Вот он 191 | ``` 192 | 193 | Всё. Теперь в декларации страницы убираем русский текст и вместо него просим ключ из хранилища: 194 | 195 | ```javascript 196 | module.exports = function (pages) { 197 | pages.declare('test-page', function (params) { 198 | var options = params.options; 199 | return { 200 | block: 'page', 201 | title: 'test page', 202 | styles: [ 203 | {url: options.assetsPath + '.css'} 204 | ], 205 | scripts: [ 206 | {url: options.assetsPath + '.' + params.lang + '.js'} 207 | ], 208 | body: [ 209 | { 210 | block: 'form', 211 | titleText: pages.i18n('form', 'title-text') // <------ Здесь зовём i18n-ключ 212 | } 213 | ] 214 | }; 215 | }); 216 | }; 217 | ``` 218 | 219 | Обновляем страницу в браузере, видим текст про кофе. Как понять, а вдруг это кеш, и у нас ничего не получилось? Можно 220 | изменить текст в кисете и проверить. Редактируем? 221 | 222 | `blocks/i18n/_keyset/i18n_keyset_form.i18n/ru.js`: 223 | ```javascript 224 | module.exports = { 225 | "form": { 226 | "title-text": "Лучший кофе на дороге — отхлебнёшь, протянешь ноги! (с)", // <----- Изменили значение 227 | "hint-content": "Иллюзионист иллюстрирует иллюзорно" 228 | } 229 | }; 230 | ``` 231 | 232 | Обновляем в браузере. Есть новый текст! Оглушительный успех ;) 233 | 234 | Мы позвали перевод, находясь внутри декларации страницы, с помощью метода `pages.i18n`, который умеет принимать два 235 | параметра — имя кисета и имя ключа: 236 | ```javascript 237 | pages.i18n('form', 'title-text') 238 | ``` 239 | 240 | Этот метод ходит в хранилище переводов, находит там нужный кисет `form`, а в нём нужный ключ `title-text` и 241 | возвращает значение ключа `Лучший кофе на ...` 242 | 243 | Похожий метод (похожий, но не этот же самый) доступен нам не только в декларации страницы, но и в `bt`-шаблонах 244 | любого блока. 245 | 246 | ## Локализация в `bt`-шаблонах 247 | 248 | Откроем `blocks/form/form.bt.js` и добавим новый элемент `hint`: 249 | ```javascript 250 | module.exports = function (bt) { 251 | 252 | bt.match('form', function (ctx) { 253 | ctx.enableAutoInit(); 254 | 255 | ctx.setTag('span'); 256 | 257 | var title = ctx.getParam('titleText'); 258 | ctx.setContent([ 259 | { 260 | elem: 'head', 261 | text: title 262 | }, 263 | { 264 | elem: 'hint', // <----- Создали новый элемент 265 | textHint: bt.lib.i18n('form', 'hint-content') // <----- Позвали ключ для него 266 | } 267 | ]); 268 | }); 269 | 270 | // Шаблон для элемента hint 271 | bt.match('form__hint', function (ctx) { 272 | ctx.setContent(ctx.getParam('textHint')); 273 | }); 274 | 275 | bt.match('form__head', function (ctx) { 276 | ctx.setTag('h1'); 277 | 278 | var headText = ctx.getParam('text'); 279 | ctx.setContent(headText); 280 | }); 281 | 282 | }; 283 | ``` 284 | 285 | В шаблоне формы мы сгенерили новый элемент `hint` и в его опцию `textHint` передали значение ключа `hint-content`. 286 | Когда обновим браузер, увидим под заголовком текст про иллюзиониста. 287 | 288 | Успех! 289 | 290 | Мы рассмотрели способы, как звать ключи, находясь в декларации страницы или в `bt`-шаблонах блока. 291 | 292 | Декларация страницы работает на сервере, создаёт статический `HTML`, который летит пользователю в браузер. 293 | `BT`-шаблоны умеют работать как на сервере, так и в браузере. 294 | 295 | А как пользоваться `i18n` в клиентском `javascript`? 296 | 297 | ## Локализация в клиентских `js`-модулях 298 | 299 | ```javascript 300 | modules.define( 301 | 'form', 302 | [ 303 | 'inherit', 304 | 'block', 305 | 'input', 306 | 'super-input', 307 | 'y-i18n' // <---------- Позвали клиентский модуль y-i18n 308 | ], 309 | function ( 310 | provide, 311 | inherit, 312 | YBlock, 313 | Input, 314 | SuperInput, 315 | i18n // <---------- Приняли его в качестве аргумента i18n 316 | ) { 317 | var form = inherit(YBlock, { 318 | __constructor: function () { 319 | this.__base.apply(this, arguments); 320 | 321 | var formDomNode = this.getDomNode(); 322 | 323 | // Создаём инпут 324 | this._greetingInput = new Input({ 325 | value: 'Привет, Бивис', 326 | name: 'loginField', 327 | placeholder: 'Инпут на сайте', 328 | 329 | parentNode: formDomNode 330 | }); 331 | this._greetingInput.on('input-submitted', this._onInputSubmitted, this); 332 | 333 | // Создаём инпут для пароля 334 | this._passwordInput = new SuperInput({ 335 | name: 'passwordField', 336 | type: 'password', 337 | placeholder: i18n('form', 'hint-content'), // <---------- Позвали любой ключ 338 | 339 | parentNode: formDomNode 340 | }); 341 | this._passwordInput.on('input-submitted', this._onInputSubmitted, this); 342 | }, 343 | 344 | /** 345 | * Реагирует на нажатие клавиши Enter в `Input` 346 | * @param {YEventEmitter} e 347 | */ 348 | _onInputSubmitted: function (e) { 349 | console.log('Форма поймала событие на Input = ', e); 350 | } 351 | }, { 352 | getBlockName: function () { 353 | return 'form'; 354 | } 355 | }); 356 | 357 | provide(form); 358 | }); 359 | ``` 360 | 361 | _Напомню, что классы `Input`, `SuperInput` и класс `Form` мы создавали с вами вместе, когда читали статью про 362 | [клиентское программирование с помощью YBlock](yblock.md). Код этих компонентов можно увидеть в отдельной ветке 363 | [input-and-form](https://github.com/bevis-ui/bevis-stub/tree/input-and-form/blocks)._ 364 | 365 | В коде выше мы смотрим на компонент `Form`. Для того, чтобы использовать локализацию в клиентсокм модуле, я минимально 366 | изменил класс `Form`, пометив все изменения комментариями. В прейсхолдер инпута для пароля я позвал строку про 367 | иллюзиониста. Обновите страницу в браузере. Успех? 368 | 369 | Обратите внимание на важный момент. 370 | 371 | В проекте есть две сущности с почти одинаковыми названиями. 372 | 373 | 1. В движке проекта _уже есть_ специальный модуль с именем `y-i18n`, который вы можете звать в своих клиентских 374 | модулях, как в примере выше. Этот модуль создавать не нужно, он просто есть по умолчанию, находится где-то в файлах 375 | `BEViS`-движка. Он умеет умеет ходить в ваше хранилище переводов и забирать оттуда строки на разных языках. 376 | 2. И есть _ваше хранилище_ переводов, которое находится по адресу `blocks/i18n`. Хранилище представлено неким блоком 377 | с именем `i18n`, и его дочерним элементом `_keyset`. Мы специально реализовали хранилище в виде так называемого 378 | `блока`, чтобы им удобно было пользоваться. Напомню, чтобы им пользоваться, нужно в зависимостях страницы указать 379 | имя этого блока и имя нужного кисета, как мы делали это выше в файле `pages/test/test-page.deps.yaml`: 380 | ``` 381 | - page 382 | - block: block 383 | elem: auto-init 384 | - input 385 | - super-input 386 | - form 387 | - block: i18n # <----- Позвали на страницу хранилище переводов 388 | keyset: form # <----- Указали, каким кисетом будем пользоваться. 389 | ``` 390 | 391 | Можно указывать столько кисетов, сколько хочется, если в `yaml`-файле перечислить их в виде массива: 392 | ``` 393 | - block: i18n 394 | keyset: [form, any-other] # <----- Позвали кисеты form и any-other 395 | ``` 396 | 397 | Зависимости можно указывать не только в страничных файлах, а в блоках. Например, мы знаем что модуль `Form` 398 | использует внутри себя переводы. Тогда кисеты можно подтягивать не в страничном `pages/test/test-page.deps.yaml`, а в 399 | блочном `blocks/form/form.deps.yaml`. Этого файла ещё нет. Создадим его и напишем в нём: 400 | ``` 401 | - block: i18n 402 | keyset: [form, any-other] 403 | ``` 404 | 405 | А в страничном `pages/test/test-page.deps.yaml` удалим эту зависимость и обновим страницу в браузере. Ничего не 406 | сломалось, иллюзионист что-то там иллюстрирует в поле для пароля. Ну, отлично. Получается, что любой блок в своём 407 | файле зависимостей может указать, от каких других блоков именно он зависит. Если зависимости указывать таким образом, 408 | гарантирован порядок — на страницу придут только те модули, которые нужно конкретным блокам на текущей странице. 409 | 410 | Обратили внимание, что мы указали зависимость к кисету `any-other`, а его нет на файловой системе? Ну и ничего не 411 | сломалось при обновлении страницы. А что случится, если мы в клиентском модуле попробуем получить ключ из этого 412 | кисета? Пробуем? Давайте отредактируем в модуле `Form` вызов конструктора инпута для пароля: 413 | 414 | ```javascript 415 | // Создаём инпут для пароля 416 | this._passwordInput = new SuperInput({ 417 | name: 'passwordField', 418 | type: 'password', 419 | placeholder: i18n('any-other', 'my-key'), // <---- Позвали несуществующий ключ 'my-key' из 420 | // несуществующего кисета 'any-other' 421 | 422 | parentNode: formDomNode 423 | }); 424 | ``` 425 | 426 | После рефреша страницы в консоли видим ошибку: 427 | ``` 428 | Uncaught Error: form init error: Keyset "any-other" was not found. 429 | ``` 430 | 431 | Отлично, приложение помогает понять, что мы что-то забыли. Давайте создадим этот кисет и будем заканчивать. 432 | 433 | Кстати, я не сказал, в приложении есть скрипт для создания кисетов из терминала: 434 | ``` 435 | make keyset 436 | # Введите имя keyset 437 | ``` 438 | 439 | Этот скрипт создаёт в хранилище нужную папку для кисета и два файла с переводами - `ru.js` и `en.js`. В каждом создаёт 440 | один фейковый ключ, который назван незатейливо `my-key`. В английском файле значение ключа установлено в `my value`, 441 | а в русском в `моё значение`. Я же говорю, кисет незатейливый :) 442 | 443 | После рефреша в поле пароля видим плейсхолдер "моё значение". 444 | 445 | Всё, теперь вы знаете всё, чтобы сделать сайт интернационализированным. Осталось самое главное :) 446 | 447 | ## Как переключать интерфейс на другой язык? 448 | 449 | Конечно, мы наделали кучу файлов с переводами, умеем из них получать строки, умеем это делать не только в страничном 450 | `btjson`, но и в `bt`-шаблонах и даже в клиентских `javascript`-модулях. А как переключить приложение на другой язык? 451 | Ведь это же самое главное :) 452 | 453 | Откроем `pages/test/test-page.page.js`: 454 | ```javascript 455 | module.exports = function (pages) { 456 | pages.declare('test-page', function (params) { 457 | var options = params.options; 458 | return { 459 | block: 'page', 460 | title: 'test page', 461 | styles: [ 462 | {url: options.assetsPath + '.css'} 463 | ], 464 | scripts: [ 465 | {url: options.assetsPath + '.' + params.lang + '.js'} // <----- Здесь! 466 | ], 467 | body: [ 468 | { 469 | block: 'form', 470 | titleText: pages.i18n('form', 'title-text') 471 | } 472 | ] 473 | }; 474 | }); 475 | }; 476 | ``` 477 | 478 | Видите строку `{url: options.assetsPath + '.' + params.lang + '.js'}`? 479 | 480 | Именно переменная `params.lang` отвечает за то, какой язык будет использоваться в _клиентских `js`-модулях_. 481 | Я не спроста выделил курсивом последние слова. Убедимся на опыте. Сделайте быструю замену прямо сейчас. Замените 482 | `params.lang` на `'en'` 483 | 484 | Было: 485 | ```javascript 486 | {url: options.assetsPath + '.' + params.lang + '.js'} 487 | ``` 488 | 489 | Стало: 490 | ```javascript 491 | {url: options.assetsPath + '.en.js'} 492 | ``` 493 | 494 | И обновите страницу в браузере. Инпуты не отренедрились? А в консоли вообще ошибка! 495 | ``` 496 | GET 404 http://localhost:8080/build/test/_test.en.js 497 | ``` 498 | 499 | Файл не загрузился, нет его! Хм, так и должно быть. Ведь файла с расширением `*.en.js` не существует. Убедитесь 500 | сами, загляните в папку для собраннх файлов `build/test`. Там есть только файлы с расширением `*.ru.js`. Дело в том, 501 | что `BEViS` ничего не знает о том, что (оказывается!) он должен собирать ещё и файлы с расширением `*.en.js`. Так 502 | давайте скажем ему. 503 | 504 | Это происходит в файле `.enb/make.js`. Это очень важный файл, в котором мы описываем конфигурацию сборки наших 505 | файлов. Там много разного написано, нас же сейчас интересует одна единственная строка: 506 | ```javascript 507 | config.setLanguages(['ru']); 508 | ``` 509 | 510 | Именно в ней мы указываем, с какими языками собирать файлы. Допишите туда `en`, чтобы получилось так: 511 | ```javascript 512 | config.setLanguages(['ru', 'en']); 513 | ``` 514 | 515 | Обновим страницу. Так, инпуты появились. Но тексты на русском языке! 516 | 517 | Погодите-погодите, один текст всё-таки на английском - тот, который внутри инпута, который мы устанавливали из 518 | клиентского модуля `form.js`. Убедились? 519 | 520 | Всё верно, так и должно быть — страничный `build/test/_test.en.js` отвечает за локализацию в клиентских модулях и 521 | клиентских `bt`-шаблонах. 522 | 523 | ## Локализация клиентских `bt`-шаблонов и клиентских `js`-модулей 524 | 525 | Запомним. Вся локализация *на клиенте* зависит от того, какой файл прилетел в браузер. Прилетит `test.ru.js` — будет 526 | сайт на русском. Прилетит `test.en.js` — будет сайт на английском. Но! Локализуются только те переменные, которые 527 | использовались в клиентском `javascript` — в клиентских `js`-модулях и в тех `bt`-шаблонах, которые работают в 528 | браузере (не на сервере!). Не очень понятно, да? 529 | 530 | 531 | Смотрите. В теле нашей страницы в конце `` загружается файл `build/test/_test.en.js`. Это собранный файл, 532 | состоящий из множества клиентских блочных модулей, таких как `blocks/form/form.js` и `blocks/input/input.js` (и ещё 533 | множество других файлов). Когда страничный файл загружается в память браузера, он инициализирует все модули, в том 534 | числе и модуль `form.js`. А в его конструкторе в том числе написано такое: 535 | 536 | ```javascript 537 | // Создаём инпут для пароля 538 | this._passwordInput = new SuperInput({ 539 | name: 'passwordField', 540 | type: 'password', 541 | placeholder: i18n('any-other', 'my-key'), 542 | 543 | parentNode: formDomNode 544 | }); 545 | ``` 546 | 547 | Что происходит после `new SuperInput({/*здесь btjson блока*/})`, вы знаете. На лету в памяти браузера (то есть, 548 | "на клиенте") `BEViS` выполняет `bt`-шаблоны, которые из `btjson`-а строят `HTML` — те самые шаблоны, которые 549 | описаны в файле `input.bt.js`. Вы должны уже помнить, что в собранный `build/test/_test.en.js` попадают не только все 550 | клиентские модули, а ещё и все `bt`-шаблоны, которые умеют строить `HTML` не только на сервере, но ещё и налету на 551 | клиенте (то есть, "в памяти браузера"). 552 | 553 | Ещё раз. 554 | 555 | Вся локализация *на клиенте* зависит от того, какой файл прилетел в браузер. Прилетит `test.ru.js` — будет сайт на 556 | русском. Прилетит `test.en.js` — будет сайт на английском. Но! Локализуются только те переменные, которые 557 | использовались в клиентском `javascript` — в клиентских `js`-модулях и в тех `bt`-шаблонах, которые работают в 558 | браузере (не на сервере!). 559 | 560 | А как быть с сервером? 561 | 562 | ## Локализация серверных `bt`-шаблонов 563 | 564 | Во-первых, если мы делаем локализацию сайта, странно хардкодить язык в декларацию страницы. Поэтому, давайте откатим 565 | последнее изменение в `pages/test/test-page.page.js`, чтобы в строке снова появилась переменная `params.lang` 566 | 567 | ```javascript 568 | {url: options.assetsPath + '.' + params.lang + '.js'} 569 | ``` 570 | 571 | Если сейчас обновим браузер и посмотрим в сгенеренный `HTML`, увидим, что благодаря этой строке запрашивается файл 572 | `build/test/_test.ru.js`. Следовательно в переменной `params.lang` хранится значение `ru`. А где же оно 573 | устанавливается? 574 | 575 | Откройте файл `server/page.js`. Кстати, вам известно, как устроено `Node.js`-приложение? Если известно, вас этот файл 576 | не испугает . А если вы ещё не знакомы с устройством `Node.js`-приложений, не пугайтесь — вам пока не надо понимать, 577 | что происходит в этом файле. Я и сам в нём не всё понимаю ;) 578 | 579 | А если серьёзно, этот файл — часть `Node.js`-приложения, отвечающая за генерацию статического `HTML`. И 580 | непосредственно генерация происходит в методе `handle`: 581 | ```javascript 582 | /** 583 | * Build page 584 | * 585 | * @returns {Promise} promise 586 | */ 587 | handle: function () { 588 | return vow.all([ 589 | this._getPages(), 590 | this._getTemplate(), 591 | this._getI18n() 592 | ]) 593 | .spread(function (pages, bt, buildI18n) { 594 | var i18n = buildI18n(); 595 | pages.setI18n(i18n); 596 | bt.lib.i18n = i18n; 597 | 598 | return pages.exec(this._pageName, { 599 | query: this._query, 600 | options: this._getPageOptions(), 601 | lang: this._lang 602 | }).then(function (btJson) { 603 | return this._applyTemplate(btJson, bt); 604 | }.bind(this)); 605 | }.bind(this)); 606 | } 607 | ``` 608 | 609 | Вдумываться в то, что здесь написано, не надо. Вообще! Если вы ещё не знакомы с `Promises`, вам совершенно непонятно, 610 | что тут происходит. Да вообще не важно! Нам бы с языком разобраться и понять, где формируется объект `params` и его 611 | переменная `params.lang`. А смотрите, именно в этом методе они и формируются. Вот где: 612 | ```javascript 613 | return pages.exec(this._pageName, { // ---- 614 | query: this._query, // | 615 | options: this._getPageOptions(), // > Вот этот объект! 616 | lang: this._lang // | 617 | }) // ---- 618 | ``` 619 | 620 | А потом этот самый объект приходит в файл `pages/test/test-page.page.js` в качества параметра `params`: 621 | ```javascript 622 | module.exports = function (pages) { 623 | pages.declare('test-page', function (params) { 624 | var options = params.options; // ^------------- Вот этот же объект! 625 | return { 626 | block: 'page', 627 | title: 'test page', 628 | styles: [ 629 | {url: options.assetsPath + '.css'} 630 | ], 631 | scripts: [ 632 | {url: options.assetsPath + '.' + params.lang + '.js'} // <----- Здесь! 633 | ], 634 | body: [ 635 | { 636 | block: 'form', 637 | titleText: pages.i18n('form', 'title-text') 638 | } 639 | ] 640 | }; 641 | }); 642 | }; 643 | ``` 644 | 645 | А в нём есть поле `lang`, которое получает значение из внутренней переменной объекта `Page` (см.строку 646 | `lang:this._lang`). А что хранится в `this._lang`? Смотрим выше, в конструктор `Page` и видим интересное: 647 | ```javascript 648 | /** 649 | * Page 650 | * 651 | * @param {String} id Relative path to page (pages/index, for example) 652 | * @param {String} pageName Page name. 653 | * @param {Request} req Node request Object 654 | * @param {Response} res Node response Object 655 | */ 656 | __constructor: function (id, pageName, req, res) { 657 | this._id = id; 658 | this._pageName = pageName; 659 | this._req = req; 660 | this._res = res; 661 | this._lang = req.query.lang || 'ru'; // <-------- Вот оно! 662 | this._query = req.query; 663 | } 664 | ``` 665 | 666 | Теперь понятно. Если не определена переменная `req.query.lang`, тогда установи `this._lang` в `ru`! 667 | 668 | Объект `req` — это объект `Node.js`-сервера, в котором собрана вся информация о `HTTP`-запросе в сервер. Там есть о 669 | запросе всё — имя страницы, которую вы запросили в браузере, все параметры запроса (я имею в виду `GET`, `POST`), 670 | все куки, `HTTP`-заголовки, — вообще всё! 671 | 672 | Конкретно параметры запроса хранятся в поле `req.query`. Следовательно, если мы запросим страницу `test` с 673 | `GET`-параметром `lang=en`, тогда наш сайт станет англоязычным, что ли? Ну-ка, попробуем: 674 | ``` 675 | http://localhost:8080/test?lang=en 676 | ``` 677 | 678 | Да, оглушительный успех! Я вижу страницу с английскими текстами. 679 | 680 | Ну, кроме первого инпута, в котором по-русски написано "Привет, Бивис". Но это не ошибка, мы же сами захардкодили эти 681 | тексты в `blocks/form/form.js`. 682 | 683 | Вот и ответ на вопрос. Чтобы серверные `bt`-шаблоны получили нужный язык, нужно его установить в параметрах генерации 684 | статического `HTML`, то есть здесь: 685 | ```javascript 686 | /** 687 | * Build page 688 | * 689 | * @returns {Promise} promise 690 | */ 691 | handle: function () { 692 | return vow.all([ 693 | this._getPages(), 694 | this._getTemplate(), 695 | this._getI18n() 696 | ]) 697 | .spread(function (pages, bt, buildI18n) { 698 | var i18n = buildI18n(); 699 | pages.setI18n(i18n); 700 | bt.lib.i18n = i18n; 701 | 702 | return pages.exec(this._pageName, { 703 | query: this._query, 704 | options: this._getPageOptions(), 705 | lang: this._lang // <----------------------- Здесь! 706 | }).then(function (btJson) { 707 | return this._applyTemplate(btJson, bt); 708 | }.bind(this)); 709 | }.bind(this)); 710 | } 711 | ``` 712 | 713 | Если в `BEViS`-приложении в запросе передавать `GET`-параметр `lang`, тогда можно легко менять язык интерфейса. 714 | Правда, есть ошибка, которую мы исправлять в `bevis-stub` не будем. Сделайте запрос к несуществующему языку, например к 715 | турецкому: 716 | ``` 717 | http://localhost:8080/test?lang=tr 718 | ``` 719 | 720 | Приложение упадёт с "Internal error", а в терминале вывалится сообщение типа такого: 721 | ``` 722 | 00:14:31 - error: Error: There is no tech for target build/test/test.lang.tr.js. 723 | ``` 724 | 725 | Нет такого языка, вот вам и ошибка. 726 | 727 | Но исправлять это мы не будем, потому что от проекта к проекту способы получать язык пользователя разнятся. Кто-то 728 | будет читать язык из `GET`-параметра (и ещё не факт, что он будет называться `lang`), кто-то будет читать его из кук, в 729 | каком-то проекте язык интерфейса будет приходить из базы данных. Поэтому это остаётся на ваше усмотрение. Ну и да, 730 | придётся чуть больше узнать про то, как устроено `Node.js`-приложение, почитать мануалы. 731 | 732 | Главное — (и вы это, бесспорно, давно поняли) чтобы серверные `bt`-шаблоны сгенерили `HTML` с нужным языком, следует 733 | в `server/page .js` в методе `handle` установить параметр `lang`. 734 | 735 | Всё, теперь всё. Теперь вы можете перевести свой сайт на разные языки. А когда это сделаете, предлагаю вам продолжить 736 | практикум. 737 | 738 | Я хочу вам рассказать о том, [как использовать `MVC`-паттерн в `BEViS`](mvc-app.md). Хочется порядка в коде проекта, 739 | хочется привычных абстракций в виде контроллеров, моделей и вью. Это делается быстро и несложно. Посмотрим вместе? 740 | 741 | Сделайте вкусного чая и приходите, я жду вас [здесь](mvc-app.md). 742 | 743 | ---- 744 | 745 | PS. Переводы проекта хранятся в папке `blocks/i18n/_keyset/*`. В Яндексе мы не создаём руками эти кисеты. У нас есть 746 | специальный внутренний веб-сервис, в котором можно завести кисеты, создать там через веб-интерфейс нужные ключи на 747 | русском языке и "призвать" переводчиков, чтобы они перевели ключи на нужные языки. А потом мы с помощью некоторого 748 | скрипта обращаемся по `HTTP` к этому сервису, как к бекенду, и получаем переводы из него в виде готовых файлов 749 | `ru.js`, `en.js`, `tr.js`. И этот же скрипт сам раскладывает файлы переводов внутри папки `blocks/i18n/_keyset/*`. 750 | 751 | Возможно, в своей команде вы тоже сделаете какой-то такой сервис, с ним удобно. А если по каким-то причинам не 752 | сделаете, не беда, хранилище переводов можно поддерживать и руками. 753 | 754 | 755 | 756 | 757 | 758 | 759 | 760 | 761 | 762 | 763 | 764 | 765 | 766 | 767 | 768 | 769 | 770 | -------------------------------------------------------------------------------- /practice/modules.md: -------------------------------------------------------------------------------- 1 | # Модульная система YMaps Modules 2 | 3 | Что такое Модульная система? Зачем и для кого? Как этим пользоваться? 4 | 5 | На эти вопросы я давно хотел получить ответы, но стеснялся их задать. Если вы такой же робкий, этот документ для вас :) 6 | 7 | Что это? 8 | -------- 9 | Вы web-разработчик, пишете сайты; на страницах верстаете формы с контролами, 10 | различные компоненты, большие и маленькие; для большинства из них 11 | пишете клиентский `js`-код. Вот и продолжайте это делать. Пока у вас нет своего клиентского `js`-кода, 12 | модульная система вам не нужна. Никому не нужен пустой фантик без конфеты внутри. 13 | 14 | Модуль - это фантик, в который вы заворачиваете конфету. На фантике написано имя конфеты и больше ничего. Оберните 15 | код пяти своих контролов каждый в свой отдельный фантик, у вас получится 5 модулей — система из пяти **ваших** 16 | модулей. Конфеты ссыпают в корзинку для сладостей, откуда конфеты удобно таскать детям. Модульная система и есть 17 | такая корзинка. 18 | 19 | Зачем это и для кого? 20 | -------------------- 21 | Как вы связываете `js`-компоненты между собой? Например, вы объявляете на странице блок с формой авторизации, 22 | пишете для неё клиентский `js`-код, в котором описываете поведение формы: если нажали на кнопку сабмита — отправь 23 | авторизационные данные аяксом, а не через стандартный `form.submit()`. 24 | 25 | И предположим, в вашем проекте уже есть специальный компонент "кнопка", в котором реализовано всё поведение кнопки, 26 | например, "если меня нажали, выкинь вверх некое событие, чтобы форма могла это событие услышать и отреагировать нужным 27 | образом". 28 | 29 | То есть, ваша "форма" должна уметь найти кнопку и слушать на ней событие клика. 30 | 31 | Вопрос: а как вы определяете, что кнопка **уже готова** взаимодействовать с вашей формой? Наверное, в `js`-файле вы 32 | сначала описываете функциональность для кнопки, а ниже - для формы? А если js-код кнопки и формы лежат в разных 33 | файлах? Тогда вы так настраиваете сборку финального `js`-файла, чтобы всё равно код формы оказался описан ниже 34 | кода кнопки? 35 | 36 | Модульная система избавляет нас от этих трудностей. Она избавляет нас от головной боли отслеживать зависимости между 37 | компонентами, поддерживать руками эти зависимости в актуальном состоянии. 38 | 39 | Если вы оформляете форму в виде `ymaps-модуля`, вы просто декларируете зависимость от кнопки, и знаете, 40 | что код формы будет активирован _только тогда_, когда активирован код кнопки. Вам не нужно самому отслеживать 41 | готовность ваших компонентов взаимодействовать друг с другом. Это делает за вас модульная система. 42 | 43 | 44 | Как этим пользоваться? 45 | --------------------- 46 | Это болванка `ymaps-модуля`: 47 | 48 | ````javascript 49 | modules.define( 50 | 'A', 51 | ['B', 'C'], 52 | function(provide, b, c) { 53 | var a = {}; 54 | 55 | provide(a); 56 | } 57 | ); 58 | ```` 59 | 60 | В этом примере ваш собственный код - единственная строка: 61 | ````javascript 62 | var a = {}; 63 | ```` 64 | 65 | В реальности, конечно, вместо этой строчки вы напишете намного больше кода, но для знакомства с модульной системой 66 | этого достаточно. 67 | 68 | Всё остальное кроме этой строки и есть модуль. Модуль - это своего рода обёртка, фантик — несколько специальных 69 | инструкций, которые и делают из вашего кода модуль. Внутрь фантика вы складываете нужные ингредиенты (зависимости от 70 | других модулей), перемешиваете, как вам хочется (пишете код своего компонента), фантик скручиваете и втыкаете 71 | в бок зубочистку-флажок с именем модуля. Вы сделали модуль `А`. 72 | 73 | Давайте посмотрим на конфету внимательно. 74 | 75 | Так объявляется модуль. Позовите метод `define()` 76 | ````javascript 77 | modules.define( 78 | ); 79 | ```` 80 | 81 | Скажите модулю: "Тебя зовут `А`" 82 | ````javascript 83 | modules.define( 84 | 'A' 85 | ); 86 | ```` 87 | 88 | Потом добавьте: "Вообще-то ты зависишь от двух модулей, их зовут`В` и `С`" 89 | ````javascript 90 | modules.define( 91 | 'A', 92 | ['B', 'C'], 93 | ); 94 | ```` 95 | 96 | Опишите код модуля в анонимной функции. В него параметрами `b` и `c` придёт код модулей `B` и `С`, и вы сможете 97 | использовать их внутри модуля. 98 | ````javascript 99 | modules.define( 100 | 'A', 101 | ['B', 'C'], 102 | function(provide, b, c) { 103 | 104 | } 105 | ); 106 | ```` 107 | 108 | Чтобы другие модули могли использовать ваш модуль, пусть он громко произнесёт вслух: 109 | "Модуль `А` - это я!". Это делается вызовом функции `provide(а)`, в которую аргументом передаётся ссылка на ваш класс 110 | или объект, то есть на сам программный код вашего модуля. 111 | ````javascript 112 | modules.define( 113 | 'A', 114 | ['B', 'C'], 115 | function(provide, b, c) { 116 | var a = {}; 117 | 118 | provide(a); 119 | } 120 | ); 121 | ```` 122 | 123 | И, наконец, напишите свой компонент. Пусть это будет что-нибудь хорошее ;) 124 | ````javascript 125 | modules.define( 126 | 'A', 127 | ['B', 'C'], 128 | function(provide, b, c) { 129 | var a = 'Yandex is a good company'; 130 | 131 | provide(a); 132 | } 133 | ); 134 | ```` 135 | 136 | Всё. Сложности закончились :) 137 | 138 | А вот так, к примеру, может выглядеть код модуля, отвечающий за форму логина. Создадим фантик, 139 | позовём в него кнопку и что-нибудь запрограммируем внутри: 140 | ````javascript 141 | modules.define( 142 | 'form', 143 | ['button'], 144 | function(provide, button) { 145 | var form = getElementById('my-form'); 146 | form.on('submit', onSubmited); 147 | 148 | function onSubmited() { 149 | if ($(button).css('disabled')) { 150 | return false; 151 | } 152 | form.submit(); 153 | } 154 | 155 | provide(form); 156 | } 157 | ); 158 | ```` 159 | 160 | Это выдуманный пример, скорее всего он нерабочий, но для иллюстрации годится. В теле анонимной функции мы программируем 161 | поведение формы в зависимости от того, активна или неактивна кнопка. А после этого методом `provide(form)` 162 | выбрасываем вверх флаг: "Форма — это я". Теперь и этот модуль может быть вызван другими модулями. Метод `provide` - 163 | это выкинутая вверх рука и всемирно известное: "Свободная касса!" 164 | 165 | Возвращаясь к аналогии с конфетой, фантик — это метод `define()` с анонимной функцией внутри. В фантик воткнута 166 | зубочистка, за которую конфетку удобно взять — это метод `provide()`. Модуль независим и работоспособен, потому что 167 | он будет запущен только после того, как все зависимости будут предоставлены (правильные программисты используют 168 | термины "разрезолвлены" или "разрешены") внутрь модуля. 169 | 170 | Осталось сказать, что зависимости резолвятся асинхронно, дерево зависимостей строится в рантайме, 171 | модули можно переопределять и доопределять. 172 | 173 | Если захотите заглянуть в репозиторий модульной системы, проследуйте сюда: 174 | [https://github.com/ymaps/modules](https://github.com/ymaps/modules/blob/master/README.ru.md), и возвращайтесь к 175 | документации про `BEViS`, я вас там жду. 176 | -------------------------------------------------------------------------------- /practice/new-block.md: -------------------------------------------------------------------------------- 1 | Первый шаг мы [сделали](new-page.md), теперь у нас есть страница `test-page.page.js`. 2 | 3 | Блок, который мы создадим в этом руководстве, будем проверять именно на ней. Если вы её создали, 4 | тогда она выглядит точно как у нас: 5 | 6 | ```javascript 7 | module.exports = function (pages) { 8 | pages.declare('index-page', function (params) { 9 | var options = params.options; 10 | return { 11 | block: 'page', 12 | title: 'index page', 13 | styles: [ 14 | {url: options.assetsPath + '.css'} 15 | ], 16 | scripts: [ 17 | {url: options.assetsPath + '.' + params.lang + '.js'} 18 | ], 19 | body: [ 20 | // здесь ваши блоки 21 | ] 22 | }; 23 | }); 24 | } 25 | ``` 26 | 27 | Если такой страницы у вас ещё нет, [создайте](new-page.md) её и возвращайтесь. 28 | 29 | # Как создать новый блок 30 | 31 | Сделаем несложный блок, например, `input`. Перед тем, как его делать, попробуем его позвать. А вдруг? 32 | 33 | ```javascript 34 | module.exports = function (pages) { 35 | pages.declare('test-page', function (params) { 36 | var options = params.options; 37 | return { 38 | block: 'page', 39 | title: 'test page', 40 | styles: [ 41 | {url: options.assetsPath + '.css'} 42 | ], 43 | scripts: [ 44 | {url: options.assetsPath + '.' + params.lang + '.js'} 45 | ], 46 | body: [ 47 | {block: 'input'} // Добавьте вызов блока 48 | ] 49 | }; 50 | }); 51 | } 52 | ``` 53 | 54 | Запустим приложение командой `make` и запросим в браузере нашу страницу `localhost:8080/test/`. Успех? В браузере 55 | появился блок, хотя мы его даже не создавали? 56 | 57 | ``` 58 | 59 | 60 | 61 | 62 | test page 63 | 64 | 65 | 66 |
67 | 68 | 69 | 70 | ``` 71 | 72 | Шаблонизатор сам создал блок. Где-то внутри bt-шаблонизатора есть дефолтный шаблон, 73 | который генерит html-тег для вашего блока, даже если вы сами не писали никакого шаблона. 74 | 75 | Это бывает удобно, но не в нашем случае. Блок `input` должен быть представлен в `HTML` как тег `input`, 76 | поэтому нам без своего шаблона не обойтись. И без стилей и без js-поведения нам не обойтись. Поэтому создадим сразу 77 | нужные файлы блока. 78 | 79 | ## Создаём файлы 80 | 81 | 82 | В консоли прервём приложение сочетанием клавиш `Ctrl-C` и воспользуемся инструментом для создания нового блока. Это небольшой bash-скрипт, который задаст вам один 83 | вопрос, а после создаст сам блок: 84 | ``` 85 | make block 86 | # Введите имя блока: input 87 | ``` 88 | 89 | На файловой системе появится директория `blocks/input`, а в ней несколько файлов. 90 | 91 | Откройте в редакторе файл `blocks/input/input.bt.js`: 92 | 93 | ```javascript 94 | module.exports = function (bt) { 95 | 96 | bt.match('input', function (ctx) { 97 | ctx.setTag('span'); 98 | 99 | ctx.setContent('Содержимое блока'); 100 | }); 101 | 102 | }; 103 | ``` 104 | 105 | У блока появился шаблон, который придумали мы, не вы. Это всё исправим, но сначала убедимся, 106 | что новый шаблон наложился на блок `input`. Как это проверить? Обратите внимание, 107 | в результате успешного наложения шаблона в `HTML` должен измениться тег, потому что в шаблоне есть два метода - один 108 | меняет тег блоку, второй задаёт конент. 109 | 110 | Сейчас там такой код: 111 | ```html 112 |
113 | ``` 114 | 115 | После наложения шаблона должен стать таким: 116 | ```html 117 | 'Содержимое блока' 118 | ``` 119 | 120 | Опять запустим проект `make`, обновим в браузере `localhost:8080/test/` и откроем `HTML`-код страницы. Ничего не 121 | произошло — тег `div` не исчез, `span` вместо него не пояился, и контента нет и в помине. 122 | 123 | Ошибка ли? 124 | 125 | Смотрим в консоль. Нет сообщений об ошибке. Значит, не ошибка, это мы что-то не доделали. 126 | 127 | Мы не описали зависимость. Что такое зависимости, [прочтите здесь](dependencies.md), сейчас для этого самое время. 128 | А если уже читали, тогда самое время добавить зависимость. 129 | 130 | На странице `test` должен появиться блок `input`, следовательно страница "зависит" от блока. 131 | Пока мы не сообщим странице эту зависимость, странице неоткуда брать информацию об этом. 132 | 133 | Откройте файл `pages/test-page/test-page.deps.yaml`. В нём только одна строка 134 | 135 | ``` 136 | - page 137 | ``` 138 | 139 | Вы уже понимаете, это означает, что страница зависит от блока `page`. 140 | 141 | И правда, в декларации страницы мы раньше звали только блок `page`, а теперь внутрь `page` мы позовём блок `input`, 142 | поэтому опишем дополнительную зависимость: 143 | 144 | ``` 145 | - page 146 | - input 147 | ``` 148 | 149 | Обновите в браузере страницу и загляните в `HTML`. Успех? Мы видим, что шаблон отработал, 150 | потому что html-тег у блока изменился так, как мы того и хотели: 151 | 152 | ```html 153 | Содержимое блока 154 | ``` 155 | 156 | Теперь напишем актуальный шаблон, который должен уметь... 157 | 158 | а) генерить блок в виде тега `input` 159 | 160 | б) устанавливать в тег значение, которые мы задали ему при декларации блока. 161 | 162 | 163 | # Пишем актуальный шаблон 164 | 165 | Попробуем набросать первый вариант, открываем файл `blocks/input/input.bt.js`. Пока он выглядит как заготовка: 166 | 167 | ```javascript 168 | module.exports = function (bt) { 169 | bt.match('input', function (ctx) { 170 | ctx.setTag('span'); 171 | 172 | ctx.setContent('Содержимое блока'); 173 | }); 174 | }; 175 | ``` 176 | 177 | Редактируем: 178 | 179 | ```javascript 180 | module.exports = function (bt) { 181 | bt.match('input', function (ctx) { 182 | ctx.setTag('input'); 183 | 184 | var currentValue = ctx.getParam('value'); 185 | ctx.setAttr('value', currentValue); 186 | }); 187 | }; 188 | ``` 189 | 190 | Разберём по-строчно. 191 | 192 | ```javascript 193 | ctx.setTag('input'); 194 | ``` 195 | Метод `setTag()` генерит в конечном `HTML` тег, заданный единственным параметром. 196 | 197 | 198 | ```javascript 199 | var currentValue = ctx.getParam('value'); 200 | ``` 201 | Метод `getParam()` принимает некий параметр "value" (откуда он, что за параметр - пока не важно) и сохраняет его 202 | значение в локальную переменную `currentValue`. 203 | 204 | ```javascript 205 | ctx.setAttr('value', currentValue); 206 | ``` 207 | Метод `setAttr()` генерит в конечном `HTML` атрибут, заданный первым параметром и устанавливает в атрибут строку, 208 | переданную вторым параметром. 209 | 210 | Сохраняем, обновляем страницу, смотрим в `HTML`. Сработало, но не всё. Нет атрибута `value` 211 | 212 | ```html 213 | 214 | ``` 215 | 216 | Всё правильно, потому что метод `setAttr(attrName, attrValue)` создаёт атрибут только если второй параметр 217 | `attrValue` не равен null. А у нас он как раз и равен null, потому что шаблон готов принять параметр, 218 | а мы такой параметр ему не передали. 219 | 220 | Передадим параметр. Смотрите, как у блока `input` я объявил кастомный параметр `value: 'Привет, Бивис'` 221 | 222 | ```javascript 223 | module.exports = function (pages) { 224 | pages.declare('test-page', function (params) { 225 | var options = params.options; 226 | return { 227 | block: 'page', 228 | title: 'test page', 229 | styles: [ 230 | {url: options.assetsPath + '.css'} 231 | ], 232 | scripts: [ 233 | {url: options.assetsPath + '.' + params.lang + '.js'} 234 | ], 235 | body: [ 236 | { 237 | block: 'input', 238 | value: 'Привет, Бивис' 239 | } 240 | ] 241 | }; 242 | }); 243 | } 244 | ``` 245 | 246 | Теперь обновите страницу в браузере. Успех? :) 247 | 248 | И в html оно выглядит так: 249 | ```html 250 | 251 | ``` 252 | 253 | _Обратите внимание, в бивисе нет зарезервированных полей, кроме `block` и `view`. Вместо `value: 'Привет, 254 | Бивис'` можно было указать любое другое, например, не знаю... `inputText: 'Привет, 255 | Бивис'`. Важно лишь в шаблоне принимать этот параметр методом `ctx.getParam('inputText')`_ 256 | 257 | И вот структура нашего блока почти готова. Стоп. Текстовое поле формы без атрибута `name`? Как форма передаст его 258 | значение на сервер? Ок, выльем и этот атрибут. Вы уже знаете, как это делается, 259 | мы могли бы и не показывать: 260 | 261 | ```javascript 262 | module.exports = function (bt) { 263 | bt.match('input', function (ctx) { 264 | ctx.setTag('input'); 265 | 266 | var currentValue = ctx.getParam('value'); 267 | ctx.setAttr('value', currentValue); 268 | ctx.setAttr('name', 'loginField'); 269 | }); 270 | }; 271 | ``` 272 | 273 | И в `HTML` оно выглядит так: 274 | ```html 275 | 276 | ``` 277 | 278 | Мы захардкодили имя будущего атрибута. Не зазорно, если мы точно знаем, что имя у инпута никогда не поменяется. 279 | В таком случае мы думаем так: "Почему бы и не прибить гвоздём"? 280 | 281 | А если серьёзно, этим шагом мы не столько "хардкодим", сколько изолируем от внешнего мира внутреннее 282 | устройство блока, никому не разрешаем извне менять значение атрибута `name`. Мы сознательно ограничиваем API 283 | этого блока. 284 | 285 | Вы можете делать API блока более открытым или... менее открытым. Это решать конкретно вам. Мы стараемся открывать 286 | блок наружу только самым минимальным образом, чтобы сохранить максимальный контроль над 287 | внутренним устройством блока. 288 | 289 | ## Стилизация 290 | 291 | Откройте в редакторе файл `blocks/input/input.styl`, там написано: 292 | 293 | ```css 294 | .input { 295 | /* здесь стили блока */ 296 | } 297 | ``` 298 | 299 | Напишите какое-нибудь свойство и обновите страницу в браузере. Я только что написал `border: 1px solid red;` и 300 | проверил — у меня в браузере вокруг блока появилась рамка. У вас тоже успех? 301 | 302 | В Stylus можно делать много разных вещей. Некоторые из них мы сознательно не используем. Точнее, 303 | мы используем лишь некоторые, которые не смогут повредить стабильности стилей. Но это тема 304 | [отдельного документа](css.md), там мы опишем подходы, которые мы себе разрешаем, и опишем вредные возможности, 305 | которые мы себе строго запрещаем. 306 | 307 | А вообще про Stylus стоит почитать на официальном 308 | сайте [http://learnboost.github.io/stylus/](http://learnboost.github.io/stylus/) 309 | 310 | Пока сделаем вид, что в `styl`-файлах мы пишем чистый `CSS` и перейдём к самому интересному. К программированию 311 | поведения блока. 312 | 313 | ## Проектирование 314 | 315 | Перед программированием поведения очень полезно спроектировать блок. 316 | Искусство проектирования может показаться нетривиальным и избыточным. 317 | Но проектирование — это то, чем мы занимаемся в процессе разработки осознанно или неосознанно, 318 | хотим мы того или нет. Делаем мы это в голове в виде мыслей или на бумаге в виде схем и диаграмм, 319 | но каждый из нас этим занимается постоянно. Займёмся и сейчас. 320 | 321 | Изобразим UML-диаграмму будущего блока. Это необязательно для вас, но мы сделаем, 322 | чтобы явно показать ход наших мыслей. 323 | 324 | Что умеет блок `input`? Пользователь вводит в него текст для того, чтобы форма отправила это значение на сервер. 325 | Следовательно, инпут должен уметь отдавать введённое значение. 326 | 327 | Отобразим это в PlantUML-диаграмме с помощью простого описания: 328 | 329 | ```puml 330 | class Input { 331 | +getValue(): String 332 | } 333 | ``` 334 | 335 | Чтобы сделать из этого красивую картинку, отправляем этот код в http://www.plantuml.com/plantuml/form : 336 | 337 | 338 | 339 | Теперь красиво и наглядно :) 340 | 341 | Двигаемся дальше. Инпут ещё должен уметь принимать в себя значение, следовательно добавим в него метод-сеттер и 342 | сделаем ещё одну красивую картинку: 343 | 344 | 345 | 346 | На этом остановимся, не будем раньше времени усложнять блок. 347 | 348 | ---- 349 | 350 | _Скорее всего вы не проектируете свои приложения с помощью `UML`-схем. Честно говоря, мы бы тоже с удовольствием не 351 | использовали эти схемы ;)_ 352 | 353 | _Нам трудно без формальных схем проектировать большие приложения, поэтому мы этим и занимаемся. Если вам 354 | интересно, мы читаем про `UML` на сайте [http://plantuml.sourceforge.net/](http://plantuml.sourceforge.net/), 355 | а красивые картинки генерим здесь: [http://www.plantuml.com/plantuml/form](http://www.plantuml.com/plantuml/form)_ 356 | 357 | ---- 358 | 359 | Что мы понимаем, когда смотрим на составленные `UML`-схемы? Мы понимаем, что наш блок пока довольно простой и умеет 360 | он только лишь значение отдавать или получать. Удерживая в голове эту информацию, как вектор, 361 | мы начинаем программировать поведение. 362 | 363 | ## Поведение 364 | 365 | Чтобы блок мог взаимодействовать с пользователем (или с другими блоками без участия пользователя), 366 | Бивису необходимо сообщить, что блок интерактивный, что для него написано `js`-поведение. 367 | 368 | Для этого мы помечаем блок, устанавливаем на нём "метку", по которой бивис-движок понимает, 369 | что для блока написано поведение. 370 | 371 | Такую метку ставит метод `enableAutoInit()`. 372 | 373 | ```javascript 374 | module.exports = function (bt) { 375 | bt.match('input', function (ctx) { 376 | ctx.enableAutoInit(); 377 | ctx.setTag('input'); 378 | ctx.setAttr('value', ctx.getParam('value')); 379 | ctx.setAttr('name', 'loginField'); 380 | }); 381 | }; 382 | ``` 383 | 384 | Он создаёт в `HTML`-коде блока специальный класс `_init`, по которому Бивис понимает, 385 | что блок интерактивный и его нужно инициализировать. 386 | 387 | ```html 388 | 389 | ``` 390 | 391 | Теперь запрограммируем поведение. Создайте файл `input.js` в директории `blocks/input` и скопируйте в него заготовку: 392 | 393 | ```javascript 394 | modules.define( 395 | 'input', 396 | ['inherit', 'block'], 397 | function (provide, inherit, YBlock) { 398 | var Input = inherit(YBlock, { 399 | // инстанс-методы 400 | }, { 401 | // статические методы 402 | }); 403 | 404 | provide(Input); 405 | } 406 | ); 407 | ``` 408 | 409 | Это модуль. Модульных систем много разных, Бивис использует 410 | [Ymaps Modules](modules.md), которые придумали и разработали 411 | разработчики из Яндекс.Карт. Пройдите по ссылке, прочитайте. 412 | В основе модулей лежит очень простая идея, и в документации к `Ymaps Modules` простым языком описывается, 413 | зачем модульная система нужна, как её использовать, и что конкретно означает каждая строчка из кода, 414 | который мы привели выше. 415 | 416 | Не стали читать? ;) Там чтения на пять минут. Мы всё таки настаиваем, вам понравится. 417 | [Прочитайте](modules.md). 418 | 419 | 420 | Прочитали? Тогда вы понимаете, что код внутри анонимной функции - это и есть код, 421 | который программирует поведение вашего `input`. Можно было бы программировать его по старинке, 422 | навешивая события на DOM-элементы самим или через jQuery-хелперы. Но мы предпочитаем работать с блоками через 423 | абстракции, чтобы не зависеть от HTML-реализации блока. 424 | 425 | Поэтому мы в зависимостях модуля `input` указали два других модуля — `inherit` и `block`. 426 | Первый модуль возвращает функцию `inherit`, которая используется для создания классов и наследования (подробное 427 | описание читайте на странице автора: 428 | [https://github.com/dfilatov/node-inherit](https://github.com/dfilatov/node-inherit). 429 | 430 | Второй модуль - это класс, который и реализует механизмы абстрагирования от DOM-узлов. Это базовый визуальный блок. 431 | 432 | _Все блоки, которые вы создаёте в проекте и которые пользователь может увидеть на страницах 433 | вашего сайта (мы такие блоки называем визуальными), должны наследоваться от 434 | модуля `block` с помощью модуля `inherit`._ 435 | 436 | В этом коде мы создаём класс `Input`, как наследника от класса `Yblock`, и пока больше ничего полезного не делаем. 437 | 438 | ```javascript 439 | var Input = inherit(YBlock, { 440 | // инстанс-методы 441 | }, { 442 | // статические методы 443 | }); 444 | ``` 445 | 446 | А этой строкой мы "провайдим" наш класс `Input` наружу. Как бы высовываем из модуля ручку наружу, 447 | за которую другие модули могут схватиться и позвать его к себе через зависимости. 448 | 449 | ```javascript 450 | provide(Input); 451 | ``` 452 | 453 | Теперь добавим пустой конструктор в блок инстанс-методов: 454 | 455 | ```javascript 456 | var Input = inherit(YBlock, { 457 | __constructor: function () { 458 | 459 | } 460 | 461 | // инстанс-методы 462 | }, { 463 | // статические методы 464 | }); 465 | ``` 466 | 467 | Метод `__constructor()` запускается автоматически, когда создаётся экземпляр класса `Input`. 468 | 469 | Точно такой же метод есть в базовом классе `YBlock`, от которого `Input` наследуется. 470 | То есть внутри `Input` мы перезаписали конструктор, а после обязательно нужно вызвать базовый конструктор, 471 | чтобы он сделал всё, "что нужно". Сейчас не так важно, что базовый констуруктор такого "нужного" там делает. Важно 472 | запомнить, что вызов базового метода происходит так: `this.__base.apply(this, arguments)`: 473 | 474 | ```javascript 475 | var Input = inherit(YBlock, { 476 | __constructor: function () { 477 | this.__base.apply(this, arguments); 478 | } 479 | 480 | // инстанс-методы 481 | }, { 482 | // статические методы 483 | }); 484 | ``` 485 | 486 | Теперь переопределим статический метод, который будет возвращать имя конкретно этого блока: 487 | 488 | ```javascript 489 | var Input = inherit(YBlock, { 490 | __constructor: function () { 491 | this.__base.apply(this, arguments); 492 | } 493 | 494 | // инстанс-методы 495 | }, { 496 | // статические методы 497 | 498 | getBlockName: function () { 499 | return 'input'; // вернуть необходимо имя блока 500 | } 501 | }); 502 | ``` 503 | 504 | В некоторых ситуациях вам нужно будет спросить имя блока, этот метод вернёт вам актуальное имя. Следует относиться 505 | к этому методу, как к обязательному. 506 | 507 | И теперь посмотрим на нашу заготовку целиком: 508 | 509 | ```javascript 510 | modules.define( 511 | 'input', // создали модуль, дали ему имя 512 | [ 513 | 'inherit', // зависимость от inherit 514 | 'block' // зависимость от YBlock 515 | ], 516 | function ( 517 | provide, 518 | inherit, // получаем функцию inherit 519 | YBlock // получаем блок YBlock 520 | ) { 521 | var Input = inherit(YBlock, { 522 | // Создаём свой конструктор 523 | __constructor: function () { 524 | // Вызываем базовый конструктор 525 | this.__base.apply(this, arguments); 526 | 527 | // здесь описываем то, что происходит сразу после создания инстанса класса 528 | } 529 | 530 | // здесь опишем инстанс-методы класса Input 531 | }, { 532 | // здесь опишем статические методы 533 | 534 | // при наследовании от YBlock, необходимо переопределить статический метод getBlockName 535 | getBlockName: function () { 536 | return 'input'; // вернуть имя блока 537 | } 538 | }); 539 | 540 | provide(Input); // Выставить из модуля наружу "ручку" 541 | } 542 | ); 543 | ``` 544 | 545 | А теперь напишем те два метода для нашего блока, ради которых всё задумано - один отдаёт значение, 546 | второй устанавливает. 547 | 548 | ```javascript 549 | modules.define( 550 | 'input', 551 | ['inherit', 'block'], 552 | function (provide, inherit, YBlock) { 553 | var Input = inherit(YBlock, { 554 | __constructor: function () { 555 | this.__base.apply(this, arguments); 556 | 557 | console.log(this.getValue()); 558 | }, 559 | 560 | getValue: function() { 561 | return this.getDomNode().val(); 562 | }, 563 | 564 | 565 | setValue: function(value) { 566 | this.getDomNode().val(value); 567 | } 568 | }, { 569 | getBlockName: function () { 570 | return 'input'; 571 | } 572 | }); 573 | 574 | provide(Input); 575 | }); 576 | ``` 577 | 578 | А чтобы проверить, что эта красота работает, в конструкторе вызовем один из методов, например `this.getValue()`, 579 | и будем ожидать, что в момент загрузки страницы в браузерной консоли появится текстовое сообщение. 580 | 581 | Проверяем? Открываем на странице firebug-консоль, 582 | обновляем страницу и... ничего не происходит. Конструктор не выполнился? А почему? 583 | 584 | А потому что по дефолту в Бивисе отключена автоматическая инициализация всех блоков. Это, главным образом, 585 | экономит процессорное время, чтобы не делать лишних действий. Решение о том, нужно ли инициализировать все блоки 586 | сразу во время загрузки страницы (или только какие-то блоки "по мере их готовности") принимаете вы — разработчик проекта. 587 | 588 | Если вы хотите, чтобы бивис инициализировал все блоки, у которых в шаблоне указано ctx.enableAutoInit(), 589 | автоматически во время загрузки страницы (а вы именно этого в данном примере хотите), 590 | вам нужно указать дополнительную зависимость для страницы. 591 | 592 | Снова откройте файл `pages/test-page/test-page.deps.yaml` и допишите: 593 | 594 | ``` 595 | - page 596 | - input 597 | - block: block 598 | elem: auto-init 599 | ``` 600 | 601 | Вы добавили зависимость от блока по имени `block` (случайная тавтология), 602 | а точнее от его элемента `auto-init`, который и занимается тем, что инициализирует все ваши блоки при загрузке 603 | страницы. 604 | 605 | Теперь обновите страницу в браузере. Успех? Я вижу в консоли тот самый текст, 606 | который есть сейчас в текстовом поле. Вы тоже видите? Значит, мы написали поведение для блока. 607 | 608 | # Что дальше? 609 | 610 | К этому моменту вы получили 90% всех знаний и том, как пользоваться бивисом. Вы умеете создавать структуру, 611 | писать стили и программировать поведение. 612 | 613 | Оставшиеся 10% - узкие прикладные задачи, которые мы с вами решим в отдельных руководствах: 614 | 615 | * Как обращаться за данными и передавать в блоки? 616 | * Как сделать такой же блок, но чуть-чуть другой 617 | * Как настроить локализацию в проекте 618 | 619 | В [документе про CSS](css.md) мы расскажем: 620 | * про то, как написать разные css-представления для одного блока 621 | * как описывать состояния блока 622 | * про опасные и безопасные приемы в Stylus (например, почему глобальные миксины это плохо, 623 | а глобальные переменные - это не плохо) 624 | 625 | В [документе про JS](yblock.md) мы расскажем: 626 | * про два способа инициализации блока 627 | * про доступ и операции над внутренностями блока 628 | * про навешивание событий 629 | 630 | В [документе про BT](bt.md) мы расскажем: 631 | * как строить внутреннюю структуру блока 632 | * как можно параметрами менять внутреннее устройство блока 633 | * как видозменять контекст (на примере DOCTYPE и layout-for-page из bevis-blog) 634 | 635 | А если вы не нашли ответа на свой вопрос, создайте issue прямо здесь и задайте этот вопрос, 636 | не стесняйтесь. Мы ответим так быстро, как только сможем :) 637 | 638 | Успехов вам! 639 | -------------------------------------------------------------------------------- /practice/new-page.md: -------------------------------------------------------------------------------- 1 | # Как создать новую страницу 2 | 3 | Вы уже склонировали `bevis-stub`, запустили в корне проекта команду `make`, увидели, 4 | как качаются какие-то библиотеки (возможно ждали долго, но это только первый раз). 5 | 6 | Но вот в консоль перестали сыпаться сообщения, и последнее, что вы видите, это нечто такое: 7 | 8 | ``` 9 | ENV= node_modules/.bin/enb make 10 | 19:16:25.764 - build started 11 | 19:16:25.982 - [rebuild] [build/index/index.levels] levels 12 | 19:16:25.982 - [rebuild] [build/index/index.bemdecl.js] file-provider 13 | 19:16:26.093 - [rebuild] [build/index/index.deps.js] deps-with-modules 14 | 19:16:26.093 - [rebuild] [build/index/index.files] files 15 | 19:16:26.093 - [rebuild] [build/index/index.dirs] files 16 | 19:16:26.093 - [isValid] [build/index/index.js] js 17 | 19:16:26.093 - [isValid] [build/index/index.css] css-stylus-with-autoprefixer 18 | 19:16:26.094 - [isValid] [build/index/_index.js] file-copy 19 | 19:16:26.094 - [isValid] [build/index/_index.css] file-copy 20 | 19:16:26.094 - build finished - 330ms 21 | 22 | DEBUG: Running node-supervisor with 23 | DEBUG: program 'server/boot.js' 24 | DEBUG: --watch 'server,configs' 25 | DEBUG: --ignore 'undefined' 26 | DEBUG: --extensions 'node|js' 27 | DEBUG: --exec 'node' 28 | 29 | DEBUG: Starting child process with 'node server/boot.js' 30 | DEBUG: Watching directory '~/bevis-stub/server' for changes. 31 | DEBUG: Watching directory '~/bevis-stub/configs' for changes. 32 | 19:16:26 - info: worker 23454 started 33 | 19:16:26 - info: app started on 8080 34 | 19:16:26 - info: worker 23455 started 35 | 19:16:26 - info: app started on 8080 36 | ``` 37 | 38 | Это означает, что проект собран, сервер запущен на 8080 порту (но это можно и поменять), и если теперь вы в браузере 39 | запросите `localhost:8080`, то увидите нашу страничку "Привет, BEViS!". 40 | 41 | ---- 42 | 43 | Всё ли так? Если нет, заведите issue, мы вам поможем :) 44 | 45 | Не понимаете всего, что написано в консоли? Не важно. Я тоже слабо понимаю ;) 46 | 47 | ---- 48 | 49 | Теперь создадим страницу `test`, которая будет отвечать пользователю по адресу `localhost:8080/test/`. 50 | 51 | 52 | Кстати, сразу проверим, что нам ответит браузер, если мы прямо сейчас сделаем запрос к несуществующей странице. Пишем 53 | в адресную строку браузера `localhost:8080/test/` и видим ответ "Internal error". Это нормально, так и должно быть :) 54 | 55 | Проверяем консоль, так и есть - консоль подтверждает, что всё идёт, как задумано: 56 | 57 | ``` 58 | 19:27:05 - error: Error: Target not found: build/test/test.page.js 59 | at Error () 60 | at TargetNotFoundError (~/bevis-stub/node_modules/enb/lib/errors/target-not-found-error.js:11:23) 61 | at module.exports.inherit._resolveTarget (~/bevis-stub/node_modules/enb/lib/make.js:383:15) 62 | at ~/bevis-stub/node_modules/enb/lib/make.js:400:36 63 | at Array.forEach (native) 64 | at module.exports.inherit._resolveTargets (~/bevis-stub/node_modules/enb/lib/make.js:399:21) 65 | at module.exports.inherit.buildTargets (~/bevis-stub/node_modules/enb/lib/make.js:433:35) 66 | at ~/bevis-stub/node_modules/enb/lib/server/server-middleware.js:65:33 67 | at Array.0 (~/bevis-stub/node_modules/vow/lib/vow.js:194:56) 68 | at callFns (~/bevis-stub/node_modules/vow/lib/vow.js:452:35) 69 | ``` 70 | 71 | ---- 72 | 73 | Забегая вперёд, ответим на вопрос тех из вас, кто внимательно-превнимательно прочитал статьи и готов поймать нас на 74 | лжи :) Мы говорили, что разработчику нужно думать только о директориях `blocks` и `pages`. Почему же в логах 75 | фигурирует странный путь `build/test`, а не `pages/test`? 76 | 77 | Потому что папка 'pages' - это директория для живых людей, а `build` - директория для робота сборщика и для 78 | http-сервера. Сборщик складывает туда собранные файлы, а http-сервер читает их там и пересылает пользователю в 79 | браузера. Пока думать о папке `build` не надо, наше дело - папка `pages`. О собираемых файлах 80 | мы будем думать, когда захотим кастомизировать сборку. Тогда же разберемся с консольным выводом — что сообщает 81 | каждая строчка. Сейчас не надо :) 82 | 83 | ---- 84 | 85 | Всё. Теперь создаём страницу `test`. В консоли прервём приложение сочетанием клавиш `Ctrl-C`. И воспользуемся инструментом для создания новой страницы. Это небольшой bash-скрипт, который задаст вам один 86 | вопрос, а после создаст саму страницу: 87 | ``` 88 | make page 89 | # Введите имя страницы: test 90 | ``` 91 | 92 | На файловой системе появится файл `pages/test-page/test-page.page.js` (рядом с ним ещё какой-то файл с расширением 93 | `yaml`, он вам понадобится, но не сейчас, не обращайте внимания). 94 | 95 | Откройте файл `pages/test-page/test-page.page.js` в редакторе: 96 | 97 | ```javascript 98 | module.exports = function (pages) { 99 | pages.declare('test-page', function (params) { 100 | var options = params.options; 101 | return { 102 | block: 'page', 103 | title: 'test page', 104 | styles: [ 105 | {url: options.assetsPath + '.css'} 106 | ], 107 | scripts: [ 108 | {url: options.assetsPath + '.' + params.lang + '.js'} 109 | ], 110 | body: [ 111 | // здесь ваши блоки 112 | ] 113 | }; 114 | }); 115 | } 116 | ``` 117 | 118 | Вот такая страница. 119 | 120 | Опять запустим проект 121 | ``` 122 | make 123 | ``` 124 | 125 | Обратите внимание на сообщения в консоли, они изменились. Теперь сборщик сообщает о том, 126 | что он собирает не только `build/index/*`, но ещё и `build/test/*` 127 | 128 | ``` 129 | 19:40:18.739 - build started 130 | 19:40:18.954 - [rebuild] [build/index/index.levels] levels 131 | 19:40:18.954 - [rebuild] [build/test/test.levels] levels 132 | 19:40:18.954 - [rebuild] [build/index/index.bemdecl.js] file-provider 133 | 19:40:18.955 - [isValid] [build/index/index.deps.js] 134 | 19:40:18.956 - [rebuild] [build/index/index.deps.js] deps-with-modules 135 | 19:40:18.956 - [rebuild] [build/index/index.files] files 136 | 19:40:18.956 - [rebuild] [build/index/index.dirs] files 137 | 19:40:18.957 - [isValid] [build/index/index.js] js 138 | 19:40:18.957 - [isValid] [build/index/index.css] css-stylus-with-autoprefixer 139 | 19:40:18.957 - [isValid] [build/index/_index.js] file-copy 140 | 19:40:18.957 - [isValid] [build/index/_index.css] file-copy 141 | 19:40:18.958 - [rebuild] [build/test/test.bemdecl.js] file-provider 142 | 19:40:18.958 - [isValid] [build/test/test.deps.js] 143 | 19:40:18.959 - [rebuild] [build/test/test.deps.js] deps-with-modules 144 | 19:40:18.959 - [rebuild] [build/test/test.files] files 145 | 19:40:18.959 - [rebuild] [build/test/test.dirs] files 146 | 19:40:18.959 - [isValid] [build/test/test.js] js 147 | 19:40:18.959 - [isValid] [build/test/test.css] css-stylus-with-autoprefixer 148 | 19:40:18.959 - [isValid] [build/test/_test.js] file-copy 149 | 19:40:18.959 - [isValid] [build/test/_test.css] file-copy 150 | 19:40:18.960 - build finished - 221ms 151 | 152 | DEBUG: Running node-supervisor with 153 | DEBUG: program 'server/boot.js' 154 | DEBUG: --watch 'server,configs' 155 | DEBUG: --ignore 'undefined' 156 | DEBUG: --extensions 'node|js' 157 | DEBUG: --exec 'node' 158 | 159 | DEBUG: Starting child process with 'node server/boot.js' 160 | DEBUG: Watching directory '~/bevis-stub/server' for changes. 161 | DEBUG: Watching directory '~/bevis-stub/configs' for changes. 162 | 19:40:19 - info: worker 23707 started 163 | 19:40:19 - info: app started on 8080 164 | 19:40:19 - info: worker 23708 started 165 | 19:40:19 - info: app started on 8080 166 | ``` 167 | 168 | Смотрим в браузере запрос к `localhost:8080/test/` 169 | 170 | Сообщения об ошибке нет, появился серый аккуратный фоновый рисунок, а если заглянуть в html-код страницы, 171 | увидим там такое: 172 | 173 | ``` 174 | 175 | 176 | 177 | 178 | test page 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | ``` 187 | 188 | Это успех, у нас есть новая страница. 189 | 190 | ## Откуда взялся весь этот HTML? 191 | 192 | Ещё раз посмотрим на декларацию файла `pages/test-page/test-page.page.js`. 193 | 194 | ```javascript 195 | module.exports = function (pages) { 196 | pages.declare('index-page', function (params) { 197 | var options = params.options; 198 | return { 199 | block: 'page', 200 | title: 'index page', 201 | styles: [ 202 | {url: options.assetsPath + '.css'} 203 | ], 204 | scripts: [ 205 | {url: options.assetsPath + '.' + params.lang + '.js'} 206 | ], 207 | body: [ 208 | // здесь ваши блоки 209 | ] 210 | }; 211 | }); 212 | } 213 | ``` 214 | 215 | В статьях мы обращали ваше внимание, что метод `pages.declare()` возвращает бивис-блоки: 216 | 217 | ```javascript 218 | return [ 219 | { block: 'header' }, 220 | { block: 'authorization' } 221 | ]; 222 | 223 | ``` 224 | 225 | и утверждали, что этого кода достаточно, чтобы Бивис сгенерил такой `html`: 226 | 227 | ```html 228 |
229 | 232 |

Демо-страница

233 |

Слоган

234 | 235 | 236 | 237 |
238 |
239 | 240 | 241 | 242 | 244 | 247 |
248 | ``` 249 | 250 | Мы не обманули, так и есть. Но если вы вдумчивый и въедливый, то уже задались вопросом, 251 | как именно генерятся обязательные для любой веб-страницы теги `html`, `head`, `body`. 252 | 253 | Мы нарочно опустили этот момент, потому что он не предсталяет отдельной ценности и сложности. Потому что структура 254 | страницы генерится отдельным блоком `page`, который поставляется сразу с бивисом, 255 | и автоматически генерится при создании заготовки новой страницы. 256 | 257 | Когда мы хотим наполнить страницу какими-то блоками, мы делаем это через декларацию этих блоков в 258 | блоке `page`, например так: 259 | 260 | ```javascript 261 | module.exports = function (pages) { 262 | pages.declare('test-page', function (params) { 263 | var options = params.options; 264 | return { 265 | block: 'page', 266 | title: 'test page', 267 | styles: [ 268 | {url: options.assetsPath + '.css'} 269 | ], 270 | scripts: [ 271 | {url: options.assetsPath + '.' + params.lang + '.js'} 272 | ], 273 | body: [ 274 | { block: 'ВАШ БЛОК НОМЕР 1' }, 275 | { block: 'ВАШ БЛОК НОМЕР 2' } 276 | ] 277 | }; 278 | }); 279 | } 280 | ``` 281 | 282 | Блок `page`, который здесь объявлен, есть в папке `/blocks`. Он формирует минимальную `html` структуру 283 | любой страницы и имеет несколько параметров, назначение которых легко понять из имён. 284 | 285 | В `title` мы передаём заголовок страницы, который окажется в теге `` секции `<head>`. 286 | Параметры `styles` и `scripts` принимают массив стилей и скриптов соответственно. Они генерят теги `<link>` или 287 | `<styles>` и `<script>`. 288 | 289 | Параметр `body` тоже ожидает на вход массив. Даже если в теле web-страницы вы хотите видеть один единственный блок, 290 | всё равно параметр `body` ожидает от вас массив. Это очень простой параметр. Можно относиться к нему, 291 | как контейнеру `<body>` из обычного `html`-файла. 292 | 293 | Детально узнать об API блока `page` можно из `jsdoc` в файле `blocks/page/page.bt.js` 294 | 295 | Страница готова к наполнению блоками. 296 | 297 | ``` 298 | localhost:8080/test/ 299 | ``` 300 | 301 | Приготовим какой-нибудь блок? 302 | 303 | Нам [сюда](new-block.md). 304 | -------------------------------------------------------------------------------- /practice/tests.md: -------------------------------------------------------------------------------- 1 | # Пишем тесты 2 | 3 | в разработке 4 | --------------------------------------------------------------------------------