--- title: 開發Angular的路由轉場動畫 # 簡報的名稱 lang: zh-tw tags: Angular # 簡報的標籤 slideOptions: # 簡報相關的設定 transition: 'slide' # 換頁動畫 width: 1200 --- # 開發Angular的路由轉場動畫 ## 前言 Angular支援Javascript animation,可以很輕易的開發出動畫效果,而動畫又分為元件(Component)與路由(Router)二種;舉例來說,讓畫面中的按扭在滑鼠移動時產生抖動或縮放等互動效果便屬於Component animation,而當畫面在切換時產生類似僕克牌在抽換的效果便屬於Router animation。 本次的動畫的目的是在換頁時產生左移與右效的效果,讓每一個頁面產生一個順序性,當從第一頁移到第二頁時,畫面會由左至右,而第二頁到第一頁時則會有由右到左的效果;在Angular中,只要依照Angular所定義的[屬性](https://angular.tw/guide/route-animations)來設定便可以將所有的動畫效果集中在單一檔案並且重覆使用。 ## 步驟一 新增動畫 在本次的範例中,預計會用到二個動畫效果-左移與右移,首先要新增一個檔案來存放這二個動畫, 使用`ng g class animations`指令來新增檔案。 ```typescript= import { query, style, animate, group } from "@angular/animations"; const ANIMATION_SPEED = "500ms"; export const slideLeft = [ query(":enter, :leave", style({ position: "fixed", width: "100%" })), group([ query(":enter", [ style({ transform: "translateX(150%)" }), animate( `${ANIMATION_SPEED} ease-in-out`, style({ transform: "translateX(0)" }) ) ]), query(":leave", [ style({ transform: "translateX(0%)" }), animate( `${ANIMATION_SPEED} ease-in-out`, style({ transform: "translateX(-150%)" }) ) ]) ]) ]; export const slideRight = [ group([ query(":enter, :leave", style({ position: "fixed", width: "100%" })), query(":enter", [ style({ transform: "translateX(-150%)" }), animate( `${ANIMATION_SPEED} ease-in-out`, style({ transform: "translateX(0%)" }) ) ]), query(":leave", [ animate( `${ANIMATION_SPEED} ease-in-out`, style({ transform: "translateX(150%)" }) ) ]) ]) ]; ``` ## 步驟二 使用const斷言來描述路由狀態 依照教學的[步驟](https://angular.tw/guide/router),一般情況會透過`RouterModule.forRoot`函式載入一組`Route[]`以合併路徑與元件,常見的語法如下: ```typescript= const routes: Routes = [ { path: 'home', component: HomeComponent }, { path: 'page1', component: FirstComponent }, { path: 'page2', component: SecondComponent }, ]; ``` 這樣的作法最大的缺點是在消費path屬性時無法得到保証;在後續的開發過程需要撰寫路徑時產生錯誤(例如`<a routerLink="/pageone">`),編譯器是無法提供任何幫助,只能在上線後透過人工的方式查詢。 在Typescript3.7版提供了類型斷言(Type assertions)的特性,這個特性可以推斷`字符串字面量`成為一種抽象類型。在說明斷言之前,要先來了解一下Typescript在類型的推斷上是如何進行。 ```typescript= let t1 = 'foo'; // string const t2 = 'foo'; // 'foo' let t3: 'foo' = 'foo'; // 'foo' const t4 = {id: 1,name: 'baz'} // { id: number, name: string } t1='bar'; // OK t2='bar'; // Cannot assign t3='bar'; // Type '"bar"' is not assignable to type '"foo"' t4.name='bar'; // OK ``` 從上述的範例中可以看到,Typescript在處理變量時會推斷成基礎型別,`t3`變數則是更嚴格的指定型別並規範了變數而達到與常數宣告同樣的效果,另外,對於常數物件內部的屬性推斷仍為基礎型別。 對於Typescript的型別推斷有了初步的認識之後,接著可以來了解一下`const assertions`(斷言) ### const assertions 斷言也是一種型別推斷的方式,具有下列特性: > 1. 基礎類型不會被擴展。 > 1. 陣列常值(Array literals)會擴展成唯讀。 > 1. 物件常值(Object literals)的屬性會擴展成唯讀。 ```typescript= const t2 = 'foo' as const; // 'foo' const paths = ["home", "page1", "page2"] as const; // readonly ["home", "page1", "page2"] const t4 = {id: 1,name: 'baz'} as const; // { readonly id: 1; readonly name: "baz";} /*****************************************/ paths[0]='page3' // Cannot assign to '0' because it is a read-only property. t4.id=2; // Cannot assign to 'id' because it is a read-only property. ``` 再舉一個物件常值的例子: ```typescript= const result = [ { kind: "circle", radius: 100 }, { kind: "square", sideLength: 50 } ] as const; for (let shape of result) { if (shape.kind === "circle") { console.log("Circle radius", shape.radius); // shape.sideLength error } else { console.log("Square side length", shape.sideLength); // shape.radius error } } ``` 在第二個範例中,kind是一種字串符字面量(stringliteral),藉由const assertions的規範使得Typescript可以直接從屬性值推斷shape的型別。 ### typeof 在typescript中,`typeof`是用來擷取指定的物件的型別並返回一個`type`,而`type`可以代表基礎類型、介面、函式或自訂物件。 ```typescript= let h = "hello"; let w: typeof h = 'word'; // let w: string /*****等價於 * type t = typeof h; // type t = string * let w :t = 'word'; * *****/ console.log(`${h} ${w}.`); // hello word. ``` ```typescript= let s = { name :"hello"}; let n: typeof s; // let n: { name: string;} /*****等價於 * type t = typeof s; // type t = { name: string;} * let n: t; // let n: { name: string;} * *****/ n.name+='world'; // OK ``` 若變數是基底型別,`typeof`會回傳基底型別;若變數是自訂型別則會回傳自訂型別的結構,這並不是一個很有用的特性,但在實務上可以搭配其他的運算符號來搭配,上述的例子中便利用`type`修飾字來保存型別並重覆利用([另一個範例](https://www.typescriptlang.org/docs/handbook/2/typeof-types.html))。又或者是搭配`Lookup Types`可以將`paths`陣列轉換成字串符字面量(string literal),使得資料本身也是型別。 ```typescript= type Paths = typeof paths[number]; // type Paths = "home" | "page1" | "page2" ``` > paths[number]是[Lookup Types](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-1.html#keyof-and-lookup-types)或稱為indexed access types。 >實務上只需要直接宣告成 type Paths = 'home' | 'page1' | 'page2' ; ### Record Record是一種泛型的結構,支援型別與型別之間的轉換,例如: ```typescript= interface EmployeeType { id: number fullname: string role: string } const employees: Record<string, EmployeeType> = { '0': { id: 1, fullname: "John Doe", role: "Designer" }, '1': { id: 2, fullname: "Ibrahima Fall", role: "Developer" }, '2': { id: 3, fullname: "Sara Duckson", role: "Developer" }, } /**** * const employees: Record<string, EmployeeType> = { '0': { id: 1, fullname: "John Doe", role: "Designer" }, '1': { id: 2, fullname: "Ibrahima Fall", role: "Developer" }, '1': { id: 3, fullname: "Sara Duckson", role: "Developer" }, } // Duplicate identifier ''1''. ****/ console.log(employees['0']); // { id: 1, fullname: "John Doe", role: "Designer" } ``` Record型別的T屬性是不允許重覆的資料,詐看之下類似一種目錄結構(key/value pair),但Record不支緩新增或刪除等操作,而只是單純的型別之間的轉換關係。更進階的作法,便是建立一個string literal型別來使用,例如: ```typescript= type EmployeeNumber = '0' | '1' | '2'; const employees: Record<EmployeeNumber| '3', EmployeeType> = { '0': { id: 1, fullname: "John Doe", role: "Designer" }, '1': { id: 2, fullname: "Ibrahima Fall", role: "Developer" }, '2': { id: 3, fullname: "Sara Duckson", role: "Developer" }, '3': { id: 3, fullname: "Air Jodan", role: "Developer" } } console.log(employees['0']); // { id: 1, fullname: "John Doe", role: ``` > 從上例可以發現,Record的型別除了EmployeeNumber之外,自行擴充了字串('3')。 如此一來,便可以利用`Paths`型別制定路徑與元件之間的關係;新增一個 ```typescript= interface IConfiguration { path: string; component: any; order: number; linkText: string; }; const pathConfiguration: Record<Paths, IConfiguration> = { home: { path: '', component: HomeComponent, order: 0, linkText: 'Home' }, page1: { path: 'page1', component: Page1Component, order: 0, linkText: 'page1' }, page2: { path: 'page2', component: Page2Component, order: 0, linkText: 'page2' } }; ```