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

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

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

第七章:給元件帶來活力

這節我們的主題是“專注酷炫一百年”;-)其實...沒那麼誇張了,但我們還是要在這一節瞭解MDL css框架、Angular2 內建的動畫特性、更復雜的元件和概括一下Angular2的元件生命週期。

更炫的登陸頁

大家不知道有沒有試用過bing(必應)搜尋引擎(在Google無法訪問的情況下,bing的英文搜尋還是不錯的選擇),這個搜尋引擎的主頁很有特點:每日都會有一張非常好看的圖作為背景。

Angular 從 0 到 1 (七)史上最簡單的 Angular 教程
image_1b36ghm4o179516kdikkbc14qp9.png-2737.5kB

我們想做的一個特效呢是類似地給登陸頁增加一個背景,但更酷的一點是,我們的背景每隔3秒會自動替換一張。由於涉及到佈局,我們先來熟悉一下CSS的框架設計。

響應式的CSS框架

目前主流的響應式css框架都有網格的概念,在我們現在使用的MDL(Material Design Lite)框架中叫做grid。在MDL中,一個頁面在PC瀏覽器上的展現寬度有12個格子(cell),在平板上有8個格子,在手機上有4個格子。即一個grid的一行在PC上是12個cell,在平板上是8個cell,在手機上是4個cell。如果一行中的cell數目大於限制數目(比如在PC上超過12個),MDL會做折行處理。標識一個grid容器也很簡單,在對應標籤加上class="mdl-grid"即可。類似的每個cell需要在對應標籤內加上class="mdl-cell"。如果要定製化grid的話,我們需要給class新增多個樣式類名,比如如果希望grid內是沒有間隔的,可以寫成class="mdl-grid mdl-grid--no-spacing";如果希望新增更多自己的定義,類似的可以寫成mdl-grid my-grid-style,然後在css中定義這個my-grid-style即可。

<div class="mdl-grid">
  <div class="mdl-cell mdl-cell--1-col">1</div>
  <div class="mdl-cell mdl-cell--1-col">1</div>
  <div class="mdl-cell mdl-cell--1-col">1</div>
  <div class="mdl-cell mdl-cell--1-col">1</div>
  <div class="mdl-cell mdl-cell--1-col">1</div>
  <div class="mdl-cell mdl-cell--1-col">1</div>
  <div class="mdl-cell mdl-cell--1-col">1</div>
  <div class="mdl-cell mdl-cell--1-col">1</div>
  <div class="mdl-cell mdl-cell--1-col">1</div>
  <div class="mdl-cell mdl-cell--1-col">1</div>
  <div class="mdl-cell mdl-cell--1-col">1</div>
  <div class="mdl-cell mdl-cell--1-col">1</div>
</div>
<div class="mdl-grid">
  <div class="mdl-cell mdl-cell--4-col">4</div>
  <div class="mdl-cell mdl-cell--4-col">4</div>
  <div class="mdl-cell mdl-cell--4-col">4</div>
</div>
<div class="mdl-grid">
  <div class="mdl-cell mdl-cell--6-col">6</div>
  <div class="mdl-cell mdl-cell--4-col">4</div>
  <div class="mdl-cell mdl-cell--2-col">2</div>
</div>
<div class="mdl-grid">
  <div class="mdl-cell mdl-cell--6-col mdl-cell--8-col-tablet">6 (8 tablet)</div>
  <div class="mdl-cell mdl-cell--4-col mdl-cell--6-col-tablet">4 (6 tablet)</div>
  <div class="mdl-cell mdl-cell--2-col mdl-cell--4-col-phone">2 (4 phone)</div>
</div>複製程式碼

Angular 從 0 到 1 (七)史上最簡單的 Angular 教程
image_1b36l1ajl1qqm1t091m89gbe1cr7m.png-49.6kB

你可以嘗試把瀏覽器的視窗縮小,讓寬度變窄,調整到一定程度後你會發現,佈局改變了,變成了下面的樣子,這就是同樣的程式碼在平板上的效果。你會發現原本的第一行折成了兩行,因為在平板上8個cell是一行。你可以試試繼續把瀏覽器的寬度變窄,看看在手機上的效果。
Angular 從 0 到 1 (七)史上最簡單的 Angular 教程
image_1b36lq1ikh3vnfkadg8rpnrm13.png-59.4kB

下面我們看看怎麼對Login頁面做改造,首先在form外套一層div,並應用grid相關的css類,當然為了設定背景圖,我們使用了一個angular屬性ngStyle,這樣讓我們可以動態的改變背景圖。grid裡面我們僅有一個有實際內容的cell,就是form了,這個form在PC和平板上都佔3個cell,在手機上佔4個cell。但為了使這個form可以放在頁面靠右的位置,我們新增了2個佔位標籤mdl-layout-spacer,標籤的作用使將cell剩餘的橫向空間佔滿。

<div
  class="mdl-grid mdl-grid--no-spacing login-container"
  [ngStyle]="{'background-image': 'url(' + photo + ')'}">
  <mdl-layout-spacer
    class="mdl-cell mdl-cell--8-col mdl-cell--4-col-tablet mdl-cell--hide-phone">
  </mdl-layout-spacer>
  <form
    class="mdl-cell mdl-cell--3-col mdl-cell--3-col-tablet mdl-cell--4-col-phone login-form"
    (ngSubmit)="onSubmit()"
    >
    <!--...(這裡省略掉其他控制元件的內容)-->
  </form>
  <mdl-layout-spacer></mdl-layout-spacer>
</div>複製程式碼

在我們還沒有找到可以動態配置的圖片源之前,為了看看頁面效果,我們可以先找一張圖片放在src\assets目錄下面,然後在LoginComponent中將其賦值給photo: photo = '/assets/login_default_bg.jpg';。接下來就看看現在的頁面效果吧。

Angular 從 0 到 1 (七)史上最簡單的 Angular 教程
image_1b37me9ik1eba1ruq98s1n041siq9.png-3810.5kB

尋找免費的圖片源

當然我們可以找到一些免費的圖片,然後存到本地來實現這個功能,但如果有一個海量的圖片庫,我們可以根據關鍵字搜尋不同的圖片不是更酷了嗎?幸運的是Bing搜尋是有API的,去 www.microsoft.com/cognitive-s… 點選Get Started for free後點選Bing Image Search申請獲得一個API key即可。

Angular 從 0 到 1 (七)史上最簡單的 Angular 教程
image_1b36ncud0epmjsjsrjqds1tka9.png-1021.8kB

申請完畢後可以在My Account中看到你的key,預設是隱藏的,點選Show連結即可看到了,點選Copy連結可以拷貝key到剪貼簿。

Angular 從 0 到 1 (七)史上最簡單的 Angular 教程
image_1b36npfqlhkq0l1fge1o8jon0m.png-109.8kB

Bing Image Search API的Request Url是:https://api.cognitive.microsoft.com/bing/v5.0/images/search,後面可以跟隨一系列引數,其中q是必選引數,指明搜尋的關鍵字。

引數 是否必選 型別 功能描述
q string 搜尋關鍵字
count number 返回的圖片數量,實際返回值可能小於指定值
offset number 要跳過的結果數量
mkt string 從那個國家搜尋,比如美國就是en-US
safeSearch string 應用過濾器過濾掉不良成人內容

知道了這些引數的意義後,我們可以在login目錄下新建一個BingImageService

import { Injectable } from '@angular/core';
import { Http, Headers, Response } from '@angular/http';
import { Observable } from 'rxjs/Rx';
import { Image } from '../domain/entities';

@Injectable()
export class BingImageService {

  imageUrl: string;
  headers = new Headers({
    'Content-Type': 'application/json',
    //把你獲得API key在這裡替換掉下面的enter-your-api-key-here
    'Ocp-Apim-Subscription-Key': 'enter-your-api-key-here'
  });

  constructor(private http: Http) {
    const q = '北極+牆紙';
    const baseUrl: string = `https://api.cognitive.microsoft.com/bing/v5.0/images/search`;
    this.imageUrl = baseUrl + `?q=${q}&count=5&mkt=zh-CN&imageType=Photo&size=Large`;
  }

  getImageUrl(): Observable<Image[]>{
    return this.http.get(this.imageUrl, { headers: this.headers })
            .map(res => res.json().value as Image[])
            .catch(this.handleError);
  }
  private handleError(error: Response) {
    console.error(error);
    return Observable.throw(error.json().error || 'Server error');
  }
}複製程式碼

然後在LoginComponent中即可呼叫這個服務,在得到返回的圖片結果後我們就可以去替換掉預設本地圖片的地址了。由於我們是得到一個圖片地址的陣列,所以我們還需要一個對這個陣列中的每張圖片做一個4秒的等待。而且我們還做了一個小處理 i = (i + 1) % length;,使得圖片可以迴圈播放。注意到我們讓LoginComponent實現了OnDestroy介面,這是由於我們希望在頁面銷燬時也同時銷燬觀察者的訂閱,而不是讓它一直跑在後臺。

//程式碼片段
export class LoginComponent implements OnDestroy {

  username = '';
  password = '';
  auth: Auth;
  slides: Image[] = [];
  photo = '/assets/login_default_bg.jpg';
  subscription: Subscription;

  constructor(
    @Inject('auth') private authService,
    @Inject('bing') private bingService,
    private router: Router) {
    this.bingService.getImageUrl()
      .subscribe((images: Image[]) => {
        this.slides = [...images];
        this.rotateImages(this.slides);
      });
  }
  ...
  ngOnDestroy(){
    this.subscription.unsubscribe();
  }
  rotateImages(arr: Image[]){
    const length = arr.length
    let i = 0;
    setInterval(() => {
      i = (i + 1) % length;
      this.photo = this.slides[i].contentUrl;
    }, 4000);
  }
}複製程式碼

來喝杯咖啡,欣賞一下我們的成果吧!

Angular 從 0 到 1 (七)史上最簡單的 Angular 教程
每隔4秒換一張背景圖的登入頁面

Angular 從 0 到 1 (七)史上最簡單的 Angular 教程
等待4秒後背景切換了

自帶動畫技能的Angular2

Angular2的目標是一站式解決方案,當然會自帶動畫技能。動畫會被定義在@Component描述性後設資料中。在新增動畫之前,先引入一些與動畫有關的類庫:

import {
  Component,
  Inject,
  trigger,
  state,
  style,
  transition,
  animate,
  OnDestroy
} from '@angular/core';複製程式碼

然後就可以在@Component後設資料中去新增動畫相關的後設資料了,我們這裡定義了一個叫loginState的動畫觸發器(trigger)。這個觸發器會在inactiveactive兩個狀態間轉換。scale(1.1)是放縮比例,意味著我們對控制元件做了1.1倍的放大。這個動畫的邏輯就是,當觸發器處於active狀態時,對應用這個觸發器狀態的控制元件做1.1倍放大處理。

@Component({
  selector: 'app-login',
  templateUrl: './login.component.html',
  styleUrls: ['./login.component.css'],
  animations: [
    trigger('loginState', [
      state('inactive', style({
        transform: 'scale(1)'
      })),
      state('active',   style({
        transform: 'scale(1.1)'
      })),
      transition('inactive => active', animate('100ms ease-in')),
      transition('active => inactive', animate('100ms ease-out'))
    ])
  ]
})複製程式碼

我們剛剛定義了一個動畫,但它還沒有被用到任何地方。要想使用它,可以在模板中用[@triggerName]="xxx"的形式來把它附加到一個或多個元素上。

      <button
        mdl-button mdl-button-type="raised"
        mdl-colored="primary"
        mdl-ripple type="submit"
        [@loginState]="loginBtnState"
        (mouseenter)="toggleLoginState(true)"
        (mouseleave)="toggleLoginState(false)">
        Login
      </button>複製程式碼

這裡我們對Login這個按鈕應用了loginState觸發器,並且繫結這個觸發器的狀態值到一個成員變數loginBtnState。而且我們定義了在滑鼠進入按鈕區域和離開按鈕區域時應該通過一個函式toggleLoginState來改變loginBtnState的值。在LoginComponent中定義這個方法即可,我們要實現的這個功能非常簡單,一行程式碼就搞定了:

  toggleLoginState(state: boolean){
    this.loginBtnState = state ? 'active' : 'inactive';
  }複製程式碼

試著將滑鼠放在按鈕上和離開按鈕區域,看看按鈕的變化的效果。

Angular 從 0 到 1 (七)史上最簡單的 Angular 教程
滑鼠離開和進入按鈕區域時不同的按鈕大小

完成遺失已久的註冊功能

我們自從完成了基本的多使用者待辦事項後就沒有增加註冊功能,現在來填補這個缺憾吧。我們打算在點選登入頁的Register按鈕時彈出一個註冊使用者的對話方塊。

Angular 從 0 到 1 (七)史上最簡單的 Angular 教程
我們要實現的註冊對話方塊效果

如果實現一個對話方塊,利用我們已經引入的angular2-mdl庫,需要幾個步驟:

我們需要在src\index.html中增加一個“對話方塊插座”(<dialog-outlet></dialog-outlet>),就是在<app-root>下面新增即可。

<!doctype html>
<html>
<head>
...
</head>
<body>
  <app-root>Loading...</app-root>
  <dialog-outlet></dialog-outlet>
</body>
</html>複製程式碼

建立dialog頁面:angular2-mdl中有很多方便內建對話方塊和宣告式方式,但我們這裡介紹一種定製化程度比較高,也略顯複雜的方式。開啟一個命令列終端,輸入 ng g c login/register-dialog

對話方塊的模板比較簡單,由一個使用者名稱輸入框、一個密碼輸入框、一個重複密碼輸入框、一個載入狀態和一個註冊按鈕組成。其中我們希望按鈕在表單驗證正確後才可用,而且在處理註冊過程中,按鈕應該不可用。在處理註冊過程中,應該有一個使用者提示。

<form [formGroup]="form">
  <h3 class="mdl-dialog__title">Register</h3>
  <div class="mdl-dialog__content">
    <mdl-textfield
      #firstElement
      type="text"
      label="Username"
      formControlName="username"
      floating-label>
    </mdl-textfield>
    <br/>
    <mdl-textfield
      type="password"
      label="Password"
      formControlName="password"
      floating-label>
    </mdl-textfield>
    <br/>
    <mdl-textfield
      type="password"
      label="Repeat Password"
      formControlName="repeatPassword"
      floating-label>
    </mdl-textfield>
  </div>
  <div class="status-bar">
    <p class="mdl-color-text--primary">{{statusMessage}}</p>
    <mdl-spinner [active]="processingRegister"></mdl-spinner>
  </div>
  <div class="mdl-dialog__actions">
    <button
      type="button"
      mdl-button
      (click)="register()"
      [disabled]="!form.valid || processingRegister"
      mdl-button-type="raised"
      mdl-colored="primary" mdl-ripple>
      Register
    </button>
  </div>
</form>複製程式碼

那麼對應的元件檔案中,我們這次沒有使用雙向繫結,而是完全採取表單的方式進行。這裡介紹幾個新面孔:

  • FormBuilder:這個其實是一個工具類,用於快速構造一個表單。
  • FormGroup:顧名思義是一組表單控制元件,一個表單可以有多個FormGroup,這個常常在比較複雜的表單中使用,用於更好的分類和控制。如果這一組中的任何一個控制元件驗證失敗,這個FormGroup的驗證狀態也是失敗的。
  • FormControl:跟蹤表單控制元件的值和驗證狀態。

Angular2 的FormControl中內建了常用的驗證器(Validator),我們在這個例子中除此之外還給出了一個自定義的驗證器 passwordMatchValidator,用於判斷是否兩次密碼輸入的是相同的。

此外呢我們還用到了一個新修飾符 @HostListener ,這個修飾符是指我們要監聽宿主(這裡是瀏覽器)的某些動作和變化。比如本例中,我們想要使用者在按Esc鍵時關閉對話方塊,但這個動作並不侷限在某個控制元件上,只要使用者點選了Esc我們就關閉對話方塊,這時我們就得監聽宿主的 keydown.esc 事件了。

//省略掉Import程式碼段和修飾符程式碼段
...
export class RegisterDialogComponent{
  @ViewChild('firstElement') private inputElement: MdlTextFieldComponent;
  public form: FormGroup;
  public processingRegister = false;
  public statusMessage = '';
  private subscription: Subscription;

  constructor(
    private dialog: MdlDialogReference,
    private fb: FormBuilder,
    private router: Router,
    @Inject('auth') private authService) {
      this.form = fb.group({
        'username':  new FormControl('',  Validators.required),
        'passwords': fb.group({
          'password': new FormControl('', Validators.required),
          'repeatPassword': new FormControl('', Validators.required)
        },{validator: this.passwordMatchValidator})
      });
      // just if you want to be informed if the dialog is hidden
      this.dialog.onHide().subscribe( (auth) => {
        console.log('login dialog hidden');
        if (auth) {
          console.log('authenticated user', auth);
        }
      });
      this.dialog.onVisible().subscribe( () => {
        this.inputElement.setFocus();
      });
  }

  passwordMatchValidator(group: FormGroup){
    this.statusMessage = '';
    let password = group.get('password').value;
    let confirm = group.get('repeatPassword').value;

    // Don't kick in until user touches both fields
    if (password.pristine || confirm.pristine) {
      return null;
    }
    if(password===confirm) {
      return null;
    }
    return {'mismatch': true};
  }

  public register() {
    this.processingRegister = true;
    this.statusMessage = 'processing your registration ...';

    this.subscription = this.authService
      .register(
        this.form.get('username').value,
        this.form.get('passwords').get('password').value)
      .subscribe( auth => {
        this.processingRegister = false;
        this.statusMessage = 'you are registered and will be signed in ...';
        setTimeout( () => {
          this.dialog.hide(auth);
          this.router.navigate(['todo']);
        }, 500);
    }, err => {
      this.processingRegister = false;
      this.statusMessage = err.message;
    });
  }

  @HostListener('keydown.esc')
  public onEsc(): void {
    if(this.subscription !== undefined)
      this.subscription.unsubscribe();
    this.dialog.hide();
  }
}複製程式碼

這樣做完後,開啟瀏覽器卻發現報錯了,這是由於我們未引入 ReactiveFormsModule 造成的, FormGroup 是由 ReactiveFormsModule 提供的,因此要在 src\app\login\login.module.ts 中引入這個模組。

Angular 從 0 到 1 (七)史上最簡單的 Angular 教程
未引入ReactiveForms引起的報錯

Restful API的實驗

現在還需要完成伺服器端的API。和以前類似的,我們需要先實驗一下json-server的API,確定各引數可行的條件下再進行編碼。由於現在我們需要進行GET以外的操作,所以如果有專業工具來輔助會比較方便,這裡推薦一個Chrome App:Postman,可以自行科學上網後在Chrome商店搜尋安裝。安裝後點左上角的應用即可看到Postman了

Angular 從 0 到 1 (七)史上最簡單的 Angular 教程
Chrome應用:Postman

點選Postman,輸入http://localhost:3000/users可以看到返回的json資料了

Angular 從 0 到 1 (七)史上最簡單的 Angular 教程
PostMan的功能區介紹

我們來試驗一下新增一個使用者,但這個時候我們已經給User的id定義成數字型別了,實在不想改成UUID了,怎麼辦呢?幸運的是json-server其實是很聰明的,如果在POST時你不給它傳入id欄位,它會認為這個id是自增長的。在Postman中將HTTP方法設成POST,在Headers中寫上 Content-Typeapplication/json。然後在Body中選擇 raw ,並寫入

{
    "username": "testUser",
    "password": "testPassword"
}複製程式碼

點選Send後可以看到,新的id自動被寫入了,這簡直太方便了,也符合一般後端開發的套路。

Angular 從 0 到 1 (七)史上最簡單的 Angular 教程
用Postman測試自增長ID

知道這點後,我們著手寫對應方法就很簡單了,首先在 UserService 中新增addUser方法。

  addUser(user: User): Observable<User>{
    return this.http.post(this.api_url, JSON.stringify(user), {headers: this.headers})
            .map(res => res.json() as User)
            .catch(this.handleError);
  }複製程式碼

在AuthService中新增一個register方法,正如我們剛剛實驗的那樣,我們只需構造一個沒有id的User物件即可。當然我們要檢查一下使用者名稱是否存在,如果不存在的話才可以註冊新使用者。這裡又碰到一個新的Rx方法 switchMap,是用來對原來流中的物件做變換後,發射變換後的流。用一個圖示來表示我們下面程式碼的邏輯是這樣的

                                  null               null
                                   /                 /
應用filter前:User=====User=====User=====...=====User======...
應用filter後:==================User=====...=====User======...
(把user===null的濾出來)          |                |
應用switchMap後:              Auth======...======Auth=====...複製程式碼
  register(username: string, password: string): Observable<Auth> {
    let toAddUser = {
      username: username,
      password: password
    };
    return this.userService
            .findUser(username)
            .filter(user => user === null)
            .switchMap(user => {
              return this.userService.addUser(toAddUser).map(u => {
                this.auth = Object.assign(
                  {},
                  { user: u, hasError: false, errMsg: null, redirectUrl: null}
                );
                this.subject.next(this.auth);
                return this.auth;
              });
            });
  }複製程式碼

開啟瀏覽器,檢查所有功能是否完整可用,正常情況下點Register你可以看到下面的介面,試著註冊一個新使用者,開始管理你的待辦事項吧。

Angular 從 0 到 1 (七)史上最簡單的 Angular 教程
完成註冊功能的頁面

Angular 2的元件生命週期

Angular 從 0 到 1 (七)史上最簡單的 Angular 教程
angular 2 的元件生命週期函式

每個元件都有一個被Angular管理的生命週期:Angular建立、渲染控制元件;建立、渲染子控制元件;當資料繫結屬性改變時做檢查;在把控制元件移除DOM之前銷燬控制元件等等。

Angular提供生命週期的“鉤子”(Hook)以便於開發者可以得到這些關鍵過程的資料以及在這些過程中做出響應的能力。

指令也有類似的生命週期“鉤子”函式,除了一些元件特有的函式外。

下面這段程式碼展現瞭如何利用 ngOnInit 這個鉤子函式

export class PeekABoo implements OnInit {
  constructor(private logger: LoggerService) { }

  // implement OnInit's `ngOnInit` method
  ngOnInit() { this.logIt(`OnInit`); }

  logIt(msg: string) {
    this.logger.log(`#${nextId++} ${msg}`);
  }
}複製程式碼

鉤子函式的介面 (比如上面例子中的 OnInit ) 從純技術的角度來說不是必須的,這是由於Javascript本身沒有介面這個概念,而Typescript最終是轉換成Javascript的。

Angular其實是通過檢查指令或元件的類中是否定義了相關方法來進行的,比如上面例子中即使不實現 OnInit 介面,只要定義了 ngOnInit() 方法,Angular就會在對應的生命週期呼叫這個方法。但是還是推薦大家使用介面,因為強型別會給我們帶來其他好處。

函式 應用範圍 目的和觸發時機
ngOnChanges 元件和指令 在ngInit之前觸發,當Angular設定資料繫結屬性或輸入性屬性時會得到一個包含當前和之前屬性值的物件(SimpleChanges)
ngOnInit 元件和指令 只呼叫一次,在設定完輸入性屬性後,通過這個函式初始化元件或指令
ngDoCheck 元件和指令 在ngInit之後,每次檢測到變化時觸發,可以在此檢查一些angular自身無法檢查的變化
ngAfterContentInit 元件 在ngDoCheck後觸發,只呼叫一次,把要裝載到元件檢視的內容初始化後
ngAfterContentChecked 元件 ngAfterContentInit之後每次ngDoCheck都會在之後觸發ngAfterContentChecked,對要裝載到元件檢視的內容進行檢查後
ngAfterViewInit 元件 在第一個ngAfterContentInit被呼叫後觸發,只呼叫一次,在angular初始化檢視後響應
ngAfterViewChecked 元件 在ngAfterViewInit後及每個ngAfterContentChecked後觸發
ngOnDestroy 元件和指令 在元件或指令被銷燬前,清理環境,可以在此處取消Observable的訂閱

小結

我們的Angular學習之旅從零開始到現在,完整的搭建了一個小應用。相信大家現在應該對Angular2有一個大概的認識了,而且也可以參與到正式的開發專案中去了。但Angular2作為一個完整框架,有很多細節我們是沒有提到的,大家可以到官方文件 angular.cn/ 去查詢和學習。

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

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

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

第一節: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 (七)

相關文章