隨著與服務器端渲染網站的對立面,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。