Angular WorkShop - 3 === 第一堂:https://hackmd.io/@lala-lee-md/angular-workshop-1 * Angular環境 * TypeScript&ES6 * Component * Data Binding 第二堂:https://hackmd.io/@lala-lee-md/angular-workshop-2 * Component Interaction * Directives * Pipes、NgModule ## 調整 MyStore 的專案結構 ### 把可重用的元件歸納到SharedModule * 把 top-bar 放到 shared.module.ts ``` ng g m shared ``` https://git.io/JvRcj ### 把跟商業邏輯有關的模組歸納到多個特定的FeatureModule * 把 product-list、product-alert 放到 product.module.ts,在 AppModule 使用 ProductModule ``` ng g m product ``` > 之後可能還會有 CartModule、ShippingModule https://git.io/JvBDO * Generate your Angular project documentation https://compodoc.app/ ## Service 服務 ### 服務及元件 * Angular 把元件和服務區分開,以提高模組性和複用性。 * 「元件」處理UI邏輯相關的資料模型,只管使用者體驗。 * 「服務」則是處理跟商業邏輯相關,像是「從伺服器獲取資料、驗證使用者輸入」等。 * 服務被注入到元件中,就可以被元件使用。 ![](https://angular.tw/generated/images/guide/architecture/dependency-injection.png) * 注入語法如下所示 ``` constructor(private service: DataService) { ... } ``` ### 提供服務的範圍 * 通常服務會被很多元件所使用,在預設的情況下,會註冊在```root```中 ```typescript= // src/app/user.service.ts import { Injectable } from '@angular/core'; @Injectable({ providedIn: 'root', }) export class UserService { } ``` * 當你希望特定的服務只提供給特定 NgModule 中的元件使用。 ```typescript= // src/app/user.service.ts import { Injectable } from '@angular/core'; import { UserModule } from './user.module'; @Injectable({ providedIn: UserModule, }) export class UserService { } ``` * 如果沒辦法指定服務是哪個特定模組,也可以在要使用服務的模組中使用 @NgModule() providers 指定。 ```typescript= // src/app/user.module.ts import { NgModule } from '@angular/core'; import { UserService } from './user.service'; @NgModule({ providers: [UserService], }) export class UserModule { } ``` * 建議使用```providedIn```的方式提供服務,因為當沒有元件注入使用它時,該服務會被搖樹優化掉。 > 搖樹優化 Tree-Shaking:把用不到的程式碼「搖」掉 > > ![](https://media1.tenor.com/images/3122611d12ef3ae60e488659dd030f13/tenor.gif?itemid=15973840) ### Service 實作 * 建立 Service ``` ng g s your-service-name ``` ``` ng g s product/product ``` * 增加 ProductService 的邏輯 ``` import { Injectable } from '@angular/core'; @Injectable({ providedIn: 'root' }) export class ProductService { items = []; constructor() { } // 加入商品至商品列表 addItem(item) { this.items.push(item); } // 取得商品列表 getItems() { return this.items; } } ``` https://git.io/JvBDp * 在元件中使用 Service 的資料 ```typescript= export class ProductListComponent implements OnInit { products = []; constructor( // 注入服務到元件中 private productService: ProductService ) { } ngOnInit(): void { this.productService.addItem({ name: 'Phone XL', price: 799, description: 'A large phone with one of the best screens' }); this.productService.addItem({ name: 'Phone Mini', price: 699, description: 'A great phone with one of the best cameras' }); this.productService.addItem({ name: 'Phone Standard', price: 299, description: '' }); this.products = this.productService.getItems(); } ``` https://git.io/JvByk ### 使用 HttpClient 服務 Angular 是使用 HttpClient 去抓取 REST APIs 回應的資料,這個方法會回傳```Observable``` 型態,使用```subscribe()```,取得到Response。 * 在asset資料夾下新增一個 product.json ``` [ { "name": "Phone XL", "price": 799, "description": "A large phone with one of the best screens" }, { "name": "Phone Mini", "price": 699, "description": "A great phone with one of the best cameras" }, { "name": "Phone Standard", "price": 299, "description": "" } ] ``` https://git.io/JvByI * 引用 HttpClientModule ``` import { HttpClientModule } from '@angular/common/http'; ``` * 在服務中使用 HttpClient ``` import { HttpClient } from '@angular/common/http'; ``` ```typescript= constructor( private httpClient: HttpClient ) { } getItems() { return this.httpClient.get('/assets/product.json'); } ``` https://github.com/lala-lee-jobs/mystore/commit/54267e01ed3c81eff1c639a33db9ddedb8a2d634 * 在元件中使用服務 ```typescript= constructor( private productService: ProductService ) { } ngOnInit(): void { this.productService.getItems().subscribe(data => { this.products = data; }); } ``` https://git.io/JvByR #### 使用 JSON Server 模擬 REST API * 新增一個 db.json 給 JSON Server 使用 ```json= { "products":[ { "id":1, "name": "Phone XL", "price": 799, "description": "A large phone with one of the best screens" }, { "id":2, "name": "Phone Mini", "price": 699, "description": "A great phone with one of the best cameras" }, { "id":3, "name": "Phone Standard", "price": 299, "description": "" } ] } ``` https://git.io/JvB7Z * 安裝 json-server 並啟用 db.json ``` // 安裝 json-server 在 global npm install -g json-server // 依照 db.json 的內容產生 REST API json-server db.json ``` 現在可以使用 http://localhost:3000/products 取得商品資料 * 修改 服務改用 JSON Server 提供的簡易REST API ```typescript= url = 'http://localhost:3000/products'; constructor( private httpClient: HttpClient ) { } // 取得商品列表 getItems() { return this.httpClient.get(this.url); } ``` https://git.io/JvB7l ## Routing 路由-網站網址流程控管 Angular Router 能讓使用者從一個頁面導航到另一個頁面。 * 在位址列中輸入一個 URL,導航到相應的頁面。 * 點選頁面上的連結,導航到新頁面。 * 點選瀏覽器的後退和前進按鈕,在瀏覽器的歷史中前後導航。 ### 匯入 Router 模組 ``` import { RouterModule, Routes } from '@angular/router'; ``` ### RouterOutlet 路由出口 用於在HTML中標出一個位置,Router會把要顯示在這個出口處的元件顯示在這裡。 * 修改 app.component.html 把 app-product-list 改成 router-outlet ```html <app-top-bar></app-top-bar> <div class="container"> <router-outlet></router-outlet> </div> ``` https://git.io/JvB78 ![](https://i.imgur.com/DyX1YQo.png) ### 路由配置 * 每個 Route 都會把一個 URL 的 path 對映到一個元件 ```typescript= const routes: Routes = [ { path: 'xxx', component: YourComponet } ]; ``` * 在 app-routing.module.ts 增加路由配置 空路徑 ```''```,表示為預設路由。預設路由使用```redirectTo``` 重定向到指定的URL path。 ```typescript= import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { ProductListComponent } from './product/product-list/product-list.component'; // 增加路由配置 const routes: Routes = [ { path: '', redirectTo:'product', pathMatch:'full' }, { path: 'product', component: ProductListComponent } ]; ``` 畫面又回來了~ https://git.io/JvB70 * 新增 product-detail 元件 至 product 模組 ``` ng g c product/product-detail -m product ``` * 完成 product-detail 的內容 http://localhost:3000/products/1 in product.service.ts ```typescript // 取得單個商品 getItem(id) { return this.httpClient.get(`${this.url}/${id}`); } ``` in product-detail.component.html ```html <h2>Product Details</h2> <div *ngIf="product"> <h3>{{ product.name }}</h3> <h4>{{ product.price | currency }}</h4> <p>{{ product.description }}</p> </div> ``` in product-detail.component.ts ```typescript product; constructor( private productService: ProductService ) { } ngOnInit(): void { this.productService.getItem(1).subscribe(data => { this.product = data; }); } ``` in app-routing.module.ts 新增一個路由 ```typescript { path: 'product/:id', component: ProductDetailComponent } ``` http://localhost:4200/product/1 https://git.io/JvB5t ### 使用 RouterLink 用來連結到特定的路由 ```html <a [routerLink]=['/path', path-part]>MyLink</a> => ex. [routerLink]=['/product', 1] => http://localhost:4200/product/1 <a [routerLink]=['/path', path-part-1, path-part-2, ... , path-part-n]>MyLink</a> => ex. [routerLink]=['/product', 1, 'test'] => http://localhost:4200/product/1/test ``` * 修改商品名稱的連結,利用 routerLink 去連結 product-detail ```html // in product-list.component.html <h3> <a [title]="product.name + ' details'" [routerLink]="['/product', product.id]"> {{ product.name }} </a> </h3> ``` https://git.io/JvBAc * 把 RouterModule import 到 SharedModule 並且 export 出來 https://git.io/JvBAC > 完成之後會發現雖然可以連結至product-detail,但固定都是取得商品id為1的內容 ### 使用路由資訊 在元件中注入```ActivatedRoute```服務,取得當前路由的相關資訊 https://angular.tw/api/router/ActivatedRoute * 修改程式碼,讓商品列表的連結點選後,商品資訊會依據商品id做內容變化 ```typescript= constructor( private productService: ProductService, private route: ActivatedRoute ) { } ngOnInit(): void { this.route.params.subscribe(params => { console.log('params', params); this.productService.getItem(params.id).subscribe(data => { this.product = data; }); }); } ``` https://git.io/JvBxe ## 複習及完善 MyStore App ### 新增購物車功能 * 建立 帶有 Routing 的 Cart Module ``` ng g m cart --routing=true ``` ![](https://i.imgur.com/iUkbIQc.png) * 在購物車模組下產生購物車元件 ``` ng g c cart/cart -m cart ``` * 在 app-routing.module.ts 加上 cart 的路由 ``` { path: 'cart', component: CartComponent} ``` * 在 top-bar.component.html 加上指向 cart 的 RouterLink ```html <a class="button fancy-button" [routerLink]="['/cart']"> <i class="material-icons">shopping_cart</i>Checkout </a> ``` ![](https://i.imgur.com/7ajaWQk.png) ### Lazy Loading * 之前的路由設定情境,NgModule 都是立即載入的。 * 就算一開始沒有使用到購物車模組,也會被載入 * 對於帶有很多路由的大型應用,考慮使用 Lazy Loading * 需要時才載入模組。例如,點下購物車路由才載入購物車模組 * Lazy Loading 可以減小初始打包檔案的尺寸,從而減少載入時間。 * 觀察 目前檔案載入的大小 ![](https://i.imgur.com/HcwWvh6.png) * 調整 app-routing.module.ts 把 CartMdoule 做 Lazy Loading ```typescript= { path: 'cart', loadChildren: () => import('./cart/cart.module').then(m => m.CartModule) } ``` https://git.io/JvRvl ![](https://i.imgur.com/F42WQB5.png) ![](https://i.imgur.com/NVmVjwE.png) > 但發現沒有正常的入Cart元件,因為還沒幫CartRoutingModule設定路由 ```typescript= // in cart-routing.module.ts const routes: Routes = [ { path: '', component: CartComponent} ]; ``` https://git.io/JvRv4 ### RouterModule.forRoot() 與 RouterModule.forChild() * forRoot() 表示為「根路由」模組。 在應用中只應該使用一次,也就是這個 AppRoutingModule 中。 * RouterModule.forChild(routes) 會在各個特性模組中使用。這種方式下 Angular 就會知道這個路由列表只負責提供額外的路由並且其設計意圖是作為特性模組使用。 ### Angular 子路由 * 調整 app-routing.module.ts 加上 product 的子路由 https://git.io/JvRZv ```typescript= { path: 'product', children: [ { path: '', component: ProductListComponent}, { path: 'detail/:id', component: ProductDetailComponent} ] } ``` * 調整 商品列表連至商品詳細頁的 RouterLink https://git.io/JvRZk ``` <h3> <a [title]="product.name + ' details'" [routerLink]="['/product/detail', product.id]"> {{ product.name }} </a> </h3> ``` ### 增加購物車服務 * 增加購物車服務 ``` ng g s cart/cart ``` * 購物車服務增加 把商品新增到購物車、返回購物車商品以及清除購物車商品的方法 https://git.io/JvRZq ```typescript= import { Injectable } from '@angular/core'; @Injectable({ providedIn: 'root' }) export class CartService { items = []; addToCart(product) { this.items.push(product); } getItems() { return this.items; } clearCart() { this.items = []; return this.items; } } ``` * 在product-detail.component.html加上Buy按鈕,按下之後把商品加到購物車 https://git.io/JvRZm ```html <button (click)="addToCart(product)">Buy</button> ``` * 在proudct-detail 加上addToCart的程式邏輯 https://git.io/JvRZG * 完善 cart 元件 顯示購物車清單 https://git.io/JvRZu in cart.compoennt.html ```html <h3>Cart</h3> <div class="cart-item" *ngFor="let item of carts"> <span>{{ item.name }}</span> <span>{{ item.price | currency }}</span> </div> ``` in cart.compoennt.ts ```typescript= import { Component, OnInit } from '@angular/core'; import { CartService } from '../cart.service'; @Component({ selector: 'app-cart', templateUrl: './cart.component.html', styleUrls: ['./cart.component.scss'] }) export class CartComponent implements OnInit { carts; constructor( private cartService: CartService ) { } ngOnInit(): void { this.carts = this.cartService.getItems(); } } ``` in top-bar.component.html ```html <a [routerLink]="['/']"> <h1>My Store</h1> </a> ``` * 加上清空購物車的功能 https://git.io/JvRZg ### 查詢運費價格 * db.json 增加 shipping 資料 給 JSON Server 使用 https://git.io/JvRZX ```json "shipping":[ { "type": "Overnight", "price": 25.99 }, { "type": "2-Day", "price": 9.99 }, { "type": "Postal", "price": 2.99 } ], ``` * 利用 ```ng generate``` 產生 Shipping Module 的所有結構 https://git.io/JvRZN * 新增 含有路由模組的 ShippingModule ``` ng g m shipping --routing=true ``` * 新增 Shipping Component ``` ng g c shipping/shipping -m shipping ``` * 新增 Shipping Service ``` ng g s shipping/shipping ``` * 調整 ShippingRoutingModule 增加 預設路由,並且 在 AppRoutingModule 使用 Lazy Loading https://git.io/JvRZA * 在 shipping.routing.module.ts 增加 預設路由 ``` const routes: Routes = [ {path: '', component: ShippingComponent} ]; ``` * 在 app.routing.module 使用 Lazy Loading ``` { path: 'shipping', loadChildren: () => import('./shipping/shipping.module').then(m => m.ShippingModule) } ``` * 在 Shipping Service 使用 JSON Server 的資料 https://git.io/JvRnG * 固定會出現的 apiurl prefix 放在 environment ``` export const environment = { production: false, apiurl: 'http://localhost:3000/' }; ``` * 在 shipping.service.ts 使用 environment ``` import { Injectable } from '@angular/core'; import { environment } from 'src/environments/environment'; import { HttpClient } from '@angular/common/http'; @Injectable({ providedIn: 'root' }) export class ShippingService { private url = `${environment.apiurl}/shipping`; constructor( private httpClient: HttpClient ) { } getItems() { return this.httpClient.get(this.url); } } ``` * 在Shipping元件使用Shipping服務取得API資料 https://git.io/JvRn4 ```typescript= export class ShippingComponent implements OnInit { shippingCosts; constructor( private shippingService: ShippingService ) { } ngOnInit(): void { this.shippingCosts = this.shippingService.getItems(); } } ``` * 在Shipping元件的範本中使用 async pipe https://git.io/JvRnB https://angular.tw/api/common/AsyncPipe ```html <h3>Shipping Prices</h3> <div class="shipping-item" *ngFor="let shipping of shippingCosts | async"> <span>{{ shipping.type }}</span> <span>{{ shipping.price | currency }}</span> </div> ``` * 在購物車元件加上運費的連結 https://git.io/JvRnR ```html <a [routerLink]="['/shipping']">Shipping Prices</a> ``` ## Q & A