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

接灰的電子產品發表於2017-01-04

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

第八章:查缺補漏大合集(上)

這一章其實是我在前七章讀者評論和私信交流時發現很多點我是要麼漏掉了,要麼自己理解有誤。那這第八和第九章就來做一個小總結吧。本章我們討論如何在Angular2中引入第三方JS庫、惰性載入路由和子路由。

第三方JS類庫的引入

這個是許多人的困惑,我們在Angular2中使用了TypeScript,但大量的類庫是沒有TypeScript怎麼辦?其實不用擔心,非常簡單。但在講方法前,我們最好還是理解一下背景。

為什麼JS不能直接使用

由於TypeScript是一個強型別語言,所以對於第三方類庫,我們需要知道它們的JavaScript裡面的暴露給外部使用的這些物件和方法的型別定義是什麼。

這個型別定義檔案長什麼樣呢?我們來看一看,你可以進入工程下的node_modules中的 @angular/common/src/directives/ng_class.d.ts:


import { DoCheck, ElementRef, IterableDiffers, KeyValueDiffers, Renderer } from '@angular/core';

export declare class NgClass implements DoCheck {
    private _iterableDiffers;
    private _keyValueDiffers;
    private _ngEl;
    private _renderer;
    private _iterableDiffer;
    private _keyValueDiffer;
    private _initialClasses;
    private _rawClass;
    constructor(_iterableDiffers: IterableDiffers, _keyValueDiffers: KeyValueDiffers, _ngEl: ElementRef, _renderer: Renderer);
    klass: string;
    ngClass: string | string[] | Set<string> | {
        [klass: string]: any;
    };
    ngDoCheck(): void;
    private _cleanupClasses(rawClassVal);
    private _applyKeyValueChanges(changes);
    private _applyIterableChanges(changes);
    private _applyInitialClasses(isCleanup);
    private _applyClasses(rawClassVal, isCleanup);
    private _toggleClass(klass, enabled);
}複製程式碼

可以看到這個檔案其實就是用來做型別定義宣告的,我們一般把這種以 .d.ts 字尾結尾的檔案叫做型別定義檔案(Type Definition)。有了這個宣告定義,我們就可以在TypeScript中使用了。這個檔案看起來也挺麻煩的,事實上真正需要你自己動手寫的類庫很少。我們來看一下一般的整合第三方類庫的過程是什麼樣子的。

標準的JS庫引入方法

我們拿百度的echarts (github.com/ecomfe/echa… npm install --save echarts ,然後我們安裝其型別定義檔案,在命令列視窗輸入 npm install --save-dev @types/echarts 。然後。。就沒有然後了。這麼簡單嗎?是滴。

注意兩件事,首先我們安裝時使用了 --save-dev 開關,因為這個型別定義檔案只對開發時有用,它並不是我們工程的依賴,只是為了編寫時的方便。
第二件事我們使用了 @types/echarts 這樣一個有點怪的名稱,其實是這樣的,微軟維護了一個海量的型別定義資料中心,這個就是 @types。那麼我們為了尋找echarts就會在 @types 這個目錄下搜尋它的二級目錄。

這樣安裝之後,你可以在本地工程目錄下的 node_modules/@types/echarts/index.d.ts 找到echarts的定義:

// Type definitions for echarts
// Project: http://echarts.baidu.com/
// Definitions by: Xie Jingyang <https://github.com/xieisabug>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped

declare namespace ECharts {
    function init(dom:HTMLDivElement|HTMLCanvasElement, theme?:Object|string, opts?:{
        devicePixelRatio?: number
        renderer?: string
    }):ECharts;

   …//此處省略大部分宣告,可以查閱本地檔案
}

declare module "echarts" {
    export = ECharts;
}複製程式碼

一般情況下,到這步就結束了,此時我們可以試驗一下是否可以使用了,在一個元件檔案中嘗試引入echarts,如果你看到了智慧提示中有你希望引入的類庫中的方法或物件,那就一切順利,接下來就可以正常使用這個類庫了。

Angular 從0到1 (八):史上最簡單的 Angular 教程
引入echarts看到智慧提示

引入庫的特殊情況

但有的時候,我們執行第二步 npm install --save-dev @types/echarts 時,會發現沒有找到對應的型別定義檔案。這個時候怎麼辦呢?
這時候要分兩種情況看,首先應該去檢查一下node_modules目錄中的你要使用的類庫子目錄(本例中是echarts)中是否有型別定義檔案,因為有的類庫會把型別定義檔案直接打包在npm的包中。比如我們前幾章接觸的angular-uuid,這個類庫其實就是直接把型別定義檔案打包在npm package中的。看下圖,如果是這種情況,那麼我們什麼都不需要做,直接使用就好了。

Angular 從0到1 (八):史上最簡單的 Angular 教程
有的類庫直接將型別定義打包在npm中

當然還有一種情形就是,這樣也找不到,或者這個類庫是我們的團隊已有的、自己寫的等等情況。這時候就得自己寫一下,也很簡單,在 src/typings.d.ts 中加上一行:

declare module 'echarts';複製程式碼

然後在要使用此類庫的元件中引入:

import * as echarts from 'echarts';複製程式碼

後面就可以正常使用了,當然這種新增方式是沒有智慧提示和自動完成的,你需要自己保證呼叫的正確性。如果覺得不爽,還是希望有提示、型別檢查等等,那就得自己寫一個型別定義檔案了,可以參考 basarat.gitbooks.io/typescript/… 去編寫自己的型別定義檔案。

惰性路由和子路由

惰性路由

在需求和功能不斷新增和修改之後,應用的尺寸將會變得更大。在某一個時間點,我們將達到一個頂點,應用 將會需要過多的時間來載入。這會帶來一定的效能問題。
如何才能解決這個問題呢?Angular2引進了非同步路由,我們可以惰性載入指定的模組或元件。這樣給我們帶來了下列好處:

  • 可以繼續開發我們的新功能,但不再增加初始載入檔案的大小。
  • 只有在使用者請求時才載入特定模組。
  • 為那些只訪問應用程式某些區域的使用者加快載入速度

還是我們一起打造一個例子說明一下,之後大家就可以清楚的理解這個概念了。我們新建一個叫Playground的module。開啟一個命令列視窗,輸入 ng g m playgorund ,這樣Angular CLI非常聰明的幫我們建立了PlaygroundModule,不光如此,它還幫我們建立了一個PlaygroundComponent。因為一般來說,我們新建一個模組肯定會至少有一個元件的。
由於要做惰性載入,我們並不需要在根模組AppModule中引入這個模組,所以我們檢查一下根模組 src/app/app.module.ts 中是否引入了PlaygroundModule,如果有,請去掉。
首先為PlaygroundModule建立自己模組的路由,我們如果遵守Google的程式碼風格建議的話,那麼就應該為每個模組建立獨立的路由檔案。

const routes: Routes = [
  { path: '', component: PlaygroundComponent },
];

@NgModule({
  imports: [ RouterModule.forChild(routes) ],
  exports: [ RouterModule ],
})
export class PlaygroundRoutingModule { }複製程式碼

在src/app/app-routing.module.ts中我們要新增一個惰性路由指向PlaygroundModule

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: 'playground',
    loadChildren: 'app/playground/playground.module#PlaygroundModule',
  }
];

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

在這段程式碼中我們看到一個新面孔,loadChildren 。路由器用 loadChildren 屬性來對映我們希望惰性載入的模組檔案,這裡是 PlaygroundModule 。路由器將接收我們的 loadChildren 字串,並把它動態載入進 PlaygroundModule ,它的路由被動態合併到我們的配置中,然後載入所請求的路由。但只有在首次載入該路由時才會這樣做,後續的請求都會立即完成。
app/playground/playground.module#PlaygroundModule 這個表示式是這樣的規則:模組的路徑#模組名稱
現在我們回顧一下,在應用啟動時,我們並沒有載入PlaygroundModule,因為在AppModule中沒有它的引用。但是當你在瀏覽器中手動輸入 http://localhost:4200/playground 時,系統在此時載入 PlaygroundModule

子路由

程式複雜了之後,一層的路由可能就不會夠用了,在一個模組內部由於功能較複雜,需要再劃分出二級甚至更多級別的路徑。這種情況下我們就需要Angular2提供的一個內建功能叫做:子路由。
我們向來認為例子是最好的說明,所以還是來做一個小功能:現在我們需要對一個叫playground的路徑下新增子路由,子路由有2個:one和two。其中one下面還有一層路徑叫three。形象的表示一下,就像下面的結構一樣。

/playground---|
              |/one
              |--------|three
              |/two複製程式碼

那麼我們還是先在專案工程目錄輸入 ng g c playground/one,然後再執行 ng g c playground/two ,還有一個three,所以再來:ng g c playground/three
現在我們有了三個元件,看看怎麼處理路由吧,原有的模組路由檔案如下:

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

import { PlaygroundComponent } from './playground.component';

const routes: Routes = [
  { 
    path: '', 
    component: PlaygroundComponent
  },
];

@NgModule({
  imports: [ RouterModule.forChild(routes) ],
  exports: [ RouterModule ],
})
export class PlaygroundRoutingModule { }複製程式碼

我們首先需要在模組的根路由下新增one和two,Angular2在路由定義陣列中對於每個路由定義物件都有一個屬性叫做children,這裡就是指定子路由的地方了。所以在下面程式碼中我們把one和two都放入了children陣列中。

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

import { PlaygroundComponent } from './playground.component';
import { OneComponent } from './one/one.component';
import { TwoComponent } from './two/two.component';

const routes: Routes = [
  { 
    path: '', 
    component: PlaygroundComponent,
    children: [
      {
        path: 'one',
        component: OneComponent,
      },
      {
        path: 'two',
        component: TwoComponent
      }
    ] 
  },
];

@NgModule({
  imports: [ RouterModule.forChild(routes) ],
  exports: [ RouterModule ],
})
export class PlaygroundRoutingModule { }複製程式碼

這只是定義了路由資料,我們還需要在某個地方顯示路由指向的元件,那麼這裡面我們還是在PlaygroundComponent的模版中把路由插座放入吧。

<ul>
  <li><a routerLink="one">One</a></li>
  <li><a routerLink="two">Two</a></li>
</ul>

<router-outlet></router-outlet>複製程式碼

現在我們試驗一下,開啟瀏覽器輸入 http://localhost:4200/playground 我們看到兩個連結,你可以分別點一下,觀察位址列。應該可以看到,點選one時,地址變成 http://localhost:4200/playground/one 在我們放置路由插座的位置也會出現one works。當然點選two時也會有對應的改變。這說明我們的子路由配置好用了!

Angular 從0到1 (八):史上最簡單的 Angular 教程
子路由的小例子

當然有的時候還需要更深的層級的子路由,其實也很簡單。就是重複我們剛才做的就好,只不過要在對應的子路由節點上。下面我們還是演練一下,在點選one之後我們希望到達一個有子路由的頁面(也就是子路由的子路由)。於是我們在OneComponent節點下又加了children,然後把ThreeComponent和對應的路徑寫入

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

import { PlaygroundComponent } from './playground.component';
import { OneComponent } from './one/one.component';
import { TwoComponent } from './two/two.component';
import { ThreeComponent } from './three/three.component';

const routes: Routes = [
  { 
    path: '', 
    component: PlaygroundComponent,
    children: [
      {
        path: 'one',
        component: OneComponent,
        children: [
          {
            path: 'three',
            component: ThreeComponent
          }
        ]
      },
      {
        path: 'two',
        component: TwoComponent
      }
    ] 
  },
];

@NgModule({
  imports: [ RouterModule.forChild(routes) ],
  exports: [ RouterModule ],
})
export class PlaygroundRoutingModule { }複製程式碼

當然,還是一樣,我們需要改造一下OneComponent的模版以便於它可以顯示子路由的內容。改動 src/app/playground/one/one.component.html 為如下內容

<p>
  one works!
</p>
<ul>
  <li><a routerLink="three">Three</a></li>
</ul>
<router-outlet></router-outlet>複製程式碼

這回我們看到如果在瀏覽器中輸入 http://localhost:4200/playground/one/three 會看到如圖所示的結果:

Angular 從0到1 (八):史上最簡單的 Angular 教程
更多層級的子路由

經過這個小練習,相信再複雜的路由你也可以搞定了。但是我要說一句,個人不是很推薦過於複雜的路由(複雜這裡指層級巢狀太多)。層級多了之後意味著這個模組太大了,負責了過多它不應該負責的事情。也就是說當要使用子路由時,一定多問自己幾遍,這樣做是必須的嗎?可以用別的方式解決嗎?是不是我的模組改拆分了?

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

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

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

相關文章