Schémas angulaires, ou comment j'ai écrit mon modèle pour cli angulaire

Bonjour, je m'appelle Maxim. Depuis plusieurs années, je fais du développement front-end. Je dois souvent gérer la mise en page de divers modèles html. Dans mon travail quotidien, j'utilise généralement le générateur de webpack avec un moteur de modèle de pug personnalisé, et j'utilise également la méthodologie BEM. Afin de me faciliter la vie, j'utilise un merveilleux paquet .


Récemment, j'avais besoin de faire un petit projet sur Angular, et comme j'avais l'habitude de travailler avec mes outils préférés, je ne voulais pas revenir au html nu. À cet égard, le problème s'est posé de savoir comment se faire des amis bempug avec un angulaire, et pas seulement se faire des amis, mais aussi générer des composants à partir de cli avec la structure dont j'avais besoin.


Peu importe comment j'ai fait tout cela, bienvenue au chat.


Pour commencer, créez un projet de test sur lequel nous allons tester notre modèle.


Nous exécutons en ligne de commande:


ng g test-project .


Dans les paramètres, j'ai choisi le préprocesseur scss, car il est plus pratique pour moi de travailler avec.


Le projet a été créé, mais les modèles de composants par défaut dans notre html, le corrigent maintenant. Tout d'abord, vous devez vous faire des amis cli angulaires avec le moteur de modèle de pug, pour cela j'ai utilisé le paquet ng-cli-pug-loader


Installez le package, pour cela, allez dans le dossier du projet et exécutez:


ng add ng-cli-pug-loader .


Vous pouvez maintenant utiliser des fichiers de modèle de pug. Ensuite, nous réécrivons le décorateur racine du composant AppComponent pour:


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

Par conséquent, nous modifions l'extension de fichier app.component.html en app.component.pug et le contenu est écrit dans la syntaxe du modèle. Dans ce fichier, j'ai tout supprimé sauf le routeur.


Enfin, commençons à créer notre générateur de composants!


Pour générer des modèles, nous devons créer notre propre schéma. J'utilise le paquet schematics-cli de @ angular-devkit. Installez le package globalement avec la commande:


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


J'ai créé le schéma dans un répertoire séparé en dehors du projet avec la commande:


schematics blank --name=bempug-component .


Nous allons dans le schéma créé, nous sommes maintenant intéressés par le fichier src / collection.json. Cela ressemble à ceci:


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

Il s'agit d'un fichier de description de notre schéma, où le paramètre est "usine": "./bempug-component/index#bempugComponent": il s'agit de la description de la fonction principale de "l'usine" de notre générateur.


Au départ, cela ressemble à ceci:


 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; }; } 

Vous pouvez faire l'exportation de la fonction par défaut, puis le paramètre "usine" peut être réécrit en "./bempug-component/index".


Ensuite, dans le répertoire de notre schéma, créez le fichier schema.json, il décrira tous les paramètres de notre schéma.


 { "$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" } } } } 

Les paramètres sont dans les propriétés, à savoir:


  • nom nom de l'entité (dans notre cas ce sera un composant);
  • Chemin est le chemin par lequel le générateur va créer les fichiers de composants;
  • Projet est le projet lui-même, dans lequel le composant sera généré;

Ajoutez quelques paramètres supplémentaires au fichier qui seront nécessaires à l'avenir.


 "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?" } 

  • module ici sera stocké un lien vers le module dans lequel le composant sera inclus, ou plutôt le module composant;
  • componentModule il y a un drapeau indiquant s'il faut créer pour le composant son propre module (alors je suis arrivé à la conclusion qu'il sera toujours créé et défini sur true);
  • export: c'est le drapeau pour exporter à partir du module dans lequel nous importons notre module composant;

Ensuite, créez une interface avec les paramètres de notre fichier de composants schema.d.ts.


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

Dans ce document, les propriétés dupliquent les propriétés de schema.json. Ensuite, préparez notre usine, accédez au fichier index.ts. Dans ce document, nous créons deux fonctions filterTemplates, qui seront responsables de la création d'un module pour un composant en fonction de la valeur de componentModule, et setupOptions, qui définit les paramètres nécessaires pour l'usine.


 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; } 

Ensuite, nous écrivons dans la fonction principale:


 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); } } 

L'usine est prête et elle peut déjà générer des fichiers de composants en traitant des modèles à partir du dossier de fichiers, qui n'est pas encore disponible. Cela n'a pas d'importance, nous créons un dossier de fichiers de composants bempug dans notre dossier de schéma dans mon cas. Dans le dossier des fichiers, créez le dossier __name@dasherize__ , lors de la génération, l'usine remplacera __name@dasherize__ par le nom du composant.


Ensuite, dans le __name@dasherize__ créez des fichiers


  • __name@dasherize__ .component.pug pug component template
  • __name@dasherize__ .component.spec.ts fichier de test unitaire pour le composant
  • __name@dasherize__ .component.ts fichier du composant lui-même
  • __name@dasherize__ -component.module.ts module module
  • __name@dasherize__ -component.scss feuille de style des composants

Nous allons maintenant ajouter la prise en charge de la mise à jour des modules à notre usine, pour cela, nous allons créer le fichier add-to-module-context.ts pour stocker les paramètres dont l'usine aura besoin pour travailler avec le module.


 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; } 

Ajoutez le support du module à l'usine.


 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); } } 

Désormais, lors de l'ajout du paramètre -m <référence de module> à la commande cli, notre module de composant ajoutera une importation au module spécifié et en ajoutera l'exportation lors de l'ajout de l'indicateur –export. Ensuite, nous ajoutons le support BEM. Pour ce faire, j'ai pris les sources du package npm bempug et créé le code dans un fichier bempugMixin.pug, que j'ai placé dans le dossier commun et à l'intérieur dans un autre dossier commun afin que le mixin soit copié dans le dossier commun du projet sur l'angulaire.


Notre tâche est que ce mixage soit connecté dans chacun de nos fichiers modèles, et non dupliqué lors de la génération de nouveaux composants, pour cela nous ajoutons cette fonctionnalité à notre usine.


 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); } } 

Il est temps de commencer à remplir nos fichiers modèles.


__name@dasherize__.component.pug :


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

Ce qui est spécifié dans <% =%> lors de la génération sera remplacé par le nom du composant.


__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(); }); }); 

Dans ce cas, <% = classify (name)%> est utilisé pour convertir le nom en 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 %>{ } 

Nous faisons la construction de notre schéma avec la commande `` npm run build``.


Tout est prêt pour générer des composants dans le projet!


Pour vérifier, revenez à notre projet Angular et créez un module.
ng gm test-schema
Ensuite, nous faisons `` npm link <chemin absolu vers le dossier du projet avec notre schéma> '', afin d'ajouter notre schéma aux node_modules du projet.


Et nous essayons le circuit avec la commande ng g bempug-component:bempug-component test -m /src/app/test-schema/test-schema.module.ts –export .
Notre schéma créera un composant et l'ajoutera au module spécifié avec l'exportation.
Le schéma est prêt, vous pouvez commencer à faire l'application sur des technologies familières.


Vous pouvez voir la version finale ici , et le package est également disponible en npm .


Lors de la création du schéma, j'ai utilisé des articles sur ce sujet, j'exprime ma gratitude aux auteurs.



Merci pour votre attention, tous ceux qui ont lu jusqu'au bout, vous êtes les meilleurs!
Et un autre projet passionnant m'attend. A très bientôt!

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


All Articles