記Angular與Django REST框架的一次合作(2):前端組件化——Angular
註:這是這個系列的第二部分,主要集中在Angular的使用方面。之前使用過AngularJS(Angular 1.x),混在Django的模板中使用,這些頁面一般完全是結果展示頁。在有Django表單輸入的頁面中,就很難將兩者很好的結合起來。自己在學習新版的Angular時,跟了2遍官方網站的“英雄指南”教程。第1次完全是照搬,熟悉了一下基本概念;第2次自己做了一些修改,寫了一個圖片分享系統(只有一個雛形,還不是特別完善)。
推薦IDE:Visual Studio Code
代碼: github地址(如果喜歡,記得star哦~)
第一部分:記Angular與Django REST框架的一次合作(1):分離 or 不分離,it‘s the question
1. Angular
作為一個新入坑web開發的人,本來一開始想選擇一個輕量級的前端+Django來開發網站。開始比較了幾個框架(主要是糾結於React和Angular),最後還是選擇了Angular(當時用的是1.x版)。之所以作出這樣的決定,在Stock Overflow中Josh David Miller對問題“Thinking in AngularJS” if I have a jQuery background? 的回答對我的影響很大。其中有一句話我很認同。
Don‘t design your page, and then change it with DOM manipulations
上面的問題是比較Angular和jQuery的,但是同樣適用從某些方面來比較Angualr和React:Angular是以html為中心的,從某種程度上可以看做是對html的一種擴展;React是以JS為中心的,在JS中混入了html。由於是初學,當時發現如果沒有先用html搭個架子,就完全搞不清楚網站的結構。
現在學習新版的Angular,反而覺得與React更像了。不得不說,組件(Component)是一種恰到好處的組織代碼的結構單元,而且感覺經過重新設計的Angular學習起來比AngularJS更容易上手,學習曲線也更加平滑(也許是因為之前的經驗)。Angular中有許多重要的基本概念,但是最重要的應該就是組件了。
1.1 突出的特點
目前了解到的關於Angualr項目中,印象最為深刻的兩個特點:
- 在導航欄中,鏈接到其他頁面不會觸發頁面重載(這也是SPA應用的一大特色);
- 在不主動刷新的情況下,整個網站可以離線使用。
2. 圖片分享系統(image-sharing-system)
這個項目完全是仿照著官方的“英雄指南”教程修改並添加了一些元素構成的。修改如下:
- 將hero換成了user;
- 每個user多了一個屬性"files",包含該用戶上傳的圖片的編號(一個數組);
- 添加了圖片相關的數據,與user的相關數據一起放在"in-memory-data.service.ts"文件中;
- 添加了"image.service"用來處理與圖片相關的數據;
- 修改了"user-detail"組件,使得進入詳情頁後可以看到每個用戶分享的圖片;
- 修改了"dashboard"組件,使得可以展示分享圖片最多的4位用戶;
圖1:http://localhost:4201/dashboard 視圖
圖2-1:http://localhost:4201/users 視圖
圖2-2:http://localhost:4201/detail/19 視圖
整個項目實現的功能:進入主頁(dashboard視圖,圖1),可以看到分享圖片最多的4位用戶,點擊每位用戶可以進入用戶的詳情頁,在主頁可以按照用戶名搜索用戶並進入詳情頁;點擊導航欄中的user,可以查看所有用戶的列表(users視圖,圖2-1),點擊每個user,可以從下方的View Details進入該user的詳情頁(detail視圖,圖2-2)。在users視圖中還可以添加、刪除用戶。
2.1 本地運行的方法
github:https://github.com/OnlyBelter/image-sharing-system
首先要安裝Node.js和Angular CLI,將項目clone到本地,然後運行下面的命令
npm install ng serve --host 0.0.0.0 --port 4201
如果運行正常,就可以在瀏覽器中查看了,http://localhost:4201/dashboard
2.2 整個程序的結構
圖3:圖片分享系統程序的結構,查看pdf
3. Angular CLI
CLI是Angular的命令行接口,使用CLI可以通過命令行創建項目,創建新的組件,服務,模塊等;而且可以用來實時編譯,測試,發布。CLI創建組件時(或服務等)會自動將組件import到app.module.ts文件中,並在NgModule中聲明。創建一個新項目後的文件結構如下圖:
圖4:Angular CLI生成的文件結構(VS Code中打開)
後面所有的修改都在app文件夾中,其他的文件一般不需要修改。
下面是一些常用的命令:
# 安裝cli npm install -g @angular/cli # 創建一個新項目 ng new my-project cd my-project # 啟動項目,可以實時編譯 ng serve --host 0.0.0.0 --port 4201 # 創建新的component ng g component my-new-component # 創建新的服務 ng g service my-new-service
CLI的文檔:https://github.com/angular/angular-cli/wiki
4. 組件(component)
組件就是“一小塊TypeScript代碼 + 一小塊html代碼 + 一小塊css代碼“,每個組件相對來說都可以實現一個比較獨立的功能。而前端最重要的功能就是內容的展示和與用戶的交互。因此組件相當於將整個大任務進行了分解:每個組件都完成一小塊任務,然後將這些組件拼在一起,就可以得到整個功能完整的網站。
比如在我自己寫的這個小項目中,一共有5個組件(圖3橙色部分):
- app.component:由Angular CLI創建時自動生成,直接被添加到了index.html中,相當於所有組件的父組件,控制著整個網站的基本結構,也是路由的出口;
- users.component:/users路由到這個視圖,主要用於顯示user列表,增加、刪除用戶;
- dashboard.component:/dashboard路由到這個視圖(根路由也被重定向到這個視圖),展示分享圖片最多的前4位用戶,按用戶名搜索;
- user-search.component:dashboard組件的子組件,負責搜索相關事務;
- user-detail.component:/detail/user-id,用於展示每個用戶的詳情;
上面的5個組件,app用於組織網站的結構,確定路由的出口;其他組件要麽負責一個獨立的頁面,或者是一個頁面的一部分。其他無論是module(例如路由模塊,圖3綠色部分)還是service(主要用於提供數據,圖3紫色部分),都是為組件的正常工作提供支持的。因此可以說,組件是位於Angular框架的中心位置的。
4.1 組件的組成
利用CLI創建一個新的component後,默認會在app文件夾下生成一個文件夾,這個文件夾內包含四個文件(以users組件為例):
- users.component.css:負責該組件的樣式;
- users.component.html:該組件的模板;
- users.component.spec.ts:一般用不到;
- users.component.ts:負責邏輯的處理,可以定義變量和函數,在模板中展示或調用;此外還可以導入相應的service,通過調用service的方法獲取數據;
4.2 組件中的構造函數(constructor)
組件的構造函數用來解決依賴註入,初始化服務或路由。其他變量的初始化不應該放在這裏,而應該放在ngOnInit中。下面是users組件中的構造函數:
// the constructor itself does nothing, the parameter simultaneously deinfes // a private userService property and identifies it as a UserService injection constructor( private userService: UserService, // 組件在構造函數中請求服務 private router: Router // 在構造函數中註入Router ) { }
初始化服務和路由後,就可以在後面通過this.userService和this.router來調用服務和路由中的方法了。
5. 服務(service)
服務是連接服務器端和組件的橋梁,使用單獨的服務可以保持組件精簡,服務可以通過http協議中的方法(get, post等)向服務器請求資源或修改、添加資源。
服務也可以通過CLI直接創建,[email protected]()修飾符。當 TypeScript [email protected]()裝飾器時,就會記下本服務的元數據。 如果 Angular 需要往這個服務中註入其它依賴,就會使用這些元數據。像上面組件的構造函數中介紹的那樣,服務可以註入到組件中,從而為組件提供數據服務。
5.1 承諾
服務總是異步的。Angular的http.get返回一個 RxJS 的Observable對象。Observable(可觀察對象)是一個管理異步數據流的強力方式。可以利用toPromise操作符把Observable轉換成Promise對象。
一個Observable對象是一個數組,其中的元素隨著時間的流逝異步地到達。 Observable幫助我們管理異步數據,例如來自後臺服務的數據。 Angular 自身使用了Observable,包括 Angular 的事件系統和它的 http 客戶端服務。為了使用Observable, Angular 采用了名為 Reactive Extensions (RxJS) 的第三方包。
這部分應該是官方教程中最復雜的一塊兒了。我打算後面單獨寫一篇博客,介紹這部分的內容。下面看一下我自己改寫的項目中user.service的實現:
5.2 服務的實現
在這部分實現了以下操作:
- 獲取所有user的數據(三種實現,直接返回一個數組、返回一個Promise對象、利用http返回一個Observable對象再轉換成Promise對象);
- 獲取單個user的數據(兩種實現,返回所有user的數據再根據id過濾、直接請求單個user的數據);
- 修改某個user的信息:利用http的put方法修改服務器端數據,使得數據可以持久化;
- 添加新的user:利用http的post方法,將數據添加到服務器端;
- 刪除已存在的user:利用http的delete方法,刪除數據;
import { Injectable } from [email protected]/core‘; import { Headers, Http } from "@angular/http"; // 有很多像toPromise這樣的操作符,用於擴展Observable,為其添加有用的能力 import ‘rxjs/add/operator/toPromise‘; import { USERS } from ‘./mock-users‘; import { User } from "./user"; @Injectable() export class UserService { private usersUrl = ‘api/users‘; constructor(private http: Http) { } //UserService暴露了getUsers方法,返回跟以前一樣的模擬數據,但它的消費者不需要知道這一點 //服務是一個分離關註點,建議你把代碼放到它自己的文件裏 getUsers(): Promise<User[]> { // return USERS; // 直接返回一個數組 return Promise.resolve(USERS); // 返回一個Promise對象 } // 延遲6s後返回 getUsersSlowly(): Promise<User[]> { return new Promise(resolve => setTimeout(() => resolve(USERS), 6000)); } // 返回所有user的數據再過濾 getUser(id: number): Promise<User> { return this.getUsers() .then(rep => rep.find(user => user.id === id)); } //Angular 的http.get返回一個 RxJS 的Observable對象 getUsersByHttp(): Promise<User[]> { return this.http.get(this.usersUrl) .toPromise() .then(res => res.json().data as User[]) .catch(this.handleError); } // 來發起一個 get-by-id 請求,直接請求單個user的數據 getUserByHttp(id: number): Promise<User> { const url = `${this.usersUrl}/${id}`; return this.http.get(url) .toPromise() .then(res => res.json().data as User) .catch(this.handleError); } private headers = new Headers({‘Content-Type‘: ‘application/json‘}); // 使用 HTTP 的 put() 方法來把修改持久化到服務端 update(user: User): Promise<User> { const url = `${this.usersUrl}/${user.id}`; return this.http.put(url, JSON.stringify(user), {headers: this.headers}) .toPromise() .then(() => user) // () .catch(this.handleError); } create(name: string): Promise<User> { return this.http .post(this.usersUrl, JSON.stringify({name: name}), {headers: this.headers}) .toPromise() // 下面的.then方法對默認返回的數據進行了加工,得到了一個完整的User對象 .then(res => res.json().data as User) .catch(this.handleError); } delete(id: number): Promise<void> { const url = `${this.usersUrl}/${id}`; return this.http.delete(url, {headers: this.headers}) .toPromise() .then(() => null) // 什麽也不返回 .catch(this.handleError); } private handleError(error: any): Promise<any> { console.error(‘An error occurred‘, error); // for demo purposes only return Promise.reject(error.message || error); } }
5.3 http請求與響應
在上面的代碼中,每次調用http.get(url),或其他http方法(post, put, delete),就相當於對相應的url發送了一次請求(Request)。發出這個請求後,收到請求的一方(一般是服務器端)總會給出一個響應(Response),這個響應可以是各種不同的形式。上面的getUsersByHttp方法中,就返回了一個User[]數組(由res.json().data得到),如果我們做一些修改:
1 //Angular 的http.get返回一個 RxJS 的Observable對象 2 getUsersByHttp(): Promise<User[]> { 3 return this.http.get(this.usersUrl) 4 .toPromise() 5 // .then(res => res.json().data as User[]) 6 .then(res => res) 7 .catch(this.handleError); 8 }
現在返回的是一個原生態的Response,如果在users組件中打印出這個Response:
1 getUsers(): void { 2 // res是UserService返回的User數組,作為參數傳遞並賦值給組件的users屬性 3 // 使用.then(res => console.log(res))可以將res打印到終端 4 this.userService.getUsersByHttp() 5 // .then(res => this.users = res); 6 .then(res => console.log(res)); 7 }
我們可以看到下面的結果:
圖5:一個標準的Response類
我們可以看到status為200,表示我們請求成功了。在_body的data中,可以看到返回的數據。
6. 一個模擬的服務器端
到目前為止,我們並沒有真正的服務器端,我們的服務器端是利用"angular-in-memory-web-api"模擬出來的一個內存數據庫。因此數據只是保存到了內存,在不刷新的情況下,暫時做到了對數據的持久化。下面是"in-memory-data.service.ts"文件中的內容:
import { InMemoryDbService } from ‘angular-in-memory-web-api‘; export class InMemoryDataService implements InMemoryDbService { // 由於沒有後端,這裏創建了一個內存數據庫來存放數據 createDb() { let users = [ { id: 11, name: ‘Mr. Nice‘, files: [1, 2] }, { id: 12, name: ‘Narco‘, files: [32] }, { id: 13, name: ‘Bombasto‘, files: [11, 5] }, { id: 14, name: ‘Celeritas‘, files: [4, 12] }, { id: 15, name: ‘Magneta‘, files: [6] }, { id: 16, name: ‘RubberMan‘, files: [21] }, { id: 17, name: ‘Dynama‘, files: [3, 7, 9] }, { id: 18, name: ‘Dr IQ‘, files: [] }, { id: 19, name: ‘Magma‘, files: [10] }, { id: 20, name: ‘Tornado‘, files: [8, 13, 14, 16] } ]; let images = [ { id: 1, userId: 11, des: ‘‘, fileUrl: ‘https://raw.githubusercontent.com/OnlyBelter/learn_neuralTalk/master/self_pic/img/butterfly1.jpg‘ }, { id: 2, userId: 11, des: ‘‘, fileUrl: ‘https://raw.githubusercontent.com/OnlyBelter/learn_neuralTalk/master/self_pic/img/cat1.jpg‘ }, { id: 3, userId: 17, des: ‘‘, fileUrl: ‘https://raw.githubusercontent.com/OnlyBelter/learn_neuralTalk/master/self_pic/img/cloud1.jpg‘ }, { id: 4, userId: 14, des: ‘‘, fileUrl: ‘https://raw.githubusercontent.com/OnlyBelter/learn_neuralTalk/master/self_pic/img/river1.jpg‘ }, { id: 5, userId: 13, des: ‘‘, fileUrl: ‘https://raw.githubusercontent.com/OnlyBelter/learn_neuralTalk/master/self_pic/img/flower1.jpg‘ }, { id: 6, userId: 15, des: ‘‘, fileUrl: ‘https://raw.githubusercontent.com/OnlyBelter/learn_neuralTalk/master/self_pic/img/disney1.jpg‘ }, { id: 7, userId: 17, des: ‘‘, fileUrl: ‘https://raw.githubusercontent.com/OnlyBelter/learn_neuralTalk/master/self_pic/img/cloud2.jpg‘ }, { id: 8, userId: 20, des: ‘‘, fileUrl: ‘https://raw.githubusercontent.com/OnlyBelter/learn_neuralTalk/master/self_pic/img/panda1.jpg‘ }, { id: 9, userId: 17, des: ‘‘, fileUrl: ‘https://raw.githubusercontent.com/OnlyBelter/learn_neuralTalk/master/self_pic/img/sunfei2.jpg‘ }, { id: 10, userId: 19, des: ‘‘, fileUrl: ‘https://raw.githubusercontent.com/OnlyBelter/learn_neuralTalk/master/self_pic/img/panda2.jpg‘ }, { id: 11, userId: 13, des: ‘‘, fileUrl: ‘https://raw.githubusercontent.com/OnlyBelter/learn_neuralTalk/master/self_pic/img/flower2.jpg‘ }, { id: 12, userId: 14, des: ‘‘, fileUrl: ‘https://raw.githubusercontent.com/OnlyBelter/learn_neuralTalk/master/self_pic/img/IMG_20161105_100414_A19.jpg‘ }, { id: 13, userId: 20, des: ‘‘, fileUrl: ‘https://raw.githubusercontent.com/OnlyBelter/learn_neuralTalk/master/self_pic/img/panda3.jpg‘ }, { id: 14, userId: 20, des: ‘‘, fileUrl: ‘https://raw.githubusercontent.com/OnlyBelter/learn_neuralTalk/master/self_pic/img/shanghai4.jpg‘ }, { id: 16, userId: 20, des: ‘‘, fileUrl: ‘https://raw.githubusercontent.com/OnlyBelter/learn_neuralTalk/master/self_pic/img/shanghai5.jpg‘ }, { id: 21, userId: 16, des: ‘‘, fileUrl: ‘https://raw.githubusercontent.com/OnlyBelter/learn_neuralTalk/master/self_pic/img/grass.jpg‘ }, { id: 32, userId: 12, des: ‘‘, fileUrl: ‘https://raw.githubusercontent.com/OnlyBelter/learn_neuralTalk/master/self_pic/img/lamp1.jpg‘ }, ]; return {users, images}; } }
查看上面的代碼,我們在內存數據庫中定義了兩個數據庫users和images;因此,我們可以在service中利用http協議中的動詞(get, post, put, delete)通過"api/users"和"api/images"這兩個url地址對這兩數據庫進行操作。在service中,對這種內存數據庫的操作和對真正的利用Django REST框架搭建的API的操作是沒有差別的。下個部分,我會嘗試用Django REST Framework搭建一個可以替代"angular-in-memory-web-api"構建的內存數據庫的,真正意義上的後端。
接下來...
第三部分:後端服務化——Django REST框架
Reference
中文版英雄教程:https://angular.cn/tutorial
https://stackoverflow.com/a/15012542/2803344
https://angular.cn/guide/glossary#observable-對象
https://stackoverflow.com/questions/35763730/difference-between-constructor-and-ngoninit/35763811#35763811
https://github.com/OnlyBelter/image-sharing-system
記Angular與Django REST框架的一次合作(2):前端組件化——Angular