0%

angular 自定义shema (一)

Angular的一个强大的功能是丰富而有效的Angular Cli,它可以自动生成脚手架程序,也可以生成各种各样的组件,甚至我们可以自己定义原理图来生成我们特定的东西。功能比较强大,所以也比较难一点,我这里分为三篇博客,也是我的一个学习历程,这是第一篇,学习基本的原理图配置和使用。

创建原理图项目

首先安装angular/schematics

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

然后我们创建自己的shema项目:

1
schematics blank --name=tcs-schema

可以看到,cli帮我们生成了好些文件:

1
2
3
4
5
6
7
8
CREATE /tcs-schema/README.md (639 bytes)
CREATE /tcs-schema/.gitignore (191 bytes)
CREATE /tcs-schema/.npmignore (64 bytes)
CREATE /tcs-schema/package.json (537 bytes)
CREATE /tcs-schema/tsconfig.json (656 bytes)
CREATE /tcs-schema/src/collection.json (225 bytes)
CREATE /tcs-schema/src/tcs-schema/index.ts (316 bytes)
CREATE /tcs-schema/src/tcs-schema/index_spec.ts (470 bytes)

我们这个生成的是ts项目,所以运行的时候需要转为js文件来运行,我们加上一个npm运行命令,可以使它动态热更新:

1
2
3
4
5
"scripts": {
"build": "tsc -p tsconfig.json",
"build:watch": "tsc -p tsconfig.json --watch",
"test": "npm run build && jasmine src/**/*_spec.js"
},

我们加上了buid:watch命令,运行这个命令:

1
npm run build:watch

这个时候可以看到,我们项目文件中将ts文件转换为了js文件。这时候我们可以来通过schematics来运行我们的原理图:

1
schematics .:tcs-schema

可以看到输出:

1
Nothing to be done.

因为目前我们什么都没做,只是建了一个schema的项目而已。

也可以在外部的其他项目中,通过相对路径来使用我们的原理图,比如:

1
2
schematics ../tcs-schema/src/collection.json:tcs-schema
Nothing to be done.

来给我们的schema加点料。调整我们的./src/tcs-schema/index.ts中的规则(rule),使它创建一个hello.js文件,并写入一条语句:console.log('hello schema')。我们只需要使用tree对象的create()方法即可:

1
2
3
4
5
6
export function tcsSchema(_options: schemaOptions): Rule {
return (tree: Tree, _context: SchematicContext) => {
tree.create('hello.js', `console.log('hello schema')`);
return tree;
};
}

再次使用tcs-schema

1
schematics .:tcs-schema

发现有输出:

1
CREATE /hello.js (27 bytes)

可以看到成功创建了hello.js,但是找死也找不到这个文件啊,这是为啥?

是应为我们通过相对路径来使用这个原理图的包,所以在这种情况下原理图运行于debug模式。调试模式产生的行为与使用--dry-run标志运行原理图的行为相同,因此只是输出了操作的结果,但是没有实质的文件,不会对文件系统提交任何更改。

要想看到真实创建的文件,我们只需要加上--debug=false或者--dry-run=false参数即可。比如:

1
schematics .:tcs-schema --debug=false

可以看到在当前目录确实生成了hello.js文件,我们可以使用node来运行它:

1
node ./hello.js

可以看到正确的输出了我们的hello schema

原理图的参数

默认情况下,原理图将每个指定的标志参数传递给_options对象,比如我们希望传递一个name给我们的`tcs-schema。

我们只需要在index.ts中从_options中获取这个参数即可:

1
2
3
4
5
6
7
export function tcsSchema(_options: schemaOptions): Rule {
return (tree: Tree, _context: SchematicContext) => {
const {name} = _options;
tree.create('hello.js', `console.log('hello ${name}')`);
return tree;
};
}

然后我们先删除之前创建的hello.js文件,然后运行:

1
schematice .:tcs-schema --name=tony

创建成功后,运行node ./hello.js,可以看到正确的输出了hello tony

我们成功的传值给了我们的schema。我们还可以做得更好。

我们可以创建schema.json文件来描述我们原理图需要的参数,这样,原理图将对传递的选项进行验证,并提示我们应该传递什么必须的参数。

./src/tcs-schema文件夹里面建schema.json文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
"$schema": "http://json-schema.org/schema",
"id": "tcsSchema",
"title": "",
"type": "object",
"description": "custom schema",
"properties": {
"name": {
"type": "string",
"description": "name",
"$default": {
"$source": "argv",
"index": 0
}

}
},
"required": [
"name"
]
}

这个json里面定义了可以传递给原理图的所有参数,并且限定了这个参数的类型,以及这个参数是否必须的验证。同时我们给name属性加上了$defualt限制,这使得name属性变成了一个位置参数,即原理图会获取第一个参数作为name,我们无需加上--name这个标志也可以创建,也就是说,下面两种形式都是可以的:

1
2
schematics .:tcs-schema tony                                                               
schematics .:tcs-schema --name=tony

现在我们只是创建了schema.json文件,还没有和我们的原理图进行关联,还是无效的。

我们需要将schema.json关联到collection.json文件中去,修改./src/collection.json文件:

1
2
3
4
5
6
7
8
9
10
{
"$schema": "../node_modules/@angular-devkit/schematics/collection-schema.json",
"schematics": {
"tcs-schema": {
"description": "A blank schematic.",
"factory": "./tcs-schema/index#tcsSchema",
"schema": "./tcs-schema/schema.json"
}
}
}

现在,这个schemo.json中限定的参数就会起作用,让我们跑起来测试下:

1
schematics .:tcs-schema

像这样不带name参数执行的时候,会抛出一个错误:

1
2
3
4
5
An error occured:
Error: Schematic input does not validate against the Schema: {}
Errors:

Data path "" should have required property 'name'.

嗯,运行良好,当我们带着name参数的时候,会正常创建。这说明我们的参数验证起作用了。

现在我们需要创建一个interface,用来告诉我们的index.ts在创建原理图的时候会包含这些参数,创建文件./src/tcs-schema/schema.ts:

1
2
3
export interface schemaOptions{
name: string;
}

然后在index.ts中引入:

1
2
3
4
5
6
7
8
9
10
import { Rule, SchematicContext, Tree } from '@angular-devkit/schematics';
import { schemaOptions } from './schema';

export function tcsSchema(_options: schemaOptions): Rule {
return (tree: Tree, _context: SchematicContext) => {
const {name} = _options;
tree.create('hello.js', `console.log('hello ${name}')`);
return tree;
};
}

通过提示增强参数

指定的选项可以提高开发者的体验,我们可以提供有用的、良好的提示,这样可以更好的帮助他们理解这些参数对原理图的作用。比如前面我们的name参数,可以加个提示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
"$schema": "http://json-schema.org/schema",
"id": "tcsSchema",
"title": "",
"type": "object",
"description": "custom schema",
"properties": {
"name": {
"type": "string",
"description": "name",
"$default": {
"$source": "argv",
"index": 0
},
"x-prompt": "你想生成个啥?"
}
},
"required": [
"name"
]
}

这样,在当执行这个原理图的时候忘记输入了name,那么并不会报错,而是出现个提示项:

1
2
schematics .:tcs-schema
? 你想生成个啥?

这样开发者可以在提示的后面输入忘记的name,然后回车,即可正确的执行原理图。

使用原理图的模板

到现在为止,我们只是使用基本的JavaScript字符串模板生成我们的目标文件,这是基本操作,但是我们需要的是在我们的项目中生成文件,我们需要Angular Schematics

我们需要先建立一个files文件夹:./src/tcs-schemo/files。(为什么名字叫叫files?因为我们在创建项目的时候,自动生成的脚手架的tsconfig.json文件中有 "exclude": [ "src/*/files/**/*"],你想换其他名字也行,需要把tsconfig.json这里也同步改一下。)

./src/tcs-schema/files下面再创建一个文件hello-__name@dasherize__.ts

__双下滑线是一个分界符,用于将_options中传递过来的name和普通字符串区分开。dasherize是一个辅助函数,它将接收的name变量的值转换为kebab大小写字符串,而@是将变量应用于辅助函数的一种方式。

举个例子,假如我们提供的name变量的值为TonyBest,那么生成的文件为hello-tony-best.ts。就是这这么神奇。

现在,我们修改hello-__name@dasherize__.ts里面的内容:

1
console.log('hello, <%= name %>');

Angular Schematics的模板语法由<%=%>标记组成,用于输出变量的值。它还支持诸如dasherize或者classify之类的函数调用,用以在模板内部调整变量的值。例如:

1
2
3
4
5
@Component({
selector: 'hello-<%= dasherize(name) %>',
})
export class Hello<%= classify(name) %>Component {
}

ok,现在模板文件已准备好,我们需要再原理图规则内部进行关联。

调整./src/tcs-schema/index.ts文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { Rule, url, apply, template, mergeWith } from '@angular-devkit/schematics';
import { schemaOptions } from './schema';
import { strings } from '@angular-devkit/core';

export function tcsSchema(_options: schemaOptions): Rule {
const sourceTemplates = url('./files');
const sourceParametrizedTemplates = apply(sourceTemplates, [
template({
..._options,
...strings,
})
]);
return mergeWith(sourceParametrizedTemplates);
}

先不去理会这一堆代码的意思,我们先跑一下结果:

1
schematics .:tcs-schema 'tony best' --debug=false

注意:这里name参数使用引号是因为我们要输入空格隔开的单词,如果我们的name是一个单词,那么没必要使用引号。

确实生成了文件hello-tony-best.ts,然后查看生成的文件内容:

1
2
3
4
5
@Component({
selector: 'hello-tony-best',
})
export class HelloTonyBestComponent {
}

nice啊~

模板辅助函数

我们可以在这里使用模板辅助函数比如dasherizeclassify是因为我们将strings对象解构后传递给了模板。

这意味着我们可以传递任何方法给模板。比如,我们希望在name的值后面加上感叹号,那么我们可以创建一个模板辅助函数:addExclamation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function addExclamation(value: string): string {
return value + '!';
}

export function tcsSchema(_options: schemaOptions): Rule {
const sourceTemplates = url('./files');
const sourceParametrizedTemplates = apply(sourceTemplates, [
template({
..._options,
...strings,
addExclamation,
})
]);
return mergeWith(sourceParametrizedTemplates);
}

然后在模板中使用:

1
2
3
4
5
6
@Component({
selector: 'hello-<%= dasherize(name) %>',
})
export class Hello<%= classify(name) %>Component {
<%= addExclamation(name) %>
}

然后可以看到生成的文件中:

1
2
3
4
5
6
@Component({
selector: 'hello-tony-best',
})
export class HelloTonyBest Component {
tony best!
}

angular-devkit/core包的strings里面包含有丰富的模板辅助函数,后续可以探索一下。

Angular Schematics 模板语法

我们目前使用了<%=%>标签语法可以用来输出变量的值。我们可以使用模板语法包括条件判断(if / else)甚至于循环(for...of

需要注意的是,控制语句的标签是<%%>,输出值的标签是<%=%>,他们是有区别的。

Angular Schematics模板似乎支持所有的JavaScript语言功能,所以我们甚至可以传递数组?

那假设我们需要接受一个书籍变量为books,这个books是一个字符串的数组,我们需要在schema.json中定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
{
"$schema": "http://json-schema.org/schema",
"id": "tcsSchema",
"title": "",
"type": "object",
"description": "custom schema",
"properties": {
"name": {
"type": "string",
"description": "name",
"$default": {
"$source": "argv",
"index": 0
},
"x-prompt": "你想生成个啥?"

},
"books": {
"type": "array",
"items": {
"type": "string"
},
"x-prompt": {
"message": "需要哪种类型的书?",
"type": "list",
"items": [
{
"value": ["CSS初级", "CSS高级"],
"label": "CSS"
},
{
"value": ["HTML初级", "HTML高级"],
"label": "HTML"
}
]
}
}
},
"required": [
"name",
"books"
]
}

然后在schema.ts中添加books的类型:

1
2
3
4
export interface schemaOptions{
name: string;
book: string[];
}

然后在模板语法中进行处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Component({
selector: 'hello-<%= dasherize(name) %>',
template: `
<h1>book list</h1>
<% if (books && books.length) { %>
<ul>
<% for (let b of books) { %>
<li><%= b %>
<% } %>
</ul>
<% } %>
`
})
export class Hello<%= classify(name) %> Component {
<%= addExclamation(name) %>
}

最后运行:

1
2
3
4
$ schematics .:tcs-schema --debug=false                                                   
? 你想生成个啥? tony
? 需要哪种类型的书? CSS
CREATE /hello-tony.ts (291 bytes)

查看生成的文件hello-tony.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
@Component({
selector: 'hello-tony',
template: `
<h1>book list</h1>
<ul>
<li>CSS初级
<li>CSS高级
</ul>
`
})
export class HelloTony Component {
tony!
}

Good!

与Angular Cli的集成

Angular Cli有一些特定的环境因素,需要对其处理,才能使我们的自定义原理图与Angular Cli可以无缝集成。

每当我们运行ng generate component的时候,我们是在某个项目中运行并生成对应组件。angular.json文件包含所有工作去项目的定义以及defaultProject属性,它将确定我们创建的组件的位置。我们可以通过在ng generate命令中显式指定--project some-app来覆盖它。

我们需要修改schema.json以接受project参数:

1
2
3
4
5
6
7
8
9
10
11
{
"$schema": "http://json-schema.org/schema",
"...": "...",
"properties": {
"...": "...",
"project": {
"type": "string",
"description": "Generation in spacific Angular Cli workspace project"
}
}
}

同时修改schema.ts

1
2
3
4
export interface schemaOptions{
name: string;
project?: string;
}

现在我们可以对原理图的规则进行调整了。我们需要做的有:

  1. 需要从angular.json文件中获取到工作区的配置。这个配置可以让我们访问到defaultProjectprojects对象。
  2. 用他们来检索特定的项目配置。
  3. 需要从项目配置中找到项目的路径(比如标准Angular Cli工作区间的src/app或者projects/some-app/src/app
  4. 解析名称和默认项目路径,以获取最终路径和所创建的文件的名称。
  5. 将最终名称传递到模板中,并使用路径将创建的文件移动到虚拟树种的适当位置。

修改我们的./src/tcs-schema/index.ts文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import { Rule, Tree, SchematicContext, SchematicsException, url, apply,  template, move, mergeWith} from '@angular-devkit/schematics';
import { schemaOptions } from './schema';
import {buildDefaultPath} from '@schematics/angular/utility/project';
import {parseName} from '@schematics/angular/utility/parse-name';
import { strings } from '@angular-devkit/core';
export function tcsSchema(_options: schemaOptions): Rule {
return (tree: Tree, _context: SchematicContext) => {
const workspaceConfigBuffer = tree.read('angular.json');
if (!workspaceConfigBuffer) {
throw new SchematicsException("Not an Angular Cli workspace");
}
const workspaceConfig = JSON.parse(workspaceConfigBuffer.toString());
const projectName = _options.project || workspaceConfig.defaultProject;
const project = workspaceConfig.projects[projectName];
const defaultProjectPath = buildDefaultPath(project);
const parsePath = parseName(defaultProjectPath, _options.name);
const {name, path} = parsePath;
const sourceTemplate = url('./files');
const sourceParametrizedTemplate = apply(sourceTemplate, [
template({
..._options,
...strings,
name,
}),
move(path)
]);
return mergeWith(sourceParametrizedTemplate)(tree, _context);
};
}

然后我们尝试运行。在非angular项目中运行:schematics .:tcs-schema tony,会报错:

1
2
An error occured:
Error: Not an Angular Cli workspace

很好,那我们继续在angular项目中运行看看:

1
ng g ../tcs-schema/src/collection.json:tcs-schema tony                  

可以看到确实生产了文件:CREATE src/app/hello-tony.ts (129 bytes)

上面是默认创建在src/app路径下面,那么我们需要像ng的脚手架一样,我们希望带着路径,如何?来尝试下:

1
2
ng g ../tcs-schema/src/collection.json:tcs-schema pages/tony
CREATE src/app/pages/hello-tony.ts (129 bytes)

可以看到,很神奇的将路径拼接在了一起,我们正确的在src/app/pages路径下面创建了文件。

ng方式的生成还带有对--help标志的支持,该标志可以正确的打印我们在shema.json文件中提供的信息,比如:

1
ng g ../tcs-schema/src/collection.json:tcs-schema --help

将会输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
Generates and/or modifies files based on a schematic.
usage: ng generate ../tcs-schema/src/collection.json:tcs-schema <name> [options]

arguments:
schematic
The schematic or collection:schematic to generate.
name
name

options:
--defaults
When true, disables interactive input prompts for options with a default.
--dry-run (-d)
When true, runs through and reports activity without writing out results.
--force (-f)
When true, forces overwriting of existing files.
--help
Shows a help message for this command in the console.
--interactive
When false, disables interactive input prompts.
--project
Generation in spacific Angular Cli workspace project

Help for schematic ../tcs-schema/src/collection.json:tcs-schema
custom schema
arguments:
name
name

options:
--project
Generation in spacific Angular Cli workspace project


To see help for a schematic run:
ng generate <schematic> --help

到这里第一阶段就可以了,我们了解了基本的创建、使用、调试,以及和Angular Cli的结合。下一阶段来分析@schematics/angular里面的component原理图的源码。

参考链接:Total Guide To Custom Angular Schematics

码字辛苦,打赏个咖啡☕️可好?💘