在angular中可以简单的这样的认为:每个界面就一个组件。比如当前我们需要一个教师添加界面,那么就对应需要一个教师添加组件`TeacherAddComponent`。 # 创建组件 我们在`app`文件夹下,创建`TeacherAddComponent`的C层`teacher-add.component.ts`以及V层`teacher-add.component.html`并输入以下内容: app/teacher-add.component.ts ```js import {Component, OnInit} from '@angular/core'; /** * 教师添加组件 */ @Component({ templateUrl: './teacher-add.component.html' ➊ }) export class TeacherAddComponent implements OnInit { ➋ ngOnInit(): void { } } ``` * ➊ 定义项目模板`teacher-add.component.html` * ➋ 初始化组件 teacher-add.component.html ```html <form id="teacherAddForm"> <div> <label for="name">姓名:</label> <input type="text" id="name" name="name"/> </div> <div> <label for="username">用户名:</label> <input type="text" id="username" name="username"> </div> <div> <label for="email">邮箱:</label> <input type="email" id="email" name="email"> </div> <div> <label>性别:</label> <label> <input type="radio" name="sex" value="true"> 男</label> <label> <input type="radio" name="sex" value="false"> 女</label> </div> <div> <button>提交</button> </div> </form> ``` # 绑定路由 app/app-routing.module.ts ```js import {NgModule} from '@angular/core'; import {Routes, RouterModule} from '@angular/router'; import {TeacherAddComponent} from './teacher-add.component'; const routes: Routes = [ { ➊ path: 'add', ➋ component: TeacherAddComponent ➌ } ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule] }) export class AppRoutingModule { } ``` * ➊ routes数组中的每一项的类型均是对象`{}`。 * ➋ 该对象中的`path`属性规定了访问路径。 * ➌ 该对象中的`component`属性规定了对应的组件。 上面的过程我们还可以称为:编排路由的索引,即访问某些URL应该对应找到些组件。在英文中`map`译成名词是地图,译成动词则可取编排索引之意,所以上述过程在英文中又叫做`mapping`。以`ing`结尾的代表正在进行的动作,所以我们刚刚在绑定路由的过程中,恰恰是在`mapping`。而`mapping`在计算机的专业术语中,又称为`映射`。 # 测试 ![](https://img.kancloud.cn/7c/52/7c52145d5bb11a50cca22ca124a0f9cf_1111x132.png) 这个错误大概再说:TeacherAddComponent组件并不属于任何模块`或者`它虽然属于某个模块(潜台词)但该模块并没有并导入到当前模块中。 之所以出现了该错误由于我同大家一样,在编写教程的时候,早就把教程中在前面讲过的一张图忘的一干二净了。 ![](https://img.kancloud.cn/4d/be/4dbe104efb560c95003f6f1cf6f3fe83_390x403.png) > 我们如此聪明的大脑会自动忘掉一些我们认为并不重要的事情然后为更重要的事情来腾挪出记忆空间。我们成长的过程其实是一次次地抽象、汇总、总结的过程,把原本繁琐的东西一点点的总结抽像压缩到大脑恰恰可以掌握的程度。而最终的压缩空间取决于我们在历史上对此类知识的掌握以及我们主动去思考的时间。 由上图可以看出:**组件是依赖于模块而存在的** ## 组件添加到模块 我们此时,再读1.5.1小节,还会收获:在`AppMoudle`的`@NgMoudle`注解的`declarations`选项是用来声明该模块下的组件的。 app.module.ts 变更前 ```js @NgModule({ declarations: [ AppComponent ], ``` app.module.ts 变更后 ```js import {TeacherAddComponent} from './teacher-add.component'; @NgModule({ declarations: [ AppComponent, TeacherAddComponent ], ``` 此时,我们访问:[http://localhost:4200/add](http://localhost:4200/add)地址时,控制台的错误消失。随之而来的新问题是:为什么还在显示`AppComponent`,而不是显示`TeacherAddComponent`。这是由于: * ① 系统定义了启动组件为`AppComponent`,则在系统启动时,永远会启动该组件。 * ② 路由定义仅代表定义了转发规则,但并不代表立即启动此规则。 * ③ 由启动的的组件`AppComponent`来决定是否启用路由规则。 ## 在`AppComponent`中启用路由 在`AppComponent`的V层的适当的位置添加代码`<router-outlet></router-outlet>`,则表示:➀加载路由组件(启用路由)➁将路由组件显示的内容显示在当前位置。 app.component.html ``` <router-outlet></router-outlet> ➊ <table> <tr> <th>序号</th> <th>姓名</th> <th>用户名</th> <th>邮箱</th> <th>性别</th> <th>注册时间</th> <th>操作</th> </tr> <tr *ngFor="let teacher of teachers" > <td>{{teacher.id}}</td> <td>{{teacher.name}}</td> <td>{{teacher.username}}</td> <td>{{teacher.email}}</td> <td> <span *ngIf="teacher.sex">女</span> <span *ngIf="!teacher.sex">男</span> </td> <td>{{teacher.createTime | date: 'yyyy-MM-dd'}}</td> <td>删除</td> </tr> </table> ``` * ➊ 放到教师列表前面 效果: ![](https://img.kancloud.cn/01/6c/016c7c32a76f3b0e1309a8081a18d66e_485x208.png) ```html <table> <tr> <th>序号</th> <th>姓名</th> <th>用户名</th> <th>邮箱</th> <th>性别</th> <th>注册时间</th> <th>操作</th> </tr> <tr *ngFor="let teacher of teachers" > <td>{{teacher.id}}</td> <td>{{teacher.name}}</td> <td>{{teacher.username}}</td> <td>{{teacher.email}}</td> <td> <span *ngIf="teacher.sex">女</span> <span *ngIf="!teacher.sex">男</span> </td> <td>{{teacher.createTime | date: 'yyyy-MM-dd'}}</td> <td>删除</td> </tr> </table> <router-outlet></router-outlet> ➊ ``` * ➊放至教师列表后面 ![](https://img.kancloud.cn/11/bd/11bd40daabf785e650559b51a279617c_514x215.png) # 本节小测 尝试再建立`TeacherEditComponent`组件,将其映射到`edit`地址上并测试。 ## 上节答案 由于`bootstrap`属性的接收值是一个数组,那么我认为是可以输入多个启动组件的。为了验证自己想的是否正确,我直接将`TeacherAddComponent`也加入到启动的列表中。 ```js import {TeacherAddComponent} from './teacher-add.component'; bootstrap: [AppComponent, TeacherAddComponent] ``` 访问[http://localhost:4200/](http://localhost:4200/)结果: `TeacherAddComponent_Host.ngfactory.js? [sm]:1 ERROR Error: The selector "ng-component" did not match any elements` > 结论一:不行,启动组件只支持一个。 其实到这我们直接记往该结论就可以了,在很多项目中我们的确只使用一个`AppComponent`组件就够了,这个结论虽然不见得是`绝对正确`的,但已经解决了我们当前碰到的问题,本着`够用的就是最好的`原则我们已经圆满的完成了任务。 ***** 前面我们讲过知识就是在不断的证真与证违中积累及升华的。下面我进一步的展示一下该理论。 看报错提示好像与启动组件的个数无关,下面我继承测试:在启动属性上只保留`TeacherAddComponent`,删除原`AppComponent`组件,看是否正常启动`TeacherAddComponent`。 ```js bootstrap: [TeacherAddComponent] ``` 结果仍然是: `TeacherAddComponent_Host.ngfactory.js? [sm]:1 ERROR Error: The selector "ng-component" did not match any elements` 此时,便发生了证违。即证明结论是`违`的,这将是对此知识点升华的开始。 > 结论二:结论一不一定正确。 ***** 观察报错信息,说这个"ng-component" `selector` 没有匹配上任何的元素。然后我们通过观察`AppComponent`与`TeacherAddComponent`发现:`TeacherAddComponent`较`AppComponent`少了个`selector`属性 app.component.ts ```js @Component({ selector: 'app-root', ➊ templateUrl: './app.component.html', styleUrls: ['./app.component.sass'] }) /** * 实现OnInit接口,该接口规定了ngOnInit方法。 * angular在组件准备完毕后,将自动调用ngOnInit方法 */ export class AppComponent implements OnInit { ``` * ➊此处有selector属性 teacher-add.component.ts ```js @Component({ ➊ templateUrl: './teacher-add.component.html' }) export class TeacherAddComponent implements OnInit { ngOnInit(): void { } } ``` ➊ 未添加selector 尝试添加selector teacher-add.component.ts ```js @Component({ selector: 'app-teacher-add', templateUrl: './teacher-add.component.html' }) ``` 此时报错信息变成了: `TeacherAddComponent_Host.ngfactory.js? [sm]:1 ERROR Error: The selector "app-teacher-add" did not match any elements` 看来的确有影响,但却没有解决根本问题。 > 结论:是否配置selector对启动成功与否无影响 。如果未给组件配置selector,则系统为其添加默认selector => `ng-component`。 ***** 我们继续瞎猜加验证。刚刚报错的`app-teacher-add`是我刚刚手动添加的,然后报错了。那么以前使用`AppComponent`,其中的`selector`是`app-root`为什么不报错呢?试试将两个组件的`selector`换一下: teacher-add.component.ts ```js @Component({ selector: 'app-root', templateUrl: './teacher-add.component.html' }) export class TeacherAddComponent implements OnInit { ngOnInit(): void { } } ``` app.component.ts ```js @Component({ selector: 'app-teacher-add', templateUrl: './app.component.html', styleUrls: ['./app.component.sass'] }) /** * 实现OnInit接口,该接口规定了ngOnInit方法。 * angular在组件准备完毕后,将自动调用ngOnInit方法 */ export class AppComponent implements OnInit { ``` 结果: ![](https://img.kancloud.cn/a6/7c/a67c21674248bc1e9d60eba16e1a335f_278x192.png) 正常显示了! > 结论:可以变更启动的组件,但该组件的`selector`必须是`app-root` ***** 我们继续猜:该组件的`selector`必须是`app-root`,为什么不可以是其它呢?可以在哪指定了`app-root`这个值。全项目搜索下看看能不能找到这个指定的它的位置。 ![](https://img.kancloud.cn/b0/67/b0678bc5653a11b7ccaab381d1702cd0_960x207.png) 从搜索的结果上不难看出,原来在`index.html`中规定了这个`app-root`,打开该文件然后将其修改为:`app-teacher-add`呢? index.html ```html <!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <title>WebApp</title> <base href="/"> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="icon" type="image/x-icon" href="favicon.ico"> </head> <body> <app-teacher-add></app-teacher-add> </body> </html> ``` 结果: `TeacherAddComponent_Host.ngfactory.js? [sm]:1 ERROR Error: The selector "app-root" did not match any elements` 我好像懂了:启动时找`TeacherAddComponent`没错,`TeacherAddComponent`的selector为`app-root`(我们前面刚改了),系统启动时在`index.html`中查找该selector`app-root`,如果没找到则报 `The selector "app-root" did not match any elements`,即这个选择器没有匹配任何元素的错误。 > 结论三:启用组件的selector必须在index.html中被使用。 ***** 最后,我们再来彻底推翻原结论一,即启动组件只能有一个。 我们先进行组件的恢复,将`TeacherAddComponent`的`selector`设置为`app-teacher-add`,将`AppComponent`的`selector` 设置为`app-root`。保持`index.html`中的`<app-teacher-add></app-teacher-add>`不变。 系统正常显示教师添加界面。 然后将两个组件都添加到启动中: ```js bootstrap: [TeacherAddComponent, AppComponent] ``` 再将`app-root`也添加到`index.html`中。 index.html ```html <body> <app-teacher-add></app-teacher-add> <app-root></app-root> </body> ``` 测试: ![](https://img.kancloud.cn/b3/f4/b3f4d18cb1d020038e8b77b7d9473a5d_493x276.png) > 结论四:启用组件可以为多个,但每个组的selector必须在index.html中被使用。 **总结:** 结论四并不见得正确,但它此时已经解决了我前面所有的问题。在当前来讲,已经是最适用的合理的答案。 > 结论是否绝对正确并不重要,重要的是它是否适用于当前自己遇到的所有同类问题。 [源码地址](https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step2.3.1.0) # 参考文档 | 名称 | 链接 | 预计学习时长(分) | | --- | --- | --- | | 入口组件 | [https://www.angular.cn/guide/entry-components](https://www.angular.cn/guide/entry-components) | 10 | | 启动过程 | [https://www.angular.cn/guide/bootstrapping](https://www.angular.cn/guide/bootstrapping) | 15 | | 路由配置 | [https://www.angular.cn/guide/router#configuration](https://www.angular.cn/guide/router#configuration) | 5 | | 路由出口 | [https://www.angular.cn/guide/router#router-outlet](https://www.angular.cn/guide/router#router-outlet) | 5 | | 组件简介 | [https://www.angular.cn/guide/architecture-components](https://www.angular.cn/guide/architecture-components) | 25 | | 源码地址 | [https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step2.3.1](https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step2.3.1) | - |