上一篇 我们讲述了基本用法,并且可以在执行命令的当前位置,使用模板创建我们所需的文件。但是它还有一个问题,就是当我们新建一个组件的时候,可能需要动态的加入到NgModule的 declarations 或者 exports 中。这篇我们就来讲述如何在创建完模板文件之后,如何将他们加入到最近的NgModule中。
相关依赖包
@angular-devkit/core
Angular开发工具辅助函数集合,其中包括我们用于处理字符串的strings 处理路径的 normalize 以及其他辅助函数
@angular-devkit/schematics
关于Schematics的所有定义和操作函数,都在这个包里。比如 Tree 、Rule、Source 以及 chain apply等。
@schematics/angualr
这个包是angular默认schematics的实现,其中的utility包为我们提供了大量辅助函数,以及常用schema定义。
typescript
对,没错,就是你认识的那个typescript。它除了使用tsc命令编译我们的文件,也可以被引入进行动态的创建、编译、操作ts文件。
一个新的原理图
为了更好的演示,我们这里新建了一个叫module-demo的原理图,将它加入到我们的collections.json中。
module-demo/schema.json
{"$schema": "http://json-schema.org/schema","id":"module-demo","title":"添加到模块的Demo","type":"object","properties": {"name":{"type":"string","description": "文件名","$default":{"$source":"argv","index":0}},"path": {"type": "string","format": "path","description": "文件路径,默认从cmd当前路径读取","visible": false}}}
module-demo/schema.ts
import { Path } from '@angular-devkit/core';export interface Schema {name: string;path: string;}
module-demo/files/name@dasherize.component.ts.template
import {Component,OnDestroy} from "@angular/core";import {Subject} from "rxjs";@Component({template:"<h1> <%= classify(name) %> is work </h1>"})export class <%= classify(name) %>Component implements OnDestroy{constructor(){}/** 销毁通知*/destroy$:Subject<void> = new Subject();ngOnDestroy(){this.destroy$.next();this.destroy$.complete();}}
module-demo/index.ts
import { strings } from '@angular-devkit/core';import { normalize } from 'path';import {apply,applyTemplates,chain,mergeWith,move,Rule,Tree,url,} from '@angular-devkit/schematics';import { Schema } from './schema';export function entry(options: Schema): Rule {return (host: Tree) => {// 首先我们默认路径已经OK了 通过ng g 命令拿到的就是我们当前的路径// 传统艺能 处理模板const templateSource = apply(url('./files'), [applyTemplates({classify: strings.classify,dasherize: strings.dasherize,name: options.name,}),move(normalize(options.path)),]);// 返回一个rulereturn chain([mergeWith(templateSource)]);};}
此时这个原理图在打包发布后,是可以被使用的,它会在我们执行ng g 命令的目录下 创建一个组件。例如 ng g schmeatics-lib:module-demo test,就会创建一个叫 test.component.ts的组件文件。
操作NgModule
我们先来整理下思路
- 先找到距离当前路径最近的NgModule,查找思路 本级 .module -> 父级.module 直到递归到根
- 修改文件内容,在 declaration:[]数组中 加入我们的组件
- 保存变更后的内容到文件
下面我们直接来贴完整代码
import { strings } from '@angular-devkit/core';import { normalize } from 'path';import {apply,applyTemplates,chain,mergeWith,move,Rule,Tree,url,} from '@angular-devkit/schematics';import { Schema } from './schema';// +++++++ 新引入的包们 +++++++++++++++import * as ts from 'typescript';import { createDefaultPath } from '@schematics/angular/utility/workspace';import {buildRelativePath,findModuleFromOptions,} from '@schematics/angular/utility/find-module';import { addDeclarationToModule } from '@schematics/angular/utility/ast-utils';import { InsertChange } from '@schematics/angular/utility/change';export function entry(options: Schema): Rule {return (host: Tree) => {// 首先我们默认路径已经OK了 通过ng g 命令拿到的就是我们当前的路径// 传统艺能 处理模板const templateSource = apply(url('./files'), [applyTemplates({classify: strings.classify,dasherize: strings.dasherize,name: options.name,}),move(normalize(options.path)),]);// ++++++++++ 开始处理NgModule ++++++++++++// 利用findModuleFromOptions函数,从我们的文件树中查找最近的NgModule,如果const modulePath = findModuleFromOptions(host, {path:options.path});if(!modulePath){throw new Error('未找到有效的ng模块')}// 然后我们从这个路径中 读取文件的内容const moduleBuffer = host.read(modulePath.toString())if(!moduleBuffer){throw new Error('未找到有效的ng模块')}// 读取模块内容转换为纯文本const moduleContext = moduleBuffer.toString('utf-8')// 创建一个typescript的ast根节点,也代表一个源文件const sourceFile = ts.createSourceFile(modulePath, // 文件路径sourceText, // 文件内容ts.ScriptTarget.Latest, // 使用的ts版本true //是否设置为父级节点);// 我们预期生成的组件路径const componentPath = `/${options.path}/${strings.dasherize(options.name)}.component`;// 使用buildRelativePath函数 计算相对于模块路径的相对路径const relativePath = buildRelativePath(modulePath, filePath);// 我们的组件名const componentName = `${strings.classify(options.name)}Component`;// 现在我们要用手头这些条件,去更新我们的虚拟文件树Tree的内容了// 我们使用了addDeclarationToModule辅助函数 添加组件到declarations数组中,并返回发生变更的部分const declarationChanges = addDeclarationToModule(sourceFile, // 我们用ts compile创建的 源文件 (ast树根节点)modulePath, // 真实的模块路径componentName, // 组件名relativePath // 组件相对于modulePath的路径);// 准备更新虚拟文件树,拿到原始的文件记录const declarationRecords = host.beginUpdate(modulePath);// 在指定位置进行差异更新for (const change of declerationChanges) {if (change instanceof InsertChange) {//这里把我们新增的内容,添加到了记录的左边decleartionRecords.insertLeft(change.pos, change.toAdd);}}// 提交变更host.commitUpdate(decleartionRecords);// 合并所有的规则为一个规则return chain([mergeWith(templateSource),()=>host]);};}
此时我们就具备将我们新建的组件写入到指定NgModule的能力了。
我们在运行 ng g 时,会为我们更新NgModule,运行结果如下
## dry run的方式运行ng g schematics-demo test --dry-run# 输出结果CREATE projects/demo/src/app/test.component.ts (507 bytes)UPDATE projects/demo/src/app/app.module.ts (383 bytes)
结语
angular的工具函数,可以让我们很方便的操作符合angular书写风格的文件,动态的添加我们的代码。 但是如果我们想做一些更有趣的事情,还需要了解很多额外的知识,比如typescript编译等等。所以写一个高质量的原理图需要学习的东西还很多,我们不能因为可以做出来一部分功能就懈怠。
