随着与服务器端渲染网站的对立面,Spas变得时尚。与往常一样,有一些利弊,但是水疗中心可以创建平稳的用户体验,如果没有这种方法,这种体验很难复制。这是Web Applications进入传统上由桌面应用程序占用的领土的一部分。 Web应用程序通常因与桌面应用程序相比迟缓而受到批评,但是Web技术的重大进步(尤其是Nodejs和Google的V8引擎以及Angular和ReactJ之类的框架)以及主流访问前所未有的计算能力意味着这意味着这是很多事情,这意味着这是很多事情。比以前更少的问题。
SPAS功能通过在客户端上加载单个HTML页面,然后动态更新内容(和DOM) - 而不是在用户单击任何地方时从服务器中检索更新的HTML页面。
NodeJS
和npm
。TypeScript
。npm install -g @angular/cli
ng new my-project
cd my-project && ng serve
localhost:4200
在您的Web浏览器中。如果您打开my-project/src/app/app.component.ts
您应该会受到以下内容的欢迎:
/** app.component.ts */
import { Component } from '@angular/core' ; // #1
@ Component ( { // #2
selector : 'app-root' , // #3
templateUrl : './app.component.html' , // #4
styleUrls : [ './app.component.css' ] // #5
} )
export class AppComponent {
title = 'app' ; // #6
}
让我们分解一点:
node_modules
文件夹中的@angular
模块导入Component
接口。@Component
Decorator将类标记为角部分。装饰器用一个选项对象进行了参数化,我们将仅利用几个参数。selector
参数定义了该组件的HTML标签是什么 - 例如,使用<app-root> … </app-root>
将其注入HTML。templateUrl
参数采用一个字符串参数,该参数指向HTML文件,该文件用作组件的视图部分。您也可以使用template
参数直接编写HTML,而不是指向文件。通常不建议这样做,除非视图部分仅仅是一两行。styleUrls
参数列出了字符串列表,其中每个字符串都是CSS文件的路径。app.component.html
引用title
。导航到app.component.html
。这是我们应用程序的入口点,也是您应该编辑的最高级别HTML。在这种情况下,一切都将渲染。
现在,我们将删除此文件的内容,并用以下内容替换:
< div class =" container " >
< h1 > Live football scores </ h1 >
< div class =" score-card-list " >
< div class =" score-card " >
< span > Team 1 </ span >
< span > 0 : 0 </ span >
< span > Team 2 </ span >
</ div >
</ div >
</ div >
翻转回您的Web浏览器,您应该看到该页面自动更新。伟大的。
从视觉上看,您可能会在此阶段感到失望,但这是一项正在进行的工作。
Angular材料是由Google团队维护的库,以便为角度应用提供易于使用的材料设计组件。这些组件的样式可以调整为您内心的内容,但它也提供了一种简单的方法,可以用最少的精力来升级原型应用的外观和感觉。
让我们使用npm install --save @angular/material @angular/cdk @angular/animations
。这将所需的文件下载到您的node_modules
文件夹中,并适当地更新packages.json
文件。
您还必须告诉Angular加载相关组件。您必须对其工作方式进行一些谷歌搜索(这是101个胶带的教程),但是基本上,您只需要修改app.modules.ts
文件即可包括所需的那些模块。
首先,添加动画类似:
import { BrowserAnimationsModule } from '@angular/platform-browser/animations' ;
@ NgModule ( {
...
imports : [ BrowserAnimationsModule ] ,
...
} )
然后添加导入我们需要的模块:
import { MatButtonModule , MatCardModule } from '@angular/material' ;
@ NgModule ( {
...
imports : [ MatButtonModule , MatCardModule ] ,
...
} )
请注意,您需要导入在应用程序中使用的任何组件。不包括整个库的原因是帮助webpack
执行树木摇晃,这基本上需要在将所有代码捆绑到几个.js
缩小文件中时遗漏未使用的代码。
完成此操作后,我们将可以访问一堆预定义的组件,您只需根据需要导入它们即可。还值得注意的是,预先建立的主题易于使用。
在角度,组件是模块化的。每个组件都有自己的CSS文件,仅适用于该特定视图。一个例外是styles.css
文件,在src
目录中找到的,该文件适用于全球。
造型不是本教程的重点,因此我们将尽量避免它进行。为了使我们的制作感觉稍微更好,这里有一些简单的CSS复制并粘贴到styles.css
中。
// Outer wrapper div
. container {
text-align : center;
}
// Wrapper for the score cards
. score-card-list {
display : flex;
flex-direction : column;
align-items : center;
}
// Each score card
. score-card {
width : 550 px ;
display : flex !important ;
}
// Home team score
. home {
flex : 1 ;
text-align : right;
}
// Away team score
. away {
flex : 1 ;
text-align : left;
}
// Score section of score card
. score {
width : 100 px ;
}
还要更新您的HTML以包括类:
...
< mat-card class =" score-card " >
< span class =" home " > Team 1 </ span >
< span class =" score " > 0 : 0 </ span >
< span class =" away " > Team 2 </ span >
</ mat-card >
...
如果您对flex
感到困惑,我强烈建议您检查Flexbox Froggy。
使用像Angular这样的框架的优点之一是,我们可以直接在HTML中实现一些编程逻辑。在我们的情况下,陆路将使生活更加轻松。
尝试以下内容:
...
< mat-card class =" score-card " *ngFor =" let team of ['Arsenal', 'Liverpool', 'Tottenham']; let i=index " >
< span class =" home " > {{ team }} </ span >
< span class =" score " > 0 : 0 </ span >
< span class =" away " > Team {{ i+1 }} </ span >
</ mat-card >
...
这引入了一系列新概念,并应产生以下内容:
要注意的概念:
{{ 'Some text = ' + a_number }}
使用服务来将数据访问远离组件。这使组件能够保持精益,并专注于支持视图。通过保持这种责任分离来简化单位测试和代码维护。
要使用CLI生成服务,您可以使用ng generate service football-data
(或ng gs football-data
适用于时间很短)。这将在您的project-dir/src/app
目录中创建两个新文件: football-data.service.ts
和football-data.service.spec.ts
。这是遵循角度约定的,测试文件获得.spec.ts
扩展名。尽管测试很有用,而且很重要,但它属于本教程的直接范围,因此您可以暂时忽略它(对不起,@cornel)。
将以下几行添加到football-data.service.ts
:
import { Injectable } from '@angular/core' ;
import { HttpHeaders } from '@angular/common/http' ;
@ Injectable ( ) // Designate this class as an injectable service
export class FootballDataService {
// Token for accessing data on football-data.org (see: https://www.football-data.org/client/register)
HEADERS = new HttpHeaders ( { 'X-Auth-Token' : 'dc25ff8a05123411sadgvde5bb16lklnmc7' } ) ;
// Convenience constant
COMPETITION_URL = 'http://api.football-data.org/v1/competitions/' ;
// ID for the Premier League
PL_ID = 445 ;
constructor ( ) { }
}
@Injectable
注释服务,该服务告诉编译器,可以将服务的实例注入组件。您还需要为每个服务指定提供商。提供者在指定的级别上有效地提供了单身人士。
例如,您可以将您的服务添加到app.module.ts
中的提供商数组,这将导致整个应用程序中的服务实例。否则,您可以将其添加到组件的提供者列表中,这将导致该组件及其上下文中任何组件可用。
警告:一旦您的应用程序增长,在服务之间创建循环依赖性变得容易。注意这一点。如果(什么时候)您收到一条看起来像类似的错误消息, Cannot resolve all parameters for MyDataService(?)
,则可能与循环依赖关系问题有关。
我们将利用Football-data.org免费提供的奇妙API。您需要获取自己的API键,但很容易做到 - 只需按照网站上的说明进行操作即可。还有更多深入的文档可用,但是在此处列出的小示例中可以看到99%的文档。
在此示例中,我们真正想做的就是在当前回合(又称“游戏周”或“比赛日”)中检索所有游戏信息。 /v1/competitions/{id}/fixtures
端点返回此信息,但对于当前季节的所有回合。为了获得单轮的信息,我们需要设置matchday
参数,例如/v1/competitions/{id}/fixtures?matchday=14
。
为了获得当前的比赛当天,我们可以要求联赛桌,因为默认情况下它会返回当前比赛。
首先,我们需要将HttpClient
服务注入我们的FootballDataService
以利用Angular的HTTP功能:
import { HttpClient , HttpHeaders } from '@angular/common/http' ;
...
constructor ( private http : HttpClient ) { }
. . .
重要的是:将一个私有变量添加到Angular服务或组件的构造函数以及特定的打字稿类型声明中,足以使Angular的黑魔法工作。编译器现在将适当的实例注入此服务,因此您可以访问它。
让我们添加一个函数以从服务器检索联赛表(和当前比赛日):
...
getTable ( ) {
return this . http . get ( this . COMPETITION_URL + this . PL_ID + '/leagueTable' ,
{ headers : this . HEADERS } ) ;
}
...
TypeScript将为您提供帮助,但是Angular的http.get
方法如下:
/**
* Construct a GET request which interprets the body as JSON and returns it.
*
* @return an `Observable` of the body as an `Object`.
*/
get ( url : string , options ?: {
headers ?: HttpHeaders | {
[ header : string ] : string | string [ ] ;
} ;
observe?: 'body' ;
params?: HttpParams | {
[ param : string ] : string | string [ ] ;
} ;
reportProgress?: boolean ;
responseType?: 'json' ;
withCredentials?: boolean ;
} ) : Observable < Object > ;
问号表示参数headers
, observe
, params
, reportProgress
, responseType
和withCredentials
都是可选options
的一部分。我们只会传递url
和options.headers
的值。
您可能想知道我们刚刚创建的返回的getTable()
函数。好吧,它返回Observable
流。正如卢克·格鲁伊斯(Luuk Gruijs)所说, Observable
S本质上是“随着时间的流逝,多个价值的懒惰收藏”。这听起来很疯狂,但实际上很简单。
简而言之, Observable
流被视为“冷”,直到被订阅为止 - IE仅一旦使用输出,该变量才会懒惰。一旦流为“热”,每当其值更改时,它将更新所有订户。它是在JavaScript中使用Promise
处理异步情况的替代方法。
在这种情况下,仅一旦Observable
变量被订阅后才GET
,因为其余接口只能返回每个呼叫的单个值。
我们可以利用typecript中静态键入的力量来使其更加明确:
...
import { Observable } from 'rxjs/Observable' ;
...
getTable ( ) : Observable < any > {
return this . http . get ( this . COMPETITION_URL + this . PL_ID + '/leagueTable' ,
{ headers : this . HEADERS } ) ;
}
...
实际上,由于足球data.org文档告诉我们确切的剩余电话期望,我们也可以进一步迈出一步,并在src/app/models/leagueTable.ts
中对对象进行建模:
import { Team } from './team' ;
export class LeagueTable {
leagueCaption : string ;
matchday : number ;
standing : Team [ ] ;
}
在src/app/models/team.ts
中:
export class Team {
teamName : string ;
crestURI : string ;
position : number ;
points : number ;
playedGames : number ;
home : {
goals : number ;
goalsAgainst : number ;
wins : number ;
draws : number ;
losses : number ;
} ;
away : {
goals : number ;
goalsAgainst : number ;
wins : number ;
draws : number ;
losses : number ;
} ;
draws : number ;
goalDifference : number ;
goals : number ;
goalsAgainst : number ;
losses : number ;
wins : number ;
}
这使我们能够将football-data.service.ts
更新为:
import 'rxjs/add/operator/map' ;
import { LeagueTable } from './models/leagueTable' ;
...
getTable ( ) : Observable < LeagueTable > {
return this . http . get ( this . COMPETITION_URL + this . PL_ID + '/leagueTable' ,
{ headers : this . HEADERS } )
. map ( res => res as LeagueTable ) ;
}
...
这将通过最大程度地减少我们在使用复杂物体时需要保持最新状态的心理模型来帮助我们保持理智,因为IDE可以指导我们。
旁注: as
关键字简单地告诉TypeScript以信任我们对象的类型,而不是试图通过某种检查来弄清楚它。危险但有用,就像大多数有趣的事情一样。
好的,请返回src/app/app.component.ts
,并添加以下行,以便将FootballDataService
注入组件:
import { FootballDataService } from './football-data.service' ;
...
export class AppComponent {
title = 'app' ;
constructor ( private footballData : FootballDataService ) { }
}
现在,我们还将在组件中添加一个ngOnInit
方法。这是Angular提供的标准生命周期挂钩,在初始化了组件的所有数据结合属性之后。它基本上是在对象的初始化时发射的,但比constructor
方法稍晚,该方法在已注册到所有输入和输出之前发射的构造方法。
一个常见的经验法则是,只始终将您要在此特殊的ngOnInit
方法而不是构造函数中调用初始化的代码。将其添加到类似的组件中:
import { Component , OnInit } from '@angular/core' ;
...
export class AppComponent implements OnInit {
...
constructor ( private footballData : FootballDataService ) { }
ngOnInit ( ) {
// Code you want to invoke on initialisation goes here
}
...
在我们的情况下,我们想加载联赛表,因此我们可以添加类似的东西:
...
ngOnInit ( ) {
// Load league table from REST service
this . footballData . getTable ( )
. subscribe (
data => console . log ( data ) ,
error => console . log ( error )
) ;
}
...
如果您在网络浏览器中打开控制台,则应该看到类似的内容:
伟大的。现在,我们有很多很酷的数据,我们将来可以做一些事情(尤其是与俱乐部波峰图像的直接链接),但是目前,我们实际上只是对当前的比赛日感兴趣。
现在,让我们向我们的数据服务添加另一个功能,以获取当前一轮固定装置的信息:
...
import { GameWeek } from './models/gameWeek' ;
...
getFixtures ( matchDay : number ) : Observable < GameWeek > {
return this . http . get ( this . COMPETITION_URL + this . PL_ID + '/fixtures?matchday=' + matchDay , { headers : this . HEADERS } )
. map ( res => res as GameWeek ) ;
}
...
GameWeek
和Fixture
定义为如下:
// src/app/models/gameWeek.ts
import { Fixture } from './fixture'
export class GameWeek {
count : number ;
fixtures : Fixture [ ] ;
}
// src/app/models/fixture.ts
export class Fixture {
awayTeamName : string ;
date : string ;
homeTeamName : string ;
matchday : number ;
result : {
goalsAwayTeam : number ;
goalsHomeTeam : number ;
halfTime : {
goalsAwayTeam : number ;
goalsHomeTeam : number ;
}
} ;
status : 'SCHEDULED' | 'TIMED' | 'POSTPONED' | 'IN_PLAY' | 'CANCELED' | 'FINISHED' ;
_links : {
awayTeam : { href : string ; } ;
competition : { href : string ; } ;
homeTeam : { href : string ; } ;
self : { href : string ; } ;
} ;
}
有了我们新获得的比赛日知识,我们可以向REST服务器询问有关当前固定装置的信息。但是,我们需要等待第一个休息呼叫才能完成第二个呼叫,然后再进行第二个呼叫。一些重构意味着我们可以很容易地在回调中这样做:
import { Component , OnInit } from '@angular/core' ;
import { FootballDataService } from './football-data.service' ;
import { LeagueTable } from './models/leagueTable' ;
@ Component ( {
selector : 'app-root' ,
templateUrl : './app.component.html' ,
styleUrls : [ './app.component.css' ] ,
providers : [ FootballDataService ]
} )
export class AppComponent implements OnInit {
title = 'app' ;
table : LeagueTable ;
gameweek : GameWeek ;
constructor ( private footballData : FootballDataService ) { }
ngOnInit ( ) {
this . getTable ( ) ;
}
getTable ( ) {
this . footballData . getTable ( )
. subscribe (
data => {
this . table = data ; // Note that we store the data locally
this . getFixtures ( data . matchday ) ; // Call this function only after receiving data from the server
} ,
error => console . log ( error )
) ;
}
getFixtures ( matchDay : number ) {
this . footballData . getFixtures ( matchDay )
. subscribe (
data => this . gameweek = data , // Again, save locally
error => console . log ( error )
) ;
}
}
现在我们到达某个地方!
由于我们将数据设置为我们的打字稿组件上的成员变量,因此可以由关联的HTML直接访问它。实际上,如果您使用的是Microsoft的开源编辑器Visual Studio Code,则可以添加Angular语言服务插件以在HTML中获得JavaScript代码完成!惊人的。它由角团队维护。
让我们从以前替换虚拟数据:
< div class =" container " >
< h1 > Live football scores </ h1 >
< div class =" score-card-list " >
< mat-card class =" score-card " *ngFor =" let fixture of gameweek.fixtures " >
< span class =" home " > {{ fixture.homeTeamName }} </ span >
< span class =" score " > {{ fixture.result.goalsHomeTeam }} : {{ fixture.result.goalsAwayTeam }} </ span >
< span class =" away " > {{ fixture.awayTeamName }} </ span >
</ mat-card >
</ div >
</ div >
注意gameweek?.fixtures
语法: ?
符号是if (gameweek != null) { return gameweek.fixtures } else { return null }
的短途手段,并且在访问只能通过异步rest呼叫填充的变量时,它是令人难以置信的。
ETO,瞧!
下一部分并不是严格必要的,但它说明了一种重要的角度做事方式,如果我们决定将其推向前进,一定会帮助保持我们的代码模块化和可容纳(请查看NativeScript作为创建的一种方式带有角和打字稿的本机移动应用程序)。
我们将将夹具得分卡分为自己的组件。从CLI: ng generate component score-card
(或ng gc score-card
)的帮助开始。这将在src/app/score-card
中创建一个.ts
, .html
和.css
文件。
打开score-card.component.ts
由熟悉的装饰员致意:
...
@ Component ( {
selector : 'app-score-card' , // Note this!
templateUrl : './score-card.component.html' ,
styleUrls : [ './score-card.component.css' ]
} )
...
注意selector
字段 - 它告诉我们如何访问组件(在这种情况下,使用<app-score-card></app-score-card>
标签)。
通过将肉从app.component.html
移至score-card.component.html
:重新分配我们的代码:
<!-- app.component.html -->
< div class =" container " >
< h1 > Live football scores </ h1 >
< div class =" score-card-list " >
< app-score-card *ngFor =" let fixture of gameweek?.fixtures " [fixture] =" fixture " > </ app-score-card >
</ div >
</ div >
<!-- score-card.component.html -->
< mat-card class =" score-card " >
< span class =" home " > {{ fixture.homeTeamName }} </ span >
< span class =" score " >
{{ fixture.result.goalsHomeTeam }} : {{ fixture.result.goalsAwayTeam }}
</ span >
< span class =" away " > {{ fixture.awayTeamName }} </ span >
</ mat-card >
注意<app-score-card>
标签中的[fixture]="fixture"
位。这就是我们在组件之间传递信息的方式。
在角度语法中, [...]
表示输入, (…)
表示输出, [(…)]
表示双向绑定。 [(…)]
也称为“香蕉盒语法”,您会以[(ngModel)]="someVariable"
的形式经常遇到它。这意味着变量值和DOM对象的值之间的双向绑定。这是使用角的关键部分。
例如,我们可以将input
标签的值直接映射到屏幕上显示的变量,并且每当input
元素的值更改时,DOM将自动更新:
< p >
What is your name?
< input type =" text " [(ngModel)] =" name " />
</ p >
< p >
Your name: {{ name }}
</ p >
您可以在此处查看一个示例。
回到足球:为了将输入值接收到组件中,我们还需要更新score-card.component.ts
如下所示:
import { Component , Input } from '@angular/core' ;
import { Fixture } from '../models/fixture' ;
@ Component ( {
selector : 'app-score-card' ,
templateUrl : './score-card.component.html' ,
styleUrls : [ './score-card.component.css' ]
} )
export class ScoreCardComponent {
@ Input ( ) fixture : Fixture ; // Note the decorator
constructor ( ) { }
}
有两种明显的方法可以在组件之间传递数据:使用@Input
/ @Output
和使用服务。
@Input()
第一种方法对于传递父母和子女之间的数据很有用,但是如果需要通过嵌套层传播数据,可能会很乏味。数据可以使用@Input
装饰器传递给子组件。如果是对象,则该输入变量将通过引用传递,或者如果是原始的对象,则将按值传递。
// someComponent.ts
// ...
import { Input } from '@angular/core'
// ...
export class SomeComponent {
// @Input() variables get set after the `constructor(...)`
// method, but before `ngOnInit()` fires
@ Input ( ) aNumber : number ; // This gets set via parent HTML
// ...
}
<!-- someComponentParent.html -->
< h1 >
I am a parent where SomeComponent gets rendered
</ h1 >
<!-- Pass a value to the aNumber variable -->
< some-component [aNumber] =" 48 " > </ some-component >
@Output()
还可以使用@Output()
装饰器将数据从孩子发射到父组件。这不是定向绑定,而是事件发射器在预定义的时间上发射。典型的用例将通知父何时在孩子中更改值时。
// someComponent.ts
// ...
import { Input , Output , EventEmitter } from '@angular/core'
// ...
export class SomeComponent {
@ Input ( ) aNumber : number ;
// Emits an event (of type `number`) to the parent
@ Output ( ) numberChanged : EventEmitter < number > = new EventEmitter < number > ( ) ;
// ...
// Event emitters need to be triggered manually
// Any object can be emitted
emitValueChanged ( ) : void {
this . numberChanged . emit ( this . aNumber ) ;
}
}
<!-- someComponentParent.html -->
< h1 >
I am a parent where SomeComponent gets rendered
</ h1 >
<!-- Pass a value to the aNumber variable -->
< some-component [aNumber] =" 48 " (valueChanged) =" aNumberChanged($event) " > </ some-component >
// someComponentParent.ts
export class SomeParentComponent {
// ...
aNumberChanged ( updatedNumber : number ) : void {
console . log ( `aNumber changed to ${ updatedNumber } ` ) ;
}
// ...
}
使用服务
使用服务在组件之间传递数据有很多方法。细节超出了本教程的范围,但是值得注意的是,此类技术的存在,因为在任何相对复杂的应用程序中都可能需要。
一般模式是在服务中定义Observable
流,其中组件可以推动消息或订阅以通知新消息。这实际上是事件总线。
在一般情况下,需要键入消息,以使听众能够辨别适用的内容。作为一个轻率的例子,该服务可以散发出PersonNameChangedEvent
,而PersonComponent
可以做出反应,而LandingPageComponent
可能会选择完全忽略此事件。
Angular在其中提供了一个单片且高度自明的框架,以构建应用程序。这提供了结构的优势 - 框架的自以为是的性质可帮助用户从一开始就设计可扩展的应用程序,并且许多复杂性都隐藏在开发人员身上。另一方面,使用Angular(和TypeScript)引入了许多样板代码,如果您构建一个小型应用程序,可能会减慢您的速度,因此值得考虑该应用程序在启动到Angular之前的位置。
但是,Angular CLI已经走了很长一段路,并且鉴于它的繁重繁重,我可能会在不久的将来对几乎每个项目使用Angular。