本文总结了 AngularJS 使用的时候容易忽略的一些坑点。适合学完 Angular 基础并且有一定事件经验的工程师阅读,喜欢的话收藏起来吧,这是一个系列文章,大概在 5 篇左右。
1. 如何引入 bulma 组件库
我们通过在 styles.css 文件中引入相关 css 文件的方式将 bulma 组件库加入到我们的项目中。
@import 'bulma/css/bulma.css';
2. Directive 的分类
在 Angular 中,我们将 directives 分成两种:structural 和 attribute. 前者的作用是控制 DOM 元素的增加和减少;而后者则是控制元素上面的 attribute, 进而控制其表现出既定的行为或者样式.
3. 将 Angular 应用部署到公网
先登录到网站 https://vercel.com/home 中,然后安装相关的依赖:
npm install -g now
然后登录到 now:
now login
最后直接运行 now 命令:
now
运行 now 之后,会发送邮件并等待确认,确认之后就开始定制化的发布流程了。发布成功之后就布置到了公网,我觉得这完全可以作为面试的演示 demo 来用了。
4. 关于 css 文件的范围问题
在 Angular 应用中,我们将影响全局的样式放在 styles.css 文件中,然后将各个组件的样式文件分别放置,例如 app.component.css 中的样式只影响 app.component 组件的样式。
这里有一个非常坑的点就是,在 app.component.css 中,你只能选中这个元素的子元素,如果你想选择 app.component 的最外层元素,你需要用到伪类 :host, 如下所示:
:host {
display: flex;
}
5. 关于 Angular Component 一些相关的知识
- 所有
Angular应用程序由多个不同的组件构成。 - 每个组件旨在实现应用中在屏幕上可见的单一【事物】。
- 一个组件封装了所有 HTML 和代码,以确保一个小部件正确工作。
- 一个组件可以在同一个应用程序中被多次重用。
- 组件可以被嵌套,或者彼此显示在内部。
- 每个应用程序都有一个名为
App组件的组件,它始终是最顶层的父组件。 - 每个组件都有自己的组件类(
Component Class)、组件模板(Component Template)、组件CSS文件(Component CSS File)和规范文件(Spec File)。
6. Angular 是如何生效的
如果我们仔细观察使用脚手架生成的 Angular 项目,不难发现,其 index.html 中的代码如下所示:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Demo</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-root></app-root>
</body>
</html>
这里并没有什么 Javascript 代码,那么是如何体现 Angular 呢?如果我们将项目跑起来,在浏览器中会发现此时的 index.html 的内容就变成了:
<html lang="en">
<head>
<meta charset="utf-8">
<title>Demo</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link rel="stylesheet" href="styles.css"></head>
<body>
<app-root></app-root>
<script src="runtime.js" type="module"></script>
<script src="polyfills.js" type="module"></script>
<script src="styles.js" defer></script>
<script src="vendor.js" type="module"></script>
<script src="main.js" type="module"></script>
</body>
</html>
可以看出来,webpack 为我们自动添加了与 angular 相关的一些脚本文件。这一定程度上反映出其工作原理。参考下面这张图。
7. Angular 启动的过程
Angular 加载每个组件类文件,并检查 'selector' 属性
- Angular 加载每个组件的类文件,并检查其中的 'selector' 属性。
Angular 然后查看加载到浏览器中的 HTML 文档
- Angular 接着查看已经加载到浏览器中的 HTML 文档。
<app-root> 找到了!Angular 找到一个具有匹配 selector 的组件
- 当遇到
<app-root>标签时,Angular 发现一个具有匹配 'selector' 的组件。
Angular 创建该组件的实例
- Angular 创建该组件的一个实例。
Angular 将实例的模板转换为真实的 HTML,然后将其插入到 app-root 元素中(即“宿主”元素)
- Angular 将组件实例的模板转换成实际的 HTML,并将其嵌入到 app-root 元素中。
在检查应用模板的过程中,Angular 看到 'app-card' 元素
- 在检查应用程序的模板时,Angular 发现了 'app-card' 元素。
Angular 创建该组件的实例
- Angular 为 'app-card' 创建了一个实例。
8. 父子组件通信原理
设置输入绑定
-
在父组件模板中找到创建子组件的位置
- 在父组件的模板文件中,找到创建子组件的代码位置。
-
决定用于从父组件到子组件通信的属性名称
- 确定一个属性名称,用于在父组件和子组件之间传递数据。
-
向子组件添加一个新的绑定,指定要传递的数据
- 在子组件上添加一个新的数据绑定,指明要传递给子组件的数据。
-
在子组件的类文件中,添加一个输入属性
- 这告诉子组件应期待父组件提供此属性的值。
-
在子组件的模板文件中,引用该输入属性
- 在子组件的模板中,使用这个输入属性来访问传递的数据。
9. ngFor 拾遗
*ngFor循环的是绑定的元素本身而不是其子元素。*ngFor和*ngIf不能同时使用在一个元素上,如果非要这么做,请使用不同的 div 层,将其分割开来。*ngFor指定序列号:<span *ngFor="let letter of randomText.split(); let i = index;"></span>"可以这样认为,那就是在等号右边的双引号中是可以写 Javascript 的。
10. faker 第三方库的使用
在很多时候,我们需要 mock 一些假数据,但是有的时候缺乏想象力,这种情况下我们就可以使用第三方库 faker 了。
- 安装 faker:
npm install faker @types/faker - 然后参考官方说明进行使用:
https://www.npmjs.com/package/faker/v/5.5.3
11. 一段非常有趣的代码
<p class="has-text-centered">
<span
[class]="compare(letter, enteredText[i])"
*ngFor="let letter of randomText.split(''); let i = index;"
>{{letter}}</span>
</p>
上面的代码中有两处需要注意:
- 虽然在形式上 i 好像是先使用后定义的,但实际上这完全没有任何的影响。
- 我们的 letter 应该紧紧的贴合左右的标签,以免不必要的空隙出现。
上面的 compare 函数长这样:
compare(randomLetter: string, enteredLetter: string){
if(!enteredLetter) return 'pending';
return randomLetter === enteredLetter ? 'correct' : 'incorrect';
}
12. 一些很有用的内置管道
12.1 number 管道
我们可以使用 number 管道来格式化 number 类型的数据。
<div>{{height | number: '1.1-0'}}</div>
表示,小数点前面一位,后面 0-1 位。
12.2 管道的连用
<div>{{miles | convert: 'cm' | number: '1.0-2'}}</div>
12.3 管道的使用范围
我们不仅可以在 interpolation 中使用管道,我们还可以在 directive 中使用管道,如下所示:
<div *ngIf="(miles | convert: 'km') > 10">{{miles | convert: 'km'}}</div>
13. 高亮当前页
<nav>
<ul class="pagination">
<li
class="page-item"
[ngClass]="{active: i === currentPage}"
*ngFor="let image of images; let i = index;"
><a class="page-link">{{i+1}}</a></li>
</ul>
</nav>
错误的代码:
<nav>
<ul class="pagination">
<li
class="page-item"
[ngClass]="{active: i === currentPage}"
*ngFor="let image of images; let i = index;"
*ngIf="i < 5"
><a class="page-link">{{i+1}}</a></li>
</ul>
</nav>
正确的代码:
<nav>
<ul class="pagination">
<ng-container *ngFor="let image of images; let i = index;">
<li
class="page-item"
[ngClass]="{active: i === currentPage}"
*ngIf="i < 5"
><a class="page-link">{{i+1}}</a></li>
</ng-container>
</ul>
</nav>
我们必须将两个指令分别放置在不同的 dom 上,但是我们又不想引入新的元素,所以只好使用 ng-container 来做结构上的调整。
14. 关于指令 ngSwitch
如下所示的代码,展示了指令 ngSwitch 的用法。
<div [ngSwitch]="currentPage">
<div *ngSwitchCase="0">Current Page is Zero</div>
<div *ngSwitchCase="2">Current Page is Two</div>
<div *ngSwitchCase="3">Current Page is Three</div>
<div *ngSwitchDefault>Unknown Current Page!</div>
</div>
15. 自定义的带参数指令
如下所示,我们自定义了一个名为 appClass 的指令,而这个指令的作用就是为了给绑定的元素上色,其内部的代码为:
import {Directive, Input, ElementRef} from '@angular/core';
@Directive({
selector: '[appClass]'
})
export class ClassDirective {
@Input() backgroundColor: string;
constructor(private element: ElementRef){
this.element.nativeElement.style.backgroundColor = this.backgroundColor;
}
}
对于上面的自定义指令,我们可以看到参数是通过被 Input 所修饰的 backgroundColor 传进来的,因此可以在 this 上面找到 backgroundColor 的值。我们可以通过下面的方式传递指令参数:
<h4 appClass [backgroundColor]="'red'">{{'Let us test our directive!'}}</h4>
那么我们能否对其进行简化呢?事实上,如果我们不纠结于 backgroundColor 这个语义化的变量名,我们将上述代码修改成:
import {Directive, Input, ElementRef} from '@angular/core';
@Directive({
selector: '[appClass]'
})
export class ClassDirective {
@Input() appClass: string;
constructor(private element: ElementRef){
this.element.nativeElement.style.backgroundColor = this.appClass;
}
}
这样的话,我们指令的使用就可以简化成如下的形式:
<h4 [appClass]="'red'" >{{'Test our own directive!'}}</h4>
但是这样依赖我们确实就丢失了语义化,得不偿失,有没有更好的办法呢?
import {Directive, Input, ElementRef} from '@angular/core';
@Directive({
selector: '[appClass]'
})
export class ClassDirective {
@Input('appClass') backgroundColor: string;
constructor(private element: ElementRef){
this.element.nativeElement.style.backgroundColor = this.backgroundColor;
}
}
这里我们做了一次映射,将传入的 appClass 的值映射成指令内部代码使用的 backgroundColor 变量的值。
我们可以通过 setter 将上面的代码改的更加具有响应式:
import {Directive, Input, ElementRef} from '@angular/core';
@Directive({
selector: '[appClass]'
})
export class ClassDirective {
constructor(private element: ElementRef){
this.element.nativeElement.style.backgroundColor = this.backgroundColor;
}
@Input('appClass') set backgroundColor(color: string){
this.element.nativeElement.style.backgroundColor = this.backgroundColor;
};
}
这样一来,每次我们在调用方修改 appClass 的值,我们的指令就会修改引用的 nativeElement 元素的背景色。上面的代码还可以继续简化成为:
import {Directive, Input, ElementRef} from '@angular/core';
@Directive({
selector: '[appClass]'
})
export class ClassDirective {
constructor(private element: ElementRef){}
@Input('appClass') set backgroundColor(color: string){
this.element.nativeElement.style.backgroundColor = this.backgroundColor;
};
}
16. 仿写 *ngFor -- 如何自定义一个结构指令
之前说过了,Angular 中的指令分成结构性的和属性性的两种,现在就让我们仿照 *ngFor 来写一个结构性质的指令吧。
export class TimesDirective {
constructor(
private viewContainer: ViewContainerRef,
private templateRef: TemplateRef<any>,
){}
@Input('appTimes') set render(times: number) {
this.viewContainer.clear();
for(let i = 0; i < times; i++){
this.viewContainer.createEnbeddedView(this.templateRef, {
index: i,
});
}
}
}
上面代码中注入的两个实例 this.templateRef 代表的是绑定自定义指令的 innerHTML. 而 this.viewContainer 指的则是绑定自定义指令的元素本身。所以:
this.viewContainer.clear();表示的含义实际上是清空绑定元素的内部;- 而下面的代码实际上则是重新生成其内部内容:
this.viewContainer.createEnbeddedView(this.templateRef, {index: i,});
上述的代码以被清空的 this.templateRef 作为原料生成新的内容,并且提供了附加信息 {index: i}, 这个附加信息是可以在使用此指令的时候获取到,如下所示:
<ng-container *appTimes="images.length; let i = index">
<li
class="page-item"
[appClass]="{ active: i === currentPage }"
*ngIf="checkWindowIndex(i)"
>
<a (click)="currentPage = i" class="page-link">{{i+1}}</a>
</li>
</g-container>
代码 *appTimes="images.length; let i = index" 中,既有输入又有输出,个人认为这并不太好。
17. 另外一个组件可 -- semantic
semantic UI 由两部分组成:Semantic CSS + Semantic JS
18. Module 的种类
我们一共具有 5 种典型的 Module, 它们分别是:
- Domain: 包含实现一个完整 feature 的所有组件。
- Routed: 可以看成是在路由表种的 Domain Module.
- Routing: 定义路由表的 Module.
- Service: 用来定义服务的 Module, 便于其在不同的 Module 中使用。
- Widget: 提供公共组件的 Module.
Module 元信息
- declarations: 声明的是本模块中创建的一些内容,包括:组件、管道、指令。
- imports: 引入其它模块。
- exports: 向外提供本模块创造的或者本模块引入的一些内容,包括:组件、管道、指令。
- providers: 旧版本中用来提供指令的方式(已经弃用);声明一些其他内容,如拦截器。
- bootstrap: 在启动模块中使用,指定项目启动之后最先显示的组件。
19. href 和 routerLink 的不同
如下所示,两个 a 标签使用了不同的方式跳转至同一个 url, 它们的不同之处在于,前者的跳转会刷新页面,而后者则不会。所以对于 SPA 而言是断然不会使用前者的方式进行路由跳转的。
<a routerLink="/elements">Elements</a>
<a href="/elements">Elements</a>
<router-outlet></router-outlet>
点击绑定 routerLink 的 a 标签之后如何得到反馈
如果想要让 a 标签知道当前页面的路由和自己将要去的路由是一致的,那么我们需要使用另外一个指令 routerLinkActive, 如下所示:
<a
routerLink="/elements"
routerLinkActive="now_active"
class="item"
>
点我跳转并自动添加名为 now_active 的类名
</a>
20. 配置路由表的注意事项
一般来说我们会使用 Routing 类型的 Module 来为整个项目配置路由,并且这个 Routing Module 的名称一般是固定的,为 AppRoutingModule. 在这个模块中,我们会配置:
const routes: Routes = [
{
path: '',
redirectTo: 'home',
pathMatch: 'full'
},
{
path: '**',
component: ErrorComponent
}
];
由于 ** 是用来兜底的,所以就使得 AppRoutingModule 在 App.module 中 imports 的时候需要放到最后,否则 ** 会拦截期望路由路径。
21. ng-content 浅用
默认情况下我们引用自定义组件的时候,自定义组件的 innerHTML 部分的内容会被丢弃,但是我们在自定义组件的模板中也可以使用 <ng-content> 标签获取这部分信息。
引用组件:
<app-divider>
Placeholder Component
</app-divider>
组件模板:
<h1>
<ng-content></ng-content>
</h1>
<div class="ui divider"></div>
渲染结果:
<h1>
Placeholder Component
</h1>
<div class="ui divider"></div>
具名插槽 -- transclusion
上述代码展示的只是默认情况下的行为,但是我们可以更加灵活的使用插槽,我们可以定义很多插槽,然后将他们渲染到自定义组件的模板的特定位置上去。
引用组件:
<app-divider>
<span class="cls2">Placeholder Component2</span>
<span class="cls1">Placeholder Component1</span>
</app-divider>
组件模板:
<h1>
<ng-content select=".cls1"></ng-content>
</h1>
<ng-content select=".cls2"></ng-content>
<div class="ui divider"></div>
渲染结果:
<h1>
<span class="cls1">Placeholder Component1</span>
</h1>
<span class="cls2">Placeholder Component2</span>
<div class="ui divider"></div>
好用虽然好用,但是有一个问题,那就是为什么我非要用 span 标签包裹文字呢?我不想引入新的额外标签,有什么好的办法呢?
ng-container
如果只是为了做插槽,那么为了避免由此引入的额外标签,我们可以使用特殊标签 ng-container:
引用组件:
<app-divider>
<ng-container class="cls2">Placeholder Component2</ng-container>
<ng-container class="cls1">Placeholder Component1</ng-container>
</app-divider>
组件模板:
<h1>
<ng-content select=".cls1"></ng-content>
</h1>
<ng-content select=".cls2"></ng-content>
<div class="ui divider"></div>
渲染结果:
<h1>
Placeholder Component1
</h1>
Placeholder Component2
<div class="ui divider"></div>
22. 路由懒加载步骤
-
选择要懒加载的模块:
- 确定哪些模块将通过懒加载进行加载。
-
从项目中移除这些模块的导入语句:
- 对于每个要懒加载的模块,移除在项目其他位置的导入语句。
-
在
AppRoutingModule中定义路由:- 在
AppRoutingModule的routes数组中定义一个路由,指定何时加载该模块。
- 在
-
编辑懒加载模块的路由文件:
- 在懒加载模块的路由文件中,编辑每个路由的
path,使其相对于在AppRoutingModule中指定的路径。
- 在懒加载模块的路由文件中,编辑每个路由的
23. routerLink 的相对路由和绝对路由
假设目前页面上的 url 为:http://localhost:4200/collections, 并且页面上有一个跳转标签:
<a [routerLink]="target_url">跳转至不同路由路径</a>
下面是 target_url 取不同值得时候跳转之后得路径:
./->http://localhost:4200/collections/->http://localhost:4200../hi->http://localhost:4200/hiBOO->http://localhost:4200/collections/BOO./BOO->http://localhost:4200/collections/BOO
可以看出来,以 dot 开头或者直接以路径字母开头的 target_url 跳转到相对于当前路径的位置;而以 / 开头的则表示绝对路由,或者相对于根路径的路由。
如何解决匹配 http://localhost:4200/collections/BOO 的时候将 http://localhost:4200/collections 和 http://localhost:4200 也匹配上的问题
这个问题在很多情况下会造成意料之外的行为,为此我们使用另外一个指令变量 routerLinkActive0ptions 来解决此问题:
<a
class="item"
routerLink="./"
routerLinkActive="my_active_cls"
[routerLinkActiveOptions]="{exact: true}"
>
Biography
</a>