Angular开发:基础内容

本篇是Angular开发中需要掌握的基础内容

基础

1.创建项目

使用 ng new 项目名 来创建一个新的项目

使用 ng new project -S 来创建一个不使用测试文件的新项目

使用 ng new project --skip-install 来创建一个新项目,且跳过安装依赖

2.单向绑定

绑定数据:

1
2
3
4
// ts 中
title: string = "标题";
// html 中
<div>{{title}}</div>

绑定属性:

1
2
3
4
// ts 中
msg: string = "提示信息";
// html 中
<div [title]="msg"></div>

绑定 html

1
2
3
4
// ts 中
h2: string = "<h2>这是h2</h2>"
// html 中
<div [innerHTML]="h2"></div>

组件

视图包装

在 Angular 中,组件的 CSS 样式被封装进了自己的视图,可以通过在组件的元数据上设置视图封装模式,你可以分别控制每个组件的封装模式

封装模式如下:

  • ShadowDom:使用浏览器原生的 Shadow DOM 来实现(不进不出,全局样式进不来,组件样式出不去)
  • Emulated:默认值,通过预处理 CSS 代码来模拟 Shadow DOM 的行为(只进不出,全局样式能进来,组件样式出不去)
  • None:不使用视图封装,即把 CSS 添加到全局样式中(能进能出)

设置封装模式:组件元数据中的 encapsulation 属性来设置组件封装模式

1
encapsulation: ViewEncapsulation.ShadowDom

组件交互

(1).@input()

父组件通过 @input() 给子组件绑定属性设置输入类数据

方法如下:

  • 在父组件中定义一个属性

    1
    title: string = "Wrysmile's Blog";
  • 在父组件的页面中给子组件传值

    1
    <app-title [title] = "title"></app-title>
  • 在子组件中注入该属性

    1
    @Input() title?: string;
  • 在子组件的页面中即可使用该属性了

    1
    <p>{{title}}</p>
  • 同时也可以在父组件中修改该值,会实时同步到子组件

    1
    2
    // html文件
    <button (click) = "titleChange()">修改</button>
    1
    2
    3
    4
    // ts文件
    titleChange(){
    this.title = "Wrysmile 的小站"
    }

(2).@output()

父组件给子组件传递一个事件,子组件通过 @output() 弹射触发事件

@Output() 必须是 EventEmitter 类型的

方法如下:

  • 在父组件中定义属性以及触发的事件

    1
    2
    3
    4
    5
    list: Array<string> = ["Angular"];
    addList(str: string){
    // 将内容添加到列表中
    this.list?.push(str);
    }
  • 在父组件的页面中给子组件绑定该事件

    1
    2
    <p *ngFor="let item of list">{{item}}</p>
    <app-title (addList) = "addList($event)"></app-title>
  • 在子组件中弹射触发父组件中的事件

    1
    2
    3
    4
    @Output() addList = new EventEmitter();
    btnAdd(){
    this.addList.emit("Vue");
    }
  • 在子组件的页面中触发定义好的事件

    1
    <button (click) = "btnAdd()">我要学Vue</button>

(3).@ViewChild()

通过 @ViewChild() 来获取子组件的实例,来获取子组件的数据

该方法需要结合第二个使用,方法如下:

  • 在父组件的页面中给子组件定义一个模板引用变量

    1
    2
    <app-title #titleDom (addList) = "addList($event)"></app-title>
    <button (click) = "viewChildAdd()">父子组件交互</button>
  • 在父组件中获取子组件的实例,然后就通过 .child 来调用子组件中的方法

    1
    2
    3
    4
    @ViewChild("titleDom") child: any;
    viewChildAdd(){
    this.child.btnAdd();
    }
  • 最后在子组件中弹射触发事件即可

组件样式

(1).:host

作用:用来选择当前组件中的子元素

场景:若想给某组件中的所有元素设置 flex 布局,可以不需要单独加一个盒子,而直接通过该伪类选择器给该组件设置 flex 布局

引申:

  • :host(.active)符合该类的组件
  • :host h2组件中符合该选择器的元素

(2).:host-content

作用:用来选择当前组件中的祖先节点

内容投影

(1).单插槽内容投影

使用 <ng-content> 来进行投影

(2).多插槽内容投影

使用 <ng-content select="[question]"> 来进行投影

动态组件

指令

1.内置指令

内置属性型指令:

  • NgClass:添加和删除一组 CSS 类

  • NgStyle:添加和删除一组 HTML 样式

  • NgModel:将数据双向绑定添加到 HTML 表单元素

内置结构型指令:

  • NgIf:从模板中创建和销毁子视图

  • NgFor:为列表中的每个条目重复渲染一个节点

  • NgSwitch:一组在备用视图之间切换的指令

2.属性型指令

建立属性型指令:

1
2
3
ng generate directive xxx

ng g d xxx

应用属性型指令:只要在相应的 HTML 模板中应用指令选择器名即可

3.结构型指令

简写形式: Angular 将结构型指令前面的星号转换为围绕宿主元素及其后代的 <ng-template>

服务

1.创建服务

使用:

1
2
3
ng generate service 服务名

ng g s 服务名

最好在单独的文件中定义组件和服务,如果需要合并在同一个文件中,则必须先定义服务,再定义组件(顺序不对时会返回运行时的空引用错误)

服务文件中需要 @injectable() 装饰器来进行配置,其中 provideIn 共有三个属性值,如下:

  • root:注入到 app.module.ts 中,所有子组件都可以是用(推荐)
  • none:不设定服务作用域(不推荐)
  • 组件名:只作用于该组件(懒加载模式)

2.依赖注入

需要在 app.module.ts 中手动进行注入

使用 import 引入服务,并在 providers 中进行注入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { ListService } from './service/list.service';
@NgModule({
declarations: [
// 组件注入
],
imports: [
BrowserModule,
AppRoutingModule,
FormsModule,
ReactiveFormsModule
],
providers: [
// 服务注入
ListService
],
bootstrap: [AppComponent]
})

依赖项注入(DI)是一种设计模式,在这种设计模式中,类会从外部源请求依赖项而不是创建它们

在组件中注入服务时需要将依赖项注入组件的 constructor()

3.服务嵌套

当某个服务(B)依赖于另一个服务(A)时,需要在该服务(B)中注入另一个服务(A)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { Injectable } from '@angular/core';
import { HEROES } from './mock-heroes';
import { Logger } from '../logger.service';
@Injectable({
providedIn: 'root',
})
export class HeroService {
// 注入Logger服务
constructor(private logger: Logger) { }
getHeroes() {
this.logger.log('Getting heroes ...');
return HEROES;
}
}

路由

  使用路由可以在 Angular 中实现路径的导航模式,具体表现就是不同视图的切换。在 Angular 中,组件的实例化与销毁、模块的加载、组件某些生命周期钩子的发起都与路由相关

路由器 是一个调度中心,是一套规则的列表,能够查询当前 URL 对应的规则,并呈现出相关的视图

路由 是列表里面的一个规则,有如下字段:

  • path:表示该路由中的 URL 路径
  • component:表示与该路由相关联的组件

1.创建路由

如果使用脚手架安装时会询问是否创建路由文件,选择是时会自动生成 app-routing.module.ts

如果使用脚手架想要跳过询问直接生成路由文件可以使用 ng new 项目名 --routing 来生成一个带有应用路由模块的基本项目

如果使用脚手架创建项目时没有使用默认配置创建路由器,可以使用如下命令进行创建,其中参数如下:

  • --flat 把这个文件放进了 src/app 中,而不是单独的目录中
  • --module=app 告诉 CLI 把它注册到 AppModuleimports 数组中
    1
    ng generate module app-routing --flat --module=app

然后用下面的代码替换生成的文件中的代码:

1
2
3
4
5
6
7
8
9
10
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';

const routes: Routes = [];

@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }

2.配置路由规则

在路由文件中引入需要的组件,并在 routes 字段中配置规则

(1).基础路由配置规则

即包含 URL 的路由配置,如下:

1
2
3
4
{
path: "hello",
component: HelloComponent
}

(2).默认路由配置规则

即根据端口进入页面时默认显示,如下:

1
2
3
4
{
path: "",
component: HomeComponent
}

但默认配置状况下,如果地址栏输入错误,会显示项目的根目录网页,并不合适,使用 通配路由配置规则 可以解决

(3).通配路由配置规则

即 URL 错误时显示该界面(此时可以配置404页面),如下;

1
2
3
4
{
path: "*",
component: HomeComponent
},

3.组件渲染输出

在根页面中使用如下语句进行渲染

1
<router-outlet></router-outlet>

4.导航

可以在 <a> 标签中进行导航

1
<a [routerLink]="['/hello']" routerLinkActive="router-link-active" >进入hello页面</a>

5.路由嵌套

在需要嵌套的路由规则中通过 children 字段来进行设置:

1
2
3
4
5
6
7
8
9
10
{
path: "home",
component: HomeComponent,
children: [
{
path: "list",
component: ListComponent
}
]
}

在需要嵌套的页面中进行渲染

1
2
<a [routerLink]="['/home/list']" routerLinkActive="router-link-active" >进入list页面</a>
<router-outlet></router-outlet>

6.路由传值

(1).queryParams

<a> 标签中添加一个 queryParams 参数

1
<a [routerLink]="['/home/list']" [queryParams]="{id: 1}" routerLinkActive="router-link-active" >进入list页面</a>

在子组件中注入 route 服务

1
2
3
constructor(
private routerInfo: ActivatedRoute
) { }

然后就可以通过 routerInfo.snapshot.queryParams 来获取传递的参数

(2).params

通过修改路由配置文件中的路径来进行传值

1
2
3
4
{
path: "hello/:name",
component: HelloComponent
},

修改超链接中 routerLink 的第二个参数

1
<a [routerLink]="['/hello', 'params传参']"  routerLinkActive="router-link-active" >进入hello页面</a>

在子组件中注入 route 服务

1
2
3
constructor(
private routerInfo: ActivatedRoute
) { }

通过 subscribe 订阅的方式获取 name 参数

1
2
3
4
this.routerInfo.params.subscribe((params: Params) => {
this.name = params['name']
console.log(this.name);
})

注意:使用该方式时参数位置必须对应

7.路由顺序

Router在匹配路由时遵循的是先到先得的策略,所以路由顺序如下:

  • 先配置静态路径的路由
  • 再配置与默认路由匹配的空路径路由
  • 最后配置通配符路径

(1).默认路径路由

当所请求的 URL 与任何路由器路径都不匹配时,就会选择该路由

作用:展示404页面或跳转到应用的主组件

**两个星号

(2).重定向路由

当时用重定向路由时,浏览器会默认跳转到该路由上

重定向路由需要设置以下三个参数:

  • path:重定向源
  • redirectTo:重定向目标
  • pathMatch:如何匹配URL(full、prefix)

配置如下:

1
2
3
4
5
6
7
8
9
{
path: 'home',
component: HomeComponent
},
{
path: '',
redirectTo: '/home',
pathMatch: 'full'
},

8.编写位置

(1).app.moudule.ts文件

在该文件中导入 RouterModule,然后在 imports 字段中填写如下代码,并在代码中书写需要定义的路由规则:

1
RouterModule.forRoot([]),

(2).路由模块

使用如下命令创建一个默认路由配置文件

1
2
3
ng generate module app-routing --module app --flat

ng g m app-routing --module app --flat

app.module.ts 中引入该配置文件

进入该文件,引入 RouterModule 以及各个组件

imports 字段中填写如下代码,并在代码中书写需要定义的路由规则:

1
RouterModule.forRoot([]),

exports 字段中导出 RouterModule

9.路由守卫

路由守卫分为如下几种:

  • CanActivate:处理导航到某路由的情况
  • CanActivateChild:处理导航到某子路由的情况
  • CanDeactivate:处理从当前路由离开的情况
  • Resolve:在路由激活之前获取路由的数据
  • CanLoad:处理异步导航到某特性模块的情况

方法

为守卫创建服务,创建的时候可以选择守卫的类型

1
2
3
ng generate gurad 守卫名

ng g g 守卫名

在路由配置文件中按如下配置即可:

1
2
3
4
5
{
path: '/your-path',
component: YourComponent,
canActivate: [YourGuard],
}

10.路由策略

  路由器导航时其中的 URL 都存在于本地,并不会请求服务器,而当前浏览器提供两种策略来展示 URL 的样式:

  • PathLocationStrategy:默认策略,支持 “HTML 5 pushState” 风格
  • HashLocationStrategy:支持“hash URL”风格,即 URL 前必须加入井号才可以避免请求服务端

默认策略

如果使用默认策略,需要在项目的 index.html 中添加 <base href> 元素才可以正常工作

如果应用的根目录是 app 目录,那么可以设置如下:

1
2
3
4
5
6
7
<head>
<meta charset="utf-8">
<title>RoutingApp</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
</head>

表单

响应式表单

模板驱动表单

表单验证

自定义验证器

国际化

步骤

(1).添加 localize 包

在项目根目录中使用以下命令添加 localize 包到项目中

1
ng add @angular/localize

(2).标记要翻译的文本

Html 文件只需要在需要进行翻译的标签中添加 i18n,如下:

1
<h2 i18n>Top Heroes</h2>

TypeScript 文件只需要在构造函数中指定以下语句即可:

1
2
3
4
title = 'Tour of Heroes';
constructor(){
this.title = $localize `Tour of Heroes`;
}

(3).提取源语言文件

使用下面的命令将标记好的文本提取到源语言文件中

1
2
ng extract-i18n								// 在项目的根目录中生成源语言文件
ng extract-i18n --output-path src/locale // 修改生成地址到 src/locale

参数说明:

  • --output-path:改变生成文件的位置
  • --format:改变生成文件的格式(XLIFF1.2、XLIFF2、XML消息包)
  • --outFile:改变生成文件的文件名

(4).生成语言文件副本

提取出来的源语言文件是一个叫做 messages.xlf,当有不同语言需求时需要为不同语言生成源语言文件副本

复制并重命名为 messages.zh.xlf,zh 即为目标语言

(5).翻译语言文件副本

交由专业的翻译人员使用 XLIFF 文件编辑器来创建和编辑翻译

打开该文件,将 <source></source> 标签复制并粘贴在其下方,改为 <target></target> 标签,然后翻译标签中的文本即可

1
2
<source>Dashboard</source>
<target>仪表盘</target>

(6).定义本地环境

修改项目的 Angular.json 中相关配置,修改如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
"你的项目名": {
"i18n": {
"sourceLocale": "en-US", // 当前本地环境语言,en表示语言,US表示国家,这里即美国英语区
"locales": { // 本地环境标识符到翻译文件的映射表
"zh": "src/locale/messages.zh.xlf"
}
},
"architect": {
"build": {
"options": {
"localize": true, // true表示为每种本地环境生成一个应用版本
"aot": true, // 本地化组件模块需要进行预先编译
}
"configurations": {
"zh": { // 使用ng serve时配置
"localize": [
"zh"
]
}
}
}
"serve": {
"configurations": {
"zh": { // 使用ng serve 时配置
"browserTarget": "你的项目名:build:zh"
}
}
}
}
}

(7).合并翻译文件

构建项目时使用以下命令:

1
ng build --localize

直接在本地运行项目时需要将上一点中后面两个进行配置后使用以下命令:

1
ng serve --open --configuration=zh

引申:生成翻译文件时自动追加新内容

直接步骤操作时,当增加新内容时都会重新生成翻译文件,之前翻译过的内容还需重新翻译并不方便,所以可以使用插件来进行追加翻译,参考自 这里

导入 ngx-i18nsupport 包:

1
npm install ngx-i18nsupport --save-dev

在项目的根目录中新增 xliffmerge.json 文件,并写入如下内容:

1
2
3
4
5
6
{
"xliffmergeOptions": {
"srcDir": "src/locale",
"genDir": "src/locale"
}
}

package.json 文件中添加翻译合并脚本:

1
2
3
4
5
6
"scripts": {
// 生成翻译文件并存放在指定目录下
"extract-i18n": "ng extract-i18n --output-path src/locale",
// 调用配置文件将修改追加到指定语言的翻译文件中
"xliffmerge": "xliffmerge --profile xliffmerge.json zh"
},

之后在生成翻译文件时执行下面语句即可生成且追加修改了:

1
npm run extract-i18n;npm run xliffmerge;

最后合并翻译文件即可

动画

简单使用

在根模块中引入 BrowserAnimationsModule

将动画功能导入组件文件中:

  • 直接导入:即执行下面的动画定义即可
  • 导入动画文件:
    • 将整个触发器都定义在动画文件中
    • 在需要的组件中引入该动画文件,并引入 HostBinding
    • 使用 @HostBinding('@detailTransition') state = 'activated'; 来绑定动画
    • 在注入器中的 animations 数组中传入该动画
  • 可复用动画:见下方

动画定义

在组件中的 @Component() 装饰器的 animations: 属性下用代码定义你要用的动画,该属性传入的是一个数组

动画触发

使用 trigger() 函数来进行触发,其包含两个参数:

  • 触发器的名字
  • 数组,需要触发的动画

动画状态和样式

在触发器的第二个参数中定义动画状态和样式

使用 state() 函数来进行定义,其包含两个参数:

  • 状态的名字
  • 该状态的样式,使用 style() 函数来进行定义,其参数为一个对象,对象中传入需要设置的样式(小驼峰命名法)

动画转场和时序

在触发器的第二个参数中定义动画转场和时序

使用 transition() 函数来进行定义,其包含两个参数:

  • 表达式,定义两个转场状态之间的方向
  • 数组,接收一个或一系列的 animate() 函数,其包含3个参数
    • 持续时间,纯数字表示毫秒,表示秒需要使用字符串 '1s'
    • 延迟时间,同上
    • 速度动画:ease-in/ease-out/ease-in-out

执行顺序:上面的动画转场设置会覆盖下面的动画转场设置

动画展示

当动画的相关配置做完以后,就可以附加到页面模板中了

给定义好的动画触发器名前面加上 @ 符号并放入方括号中,然后使用属性绑定语法来绑定到模板表达式上,如下:

1
2
3
<div class="open-close-container" [@openClose]="isOpen ? 'open' : 'close'">
<p>盒子现在是 {{isOpen ? '开启' : '关闭'}}!</p>
</div>

动画禁用

当我们想要禁用某元素及其子元素的动画时,可以给该元素绑定 @.disabled,当其值为 true 时会禁止渲染所有动画

转场与触发器

通配符

使用 * 通配符表示任意状态,类似于路由,我们可以把以下状态放置到最后,当所有转场都不匹配时,就使用通配符定义的转场

1
2
3
transition('* => *', [
animate('1s')
]),

void

场景:使用 void 状态可以为进入或离开页面配置转场

使用:可以配合通配符进行使用

  • 当元素离开视图时,就会触发 * => void(别名:leave) 转场,而不管它离开前处于什么状态
  • 当元素进入视图时,就会触发 void => *(别名:enter) 转场,而不管它进入时处于什么状态

注意

  • 通配符状态 * 会匹配任何状态 —— 包括 void
  • 使用别名时可以与 ngIf/ngFor 一起使用

父子动画

每次在 Angular 中触发动画时,父动画始终会优先,而子动画会被阻塞

为了运行子动画,父动画必须查询出包含子动画的每个元素,然后使用 animateChild() 函数来运行它们

若父动画被禁用时,子动画可以使用如下方式运行:

  • 父动画可以使用 query() 函数来收集 HTML 模板中位于禁止动画区域内部的元素。这些元素仍然可以播放动画
  • 子动画可以被父动画查询,并且之后使用 animateChild() 来播放它

复杂序列

用于控制复杂序列的函数有:

  • query():查找一个或多个内部 HTML 元素,其包含两个参数:
    • css 选择器
    • 数组,传入需要设置的样式和 stagger() 函数
  • stagger():为多元素动画应用级联延迟,其包含两个参数:
    • 延迟时间
    • 数组,传入一个或多个 animate()
  • group():并行执行多个动画步骤,其包含一个数组参数,传入一个或多个 animate()
  • sequence():逐个顺序执行多个动画步骤,其包含两个参数:
    • style() 函数用来立即应用所指定的样式数据
    • animate() 函数用来在一定的时间间隔内应用样式数据

可复用动画

可以将动画单独定义在一个文件中,然后在需要的组件中引入即可

使用方法如下:

  • 在 ts 文件中导出一个用 const 定义的变量,其包含两个参数:style()animate()

    1
    2
    3
    4
    5
    6
    7
    8
    export const transAnimation = animation([
    style({
    height: '{{height}}',
    opacity: '{{opacity}}',
    backgroundColor: '{{backgroundColor}}'
    }),
    animate('{{time}}')
    ]);
  • 在组件设置的 animation() 函数的数组中使用 useAnimation() 函数来调用自定义动画,其包含两个参数:动画文件名和参数配置对象

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    transition('open => close', [
    useAnimation(transAnimation, {
    params: {
    height: 0,
    opacity: 1,
    backgroundColor: 'red',
    time: '1s'
    }
    })
    ]),

服务端渲染

基础

使用 Express 来进行渲染

优点

通过搜索引擎优化(SEO)来帮助网络爬虫
提升在手机和低功耗设备上的性能
迅速显示出第一个支持首次内容绘制(FCP)的页面

使用方法

在项目根目录的命令行窗口使用如下命令创建 app.server.module.ts

1
ng add @nguniversal/express-engine

渲染项目:

1
npm run dev:ssr

报错

error NG8002

报错信息:Can't bind to 'formControl' since it isn't a known property of 'input'.

报错场景:试图给 <input> 标签施加 formControl 这个 Directive 的时候出现

解决方法:在 app.module.ts 中导入 ReactiveFormsModule 模块,并在 NGModule 中的 imports 字段中引入

ng build 后的错误

参考这里

代码内错误

报错信息:类型“string | null”的参数不能赋给类型“string”的参数

报错场景:当将 localstorage 获取到的值转化为JSON格式时出现

报错原因:localstorage 返回的类型为 string || null,而 JSON.parse() 函数接收的是字符串类型,所以会报错

解决办法:

  • 先使用判断语句判断返回值是否为空,不为空时强制将其类型设置为 string
  • 也可以在 JSON.parse() 函数中使用或运算符,当 localstorage 返回为 null 时就使用或运算符后面的内容

终端错误

error TS2322

报错信息: Type ‘Event’ is not assignable to type ‘string’.

报错场景:当我在模板中使用 [(ngModule)] 时出现该错误

报错原因:缺少 FormModule 模块

解决办法:

  • 如果项目中只有一个 module,那么就在 app.module.ts 中引入
  • 如果项目中有其他 module,且该模板基于该 module,那么就在该 module 中进行引入

页面控制台错误

报错信息:No value accessor for form control with unspecified name attribute

报错场景:当我在模板中使用 [(ngModule)] 时出现该错误

报错原因:双向绑定出现在了非常规的地方,比如我这里是绑定在了 <label> 标签上,同理 <span>/<div> 等都会出现

解决办法:在双向绑定的标签内加入这一选项 ngDefaultControl,用于写入值和侦听输入元素更改

开发技巧

1.求数组中最大值

方法如下:

  • 排序
  • 使用 Math.max.apply(null, arr) ,apply方法可以将传入的数组展开为对应的参数,然后再执行max方法即可

2.(click)妙用

可以直接在其中修改值,如 (click)="isVisible = true;"

也可以传入多个事件,如 (click)="clearModel();changeTab('/model/create' , 'split');"