1. 程式人生 > >記Angular與Django REST框架的一次合作(2):前端組件化——Angular

記Angular與Django REST框架的一次合作(2):前端組件化——Angular

服務器 信息 outer demo cli 組成 控制 set 根據

:這是這個系列的第二部分,主要集中在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.userServicethis.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};
  }
}

查看上面的代碼,我們在內存數據庫中定義了兩個數據庫usersimages;因此,我們可以在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