What's new in Angular 17 Anguar17更新后小编看DevUI社区群里发起了激烈的讨论,相比其他版本评价只能用"除却巫山不是云"来形容,可能略有夸张但是整体看确实让开发者体验提升不少。DevUI也借此契机将Angular的入门教程同步更新,为了让更多小伙伴可以快速上车~
由于Angular的学习曲线初始阶段比较陡峭,一些重要概念不是很好理解入门,因此,计划开展Angular培训系列课程,由浅入深的帮助大家掌握Angular的基本概念、重要特性、核心思想,最终达到能正确灵活运用Angular、高效开发前端页面的目的。
感谢我们的DevUI社区贡献者smileHoo提供的优质文章!
本篇是该系列课程的第一期内容。
一、了解Angular
What is Angular
Angular 是一个应用设计框架与开发平台
,旨在创建高效而精致的单页面应用
。
那么什么是单页面应用呢?
(单页面结构视图)
(多页面结构视图)
如图。单页面(SPA)
,顾名思义,指的是一个 Web 网站中只有唯一的一个 HTML 页面,所有的功能与交互都在这唯一的一个页面内完成。浏览器一开始会加载必需的公共资源,当进行跳转的时候,仅刷新局部资源;多页面(MPA)
跳转刷新所有资源,每个公共资源(js、css等)需选择性重新加载。所以SPA会有较好的用户体验。
Why use Angular
优势:
- 模板功能强大丰富,自带了丰富的angular
指令
。还可以根据需要自定义指令。 Angular CLI
这个命令行工具,可快速轻松地搭建应用程序。使用简单的命令,可以启动项目、生成组件、库和服务、添加功能、测试和更新当前项目。- 是一个比较完善的前端
框架
,包含服务,模板,数据双向绑定,模块化,路由,过滤器,依赖注入等诸多功能。
学习难点:
- 框架大,知识范围广,学习成本高,上手难度大。
- 灵活度高,对程序员本身要求高。
- 国内应用度较小,网上相关教程文档少,加大了学习难度。
二、搭建环境
前提条件
Nodejs
前往官方网站下载安装包nvm
Angular开发经常会涉及到不同环境需要的node版本不同,使用nvm管理node版本Npm
包管理工具, 默认随 Node.js 一起安装
安装Angular CLI
java
npm install -g @angular/cli //安装命令
ng v //安装成功则显示版本号
创建及运行项目
arduino
ng new <app-name> //创建工作区和初始应用
cd <app-name>
ng serve --open //运行应用
ng new后,cli将默认为项目生成一个初始页面,并在运行后于浏览器中展示以下页面:
三、项目结构
这是ng new后cli构建项目的时自动生成的文件,其中的大部分是一些配置相关的文件,初学者可以暂时搁置,值得注意的文件如下:
第一层目录
- 【node_modules 】 第三方依赖包存放目录
- 【src 】 应用源代码目录
- 【package.json 】 这是一个标准的npm工具的配置文件,这个文件里面列出了该应用程序所使用的第三方依赖包。下载完成后会放在node_modules这个文件夹下。
- 【package-lock.json 】运行npm install时生成的一份文件。用以记录
当前状态下
实际安装的各个npm 包的具体来源和版本号。
src目录
- 【 app目录】包含应用的组件和模块,我们要写的代码都在这个目录。项目最初被构建的时候,就自动生成了根组件和根模块,像刚刚图片里渲染的网页内容,就是写在根组件里的。
- 【 assets目录 】资源目录,存储静态资源的,比如图片,字体等。
- 【 environments目录 】环境配置。包含为不同构建环境定义的相关的文件,Angular CLI 默认创建了两个环境,分别是开发环境和生产环境。
- 【 index.html 】整个应用的根html,程序启动就是访问这个页面。
- 【 main.ts 】整个项目的入口点,Angular通过这个文件来启动项目。
- 【 polyfills.ts 】与浏览器兼容性相关的文件。
- 【 styles.css 】主要是放一些全局的样式。
- 【 test.ts 】单元测试的入口。
四、架构简介
这个架构图展现了 Angular 应用中的 8 个主要构造块:
- 模块
- 组件
- 模板
- 元数据
- 数据绑定
- 指令
- 服务
- 依赖注入
工作流程
首先左上角是模块
,angular是模块化的,每个模块代表一组功能的集合,在模块当中可以有组件,服务等,当前模块还可能会依赖其他模块。
右下角是组件类
,组件类是编写和界面直接相关的业务逻辑的地方,在组件类中可以编写事件处理函数,可以注入服务类的实例对象等等。
左下角就是服务
,通常用来处理那些跟视图无关的事情,比如从服务器获取数据等等。
接下来我们看图中的中上部分,template,它表示的是组件的模板
,在模板中可以编写html标签描述界面内容,也可以使用指令扩展元素的功能。可以通过数据绑定
,实现模板和组件之间的通信,实现在模板中展示甚至修改组件类中的数据,以及进行一些事件处理。
组件类和模板是通过元数据
关联起来,这里的元数据,就是传递给component装饰器装饰器的对象参数。
最右边是指令
,指令是为模板元素添加额外行为的类,用于扩展DOM 元素或组件的功能。
五、模块
Angular 应用是模块化的,并且 Angular 有自己的模块系统,它被称为 Angular 模块或 NgModul。NgModule 把组件、指令和管道打包成内聚的功能块,每个模块聚焦于一个特性区域、业务领域、工作流或通用工具。
每个 Angular 应用都至少有一个模块,也就是根模块
。通过引导这个根模块就可以启动整个项目。
对于那些只有少量组件的简单应用,根模块就是你所需的一切。随着应用的成长,你要把这个根模块重构成一些特性模块
,它们代表一组密切相关的功能集。然后你再把这些模块导入到根模块中。
根模块文件
python
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
首先import导入相关的依赖包,在 import 语句之后,是一个带有 @NgModule 装饰器
的类。 @NgModule 装饰器表明 AppModule 是一个 NgModule 类。@NgModule 获取一个元数据对象
,它会告诉 Angular 如何编译和启动本应用。
- declarations: 当前模块拥有哪些组件、指令、管道等
- imports:模块依赖项
- providers:模块所能提供的服务
- bootstrap:指定模块加载时,哪些组件需要一起加载
六、组件
组件是 Angular 的核心,其它的一切都是围绕组件设计的:服务为组件提供业务逻辑、模块对组件进行打包。
创建组件
在终端窗口中,导航到要放置你应用的目录。运行如下命令,其中 component-name 是新组件的名字。
xml
ng generate component <component-name>
每个组件生成后,包括以下几个文件:
css样式表
,用来编写组件的私有样式,也可以采用LESS、SCSS等其他的文件格式HTML 模板
,用于声明组件要渲染的内容组件类
,用来编写和组件直接相关的界面逻辑,在其中要关联该组件的组件模板和组件样式。
组件类
kotlin
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent {
title = 'my-app';
}
首先import导入相关的依赖包,在 import 语句之后,是一个带有@component 装饰器的类。 @component 装饰器
表明 AppComponent 是一个组件类。@component 获取一个元数据对象
,通过它将模板与组件类关联起来。
元数据中还传入了一个selector
参数,这是一个简单的 CSS 选择器,当发现页面有相应选择器时,Angular 会把选择器所在位置替换为组件的模板,也就是说相应的标签实际上就代表了这个组件渲染的内容。
元数据下面就是组件类
,组件类里面可以定义相关的属性和方法。
七、生命周期
生命周期函数通俗的讲就是组件或指令创建、更新、销毁的时候会触发的一系列的方法,也就是说只要达到了触发条件,他们就会自动执行。通过它们我们可以在适当的时候对组件或指令实例进行操作。
生命周期函数
ngOnChanges
:当 Angular 设置或重新设置数据绑定的输入属性时响应。时间上位于OnInit之前。初始化和值发生变化时调用。ngOnInit
:用于初始化页面内容,只执行一次。时间上位于OnChanges之后。ngDoCheck
:每次变更检测时执行,并会在发生Angular自己本身不能捕获的变化时作出反应。紧跟在ngOnChanges 和 ngOnInit钩子之后执行。慎用。ngAfterContentInit
:当 Angular 把外部内容投影进组件视图或指令所在的视图之后调用。第一次 ngDoCheck 之后调用,只调用一次。ngAfterContentChecked
:每当 Angular 检查完被投影到组件或指令中的内容之后调用。ngAfterContentInit和每次 ngDoCheck之后调用。ngAfterViewInit
:当 Angular 初始化完组件视图及其子视图或包含该指令的视图之后调用。第一次 ngAfterContentChecked之后调用,只调用一次。afterViewChecked
:每当 Angular 做完组件视图和子视图或包含该指令的视图的变更检测之后调用。ngAfterViewInit和每次 ngAfterContentChecked 之后调用。ngOnDestroy
:每当 Angular 每次销毁指令/组件之前调用。
生命周期的整个流程
首先生命周期函数执行前,会执行构造方法
,主要用于注入依赖关系。 接下来进入组件的生命周期,整体可以分为三个步骤,分别是初始化、变化检测、组件销毁。
step1 组件初始化
:
- onChanges
- 初始化
- 做检查
- 投影内容初始化
- 投影内容变更检测
- 视图初始化
- 视图变更检测
step2 每次变化检测
:
- onChanges
- 做检查
- 投影内容变更检测
- 视图变更检测
step3 组件销毁
:
- onDestroy
八、数据绑定
数据不是一成不变的,可能随着用户操作或者一些功能发生变化。数据绑定就是一种使得视图与组件之间的数据能够保持一致的技术。
通常将数据绑定分为两类:单向数据绑定和双向数据绑定。
从这个图片来看一下,要从组件类中输出数据到模板中的html代码中,可以使用字符串插值,或者属性绑定。另一个方向,如果用户单击模板上的按钮,需要在ts代码中触发一些事情,那么这就用到事件绑定,对于用户的行为作出响应。这两种情况是单向数据绑定
。
还有另一种形式,就是结合了这两个方向,能够同时对事件作出反应并输出一些东西,这就要用到双向绑定
。
插值绑定( model => view )
插值绑定属于单向数据绑定,值从组件类中流入模板,用于在模板中显示一些组件类中的数据。
语法:{{ data }}
用法:
- 拼接字符串
- 调用函数
- 数学计算
- 使用管道
示例如下:
html
<!--插值绑定-->
<h1>插值绑定</h1>
<!--拼接字符串-->
<h3>Welcome to {{ title }}</h3>
<!--调用函数-->
<h3>{{ getTitle() }}</h3>
<!--数学计算-->
<h3>100x80 = {{100x80}}</h3>
<!--使用管道-->
<h3>pipe: {{title | uppercase}}</h3>
属性绑定( model => view )
属性绑定属于单向数据绑定,值从组件的属性流入目标元素的属性,使用属性绑定将组件属性值设置为元素属性。
语法:[property] = "data" 用法:
- 普通属性
- class属性
- style属性
示例如下:
html
<h1>属性绑定</h1>
<!--普通属性-->
<button [disabled]="isDisabled">I am disabled</button>
<!--class属性-->
<button class="btn btn-primary" [class.btnGreen]="classGreen" > class Binding </button>
<button class="btn btn-primary" [class]="{'btnGreen': classGreen, 'btnBlue': classBlue}"> class Binding </button>
<!--style属性-->
<button [style.color]="styleChange ? 'blue': 'red'"> style Binding </button>
<button [style]="{'color': 'red'}">style Binding</button>
事件绑定( view => model )
事件绑定属于单向数据绑定,是从视图到组件的数据改变,通常是由用户直接或间接触发的,比如用户的键盘输入、鼠标点击等动作。
语法:(event)="expression"
示例如下:
html
<h1>事件绑定</h1>
<button type="button" (click)="onClick()">点击按钮触发事件</button>
双向绑定 ( view <=> model )
双向数据绑定意味着数据是双向流动的:既可以从组件流向视图,又可以从视图流向组件。当我们在组件中修改了数据时,修改后的数据会自动显示在视图;当用户在视图修改了数据时,修改后的数据会自动同步到组件。这种绑定方式常见于表单中。
表单语法: [(ngModel)] = "data"
示例如下:
html
<h1>双向绑定</h1>
<h3>{{title}}</h3>
<input [(ngModel)]="title">
九、组件通信
在Angular组件之间共享数据,有以下几种方式:
- 父组件至子组件: 通过 @Input 共享数据
- 子组件至父组件: 通过 @Output EventEmitter 共享数据
- 子组件至父组件: 通过 本地变量共享数据
- 子组件至父组件: 通过 @ViewChild 共享数据
- 不相关组件: 通过 service 共享数据,
除此之外,还有缓存、广播等。
父组件至子组件: 通过 @Input 共享数据
案例:在子组件中直接列出出父组件中定义的所有的英雄。
parent.component.ts:
ts
@Component({
selector:'parent',
template: `
<p>
<child [heroList]="heroes"></child>
</p>
`,
styles: [],
})
export class ParentComponent {
heroes: string[] = ['Dr. Nice', 'Bombasto', 'Celeritas', 'Magneta', 'RubberMan'];
constructor(){}
}
child.component.ts:
ts
@Component({
selector:'child',
template: `
<p>父组件至子组件</p>
<ul>
<li *ngFor="let hero of heroList">{{hero}}</li>
</ul>
`,
styles: [],
})
export class ChildComponent {
@Input() heroList: any;
constructor(){}
}
页面效果如下:
子组件至父组件: 通过 @Output EventEmitter 共享数据
案例:子组件提供两个按钮进行投票,而父组件中需要实时显示投票结果。
parent.component.ts:
ts
@Component({
selector:'parent',
template: `
<div>
agreed: {{agreed}} disagreed: {{disAgreed}}
</div>
<child (vote)="onVote($event)"></child>
`,
styles: [],
})
export class ParentComponent {
@Output() vote = new EventEmitter<boolean>();
agreed: number = 0;
disAgreed: number = 0;
constructor(){}
onVote(agree: boolean) {
agree ? this.agreed++ : this.disAgreed++;
}
}
child.component.ts:
ts
import {Component, EventEmitter, Output} from "@angular/core";
@Component({
selector:'child',
template: `
<p>子组件至父组件</p>
<div>
<button (click)="takeVote(true)">Agree</button>
</div>
<div>
<button (click)="takeVote(false)">Disagree</button>
</div>
`,
styles: [],
})
export class ChildComponent {
@Output() vote = new EventEmitter<boolean>();
takeVote(agree: boolean) {
this.vote.emit(agree);
}
constructor(){}
}
页面效果如下:
子组件至父组件: 通过本地变量共享数据
案例:父组件里提供样式,但调用子组件方法,点击后设置地址由"上海"变为"北京"
parent.component.ts:
ts
@Component({
selector:'parent',
template: `
<child #childComponent></child>
<div>{{childComponent.address}}</div>
<button (click)="childComponent.setAddress('Beijing')">click</button>
`,
styles: [],
})
export class ParentComponent {
constructor(){}
}
child.component.ts:
ts
@Component({
selector:'child',
template: ` `,
styles: [],
})
export class ChildComponent {
address = 'Shanghai';
setAddress(address: string): void {
this.address = address;
}
constructor() { }
}
页面效果如下:
局限性
:父组件-子组件的连接
必须全部在父组件的模板
中进行。如果父组件的类
需要读取子组件的属性值或调用子组件的方法,就不能使用本地变量方法。
子组件至父组件: 通过 @ViewChild 共享数据
父组件同时可以访问子组件中非私有的属性和方法。
parent.component.ts:
ts
@ViewChild(ChildComponent) private childComponent!: ChildComponent;
不相关组件: 通过 service 共享数据
案例:点击side-bar-toggle控制side-bar的显示或隐藏。
side-bar-toggle.component.ts:
ts
@Component({
selector: 'app-side-bar-toggle',
templateUrl: './side-bar-toggle.component.html',
styleUrls: ['./side-bar-toggle.component.css']
})
export class SideBarToggleComponent {
constructor(
private sideBarService: SideBarService
) { }
@HostListener('click')
click() {
this.sideBarService.toggle();
}
}
side-bar.component.ts:
ts
@Component({
selector: 'app-side-bar',
templateUrl: './side-bar.component.html',
styleUrls: ['./side-bar.component.css']
})
export class SideBarComponent {
@HostBinding('class.is-open')
isOpen = false;
constructor(
private sideBarService: SideBarService
) { }
ngOnInit() {
this.sideBarService.change.subscribe(isOpen => {
this.isOpen = isOpen;
});
}
}
side-bar.service.ts:
ts
@Injectable()
export class SideBarService {
isOpen = false;
@Output() change: EventEmitter<boolean> = new EventEmitter();
toggle() {
this.isOpen = !this.isOpen;
this.change.emit(this.isOpen);
}
}
在这个案例中通过toggle的这个组件,实现侧边栏的开关。这里涉及到一个参数isopen
的变化,点击toggle
这个组件,会改变isopen
这个变量的值,而side-bar
这个组件,也需要这个变量改变样式,达到一个侧边栏开关的效果。
具体的做法是,将这个变量和改变变量值的方法都放在service
里面,在toggle组件类的构造方法里注入这个服务,当监测到点击后,会发送事件流,side-bar这个组件类同样注入这个服务,订阅事件,获取到当前的变量值,从而实现了不相关组件的数据共享。
参考文献
🔥 加入我们
DevUI是面向企业中后台产品的开源前端解决方案,其设计价值观基于高效、开放、可信、乐趣四种自然与人文相结合的理念,旨在为设计师、前端开发者提供标准的设计体系,并满足各类落地场景,是一款企业级开箱即用的产品。
- DevUI Design 官网: devui.design/home
- GitHub仓库: github.com/DevCloudFE/...
如果你今天刚刚加入我们,可以先看看官网上的示例组件,你可以在左侧导航栏中切换想要查看的组件,然后通过右侧的快速前往在不同Demo之间切换。
如果你准备添加 Vue DevUI,请前往快速开始文档,只需要几行代码。
如果你对我们的开源项目感兴趣,并希望参与共建,欢迎加入我们的开源社区,关注DevUI微信公众号:DevUI 。
文 / DevUI社区贡献者 张笑