0%

angular 自定义schema (三)

前面我们学习了如何创建自己的schema,以及Angular Cli中component的源代码解读。现在来实践一下,来创建一个我们自己的schame,可以用来创建“page”类型的componet

为什么要创建“page”类型的组件?

比如我们有一个angular项目,在项目的开发过程中,我们会创建一些路由,这些路由需要绑定到对应的组件,做为页面的承载,然后我们会根据页面上部分不同的功能,抽取为单独的组件,最后可以随意自由组合组件来输出我们的页面。虽然说一切都是组件,但是我们的组件多起来后,一眼看去,无法分辨出来到底哪些只是个组件,而哪些是作为页面的。由此,我们需要创建”page“类型的组件,这些组件,一看就知道是作为一个独立的页面,而且它不会作为组件出现在别的组件中的。

比如,我们有一个home组件是个页面,那么我们需要创建的内容有:

  • home.page.ts
  • home.page.less
  • home.page.html

这样,可以和其他的单纯的组件相互区分开来。

创建schema项目

可以参考文章:angular 自定义shema(一),我们会创建tcs-schema这个自定义shema项目。

创建好项目之后,我们需要创建一个自定义的shema:page

先看下文件目录:

blog custom schema

files文件夹为模板文件夹,我们只需要创建必须的ts文件、html文件以及样式文件。

schema.json文件

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
{
"$schema": "http://json-schema.org/schema",
"id": "page",
"title": "page schema",
"description": "my page",
"properties": {
"name": {
"type": "string",
"description": "page name",
"$default": {
"$source": "argv",
"index": 0
},
"x-prompt": "你想生成什么page?"
},
"project": {
"type": "string",
"description": "Angular Project",
"$default": {
"$source": "projectName"
}
},
"skipImport": {
"type": "boolean",
"description": "是否跳过写入NgModule"
},
"style": {
"description": "page的样式文件,和系统保持一致",
"default": "css",
"enum": [
"css",
"scss",
"less"
]
},
"type": {
"description": "创建的组件的后缀,比如这里应当是.page",
"default": "page"
},
"flat": {
"type": "boolean",
"default": false,
"description": "如果是true的话,在当前项目的顶层创建新文件夹。"
},
"viewEncapsulation": {
"description": "The view encapsulation strategy to use in the new component.",
"enum": ["Emulated", "Native", "None", "ShadowDom"],
"type": "string",
"alias": "v",
"x-user-analytics": 11
},
"changeDetection": {
"description": "The change detection strategy to use in the new component.",
"enum": ["Default", "OnPush"],
"type": "string",
"default": "Default",
"alias": "c"
},
"skipSelector": {
"type": "boolean",
"default": false,
"description": "Specifies if the component should have a selector or not."
},
"inlineStyle": {
"description": "When true, includes styles inline in the component.ts file. Only CSS styles can be included inline. By default, an external styles file is created and referenced in the component.ts file.",
"type": "boolean",
"default": false,
"alias": "s",
"x-user-analytics": 9
},
"inlineTemplate": {
"description": "When true, includes template inline in the component.ts file. By default, an external template file is created and referenced in the component.ts file.",
"type": "boolean",
"default": false,
"alias": "t",
"x-user-analytics": 10
}
},
"required": [
"name"
]
}

需要特别注意的是这个type类型,我们这里设定为page。这样,我们模板接受到的__type__参数的值为page,我们即可创建page类型的组件。

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
import {
mergeWith,
apply,
Rule,
url,
Tree,
move,
filter,
applyTemplates,
noop,
chain,
SchematicsException
} from "@angular-devkit/schematics";
import {PageOptions} from "./schema";
import {buildDefaultPath, getWorkspace} from "@schematics/angular/utility/workspace";
import {buildRelativePath, findModuleFromOptions} from "@schematics/angular/utility/find-module";
import {parseName} from "@schematics/angular/utility/parse-name";
import {validateHtmlSelector, validateName} from "@schematics/angular/utility/validation";
import {strings} from "@angular-devkit/core";
import {applyLintFix} from "@schematics/angular/utility/lint-fix";
import * as ts from "@schematics/angular/third_party/github.com/Microsoft/TypeScript/lib/typescript";
import {
addDeclarationToModule,
addEntryComponentToModule,
addExportToModule
} from "@schematics/angular/utility/ast-utils";
import {InsertChange} from "@schematics/angular/utility/change";

function buildSelector(options: PageOptions, projectPrefix: string) {
let selector = strings.dasherize(options.name);
if (options.prefix) {
selector = `${options.prefix}-${selector}`;
} else if (options.prefix === undefined && projectPrefix) {
selector = `${projectPrefix}-${selector}`;
}
return selector;
}

function readIntoSourceFile(host: Tree, modulePath: string): ts.SourceFile {
const text = host.read(modulePath);
if (text === null) {
throw new SchematicsException(`File ${modulePath} does not exist.`);
}
const sourceText = text.toString('utf-8');

return ts.createSourceFile(modulePath, sourceText, ts.ScriptTarget.Latest, true);
}

function addDeclarationToNgModule(options: PageOptions) {
return (host: Tree) => {
if (options.skipImport || !options.module) {
return host;
}

options.type = !!options.type ? options.type : 'Component';

const modulePath = options.module;
const source = readIntoSourceFile(host, modulePath);

const componentPath = `/${options.path}/`
+ (options.flat ? '' : strings.dasherize(options.name) + '/')
+ strings.dasherize(options.name)
+ '.'
+ strings.dasherize(options.type);
const relativePath = buildRelativePath(modulePath, componentPath);
const classifiedName = strings.classify(options.name) + strings.classify(options.type);
const declarationChanges = addDeclarationToModule(source,
modulePath,
classifiedName,
relativePath);

const declarationRecorder = host.beginUpdate(modulePath);
for (const change of declarationChanges) {
if (change instanceof InsertChange) {
declarationRecorder.insertLeft(change.pos, change.toAdd);
}
}
host.commitUpdate(declarationRecorder);

if (options.export) {
// Need to refresh the AST because we overwrote the file in the host.
const source = readIntoSourceFile(host, modulePath);

const exportRecorder = host.beginUpdate(modulePath);
const exportChanges = addExportToModule(source, modulePath,
strings.classify(options.name) + strings.classify(options.type),
relativePath);

for (const change of exportChanges) {
if (change instanceof InsertChange) {
exportRecorder.insertLeft(change.pos, change.toAdd);
}
}
host.commitUpdate(exportRecorder);
}

if (options.entryComponent) {
// Need to refresh the AST because we overwrote the file in the host.
const source = readIntoSourceFile(host, modulePath);

const entryComponentRecorder = host.beginUpdate(modulePath);
const entryComponentChanges = addEntryComponentToModule(
source, modulePath,
strings.classify(options.name) + strings.classify(options.type),
relativePath);

for (const change of entryComponentChanges) {
if (change instanceof InsertChange) {
entryComponentRecorder.insertLeft(change.pos, change.toAdd);
}
}
host.commitUpdate(entryComponentRecorder);
}


return host;
};
}
export function page(_options: PageOptions): Rule {
return async (host: Tree) => {

const workspace = await getWorkspace(host);
const project = workspace.projects.get(_options.project as string);
// 获得项目的path配置
if (_options.path === undefined && project) {
_options.path = buildDefaultPath(project);
}
// 获得module
_options.module = findModuleFromOptions(host, _options);

// 从name中获取path
const parsedPath = parseName(_options.path as string, _options.name);
_options.name = parsedPath.name;
_options.path = parsedPath.path;
_options.selector = _options.selector || buildSelector(_options, project && project.prefix || '');
validateName(_options.name);
validateHtmlSelector(_options.selector);

const templateSource = apply(url('./files'), [
_options.skipTests ? filter(path => !path.endsWith('.spec.ts.template')) : noop(),
_options.inlineStyle ? filter(path => !path.endsWith('.__style__.template')) : noop(),
_options.inlineTemplate ? filter(path => !path.endsWith('.html.template')) : noop(),
applyTemplates({
...strings,
'if-flat': (s: string) => _options.flat ? '' : s,
..._options,
}),
move(parsedPath.path),
]);

return chain([
addDeclarationToNgModule(_options),
mergeWith(templateSource),
_options.lintFix ? applyLintFix(_options.path) : noop(),
]);
}
}

files模板文件

首先,files下面是一个大的文件夹:__name@dasherize@if-flat__,我们使用原理图创建这个文件夹,然后这个文件夹里面是我们需要的三个文件。

html文件

html文件为:__name@dasherize__.__type@dasherize__.html.template。这里主要是两个参数:name以及type

里面的内容我们可以自定义,比如我们只需要简单的一个标记:

1
<p><%= dasherize(name) %></p>

我们希望在page组件里面对样式有个约束,希望在这个组件里面定义的样式不要影响其他组件,那么我们需要给这个组件的顶层加个选择器,然后这个组件的样式定义在这个组件选择器的下面,我们可以生成这样的html:

1
2
3
<div class="<%= dasherize(name) %>-page">
<p><%= dasherize(name) %> created !</p>
</div>

样式文件

样式文件为:__name@dasherize__.__type@dasherize__.__style__.template。这里除过nametype以外,还接受style参数,比如当我们的项目中的样式文件为lessscss时,创建的样式文件的文件类型就为lessscss。当然,默认是css

结合上面的需求,我们可以将样式文件中写入约束的类:

1
2
3
.<%= dasherize(name) %>-page {

}

ts文件

ts文件为:__name@dasherize__.__type@dasherize__.ts.template。生成的ts文件中需要有组件的定义,以及一些其他操作,直接看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { Component, OnInit<% if(!!viewEncapsulation) { %>, ViewEncapsulation<% }%><% if(changeDetection !== 'Default') { %>, ChangeDetectionStrategy<% }%> } from '@angular/core';

@Component({<% if(!skipSelector) {%>
selector: '<%= selector %>',<%}%><% if(inlineTemplate) { %>
template: `
<p>
<%= dasherize(name) %> works!
</p>
`,<% } else { %>
templateUrl: './<%= dasherize(name) %>.<%= dasherize(type) %>.html',<% } if(inlineStyle) { %>
styles: []<% } else { %>
styleUrls: ['./<%= dasherize(name) %>.<%= dasherize(type) %>.<%= style %>']<% } %><% if(!!viewEncapsulation) { %>,
encapsulation: ViewEncapsulation.<%= viewEncapsulation %><% } if (changeDetection !== 'Default') { %>,
changeDetection: ChangeDetectionStrategy.<%= changeDetection %><% } %>
})
export class <%= classify(name) %><%= classify(type) %> implements OnInit {

constructor() { }

ngOnInit() {
}

}

测试

在angular项目中测试:

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

分析上面这个命令,我的原理图项目和angular项目是同级的,我在angular 项目中运行上面的命令,所以需要../tsc-schema

tcs-schema/src/collection.json中使用page来在angular项目中的src/app/components/路径里面创建page类型组件,名字为tony-best

最后运行结果:

custom schema result

发布&使用

到目前为止,我们都是本地调试的,正式使用的时候,我们需要把它发布为一个npm的包。引入后进行使用。

发包之前需要登录npm:

1
npm adduser

然后按照提示输入用户名、密码、邮箱,即可登录。(忘记用户名可以去npmjs.com查看)

然后发布:

1
npm publish

发布成功后,回到我们的angular项目,然后安装包:

1
npm i tcs-schema --save-dev

安装成功后,可以直接使用:

1
ng g tcs-schema:page components/tony-best

这样,即可在src/app/components下面创建好page类型组件tony-best


完~ 😷

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