Русский
Русский
English
Статистика
Реклама

Angular2

Дружим Angular с Google

27.08.2020 18:04:37 | Автор: admin

Дружим Angular с Google


Google ненавидит SPA


Когда мы говорим про современные интернет магазины, мы представляем себе тяжелые для понимания серверы, рендрящие тысячи статических страничек. Причем именно эти тысячи отрендеренных страниц одна из причин почему Single Page Applications не прижились в электронной коммерции. Даже крупнейшие магазины электронной коммерции по-прежнему выглядят как куча статических страниц, для пользователя это нескончаемый цикл кликов, ожидания и перезагрузки страниц.


Одностраничные приложения приятно отличаются динамичностью взаимодействия с пользователем и более сложным UX. Но как не прискорбно обычно пользовательский опыт приносится в жертву SEO оптимизации. Для сеошника сайт на angular это такая себе проблема поискаовикам трудно индексировать страницы с динамическим контентом.


Другой недостаток SPA это превью сайта. Например, пользователь только что купил новый телевизор в нашем интернет магазине, и хочет порекомендовать его своим друзьям в соцсетях. Превью ссылки в случае Angular будет выглядеть так:


site preview


Мы любим JS и Angular. Мы верим, что классный и удобный UX может быть построен с на этом стеке технологий, и мы можем решить все сопутствующие проблемы. В какой-то момент мы наткнулись на Angular Universal. Это модуль Angular для рендеринга на стороне сервера. Сначала нам показалось что вот оно решение, но радость была преждевременной и отсутствие больших проектов, с его применением, было доказательством этого. Шесть месяцев назад мы надеялись найти production ready решение, но поняли, что нет больших проектов, написанных на Universal.


В итоге, мы начали разрабатывать компоненты для интернет-магазина, используя обычный Angular 2, и ждали, когда Universal будет объединен с Angular Core. На данный момент слияния проектов еще не поизошло, и пока не ясно, когда это произойдет (или как итоговый вариант будет совместим с текущей реализацией), однако сам Universal уже перекочевал в github репозиторий Angular.


Не смотря на эти трудности, наша цель осталась неизменной создавать классные веб-приложения на Angular с серверным рендерингом для индексации поисковиками. В результате наша команда разработала более 20 универсальных компонентов для быстрой сборки интернет-магазина. Как в итоге это было достигнуто?


Что такое Angular Universal


Прежде всего, давайте обсудим, что такое Angular Universal. Когда мы запустим наше приложение на Angular 2 и откроем исходный код, увидим что-то вроде этого:


<!DOCTYPE html><html><head>  <meta charset="utf-8">  <title>Angular 2 app</title>  <!-- base url -->  <base href="http://personeltest.ru/aways/habr.com/"><body>  <app></app></body></html>

Фронтенд фреймворки, такие как Angular, динамически добавляют контент, стили и данные в тег .
Для приложения ориентированного в первую очередь на пользователя критически важно иметь аналитику по действиям пользователя и по индексации сайта поисковиками. Ну и конечно мы хотим чтобы наш контент индексироваля и был в топе выдачи google.

Для решения проблем c индексацией Angular Universal дает нам возможность выполнять рендеринг на стороне сервера. Наша страница будет создаваться на бэкэнд-сервере, написанном на Node.Js, .NET или другом языке, и браузер пользователя получит страницу со всеми привычными тегами в ней -заголовками, мета-тегами и контентом.


В нашем случае это отлично, потому что краулеры поисковиков очень хорошо подготовлены к индексированию статических веб-страниц. Мы разрабатываем стандартное приложение на Angular, а Universal заботится о серверном рендеринге.


До этого момента все выглядит не плохо? Возможно, но дьявол скрыт в мелочах, как всегда.


Итак, мы хотим поделиться с вами подводными камнями, с которыми мы столкнулись на нашем пути.


Подводные камни Angular Universal


Не трогайте DOM


Когда мы начали тестировать компоненты нашего магазина с помощью Universal, нам пришлось потратить некоторое время, чтобы понять, почему наш сервер падает при запуске без вывода серверной страницы. Например, у нас есть компонент Session Flow component, который отслеживает активность пользователя во время сессии (перемещения пользователя, клики, рефферер, информация об устройстве пользователя и т.д.). После поиска информации в issues на GitHub мы поняли, что в Universal нет обертки над DOM.


DOM на сервере не существует.


Если вы склонируете этот Angular Universal стартер и откроете browser.module.ts вы увидите, что в массиве providers разработчики Universal предоставляют дваboolean значения:


providers: [    { provide: 'isBrowser', useValue: isBrowser },    { provide: 'isNode', useValue: isNode },    ...  ]

Итак, если вы хотите чтобы ренедеринг на сервере работал, вы должны заинжектить в свои компоненты эти переменные и обернуть в проверку среды выполнения фрагменты кода, где непосредственно происходит взаимодействие с DOM. Например, вот так:


@Injectable()export class SessionFlow{    private reffererUrl : string;    constructor(@Inject('isBrowser') private isBrowser){        if(isBrowser){            this.reffererUrl = document.referrer;        }    }}

Universal автоматически добавляет false, если это сервер, и true, если браузер. Может быть, позже разработчики Universal пересмотрят эту реализацию и нам не придется беспокоиться об этом.


Если вы хотите активно взаимодействовать с элементами DOM, используйте сервисы Angular API, такие какElementRef, Renderer или ViewContainer.


Правильный роутинг


Поскольку сервер отражает наше приложение, у нас была проблема с роутингом.


Ваш роутинг на клиенте, написанный на Angular, должен соответствовать роутингу на сервере.


Поэтому не забудьте добавить тот же роутинг на сервере что и на клиенте. Например, у нас есть такие роуты на клиенте:


    [      { path: '', redirectTo: '/home', pathMatch: 'full' },      { path: 'products', component: ProductsComponent },      { path: 'product/:id', component: ProductComponent}    ]

Тогда нужно создать файл server.routes.ts с массивом роутов сервера. Корневой маршрут можно не добавлять:


export const routes: string[] = [  'products',  'product/:id'];

Наконец, добавьте роуты на сервер:


import { routes } from './server.routes';... other server configurationapp.get('/', ngApp);routes.forEach(route => {  app.get(`/${route}`, ngApp);  app.get(`/${route}/*`, ngApp);});

Пререндеринг стартовой страницы


Одной из наиболее важных особенностей Angular Universal является пререндеринг. Из исследования Kissmetrics 47% потребителей ожидают, что веб-страница загрузится за 2 секунды или даже менее. Для нас было очень важно отобразить страницу как можно быстрее. Таким образом, пререндеринг в Universal как раз про нашу задачу. Давайте подробнее рассмотрим, что это такое и как его использовать?


Когда пользователь открывает URL нашего магазина, Universal немедленно возвращает предварительно подготовленную HTML страничку с контентом, а уже затем затем начинает загружать все приложение в фоновом режиме. Как только приложение полностью загрузится, Universal подменяет изначальную страницу нашим приложением. Вы спросите, что будет, если пользователь начнет взаимодействовать со страницей до загрузки приложения? Не беспокойтесь, библиотека Preboot.js запишет все события, которые выполнит пользователь и после загрузки приложения выполнит их уже в приложении.


Чтобы включить пререндеринг, просто добавьте в конфигурацию сервера preboot: true:


res.render('index', {      req,      res,      preboot: true,      baseUrl: '/',      requestUrl: req.originalUrl,      originUrl: `http://localhost:${ app.get('port') }`    });  });

Добавление мета-тегов


В случае SEO-ориентированного сайта добавление мета-тегов очень важно. Например, у нас есть Product component, который представляет страницу определенного продукта. Он должен асинхронно получать данные о продуктах из базы данных и на основе этих данных добавлять мета-теги и другую специальную информацию для поискового робота.


Команда Angular Universal создала сервис angular2-meta, чтобы легко манипулировать мета-тегами. Вставьте мета-сервис в ваш компонент и несколько строк кода добавлят мета-теги в вашу страницу:


import { Meta, MetaDefinition } from './../../angular2-meta';@Component({  selector: 'main-page',  templateUrl: './main-page.component.html',  styleUrls: ['./main-page.component.scss']})export class MainPageComponent {  constructor(private metaService: Meta){    const name: MetaDefinition = {      name: 'application-name',      content: 'application-content'    };    metaService.addTags(name);  }}

В следующей версии Angular этот сервис будет перемещен в @angular/platform-server


Кэширование данных


Angular Universal запускает ваш XHR запрос дважды: один на сервере, а другой при загрузке приложения магазина.


Но зачем нам нужно запрашивать данные на сервере дважды? PatricJs создал пример, как сделать Http-запрос на сервере один раз и закэшировать полученные данные для клиента. Посмотреть исходный код примера можно здесь. Чтобы использовать его заинжекте Model service и вызовите метод get для выполнения http-вызовов с кешированием:


    public data;    constructor(public model: ModelService) {        this.universalInit();    }    universalInit() {        this.model.get('/data.json').subscribe(data => {        this.data = data;        });    }

Выводы


Рендеринг на стороне сервера с помощью Angular Universal позволяет нам
создавать клиентоориентированные приложения электронной коммерции более не переживая об индексации вашего приложения. Кроме того, функция prendering позволяет сразу показать сайт для вашего клиента, улучшая время рендеринга (что довольно неприятный момент для Angular приложений из-за большого размера самой библиотеки).

Подробнее..

Из песочницы Создание микросервисной архитектуры с использованием single-spa (миграция существующего проекта)

28.10.2020 18:06:09 | Автор: admin
image

Это первая статья по в данной теме, всего их планируется 3:

  1. * Создание root application из вашего существующего проекта, добавление в него 3 микро-приложения (vue, react, angular)
  2. Общение между микро-приложениями
  3. Работа с git (deploy, обновления)

Оглавление


  1. Общая часть
  2. Зачем это нужно
  3. Создание root контейнера (определение см. ниже) из вашего монолита
  4. Создаеммикро-приложение VUE (vue-app)
  5. Создаем микро-приложение REACT (react-app)
  6. Создаем микро-приложение ANGULAR (angular-app)

1. Общая часть


Задача этой статьи: добавить возможность использовать существующий монолитный проект как root контейнер для микросервисной архитектуры.

Существующий проект выполнен на angular 9.

Для микросервисной архитектуры используем библиотеку single-spa.

В root проект необходимо добавить 3 проекта, используем разные технологии: vue-app, angular-app, react-app (см. п. 4, 5, 6).

Параллельно с созданием этой статьи, я стараюсь внедрить эту архитектуру в боевой проект, над которым в данный момент работаю. Следовательно буду стараться описывать все ошибки, которые у меня возникнут в процессе разработки и их решения.

Root application (далее root) корень (контейнер) нашего приложения. В него мы будем класть (регистрировать) все наши микросервисы. Если вы уже имеете какой либо проект и хотите реализовать в нем эту архитектуру, то root application будет именно ваш существующий проект, откуда со временем вы будете стараться выгрызать куски вашего приложения, создавать отдельные микросервисы и регистрировать его в этом контейнере.

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

К примеру мы решили переехать с angular на vue полностью, но проект жирный, и в данный момент приносит много денег бизнесу.

Без микросервисной архитектуры, в мыслях бы этого не могло появиться, только у отчаянных людей, которые верят в единорогов и что мы все голограмма.
Для того, чтобы перейти на новую технологию в реальности необходимо переписать весь проект, и только тогда мы смогли бы кайфануть от его появления на бою.

Другой вариант, это микросервисная архитектура. Вы можете создать root проект из своего монолита, добавить туда новый проект на том же vue, настроить роуминг в root, готово. Можно лить в бой, постепенно выпиливать с root проекта небольшие кусочки и переносить их в ваш vue микро-проект. В результате в вашем root контейнере останется только те файлы, которые необходимы для импорта вашего нового проекта.

Это можно сделать прям здесь и сейчас, без потерь, крови и главное реально.
В качестве root я буду использовать angular, так как существующий проект был написан именно на нем.

Общий интерфейс, в который будет заворачиваться single page application:

bootstrap(mounter, bus) вызывается после загрузки сервиса, скажет в какой элемент дома нужно монтироваться, даст ему шину сообщений на которую микросервис у себя подпишется и сможет слушай и посылать запросы и команду

mount() монтировать приложение из дома

unmount() демонтаж приложения

unload() выгрузка приложения

В коде я локально по месту использования буду еще раз в описывать работу каждого метода.

2. Зачем это нужно


Начнем в этом пункте строго по порядку.

Существует 2 типа архитектуры:

  1. Монолит
  2. Микросервисная архитектура

image

С монолитом все довольно просто и максимально всем нам знакомо. Сильная связанность, огромные блоки кода, общий репозиторий, куча методов.

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

Проблема появляется когда разрастается ваш проект, появляется много отдельного, сложного функционала разного назначения. Весь этот функционал начинает завязываться внутри проекта на какие то общие модели, состояния, утилиты, интерфейсы, методы и тп.

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

В придачу ко всему этому срабатывает закон Иглсона, который говорит, чтоВаш код, который вы не просматривали 6 или более месяцев, выглядит так, будто его написал кто-то другой.

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

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

Всегда ли так происходит с монолитом?

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

В первую очередь нам необходимо обратить внимание на параметры нашего проекта.

Попробую вынести пункты, по которым можно понять, так ли нам необходима микросервисная архитектура:

  • Над проектом работает 2 и более команды, количество фронтенд разработчиков 10+;
  • Ваш проект состоит из 2 и более бизнес модели, например у вас интернет магазин с огромным количеством товаров, фильтров, нотификации, и функционал курьерского распределения доставок (2 отдельные не маленькие бизнес модели, которые будут друг другу мешать). Это все может жить отдельно и не зависеть друг от друга.
  • Набор возможностей UI растёт ежедневно или еженедельно, не оказывая влияния на остальную часть системы.

Микрофронтенды применяются для того, чтобы:

  • Отдельные части фронтенда могли разрабатываться, тестироваться и развёртыватьсянезависимо;
  • Отдельные части фронтенда могли быть добавлены, удалены или заменены без повторной сборки;
  • Разные части фронтенда могли быть созданы с помощьюразныхтехнологий.
  • В идеале, связи между этими микро-приложениями не должно быть от слова совсем, поэтому этот пункт должен тоже влиять на принятии решения о выпиливании бизнес-кейсов (или кейсы для расширенного пользовательского опыта) в отдельный микро-сервис.
  • Скорость разработки должна быть постоянной, несмотря на рост приложения
  • Разные команды должны иметь возможность использовать собственные инструменты.

Какие бонусы мы еще можем получить от single-spa библиотеки?

  • Вы можете управлять большими общими зависимостями (например, библиотеками React, Vue или Angular) проще с помощью карты импорта, как вы увидите позже в этой должности.
  • Single-spa имеет ленивую загрузку включен для модулей в браузере, так что ваше приложение будет загружать модули только тогда, когда это необходимо.
  • Разделение переднего конца на несколько модулей в браузере позволяет разрабатывать и развертывать приложение независимо друг от друга.

Микросервис в моем понимании самостоятельный single page application, который будет решать только одну задачу пользователя. Это приложение так же не должно решать задачу команды целиком.

SystemJS это библиотека JS с открытым исходным кодом, которая обычно используется в качестве полифилла для браузеров.

Полифилл является частью кода JS, используемого для обеспечения современной функциональности для старых браузеров, которые не поддерживают его.

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

Например, можно использовать карту импорта для библиотеки React, которая загружается через CDN:

НО!

Если вы создаете проект с нуля, даже с учетом того, что вы определили все параметры вашего проекта, решили что у вас будет огромный Мега супер проект с командой 30+ человек, постойте!

Мне очень нравится мысль небезызвестного основоположника идеи микросервисов Мартин Фаулер.

Он предложил объединить монолитный подход и микросервисы в один (MonolithFirst). Основная его идея звучит так:
не следует начинать новый проект с микросервисов даже при полной уверенности, что будущее приложение будет достаточно большим, чтобы оправдать такой подход

Так же здесь опишу минусы использования такой архитектуры:

  • Взаимодействие между фрагментами невозможно обеспечить стандартными ламповыми методами (DI, например).
  • Как быть с общими зависимостями? Ведь размер приложения будет расти как на дрожжах, если их не выносить из фрагментов.
  • За роутинг в конечном приложении все равно должен отвечать кто-то один.
  • Неясно, что делать с тем, что разные микросервисы могут находиться на разных доменах
  • Что делать, если один из фрагментов недоступен / не может отрисоваться.

3. Создание root контейнера


И так, хватит теории, пора начинать.

Заходим в консоль

ng add single-spa-angularnpm i systemjs@6.1.4,npm i -d @types/systemjs@6.1.0,npm import-map-overrides@1.8.0

В ts.config.app.json глобально импортируем декларации (типы)

//ts.config.app.json"compilerOptions": {"outDir": "./out-tsc/app","types": [(+)"systemjs"]},


Добавляем вapp-routing.module.ts все микроприложения, которые мы добавим в root

//app-routing.module.ts{path: 'vue-app',children: [{path: '**',loadChildren: ( ) => import('./spa-host/spa-host.module').then(m => m.SpaHostModule),data: { app: '@somename/vue-app' }}]},{path: 'angular-app',children: [{path: '**',loadChildren: ( ) => import('./spa-host/spa-host.module').then(m => m.SpaHostModule),data: { app: '@somename/angular-app' }}]},{path: 'react-app',children: [{path: '**',loadChildren: ( ) => import('./spa-host/spa-host.module').then(m => m.SpaHostModule),data: { app: '@somename/react-app' }}]},

Так же нужно добавить config

// extra-webpack.config.jsonmodule.exports = (angularWebpackConfig, options) => {return {...angularWebpackConfig,module: {...angularWebpackConfig.module,rules: [...angularWebpackConfig.module.rules,{parser: {system: false}}]}};}

Изменим файл package.json, добавим в него все необходимые для работы либы

// package.json"dependencies": {...,(+) "single-spa": "^5.4.2",(+) "single-spa-angular": "^4.2.0",(+) "import-map-overrides": "^1.8.0",(+) "systemjs": "^6.1.4",}"devDependencies": {...,(+)"@angular-builders/custom-webpack": "^9",(+)"@types/systemjs": "^6.1.0",}

Добавляем необходимые библиотеки в angular.json

// angular.json{  ...,"architect": {"build": {...,"scripts": [...,(+)"node_modules/systemjs/dist/system.min.js",(+)"node_modules/systemjs/dist/extras/amd.min.js",(+)"node_modules/systemjs/dist/extras/named-exports.min.js",(+)"node_modules/systemjs/dist/extras/named-register.min.js",(+)"node_modules/import-map-overrides/dist/import-map-overrides.js"]}}},

В корне проекта создаем папку single-spa. В него добавим 2 файла.

1. route-reuse-strategy.ts файл маршрутизации наших микросервисов.
Если дочернее приложение выполняет маршрутизацию внутри себя, это приложение интерпретирует это как изменение маршрута.

По умолчанию это приведет к уничтожению текущего компонента и замене его новым экземпляром того же компонента spa-host.

Эта стратегия повторного использования маршрута смотрит на routeData.app, чтобы определить, должен ли новый маршрут быть обрабатывается как тот же маршрут, что и предыдущий, гарантируя, что мы не перемонтируем дочернее приложение, когда указанное дочернее приложение маршруты внутри себя.

// route-reuse-strategy.tsimport { RouteReuseStrategy, ActivatedRouteSnapshot, DetachedRouteHandle } from '@angular/router';import { Injectable } from '@angular/core';@Injectable()export class MicroFrontendRouteReuseStrategy extends RouteReuseStrategy {shouldDetach(): boolean {// маршрут не сохраняетсяreturn false;}store(): void { }shouldAttach(): boolean {return false;}// время присоединения маршрутаretrieve(): DetachedRouteHandle {return null;}shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {return future.routeConfig === curr.routeConfig || (future.data.app && (future.data.app === curr.data.app));}}

2. Сервис single-spa.service.ts

В сервисе будет храниться метод монтирования (mount) и демонтирования (unmount) микро-фронтенд приложений.

mount функция жизненного цикла, которая будет вызываться всякий раз, когда зарегистрированное приложение не смонтировано, но его функция активности возвращает true. При вызове эта функция должна просмотреть URL-адрес, чтобы определить активный маршрут, а затем создать элементы DOM, события DOM и т.п.

unmount функция жизненного цикла, которая будет вызываться всякий раз, когда монтируется зарегистрированное приложение, но ее функция активности возвращает false. При вызове эта функция должна очищать все элементы DOM.

//single-spa.service.tsimport { Injectable } from '@angular/core';import { mountRootParcel, Parcel, ParcelConfig } from 'single-spa';import { Observable, from, of } from 'rxjs';import { catchError, tap } from 'rxjs/operators';@Injectable({providedIn: 'root',})export class SingleSpaService {private loadedParcels: {[appName: string]: Parcel;} = {};mount(appName: string, domElement: HTMLElement): Observable<unknown> {return from(System.import<ParcelConfig>(appName)).pipe(tap((app: ParcelConfig) => {this.loadedParcels[appName] = mountRootParcel(app, {domElement});}));}unmount(appName: string): Observable<unknown> {return from(this.loadedParcels[appName].unmount()).pipe(tap(( ) => delete this.loadedParcels[appName]));}}

Далее создаем директорию container/app/spa-host.

Этот модуль будет реализовывать регистрации и отображение наших микро-фронтенд приложений в root.

Добавим в модуль 3 файла.

1. Сам модуль spa-host.module.ts

//spa-host.module.tsimport { RouterModule, Routes } from '@angular/router';import { CommonModule } from '@angular/common';import { NgModule } from '@angular/core';import { SpaUnmountGuard } from './spa-unmount.guard';import { SpaHostComponent } from './spa-host.component';const routes: Routes = [{path: '',canDeactivate: [SpaUnmountGuard],component: SpaHostComponent,},];@NgModule({declarations: [SpaHostComponent],imports: [CommonModule, RouterModule.forChild(routes)]})export class SpaHostModule {}

2. Компонент spa-host.component.ts координирует монтаж и демонтаж микро-фронтенд приложений

// spa-host.component.ts import { Component, OnInit, ViewChild, ElementRef, OnDestroy, ChangeDetectionStrategy } from '@angular/core';import { ActivatedRoute } from '@angular/router';import { Observable } from 'rxjs';import {SingleSpaService} from '../../single-spa/single-spa.service';@Component({selector: 'app-spa-host',template: '<div #appContainer></div>',changeDetection: ChangeDetectionStrategy.OnPush})export class SpaHostComponent implements OnInit {@ViewChild('appContainer', { static: true })appContainerRef: ElementRef;appName: string;constructor(private singleSpaService: SingleSpaService, private route: ActivatedRoute) { }ngOnInit() {// тащим название подгружаемой картыthis.appName = this.route.snapshot.data.app;this.mount().subscribe();}// собираем наш подгруженный проект по выбранному роутуmount(): Observable<unknown> {return this.singleSpaService.mount(this.appName, this.appContainerRef.nativeElement);}// разбираемunmount(): Observable<unknown> {return this.singleSpaService.unmount(this.appName);}}

3. spa-unmount.guard.ts проверяет, если имяприложения вроуте другое,разбираемпредыдущий сервис, если тоже, просто переходим на него.

// spa-unmount.guard.tsimport { Injectable } from '@angular/core';import { CanDeactivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';import { Observable } from 'rxjs';import { map } from 'rxjs/operators';import { SpaHostComponent } from './spa-host.component';@Injectable({ providedIn: 'root' })export class SpaUnmountGuard implements CanDeactivate<SpaHostComponent> {canDeactivate(component: SpaHostComponent,currentRoute: ActivatedRouteSnapshot,currentState: RouterStateSnapshot,nextState: RouterStateSnapshot): boolean | Observable<boolean> {const currentApp = component.appName;const nextApp = this.extractAppDataFromRouteTree(nextState.root);if (currentApp === nextApp) {return true;}return component.unmount().pipe(map(_ => true));}private extractAppDataFromRouteTree(routeFragment: ActivatedRouteSnapshot): string {if (routeFragment.data && routeFragment.data.app) {return routeFragment.data.app;}if (!routeFragment.children.length) {return null;}return routeFragment.children.map(r => this.extractAppDataFromRouteTree(r)).find(r => r !== null);}}

Регистрируем все что добавили в в app.module

//app.module.tsproviders: [...,{(+)provide: RouteReuseStrategy,(+)useClass: MicroFrontendRouteReuseStrategy}]

Изменим main.js.

// main.tsimport { enableProdMode, NgZone } from '@angular/core';import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';import { start as singleSpaStart } from 'single-spa';import { getSingleSpaExtraProviders } from 'single-spa-angular';import { AppModule } from './app/app.module';import { PlatformLocation } from '@angular/common';if (environment.production) {enableProdMode();}singleSpaStart();// название проектаconst appId = 'container-app';// Так как наше приложение использует маршрутизацию, мне необходимо импортировать функцию getSingleSpaExtraProviders.platformBrowserDynamic(getSingleSpaExtraProviders()).bootstrapModule(AppModule).then(module => {NgZone.isInAngularZone = () => {// @ts-ignorereturn window.Zone.current._properties[appId] === true;};const rootPlatformLocation = module.injector.get(PlatformLocation) as any;const rootZone = module.injector.get(NgZone);// tslint:disable-next-line:no-string-literalrootZone['_inner']._properties[appId] = true;rootPlatformLocation.setNgZone(rootZone);}).catch(err => {});

Далее создаем файл import-map.json в папке share. Файл нужен для добавления карт импорта.
В данный момент он будет у нас пустой и наполняться по мере добавления в root приложений.

<head><!doctype html><html lang="en"><head><meta charset="utf-8"><title>My first microfrontend root project</title><base href="http://personeltest.ru/aways/habr.com/">...(+)<meta name="importmap-type" content="systemjs-importmap" /><script type="systemjs-importmap" src="http://personeltest.ru/aways/habr.com/assets/import-map.json"></script></head><body><app-root></app-root><import-map-overrides-full></import-map-overrides-full><noscript>Please enable JavaScript to continue using this application.</noscript></body></html>

4. Создаем микро-приложение VUE (vue-app)


Теперь, когда мы добавили в свой монолитный проект возможность стать root приложением, пора создать свое первое внешнее микро-приложение с single-spa.

Во-первых, нам нужно установить глобально create-single-spa, интерфейс командной строки, который поможет нам создавать новые проекты single-spa с помощью простых команд.

Заходим в консоль

npm install --global create-single-spa

Создаем простое приложение vue с помощью команды в консоле

create-single-spa

Интерфей командной строки предложит выбрать директорию, название проекта, организации и тип приложения для создания

image

? Directory for new project vue-app ? Select type to generate single-spa application / parcel ? Which framework do you want to use? vue ? Which package manager do you want to use? npm ? Organization name (use lowercase and dashes) somename 

Запускаем наше микро-приложение

npm i npmrun serve--port 8000

Когда мы введем путь в браузере localhost:8080/, в случае с vue мы увидим пустой экран. Что же произошло?
Так как в созданном микро-приложении нет файла index.js.

Single-spa предоставляет игровую площадку, с которой можно загружать приложение через интернет, поэтому давайте сначала воспользуемся ей.

Добавим в index.js
single-spa-playground.org/playground/instant-test?name=@some-name/vue-app&url=8000
При создании root приложения, мы заранее добавили карту для загрузки нашего vue проекта.

{"imports": { ... , "vue": "https://unpkg.com/vue",  "vue-router": "https://cdn.jsdelivr.net/npm/vue-router@3.0.7/dist/vue-router.min.js", "@somename/vue-app": "//localhost:8080/js/app.js"}}

Готова! Теперь с нашего angular root проекта мы можем загружать микро-приложения, написанное на vue.

5. Создаем микро-приложение REACT (react-app)


Создаем так же простое приложение react с помощью команды в консоле

create-single-spa

Название организации:somename

Название проекта:react-app

? Directory for new project react-app ? Select type to generate single-spa application / parcel ? Which framework do you want to use? react ? Which package manager do you want to use? npm ? Organization name (use lowercase and dashes) somename 

Проверим, добавили ли мы карту импорта в нашем root приложении

{"imports": { ... ,    "react": "https://cdn.jsdelivr.net/npm/react@16.13.1/umd/react.development.js",    "react-dom": "https://cdn.jsdelivr.net/npm/react-dom@16.13.1/umd/react-dom.development.js",    "@somename/react-app": "//localhost:8081/somename-projname.js",}}

Готово! Теперь по нашем роуту react-app у нас загружается react микро-проект.

6. Создаем микро-приложение ANGULAR (angular-app)


Angular микро-приложение создаем абсолютно так же, как и 2 предыдущих

create-single-spa

Название организации:somename

Название проекта:angular-app

? Directory for new project angular-app ? Select type to generate single-spa application / parcel ? Which framework do you want to use? angular ? Which package manager do you want to use? npm ? Organization name (use lowercase and dashes) somename 

Проверим, добавили ли мы карту импорта в нашем root приложении

{    "imports": {     ... ,    "@somename/angular-app": "//localhost:8082/main.js",     }}

Запускаем, проверяем, все должно работать.

Это мой первый пост на Хабре, буду очень благодарен за ваши комментарии.
Подробнее..

Почему мы должны выбросить React и взяться за Angular

15.06.2021 14:10:21 | Автор: admin

Хочу представить перевод довольно интересной статьи Сэма Редмонда, Why We Should Throw Out React and Pick Up Angular. На мой взгляд, статья описывает основные возможности Angular. Она может показаться довольно вызывающей, но постарайтесь отнестись к ней немного с юмором :)

Итак, я уже немного написал о том, почему, как мне кажется, мы должны перестать использовать React. Чтобы подвести итог этой статьи, я перечислю несколько проблем, с которыми я столкнулся в React:

  1. Он популярен в основном от того, что вокруг него много шумихи.

  2. Он даёт слишком много свободы. Это приводит к фундаментальным ошибкам на ранней стадии разработки приложения, которые проявляются не сразу.

  3. Использует не оправдано много памяти и не поддаётся оптимизации(not tree-shakable).

  4. Сложность React приложения растёт экспоненциально с ростом размера приложения и это затрудняет его обслуживание.

  5. Нет ничего встроенного (например, обработка форм). По-этому вам нужно написать много кода, что бы это как-то компенсировать или использовать кучу сторонних библиотек.

  6. Обновление вашего приложения до последней версии React часто сопряжено с полным переписыванием этого самого приложения.

Это самые частые проблемы, с которыми я столкнулся, работая с различными проектами на React. В этой статье я хочу поделиться с вами, как Angular решает большую часть вышеперечисленных проблем и позволяет вам сосредоточиться на том, что вы создаёте, а не на том, как вы создаёте своё приложение.

All aboard the hype train

Angular также получает изрядное количество хайпа, так что я не могу сказать, что Angular решает эту проблему. Однако, я не думаю, что Angular получает такое же количество хайпа, как React. Мне кажется, что в основном это связано с ребрендингом, который сделал Google.. Сначала был AngularJs, который был как дымящаяся куча мусора. Но надо отдать должное Google, они решили полностью отремонтировать AngularJs и превратить его в Angular (или Angular 2), что является гигантским улучшением. Впрочем, это улучшение стоило ему популярности, как мне кажется.

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

Я убеждён, что Angular вполне заслуживает того хайпа, который он получает. Разработчики Angular создали невероятно хорошо продуманный фрэймворк, который обладает отличной производительностью и довольно прост в использовании. В этой статье я расскажу о преимуществах от использования Angular и, вероятно, после прочтения вы согласитесь с тем, что Angular заслужил свой хайп.

Вы получаете то, что вам нужно

Одна из больших разниц между Angular и React в простом факте, что Angular является полноценным фреймворком в противовес обычной библиотеке. Как я уже показал в предыдущей статье, React пытается совместить лучшее из библиотеки и фреймворка, но делает это плохо. В отличие от этого, Angular не пытается быть библиотекой. Он идёт ва-банк, чтобы быть фреймворком и делает это хорошо.

С базовой установкой вы получаете почти всё, что вам нужно, для того, чтобы создать любое приложение без необходимости устанавливать сторонние библиотеки (я уже знаю, что вы подумали, но об это позже). С Angular вы получаете надёжный фрэймворк, который позволяет, а не сдерживает вас. Вы больше не должны тратить уйму времени, чтобы писать кучу сложного кода, чтобы обработать и провалидировать форму, потому что это всё обрабатывается внутри фреймворка.

Кроме того вы получаете такую штуку, как Angular CLI, который один из самых мощных инструментов на поясе у Angular.

разработчики, использующие Angular CLIразработчики, использующие Angular CLI

Одна из самых больших проблем с React - это отсутствие стандартов. Если вы изучили одно React приложение, то вы изучили одно React приложение, потому что все они совершенно разные. С другой стороны, если вы изучили одно Angular приложение, то вы изучили все Angular приложения и Angular CLI является основным драйвером, стоящим за этим.

В отличие от React в Angular есть правильные и неправильные способы. Использование Angular CLI обычно всегда гарантирует, что всё будете делать правильно. Давайте возьмём самое начало. Мы хотим создать новое приложение. Как мы это сделаем?

ng new my-app

Да, вот, пожалуй и всё. Запустите эту команду и CLI настроит кучу вещей за вас. Он даже даст вам некоторый выбор, такой как использование линтинга и роутинга, перед тем, как будет создано приложение. Итак, вот что сделает CLI:

  1. Он создаст новую директорию my-app и проинициализирует в ней Angular приложение. А также установит зависимости.

  2. Настраивает инфраструктуру и интегрирует в неё приложение, снабдив вас всем, что нужно для запуска.

  3. Вместе с инфраструктурой вы также получаете встроенную базовую реализацию end-to-end тестов на Protractor, в которую вы можете добавлять тесты, по мере развития вашего приложения.

  4. Angular даёт вам простой в использовании конфигурационный файл (angular.json). В нём вы можете легко настроить, как Angular собирает приложение и даже то, как эта сборка делается средой окружения.

  5. Говоря о среде окружения, Angular имеет в своём составе простую и хорошо типизированную систему управления этой самой средой.

Ещё много есть того, чего я возможн коснусь, но и это уже очень круто, да ещё и прямо из коробки. В React вы такого не получите. Что ещё даёт вам CLI?

Кроме создания самого приложения и каркаса к нему, он будет также управлять приложением за вас. Скажем, у вас уже есть новенькое приложение и вы хотите создать компонент.

ng generate component my-component OR ng g c my-component

Существует два способа как создать компонент и они довольно долгие, по сравнению с короткой командой, что довольно приятно. Эта команда делает следующее:

  1. Создаёт директорию с именем my-component и помещает в него пустой компонент.

  2. Автоматически генерирует unit tests для данного компонента.

  3. Автоматически встраивает ваш компонент в инфраструктуру приложения.

Путь Angular

Я упомянул ранее о правильных и неправильных способах делать разные вещи, а также я упомянул о том, что происходит вокруг создания инфраструктуры. Но как это выглядит на самом деле? Я не буду приводить подробного руководства, а лишь кратко пройдусь по главным концептам. Некоторые примеры вы сможете найти на stackblitz:

https://stackblitz.com/edit/angular-examples-modules

ng g m buttonng g c button

В этом примере у нас есть папка button. В этой папке есть модуль button, компонент button, тестовый файл, файл стилей и HTML файл.

Это было автоматически сгенерировано Angular CLI. Если вы заметили, команды имеют определённый порядок и на это есть причина. Во-первых нам нужен модуль, чтобы включить в него наш компонент button. Затем мы создаём компонент button и CLI автоматически импортирует его в модуль. Затем мы экспортируем наш компонент для того, чтобы мы могли использовать его в других модулях нашего приложения.

Чтобы всё было просто, давайте импортируем это в app.module.ts. Всё, что мы сделаем, это импортируем наш компонент ButtonModule в app.module.ts и потом включим его в раздел imports декоратора @NgModule приложения AppModule.

Вот и всё. Теперь мы можем использовать тэг <app-button></app-button> в app.component.html файле.

Это самый простой пример реализации модулей в Angular и его можно реализовать другими способами. Но базовая реализация, которую мы здесь увидели, в основном остаётся такой же, независимо от того, как вы её используете. Поэтому, даже если ваш случай отличается от приведённого примера, это не усложнит вам жизнь.

Доставьте меня из пункта А в пункт Б

Я хотел бы коснуться ещё одной фичи, которую предоставляет вам Angular из коробки. Это роутинг и ленивая загрузка модулей. Я не буду здесь сильно углубляться и приведу небольшой пример. Если вы хотите подробнее разобрать эти фичи, попробуйте это сделать своими руками в Angular.

Как хороший CLI, Angular имеет опции. Одна из них задействует роутинг во время создания инфраструктуры приложения.

ng new my-app --routing

На сегодняшний день вы можете просто выбрать опцию во время установки, без указания флага. Но флаг тоже можно указывать, если вы так больше предпочитаете. Когда вы выбираете routing, Angular создаёт файл роутинга за вас. Он будет служить вам в качестве основного конфигурационного файл. Если вы захотите создать модули для ваших страниц, вы также можете указать флаг routing. CLI тогда создаст конфиг роутинга для этого модуля.

В созданном конфиге роутинга вы сможете легко настроить ленивую загрузку модуля:

const routes: Routes = [   {   path: 'main',   loadChildren: () =>    import('src/app/routes/main/main.module').then((mod) => mod.MainModule)   },];

Если вы прописываете ленивую загрузку, то вы должны использовать синтаксис асинхронного импорта модуля, который хотите загрузить. Это означает, что когда приложение загружается, клиент не должен загружать код для модуля main, каких либо зависимостей или чего-то, что модуль main использует до тех пор, пока пользователю не понадобиться загрузить это. Это сделает начальную загрузку приложения более лёгкой и быстрой и гарантирует, что пользователь не будет ждать, пока не загрузится всё приложение целиком. Самое замечательное, что эта возможность предоставляется из коробки и всё, что вам нужно, это использовать маленький флаг при создании вашего приложения и модулей.

Переиспользование делает вашу жизнь проще

Итак, мы рассмотрели, как Angular использует Modules для организации страниц и компонент и хочу остановиться на этом немного подробнее. Вероятно, что вы уже знаете, что Angular использует TypeScript вместо обычного javascript. Это явное преимущество Angular использует в полной мере.

Полагаю, что вы уже говорите - React тоже использует TypeScript, и будете правы. Но React использует Typescript не в полной мере. Типизация - это только часть того, что делает TypeScript таким продвинутым. Другая часть состоит в том, что вы можете в полной мере использовать классы и декораторы. React дал ясно понять, что движется в сторону функциональных компонент, а не в сторону компонент основанных на классах, что отнимает половину всей мощи TypeScript. Теперь стало намного сложнее переиспользовать функциональность в моём приложении и оно стало более сложным в обслуживании.

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

@my-decorator()export class MyClass {}

Это сделает мой код более читаемым и удобным в обслуживании даже при добавлении большей функциональности к тому, что я создаю.

Прежде чем я начну, мне хотелось бы поблагодарить Виктора Савкина за его превосходную статью, из которой я многое почерпнул об Angular Ivy.

Understanding Angular Ivy: Incremental DOM and Virtual DOM

В предыдущей статье я немного рассказывал о том, что виртуальный DOM это своего рода пожиратель памяти и что невозможно оптимизировать расход этой памяти (tree shaking), поскольку виртуальное дерево создаётся всякий раз заново при перерисовке. Всё изменилось с приходом Angular Ivy. Вы сможете прочитать как работает Ivy в статье Виктора. Я лишь приведу некоторые моменты из неё.

Angular Ivy использует так называемый инкрементный DOM. Идея заключается в том, что каждый компонент ссылается на набор инструкций, которые известны на стадии компиляции. И если некоторые инструкции не используются, то они исключаются из сборки.

В отличие от виртуального DOM, в инкрементном DOM память расходуется при перерисовке дерева DOM, если только в нем произошли изменения (то есть были удалены или добавлены элементы). Кроме того, выделение памяти пропорционально размеру изменений. Это здорово повышает эффективность в рантайме, особенно для мобильных устройств.

Помните, как я ранее сказал: Я уже знаю, что вы думаете и мы к этому вернёмся? Давайте разберём это. В том месте статьи я уверен, что вы подумали про себя: это всё прелестно, но что если мне не понадобиться ВСЁ, что Angular предоставляет? Хорошо, ну вы сами вдумайтесь, если вы не используете какую-то часть Angular, она просто не попадёт в сборку! Их технология оптимизации сборки постоянно улучшается и вы получите более стройные билды, в особенности если вы не используете абсолютно всё, что есть в Angular.

Апгрейд - проще простого

Если вы читаете это, то вероятно вы, вы уже проработали каким-то разработчиком ПО. Полагаю, что не нужно объяснять какую боль может причинить переход на новую версию библиотеки или фреймворка. Апгрейды - это неотъемлемая часть жизни и вместе с Angular, они больше не будут болью.

Я долго работал с Angular и видел все апдейты с первого релиза (я даже работал с AngularJs, но предпочитаю не говорить об этом). Безусловно Angular прошёл долгий путь, как и CLI. Где в 2018 году в Angular CLI появилась ещё одна команда - ng update. Вы можете использовать её так:

ng update @angular/core

Дальше происходит магия. Все зависимости ядра Angular обновятся до последней версии. Если ваш код нуждается в обновлении, CLI сделает это за вас и, если нельзя, то скажет, где вам нужно самим вручную обновить свой код. Обновление до последней версии Angular займёт от нескольких секунд до нескольких минут, в то время, как с React это может занять от нескольких часов до нескольких дней (или даже недель).

Это отчасти потому, что Angular CLI такой мощный, а отчасти, что команда разработчиков Angular так выкладывает обновления. Они заботятся не о том, чтобы в релиз влезло как можно больше критических изменений, которые заставят разработчиков полностью переписать свои приложения. Они всегда следят за тем, чтобы критические изменения были обратно совместимыми для одной или двух предыдущих версий, чтобы дать возможность разработчикам внести большие изменения, если необходимо. Я пользуюсь этим постоянно и это мощное средство для обслуживания Angular

Давайте свяжем всё вместе

Хорошо, в этой статье мы о многом поговорили, и мне кажется, что я бы хотел ещё многое сказать. Тем не менее, мне кажется я коснулся всех болевых точек, которые я упоминал в начале статьи.

Используя возможности Angular и Angular CLI, вы сможете сделать ваши приложения более совместимыми. В то время как некоторые приложения могут отличаться настройками, принципы остаются одинаковыми, по-этому порог вхождения в новый проект будет не таким уж высоким. Один раз выучи - применяй везде!

Хорошая совместимость - уменьшение сложности обслуживания. Когда ваш проект растёт, стоимость обслуживания не будет возрастать экспоненциально. Наверное это самая большая проблема в React приложениях, которую превосходно решает Angular. Следуя тому же ходу мысли, мы также получаем всё, что нам нужно прямо из ядра Angular. Обработка форм? Пожалуйста. Роутинг? Пожалуйста. Ленивая загрузка? Пожалуйста. Я мог бы продолжить, но остановлюсь на этом. Даже если вы что-то не используете, то это не войдет в ваш билд, потому что всё, что в ядре Angualr является оптимизируемым деревом (tree shakable), включая рендеринг.

Чтобы завершить статью, скажу последнее. Используйте Angular. Смотрите видео на YouTube. Читайте документацию или учитесь, как у вас получается лучше всего. А затем смело бросайте React в мусор, потому что он вам больше не понадобится.

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

Подробнее..
Категории: Typescript , React , Reactjs , Angular , Angular2 , Upgrade

Перевод Отслеживание состояния компонентов в Angular c помощью ng-set-state

19.04.2021 18:14:05 | Автор: admin

В предыдущей статье (Angular Components with Extracted Immutable State) я показал, почему изменение полей компонентов без каких-либо ограничений - это не всегда хорошо, а также представил библиотеку, которая позволяет упорядочить изменения состояния компонентов.

С тех пор я немного изменил её концепцию и упростил использование. На этот раз я сосредоточусь на простом (на первый взгляд) примере того, как eё можно использовать в сценариях, где обычно потребовался бы rxJS.

Основная Идея

Есть неизменяемый объект, который представляет все значения полей некоторого компонента:

Каждый раз, когда какое-либо поле (или несколько значений полей) компонента изменяется, то создается новый неизменяемый объект, содержащий комбинацию старых и новых значений:

Сравнивая эти два объекта, можно определить какие именно поля изменились, и если есть логические зависимости от этих полей, то тогда выполнится соответствующее вычисление значений этих зависимостей. После того как это вычисление завершено, будет создан 3-й объект, содержащий как исходные значения 2-го объекта, так и вновь вычисленные:

Теперь у нас есть новый объект, который также можно сравнить с предыдущим, и при необходимости можно будет вычислить новые зависимости. Этот сценарий может быть повторен много раз до тех пор, пока мы не получим полностью согласованный объект, после чего Angular отобразит эти данные:

Простая Форма Приветствия

Давайте создадим простую форму приветствия (исходный код на stackblitz):

simple-greeting-form.component.ts

@Component({  selector: 'app-simple-greeting-form',  templateUrl: './simple-greeting-form.component.html'})export class SimpleGreetingFormComponent {  userName: string;  greeting:  string;}

simple-greeting-form.component.html

<div class="form-root">    <h1>Greeting Form</h1>  <label for="ni">Name</label><br />  <input [(ngModel)]="userName" id="ni" />  <h1>{{greeting}}</h1></div>

Очевидно, что поле greeting зависит от поля userName, и существует несколько способов выразить эту зависимость:

  1. Преобразовать greeting в свойство с геттером, но в этом случае его значение будет вычисляться в каждом цикле обнаружения изменений (change detection);

  2. Преобразовать userName в свойство с сеттером, который обновит значение поле greeting;

  3. Создать обработчик событийя ngModelChange, но это избыточно усложнит код;

Эти способы будут работать, но если какое-то другое поле зависит от приветствия (greeting, greeting counter) или greeting зависит от нескольких полей (например, greeting = f (userName, template)), то ни один из этих методов не поможет, поэтому предлагается другой подход:

@Component(...)@StateTracking()export class SimpleGreetingFormComponent {  userName: string;  greeting:  string;  @With("userName")  public static greet(state: ComponentState<SimpleGreetingFormComponent>)    : ComponentStateDiff<SimpleGreetingFormComponent>  {    const userName = state.userName === ""       ? "'Anonymous'"       : state.userName;    return {      greeting: `Hello, ${userName}!`    }  }}

Для начала компонент должен быть отмечен декоратором @StateTracking или же в конструкторе должна быть вызвана функция initializeStateTracking (декораторы компонентов иногда работают некорректно в некоторых старых версиях Angular):

@Component(...)export class SimpleGreetingFormComponent {  userName: string;  greeting:  string;    constructor(){    initializeStateTracking(this);  }}

Декоратор @StateTracking (или функция initializeStateTracking) находит все те поля компонента, от которых могут зависеть другие поля, и заменяет их свойствами с геттерами и сеттерами, так чтобы библиотека могла отслеживать изменения.

Далее определяем функцию перехода в новое состояние:

  ...  @With("userName")  public static greet(state: ComponentState<SimpleGreetingFormComponent>)    : ComponentStateDiff<SimpleGreetingFormComponent>  {      ...  }  ...

Каждая функция перехода получает в виде аргумента объект, который представляет текущее состояние компонента, и эта функция должна вернуть объект, который будет содержать только обновленные поля. Это объект будет объединен с копией объекта текущего состояния, и эта новая модифицированная копия станет новым состоянием компонента.

При желании вы можете добавить второй аргумент, который получит объект предыдущего состояния.

Если же вы определите третий параметр, то он получит объект разницы между текущим и предыдущим состояниями:

@With("userName")public static greet(  state: ComponentState<SimpleGreetingFormComponent>,  previous: ComponentState<SimpleGreetingFormComponent>,  diff: ComponentStateDiff<SimpleGreetingFormComponent>): ComponentStateDiff<SimpleGreetingFormComponent>{  ...}

ComponentState и ComponentStateDiff это прокси типы (Typescript mapped types), которые отфильтровывают методы и источники событий (event emitters). Также ComponentState отмечает все поля как только для чтения (ведь состояние неизменяемо (immutable)), а ComponentStateDiff отмечает все поля как необязательные, поскольку функция перехода может возвращать любое подмножество исходного состояния.

Для простоты определим алиасы этих типов:

type State = ComponentState<SimpleGreetingFormComponent>;type NewState = ComponentStateDiff<SimpleGreetingFormComponent>;...  @With("userName")  public static greet(state: State): NewState  {    ...  }

Декоратор @With получает список имен полей, изменение значений которых вызовет соответствующий декорированный статический (!) метод класса компонента. Typescript проверит, что класс на самом деле содержит объявленные поля и что метод является статическим (функции переходов должны быть чистыми (pure)).

Логирование изменений

Теперь форма отображает соответствующее приветствие при любом изменении имени. Посмотрим, как меняется состояние компонента:

@Component(...)@StateTracking<SimpleGreetingFormComponent>({  onStateApplied: (c,s,p)=> c.onStateApplied(s,p)})export class SimpleGreetingFormComponent {  userName: string;  greeting:  string;  private onStateApplied(current: State, previous: State){    console.log("Transition:")    console.log(`${JSON.stringify(previous)} =>`)    console.log(`${JSON.stringify(current)}`)  }  @With("userName")  public static greet(state: State): NewState  {      ...  }  }

onStateApplied это функция-перехватчик (hook), которая вызывается каждый раз, когда состояние компонента становится согласованным - это означает, что все функции перехода были вызваны и больше никаких изменений не обнаружено:

Transition:{} =>{"userName":"B","greeting":"Hello, B!"}Transition:{"userName":"B","greeting":"Hello, B!"} =>{"userName":"Bo","greeting":"Hello, Bo!"}Transition:{"userName":"Bo","greeting":"Hello, Bo!"} =>{"userName":"Bob","greeting":"Hello, Bob!"}

Как мы видим, компонент переходит в новое состояние каждый раз, когда пользователь вводит следующий символ имени, и поле приветствия при этом немедленно обновляется. Если необходимо предотвратить обновление приветствия при каждом изменении имени, то это можно легко сделать, добавив расширение Debounce к декоратору @With:

@With("userName").Debounce(3000/*ms*/)public static greet(state: State): NewState{    ...}...

Теперь библиотека ждет 3 секунды после последнего изменения в имени и только затем выполняет переход:

Transition:{} =>{"userName":"B"}Transition:{"userName":"B"} =>{"userName":"Bo"}Transition:{"userName":"Bo"} =>{"userName":"Bob"}Transition:{"userName":"Bob"} =>{"userName":"Bob","greeting":"Hello, Bob!"}

Добавим индикацию того, что форма находится в режиме ожидания:

...export class SimpleGreetingFormComponent {  userName: string;  greeting:  string;  isThinking:  boolean = false;  ...  @With("userName")  public static onNameChanged(state: State): NewState{    return{      isThinking: true    }  }  @With("userName").Debounce(3000/*ms*/)  public static greet(state: State): NewState  {    const userName = state.userName === ""       ? "'Anonymous'"       : state.userName;    return {      greeting: `Hello, ${userName}!`,      isThinking: false    }  }}
...<h1 *ngIf="!isThinking">{{greeting}}</h1><h1 *ngIf="isThinking">Thinking...</h1>...

Кажется, что это работает, но есть проблема - если пользователь начал печатать, а затем решил вернуть исходное имя в течение заданных 3 секунд, то с точки зрения библиотеки поле greeting не изменилось, и функция перехода не будет вызвана, а форма будет показывать Thinking до тех пор, пока вы не набираете другое имя. Это можно решить, добавив декоратор @Emitter() для поля userName:

@Emitter()userName: string;

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

Однако есть и другое решение - когда форма перестает "думать", она может установить для userName значение null, и тогда пользователю придется начать вводить новое имя:

...@With("userName")public static onNameChanged(state: State): NewState{  if(state.userName == null){    return null;  }  return{    isThinking: true  }}@With("userName").Debounce(3000/*ms*/)public static greet(state: State): NewState{  if(state.userName == null){    return null;  }    const userName = state.userName === ""     ? "'Anonymous'"     : state.userName;  return {    greeting: `Hello, ${userName}!`,    isThinking: false,    userName: null  }}...

А теперь давайте подумаем о ситуации, когда пользователь нетерпелив и хочет сразу получить результат. Что ж, позволим ему нажать [Enter] ((keydown.enter) = "onEnter ()"), чтобы немедленно получить приветствие:

...userName: string | null;immediateUserName: string | null;onEnter(){  this.immediateUserName = this.userName;}...@With("userName")public static onNameChanged(state: State): NewState{  ...}@With("userName").Debounce(3000/*ms*/)public static greet(state: State): NewState {  ...}@With("immediateUserName")public static onImmediateUserName(state: State): NewState{  if(state.immediateUserName == null){    return null;  }  const userName = state.immediateUserName === ""     ? "'Anonymous'"     : state.immediateUserName;  return {    greeting: `Hello, ${userName}!!!`,    isThinking: false,    userName: null,    immediateUserName: null  }}...

Ещё было бы неплохо узнать, сколько времени ждать, если пользователь не нажимает [Enter] - какой-нибудь счетчик обратного отсчёта был бы очень полезен:

<h1 *ngIf="isThinking">Thinking ({{countdown}} sec)...</h1>
...countdown: number = 0;...@With("userName")public static onNameChanged(state: State): NewState{  if(state.userName == null){    return null;  }  return{    isThinking: true,    countdown: 3  }}...@With("countdown").Debounce(1000/*ms*/)public static countdownTick(state: State): NewState{  if(state.countdown <= 0) {    return null  }  return {countdown: state.countdown-1};}

и вот как это выглядит:

Обратный отсчет также следует сбрасывать каждый раз, когда готово новое приветствие. Это предотвращает ситуацию, когда пользователь сразу нажимает [Enter], а обратный отсчет в этот момент остается 3 - после этого он перестает работать, так как его значение больше никогда не изменится. Для простоты сбросим все поля, зависящие от флага isThinking:

...@With("isThinking")static reset(state: State): NewState{  if(!state.isThinking){    return{      userName: null,      immediateUserName: null,      countdown: 0    };  }  return null;}...

Обнаружение Изменений (Change Detection)

Очевидно, что обратный отсчет работает асинхронно, и этот факт не вызывает никаких проблем с обнаружением изменений в Angular, пока стратегия обнаружения изменений - Default. Однако, если стратегия - OnPush, то ничто не может сообщить компоненту, что его состояние меняется во время обратного отсчета.

К счастью, мы уже определили функцию обратного вызова, которая вызывается каждый раз, когда состояние компонента только что изменилось, поэтому единственное, что требуется, - это добавить туда явное обнаружение изменений:

...constructor(readonly changeDetector: ChangeDetectorRef){}...private onStateApplied(current: State, previous: State){  this.changeDetector.detectChanges();  ...

Теперь он работает должным образом даже с OnPush стратегией обнаружения (Change Detection Strategy).

Исходящие Параметры (Output Properties)

Библиотека обнаруживает все источники событий (Event emitters) компонентов и вызывает их, когда поменялись значения привязанных к этим источникам полей. По умолчанию привязка выполняется с использованием суффикса Change в названиях источников событий:

greeting:  string;@Output()greetingChange = new EventEmitter<string>();

Распределенное Состояние

Обычно, когда компонент уничтожается (например, скрывается с помощью *ngIf), все ещё не завершенные асинхронные операции, инициированные этим компонентом, заканчиваются без каких либо значимых изменений. Однако библиотека позволяет выделить состояние компонента со всеми его переходами в отдельный объект, который может существовать независимо от компонента. Более того, такой объект может использоваться несколькими компонентами одновременно!

Давайте превратим компонент формы приветствия в сервис:

greeting-service.ts

@StateTracking({includeAllPredefinedFields:true})export class GreetingService implements IGreetingServiceForm {  userName: string | null = null;  immediateUserName: string | null = null;  greeting:  string = null;  isThinking:  boolean = false;  countdown: number = 0;  @With("userName")  static onNameChanged(state: State): NewState{    ...  }  @With("userName").Debounce(3000/*ms*/)  static greet(state: State): NewState  {    ...  }  @With("immediateUserName")  static onImmediateUserName(state: State): NewState{    ...  }  @With("countdown").Debounce(1000/*ms*/)  static countdownTick(state: State): NewState{    ...  }  @With("isThinking")  static reset(state: State): NewState{    ...  }}

и добавим его в провайдеры главного модуля.

includeAllPredefinedFields означает, что все поля с некоторым начальным значением (даже если оно null) будут автоматически включены в объекты состояния.

Чтобы использовать подобный сервис внутри компонента, нужно выполнить следующие действия:

  1. Внедрить экземпляр службы в компонент через dependency injection;

  2. Передать экземпляр сервиса в инициализатор трекера состояния;

  3. Отметить поля компонента, которые будут привязаны к соответствующим полям сервиса;

  4. Подписаться на изменения состояния сервиса - это необходимо, когда требуется явное обнаружение изменений или стратегия обнаружения изменений - OnPush.

После выполнения этих шагов компонент будет выглядеть так:

@Component({...  changeDetection: ChangeDetectionStrategy.OnPush})export class ComplexGreetingFormComponent   implements OnDestroy, IGreetingServiceForm {  private _subscription: ISharedStateChangeSubscription;  @BindToShared()  userName: string | null;  @BindToShared()  immediateUserName: string | null;  @BindToShared()  greeting:  string;  @BindToShared()  isThinking:  boolean = false;  @BindToShared()  countdown: number = 0;  constructor(greetingService: GreetingService, cd: ChangeDetectorRef) {    const handler = initializeStateTracking<ComplexGreetingFormComponent>(this,{      sharedStateTracker: greetingService,      onStateApplied: ()=>cd.detectChanges()    });    this._subscription = handler.subscribeSharedStateChange();  }  ngOnDestroy(){    this._subscription.unsubscribe();  }  public onEnter(){    this.immediateUserName = this.userName;  }}

Экземпляр службы передается в функцию initializeStateTracking (также возможно использование декоратора @StateTracking(), но это потребует больше усилий), которая возвращает объект с помощью методов которого можно управлять работой библиотеки в указанном компоненте.

Подписка (_subscription: ISharedStateChangeSubscription) требуется для вызова функции обратного вызова onStateApplied при изменении состояния сервиса или для вызова локальной функции перехода, которая зависит от поля (полей) сервиса. Если компонент использует стратегию обнаружения изменений Default и отсутствуют локальные функции перехода, то подписка не требуется.

Не забудьте отказаться от подписки при уничтожении компонента, дабы избежать утечек памяти. В качестве альтернативы вы можете вызвать функции handler.release() или releaseStateTracking(this), чтобы отказаться от подписок на компоненты, но эти методы также отменяют все незавершенные асинхронные операции, что не всегда желательно.

Составное Распределенное Состояние

Библиотека позволяет использовать общие состояния не только в компонентах, но и в других сервисах.

Давайте создадим сервис, который будет регистрировать все приветствия и имитировать их отправку на сервер:

export type LogItem = {  id: number | null  greeting: string,  status: LogItemState,}@Injectable()export class GreetingLogService implements IGreetingServiceLog, IGreetingServiceOutput {  @BindToShared()  greeting:  string;  log: LogItem[] = [];  logVersion: number = 0;  identity: number = 0;  pendingCount: number = 0;  savingCount: number = 0;  ...  constructor(greetingService: GreetingService){    const handler = initializeStateTracking(this,{      sharedStateTracker: greetingService,       includeAllPredefinedFields: true});          handler.subscribeSharedStateChange();      }  ...}

При каждом изменении значения поля greeting, это значение будет добавлено в массив журнала log. logVersion будет увеличиваться каждый раз при изменении массива, чтобы обеспечить обнаружение изменений, не делая при этом массив журнала неизменяемым:

...@With("greeting")static onNewGreeting(state: State): NewState{    state.log.push({id: null, greeting: state.greeting, status: "pending"});    return {logVersion: state.logVersion+1};}...

Сервис не сразу отправляет новые приветствия "на сервер", он будет ждать некоторое время, чтобы накопить сразу несколько изменений:

@With("logVersion")static checkStatus(state: State): NewState{  let pendingCount = state.pendingCount;  for(const item of state.log){    if(item.status === "pending"){      pendingCount++;    }    else if(item.status === "saving"){      savingCount++;    }  }  return {pendingCount, savingCount};}@With("pendingCount").Debounce(2000/*ms*/)static initSave(state: State): NewState{  if(state.pendingCount< 1){    return null;  }  for(const item of state.log){    if(item.status === "pending"){      item.status = "saving";    }  }  return {logVersion: state.logVersion+1};}

И наконец, собственно, сама отправка на сервер:

...  @WithAsync("savingCount").OnConcurrentLaunchPutAfter()  static async save(getState: ()=>State): Promise<NewState>{      const initialState = getState();      if(initialState.savingCount < 1){        return null;      }      const savingBatch = initialState.log.filter(i=>i.status === "saving");      await delayMs(2000);//Simulates sending data to server       const stateAfterSave = getState();      let identity = stateAfterSave.identity;      savingBatch.forEach(l=>{        l.status = "saved",        l.id = ++identity      });      return {        logVersion: stateAfterSave.logVersion+1,        identity: identity      };        }...

Эта функция перехода отличается от предыдущих тем, что она асинхронная:

  1. Она отмечена декоратором WithAsync вместо With;

  2. Декоратор имеет спецификацию поведения параллельного запуска (в данном случае OnConcurrentLaunchPutAfter);

  3. Вместо объекта текущего состояния он получает функцию, которая возвращает текущее состояние в момент вызова.

Таким же образом мы можем реализовать удаление и восстановление приветствий, но я пропущу эту часть, так как в ней нет ничего нового. В результате наша форма будет выглядеть так:


Мы только что рассмотрели пример пользовательского интерфейса с относительно сложным асинхронным поведением. Однако оказывается, что реализовать такое поведение не так уж и сложно, используя концепцию серии неизменяемых состояний. По крайней мере, это можно рассмотреть как альтернативу RxJs.


  1. Код статьи на stackblitz: https://stackblitz.com/edit/set-state-greet;

  2. Ссылка на пердыдущую статью: Angular Components with Extracted Immutable State;

  3. Ссылка не исходный код ng-set-state: https://github.com/0x1000000/ngSetState

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru