上一篇文章我们了解了自定义原理图的基本操作,可以简单生成文件,并和Angular Cli进行链接,我们如何真正的创建我们自己的原理图?在使用Angular Cli
自带的命令创建组件的时候是可以更新模块里面声明组价的语句的,那么它是如何实现的?我们来细致的分析下@schematics/angular/component
的源代码。
文件组织结构
index.ts
里面是处理模板和项目对接的逻辑schema.json
里面是这个声明这个原理图里需要的参数files
里面是模板文件,会根据参数生成最终的组件文件(html文件、样式文件、spec测试文件、ts文件)
index.ts文件分析
首先看文件里面的方法:
function readIntoSourcefile(host: Tree, moudlePath: string): ts.SourceFile
function addDeclarationToNgMoudle(options: CommponentOptions): Rule
function 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”类型的组件。