Технопарк, весна, 2019 г.
Какие-то определения с Википедии...
Если рассматривать приложение как систему — т.е. набор компонентов, объединенных для выполнения определенной функции:Архитектура идентифицирует главные компоненты системы и способы их взаимодействия. Также это выбор таких решений, которые интерпретируются как основополагающие и не подлежащие изменению в будущем
Архитектура — это организация системы, воплощенная в её компонентах, их отношениях между собой и с окружением.
Как определить, является ли какое-то решение архитектурным. Как определить качество архитектурного решения?
Задайте себе вопрос:
А что если я ошибся и мне придется изменить это решение в будущем? Какие будут последствия?
Если некоторое решение размазано ровным слоем по всему приложению, то стоимость его изменения будет огромной, а значит это решение является архитектурным
Где про всё это почитать?
Во главе функциональной декомпозиции лежит паттерн Модуль
Модуль — это Функция + Данные, необходимые для её выполнения
(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);
Почти все из них здесь
Internal Cohesion — сопряженность или «сплоченность» внутри модуля (составных частей модуля друг с другом)
External Coupling — связанность взаимодействующих друг с другом модулей.
Модули, полученные в результате декомпозиции, должны быть максимально сопряженны внутри (high internal cohesion) и минимально связанны друг с другом (low external coupling)
Модули, на которые разбивается система, должны быть, по возможности, независимы или слабо связанны друг с другом. Они должны иметь возможность взаимодействовать, но при этом как можно меньше знать друг о друге
Интерфейс подписки на события
// event-emitterclass SomeService {constructor () { ... }do () { ... }addEventListener(event, callback) { ... }removeEventListener(event, callback) { ... }emitEvent(eventName, eventData) { ... }}
Интерфейс подписки на события
// on-offclass SomeService {constructor () { ... }do () { ... }on(event, callback) { ... }off(event, callback) { ... }emit(eventName, eventData) { ... }}
Интерфейс подписки на события
// pub-subclass SomeService {constructor () { ... }do () { ... }subscribe(event, callback) { ... }unsubscribe(event, callback) { ... }publish(eventName, eventData) { ... }}
Интерфейс подписки на события
// event-dispatcherclass SomeService {constructor () { ... }do () { ... }attachEvent(event, callback) { ... }detachEvent(event, callback) { ... }dispatchEvent(eventName, eventData) { ... }}
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)})}}
const service = new SomeService();// функция-обработчик событияconst onload = function (data) { console.log(data); }// подписываемся на событиеservice.on('loaded', onload);service.emit('loaded', {data: 42}); // событие 1service.emit('loaded', {foo: 'bar'}); // событие 2// отписываемся от событияservice.off('loaded', onload);
const service = new SomeService();// функция-обработчик событияconst onload = function (data) { console.log(data); }// подписываемся на событиеservice.on('loaded', onload);service.emit('loaded', {data: 42}); // событие 1service.emit('loaded', {foo: 'bar'}); // событие 2// отписываемся от событияservice.off('loaded', onload);
// services/user-service.jsclass 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.jsconst 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.jsclass 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.jsclass ScoreBoardBlock {onUsersLoaded (users) {this.el.innerHTML = tmpl({users: users});}}
// main.jsUserService.on('signed', menuSection.reRender.bind(menuSection));UserService.on('users-loaded', scoreboard.onUsersLoaded.bind(scoreboard));// ...UserService.auth('login', 'password');
main.js)// плохо... дублирование кодаclass UserService {on(event, callback) { ... }emit(eventName, eventData) { ... }}class GameService {on(event, callback) { ... }emit(eventName, eventData) { ... }}class ShopService {// ...}
class UserService extends Observable {// ...}class GameService extends Observable {// ...}// а если таких модулей-интерфейсов будет несколько?class ShopService extends Observable, Loggable, Serializable {// ...}
Примесь (англ. 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);}}
Когда в системе присутствует большое количество модулей, их прямое взаимодействие друг с другом (даже с учётом применения publish-subscriber подхода) становится слишком сложным. Поэтому имеет смысл взаимодействие «все со всеми» заменить на взаимодействие «один со всеми». Для этого вводится некий обобщенный посредник — медиатор
// modules/event-bus.jsexport default class EventBus {on(event, callback) { ... }off(event, callback) { ... }emit(eventName, eventData) { ... }}// main.jsimport EventBus from './modules/event-bus.js';window.bus = new EventBus();
// services/user-service.jsexport default class UserService {login () {HTTP.post({ ... }).then(function (user) {// do stuffwindow.bus.emit('user:logged-in', user);})}}
// blocks/user-profile.jsexport default class UserProfile {constructor () {window.bus.on('user:logged-in', function (user) {// do stuffthis.render();}.bind(this))}}
// modules/event-bus.jsclass EventBus {on(event, callback) { ... }off(event, callback) { ... }emit(eventName, eventData) { ... }}export default new EventBus();
// blocks/user-profile.jsimport bus from '../modules/event-bus.js';export default class UserProfile {constructor () {bus.on('user:logged-in', function (user) {// do stuffthis.render();}.bind(this))}}
// modules/event-bus.jsexport default class EventBus {constructor () {if (EventBus.__instance) {return EventBus.__instance;}// initialization logicEventBus.__instance = this;}}
// main.jsimport EventBus from './modules/event-bus.js';const bus1 = new EventBus();const bus2 = new EventBus();bus1 === bus2; // true
Шаблон MVC (Модель-Вид-Контроллер или Модель-Состояние-Поведение) описывает простой способ построения структуры приложения, целью которого является отделение бизнес-логики от пользовательского интерфейса. В результате, приложение легче масштабируется, тестируется, сопровождается и, конечно же, реализуется
ViewMenuView, SignView, ScoreboardView...View генерируется с помощью шаблонизатораViewView
На сервере роутинг — это процесс определения маршрута внутри приложения в зависимости от запроса. Проще говоря, это поиск контроллера по запрошенному 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 — браузерное API, позволяет манипулировать историей браузера в пределах сессии , а именно историей о посещённых страницах в пределах вкладки или фрейма, загруженного внутри страницы. Позволяет перемещаться по истории переходов, а так же управлять содержимым адресной строки браузера
// Перемещение вперед и назад по историиwindow.history.back(); // работает как кнопка "Назад"window.history.forward(); // работает как кнопка "Вперёд"window.history.go(-2); // перемещение на несколько записейwindow.history.go( 2);const length = window.history.length; // количество записей
// Изменение историиconst state = { foo: 'bar' };window.history.pushState(state, // объект состояния'Page Title', // заголовок состояния'/pages/menu' // URL новой записи (same origin));window.history.replaceState(state2, 'Other Title', '/another/page');
Событие
popstateотсылается объектуwindowкаждый раз, когда активная запись истории меняется между двумя записями истории для одного и того же документа
Простой вызов pushState() или replaceState() не вызовет событие popstate.
Оно срабатывает только тогда, когда происходят какие-то действия в браузере, такие как нажатие кнопки "назад"
(или вызов history.back() из JavaScript)
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=2history.back(); // /menu?page=1history.go(2); // /scores?page=3
Router.register('/', MenuView);Router.register('/signup', SignupView);Router.register('/scores/pages/{page}', ScoreboardView);...Router.go('/scores/page/3'); // переход на 3 страницу (пагинация)
Простота. В написании, чтении, реализации идейслабо связан с HTML и лишь дополняет егоГибкость => каскадирование и наследование/* Селекторы! */* /* универсальный селектор */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;.....}
.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-skinbackground: #BADA55; .header .colors-skin}
.my-element button { ... } создаем отдельный стиль .controlдля конкретного случаяbutton будут выглядеть одинаково.control. Работает как mixin.my-element button не нужно переопределять, если передумалиСтраница проекта — Github
Документация
Блок в методологии БЭМ — функционально независимый компонент страницы, который может быть повторно использован. В HTML блоки представлены атрибутом
class
Страница проекта — Придумано в Яндексе
Элемент в методологии БЭМ — составная часть блока, которая не может использоваться в отрыве от него
имя-блока__имя-элемента. Имя элемента
отделяется от имени блока двумя подчеркиваниями
Модификатор в методологии БЭМ — сущность, определяющая внешний вид, состояние или поведение блока либо элемента.
<form class="login-form"><input type="text"<input 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"<input 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"<input 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"<input 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"<input 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"<input 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"<input 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 { ... }
button.html + button.css + button.js.animated, .themed, .hidden, .row.js-class, data-mnemo="id"Что почитать: Культ карго CSS и Архитектура CSS — Web Стандарты
Идея: представить стили через объект, записывать их в тег style, когда посчитаем нужным
Но зачем???Реализация — CSS in JS