前後端分離後,前端除了負責顯示邏輯外,最重要的就是與 API 溝通。在 JQuery 只要使用 $.ajax()
就可存取 API;但在 Angular,則必須透過 service 與 DI container ,component 才可存取 API,完全遵守 依賴反轉原則
。
Version
Node.js 8.9.0
Angular 5.1.0
JSON Server 0.12.1
User Story
-
Add Post
: 使用POST
對 API 新增資料 -
Show All Posts
: 使用GET
對 API 抓資料
Architecture
-
AppComponent
: 專門負責顯示邏輯 -
PostService
: 專門負責連接 API 與資料處理部份 -
IPostService
: 由AppComponent
觀點所定義出的 interface -
HttpClient
: Angular 4.3 所新增處理 AJAX 的 class
依賴反轉原則
高階模組不應該依賴低階模組;兩著都應該依賴高階模組所定義的 interface
AppComponent
不直接依賴 PostService
,且 AppComponent
與 PostService
都反過來依賴 AppComponent
所定義的 IPostService
interface。
Implementation
JSON Server
前後端分離後,前後端所依據的就是 JSON 資料,傳統前端會在先 var
一個 JSON,等要連資料庫時,再用 $.ajax()
去改變 JSON 資料,這樣雖然可行,但必須去修改 code。
比較好的方式是改用 JSON Server,讓我們實際去模擬 HTTP 的 GET/POST/PUT/PATCH 與 DELETE,只要透過 proxy,就會連上 JSON Server,若不透過 proxy,就連上 API Server,無論怎麼切換,都不用去修改 code。
無論是真的連接 API Server,或是透過 JSON Server,都不用去修改 code。
安裝 JSON Server
$ npm install -g json-server
將 JSON Server 安裝在 global 環境。
第一次啟動 JSON Server
~$ cd MyProject ~/MyProject$ mkdir json-server ~/MyProject$ cd json-server ~/MyProject/json-server$ json-server db.json
- 進入專案目錄
- 在專案目錄建立
json-server
子目錄 - 進入
json-server
子目錄 - 第一次啟動 JSON Server,指定
db.json
為資料庫檔案 - JSON Server 預設會啟動在
http://localhost:3000
,並提供posts
、comments
與profile
3 個 table
- 若
db.json
存在,則 JSON Server 會以此檔案為資料庫,若不存在,則會建立新的db.json
- 預設會有
posts
、comments
與profile
3 個 table
測試 JSON Server
預設 db.json
已經有資料,可藉此測試 JSONServer 是否有成功啟動。
http://localhost:3000/posts
使用 Postman 測試 GET
- 選擇
GET
- 輸入
http://localhost:3000/posts
- 正確回傳 JSON
使用 Postman 測試 POST
- 選擇
POST
- 輸入
http://localhost:3000/posts
- 選擇
Headers
- Key 為
Content-Type
,value 為application/json
- 選擇
Body
- 選擇
raw
- 選擇
JSON (application/json)
- 輸入要新增的 JSON 資料
不用包含 id
,JSON Server 會自動幫你新增
- 選擇
db.json
- 剛剛由 Postman 新增的資料會寫入
db.json
建立 JSON Server Routes
我們可以自行在 db.json
建立新的 table,但預設 URI 都會在 root,可能不適合使用 (一般都會放在 /api
下,如 /api/posts
),我們可自行加入 route 加以 mapping。
json-server/routes.json
{ "/api/posts": "/posts" }
在 json-server
目錄下新增 routes.json
,將 /api/posts
對應到 JSON Server 的 /posts
。

- 在
json-server
目錄下新增routes.json
- 將
/api/routes
對應到 JSON Server 的/posts
建立 Angular CLI Proxy
Angular CLI 預設並不會使用 JSON Server,因此我們必須讓 Angular 會透過 proxy 去打 JSON Server。
proxy.conf.json
{ "/api": { "target": "http://localhost:3000", "secure": false } }
在專案根目錄下新增 proxy.conf.json
,將 /api
mapping 到 http://localhost:3000
,也就是 JSON Server。
- 在專案根目錄下新增
proxy.conf.json
- 將
/api
對應到 JSON Server 的http://localhost:3000
由 npm 管理 JSON Server 與 Proxy
由於每次都要啟動 JSON Server 與 proxy,且參數有點繁瑣,比較好的方式是統一由 npm 來管理
package.json
{ "name": "ng5-http-client", "version": "0.0.0", "license": "MIT", "scripts": { "ng": "ng", "start": "ng serve", "proxy": "ng serve --proxy-config proxy.conf.json", "build": "ng build", "test": "ng test", "lint": "ng lint", "e2e": "ng e2e", "json-server": "json-server ./json-server/db.json --routes ./json-server/routes.json" }, (略) }
13 行
"json-server": "json-server ./json-server/db.json --routes ./json-server/routes.json"
新增 npm json-server
,除了指定使用 db.json
外,還會使用自訂 route。
第 8 行
"proxy": "ng serve --proxy-config proxy.conf.json",
原本 npm start
得以保留,此為不透過 proxy 方式,另外新增 npm proxy
,將會透過 proxy 使用 JSON Server。
- 選擇
package.json
- 新增
proxy
使 Angular CLI 透過 proxy 使用 JSON Server - 新增
json-server
啟動 JSON Server
每次要開發專案時 :
~/MyProject$ npm run json-server
先 npm run json-server
執行 JSON Server
~/MyProject$ npm run proxy
再 npm run proxy
透過 proxy 使用 JSON Server
必須開兩個 terminal 分別執行 npm run json-server
與 npm run proxy
,因為都必須同時在背景執行
JSON Schema to TypeScript
後端會以 JSON Schema 定義 JSON 格式與型別,而 Angular 亦會使用 interface 定義 JSON 的型別提供檢查。
透過 JSON Schema to TypeScript
,我們可以直接將 JSON Schema 轉成 TypeScript interface,可節省時間,也可避免 typo。
安裝 JSON Schema to TypeScript
$ npm install -g json-schema-to-typescript
將 JSON schema to TypeScript 安裝在 global 環境。
JSON Schema
post.schema.json
{ "title": "Post", "type": "object", "properties": { "id": { "type": "number" }, "title": { "type": "string" }, "author": { "type": "string" } }, "additionalProperties": false, "required": ["title", "author"] }
定義了 id
、 title
與 author
3 個欄位,其中 title
與 author
為必填。
將 JSON Schema 轉 TypeScript Interface
~/MyProject/src/app/model$ json2ts post.schema.json post.model.ts
目前將 post.schema.json
放在 /src/app/model
目錄下。
使用 json2ts
將 post.schema.json
轉成 post.model.ts
。
TypeScript Interface
post.model.ts
/** * This file was automatically generated by json-schema-to-typescript. * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, * and run json-schema-to-typescript to regenerate this file. */ export interface Post { id?: number; title: string; author: string; }
由 json2ts
所轉出來的 TypeScript interface,最上面有 json2ts
所產生的註解。
將然若有 JSON 欄位異動,請不要直接修改 TypeScript interface,而應該直接去修改 JSON Schema,再由 json2ts
產生 TypeScript interface
- 由
json2ts
所產生的post.model.ts
- 由
json2ts
所產生的註解 - 由
json2ts
所轉出的 TypeScript interface
AppComponent
app.component.html
<input type="text" [(ngModel)]="title"> <button (click)="onClick()">Add Post</button> <ul> <li *ngFor="let post of posts|async"> {{ post.id}} {{post.title}} {{post.author}} </li> </ul>
第 1 行
<input type="text" [(ngModel)]="title">
title
的輸入框,直接使用 ngModel
的 two way binding 到 title
field。
第 2 行
<button (click)="onClick()">Add Post</button>
Button 的 click
event 對應到 AppComponent.onClick()
。
第 4 行
<ul> <li *ngFor="let post of posts|async"> {{ post.id}} {{post.title}} {{post.author}} </li> </ul>
使用 *ngFor
將所有 posts
展開。
因為 posts
為 Observable
,所以要搭配 async
pipe。
app.component.ts
import { Component, OnInit } from '@angular/core'; import { IPostService } from './service/post/ipostservice.interface'; import { Post } from './model/post.model'; import { Observable } from 'rxjs/Observable'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent implements OnInit { title: string; posts: Observable<Post[]>; constructor(private postService: IPostService) { } ngOnInit(): void { this.posts = this.postService.getAllPosts(); } onClick() { const post = <Post>{ 'title' : this.title, 'author' : 'Sam' }; this.postService.addPost(post); this.posts = this.postService.getAllPosts(); } }
12 行
title: string; posts: Observable<Post[]>;
要與 HTML 做 data binding 的東西都會宣告在 public field。
-
title
: 給<input type="text">
做 two-way binding -
posts
: 給*ngFor
用的Observable
15 行
constructor(private postService: IPostService) { }
將 PostService
依賴注入,其型別為 IPostService
interface。
依賴反轉原則
高階模組不應該依賴低階模組;兩著都應該依賴高階模組所定義的 interface
所以 AppComponent
不應該直接依賴 PostService
,而應該依賴 IPostService
interface,因此 PostService
的型別是 IPostService
。
18 行
ngOnInit(): void { this.posts = this.postService.getAllPosts(); }
一載入時,希望能顯示所有 Post
。
呼叫 PostService
的 getAllPosts()
,也因此期望 IPostService
interface 有 getAllPosts()
。
22 行
onClick() { const post = <Post>{ 'title' : this.title, 'author' : 'Sam' }; this.postService.addPost(post); this.posts = this.postService.getAllPosts(); }
新增 Post
到資料庫。
呼叫 PostService
的 addPost()
,傳入 Post
model,也因此希望 IPostService
interface 有 addPost()
。
IPostService
ipostservice.interface.ts
import { Post } from '../../model/post.model'; import { Observable } from 'rxjs/Observable'; export abstract class IPostService { abstract addPost(post: Post): void; abstract getAllPosts(): Observable<Post[]>; }
根據 AppComponent
的需求,我們希望 IPostService
有 addPost()
與 getAllPosts()
兩個 method。
IPostService
觀念上是 interface,所以理想上應該這樣寫 :
export interface IPostService { addPost(post: Post): void; getAllPosts(): Observable<Post[]>; }
但若實際這樣寫,TypeScript 編譯沒問題,但 ng serve
時會執行錯誤。
主要在於 interface 是 TypeScript 所擴充,在編譯成 JavaScript 後並沒有 interface,導致 DI container 在執行時找不到 interface 而造成 runtime 錯誤。
也就是說 interface 在 TypeScript 主要是用來 編譯檢查
。
抽象
除了使用 interface 實現外,也可以使用 abstract class
,在 ES6 有 abstract class
,在 ES5 也有相對應的解法,所以目前必須改用 abstract class
取代 interface
實現 抽象
。
在 Angular 的 DI container,觀念上是 interface,但實作上須與 JavaScript 妥協,改用 abstract class
實踐 interface
。
PostService
post.service.ts
import { Injectable } from '@angular/core'; import { IPostService } from './ipostservice.interface'; import { Post } from '../../model/post.model'; import { Observable } from 'rxjs/Observable'; import { HttpClient } from '@angular/common/http'; @Injectable() export class PostService implements IPostService { constructor(private httpClient: HttpClient) { } addPost(post: Post): void { this.httpClient .post<Post>('api/posts', post) .subscribe(); } getAllPosts(): Observable<Post[]> { return this.httpClient .get<Post[]>('api/posts'); } }
第 8 行
export class PostService implements IPostService {
因為 依賴反轉原則
, PostService
必須實作 IPostService
interface。
第 9 行
constructor(private httpClient: HttpClient) { }
因為要使用 HttpClient
,將 HttpClient
注入到 PostService
。
11 行
addPost(post: Post): void { this.httpClient .post<Post>('api/posts', post) .subscribe(); }
使用 HttpClient.post<T>()
執行 POST,將 Post
model 傳入,其中 <T>
傳入 Post
model 的型別 Post
。
由於 post<T>()
回傳為 Observable
,最後要下 subscribe()
才會執行 post<T>()
。
17 行
getAllPosts(): Observable<Post[]> { return this.httpClient .get<Post[]>('api/posts'); }
使用 HttpClient.get<T>()
執行 GET,其中 <T>
為回傳的 Post[]
型別。
在 Angular 2 所提供的 Http
,至今仍可用,但用法叫麻煩 :
return this.http.get(‘api/posts’)
.map(response => response.json());
Angular 4.3 的 HttpClient
改用泛型後,除了在 get()
與 post()
都可以明顯看到型別,語意較佳,且寫法也教精簡。
AppModule
app.module.ts
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { AppComponent } from './app.component'; import { IPostService } from './service/post/ipostservice.interface'; import { PostService } from './service/post/post.service'; import { HttpClientModule } from '@angular/common/http'; import { FormsModule } from '@angular/forms'; @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, HttpClientModule, FormsModule ], providers: [ {provide: IPostService, useClass: PostService} ], bootstrap: [AppComponent] }) export class AppModule { }
13 行
imports: [ BrowserModule, HttpClientModule, FormsModule ],
-
HttpClient
須使用HttpClientModule
-
[(ngModel)]
須使用FormsModule
18 行
providers: [ {provide: IPostService, useClass: PostService} ],
DI container 一定要提供 interface 與 class 的 mapping。
在 providers
後為 object array,每一個 object 為 interface 與 class 的 mapping。
- provide : interface 名稱
- useClass : class 名稱
為什麼要使用 Service?
或許你會認為,明明使用 $.ajax()
向後端 API 抓資料是很單純的事情,為什麼 Angular 還要大費周章透過 Service + DI container,而不是提供一個簡單的 function 就好了嗎?有幾個原因:
- 將來若其他 component 要使用 API 時,將 service 直接 DI 進 compoent 即可。
- 將來若要對 API 的 service 做抽換,可直接透過 DI 換掉即可。
- 將來若要對 component 做單元測試,可輕易的
spyOn
API service 即可。
間單的說,將 API 部分獨立成 service,目的要使 component 與 API 解耦合 ,且讓 component 不直接相依於 service,兩者僅相依於 component 所定義的 interface,符合 依賴反轉原則
。
Sample Code
完整的範例可以在我的 GitHub 上找到。
Conclusion
- JSON Server : 讓我們在開發階段模擬真實的 API server
- JSON Schema to TypeScript : 將 JSON Schema 轉成 TypeScript interface,更有效率也避免 typo
- HttpClient : 語意更佳,也更簡單的寫法存取 API
- Angular DI Container : Angular 實現
依賴反傳
與依賴注入
的解決方案
注:本文内容来自互联网,旨在为开发者提供分享、交流的平台。如有涉及文章版权等事宜,请你联系站长进行处理。