文档结构  
翻译进度:已翻译     翻译赏金:0 元 (?)    ¥ 我要打赏

我开始创建Angular 2应用程序的时候,Angular 2还是测试版(回到3个月前)。 为了跟上Angular 2的变化,我在6月份写了一个关于使用RC1开发的教程 。但在本月的早些时候发布了RC5,许多事情再次改变 。 我认为Scott Davis在推文中总结得很好。

他们一直在说 “候选版本”,但我并不认为这代表着他们真正想要的含义...

/cc #angular2#rc5https://t.co/WmNalTYgTN

— Scott Davis (@scottdavis99) August 10, 2016

为了跟上Angular 2的快速变化,我决定再写另一个教程,这次使用 Angular CLI。    在我写最后一篇教程时,我发现的其中最大的变化是测试基础设施的变化。 由于 Angular's的测试文档 最近还没有更新,所以我希望本教程对大家将有所帮助。

第 1 段(可获 1.7 积分)

下面是一个目录,你可以跳转到特定的部分。

  • 你会创建什么
  • 你需要什么
  • 创建项目
  • 运行应用程序
  • 添加搜索功能
    • 基础
    • 后端
  • 添加编辑功能
  • 测试和CI (在下一篇文章中讨论)

你会创建什么

你将使用 Angular CLI创建一个简单的web应用程序, Angular CLI是一个全新的 Angular 2开发工具。  你将会创建一个具有搜索和编辑功能的应用程序。

你需要什么

第 2 段(可获 1.65 积分)

由于新版本的Angular CLI(beta 10)使用的是Angular 2 RC4。  因此,我使用Angular CLI的主分支来创建本教程。 为此,克隆 angular-cli 项目,并在克隆的目录中运行npm link。 如果您有问题,请参阅 #1733.。

Angular Augury 是一个Google Chrome开发工具扩展,专门用于调试Angular 2应用程序。 我现在并不需要它,但我相信它迟早会派上用场。

创建你的项目

创建一个新的项目,使用 ng new 命令:

ng new ng2-demo


这样将会创建一个 ng2-demo 项目并且在项目中运行 npm install 命令, 完成整个过程大约需要一分钟,但这跟你的网速相关,花费时间可能有所不同。

[mraible:~/dev] 45s $ ng new ng2-demo
installing ng2
  create .editorconfig
  create README.md
  create src/app/app.component.css
  create src/app/app.component.html
  create src/app/app.component.spec.ts
  create src/app/app.component.ts
  create src/app/environment.ts
  create src/app/index.ts
  create src/app/shared/index.ts
  create src/favicon.ico
  create src/index.html
  create src/main.ts
  create src/system-config.ts
  create src/tsconfig.json
  create src/typings.d.ts
  create angular-cli-build.js
  create angular-cli.json
  create config/environment.dev.ts
  create config/environment.js
  create config/environment.prod.ts
  create config/karma-test-shim.js
  create config/karma.conf.js
  create config/protractor.conf.js
  create e2e/app.e2e-spec.ts
  create e2e/app.po.ts
  create e2e/tsconfig.json
  create e2e/typings.d.ts
  create .gitignore
  create package.json
  create public/.npmignore
  create tslint.json
  create typings.json
Successfully initialized git.
- Installing packages for tooling via npm
  -- es6-shim (global)
  -- angular-protractor (global dev)
  -- jasmine (global dev)
  -- selenium-webdriver (global dev)

Installed packages for tooling via npm.
[mraible:~/dev] 1m5s $
第 3 段(可获 1.41 积分)

你可以使用ng --version 命令来查看Angular CLI的版本

$ ng --version
angular-cli: local (v1.0.0-beta.11-webpack.2, branch: master)
node: 4.4.7
os: darwin x64


运行应用程序

这个项目配置了一个简单的Web服务器来进行开发。  如果需要启动它,请运行:

ng serve


你在http://localhost:4200站点下,应该可以看到如下图所示的内容:

Default Homepage

为了确保你的新项目的测试通过,可以运行ng test进行测试:

$ ng test
Built project successfully. Stored in "dist/".
...
Chrome 52.0.2743 (Mac OS X 10.11.6): Executed 2 of 2 SUCCESS (0.039 secs / 0.012 secs)
第 4 段(可获 0.69 积分)

添加搜索功能

要添加搜索功能,请在IDE或您喜欢的文本编辑器中打开项目。 对于IntelliJ IDEA,使用文件>新建项目>静态Web 并指向 ng2-demo 目录。

基础

在终端窗口中,cd进入项目的目录,并运行以下命令。 这将创建一个搜索组件。

$ ng g component search
installing component
  create src/app/search/search.component.css
  create src/app/search/search.component.html
  create src/app/search/search.component.spec.ts
  create src/app/search/search.component.ts
  create src/app/search/index.ts
第 5 段(可获 0.71 积分)

添加搜索路径

在以前的CLI版本中,您可以生成路由和组件。 但是,由于beta 8,路由生成已禁用。  但是在未来的版本中可能会重新启用。

Angular 2 RC5的 路由器文档 为你提供了刚刚生成的 SearchComponent 组件设置路由所需的信息。 这里是一个快速摘要:

创建src/app/app.routing.ts 文件来定义你的路由。

import { Routes, RouterModule } from '@angular/router';
import { SearchComponent } from './search/index';

const appRoutes: Routes = [
  { path: 'search', component: SearchComponent },
  { path: '', redirectTo: '/search', pathMatch: 'full' }
];

export const appRoutingProviders: any[] = [];

export const routing = RouterModule.forRoot(appRoutes);
第 6 段(可获 0.83 积分)

如果有最后一个路径的重定向,那么不能匹配任何路由: ':''控制台错误”。

src/app/app.module.ts文件中,导入您导出的两个常量并在@NgModule中配置它们:

import { routing, appRoutingProviders } from './app.routing';

import { SearchComponent } from './search/search.component';

@NgModule({
  ...
  imports: [
    ...
    routing
  ],
  providers: [appRoutingProviders],
  ...
})
export class AppModule { }


src/app/app.component.html 文件中,添加一个RouterOutlet用来显示路由。

<!-- Routed views go here -->
<router-outlet></router-outlet>
第 7 段(可获 0.4 积分)

现在你已已经完成路由设置,您可以继续编写搜索功能。

为了可以导航到SearchComponent组件,您可以在 src/app/app.component.html 文件中添加一个链接。

<nav>
  <a routerLink="/search" routerLinkActive="active">Search</a>
</nav>

打开src/app/search/search.component.html文件,并且将其默认 HTML替换成如下内容:

<h2>Search</h2>
<form>
  <input type="search" name="query" [(ngModel)]="query" (keyup.enter)="search()">
  <button type="button" (click)="search()">Search</button>
</form>
<pre>{{searchResults | json}}</pre>

如果你的 ng serve 仍然正在运行,那么你的浏览器应该会自动刷新。 如果没有,导航到 http://localhost:4200  站点,这时你应该看到搜索表单。

第 8 段(可获 0.71 积分)

Search component

如果你想为这个组件添加CSS,打开 src/app/search/search.component.css 并添加一些CSS。 例如:

:host {
  display: block;
  padding: 0 20px;
}


本节介绍了如何使用Angular CLI为基本Angular 2应用程序创建新组件。 下一节将介绍如何使用JSON文件和localStorage创建假API。

后端

为了获取搜索结果,请创建一个SearchService,向JSON文件发出HTTP请求。  用新创建的服务启动。

ng g service search

将生成的search.service.ts文件及其测试移动到app / shared / search文件夹。  你需要自行创建该目录。

然后,创建src / app / shared / search / data / people.json文件来保存你的数据。

[
  {
    "id": 1,
    "name": "Peyton Manning",
    "phone": "(303) 567-8910",
    "address": {
      "street": "1234 Main Street",
      "city": "Greenwood Village",
      "state": "CO",
      "zip": "80111"
    }
  },
  {
    "id": 2,
    "name": "Demaryius Thomas",
    "phone": "(720) 213-9876",
    "address": {
      "street": "5555 Marion Street",
      "city": "Denver",
      "state": "CO",
      "zip": "80202"
    }
  },
  {
    "id": 3,
    "name": "Von Miller",
    "phone": "(917) 323-2333",
    "address": {
      "street": "14 Mountain Way",
      "city": "Vail",
      "state": "CO",
      "zip": "81657"
    }
  }
]
第 9 段(可获 1.23 积分)

修改 src/app/shared/search/search.service.ts 文件,并在其构造函数中提供Http作为依赖项。 在同一个文件中,创建一个getAll()方法来收集所有人的请求。 另外,定义 AddressPerson 类,这些类JSON将进行编组。

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

@Injectable()
export class SearchService {
  constructor(private http: Http) {}

  getAll() {
    return this.http.get('app/shared/search/data/people.json').map((res: Response) => res.json());
  }
}

export class Address {
  street: string;
  city: string;
  state: string;
  zip: string;

  constructor(obj?: any) {
    this.street = obj && obj.street || null;
    this.city = obj && obj.city || null;
    this.state = obj && obj.state || null;
    this.zip = obj && obj.zip || null;
  }
}

export class Person {
  id: number;
  name: string;
  phone: string;
  address: Address;

  constructor(obj?: any) {
    this.id = obj && Number(obj.id) || null;
    this.name = obj && obj.name || null;
    this.phone = obj && obj.phone || null;
    this.address = obj && obj.address || null;
  }
}
第 10 段(可获 0.4 积分)

为了使这些类可供组件使用,请编辑src / app / shared / index.ts并添加以下内容:

export * from './search/search.service';

search.component.ts文件中, 为这些类添加导入:

import { Person, SearchService } from '../shared/index';

您现在可以添加querysearchResults 变量。 当你进行到这一步时,修改构造函数以注入 SearchService

export class SearchComponent implements OnInit {
  query: string;
  searchResults: Array<Person>;

  constructor(private searchService: SearchService) {}

然后实现search()方法来调用服务的getAll()方法。

第 11 段(可获 0.59 积分)
search(): void {
  this.searchService.getAll().subscribe(
    data => { this.searchResults = data; },
    error => console.log(error)
  );
}

此时,您可能会在浏览器的控制台中看到以下消息。

ORIGINAL EXCEPTION: No provider for SearchService!

为了修复上面的“No provider”错误,请更新app.component.ts 文件来导入 SearchService ,并将该服务添加到提供程序列表。

import { SearchService } from './shared/index';

@Component({
  ...
  styleUrls: ['app.component.css'],
  viewProviders: [SearchService]
})
第 12 段(可获 0.45 积分)

现在点击搜索按钮应该可以工作了。  但要是使结果更好看,请删除<pre>标记并将其替换为<table>。

<table *ngIf="searchResults">
  <thead>
  <tr>
    <th>Name</th>
    <th>Phone</th>
    <th>Address</th>
  </tr>
  </thead>
  <tbody>
  <tr *ngFor="let person of searchResults; let i=index">
    <td>{{person.name}}</td>
    <td>{{person.phone}}</td>
    <td>{{person.address.street}}<br/>
      {{person.address.city}}, {{person.address.state}} {{person.address.zip}}
    </td>
  </tr>
  </tbody>
</table>

然后添加一些额外的CSS以改进其表布局。

第 13 段(可获 0.39 积分)
table {
  margin-top: 10px;
  border-collapse: collapse;
}

th {
  text-align: left;
  border-bottom: 2px solid #ddd;
  padding: 8px;
}

td {
  border-top: 1px solid #ddd;
  padding: 8px;
}

现在搜索的结果看起来效果更好

Search Results

但是等等,我们还是没有搜索功能! 要添加搜索功能,请向SearchService添加search()方法。

search(q: string) {
  if (!q || q === '*') {
    q = '';
  } else {
    q = q.toLowerCase();
  }
  return this.getAll().map(data => {
    let results: any = [];
    data.map(item => {
      if (JSON.stringify(item).toLowerCase().includes(q)) {
        results.push(item);
      }
    });
    return results;
  });
}
第 14 段(可获 0.3 积分)

然后 重构 SearchComponent ,用query 变量来调用该方法.

search(): void {
  this.searchService.search(this.query).subscribe(
    data => { this.searchResults = data; },
    error => console.log(error)
  );
}

现在,搜索结果将按您输入的查询值过滤。

本节向您介绍如何获取和显示搜索结果。 下一部分基于此并展示如何编辑和保存记录。

添加编辑功能

修改search.component.html文件 添加用于编辑人员的点击的句柄。

 <td><a (click)="onSelect(person)">{person.name}</a></td> 
第 15 段(可获 0.78 积分)

在以前的Angular 2版本中,你可以将带有参数的链接直接嵌入到HTML中。 例如:

<a [routerLink]="['/edit', person.id]">

不幸的是,这不适用于RC5。 另一个问题是添加href =“”导致页面刷新。如果没有href,那么链接看起来不像链接。 如果你知道这个问题的解决方案,还请你告知我一声。

然后,添加 onSelect(person)search.component.ts文件中。  你需要导入路由器并将其设置为一个局部变量来完成这项工作。

import { Router } from '@angular/router';
...
export class SearchComponent implements OnInit {
  ...

  constructor(private searchService: SearchService, private router: Router) { }

  ...

  onSelect(person: Person) {
    this.router.navigate(['/edit', person.id]);
  }
}
第 16 段(可获 0.98 积分)

运行以下命令生成EditComponent.

$ ng g component edit
installing component
  create src/app/edit/edit.component.css
  create src/app/edit/edit.component.html
  create src/app/edit/edit.component.spec.ts
  create src/app/edit/edit.component.ts
  create src/app/edit/index.ts

app.routing.ts 文件中为此组件添加路由:

import { EditComponent } from './edit/index';

const appRoutes: Routes = [
  { path: 'search', component: SearchComponent },
  { path: 'edit/:id', component: EditComponent },
  { path: '', redirectTo: '/search', pathMatch: 'full' }
];

更新 src/app/edit/edit.component.html 文件来显示可编辑的表单。 你可能注意到我已经添加了id属性到大多数元素。 这是为了使用Protractor来编写集成测试时更容易。

<div *ngIf="person">
  <h3>{{editName}}</h3>
  <div>
    <label>Id:</label>
    {{person.id}}
  </div>
  <div>
    <label>Name:</label>
    <input [(ngModel)]="editName" name="name" id="name" placeholder="name"/>
  </div>
  <div>
    <label>Phone:</label>
    <input [(ngModel)]="editPhone" name="phone" id="phone" placeholder="Phone"/>
  </div>
  <fieldset>
    <legend>Address:</legend>
    <address>
      <input [(ngModel)]="editAddress.street" id="street"><br/>
      <input [(ngModel)]="editAddress.city" id="city">,
      <input [(ngModel)]="editAddress.state" id="state" size="2">
      <input [(ngModel)]="editAddress.zip" id="zip" size="5">
    </address>
  </fieldset>
  <button (click)="save()" id="save">Save</button>
  <button (click)="cancel()" id="cancel">Cancel</button>
</div>
第 17 段(可获 0.53 积分)

修改 EditComponent 以导入模型和服务类,并使用 SearchService 来获取数据。

import { Component, OnInit, OnDestroy } from '@angular/core';
import { Address, Person, SearchService } from '../shared/index';
import { Subscription } from 'rxjs';
import { ActivatedRoute, Router } from '@angular/router';

@Component({
  selector: 'app-edit',
  templateUrl: 'edit.component.html',
  styleUrls: ['edit.component.css']
})
export class EditComponent implements OnInit, OnDestroy {
  person: Person;
  editName: string;
  editPhone: string;
  editAddress: Address;

  sub: Subscription;

  constructor(private route: ActivatedRoute,
              private router: Router,
              private service: SearchService) {
  }

  ngOnInit() {
    this.sub = this.route.params.subscribe(params => {
      let id = + params['id']; // (+) converts string 'id' to a number
      this.service.get(id).subscribe(person => {
        if (person) {
          this.editName = person.name;
          this.editPhone = person.phone;
          this.editAddress = person.address;
          this.person = person;
        } else {
          this.gotoList();
        }
      });
    });
  }

  ngOnDestroy() {
    this.sub.unsubscribe();
  }

  cancel() {
    this.router.navigate(['/search']);
  }

  save() {
    this.person.name = this.editName;
    this.person.phone = this.editPhone;
    this.person.address = this.editAddress;
    this.service.save(this.person);
    this.gotoList();
  }

  gotoList() {
    if (this.person) {
      this.router.navigate(['/search', {term: this.person.name} ]);
    } else {
      this.router.navigate(['/search']);
    }
  }
}

修改 SearchService 以包含通过其ID查找人员并保存的功能。 当你进行到这一步的时候,修改 search() 方法来获取 localStorage 中更新的对象。

search(q: string) {
  if (!q || q === '*') {
    q = '';
  } else {
    q = q.toLowerCase();
  }
  return this.getAll().map(data => {
    let results: any = [];
    data.map(item => {
      // check for item in localStorage
      if (localStorage['person' + item.id]) {
        item = JSON.parse(localStorage['person' + item.id]);
      }
      if (JSON.stringify(item).toLowerCase().includes(q)) {
        results.push(item);
      }
    });
    return results;
  });
}

get(id: number) {
  return this.getAll().map(all => {
    if (localStorage['person' + id]) {
      return JSON.parse(localStorage['person' + id]);
    }
    return all.find(e => e.id === id);
  });
}

save(person: Person) {
  localStorage['person' + person.id] = JSON.stringify(person);
}
第 18 段(可获 0.54 积分)

如果你想使表单看起来更美观一些,你可以添加CSS到 src/app/edit/edit.component.css 文件中。

:host {
  display: block;
  padding: 0 20px;
}

button {
  margin-top: 10px;
}

此时,你应该能够搜索某人并可以更新其信息。

Edit form

在src/app/edit/edit.component.html 文件中的<form>调用 save() 函数来更新人员的数据。你已经实现了上面的所有过程。 该函数调用 gotoList() 函数,会在将用户搜索的结构返回到搜索屏幕时,将 person's 的名字附加到URL。

gotoList() {
  if (this.person) {
    this.router.navigate(['/search', {term: this.person.name} ]);
  } else {
    this.router.navigate(['/search']);
  }
}
第 19 段(可获 0.9 积分)

当您执行此URL时,由于 SearchComponent 不会自动执行搜索,因此需要在其构造函数中添加以下逻辑。

import { Router, ActivatedRoute } from '@angular/router';
import { Subscription } from 'rxjs';
...
  sub: Subscription;

  constructor(private searchService: SearchService, private router: Router, private route: ActivatedRoute) {
    this.sub = this.route.params.subscribe(params => {
      if (params['term']) {
        this.query = decodeURIComponent(params['term']);
        this.search();
      }
    });
  }
第 20 段(可获 0.29 积分)

您将需要实现 OnDestroy 并定义 ngOnDestroy 方法来清理此订阅。

import { Component, OnInit, OnDestroy } from '@angular/core';

export class SearchComponent implements OnInit, OnDestroy {
...
  ngOnDestroy() {
    this.sub.unsubscribe();
  }
}

进行完所有这些更改后,你应该能够搜索/编辑/更新某人的信息。 如果它可以正常工作 --- 干的漂亮!

结论

等等,我们还没有完成! 请继续关注第2部分,我们将继续在Angular 2中进行自动化测试和持续集成。

第 21 段(可获 0.8 积分)

文章评论