Давайте представим, что вас перевели на новый проект. Или вы сменили работу и о проекте максимум только слышали. Вот вы садитесь за рабочее место, к вам приходит менеджер, жмёт руку и… прямо сходу открывает страницу проекта, тыкает пальцем в монитор и просит вставить «информер о предстоящем событии Х». На этом вы расстаётесь… Что делать? С чего начать? Как создать «информер»? Где найти нужный шаблон? И море других вопросов.
Под катом будет рассказ, как мы стараемся организовать эти процессы, какие инструменты создаём для препарирования SPA. Кроме этого, мы поговорим о технических подробностях реализации Live Coding / Hot Reload и чуток о VirtualDom и React с Angular.
Приступим. Итак, вот вы и остались один на один с проектом, тимлид сообщил, где найти репозиторий, а дальше — читай README.md, и всё.
README.md
Это отправная точка при погружении в проект, она встречает вас базовой информацией:
- Установка — тут описаны все шаги, как запустить проект, чтобы даже «дизайнер» мог это сделать;
- Запуск — примеры базовых команд для запуска проекта;
- Опции запуска — список всех возможных параметров и их описание;
- «Первые шаги» — собственно, это и есть нужный раздел:
- быстрый поиск UI-блока — описание инструмента для препарирования приложения;
- «Что? Где? Когда?» — краткое описание структуры проекта;
- создание UI-блока — минимальная информация, как создать UI-блок;
- «Логика приложения, или где искать обработку событий?»;
- Примеры/скринкасты — экспериментальный раздел с примерами.
На всё про всё уйдёт примерно пять минут. Самое главное, что вы узнаете из README: для решения задачи нужно:
- Установить NodeJS/npm.
- Клонировать репозиторий проекта.
- Запустить
npm install
иnpm start
. - Открыть проект в браузере и нажать на «пипетку» в правом нижнем углу. ;]
Но давайте по порядку.
Установка
Мы уже очень давно используем пакетную разработку, поэтому многие части (grunt- и gulp-таски, утилиты, UI-компоненты и т. п.) разрабатываются как отдельные npm- или jam-пакеты. Такой подход даёт возможность максимально переиспользовать код между проектами, обеспечивает версионность (по semver) и, кроме этого, позволяет собрать инфраструктуру для каждого пакета именно под задачу. И главное, никакого легаси, пакет самостоятелен и, кто знает, может со временем превратиться в хороший opensource.
Кроме этого, не забывайте применять npm-хуки, например postinstall
. Мы его используем для установки таких git-хуков, как:
- pre-commit — проверка стиля кодирования (ESLint);
- pre-push — запуск тестов;
- post-merge — запуск
npm install
иjam install
.
Последний хук может показаться странным, но, когда вы работаете с кучей пакетов, которые динамично обновляются, без него никак. Набрав git pull
, разработчик должен получить актуальную версию проекта, чего можно достичь, только принудительно запустив npm install
.
Если проект зависит от npm или другого стороннего менеджера пакетов, позаботьтесь о локальном registry, чтобы не зависеть от внешнего мира и его проблем (left-pad, Роскомнадзор и т. п.).
Запуск
npm start
— всё, что нужно знать, и неважно, что у вас под капотом: gulp, grunt, webpack… Выше я уже писал, что в README.md есть описание параметров запуска: при старте приложение читает README.md, парсит список опций и их описания и, если вы используете неизвестную или недокументированную опцию, выдаёт ошибку. Вот таким нехитрым способом решается проблема документации: нет описания — нет опции.
Пример запуска:
npm start -- --xhr --auth=oauth --build
> project-name@0.1.0 start /git/project-name/
> node ./ "--xhr" "--auth=oauth" "--build"
- Ветка: master (Sun Aug 29 2016 10:28:06 GMT+0300 (MSK))
- Дополнительные опции
- xhr: true (загрузка статики через `XMLHttpRequest`)
- auth: oauth (авторизация через `proxy`, `oauth`, `account`)
- build: true (использовать сборку)
- Сборка проекта
- Запуск сервера на 3000
- Сервер запущен: localhost:3000
Первые шаги
Вернемся к задаче. Итак, README.md прочитан, проект установлен и запущен, переходим к пункту «быстрый поиск блока, или „пипетка“ — наше всё».
«Пипетка» — это инструмент для анализа структуры компонентов и их параметров. Чтобы ей воспользоваться, открываем браузер, кликаем на «пипетку» и выбираем место, куда «менеджер ткнул пальцем».
Инспектор
Снизу появилась панель инспектора, который показывает структуру блоков, находящихся под курсором. Найдя нужный, кликаем по нему. Теперь мы можем посмотреть всю цепочку вложенных блоков, а также выяснить, в каком файле и строке они вызываются.
Теперь кликаем на название файла, и… открывается IDE, а курсор установлен в нужную строчку. Рядом есть «глаз», если нажать на него — откроется GUI/просмотрщик с выбранным блоком.
Всё, основные точки входа нашли, теперь приступим к добавлению «информера».
Создание UI-блока
Есть два пути создания блока (оба описаны в README):
- через консоль;
- используя GUI.
Консольный инструмент нужен, когда нет возможности использовать GUI, во всех остальных случаях удобнее и нагляднее прибегать к GUI.
GUI
Это веб-интерфейс для просмотра, а главное, разработки UI-блоков проекта.
Что он умеет:
- просмотр списка всех блоков;
- простой поиск по названию и ключевым словам;
- вывод всех вариантов использования для конкретного блока;
- создание нового блока;
- переименование блока.
Первым делом нужно узнать, нет ли в проекте подобных информеров. Воспользовавшись поиском, находим подобный блок, опять используем «пипетку» для изучения его структуры и нажимаем «+», вводим название нового блока, кликаем «ОК», после чего GUI открывает просмотр созданного блока. Снова используем пипетку и открываем IDE для редактирования css/шаблона/js.
Итак, что же произошло? После нажатия кнопки «ОК» GUI создаёт папку с типовым блоком, который в нашей архитектуре состоит минимум из четырёх файлов:
- block-name.html — шаблон блока
<div></div>
- block-name.scss — стили
.block-name { padding: 10px; background: red; }
- block-name.js — описание поведения
import feast from 'feast'; import template from 'feast-tpl!./block-name'; import styleSheet from 'feast-css!./block-name'; /** * @class UIBlockName * @extends feast.Block */ export default feast.Block.extend({ name: 'block-name', template, styleSheet });
- block-name.spec.js — спецификация, на основе которой строятся примеры использования
export default { 'base': { attrs: {} } };
При редактировании любого из этих файлов все изменения применяются без перезагрузки страницы. Это не просто модная забава, а огромная экономия времени. Блоки могут иметь логику, а Hot Reload позволяет не терять текущее состояние, что происходит при F5 / cmd + r. Ещё при редактировании шаблона подключённые блоки автоматически обновляются. Иными словами, GUI немного программируют за вас. ;]
Вот так, почти ничего не зная о проекте, можно добавить новый блок. Вам не требуется читать километры документации, чтобы выполнить обычную задачу. Но это не значит, что «километры» не нужны: ещё как нужны — для углубления знаний и жизни проекта без его основных мейнтейнеров. Например, для работы с API и бизнес-логикой у нас есть внутренняя JSSDK, документация которой генерируется на основе JSDoc3.
Мини-итог
Изучать документацию и кодовую базу проекта нужно и правильно, но уже на стадии основательного погружения, поначалу же достаточно описать сценарии выполнения типовых задач. Такие инструкции должны быть легкими и интуитивно понятными. Автоматизируйте всё, что можно автоматизировать. Как видите, в нашем случае это не просто создание блока: автоматизация начинается с установки проекта, хуков, обновления пакетов и т. д. Вход в проект должен быть лёгок и весел ;]
Техническая часть
Начну немного издалека. В начале 2012 года мы создали свой шаблонизатор Fest. Он преобразовывал XML в js-функцию, которую можно использовать на клиенте и сервере. Функция принимала объект параметров и выдавала строку: классический js-шаблонизатор. Только, в отличие от собратьев, функция на тот момент была супероптимизирована, мы могли запускать её на чистом V8, добившись производительности Си-шаблонизатора, который применяли раньше.
[XML -> JSFUNC -> STRING -> DOM]
За это время на базе Fest мы разработали внутреннюю библиотеку блоков, которая используется сразу на нескольких проектах (Почта, Облако и др.). То есть кнопки, инпуты, формы, списки и т. д. у нас общие. Собственно, это и были первые шаги по структурированию верстки и компонентов.
Время шло, и всё острее вставал вопрос «Как нам жить дальше?», ведь Fest возвращает только строку, обновить состояние можно двумя способами: либо «перерисовать всё», либо «точечно воздействовать на DOM из JS».
Конечно, приходится использовать оба подхода: где-то проще и быстрей перерисовать всё, где-то нужно изменить только один css-класс. В целом при работе с шаблонизатором, который выдает строку, есть свои плюсы/минусы, и это отнюдь не производительность, как многие сейчас думают. Основных проблем несколько:
- Перерисовать всё — переинициализация событий, повторное получение ссылок на нужные DOM-фрагменты, «мигание» картинок и т. п.
- Точечное воздействие — размывает и дублирует логику, усложняя разработку.
Поэтому мы начали двигаться дальше, но с возможностью минимального переписывания уже готовых компонентов.
Экспериментов было много. Пробовали ввести data-binding, очень похожий на ангуляровский, но, в отличие от него, Fest всё так же выдавал строку, а data-binding накладывался уже после вставки в DOM. Это позволило сохранить изначальную скорость и работу через V8. Увы, на больших списках у нас остались те же проблемы с аля-$digest, что и у ангуляра, хоть наша реализация и была немного быстрей (в рамках наших задач).
Со временем на рынок вышел React и подарил нам VirtualDom. Проведя бенчмарки, я немного впал в уныние: базовый «список писем» получился примерно в три раза медленней, чем у нас (и это с урезанной реализацией). К тому же мы хотели не переписать свой код, а только заменить принцип обновления шаблона. Но нет худа без добра: React дал толчок всему js-сообществу, и в скором времени, как грибы, начали расти альтернативные реализации vdom: Incremental DOM, morphdom, Deku, mithril, Bobril и многие другие.
Дело оставалось за малым: провести бенчмарки на наших задачах, выбрать подходящий и написать для наших шаблонов транспилер.
[XHTML -> JSFUNC -> VDOM? -> DOM]
Но главной целью было получить максимально комфортную разработку блоков, а именно:
- Интерфейс создания, просмотра и тестирования блоков.
- Live coding (CSS/HTML/JS).
- Автоматизация создания/редактирования блоков.
- Инструменты для инспектирования компонентов.
- Визуализация связей между компонентами.
Кроме этого, у нас уже были GUI/веб-интерфейс к текущей библиотеке блоков, осталось только унифицировать идею, чтобы каждый проект мог без особой боли развернуть GUI для себя.
Разработка
Live Coding
Думаю, не ошибусь, если скажу: все знают, что такое Webpack и BrowserSync. О них написано много, так что заострять внимание на них не буду, а покажу альтернативный путь: как быть, когда вам не подходят коробочные решения. Только не думайте, что я призываю вас изобретать велосипед: отнюдь, это просто более низкоуровневый вариант, про который многие забывают и тратят уйму времени на «прикручивание» того же Webpack.
Если это так, то node-watch + socket.io — всё, что вам нужно. Два готовых инструмента, которые вы легко можете интегрировать в свой проект.
const fs = require('fs');
const http = require('http');
const watch = require('node-watch');
const socket = require('socket.io');
cosnt PORT = 1234;
const app = http.createServer((req, res) => {
res.writeHead(200, {'Content-Type': ‘html/text’});
res.end();
});
const io = socket(app);
app.listen(PORT, () => {
watch('path/to', {recursive: true}, (file) => {
fs.readFile(file, (err, content) => {
const ext = file.split('.').pop();
io.emit(`file-changed:${ext}`, {file, content});
});
});
});
<script src=”//cdnjs.cloudflare.com/ajax/libs/socket.io/1.3.5/socket.io”></script>
<script>
const io = io(location.protocol + '//' + location.host)
socket.on('file-changed:html, function (data) {
// data.file, data.content
});
</script>
Вот и всё, теперь на клиенте можно получать изменения.
В реальности у нас всё примерно так и выглядит, основное отличие от приведённого листинга — это препроцессинг JS и CSS при отдаче на клиент. Да, именно так; в отличие от Webpack, у нас в dev-среде не используются банды, файлы преобразуются по требованию.
Горячее обновление блоков
Чтобы вдохнуть новую жизнь в fest, понадобилось выбрать библиотеку для работы с vdom и написать транспилер для xhtml/xml, учесть проблемы реализации и решить их.
Какие проблемы? Например, чтобы добавить новый функционал (обработку конструкции/тега), приходилось вносить изменения в библиотеку и поднимать версию. Кроме этого, шаблоны можно было скомпилировать только на сервере.
Так и появился Feast. ;]
Еще он преобразует xml/xhtml в JSFUNC, но эта функция возвращает не строку, а JSON, который дальше передается citojs (это очень шустрая и простенькая библиотека для работы с vdom), а citojs уже строит или обновляет vdom.
Кроме этого, теперь компиляция шаблонов происходит прямо на клиенте, поэтому шаблоны отдаются «как есть» и на клиенте преобразуются сначала в AST, а потом, согласно правилам трансформации, в JSFUNC.
// <fn:for data="attrs.items" as="key" value="item">...</fn:for>
'fn:for': {
scope: true,
required: ['data'],
expressions: ['data'],
prepare: (node, {as, key, data}) => ({
as: attrs.as || '$value',
key: attrs.key || '$index',
data
}),
toCode: () => ['EACH($data, @@.children, function (&as, &key) {', '});']);
}
Это позволило решить сразу несколько проблем:
- больше не нужен сервер для компиляции;
- размер даже избыточного html меньше, чем скомпилированного JS;
- правила трансформации можно дополнять на лету;
- максимум метаинформации.
Поэтому при получении нового html на клиенте он заново транслируется в JS-функцию и вызывается перерендер всех блоков, созданных на основе этого шаблона:
socket.on('file-changed:html', (data) => {
const updatedFile = data.file;
feast.Block.all.some(Block => {
if (updatedFile === Block.prototype.template.file) {
const template = feast.parse(data.content, updatedFile);
Block.setTemplate(template);
Block.getInstances().forEach(block => block.render());
return true;
}
});
});
Для CSS примерно такая же логика, главным изменением было внедрение CSS-модульности, чтобы раз и навсегда распрощаться с main.css и доставлять css вместе с кодом компонента, а также защитить селекторы от пересечения и возможности обфускации.
CSS Modules
Как бы громко это ни звучало, но сам процесс достаточно простой и уже был известен (например), но мало распространен из-за отсутствия удобных инструментов. Всё изменилось с появлением postcss и webpack. Прежде чем перейти к нашей реализации, взглянем, как это работает у других, например у React и Angular2.
React + webpack
import React from 'react';
import styles from './button.css';
export default class Button extends React.Component {
render () {
return <button className={styles.btn}>
<span className={styles.icon}><Icon name={this.props.icon}/></span>
<span className={styles.text}>{this.props.value}</span>
</button>;
}
}
React + webpack + react-css-modules
import React from 'react';
import CSSModules from 'react-css-modules';
import styles from './button.css';
class Button extends React.Component {
render () {
return <button styleName='btn'>
<span styleName='icon'><Icon name={this.props.icon}/></span>
<span styleName='text'>{this.props.value}</span>
</button>;
}
}
export default CSSModules(Button, styles);
@CSSModules(styles)
export default class Button extends React.Component {
// ...
}
Angular2
В отличие от React, Ангуляр поддерживает подобие модульности из коробки. По умолчанию он ко всем селекторам добавляет специфичности в виде уникального атрибута, но, если выставить определённый «флажок», будет использовать shadow dom.
@Component({
selector: `my-app`,
template: `<div class="app">{{text}}</div>`,
styles: [`.app { ... }`] // .app[_ngcontent-mjn-1] { }
});
export class App {
// …
}
Наш вариант — что-то среднее, для него не нужно специально подготавливать шаблон, достаточно просто подгрузить css и добавить его в описание блока:
import feast from 'feast';
import template from 'feast-tpl!./button.html';
import styleSheet from 'feast-css!./button.css';
export default feast.Block.extend({
name: 'button',
template,
styleSheet,
});
Кроме этого, есть ещё экспериментальная ветка не просто с заменой классов, а с полноценным inline стилей. Это может пригодиться для работы на слабых устройствах (телевизоры и пр.).
Собственно, сама ветка выглядит так:
const file = "path/to/file.css";
fetch(file)
.then(res => res.text())
.then(cssText => toCSSModule(file, cssText))
.then(updateCSSModuleAndRerenderBlocks)
;
function toModule(file, cssText) {
const exports = {};
cssText = cssText.replace(R_CSS_SELECTOR, (_, name) => {
exports[name] = simpleHash(url + name);
return '.' + exports[name];
});
return {file, cssText, exports};
}
Как видите, абсолютно никакой магии, всё очень банально: получаем css как текст, находим все селекторы, при помощи простого алгоритма считаем hash и сохраняем в объект экспорта [оригинальное название] => [новое].
Ну и самое интересное: JS, что с ним?
JS / Hot Reload
Рассмотрим пример. Допустим, у нас есть класс Foo
:
class Foo {
constructor(value) {
this.value = value;
}
log() {
console.log(`Foo: ${this.value}`, this instanceof Foo);
}
}
Дальше где-то в коде:
var foo = new Foo(123);
foo.log(); // "Foo: 123", true
После чего мы решаем обновить реализацию на NewFoo
:
class NewFoo {
constructor(value) {
this.value = value;
}
log() {
console.log(`NewFoo: ${this.value}`, this instanceof NewFoo);
}
});
Да так, чтобы уже созданные экземпляры продолжили работать корректно.
foo.log(); // "NewFoo: 123", true
foo instanceof Foo; // true
Чтобы проделать такой фокус, не нужен препроцессинг, достаточно чистого JS:
function replaceClass(OldClass, NewClass) {
const newProto = NewClass.prototype;
OldClass.prototype.__proto__ = newProto;
// Обновляем статические методы
Object.keys(NewClass).forEach(name => {
OldClass[name] = NewClass[name];
});
// Переносим методы прототипа
Object.getOwnPropertyNames(newProto).forEach(name => {
OldClass.prototype[name] = newProto[name];
});
}
Да, вот и вся функция, десять строк — и JS Hot Reload готов. Почти. Я специально не стал перегружать эту функцию, а показал только суть. По-хорошему нужно ещё пометить старые методы, которых больше нет, как неудалённые.
Но тут есть проблема :]
replaceClass(Foo, class NewFoo { /* ... */});
foo.constructor === Foo; // false (!!!)
Решить её можно несколькими способами:
- Всё же использовать Webpack, он оборачивает создание класса в специальный враппер, который возвращает и обновляет создаваемый класс.
- Применять обвязку для создания классов, например
createClass('MyClassName', {...});
. - Ещё можно обратиться к Proxy, но тут тоже понадобится препроцессинг
В итоге наша схема выглядит так:
socket.on('file-changed:js', (data) => {
const updatedFile = data.file;
new Function(‘define’, data.content)(hotDefine);
});
hotDefine
занимается всей магией: вместо запрашиваемого объекта (например, feast) возвращает не оригинальный, а специальный FeastHotUpdater
, который и обновляет реализацию.
Инструменты для анализа кода
Как я показал в примере, на данный момент основной инструмент, который позволяет инспектировать элементы прямо из браузера, — это «пипетка». Одна из приятных фич — открытие нужного файла в IDE. Для этого используется замечательная библиотека Романа Дворнова lahmatiy/open-in-editor:
const openInEditor = require('open-in-editor');
const editor = openInEditor.configure(
{editor: ‘phpstorm’},
(err) => console.error('Something went wrong: ' + err)
);
editor.open('path/to/file.js:3:10')
.catch(err => {
console.error('[open-in-editor] Ooops:', err);
});
Еще у Романа есть подобный компонент для инспекции React и Backbone, который умеет намного больше моего, да и выглядит суперски. ;]
Те, кто хорошо знаком с React, Ember, Angular, Backbone, прекрасно знают и о таких решениях, как React Developer Tools, Ember Inspect, Batarand, Backbone Debugger и др. Всё это расширения DevTools для препарирования положения.
Сначала у меня в планах был именно экстеншен, благо API Хрома располагает к этому + есть примеры, а все выше перечисленные расширения лежат на github, так что вы всегда можете посмотреть реализацию.
Но увы, нельзя поставить расширение пользователю, а нам очень часто приходится изучать проблемы на машинах не только коллег. Поэтому пока я сконцентрировался на инструментах, через которые можно получить максимум информации в браузере без его перезагрузки. Тут и раскрывается вся прелесть компиляции шаблонов на клиенте: вам не нужны две сборки (боевая и dev), сборка всегда одна, при дебаге вы всегда получите всю возможную метаинформацию о компоненте.
Что ещё?
Логирование
Баги бывают всегда — это не беда; беда, если вы не можете понять, что произошло раньше. Поэтому мы уделяем много внимания логированию. Идеальная ситуация, если вы в любой момент можете открыть консоль в бою и понять, что стряслось после ваших действий.
Покрытие кода
По большей части это только эксперимент, но его вполне можно использовать для проверки качества ручных тестов. Берём istanbul, прогоняем через него код и раскатываем на тестовые машины, дальше раз в N секунд скидываем в лог покрытие. Вот таким нехитрым способом можно увидеть, насколько хорошо написаны у вас сценарии для тестеров, покрывают ли они функционал.
Анализ структуры приложения
Чем дальше, тем больше приложение растёт, ветвится, и однажды его структура становится непонятной. Так выглядела первая попытка ;]
Думаю, идея ясна: это дерево не просто вложенности блоков, но и условий циклов. После нескольких итераций результат стал более читаемый (несовершенный, но его уже можно использовать).
Сейчас это интерактивное дерево содержит сведения, как блоки вложены друг в друга, под какими условиями и циклами; иными словами, можно окинуть взглядом всё сразу. Но основная цель этого инструмента — просмотр активных узлов в зависимости от входных параметров и нахождение мертвых зон (увы, пока не доделано, так что показать не смогу).
Timeline
Громко будет сказано, но ближайший аналог, только очень простой, — это DevTools Timeline. На сегодня он умеет отображать процессы, происходящие внутри приложения (рендер, события, изменения моделей и т. п.). Это позволяет быстро понять, что именно и в какой последовательности изменилось, сколько времени это заняло. Кроме того timeline уже не раз помог выявить аномальное поведение (лишний перерендер или подозрительные изменения моделей).
Заключение
Не важно, какой фреймворк вы используете, под капотом всё равно будет код, написанный вами, со своей специфичной логикой и стилем, – вот о чем нужно помнить. Поэтому документируйте, описывайте и автоматизируйте эту «специфику». Не бойтесь создавать инструменты под себя, даже маленький bash-скрипт может существенно упростить вашу жизнь. Кроме этого, обязательно ищите готовые инструменты, даже если вам кажется, что таких нет. Чем популярнее инструмент, который вы используете, тем больше его сообщество. У таких решений, как React, Vue, Ember, Angular, – хорошая поддержка Live Coding, расширения для Dev Tools и многое другое. Например, для React недавно вышла уже вторая версия react-storybook.
P.S. А теперь небольшой опрос.
Автор: Mail.Ru Group