Skema sudut, atau bagaimana saya menulis template untuk cli sudut

Halo, nama saya Maxim. Selama beberapa tahun sekarang saya telah melakukan pengembangan front-end. Saya sering harus berurusan dengan tata letak berbagai templat html. Dalam pekerjaan sehari-hari saya, saya biasanya menggunakan pembangun webpack dengan mesin templat pug kustom, dan saya juga menggunakan metodologi BEM. Untuk membuat hidup saya lebih mudah, saya menggunakan paket yang luar biasa .


Baru-baru ini, saya perlu membuat proyek kecil tentang Angular, dan karena saya terbiasa bekerja dengan alat favorit saya, saya tidak ingin kembali ke html kosong. Dalam hubungan ini, muncul masalah bagaimana membuat teman bempug dengan sudut, dan tidak hanya berteman, tetapi juga menghasilkan komponen dari cli dengan struktur yang saya butuhkan.


Siapa yang peduli bagaimana saya melakukan semuanya, selamat datang di kucing.


Untuk memulai, buat proyek uji di mana kami akan menguji templat kami.


Kami mengeksekusi di baris perintah:


ng g test-project .


Dalam pengaturan, saya memilih preprocessor scss, karena lebih nyaman bagi saya untuk bekerja dengannya.


Proyek ini dibuat, tetapi templat komponen default di html kami, sekarang perbaiki. Pertama-tama, Anda perlu berteman sudut tajam dengan mesin tempel pug, untuk ini saya menggunakan paket ng-cli-pug-loader


Instal paket, untuk ini, buka folder proyek dan jalankan:


ng add ng-cli-pug-loader .


Sekarang Anda dapat menggunakan file templat pug. Selanjutnya, kami menulis ulang dekorator root komponen AppComponent ke:


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

Karenanya, kami mengubah ekstensi file app.component.html menjadi app.component.pug, dan kontennya ditulis dalam sintaks templat. Dalam file ini, saya menghapus semuanya kecuali router.


Akhirnya, mari mulai membuat generator komponen kami!


Untuk menghasilkan template, kita perlu membuat skema kita sendiri. Saya menggunakan paket schematics-cli dari @ angular-devkit. Instal paket secara global dengan perintah:


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


Saya membuat skema dalam direktori terpisah di luar proyek dengan perintah:


schematics blank --name=bempug-component .


Kita masuk ke skema yang dibuat, kita sekarang tertarik pada file src / collection.json. Ini terlihat seperti ini:


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

Ini adalah file deskripsi dari skema kami, di mana parameternya adalah "pabrik": "./bempug-component/index#bempugComponent": ini adalah deskripsi fungsi utama "pabrik" generator kami.


Awalnya, tampilannya seperti ini:


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

Anda dapat membuat fungsi ekspor secara default, maka parameter "pabrik" dapat ditulis ulang sebagai "./bempug-component/index".


Selanjutnya, di direktori skema kami, buat file schema.json, itu akan menjelaskan semua parameter skema kami.


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

Parameter ada di properti, yaitu:


  • nama nama entitas (dalam kasus kami akan menjadi komponen);
  • Path adalah path yang digunakan generator untuk membuat file komponen;
  • Proyek adalah proyek itu sendiri, di mana komponen akan dihasilkan;

Tambahkan beberapa parameter lagi ke file yang akan dibutuhkan di masa mendatang.


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

  • modul di sini akan disimpan tautan ke modul di mana komponen akan dimasukkan, atau lebih tepatnya modul komponen;
  • componentModule ada flag apakah akan membuat untuk komponen modulnya sendiri (kemudian saya sampai pada kesimpulan bahwa itu akan selalu dibuat dan set ke true);
  • ekspor: ini adalah tanda apakah akan mengekspor dari modul ke mana kita mengimpor modul komponen kita;

Selanjutnya, kami membuat antarmuka dengan parameter schema.d.ts file komponen kami.


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

Di dalamnya, properti duplikat properti dari schema.json. Selanjutnya, siapkan pabrik kami, buka file index.ts. Di dalamnya, kami membuat dua fungsi filterTemplates, yang akan bertanggung jawab untuk membuat modul untuk komponen tergantung pada nilai componentModule, dan setupOptions, yang mengatur parameter yang diperlukan untuk pabrik.


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

Selanjutnya, kita menulis di fungsi utama:


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

Pabrik siap dan sudah dapat menghasilkan file komponen dengan memproses template dari folder file, yang belum tersedia. Tidak masalah, kami membuat folder file komponen bempug dalam folder skema kami dalam kasus saya. Dalam folder file, buat folder __name@dasherize__ , selama pembuatan, pabrik akan mengganti __name@dasherize__ dengan nama komponen.


Selanjutnya, di dalam __name@dasherize__ buat file


  • __name@dasherize__ templat komponen pesek
  • __name@dasherize__ file uji unit untuk komponen
  • __name@dasherize__ file dari komponen itu sendiri
  • __name@dasherize__ -component.module.ts modul komponen
  • __name@dasherize__ -component.scss stylesheet komponen

Sekarang kita akan menambahkan dukungan untuk memperbarui modul ke pabrik kami, untuk ini kami akan membuat file add-to-module-context.ts untuk menyimpan parameter yang diperlukan pabrik untuk bekerja dengan modul.


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

Tambahkan dukungan modul ke pabrik.


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

Sekarang, ketika menambahkan parameter -m <module reference> ke perintah cli, modul komponen kami akan menambahkan import ke modul yang ditentukan dan menambahkan ekspor darinya ketika menambahkan flag –export. Selanjutnya kita tambahkan dukungan BEM. Untuk melakukan ini, saya mengambil sumber paket bempug npm dan membuat kode dalam satu file bempugMixin.pug, yang saya tempatkan di folder umum dan di dalam folder umum lainnya sehingga mixin disalin ke folder umum di proyek pada sudut.


Tugas kami adalah bahwa mixin ini terhubung di masing-masing file template kami, dan tidak digandakan ketika membuat komponen baru, untuk ini kami akan menambahkan fungsionalitas ini ke pabrik kami.


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

Saatnya mulai mengisi file templat kami.


__name@dasherize__.component.pug :


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

Apa yang ditentukan dalam <% =%> selama pembuatan akan diganti dengan nama komponen.


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

Dalam hal ini, <% = mengklasifikasikan (nama)%> digunakan untuk melemparkan nama ke 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 %>{ } 

Kami membuat build skema kami dengan perintah `` npm run build``.


Semuanya siap untuk menghasilkan komponen dalam proyek!


Untuk memeriksanya, kembali ke proyek Angular kami dan buat modul.
ng gm test-schema
Selanjutnya, kita melakukan `` tautan npm <path absolut ke folder proyek dengan skema kami> '', untuk menambahkan skema kami ke node_modules proyek.


Dan kami mencoba rangkaian dengan perintah ng g bempug-component:bempug-component test -m /src/app/test-schema/test-schema.module.ts –export .
Skema kami akan membuat komponen dan menambahkannya ke modul yang ditentukan dengan ekspor.
Skema siap, Anda dapat mulai membuat aplikasi pada teknologi yang sudah dikenal.


Anda dapat melihat versi finalnya di sini , dan paketnya juga tersedia dalam npm .


Saat membuat skema saya menggunakan artikel tentang topik ini, saya mengucapkan terima kasih kepada penulis.



Terima kasih atas perhatian Anda, semua orang yang membaca sampai akhir, Anda adalah yang terbaik!
Dan proyek menarik lainnya menanti saya. Sampai ketemu lagi!

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


All Articles