Angular除了框架本身的优秀外,它的Cli工具也是强大的一批。其中原理图大家都用过,但并不是每个人都下功夫去了解过它。

那么原理图到底都能做什么呢?它可以做到以下几件事:

  • 为Angular工程添加库
  • 升级Angular工程中的库
  • 生成代码

对,你没有看错,我们常用的ng new 、ng update 、 ng generate命令,都在使用原理图为我们生成文件。所以原理图的本体,就是一段代码生成器!!!
作为一条外包狗,日常crud码字工。在日常工作中如果可以合理的利用原理图来生成代码,可以有效提高50%左右的工作效率,降低浪费在重复代码上的时间!!!

原理图基础

为了便于我们创建属于自己的原理图,angular为我们提供了用于创建和调试原理图的cli,我们需要先安装它,基础部分的示例都基于schematics-cli进行讲述

  1. # 安装原理图的cli
  2. npm install -g @angular-devkit/schematics-cli
  3. # 你可以使用它创建新的原理图
  4. schematics blank [你的原理图名字]
  5. # 也可以用他执行调试已有的原理图
  6. schematics [你原理图的路径]:[原理图的名称] [arguments]

使用schematic-cli创建出来的原理图大概结构如下
image.png

这里我们需要了先解下它的构成

collection.json

这是一个原理图的集合,其中记录了我们提供的所有原理图,其文件内容如下

  1. {
  2. // 基础结构
  3. "$schema": "../node_modules/@angular-devkit/schematics/collection-schema.json",
  4. // 原理图集合
  5. "schematics": {
  6. // 一个叫demo的原理图
  7. "demo": {
  8. // 它的说明
  9. "description": "一个演示用的原理图",
  10. // 工厂函数 格式为 [文件路径]#入口函数名
  11. "factory": "./demo/index#entry",
  12. // 我们自定义的schema结构,schema用于规定参数以及交互
  13. "schema": "./demo/schema.json"
  14. }
  15. }
  16. }

[原理图]/index.ts

这是原理图的实现,其导出一个产生规则的入口函数,文件内容如下

  1. import { Schema } from './schema';
  2. import { Rule, SchematicContext, Tree } from '@angular-devkit/schematics';
  3. /**
  4. * 入口函数
  5. * 可以拿到Schema提供的参数信息
  6. * 并返回一个Rule(规则)
  7. */
  8. export function entry(options: Schema): Rule {
  9. // 这是Rule的定义 (tree:Tree,context:SchematicContext)=> Tree|Rule
  10. return (tree: Tree, context: SchematicContext) => {
  11. return tree;
  12. };
  13. }

我们原理图的最终实现,都会在这个函数里,这里相关的概念后边会慢慢讲,我们先来看下schema.json与schema.ts

schema.json

  1. {
  2. // 同样 定义了基础结构的来源
  3. "$schema": "http://json-schema.org/schema",
  4. // 这个schema的唯一标识
  5. "id":"--demo-schematic",
  6. // 应用help函数时,显示的title
  7. "title": "一个演示用的原理图",
  8. // 属性列表,会映射到入口函数的options
  9. "properties": {
  10. // 参数名称
  11. "name":{
  12. // 参数说明
  13. "description": "文件名",
  14. // 参数类型,支持 boolean number string array object
  15. "type":"string",
  16. // 默认值
  17. "$default":{
  18. // 数据源 来自标准输入的参数列表
  19. "$source":"argv",
  20. // 索引1
  21. "index":0
  22. }
  23. }
  24. }
  25. }

schema.json的存在,会让我们在使用原理图的时候交互更加友好。比如提供了帮助文档,改变了参数的传入方式,以及可以进行部分交互操作,比如从列表中选择等。当然schema.json是可选的,如果不使用,我们仍然可以使用 —[参数名]=[值] 的方式去传递我们的参数。
schema.ts 则只是个简单的ts类型定义,帮助我们在开发中,使用参数。其内容如下

  1. /*
  2. * 对应schema.json的定义
  3. */
  4. export interface Schema {
  5. // 文件名
  6. name: string;
  7. }

以上为一个完整原理图需要的相关文件,下面我们来看一下实现原理图需要了解的一些相关定义。

Rule(规则)

我们的每一个原理图,都是一个规则,在这个规则之后,可能合并了多个规则。最终原理图的运行,就是依赖规则进行的。
Rule本身是一个函数,它接受tree和context做为参数,并在最后期望返回Tree对象或者一个新的Rule函数,它的定义如下:

  1. export declare type Rule = (tree: Tree, context: SchematicContext) => Tree | Observable<Tree> | Rule | Promise<void | Rule> | void;

在这个函数中,我们通过操作tree 或者 合并其他rule, 来实现我们的逻辑

Tree

tree是一个虚拟的文件树,它为我们提供了操作文件需要的方法。在tree上发生的变化,都会反映在tree上,但是不会发生在真正的文件系统中。
tree的定义如下:

  1. export interface Tree {
  2. // 产生一个新的tree
  3. branch(): Tree;
  4. // 合并另一个tree
  5. merge(other: Tree, strategy?: MergeStrategy): void;
  6. // 只读 树根,也就是入口文件夹
  7. readonly root: DirEntry;
  8. // 在文件树中读取某一段内容
  9. read(path: string): Buffer | null;
  10. // 判断是否在文件树中已经存在
  11. exists(path: string): boolean;
  12. // 获取某个文件
  13. get(path: string): FileEntry | null;
  14. // 获取某个目录
  15. getDir(path: string): DirEntry;
  16. // 访问某个文件
  17. visit(visitor: FileVisitor): void;
  18. // 覆盖某个文件
  19. overwrite(path: string, content: Buffer | string): void;
  20. // 更新
  21. beginUpdate(path: string): UpdateRecorder;
  22. // 提交更新
  23. commitUpdate(record: UpdateRecorder): void;
  24. // 创建一个虚拟文件
  25. create(path: string, content: Buffer | string): void;
  26. // 删除某个虚拟文件
  27. delete(path: string): void;
  28. // 抽命名某个虚拟文件
  29. rename(from: string, to: string): void;
  30. // 应用某个动作
  31. apply(action: Action, strategy?: MergeStrategy): void;
  32. // 动作列表
  33. readonly actions: Action[];
  34. }

SchematicsContext

schematicsContext为我们提供了原理图的执行上下文,它提供了一些全局配置,以及一些相关操作。这里我们比较常用的是其中的logger。

完善我们的入口函数

认识了Rule和Tree 我们就可以为我们的入口函数,增加一个生成html文件的逻辑了

  1. /**
  2. * 入口函数
  3. * 可以拿到Schema提供的参数信息
  4. * 并返回一个Rule(规则)
  5. *
  6. * @param options
  7. */
  8. export function entry(options: Schema): Rule {
  9. return (tree: Tree, context: SchematicContext) => {
  10. // 使用logger输出一个信息
  11. context.logger.info(`create file name:${options.name}`);
  12. // 我们使用tree 创建一个
  13. tree.create(`./${options.name}.html`, '<h1>hello-world</h1>');
  14. return tree;
  15. };
  16. }

调试模式运行原理图

  1. # 编译我们的原理图
  2. yarn run build // 实际为执行 tsc -p tsconfig.schematics.json
  3. # 调试模式运行原理图
  4. schematics ./schematics:hello test --dry-run=true
  5. # 输出内容
  6. # create file name:test
  7. # CREATE test.html (20 bytes)

此时我们已经可以看到原理图会创建一个叫 test.html的文件。至此我们的Demo原理图,已经实现了。
但是它还存在很多问题,比如并不会真正创建文件,比如创建后的文件路径问题,以及发布问题,等等,下面我们直接在angular库中创建一个原理图,并将其打包发布。

在Angular库中实现一个完整的原理图

angular的官方文档实际上介绍了如何创建一个库中的原理图,但是文档中的东西…… emm 总会有很多奇怪的情况……

创建项目

  1. # 创建一个叫demo的 angular项目
  2. ng new demo
  3. # 创建一个叫schematics-lib的库 一会我们的原理图就实现在里面
  4. cd ./demo && ng g lib schematics-lib

创建原理图

现在在我们的库项目中创建一个叫schematics的文件夹,其内容格式与我们之前讲的原理图结构相同。
完整的原理图实现代码如下:

collection.json
  1. {
  2. "$schema": "../../../node_modules/@angular-devkit/schematics/collection-schema.json",
  3. "schematics": {
  4. "demo": {
  5. "description": "一个演示用的原理图",
  6. "factory": "./demo/index#entry",
  7. "schema": "./demo/schema.json"
  8. }
  9. }
  10. }

demo/index.ts
  1. import { Schema } from './schema';
  2. import { Rule, SchematicContext, Tree } from '@angular-devkit/schematics';
  3. /**
  4. * 入口函数
  5. * 可以拿到Schema提供的参数信息
  6. * 并返回一个Rule(规则)
  7. *
  8. * @param options
  9. */
  10. export function entry(options: Schema): Rule {
  11. return (tree: Tree, context: SchematicContext) => {
  12. // 使用logger输出一个信息
  13. context.logger.info(`create file name:${options.name}`);
  14. tree.create(`./${options.name}.html`, '<h1>hello-world</h1>');
  15. return tree;
  16. };
  17. }

demo/schema.json
  1. {
  2. "$schema": "http://json-schema.org/schema",
  3. "id":"demo-schematic",
  4. "title": "一个演示用的原理图",
  5. "properties": {
  6. "name":{
  7. "description": "文件名",
  8. "type":"string",
  9. "$default":{
  10. "$source":"argv",
  11. "index":0
  12. }
  13. }
  14. }
  15. }

demo/schema.ts
  1. export interface Schema {
  2. name: string;
  3. }

由于我们这次是手动创建的原理图,因此我们需要自己新建一个tsconfig.schematics.json,代码如下

  1. {
  2. "compilerOptions": {
  3. "baseUrl": ".",
  4. "lib": [
  5. "es2018",
  6. "dom"
  7. ],
  8. "declaration": true,
  9. "module": "commonjs",
  10. "moduleResolution": "node",
  11. "noEmitOnError": true,
  12. "noFallthroughCasesInSwitch": true,
  13. "noImplicitAny": true,
  14. "noImplicitThis": true,
  15. "noUnusedParameters": true,
  16. "noUnusedLocals": true,
  17. "rootDir": "schematics",
  18. // 注意这里 我们需要配置ts文件编译后输出的位置
  19. "outDir": "../../dist/schematics-lib/schematics",
  20. "skipDefaultLibCheck": true,
  21. "skipLibCheck": true,
  22. "sourceMap": true,
  23. "strictNullChecks": true,
  24. "target": "es6",
  25. "types": [
  26. "jasmine",
  27. "node"
  28. ]
  29. },
  30. "include": [
  31. "schematics/**/*"
  32. ],
  33. "exclude": [
  34. "schematics/*/files/**/*"
  35. ]
  36. }

以及还需要在库项目的package.json中添加schematics的路径配置

  1. {
  2. // 指向我们的原理图集合
  3. "schematics": "./schematics/collection.json"
  4. }

最后还要在库的package.json中配置一下scripts,以便于自动拷贝相关文件,这里有一个坑,就是官方文档给出的脚本只能运行在linux上,而mac和windows由于系统命令的不同,需要自行替换
这里贴出官方原版

  1. {
  2. "scripts": {
  3. "build": "../../node_modules/.bin/tsc -p tsconfig.schematics.json",
  4. "copy:schemas": "cp --parents schematics/*/schema.json ../../dist/my-lib/",
  5. "copy:collection": "cp schematics/collection.json ../../dist/my-lib/schematics/collection.json",
  6. "postbuild": "npm run copy:schemas && npm run copy:collection"
  7. },
  8. // 指向我们的原理图集合
  9. "schematics": "./schematics/collection.json"
  10. }

而我们的mac版本是这样的 用rsync 替换掉 cp —parents

  1. {
  2. "scripts": {
  3. "build": "ng build schematics-lib --prod && ../../node_modules/.bin/tsc -p tsconfig.schematics.json",
  4. "copy:schemas": "rsync -R schematics/*/schema.json ../../dist/schematics-lib/",
  5. "copy:collection": "cp schematics/collection.json ../../dist/schematics-lib/schematics/collection.json",
  6. "postbuild": "npm run copy:schemas && npm run copy:collection"
  7. },
  8. // 指向我们的原理图集合
  9. "schematics": "./schematics/collection.json"
  10. }

完成以上步骤之后 我们可以在库项目的目录下 运行 npm run build了。
一切完成之后,我们可以看到在demo项目的 /dist文件夹下已经有了schematics-lib文件夹 大概像这样
image.png

现在我们一定十分想再我们的angular项目中,使用我们的原理图了吧?

如果我们在项目根目录运行 ng g 那么我们的命令大概是这样的

  1. ng g ./dist/schematics-lib:demo test

而我们在 /src/app下运行的话 则命令变成了这样

  1. ng g ../../dist/schematics-lib:demo test

这样的使用方式恶心吗? 恶心,那是相当恶心了。 怎么解决呢?两种方式

  1. 将我们的dist 发布到npm上 , 在目标项目中 install回来
  2. 使用npm link 创建一个链接

这里我们来看一下npm link这个命令

npm link

npm link这个命令,会将当前文件夹在全局的node_modules文件夹下创建一个链接,使用方式像这样

  1. # 到 dist/schematics-lib目录下
  2. cd dist/schematics-lib
  3. # 创建链接
  4. npm link
  5. # 这时候会再node_modules里 生成一个叫 schematics-lib的链接 指向当前文件夹

然后在我们目标的项目下 链接这个链接

  1. # 退到我们的angular项目文件夹
  2. cd ../../
  3. # 创建与schematics-lib的链接
  4. npm link schematics-lib

此时我们就可以愉快的使用我们刚刚创建的原理图了,不管在哪个文件夹下 我们都可以使用

  1. ng g schematics-lib:demo test

来创建我们的 test.html了。

路径问题

等等? 为什么我们的文件夹 永远与angular.json同级? 看看我们刚刚入口函数中的代码

  1. tree.create(`./${options.name}.html`, '<h1>hello-world</h1>');

这里我们相对于tree的root,创建了一个文件。 然而这个tree的root,就是与angular.json平级的……
因此我们需要通过别的方式获取我们当前执行命令行的路径,这里我们可以在schema.json中,配置一个path

  1. {
  2. "properties": {
  3. "name":{
  4. "description": "文件名",
  5. "type":"string",
  6. "$default":{
  7. "$source":"argv",
  8. "index":0
  9. }
  10. },
  11. "path":{
  12. "type": "string",
  13. "format": "path",
  14. "description": "当前路径",
  15. "visible": false
  16. }
  17. }
  18. }

这个path 是通过format方式获取的,我们在命令行中不能通过传参的方式进行修改,同时它对于最终用户也是不可见的。

另外我们还需要更新我们的schema.ts 与入口函数

  1. // schema.ts
  2. export interface Schema {
  3. name: string;
  4. path: string;
  5. }
  6. // index.ts
  7. export function entry(options: Schema): Rule {
  8. return (tree: Tree, context: SchematicContext) => {
  9. tree.create(`${options.path}/${options.name}.html`, '<h1>hello-world</h1>');
  10. return tree;
  11. };
  12. }

我们再重新build,并且使用 ng g命令运行,我们会发现我们的文件已经可以输出到指定目录下了。

使用模板文件

我们解决了生成文件的路径问题,但是还是很不爽,因为我们有时候不是简简单单的只生成一个只有一句话的文件,我们可能要生成一系列的模板,这时候,我们就需要创建属于自己的模板文件了。

现在,我们在demo这个原理图下,创建一个files文件夹。为什么叫files呢?这是我们在刚刚的tsconfig中的约定,排除掉了/files文件夹中文件的编译。当然你也可以改成其他名字,只要同步修改tsconfig.json就可以了

我们的模板文件名字,也是可以替换的,它的约定有如下两种

  • name
  • name@dasherize

第一种,就是把你提供的name字符串原样替换,第二种,则将大写字母 替换成 -小写 的形式

下面我们来建2个模板文件
文件:name@dasherize.component.html.template

  1. <h1> <%= classify(name) %> </h1>

文件:name@dasherize.component.ts.template

  1. import {Component} from "@angular/core"
  2. @Component({
  3. templateUrl:"./<%= dasherize(name) %>.component.html",
  4. })
  5. export class <%= classify(name) %>Component{
  6. }

在这里 我们看到了几个特殊的语法

  • <%= %>
  • classify() dasherize() 等等

其中 <%= %>语法为变量绑定语法,用于绑定传入模板的变量值,还有更多的绑定语法,请参照官方文档。
而classify这些函数,则是处理字符串的函数,他们都是传入模板的变量。

下面我们来看看入口函数如何应用模板:

  1. import { Schema } from './schema';
  2. import {
  3. apply,
  4. applyTemplates,
  5. mergeWith,
  6. move,
  7. Rule,
  8. SchematicContext,
  9. Tree,
  10. url,
  11. } from '@angular-devkit/schematics';
  12. import { strings, normalize } from '@angular-devkit/core';
  13. /**
  14. * 入口函数
  15. * 可以拿到Schema提供的参数信息
  16. * 并返回一个Rule(规则)
  17. *
  18. * @param options
  19. */
  20. export function entry(options: Schema): Rule {
  21. return (tree: Tree, context: SchematicContext) => {
  22. // 使用logger输出一个信息
  23. context.logger.info(`create file name:${options.name}`);
  24. // 模板文件的源
  25. const templateSource = url('./files');
  26. // 模板应用的规则列表
  27. const templateRules = [
  28. // 应用到模板中的变量
  29. applyTemplates({
  30. // 对应模板中的classify 用于将 -小写 转换为大写
  31. classify: strings.classify,
  32. // 对应模板中的dasherize 用于将大写字母转换为 -小写
  33. dasherize: strings.dasherize,
  34. // name变量
  35. name: options.name,
  36. }),
  37. // 移动模板文件到指定位置
  38. move(normalize(options.path)),
  39. ];
  40. // 应用规则到source 并返回新的source
  41. const templateApplySource = apply(templateSource, templateRules);
  42. // 应用合并后的规则并返回tree
  43. return mergeWith(templateApplySource)(tree, context);
  44. };
  45. }

这里面 我们用到了很多辅助函数,帮助我们加载文件,处理相关规则。
用于操作tree 和rule的函数 都位于 @angular-devkit/schematics 这个包中。
而@angular-devkit/core这个包,则提供了大量用于操作路径、字符串、工作空间的辅助函数。

至此我们基于angular库的原理图,已经可以正常的跑起来了。虽然它没有那么完美,比如将我们生成的组件自动导入NgModule中。但是它已经可以生成绝大多数模板代码了,不是吗?

无论如何,我们需要记住。angular的原理图,是跑在node环境下的。因此node的包都可以安装进来使用,这就让原理图这个代码生成器有了无限的可能。

本人也是初学者,从入坑原理图,到写出这篇文章,整整在里面坑了三天,因此也希望更多的人可以对原理图感兴趣,并且共同交流,用好这个angular为我们带来的强大工具。