第一節:Angular 2.0 從0到1 (一)
第二節:Angular 2.0 從0到1 (二)
第三節:Angular 2.0 從0到1 (三)
第四節:Angular 2.0 從0到1 (四)
第五節:Angular 2.0 從0到1 (五)javascript
第四節咱們完成的Todo的基本功能看起來還不錯,可是有個大問題,就是每一個用戶看到的都是同樣的待辦事項,咱們但願的是每一個用戶擁有本身的待辦事項列表。咱們來分析一下怎麼作,若是每一個todo對象帶一個UserId屬性是否是能夠解決呢?好像能夠,邏輯大概是這樣:用戶登陸後轉到/todo,TodoComponent獲得當前用戶的UserId,而後調用TodoService中的方法,傳入當前用戶的UserId,TodoService中按UserId去篩選當前用戶的Todos。
但惋惜咱們目前的LoginComponent仍是個實驗品,不少功能的缺失,咱們是先去作Login呢,仍是利用現有的Todo對象先試驗一下呢?我我的的習慣是先進行試驗。css
按以前咱們分析的,給todo加一個userId屬性,咱們手動給咱們目前的數據加上userId屬性吧。更改todo\todo-data.json
爲下面的樣子:html
{ "todos": [ { "id": "bf75769b-4810-64e9-d154-418ff2dbf55e", "desc": "getting up", "completed": false, "userId": 1 }, { "id": "5894a12f-dae1-5ab0-5761-1371ba4f703e", "desc": "have breakfast", "completed": true, "userId": 2 }, { "id": "0d2596c4-216b-df3d-1608-633899c5a549", "desc": "go to school", "completed": true, "userId": 1 }, { "id": "0b1f6614-1def-3346-f070-d6d39c02d6b7", "desc": "test", "completed": false, "userId": 2 }, { "id": "c1e02a43-6364-5515-1652-a772f0fab7b3", "desc": "This is a te", "completed": false, "userId": 1 } ] }
若是你尚未啓動json-server的話讓咱們啓動它: json-server ./src/app/todo/todo-data.json
,而後打開瀏覽器在地址欄輸入http://localhost:3000/todos/?userId=2
你會看到只有userId=2
的json被輸出了java
[ { "id": "5894a12f-dae1-5ab0-5761-1371ba4f703e", "desc": "have breakfast", "completed": true, "userId": 2 }, { "id": "0b1f6614-1def-3346-f070-d6d39c02d6b7", "desc": "test", "completed": false, "userId": 2 } ]
有興趣的話能夠再試試http://localhost:3000/todos/?userId=2&completed=false
或其餘組合查詢。如今todo
有了userId
字段,但咱們尚未User對象,User的json表現形式看起來應該是這樣:webpack
{ "id": 1, "username": "wang", "password": "1234" }
固然這個表現形式有不少問題,好比密碼是明文的,這些問題咱們先無論,但大概樣子是相似的。那麼如今若是要創建User數據庫的話,咱們應該新建一個user-data.json
git
{ "users": [ { "id": 1, "username": "wang", "password": "1234" }, { "id": 2, "username": "peng", "password": "5678" } ] }
但這樣作的話感受單獨爲其建一個文件有點不值得,咱們乾脆把user和todo數據都放在一個文件吧,如今刪除./src/app/todo/todo-data.json
刪除,在src\app
下面新建一個data.json
github
//src\app\data.json { "todos": [ { "id": "bf75769b-4810-64e9-d154-418ff2dbf55e", "desc": "getting up", "completed": false, "userId": 1 }, { "id": "5894a12f-dae1-5ab0-5761-1371ba4f703e", "desc": "have breakfast", "completed": true, "userId": 2 }, { "id": "0d2596c4-216b-df3d-1608-633899c5a549", "desc": "go to school", "completed": true, "userId": 1 }, { "id": "0b1f6614-1def-3346-f070-d6d39c02d6b7", "desc": "test", "completed": false, "userId": 2 }, { "id": "c1e02a43-6364-5515-1652-a772f0fab7b3", "desc": "This is a te", "completed": false, "userId": 1 } ], "users": [ { "id": 1, "username": "wang", "password": "1234" }, { "id": 2, "username": "peng", "password": "5678" } ] }
固然有了數據,咱們就得有對應的對象,基於一樣的理由,咱們把全部的entity對象都放在一個文件:刪除src\app\todo\todo.model.ts
,在src\app
下新建一個目錄domain,而後在domain下新建一個entities.ts
,請別忘了更新全部的引用。web
export class Todo { id: string; desc: string; completed: boolean; userId: number; } export class User { id: number; username: string; password: string; }
咱們來梳理一下用戶驗證的流程chrome
存儲要訪問的URL數據庫
根據本地的已登陸標識判斷是否此用戶已經登陸,若是已登陸就直接放行
若是未登陸導航到登陸頁面 用戶填寫用戶名和密碼進行登陸
系統根據用戶名查找用戶表中是否存在此用戶,若是不存在此用戶,返回錯誤
若是存在對比填寫的密碼和存儲的密碼是否一致,若是不一致,返回錯誤
若是一致,存儲此用戶的已登陸標識到本地
導航到本來要訪問的URL即第一步中存儲的URL,刪掉本地存儲的URL
看上去咱們須要實現
UserService:用於經過用戶名查找用戶並返回用戶
AuthService:用於認證用戶,其中須要利用UserService的方法
AuthGuard:路由攔截器,用於攔截到路由後經過AuthService來知道此用戶是否有權限訪問該路由,根據結果導航到不一樣路徑。
看到這裏,你可能有些疑問,爲何咱們不把UserService和AuthService合併呢?這是由於UserService是用於對用戶的操做的,不光認證流程須要用到它,咱們將來要實現的一系列功能都要用到它,好比註冊用戶,後臺用戶管理,以及主頁要顯示用戶名稱等。
根據這個邏輯流程,咱們來組織一下代碼。開始以前咱們想把認證相關的代碼組織在一個新的模塊下,咱們暫時叫它core
吧。在src\app
下新建一個core
目錄,而後在core
下面新建一個core.module.ts
import { ModuleWithProviders, NgModule, Optional, SkipSelf } from '@angular/core'; import { CommonModule } from '@angular/common'; @NgModule({ imports: [ CommonModule ] }) export class CoreModule { constructor (@Optional() @SkipSelf() parentModule: CoreModule) { if (parentModule) { throw new Error( 'CoreModule is already loaded. Import it in the AppModule only'); } }
注意到這個模塊和其餘模塊不太同樣,緣由是咱們但願只在應用啓動時導入它一次,而不會在其它地方導入它。在模塊的構造函數中咱們會要求Angular把CoreModule注入自身,這看起來像一個危險的循環注入。不過,@SkipSelf
裝飾器意味着在當前注入器的全部祖先注入器中尋找CoreModule。若是該構造函數在咱們所指望的AppModule中運行,就沒有任何祖先注入器可以提供CoreModule的實例,因而注入器會放棄查找。默認狀況下,當注入器找不到想找的提供商時,會拋出一個錯誤。 但@Optional
裝飾器表示找不到該服務也無所謂。 因而注入器會返回null,parentModule參數也就被賦成了空值,而構造函數沒有任何異常。
那麼咱們在何時會須要這樣一個模塊?好比在這個模塊中咱們可能會要提供用戶服務(UserService),這樣的服務系統各個地方都須要,但咱們不但願它被建立屢次,但願它是一個單例。再好比某些只應用於AppComponent
模板的一次性組件,沒有必要共享它們,然而若是把它們留在根目錄,仍是顯得太亂了。咱們能夠經過這種形式隱藏它們的實現細節。而後經過根模塊AppModule導入CoreModule來獲取其能力。
首先咱們來看看Angular內建的路由守衛機制,在實際工做中咱們經常會碰到下列需求:
該用戶可能無權導航到目標組件。 導航前須要用戶先登陸(認證)。
在顯示目標組件前,咱們可能得先獲取某些數據。
在離開組件前,咱們可能要先保存修改。
咱們可能要詢問用戶:你是否要放棄本次更改,而不用保存它們?
咱們能夠往路由配置中添加守衛,來處理這些場景。守衛返回true
,導航過程會繼續;返回false
,導航過程會終止,且用戶會留在原地(守衛還能夠告訴路由器導航到別處,這樣也取消當前的導航)。
路由器支持多種守衛:
用CanActivate來處理導航到某路由的狀況。
用CanActivateChild處理導航到子路由的狀況。
用CanDeactivate來處理從當前路由離開的狀況。
用Resolve在路由激活以前獲取路由數據。
用CanLoad來處理異步導航到某特性模塊的狀況。
在分層路由的每一個級別上,咱們均可以設置多個守衛。路由器會先按照從最深的子路由由下往上檢查的順序來檢查CanDeactivate
守護條件。而後它會按照從上到下的順序檢查CanActivate
守衛。若是任何守衛返回false
,其它還沒有完成的守衛會被取消,這樣整個導航就被取消了。
本例中咱們但願用戶未登陸前不能訪問todo,那麼須要使用CanActivate
import { AuthGuardService } from '../core/auth-guard.service'; const routes: Routes = [ { path: 'todo/:filter', canActivate: [AuthGuardService], component: TodoComponent } ];
固然光這麼寫是沒有用的,下面咱們來創建一個AuthGuardService
,命令行中鍵入ng g s core/auth-guard
(angular-cli對於Camel寫法的文件名是採用-
來分隔每一個大寫的詞)。
import { Injectable, Inject } from '@angular/core'; import { CanActivate, Router, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; @Injectable() export class AuthGuardService implements CanActivate { constructor(private router: Router) { } canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean { //取得用戶訪問的URL let url: string = state.url; return this.checkLogin(url); } checkLogin(url: string): boolean { //若是用戶已經登陸就放行 if (localStorage.getItem('userId') !== null) { return true; } //不然,存儲要訪問的URl到本地 localStorage.setItem('redirectUrl', url); //而後導航到登錄頁面 this.router.navigate(['/login']); //返回false,取消導航 return false; } }
觀察上面代碼,咱們發現本地存儲的userId的存在與否決定了用戶是否已登陸的狀態,這固然是一個漏洞百出的實現,但咱們暫且不去管它。如今咱們要在登陸時把這個狀態值寫進去。咱們新建一個登陸鑑權的AuthService
:ng g s core/auth
import { Injectable, Inject } from '@angular/core'; import { Http, Headers, Response } from '@angular/http'; import 'rxjs/add/operator/toPromise'; import { Auth } from '../domain/entities'; @Injectable() export class AuthService { constructor(private http: Http, @Inject('user') private userService) { } loginWithCredentials(username: string, password: string): Promise<Auth> { return this.userService .findUser(username) .then(user => { let auth = new Auth(); localStorage.removeItem('userId'); let redirectUrl = (localStorage.getItem('redirectUrl') === null)? '/': localStorage.getItem('redirectUrl'); auth.redirectUrl = redirectUrl; if (null === user){ auth.hasError = true; auth.errMsg = 'user not found'; } else if (password === user.password) { auth.user = Object.assign({}, user); auth.hasError = false; localStorage.setItem('userId',user.id); } else { auth.hasError = true; auth.errMsg = 'password not match'; } return auth; }) .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); } }
注意到咱們返回了一個Auth對象,這是由於咱們要知道幾件事:
用戶最初要導航的頁面URL
用戶對象
若是發生錯誤的話,是什麼錯誤,咱們須要反饋給用戶
這個Auth對象一樣在src\app\domain\entities.ts
中聲明
export class Auth { user: User; hasError: boolean; errMsg: string; redirectUrl: string; }
固然咱們還得實現UserService:ng g s user
import { Injectable } from '@angular/core'; import { Http, Headers, Response } from '@angular/http'; import 'rxjs/add/operator/toPromise'; import { User } from '../domain/entities'; @Injectable() export class UserService { private api_url = 'http://localhost:3000/users'; constructor(private http: Http) { } findUser(username: string): Promise<User> { const url = `${this.api_url}/?username=${username}`; return this.http.get(url) .toPromise() .then(res => { let users = res.json() as User[]; return (users.length>0)?users[0]: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); } }
這段代碼比較簡單,就不細講了。下面咱們改造一下src\app\login\login.component.html
,在原來用戶名的驗證信息下加入,用於顯示用戶不存在或者密碼不對的狀況
<div *ngIf="usernameRef.errors?.required">this is required</div> <div *ngIf="usernameRef.errors?.minlength">should be at least 3 charactors</div> <!--add the code below--> <div *ngIf="auth?.hasError">{{auth.errMsg}}</div>
固然咱們還得改造src\app\login\login.component.ts
import { Component, OnInit, Inject } from '@angular/core'; import { Router, ActivatedRoute, Params } from '@angular/router'; import { Auth } from '../domain/entities'; @Component({ selector: 'app-login', templateUrl: './login.component.html', styleUrls: ['./login.component.css'] }) export class LoginComponent implements OnInit { username = ''; password = ''; auth: Auth; constructor(@Inject('auth') private service, private router: Router) { } ngOnInit() { } onSubmit(formValue){ this.service .loginWithCredentials(formValue.login.username, formValue.login.password) .then(auth => { let redirectUrl = (auth.redirectUrl === null)? '/': auth.redirectUrl; if(!auth.hasError){ this.router.navigate([redirectUrl]); localStorage.removeItem('redirectUrl'); } else { this.auth = Object.assign({}, auth); } }); } }
而後咱們別忘了在core模塊中聲明咱們的服務src\app\core\core.module.ts
import { ModuleWithProviders, NgModule, Optional, SkipSelf } from '@angular/core'; import { CommonModule } from '@angular/common'; import { AuthService } from './auth.service'; import { UserService } from './user.service'; import { AuthGuardService } from './auth-guard.service'; @NgModule({ imports: [ CommonModule ], providers: [ { provide: 'auth', useClass: AuthService }, { provide: 'user', useClass: UserService }, AuthGuardService ] }) export class CoreModule { constructor (@Optional() @SkipSelf() parentModule: CoreModule) { if (parentModule) { throw new Error( 'CoreModule is already loaded. Import it in the AppModule only'); } } }
最後咱們得改寫一下TodoService
,由於咱們訪問的URL變了,要傳遞的數據也有些變化
//todo.service.ts代碼片斷 // POST /todos addTodo(desc:string): Promise<Todo> { //「+」是一個簡易方法能夠把string轉成number const userId:number = +localStorage.getItem('userId'); let todo = { id: UUID.UUID(), desc: desc, completed: false, userId }; return this.http .post(this.api_url, JSON.stringify(todo), {headers: this.headers}) .toPromise() .then(res => res.json() as Todo) .catch(this.handleError); } // GET /todos getTodos(): Promise<Todo[]>{ const userId = +localStorage.getItem('userId'); const url = `${this.api_url}/?userId=${userId}`; return this.http.get(url) .toPromise() .then(res => res.json() as Todo[]) .catch(this.handleError); } // GET /todos?completed=true/false filterTodos(filter: string): Promise<Todo[]> { const userId:number = +localStorage.getItem('userId'); const url = `${this.api_url}/?userId=${userId}`; switch(filter){ case 'ACTIVE': return this.http .get(`${url}&completed=false`) .toPromise() .then(res => res.json() as Todo[]) .catch(this.handleError); case 'COMPLETED': return this.http .get(`${url}&completed=true`) .toPromise() .then(res => res.json() as Todo[]) .catch(this.handleError); default: return this.getTodos(); } }
如今應該已經ok了,咱們來看看效果:
用戶密碼不匹配時,顯示password not match
用戶不存在時,顯示user not found
直接在瀏覽器地址欄輸入http://localhost:4200/todo
,你會發現被從新導航到了login
。輸入正確的用戶名密碼後,咱們被導航到了todo,如今每一個用戶均可以建立屬於本身的待辦事項了。
Angular團隊推薦把路由模塊化,這樣便於使業務邏輯和路由鬆耦合。雖然目前在咱們的應用中感受用處不大,但按官方推薦的方式仍是和你們一塊兒改造一下吧。刪掉原有的app.routes.ts
和todo.routes.ts
。添加app-routing.module.ts
:
import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { LoginComponent } from './login/login.component'; const routes: Routes = [ { path: '', redirectTo: 'login', pathMatch: 'full' }, { path: 'login', component: LoginComponent }, { path: 'todo', redirectTo: 'todo/ALL' } ]; @NgModule({ imports: [ RouterModule.forRoot(routes) ], exports: [ RouterModule ] }) export class AppRoutingModule {}
以及src\app\todo\todo-routing.module.ts
import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { TodoComponent } from './todo.component'; import { AuthGuardService } from '../core/auth-guard.service'; const routes: Routes = [ { path: 'todo/:filter', canActivate: [AuthGuardService], component: TodoComponent } ]; @NgModule({ imports: [ RouterModule.forChild(routes) ], exports: [ RouterModule ] }) export class TodoRoutingModule { }
並分別在AppModule和TodoModule中引入路由模塊。
有讀者問如何用vscode進行debug,這章咱們來介紹一下。首先須要安裝一個vscode插件,點擊左側最下面的圖標或者「在查看菜單中選擇命令面板,輸入install,選擇擴展:安裝擴展」,而後輸入「debugger for chrome」回車,點擊安裝便可。
而後點擊最左邊的倒數第二個按鈕
若是是第一次使用的話,齒輪圖標上會有個紅點,點擊選擇debugger for chrome
,vscode會幫你建立一個配置文件,這個文件位於\.vscode\launch.json
是debugger的配置文件,請改寫成下面的樣子。注意若是是MacOSX或者Linux,請把userDataDir
替換成對應的臨時目錄,另外把"webpack:///C:*":"C:/*"
替換成"webpack:///*": "/*"
,這句是由於angular-cli是採用webpack打包的,若是沒有使用angular-cli不須要添加這句。
{ "version": "0.2.0", "configurations": [ { "name": "Launch Chrome against localhost, with sourcemaps", "type": "chrome", "request": "launch", "url": "http://localhost:4200", "sourceMaps": true, "runtimeArgs": [ "--disable-session-crashed-bubble", "--disable-infobars" ], "diagnosticLogging": true, "webRoot": "${workspaceRoot}/src", //windows setup "userDataDir": "C:\\temp\\chromeDummyDir", "sourceMapPathOverrides": { "webpack:///C:*":"C:/*" //use "webpack:///*": "/*" on Linux/OSX } }, { "name": "Attach to Chrome, with sourcemaps", "type": "chrome", "request": "attach", "port": 9222, "sourceMaps": true, "diagnosticLogging": true, "webRoot": "${workspaceRoot}/src", "sourceMapPathOverrides": { "webpack:///C:*":"C:/*" } } ] }
如今你能夠試着在源碼中設置一個斷點,點擊debug視圖中的debug按鈕,能夠嘗試右鍵點擊變量把它放到監視中看看變量值或者逐步調試應用。
本章完整代碼見: https://github.com/wpcfan/awe...
第一節:Angular 2.0 從0到1 (一)
第二節:Angular 2.0 從0到1 (二)
第三節:Angular 2.0 從0到1 (三)
第四節:Angular 2.0 從0到1 (四)
第五節:Angular 2.0 從0到1 (五)