Angular

Часть 1, Часть 2, Часть 3

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

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

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

Но как мы будем визуализировать приложения внутри системного терминала? Думаю, самое простое  —  это найти подходящую библиотеку, способную создавать виджеты в терминале с помощью графики ASCII. Я решил использовать blessed  —  библиотеку с высокоуровневым API терминала для node.js. Первым делом давайте установим библиотеку следующей командой:

npm install blessed @types/blessed

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

Вот упрощённое описание визуализатора на Angular:

export abstract class Renderer2 {

  abstract createElement(name: string, namespace?: string|null): any;

  abstract createText(value: string): any;

  abstract appendChild(parent: any, newChild: any): void;

  abstract addClass(el: any, name: string): void;

  abstract removeClass(el: any, name: string): void;

  // ...
}

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

С другой стороны, у нас есть библиотека со следующим интерфейсом:

const blessed = require('blessed');
// Создать экран
const screen = blessed.screen();
// Создать элементы
const box = blessed.box();
const table = blessed.table();
// Добавить элементы на экран
table.append(box);
screen.append(table);
// Вывести на экран все изменения
screen.render();

Заметьте, что blessed – это обыкновенная библиотека node.js, которая предоставляет screen и ряд компонентов. Screen – это что-то вроде document браузера и служит корневым элементом приложения, а также содержит несколько полезных интерфейсов API.

Экран

Приступим к интеграции нашего визуализатора со screen. Прежде всего, создаём отдельный сервис Screen для визуализатора.

import { Injectable } from '@angular/core';
import * as blessed from 'blessed';
import { Widgets } from 'blessed';
@Injectable()
export class Screen {
  private screen: Widgets.Screen;
  constructor() {
    this.screen = blessed.screen({ smartCSR: true });
    this.setupExitListener();
  }
  constructor() {
    this.screen = blessed.screen({ smartCSR: true });
    this.setupExitListener();
  }
  private setupExitListener() {
    this.screen.key(['C-c'], () => process.exit(0));
  }
}

Здесь у нас базовая реализация. 

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

process.exit  —  это стандартный API node.js, с помощью которого скрипт может выйти сам, используя необходимый код. 0 означает, что процесс завершается без ошибок. 

Кроме того, Screen даёт возможность выбрать корневой элемент для нашего приложения с помощью вызова selectRootElement.

Реестр элементов

У нас есть Screen. Мы можем выбрать корневой элемент. Самое время заняться созданием элементов. 

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

Например, я решил поместить логику создания элементов blessed в специальный сервис ElementsRegistry, который отвечает за создание элементов blessed посредством единой функции createElement:

import { Injectable } from '@angular/core';
import * as blessed from 'blessed';
import { Widgets } from 'blessed';
export type ElementFactory = (any) => Widgets.BoxElement;
export const elementsFactory: Map<string, ElementFactory> = new Map()
  .set('text', blessed.text)
  .set('box', blessed.box)
  .set('table', blessed.table)
@Injectable()
export class ElementsRegistry {
  createElement(name: string, options: any = {}): Widgets.BoxElement {
    let elementFactory: ElementFactory = elementsFactory.get(name);
    if (!elementFactory) {
      elementFactory = elementsFactory.get('box');
    }
    return elementFactory({ ...options, screen: this.screen });
  }
}

Как видим, ElementsRegistry имеет единый метод createElement, который пытается найти требуемый элемент на карте элементов и вернуть экземпляр этого элемента. В случае если элемент не найден, ElementsRegistry возвращается к элементу box, который является аналогом элемента div в браузере.

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

Вот базовая реализация визуализатора:

export class TerminalRenderer implements Renderer2 {
  constructor(private screen: Screen, private elementsRegistry: ElementsRegistry) {
  }
  createElement(name: string, namespace?: string | null): any {
    return this.elementsRegistry.createElement(name);
  }
  createText(value: string): any {
    return this.elementsRegistry.createElement('text', { content: value });
  }
  selectRootElement(): Widgets.Screen {
    return this.screen.selectRootElement();
  }
  appendChild(parent: Widgets.BlessedElement, newChild: Widgets.BlessedElement): void {
    parent.append(newChild);
  }
  setAttribute(el: Widgets.BlessedElement, name: string, value: string, namespace?: string | null): void {
    el[name] = value;
  }
  setValue(node: Widgets.BlessedElement, value: string): void {
    node.setContent(value);
  }
}

Я решил реализовать здесь лишь малую часть всех требуемых методов, оставив остальные реализации вам. Итак, здесь у нас есть класс TerminalRenderer, который реализует интерфейс Renderer2. Он использует приведённые выше Screen и ElementsRegistry для создания элементов.

Обратите внимание, что здесь TerminalRenderer не является сервисом Injectable. Angular требует, чтобы Renderer создавался через RendererFactory. Давайте создадим его:

@Injectable()
export class TerminalRendererFactory implements RendererFactory2 {
  
  constructor(private screen: Screen, private elementsRegistry: ElementsRegistry)
  createRenderer(): Renderer2 {
    return new TerminalRenderer(this.screen, this.elementsRegistry);
  }
}

TerminalRendererFactory здесь реализует интерфейс RendererFactory2 и только один метод  —  createRenderer, который создаёт новый экземпляр с требующимися сервисами.

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

Читайте также:


Перевод статьи Nikita Poltoratsky: Angular Platforms in depth. Part 3. Rendering Angular applications in Terminal