本文目錄
- 一、專案起步
- 二、編寫路由元件
- 三、編寫頁面元件
- 四、編寫服務
- 五、引入RxJS
- 六、改造元件
- 1.新增歷史記錄元件
- 2.新增和刪除歷史記錄
- 七、HTTP改造
- 1.引入HTTP
- 2.通過HTTP請求資料
- 3.通過HTTP修改資料
- 4.通過HTTP增加資料
- 5.通過HTTP刪除資料
- 6.通過HTTP查詢資料
本專案原始碼放在github
六、改造元件
從這裡開始,我們要使用RxJS來改造元件和新增新功能了,讓整個專案更加完善。
1.新增歷史記錄元件
- 建立
HistoryComponent
元件
ng g component hostory
複製程式碼
然後在app.component.html
資料夾中新增元件:
<!-- app.component.html -->
<app-history></app-history>
複製程式碼
2.新增增刪改查功能
這裡我們要開始做書本的增刪改查功能,需要先建立一個HistoryService
服務,方便我們實現這幾個功能:
- 建立
HistoryService
服務
ng g service history
複製程式碼
然後在生成的ts檔案中,增加add
和clear
方法,add
方法用來新增歷史記錄到history
陣列中,clear
方法則是清空history
陣列:
// history.service.ts
export class HistoryService {
history: string[] = [];
add(history: string){
this.history.push(history);
}
clear(){
this.history = [];
}
}
複製程式碼
- 使用
HistoryService
服務
在將這個服務,注入到BooksService
中,並改造getBooks
方法:
// books.service.ts
import { HistoryService } from './history.service';
constructor(
private historyservice: HistoryService
) { }
getBooks(): void{
this.historyservice.add('請求書本資料')
this.booksservice.getBookList()
.subscribe(books => this.books = books);
}
複製程式碼
也可以用相同方法,在IndexComponent
中新增訪問首頁書本列表
的記錄。
// index.component.ts
import { HistoryService } from '../history.service';
constructor(
private booksservice: BooksService,
private historyservice: HistoryService
) { }
getBooks(): void{
this.historyservice.add('訪問首頁書本列表');
this.booksservice.getBookList()
.subscribe(books => this.books = books);
}
複製程式碼
接下來,將我們的HistoryService
注入到HistoryComponent
中,然後才能將歷史資料顯示到頁面上:
// history.component.ts
import { HistoryService } from '../history.service';
export class HistoryComponent implements OnInit {
constructor(private historyservice: HistoryService) { }
ngOnInit() {}
}
複製程式碼
<!-- history.component.html -->
<div *ngIf="historyservice.history.length">
<h2>操作歷史:</h2>
<div>
<button class="clear"
(click)="historyservice.clear()"
>清除</button>
<div *ngFor="let item of historyservice.history">{{item}}</div>
</div>
</div>
複製程式碼
程式碼解釋:
*ngIf="historyservice.history.length"
,是為了防止還沒有拿到歷史資料,導致後面的報錯。
(click)="historyservice.clear()"
, 繫結我們服務中的clear
事件,實現清除快取。
*ngFor="let item of historyservice.history"
,將我們的歷史資料渲染到頁面上。
到了這一步,就能看到歷史資料了,每次也換到首頁,都會增加一條。
接下來,我們要在書本詳情頁也加上歷史記錄的統計,匯入檔案,注入服務,然後改造getBooks
方法,實現歷史記錄的統計:
// detail.component.ts
import { HistoryService } from '../history.service';
export class DetailComponent implements OnInit {
constructor(
private route: ActivatedRoute,
private location: Location,
private booksservice: BooksService,
private historyservice: HistoryService
) { }
//...
getBooks(id: number): void {
this.books = this.booksservice.getBook(id);
this.historyservice.add(`檢視書本${this.books.title},id為${this.books.id}`);
console.log(this.books)
}
}
複製程式碼
這時候就可以在歷史記錄中,看到這些操作的記錄了,並且清除按鈕也正常使用。
七、HTTP改造
原本我只想寫到上一章,但是想到,我們實際開發中,哪有什麼本地資料,基本上資料都是要從服務端去請求,所以這邊也有必要引入這一張,模擬實際的HTTP請求。
1.引入HTTP
在這一章,我們使用Angular提供的 HttpClient
來新增一些資料持久化特性。
然後實現對書本資料進行獲取,增加,修改,刪除和查詢功能。
HttpClient
是Angular通過 HTTP 與遠端伺服器通訊的機制。
這裡我們為了讓HttpClient
在整個應用全域性使用,所以將HttpClient
匯入到根模組app.module.ts
中,然後把它加入 @NgModule.imports
陣列:
import { HttpClientModule } from '@angular/common/http';
@NgModule({
//...
imports: [
BrowserModule,
AppRoutingModule,
HttpClientModule
],
//...
})
複製程式碼
這邊我們使用 記憶體 Web API(In-memory Web API) 模擬出的遠端資料伺服器通訊。
注意: 這個記憶體 Web API 模組與 Angular 中的 HTTP 模組無關。
通過下面命令來安裝:
npm install angular-in-memory-web-api --save
複製程式碼
然後在app.module.ts
中匯入 HttpClientInMemoryWebApiModule
和 InMemoryDataService
類(後面建立):
// app.module.ts
import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api';
import { InMemoryDataService } from './in-memory-data.service';
@NgModule({
// ...
imports: [
// ...
HttpClientInMemoryWebApiModule.forRoot(
InMemoryDataService, {dataEncapsulation:false}
)
],
// ...
})
export class AppModule { }
複製程式碼
知識點:
forRoot()
配置方法接受一個 InMemoryDataService 類(初期的記憶體資料庫)作為引數。
然後我們要建立InMemoryDataService
類:
ng g service InMemoryData
複製程式碼
並將生成的in-memory-data.service.ts
修改為:
// in-memory-data.service.ts
import { Injectable } from '@angular/core';
import { InMemoryDbService } from 'angular-in-memory-web-api';
import { Books } from './books';
@Injectable({
providedIn: 'root'
})
export class InMemoryDataService implements InMemoryDbService {
createDb(){
const books = [
{
id: 1,
url: 'https://img3.doubanio.com/view/subject/m/public/s29988481.jpg',
title: '像火焰像灰燼',
author: '程姬',
},
// 省略其他9條資料
];
return {books};
}
constructor() { }
}
複製程式碼
這裡先總結InMemoryDbService
所提供的RESTful API,後面都要用到:
例如如果url
是api/books
,那麼
- 查詢所有成員:以GET方法訪問
api/books
- 查詢某個成員:以GET方法訪問
api/books/id
,比如id
是1
,那麼訪問api/books/1
- 更新某個成員:以PUT方法訪問
api/books/id
- 刪除某個成員:以DELETE方法訪問
api/books/id
- 增加一個成員:以POST方法訪問
api/books
2.通過HTTP請求資料
現在要為接下來的網路請求做一些準備,先在books.service.ts
中引入HTTP符號,然後注入HttpClient
並改造:
// books.service.ts
import { HttpClient, HttpHeaders} from '@angular/common/http';
// ...
export class BooksService {
constructor(
private historyservice: HistoryService,
private http: HttpClient
) { }
private log(histories: string){
this.historyservice.add(`正在執行:${histories}`)
}
private booksUrl = 'api/books'; // 提供一個API供呼叫
// ...
}
複製程式碼
這裡我們還新增一個私有方法log
和一個私有變數booksUrl
。
接下來我們要開始發起http請求資料,開始改造getBookList
方法:
// books.service.ts
// ...
getBookList(): Observable<Books[]> {
this.historyservice.add('請求書本資料')
return this.http.get<Books[]>(this.booksUrl);
}
// ...
複製程式碼
這裡我們使用 http.get
替換了 of
,其它沒修改,但是應用仍然在正常工作,這是因為這兩個函式都返回了 Observable<Hero[]>
。
實際開發中,我們還需要考慮到請求的錯誤處理,要捕獲錯誤,我們就要使用 RxJS 的 catchError()
操作符來建立對 Observable 結果的處理管道(pipe)。
我們引入catchError
並改造原本getBookList
方法:
// books.service.ts
getBookList(): Observable<Books[]> {
this.historyservice.add('請求書本資料')
return this.http.get<Books[]>(this.booksUrl).pipe(
catchError(this.handleError<Books[]>('getHeroes', []))
);
}
private handleError<T> (operation = 'operation', result?: T) {
return (error: any): Observable<T> => {
this.log(`${operation} 失敗: ${error.message}`); // 發出錯誤通知
return of(result as T); // 返回空結果避免程式出錯
};
}
複製程式碼
知識點:
.pipe()
方法用來擴充套件 Observable
的結果。
catchError()
操作符會攔截失敗的 Observable。並把錯誤物件傳給錯誤處理器,錯誤處理器會處理這個錯誤。
handleError()
錯誤處理函式做了兩件事,發出錯誤通知和返回空結果避免程式出錯。
這裡還需要使用tap
操作符改造getBookList
方法,來窺探Observable
資料流,它會檢視Observable
的值,然後我們使用log
方法,記錄一條歷史記錄。
tap
回撥不會改變這些值本身。
// books.service.ts
getBookList(): Observable<Books[]> {
return this.http.get<Books[]>(this.booksUrl)
.pipe(
tap( _ => this.log('請求書本資料')),
catchError(this.handleError<Books[]>('getHeroes', []))
);
}
複製程式碼
3.通過HTTP修改資料
這裡我們需要在原來DetailComponent
上面,新增一個輸入框、儲存按鈕和返回按鈕,就像這樣:
<!-- detail.component.html -->
<!-- 前面程式碼省略 -->
<div>
<h2>修改資訊:</h2>
<label>新標題:
<input [(ngModel)]="books.title" placeholder="請輸入新標題">
</label>
<button (click)="save()">儲存</button>
<button (click)="goBack()">返回</button>
</div>
複製程式碼
這邊切記一點,一定要在app.module.ts
中引入 FormsModule
模組,並在@NgModule
的imports
中引入,不然要報錯了。
// app.module.ts
// ...
import { FormsModule } from '@angular/forms';
@NgModule({
// ...
imports: [
// ...
FormsModule
],
// ...
})
複製程式碼
input
框繫結書本的標題books.title
,而儲存按鈕繫結一個save()
方法,這裡還要實現這個方法:
// detail.component.ts
save(): void {
this.historyservice.updateBooks(this.books)
.subscribe(() => this.goBack());
}
goBack(): void {
this.location.back();
}
複製程式碼
這裡通過呼叫BooksService
的updateBooks
方法,將當前修改後的書本資訊修改到源資料中,這裡我們需要去books.service.ts
中新增updateBooks
方法:
// books.service.ts
// ...
updateBooks(books: Books): Observable<any>{
return this.http.put(this.booksUrl, books, httpOptions).pipe(
tap(_ => this.log(`修改書本的id是${books.id}`)),
catchError(this.handleError<Books>(`getBooks請求是id為${books.id}`))
)
}
// ...
複製程式碼
知識點:
HttpClient.put()
方法接受三個引數:URL 地址
、要修改的資料
和其他選項
。
httpOptions
常量需要定義在@Injectable
修飾器之前。
現在,我們點選首頁,選擇一本書進入詳情,修改標題然後儲存,會發現,首頁上這本書的名稱也會跟著改變呢。這算是好了。
4.通過HTTP增加資料
我們可以新增一個頁面,並新增上路由和按鈕:
ng g component add
複製程式碼
新增路由:
// app-routing.module.ts
// ...
import { AddComponent } from './add/add.component';
const routes: Routes = [
{ path: '', redirectTo:'/index', pathMatch:'full' },
{ path: 'index', component: IndexComponent},
{ path: 'detail/:id', component: DetailComponent},
{ path: 'add', component: AddComponent},
]
複製程式碼
新增路由入口:
<!-- app.component.html -->
<!-- 省略一些程式碼 -->
<a routerLink="/add">新增書本</a>
複製程式碼
編輯新增書本的頁面:
<!-- add.component.html -->
<div class="add">
<h2>新增書本:</h2>
<label>標題:
<input [(ngModel)]="books.title" placeholder="請輸入標題">
</label>
<label>作者:
<input [(ngModel)]="books.author" placeholder="請輸入作者">
</label>
<label>書本id:
<input [(ngModel)]="books.id" placeholder="請輸入書本id">
</label>
<label>封面地址:
<input [(ngModel)]="books.url" placeholder="請輸入封面地址">
</label>
<div><button (click)="add(books)">新增</button></div>
</div>
複製程式碼
初始化新增書本的資料:
// add.component.ts
// ...
import { Books } from '../books';
import { BooksService } from '../books.service';
import { HistoryService } from '../history.service';
import { Location } from '@angular/common';
export class AddComponent implements OnInit {
books: Books = {
id: 0,
url: '',
title: '',
author: ''
}
constructor(
private location: Location,
private booksservice: BooksService,
private historyservice: HistoryService
) { }
ngOnInit() {}
add(books: Books): void{
books.title = books.title.trim();
books.author = books.author.trim();
this.booksservice.addBooks(books)
.subscribe( book => {
this.historyservice.add(`新增書本${books.title},id為${books.id}`);
this.location.back();
});
}
}
複製程式碼
然後在books.service.ts
中新增addBooks
方法,來新增一本書本的資料:
// books.service.ts
addBooks(books: Books): Observable<Books>{
return this.http.post<Books>(this.booksUrl, books, httpOptions).pipe(
tap((newBook: Books) => this.log(`新增書本的id為${newBook.id}`)),
catchError(this.handleError<Books>('新增新書'))
);
}
複製程式碼
現在就可以正常新增書本啦。
5.通過HTTP刪除資料
這裡我們先為每個書本後面新增一個刪除按鈕,並繫結刪除事件delete
:
<!-- books.component.html -->
<!-- 省略一些程式碼 -->
<span class="delete" (click)="delete(list)">X</span>
複製程式碼
// books.component.ts
import { BooksService } from '../books.service';
export class BooksComponent implements OnInit {
@Input() list: Books;
constructor(
private booksservice: BooksService
) { }
// ...
delete(books: Books): void {
this.booksservice.deleteBooks(books)
.subscribe();
}
}
複製程式碼
然後還要再books.service.ts
中新增deleteBooks
方法來刪除:
// books.service.ts
deleteBooks(books: Books): Observable<Books>{
const id = books.id;
const url = `${this.booksUrl}/${id}`;
return this.http.delete<Books>(url, httpOptions).pipe(
tap(_ => this.log(`刪除書本${books.title},id為${books.id}`)),
catchError(this.handleError<Books>('刪除書本'))
);
}
複製程式碼
這裡需要在刪除書本結束後,通知IndexComponent
將資料列表中的這條資料刪除,這裡還需要再瞭解一下Angular 父子元件資料通訊。
然後我們在父元件IndexComponent
上新增change
事件監聽,並傳入本地的funChange
:
<!-- index.component.html -->
<app-books *ngFor="let item of books" [list]="item"
(change) = "funChange(item, $event)"
></app-books>
複製程式碼
在對應的index.component.ts
中新增funChange
方法:
// index.component.ts
funChange(books, $event){
this.books = this.books.filter(h => h.id !== books.id);
}
複製程式碼
再來,我們在子元件BooksComponent
上多匯入Output
和EventEmitter
,並新增@Output()
修飾器和呼叫emit
:
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
export class BooksComponent implements OnInit {
// ...
@Output()
change = new EventEmitter()
// ...
delete(books: Books): void {
this.booksservice.deleteBooks(books)
.subscribe(()=>{
this.change.emit(books);
});
}
}
複製程式碼
這樣就實現了我們父子元件之間的事件傳遞啦,現在我們的頁面還是正常執行,並且刪除一條資料後,頁面資料會更新。
6.通過HTTP查詢資料
還是在books.service.ts
,我們新增一個方法getBooks
,來實現通過ID來查詢指定書本,因為我們是通過ID查詢,所以返回的是單個資料,這裡就是Observable<Books>
型別:
// books.service.ts
getBooks(id: number): Observable<Books>{
const url = `${this.booksUrl}/${id}`;
return this.http.get<Books>(url).pipe(
tap( _ => this.log(`請求書本的id為${id}`)),
catchError(this.handleError<Books>(`getBooks請求是id為${id}`))
)
}
複製程式碼
注意,這裡 getBooks
會返回 Observable<Books>
,是一個可觀察的單個物件,而不是一個可觀察的物件陣列。
八、結語
這個專案其實很簡單,但是我還是一步一步的寫下來,一方面讓自己更熟悉Angular,另一方面也是希望能幫助到更多朋友哈~ 最終效果:
本部分內容到這結束
Author | 王平安 |
---|---|
pingan8787@qq.com | |
博 客 | www.pingan8787.com |
微 信 | pingan8787 |
每日文章推薦 | https://github.com/pingan8787/Leo_Reading/issues |
JS小冊 | js.pingan8787.com |
微信公眾號 | 前端自習課 |