Wednesday, December 11, 2019

SCSS

SCSS (Sassy CSS) 是加上了 Sass 語法強化過後的 CSS 樣式表,如果只有 Sass 的話,主要的差異是使用縮排區分定義區塊 (CSS 使用大括號 { }),並且使用換行作為樣式分段 (CSS 使用分號 ;)。

可以在所有地方使用的語法:

變數宣告

$var: value 的型式,值的部份可以是數值,也可以是算式。

要注意,在 CSS 中也有變數 (variables; 或稱 custom properties),為 -- 開頭。

流程控制
  • @if
  • @each
  • @for
  • @while
編譯時期指引

產生錯誤或是偵錯訊息的編譯時期指引: @error (編譯會終止), @warn, @debug

只可以用在最上層的樣式定義內的敘述: @use, @import, @mixin, @function

模組 (@use)的語法目前只有 DartSass 支援,利用這個語法可以處理名稱空間汙染的問題。

舊有的 @import 語法,因為有名稱空間汙染的問題,看起來是計劃慢慢汰除掉。

Parent Selector (&) 用來巢狀的樣式定義中,在內層定義參考上一層的樣式符號名稱,。

Placeholder Selector (%...) 是給樣式定義放置一個暫時的名字,編譯後會把有 @extend 的 Selector 代換進去。

在樣式定義中,可以利用 nest block 集中定義前綴一樣的樣式屬性 (properties):

.info-page {
  margin: auto {
    bottom: 10px;
    top: 2px;
  }
}

編譯後會變成:

.info-page {
  margin: auto;
  margin-bottom: 10px;
  margin-top: 2px;
}

@function 用來定義產生樣式屬性值 (property value) 的邏輯,@mixin...@include 則是用來產生樣式定義。

Interpolation (#{ }) 可以用來將值嵌入 Selector 規則裡,或是 Property 的 Key 或 Value 中。當嵌入 Property Value 時,通常是用來產生一個字串,或是連結的值。

使用 @mixin 或是 placeholder (%) 的時機要看語意,一般來說 Placeholder 會比較著重於 Selector 間的繼承關係,比如 %error-base.error-low, .error-high 的關係;而 @mixin 就比較是單純的功能性的,像是 @mixin container-5-cols 之類的。

這部份在官方文件的 Extends or Mixins? 有些說明:

Extends and mixins are both ways of encapsulating and re-using styles in Sass, which naturally raises the question of when to use which one. Mixins are obviously necessary when you need to configure the styles using arguments, but what if they’re just a chunk of styles?

As a rule of thumb, extends are the best option when you’re expressing a relationship between semantic classes (or other semantic selectors). Because an element with class .error--serious is an error, it makes sense for it to extend .error. But for non-semantic collections of styles, writing a mixin can avoid cascade headaches and make it easier to configure down the line.

Saturday, May 25, 2019

CSRF / XSRF

CSRF (Cross-Site Request Forgery; 也會簡寫為 XSRF) 攻擊的防禦要另外在伺服器上做,稍微筆記一下原理。

這個攻擊的名稱除了常見的 CSRF / XSRF 之外,也可能被稱為: Sea Surf, Session Riding, Hostile Linking, One-click Attack 等。

簡介

這個攻擊主要是欺騙使用者去觸發「預期之外」的操作 (unwanted actions)。

攻擊目標在於「狀態的改變」,因為伺服器的回復攻擊者不太容易取得,因此偷竊資訊不是主要目標。

可能的攻擊目標類似: 盜買或盜賣、轉帳、更改帳號中的備援電子郵件位址、新增或變更紀錄等。

攻擊時可能會混用一些社交工程的手法,欺騙使用者開啟特製的網頁。

原理

由於瀏覽器在對網站發送 request 時,會附帶上代表使用者身份的資訊,如: session cookies, IP address 等,因此攻擊者有機會製作一個連結或是按鈕,讓使用者誤擊,達到以該使用者身份觸發特定網站上的操作。

一般來說攻擊者不太容易取得操作後的結果,因此主要會是針對「狀態的變更」作為攻擊目標,而非竊取資訊。

搭配 XSS 的話,攻擊強度會倍增,使用者更容易點擊到含有攻擊標籤或程式的頁面,也更容易繞過安全機制。這樣的安全缺失稱為 "Stored CSRF Flaws"。

無效的防禦方式

使用祕密的 cookie

基本上所有 cookies 都會傳,所以這個方式沒什麼功能。

主流的瀏覽器有個 SameSiteSet-Cookie 標記,可以指引 cookies 只在來源是該網站時帶入 request 上,但不應該只依賴這個機制。

只接受 POST request

有很多方法可以騙使用者觸發 POST request 查詢。

多階段的交易

攻擊者還是有可能完成攻擊。

URL 改寫

這個防禦方式指的是將部份認証用資訊編在 URL 裡面,這樣攻擊者不容易預期操作的 URL 位址,來達到防禦的目的。

利用這個方式可以一定程度迴避 CSRF 攻擊,但將認証資訊嵌在網址中不是好的安全實作方法,並不建議完全依賴這個策略。

HTTPS

完全沒差。

防禦注意事項

需完全防禦 XSS

XSS 將會使得絕大多數的 CSRF 防禦失效,因為攻擊者將可以使用 XMLHttpRequest 來取得安全機制中的 token 值。

要保護的資源

一般來說,如果遵照 RFC2616 9.1.1 的建議: 「GET 與 HEAD 為 safe-method,不應該造成 side-effect。」,那麼主要要保護的資源會是 <form> 標籤或 XHR/AJAX 使用的端點。

主要防禦手段

Token-based 的防禦

在 request 中嵌入覆核用的 token 值,可以嵌在:

  • <form> 的隱藏欄位中 (form POST),或
  • HTTP 的標頭中 (XHR),或
  • URL 中 (GET; 非常不建議)。

不是很建議將 token 嵌在 URL 中,因為很可能會透過瀏覽器的歷史紀錄或是 Refer 而洩漏,反而提供攻擊者機會。

使用 Token-based 防禦有以下注意事項:

  • 使用夠強的 Encryption 或 HMAC 函數,建議使用 AES256-GCM (encryption) 或 SHA256/512 (HMAC) 作為加密與簽章函數。
  • 需妥善實作 Key rotation 與 Token life-time 的維護。

常見的實作方式:

Synchronizer Token Pattern

將 token 存在 session 中,每次操作後就產生新的 token 值。

這個技巧原本是用來避免重複送出表單,或是防禦重送攻擊。配合 <form> 實作時要注意如果使用者按下上一頁,嵌在 <form> 的 token 可能已經無效,需做相關處理。

Encryption-based Token Pattern

將 User ID、時戳、一次性識別碼 (nonce)等資訊使用金鑰加密,作為 token 值。

伺服端在處理後續的操作時在解碼並驗證時戳是否仍有效,透過時戳與 nonce 可以一定程度防禦重送攻擊。

HMAC Based Token Pattern

這個方式類似 Encryption-based token pattern 的作法,但使用 HMAC 取代加密。

投入雜湊函式的值除了 Encryption-based token pattern 中的值之外,建議增加一個每次都不一樣的值,比如說 operation-type 之類的,以避免重送攻擊。相同的目的在 Encryption-based token pattern 中,使用 nonce 來達到增加隨機性的目的。

登入頁面的防護建議特別處理,避免使用者默默地被登入為攻擊者的帳號,以達到竊取使用者線上行為的目的。

Build Thrift compiler (without libraries)

最近在 Linux 與 OS X 上編了 Thrift 0.12.0 要用,跟之前 0.10.0 狀況好像不太一樣了。

Built Thrift 0.12.0 on Linux and OS X recently. The steps to get successful build is different from 0.10.0.

我只需要 Thrift 的 IDL 編譯器,不需要語言的函式庫,用下面的方法可以建置出來:

All I need is Thrift compiler. I excluded libraries for languages to speed-up the build process. Here is the commands to complete the build:

  1. 將會需要 C++ 編譯器,系統上要有基本的建置環境。另外比較特殊的是 Flex 與 Bison (>= 2.5) 這兩個工具,也需要事先安裝好。

    Essential build environment is required. Flex and Bison (>= 2.5) are also required.

    Ref: Apache Thrift Requirements

  2. 會需要 Boost 的標頭檔,因此要先建置出 Boost 標頭檔:

    We will need Boost. Headers are good enough for completing build. Generate Boost headers first:

    $ ./bootstrap.sh --prefix=/tmp/p

    $ ./b2 --with-headers

  3. 開始正式建置 Thrift 編譯器:

    Configure and build the Thrift compiler:

    $ ./configure CXXFLAGS=-I/private/tmp/b/boost_1_70_0 --enable-libs=no --enable-tests=no --enable-tutorial=no --with-boost=/tmp/p --prefix=/target/thrift-0.12.0

    $ make

    $ make install

    比較特殊的是要使用 CXXFLAGS= 來讓建置過程可以找到 Boost 標頭檔,說明文件中的 --with-boost= 似乎是完全沒作用。

    The most tricky part is instructing build process to reach Boost header with CXXFLAGS= flag. The --with-boost= flag in help message does not work for me.

Wednesday, February 13, 2019

AngularDart

Dart 針對網頁應用的網站在 https://webdev.dartlang.org/ 上,目前主推是基於 Angular Dart 框架的開發。

也是有其他框架或是更基礎的作法,不過指引的網頁放在很邊緣的地方,簡單來說就是: Angular Dart, Polymer Dart, Low-level HTML 這些選項。

Get Started 大概就是介紹一些工具:

  • DartPad 可以直接在瀏覽器中試驗 Dart 程式碼。
  • 下載與安裝 Dart SDK 到本機端。
  • 下載與安裝 IDE (整合開發環境) 到本機端,官網是推 JetBrains 的 WebStorm, 不過我用 CE 版的 IntelliJ IDEA 加上 Dart Plug-In 也算是還過的去。
  • 介紹怎麼用 IDE 開測試網頁,背後其實就是 pub serve 這樣。自己直接下指令好像會比較順,有碰到幾次透過 IDE 啟動但跑得不太順。

文件層次結構有點怪,總之在 Angular Dart > Guide > Overview 下,說明了整個文件的結構 XD 大概就是:

Guide
說明基礎觀念。
Tutorial
一步一步建立一個應用程式。
Advanced
進階主題。

目前還不確定先看 Guide 好還是先跑 Tutorial 好,簡單看過感覺好像都可以。


Architecture Overview 介紹了整個概念、架構、主要的物件類別。大概是下面這些組成:

Template
使用 Angular 擴充過的 HTML 撰寫樣板。
Component
撰寫 Component 類別來使用 Template。
Service
應用程式的邏輯放在 Service 類別裡。
Module
Service 與 Component 封裝在 Module 裡面。

程式 (網頁) 打開是一個稱為 bootstrap the root module 的過程,會把 Angular 載入,然後使用者就可以跟介面互動,互動的事件會驅動 Angular 讓介面做出對應的變化。

AngularDart 的 module 跟 Dart 的 module 或 package 是差不多的概念,總之是一個編譯的單元。

除了最基本的 root module 之外,比較大的程式會分出數個 feature module 來將程式模組化。於最單純的情境下,在 root module 裡面就只有一個 component,稱為 root component,通常會命名為 AppComponent。

Component 可以控制一小塊稱為 view 的畫面,控制邏輯定義在一個類別裡面。在 Component 中會儲存有部份的應用程式的數據,主要是要支援 view 的顯示,比如說: 在 view 中顯示了人員列表,那麼 component 中可能就會有 List<Person> 物件實體。在 Component 中,常會關聯有一個 Service 物件的實體,用來提供數據資料,這部份直接看文件裡的範例可能會比較清楚。另外 component 有生命週期,可以藉由實作對應的介面方法來對生命週期中的事件做出反應: Lifecycle Hooks

Template 基本上就是加上 Angular 樣板語法 (Template Syntax) 的 HTML,除了標準的 HTML tag 與 Angular Template Syntax 之外,也可以使用自定義的 Angular 元件標籤。

Metadata 用來描述 Dart 類別是什麼樣的 Angular 物件類別,或者說是把用 Dart 類別實作的邏輯接入 AngularDart 框架的方法,基本上就是用 Dart 的 @annotation 達成。比如說: 透過將 @Component 加到一個類別上,就可以告訴 Angular 框架這個類別是一個 Angular Component 類別。在 @Component 上,還會描述諸如 template 路徑 (templateUrl)、找尋對應標籤用的 CSS selector (selector) 等資訊。除了 @Component 之外,還有 @Injectable, @Input, @Output 等 annotation 可以使用。

透過 @Component 就可以把 template 跟 component 邏輯繫結在一起,所以架構圖上在 template 與 component 之間,用不一樣的圖例畫了 metadata 在中間。

Data binding (資料繫結) 可以讓資料在 Component 與 DOM 之間被推送或是拉取的動作由框架代勞,而不用自己手動寫一堆程式碼來做更新 UI 或是從 UI 取值這些讓人煩躁的事情。要使用 data binding 基本上就是加註到 template 上,描述資料怎麼從 component 拉取出來或推送進去。有 4 種 data binding 的語法:

{{hero.name}}

這個語法稱為 Interpolation,功能是把資料值從 component 取出,轉換成文字後嵌入 (interpolate) 到 DOM 裡面。

<li>{{hero.name}}</li>

這是一個從 component 到 DOM 的單向 binding,主要是處理文字的資料。

[hero]

這個語法稱為 Property binding,功能是把資料值從 component 取出,賦值到所嵌入的 child component 的屬性上。

像下面這個例子,就是從目前的 component (在此例是 HeroListComponent) 中,取出 selectedHero 屬性的值,賦值 (assign) 到被嵌入的 child component (在此例是 HeroDetailComponent) 中的 hero 屬性中。

<hero-detail [hero]="selectedHero"></hero-detail>

文件上的圖是說這是一個從 component 到 DOM 的單向 binding,不過因為資料會先被另一個 component (child component) 接收到,再視接收的 component 的處理邏輯反應到 DOM 上,感覺上也可以說是 component 到 component 的單向 binding。

這個語法可以用來傳遞物件實體,這點跟 interpolation 語法不太一樣。

(click)

這個語法稱為 Event binding,功能是在使用者事件發生的時候呼叫指定的方法。

<li (click)="selectHero(hero)"></li>

這是一個從 DOM 到 component 的單向 binding。

[(ngModel)]

這個語法稱為 Two-way binding,功能是雙向的在 DOM 或 component 以及 component 之間雙向的更新資料值。

<input [(ngModel)]="hero.name">

語法 [(x)]="p" 會假設使用了這個語法的 child component 上有屬性 x 以及會在事件發生時產生新的屬性值的事件方法 xChange,在初始化的時候會拿關聯的屬性 p 來設定 x 的值,並會在 xChange 產出新的值的時候更新關聯的屬性 p 的內容。屬性 p 的變動初步測試起來也可以反應到 x 上,不過之前在 Polymer Dart 上被搞到快不行,之後可能還是要留意。

一般的 HTML element 並不支援 x-xChange 的協定,因此有 ngModel 這個 directive 來包裝傳統的 HTML element,讓他們可以使用 two-way binding。

在 Angular 中是依據 Directive 的指引從 template 的 DOM 結構產生 (render) 出最後的 DOM 結構,一個 Directive 就是一個有 @Directive annotation 的類別。

技術上來說 Component 也是一個 Directive,不過因為 Component 算是整個 Angular 運作的中心,因此在架構上會拉出來說明。

從行為上來看,Directive 有分成 structural directiveattribute directive 兩種,差別就是轉換的前後 DOM 結構是不是一樣。

Structural directive 在標記上要有一個星號 (*) 在標記的名稱前面,像是 *ngIf 這樣。實際上是會先產生一個 <template></template> 標籤,把被標記的標籤放進去,然後再把 <template> 丟進 directive 的 class 中處理。

在一個標籤中不可以有兩個以上的 structural directive,因為 structural directive 會變更 DOM 結構,變更動作的先後會對結果有影響,因此透過禁止有兩個以上的 structural directive 來迴避這個順序的問題。

常見的 structural directive 有: ngIf, ngFor, ngSwitch

Attribute directive 主要是用來變更一個元素或節點的外觀或行為。因為設定上看起來很像 DOM 節點的 attribute,所以稱為 attribute directive。

常見的 attribute directive 有: ngModel, ngStyle, ngClass

Service 基本上就是一個封裝好作業邏輯的類別實體,裡面就是實作應用程式邏輯、資料下載、資料驗證、訊息交換、紀錄等等,在 Component 裡面就是單純的呼叫 Service 提供的方法,並操作 view 做出回應。

對 Angular 來說,Service 並不是特別的組件,Angular 本身不會去看到或是識別出 Service 組件。如果要把邏輯通通實作在 Component 中也不是不行,但是這樣從程式的結構來看就是比較不理想。

AngularDart 實作了 Dependency Injection 機制來協助管理 Service 實體,讓 Service 的物件實體的產生跟 Component 的邏輯抽離。

使用 Angular 的 Dependency Injection (DI) 框架,大致上就是:

  1. 要被當成 Service 組件注入的類別要加註 @Injectable() (注意: 是產生一個 Injectable 物件實體來做 annotation)
  2. 在層次上開始使用到 Service 的 @Component annotation 上,給定 providers 參數來描述所要導入的 dependency 物件。
  3. 要用到 Service 組件的 Component 中,在建構子中指定所要使用的 Service 組件的型別。

依照 Hierarchical Dependency Injectors 的說法,在 AngularDart 框架裡頭會維護一個跟 DOM tree 結構大致上一致的 Injector tree 來進行 dependency resolve 的作業。在建立 Component 物件實體時,Injector 會沿著 Injector tree 往上去找尋適當的 Service 物件實體,當找不到的時候,就會依照 @Component 上 providers 參數所設定的 Provider 組態來建立 Service 物件實體。

Dart 套件工具

Dart SDK 裡面包了一個稱為 pub 的套件管理工具程式,說明網頁的標題是 Pub Package and Asset Manager

一個含有 pubspec.yaml 設定檔的資料夾就是一個 Dart 的 package,裡面原則上有個 lib/ 資料夾,詳情可以參考: Create Library Packages

在 pubspec 裡面,主要是設定名稱、相依套件 (dependency)、轉換器 (transformer)。

相依套件可以設定名稱,也可以設定成由 git 取得: Pub Dependencies

如果要自建套件發佈站: https://github.com/dart-lang/pub-dartlang-dart

開發時期才需要的套件,比如單元測試用的套件,可以列在 dev_dependencies 中。這主要是針對作為函式庫的套件,這樣僅僅使用到函式庫的話,就不用連測試用的套件也得安裝。

設定相依性的時候,版本規則比較特殊的是 Caret Syntax。意思是依照 semantic versioning 取相容的版本,舉例來說: ^5.2.3 會等同於 >=5.2.3 <6.0.0。關於相依性解析時,版本編號的解釋,可以參考: Pub Versioning Philosophy

整個建置的部份在各個平台間的整合與一致化似乎還不是很完整,目前 pub 指令的說明,包含 Pub Package and Asset ManagerPub Commands 裡面是說 pub buildpub serve 是專門針對 web 開發的:

Two additional commands (pub build and pub serve) are specific to web development.

這兩個子命令的文件也是丟在 Dart for the Web 那邊,但是 pub build 裡面用到的 transformer 又是在 pub 本身的文件裡頭介紹的 XDDD 可能整理的還不是很好,未來可能還會有變化。

一些名詞解釋: Glossary of Pub Terms -

Lockfile

一個檔案名稱為 pubspec.lock 的檔案,裡面會記載所安裝的套件的版本,會透過 pub get, pub upgrade, pub downgrade 等套件管理指令進行更新修改。

Application package

整個套件是作為應用程式使用,使用到的 dependencies 版本通常會是透過 Lockfile 鎖定在特定的版本上,在 pubspec.yaml 裡的版本規則反而是會用很隨意的方式設定,所以會建議 Lockfile 簽入版本控制中。

Library package

套件是作為函式庫使用為主,相依的 dependencies 版本應該儘量指定寬一點。

Asset

基本上就是所有影響到最後要佈署的產出的所有東西,包含原始碼、樣式表、圖檔等等。大概可以粗略的這樣分類:

  • pub buildpub serve來說:

    • Source asset: 在儲存媒體上的檔案,會被 pub 讀進去的,也就是原始的檔案。
    • Generated asset: 丟給 client 或是寫出到儲存媒體的檔案,會被 pub 輸出,也就是最後的結果產出。
  • transformer 的角度來看:

    • Input asset: 輸入的內容,可能是剛從 source asset 讀入,或是前一級的 transformer 的輸出。
    • Output asset: 輸出的內容,可能會被作為 generated asset 輸出,或是作為下一級 transformer 的輸入。

如果要自己寫 Transformer:

實作 Transformer 時所使用的套件是: barback

基本上要實作 Transformer 就是提供 .apply() (或是 .apply() 如果是實作 AggregateTransformer 的話,參數的型別不太一樣) 方法,要轉換的輸入會由外部用 Transform 物件傳入,轉換後把輸出用 Asset 物件包裝出來加到 Transform 物件中。

專案的資料夾 layout 可以參考: Pub Package Layout Conventions

不要加入版控的資料夾或檔案,可以參考 What Not to Commit (除了 Dart 的檔案外,裡面還有提到一些 IDE 所產生的檔案):

  • .packages: 由 pub 管理的所相依套件檔案。
  • doc/api: 在 doc/ 下的 api/ 資料夾是 dartdoc 產生出來的。
  • pubspec.lock: 看著辦,建議是 application package 的話要簽入版控。

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

TypeScript 基本語言元素

Microsoft 的文件安排跟 Google 稍微有點不太一樣,在 Tutorial 中的東西非常少,絕大多數都放在 Handbook 中,是蠻有趣的策略。從 URL 上來看,一開始可能甚至沒有打算特別作 Tutorial 的部份。

除了官方文件外,另外有一個 TypeScript Deep Dive 在 Google 搜尋結果上排名也蠻前面的。

測試簡單的語法時,可以使用 TypeScript Playground。看起來分享功能是透過 URL 來做的,所以不能分享太長的程式碼片斷。

語言的規格文件有放在 Github 上面: TypeScript Language Specification


型別標注 (type annotation) 類似一些新的語言,都是放在變數名稱後面:

let username: string;
function func(user: Person) { ... }
interface Name {
    firstName: string;
    lastName: string;
}

利用 interface 來描述物件的外觀,包含有什麼屬性、方法等。一個物件是不是吻合特定的 interface 會自動的透過檢查內部結構來達到,並不特別需要使用 implements 之類的描述。換言之,跟 Go 一樣,也是 duck typing 的型別系統。

class 上,建構子是使用 constructor 關鍵字來宣告。如果建構子參數加上了 public, protected, private (存取範圍修飾子; accessibility modifier) 或是 readonly 關鍵字,那麼稱為 Parameter Properties,參數將會直接成為屬性。


class Octopus {
    readonly numberOfLegs: number = 8;
    constructor(readonly name: string) {
    }
}

基本型別:

boolean

布林值,使用 true 或是 false 表示。

number

跟 JavaScript 相同,所有數值實質上都是浮點數。

定數 (literal) 除了十進位之外,也可以使用 Hex (0xF01D), Binary (0b11010010), Oct (0o0755) 來表示。

string

如同 JavaScript 一樣,可以用雙引號或是單引號來定義字串定數。

另外可以使用 backtick (backquote; `) 加上 ${ expr } 來定義 Template string:

let sentence: string = `Hello, my name is ${ fullName }.

I'll be ${ age + 1 } years old next month.`;

Template string 可以有很多行,這部份有點類似 Python 的 """ ... """ 語法。

Array

陣列可以使用 [] 或是泛型陣列語法 (Array<elementType>) 來宣告:

  • let l: number[] = [1, 2, 3];
  • let l: Array<number> = [1, 2, 3];
Tuple

數對/數組 (Tuple) 宣告的語法是 let my_tuple: [elementType1, elementType2, ...];。轉換成 JavaScript 時,是轉換成陣列。

使用元素時,使用類似陣列的方式: my_tuple[1]

也可以參照超出原本定義的範圍的元素,型別會被當作所宣告的所有元素型別的聯集型別 (union type),不過取用前需要先賦值。

let x: [string, number] = ["Hello", 18];
x[2].toString();  // Error, 還沒宣告
x[2] = 100;  // OK, 100 is (string | number)
x[2].toString();  // OK
x[2].substr(1);  // Error, 可使用屬性與方法是 string 與 number 所提供的交集
x[2] = "Today";  // OK, "Today" is (string | number)
x[2] = true;  // Error, true is not (string | number)

另外在初始化時,必須同時賦值,至少在 2.7 是這樣,跟對應過去的 JavaScript 程式有關。

也就是說: 這樣寫可以,但是這樣寫不行

enum

建立列舉的語法是 enum Color {Red, Green, Blue},基本上數值代號是依照宣告順序從 0 開始,不過也可以設定值: enum Color {Red = 1, Green, Blue}

上面的例子,在 2.7 轉換出來的程式碼是這樣:

var Color;
(function (Color) {
    Color[Color["Red"] = 0] = "Red";
    Color[Color["Green"] = 1] = "Green";
    Color[Color["Blue"] = 2] = "Blue";
})(Color || (Color = {}));
any

基本上就是任意值,比 Object 類別還廣。

某種層面來說,這是一個脫離 (opt-out) 行別檢查的方式。

void

原則上是用在描述函數回傳型別時使用,如果用來宣告變數的型別,該變數將只能繫結 nullundefined 上去。

null 與 undefined

在 TypeScript 中,null 與 undefined 同時也是型別。也就是說,可以宣告一個變數,型別是 null (或是 undefined),不過這樣子這個變數只能設定為 null (或 undefined)。

在預設的組態下,null 與 undefined 是所有型別的子型別。也就是說,可以把 null 或 undefined 賦值給任何型別的變數。

但如果開啟了 --strictNullChecks 檢查,那麼 null 與 undefined 將只會是 void 型別的子型別。在這個情況下,如果要讓一個變數或是參數可以接受 null 為參數,則需要使用 union type 來宣告,比如一個可為空值的字串: string | null

使用 --strictNullChecks 是被建議的,可以提早發現一些邏輯錯誤。

never

通常用在宣告一個函式或方法不會正常的結束,在函式或方法的內部邏輯是無窮迴圈或僅會拋出例外時使用。

Type assertion

類似型別轉換,不過轉出來的程式碼事實上並不會進行任何檢查,只是告知編譯器而已,不會有 overhead 在執行時期發生,程式設計師必須確定要轉換的變數確實是所指定的型別。語法有兩種:

  • let strLength: number = (<string>someValue).length;
  • let strLength: number = (someValue as string).length;

變數宣告在 TypeScript 中建議多多使用 let 關鍵字,雖然 JavaScript 的 var 也可以用,不過相當雷就是。

var let
Scoping function lexical / block
Shadowing No Yes
Capturing (Closure) function block (per-iteration)

比較重要的就是,使用 var 的場合下:

  • 變數的 scope 是函數,沒有在管 block level 的,整個命名空間的分割是透過函數作為邊界,在一個函數內重複定義的變數都是繫結在同一個變數實體上。
  • 也因為變數的 scope 是函數,所以利用 function 建立 closure 時,變數是繫結在當下的函數空間上。

使用 let 的話:

  • 變數的 scope 跟目前流行的語言一樣,是 block level 的,在 inner level 定義的變數可以遮蔽掉 outer level 定義的變數。
  • 建立 closure 時,一個 block 一層,不過 loop 比較特殊一點,每個 iteration 會產生一個新的 closure,所以每個 iteration 的計數用變數是不同的 instance。

下面這段程式:

function scope() {
 let x: number = 0;
 for(let i = 0; i < 3; i++) {
  setTimeout(function() { x = x + 1; console.log(`> ${ i }, [${ x }]`); }, 100 * (i + 1));
 }
 return function() {
  console.log(`x = [${ x }]`);
 };
}

let f = scope();
setTimeout(f, 500);

會輸出:

> 0, [1]
> 1, [2]
> 2, [3]
x = [3]

宣告常數可以使用 const 關鍵字,用起來跟用 let 基本上是一樣的,不過要注意,如果繫結的目標是一個物件實體的話,只是說所建立的變數參考是不變的,物件實體的內容還是可以被改變。要確保物件實體的內容不改變的話,必須要把屬性加上 readonly 宣告。

在 TypeScript 可以做 parallel assignment,也就是類似 Python 的 unpacking assignment。用法相當的多:

  • let input = [1, 2];
    let [first, second] = input;
  • [first, second] = [second, first];  // swap variable
  • function f([first, second]: [number, number]) {
        // ...
    }
    f([1, 2]);
  • let [first, ...rest] = [1, 2, 3, 4, 5];
    console.log(first); // outputs 1
    console.log(rest);  // outputs [ 2, 3, 4, 5 ]
  • let [first] = [1, 2, 3, 4, 5];  // first = 1, 丟棄其他的數值
  • let [, second, , fourth] = [1, 2, 3, 4];  // second = 2, fourth = 4, 丟棄其他的數值

物件也可以做 destructuring (Object destructuring),感覺就有點誇張了,可能是要用在函數參數上吧!不過這樣傳參數就變成要傳匿名物件,感覺也是怪怪的。

另外也可以做陣列或是物件的融合,稱為 spread:

let first = [1, 2];
let second = [3, 4];
let bothPlus = [0, ...first, ...second, 5];
// bothPlus 變成 [0, 1, 2, 3, 4, 5]
let defaults = { food: "spicy", price: "$$", ambiance: "noisy" };
let search = { ...defaults, food: "rich" };

經過 spread 操作後,原始陣列或物件的元素會做一個 shallow copy 到新的陣列或物件中。此外,針對物件的 spread 不會複製方法,只有屬性被複製。

定義 interface 時,可以對屬性加上問號 (?) 讓該屬性變成選用的屬性 (optional property)。

interface SquareConfig {
    color?: string;
    width?: number;
}

屬性可以加上 readonly 關鍵字,讓該屬性變成唯讀的。

interface Point {
    readonly x: number;
    readonly y: number;
}

透過 interface 還可以定義函式型別等特殊的型別:

Function type
interface SearchFunc {
    (subject_text: string, max_entry_count: number): string[];
}
Indexable type

主要用來限制用來索引的變數型別,在 JavaScript 中預設是可以用任何型別的值來作為陣列的索引。

interface StringArray {
    [index: number]: string;
}

混用 number 跟 string 作為索引型別其實也是可行的,不過要注意因為這個情況下 JavaScript 底層會把 number 轉成 string 來作為索引,因此 number 對應的元素型態必須是 string 所對應元素型態的子類別。

class Animal {
    name: string;
}
class Dog extends Animal {
    breed: string;
}

interface Okay {
    [x: number]: Dog;
    [x: string]: Animal;
}

interface NotOkay {
    [x: number]: Animal;
    [x: string]: Dog;
}

此外,也可以讓陣列成為唯讀的:

interface ReadonlyStringArray {
    readonly [index: number]: string;
}

定義 class 的時候,可以透過 implements 關鍵字宣告類別有實作特定的 interface

classinterface 中,兩者都可以利用 extends 關鍵字來進行繼承,都可以進行多重繼承。

此外,在 interface 上甚至可以繼承 class,不過在這個情況下,只有屬性與方法的介面定義會被繼承到 interface 上頭,方法的實作不會被繼承。

下面是一些莫名其妙的用法:

  • 因為 interface 定義的是 instance 上的限制,因此不可能在針對 instance 的 interface 定義針對建構子的介面定義,因為建構子對所定義的 class 來說是 static 的。

    這個問題可以透過額外定義一個內含 new 介面的 interface 來繞過,但就沒辦法很明確的陳述該類別建構子吻合某特定 interface 了,只能透過把 class 物件傳入函式來檢查。

  • 因為 JavaScript 的函數本身是 Object,然後 Object 可以隨意的增加屬性,因此可以定義出很奇怪的 interface:

    interface Counter {
        (start: number): string;
        interval: number;
        reset(): void;
    }

    要造出這種怪東西的實體,可以這樣做:

    function getCounter(): Counter {
        let counter = <Counter>function (start: number) { };
        counter.interval = 123;
        counter.reset = function () { };
        return counter;
    }

定義 class 時,建構子使用 constructor 關鍵字來定義,可以使用 thissuper 來參考實體本身以及上層類別中的成員。

成員存取權限跟 Java 很相似,預設是 public,另外有 protectedprivate 可以使用。宣告為 protected 的成員,可以被衍生的類別成員所使用。

如果屬性不希望被更動,可以使用 readonly 修飾字,加上了這個修飾字之後,該屬性只能在建構子中被初始化。

如果要細緻的控制執的存取,可以使用 getset 來程式化屬性的存取,這部份跟 Dart 的設計類似。

class Employee {
    private _fullName: string;

    get fullName(): string {
        return this._fullName;
    }

    set fullName(newName: string) {
        if (newName) {
            this._fullName = newName;
        } else {
            this._fullName = "No name";
        }
    }
}

另外也可以用 static 來建立靜態成員,語法跟 Java 相同。也可以使用 abstractclassfunction 上,來建立抽象類別。

由於 class 定義最後會被轉換成一個產生 object 的 function 物件,因此有時候會看到在 TypeScript 文件中用 constructor function 這樣的名稱來指稱一個 class 定義。

透過型別推理,下面幾個函式變數的寫法有一樣的結果:

  • let myAdd: (baseValue: number, increment: number) => number = function(x: number, y: number): number { return x + y; };
  • let myAdd = function(x: number, y: number): number { return x + y; };
  • let myAdd: (baseValue: number, increment: number) => number = function(x, y) { return x + y; };

函數的參數可以是選用 (optional) 的:

function buildName(firstName: string, lastName?: string) { ... }

也可以幫參數設定預設值:

function buildName(firstName: string, lastName = "Smith") { ... }

選用的參數必須在非選用的 (required) 參數之後,不過預設值就沒有這個限制。呼叫時把參數值設定為 undefined 就會使用預設值,因此可以不受位置的限制。

如果使用了 --strictNullChecks 參數進行編譯,選用的參數在型別中會自動的被加上 | undefined 成為 union type,這點在選用的類別屬性上也是一樣的。

可以用 ... 來取得剩餘的參數,比較類似 Python 的 *args 的用法:

function buildName(firstName: string, ...restOfName: string[]) { ... }

在 JavaScript 中的 this 會參考到目前的 context 上,也就是函式物件是怎麼呼叫的。文件上的例子是一個物件中的方法傳回了一個函式物件,程式把傳回的函式物件存在全域變數中,此時呼叫這個函式物件會發現他的 this 是 window 這個全域物件實體。

在 TypeScript 中可以利用新的 arrow function 語法,這樣子所建立出來的函式物件會取定義函式物件時的 this 來作為函式物件被呼叫時要使用的 this 參考。

傳統的 JavaScript 函式定義:
function () { ... }
TypeScript/ECMAScript6 的 arrow function 函式定義:
() => { ... }

基本上,就算是在 TypeScript 中,函式或物件方法仍然是用傳統的 JavaScript function 去看待,因此要注意 this 到底是參考到什麼的問題。

可以加上 this 的型別標注,並在編譯時開啟 --noImplicitThis 編譯選項,來讓編譯器提出關於 this 型別的相關警告。

在沒有特別設定的 function 中,this 型別會預設為 any,這點即使是物件方法也一樣。加上關於 this 的物件宣告,就可以提示所預期的 this 參考應有的型別。

class Handler {
    onClick(this: Handler, e: Event) {
        // ...
    }
}

但是如果要把物件的方法當成給 DOM event 的 callback function 來使用,要注意是無法正常使用 this 的。因為 callback function 會被當成一般的函式呼叫,所以呼叫時的 this 必須是設定為 void 型別 。換句話說,在 callback function 的原型定義中,this 型別必須為 void 型別。因此,如果要把物件方法當成 callback function 使用,該方法必須特別使用 => (arrow function) 來定義:

class Handler {
    info: string;
    onClick = (e: Event) => {
        this.info;
        // ...
    }
}

要注意的是,每個使用 arrow function 定義出來的函式或方法,背後會帶一包 closure,因此會比傳統的 function 還要佔空間。

TypeScript 也有 function overload,不過看起來只是增加型別檢查的部份,實作上還是要寫一個參數與回傳值都是 any 的函式實體,並在裡面用 typeof== 來針對輸入的行別檢查並做出對應反應。

function pickCard(x: {suit: string; card: number; }[]): number;
function pickCard(x: number): {suit: string; card: number; };
function pickCard(x): any {
    if (typeof x == "object") { ... }
    else if (typeof x == "number") { ... }
}

以上面的例子來說,最後的 (x): any 實作是不會被算進去的,因此實際上的 overload 只有兩個。

此外,檢查 overload 時,會從給定清單的上面往下面找,因此要把比較明確的放在較上方宣告。

泛型的部份與 Java 很相似,呼叫時的語法大概是 func<string>(arg0, arg1, ...) 這樣,不過也支援型別自動推理 (type argument inference): func("my-string") 來簡略化程式碼。

也支援在泛型型別上增加條件:

  • function loggingIdentity<T extends Lengthwise>(arg: T): T { ... }
  • function getProperty<T, K extends keyof T>(obj: T, key: K) { ... }

如果要使用泛型在 factory 函式上,在手冊上有個有點複雜的範例可以參考。

型別推理 (type inference) 基本上發生在賦值時,策略大概有:

基本

只是純量的賦值,如: let x = 3;

最佳共同型別 (best common type)

在陣列上時,會儘量找出陣列元素中的共同型別,如果沒有辦法找出共同型別時,會用 union type 來處理。

脈絡 (contextual type)

如果是賦值到一個已知型別的屬性或是函數呼叫的參數值,會利用該型別來檢查傳入的參數型別。

TypeScript 在物件的型別相容上使用 structural subtyping 作為理論基礎,也就是說,物件實體是不是等同於或相容於某個型別,是透過實體上有哪些屬性與方法來決定。

部份無法在 compile time 確認是型別安全的操作在 TypeScript 是被允許的,因此 TypeScript 學理上是 not soundness 的語言。從例子看起來,主要是有 callback 的狀況下會有允許 unsound 的設計。

比如這樣一個函式:

function listenEvent(eventType: EventType, handler: (n: Event) => void) { ... }

下面這樣呼叫是 Unsound 的,因為 MouseEvent 比 Event 還嚴格,但是基於 assignment 的角度來檢查型別,所以這樣寫還是會過,而且這樣寫也比較好懂:

listenEvent(EventType.Mouse, (e: MouseEvent) => { ... });

如果要維持 Soundness 的話,可以這樣寫,但是這樣寫就比較不容易閱讀:

listenEvent(EventType.Mouse, (e: Event) => { ... ((<MouseEvent>e).x + "," + (<MouseEvent>e).y)); ... });

列舉 (enum) 跟數值 (number) 是可以互相相容的,不過不同的 enum 之間是不相容的。

類別 (class) 的型別相容檢查只針對 instance 上的屬性與方法檢查,建構子與 static member 不在檢查範圍內。類別實體檢查型別相容性時,會額外考慮 private 與 protected 的成員,檢查目標也要有同樣的成員,而且必須是在同一個類別定義的,這樣的特性讓子類別可以相容於上層類別,但不會與不同繼承關係下的其他類別實體相容。

Intersection type 語法是 T & U & V ... 這樣寫,代表這個融合後型別的實體必須有「所有」基礎類別的屬性與方法。對應到其他語言會是 Mixin 的特性,不過看起來 TypeScript 本身並沒有特別有 Mixin 的支援,至少範例裡面把兩個類別實作融在一起的部份是另外寫迴圈做的。

Union type 語法是 T | U | V ... 這樣寫,代表融合的型別只需要有基礎類別中「共同」的屬性與方法,常用在參數可以接受兩種以上型別時。

使用 union type 時可以搭配稱為 type guard 的 if 語法,來簡化程式碼。原本要利用 type assertion 的程式碼:

let pet: Fish | Bird = getSmallPet();

if ((<Fish>pet).swim) {
    (<Fish>pet).swim();
}
else {
    (<Bird>pet).fly();
}

可以透過特定的型別檢查判斷式,搭配 if ... else if ... else 分支,在 if 內的程式碼區塊中,變數的型別會被局部的窄化 (narrow) 為所檢查通過的型別。

型別檢查的判斷式有幾種型式:

使用者定義的檢查 (type predicate)

利用撰寫一個傳回 type predicate 的函式,這個函式的參數型別必須是要檢查的 union type,傳回一個型別為 type predicate 的布林值:

function isFish(pet: Fish | Bird): pet is Fish {
    return (<Fish>pet).swim !== undefined;
}

if (isFish(pet)) {
    pet.swim();
}
else {
    pet.fly();
}

這樣子可以自行撰寫型別檢查的邏輯,可以做類似 feature-based 的分支。

typeof

透過 typeof 關鍵字可以取得物件實體型別的字串表示,可以透過判斷型別名稱字串來做型別的檢查,但是只有非常有限的型別被支援。

支援的檢查陳述式為 typeof v === "typename<"typeof v !== "typename<" 兩種,支援的型別 (typename) 有: number, string, boolean, symbol,其他型別的字串也可以寫,但是不會有 type guard 的效果。

使用範例如下:

function padLeft(value: string, padding: string | number) {
    if (typeof padding === "number") {
        ...
    } else if (typeof padding === "string") {
        ...
    }
    ...
}
instanceof

也可以使用 instanceof 來進行 type guard:

if (pet instanceof Fish) {
    pet.swim();
}
else {
    pet.fly();
}

instanceof 的右側必須要是一個 constructor function,程式碼區塊內會把物件實體變數的型別依照下面邏輯順序判斷進行 narrow:

  1. 如果該 constructor function 的 prototype 有設定型別 (不是 any) 的話,那就使用該型別;或是
  2. 該 constructor function 所定義的所有可回傳型別的 union type。
針對 null 的處理

如果變數是 null 的 union type 的話,可以利用 v == null 來進行 type guard:

function f(sn: string | null): string {
    if (sn == null) {
        return "default";
    }
    else {
        return sn;
    }
}

看起來大概就是: 如果要做基本型別的 type guard 可以使用 typeof;如果是類別的話,則使用 instanceof 來進行。

對於 Nullable type (也就是有 null 或 undefined 一起作為 union type 的變數) 在除去 null 的時候可以利用 terser operator: x = x || "default";

當編譯器無法自動追蹤出變數已經不可能為 null 或 undefined 值時,可以在該變數識別字後加上驚嘆號 (type assertion operator): title = name!.charAt(1) ... 告訴編譯器該值已不可能為空值。

使用 type 關鍵字可以定義型別的別名 (type alias),類似 C/C++ 的 typedef 關鍵字。型別別名在編譯後會被展開,此外也不能進行 extends 或是 implements 等擴充。

可以在 type alias 中使用泛型或是參考到自己:

type Tree<T> = {
    value: T;
    left: Tree<T>;
    right: Tree<T>;
}

或是搭配 intersection type 使用:

type LinkedList<T> = T & { next: LinkedList<T> };

字串或是數值的定數 (literal) 也是型別,稱為 literal type,可以搭配 union type 或 type alias 等特性使用。

比如對於 type alias: type Easing = "ease-in" | "ease-out" | "ease-in-out"; 如果有變數宣告為 Easing 型別,那麼該變數中可以接受的值就只有所指定的字串。

另外也可以搭配 overload 使用:

function createElement(tagName: "img"): HTMLImageElement;
function createElement(tagName: "input"): HTMLInputElement;
// overloads ...
function createElement(tagName: string): Element {
    // 實作
}

Literal type 跟 enum member type 合稱為 singleton type,不過常會不精準的混用。

類別的方法可以用 this 當成傳回型別,這樣被繼承之後,舊的方法傳回來的物件參考還是新的類別型別,可以便利 chained invoke 的程式撰寫方法。

因為 JavaScript 物件可以用類似關聯陣列的方式存取,因此也有相關的 operator 可以使用,相關的 feature 稱為 index type

function getProperty<T, K extends keyof T>(o: T, name: K): T[K] {
    return o[name]; // o[name] is of type T[K]
}

let name: string = getProperty(person, 'name');
let age: number = getProperty(person, 'age');
let unknown = getProperty(person, 'unknown'); // ERR: 'unknown' is not in 'name' | 'age'

透過 keyof T 可以將給定的型別 T 中所有的 public 屬性與方法的名稱取出並構成 union type,這個操作稱為 index type query operator。要特別注意,是取出為型別 (union type) 而不是一個字串陣列。

取得類別中特定屬性的型別可以用 T[K] 運算子,會需要搭配 K extends keyof T 來使用,這個運算子稱為 indexed access operator,可以取出 T 類別中 K 所指涉的屬性或方法的型別。

感覺上在終端的程式上 index type 相關 operator 好像沒辦法使用,但是在 library 或 framework 上應該就可以用上,來跟終端程式碼的自訂型別進行整合。

還可以透過 mapped type 來將所有的 public 成員型別做一個系統性的變更:

type Readonly<T> = {
    readonly [P in keyof T]: T[P];
}

type Partial<T> = {
    [P in keyof T]?: T[P];
}

type Nullable<T> = {
    [P in keyof T]: T[P] | null;
}

上面這幾個算是常用的,有納入標準函式庫中: ECMAScript APIs

在 ECMAScript 2015 增加了 Symbol 建構函式跟 symbol 型別,看起來是用來建立匿名的識別字 (identifier): MDN: Glossary - Symbol, TypeScript Handbook: Symbols

除了一般的 for (...; ...; ...) 之外,迴圈還有 for (... of ...)for (... in ...) 兩種,分別是對 container 的元素本身,以及索引鍵值遊走。

在 Handbook 上針對元素遊走 (for-of) 有特別提到,因為是比較新的 ES6 的語法,如果 TypeScript 的 target 是設定為 ES5 或更早版本的話,只能用在 Array 型別上。不過,在 2.3 的 release node 裡頭好像又說目前可以轉換的程度有比較厲害,所以看來只能用試的了。

在 MDN 的文章對於 Iterator 的說明比較清楚: Symbol.iterator

要使用的話可以參考 TypeScript Deep Dive: Iterator 章節,有實作上的細節。

import 或是 export 的程式碼檔案,就自動會被視為 module 看待。沒有 import 或是 export 的程式碼檔案,定義的內容就會當成全域的內容,對所有程式碼與模組開放。模組內的變數或方法必須要有 export 修飾字才可以被其他模組 import引用,可以一次匯入或匯出複數個變數或方法,匯入時可以利用 as 設定新的名稱。

如果一個模組只 export 了一個參考,可以使用 export default 的方式,程式會比較清楚。這樣匯出的參考,在匯入時一律需要給定別名。

在模組內可以定義 namespace 來在內部切割命名空間,另外也可以搭配 /// <reference path="code-file.ts" /> 來匯入。

因為 JavaScript 的模組化實在太多嘗試了,所以感覺這部份 TypeScript 也有點亂,有各種相容語法。

使用 import ... from ... 時,搜尋模組有 Classic 以及 Node 兩種模式,原則上在使用 Angular 時應該是使用 Node 模式。

以在 /root/src/moduleA.ts 匯入為例:

相對路徑匯入 (relative import; 匯入路徑由 /, ./../ 開頭): import { b } from "./moduleB"
  1. /root/src/moduleB.{ts, tsx, d.ts}
  2. /root/src/moduleB/package.json (設定內的 types 屬性所指定的目標)
  3. /root/src/moduleB/index.{ts, tsx, d.ts}
非相對路徑匯入 (non-relative import; 匯入路徑不是由 /, ./../ 開頭): import { b } from "moduleB"
  1. /root/src/node_modules/moduleB.{ts, tsx, d.ts}
  2. /root/src/node_modules/moduleB/package.json (設定內的 types 屬性所指定的目標)
  3. /root/src/node_modules/moduleB/index.{ts, tsx, d.ts}
  4. /root/node_modules/moduleB.{ts, tsx, d.ts}
  5. /root/node_modules/moduleB/package.json (設定內的 types 屬性所指定的目標)
  6. /root/node_modules/moduleB/index.{ts, tsx, d.ts}
  7. /node_modules/moduleB.{ts, tsx, d.ts}
  8. /node_modules/moduleB/package.json (設定內的 types 屬性所指定的目標)
  9. /node_modules/moduleB/index.{ts, tsx, d.ts}

以程式碼所在資料夾為基礎無法找到時會往上層找,這部份是模擬 Node.js 的行為,依據 Node.js 的文件是會找到根目錄為止。

Triple-slash directives 就是指檔案最上頭的 /// <... /> 那個指引,比較會用到的應該就:

/// <reference path="..." />

主要是用來宣告相依順序,雖然有點類似 C/C++ 的 #include ... 但是實際參考到的檔案並不會在輸出中出現。

也就是說,如果參考到的檔案也希望一起編到輸出的 .js 檔的話,輸入檔案清單也要提到那些檔案,產生 JavaScript 程式碼時也會一句這邊的設定決定輸出的程式碼區塊的先後順序。

這個指引使用的路徑基本上是檔案相對路徑,應該是以模組內的使用為主。

/// <reference types="..." />

主要是用來參考自行撰寫的 .d.ts 用,會轉換為 @types/.../index.d.ts 後,用類似 module 的找尋邏輯去找。

需要搭配 tsconfig.json 中的設定使用: @types, typeRoots and types

使用 tsconfig.json 可以設定要給 compiler (tsc) 的相關組態設定,當執行 tsc 但沒給輸入的程式碼檔案時,就會從目前工作資料夾開始找 tsconfig.json 來使用,找不到時會往上層資料夾去找。另外也可以使用 -p 或 --project 參數,可以接受資料夾或是一個 JSON 檔案路徑,當給定資料夾時會去找該資料夾下的 tsconfig.json 檔。

NPM 基本操作

使用 Node.js 為基礎的工具時,基本上都會碰上 npm 這個工具。

安裝套件使用的是 npm install [套件] 子指令,參照套件的描述方法有:

  • (沒參數,工作目錄需在套件資料夾中)
  • [<@scope>/]<name>
  • [<@scope>/]<name>@<tag>
  • [<@scope>/]<name>@<version>
  • [<@scope>/]<name>@<version range>
  • <git-host>:<git-user>/<repo-name>
  • <git repo url>
  • <tarball file>
  • <tarball url>
  • <folder>

在 npm 中,「套件」可以是資料夾、壓縮檔或 Git 檔案庫,只要要作為套件的基礎資料夾中有 package.json 檔案即可。

相依套件的版本管理則是透過 npm-shrinkwrap.jsonpackage-lock.json 檔案,基本上如果套件要發佈的話,只有 npm-shrinkwrap.json 會有意義,後面的 package-lock.json 只會在 top-level 套件有作用,比較只是開發時期團隊可以共用來鎖定目前的相依版本。

依照 npm 的規劃,一般函式庫套件應該只有 package-lock.json 以作為其他開發者的參考,在安裝時期不會有任何作用,版本的解析會依照 package.json 中的描述進行。如果是作為 CLI 工具等「全域」的套件,則可以加上 npm-shrinkwrap.json 來鎖定確定可以正常運作的版本。

參考套件時,使用 <name> 系列的方法所參考的名稱或是版本是指 npm registry 中的名稱與版本。

套件發佈時可以選擇發佈在使用者或組織專屬的 scope 下,參考時的語法是 @scope/name。利用 scope 可以把關聯的套件放在一起,也比較不用擔心名稱空間衝突的問題。

套件檔案的安排可以在 npm-folders 看到:

使用 Global 模式安裝 (加上 -g | --global 參數) 時:

套件檔案會裝到 Node.js 安裝路徑上的 lib/node_modules/{套件識別路徑} 資料夾中,如果有可執行的指令稿,會連結到安裝路徑的 bin/ 中。

使用 Local 模式安裝 (預設) 時:

套件檔案會安裝到目前工作專案資料夾下的 node_modules/{套件識別路徑} 資料夾中,如果有可執行的指令稿,會連結到目前工作資料夾下的 node_modules/.bin/ 中。

npm 在找尋工作專案資料夾的方法是一層一層往上找尋含有 package.json 檔或 node_modules/ 資料夾的資料夾,作為工作專案資料夾。

從測試來看,如果是 Global 模式時,相依的套件會安裝到套件識別路徑下的 node_modules/ 中,而 Local 模式的話則不會。

舉例來說,在分別使用 Global 與 Local 模式安裝 @angular/cli 的話,相依的 typescript 套件被放置的路徑會分別是:

  • Global: ${NODE-PREFIX}/lib/node_modules/@angular/cli/node_modules/typescript/
  • Local: ${PROJECT}/node_modules/typescript/