--- tags: Angular --- # 從零開始的Angular教學 > 課程時間:約1個下午到1周 <style> .hl { background-color: greenyellow; color: maroon; font-weight: bold; } </style> [TOC] --- ## 零、Angular是啥? ![AngularImg](https://angular.io/assets/images/logos/angular/angular.svg) > 圖片來自:[Angular正體中文站](https://angular.tw/) ### 1. 為什麼前端框架? jQuery + Bootstrap不是很好嗎?為啥冒出像是React、Angular、Vue這種東東? 簡單來說,如果你的專案中會<span class="hl">重複使用到一些元件、前端可能需要處理並顯示複雜的後端回傳資料、想要提升使用者體驗</span>。那你最好使用一個前端的框架來幫助你開發。 > 延伸閱讀:[Do We Really Need a Front-end Framework?](https://medium.com/swlh/do-we-really-need-a-front-end-framework-e8c8c3e4df0b) ### 2. 我該用哪個前端框架呢? > 資料來源:[Angular vs React vs Vue: Which Framework to Choose](https://www.codeinwp.com/blog/angular-vs-vue-vs-react/) 前端框架不只一種,所以我們稍微比較一下最紅的三個吧: *(2021/05/03 更新)* | | Angular | React | Vue | | -------------:|:-----------:|:--------------:|:---------------:| | 釋出時間 | 2010 | 2013 | 2014 | | 官網 | angular.io | reactjs.org | vuejs.org | | 大約大小 (KB) | 500 | 100 | 80 | | 目前版本 | 11 | 17.X | 3.0.X | | 使用企業 | Google, Wix | Facebook, Uber | Alibaba, GitLab | 如果我的時間不多,想要學習CP值最高的框架,有推薦的嗎? 我們來看看工作機會吧: | 工作需求 | Angular | React | Vue | | -----------:|:-------:|:-----:|:-----:| | LinkedIn | 72747 | 70963 | 11590 | | Indeed | 15141 | 14595 | 1810 | | SimplyHired | 11357 | 10508 | 1526 | | Dice | 4719 | 3529 | 331 | | AngelList | 2350 | 4383 | 419 | | Hired | 13 | 9 | 0 | | Remote | 1069 | 1136 | 166 | 開發者們最愛的框架? *(2021/05/03 更新)* | Github Repo | Angular | React | Vue | | -------------- |:-------:|:-----:|:-----:| | # Watchers | 3.2k | 6.7k | 6.3k | | # Stars | 72.9k | 168k | 183k | | # Forks | 19.1k | 33.8k | 28.9k | | # Contributors | 1,398 | 1,542 | 399 | | # Used By | 1.8m | 6.3m | 144k | 綜合上面的表格看起來企業好像比較喜歡Angular跟React,Vue可能因為比較新所以工作機會比較少,但是因為輕量且來自開源社群所以深受開發者們愛戴呢。 ### 3. 我看完第2點了,不過我有些問題! **Q1:為甚麼Angular的大小最大啊?** **A1:** - 如果你使用React或Vue,你需要某些功能,譬如說PWA,你可能需要東裝一個套件西裝一個套件,最後可能還要自己處理Service Worker的註冊問題。 - 但如果你使用Angular呢? 只要輸入:`ng add @angular/pwa` 噹啷~你的專案就會自動註冊Service Worker跟自動配置manifest的cache項目囉~ - Angular的Package Size最大的原因就是因為它已經把你需要的東西都準備好了,<span class="hl">你只需要透過Angular CLI就可以完成多數專案的操作</span>。 **Q2:為什麼企業會偏愛Angular呢?** **A2:** 1. **Typescript**:最大的原因是因為Angular預設使用Typescript,還不知道Typescript的同學就想像是強型態、更加物件導向化的Javascript。Typescript相較Javascript更適合用來開發更大更複雜的專案,除了語法差別外Typescript還有一個特點就是它要透過編譯轉換成Javascript,<span class="hl">透過Typescript編譯器可以向下編譯Javascript支援舊版的瀏覽器</span>。 2. **Google**,Angular的開發者是Google,所以跟很多Google的產品有深度的結合。例如Google的Firebase,<span class="hl">使用[Angular Fire](https://github.com/angular/angularfire)就可以很簡單的對Firebase操作</span>,結合Firebase可以減少佈署的時間因為資料庫和後端都在Firebase上了。 3. **測試環境**:Angular原生支援使用Jasmine + Karma進行單元測試。 **Q3:為什麼喜歡Angular的人好像不是很多?** **A3:** - 簡單來說... ![Angular_Learning_Curve](https://hackmd.io/_uploads/BkWECrgBA.png) - Angular是上面三的框架中學習曲線最陡峭的。<span class="hl">想像Angular是一個前端完整解決方案,不管你有什麼需求它幾乎都可以完成</span>。但是也因為功能很多很齊全,要學習的東西也會變的更多更複雜。 --- ## 一、先安裝Node.js和Angular CLI 1. 在開始學習Angular前,我們要先安裝Nodejs。 Windows跟MacOS的使用者可以去Node的[**官方下載頁面**](https://nodejs.org/zh-tw/download/)下載安裝檔。 Linux使用者請自行參考不同發行版的安裝方式,請注意要安裝Node最新LTS以上的版本。 > [使用套件管理工具安裝](https://nodejs.org/en/download/package-manager/#debian-and-ubuntu-based-linux-distributions-enterprise-linux-fedora-and-snap-packages) > Debian / Ubuntu / deb / rpm / snap $\rightarrow$ [Binary發行版安裝](https://github.com/nodesource/distributions/blob/master/README.md) 安裝完後確定一下Node版本>=LTS,並且npm也有成功安裝。 ![node_version](https://i.imgur.com/0qwDYzo.png) *在本教學文中會使用 npm 作為套件管理工具,如果你要用其他工具例如 yarn 也可以,但是要注意套件管理工具不可以混用喔。* 2. 安裝Angular CLI `npm i -g @angular/cli` 等npm安裝完後輸入:`ng --version` 應該會看到類似的東西 ![ng_version](https://i.imgur.com/6C0e2Xc.png) --- ## 二、我準備好了 很好,來開始第一個Angular專案。 我們利用一個簡單的代辦事項清單網頁來認識一下Angular的各個面向。 ### 功能需求 1. 新增/刪除/修改/查詢代辦事項 2. 網頁要可以被安裝(PWA) ### 非功能需求 1. 介面不能太醜 2. 簡單的RWD 3. ~~我不想架設後端~~ ### 開始寫程式囉 *現在11啦rrrrr* ~~*ㄎㄅ...寫到這裡的時候Angular更新10版了辣*~~ ~~*反正我先用Angular 9寫,應該不會差太多。*~~ *Angular有提供版本更新教學,請參考:https://update.angular.io/* > 你可以在 Github 找到完整的專案原始碼: > https://github.com/stanley2058/AngularExample.git #### 1. 建立Angular專案 建立Angular專案非常的簡單,打開你的命令列輸入:`ng new TodoList` 使用Angular routing,然後我們先用CSS就好。 ``` > ng new TodoList ? Would you like to add Angular routing? Yes ? Which stylesheet format would you like to use? CSS ``` 等待Angular CLI安裝完需要的東西後就建立好專案了。 很喜歡版控的同學不用擔心,Angular CLI建立專案時就會幫我們初始化本地端的Git Repository了。 #### 2. 執行Angular開發用Server 進入**TodoList資料夾**,在命令列輸入:`ng serve --open` 等編譯完它就會幫你開啟網頁。 開起來後會看到類似這樣的頁面,這是Angular自動生成的首頁。 [![angular_fp](https://i.imgur.com/wIScyE1.png)](https://i.imgur.com/wIScyE1.png) *(點圖片可以看全圖)* #### 3. 修改首頁 來修改一下首頁,但是打開src/index.html卻看起來好像不對? body裡面只有一個 `<app-root></app-root>` 欸!? 別急,app-root是component的selector。 說明一下,我們可以在自己定義的元件(component)裡面定義選取名稱(selector),而當我們在html template裡面使用我們定義的selector時Angular會自己幫我們帶入該元件。 所以實際上現在顯示的東西應該在AppComponent裡面,打開app/app.component.html就會看到現在顯示在網頁上的東西了。 把app.component.html裡面的東西全選然後刪掉留下 `<router-outlet></router-outlet>` ,儲存後就會看到網頁自動更新成空白畫面了。 <span class="hl">注意:\<router-outlet\>\</router-outlet\>是Angular Router的根元素,Angular Router會把\<router-outlet\>\</router-outlet\>替換成對應的template。</span> --- 為了方便,我們使用Angular Material來幫我們快速建立各種元件。 `ng add @angular/material` 先選擇一些基本的東西: ```= Installing packages for tooling via npm. Installed packages for tooling via npm. ? Choose a prebuilt theme name, or "custom" for a custom theme: Indigo/Pink [ Preview: https://material.angular.io?theme=indigo-pink ] ? Set up global Angular Material typography styles? Yes ? Set up browser animations for Angular Material? Yes UPDATE package.json (1349 bytes) √ Packages installed successfully. UPDATE src/app/app.module.ts (502 bytes) UPDATE angular.json (3748 bytes) UPDATE src/index.html (511 bytes) UPDATE src/styles.css (181 bytes) ``` --- 再來,我們新增一個元件來作為TodoList的主要頁面。 `ng g c todo-page` > *`ng g c $component` 等於 `ng generate component $component`* 建立完成之後網頁並沒有變化? 對,因為我們要把建立完的元件放進來才會正常顯示。 一般來說只要像使用一般html tag一樣把selector放進html template就可以了,但是現在我們需要把首頁導向到todo-page,所以我們要設定Angular Rotuer的路由。 打開app/app-routing.module.ts 並在routes裡面新增對應的參數。 path是網址列host後面的路徑,component則是要routing到的元件。 ```typescript= import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; const routes: Routes = [ { path: "", component: TodoPageComponent } ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule] }) export class AppRoutingModule { } ``` 儲存後在首頁就會看到 `todo-page works!`。 #### 4. 新增一個網站header 首先,我們先建立一個header元件。 `ng g c header` 到app/app.module.ts裡面引入Angular Material的元件。 ```typescript= import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { AppRoutingModule } from './app-routing.module'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; // angular material import import { MatToolbarModule } from '@angular/material/toolbar'; import { MatMenuModule } from '@angular/material/menu'; import { MatIconModule } from '@angular/material/icon'; import { MatButtonModule } from '@angular/material/button'; // component import import { AppComponent } from './app.component'; import { TodoPageComponent } from './todo-page/todo-page.component'; import { HeaderComponent } from './header/header.component'; @NgModule({ declarations: [ AppComponent, TodoPageComponent, HeaderComponent ], imports: [ BrowserModule, AppRoutingModule, BrowserAnimationsModule, MatToolbarModule, MatMenuModule, MatIconModule, MatButtonModule, ], providers: [], bootstrap: [AppComponent] }) export class AppModule { } ``` 完成header.component.html的內容 ```htmlmixed= <mat-toolbar color="primary"> <div>Todo List</div> <div class="navRightButton"> <button mat-icon-button [matMenuTriggerFor]="appMenu" aria-label="Example icon-button with a menu"> <mat-icon>more_vert</mat-icon> </button> <mat-menu #appMenu="matMenu"> <button mat-menu-item><mat-icon>list</mat-icon>Todo List</button> <button mat-menu-item><mat-icon>info</mat-icon>About</button> </mat-menu> </div> </mat-toolbar> ``` 加上一點style,header.component.css ```css= .navRightButton { position: absolute; right: 20px; } ``` 最後到todo-page.component.html裡面加上header的tag ```htmlmixed= <div> <app-header></app-header> </div> ``` 儲存後應該會看到類似的畫面: ![todoList-header](https://i.imgur.com/qBD2MW0.png) 這樣就完成了一個可以重複使用的header元件。 #### 5. 完成 TODO List 頁面的內容 現在要製作一個簡單的清單可以新增、刪除跟修改代辦事項 在開始前,我們需要加入一些 Angular Material 的引入到 `app.module.ts` 裡面。 現在變成這樣: ```typescript= import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { AppRoutingModule } from './app-routing.module'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; // angular material import import { MatToolbarModule } from '@angular/material/toolbar'; import { MatMenuModule } from '@angular/material/menu'; import { MatIconModule } from '@angular/material/icon'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatListModule } from '@angular/material/list'; import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatTooltipModule } from '@angular/material/tooltip'; import { MatInputModule } from '@angular/material/input'; // component import import { AppComponent } from './app.component'; import { TodoPageComponent } from './todo-page/todo-page.component'; import { HeaderComponent } from './header/header.component'; @NgModule({ declarations: [ AppComponent, TodoPageComponent, HeaderComponent ], imports: [ BrowserModule, AppRoutingModule, BrowserAnimationsModule, MatToolbarModule, MatMenuModule, MatIconModule, MatButtonModule, MatCardModule, MatListModule, MatCheckboxModule, MatTooltipModule, MatInputModule, ], providers: [], bootstrap: [AppComponent] }) export class AppModule { } ``` *(你可能需要重開 dev server 來讓錯誤消失)* 增加完 import 後就可以在我們的程式中使用了。 首先,我們先把HTML做好,然後稍微給一點CSS: `todo-page.component.html` ```htmlmixed= <div> <app-header></app-header> <div class="todoListBody"> <div class="inputDiv"> <mat-form-field> <mat-label>New Todo Item</mat-label> <input matInput #todoInput (keypress)="inputKeypress($event)"> </mat-form-field> <button *ngIf="!todoEditing" class="addBtn" mat-flat-button color="primary" (click)="add()">Add</button> <button *ngIf="todoEditing" class="addBtn" mat-flat-button color="accent" (click)="finishEdit()">Edit</button> </div> <mat-card> <mat-list> <mat-list-item *ngFor="let todo of todoList"> <div class="todo"> <mat-checkbox [checked]="todo.status" (change)="changeStatus(todo)"> <div [ngClass]="{todoContext: true, done: todo.status}">{{todo.context}}</div> </mat-checkbox> <span> <mat-icon class="controls edit" matTooltip="Edit" (click)="edit(todo)">edit</mat-icon> <mat-icon class="controls delete" matTooltip="Delete" (click)="delete(todo)">delete</mat-icon> </span> </div> </mat-list-item> </mat-list> </mat-card> </div> </div> ``` 在這邊稍微解釋一下 HTML 內的一些特殊語法: - `*ngIf="條件"` 如果條件是 false 則該元素會從頁面上被移除。 - `(keypress)="func($event)"` 這是 Angular 綁定事件的方法,`keypress` 是一個 `@Output()` 有機會會再提到,這裡的 `$event` 是 HTML 的 KeyboardEvent。 - `[checked]=""` checked 是一個 `@Input`,可以在元素的 tag 上面給他值。 - `*ngFor="let todo of todoList"` 是一個 for of 迴圈,todo 是 todoList 中的物件。而 `*ngFor` 所寫在的 tag 會被重複印出 for 迴圈執行的次數。 - `{{todo.context}}` {{ }} 是 Angular 的 template 語法,裡面的變數會被當做字串寫到 HTML 裡面。template 有很多其他功能,包含自動訂閱 Observable 和取消訂閱、顯示特定格式...etc。 - `#todoInput` # 可以用來標示元素的名稱,在 ts 裡面可以搭配 ViewChild 使用。 `todo-page.component.css` ```css= .todoListBody { position: relative; width: 400px; top: 30px; left: calc(50vw - 200px); } .todo { display: flex; justify-content: space-between; width: 100%; } .addBtn { margin-left: 10px; } .inputDiv { text-align: center; margin-bottom: 10px; } .controls { cursor: pointer; } .controls:hover { color: purple; } .edit { color: goldenrod; } .delete { color: red; } .done { text-decoration: line-through; } .todoContext { max-width: 250px; overflow: auto; } @media screen and (max-width: 450px) { .todoListBody { width: 90vw; left: 5vw; } .todoContext { max-width: 30vw; } } ``` 上面的 HTML 使用了一些東西,所以要在 ts 檔裡面先把 function 名稱寫出來,不過在那之前,我們先定義一下 todo 物件的資料結構。 在 todo-page 資料夾下面新增一個檔案 `Todo.ts` ```typescript= export class Todo { id: string; status: boolean; context: string; } ``` 然後再回來完成 ts 檔的內容: ```typescript= import { Component, OnInit} from '@angular/core'; import { Todo } from './Todo'; @Component({ selector: 'app-todo-page', templateUrl: './todo-page.component.html', styleUrls: ['./todo-page.component.css'] }) export class TodoPageComponent implements OnInit { todoList: Todo[] = []; todoEditing: Todo; constructor() { } ngOnInit(): void { } changeStatus(todo: Todo): void { } deleteTodo(todo: Todo): void { } edit(todo: Todo): void { } finishEdit(): void { } add(): void { } inputKeypress($event: KeyboardEvent): void { } } ``` 我們剛剛定義 Todo 物件的時候有給一個 id 欄位,是為了刪除和修改時使用的。我們可以裝一個簡單的 uuid 套件來生成 id。 `npm i uuid` 安裝完後在 ts 檔裡面 import uuid: ```typescript= import { v4 as uuidv4 } from 'uuid'; ``` 然後在 `ngOnInit()` 中加上: ```typescript= ngOnInit(): void { this.todoList.push({ id: uuidv4(), status: false, context: "Test1" }); this.todoList.push({ id: uuidv4(), status: true, context: "Test2" }); } ``` 現在你應該會看到你的畫面長的像是這樣: ![](https://i.imgur.com/IIWCQEs.png) 再來我們要把邏輯的部分完成,為了讓程式比較簡單一點,所以暫時使用 localstorage 來存放我們的 todoList。 ```typescript= import { Component, ElementRef, OnInit, ViewChild } from '@angular/core'; import { Todo } from './Todo'; import { v4 as uuidv4 } from 'uuid'; import { MatInput } from '@angular/material/input'; @Component({ selector: 'app-todo-page', templateUrl: './todo-page.component.html', styleUrls: ['./todo-page.component.css'] }) export class TodoPageComponent implements OnInit { @ViewChild("todoInput") todoInput: ElementRef<MatInput>; todoList: Todo[] = []; todoEditing: Todo; constructor() { } ngOnInit(): void { const todoJson = localStorage.getItem("todolist"); if (todoJson) this.todoList = JSON.parse(todoJson); } changeStatus(todo: Todo): void { todo.status = !todo.status; localStorage.setItem("todolist", JSON.stringify(this.todoList)); } deleteTodo(todo: Todo): void { this.todoList = this.todoList.filter(t => t.id !== todo.id); localStorage.setItem("todolist", JSON.stringify(this.todoList)); } edit(todo: Todo): void { this.todoEditing = todo; this.todoInput.nativeElement.value = todo.context; localStorage.setItem("todolist", JSON.stringify(this.todoList)); } finishEdit(): void { this.todoList.forEach(t => { if (t.id === this.todoEditing.id) { t.context = this.todoInput.nativeElement.value; } }); this.todoEditing = null; this.todoInput.nativeElement.value = ""; localStorage.setItem("todolist", JSON.stringify(this.todoList)); } add(): void { const context = this.todoInput.nativeElement.value.trim(); if (!context) return; this.todoList.push({ id: uuidv4(), status: false, context }); this.todoInput.nativeElement.value = ""; localStorage.setItem("todolist", JSON.stringify(this.todoList)); } inputKeypress($event: KeyboardEvent): void { if ($event.key === "Enter") { if (this.todoEditing) this.finishEdit(); else this.add(); } } } ``` 解說一下比較特別的地方: - `@ViewChild("todoInput")` ViewChild 可以把 HTML 裡面有 `#todoInput` 的元素抓出來。抓出來會是一個 ElementRef 物件。 到這邊就完成了主要的功能。 #### 6. 新增 About 頁面 `ng g c about` 把我們之前做的 header 放進來,然後加上一點簡單的敘述。 `about.component.html` ```htmlmixed= <div> <app-header></app-header> <mat-card class="about"> <img src="https://angular.io/assets/images/logos/angular/angular.svg" alt="angular"> <div><b>Powered By Angular & Angular Material</b></div> <div><a href="https://angular.io" target="_blank">See More About Angular</a></div> <div><a href="https://material.angular.io" target="_blank">See More About Angular Material</a></div> </mat-card> </div> ``` 簡單的 css ```css= .about { position: relative; width: 300px; top: 30px; left: calc(50vw - 150px); } a { text-decoration: none; } ``` 在 router 增加 about 的路徑 `app-routing.module.ts` ```typescript=7 const routes: Routes = [ { path: "about", component: AboutComponent }, { path: "", component: TodoPageComponent } ]; ``` 再來到 header 的 ts 增加一個 navigate 的 function,在這邊我們會使用 angular 的 Router 來幫我們導向。 ```typescript= import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; @Component({ selector: 'app-header', templateUrl: './header.component.html', styleUrls: ['./header.component.css'] }) export class HeaderComponent implements OnInit { constructor(private router: Router) { } ngOnInit(): void { } navigate(path: string) { this.router.navigate([path]); } } ``` 最後到 header 的 HTML 補上兩個按鈕按下的動作: ```htmlmixed=8 <button mat-menu-item (click)="navigate('')"><mat-icon>list</mat-icon>Todo List</button> <button mat-menu-item (click)="navigate('about')"><mat-icon>info</mat-icon>About</button> ``` 這樣就完成了 About 頁面了。 #### 7. 加上 PWA 這是最後也是最簡單的一個步驟: `ng add @angular/pwa` Angular 會自動幫你安裝 pwa 到你的專案裡面,pwa 的詳細設定這邊就不多說了。因為 pwa 需要 service worker 才能運作,所以我們要先用生產環境 build 我們的 angular 專案: `ng build --prod` 我們可以透過一個簡單的測試伺服器來看看我們的pwa是不是已經成功建置了。 `npm i -g http-server` 到 dist/TodoList 裡面,執行: `http-server` 然後到 http://localhost:8080 就可以看到我們的網頁了。 ![](https://i.imgur.com/MTiPBLc.png) *要注意的是,如果沒有 https 的話 service worker 只會在 localhost上註冊。* --- ## 三、後記 到這邊就完成了你的第一個 Angular 專案啦~~ 恭喜你走到這一步,希望你學到了一些東西,寫這篇文章真的是一波三折,中間事情實在太多了,開始寫的時候 Angular 版本是 9 現在都11了(我的天...),不過 Angular 改版其實沒有那麼快,大概的時間約莫是半年一大版,也有提供很好用的[升級工具跟指南](https://update.angular.io/),所以升級版本真的是蠻簡單的。 這真的是一個非常簡單的 Angular 範例,Angular 跟其他框架最不一樣的地方就是它把幾乎所有你有可能用到的東西都包含在裡面了,所以這上面提到的大概只佔了不到10%的功能,我都還沒提到 rxjs 呢(笑)。 這篇教學差不多就到這邊了,畢竟真的也已經夠長了ww