Description
使用angular schematics快速生成代码
什么是Schematics?
Schematics是改变现存文件系统的生成器。有了Schematics我们可以:
- 创建文件
- 重构现存文件,或者
- 到处移动文件
Schematics能做什么?
总体上,Schematics可以:
- 为Angular工程添加库
- 升级Angular工程中的库
- 生成代码
在你自己的工程或者在你所在的组织中使用Schematics是具有无限可能的。下面一些例子展现了你或者你的组织或如何从创建一个schematics collection中获益:
- 在应用中生成通用UI模板
- 使用预先定义的模板或布局生成组织指定的组件
- 强制使用组织内架构
Schematics现在是Angular生态圈的一部分,不仅限于Angular工程,你可以生成想要模板。
CLI集成?
是的,schematics与Angular CLI紧密集成。你可以在下列的CLI命令中使用schematics:
- ng add
- ng generate
- ng update
什么是Collection?
Collection是一系列的schematic。我们会在工程中collection.json中为每个schematic定义元数据。
安装
首先,使用npm或者yarn安装schematics的CLI:
npm install -g @angular-devkit/schematics-cli
yarn add -g @angular-devkit/schematics-cli
快速开始
这会创建名为 demo-schema
的文件夹,在其中已经创建了多个文件,如下所示。
schematics blank --name=demo-schema
我们使用 blank 为我们后继的工作打好基础。
collection.json
{
"$schema": "../node_modules/@angular-devkit/schematics/collection-schema.json",
"schematics": {
"demo-schema": {
"aliases": ["demo"], //需要自己添加
"factory": "./demo-schema/index.ts#demoSchema",
"description": "A blank schematic.",
"schema": "./demo-schema/schema.json" //需要自己添加
}
}
}
- $schema => 定义该 collection 架构的 url 地址.
- schematics => 这是你的 schematics 定义.
- demo-schema => 以后使用这个 schematics 的 cli 名称.
- aliases => 别名.
- factory => 定义代码.
- description => 简单的说明.
- schema => 你的 schema 的设置. 这个文件的内容应该如下所示。我们在其中定义了多个自定义的选项,在使用这个 Schematics 的时候,可以通过这些选项来设置生成的内容。
关于怎么创建
schema.json
,下面实战项目来说明。
入口函数
打开src/demo-schema/index.ts
文件,看看内容:
import { Rule, SchematicContext, Tree } from '@angular-devkit/schematics';
// You don't have to export the function as default. You can also have more than one rule factory
// per file.
export function demoSchema(_options: any): Rule {
return (tree: Tree, _context: SchematicContext) => {
return tree;
};
}
- 我们export了会作为"entry function"调用的
demoSchema
函数 - 函数接受一个
_options
参数,它是命令行参数的键值对对象,和我们定义的schema.json
有关 - 这个函数是一个高阶函数,接受或者返回一个函数引用。此处,这个函数返回一个接受
Tree
和SchmaticsContext
对象的函数
什么是Tree?
Tree是变化的待命区域,包含源文件系统和一系列应用到其上面的变化。
我们能使用tree完成这些事情:
- read(path: string): Buffer | null: 读取指定的路径
- exists(path: string): boolean: 确定路径是否存在
- create(path: string, content: Buffer | string): void: 在指定路径使用指定内容创建新文件
- beginUpdate(path: string): UpdateRecorder: 为在指定路径的文件返回一个新的UpdateRecorder实例
- commitUpdate(record: UpdateRecorder): void: 提交UpdateRecorder中的动作,简单理解就是更新指定文件内容(实战中会用到)
什么是Rule?
Rule是一个根据SchematicContext为一个Tree应用动作的函数,入口函数返回了一个Rule。
declare type Rule = (tree: Tree, context: SchematicContext) => Tree | Observable<Tree> | Rule | void;
构建和执行
要运行我们的示例,首先需要构建它,然后使用schematics命令行工具,将schematic项目目录的路径作为集合。从我们项目的根:
npm run build
# ... 等待构建完成
schematics .:demo-schema --name=test --dry-run
# ...查看在控制台生成创建的文件日志
注意:使用
--dry-run
不会生成文件,在调试时候可以使用它,如果想要创建正在的文件,去掉即可。使用npm run build -- -w
命令,修改index.ts
文件以后自动构建。
如何使用在angular项目中
使用短连安装:
npm link demo-schema
使用ng generate
运行:
ng generate demo-schema:demo-schema
- 第一个demo-schema => 是
package.json
的name
- 第二个demo-schema => 是我们运行的schematics
实战项目
最新在公司项目需要把之前的项目的通用组件提取出来,做成一个单独的组件库并且带上demo。创建了一个新的工程。组件库使用ng-packagr
,如果直接使用它去打包,会全部打包到一起,这样就会有问题,加载的时候特别大。ng-packagr
提供的二次入口,可以解决这个问题,但是又会有新问题,必须要要和src
同级目录。如果使用angular-cli
默认去生成都会自动添加到src/lib
里面,虽然可以修改path
,但是问题是一个组件模块里面有一些特定的文件:
- module
- component(包含css,html,ts)
- service
- directive
- class
- types
大概就这些,如果这些用angular-cli,去生成,需要6次才能完成,也可以写一个shell
,一次性完成。但是发现太麻烦,有些东西无法控制,如果需要定制,那就需要自己来写schematics
。
这里有2部分不一样的实战内容,一种是根据固定内容直接生成模板,一种是生成模板以后修改已有关联的文件。
为什么会有这2个,第一个是为了生成组件模块,第二个是为了生成演示组件模块。
创建一个schematics
schematics blank --name=tools
这时候也会创建src/tools
,我们把它改成ui
,把collection.json
文件里面也修改了。
实战1
创建ui/schema.json
文件:
{
"$schema": "http://json-schema.org/schema",
"id": "ui",
"title": "UI Schema",
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "生成一个组件模块",
"$default": {
"$source": "argv",
"index": 0
},
"x-prompt": "组件使用什么名称?"
},
"path": {
"type": "string",
"default": "projects/ui",
"description": "生成目标的文件夹路径"
},
"service": {
"type": "boolean",
"default": false,
"description": "标志是否应该生成service"
},
"directive": {
"type": "boolean",
"default": false,
"description": "标志是否应该生成directive"
},
"class": {
"type": "boolean",
"default": false,
"description": "标志是否应该生成class"
},
"types": {
"type": "boolean",
"default": false,
"description": "标志是否应该生成types/interfaces"
}
},
"required": ["name"]
}
这里可以设置你的 schematics
的命令选项,类似于在使用 ng g c --name=user
的--name
命令。
创建ui/schema.ts
文件:
export interface SimpleOptions {
name: string;
path: string;
service?: boolean;
directive?: boolean;
class?: boolean;
types?: boolean;
}
修改ui/index.ts
:
import { strings } from '@angular-devkit/core';
import {
apply,
applyTemplates,
branchAndMerge,
chain,
filter,
mergeWith,
move,
noop,
Rule,
SchematicContext,
Tree,
url,
} from '@angular-devkit/schematics';
import { parseName } from '@schematics/angular/utility/parse-name';
import { SimpleOptions } from './schema';
export function simple(_options: SimpleOptions): Rule {
return (tree: Tree, _context: SchematicContext) => {
...code
}
}
- 先处理路径:
if (!_options.path) {
throw new Error('path不能为空');
}
// 处理路径
const projectPath = `/${_options.path}/${_options.name}`;
const parsedPath = parseName(projectPath, _options.name);
_options.name = parsedPath.name;
_options.path = parsedPath.path;
- 获取模板源:
const templateSource = apply(url('./files'), [
applyTemplates({
...strings,
..._options,
}),
move(_options.path),
]);
我们模块在files
文件下,applyTemplates
把我们的配置转换成模板可以使用的变量并生成文件,move
把生成好的文件移动到目标路径下。
- 返回rule
const rule = chain([branchAndMerge(chain([mergeWith(templateSource)]))]);
return rule(tree, _context);
我们前面也也介绍了,入口函数总是要返回一个rule
。chain
验证我们的配置规则。
整体看起来比较简单,这样就已经完成的整个的生成命令,下面就是关键模块定义:
- 模板放在
files
文件下 - 以
.template
后缀结尾 - 要替换变量使用
__变量__
方式 - 需要处理变量要以
__变量@方法名__
举例:
__name@dasherize__.class.ts.template
将name变量驼峰式写法转换为连字符的写法。
模板里面如何使用,语法和EJS
一样
标签含义:
<%
'脚本' 标签,用于流程控制,无输出。<%_
删除其前面的空格符<%=
输出数据到模板(输出是转义 HTML 标签)<%-
输出非转义的数据到模板<%#
注释标签,不执行、不输出内容<%%
输出字符串 '<%'%>
一般结束标签-%>
删除紧随其后的换行符_%>
将结束标签后面的空格符删除
语法示例:
<%# 变量 %>
<%= classify(name) %>
<%# 流程判断 %>
<% if (user) { %>
<h2><%= user.name %></h2>
<% } %>
<%# 循环 %>
<ul>
<% users.forEach(function(user){ %>
<%- include('user/show', {user: user}); %>
<% }); %>
</ul>
会大量使用变量和少量的流程判断,变量一般都会使用内置的模板方法来配合使用:
内置的模板变量方法:
- classify:连字符写法转换为大驼峰式的写法
- dasherize:驼峰式写法转换为连字符的写法
更多模板变量:
node_modules\@angular-devkit\core\src\utils\strings.d.ts
内置的模板方法主要命名转换,如果不能满足你需求,可以自己定义:
const utils = {
...自定义方法
}
`applyTemplates({utils: utils})`。
举个几个例子:
name@dasherize.module.ts.template
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { Sim<%= classify(name) %>Component } from './<%= dasherize(name) %>.component';
@NgModule({
declarations: [Sim<%= classify(name) %>Component],
imports: [CommonModule],
exports: [Sim<%= classify(name) %>Component],
providers: [],
})
export class Sim<%= classify(name) %>Module {}
name@dasherize.component.ts.template
import {
ChangeDetectionStrategy,
Component,
ElementRef,
HostBinding,
HostListener,
Input,
OnDestroy,
OnInit,
ViewEncapsulation,
} from '@angular/core';
@Component({
selector: 'sim-<%= dasherize(name) %>',
templateUrl: './<%= dasherize(name) %>.component.html',
styleUrls: ['./<%= dasherize(name) %>.component.scss'],
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class Sim<%= classify(name) %>Component implements OnInit, OnDestroy {
constructor(protected elementRef: ElementRef) {}
public ngOnInit(): void {
}
public ngOnDestroy(): void {
}
}
index.ts.template
export * from './<%= dasherize(name) %>.component';
export * from './<%= dasherize(name) %>.module';
我们可以构建试一下:
cd tools
npm run build
schematics .:ui --name=test --dry-run
- index.ts 二次入口打包入口
- package.json 二次入口打包必备配置
- README.md 组件说明
基本已经完成我们想要的,还有几个文件生成是可选的,我们需要配置处理一下:
const templateSource = apply(url('./files'), [
_options.service ? noop() : filter(path => !path.endsWith('.service.ts.template')),
_options.class ? noop() : filter(path => !path.endsWith('.class.ts.template')),
_options.directive ? noop() : filter(path => !path.endsWith('.directive.ts.template')),
_options.types ? noop() : filter(path => !path.endsWith('.type.ts.template')),
applyTemplates({
...strings,
..._options,
}),
move(_options.path),
]);
如果是true,就忽略,如果是false,就排除这个后缀结尾文件。
schematics .:ui --name=test --dry-run --service
注意:不需要写=true
。
npm link tools
ng g tools:ui --name="test" --dry-run
ng g tools:ui --name="test"
基本已经完成了,下面介绍一个进阶实战。
实战2
我们想要创建多个schematics
,需要手动添加,我们需要文件:
src/demo/index.ts
src/demo/index_spec.ts
src/demo/schema.json
src/demo/schema.ts
src/demo/files
然后在src/collection.json
里添加申明:
"schematics": {
...
"demo": {
"description": "A blank schematic.",
"factory": "./demo/index#demo",
"schema": "./demo/schema.json"
}
}
配置schema.json
:
{
"$schema": "http://json-schema.org/schema",
"id": "demo",
"title": "simple demo Schema",
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "生成一个组件模块",
"$default": {
"$source": "argv",
"index": 0
},
"x-prompt": "组件使用什么名称?"
},
"path": {
"type": "string",
"default": "/projects/demo",
"description": "生成目标的文件夹路径"
}
},
"required": ["name"]
}
先保留这些Schema
,后面来丰富。
我们把前面介绍的入口函数拷贝到src/demo/index.ts
里,构建编译:
cd tools
npm run build
schematics .:demo --name=test --dry-run
# ...Nothing to be done.
这个实战和前面实战有些不一样,前面的只是一个替换生成,相当于一个入门级的,很容易学会,现在介绍一个高级点的,不光要替换生成,还要去改变已有文件的依赖关系。
使用angular-cli的时候,创建组件以后,会自动去关联的模块里面去申明,这个是怎么做到的?
我们就需要实现一个类似的功能,有一个功能需要去展示UI组件的demo,每次创建都是相当于有一套对应的模板,但是每次创建以后都是一个新的的页面,也需要一个路由规则需要添加,如果我们单纯创建一套demo组件的文件,还需要去手动添加路由,这样就比较麻烦,现在就需要自动完成这个功能。我们一起来实现它吧。
这里我们就用上beginUpdate
和commitUpdate
2个方法来实现
先介绍一下需要实现的功能:
我有三个文件夹:
guides 快速指南
experimental 实验功能
components 组件库
- guides 没有子路由
- experimental 有子路由没有菜单分组
- components 有子路由有菜单分组
书写路由时候,每个ui组件,都是一个独立模块,使用懒加载模块方式,这样所有的懒加载路由都是平级的。
举个栗子:
const routes: Routes = [
{
path: '',
component: ComponentsComponent,
children: [
{
path: '',
redirectTo: 'button',
pathMatch: 'full',
},
{
path: 'button',
loadChildren: './button/button.module#ButtonModule',
},
{
path: 'card',
loadChildren: './card/card.module#CardModule',
},
{
path: 'divider',
loadChildren: './divider/divider.module#DividerModule',
},
],
},
];
其实angular也有自带添加路由依赖方法,但是只能添加一级路由,不能添加子路由,我们这个需求就是需要添加子路由。
- 验证路径
if (!_options.path) {
throw new Error('path不能为空');
}
// 处理路径
const parsedPath = parseName(_options.path, _options.name);
_options.name = parsedPath.name;
_options.path = parsedPath.path;
- 生成模板
const templateSource = apply(url('./files'), [
applyTemplates({
...strings,
..._options,
}),
move(parsedPath.path),
]);
模板这块就不在说明了,和实战1是一样处理的,去files文件夹里面创建对应的模板即可。
- 返回rule
const rule = chain([addDeclarationToNgModule(_options), mergeWith(templateSource)]);
return rule(tree, _context);
其他没有什么好说明的,addDeclarationToNgModule
是我们需要重点说明的,也是这个实战的核心。
function addDeclarationToNgModule(options: DemoOptions): Rule {
return (host: Tree) => {
// 路由模块路径
const modulePath = `${options.path}/${options.module}/${options.module}-routing.module.ts`;
// 懒加载模块名字
const namePath = strings.dasherize(options.name);
// 需要刷新AST,因为我们需要覆盖目标文件。
const source = readIntoSourceFile(host, modulePath);
// 获取更新文件
const routesRecorder = host.beginUpdate(modulePath);
// 获取变更信息
const routesChanges = addRoutesToModule(source, modulePath, buildRoute(options, namePath)) as InsertChange;
// 在多少行位置插入指定内容
routesRecorder.insertLeft(routesChanges.pos, routesChanges.toAdd);
// 更新文件
host.commitUpdate(routesRecorder);
};
}
这里有3个依赖方法:
- readIntoSourceFile: 读取ts文件,使用
ts.createSourceFile
为我们解析文件源 AST - addRoutesToModule:获取变更信息重要方法
- buildRoute:组装新的路由信息
说实话,我对
AST
这个玩意不熟,之前使用ng-packagr
时候出现一个bug,通过源码拿到AST
,修复这个bug,一直自用,不过后来ng-packagr
已经通过其他方式修复了。
// 这个是一个工具方法
function readIntoSourceFile(host: Tree, modulePath: string): ts.SourceFile {
// 先试着用Tree方法读文件
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);
}
// 我现在还用的angular7,懒加载路由还是老的写法,在等angular9更新。
function buildRoute(options: DemoOptions, modulePath: string) {
const moduleName = `${strings.classify(options.name)}Module`;
const loadChildren = normalize(`'./${modulePath}/${modulePath}.module#${moduleName}'`);
return `{ path: '${modulePath}', loadChildren: ${loadChildren} }`;
}
addRoutesToModule
内容太多,创建一个utils.ts
文件来处理它。
大部分也是借鉴angular-cli的addRouteDeclarationToModule方法,改成我们想要。
export function addRoutesToModule(source: ts.SourceFile, fileToAdd: string, routeLiteral: string): Change {
const routerModuleExpr = getRouterModuleDeclaration(source);
if (!routerModuleExpr) {
throw new Error(`Couldn't find a route declaration in ${fileToAdd}.`);
}
const scopeConfigMethodArgs = (routerModuleExpr as ts.CallExpression).arguments;
if (!scopeConfigMethodArgs.length) {
const { line } = source.getLineAndCharacterOfPosition(routerModuleExpr.getStart());
throw new Error(`The router module method doesn't have arguments ` + `at line ${line} in ${fileToAdd}`);
}
let routesArr: ts.ArrayLiteralExpression | undefined;
const routesArg = scopeConfigMethodArgs[0];
// 检查路由声明数组是RouterModule的内联参数还是独立变量
if (ts.isArrayLiteralExpression(routesArg)) {
routesArr = routesArg;
} else {
const routesVarName = routesArg.getText();
let routesVar;
if (routesArg.kind === ts.SyntaxKind.Identifier) {
routesVar = source.statements
.filter((s: ts.Statement) => s.kind === ts.SyntaxKind.VariableStatement)
.find((v: ts.VariableStatement) => {
return v.declarationList.declarations[0].name.getText() === routesVarName;
}) as ts.VariableStatement | undefined;
}
if (!routesVar) {
const { line } = source.getLineAndCharacterOfPosition(routesArg.getStart());
throw new Error(`No route declaration array was found that corresponds ` + `to router module at line ${line} in ${fileToAdd}`);
}
routesArr = findNodes(routesVar, ts.SyntaxKind.ArrayLiteralExpression, 1)[0] as ts.ArrayLiteralExpression;
}
const occurrencesCount = routesArr.elements.length;
const text = routesArr.getFullText(source);
let route: string = routeLiteral;
let insertPos = routesArr.elements.pos;
if (occurrencesCount > 0) {
// 不一样的开始
// 获取最后一个element
const lastRouteLiteral = [...routesArr.elements].pop() as ts.Expression;
// 从当前元素的属性里面获取`children`属性token信息
const children = (ts.isObjectLiteralExpression(lastRouteLiteral) &&
lastRouteLiteral.properties.find(n => {
return ts.isPropertyAssignment(n) && ts.isIdentifier(n.name) && n.name.text === 'children';
})) as ts.PropertyAssignment;
if (!children) {
throw new Error('"children" does not exist.');
}
// 处理路由字符串
const indentation = text.match(/\r?\n(\r?)\s*/) || [];
const routeText = `${indentation[0] || ' '}${routeLiteral}`;
// 获取当前`children`结束位置
insertPos = (children.initializer as ts.ArrayLiteralExpression).elements.end;
// 拼接路由信息
route = `${routeText},`;
// 不一样的结束
}
return new InsertChange(fileToAdd, insertPos, route);
}
注意:这里有些代码相当于写死了,因为我本身都是固定的。
那些一样都不过多的解释,你需要知道最终拿到的是:const routes: Routes = []
即可。
所以按angular自带的addRouteDeclarationToModule方法,操作总是routes.push(newRoute)
这样的操作,而我们需要的操作是routes[0].children.push(newRoute)
,就需要自己弄了。
我们拿到lastRouteLiteral
,注意:其实这个有个bug,如果我们路由里面改了,这个就挂了。
NodeObject {
pos: 193,
end: 276,
flags: 0,
transformFlags: undefined,
parent:
NodeObject {
pos: 191,
end: 280,
flags: 0,
transformFlags: undefined,
parent:
NodeObject {
pos: 174,
end: 280,
flags: 0,
transformFlags: undefined,
parent: [Object],
kind: 235,
name: [Object],
type: [Object],
initializer: [Circular],
_children: [Array] },
kind: 185,
multiLine: true,
elements: [ [Circular], pos: 193, end: 277, hasTrailingComma: true ],
_children: [ [Object], [Object], [Object] ] },
kind: 186,
multiLine: true,
properties:
[ NodeObject {
pos: 198,
end: 212,
flags: 0,
transformFlags: undefined,
parent: [Circular],
kind: 273,
decorators: undefined,
modifiers: undefined,
name: [Object],
questionToken: undefined,
exclamationToken: undefined,
initializer: [Object],
_children: [Array] },
NodeObject {
pos: 213,
end: 251,
flags: 0,
transformFlags: undefined,
parent: [Circular],
kind: 273,
decorators: undefined,
modifiers: undefined,
name: [Object],
questionToken: undefined,
exclamationToken: undefined,
initializer: [Object],
_children: [Array] },
NodeObject {
pos: 252,
end: 270,
flags: 0,
transformFlags: undefined,
parent: [Circular],
kind: 273,
decorators: undefined,
modifiers: undefined,
name: [Object],
questionToken: undefined,
exclamationToken: undefined,
initializer: [Object],
_children: [Array] },
pos: 198,
end: 271,
hasTrailingComma: true ],
_children:
[ TokenObject { pos: 193, end: 198, flags: 0, parent: [Circular], kind: 17 },
NodeObject {
pos: 198,
end: 271,
flags: 0,
transformFlags: undefined,
parent: [Circular],
kind: 304,
_children: [Array] },
TokenObject { pos: 271, end: 276, flags: 0, parent: [Circular], kind: 18 } ] }
这里拿的对应信息就是:
{
path: '',
component: ComponentsComponent,
children: []
}
lastRouteLiteral.properties
是一个数组,我们这个{}
里面有几项,就会有几个数组。我们只关心children
属性,就通过find查找目标,它有可能是undefined
,需要处理一下。
我们来打印children
:
我大家演示2个不一样的:
路由配置里children
空的
NodeObject {
pos: 252,
end: 270,
flags: 0,
transformFlags: undefined,
parent:
NodeObject {
pos: 193,
end: 276,
flags: 0,
transformFlags: undefined,
parent:
NodeObject {
pos: 191,
end: 280,
flags: 0,
transformFlags: undefined,
parent: [Object],
kind: 185,
multiLine: true,
elements: [Array],
_children: [Array] },
kind: 186,
multiLine: true,
properties:
[ [Object],
[Object],
[Circular],
pos: 198,
end: 271,
hasTrailingComma: true ],
_children: [ [Object], [Object], [Object] ] },
kind: 273,
decorators: undefined,
modifiers: undefined,
name:
IdentifierObject {
pos: 252,
end: 266,
flags: 0,
parent: [Circular],
escapedText: 'children' },
questionToken: undefined,
exclamationToken: undefined,
initializer:
NodeObject {
pos: 267,
end: 270,
flags: 0,
transformFlags: undefined,
parent: [Circular],
kind: 185,
elements: [ pos: 269, end: 269 ],
_children: [ [Object], [Object], [Object] ] },
_children:
[ IdentifierObject {
pos: 252,
end: 266,
flags: 0,
parent: [Circular],
escapedText: 'children' },
TokenObject { pos: 266, end: 267, flags: 0, parent: [Circular], kind: 56 },
NodeObject {
pos: 267,
end: 270,
flags: 0,
transformFlags: undefined,
parent: [Circular],
kind: 185,
elements: [Array],
_children: [Array] } ] }
一个路由配置里children
有的
NodeObject {
pos: 239,
end: 655,
flags: 0,
transformFlags: undefined,
parent:
NodeObject {
pos: 185,
end: 660,
flags: 0,
transformFlags: undefined,
parent:
NodeObject {
pos: 183,
end: 663,
flags: 0,
transformFlags: undefined,
parent: [Object],
kind: 185,
multiLine: true,
elements: [Array],
_children: [Array] },
kind: 186,
multiLine: true,
properties:
[ [Object],
[Object],
[Circular],
pos: 189,
end: 656,
hasTrailingComma: true ],
_children: [ [Object], [Object], [Object] ] },
kind: 273,
decorators: undefined,
modifiers: undefined,
name:
IdentifierObject {
pos: 239,
end: 252,
flags: 0,
parent: [Circular],
escapedText: 'children' },
questionToken: undefined,
exclamationToken: undefined,
initializer:
NodeObject {
pos: 253,
end: 655,
flags: 0,
transformFlags: undefined,
parent: [Circular],
kind: 185,
multiLine: true,
elements:
[ [Object],
[Object],
[Object],
[Object],
pos: 255,
end: 649,
hasTrailingComma: true ],
_children: [ [Object], [Object], [Object] ] },
_children:
[ IdentifierObject {
pos: 239,
end: 252,
flags: 0,
parent: [Circular],
escapedText: 'children' },
TokenObject { pos: 252, end: 253, flags: 0, parent: [Circular], kind: 56 },
NodeObject {
pos: 253,
end: 655,
flags: 0,
transformFlags: undefined,
parent: [Circular],
kind: 185,
multiLine: true,
elements: [Array],
_children: [Array] } ] }
这里给大家科普几个数据就好了:
- pos:起始位置
- end: 结束位置
- parent:父节点
- initializer:自己
- elements:子节点
主要看elements
变化,空里面只有2个[pos, end],如果不是空里面就会有子节点,你现在是不是可以干点其他事情了。(ps:如果想批量更新之前内容是不是想想也容易了)
注意:如果你要调试,一定要用npm link
安装,根目录使用ng g tools:demo --name=test
。
先试试默认的路由添加:
ng g tools:demo --name=test
ng g tools:demo --name=test --module=experimental
大功告成,欢迎交流,发现更多好玩的东西。