Технопарк, весна, 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-emitter
class SomeService {
constructor () { ... }
do () { ... }
addEventListener(event, callback) { ... }
removeEventListener(event, callback) { ... }
emitEvent(eventName, eventData) { ... }
}
Интерфейс подписки на события
// on-off
class SomeService {
constructor () { ... }
do () { ... }
on(event, callback) { ... }
off(event, callback) { ... }
emit(eventName, eventData) { ... }
}
Интерфейс подписки на события
// pub-sub
class SomeService {
constructor () { ... }
do () { ... }
subscribe(event, callback) { ... }
unsubscribe(event, callback) { ... }
publish(eventName, eventData) { ... }
}
Интерфейс подписки на события
// event-dispatcher
class 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}); // событие 1
service.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}); // событие 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');
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.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
Шаблон MVC (Модель-Вид-Контроллер или Модель-Состояние-Поведение) описывает простой способ построения структуры приложения, целью которого является отделение бизнес-логики от пользовательского интерфейса. В результате, приложение легче масштабируется, тестируется, сопровождается и, конечно же, реализуется
View
MenuView, SignView, ScoreboardView...
View
генерируется с помощью шаблонизатораView
View
На сервере роутинг — это процесс определения маршрута внутри приложения в зависимости от запроса. Проще говоря, это поиск контроллера по запрошенному 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=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 страницу (пагинация)
Простота.
В написании, чтении, реализации идейслабо связан с 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-skin
background: #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