角度示意图,或我如何编写角度cli模板

您好,我叫Maxim。 几年来,我一直在进行前端开发。 我经常不得不处理各种HTML模板的布局。 在日常工作中,我通常使用带有自定义pug模板引擎的webpack构建器,并且还使用BEM方法。 为了使我的生活更轻松,我使用了一个精美的包装


最近,我需要在Angular上做一个小项目,并且由于我习惯了使用自己喜欢的工具,所以我不想回到纯HTML上。 在这方面,出现了一个问题,即如何用某个角度结交bempug朋友,不仅要结交朋友,还需要用cli生成具有所需结构的组件。


谁在乎我是如何做到的,欢迎来爱猫。


首先,创建一个测试项目,在该项目上我们将测试模板。


我们在命令行执行:


ng g test-project


在设置中,我选择了scss预处理程序,因为使用它更方便。


该项目已创建,但是html中的默认组件模板现已修复。 首先,您需要使用pug模板引擎结识cli朋友,为此,我使用了ng-cli-pug-loader软件包


安装软件包,为此,请转到项目文件夹并执行:


ng add ng-cli-pug-loader


现在,您可以使用哈巴狗模板文件。 接下来,我们将AppComponent组件的根装饰器重写为:


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

因此,我们将文件扩展名app.component.html更改为app.component.pug,并且内容以模板语法编写。 在此文件中,我删除了除路由器以外的所有内容。


最后,让我们开始创建组件生成器!


要生成模板,我们需要创建自己的方案。 我正在使用@ angle-devkit中的diagrams-cli包。 使用以下命令全局安装软件包:


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


我使用以下命令在项目外部的单独目录中创建了方案:


schematics blank --name=bempug-component


我们进入创建的方案,现在对src / collection.json文件感兴趣。 看起来像这样:


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

这是我们方案的描述文件,其中参数为“ factory”:“ ./bempug-component/index#bempugComponent”:这是生成器“ factory”主要功能的描述。


最初,它看起来像这样:


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

您可以默认使函数导出,然后可以将“ factory”参数重写为“ ./bempug-component/index”。


接下来,在方案的目录中,创建文件schema.json,它将描述方案的所有参数。


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

参数位于属性中,即:


  • 名称实体名称(在我们的例子中将是组件);
  • 路径是生成器用来创建组件文件的路径;
  • 项目是项目本身,将在其中生成组件。

将更多参数添加到将来需要的文件中。


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

  • 这里的模块将存储指向其中将包含组件的模块或组件模块的链接;
  • componentModule有一个标志是否为组件创建自己的模块(然后我得出结论,它将始终被创建并将其设置为true);
  • 导出:这是是否从要导入组件模块的模块中导出的标志;

接下来,我们使用组件的参数schema.d.ts文件创建一个接口。


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

在其中,属性复制了schema.json中的属性。 接下来,准备我们的工厂,转到index.ts文件。 在其中,我们创建两个filterTemplates函数,这些函数将负责根据componentModule的值为组件创建一个模块,以及setupOptions,这将设置工厂所需的参数。


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

接下来,我们编写main函数:


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

工厂已经准备就绪,它可以通过处理files文件夹中的模板来生成组件文件,该文件夹尚不可用。 没关系,就我而言,我们在方案文件夹中创建一个bempug-component文件文件夹。 在文件文件夹中,创建文件夹__name@dasherize__ ,在生成过程中,工厂将__name@dasherize__替换为组件名称。


接下来,在__name@dasherize__创建文件


  • __name@dasherize__帕格组件模板
  • __name@dasherize__组件的单元测试文件
  • 组件本身的__name@dasherize__文件
  • __name@dasherize__组件模块
  • __name@dasherize__组件样式表

现在,我们将向工厂添加对更新模块的支持,为此,我们将创建add-to-module-context.ts文件,以存储工厂使用该模块所需的参数。


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

将模块支持添加到工厂。


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

现在,将-m <模块参考>参数添加到cli命令时,我们的组件模块将在指定的模块中添加导入,并在添加–export标志时从其添加导出。 接下来,我们添加BEM支持。 为此,我获取了npm bempug包的源代码,并将代码制作在一个bempugMixin.pug文件中,该文件放置在common文件夹中,并放置在另一个common文件夹中,以便将mixin复制到有角度的项目中的common文件夹中。


我们的任务是在每个模板文件中都连接该mixin,并且在生成新组件时不进行重复,为此,我们将此功能添加到了工厂中。


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

是时候开始填写我们的模板文件了。


__name@dasherize__.component.pug


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

生成期间在<%=%>中指定的内容将替换为组件名称。


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

在这种情况下,<%= classify(name)%>用于将名称转换为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 %>{ } 

我们使用命令``npm run build''来构建方案。


一切准备就绪,可以在项目中生成组件!


要进行检查,请返回我们的Angular项目并创建一个模块。
ng gm test-schema
接下来,我们执行``npm link <绝对路径到带有我们scheme的项目文件夹>'',以便将我们的scheme添加到项目的node_modules。


然后,我们使用ng g bempug-component:bempug-component test -m /src/app/test-schema/test-schema.module.ts –export尝试电路ng g bempug-component:bempug-component test -m /src/app/test-schema/test-schema.module.ts –export
我们的方案将创建一个组件,并通过export将其添加到指定的模块中。
该方案已准备就绪,您可以开始使用熟悉的技术制作该应用程序。


您可以在此处查看最终版本,该软件包也可在npm中获得


在创建方案时,我使用了有关该主题的文章,对作者表示感谢。



感谢您的关注,读到最后的每个人,您都是最好的!
而另一个激动人心的项目正等着我。 待会见!

Source: https://habr.com/ru/post/zh-CN469509/


All Articles