Angular 從 0 到 1 (六)史上最簡單的 Angular 教程

接灰的電子產品發表於2016-12-26

第一節:初識Angular-CLI
第二節:登入元件的構建
第三節:建立一個待辦事項應用
第四節:進化!模組化你的應用
第五節:多使用者版本的待辦事項應用
第六節:使用第三方樣式庫及模組優化用
第七節:給元件帶來活力
Rx--隱藏在 Angular 中利劍
Redux你的 Angular 應用
第八節:查缺補漏大合集(上)
第九節:查缺補漏大合集(下)

第六節:使用第三方樣式庫及模組優化

生產環境初體驗

用angular-cli建立生產環境是非常簡單的,只需輸入ng build --prod --aot即可。--prod會使用生產環境的配置檔案,--aot會使用AOT替代JIT進行編譯。現在實驗一下

Angular 從 0 到 1 (六)史上最簡單的 Angular 教程
image_1b2m0102o1d721c438jr18r9f889.png-238.5kB

仔細看一下命令列輸出,我們應該可以猜到angular移除了一些沒有用到的類庫(Google稱之為Shaking過程),對js和css等進行了壓縮等優化工作。angular在我們的專案根目錄下建立了一個dist資料夾,用於生產環境的檔案就輸出在這個資料夾了。
Angular 從 0 到 1 (六)史上最簡單的 Angular 教程
image_1b2m07bdvqk91aaodsd16pd2kuv.png-116.5kB

我們安裝一個http-server,npm i -g http-server,然後在dist目錄鍵入http-server .。開啟瀏覽器進入http://localhost:8080,我們會看到網頁開啟了。但如果開啟console,或者試著登入一下,你會發現存在很多錯誤。
Angular 從 0 到 1 (六)史上最簡單的 Angular 教程
image_1b2m0l4teqja2f016s61g5o14261c.png-158.4kB

這是由於angular-cli當前的bug產生的,目前需要對路由做hash處理。

...
@NgModule({
  imports: [
    RouterModule.forRoot(routes, { useHash: true })
  ],
  exports: [
    RouterModule
  ]
})
...複製程式碼

只需在app-routing.module.ts中為RouterModule配置{ useHash: true }的屬性即可。這樣的話angular會在url上加上一個#,比如login的url現在是http://localhost:8080/#/login。這樣改動後,功能又好用了。以後我們專案如果需要釋出到生產環境的,大家利用angular-cli可以很方便的處理了。然後下面我們回到開發環境,請關掉8080埠的http伺服器,並刪掉dist。

第三方樣式庫

之前我們使用的是自己為各個元件寫樣式,其實angular團隊有一套官方的符合Material Design的內建元件庫:github.com/angular/mat…(這個庫還屬於早期階段,很多控制元件不可用,所以大家可以關注,但現階段不建議在生產環境中使用)。除了官方之外,目前有大量的比較成熟的樣式庫,比如bootstrap,material-design-lite等。我們這節課以material-design-lite來看一下怎麼使用。Material Desing Lite是Google為web開發的一套基於Material Design的樣式庫。由於是Google開發的,所以你要去訪問之前要科學上網。我們當然可以直接使用官方的css樣式庫,但是有好心人已經幫我們封裝成了比較好用的元件模組了,元件模組的好處是可以使模板寫起來更簡潔,而且易於擴充套件。現在開啟一個terminal輸入npm install --save angular2-mdl。然後在你需要使用MDL元件的模組中引入MdlModule。我們首先希望改造一下我們的AppComponent,目前它只有一句簡陋的文字輸出。

<mdl-layout mdl-layout-fixed-header mdl-layout-header-seamed>
  <mdl-layout-header>
    <mdl-layout-header-row>
      <mdl-layout-title>Awesome Todos</mdl-layout-title>
      <mdl-layout-spacer></mdl-layout-spacer>
      <!-- Navigation. We hide it in small screens. -->
      <nav class="mdl-navigation">
        <a class="mdl-navigation__link">Logout</a>
      </nav>
    </mdl-layout-header-row>
  </mdl-layout-header>
  <mdl-layout-drawer>
    <mdl-layout-title>Title</mdl-layout-title>
    <nav class="mdl-navigation">
      <a class="mdl-navigation__link">Link</a>
    </nav>
  </mdl-layout-drawer>
  <mdl-layout-content class="content">
    <router-outlet></router-outlet>
  </mdl-layout-content>
</mdl-layout>複製程式碼

這段程式碼裡面mdl開頭的標籤都是我們剛引入的元件庫封裝的元件,具體的用法可以去 mseemann.io/angular2-md… 參考文件資料。<mdl-layout></mdl-layout>是一個佈局元件,mdl-layout-fixed-header是一個可以讓header固定在頁面頂部的屬性,mdl-layout-header-seamed是要header沒有陰影。mdl-layout-header是一個頂部元件,mdl-layout-header-row是在頂部元件中形成一行的容器。mdl-layout-spacer是一個佔位的元件,它會把元件剩餘位置佔滿,防止出現錯位。mdl-layout-drawer是一個抽屜元件,和Android的標準應用類似,點選頂部選單圖示會從側面滑出一個選單。別忘了在AppModule中引入

...
import { MdlModule } from 'angular2-mdl';
...
@NgModule({
  ...
  imports: [
    ...
    MdlModule,
    ...
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }複製程式碼

我們為了使用,還需要對顏色做個定製,這個定製需要使用一種CSS的預編譯技術叫SASS,需要建立一個src\styles.scss,然後定義Material Design的顏色,具體顏色名字的定義是在Google調色盤類中定義的,可以去這裡檢視

@import "~angular2-mdl/scss/color-definitions";

$color-primary: $palette-blue-500;
$color-primary-dark: $palette-blue-700;
$color-accent: $palette-amber-A200;
$color-primary-contrast: $color-dark-contrast;
$color-accent-contrast: $color-dark-contrast;

@import '~angular2-mdl/scss/material-design-lite';複製程式碼

由於我們使用的CLI並不知道我們採用了預編譯的css,所以需要改一下angular-cli.json,把styles改寫成下面的樣子

"styles": [
        "styles.scss"
      ],複製程式碼

儲存後開啟瀏覽器看一下效果:

Angular 從 0 到 1 (六)史上最簡單的 Angular 教程
image_1b2g0jju71mdsnd3k2v174k7129.png-11.5kB

我們接下來改造一下login的模板

<div>
  <form (ngSubmit)="onSubmit()">
    <mdl-textfield
      type="text"
      label="Username..."
      name="username"
      floating-label
      required
      [(ngModel)]="username"
      #usernameRef="ngModel"
      >
    </mdl-textfield>
    <div *ngIf="auth?.hasError" >
      {{auth?.errMsg}}
    </div>
    <mdl-textfield
      type="password"
      label="Password..."
      name="password"
      floating-label
      required
      [(ngModel)]="password"
      #passwordRef="ngModel">
    </mdl-textfield>
    <button 
      mdl-button mdl-button-type="raised" 
      mdl-colored="primary" 
      mdl-ripple type="submit">
      Login
    </button>
  </form>
</div>複製程式碼

由於採用了符合Material Design的元件,我們就不需要原來的用於驗證的div了。

Angular 從 0 到 1 (六)史上最簡單的 Angular 教程
image_1b2g1csop1684jfghpphffui9m.png-17kB

下面看一下Todo,原來我們在css中用了svg來改寫核取方塊的樣子,現在我們試試用mdl來做。在todo-list.component.html中把ToggleAll改寫成下面的樣子

<mdl-icon-toggle class="toggle-all" [mdl-ripple]="true" (click)="onToggleAllTriggered()">expand_more</mdl-icon-toggle>複製程式碼

這個標籤是把一個圖示做成可核取方塊的效果,這裡用到了Google的icon font,所以需要在index.html中引入

<!doctype html>
<html>
<head>
  ...
  <link rel="stylesheet" href="https://fonts.lug.ustc.edu.cn/icon?family=Material+Icons">
</head>
<body>
  <app-root>Loading...</app-root>
</body>
</html>複製程式碼

我們用了科大的映象,因為Google的產品,你懂的。
當然TodoItem模板中的checkbox也需要改造成

<mdl-icon-toggle (click)="toggle()" [(ngModel)]="isChecked">check_circle</mdl-icon-toggle>複製程式碼

Todo變成下面的樣子,也還不錯啊~~

Angular 從 0 到 1 (六)史上最簡單的 Angular 教程
image_1b2g1e0261mkmtp61kjm6f94g513.png-81.7kB

模組優化

現在仔細看一下我們的各個模組定義,發現我們不斷地重複引入了CommonModuleFormsModuleMdlModule,這些如果在大部分的元件中都會用到話,我們不妨建立一個SharedModule (src\app\shared\shared.module.ts

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { MdlModule } from 'angular2-mdl';

@NgModule({
  imports: [
    CommonModule,
    FormsModule,
    MdlModule
  ],
  exports: [
    CommonModule,
    FormsModule,
    MdlModule
  ]
})
export class SharedModule { }複製程式碼

這個模組的作用是把常用的元件和模組打包起來(雖然現在沒有元件,只是把常用的模組匯入又匯出),這樣在其他模組中只需引入這個模組即可,比如TodoModule現在看起來是下面的樣子:

...
import { SharedModule } from '../shared/shared.module';
...
@NgModule({
  imports: [
    SharedModule,
    ...
  ],
  declarations: [
    TodoComponent,
    ...
  ],
  providers: [
    {provide: 'todoService', useClass: TodoService}
    ],
})
export class TodoModule {}複製程式碼

多個不同元件間的通訊

下面我們要實現這樣一個功能:在使用者未登入時,頂部選單中只有Login一個連結可見,使用者登入後,頂部選單中有三個連結,一個是Todo,一個是使用者個人資訊,另一個是Logout。按這個需求將頂部選單改造成如下:

<!--src\app\app.component.html-->
<mdl-layout mdl-layout-fixed-header mdl-layout-header-seamed>
  <mdl-layout-header>
    <mdl-layout-header-row>
      <mdl-layout-title>{{title}}</mdl-layout-title>
      <mdl-layout-spacer></mdl-layout-spacer>
      <!-- Navigation. We hide it in small screens. -->
      <nav class="mdl-navigation" *ngIf="auth?.user?.username !== null">
        <a class="mdl-navigation__link" routerLink="todo">Todos</a>
      </nav>
      <nav class="mdl-navigation" *ngIf="auth?.user?.username !== null">
        <a class="mdl-navigation__link" routerLink="profile">{{auth.user.username}}</a>
      </nav>
      <nav class="mdl-navigation">
        <a class="mdl-navigation__link" *ngIf="auth?.user?.username === null" (click)="login()">
          Login
        </a>
        <a class="mdl-navigation__link" *ngIf="auth?.user?.username !== null" (click)="logout()">
          Logout
        </a>
      </nav>
    </mdl-layout-header-row>
  </mdl-layout-header>
  <mdl-layout-drawer>
    <mdl-layout-title>{{title}}</mdl-layout-title>
    <nav class="mdl-navigation">
      <a class="mdl-navigation__link">Link</a>
    </nav>
  </mdl-layout-drawer>
  <mdl-layout-content class="content">
    <router-outlet></router-outlet>
  </mdl-layout-content>
</mdl-layout>複製程式碼

這樣改造完後的頁面結構是頂部選單隻載入一次,底下的內容隨著不同路由顯示不同內容。但如果我們要在login後頂部選單也隨之改變的話,我們一定要實現某種通訊機制。前面我們講過EventEmiiter,當然我們可以將整個頁面當成父控制元件,頂部選單是子控制元件的形式,但這時你發現由於我們是用路由插座(<router-outlet></router-outlet>) l來顯示內容的,所以無法採用子控制元件的形式傳遞資訊。

這種情況就要引入Rx了,rx的學習門檻較高,也不是本教程的重點,但我還是這裡嘗試著解釋一下。Rx是響應式程式設計的利器,它的學習門檻來自於思維方式的轉變,從傳統的程式設計思維轉成流式思維:Rx總體來看是一個資料流或訊號流,所有的操作符都是為了對這個流進行控制。寫Rx時要對系統資料或訊號的完整邏輯流程先想清楚,然後就比較好寫了。

其實在Angular2中,Rx是無處不在的,還記得我們之前總用到toPromise()這個方法嗎?其實這個方法是給不太熟悉Rx的同學用的,Angular本身返回的就是Observable。我們現在把UserService改成Rx版本

import { Injectable } from '@angular/core';

import { Http, Headers, Response } from '@angular/http';
import { Observable } from 'rxjs/Rx';
import 'rxjs/add/operator/map';

import { User } from '../domain/entities';

@Injectable()
export class UserService {

  private api_url = 'http://localhost:3000/users';

  constructor(private http: Http) { }

  getUser(userId: number): Observable<User> {
    const url = `${this.api_url}/${userId}`;
    return this.http.get(url)
              .map(res => res.json() as User);
  }
  findUser(username: string): Observable<User> {
    const url = `${this.api_url}/?username=${username}`;
    return this.http.get(url)
              .map(res => {
                let users = res.json() as User[];
                return (users.length>0) ? users[0] : null;
              });
  }
}複製程式碼

大家可能注意到了,其實有沒有Promise都無所謂,大概的寫法也是類似的,只不過返回的是Observable。這裡改了之後,相關呼叫的地方都要改一下,比如LoginComponent:

import { Component, 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 {

  username = '';
  password = '';
  auth: Auth;
  constructor(@Inject('auth') private service, private router: Router) { }

  onSubmit(){
    this.service
      .loginWithCredentials(this.username, this.password)
      .subscribe(auth => {
        this.auth = Object.assign({}, auth);
        if(!auth.hasError){
          this.router.navigate(['todo']);
        }
      });
  }
}複製程式碼

AuthService也需要改寫一下

import { Injectable, Inject } from '@angular/core';
import { Http, Headers, Response } from '@angular/http';

import { ReplaySubject, Observable } from 'rxjs/Rx';
import 'rxjs/add/operator/map';
import { Auth } from '../domain/entities';

@Injectable()
export class AuthService {
  auth: Auth = {hasError: true, redirectUrl: '', errMsg: 'not logged in'};
  subject: ReplaySubject<Auth> = new ReplaySubject<Auth>(1);
  constructor(private http: Http, @Inject('user') private userService) {
  }
  getAuth(): Observable<Auth> {
    return this.subject.asObservable();
  }
  unAuth(): void {
    this.auth = Object.assign(
      {},
      this.auth,
      {user: null, hasError: true, redirectUrl: '', errMsg: 'not logged in'});
    this.subject.next(this.auth);
  }
  loginWithCredentials(username: string, password: string): Observable<Auth> {
    return this.userService
      .findUser(username)
      .map(user => {
        let auth = new Auth();
        if (null === user){
          auth.user = null;
          auth.hasError = true;
          auth.errMsg = 'user not found';
        } else if (password === user.password) {
          auth.user = user;
          auth.hasError = false;
          auth.errMsg = null;
        } else {
          auth.user = null;
          auth.hasError = true;
          auth.errMsg = 'password not match';
        }
        this.auth = Object.assign({}, auth);
        this.subject.next(this.auth);
        return this.auth;
      });
  }
}複製程式碼

這裡注意到我們引入了一個新概念:Subject。Subject 既是Observer(觀察者)也是Observable(被觀察物件)。這裡採用Subject的原因是我們在Login時改變了Auth的屬性,但由於這個Login方法是Login頁面顯性呼叫的,其他需要觀察Auth變化的地方呼叫的是getAuth()方法。這樣的話,我們需要在Auth發生變化時推送變化出去,我們在loginWithCredentials方法中以this.subject.next(this.auth);寫入其變化,在getAuth()中用return this.subject.asObservable();將Subject轉換成Observable。

Auth:{}     Auth{user: {id: 1...}}     沒有Auth資料發射了
|============|===========================|=====
登入前      登入後                     todo路由守衛啟用複製程式碼

但為什麼是ReplaySubject呢?我們在執行登入時,如果鑑權成功,會導航到某個路由(這裡是todo),這時會引發CanActivate的檢查,而此時最新的Auth已經發射完畢,CanActivate檢查時會發現沒有Auth資料。這種情況下我們需要快取最近的一份Auth資料,無論誰,什麼時間訂閱,只要沒有更新的資料,我們就推送最近的一份給它,這就是ReplaySubject的意義所在。

下面我們改寫路由守衛

import { Injectable, Inject } from '@angular/core';
import {
  CanActivate,
  CanLoad,
  Router,
  Route,
  ActivatedRouteSnapshot,
  RouterStateSnapshot }    from '@angular/router';
import { Observable } from 'rxjs/Rx';
import 'rxjs/add/operator/map';

@Injectable()
export class AuthGuardService implements CanActivate, CanLoad {

  constructor(
    private router: Router,
    @Inject('auth') private authService) { }

  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
    let url: string = state.url;

    return this.authService.getAuth()
      .map(auth => !auth.hasError);
  }
  canLoad(route: Route): Observable<boolean> {
    let url = `/${route.path}`;

    return this.authService.getAuth()
      .map(auth => !auth.hasError);
  }
}複製程式碼

這裡你會發現多了一個canLoad方法,canActivate是用於是否可以進入某個url,而canLoad是決定是否載入某個url對應的模組。所以需要再改下路由

import { NgModule }     from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { LoginComponent } from './login/login.component';
import { AuthGuardService } from './core/auth-guard.service';

const routes: Routes = [
  {
    path: '',
    redirectTo: 'login',
    pathMatch: 'full'
  },
  {
    path: 'todo',
    redirectTo: 'todo/ALL',
    canLoad: [AuthGuardService]
  }
];

@NgModule({
  imports: [
    RouterModule.forRoot(routes, { useHash: true })
  ],
  exports: [
    RouterModule
  ]
})
export class AppRoutingModule {}複製程式碼

現在開啟瀏覽器欣賞一下我們的成果。

Angular 從 0 到 1 (六)史上最簡單的 Angular 教程
image_1b2o9tso51ald1u0e1nr59gi119i9.png-66.5kB

本節程式碼:github.com/wpcfan/awes…

紙書出版了,比網上內容豐富充實了,歡迎大家訂購!
京東連結:item.m.jd.com/product/120…

Angular 從 0 到 1 (六)史上最簡單的 Angular 教程
Angular從零到一

相關文章