Wednesday, February 13, 2019

Angular with TypeScript

現在 Angular 有自己的 CLI 工具了,可以處理建立專案、產生元件、測試、檢驗等等瑣事。

ng new [PROJECT_FOLDER_NAME]
建立新專案。
ng serve
啟動測試伺服器。
ng help [COMMAND]
顯示說明。

產生的專案預設會有所有東西,包含簡單的測試,還會建立 Git repo 並自動做 import commit 出來,有選項可以關掉。

建新專案時可以順帶給定一些參數,調整後面產生的 component 等程式的預設組態,比如下面是幾個常用的:

ng new [PROJECT_NAME] --prefix=[SELECTOR_PREFIX (預設值: app)] --style=[STYLE_EXT (預設值: css)] --routing

打定主意不用內建的測試框架 (或是根本不打算測試的話 wwwwww) 的話,還可以加上 --skip-tests 來略過產生測試的程式碼。

在自動產生的程式碼中,進入點是 src/main.ts,在這邊會載入 src/app/app.module.ts 裡的定義,然後接著就會啟動 src/app/app.component.ts 裡的 AppComponent 作為 root component 顯示。

更新框架套件只要 ng update [LIBRARY_NAME] 即可,如果加上 --all 則會將因為相依性帶進來的開發套件一起更新。

Getting started: Project file review 中,有說明專案目錄下的檔案用途。

產生 Component 使用 ng generate component [COMPONENT_NAME] 指令,會在 src/app/ 下面產生指定名稱的資料夾與相關程式檔案,並更新 src/app/app.module.ts 檔案。

使用 ng generate 產生 service 或其他 module 時,要額外使用 --app= 選項,否則不會更新 src/app/app.module.ts 檔案,要手動將新產生的 Module 增加進去。

看起來目前還沒有針對 component library 的 generate 指令,如果要做 component library 可能要參考 Angular Material 的作法。

使用 ngModel 前,必須先在 app.module.ts 中 由 NgModel 修飾過的 AppModule 類別引入 FormsModule 才可以使用。事實上所有元件都必須在 NgModel 中引入,才可以使用。

在 Angular 中,使用了 RxJSObservable 實作來處理非同步的操作。

通常會用上 Observable 的多會是遠端的資源,因此還會有一個屬性儲存當前可用的實體,在 Angular 的慣例中 (convention) 會把存放 Observable 屬性名稱的尾端加上一個錢號 ($) 以作為區別儲存實際值的屬性。

Routing 的部份,慣例上要增加一個特殊的 AppRoutingModule 來處理在 view 之間切換的事情。使用下面的指令新增:

ng generate module app-routing --flat --module=app

產生在 view 之間切換的連結時,要使用 RouterLink directive 來讓 <a> 的 href 屬性可以根據給 RouterModule 的參數來產生適當的連結。

要使用 HTTP End-point 的話,已經包裝好了 HttpClient 模組可以使用。跟其他模組一樣,要在 AppModule 中匯入後就可以使用。

架構上,最基礎的 Angular 建構元素是 NgModule,用來表達 Component 等 Angular 應用程式組件。每個 Angular 應用程式至少會有一個 root module 來 bootstrap 整個應用程式,引入其他的 feature module 來完成應用程式的功能。

Component 會決定 view 的定義,提供給 Angular 框架來依照所定義的邏輯與當下取得的資料狀況來選用與調整。在每個 Angular 應用程式中,會有至少一個 root component 來作為最基底的組件。

Component 中使用 Service 所提供的與 view 無直接關係的相關功能,提供服務的組件作為 dependencyinject 到 Component 中。

Component 與 Service 都是一般的 class,但是透過 decorator 把相關的模組類型資訊等各項後設資訊加到 class 上。

  • 對於 Component 來說,所附加的主要是 template 資訊,也就是內含 directive 與 binding markup 的 HTML 內容。

    透過對類別加上 @Component() 裝飾子來定義出 Component 模組,關聯樣板的相關屬性是 selector, templateUrl, styleUrls 等,與 AngularDart 相同。

  • 對於 Service 來說,所附加的主要是讓 Angular 進行 Dependency Injection 所需要的資訊。

    將類別標示為 Service 所使用的是 @Injectable() 裝飾子,沒有特別的參數。

一般來說,應用程式的 root module 會稱為 AppModule

@NgModule() 中主要的參數:

declarations

在這個 NgModule 下開發的 Components, Directives, Pipes 要加到這個陣列裡面,在 NgModule FAQs: What classes should I add to the declarations array? 裡頭有大略講到用法。

exports

要讓其他 NgModule 使用的組件,也是陣列型式。

imports

要導入這個 NgModule 中使用的其他 NgModule,比如在樣板要使用 NgModel 的話就會需要 FormModule 模組。

providers

要使用到的服務,在這邊導入會變成全域的。也可以在 Component 層級進行導入來限制 scope 的範圍,可以參考 Hierarchical Dependency Injectors 的說明。

bootstrap

指定 root component,只有 root module 需要定義這個屬性。

可以在 Component 中利用 Life-cycle hook 定義 Angular 在建立、更新、摧毀等各階段要執行的邏輯,比如常見的 ngOnInit() 即為一例。

@Component() 中,常用的參數有:

selector

用來獲取這個元件在 DOM 中所在位置的 CSS selector 規則,當依規則找到節點後,會將元件安插在該節點上。

templateUrl

樣板的相對位址,樣板基本上就是 HTML 扣除 <script /> 等敏感標籤後加上樣板語法樣板語法主要是加在標籤的屬性部份。

providers

這個元件所需要使用的 service 類別所構成的陣列,將會由 Angular 透過 Dependency Injection 往建構子注入。

如果要在程式裡頭定義 template 的話,可以使用 ECMAScript 2015 backticks (`) 語法,功能與 Python 的 """ ... """ 語法大致相同。

所產生的 view 會由 Angular 自動的依據樣板定義的邏輯更新,更新的時機稱為 change detection cycle,多為與 view 相關的同步作業完成時,比如鍵盤事件、計時器完成、HTTP 傳輸完成等。

輸出值到 view 上會透過 template expression 來達成,在 template expression 中只可以參考到 Component 上的元素以及部份不會產生 side effect 的 JavaScript 運算子,不可以使用全域空間像是 window.location 或是 Math.max() 等元素。

可以在 template 中參考同樣板中的其他物件,首先用井號 (#) 加上 template reference variable 標記 (如: <input #heroInput>) 接著就可以在 Interpolation 中使用 (如: {{heroInput.value}})。

要特別注意,因為 template reference variable 對實體的繫結只會在初始化樣板時發生一次,而且 scope 是整個樣板,是不可以重複的,也就是不能在 *ngFor 中使用。如果要搭配 *ngFor 使用的話,可能要考慮做一個獨立的元件吧!

要對事件反應的話,是透過 template statement 搭配 event binding 來達成。在 template statement 中,大多是直接呼叫 Component 上的方法,或是簡單的 assignment 敘述。在定義 event binding 時,會多一個 $event 變數可以參考,代表 DOM event 物件。

HTML attribute 與 DOM property 不同,可以將 HTML attribute 視為初始化 DOM property 的設定,一旦 DOM 初始化完成後,就是透過 DOM property 來改變顯示或是互動的反應。在 template 中,操作的對象是 DOM property 的值。要注意,HTML attribute 與 DOM property 並不一定是 1-1 對應,此外,即使名稱相同,反應可能會不同。

Binding syntax: An overview 中,可以看到各種繫結語法的作用對象與範例。

Angular 區分 Component 與 Service 主要是為了:

  • 讓 Component 專注於朔造使用者體驗上,理想上 Component 只要提供 Data-binding 必要的屬性與方法,作為 View 與 Model (Application Logic) 中間的介接角色。
  • 不應在 Component 中處理有關資料擷取、輸入驗證、紀錄等邏輯,而應將這些邏輯抽到 Service 中,如此可以在不同的場合使用相同的 Service 以提升應用程式在不同需要下的可適應性。
Injector

整個 Angular 的 DI 機制的主要機制實作,應用程式不需要自己建立 Injector 實體,框架會在 Bootstrap 階段進行準備。

Container

Injector 會為每個所建立的 dependency 實體維護一個 Container 來儲存相關的資料,並在可能的時候儘量的再利用所建立的 dependency 實體。

Provider

Provider 是指要給 Injector 用來建立 dependency instance 的邏輯,對 Service 來說會是定義 Service 的類別。

如果在 root module 加 provider 的話,那麼會建立一個給全域使用的 instance。

如果在 Component 中加 provider 的話,每個 component instance 都會有一份獨立的 instance 給該 component instance 以及其下的 component instance 使用。

在 Angular 中有預先建置了一些表單的機制,像是雙向資料繫節 (two-way data binding), 變更追蹤 (change tracking), 表單資料驗證 (validation), 錯誤處理 (error handling) 等框架。

極端來看,最基本的取資料的方式就是用事件繫結 (event binding) 來取得使用者的輸入,比如 <input (keyup)="handle_keyup($event)"> 這樣,然後再實作處理鍵盤輸入事件的方法來從事件物件中取出使用者的動作。這麼做的缺點就是使用者看到的部份 (HTML) 跟程式的邏輯 (TypeScript) 綁的太緊密,當視覺部份有調整時,邏輯就必須跟著調整。

一個稍微好一點的作法是利用 template reference variable 來取得輸入值: <input #box (keyup)="handle_input(#box.value)">,這樣在邏輯端就不用非常知道前端事件來源的細節。

<form> 上加上 template reference variable 繫結的 ngForm directive 並在各 input control 加上名為 name 的 property 的話,就可以利用這個 reference variable 在樣板上或是利用 @View() 去存取 form 內的各個 input control 物件。

在 input control 使用 ngModel directive 除了可以用在雙向資料繫結外,也可以讓 Angular 自動基於輸入的狀態加上 class name: Track control state and validity with ngModel

要跨越 HTML element 存取 Angular 附加到 input control 的資訊的話,可以利用將 template reference variable 繫結到 ngModel 來達成:

<input [(ngModel)]="model.name" name="name" #name="ngModel">
<div [hidden]="name.valid || name.pristine">...

利用 @ViewChild 可以在 TypeScript 中存取到 HTML 裡面定義的元件參考,原生元件要用 ElementRef 型別。

另外就是 ViewChild 獲取元件參考的時機,利用 static 參數來控制,原則上就是如果要在 OnInit 取用的話,就會需要設為 true,但是該元件就不能使用 *ngIf, *ngFor 之類的樣板指令,詳情參考 Static Query Migration Guide

No comments: