Статьи

Разработка угловых приложений без бэкенда с использованием MockBackend

В этой статье мы покажем, как разрабатывать приложения с классом Angular 2 MockBackend, предоставляя интерфейсным командам возможность стать независимыми от серверной части и удобный интерфейс, снижающий риск структурных изменений.

Развертывание ваших передовых и серверных команд на полной скорости — это, безусловно, то, что ищет каждая компания. Однако часто команды попадают в яму блокирования зависимостей. Это ситуации, когда предстоящая работа одной команды блокируется пользовательской историей, принадлежащей другой команде.

Одним из таких примеров является процесс связи между интерфейсом и сервером. В последнее время API REST взошли на трон так называемых стандартов связи. Преимущество использования JSON, простого, но эффективного формата передачи данных, заключается в том, что работникам внешнего интерфейса больше не нужно заботиться о реальном сервере. Все, что пересекает провод, потребляется напрямую и может использоваться для передачи данных в ваше приложение. Поэтому неудивительно, что эти элементарные сущности часто вообще не моделируются на внешнем интерфейсе и потребляются по мере их поступления. Это подводит нас к фундаментальной проблеме необходимости ждать, пока бэкэнд-команда предоставит что-то полезное. Как показано на следующем рисунке, мы видим, что обе команды стартуют параллельно, но в определенное время одна команда продолжает ждать, пока другая догонит ее.

Diagram showing how front-end tasks depend on the back-end team finishing the API

Кроме того, отсутствие фиксированной структуры делает каждое изменение потенциально опасным. Таким образом, цель этой статьи — представить способ, которым внешние группы могут стать независимыми от серверной части и в то же время предоставить полезный интерфейс, который снижает риск структурных изменений.

Эта статья была обновлена ​​в соответствии с недавним выпуском версии 2.1.2 Angular. Связанный пример приложения Plunkr также был обновлен.

Система продажи билетов без реального бэк-энда

Чтобы достичь этой независимости, необходимо начать заранее думать о вашем проекте. Какие объекты вы собираетесь использовать? В результате чего возникают конечные точки связи?

Это можно сделать, создав небольшую таблицу с выделением необходимых конечных точек REST и описанием их назначения. Помните, что причина, по которой мы делаем это заранее, заключается в том, что обе стороны договариваются об общей структуре общения. Это не означает, что это должно быть сделано идеально, но это должно помочь вам начать работу с наиболее важными шагами. Со временем просто обновите свой интерфейс соответственно новым необходимым маршрутам.

Фактический процесс создания бесконечной среды состоит в том, чтобы перехватывать все HTTP-запросы и вместо того, чтобы позволять им выходить в дикую природу, и отвечать поддельным ответом, содержащим информацию, которую мы хотели бы получить. Эта статья продемонстрирует подход, описав простую систему продажи билетов. Он использует конечные точки, показанные в следующей таблице.

Обратите внимание, что в примере используется глагол POST для обновления и создания маршрута. Другой вариант — использовать PUT для процесса обновления . Имейте в виду, однако, что PUT должен быть идемпотентным , то есть каждый последующий вызов должен давать один и тот же результат. Не стесняйтесь выбирать то, что соответствует вашим потребностям.

метод маршрут Тело запроса Описание
ПОЛУЧИТЬ /проездной билет Никто Запросить все билеты
ПОЛУЧИТЬ / Билет /: идентификатор Никто Запросить один билет через предоставленный параметр: 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 . Таким образом, мы не будем тратить слишком много времени на объяснение каждой его части. Если вы ищете вводную статью, взгляните на Начало работы с Angular 2, используя TypeScript . В этой статье вы можете просто открыть вышеупомянутый Plunker, чтобы следовать частям кода, описанным ниже.

Поскольку большинство одностраничных приложений начинаются с файла index.html , давайте сначала посмотрим на это. Первый раздел импортирует необходимые полифилы. Затем мы можем увидеть еще одну ссылку на system.config.js которая, помимо прочего, настраивает сторонние зависимости и файлы приложений Angular. Реактивные расширения (Rx) на самом деле не являются истинной зависимостью, а упрощают работу с наблюдаемыми Angular, которые являются заменой ранее использованных обещаний. Я настоятельно рекомендую эту статью Кори Райлан, чтобы узнать больше об этой теме.

Обратите внимание, что ручная ссылка на скрипт не является рекомендуемым способом создания готовых приложений. Вы должны использовать менеджер пакетов, такой как npm или jspm . Последний работает рука об руку с SystemJS, описанным во втором разделе. SystemJS является загрузчиком модулей, ранее основанным на проекте ECMAScript 2015, и теперь является частью спецификации загрузчика WHATWG . Таким образом, он позволяет использовать синтаксис import x from 'module' . Чтобы правильно его использовать, нам нужно настроить его внутри ранее упомянутого файла system.config.js а затем импортировать приложение основной точки входа app , которое указывает на файл app/boot.ts

Эта статья не будет system.config.js в детали system.config.js как это всего лишь пример, основанный на примере Angular Quickstart.

Наконец, мы создаем приложение с помощью пользовательского тега с именем 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 .

Затем свойство import используется для объявления дополнительных зависимостей модуля, за которыми следуют 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 многократно используется для отображения нескольких сущностей билетов.

Screenshot of the ticket system, highlighting the separate components

Компонент приложения настроен для использования с селектором my-app , загружая шаблон index.html расположенный в подпапке templates . Наконец, providers сообщают DI Angular, что мы хотели бы получить экземпляр TicketService .

 ... @Component({ selector: 'my-app', templateUrl: 'app/templates/index.html', providers: [TicketService] }) export class AppComponent { 

Далее мы определяем свойство класса db , которое будет содержать набор поддельных билетов.

 // 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 а также фальшивый TicketService . Здесь мы теперь подписываемся на поток connections . Для каждого исходящего запроса мы теперь проверяем его request.method и request.url , чтобы выяснить, какой тип конечной точки запрашивается. Если правильный маршрут совпадает, мы отвечаем, используя метод mockRespond , с новым 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 , чтобы выполнить поиск по регулярному выражению в request.url . После этого мы ищем заданный идентификатор и отвечаем соответствующей сущностью заявки.

 // 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. Кроме того, работа довольно проста. Сначала мы проверяем, существует ли билет, и обновляем его, в противном случае создаем новый и отправляем обратно с ответом. Мы делаем это для того, чтобы сообщить запрашивающей стороне о новом идентификаторе билета.

  // 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 хук жизненного цикла страницы 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 мы затем создали нашу поддельную базу данных, перехватили каждый исходящий запрос и ответили специальным поддельным ответом. Преимущества, которые мы получили, теперь — полная независимость от серверной части и, в то же время, определенный интерфейс. Теперь, когда производственный бэкэнд будет создан, все, что нам нужно сделать, — это удалить переопределение внедрения зависимостей и фальшивый бэкэнд, и мы готовы к работе.

Эта статья была рецензирована Дэном Принсом и Раби Кираном . Спасибо всем рецензентам SitePoint за то, что сделали контент SitePoint как можно лучше!