基于Angular 2 CLI開發CRUD應用程序
譯文【譯者注】本文應用開發及測試環境為Mac平臺(即類Linux環境);因此,使用Windows平臺的讀者在使用CLI時可能需要作一定的調整。所謂CLI,實際上是Angular 2新引入的一種命令行操作方式,在這種方式下能夠對常規的Angular 2操作以更快的方式實現。另外,本文作者使用的Javascript腳本是TypeScript。還有,作者使用了Karma工具(https://karma-runner.github.io/)對文中的TypeScript腳本進行了較全面的單元測試。因此,雖然本文介紹的是一個基礎型Angular 2實例開發過程,但是還是值得一讀。
簡介
Angular 2是一個世界著名的開源Web前端開發框架,用于構建跨移動設備和桌面平臺的Web應用程序。在本文中,我們將開發一個基于Angular 2 CLI方式的Todo Web應用程序。這個程序中實現的基本功能包括允許用戶:
使用輸入字段快速創建新的todo任務
切換todo任務的完成與未完成狀態
刪除不再需要的todo任務,等等
【注意】本文示例工程源碼下載地址是https://github.com/sitepoint-editors/angular2-todo-app。上述工程的一個在線展示網址是https://sitepoint-editors.github.io/angular2-todo-app/。下面僅給出這個程序的一個靜態截圖。
Angular CLI簡介
創建一個新的Angular 2應用程序最簡單的方法之一是使用全新的Angular命令行界面(CLI)。CLI允許您實現: 生成新的Angular 2應用程序的樣板文件代碼
向現有的Angular 2應用程序添加指定的功能(包括組件、指令、服務、管道等)
若要安裝Angular的CLI,請運行如下命令:
$ npm install -g angular-cli
這將在您的系統中以全局方式安裝ng命令。
為了驗證您的安裝是否成功,您可以運行如下命令:
$ ng version
這個命令應會顯示你已經安裝的angular-cli版本號。更多的細節,請參考官方安裝說明(https://github.com/angular/angular-cli#installation)。
生成Todo應用程序
現在,我們已經安裝了Angular CLI。下面,我們可以使用它來生成我們的Todo應用程序了,命令如下:
- $ ng new angular2-todo-app
這將創建一個目錄結構,其中包含我們所需要的一切基礎內容,如下圖所示:
├── angular-cli-build.js
├── angular-cli.json
├── config
│ ├── environment.dev.ts
│ ├── environment.js
│ ├── environment.prod.ts
│ ├── karma.conf.js
│ ├── karma-test-shim.js
│ └── protractor.conf.js
├── e2e
│ ├── app.e2e-spec.ts
│ ├── app.po.ts
│ ├── tsconfig.json
│ └── typings.d.ts
├── package.json
├── public
├── README.md
├── src
│ ├── app
│ │ ├── app.component.css
│ │ ├── app.component.html
│ │ ├── app.component.spec.ts
│ │ ├── app.component.ts
│ │ ├── environment.ts
│ │ ├── index.ts
│ │ └── shared
│ │ └── index.ts
│ ├── favicon.ico
│ ├── index.html
│ ├── main.ts
│ ├── system-config.ts
│ ├── tsconfig.json
│ └── typings.d.ts
├── tslint.json
├── typings
│ └── ...
└── typings.json
現在,你可以運行如下命令:
#切換到CLI剛剛為你創建的新目錄下
- $ cd angular2-todo-app
#啟動開發服務器
- $ ng serve
上述命令將啟動一個本地開發服務器,你可以在你的瀏覽器中導航到如下URL來觀察你的程序的初始界面:
http://localhost:4200/
使用Angular組件
當我們使用ng new命令時,Angular CLI已經為我們生成整個Angular 2應用程序的樣板內容了。但它并非僅提供這些功能。它還可以幫助我們通過ng generate命令把其他對象添加到我們現有的Angular應用程序中,命令如下:
- # Generate a new component
- $ ng generate component my-new-component
- # Generate a new directive
- $ ng generate directive my-new-directive
- # Generate a new pipe
- $ ng generate pipe my-new-pipe
- # Generate a new service
- $ ng generate service my-new-service
- # Generate a new class
- $ ng generate class my-new-class
- # Generate a new interface
- $ ng generate interface my-new-interface
- # Generate a new enum
- $ ng generate enum my-new-enum
【提示】如果您還不熟悉Angular 2程序中的基本模塊,特別推薦您先讀一下這篇文章(https://angular.io/docs/ts/latest/quickstart.html)。
為了滿足我們的Todo程序的需要,我們還需要實現如下功能:
創建一個Todo類來描述單個todo任務
創建一個TodoService服務來實現創建、更新和刪除已有的todo任務
開發一個TodoApp組件來顯示用戶界面
下面,讓我們一項一項地完成這些任務。
創建Todo類
因為我們使用的是TypeScript腳本語言,所以我們可以使用一個類來描述Todo任務項。我們可以通過Angular CLI命令來生成一個Todo類,命令如下:
- $ ng generate class Todo
上述命令將生成如下兩個文件:
src/app/todo.spec.ts
src/app/todo.ts
讓我們打開文件src/app/todo.ts,并使用如下內容替換掉原來內容:
- export class Todo {
- id: number;
- title: string = '';
- complete: boolean = false;
- constructor(values: Object = {}) {
- Object.assign(this, values);
- }
- }
每一個Todo項都有三個屬性:
id:數字類型,對應于todo項的唯一的ID值
title:字符串類型,對應于todo項的標題
complete:布爾類型,指明當前todo項是否已完成
接下來,開始建立構造函數代碼,從而允許我們在實例化過程中指定屬性值:
- let todo = new Todo({
- title: 'Read SitePoint article',
- complete: false
- });
注意,CLI已經為我們生成了文件src/app/todo.spec.ts,所以我們可以添加一個單元測試來確保上述構造器按我們的期望結果那樣工作:
- import {
- beforeEach, beforeEachProviders,
- describe, xdescribe,
- expect, it, xit,
- async, inject
- } from '@angular/core/testing';
- import {Todo} from './todo';
- describe('Todo', () => {
- it('should create an instance', () => {
- expect(new Todo()).toBeTruthy();
- });
- it('should accept values in the constructor', () => {
- let todo = new Todo({
- title: 'hello',
- complete: true
- });
- expect(todo.title).toEqual('hello');
- expect(todo.complete).toEqual(true);
- });
- });
為了驗證我們的代碼是否按預期方式工作,我們現在可以運行下面的單元測試命令:
- $ ng test
這個命令將運行Karma程序(https://karma-runner.github.io/)來運行上面我們創建的所有單元測試代碼。
目前,我們已經有了一個Todo類。接下來,我們要創建Todo服務來管理所有todo任務項。
創建TodoService服務
TodoService服務將負責管理我們的Todo項目。在以后的文章中,你將會看到我們如何與REST API進行通信;但現在,我們只是在內存中存儲所有的數據。
讓我們再次使用Angular CLI來生成我們的服務:
- $ ng generate service Todo
生成內容如下:
src/app/todo.service.spec.ts
src/app/todo.service.ts
現在,我們可以把todo管理邏輯添加到我們的TodoService了,內容如下:
- import {Injectable} from '@angular/core';
- import {Todo} from './todo';
- @Injectable()
- export class TodoService {
- // Placeholder for last id so we can simulate
- // automatic incrementing of id's
- lastId: number = 0;
- // Placeholder for todo's
- todos: Todo[] = [];
- constructor() {
- }
- // Simulate POST /todos
- addTodo(todo: Todo): TodoService {
- if (!todo.id) {
- todo.id = ++this.lastId;
- }
- this.todos.push(todo);
- return this;
- }
- // Simulate DELETE /todos/:id
- deleteTodoById(id: number): TodoService {
- this.todos = this.todos
- .filter(todo => todo.id !== id);
- return this;
- }
- // Simulate PUT /todos/:id
- updateTodoById(id: number, values: Object = {}): Todo {
- let todo = this.getTodoById(id);
- if (!todo) {
- return null;
- }
- Object.assign(todo, values);
- return todo;
- }
- // Simulate GET /todos
- getAllTodos(): Todo[] {
- return this.todos;
- }
- // Simulate GET /todos/:id
- getTodoById(id: number): Todo {
- return this.todos
- .filter(todo => todo.id === id)
- .pop();
- }
- // Toggle todo complete
- toggleTodoComplete(todo: Todo){
- let updatedTodo = this.updateTodoById(todo.id, {
- complete: !todo.complete
- });
- return updatedTodo;
- }
- }
就本文目的而言,上述方法的具體實現細節并不至關重要。關鍵的內容是我們要實現服務中的業務邏輯。
為了確保我們的邏輯按預期方式工作,讓我們向文件src/app/todo.service.spec.ts(已經由CLI生成)添加單元測試。
因為Angular CLI已經為我們生成了樣板代碼,所以我們只需要實現測試即可:
- import {
- beforeEach, beforeEachProviders,
- describe, xdescribe,
- expect, it, xit,
- async, inject
- } from '@angular/core/testing';
- import {Todo} from './todo';
- import {TodoService} from './todo.service';
- describe('Todo Service', () => {
- beforeEachProviders(() => [TodoService]);
- describe('#getAllTodos()', () => {
- it('should return an empty array by default', inject([TodoService], (service: TodoService) => {
- expect(service.getAllTodos()).toEqual([]);
- }));
- it('should return all todos', inject([TodoService], (service: TodoService) => {
- let todo1 = new Todo({title: 'Hello 1', complete: false});
- let todo2 = new Todo({title: 'Hello 2', complete: true});
- service.addTodo(todo1);
- service.addTodo(todo2);
- expect(service.getAllTodos()).toEqual([todo1, todo2]);
- }));
- });
- describe('#save(todo)', () => {
- it('should automatically assign an incrementing id', inject([TodoService], (service: TodoService) => {
- let todo1 = new Todo({title: 'Hello 1', complete: false});
- let todo2 = new Todo({title: 'Hello 2', complete: true});
- service.addTodo(todo1);
- service.addTodo(todo2);
- expect(service.getTodoById(1)).toEqual(todo1);
- expect(service.getTodoById(2)).toEqual(todo2);
- }));
- });
- describe('#deleteTodoById(id)', () => {
- it('should remove todo with the corresponding id', inject([TodoService], (service: TodoService) => {
- let todo1 = new Todo({title: 'Hello 1', complete: false});
- let todo2 = new Todo({title: 'Hello 2', complete: true});
- service.addTodo(todo1);
- service.addTodo(todo2);
- expect(service.getAllTodos()).toEqual([todo1, todo2]);
- service.deleteTodoById(1);
- expect(service.getAllTodos()).toEqual([todo2]);
- service.deleteTodoById(2);
- expect(service.getAllTodos()).toEqual([]);
- }));
- it('should not removing anything if todo with corresponding id is not found', inject([TodoService], (service: TodoService) => {
- let todo1 = new Todo({title: 'Hello 1', complete: false});
- let todo2 = new Todo({title: 'Hello 2', complete: true});
- service.addTodo(todo1);
- service.addTodo(todo2);
- expect(service.getAllTodos()).toEqual([todo1, todo2]);
- service.deleteTodoById(3);
- expect(service.getAllTodos()).toEqual([todo1, todo2]);
- }));
- });
- describe('#updateTodoById(id, values)', () => {
- it('should return todo with the corresponding id and updated data', inject([TodoService], (service: TodoService) => {
- let todo = new Todo({title: 'Hello 1', complete: false});
- service.addTodo(todo);
- let updatedTodo = service.updateTodoById(1, {
- title: 'new title'
- });
- expect(updatedTodo.title).toEqual('new title');
- }));
- it('should return null if todo is not found', inject([TodoService], (service: TodoService) => {
- let todo = new Todo({title: 'Hello 1', complete: false});
- service.addTodo(todo);
- let updatedTodo = service.updateTodoById(2, {
- title: 'new title'
- });
- expect(updatedTodo).toEqual(null);
- }));
- });
- describe('#toggleTodoComplete(todo)', () => {
- it('should return the updated todo with inverse complete status', inject([TodoService], (service: TodoService) => {
- let todo = new Todo({title: 'Hello 1', complete: false});
- service.addTodo(todo);
- let updatedTodo = service.toggleTodoComplete(todo);
- expect(updatedTodo.complete).toEqual(true);
- service.toggleTodoComplete(todo);
- expect(updatedTodo.complete).toEqual(false);
- }));
- });
- });
【提示】Karma工具中預配置了Jasmine(https://github.com/jasmine/jasmine),你可以閱讀資料http://jasmine.github.io/2.4/introduction.html來更多地了解有關它的語法。
為了校驗我們編寫的業務邏輯都是有效的,讓我們再來運行單元測試:
- $ ng test
現在,既然我們已經有了一個可以使用的TodoService,那么接下來我們要實現程序的UI部分了。
值得注意的是,在Angular 2中,部分界面是使用組件(Components)來描述的。
創建TodoApp組件
讓我們再一次使用CLI來生成我們所需要的程序組件吧:
- $ ng generate component TodoApp
上述命令生成內容如下:
- src/app/todo-app/todo-app.component.css
- src/app/todo-app/todo-app.component.html
- src/app/todo-app/todo-app.component.spec.ts
- src/app/todo-app/todo-app.component.ts
- src/app/todo-app/index.ts
【提示】 模板和樣式可以在內聯的腳本文件內指定。默認情況下,Angular的CLI將創建單獨的文件;所以,在這篇文章中我們也是使用單獨的文件。
接下來,讓我們把組件的視圖添加到文件src/app/todo-app/todo-app.component.html中:
- <section class="todoapp">
- <header class="header">
- <h1>Todos</h1>
- <input class="new-todo" placeholder="What needs to be done?" autofocus="" [(ngModel)]="newTodo.title" (keyup.enter)="addTodo()">
- </header>
- <section class="main" *ngIf="todos.length > 0">
- <ul class="todo-list">
- <li *ngFor="let todo of todos" [class.completed]="todo.complete">
- <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>
- </li>
- </ul>
- </section>
- <footer class="footer" *ngIf="todos.length > 0">
- <span class="todo-count"><strong>{{todos.length}}</strong> {{todos.length == 1 ? 'item' : 'items'}} left</span>
- </footer>
- </section>
在此,我們使用了Angular的超級短小的模板語法表達方式——而這是你以前從未遇到過的:
[property]="expression":把屬性設置為expression的結果
(event)=”statement”:當事情發生時執行statement
[(property)]="expression":使用expression創建雙向綁定
[class.special]="expression":當expression為真時在元素上添加special類
[style.color]="expression":把css屬性color設置為expression的結果
【提示】如果你還不熟悉Angular的模板語法,那么你應當閱讀一下官方有關文檔,地址是https://angular.io/docs/ts/latest/guide/template-syntax.html。
下面,讓我們具體地看一下上面的代碼對我們的視圖的影響。首先,在頂部使用了一個Input控件來創建一個新的todo項:
- <input class="new-todo" placeholder="What needs to be done?" autofocus="" [(ngModel)]="newTodo.title" (keyup.enter)="addTodo()">
在這里:
[(ngModel)]="newTodo.title":在input值與newTodo.title之間創建一個雙向綁定。
(keyup.enter)=”addTodo()”:在Input控件中輸入內容并在按下回車時告訴Angular執行addTodo()命令。
【提示】目前你先不用擔心newTodo和addTodo()的存在問題,稍后會做這項工作。現在,只需盡力弄懂視圖語義即可。
接下來,使用一個節顯示todo部分:
- <section class="main" *ngIf="todos.length > 0">
其中,*ngIf="todos.length > 0"的含義是:當至少有一個todo項時,僅顯示section部分及其所有后代節點的內容。
在該節中,我們要求Angular為每一個todo生成一個li元素:
- <li *ngFor="let todo of todos" [class.completed]="todo.complete">
其中:
*ngFor="let todo of todos":遍歷所有的todo并在每一次循環中把當前todo賦值給一個命名為todo的變量。
[class.completed]="todo.complete":當todo.complete為真時把CSS類completed應用于元素li。
最后,我們通過ngFor循環顯示每一個todo項目的詳細信息:
- <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>
在這里:
(click)="toggleTodoComplete(todo)":當勾選復選框時執行toggleTodoComplete(todo)
[checked]="todo.complete":把值todo.complete賦給元素的checked屬性
(click)="removeTodo(todo)":當點擊刪除按鈕時執行removeTodo(todo)
好,讓我們稍微喘口氣吧。到此我們已經使用了不少新的語法格式。
你可能想知道像addTodo()和newTodo.title這樣的表達式是如何計算的。到目前,我們還沒有定義它們,那么Angular是如何理解我們的意圖的呢?
這正是表達式上下文(expression context)產生的原因。一個組件的表達式上下文就是組件實例。而組件實例就是組件類的一個實例。
我們的TodoAppComponent的組件類定義于文件src/app/todo-app/todo-app.component.ts中。
Angular CLI已經為我們的TodoAppComponent類創建了模板,代碼如下:
- import { Component } from '@angular/core';
- @Component({
- moduleId: module.id,
- selector: 'app-todo-app',
- templateUrl: 'todo-app.component.html',
- styleUrls: ['todo-app.component.css']
- })
- export class TodoAppComponent {
- constructor() {}
- }
所以,我們可以馬上開始加入我們自定義的邏輯。我們將需要TodoService實例;因此,讓我們開始將它注入到我們的組件中。
首先,我們導入TodoService類,并在組件的修飾詞數組部分指定它:
- // Import class so we can register it as dependency injection token
- import {TodoService} from '../todo.service';
- @Component({
- // ...
- providers: [TodoService]
- })
- export class TodoAppComponent {
- // ...
- }
TodoAppComponent的依賴注入器現在能夠識別出TodoService類為依賴性注入符號并在我們要求時返回TodoService的單一實例。
【提示】Angular的依賴注入系統能夠接受各種各樣的依賴項注入。上述語法只是類提供器(Class Provider:使用單例模式提供依賴性)格式的一個速記表示。有關此內容更多的細節,請參考官方的網址https://angular.io/docs/ts/latest/guide/dependency-injection.html。
現在,組件的依賴注入器知道它需要提供什么了,我們要求它通過在TodoAppComponent構造函數中指定依賴項來在我們的組件中注入TodoService實例:
- // Import class so we can use it as dependency injection token in the constructor
- import {TodoService} from '../todo.service';
- @Component({
- // ...
- })
- export class TodoAppComponent {
- // Ask Angular DI system to inject the dependency
- // associated with the dependency injection token `TodoService`
- // and assign it to a property called `todoService`
- constructor(private todoService: TodoService) {
- }
- // Service is now available as this.todoService
- toggleTodoComplete(todo) {
- this.todoService.toggleTodoComplete(todo);
- }
- }
現在,我們可以實現我們的視圖中需要的所有邏輯了。為此,只需要向我們 TodoAppComponent類中添加屬性和方法就可以了:
- import {Component} from '@angular/core';
- import {Todo} from '../todo';
- import {TodoService} from '../todo.service';
- @Component({
- moduleId: module.id,
- selector: 'todo-app',
- templateUrl: 'todo-app.component.html',
- styleUrls: ['todo-app.component.css'],
- providers: [TodoService]
- })
- export class TodoAppComponent {
- newTodo: Todo = new Todo();
- constructor(private todoService: TodoService) {
- }
- addTodo() {
- this.todoService.addTodo(this.newTodo);
- this.newTodo = new Todo();
- }
- toggleTodoComplete(todo) {
- this.todoService.toggleTodoComplete(todo);
- }
- removeTodo(todo) {
- this.todoService.deleteTodoById(todo.id);
- }
- get todos() {
- return this.todoService.getAllTodos();
- }
- }
當組件類實例化時,我們首先實例化一個newTodo屬性并分配新的Todo()。下面的代碼展示了在我們的視圖中添加的雙向綁定到的newTodo:
- <input class="new-todo" placeholder="What needs to be done?" autofocus="" [(ngModel)]="newTodo.title" (keyup.enter)="addTodo()">
無論視圖中的輸入值何時改變,組件實例中的值都被更新。而無論組件實例中的輸入值何時改變,視圖中的輸入元素中的值都將更改。
接下來,我們要實現我們的視圖中使用的所有方法。
它們的具體實現代碼很短,應該是不需要給予過多解釋的,因為我們已經把所有業務邏輯委派到todoService了。
【提示】把業務邏輯委派到一個專門的服務中是一種良好的編程實踐,因為它使我們能夠集中精力管理和測試業務邏輯。
最后,在結束本教程前,讓我們來了解一下Angular CLI的最后一個很酷的功能吧。
部署到GitHub網站
Angular的CLI使得將我們的應用部署到GitHub頁變得超級簡單——使用類似于下面的這樣一個命令即可搞定:
- $ ng github-pages:deploy --message 'deploy(dist): deploy on GitHub pages'
這個github-pages:deploy命令告訴Angular CLI生成我們的Angular應用的一個靜態版本,并將它推送到我們的GitHub倉庫的gh-pages分支下。相應的輸出結果如下所示:
- $ ng github-pages:deploy --message 'deploy(dist): deploy on GitHub pages'
- Built project successfully. Stored in "dist/".
- Deployed! Visit https://sitepoint-editors.github.io/angular2-todo-app/
- Github pages might take a few minutes to show the deployed site.
現在,我們的應用程序可以通過網站地址https://sitepoint-editors.github.io/angular2-todo-app/進行訪問了。
趕快去打開這個網址去試試吧。
小結
Angular 2無疑是一只猛獸!一只非常強大的猛獸!
在本文中,我向你介紹了很多很多。現在,讓我們回顧一下我們在這篇文章中所學到的內容吧:
我們學習了如何安裝Angular CLI并了解了在創建新的應用程序或添加現有應用程序的新特征時它如何節約我們的時間。
我們學習了如何在一個Angular服務中實現業務邏輯以及如何使用單元測試來測試我們的業務邏輯。
我們學習了如何使用組件與用戶交互以及如何使用依賴注入委派邏輯到服務中。
我們學習了Angular模板語法基礎知識,并簡要地談論了Angular依賴項注入的工作原理。
最后,我們學習了如何把我們的應用程序快速部署到GitHub網頁。
在以后的文章中,我們還有很多有關Angular 2的內容探討,例如:
使用Angular 2 HTTP服務與REST API后端進行通信
使用Angular管道功能過濾todo內容
通過路由來使本文中的應用變成一個多頁式應用程序
以及其他更多更多……
所以,敬請期待更多的關于Angular 2這個奇妙的世界吧。