上一篇 我们讲述了基本用法,并且可以在执行命令的当前位置,使用模板创建我们所需的文件。但是它还有一个问题,就是当我们新建一个组件的时候,可能需要动态的加入到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

  1. {
  2. "$schema": "http://json-schema.org/schema",
  3. "id":"module-demo",
  4. "title":"添加到模块的Demo",
  5. "type":"object",
  6. "properties": {
  7. "name":{
  8. "type":"string",
  9. "description": "文件名",
  10. "$default":{
  11. "$source":"argv",
  12. "index":0
  13. }
  14. },
  15. "path": {
  16. "type": "string",
  17. "format": "path",
  18. "description": "文件路径,默认从cmd当前路径读取",
  19. "visible": false
  20. }
  21. }
  22. }

module-demo/schema.ts

  1. import { Path } from '@angular-devkit/core';
  2. export interface Schema {
  3. name: string;
  4. path: string;
  5. }

module-demo/files/name@dasherize.component.ts.template

  1. import {Component,OnDestroy} from "@angular/core";
  2. import {Subject} from "rxjs";
  3. @Component({
  4. template:"<h1> <%= classify(name) %> is work </h1>"
  5. })
  6. export class <%= classify(name) %>Component implements OnDestroy{
  7. constructor(){}
  8. /*
  9. * 销毁通知
  10. */
  11. destroy$:Subject<void> = new Subject();
  12. ngOnDestroy(){
  13. this.destroy$.next();
  14. this.destroy$.complete();
  15. }
  16. }

module-demo/index.ts

  1. import { strings } from '@angular-devkit/core';
  2. import { normalize } from 'path';
  3. import {
  4. apply,
  5. applyTemplates,
  6. chain,
  7. mergeWith,
  8. move,
  9. Rule,
  10. Tree,
  11. url,
  12. } from '@angular-devkit/schematics';
  13. import { Schema } from './schema';
  14. export function entry(options: Schema): Rule {
  15. return (host: Tree) => {
  16. // 首先我们默认路径已经OK了 通过ng g 命令拿到的就是我们当前的路径
  17. // 传统艺能 处理模板
  18. const templateSource = apply(url('./files'), [
  19. applyTemplates({
  20. classify: strings.classify,
  21. dasherize: strings.dasherize,
  22. name: options.name,
  23. }),
  24. move(normalize(options.path)),
  25. ]);
  26. // 返回一个rule
  27. return chain([mergeWith(templateSource)]);
  28. };
  29. }

此时这个原理图在打包发布后,是可以被使用的,它会在我们执行ng g 命令的目录下 创建一个组件。例如 ng g schmeatics-lib:module-demo test,就会创建一个叫 test.component.ts的组件文件。

操作NgModule

我们先来整理下思路

  1. 先找到距离当前路径最近的NgModule,查找思路 本级 .module -> 父级.module 直到递归到根
  2. 修改文件内容,在 declaration:[]数组中 加入我们的组件
  3. 保存变更后的内容到文件

下面我们直接来贴完整代码

  1. import { strings } from '@angular-devkit/core';
  2. import { normalize } from 'path';
  3. import {
  4. apply,
  5. applyTemplates,
  6. chain,
  7. mergeWith,
  8. move,
  9. Rule,
  10. Tree,
  11. url,
  12. } from '@angular-devkit/schematics';
  13. import { Schema } from './schema';
  14. // +++++++ 新引入的包们 +++++++++++++++
  15. import * as ts from 'typescript';
  16. import { createDefaultPath } from '@schematics/angular/utility/workspace';
  17. import {
  18. buildRelativePath,
  19. findModuleFromOptions,
  20. } from '@schematics/angular/utility/find-module';
  21. import { addDeclarationToModule } from '@schematics/angular/utility/ast-utils';
  22. import { InsertChange } from '@schematics/angular/utility/change';
  23. export function entry(options: Schema): Rule {
  24. return (host: Tree) => {
  25. // 首先我们默认路径已经OK了 通过ng g 命令拿到的就是我们当前的路径
  26. // 传统艺能 处理模板
  27. const templateSource = apply(url('./files'), [
  28. applyTemplates({
  29. classify: strings.classify,
  30. dasherize: strings.dasherize,
  31. name: options.name,
  32. }),
  33. move(normalize(options.path)),
  34. ]);
  35. // ++++++++++ 开始处理NgModule ++++++++++++
  36. // 利用findModuleFromOptions函数,从我们的文件树中查找最近的NgModule,如果
  37. const modulePath = findModuleFromOptions(host, {path:options.path});
  38. if(!modulePath){
  39. throw new Error('未找到有效的ng模块')
  40. }
  41. // 然后我们从这个路径中 读取文件的内容
  42. const moduleBuffer = host.read(modulePath.toString())
  43. if(!moduleBuffer){
  44. throw new Error('未找到有效的ng模块')
  45. }
  46. // 读取模块内容转换为纯文本
  47. const moduleContext = moduleBuffer.toString('utf-8')
  48. // 创建一个typescript的ast根节点,也代表一个源文件
  49. const sourceFile = ts.createSourceFile(
  50. modulePath, // 文件路径
  51. sourceText, // 文件内容
  52. ts.ScriptTarget.Latest, // 使用的ts版本
  53. true //是否设置为父级节点
  54. );
  55. // 我们预期生成的组件路径
  56. const componentPath = `/${options.path}/${strings.dasherize(
  57. options.name
  58. )}.component`;
  59. // 使用buildRelativePath函数 计算相对于模块路径的相对路径
  60. const relativePath = buildRelativePath(modulePath, filePath);
  61. // 我们的组件名
  62. const componentName = `${strings.classify(options.name)}Component`;
  63. // 现在我们要用手头这些条件,去更新我们的虚拟文件树Tree的内容了
  64. // 我们使用了addDeclarationToModule辅助函数 添加组件到declarations数组中,并返回发生变更的部分
  65. const declarationChanges = addDeclarationToModule(
  66. sourceFile, // 我们用ts compile创建的 源文件 (ast树根节点)
  67. modulePath, // 真实的模块路径
  68. componentName, // 组件名
  69. relativePath // 组件相对于modulePath的路径
  70. );
  71. // 准备更新虚拟文件树,拿到原始的文件记录
  72. const declarationRecords = host.beginUpdate(modulePath);
  73. // 在指定位置进行差异更新
  74. for (const change of declerationChanges) {
  75. if (change instanceof InsertChange) {
  76. //这里把我们新增的内容,添加到了记录的左边
  77. decleartionRecords.insertLeft(change.pos, change.toAdd);
  78. }
  79. }
  80. // 提交变更
  81. host.commitUpdate(decleartionRecords);
  82. // 合并所有的规则为一个规则
  83. return chain([mergeWith(templateSource),()=>host]);
  84. };
  85. }

此时我们就具备将我们新建的组件写入到指定NgModule的能力了。

我们在运行 ng g 时,会为我们更新NgModule,运行结果如下

  1. ## dry run的方式运行
  2. ng g schematics-demo test --dry-run
  3. # 输出结果
  4. CREATE projects/demo/src/app/test.component.ts (507 bytes)
  5. UPDATE projects/demo/src/app/app.module.ts (383 bytes)

结语

angular的工具函数,可以让我们很方便的操作符合angular书写风格的文件,动态的添加我们的代码。 但是如果我们想做一些更有趣的事情,还需要了解很多额外的知识,比如typescript编译等等。所以写一个高质量的原理图需要学习的东西还很多,我们不能因为可以做出来一部分功能就懈怠。