第一節:初識Angular-CLI
第二節:登陸組件的構建
第三節:創建一個待辦事項應用
第四節:進化!模塊化你的應用
第五節:多用戶版本的待辦事項應用
第六節:使用第三方樣式庫及模塊優化用
第七節:給組件帶來活力
Rx--隱藏在Angular 2.x中利劍
Redux你的Angular 2應用
第八節:查缺補漏大合集(上)
第九節:查缺補漏大合集(下)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
看上去咱們須要實現數據庫
根據這個邏輯流程,咱們來組織一下代碼。開始以前咱們想把認證相關的代碼組織在一個新的模塊下,咱們暫時叫它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
,導航過程會終止,且用戶會留在原地(守衛還能夠告訴路由器導航到別處,這樣也取消當前的導航)。
路由器支持多種守衛:
在分層路由的每一個級別上,咱們均可以設置多個守衛。路由器會先按照從最深的子路由由下往上檢查的順序來檢查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對象,這是由於咱們要知道幾件事:
這個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按鈕,能夠嘗試右鍵點擊變量把它放到監視中看看變量值或者逐步調試應用。
本章完整代碼見: github.com/wpcfan/awes…
紙書出版了,比網上內容豐富充實了,歡迎你們訂購!
京東連接:item.m.jd.com/product/120…
第一節: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 (五)
第六節:Angular 2.0 從0到1 (六)
第七節:Angular 2.0 從0到1 (七)
第八節:Angular 2.0 從0到1 (八)