AngularJS 拾遗(一)

156 阅读12分钟

本文总结了 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 相关的一些脚本文件。这一定程度上反映出其工作原理。参考下面这张图。

ngStart.png

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. 父子组件通信原理

设置输入绑定

  1. 在父组件模板中找到创建子组件的位置

    • 在父组件的模板文件中,找到创建子组件的代码位置。
  2. 决定用于从父组件到子组件通信的属性名称

    • 确定一个属性名称,用于在父组件和子组件之间传递数据。
  3. 向子组件添加一个新的绑定,指定要传递的数据

    • 在子组件上添加一个新的数据绑定,指明要传递给子组件的数据。
  4. 在子组件的类文件中,添加一个输入属性

    • 这告诉子组件应期待父组件提供此属性的值。
  5. 在子组件的模板文件中,引用该输入属性

    • 在子组件的模板中,使用这个输入属性来访问传递的数据。

9. ngFor 拾遗

  1. *ngFor 循环的是绑定的元素本身而不是其子元素。
  2. *ngFor*ngIf 不能同时使用在一个元素上,如果非要这么做,请使用不同的 div 层,将其分割开来。
  3. *ngFor 指定序列号:<span *ngFor="let letter of randomText.split(); let i = index;"></span>" 可以这样认为,那就是在等号右边的双引号中是可以写 Javascript 的。

10. faker 第三方库的使用

在很多时候,我们需要 mock 一些假数据,但是有的时候缺乏想象力,这种情况下我们就可以使用第三方库 faker 了。

  1. 安装 faker: npm install faker @types/faker
  2. 然后参考官方说明进行使用: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>

上面的代码中有两处需要注意:

  1. 虽然在形式上 i 好像是先使用后定义的,但实际上这完全没有任何的影响。
  2. 我们的 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 指的则是绑定自定义指令的元素本身。所以:

  1. this.viewContainer.clear(); 表示的含义实际上是清空绑定元素的内部;
  2. 而下面的代码实际上则是重新生成其内部内容:
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, 它们分别是:

  1. Domain: 包含实现一个完整 feature 的所有组件。
  2. Routed: 可以看成是在路由表种的 Domain Module.
  3. Routing: 定义路由表的 Module.
  4. Service: 用来定义服务的 Module, 便于其在不同的 Module 中使用。
  5. Widget: 提供公共组件的 Module.

Module 元信息

  1. declarations: 声明的是本模块中创建的一些内容,包括:组件、管道、指令。
  2. imports: 引入其它模块
  3. exports: 向外提供本模块创造的或者本模块引入的一些内容,包括:组件、管道、指令。
  4. providers: 旧版本中用来提供指令的方式(已经弃用);声明一些其他内容,如拦截器。
  5. 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. 路由懒加载步骤

  1. 选择要懒加载的模块

    • 确定哪些模块将通过懒加载进行加载。
  2. 从项目中移除这些模块的导入语句

    • 对于每个要懒加载的模块,移除在项目其他位置的导入语句。
  3. AppRoutingModule 中定义路由

    • AppRoutingModuleroutes 数组中定义一个路由,指定何时加载该模块。
  4. 编辑懒加载模块的路由文件

    • 在懒加载模块的路由文件中,编辑每个路由的 path,使其相对于在 AppRoutingModule 中指定的路径。

23. routerLink 的相对路由和绝对路由

假设目前页面上的 url 为:http://localhost:4200/collections, 并且页面上有一个跳转标签:

<a [routerLink]="target_url">跳转至不同路由路径</a>

下面是 target_url 取不同值得时候跳转之后得路径:

  1. ./ -> http://localhost:4200/collections
  2. / -> http://localhost:4200
  3. ../hi -> http://localhost:4200/hi
  4. BOO -> http://localhost:4200/collections/BOO
  5. ./BOO -> http://localhost:4200/collections/BOO

可以看出来,以 dot 开头或者直接以路径字母开头的 target_url 跳转到相对于当前路径的位置;而以 / 开头的则表示绝对路由,或者相对于根路径的路由。

如何解决匹配 http://localhost:4200/collections/BOO 的时候将 http://localhost:4200/collectionshttp://localhost:4200 也匹配上的问题

这个问题在很多情况下会造成意料之外的行为,为此我们使用另外一个指令变量 routerLinkActive0ptions 来解决此问题:

<a 
    class="item" 
    routerLink="./" 
    routerLinkActive="my_active_cls" 
    [routerLinkActiveOptions]="{exact: true}"
>
    Biography
</a>