Esquemas angulares, ou como eu escrevi meu modelo para cli angular

Olá, meu nome é Maxim. Há vários anos, estou desenvolvendo front-end. Muitas vezes tenho que lidar com o layout de vários modelos de html. No meu trabalho diário, geralmente uso o construtor webpack com um mecanismo de modelo de pug personalizado e também uso a metodologia BEM. Para facilitar minha vida, uso um pacote maravilhoso .


Recentemente, precisei fazer um pequeno projeto no Angular e, como estava acostumado a trabalhar com minhas ferramentas favoritas, não queria voltar ao html vazio. Nesse contexto, surgiu o problema de como fazer amigos confusos com um angular, e não apenas fazer amigos, mas também gerar componentes do cli com a estrutura de que eu precisava.


Quem se importa como eu fiz tudo, bem-vindo ao gato.


Para começar, crie um projeto de teste no qual testaremos nosso modelo.


Executamos na linha de comando:


ng g test-project .


Nas configurações, escolhi o pré-processador scss, pois é mais conveniente trabalhar com ele.


O projeto foi criado, mas os modelos de componentes padrão em nosso html agora o corrigem. Primeiro de tudo, você precisa fazer amigos angulares do cli com o mecanismo de modelo de pug, para isso usei o pacote ng-cli-pug-loader


Instale o pacote, para isso, vá para a pasta do projeto e execute:


ng add ng-cli-pug-loader .


Agora você pode usar arquivos de modelo de pug. Em seguida, reescrevemos o decorador raiz do componente AppComponent para:


  @Component({ selector: 'app-root', templateUrl: './app.component.pug', styleUrls: ['./app.component.scss'] }) 

Assim, alteramos a extensão do arquivo app.component.html para app.component.pug, e o conteúdo é gravado na sintaxe do modelo. Neste arquivo, eu apaguei tudo, exceto o roteador.


Finalmente, vamos começar a criar nosso gerador de componentes!


Para gerar modelos, precisamos criar nosso próprio esquema. Estou usando o pacote schematics-cli do @ angular-devkit. Instale o pacote globalmente com o comando:


npm install -g @angular-devkit/schematics-cli .


Eu criei o esquema em um diretório separado fora do projeto com o comando:


schematics blank --name=bempug-component .


Entramos no esquema criado, agora estamos interessados ​​no arquivo src / collection.json. É assim:


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

Este é um arquivo de descrição do nosso esquema, onde o parâmetro é "factory": "./bempug-component/index#bempugComponent": esta é a descrição da função principal da "fábrica" ​​do nosso gerador.


Inicialmente, parece algo como isto:


 import { Rule, SchematicContext, Tree } from '@angular-devkit/schematics'; // You don't have to export the function as default. You can also have more than one rule factory // per file. export function bempugComponent(options: any): Rule { return (tree: Tree, _context: SchematicContext) => { return tree; }; } 

Você pode exportar a função por padrão, e o parâmetro "factory" pode ser reescrito como "./bempug-component/index".


Em seguida, no diretório do nosso esquema, crie o arquivo schema.json, ele descreverá todos os parâmetros do nosso esquema.


 { "$schema": "http://json-schema.org/schema", "id": "SchemanticsForMenu", "title": "Bempug Schema", "type": "object", "properties": { "name": { "type": "string", "$default": { "$source": "argv", "index": 0 } }, "path": { "type": "string", "format": "path", "description": "The path to create the component.", "visible": false }, "project": { "type": "string", "description": "The name of the project.", "$default": { "$source": "projectName" } } } } 

Os parâmetros estão em propriedades, a saber:


  • nome nome da entidade (no nosso caso, será um componente);
  • Caminho é o caminho pelo qual o gerador criará os arquivos do componente;
  • Projeto é o próprio projeto, no qual o componente será gerado;

Adicione mais alguns parâmetros ao arquivo que serão necessários no futuro.


 "module": { "type": "string", "description": "The declaring module.", "alias": "m" }, "componentModule": { "type": "boolean", "default": true, "description": "Patern module per Component", "alias": "mc" }, "export": { "type": "boolean", "default": false, "description": "Export component from module?" } 

  • O módulo aqui será armazenado em um link para o módulo no qual o componente será incluído, ou melhor, o módulo do componente;
  • componentModule existe um sinalizador para criar para o componente seu próprio módulo (então cheguei à conclusão de que ele sempre será criado e configurado como true);
  • exportar: este é o sinalizador para exportar do módulo para o qual estamos importando nosso módulo componente;

Em seguida, criamos uma interface com os parâmetros do nosso componente, o arquivo schema.d.ts.


 export interface BemPugOptions { name: string; project?: string; path?: string; module?: string; componentModule?: boolean; module?: string; export?: boolean; bemPugMixinPath?: string; } 

Nele, as propriedades duplicam propriedades de schema.json. Em seguida, prepare nossa fábrica, vá para o arquivo index.ts. Nele, criamos duas funções filterTemplates, que serão responsáveis ​​por criar um módulo para um componente, dependendo do valor de componentModule, e setupOptions, que define os parâmetros necessários para a fábrica.


 function filterTemplates(options: BemPugOptions): Rule { if (!options.componentModule) { return filter(path => !path.match(/\.module\.ts$/) && !path.match(/-item\.ts$/) && !path.match(/\.bak$/)); } return filter(path => !path.match(/\.bak$/)); } function setupOptions(options: BemPugOptions, host: Tree): void { const workspace = getWorkspace(host); if (!options.project) { options.project = Object.keys(workspace.projects)[0]; } const project = workspace.projects[options.project]; if (options.path === undefined) { const projectDirName = project.projectType === 'application' ? 'app' : 'lib'; options.path = `/${project.root}/src/${projectDirName}`; } const parsedPath = parseName(options.path, options.name); options.name = parsedPath.name; options.path = parsedPath.path; } 

Em seguida, escrevemos na função principal:


 export function bempugComponent(options: BemPugOptions): Rule { return (host: Tree, context: SchematicContext) => { setupOptions(options, host); const templateSource = apply(url('./files'), [ filterTemplates(options), template({ ...strings, ...options }), move(options.path || '') ]); const rule = chain([ branchAndMerge(chain([ mergeWith(templateSource), ])) ]); return rule(host, context); } } 

A fábrica está pronta e já pode gerar arquivos de componentes processando modelos da pasta de arquivos, que ainda não está disponível. Não importa, criamos uma pasta de arquivos do componente bempug em nossa pasta de esquema no meu caso. Na pasta de arquivos, crie a pasta __name@dasherize__ , durante a geração, a fábrica substituirá __name@dasherize__ pelo nome do componente.


Em seguida, dentro da __name@dasherize__ crie arquivos


  • __name@dasherize__ modelo de componente de pug
  • __name@dasherize__ arquivo de teste de unidade para o componente
  • __name@dasherize__ do próprio componente
  • __name@dasherize__ componente __name@dasherize__ -component.module.ts
  • __name@dasherize__ -component.scss folha de estilo do componente

Agora adicionaremos suporte para atualizar os módulos em nossa fábrica, para isso criamos o arquivo add-to-module-context.ts para armazenar os parâmetros que a fábrica precisará para trabalhar com o módulo.


 import * as ts from 'typescript'; export class AddToModuleContext { // source of the module file source: ts.SourceFile; // the relative path that points from // the module file to the component file relativePath: string; // name of the component class classifiedName: string; } 

Adicione suporte ao módulo à fábrica.


 const stringUtils = { dasherize, classify }; // You don't have to export the function as default. You can also have more than one rule factory // per file. function filterTemplates(options: BemPugOptions): Rule { if (!options.componentModule) { return filter(path => !path.match(/\.module\.ts$/) && !path.match(/-item\.ts$/) && !path.match(/\.bak$/)); } return filter(path => !path.match(/\.bak$/)); } function setupOptions(options: BemPugOptions, host: Tree): void { const workspace = getWorkspace(host); if (!options.project) { options.project = Object.keys(workspace.projects)[0]; } const project = workspace.projects[options.project]; if (options.path === undefined) { const projectDirName = project.projectType === 'application' ? 'app' : 'lib'; options.path = `/${project.root}/src/${projectDirName}`; } const parsedPath = parseName(options.path, options.name); options.name = parsedPath.name; options.path = parsedPath.path; options.module = options.module || findModuleFromOptions(host, options) || ''; } export function createAddToModuleContext(host: Tree, options: ModuleOptions, componentPath: string): AddToModuleContext { const result = new AddToModuleContext(); if (!options.module) { throw new SchematicsException(`Module not found.`); } // Reading the module file const text = host.read(options.module); if (text === null) { throw new SchematicsException(`File ${options.module} does not exist.`); } const sourceText = text.toString('utf-8'); result.source = ts.createSourceFile(options.module, sourceText, ts.ScriptTarget.Latest, true); result.relativePath = buildRelativePath(options.module, componentPath); result.classifiedName = stringUtils.classify(`${options.name}ComponentModule`); return result; } function addDeclaration(host: Tree, options: ModuleOptions, componentPath: string) { const context = createAddToModuleContext(host, options, componentPath); const modulePath = options.module || ''; const declarationChanges = addImportToModule( context.source, modulePath, context.classifiedName, context.relativePath); const declarationRecorder = host.beginUpdate(modulePath); for (const change of declarationChanges) { if (change instanceof InsertChange) { declarationRecorder.insertLeft(change.pos, change.toAdd); } } host.commitUpdate(declarationRecorder); }; function addExport(host: Tree, options: ModuleOptions, componentPath: string) { const context = createAddToModuleContext(host, options, componentPath); const modulePath = options.module || ''; const exportChanges = addExportToModule( context.source, modulePath, context.classifiedName, context.relativePath); const exportRecorder = host.beginUpdate(modulePath); for (const change of exportChanges) { if (change instanceof InsertChange) { exportRecorder.insertLeft(change.pos, change.toAdd); } } host.commitUpdate(exportRecorder); }; export function addDeclarationToNgModule(options: ModuleOptions, exports: boolean, componentPath: string): Rule { return (host: Tree) => { addDeclaration(host, options, componentPath); if (exports) { addExport(host, options, componentPath); } return host; }; } export function bempugComponent(options: BemPugOptions): Rule { return (host: Tree, context: SchematicContext) => { setupOptions(options, host); deleteCommon(host); const templateSource = apply(url('./files'), [ filterTemplates(options), template({ ...strings, ...options }), move(options.path || '') ]); const rule = chain([ branchAndMerge(chain([ mergeWith(templateSource), addDeclarationToNgModule(options, !!options.export, `${options.path}/${options.name}/${options.name}-component.module` || '') ])) ]); return rule(host, context); } } 

Agora, ao adicionar o parâmetro -m <module reference> ao comando cli, nosso módulo componente adicionará importação ao módulo especificado e adicionará a exportação ao adicionar o sinalizador –export. Em seguida, adicionamos suporte ao BEM. Para fazer isso, peguei as fontes do pacote npm bempug e criei o código em um arquivo bempugMixin.pug, que coloquei na pasta comum e dentro de outra pasta comum para que o mixin seja copiado para a pasta comum no projeto no angular.


Nossa tarefa é que esse mixin esteja conectado em cada um de nossos arquivos de modelo, e não seja duplicado ao gerar novos componentes. Para isso, adicionaremos essa funcionalidade à nossa fábrica.


 import { Rule, SchematicContext, Tree, filter, apply, template, move, chain, branchAndMerge, mergeWith, url, SchematicsException } from '@angular-devkit/schematics'; import {BemPugOptions} from "./schema"; import {getWorkspace} from "@schematics/angular/utility/config"; import {parseName} from "@schematics/angular/utility/parse-name"; import {normalize, strings} from "@angular-devkit/core"; import { AddToModuleContext } from './add-to-module-context'; import * as ts from 'typescript'; import {classify, dasherize} from "@angular-devkit/core/src/utils/strings"; import {buildRelativePath, findModuleFromOptions, ModuleOptions} from "@schematics/angular/utility/find-module"; import {addExportToModule, addImportToModule} from "@schematics/angular/utility/ast-utils"; import {InsertChange} from "@schematics/angular/utility/change"; const stringUtils = { dasherize, classify }; // You don't have to export the function as default. You can also have more than one rule factory // per file. function filterTemplates(options: BemPugOptions): Rule { if (!options.componentModule) { return filter(path => !path.match(/\.module\.ts$/) && !path.match(/-item\.ts$/) && !path.match(/\.bak$/)); } return filter(path => !path.match(/\.bak$/)); } function setupOptions(options: BemPugOptions, host: Tree): void { const workspace = getWorkspace(host); if (!options.project) { options.project = Object.keys(workspace.projects)[0]; } const project = workspace.projects[options.project]; if (options.path === undefined) { const projectDirName = project.projectType === 'application' ? 'app' : 'lib'; options.path = `/${project.root}/src/${projectDirName}`; } const parsedPath = parseName(options.path, options.name); options.name = parsedPath.name; options.path = parsedPath.path; options.module = options.module || findModuleFromOptions(host, options) || ''; options.bemPugMixinPath = buildRelativePath(`${options.path}/${options.name}/${options.name}.component.ts`, `/src/app/common/bempugMixin.pug`); } export function createAddToModuleContext(host: Tree, options: ModuleOptions, componentPath: string): AddToModuleContext { const result = new AddToModuleContext(); if (!options.module) { throw new SchematicsException(`Module not found.`); } // Reading the module file const text = host.read(options.module); if (text === null) { throw new SchematicsException(`File ${options.module} does not exist.`); } const sourceText = text.toString('utf-8'); result.source = ts.createSourceFile(options.module, sourceText, ts.ScriptTarget.Latest, true); result.relativePath = buildRelativePath(options.module, componentPath); result.classifiedName = stringUtils.classify(`${options.name}ComponentModule`); return result; } function addDeclaration(host: Tree, options: ModuleOptions, componentPath: string) { const context = createAddToModuleContext(host, options, componentPath); const modulePath = options.module || ''; const declarationChanges = addImportToModule( context.source, modulePath, context.classifiedName, context.relativePath); const declarationRecorder = host.beginUpdate(modulePath); for (const change of declarationChanges) { if (change instanceof InsertChange) { declarationRecorder.insertLeft(change.pos, change.toAdd); } } host.commitUpdate(declarationRecorder); }; function addExport(host: Tree, options: ModuleOptions, componentPath: string) { const context = createAddToModuleContext(host, options, componentPath); const modulePath = options.module || ''; const exportChanges = addExportToModule( context.source, modulePath, context.classifiedName, context.relativePath); const exportRecorder = host.beginUpdate(modulePath); for (const change of exportChanges) { if (change instanceof InsertChange) { exportRecorder.insertLeft(change.pos, change.toAdd); } } host.commitUpdate(exportRecorder); }; export function addDeclarationToNgModule(options: ModuleOptions, exports: boolean, componentPath: string): Rule { return (host: Tree) => { addDeclaration(host, options, componentPath); if (exports) { addExport(host, options, componentPath); } return host; }; } function deleteCommon(host: Tree) { const path = `/src/app/common/bempugMixin.pug`; if(host.exists(path)) { host.delete(`/src/app/common/bempugMixin.pug`); } } export function bempugComponent(options: BemPugOptions): Rule { return (host: Tree, context: SchematicContext) => { setupOptions(options, host); deleteCommon(host); const templateSource = apply(url('./files'), [ filterTemplates(options), template({ ...strings, ...options }), move(options.path || '') ]); const mixinSource = apply(url('./common'), [ template({ ...strings, ...options }), move('/src/app/' || '') ]); const rule = chain([ branchAndMerge(chain([ mergeWith(templateSource), mergeWith(mixinSource), addDeclarationToNgModule(options, !!options.export, `${options.path}/${options.name}/${options.name}-component.module` || '') ]), 14) ]); return rule(host, context); } } 

É hora de começar a preencher nossos arquivos de modelo.


__name@dasherize__.component.pug :


 include <%= bemPugMixinPath %> +b('<%= name %>') +e('item', {m:'test'}) | <%= name %> works 

O que é especificado em <% =%> durante a geração será substituído pelo nome do componente.


__name@dasherize__.component.spec.ts:


 import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import {NO_ERRORS_SCHEMA} from '@angular/core'; import { <%= classify(name) %>ComponentModule } from './<%= name %>-component.module'; import { <%= classify(name) %>Component } from './<%= name %>.component'; describe('<%= classify(name) %>Component', () => { let component: <%= classify(name) %>Component; let fixture: ComponentFixture<<%= classify(name) %>Component>; beforeEach(async(() => { TestBed.configureTestingModule({ imports: [<%= classify(name) %>ComponentModule], declarations: [], schemas: [ NO_ERRORS_SCHEMA ] }) .compileComponents(); })); beforeEach(() => { fixture = TestBed.createComponent(<%= classify(name) %>Component); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); }); 

Nesse caso, <% = classify (name)%> é usado para converter o nome no CamelCase.


__name@dasherize__.component.ts:


 import { Component, OnInit, ViewEncapsulation} from '@angular/core'; @Component({ selector: 'app-<%=dasherize(name)%>-component', templateUrl: '<%=dasherize(name)%>.component.pug', styleUrls: ['./<%=dasherize(name)%>-component.scss'], encapsulation: ViewEncapsulation.None }) export class <%= classify(name) %>Component implements OnInit { constructor() {} ngOnInit(): void { } } 

__name@dasherize__-component.module.ts:


 import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import {<%= classify(name) %>Component} from './<%= name %>.component'; @NgModule({ declarations: [ <%= classify(name) %>Component, ], imports: [ CommonModule ], exports: [ <%= classify(name) %>Component, ] }) export class <%= classify(name) %>ComponentModule { } 

__name@dasherize__-component.scss:


 .<%= name %>{ } 

Fazemos a construção do nosso esquema com o comando `` npm run build``.


Tudo está pronto para gerar componentes no projeto!


Para verificar, volte ao nosso projeto Angular e crie um módulo.
ng gm test-schema
Em seguida, fazemos `` npm link <caminho absoluto para a pasta do projeto com nosso esquema> '', para adicionar nosso esquema aos node_modules do projeto.


E tentamos o circuito com o ng g bempug-component:bempug-component test -m /src/app/test-schema/test-schema.module.ts –export .
Nosso esquema criará um componente e o adicionará ao módulo especificado com exportação.
O esquema está pronto, você pode começar a fazer o aplicativo em tecnologias familiares.


Você pode ver a versão final aqui , e também o pacote está disponível no npm .


Ao criar o esquema, usei artigos sobre esse tópico, expresso minha gratidão aos autores.



Obrigado por sua atenção, todos que lêem até o fim, você é o melhor!
E outro projeto emocionante me espera. Até breve!

Source: https://habr.com/ru/post/pt469509/


All Articles