Архитектура web-приложений, роутинг, методологии написания CSS

Технопарк, весна, 2019 г.

Архитектура web-приложений
Слайды доступны по ссылке
frontend-park-mailru.firebaseapp.com

Архитектура

Какие-то определения с Википедии...

Если рассматривать приложение как систему — т.е. набор компонентов, объединенных для выполнения определенной функции:

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

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

Архитектурные решения

Как определить, является ли какое-то решение архитектурным. Как определить качество архитектурного решения?
Задайте себе вопрос:

А что если я ошибся и мне придется изменить это решение в будущем? Какие будут последствия?

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

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

Критерии хорошего дизайна системы

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

Критерии плохого дизайна системы

Методологии и принципы
проектирования ПО

0LHRg9C00YMg0L/RgNC+0YHRgtC40YLRg9GC0LrQvtC5 (base64)

Методологии и принципы
проектирования ПО

Где про всё это почитать?

S.O.L.I.D-ый JavaScript open_in_new

Как же создать хорошую архитектуру?

Использовать проверенные подходы!

Декомпозиция как основа
модульной архитектуры

Хорошая декомпозиция должна быть иерархической

Хорошая декомпозиция должна
быть иерархической

Хорошая декомпозиция должна быть функциональной

Хорошая декомпозиция должна
быть функциональной

Во главе функциональной декомпозиции лежит паттерн Модуль

Модуль — это Функция + Данные, необходимые для её выполнения

Простейший модуль на основе
самовызывающейся функции

		(function () {
		    const data1 = ... ; const data2 = ... ;  // локальные переменные
		    const Base = window.Base; // импортируем модуль
		    class Module extends Base {
		        constructor () { ... }
		        do () {
		            console.log(data1, data2);
		        }
		    }
		 
		    window.Module = Module; // экспортируем модуль
		})();
		 
	

Простейший модуль на основе
самовызывающейся функции

		// Использование
		const Module = window.Module; // импортируем модуль
		const m1 = new Module();
		const m2 = new Module();
		// ...
		 
		m1.do();
		// ...
		 
	

Используем всего лишь одну
глобальную переменную!

		(function (modules) {
		    const Base = modules.Base; // импортируем модуль
		    class Module extends Base {
		        constructor () { ... }
		        do () { ... }
		    }
		 
		    modules.Module = Module; // экспортируем модуль
		})(window.___all_modules);
		 
	

Множество систем определения
модулей

Почти все из них здесь

PRO TIPs использования модулей

Антон Немцев — Паттерны JavaScript open_in_new

Хорошая декомпозиция должна создавать слабосвязанную систему

Парочка определений

Internal Cohesion — сопряженность или «сплоченность» внутри модуля (составных частей модуля друг с другом)

External Coupling — связанность взаимодействующих друг с другом модулей.

Хорошая декомпозиция должна
создавать слабосвязанную систему

Модули, полученные в результате декомпозиции, должны быть максимально сопряженны внутри (high internal cohesion) и минимально связанны друг с другом (low external coupling)

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

Ослаблять связанность нужно на
всех уровнях иерархии

Способы снижения связанности
модулей

Паттерн Observable

Интерфейс подписки на события

		// event-emitter
		class SomeService {
		    constructor () { ... }
		    do () { ... }
		 
		    addEventListener(event, callback) { ... }
		    removeEventListener(event, callback) { ... }
		    emitEvent(eventName, eventData) { ... }
		}
		 
	

Паттерн Observable

Интерфейс подписки на события

		// on-off
		class SomeService {
		    constructor () { ... }
		    do () { ... }
		 
		    on(event, callback) { ... }
		    off(event, callback) { ... }
		    emit(eventName, eventData) { ... }
		}
		 
	

Паттерн Observable

Интерфейс подписки на события

		// pub-sub
		class SomeService {
		    constructor () { ... }
		    do () { ... }
		 
		    subscribe(event, callback) { ... }
		    unsubscribe(event, callback) { ... }
		    publish(eventName, eventData) { ... }
		}
		 
	

Паттерн Observable

Интерфейс подписки на события

		// event-dispatcher
		class SomeService {
		    constructor () { ... }
		    do () { ... }
		 
		    attachEvent(event, callback) { ... }
		    detachEvent(event, callback) { ... }
		    dispatchEvent(eventName, eventData) { ... }
		}
		 
	

Реализация паттерна Observable

		class SomeService {
		    on(event, callback) {    // подписываемся на событие
		        this.listeners[event].push(callback)
		    }
		    off(event, callback) {   // отписываемся от события
		        this.listeners[event] = this.listeners[event]
		            .filter(function (listener) { return listener !== callback; })
		    }
		    emit(event, data) {      // публикуем (диспатчим, эмитим) событие
		        this.listeners[event].forEach(function (listener) {
		            listener(data)
		        })
		    }
		}
		 
	

Реализация паттерна Observable

		const service = new SomeService();
		// функция-обработчик события
		const onload = function (data) { console.log(data); }
		 
		// подписываемся на событие
		service.on('loaded', onload);
		service.emit('loaded', {data: 42});    // событие 1
		service.emit('loaded', {foo: 'bar'});  // событие 2
		// отписываемся от события
		service.off('loaded', onload);
		 
	

Реализация паттерна Observable

		const service = new SomeService();
		// функция-обработчик события
		const onload = function (data) { console.log(data); }
		 
		// подписываемся на событие
		service.on('loaded', onload);
		service.emit('loaded', {data: 42});    // событие 1
		service.emit('loaded', {foo: 'bar'});  // событие 2
		// отписываемся от события
		service.off('loaded', onload);
		 
	

Итак, как это помогает уменьшить связность приложения?

Что было до?

		// services/user-service.js
		class UserService {
		    auth (login, password) {
		        return HTTP.POST('/api', {login, password})
		            .then(function (user) {
		                this.user = user;
		                this.menuSection.reRender();
		                this.signupSection.hide();
		                this.profileSection.show(user);
		                // ...
		            }.bind(this));
		    }
		}
		 
	

Что было до?

		// blocks/scoreboard.js
		const UserService = window.UserService;
		 
		class ScoreBoardBlock {
		    constructor () {
		        this.userService = new UserService();
		    }
		 
		    render (login, password) {
		        const users = this.userService.getUsers();
		        this.el.innerHTML = tmpl({users: users});
		    }
		}
		 
	

Как станет?

		// services/user-service.js
		class UserService {
		    auth (login, password) {
		        return HTTP.POST('/api', {login, password})
		            .then(function (user) {
		                this.user = user;
		                this.emit('signed', user);
		                // всё!
		            }.bind(this));
		    }
		    on(event, callback) { ... }
		    emit(eventName, eventData) { ... }
		}
		 
	

Как станет?

		// blocks/scoreboard.js
		class ScoreBoardBlock {
		    onUsersLoaded (users) {
		        this.el.innerHTML = tmpl({users: users});
		    }
		}
		 
	
		// main.js
		UserService.on('signed', menuSection.reRender.bind(menuSection));
		UserService.on('users-loaded', scoreboard.onUsersLoaded.bind(scoreboard));
		// ...
		UserService.auth('login', 'password');
		 
	

В чём преимущество?

А если таких Observable-модулей очень много?

Реализовывать паттерн Observable
в каждом модуле?

		// плохо... дублирование кода
		class UserService {
		    on(event, callback) { ... }
		    emit(eventName, eventData) { ... }
		}
		class GameService {
		    on(event, callback) { ... }
		    emit(eventName, eventData) { ... }
		}
		class ShopService {
		    // ...
		}
		 
	

Выделить Observable в отдельный
модуль и отнаследоваться от него?

		class UserService extends Observable {
		    // ...
		}
		class GameService extends Observable {
		    // ...
		}
		 
		// а если таких модулей-интерфейсов будет несколько?
		class ShopService extends Observable, Loggable, Serializable {
		    // ...
		}
		 
	

Паттерн Примесь (Mixin)

Паттерн Примесь (Mixin)

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

Подробнее в статье по ссылке и на learn.javascript.ru

Использование примеси

		const ObservableMixin = {
		    on(event, callback) { ... }
		    emit(eventName, eventData) { ... }
		    // ...
		}
		class UserService {
		    constructor () {
		        this.emit('event', { ... });
		        // ...
		    }
		}
		Object.assign(UserService.prototype, ObservableMixin);
		 
	

А что кроме наследования?

Как создать класс МФУ?

		class ЧБПринтер {
		    print (doc) { ... }
		}
		 
		class ЦветнойПринтер {
		    print (doc) { ... }
		}
		 
		class Сканер {
		    scan (doc) { ... }
		}
		 
	

Наследование Композиция!

		class МФУ {
		    constructor () {
		        this.чбПринтер = new ЧБПринтер();
		        this.цветнойПринтер = new ЦветнойПринтер();
		        this.сканер = new Сканер();
		        // ...
		    }
		 
		    scan (doc) {
		        return this.сканер.scan(doc);
		    }
		}
		 
	

А если будет много Observable-модулей?

Паттерн медиатор

Когда в системе присутствует большое количество модулей, их прямое взаимодействие друг с другом (даже с учётом применения publish-subscriber подхода) становится слишком сложным. Поэтому имеет смысл взаимодействие «все со всеми» заменить на взаимодействие «один со всеми». Для этого вводится некий обобщенный посредник — медиатор

Паттерн медиатор

		// modules/event-bus.js
		export default class EventBus {
		    on(event, callback) { ... }
		    off(event, callback) { ... }
		    emit(eventName, eventData) { ... }
		}
		 
		// main.js
		import EventBus from './modules/event-bus.js';
		window.bus = new EventBus();
		 
	

Паттерн медиатор

		// services/user-service.js
		export default class UserService {
		    login () {
		        HTTP.post({ ... })
		            .then(function (user) {
		                // do stuff
		                
		                window.bus.emit('user:logged-in', user);
		            })
		    }
		}
		 
	

Паттерн медиатор

		// blocks/user-profile.js
		export default class UserProfile {
		    constructor () {
		        window.bus.on('user:logged-in', function (user) {
		            // do stuff
		            
		            this.render();
		        }.bind(this))
		    }
		}
		 
	

Глобальные переменные — зло!

Паттерн синглтон

		// modules/event-bus.js
		class EventBus {
		    on(event, callback) { ... }
		    off(event, callback) { ... }
		    emit(eventName, eventData) { ... }
		}
		 
		export default new EventBus();
		 
	

Паттерн синглтон

		// blocks/user-profile.js
		import bus from '../modules/event-bus.js';
		 
		export default class UserProfile {
		    constructor () {
		        bus.on('user:logged-in', function (user) {
		            // do stuff
		            
		            this.render();
		        }.bind(this))
		    }
		}
		 
	

А можно и вот так

		// modules/event-bus.js
		export default class EventBus {
		    constructor () {
		        if (EventBus.__instance) {
		            return EventBus.__instance;
		        }
		        // initialization logic
		        
		        EventBus.__instance = this;
		    }
		}
		 
	

А можно и вот так

		// main.js
		import EventBus from './modules/event-bus.js';
		 
		const bus1 = new EventBus();
		const bus2 = new EventBus();
		 
		bus1 === bus2; // true
		 
	

PRO TIPs хорошей декомпозиции

Какие ещё есть подходы?

Как проектировать хорошую архитектуру open_in_new
очень крутая статья

Шаблоны MV*

MV-паттерны для проектирования
веб-приложений

Шаблон MVC

Шаблон MVC (Модель-Вид-Контроллер или Модель-Состояние-Поведение) описывает простой способ построения структуры приложения, целью которого является отделение бизнес-логики от пользовательского интерфейса. В результате, приложение легче масштабируется, тестируется, сопровождается и, конечно же, реализуется

Модели в MVC

Представления (aka вьюхи) в MVC

Контроллеры в MVC

Собираем всё воедино

Как нам сделать шаг в
сторону MVC?

Как нам сделать шаг в
сторону MVC?

Роутинг в web-приложениях

Вернёмся немного назад

А если Views очень много?

Вспоминаем про паттерн "Медиатор"

Роутер

Что такое роутинг?

На сервере роутинг — это процесс определения маршрута внутри приложения в зависимости от запроса. Проще говоря, это поиск контроллера по запрошенному URL и выполнение соответствующих действий

На клиенте роутинг позволяет установить соответствие между состоянием приложения и View, которая будет отображаться. Таким образом, роутинг — это вариант реализации паттерна "Медиатор" для MV* архитектуры приложений

Кроме этого, роутеры решают ещё одну очень важную задачу. Они позволяют эмулировать историю переходов в SPA-приложениях

Что такое роутинг?

Таким образом взаимодействие и переключение между View происходит посредством роутера, а сами View друг о друге ничего не знают

		Router.register({state: 'main'}, MenuView);
		Router.register({state: 'signup'}, SignupView);
		Router.register({state: 'scores'}, ScoreboardView);
		...
		// переход на 3 страницу (пагинация)
		Router.go({state: 'scores', params: {page: 3}});
		 
	

Что такое роутинг?

		class Router {
		    constructor() { ... }
		    register(path: string, view: View) { ... }
		    start() { ... }      // запустить роутер
		    go(path: string) { ... }
		    back() { ... }       // переход назад по истории браузера
		    forward() { ... }    // переход вперёд по истории браузера
		}
		 
	

History API

History API — браузерное API, позволяет манипулировать историей браузера в пределах сессии , а именно историей о посещённых страницах в пределах вкладки или фрейма, загруженного внутри страницы. Позволяет перемещаться по истории переходов, а так же управлять содержимым адресной строки браузера

History API

		// Перемещение вперед и назад по истории
		window.history.back(); // работает как кнопка "Назад"
		window.history.forward(); // работает как кнопка "Вперёд"
		 
		window.history.go(-2); // перемещение на несколько записей
		window.history.go( 2);
		 
		const length = window.history.length; // количество записей
		 
	

History API

		// Изменение истории
		const state = { foo: 'bar' };
		window.history.pushState(
		    state,         // объект состояния
		    'Page Title',  // заголовок состояния
		    '/pages/menu'  // URL новой записи (same origin)
		);
		window.history.replaceState(state2, 'Other Title', '/another/page');
		 
	

History API

Событие popstate отсылается объекту window каждый раз, когда активная запись истории меняется между двумя записями истории для одного и того же документа

Простой вызов pushState() или replaceState() не вызовет событие popstate. Оно срабатывает только тогда, когда происходят какие-то действия в браузере, такие как нажатие кнопки "назад" (или вызов history.back() из JavaScript)

History API

		window.onpopstate = event => console.log(location.pathname);
		 
		history.pushState({ page: 1 }, 'Title 1', '/menu?page=1');
		history.pushState({ page: 2 }, 'Title 2', '/app?page=2');
		history.pushState({ page: 3 }, 'Title 3', '/scores?page=3');
		history.back();   // /app?page=2
		history.back();   // /menu?page=1
		history.go(2);    // /scores?page=3
		 
	

Что такое роутинг?

		Router.register('/', MenuView);
		Router.register('/signup', SignupView);
		Router.register('/scores/pages/{page}', ScoreboardView);
		...
		Router.go('/scores/page/3'); // переход на 3 страницу (пагинация)
		 
	

Практика (если есть время...)

"Архитектура" CSS

С JS'ом разобрались, а с CSS что?

Дизайн языка

Подробнее о принципах в документации W3C

Как стили попадают на страницу

Писать стили — одно удовольствие

		/* Селекторы! */
		 
		*                                  /* универсальный селектор */
		div, span, a                       /* селекторы по имени тегов */
		.class                             /* селекторы по имени классов */
		#id                                /* селекторы по идентификаторам */
		[type="text"], [src*="/img/"]      /* селекторы по атрибутам */
		:first-child, :visited, :nth-of-type(An+B), :empty ... 
		::before, ::placeholder, ::selection, ::first-letter ... 
		a > a, a + a , a ~ a               /* вложенность и каскадирование */
	

Можно создавать правила любой степени вложенности — "каскад".

Как понять,

какой селектор сработает?

Какие могут быть проблемы?

Задача: один и тот же компонент должен выглядеть по-разному в зависимости от страницы

		/* Изменение компонентов в зависимости от родителя */
		.button { border: 1px solid black; }
		#sidebar .button { border-color: red; }
		#header .button { border-color: green; }
		#menu .button { border-color: blue; }
		 
	

Какие могут быть проблемы?

Задача: найти элемент на странице "в слепую"

		/* Глубокая степень вложенности */
		#main-nav ul li ul li ol span div { ... }
		#content .article h1:first-child [name=accent] { ... }
		#sidebar > div > h3 + p a ~ strong { ... }
		 
	
Проблема: сильная связанность со структурой документа

Какие могут быть проблемы?

Задача: сделать стили более понятными для всех
		/* Широко используемые имена классов */
		.article { ... }
		.article .header { ... }
		.article .title { ... }
		.article .content { ... }
		.article .section { ... }
		 
	
Проблема не очевидна, но она есть!

Какие могут быть проблемы?

Задача: сделать стили более понятными для всех
			/* Широко используемые имена классов */
			.article { ... }
			.article .header { ... }
			.article .title { ... }
			.article .content { ... }
			.article .section { ... }
			 
		
Проблема: пересечение имён с внешними библиотеками или даже внутри собственного проекта

Какие могут быть проблемы?

		/* Супер классы! */
		.super-class {
		  margin: 10px;
		  position: absolute;
		  background: black;
		  color: white;
		  transition: color 0.2s;
		  .....
		}
		 
	

Признаки хорошей архитектуры
(всё то же самое)

Решение? Использовать зарекомендовавшие себя методологии!
Подробнее про них в статье "Способы организации CSS кода"

Доклад о методологиях (альтернативный)
Перечень всех методологий из доклада

OOCSS — Объектно-ориентированный CSS

Объектно-ориентированный CSS
OOCSS

Разделение структуры и оформления
			.header {
			  color: #000;
			  background: #BADA55;
			  width: 960px;
			  margin: 0 auto;
			}
		
			.footer {
			 color: #000;
			 background: #BADA55;
			  text-align: center;
			  padding-top: 20px;
			}
		
Решение? Вынести повторяющуюся визуальную структуру в миксин
		.colors-skin {                  /* Использование */
		  color: #000;                  .footer .colors-skin
		  background: #BADA55;          .header .colors-skin
		}
	

Объектно-ориентированный CSS
OOCSS

Разделение контейнера и содержимого.
Принцип: внешний вид элемента не зависит от того, где он расположен. Вместо .my-element button { ... } создаем отдельный стиль .controlдля конкретного случая
Зачем?

Страница проекта — Github

SMACSS — Масштабируемая и
модульная архитектура для CSS

SMACSS расшифровывается как «масштабируемая и модульная архитектура для CSS» (Scalable and Modular Architecture for CSS). Основная цель подхода — уменьшение количества кода и упрощение его поддержки Документация

БЭМ — Блок, Элемент, Модификатор

Методология БЭМ — Блок

Блок в методологии БЭМ — функционально независимый компонент страницы, который может быть повторно использован. В HTML блоки представлены атрибутом class

Страница проекта — Придумано в Яндексе

Методология БЭМ — Элемент

Элемент в методологии БЭМ — составная часть блока, которая не может использоваться в отрыве от него

Методология БЭМ — Модификатор

Модификатор в методологии БЭМ — сущность, определяющая внешний вид, состояние или поведение блока либо элемента.

Форма по БЭМу

		<form class="login-form">
		    <input type="text" 
		           class="text-input login-form__username-input"/>
		    <div class="login-form__buttons">
		        <button class="login-form__submit button">
		            <span class="button__capture button__capture_red">CLICK ME</span>
		        </button>
		        <button class="login-form__reset login-form__reset_disabled button">
		            RESET</button>
		    </div>
		</form>
		 
	

Форма по БЭМу

Основной блок
		<form class="login-form">
		    <input type="text" 
		           class="text-input login-form__username-input"/>
		    <div class="login-form__buttons">
		        <button class="login-form__submit button">
		            <span class="button__capture button__capture_red">CLICK ME</span>
		        </button>
		        <button class="login-form__reset login-form__reset_disabled button">
		            RESET</button>
		    </div>
		</form>
		 
	

Форма по БЭМу

Блок и его элементы
		<form class="login-form">
		    <input type="text" 
		           class="text-input login-form__username-input"/>
		    <div class="login-form__buttons">
		        <button class="login-form__submit button">
		            <span class="button__capture button__capture_red">CLICK ME</span>
		        </button>
		        <button class="login-form__reset login-form__reset_disabled button">
		            RESET</button>
		    </div>
		</form>
		 
	

Форма по БЭМу

Вложенные блоки
		<form class="login-form">
		    <input type="text" 
		           class="text-input login-form__username-input"/>
		    <div class="login-form__buttons">
		        <button class="login-form__submit button">
		            <span class="button__capture button__capture_red">CLICK ME</span>
		        </button>
		        <button class="login-form__reset login-form__reset_disabled button">
		            RESET</button>
		    </div>
		</form>
		 
	

Форма по БЭМу

Вложенный блок и его элементы
		<form class="login-form">
		    <input type="text" 
		           class="text-input login-form__username-input"/>
		    <div class="login-form__buttons">
		        <button class="login-form__submit button">
		            <span class="button__capture button__capture_red">CLICK ME</span>
		        </button>
		        <button class="login-form__reset login-form__reset_disabled button">
		            RESET</button>
		    </div>
		</form>
		 
	

Форма по БЭМу

Модификаторы
		<form class="login-form">
		    <input type="text" 
		           class="text-input login-form__username-input"/>
		    <div class="login-form__buttons">
		        <button class="login-form__submit button">
		            <span class="button__capture button__capture_red">CLICK ME</span>
		        </button>
		        <button class="login-form__reset login-form__reset_disabled button">
		            RESET</button>
		    </div>
		</form>
		 
	

Форма по БЭМу

Javascript
		<form class="login-form">
		    <input type="text" 
		           class="text-input login-form__username-input"/>
		    <div class="login-form__buttons">
		        <button class="login-form__submit button js-login-button">

		             <span class="button__capture button__capture_red">CLICK ME</span>

		        </button>
		        <button class="login-form__reset login-form__reset_disabled button">
		            RESET</button>
		    </div>
		</form>
		 
	

Форма по БЭМу

		/* блок */
		.login-form { ... }
		/* эмененты */
		.login-form__buttons { ... }
		.login-form__submit { ... }
		.login-form__reset { ... }
		.login-form__username-input { ... }
		.login-form__password-input { ... }
		/* модификаторы */
		.login-form__submit_disabled { ... }
		.login-form__reset_disabled { ... }
		 
	

Форма по БЭМу

		/* блок */
		.button { ... }
		/* элементы */
		.button__capture { ... }
		.button__icon { ... }
		/* модификаторы */
		.button_big { ... }
		.button_inverted { ... }
		.button__capture_red { ... }
		.button__capture_green { ... }
		 
	

Быстрые выводы

Что почитать: Культ карго CSS и Архитектура CSS — Web Стандарты

CSS-in-JS

CSS через JS

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

Но зачем???

Реализация — CSS in JS

"CSS в JavaScript: будущее компонентных стилей" — Хабр

Практика

Полезные ссылки

Всем спасибо!