1. 程式人生 > >Angular-cli 6.x +TS打造經典Todo案例

Angular-cli 6.x +TS打造經典Todo案例

一、閒嘮嗑幾句

以前用Angular的時候也只是用的js構建的應用,還未涉及ts;趁這次有機會,有時間用ts、angular的腳手架工具angular-cli構建一個經典的todo案例;幾乎所有的vue、react的基礎經典案例都離不開Todo List的應用構建。所以這次也是以Todo案例,來簡單瞭解下ts構建單頁面應用,以及請求響應邏輯處理。這次後端的邏輯主要採用前端工具json-server偽裝處理後端邏輯,返回REST API;

最終效果預覽圖:

二、開始配置環境

1.安裝 Angular-cli 2.初始化一個自定義的anglar專案 3.全域性安裝json-server 4.定義基本的資料庫json檔案 5.開啟專案服務,開啟json後臺服務

注:

1.這部分可以參考我的其他博文:

https://blog.csdn.net/WU5229485/article/details/82917389 https://blog.csdn.net/WU5229485/article/details/83028007

2.json資料庫檔案(db,json)

{
  "todos": [
    {
      "id": 1,
      "title": "Read SitePoint article",
      "complete": false
    },
    {
      "id": 2,
      "title": "Clean inbox",
      "complete": false
    },
    {
      "id": 3,
      "title": "Make restaurant reservation",
      "complete": false
    }
  ]
}

三、建立應用的基礎服務配置

1.建立開發環境和生產環境的分別對應的URL

(1)在開發環境和生產環境儲存不同的API URL

(2)Angular-cli 預設提供兩種環境配置:

src/environments/environment.ts      # 開發環境
src/environments/environment.prod.ts.  # 上線環境

(3)命令列動態載入( 就不用手動改程式碼啦 ~ )

ng serve 或者 ng build 預設載入開發環境 src/environments/environment.ts
ng serve --environment prod 或者 ng build --environment prod 預設載入上線環境 src/environments/environment.prod.ts

2.建立後臺增刪改查處理邏輯(基類、控制類)

(1)建立Todo基類,定義應用所需要的公共屬性

export class Todo {
      id: number;
      title = '';
      complete = false;

      constructor(values: Object = {}) {
        Object.assign(this, values);
      }
    }

(2)建立 TodoDataService 控制類,AppServer類中的方法,向其AppServer類方法中傳遞Todo引數

import {Injectable} from '@angular/core';
  import {Todo} from './todo';

  @Injectable()
  export class TodoDataService {

    // Placeholder for last id so we can simulate
    // automatic incrementing of ids
    lastId: number = 0;

    // Placeholder for todos
    todos: Todo[] = [];

    constructor() {
    }

    //  POST /todos
    addTodo(todo: Todo): TodoDataService {}

    //  DELETE /todos/:id
    deleteTodoById(id: number): TodoDataService {}

    //  PUT /todos/:id
    updateTodoById(id: number, values: Object = {}): Todo {}

    //  GET /todos
    getAllTodos(): Todo[] {}

    // GET /todos/:id
    getTodoById(id: number): Todo {}

    // Toggle todo complete
    toggleTodoComplete(todo: Todo){}

 }

(3)建立TodoDataService 控制類的單元測試

import { TestBed, inject } from '@angular/core/testing';
import { TodoDataService } from './todo-data.service';
import { ApiService } from './api.service';
import { ApiMockService } from './api-mock.service';

describe('TodoDataService', () => {
  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [
        TodoDataService,
        {
          provide: ApiService,
          useClass: ApiMockService
        }
      ]
    });
  });

  it('should ...', inject([TodoDataService], (service: TodoDataService) => {
    expect(service).toBeTruthy();
  }));

});

3. 建立與後臺聯調的AppServer類,在類中實現對db.json資料庫檔案的增刪改查,然後在TodoDataService例項化AppSrever類,呼叫其內部公共方法,返回db.json中的資料,賦值給Todo類中的屬性

import { Injectable } from '@angular/core';
import { environment } from 'environments/environment';
import { Http, Response } from '@angular/http';
import { Todo } from './todo';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/catch';
import 'rxjs/add/observable/throw';

const API_URL = environment.apiUrl;

@Injectable()
export class ApiService {

  constructor(
    private http: Http
  ) {
  }

  public getAllTodos(): Observable<Todo[]> {
    return this.http
      .get(API_URL + '/todos')
      .map(response => {
        const todos = response.json();
        return todos.map((todo) => new Todo(todo));
      })
      .catch(this.handleError);
  }

  public createTodo(todo: Todo): Observable<Todo> {
    return this.http
      .post(API_URL + '/todos', todo)
      .map(response => {
        return new Todo(response.json());
      })
      .catch(this.handleError);
  }

  public getTodoById(todoId: number): Observable<Todo> {
    return this.http
      .get(API_URL + '/todos/' + todoId)
      .map(response => {
        return new Todo(response.json());
      })
      .catch(this.handleError);
  }

  public updateTodo(todo: Todo): Observable<Todo> {
    return this.http
      .put(API_URL + '/todos/' + todo.id, todo)
      .map(response => {
        return new Todo(response.json());
      })
      .catch(this.handleError);
  }

  public deleteTodoById(todoId: number): Observable<null> {
    return this.http
      .delete(API_URL + '/todos/' + todoId)
      .map(response => null)
      .catch(this.handleError);
  }

  private handleError (error: Response | any) {
    console.error('ApiService::handleError', error);
    return Observable.throw(error);
  }
  
}

四、元件互動

1. 一共包括下列元件:

Todos 最外層盒子:app.component Todos 頭部:todo-list-header Todos 腳部:todo-list-footer Todos 列表盒子:todo-list Todos 單個列表項:todo-list-item

2. 新增一條Todo列表項邏輯,涉及元件(todo-list-header、app.component):

(1)在todo-list-header中,回車實現新增操作

2-1-1.結構:

<header class="header">
  <h1>Todos</h1>
  <app-site-header></app-site-header>
  <input class="new-todo" placeholder="What needs to be done?" autofocus="" [(ngModel)]="newTodo.title"
         (keyup.enter)="addTodo()">
</header>

2-1-2. 邏輯操作

import { Component, Output, EventEmitter } from '@angular/core';
import { Todo } from '../todo';

@Component({
  selector: 'app-todo-list-header',
  templateUrl: './todo-list-header.component.html',
  styleUrls: ['./todo-list-header.component.css']
})
export class TodoListHeaderComponent {

  newTodo: Todo = new Todo();

  @Output()
  add: EventEmitter<Todo> = new EventEmitter();

  constructor() {
  }

  addTodo() {
    this.add.emit(this.newTodo);
    this.newTodo = new Todo();
  }

}

(2)app.component元件中呼叫todo-list-header,並且傳遞方法繫結

2-2-1.結構:

<app-todo-list-header  (add)="onAddTodo($event)" ></app-todo-list-header>

2-2-2. 邏輯操作:

onAddTodo(todo) {
    this.todoDataService
      .addTodo(todo)
      .subscribe(
        (newTodo) => {
          this.todos = this.todos.concat(newTodo);
        }
      );
  }

(3)然後把更新的todos傳遞給子元件todo-list-footer、todo-list

 <app-todo-list  [todos]="todos" ></app-todo-list>
 <app-todo-list-footer [todos]="todos" ></app-todo-list-footer>

3. 刪除和更新一條Todo列表項邏輯

(1)觸發刪除和更改選中狀態的元件todo-list-item

3-1-1. 結構:

<div class="view">
  <input class="toggle" type="checkbox" (click)="toggleTodoComplete(todo)" [checked]="todo.complete">
  <label>{{todo.title}}</label>
  <button class="destroy" (click)="removeTodo(todo)"></button>
</div>

3-1-2. 邏輯處理

  @Input() todo: Todo;

  @Output()
  remove: EventEmitter<Todo> = new EventEmitter();

  @Output()
  toggleComplete: EventEmitter<Todo> = new EventEmitter();

  constructor() {
  }

  toggleTodoComplete(todo: Todo) {
    this.toggleComplete.emit(todo);
    console.log(todo);
  }

  removeTodo(todo: Todo) {
    this.remove.emit(todo);
  }

(2)由於toggleComplete和remove方法是父元件todo-list傳遞過來的方法,所以需要在負元件中實現

3-2-1. todo-list元件中呼叫toto-list-item元件

<section class="main" *ngIf="todos.length > 0">
  <ul class="todo-list">
    <li *ngFor="let todo of todos" [class.completed]="todo.complete">
      <app-todo-list-item
        [todo]="todo"
        (toggleComplete)="onToggleTodoComplete($event)"
        (remove)="onRemoveTodo($event)"></app-todo-list-item>
    </li>
  </ul>
</section>

3-2-2. 父元件todo-list邏輯處理 

 @Input()
  todos: Todo[];

  @Output()
  remove: EventEmitter<Todo> = new EventEmitter();

  @Output()
  toggleComplete: EventEmitter<Todo> = new EventEmitter();

  constructor() {
  }

  onToggleTodoComplete(todo: Todo) {
    this.toggleComplete.emit(todo);
  }

  onRemoveTodo(todo: Todo) {
    this.remove.emit(todo);
  }

(3)在元件todo-list中,remove和toggleComplete方法依舊是其父元件傳遞過來,所以需要在todo-list父元件app.component元件中實現

3-3-1. 結構元件呼叫

<app-todo-list
    [todos]="todos"
    (toggleComplete)="onToggleTodoComplete($event)"
    (remove)="onRemoveTodo($event)"
></app-todo-list>

3-3-2. 邏輯處理 

onToggleTodoComplete(todo) {
    this.todoDataService
      .toggleTodoComplete(todo)
      .subscribe(
        (updatedTodo) => {
          todo = updatedTodo;
        }
      );
  }

  onRemoveTodo(todo) {
    this.todoDataService
      .deleteTodoById(todo.id)
      .subscribe(
        (_) => {
          this.todos = this.todos.filter((t) => t.id !== todo.id);
        }
      );
  }

* 資料傳遞總結:

整個資料經歷了兩次連續傳遞:

第一次: 元件todo-list-item傳遞到todo-list中 第二次: 元件todo-list傳遞到app.component中

五、GITHUB原始碼地址