上一篇文章我们了解了自定义原理图的基本操作,可以简单生成文件,并和Angular Cli进行链接,我们如何真正的创建我们自己的原理图?在使用Angular Cli自带的命令创建组件的时候是可以更新模块里面声明组价的语句的,那么它是如何实现的?我们来细致的分析下@schematics/angular/component的源代码。
文件组织结构

index.ts里面是处理模板和项目对接的逻辑schema.json里面是这个声明这个原理图里需要的参数files里面是模板文件,会根据参数生成最终的组件文件(html文件、样式文件、spec测试文件、ts文件)
index.ts文件分析
首先看文件里面的方法:
function readIntoSourcefile(host: Tree, moudlePath: string): ts.SourceFilefunction addDeclarationToNgMoudle(options: CommponentOptions): Rulefunction buildSelector(options: Components, projectPrefix: string): void
根据方法名字(以及概览一下index.ts)来看,readIntoSourceFile是读取对应的module源文件,以辅助addDeclarationToNgModule方法在将新创建的component声明到module文件的时候去更新module文件。而buildSelector方法是处理生成的组件的元信息selector的选择器字符串。
主体方法 export default function (options: ComponentOptions): Rule
主体方法返回了一个异步的方法,因为方法内部使用await调用了一个异步获取Angular项目配置的文件。
1 | return async (host: tree) => {...} |
获取Angular的项目配置
1 | const workspace = await getWorkspace(host); |
这里getWorkspace方法是库提供的:import {getWorkspace} from "@schematics/angular/utility/workspace";,从当前的Angular中获取angular.json文件。然后获取参数中project的配置:workspace.projects.get(options.project as string)。如果我们不传project参数的话,应该是取的默认项目,这个在angular.json有对应的配置项的。
1 | if (options.path === undefined && project) { |
如果在执行component的原理图的时候没有输入path参数,那么会从angular.json中读取默认的项目的的path。
获取距离最近的NgModule
我们在执行ng g c命令来创建组件的时候会将组件的声明写入“合适”的ngModule中。获取合适的ngModule:
1 | options.module = findModuleFromOptions(host, options); |
findModuleFromOptions方法是组件提供的方法:import {findModuleFromOptions} from "@schematics/angular/utility/find-module"。
解析路径、名称、选择器
1 | const parsedPath = parseName(options.path as string, options.name); |
上面这一段是为了从options.name中解析路径、组件名称、以及组件的选择器。
比如,当我们想在src/app/pages/user下面创建create组件,那么我们的命令是:
1 | ng g c pages/user/create |
上面的代码会解析出来,我们的路径是:src/app/pages/user/create,组件名称是create,选择器是app-create。
继续解析上面的代码,第一行中的parseName是组件提供的方法import {parseName} from "@schematics/angular/utility/parse-name",而buildSelector方法是在这里自定义的,方法比较短,我们来看下这个方法:
1 | function buildSelector(options: ComponentOptions, projectPrefix: string) { |
很容易理解,首先说下strings.dasherize(),这个strings是库提供的的一组字符串处理方法,需要引入:import {strings} from "@angular-devkit/core";,在看strings.dasherize()方法的作用是,用破折号替换下划线、空格或小驼峰。也就是格式化name字符串为xxx-xxx的形式。
然后就是看有没有prefix参数,有的话就是prefix-name,没有的话就是app-name,这个很有用,防止项目中组件过多导致选择器重复,我们可以在创建子类的组件的时候,加上prefix修饰。
然后是验证组件的名字和选择器:
1 | validateName(options.name); |
validateName和validateHtmlSelector是组件提供的方法,需要引入:import {validateHtmlSelector, validateName} from "@schematics/angular/utility/validation";,用来验证name和selector的合法性。
应用于模板文件
1 | const templateSource = apply(url('./files'), [ |
这里用的库的方法很多,我们需要慢慢来分析
apply(source: Source, rules: Rule[]): Source对源文件应用多个规则,并返回转换后的源。url(path: string)从路径字符串获取模板文件。filter()类似于array的filter方法,过滤掉满足条件的项noop()返回一个空的规则applyTemplates()将参数以及strings方法应用于模板文件move()移动处理后的模板到特定目录。
以上方法均需要引入库方法:import {mergeWith, apply, Rule, url, Tree, move, filter, applyTemplates, noop} from "@angular-devkit/schematics";
总的来说,这一段就是应用输入的参数,比如有是否跳过测试文件,是否是内联样式,是否是内联html等。
最后一哆嗦
1 | return chain([ |
最后的规则组合。这里也有几个库方法:
chain()将多个规则链接为一个规则。mergeWith()将输入源与输入树合并applyLintFix()这个不知道是什么方法,根据字面意思来看,好像是应用tslint修复?(参数控制的,而且这个参数也不怎么常用)
再来看addDeclarationToNgModule(options)方法,这个方法是自定义的,比较长,我们做为一个分支另说。我们只需要知道这个方法是将组件的声明添加到ngModule里面的即可。
schema.json文件分析
我们知道,schema.ts中定义了我们这个原理图可以接受的参数,那么schema.json有什么用?当然,这个更有用,它不仅提供的了参数的描述,也附带了更多的功能,类似于,定义参数的默认值,定义参数的命令行提示,定义哪些参数是必须的等等。
它的结构是:
1 | { |
里面重要的是id, properties以及required:
id声明这个原理图的id,类似于它的”名字“。(猜想:好想和angular.json文件有关联,后续再发现)- `properties’ 这个原理图需要接受的参数,都可以在这个里面进行定义。
required声明在执行这个原理图的时候必须的参数
在properities中我们也着重几个常用且重要的来看下。
name 参数
1 | "name": { |
这里描述了name参数的类型、介绍,以及默认第一个不加标志的参数就是name的值。这样我们可以直接使用:
1 | ng g c tony |
这样来创建一个组件,而原理图接受的name参数就是tony',等效于:ng g c –name=tony`。
project 参数
1 | "project": { |
这里定义了project的默认值,当我们创建组件的时候,如果没有特意说明project参数,那么就会拿angular.json中的defaultProject里面的默认项目。这个很有用。
其他的有:
style定义了默认的样式文件后缀以及样式文件后缀的枚举。type定义了这个原理图创建的文件的后缀,这里是component。prefix选择器的前缀。默认是app。
模板文件分析
html文件
html文件是__name@dasherize__.__type@dasherize__.html.template,我们可以结合前面的strings里面的方法来分析,这里:
__name@dasherize__是输出name变量,并应用dasherize方法,也就是将name字符串解析为短横线的格式。__type@dasherize__是输出type变量,并应用dasherize方法,同上。
举个例子,我们的创建语句是:ng g c tonyBest,那么创建的html文件就是:tony-best.component.html。
再看模板内容:
1 | <p><%= dasherize(name) %> works!</p> |
很简单的一句话,这样就是:<p>tony-best workes</p>
样式文件
样式文件是:__name@dasherize__.__type@dasherize__.__style__.template,结合上面html文件,唯一的区别是这里的__style__,这是输出style变量的值,假如我们的默认样式是css,那么这里会生成:tony-best.component.css。
ts文件
ts文件是:__name@dasherize__.__type@dasherize__.ts.template,这里同上,我们可以知道,它会生成文件:tony-best.component.ts
ts文件里面内容比较多,但都是判断语句,我们可以来看:
1 |
|
我们先来分析第一行:
1 | import { |
其实这里是根据viewEncapsulation和changeDection变量的值,来从@angular/core中引用ViewEncapsulation, ChangeDetectionStrategy。
再看@Component的元数据部分:
selector判断skipSelector参数,是否不需要selector,如果不需要就不出现这一行。template判断inlineTemplate参数,是否是内联html,如果是内联的html,就输出这一行的内容templateUrl如果不是内联html,就将生成的xxx.component.html放到这个字符串数组里面。style如果是内联css就保留这个元数据styleUrl如果不是内联css就应用刚生成的样式文件。encapsulation根据参数,如果没设定encapsulation就不输出这一行,有设定就是设定的值。changeDetection根据参数,如果没有设定就不输出这一行,如果设定了就是设定的值。
接下来就是组件的类名:
1 | export class <%= classify(name) %><%= classify(type) %> implements OnInit {} |
classify方法是生成大驼峰字符串的方法。比如,我们上面的ng g c tonyBest,生成的ts文件里面的这个就是:
1 | export class TonyBestComponent implates OnInit {} |
然后下面就是默认两个方法:construct和ngOninit。这是组件常用的方法,生成的时候带着这两个方法还是很有必要的。
重点看下如何写入module的
本想着再开个文章,算了,成热打铁,来看如何写入NgModule的。先看addDeclarationToNgModule方法的主体结构:
1 | addDeclarationToNgModule(options: ComponentOptions) { |
它对Tree做了一系列处理,然后返回。来看看内脏内部,我们分段来分析,首先看第一段:
1 | if (options.skipImport || !options.module) { |
首先判断skipImport和options.module,如果不存在options.module或者参数skipImport为真的话,等于没必要写入ngModule,所以就直接返回。
下面是确认了options.type,我们这里是component。然后获得ngModule的路径,然后读取这个ngModule的源文件。来看看readIntoSourceFile方法:
1 | function readIntoSourceFile(host: Tree, modulePath: string): ts.SourceFile { |
这个方法是确保这个ngModule文件存在并可读,然后读取源文件并返回,没啥可说的。
然后接下来继续回到addDeclarationToNgModule看组件如何写入ngModule:
1 | const componentPath = `/${options.path}/` |
首先通过拼接生成的组件的ts文件的路径放在变量componentPath中。比如我们前面生成的TonyBest.component.ts组件的话,它的路径是:src/app/components/tony-best/tony-best.component.ts。我们在ngModule中引用组件的时候是相对路径,不会是绝对路径,所以这里调用buildRelativePath()方法生成相对路径,存放在relativePath变量中。然后classifiedName中存放这个组件的名字,然后调用addDeclarationToModule方法,将前面准备的东西都塞给它去处理,然后通过host.beginUpdate和host.commitUpdate即可完成更新。
PS:这里说的比较糙,但都是库提供的方法,具体后续可以研究。
之后是两个判断,看参数中有没有export和entryComponent这两个参数,这也不怎么重要,在创建组件的时候并不一定想的面面俱到,如果创建后有需要可以手动引入,但是自动引入这不是很酷么~😎
ok,我们这里也就结束了,我们主要是分析了@schematics/angular/components里面我们使用cli创建component组建的时候的运行过程。有些事情,你研究研究总感觉是魔法,研究清楚了,就可以为自己所用,解开那层神秘感,探寻内部的原理,这就是探索的乐趣。
下一篇我们再开始用掌握的东西来创建我们自己的原理图,来创建“page”类型的组件。