2020年9月9日水曜日

[Web開發]angular 9 筆記

之前是使用angularJS寫前端。
angularJS有個最大的罩門就是不適合做uglify。對於程式碼的保護可以說是沒有。
整個模組也有點大,網頁啟動的時間偏長。

anguar 9 在評估之後,有以下幾點符合需求:
  • 有自己的編譯工具,可以滿足minify / uglify / tree shaking等等減少含入程式碼或是模組的能力
  • 導入typescript,可以撰寫型別較為嚴謹的設計(也可以不使用)
  • lazy-loading頁面,減少啟動頁面到出現讀取畫面的所需時間跟資料量,也可以減少編譯時間。個人認為是很大的優勢。把頁面分開撰寫,編譯的時候只會編譯有改到的頁面。
  • 預設可以使用sass/scss (因此導入bootstrap4也可以加上自定義的顏色組合)
  • 模組化設計

於是就開始學習使用angular 9了。希望以下的資訊可以幫助到各位。
以下簡稱為angular。

前置作業:
建立node.js的開發環境。  到 https://nodejs.org/ 下載安裝包並安裝在作業系統內。
筆者是使用MAC OSX為開發環境,以下的動作也都是以OSX為主。

安裝angular開發環境:
npm install -g @angular/cli

不知所措的第一步:建立全新的project。angular因為需要編譯,其實開發環境是相對複雜的。之後再來研究製作出來的檔案。
在想要建立project的路徑之下執行:
ng new <project name>
本文使用「angularTest」作為project名稱。

「ng」是angular開發環境的執行指令。在angular開發環境安裝成功後應該就可以使用。若是不行的話代表angular的開發環境沒有安裝正確。

接著會有兩個選項需要選擇:
? Would you like to add Angular routing? Yes
angular是靠routing來實作lazy-loading,基本上是選yes。選No的話之後要使用routing的話,就要自己加上routing的設計,會比較麻煩些。選擇yes,就算要製作的網頁只有一頁,也沒有使用上的問題。


? Which stylesheet format would you like to use?
  CSS
SCSS   [ https://sass-lang.com/documentation/syntax#scss                ]
  Sass   [ https://sass-lang.com/documentation/syntax#the-indented-syntax ]
  Less   [ http://lesscss.org                                             ]
  Stylus [ http://stylus-lang.com                                         ]
筆者是使用Scss。為了可以比較簡單的客製bootstrap4。

接著就會建立一堆檔案,跟安裝angular會用到的node_modules。時間很長。

CREATE angularTest/README.md (1028 bytes)
整個project的解說檔。其他的開發者拿到這個程式包的時候第一個會看的檔案。請自行撰寫。
CREATE angularTest/.editorconfig (246 bytes)
編輯器的設定。使用支援這個檔案的編輯器的話就會套用。包含了文字編碼的指定,括號階層的表示方法
CREATE angularTest/.gitignore (631 bytes)
記載git要忽略的檔案/目錄列表。像是編譯的cache檔案,系統檔案,ide編輯器的設定等等
CREATE angularTest/angular.json (3695 bytes)
angular的設定檔。為重要檔案。記載各種ng指令的設定。
  • build:編譯project的時候所使用的設定。
  • serve:執行內建web server的時候所使用的設定。(內建web server,可以快速開發。因為只要修改程式碼就會自動編譯並觸發瀏覽器重新讀取。)
  • test:執行測試模組。進入點是test.ts。angular的測試模組是使用karma。會自動啟動瀏覽器,經由瀏覽器render。測試規則寫在*.spec.ts檔案。ui可以使用xpath selector取得render之後的資料。
  • lint:找出語法錯誤。雖然編譯的時候就會提醒嚴重到無法編譯的錯誤,要是有嚴謹的撰寫規範的話,
  • extract-i18n:輸出翻譯檔案。會把所有依照多國語系的格式,撰寫文字的呈現方式,輸出為「messages.xlf」檔案。
  • e2e:End to End test。可以想成是「系統整合測試」。使用的模組是protractor。這邊就不深入討論。
CREATE angularTest/package.json (1289 bytes)

node.js的模組設定檔。記載有使用到的模組。
應該不需要手動修改,要增加模組的話就直接使用npm做安裝即可。

CREATE angularTest/tsconfig.json (489 bytes)
typescript的編譯設定檔。目前沒動到。

CREATE angularTest/tslint.json (1953 bytes)
typescript的語法檢查設定檔。目前沒動到。

CREATE angularTest/browserslist (429 bytes)
似乎是輸出js/css的時候會根據瀏覽器的相容性做修改。目前沒動到。

CREATE angularTest/karma.conf.js (1023 bytes)
karma的設定,目前沒動到。預設是使用Chrome瀏覽器做測試。

CREATE angularTest/tsconfig.app.json (210 bytes)
CREATE angularTest/tsconfig.spec.json (270 bytes)
這兩個目前沒動到。

CREATE angularTest/src/favicon.ico (948 bytes)
在瀏覽器的Tab或是網址列前方顯示的小圖。依需求替換。或是修改index.html的<link rel="icon">的tag。

CREATE angularTest/src/index.html (297 bytes)
網頁的進入點。其中<app-root></app-root>是angular的進入點。

CREATE angularTest/src/main.ts (372 bytes)
angular的進入點。不需修改。
裡面做了幾件事情:
  • 導入app.module.js。也就是angular的root模組。
  • 是否使用產品模式。差異:編譯時不會注入除錯資訊,編譯完成的檔案size會小很多。(至少50%的差異)

CREATE angularTest/src/polyfills.ts (2835 bytes)
polyfill的設定。若是目標的支援瀏覽器有IE10跟IE11這種比較舊的,本檔內容有列出一些針對瀏覽器的部分需要啟動。
本文觸及的部分不需修改。

CREATE angularTest/src/styles.scss (80 bytes)
整個project的全域css定義。會在這邊導入bootstrap跟material design。

CREATE angularTest/src/test.ts (753 bytes)
karma測試的定義。裡面描述的動作是「把所有此目錄之下的"*.spec.ts"導入並執行測試」。不需修改。

CREATE angularTest/src/assets/.gitkeep (0 bytes)
git相關檔案。不需修改。

CREATE angularTest/src/environments/environment.prod.ts (51 bytes)
使用參數ng build --prod 編譯angular的時候所使用的環境變數檔案。
CREATE angularTest/src/environments/environment.ts (662 bytes)
使用參數ng build 編譯angular的時候所使用的環境變數檔案。
這兩個檔案是靠ng build的參數「--prod」做切換。有加參數時使用environment.prod.ts,沒加這個參數的時候使用environment.ts檔案。
切換的定義方式請參考angular.json的這個區塊:
"configurations": {
"production": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],

CREATE angularTest/src/app/app-routing.module.ts (246 bytes)
定義angular的url routing的運作方式。之後會解說。

CREATE angularTest/src/app/app.module.ts (393 bytes)
算是angular的進入點:根模組。

CREATE angularTest/src/app/app.component.scss (0 bytes)
根模組的scss檔案。這個模組專有的css定義請寫在這裡。
angular的css是寫在每一個component或是模組內。
影響範圍是以樹狀的方式向下作用。同一階的不會互相作用。
假設以下狀態:(被導入的模組就被掛在下一層)
a.module
a.scss
  ---b.module
  ---b.scss
  ---c.component
  ---c.scss
寫在a.scss的定義,會影響到b跟c。寫在b的不會影響到a跟c。寫在c的不會影響到a跟b。


CREATE angularTest/src/app/app.component.html (25755 bytes)
根模組的component的html檔案。雖然模組裡面不一定要有component...
angular init的預設會給一個測試頁。此檔案也就是測試頁的html內文。

CREATE angularTest/src/app/app.component.spec.ts (1074 bytes)
根模組的測試規格。angular init的預設會給一個測試頁,所以這個檔案裡面所描述的測試規格也就是測試頁的規格:「必須要有title,值為"angularTest"」。也可以藉由觀察此檔案來了解如何撰寫karma的測試碼。

CREATE angularTest/src/app/app.component.ts (216 bytes)
根模組的component的typeScript檔案。雖然模組裡面不一定要有component...
angular init的預設會給一個測試頁。此檔案也就是測試頁會用到的typeScript。


CREATE angularTest/e2e/protractor.conf.js (808 bytes)
protractor是整合測試會使用的模組,具有操作網頁的顯示物件的能力。

 
CREATE angularTest/e2e/tsconfig.json (214 bytes) 
protractor所使用的tsconfig。會繼承project根目錄的tsconfig.json。

 
CREATE angularTest/e2e/src/app.e2e-spec.ts (644 bytes) 
protractor的測試規格。可以藉由觀察此檔案來了解如何撰寫protractor的測試碼。
 
 
 
CREATE angularTest/e2e/src/app.po.ts (301 bytes)
protractor的class定義。可以藉由觀察此檔案來了解如何撰寫protractor的測試class。
通常是如何取用頁面上所顯示的資料。
 
 
 
這樣就建立完成新的project了。檔案很多,有個印象就好。
 
 

測試/執行剛建立的project

撰寫程式碼的第一步,通常是先確定編譯動作不是正常,與編譯出來的結果是不是可以執行。也就是所謂的「Hello world」。

angular不只是給你「Hello World」,給你的是Hello Page。

進入之前建立的project的目錄(此目錄定義為project 根目錄,之後統稱為根目錄),執行「ng serve」,angular就會開始編譯:

編譯完之後,就會提示可以使用瀏覽器連線「 http://localhost:4200 」。
照做之後就會看到這個樣式的網頁:

代表angular自動建立的開發測試環境是正常運作的。
在這個環境下,若是修改程式碼(html / css / javascript),會發現編譯動作會自動執行,網頁也會自動重新整理。對於ui的設計與調整來說,真的是十分方便。
 
要停止測試環境的運作,直接在命令列按下「Ctrl+C」即可。
 
 
 
 

了解angular設計架構


index.html / styles.scss

既然angular給了Hello Page,就來了解頁面的架構吧。先看看跟angular比較無關的html基本要素:index.html / styles.scss。

打開index.html:
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>AngularTest</title>
  <base href="/">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
  <app-root></app-root>
</body>
</html>


很明顯的<app-root></app-root>是html注入的進入點。
index.html可以說是幾乎沒有要改的地方。
要追加不是npm管理的其他javascript模組(像是facebook sdk),或是meta data的話,就必須加在此檔案內。可以使用npm安裝跟管理的模組,後面會提到如何在angular的架構下使用。


angular編譯


有注意到,index.html裡面並沒有使用javascript跟導入css的地方?
因為angular在編譯的時候才會修改index.html,做導入的動作。
我們來看看編譯過的index.html,順便了解angular編譯的方式。
 
在根目錄執行「ng build」
ng build有許多參數可以使用,在此列出幾個筆者有用到的好用參數。
  • --prod 
    設定編譯為產品模式。此模式不會有任何的除錯參考訊息,檔案也會最小化,加上擾亂程式碼的方法。會大幅加快網頁的載入,開放給他人使用的產品都建議使用此模式。
  • --base-href  
    修改index.html的 <base href="/"> tag。若是編譯出來的頁面,在要放置檔案的http server,並不是放置在http的根目錄,藉由修改base href的相對路徑去取得其他的導入檔案(例如css / icon /圖檔)將會非常方便。
    假設目標的http server,編譯好的網頁的路徑是指定為「http://localhost:4200/test」,就加上參數 --base-href=/test/
  • --output-hashing  none 
    指定輸出的script檔案是否命名為亂碼。angular的編譯,預設是每次編譯出來的檔案名都會不一樣。若是index.html必須使用其它的render方式產生,例如ejs,這樣的話,script的名稱持續變化,使用上就得自行修改導入的檔案名,會比較麻煩。加上此參數,輸出檔案的命名就不會變動。
  • --extract-css false  
    angular預設對於「styles.scss」檔案的處理:會在index.html追加一行
    <link rel="stylesheet" href="styles.css">
    來導入「styles.scss」(全域都會用到的css定義)。

    此參數的作用:指定不輸出「styles.css」檔案,改用js檔案注入的方式。將會產生「styles-es5.js」跟「styles-es2015.js」。還沒有發現使用上的差異。
  • --output-path=<project的相對路徑> 這個參數可以指定編譯完成的檔案要放在哪裡。使用相對路徑處理,路徑起點是project目錄。
 
若是沒有指定編譯輸出的目標路徑,編譯之後的檔案會放在project的dist目錄下面。
輸出結果的檔案列表:

這些檔案就可以直接放到web server的指定目錄,使用瀏覽器存取index.html顯示網頁。
來看看編譯過的index.html:
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>AngularTest</title>
  <base href="/">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
<link rel="stylesheet" href="styles.css"></head>
<body>
  <app-root></app-root>
<script src="runtime-es2015.js" type="module"></script><script src="runtime-es5.js" nomodule defer></script><script src="polyfills-es5.js" nomodule defer></script><script src="polyfills-es2015.js" type="module"></script><script src="main-es2015.js" type="module"></script><script src="main-es5.js" nomodule defer></script></body>
</html>


紅色是標記出來被編譯動作修改過的部分。可以看得出來,這是沒有使用--extract-css false」參數的結果。因為有導入styles.css。
在body tag結束之前注入了好幾個js檔案,這些檔案就是angular編譯後的結果。
 
 

styles.css基本

看看未編譯的styles.scss:
/* You can add global styles to this file, and also import other style files */


只有一行註解。我們將會在此檔案導入bootstrap4。
執行「npm install bootstrap」安裝bootstrap在project裡面。這樣angular編譯的時候才能取到bootstrap的檔案。要不要使用npm 的save參數就隨意。若是採用的分發方式是在server上面編譯angular,那就建議save。

導入bootstrap之後的styles.scss:
/* You can add global styles to this file, and also import other style files */
//在此舉例如何修改bootstrap的定義。
//注意:修改bootstrap的定義的動作,必須指定在「import bootstrap」之前。之後做的修改都會無效。

//假設我們覺得bootstrap原本的spacer定義不夠細緻,想要多一點選擇。
$spacer: 1rem !default; //重新指定一次基本值。就算沒有更動基本值... 不這樣做的話,scss編譯會直接失敗。
$spacers: (
  0: 0,
  1: $spacer * .25,
  2: $spacer * .5,
  3: $spacer * .75,
  4: $spacer,
  5: $spacer * 1.25,
  6: $spacer * 1.5,
  7: $spacer * 1.75,
  8: $spacer * 2,
  9: $spacer * 2.25,
  10: $spacer * 2.5,
  11: $spacer * 2.75,
  12: $spacer * 3,
) ;

 
//假設覺得bootstrap原本的按鈕圓弧半徑不夠圓,想修改按鈕的圓弧半徑...
$btn-border-radius: 10px;  //$btn-border-radius-base 是 bootstrap 3的命名. bootstrap 4 更名為 「$btn-border-radius」


@import "~bootstrap/scss/bootstrap";  //進入bootstrap的世界
 
存檔,試著編譯。編譯之後將會發現編譯出來的styles.css會成為一百多K的大小。
之後在html文檔或是後面提到的ui元件裡面就可以使用bootstrap的定義了。
 
 
 

app.module.ts

main.ts是angular的進入點。主要動作:
 
import { AppModule } from './app/app.module';
platformBrowserDynamic().bootstrapModule(AppModule)
  .catch(err => console.error(err));

import語法:大括號內的是javascript class name。 from後面的是路徑跟檔案名。
基本上是使用相對路徑,根目錄就是所建立的project的根目錄。
 
假設現在我們要導入的是非angular系統提供,但可以用npm安裝並管理的模組(通稱為第三方模組)。在此以moment模組舉例,可以在project根目錄使用「npm install moment --save」安裝模組。
然後在會用到這個模組的檔案裡面,使用以下語法就可以使用該模組:
import * as moment from 'moment';
同樣的,被import的模組需要使用export指令定義可以export的東西。
之後就會看到export語法。被定義export的名字才能被import使用。


platformBrowserDynamic是angular對於不同瀏覽器跟環境的運作方式的一個處理器。有興趣可以參考官網介紹。這邊就不做探討。
 
 
AppModule是angular架構的「根模組」。
angular的環境,必須要有一個「根模組」,再從這個「根模組」去載入其他的模組跟元件。

「模組」是一或多個「元件」與「模組」的集合。元件分為下列幾種:
  • html的ui元件:命名為「component」。通常包含以元件名為主檔名的html基本元素「css / html (命名為template) / ts(定義元件的script相關操作)」的一組檔案。元件的大小就看架構的設計方式。元件內當然也可以引用元件,但是「導入」(讓此元件可以看到想引用的元件的相關定義)的動作必須定義在「模組」。
  • directive:意指存在於<tag>內的修飾詞。
  • service:跟ui元件的區別,主要是「ui的有無」,「資料需不需要共用」。

「模組」的主要目的
  • 定義導入的模組跟元件
  • 定義首要顯示的元件(只有根模組必須指定)
  • 決定「後載注入」(angular的重要功能之一)的定義域


 

根模組的定義解析

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

BrowserModule:angular跟瀏覽器介接的套件。根模組必須導入。也只需要在根模組導入即可。

NgModule:要使用「@NgModule」語法的地方就需要導入。備註:「@」是TypeScript的decorator語法,跟import from的「@angular」這個@是angular模組名稱的一部分。意義不同。
 
AppRoutingModule:在project創建的時候,有勾選要使用Angular routing功能,建立出來的app.module就會自動建立這個「app-routing.module」。之後會提到routing的寫法跟後載注入的使用法。
 
AppComponent:自定義的ui元件。畢竟是根模組,至少要有個ui元件放些html顯示吧?






@NgModule()的解析

最前面的幾個import指令,只是讓app.module這個檔案可以取得其他檔案的資料定義。
要注意的是,有些import有先後順序的關係。像是browser module一定要放在最前面。http module要擺browser module後面。有主從關係的,上一層也要擺前面。
有用到angular特定的定義,像是@NgModule / @Component ,必須import 對應的angular core module。
  例:「import { Component } from '@angular/core';」
     「import { NgModule } from '@angular/core';」
     「import { Injectable } from '@angular/core';」



要讓angular知道有個「angular模組」的存在,必須使用@NgModule()函式,給出一個模組定義的物件資料。裡面包含資訊:
  • declarations:所有在此模組內會用到的ui元件。
    注意:需包含面下面提到的entryComponents跟bootstrap裡面所定義的所有元件。

    注意:component/pipes/directives只能定義(加在module的declarations裡面)在一個模組內。若同時定義在多個模組內,compiler會錯誤。若是該元件要給多個模組使用,建議定義在一個「共享模組」內。(下面會提到「共享模組」的概念)

  • imports:所有在此模組內會用到的其他模組。
    注意:定義在這裏的其他模組,裡面的ui元件,此模組都可以取用,不需在declarations再次定義。

    注意:各個module不要互相寫imports,會造成循環參考。

  • entryComponents: 必須在此模組準備好的時候存在的元件。主要目的:若這個元件並沒有在html內使用<tag>定義的話(像是用js即時產生並注入的html),就必須列在這裡讓angular準備好。因為angular注入的方式是靠html tag分辨。

    有兩個地方會執行這樣的動作:
    • 根模組的bootstrap宣告(模組啟動的時候強制載入,讓index.html可以直接代換) 
    • route宣告(在route切換的時候會直接載入該component,把「html template的<router-outlet/>」代換掉)。

  • bootstrap:強制最優先處理的元件。寫在這裡的元件,在angular做完初始化之後就會立即注入html內,將會馬上顯示。此元件也將會是entryComponents。最常見的應用就是先給一個網頁啟動的讀取畫面。要注意的是,指定為bootstrap的所有元件,在index.html裡面必須有<tag>定義,否則在執行階段會因為找不到注入點而出錯。

  • exports:只有宣告在exports裡面的component/pipes/directives,其他的模組導入此模組之後才可以使用。
    也可以export該模組import的其他模組,這樣有用到此模組的模組,就不用再寫此模組import過的模組。

    方便的應用:
    • 可以定義一個共享模組,裡面import並export angular的common module。其他導入此共享module的module,就不需在imports裡面寫上 angular common module。angular Material模組很大又很多。只想挑幾個元件來用,也可以寫在這個共享模組裡面。

  • providers:寫在providers裡面的service,會動態產生service object。定義在同一個模組之內的component/pipes/directives都可以直接使用。而且會比component先建立,component的constructor可以直接存取service。(關於component如何存取service,請參考之後的解說)

為什麼要區分entryComponents / bootstrap:為了要tree-shaking。(一種在同一階段只要取得最小資源的資料分類方式。可以讓app在一開始的時候快速的出現一個loading頁面,然後等待資料讀完。)
 
最後就是export 的寫法。必須有,一個NgModule / Component定義只能有一個class定義。class定義必須寫在NgModule / Component定義的下方。

想在一個檔案面寫兩個Component,可以這樣寫:
@Component({..............})
export class AppModule2 {............}
 
@Component({...............})
export class AppModule {.............. }






NgComponent解析

 有了module,接著就來看看project自動產生的component吧。這個部分因為原始碼很長,只會挑出筆者覺得需要介紹的部分。
 
 
打開app.component.ts:
import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent {
  title = 'angularTest';
}
 
import Component是必須的。要讓angular知道有個「angular元件」的存在,必須使用@Component()函式,給出一個模組定義的物件資料。裡面包含資訊:
  • selector:定義使用的tag name。請注意index.html裡面標記相同顏色的地方。在html裡面有以selector為名的tag,整個tag會以此component所處理過的內容代換。
  • templateUrl:要注入的html語法的檔案路徑
  • template:html語法也可以直接以字串的方式寫在這裡。若是templateUrl同時存在的話,只使用templateUrl的定義
  • styleUrls:css定義檔的位置。注意:元件內的css定義,不會影響其他元件。
  • styles:css也可以直接以字串的方式寫在這裡。
  • providers:寫在providers裡面的service,會動態產生service object。component的constructor可以直接存取service。(關於component如何存取service,請參考之後的解說)

接著依然是export class的動作。不過多了一個參數:title。大括號內,就是定義component class的所有參數跟動作的地方。

title這個參數使用到的地方,在app.component.html裡面的這一行:
「<span>{{ title }} app is running!</span>」
因為定義了「title = 'angularTest'」,之前使用瀏覽器連線「 http://localhost:4200 」的動作,就會看到「angularTest app is running!」的文字。代表angular的參數代換功能有正常運作。


大致上了解angular的檔案跟系統架構之後,就可以開始進入比較細節的部分了。
 
補充一些基本範例沒提到的component相關的理解:
  • component不需宣告導入其他的component。導入其他component的動作要在modules的宣告裡面處理。
  • component加上module宣告即可成為module。雖然一個module就可以包含任何component或是任何module,建議依功能或是ui區塊來做module的分隔。
  • component因為包含ui(html template)跟css,建議用在有ui的地方。純data的操作建議設計為service。
  • component class要使用其他的ui元件,通常是在template內使用該元件的tag指定。那要使用service呢?
    必須,也只能在class constructor的參數輸入區塊定義。 若是在constructor結束之後還會用到service,那就要使用class內的local變數自行儲存。

    例:
    constructor(private logger: Logger) { this.logger = logger; }
    這樣component class內的其他動作才能使用logger這個元件。

  • component class的constructor可以access到的service範圍:任何在這個階段已經被定義過的元件。
    以前面提到的的AppModule舉例,就是根模組裡面定義的declarations / imports陣列裡面的所有module(也就是BrowserModule, AppRoutingModule)裡面的service。

    若是在constructor的輸入參數裡面寫了class名稱,卻沒有module import / local import之類的,編譯會直接錯誤。就依狀況去撰寫定義處理。constructor的輸入參數沒有先後順序。angular會自動解析該給哪個元件。
 
 
 

angular template語法介紹


  • #objName:angular的命名是「Template reference variable」。
    定義在html tag內,objName就會成為該html tag的代表物件。

    例:定義一個<input type="text" #objName /> ,template內的其他地方就可以用{{objName.value}}來取得input的值。就算是在template比較後面的地方宣告物件,前面的陳述式也是可取用此物件,沒有宣告先後順序的問題。也因為作用區域是整個template,在同一個template內不要重複定義。

    在component的class定義裡面要取得此參數,可以使用下面這個方法取得: @ViewChild('objName') obj;
    或是靠作用區域內的指定物件event傳入。像是使用event binding: <button (click)="btnClick(objName.value)">


    需要注意的是參數的代表意義:(建議參考angular的範例程式碼
    • 若是定義在component的tag裡面,代表整個component class。
    • 若是定義在html 標準tag裡面,代表的是這個html tag。
      例:<input type="text" #objName />,在其他地方列印objName,會發現輸出是「[object HTMLInputElement]」。若進一步在input裡面隨便輸入一些文字,列印出來的會是「{"value": "(輸入的文字)"}」
    • 若是有指定參考目標, angular會去搜尋相關的定義。
      例:<form #objName="ngForm"></form>,objName就會是整個form的物件。

      覺得"ngForm"看似找不到產生點...?
      這是angular的自動處理。有定義<form>這個html 標準tag的話,angular就會產生一個物件「ngForm」代表這個form。
      做了「#objName="ngForm"」這樣的指定之後,js或是template的其他部份才能用objName這個參數來對整個form的資料做存取。



  • {{object?.property}}: interpolation(套用資料)。有一部份的js語法可用,也可以call function。

    這是單向的資料更新。參數的值被之後提到的雙向綁定動作,或是在component class內所定義的動作內被變更,這個大括號地方的資料就會自動更新。若是做的事情是call function,則只會動作一次,不會隨資料變動而自動更新。也可以用參數+function的方式變通。不過還是建議直接用一個參數去做。

    object?這樣的「參數名加上問號」的寫法,可以保護在使用物件的property的時候,若是物件為null或是undefined,angular因為物件存取失敗而沒有處理完資料,讓畫面產生資料錯誤。

    注意:interpolation的文字,若是含有html tag,會把html tag當做文字處理。這是為了防止資料注入漏洞。

  • [...]: html tag property/attribute binding。只能寫在html tag裡面。
    binding的表示式,只能有讀取跟運算動作,不可以有任何參數設定的操作。
    例:<img [src]="itemImageUrl"> 代表把src這個property設為itemImageUrl的參數值。

    若參數值的類型是boolean,就代表該tag property是否會出現在tag內。
    例:<div [hidden]="isHidden"/> ,若是component內的isHidden參數為true,div tag的表現會是<div hidden />

    若是要使用attribute binding的話,需用以下格式:
    [attr.attributename]="angular expression"
    常見的使用方式是綁定div class的其中一個值。
    例:<div class="btn active" >
    此時就可以使用<div class="btn" [class.active] = "isBtnCanActive"> 這樣的寫法,來讓class這個property裡面動態增減active屬性。

    一般建議不能用property binding的地方才使用attribute binding。例如aria跟svg這兩個tag,都只有attribute。另外,attribute命名格式不符合javascript property name的限制的話,也不可使用attribute binding。像是css定義常常會使用「-」號,就不適合使用attribute binding。硬要用的話就是不會動...

    基本上,property都建議全部小寫。不過有些property會分別大小寫,就必須要考慮大小寫。例如<td>的colSpan。property的格式請參考html5的文件。

    property binding / interpolation的差異:
    <img src="{{itemImageUrl}}">  <img [src]="itemImageUrl"> 其實是做一樣的事情。不過要設定非字串的資料的話(像是true/false, attribute的增減)就得用property binding了。

    property binding是方便好用的component之間的溝通方式。
    component內部定義的參數,若是需要靠template來做初始設定值,可以在參數加上@Input()修飾字,就可以在template指定初始值。請參考文件範例

  • html tag property/attribute biding續探:
    property binding,傳入值除了基本的字串之外,還可以直接給object或是array of string。此法主要用在class這個property,又稱為class binding。

    例: <div class="btn btn-red" />  可以寫為
    <div [class]="params"> 其中params的定義: {"btn" : true, "btn-red" : true, "btn-blue" : false}

    例: <div class="btn btn-red" />  可以寫為
    <div [class]="params"> 其中params的定義: ["btn","btn-red"]

  • style binding: 跟class binding非常接近的方式。傳入值除了基本的字串之外,可以直接給object。(array of string似乎是不行。)


    例:
    <div style="width : 100%;"> -> <div [style.width] = "100%"> (attribute binding)
    例:
    <div style="width : 100%;"> -> <div [style] = "params">
    其中params的定義: {"width" : "100%"}


    style binding衝突的套用強弱順序(由強而弱):
    1.style的attribute binding 2.property binding 3. hardcode

    style binding衝突的元件套用強弱順序(由強而弱):
    1. template  2.directive  3.component

    style fallback:當時最強的設定值為undefined的話,會套用次強的設定。但是最強的設定值為null的話,該style的指定會被清除。


  • ():event binding。
    event的名字會跟javascript本身的名字不同,請注意。像是js的onClick會寫成(click),<form (ngSubmit)="...">就是js form的submit event。

    $event為event binding特有的內建參數,可以直接傳入要call的function。例:<button (click)="onSave($event)"/>

    若event是由angular以外的介面發起的(例如browser),那$event就會是javascript的基本格式,會有$event.target.value property。


    有了event binding,html template裡面的元件就具備了傳遞訊息的能力。
    下一步就是:若是我們想要在元件之間互相傳遞訊息要如何做?
    angular提出的方案是event emitter。
    event emitter可以說是RxJS的observer,使用方式當然也就是RxJS的observer / subscribe。在component class裡面定義event emitter之後,要被通知的元件或是service,使用subscribe的方式加入。
    若是改用html template來做,在定義event emitter之後,其他的元件可以使用event binging的方式指定,不需寫subscribe的動作。是angular幫忙做的簡化。

    例:
    假設有個app-root元件,想要收到addItemOutput這個元件的更動event:
    <app-root><addItemOutput (newItemEvent)="receiveNewItem($event)" /></app-root>

    newItemEvent是addItemOutput這個component定義的event emitter,receiveNewItem是app-root component裡面所定義的接收動作。這樣就可以收到addItemOutput丟出來的資料。


  • 覺得上面提到的元件之間的資料傳遞方式還是很麻煩?angular提供的超方便手段就是...
    [(property)]:雙向binding。結合property binding(input)跟event binding(output)。連寫event emitter都不需要。

    例:<app-sizer [(size)]="fontSizePx"></app-sizer>,fontSizePx變動,也會更動size的值。反過來也一樣,size變動,就會去更動fontSizePx。




  • directives: 在html template的tag內的任何地方都可以導入。通常是用在css的定義更換。
    component是directive的其中一種。
    directives也可以擁有provider(service)。

    selector的定義方式:可以做比component更細微的條件判定。請參考文件
    例:
    • tagname1,tagname2: 在tag內的任何地方標記都有效。用逗號的話為多重比對,只要符合其中一個tag就會有效
    • .class:只有class name有效(也就是在<div class="...">裡面標記時會作用)
    • [attribute]: 只有在attribute裡面標記時有效

    還可以結合判斷條件。例: input[type=text] 就是只有在 <input type="text"/> tag內定義才會作用


     
  • 一些重要的directive:
    • ngClass / ngStyles這兩個directive建議不要使用。之後可能會廢除。(已經有class / style binding可用了)

    • ngModel:代表該html tag的物件。只能用在<form>跟<input>tag。會依照所套用的tag而有不同的資料。像是text input,得到的是輸入值。

      要進一步使用ngModel的雙向binding功能,必須在module裡面導入FormsModule。

      使用法簡介: <input [(ngModel)] = "objName"> 這樣寫,代表把這個input的輸入值跟objName這個參數做雙向綁定。這樣在component 的class內定義objName,就可以取這個input的值,也可以設定這個input的值。


      若是在html template的其他地方要取得這個input相關的其他參數,像是輸入值有沒有被改過,假如設定這個input是必填,那現在的狀況是不是合於需求,就需要使用#ctrl="ngModel"的寫法來取得這個input,除了輸入值以外的相關參數值。(「ctrl」是自行命名)

      還可以在資料有變化的時候,執行一些動作: <input [(ngModel)] = "objName" (ngModelChange)="someWhatProcess($event)" >
      解說:
      「$event」是javascript的event觸發的時候會提供的資料,為固定保留字。
      ngModelChange:ngModel相關的指令。有任何的資料更動都會發出這個event。

      另外,不使用ngModel的雙向binding寫法:
      <input [value]="currentItem.name" (input)= "currentItem.name = $event.target.value" id="without">
      解說:
      (input):該input的input event。
      「$event」是javascript的event觸發的時候會提供的資料,為固定保留字。
       
    • *ngIf: 若是true的話,使用了這個定義的<tag></tag>的內文都會被注入html裡面。若是false,內文就不會被注入。跟使用hidden directive相比,若是內文很大又重,就會有明顯效率上的差異。

    • *ngFor:在這個tag裡面的所有內容都會被重複產生,依照迴圈的執行次數。

    • ngSwitch:在這個tag內,只有符合「*ngSwitchCase」條件的tag會被注入。
      例:<div [ngSwitch]="currentItem.feature">
        <app-stout-item    *ngSwitchCase="'stout'" ></app-stout-item>
        <app-device-item   *ngSwitchCase="'slim'" ></app-device-item>
        <app-lost-item     *ngSwitchCase="'vintage'" ></app-lost-item>
        <app-best-item     *ngSwitchCase="'bright'"></app-best-item>
      <!-- . . . -->
        <app-unknown-item  *ngSwitchDefault ></app-unknown-item>
      </div>

      注意:ngSwitch不要加「*」號。

    為什麼有的directive必須加「*」號:
    在directive前面加上*號的話會成為語法糖。作用是轉成<ng-template >...</ng-template>的格式。請參考文件
    由上面的interpolation / databinding的定義來看,angular的設計是直譯語言。ngFor是迴圈的功能,只靠直譯的取物件/陣列,要怎麼做到其他的語言所使用的特定語法?
    angular提出的方案是再包一層,這樣才能做原本的語法的指令擴展。

    以*ngFor為例,「let hero of heroes」,「let」跟「of」都是ngFor的專用語法。(let雖然跟javascript的參數宣告指令同名,但是寫成字串之後,要怎麼翻譯就是angular的事了)
    而*ngIf跟*ngSwitchCase的星號,則是為了達到不注入整個內文的功能。希望有助於各位的理解。

    angular必須這樣寫的指令是以下這幾個,只能死記:
    • *ngIf
    • *ngFor
    • *ngSwitchCase




  • pipes: 在html template裡面使用{{ parameter | pipename }}的方式運作。只有interpolation可以使用pipe。



angular service

適合被定義為service的使用情況:
  • 沒有ui
  • 需要被其他元件共用
  • 同樣的資料格式跟存取方式,不同的資料來源
例:http的存取,使用者資訊,購物車,商品資訊...

一個簡單的service的定義:
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Injectable({
  providedIn: 'root'
})

export class MyService {

  constructor(private http: HttpClient) {
    this.http = http;
  }
 
  query(): Observable<any> // 給使用者執行的動作。會回傳一個http的觀察對象,讓使用者執行subscribe,就可以執行http存取該網頁。
  {
    let queryHttp$: Observable<any>;  //"$"是RxJS的observable的習慣修飾字
    queryHttp$ = this.http.get("http://www.123.xyz");
    return queryHttp$;
  }
}

重點就是Injectable的定義。沒有這個定義就無法成為service。裡面的「providedIn」參數,目的是指定這個server的作用區域。有下列幾個值可選:
  • root:只有一個,而且建立在root。會在angular bootstrap階段就被建立,所有元件都可以存取。
  • 「platform」跟「any」這兩個選項沒使用過,不確定作用方式。
  • 不給參數:有使用到(被定義在元件或是模組的providers參數內)的地方,都會建立一個獨立的service物件。
 
關於service的作用區域:
  • 在component的providers指定,該service就只能讓component存取(建議定義為該component專用service)。定義在module內,該模組包含進來的所有component都可以存取。所以component需要獨立service資料的話,就把service定義在component的providers裡面。

  • 在根模組(或是使用{providedIn: 'root'}參數)定義的service,才能被所有的模組存取(包含之後會提到的「後載模組」)
    定義在root module跟root component的差別:只有定義在root module的service可以被後載module看到。

  • service物件的數目:
    • 在Injectable定義「providedIn」參數:會成為「單一service」。
    • 沒有在Injectable定義「providedIn」參數,service物件的數目就由provider的定義來決定。
      若是定義在component,該component產生幾個,service就有幾個。
      若是定義在module,該module產生幾個,service就有幾個。

  • 需要同時access上層跟component內定義的同一名稱的service,後面的Dependency Injection的章節會提到修飾字,使用一個@Self(), 一個@SkipSelf()的方法即可分別取得本身跟非本身(也就是會提供上層)的service元件。

  • 注意:module的imports定義的多個module都定義了同一個service,會取得表列的第一個模組的service物件。


 
 

 router與後載(lazy-load)模組

 
有了template,模組,元件,service等等定義,已經足夠寫一個具有互動能力的網頁。隨著頁面跟功能的擴展,資料量會越來越多。一次讀取整個網頁資料並呈現,所需要的時間會越來越長。使用者就會等到不耐煩...
 
所以現在的網頁設計主流是先給你一個「歡迎頁面」,然後在後面用力的讀資料...
angular對於這樣的設計有還不錯的支援度。只是使用方式筆者覺得有點彆扭,大部分是得死記的做法,所以寫下來...
 
在前面創建project的時候,有這個選項:
 ? Would you like to add Angular routing? Yes
這樣選擇,就會多創建一個檔案:app-routing.module.ts
在app.module.ts裡面也可以觀察到import的動作。
 
第一步:在根元件的template放上這個tag:<router-outlet></router-outlet>
才會顯示route處理的結果。
觀察已經創建好的app.component.html,會發現最後一行已經幫忙加好了。
 
直接來看app-routing.module的寫法,並補上需要的設計:
 
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { firstComponent } from './app.firstcomponent';  //假設有一個元件要用route的方式處理,不需後載。

const routes: Routes = [
  { path: 'home', component: firstComponent },
  { path: 'list', loadChildren: () => import('./listpage/listpage.module').then(m => m.ListpageModule) },
  { path: '**',   redirectTo: 'home' },
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }
import的部分就請照著寫。主要是routes這個陣列。
每一個陣列的值都是一條route的規則。先後順序會是比對的順序。
 
 
第一個規則: 「path:'home'」,代表定義的url path是「 http://<server root>/home」。只要url被設定為這個狀況,就會做這個規則的事情。
定義的動作: 「component: firstComponent」,會注入firstComponent的處理後的template。

第二個規則: 「path:'list'」,代表定義的url path是「 http://<server root>/list」。
定義的動作: loadChildren: () => import('./listpage/listpage.module').then(m => m.ListpageModule) 這樣的寫法,會去讀取listpage.module的相關檔案,進行處理,然後注入template。 注意:「ListpageModule」會是module檔案裡面的「export class ListpageModule」這行的class name。
這個寫法也是固定的,就照寫。模組名跟import的路徑就請自行代換

第三個規則: 「path:'**'」 代表wildcard,就是所有的狀況都會符合。所以必須寫在最後面。
定義的動作: 「redirectTo: 'home'」,代表不管怎樣的url,都會修改url為「 http://<server root>/home」。

小技巧:根據angular的文件,redirectTo的目標路徑需要在前面加上"/"。以上面的例子,會寫成「redirectTo: '/home'」。不過這樣寫,會把url參數清除。(?a=b&c=d等等)
要保留url參數,在前面不要加上"/"即可。
 
 
 
NgModule的寫法也請直接照抄。
「RouterModule.forRoot(routes)」用在根router。若是需要有階層的後載routes,其他階層都使用
「RouterModule.forChild(routes)」的寫法。
 
如何使用ng指令,在route的定義之下新增模組:
ng generate module <模組名稱> --routing --module <要追加route定義的上層模組>
使用 「--routing」參數,建立的新模組就會擁有routing的定義。此模組之下需要多頁面的話,就需要這樣定義。
使用「--module <要追加route定義的上層模組>」參數,就會修改指定的上層模組的<上層模組名稱>.module.js檔案,追加此模組的定義。不過routing方式還是得自己加在「<上層模組名稱>-routing.module.js」檔案裡面。
 
 
 
 
 

Depedency Injection(DI)

 
筆者覺得angular最難理解的就是DI了。DI主要是解釋在元件/service/directives的constructor宣告輸入參數的時候,angular如何決定要提供的資料物件。
除非需要實作非常複雜的設計, 對於DI可以留下印象就好。常見的應用是建立網頁的測試介面。要測試的資料可以使用檔案當做來源,也可以使用server做資料存取。
參考官網文件  官網的demo寫得很好也夠複雜,建議參考會比較容易理解。

angular的Injector有以下幾種:
  • ElementInjector:component / directives都會使用這個Injector。
  • ModuleInjector: module專用
  • (root)ModuleInjector:又稱為platform Injector。bootstrap module專用。
  • NullInjector:丟出錯誤使用



constructor DI 的規則:
  1.先找自己的ElementInjector
  2.找上一層的ElementInjector,一直找到根
  3.找ModuleInjector,一直找到根
  4.都找不到,丟出錯誤

DI可以指定的搜尋規則:
  • @Host():依照上面的基本規則,搜尋到此停下,不會再往上找。然後依有沒有找到的規則處理。
  • @Self():只允許自己定義的service provider。就是只用DI第1條規則的意思。通常用在component自己定義provider的service物件。
  • @SkipSelf():只找自己以外的service provider。就是直接跳過DI第1條規則的意思。
  • @Optional():照用第1~3條規則,更動DI第4條規則,把丟出錯誤改為回傳null。
providers vs. viewProviders: viewProviders只限於元件內可以存取,providers與允許被下一階的元件存取。請參考官方範例

providers的基本格式:[ClassName, ...]
providers特別指定的格式:[{ provide: Logger, useClass: BetterLogger, useValue: {param1: value1, ...} }, ...] 
參數解說:
  • provide 是service Class。
  • useClass為代換的子Class。
    特別指定的用處:可以定義一個parent service,然後發展幾個子service,擁有同樣的介面但是操作方法不同。
    例:建立一個oauth介面,再建立f社跟G社的存取方式的子service。根據不同的狀況或是元件,使用不同的資料取得方式。
  • useValue:service的初始化參數。
如何使用細部指定格式的provider:
constructor(@Inject(Logger) private logger : Logger) {logger.log(...);}
 
 
 
factory provider: service在建立時必須依靠其他service的資料的時候使用。
跟angular以外的javascript套件互接的時候可用。
注意:所有的angular參數定義,不可以跟有導入的非angular套件的名字重複。(因為會定義衝突)

service初始值的給定也可以用factory的設定法。例:  
    @Injectable({
      providedIn: 'root',
      useFactory: () => new Service('dependency'),
    })

 
如何以serivice的方式導入未經class定義的物件(typescript的interface不算是class)
假設有這樣的一個interface:
export interface AppConfig {
  apiEndpoint: string;
  title: string;
}
  • 乖乖的定義class
  • 直接定義injection token。
    例: export const APP_CONFIG = new InjectionToken<AppConfig>('app.config', 這邊可以放「@Injectable()的參數」);
    注意:'app.config'這個字串是一個獨立的id,不能與其他的InjectionToken重複
  • 如何使用:  constructor(@Inject(APP_CONFIG) private config) {this.title = config.title;}
    (請注意private修飾字的出現位置。不寫的話,編譯會錯誤)




雜記

 
開發環境相關:
  • 若是使用ng serve環境開發,寫了新的module,但是在template裡面寫了directive卻沒反應,建議重跑ng serve。
 
 
 

TypeScript / RxJS相關:
  • clone object:
    let data;
    let newObj: interfacename = {...data};  //ES6的新語法:Spread syntax
  • 前面有提到,object的property不可以有"-"號。不過使用字串指定的方式就可以突破限制。
    例:let topBarClass= { "bg-white": true, "p-4" : true};

 
angular執行 / 觸發條件相關:
  • constructor的參數宣告,強制必須指定private / public才能編譯通過。
  • ngOnInit跟 constructor的執行條件:
    ngOnInit要有被定義在component的import / provider才會執行。
    constructor只要有import就會被執行。但是不要在constructor內執行複雜的動作(像是function call或是非同步動作)
  • service雖然標注「provideIn:'root'」, 也需要至少有一個component的constructor注入才會初始化。
  • ngModule的bootstrap跟declarations是分開處理的。module有用到的component必須宣告。若是component需要bootstrap,還要再加在bootstrap裡面。
  • 若是data binding的對象是一個object,像是使用class / style binding指定一個物件。這時會發現修改物件的一個property的值,ui不會有變化。經測試發現需要重新指定一個新的物件。可以使用spread syntax,後面加上逗點去修改property的值。
  • 如何在angular環境使用第三方模組: (window as any).<模組的export name>();
  • 如何讓非angular環境call angular的動作:使用function bind的方法。
    例:setTimeout(function () { this.someAction(); }.bind(this), 500);
 


css / bootstrap4 相關:
  • bootstrap4的row,可以加上「justify-content-center」,這樣的話col所指定的總grid數小於12,或是沒有col的時候,會自動把這個grid水平置中。
  • <div>內文只有文字的狀態要做文字置中:「text-center」
  • 修改過的bootstrap主參數,要拿掉「!default」文字,要不然不會動。
    例:$spacer: 0.5rem;
  • theme-colors的用法:定義之後,就可以使用btn-loginbutton這樣的寫法來指定該元件的色調。例:
    $theme-colors: (
      "loginbutton": #AC1700,
      "popupbtn": #335397
    );
  • 定義一個完整的按鈕相關動作的顏色:可以使用下面這個方法。
    $lightcolor: lighten(#335397, 10%);

    .btn-light {
      @include button-variant($lightcolor, darken($lightcolor, 7.5%), darken($lightcolor, 10%), lighten($lightcolor,5%), darken($lightcolor, 20%), darken($lightcolor,20%));
    }
    其中,button-variant 參數列表: button-variant(background, border-color, hover:background-color, hover:background-border-color, active:background-color, active:background-border-color)

    lighten跟darken都是bootstrap的語法。

 
angular cdk相關:
  • overlay的backdrop有兩個意義:
    • 加上灰階半透明
    • 點選灰階的部分會自動關閉overlay
  • backdrop跟panel的css define不能放在component裡面。不會套用...
  • bootstrap的fix-top,z-index為1030。angular material overlay的z-index為1000,使用bootstrap的fix-top定義,利用angular material overlay實作全畫面的alert會遮不到topbar。
    建議照抄一個bootstrap的fix-top,修改命名為my-fix-top之類的,修改z-index為小於1000的值。


如何導入多語系翻譯:
  • ng add @angular/localize
  • 依照規格加上i18n attribute
    建議translate id要自己定義,不要使用亂數產生的。這樣會很難管理,而且不確定什麼時候會變動。所以建議使用這樣的格式:
    <element i18n="@@translate_id"></element>
  • 使用 ng extract-i18n --format=json 解出翻譯檔
  • 翻譯過後另存新檔案,避免下次執行extract-i18n被蓋掉
  • angular.json 裡面設定預設語系跟編譯時若是指定語系,需要包進去的其他語系。
    例:

      "i18n": {
        "sourceLocale": "zh-TW",
        "locales": {
          "en": {
            "translation": "src/locales/messages.en.json"
          }
        }
      },

  • angular.json的 architect.build.options裡面要加上localize參數,才會編譯翻譯檔。
    「"localize": true」表示編譯全部i18n 參數有定義的翻譯檔。
    也可以指定只編譯哪些語系的翻譯檔,使用陣列方式指定。
    例:「"localize": ["zh-TW", "en-US"]」

    若是要使用ng serve,要改成指定單一語言(ng serve不支援多語言編譯)。
    語法:「"localize": ["zh-TW"]」
  • 預設語言不能指定翻譯檔案。編譯會出錯。









 
 

0 件のコメント:

コメントを投稿