Расширьте Angular Schematics, чтобы настроить процесс разработки

2447
Расширьте Angular Schematics, чтобы настроить процесс разработки
Расширьте Angular Schematics, чтобы настроить процесс разработки

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

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

Уверяю вас, в этом нет ничего плохого — это не будет статья о DRY и о том, как удалить дубликаты в вашем проекте с помощью ООП-трюков. Иногда создание сложной, вложенной архитектуры только для того, чтобы удалить некоторые дублирующиеся элементы, недостаточно или не является лучшим, что вы можете сделать — простота и читабельность ваших классов так же важны. Это сценарий, в котором Угловые схемы становятся невероятно полезными.

Схемы

Я думаю, что каждый разработчик Angular использует Угловые схемы, по крайней мере, при создании компонентов или сервисов, но если вы не знакомы с этим ценным инструментом — краткое объяснение:

Angular Schematics предлагает десятки команд, которые вы можете использовать для создания файлов, запуска миграции, обновления файлов и т. Д. Например, чтобы создать каталог компонентов вместе с шаблоном, таблицей стилей и файлом класса компонентов, вы можете просто запустить ng generate component my-component в терминале или использовать короткую версию ng g c my-component. Schematics позаботится обо всем и создаст три файла с примером кода. Очень полезно, правда?

Почему это должно меня интересовать?

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

  • большинство компонентов используют хранилище GNRH для отправки действий или сбора данных для отображения их в шаблоне. Поэтому разработчики создают компоненты с помощью команды ng g c и вручную вводят класс Store в конструктор компонентов.
  • почти каждый компонент полагается на Наблюдаемые объекты, и поэтому ему необходимо управлять подписками, созданными внутри него. Как обычно это делают разработчики? Они должны сделать частное поле, которое соберет все подписки вместе, добавить еще один интерфейс, который реализует класс — onDestroy, создать метод ngOnDestroy и, наконец, закрыть все подписки в методе ngOnDestroy.
  • службы используются для связи REST с API, поэтому они должны иметь введенный HttpClient. Я думаю, вы уже знаете, что разработчики должны делать после каждого создания сервиса.

Не кажется ли вам, что это пустая трата времени разработчика, возможно, введение ошибок и несоответствий между классами? Представьте себе ситуацию, когда один разработчик называет объект HttpClient как http, а другой разработчик больше любит HttpClient ( проект в конечном итоге будет иметь по крайней мере два слова для одной и той же вещи в решении), не очень читабельный.

Должен ли я создавать все сам?

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

Когда я думал о внедрении пользовательских схем, меня больше всего беспокоил вопрос: а что, если я не хочу создавать свои схемы с нуля? С угловатыми все в порядке. Мне просто нужно немного расширить его — добавить в них какой-нибудь пользовательский код. Точно так же, как в примерах, которые я представил вам выше. Речь идет не о создании чего-то нового, а о расширении конфигурации по умолчанию, которую дает нам Угловая схема.

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

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

Что это за план?

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

Давайте погрузимся в код!

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

Создание библиотеки

Начнем с создания библиотеки схем. Во-первых, нам нужно установить schematics-cli:

npm install -g @angular-devkit/schematics-cliv

Используя schematics-cli, мы теперь готовы создать проект schematics:

schematics blank --name=subscription-component

Команда blank schematic создаст проект с настроенными typescript, package.json и начальной схемой. Структура проекта должна выглядеть так:

subscription-component/
    src/
        subscription-component/
            index.ts
            index_spec.ts
        collection.json
    package.json
    tsconfig.json

Помимо стандартных файлов, которые вы наверняка узнаете, есть два, которые стоит кратко объяснить:

  • collections.json — это основной файл, в котором определены все схемы, которые этот проект будет выставлять
  • подписке-component/index.ts — это основной файл вашей схемы, он содержит фабричную функцию, которую Schematics будет использовать для генерации нашего компонента

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

{
    "$schema": "../node_modules/@angular-devkit/schematics/collection-schema.json",
    "schematics": {
        "subscription-component": {
            "description": "A blank schematic.",
            "factory": "./subscription-component/index#subscriptionComponent"
        }
    }
}

collection.json

Итак, что же находится внутри фабричной функции?

export function subscriptionComponent(_options: any): Rule {
    return (tree: Tree, _context: SchematicContext) => {
        return tree;
    };
}

index.ts

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

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

Реализация схемы компонентов

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

Схема

Чтобы определить входные параметры, нам нужно создать дополнительный файл schema.json в каталоге schematic (so inside /subscription-component). Схема может содержать множество практических полей для описания поведения скрипта. Вот простой пример для наших нужд:

{
    "$schema": "http://json-schema.org/schema",
    "id": "SubscriptionComponentSchema",
    "type": "object",
    "properties": {
        "path": {
            "type": "string",
            "format": "path",
            "visible": false
        },
        "name": {
            "type": "string",
            "$default": {
                "$source": "argv",
                "index": 0
            }
        }
    },
    "required": [
        "name"
    ]
}

schema.json

Ранее упомянутая схема даст нам тот же результат, что и вход схемы компонента по умолчанию ng generate component. Когда кто-то запускает в терминале ng generate component shared-components/custom-dialog, он устанавливает путь свойства в shared-components/ и имя в custom-dialog. Это все, что нам нужно. Схема должна быть включена в определение схемы в файле коллекции путем добавления такого поля:

"schema": "./subscription-component/schema.json"

collection.json

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

Фабрика

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

Как работает генерация файлов, не вдаваясь в подробности? Для каждого файла, созданного с помощью Schematics, заводская функция снабжена файлом шаблона. Файл шаблона содержит определение того, как должен быть построен файл.

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

Первый шаг: отформатируйте входные параметры, чтобы их можно было безопасно использовать для создания файлов:

_options.name = basename(_options.name);
_options.path = normalize('/' + dirname((_options.path + '/' + _options.name)));

index.ts

Если вы забыли, _options-это аргумент фабрики, которая сохраняет все входные параметры. Все функции для форматирования этих параметров взяты из пакета @angular-devkit.

Второй шаг: создание исходного шаблона для класса компонентов typescript

const templateSource = apply(
    url('./files'),
    [
        template(_options),
        move(normalize(_options.path)),
    ],
);

index.ts

Каталог ./files будет содержать файл с определением шаблона. Как это работает?

Schematics создает источники шаблонов на основе статических файлов из предоставленного каталога и правил, применяемых к ним. Набор правил определяет, что делать с Источником. В нашем примере создается шаблон с использованием _options, которые сохраняют динамические данные, а затем перемещают все источники в путь.

Третий, последний шаг: объединение схематических фабрик.

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

return chain([
    externalSchematic('@schematics/angular', 'component', _options),
    mergeWith(templateSource, MergeStrategy.Overwrite),
]);

Функция external Schematic создает Правило из любых внешних схем — в нашем случае Угловых по умолчанию. Затем использует функцию mergeWith для слияния правил, используя соответствующую стратегию слияния, предоставленную в качестве второго аргумента. Это создает для нас окончательное Правило, которое создаст для нас целый компонент.

Заводская функция должна, наконец, выглядеть так:

export function subscriptionComponent(_options: any): Rule {
    return (_tree: Tree, _context: SchematicContext) => {
        _options.name = basename(_options.name);
        _options.path = normalize('/' + dirname((_options.path + '/' + _options.name)));
        
        const templateSource = apply(
            url('./files'), [
                template(_options),
                move(_options.path),
            ],
        );
        
        return chain([
            externalSchematic('@schematics/angular', 'component', _options),
            mergeWith(templateSource, MergeStrategy.Overwrite),
        ]);
    };
}

Теперь пришло время написать наш шаблон!

Шаблон

Написание шаблона — относительно простая работа, потому что он должен выглядеть так же, как и хорошо известный сгенерированный файл, за исключением одного отличия-все динамическое содержимое, например, имя компонента, должно быть записано внутри специальных тегов ( < % = ,%>), чтобы вывести значение.

Файлы шаблонов хранятся в каталоге /files, и их имена должны быть записаны в определенном формате, чтобы обеспечить динамическое именование файлов. В нашем случае мы хотим создать файл typescript, который будет иметь вид component-name.component.ts, предполагая, что «component-name» — это наше имя, предоставленное в качестве входных данных. Для этого нам нужно создать файл шаблона с именем: __name@dasherize__.component.ts. Двойное подчеркивание отделяет динамическое содержимое от простой строки, а dasherize-это угловая функция, которая сделает строку «kebab-case» из имени. Мы должны использовать тот же подход для именования каталога, поэтому нам нужно поместить файл шаблона в папку с именем __name@dasherize__.

Теперь собственно шаблон:

import { Component, OnInit } from '@angular/core';

@Component({
    selector: 'app-<%= dasherize(name) %>-component',
    templateUrl: './<%= dasherize(name) %>.component.html',
    styleUrls: ['./<%= dasherize(name) %>.component.scss'],
})
export class <%= classify(name) %>Component implements OnInit {
    
    constructor() { }
    
    ngOnInit(): void {
    }
}

files/__name@dasherize__/__name@dasherize__.component.ts

Я думаю, ты узнаешь паттерн? Это хорошо известный класс компонентов, но с использованием динамических строк внутри специальных тегов.

Для файлов и селектора я использую функцию dasherize(name), чтобы сделать имя kebab-case. Имейте в виду, что эта функция еще не предусмотрена для шаблона — нам нужно будет ее добавить. Имя класса использует функцию classify(name), которая преобразует имя в формат UpperCamelCase — мы должны предоставить и эту функцию. Итак, чтобы подвести итог, нам нужно предоставить две функции и имя компонента.

Давайте рассмотрим функцию создания исходного кода шаблона:

template(_options),

index.ts — template source

Все очень просто. Как вы видите, он уже содержит переменную name для нас (inside _options object). Это означает, что нам нужно только предоставить classify и dasherize функций. Давайте сделаем это!

template({
    ..._options,
    classify: strings.classify,
    dasherize: strings.dasherize,
}),

index.ts — template source

strings-это коллекция из пакета @angular-devkit, поэтому единственное, что нам нужно сделать, — это выставить элементы, которые мы хотим использовать.

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

Вот как выглядит шаблон класса:

export class <%= classify(name) %>Component implements OnInit, OnDestroy {
    private readonly subscription: Subscription = new Subscription();

    constructor() { }

    ngOnInit(): void {
    }
    
    ngOnDestroy(): void {
        this.subscription.unsubscribe();
    }
}

files/__name@dasherize__/__name@dasherize__.component.ts

Collection — окончательная схематическая конфигурация

Я хочу переопределить стандартную схему компонентов ng g c / ng generate component schematic, поэтому внутри collection.json мне нужно изменить имя на “component” и добавить псевдоним “c”.

"component": {
    "aliases": [
        "c"
    ],
    "factory": "./subscription-component/index#subscriptionComponent",
    "schema": "./subscription-component/schema.json"
}

collection.json

Мы запускаем схему!

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

Нам нужно связать наш каталог схем с проектом Angular и установить его в качестве коллекции схем по умолчанию, чтобы переопределить @angular/schematics.

Прежде всего, нам нужно построить нашу библиотеку со схемой. Используйте приведенную ниже команду в каталоге библиотеки:

npm run build

Чтобы добавить наш проект schematics в качестве зависимости для Angular запуска в каталоге проекта:

npm install --save-dev ../path/to/subscription-component

Чтобы переопределить коллекцию по умолчанию, вам нужно добавить ее в файл angular.json в качестве свойства clip в главном объекте.

"cli": {
    "defaultCollection": "subscription-component"
}

angular.json

Затем в том же файле нам нужно предоставить параметры по умолчанию, точно так же, как они предоставляются для @angular/schematics:

"schematics": {
    "@schematics/angular:component": {
        "style": "scss"
    },
    "subscription-component:component": {
        "style": "scss"
    }
},

angular.json

— Готовы?

Больше никаких настроек, обещаю. Теперь вы должны иметь возможность запустить свою пользовательскую схему для создания компонента с персонализированным контентом проекта!

ng g c app/my-component

Результат должен выглядеть так, и вы должны быть гордым обладателем совершенно нового компонента!

CREATE src/app/my-component/my-component.component.scss (0 bytes)
CREATE src/app/my-component/my-component.component.html (21 bytes)
CREATE src/app/my-component/my-component.component.spec.ts (626 bytes)
CREATE src/app/my-component/my-component.component.ts (498 bytes)
UPDATE src/app/app.module.ts (470 bytes)

Отличная работа! Ты сделал это! Проверьте файл component.ts на наличие кода управления подпиской.

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

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

Обновление Schema.json
Я добавлю дополнительное определение параметра:

"subscriptionManagement": {
    "description": "Include subscription management code in the component class",
    "type": "boolean",
    "default": true,
    "alias": "subscription"
}

schema.json

Шаблон
Теперь внутри файла шаблона мы будем использовать нечто, называемое условными шаблонами. Этот метод будет генерировать различные фрагменты шаблона на основе динамических данных, передаваемых в _options. Объект options уже содержит флаг subscriptionManagement, поскольку он является входным параметром, поэтому нам не нужно менять завод.

Условные шаблоны используют аналогичный синтаксис, как описано в предыдущих шагах. Ключевые слова, которые мы можем использовать для построения условий, — это if и else. Поэтому в нашем случае мы можем реализовать его следующим образом:

export class <%= classify(name) %>Component implements OnInit <% if (subscriptionManagement) {%>, OnDestroy <% }%> {

    <% if (subscriptionManagement) {%>
    private readonly subscription: Subscription = new Subscription();
    <% }%>
    
    constructor() { }

    ngOnInit(): void {
    }
    
    <% if (subscriptionManagement) {%>
    ngOnDestroy(): void {
        this.subscription.unsubscribe();
    }
    <% }%>
}

files/__name@dasherize__/__name@dasherize__.component.ts

Вот и все! Теперь постройте схему, установите ее и проверьте, как она работает.

ng g c app/my-component

должен генерировать компонент как и раньше

ng g c app/my-component --subscription=false

должен генерировать компонент без дополнительного кода подписки

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

используйте реестр NPM для публикации ваших схем. Это облегчит добавление вашей схемы для других разработчиков
обеспечьте поддержку ng add для вашей библиотеки. Помните, когда нам пришлось вручную установить коллекцию по умолчанию в angular.json? Все это может быть сделано автоматически, если вы сделаете еще одну схему для команды ng add
используйте схемы миграции, чтобы помочь разработчикам при внесении некоторых критических изменений
добавить модульные тесты! Думаю, мне не нужно объяснять, почему
расширьте заводскую функцию, чтобы использовать стандартные параметры Angular проекта, такие как стили, префикс селектора и т. Д.
Резюме
Я надеюсь, что вы нашли эту статью интересной и у вас уже есть некоторые идеи о том, как вы могли бы применить ее в своем проекте для улучшения процесса разработки. Я решительно призываю вас сделать это! Это не должно занимать много усилий — начните с какой-то концепции минимального значения, включите в схему один из критических фрагментов, который копируется снова и снова, и со временем добавьте еще.

Чтобы вкратце напомнить вам, что именно мы сделали, вот краткий список шагов по реализации пользовательской схемы, которая переопределяет угловую схему по умолчанию:

1- создайте пустую схему с помощью встроенной команды
2- implement schematic
— заводская функция
— файл шаблона
— схема с определениями входных параметров
— коллекция, которая определяет открытые схемы
3- добавления схемы в Angular проект
Это не долгий процесс, поэтому, если вы чувствуете, что он может быть полезен для вас, попробуйте! Вы не будете разочарованы.

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

Это все. Спасибо, что осталась со мной!

Наслаждайтесь тем, что тратите меньше времени на копирование и вставку и больше на фактическую разработку!

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

источник