angular 手机端开发
在本文中,我们展示了如何使用Angular 2 MockBackend类开发应用程序,为前端团队提供了一种独立于后端的方法,并提供了一种有用的界面,可以降低结构变更的风险。
每个公司都在寻找使您的前端和后端团队达到最高速度的方法。 但是,团队经常陷入阻碍依赖的陷阱。 在这种情况下,一个团队的即将开展的工作被另一团队拥有的用户故事阻止了。
这些示例之一是前端和后端之间的通信过程。 近年来,REST API登上了所谓通信标准的宝座。 使用JSON(一种简单而有效的数据传输格式)的好处是,前端工作人员不再需要关心实际的后端。 任何跨越线路的都是直接消耗的,并且可以被利用来将数据带入您的应用程序。 因此,毫不奇怪的是,这些基本实体通常根本不在前端建模,而在它们到达时就被消耗掉了。 这给我们带来了根本的问题,即必须等待后端团队提供有用的东西。 如下图所示,我们看到两个团队并行启动,但是在某个时间,一个团队一直在等待另一个团队追赶。
除此之外,没有一种固定的结构会使每个更改成为潜在的危险。 因此,本文的重点是提出一种方法,使前端团队可以独立于后端,同时提供有用的界面,以减少结构更改的风险。
本文已根据Angular 2.1.2版本的最新更新进行了更新。 链接的Plunkr示例应用程序也已更新。
没有真实后端的票务系统
为了实现这种独立性,必须开始对项目进行思考。 您将使用什么实体? 因此,将导致哪些通信端点?
这可以通过创建一个小表突出显示必要的REST端点并描述其目的来完成。 记住我们之所以这样做是因为双方要就共同的沟通结构达成一致。 这并不意味着它必须完美地完成,但是它应该可以帮助您开始最重要的步骤。 随着时间的流逝,只需使用所需的新路由相应地更新您的界面即可。
创建后端环境的实际过程是捕获所有HTTP请求,而不是让它们发疯,然后用包含我们想要的信息的假响应进行回复。 本文将通过描述一个简单的票务系统来演示该方法。 它使用下表中显示的端点。
请注意,该示例将POST
动词用于更新和创建路由。 另一种选择是利用PUT
进行更新 。 但是请记住,PUT应该是幂等的 ,这意味着每个连续的调用都必须产生相同的结果。 随意选择您需要的任何套房。
方法 | 路线 | 要求正文 | 描述 |
---|---|---|---|
得到 | /票 | 没有 | 索取所有门票 |
得到 | / ticket /:id | 没有 | 通过提供的:id参数请求一张票证 |
开机自检 | /票 | 票务实体 | 创建新票证或更新现有票证 |
删除 | / ticket /:id | 没有 | 删除由:id参数标识的票证 |
表1:售票系统的消耗端点
Ticket实体是一个简单的TypeScript类,其中包含一些基本的票证信息:
export class Ticket {
public _id: string;
public title: string;
public assignedTo: string;
public description: string;
public percentageComplete: number;
constructor(id: string, title: string, assignedTo: string,
description: string, percentageComplete: number) {
this._id = id;
this.title = title;
this.assignedTo = assignedTo;
this.description = description;
this.percentageComplete = percentageComplete;
}
}
ticket.entity.ts
描述票证实体
您可以在Plunker上找到完整的代码以及此示例的预览:
Angular 2项目设置
理论足够多,让我们开始编写一些代码。 此处显示的项目结构是基于建议的《 Angular 2入门指南》构建的。 这样,我们将不会浪费太多时间来解释它的每个部分。 如果您正在寻找介绍性文章,请查看使用TypeScript的Angular 2入门 。 对于本文,您可以打开上述的Plunker来遵循下面说明的代码部分。
由于大多数单页应用程序都是以index.html
文件开头的,所以让我们先来看一下。 第一部分导入必要的polyfill。 接下来,我们可以看到对system.config.js
另一个引用,该引用除其他外还配置了第三方依赖关系和Angular的应用程序文件。 Reactive Extensions(Rx)实际上不是真正的依赖项,但是可以简化Angular的observable的工作,它们是以前使用的Promises的替代品。 我强烈推荐Cory Rylan撰写的这篇文章,以了解有关此主题的更多信息。
请注意,建议不要使用手动脚本引用来创建可用于生产环境的应用程序。 您应该使用像npm或jspm这样的包管理器。 后一部分与SystemJS配合使用,如第二部分所述。 SystemJS是以前基于ECMAScript 2015草案的模块加载器,现在是WHATWG加载器规范的一部分 。 因此,它可以使用import x from 'module'
语法中的import x from 'module'
。 为了正确使用它,我们需要在前面提到的文件system.config.js
对其进行配置,然后导入该应用程序的主入口点app
,该app
指向文件app/boot.ts
本文不会深入探讨system.config.js
细节,因为这些只是作为示例,是基于Angular Quickstart示例的。
免费学习PHP!
全面介绍PHP和MySQL,从而实现服务器端编程的飞跃。
原价$ 11.95 您的完全免费
最后,我们使用名为my-app
的自定义标签创建应用my-app
。 这些称为“组件”,在某种程度上可与Angular.JS 1.x指令相提并论。
<!DOCTYPE html>
<html>
<head>
<title>ng2 Ticketing System</title>
<!-- 1. Load libraries -->
<!-- Polyfill(s) for older browsers -->
<script src="https://unpkg.com/core-js/client/shim.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.10.1/lodash.min.js"></script>
<script src="https://unpkg.com/zone.js@0.6.25?main=browser"></script>
<script src="https://unpkg.com/reflect-metadata@0.1.8"></script>
<script src="https://unpkg.com/systemjs@0.19.39/dist/system.src.js"></script>
<!-- 2. Configure SystemJS -->
<script src="system.config.js"></script>
<script>
System.import('app')
.then(null, console.error.bind(console));
</script>
<meta charset="utf-8"/>
<link href="vendor/bootstrap/css/bootstrap.min.css" rel="stylesheet"/>
<link rel="stylesheet" href="styles.css"/>
</head>
<!-- 3. Display the application -->
<body>
<my -app>Loading ...</my>
</body>
</html>
boot.ts
文件用于将Angular引导到my-app
组件中。 连同所有特定于应用程序的代码,它位于文件夹app
。 在boot.ts
内部,我们将执行必要的第一步,以利用boot.ts
后端,该后端将替代真实的后端。
我们首先创建一个根模块来容纳我们的应用程序。 它的provider
部分用于告诉Angular的DI(依赖项注入)系统我们要使用的类的实际实例以及所需的依赖项。 BaseRequestOptions
提供了常规的http帮助器,而MockBackend注册了一个模拟实现的实例,我们将用它来创建虚假的回复。 如果查看第三个提供程序配置,创建Http
服务的自定义实例,我们可以看到请求的依赖项( deps
)传递给useFactory
方法。 然后将它们用于创建Http
的新实例。
然后,使用imports
属性声明其他模块依赖项,然后declarations
,以注册根模块的所有可用组件。 通过模块范围的注册,每个组件都可以知道可用的组件,而不必像以前的Angular 2版本那样显式声明指令请求。最后一个属性bootstrap
用于声明哪个组件应该是入口点。
最后,使用bootstrapModule方法启动应用程序。
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { MockBackend } from '@angular/http/testing';
import { Http, BaseRequestOptions } from '@angular/http';
import { FormsModule } from '@angular/forms';
import {AppComponent} from './app.component';
import {TicketComponent} from './ticket.component';
@NgModule({
providers: [
BaseRequestOptions,
MockBackend,
{
provide: Http,
deps: [MockBackend, BaseRequestOptions],
useFactory: (backend, options) => { return new Http(backend, options); }
}
],
imports: [BrowserModule, FormsModule],
declarations: [ AppComponent, TicketComponent ],
bootstrap: [AppComponent]
})
export class AppModule { }
const platform = platformBrowserDynamic();
platform.bootstrapModule(AppModule);
MockBackend类最初旨在用于单元测试方案中,以便模拟实际的服务器调用,从而保持单元测试的快速和隔离。 您可以在官方Http文档中阅读有关此内容的更多信息。
使用组件
现在该看一下完成的应用程序,以标识我们将要使用的组件。 与每个Angular 2应用程序一样,有一个所谓的AppComponent
,它充当该应用程序的主要入口点。 它也可以用作容器,显示常规导航和托管子组件。 说到这些,我们可以看到TicketComponent
被重复用于显示多个票证实体。
应用程序组件配置为与选择器my-app
,加载位于templates
子文件夹中的模板index.html
。 最后, providers
告诉Angular的DI,我们想获取TicketService
的实例。
...
@Component({
selector: 'my-app',
templateUrl: 'app/templates/index.html',
providers: [TicketService]
})
export class AppComponent {
接下来,我们定义一个db
类属性,该属性将保存一组伪造的Tickets。
// Fake Tickets DB
private db: Ticket[] = [
new Ticket(
'1', 'Missing Exception', 'John Smith',
'Method XYZ should throw exception in case ABC', 0),
new Ticket(
'2', 'Log errors', 'John Smith',
'Logs need to be persisted to a local file', 24),
new Ticket(
'3', 'Update AngularJS', 'John Smith',
'Need to update the App to AngularJS version 1.5', 0),
new Ticket(
'4', 'Border is missing', 'Jane Doe',
'The element div.demo has no border defined', 100),
new Ticket(
'5', 'Introduce responsive grid', 'Jane Doe',
'Implement reponsive grid for better displays on mobile devices', 17)
];
现在,构造函数将接收注入的TicketService
以及伪造的后端。 在这里,我们现在订阅connections
流。 对于每个传出的请求,我们现在要检查其request.method
和request.url
,以找出请求的端点类型。 如果匹配了正确的路由,我们将使用mockRespond
方法进行回复,并使用包含Response
结果的新Response
作为正文,并使用ResponseOptions
类进行初始化。
constructor(private service: TicketService, private backend: MockBackend) {
this.backend.connections.subscribe( c => {
let singleTicketMatcher = /\/api\/ticket\/([0-9]+)/i;
// return all tickets
// GET: /ticket
if (c.request.url === "http://localhost:8080/api/ticket" && c.request.method === 0) {
let res = new Response( new ResponseOptions({
body: JSON.stringify(this.db)
}));
c.mockRespond(res);
}
请求单张票证时,我们使用singleTicketMatcher
定义的singleTicketMatcher
以便对request.url
执行正则表达式搜索。 之后,我们搜索给定的ID,并与相应的票证实体进行回复。
// return ticket matching the given id
// GET: /ticket/:id
else if (c.request.url.match(singleTicketMatcher) && c.request.method === 0) {
let matches = this.db.filter( (t) => {
return t._id == c.request.url.match(singleTicketMatcher)[1]
});
c.mockRespond(new Response( new ResponseOptions({
body: JSON.stringify(matches[0])
})));
}
在更新和创建新票证的情况下,我们通过请求正文而不是查询参数或URL模式来获取票证实体。 除此之外,工作非常简单。 我们首先检查故障单是否已存在并对其进行更新,否则我们将创建一个新故障单并将其与响应一起发送回去。 我们这样做是为了通知请求者新的票证ID。
// Add or update a ticket
// POST: /ticket
else if (c.request.url === 'http://localhost:8080/api/ticket' && c.request.method === 1) {
let newTicket: Ticket = JSON.parse(c.request._body);
let existingTicket = this.db.filter( (ticket: Ticket) => { return ticket._id == newTicket._id});
if (existingTicket && existingTicket.length === 1) {
Object.assign(existingTicket[0], newTicket);
c.mockRespond(new Response( new ResponseOptions({
body: JSON.stringify(existingTicket[0])
})));
} else {
newTicket._id = parseInt(_.max(this.db, function(t) {
return t._id;
})._id || 0, 10) + 1 + '';
this.db.push(newTicket);
c.mockRespond(new Response( new ResponseOptions({
body: JSON.stringify(newTicket)
})));
}
}
// Delete a ticket
// DELETE: /ticket/:id
else if (c.request.url.match(singleTicketMatcher) && c.request.method === 3) {
let ticketId = c.request.url.match(singleTicketMatcher)[1];
let pos = _.indexOf(_.pluck(this.db, '_id'), ticketId);
this.db.splice(pos, 1);
c.mockRespond(new Response( new ResponseOptions({
body: JSON.stringify({})
})));
}
});
}
最后但并非最不重要的一点是,当组件完全呈现后,页面生命周期挂钩ngOnInit
将触发所有票证的加载。
public ngOnInit() {
this.service.loadAllTickets();
}
}
在实际的生产应用程序中,您将模拟设置分离到单独的服务中,并将其作为依赖项注入到AppComponent中。 甚至更好的是,您将创建一个包含假服务器的全新模块,并将其添加到应用的根模块中。 为了使演示更简单,此处将其省略。
查看TicketComponent
我们可以看到除了组件装饰器之外,没有什么有趣的事情发生。 我们将ticket
定义为选择器,并再次指向一个单独的模板文件。 现在,与AppComponent
相反,我们期望AppComponent
创建一个票证标签,并带有一个名为title
的属性,并获得要呈现的实体。
然后,构造函数最终获得注入的TicketService
,并将其分配给类属性service
。
import {
Component,
Input
} from '@angular/core';
import {Ticket} from './ticket.entity';
import {TicketService} from './ticket.service';
@Component({
moduleId: module.id,
selector: 'ticket',
templateUrl: 'templates/ticket.html',
//providers: [TicketService] < -- this would override the parent DI instance
})
export class TicketComponent {
@Input('ticket') ticket: Ticket;
constructor(private service: TicketService) { }
}
票务服务
最后缺少的是TicketService
,用于将Ajax调用从组件中抽象出来。 如我们所见,它期望注入http
服务。 现在,记住最初的boot.ts
文件,我们知道提供的实例将是具有boot.ts
的实例。 通过利用HTTP
服务请求方法(例如post
或get
,映射结果(在这种情况下将是假答复)并继续使用自定义应用程序逻辑,实际请求保持不变。
import {Ticket} from './ticket.entity';
import {Injectable} from '@angular/core';
import {Http, Headers} from '@angular/http';
import 'rxjs/add/operator/map';
@Injectable()
export class TicketService {
tickets: Ticket[] = [];
constructor(private http: Http) {
}
addNewTicket() {
var headers = new Headers();
headers.append('Content-Type', 'application/json');
var newTicket = new Ticket("0", 'New Ticket', 'Nobody', 'Enter ticket description here', 0);
this.http
.post('http://localhost:8080/api/ticket', JSON.stringify(newTicket), headers)
.map(res => res.json())
.subscribe(
data => this.tickets.push(data),
err => this.logError(err),
() => console.log('Updated Ticket')
);
}
saveTicket(ticket: Ticket) {
...
}
deleteTicket(ticket: Ticket) {
...
}
loadAllTickets() {
...
}
loadTicketById(id) {
...
}
logError(err) {
console.error('There was an error: ' + err);
}
}
结论
总结一下,我们看到了Angular的依赖项注入如何帮助我们用XHRBackend
替换HTTP
服务的默认XHRBackend
。 然后,在AppComponent
内部,我们创建了虚假数据库,拦截了每个传出的请求,并回复了自定义的虚假响应。 现在,我们获得的收益是完全独立于后端团队,同时具有定义的界面。 现在,一旦生产后端到位,我们所需要做的就是删除依赖项注入覆盖和伪造的后端,我们很好。
本文由Dan Prince和Rabi Kiran进行同行评审。 感谢所有SitePoint的同行评审员使SitePoint内容达到最佳状态!
angular 手机端开发