Технопарк, осень, 2024 г.
Какие-то определения с Википедии...
Если рассматривать приложение как систему — т.е. набор компонентов, объединенных для выполнения определенной функции:Архитектура идентифицирует главные компоненты системы и способы их взаимодействия. Также это выбор таких решений, которые интерпретируются как основополагающие и не подлежащие изменению в будущем
Архитектура — это организация системы, воплощенная в её компонентах, их отношениях между собой и с окружением.
Как определить, является ли какое-то решение архитектурным. Как определить качество архитектурного решения?
Задайте себе вопрос:
А что если я ошибся и мне придется изменить это решение в будущем? Какие будут последствия?
Если некоторое решение размазано ровным слоем по всему приложению, то стоимость его изменения будет огромной, а значит это решение является архитектурным
Где про всё это почитать?
Во главе функциональной декомпозиции лежит паттерн Модуль
Модуль — это Функция + Данные, необходимые для её выполнения
(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);
// 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
Flux-архитектура — архитектурный подход или набор шаблонов программирования для построения пользовательского интерфейса веб-приложений, сочетающийся с реактивным программированием и построенный на однонаправленных потоках данных.
Основной отличительной особенностью Flux является односторонняя направленность передачи данных между компонентами Flux-архитектуры. Архитектура накладывает ограничения на поток данных, в частности, исключая возможность обновления состояния компонентов самими собой. Такой подход делает поток данных предсказуемым и позволяет легче проследить причины возможных ошибок в программном обеспечении
В минимальном варианте Flux-архитектура может содержать три слоя, взаимодействующие по порядку:
Action – выражение событий (обычно - простой объект). Диспетчеры передают действия нижележащим компонентам (хранилищам) по одному. Новое действие не передаётся пока предыдущее полностью не обработано компонентами. Действия из-за работы источника действия, например, пользователя, поступают асинхронно, но их диспетчеризация является синхронным процессом. Кроме имени, действия могут иметь полезную нагрузку (payload), содержащую относящиеся к действию данные.
// {"name": "IncreaseCount", "payload": {"inc": 1}}
Dispatcher предназначен для передачи действий хранилищам. В упрощённом варианте диспетчер может вообще не выделяться, как единственный на всё приложение. В диспетчере хранилища регистрируют свои функции обратного вызова (callback) и зависимости между хранилищами.
Store является местом, где сосредоточено состояние (state) приложения. Остальные компоненты, согласно Flux, не имеют значимого (с точки зрения архитектуры) состояния. Изменение состояния хранилища происходит строго на основе данных действия и старого состояния хранилища (т.е. является чистой функцией)
View – компонент, обычно отвечающий за выдачу информации пользователю. Во Flux-архитектуре, которая может технически не касаться внутреннего устройства представлений вообще, это — конечная точка потоков данных. Для информационной архитектуры важно только, что данные попадают в систему (то есть, обратно в хранилища) только через действия
На сервере роутинг — это процесс определения маршрута внутри приложения в зависимости от запроса. Проще говоря, это поиск контроллера по запрошенному 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