В этой главе:
• создание простого веб-сервера с использованием таких сред, как Node и Express;
• создание запросов к серверу из Angular с применением API Http-объектов;
• обмен данными с Node-сервером из Angular-клиентов с помощью протокола HTTP;
• заключение WebSocket-клиента в сервис Angular, генерирующий доступный для наблюдения поток;
• передача данных с сервера нескольким клиентам через WebSocket-объекты.
Angular-приложения могут обмениваться данными с любым веб-сервером, поддерживающим HTTP- или WebSocet-протоколы, независимо от того, какая из платформ используется на серверной стороне. До сих пор внимание уделялось в основном серверной стороне Angular-приложений, за исключением описанного в главе 5 примера с сервисом погоды. В настоящей главе вопрос обмена данными с веб-серверами будет рассмотрен более подробно.
Сначала мы представим краткий обзор принадлежащего Angular Http-объекта, а затем предложим создать веб-сервер с помощью TypeScript и Node.js. Этот сервер будет предоставлять данные для всех примеров кода, включая онлайн-аукцион. Затем мы рассмотрим вопросы создания в клиентском коде HTTP-запросов к веб-серверам и наблюдаемых объектов, описанных в главе 5. Кроме того, уделим внимание вопросу обмена данными с сервером через WebSocket-объекты, сделав акцент на принудительной доставке данных на серверную сторону.
В разделе «Практикум» вы реализуете функцию поиска товара, в которой данные по аукционным товарам и обзоры будут поступать с сервера через HTTP-запросы. Вдобавок вы реализуете уведомление о предлагаемой цене товара, отправляемое сервером с использованием WebSocket-протокола.
В веб-приложениях HTTP-запросы запускаются в асинхронном режиме, сохраняющем отзывчивость пользовательского интерфейса и позволяющем пользователю продолжать работать с приложением в ходе обработки сервером этих запросов. Асинхронные HTTP-запросы могут реализовываться с использованием функций обратного вызова, промисов или наблюдаемых объектов. Хотя промисы и исключают все неудобства, связанные с функциями обратных вызовов (см. приложение А), они имеют следующие недостатки:
• отсутствие способов прекращения отложенного запроса, совершенного с помощью промиса;
• отсутствие со стороны промисов предложенного способа обработки продолжительного потока, состоящего из фрагментов данных, поступающих со временем. При разрешении промиса или его отклонении клиент получает либо данные, либо сообщение об ошибке, но в любом случае это будет неделимый фрагмент данных.
У наблюдателей такие недостатки отсутствуют. В подразделе 5.2.2 рассматривался сценарий на основе применения промиса, в результате чего для извлечения коммерческого предложения на акцию получалась масса ненужных запросов, порождающих ненужный сетевой трафик. Затем в подразделе 5.2.3, в примере с сервисом погоды, был показан способ прекращения HTTP-запросов, осуществляемый с помощью наблюдателей.
А теперь рассмотрим имеющуюся в Angular реализацию класса Http, включенного в пакет @angular/. В него входят несколько классов и интерфейсов, как описано в документации по Angular HTTP-клиенту, которую можно найти на . Если заглянуть в содержимое файла определения типа @angular/, то в классе Http можно увидеть следующие API:
import {Observable} from 'rxjs/Observable';
...
export declare class Http {
...
constructor(_backend: ConnectionBackend, _defaultOptions: RequestOptions);
request(url: string | Request, options?: RequestOptionsArgs):
Observable<Response>;
get(url: string, options?: RequestOptionsArgs): Observable<Response>;
post(url: string, body: string, options?: RequestOptionsArgs):
Observable<Response>;
put(url: string, body: string, options?: RequestOptionsArgs):
Observable<Response>;
delete(url: string, options?: RequestOptionsArgs): Observable<Response>;
patch(url: string, body: string, options?: RequestOptionsArgs):
Observable<Response>;
head(url: string, options?: RequestOptionsArgs): Observable<Response>;
}
Этот код написан на TypeScript, и у методов каждого Http-объекта есть обязательный аргумент url, который может быть либо string-, либо Request-объектом. Можно также передавать необязательный объект типа RequestOptionArgs. Каждый метод возвращает объект типа Observable, в который заключен объект типа Response.
Один из способов использования метода get() API Http-объекта, передающего URL в виде string-объекта, показан в следующем фрагменте кода:
constructor(private : Http) {
this.(...);
}
Здесь не указан полный URL (такой как ), поскольку предполагается, что Angular-приложение делает запрос к серверу по месту его развертывания, поэтому базовая часть веб-адреса может быть опущена. Метод subscribe() должен получить объект-наблюдатель с кодом для обработки полученных данных и ошибок.
Объект Request предлагает более универсальный API, позволяющий отдельно создавать Request-экземпляр, указывая HTTP-метод, и включать параметры поиска и Header-объект:
let myHeaders:Headers = new Headers();
myHeaders.append('Authorization', 'Basic QWxhZGRpb');
this.
.request(new Request({
headers: myHeaders,
method: RequestMethod.Get,
url: '/products',
search: 'zipcode=10001'
}))
.subscribe(...);
Объект RequestOptionsArgs объявлен как TypeScript-интерфейс:
export interface RequestOptionsArgs {
url?: string;
method?: string | RequestMethod;
search?: string | URLSearchParams;
headers?: Headers;
body?: any;
withCredentials?: boolean;
responseType?: ResponseContentType;
}
Все элементы интерфейса являются необязательными, но если вы решите их применить, то компилятор TypeScript обеспечит предоставление вами значений правильного типа данных:
var myRequest: RequestOptionsArgs = {
url: '/products',
method: 'Get'
};
this.
.request(new Request(myRequest))
.subscribe(...)
В разделе «Практикум» будет показан пример использования свойства search объекта RequestOptionsArgs для создания HTTP-запроса, имеющего параметры строки запроса.
Что такое API Fetch В настоящее время прилагаются усилия по унификации процесса сбора ресурсов в Интернете. Заменой объекту XMLHttpRequest может послужить API Fetch (/). В нем определяются универсальные объекты Request и Response, которые могут применяться не только с HTTP, но также и с другими новыми веб-технологиями, например с Service Workers и Cache API. При использовании API Fetch HTTP-запросы делаются с помощью глобальной функции fetch():
Чтобы извлечь из ответа содержимое тела, нужно воспользоваться одним из методов Response-объекта. Каждым методом ожидается, что тело будет иметь вполне определенный формат. Тело считывается методом text() как обычный текст, который, в свою очередь, возвращает Promise-объект. В отличие от имеющегося в Angular Http-сервиса на основе наблюдателей, API Fetch основан на промисах. Он упомянут в документации по Angular, поскольку им инспирировано несколько имеющихся в Angular классов и интерфейсов (например Request, Response и RequestOptionsArgs). |
Чуть позже в этой главе будет показано, как выполнять запросы с помощью API Http-объекта и как обрабатывать HTTP-ответы путем подписки на наблюдаемые потоки. В главе 5 использовался сервер сервиса погоды, а здесь будет создан ваш собственный веб-сервер, задействующий среду Node.js.
Вести разработку и развертывание веб-серверов позволяют многие платформы. В этой книге мы решили воспользоваться Node.js, руководствуясь следующими соображениями:
• чтобы разбираться в коде, не нужно учить новый язык программирования;
• Node позволяет создавать автономные приложения (такие как серверы);
• Node отлично справляется с задачами в области обмена данными, задействуя HTTP или WebSockets;
• применение Node позволяет продолжить написание кода в TypeScript, поэтому нам не придется объяснять, как создается веб-сервер на Java, .NET или Python.
Простой сервер в Node может быть написан всего лишь с помощью нескольких строчек кода, и вы начнете работы с самого простого варианта. Затем напишете веб-сервер, способный обслуживать данные в формате JSON (который, конечно же, будет применен в описаниях товаров) с использованием протокола HTTP. Чуть позже создадите еще одну версию сервера, способную вести обмен данными с клиентом через WebSocket-подключение. И наконец, в проекте, разрабатываемом в рамках практикума, мы научим вас создавать клиентскую часть аукциона, совершающую обмен данными с вашим веб-сервером.
В этом подразделе будет создано автономное Node-приложение, запускаемое в качестве сервера, поддерживающего примеры кода, созданные с помощью среды Angular. При обоюдной готовности серверной и клиентской сторон каталог проекта приобретет структуру, показанную на рис. 8.1.
Рис. 8.1. Структура проекта Angular-Node-приложения
ПРИМЕЧАНИЕ
Если вы уже запустили примеры кода из приложения Б, то компилятор TypeScript на ваш компьютер установлен. Если же нет, то сделайте это сейчас.
Начнем с создания каталога по имени с подкаталогом server. Создаваемый здесь новый Node-проект следует настроить запуском следующей команды:
npm init –y
В главе 2 уже упоминалось, что ключ -y заставляет npm создать конфигурационный файл package.json с исходными установками, не выдавая никаких приглашений на выбор каких-либо вариантов.
Затем нужно создать файл hello_server.ts, имеющий следующее содержимое (листинг 8.1).
Листинг 8.1. hello_server.ts
Код в листинге 8.1 нуждается в транспиляции, поэтому для настройки tsc-компилятора в каталоге _samples нужно создать файл tsconfig.json (листинг 8.2).
Листинг 8.2. Содержимое файла tsconfig.json
После запуска команды npm run tsc в каталоге build будет сохранен транспилированный файл hello_server.js, и ваш веб-сервер можно будет запустить:
node build/hello_server.js
Node будет запущен с движком V8 JavaScript, который запустит сценарий из hello_server.js; он создаст веб-сервер и выведет в консоли следующее сообщение: Listening on . Если вы введете в свой браузер этот URL, то увидите веб-страницу с текстом Hello World!.
TypeScript 2.0 и @types В этом проекте применяется установленный локально компилятор tsc версии 2.0, использующий пакеты @types для установки файлов, определяющих типы. Дело в том, что прежние версии tsc не поддерживают ключ компилятора types, и если у вас имеется глобально установленная устаревшая версия tsc, то она будет задействована при запуске tsc; это вызовет ошибки компиляции. Чтобы убедиться в том, что используется локальная версия tsc, настройте ее в разделе scripts файла package.json командой ("tsc": "tsc") и запустите компилятор, введя команду npm run tsc для транспиляции серверных файлов. Запустите данную команду из того же самого каталога, где находится файл tsconfig.json (из корневого каталога в примерах кода для этой главы). |
Во избежание ошибок компиляции TypeScript для Node требуется наличие файлов определения типов (см. приложение Б). Чтобы установить необходимые Node определения типов для подобного проекта, нужно из корневого каталога вашего проекта запустить следующую команду:
npm i @types/node --save
Если используются примеры кода, предоставленные с этой главой, то можно запустить команду npm install, поскольку в файл package.json для Node включена зависимость @types/node:
"@types/node": "^4.0.30"
Во всех рассмотренных до сих пор примерах кода аукциона данные о товарах и обзорах были жестко заданы в файле product-service.ts в виде массивов объектов в формате JSON. В разделе «Практикум» эти данные будут перемещены на сервер, в связи с чем веб-серверу Node нужно знать порядок обслуживания формата JSON.
Чтобы отправить данные в формате JSON в браузер, нужно внести изменения в заголовок для указания MIME-типа application/json:
const server = , response) => {
response.writeHead(200, {'Content-Type': 'application/json'});
response.end('{"message": "Hello Json!"}\n');
});
Этого фрагмента достаточно для иллюстрации отправки JSON-данных, однако на настоящих серверах выполняется большее количество функций, например чтение файлов, маршрутизация и обработка различных HTTP-запросов (GET, POST и т.д.). Чуть позже, в примере с аукционом, в зависимости от запроса вам придется откликаться, используя любые данные о товарах или обзоре.
Чтобы свести ручное программирование к минимуму, установим Express (), Node-среду, предоставляющую набор свойств, требуемых всеми веб-приложениями. Весь арсенал ее функциональных средств применять не придется, но она поможет создать веб-сервис на основе передачи состояния представления (RESTful), который будет возвращать данные в JSON-формате.
Для установки среды Express запустите из каталога следующую команду:
npm install express --save
При ее выполнении система Express будет загружена в папку node_modules, а в разделе dependencies файла package.json будут обновлены зависимости.
Поскольку в файле этого проекта есть запись "@types/express": "^4.0.31", все типы определений для Express в вашем каталоге node_modules уже имеются. Но если нужно установить их в какой-либо другой проект, то запустите следующую команду:
npm i @types/express --save
Теперь систему Express можно импортировать в ваше приложение и приступить к использованию ее API при написании кода на TypeScript. В листинге 8.3 показано содержимое файла my-express-server.ts, в котором реализуется подпрограмма HTTP-запросов GET на стороне сервера.
Листинг 8.3. Содержимое файла my-express-server.ts
Если вместо деструктурирования задействован синтаксис ES5, то вместо одной строки кода придется применить две:
var address = server.address().address;
var port = server.address().port;
Транспилируйте содержимое файла my-express-server.ts, введя команду npm run tsc, и запустите этот сервер (node build/my-express-server.js). Как показано на рис. 8.2, в зависимости от веб-адреса, введенного в браузер, вы сможете запрашивать либо товары, либо сервисы.
Рис. 8.2. Маршрутизация на стороне сервера с использованием Express
ПРИМЕЧАНИЕ
Для отладки Node-приложений обратитесь к предпочитаемой вами IDE-документации. Можно также прибегнуть к средству командной строки node-inspector ().
Примеры кода, работающего на стороне сервера, написаны на TypeScript, следовательно, прежде чем приступать к развертыванию кода в Node, для транспиляции этого кода в JavaScript нужно воспользоваться компилятором tsc. В подразделе Б.3.1 приложения Б будет рассмотрен ключ компиляции –w, с помощью которого tsc запускается в режиме отслеживания. Как только в файле TypeScript произойдут изменения, выполнится автоматическая перекомпиляция. Для включения режима автоматической компиляции вашего кода нужно, находясь в каталоге с исходными файлами, открыть отдельное командное окно и запустить в нем следующую команду:
tsc -w
Когда не указаны имена файлов, tsc для ключей компиляции задействует файл tsconfig.json. Теперь, как только вы внесете изменения в код TypeScript и сохраните файл, компилятор сгенерирует в каталоге build, как указано в файле tsconfig.json, соответствующий файл с расширением .js. Следовательно, для запуска вашего веб-сервера с Node можно воспользоваться этой командой:
node build/my-express-server.js
Живая перекомпиляция кода TypeScript приносит реальную пользу, но Node-сервер не станет автоматически фиксировать изменения кода после своего запуска. Чтобы не пришлось перезапускать Node-сервер вручную с целью увидеть внесенные в код изменения в действии, можно воспользоваться весьма полезной утилитой Nodemon (). Она отследит любые изменения в вашем исходном коде и, как только они будут замечены, автоматически перезапустит сервер и перезагрузит код.
Утилиту Nodemon можно запустить либо глобально, либо локально. Для глобальной установки следует применять такую команду:
npm install -g nodemon
А эта команда запустит ваш сервер в режиме отслеживания:
nodemon build/my-express-server.js
Установите Nodemon локально (npm install nodemon --save-dev) и введите сценарии npm () в файл package.json (листинг 8.4).
Листинг 8.4. Содержимое файла package.json
"scripts": {
"tsc": "tsc",
"start": "node build/my-express-server.js",
"dev": "nodemon build/my-express-server.js"
},
"devDependencies": {
"nodemon": "^1.8.1"
}
Пользуясь этими настройками, вы можете запустить сервер в режиме развертывания с помощью команды npm run dev (с автоматическим перезапуском и перезагрузкой) или в режиме prod-конфигурации через команду npm start (без перезапуска и перезагрузки). Мы присвоили команде запуска утилиты Nodemon имя dev, но вы можете выбрать имя по своему усмотрению, например startNodemon.
Вашей конечной целью является обслуживание товаров и обзоров для приложения-аукциона. В этом подразделе будет показан способ подготовки Node-сервера, имеющего конечные точки REST, к получению HTTP-запросов GET, направляемых с целью обслуживания товаров, данные о которых имеют JSON-формат.
Код в файле my-express-server.ts будет изменен с целью обслуживания либо всех товаров, либо конкретно указанного одного товара (по его ID). Показанная далее измененная версия этого приложения находится в файле auction-rest-server.ts (листинг 8.5).
Листинг 8.5. Содержимое файла auction-rest-server.ts
Теперь вы можете запустить в Node приложение auction-rest-server.ts (выполните команду nodemon build/auction-rest-server.js) и посмотреть, получает браузер все товары или выбранный товар. На рис. 8.3 показано окно браузера после ввода URL . Наш сервер возвращает все товары в JSON-формате.
Рис. 8.3. Node-сервер отвечает на ввод адреса
На рис. 8.4. показано окно браузера после того, как в его адресную строку был введен URL . На этот раз сервером возвращены только данные о товаре, чей идентификатор имеет значение 1.
Рис. 8.4. Node-сервер отвечает на ввод адреса
Сервер готов. Теперь можно изучить способы инициирования HTTP-запросов и обработки ответов в Angular-приложениях.
Ранее в настоящей главе была создана папка , содержащая файл auction-rest-server.ts. Это код Node-приложения, отвечающего на HTTP-запросы GET и предоставляющего сведения о товарах. В этом разделе будет создан Angular-клиент, который станет выдавать HTTP-запросы и обрабатывать сведения о товаре в качестве Observable-объекта, возвращенного вашим сервером. Код Angular-приложения будет находиться в подкаталоге client (см. рис. 8.1).
Обычное веб-приложение, развернутое на сервере, включает статические ресурсы (такие как код HTML, изображения, код CSS и код JavaScript), которые должны быть загружены в браузер, когда пользователь вводит веб-адрес приложения. Поскольку нами используется среда SystemJS, выполняющая транспиляцию динамически, допускается также присутствие в качестве статических ресурсов и файлов TypeScript.
С точки зрения Node та часть этого приложения, которая относится к Angular, рассматривается как статические ресурсы. Поскольку Angular-приложения загружают зависимости из node_modules, данный каталог тоже принадлежит к статическим ресурсам, востребуемым браузером.
В среде Express имеется специальный API для указания каталогов со статическими ресурсами, и вы внесете небольшие изменения в файл auction-rest-server.ts, показанный в листинге 8.5. В этом файле вы не станете указывать такой каталог, поскольку здесь не было развернуто какое-либо клиентское приложение. Новая версия данного файла будет называться auction-rest-server-angular.ts. Сначала добавьте следующие строки:
import * as path from "path";
app.use('/', express.static(path.join(__dirname, '..', 'client')));
app.use('/node_modules', express.static(path.join(__dirname, '..',
'node_modules')));
Когда браузер запрашивает статические ресурсы, Node ищет их в каталогах client и node_modules. Здесь, чтобы гарантировать создание путевого имени файла на кросс-платформенной основе, вы воспользуетесь имеющимся в Node API path.join. Его можно применять, когда нужно выстроить абсолютный путь к указанному файлу; примеры будут показаны чуть позже.
Сохраним на сервере те же самые конечные точки REST:
• / служит в качестве main.html, то есть начальной страницы приложения;
• /products предоставляет все товары;
• /products/:id предоставляет товар по его идентификатору ID.
В отличие от приложения my_express_server.ts, здесь вам не нужно, чтобы Node обрабатывал базовый URL. Он должен отправлять файл main.html в браузер. Измените в файле auction-rest-server-angular.ts маршрут для базового URL /, чтобы он приобрел следующий вид:
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, '../client/main.html'));
});
Теперь, когда пользователь вводит в браузере веб-адрес Node-сервера, сначала обслуживается файл main.html. Затем сервер загружает ваше Angular-приложение со всеми зависимостями.
Общий конфигурационный файл NPM. В новой версии файла package.json (листинг 8.6) будут сочетаться все зависимости, требуемые как для кода, имеющего отношение к Node, так и для вашего Angular-приложения. Обратите внимание на то, что в разделе scripts объявляются несколько команд. Первая предназначена для запуска локально установленного компилятора tsc, а другие нужны для запуска Node-серверов для примеров кода, включенных в эту главу.
Листинг 8.6. Модифицированный файл package.json
{
"private": true,
"scripts": {
"tsc": "tsc",
"start": "node build/my-express-server.js",
"dev": "nodemon build/my-express-server.js",
"devRest": "nodemon build/auction-rest-server.js",
"restServer": "nodemon build/auction-rest-server-angular.js",
"simpleWsServer": "node build/simple-websocket-server.js",
"twowayWsServer": "nodemon build/two-way-websocket-server.js",
"bidServer": "nodemon build/bids/bid-server.js"
},
"dependencies": {
"@angular/common": "^2.0.0",
"@angular/compiler": "^2.0.0",
"@angular/core": "^2.0.0",
"@angular/forms": "^2.0.0",
"@angular/": "^2.0.0",
"@angular/platform-browser": "^2.0.0",
"@angular/platform-browser-dynamic": "^2.0.0",
"@angular/router": "^3.0.0",
"core-js": "^2.4.0",
"rxjs": "5.0.0-beta.12",
"systemjs": "0.19.37",
"zone.js": "0.6.21",
"@types/express": "^4.0.31",
"@types/node": "^4.0.30",
"express": "^4.14.0",
"ws": "^1.1.1"
},
"devDependencies": {
"@types/es6-shim": "0.0.30",
"@types/ws": "0.0.29",
"nodemon": "^1.8.1",
"typescript": "^2.0.0"
}
}
Обратите внимание: здесь включен пакет @angular/, в котором содержится поддержка HTTP-протокола со стороны Angular. Кроме того, включены ws и @types/ws — они понадобятся в этой главе чуть позже, когда дело дойдет до поддержки WebSocket.
npm-сценарии Менеджер пакетов npm поддерживает свойство scripts в package.json с более чем десятком сценариев, доступных в готовом виде (подробности можно найти в документации npm-scripts на ). Применительно к вашей специфике разработки и развертывания вы можете добавить и новые команды. Одни из этих сценариев нужно запускать вручную (например, npm start), а другие вызываются автоматически (например, postinstall). Как правило, если какая-либо команда в разделе scripts начинается с префикса post, то она будет запущена автоматически после той команды, которая указана после данного префикса. Например, в случае определения команды i "postinstall" : "myCustomIstall.js" при каждом запуске npm install будет запускаться и сценарий myCustomIstall.js. |
Точно так же, если у команды имеется префикс pre, она будет запущена до запуска команды, названной после этого префикса. Например, в подразделе 10.3.2 в файле package.json вы увидите следующие команды: "prebuild": "npm run clean && npm run test", "build": "webpack --config webpack.prod.config.js --progress --profile -- colors" Если запустить команду build, то npm сначала запустит сценарий, определенный в prebuild, а затем — сценарий, определенный в build. До сих пор вы использовали только две команды: npm start и npm run dev. Но можете добавить к разделу scripts своего файла package.json какие угодно команды. Например, обе команды в предыдущем примере, и build, и prebuild, были из категории команд, определенных пользователем. |
Сравнение общих и раздельных конфигурационных файлов Все примеры кода в этой главе для клиента и сервера принадлежат единому npm-проекту и совместно задействуют один и тот же файл package.json. Все зависимости и типизация применяются клиентскими и серверными приложениями совместно. Подобная настройка может сэкономить время для установки зависимостей и пространство на диске, поскольку некоторые из зависимостей могут совместно использоваться как клиентом, так и сервером. Но содержание кода для клиента и сервера в едином проекте может стать причиной усложнения процесса автоматической сборки в силу двух обстоятельств: клиенту и серверу могут понадобиться конфликтующие между собой версии конкретной зависимости; вы используете средство автоматической сборки, которому могут потребоваться различные конфигурации для клиента и сервера, и их каталоги node_modules не будут находиться в корневом каталоге проекта. В главе 10 вам придется разделить клиентскую и серверную части онлайн-аукциона на два независимых npm-проекта. |
Следующим шагом станет добавление Angular-приложения в каталог client.
Когда имеющийся в Angular Http-объект выполняет запрос, ответ возвращается в виде Observable-объекта, и клиентский код будет обрабатывать его, используя метод subscribe(). Начнем с простого приложения (client/app/main.ts), которое извлекает все товары из Node-сервера и выводит их, задействуя неупорядоченный HTML-список (листинг 8.7).
Листинг 8.7. Содержимое файла client/app/main.ts
Чтобы увидеть функцию обратного вызова error в действии, измените конечную точку с '/products' на что-нибудь другое. Ваше Angular-приложение выведет в консоли следующее сообщение: Can't get products. Error code: 404, URL: .
ПРИМЕЧАНИЕ
HTTP-запрос GET отправляется на сервер, только когда вызывается метод subscribe(), и не отправляется при вызове метода get().
Теперь уже можно запустить сервер и ввести его URL в браузер, чтобы увидеть Angular-приложение в работе. Запустить Node-сервер можно, либо написав длинную команду:
node build/auction-rest-server-angular.js
либо используя npm-сценарий, который был определен вами в файле package.json:
npm run restServer
Откройте браузер, указав в нем адрес , и увидите Angular-приложение, показанное на рис. 8.5.
Рис. 8.5. Извлечение всех продуктов из Node-сервера
ПРИМЕЧАНИЕ
Убедитесь в том, что в файле client/systemjs.config.js пакет app отображается на main.ts.
СОВЕТ
Можно выполнить HTTP-запрос GET, передающий параметры в URL после знака вопроса (например, myserver.com?param1=val1¶m2=val2). Метод Http.get() может принять второй параметр, являющийся объектом, реализующим RequestOptionsArgs. Поле search, имеющееся у RequestOptionsArgs, применяется для установки в качестве его значения либо строки, либо URLSearchParams-объекта. Пример использования URLSearchParams будет показан в разделе «Практикум».
В предыдущем подразделе велась работа с наблюдаемым потоком товаров в коде TypeScript путем вызова метода subscribe(). Angular предлагает альтернативный синтаксис, рассмотренный в главе 5 и позволяющий обрабатывать наблюдаемые объекты прямо в шаблоне компонента с помощью каналов (pipes).
Angular включает канал AsyncPipe (или async при использовании в шаблонах), который может получать в качестве ввода Promise- или Observable-объект и подписываться на него в автоматическом режиме. Чтобы увидеть все это в действии, внесем в код из предыдущего подраздела следующие изменения:
• изменим тип переменной products с Array на Observable;
• удалим объявление переменной theDataSource;
• удалим из кода вызов subscribe(). Объект Observable, который возвращается (), будет присваиваться переменной products;
• добавим в шаблоне канал async к циклу *ngFor.
Все эти изменения реализованы в следующем коде (main-asyncpipe.ts) (листинг 8.8).
Листинг 8.8. Содержимое файла main-asyncpipe.ts
Вывод при запуске этого приложения будет таким же, как и на рис. 8.5.
ПРИМЕЧАНИЕ
Эта версия AppComponent с async короче версии, показанной в листинге 8.7. Но код, в котором subscribe() вызывается явным образом, проще тестировать.
В этом подразделе мы покажем пример внедряемого класса ProductService, в котором будет инкапсулироваться обмен данными с сервером по протоколу HTTP. Вы создадите небольшое приложение, в котором пользователь сможет вводить идентификатор товара и заставлять приложение выполнять запрос к конечной точке сервера /products/:id.
Пользователь вводит идентификатор товара и нажимает кнопку, запуская тем самым подписку на свойство по имени productDetails объекта типа Observable, принадлежащее объекту ProductService. На рис. 8.6 показаны внедряемые объекты приложения, которые вам предстоит создать.
Рис. 8.6. Рабочий поток «клиент — сервер»
В главе 7 вы познакомились с API Forms. Здесь с помощью простой формы вы создадите компонент AppComponent, у которого есть поле ввода данных и кнопка поиска товара Find Product (Найти продукт). Это приложение станет обмениваться данными с ранее созданным веб-сервером Node, клиентская часть будет реализована в двух последовательных улучшениях. В первой версии (main-form.ts) (листинг 8.9) класс ProductService использоваться не будет. Компонент AppComponent получит внедренный Http-объект и выполнит запрос к серверу.
Листинг 8.9. Содержимое файла main-form.ts
На рис. 8.7 показан снимок экрана, сделанный после ввода в поле идентификатора товара цифры 2 и нажатия кнопки Find Product (Найти продукт), инициирующей отправку запроса по URL . Сервер Node Express соотносит /products/2 с соответствующей конечной точкой REST и направляет этот запрос к методу, определенному как app.get('/products/:id').
Рис. 8.7. Получение сведений о товаре по идентификатору
Теперь введем класс ProductService (product-service.ts). В листинге 8.9 Http-объект внедряется в конструктор AppComponent. Теперь нужно переместить код, использующий Http-объект в ProductService, чтобы код стал отражением показанной на рис. 8.6 архитектуры (листинг 8.10).
Листинг 8.10. product-service.ts
Класс ProductService использует внедрение зависимости (DI). Декоратор @Injectable() заставляет компилятор TypeScript создать для ProductService метаданные, и применять данный декоратор здесь необходимо. Когда вы внедряли Http-объект в компонент, имеющий еще один декоратор (@Component), это был сигнал компилятору TypeScript создать метаданные для компонента, требуемого для DI. Если в классе ProductService не имелось никаких декораторов, то компилятор TypeScript не станет создавать для него никаких метаданных и механизм Angular DI не узнает, что ему следует выполнить какое-то внедрение в ProductService. Для классов, представляющих сервисы, требуется простое присутствие декоратора @Injectable(), и вы не должны забывать включать в файле tsconfig.json настройку "emitDecoratorMetadata": true.
Подписчиком на наблюдаемый поток, производимый ProductService, станет новая версия AppComponent (main-with-service.ts) (листинг 8.11).
Листинг 8.11. Содержимое файла main-with-service.ts
Здесь ProductService является не компонентом, а классом, и Angular не позволяет указывать для классов поставщиков. В результате поставщик указывается для Http-объекта в AppComponent путем включения свойства providers в декоратор @Component. Другой возможный вариант может заключаться в объявлении поставщиков в @NgModule. В данном конкретном приложении это ни на что не повлияет.
В главе 4 в процессе обсуждения DI мы упоминали о способности Angular внедрять объекты, и если у них имеются свои собственные зависимости, то Angular займется и их внедрением. В листинге 8.11 доказывается, что имеющийся в Angular модуль DI работает в точном соответствии с нашими ожиданиями.
WebSocket является двоичным протоколом с низким уровнем издержек, поддерживаемым всеми современными браузерами. Как показано на рис. 8.8, при использовании HTTP-протокола, основанного на запросах, клиент отправляет запрос на соединение с сервером и ожидает поступления ответа (полудуплексный режим связи). А как показано на рис. 8.9, протокол WebSocket позволяет данным одновременно путешествовать по соединению в обоих направлениях (полнодуплексный режим связи). WebSocket-соединение постоянно поддерживается в рабочем состоянии, что дает дополнительные преимущества: низкое значение задержки во взаимодействии сервера и клиента.
|
|
|
|
|
|
Рис. 8.8. Полудуплексный обмен данными |
| Рис. 8.9. Полнодуплексный обмен данными |
По сравнению с обычным HTTP-протоколом типа «запрос — ответ», добавляющим к данным приложения несколько сотен байт (в заголовках), издержки при использовании веб-сокетов составляют всего лишь пару байт. Если вам не знакома эта технология, то обратитесь к ресурсу или к одному из множества руководств, доступных в Интернете.
Веб-сокеты поддерживаются большинством платформ, работающих на стороне сервера (Java, .NET, Python и др.), но вы для реализации вашего сервера на основе веб-сокета продолжите работать с платформой Node. Вы реализуете один конкретный вариант применения: сервер будет выдавать данные клиенту, использующему браузер, как только клиент подключится к сокету. Мы намеренно не станем отправлять запрос данных от клиента, для демонстрации того, что веб-сокеты не работают по схеме «запрос — ответ». Приступить к отправке данных по веб-сокет-соединению может любая из сторон.
WebSocket-протокол реализуется несколькими Node-пакетами, но здесь будет использоваться npm-пакет ws (). Установите этот пакет, подав следующую команду, находясь в каталоге вашего проекта:
npm install ws --save
Затем установите для ws файл определения типов:
npm install @types/ws --save-dev
Теперь компилятор TypeScript не станет возражать при использовании API из пакета ws. Кроме этого, данный файл пригодится для просмотра доступных API и типов.
Ваш первый WebSocket-сервер будет предельно прост: он станет выдавать клиенту текст This message was pushed by the WebSocket server, как только будет установлено соединение. Вы намеренно не хотите, чтобы клиент отправлял на сервер какой-либо запрос, для демонстрации того, что сокет является «улицей с двухсторонним движением» и сервер может выдавать данные без церемонии запроса.
Приложение, показанное в листинге 8.12 (simple-websocket-server.ts), создает два сервера. HTTP-сервер будет запущен с использованием порта 8000 и станет отвечать за отправку клиенту исходного HTML-файла. А WebSocket-сервер будет запущен с применением порта 8085 и станет обмениваться данными со всеми подключившимися клиентами через этот порт.
Листинг 8.12. simple-websocket-server.ts
ПРИМЕЧАНИЕ
В листинге 8.12 из ws импортируется только модуль Server. При использовании других экспортируемых элементов можно задействовать запись import * as ws from “ws”; .
В листинге 8.12 серверы HTTP и WebSocket запущены на разных портах, но можно повторно применять один и тот же порт, предоставив конструктору WsServer заново созданный экземпляр класса :
const = app.listen(8000, "localhost", () => {...});
const wsServer: WsServer = new WsServer({server: });.
В разделе «Практикум» порт 8000 будет использоваться для обоих протоколов обмена данными: и HTTP, и WebSocket (см. файл server/auction.ts).
ПРИМЕЧАНИЕ
Как только к серверу подключится новый клиент, ссылка на это соединение добавляется к массиву wsServer.clients, чтобы сообщения при необходимости можно было распространять среди всех подключенных клиентов: wsServer.clients.forEach (client => client.send(‘…’)); .
Содержимое клиентского файла simple-websocket-client.html показано в листинге 8.13. Этот клиент не применяет ни Angular, ни TypeScript. Как только данный файл загружается в браузер, его сценарий подключается к вашему WebSocket-серверу по адресу ws://localhost:8085. Учтите, что протоколом является ws, а не . Для безопасного сокет-подключения следует воспользоваться протоколом wss.
Листинг 8.13. simple-websocket-client.html
Для запуска сервера, выдающего данные клиенту, запустите Node-сервер (node build/simple-websocket-server.js или npm simpleWsServer). Он выведет в консоли следующее сообщение:
WebSocket server is listening on port 8085
HTTP Server is listening on 8000
ПРИМЕЧАНИЕ
При изменении кода, находящегося в каталоге server, не забудьте запустить команду npm run tsc в корневом каталоге своего проекта, чтобы создать свежую версию вашего кода JavaScript в каталоге build. В противном случае команда node загрузит старый файл JavaScript.
Чтобы получить сообщение, выданное сервером, откройте в браузере страницу . Будет выведено сообщение, показанное на рис. 8.10.
В данном примере HTTP-протокол используется только для начальной загрузки HTML-файла. Затем клиент запрашивает обновление протокола до WebSocket (код состояния 101), и с этого момента приложение уже не будет применять протокол HTTP.
Рис. 8.10. Получение сообщения от сокета
СОВЕТ
Отслеживать сообщения, проходящие через сокет, можно с помощью вкладки Frames в Developer Tools (Инструменты разработчика) браузера Chrome.
В предыдущем подразделе был создан код клиента на JavaScript (без Angular) с использованием принадлежащего браузеру WebSocket-объекта. Теперь будет показано, как создается сервис, заключающий браузерный объект WebSocket в наблюдаемый поток, чтобы Angular-компоненты могли подписаться на сообщения, поступающие от сервера через сокет-соединение.
Ранее, в подразделе 8.3.2, код, получающий данные о товаре, был структурирован следующим образом (в псевдокоде):
this.')
.subscribe(
data => handleNextDataElement(),
err => handleErrors(),
() => handleStreamCompletion()
);
По сути, вашей целью было написание кода приложения, задействующего наблюдаемый поток, предоставляемый имеющимся в Angular Http-сервисом. Но в данном фреймворке нет сервиса, который производил бы наблюдаемый объект из WebSocket-соединения, так что требуемый сервис придется создавать. Тогда Angular-клиент получит возможность подписываться на сообщения, приходящие от WebSocket-соединения, точно так же, как это делалось при использовании Http-объекта.
Теперь вы создадите небольшое Angular-приложение, которое не будет использовать WebSocket-сервер, но покажет, как заключить логику функционирования в Angular-сервис, выдающий данные с помощью наблюдаемого потока. Начнем с создания наблюдающего сервиса, который будет выдавать жестко заданные значения без фактического подключения к сокету. В листинге 8.14 создается сервис, выдающий каждую секунду текущее время.
Листинг 8.14. custom-observable-service.ts
import {Observable} from 'rxjs/Rx';
export class CustomObservableService{
createObservableService(): Observable<Date>{
return new Observable(
observer => {
setInterval(() =>
observer.next(new Date())
, 1000);
}
);
}
}
Здесь создается наблюдаемый объект, исходя из предположения, что подписчик предоставит Observer-объект, который знает, что делать с данными, предоставленными наблюдаемым объектом. Как только наблюдаемый объект вызывает в отношении наблюдателя метод next(), подписчик получает значение, предоставленное в качестве аргумента (в данном примере это new Date()). Поток данных никогда не выдает ошибку и никогда не завершается.
ПРИМЕЧАНИЕ
Можно также создать подписчика на наблюдаемый объект путем явного вызова метода Subscriber.create(). Соответствующий пример будет показан в разделе «Практикум».
Компонент AppComponent в листинге 8.15 получает внедренный CustomObservableService, вызывает метод createObservableService(), который возвращает Observable-объект, и подписывается на него, создавая наблюдателя, знающего, что делать с данными. Наблюдатель в данном примере присваивает полученное время переменной currentTime.
Листинг 8.15. Содержимое файла custom-observable-service-subscriber.ts
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { NgModule, Component } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import 'rxjs/add/operator/map';
import {CustomObservableService} from "./custom-observable-service";
@Component({
selector: 'app',
providers: [ CustomObservableService ],
template: `<h1>Simple subscriber to a service</h1>
Current time: {{currentTime | date: 'jms'}}
`})
class AppComponent {
currentTime: Date;
constructor(private sampleService: CustomObservableService) {
this.sampleService.createObservableService()
.subscribe( data => this.currentTime = data );
}
}
@NgModule({
imports: [ BrowserModule],
declarations: [ AppComponent],
bootstrap: [ AppComponent ]
})
class AppModule { }
platformBrowserDynamic().bootstrapModule(AppModule);
Для этого приложения в корневом каталоге проекта будет создан файл index.html. В приложении не используются никакие серверы, и его можно запустить путем ввода команды live-server в окне терминала. Текущее время в окне браузера будет обновляться каждую секунду. Здесь применяется канал DatePipe с форматом 'jms', обеспечивающий отображение только часов, минут и секунд (все форматы данных описаны в документации Angular DatePipe на ).
Это очень простой пример, но он демонстрирует основной прием для упаковки любой логики приложения в наблюдаемый поток и подписки на него. В данном случае используется метод setInterval(), но его можно заменить любым кодом конкретного назначения, создающим одно значение и более и отправляющим их в потоке.
Не забудьте про обработку ошибок и при надобности про завершение потока. В следующем фрагменте кода показывается простой наблюдаемый объект, отправляющий наблюдателю один элемент, способный выдать ошибку и сообщить наблюдателю о том, что поток завершен:
return new Observable(
observer => {
try {
observer.next('Hello from observable');
//throw ("Got an error");
} catch(err) {
observer.error(err);
} finally{
observer.complete();
}
}
);
Если в строку со throw не включить комментарий, то вызывается observer.error(), в результате чего в отношении подписчика вызывается обработчик при наличии такового.
Теперь научим Angular-сервис обмениваться данными с WebSocket-сервером.
Создадим небольшое Angular-приложение с WebSocket-сервисом (на стороне клиента) который взаимодействует с Node WebSocket-сервером. Серверный уровень может быть реализован с применением любой технологии, поддерживающей веб-сокеты. Архитектура такого приложения показана на рис. 8.11.
Рис. 8.11. Взаимодействие Angular-приложения с сервером с помощью сокета
Код в листинге 8.16 заключает WebSocket-объект браузера в наблюдаемый поток. Данный сервис создает экземпляр класса WebSocket, который подключается к серверу на основе предоставленного URL, и этот экземпляр обрабатывает сообщения, полученные с сервера. У объекта WebSocketService также имеется метод sendMessage(), позволяющий клиенту отправлять сообщения на сервер.
Листинг 8.16. Содержимое файла websocket-observable-service.ts
import {Observable} from 'rxjs/Rx';
export class WebSocketService{
ws: WebSocket;
createObservableSocket(url:string):Observable{
this.ws = new WebSocket(url);
return new Observable(
observer => {
this.ws.onmessage = (event) =>
observer.next(event.data);
this.ws.onerror = (event) => observer.error(event);
this.ws.onclose = (event) => observer.complete();
}
);
}
sendMessage(message: any){
this.ws.send(message);
}
}
ПРИМЕЧАНИЕ
В листинге 8.16 показан один из способов создания наблюдаемого объекта из WebSocket. В качестве альтернативы для получения того же результата можно воспользоваться методом Observable.webSocket().
В листинге 8.17 показан код компонента AppComponent, который подписывается на WebSocketService, внедряемый в AppComponent, представленный на рис. 8.11. Этот элемент может также отправлять сообщения серверу, когда пользователь нажимает кнопку Send Msg to Server (Отправить сообщение серверу).
Листинг 8.17. Содержимое файла websocket-observable-service-subscriber.ts
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { NgModule, Component } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import {WebSocketService} from "./websocket-observable-service";
@Component({
selector: 'app',
providers: [ WebSocketService ],
template: `<h1>Angular subscriber to WebSocket service</h1>
{{messageFromServer}}<br>
<button (click)="sendMessageToServer()">Send msg to Server</button>
`})
class AppComponent {
messageFromServer: string;
constructor(private wsService: WebSocketService) {
this.wsService.createObservableSocket("ws://localhost:8085")
.subscribe(
data => {
this.messageFromServer = data;
},
err => console.log( err),
() => console.log( 'The observable stream is complete')
);
}
sendMessageToServer(){
console.log("Sending message to WebSocket server");
this.wsService.sendMessage("Hello from client");
}
}
@NgModule({
imports: [ BrowserModule],
declarations: [ AppComponent],
bootstrap: [ AppComponent ]
})
class AppModule { }
platformBrowserDynamic().bootstrapModule(AppModule);
HTML-файл, выводящий этот компонент на экран, называется two-way-websocket-client.html (листинг 8.18). Нужно убедиться в том, что в качестве основного сценария приложения в systemjs.config.js фигурирует websocket-observable-service-subscriber.
Листинг 8.18. Содержимое файла two-way-websocket-client.html
<!DOCTYPE html>
<html>
<head>
<title>Http samples</title>
<script src="/
polyfill.js?features=Intl.~locale.en"></script>
<script src="node_modules/zone.js/dist/zone.js"></script>
<script src="node_modules/typescript/lib/typescript.js"></script>
<script src="node_modules/reflect-metadata/Reflect.js"></script>
<script src="node_modules/rxjs/bundles/Rx.js"></script>
<script src="node_modules/systemjs/dist/system.src.js"></script>
<script src="systemjs.config.js"></script>
<script>
System.import('app').catch(function (err) {console.error(err);});
</script>
</head>
<body>
<app>Loading...</app>
</body>
</html>
И наконец, нужно создать еще одну версию simple-websocket-server.ts, чтобы обслуживать HTML-файл с другим Angular-клиентом. Этот сервер будет реализован в файле two-way-websocket-server.ts, и в нем будет практически такой же код с двумя небольшими изменениями (представлены ниже).
1. Когда сервер получает запрос к базовому URL, возникает необходимость обслуживания предыдущего HTML для клиента:
app.get('/', (req, res) => { res.sendFile(path.join(__dirname, '..',
'client/two-way-websocket-client.html'));
});
2. Для обработки сообщений, поступающих от клиента, нужно добавить обработчик on('message'):
wsServer.on('connection',
websocket => {
websocket.send('This message was pushed by the WebSocket server');
websocket.on('message',
message => console.log("Server received: %s",
message));
});
Чтобы увидеть это приложение в работе, запустите nodemon build/two-way-websocket_server.js (или воспользуйтесь командой npm run twowayWsServer, сконфигурированной в файле package.json) и откройте браузер по адресу localhost:8000. Будет выведено окно с сообщением, выданным из Node, и если нажать кнопку, то на сервер будет отправлено сообщение Hello from client. Снимок экрана, показанный на рис. 8.12, был сделан после однократного нажатия кнопки (в инструментах разработчика (Chrome Developer Tools) была открыта вкладка Frames, находящаяся во вкладке Network (Сеть)).
Рис. 8.12. Получение сообщения в Angular от Node
Теперь, усвоив порядок обмена данными с сервером через протоколы HTTP и WebSocket, научим онлайн-аукцион взаимодействовать с Node-сервером.
В версию аукциона этой главы добавлен весьма значительный объем кода, так что мы решили избавить вас от его ввода. В данном практикуме будут рассмотрены только новые и измененные фрагменты, присутствующие в новой версии приложения-аукциона, созданные при изучении текущей главы. Эта версия приложения нацелена на решение двух основных задач.
• Реализация функциональных возможностей по поиску товара. Компонент SearchComponent подключит аукцион к Node-серверу через HTTP, а сведения о товарах и обзоры будут поступать с сервера.
• Добавление выдаваемых сервером уведомлений о ценовых предложениях с использованием WebSocket-протокола, чтобы пользователь мог подписаться и наблюдать за ценами на выбранный товар.
Основные игроки, вовлекаемые в реализацию поиска товара, представлены на рис. 8.13.
Рис. 8.13. Реализация поиска товара
Обозначение DI на рисунке означает внедрение зависимости (dependency injection). Angular внедряет Http-объект в ProductService, который, в свою очередь, внедряет три компонента: HomeComponent, SearchComponent и ProductDetailComponent. Объект ProductService отвечает за весь обмен данными с сервером.
ПРИМЕЧАНИЕ
В этом проекте используется Node-сервер, но вы можете воспользоваться любой технологией, поддерживающей протоколы HTTP и WebSocket, например Java, .NET, Python, Ruby и т.д.
Как уже упоминалось, мы дадим краткое пояснение относительно изменений в коде, внесенных в различные сценарии, но вам придется самостоятельно выполнить подробный пересмотр кода аукциона. В версии аукциона, предлагаемой в этой главе, раздел scripts выглядит следующим образом:
"scripts": {
"tsc": "tsc",
"start": "node build/auction.js",
"dev": "nodemon build/auction.js"
}
Ввод команды npm start приведет к запуску вашего Node-сервера, загружающего сценарий auction.js. В этом проекте в файле tsconfig.json каталог build указывается в качестве выходного для TypeScript-компилятора. При вводе команды npm run tsc во время нахождения в корневом каталоге проекта создаются два файла, auction.js и model.js. Если в вашем распоряжении имеется версия компилятора TypeScript 2.0 или выше, установленная глобально, то можно просто запустить команду tsc.
В исходном TypeScript-файле auction.ts содержится код, реализующий серверы HTTP и WebSocket, а в файле model.ts — данные, которые теперь размещаются на сервере. Ввод команды npm run dev приведет к запуску вашего Node-сервера в режиме живой перезагрузки.
Главная страница аукциона имеет в левой части форму поиска Search (Поиск); пользователь может ввести критерий поиска, нажать кнопку с таким же именем и получить данные о соответствующем товаре с сервера. Как показано на рис. 8.13, класс ProductService несет ответственность за весь обмен данными с сервером по протоколу HTTP, включая начальную загрузку сведений о товарах или поиск товаров по конкретным критериям.
До сих пор данные, касающиеся товаров, и обзоры были жестко заданы в коде на клиентской стороне в классе ProductService; при запуске приложения оно показывало все жестко заданные товары в компоненте HomeComponent. При щелчке пользователя на товаре маршрутизатор выполнял переход к компоненту ProductDetailComponent, который показывал сведения о товарах и обзоры, также жестко заданные в ProductService.
Теперь нужно, чтобы сведения о товарах и обзоры размещались на сервере. Код, который будет запущен как Node-приложение (веб-сервер), содержится в файлах server/auction.ts и server/model.ts. В файле auction.ts реализованы функциональные свойства HTTP и WebSocket, а в файле model.ts объявляются классы Product и Review, а также массивы с данными products и reviews. Кроме того, эти массивы были удалены из файла client/app/services/product-service.ts.
ПРИМЕЧАНИЕ
Класс Product имеет новое свойство categories, которое будет использоваться в SearchComponent.
Этот класс получит внедренный Http-объект, и большинство методов данного класса станут возвращать наблюдаемые потоки, созданные HTTP-запросами. Новая версия метода getProducts() показана в следующем фрагменте кода:
getProducts(): Observable<Product[]> {
return this.')
.map(response => response.json());
}
Следует напомнить, что предыдущий метод не выдавал HTTP-запрос GET, пока какой-либо объект не подписывался на метод getProducts() или же пока шаблон компонента не использовал в отношении возвращаемых этим методом данных канал AsyncPipe (пример можно найти в компоненте HomeComponent).
Похоже выглядит и метод getProductById():
getProductById(productId: number): Observable<Product> {
return this.}`)
}
Метод getReviewsForProduct() также возвращает значение типа Observable:
getReviewsForProduct(productId: number): Observable<Review[]> {
return this.
.get(`/products/${productId}/reviews`)
.map(response => response.json())
.map(reviews => reviews.map(
(r: any) => new Review(r.id, r.productId, new Date(r.timestamp),
r.user, r.rating, r.comment)));
}
Новый метод ProductService.search() используется, когда пользователь нажимает кнопку Search (Поиск), принадлежащую компоненту SearchComponent:
search(params: ProductSearchParams): Observable<Product[]> {
return this.
.get('/products', {search: encodeParams(params)})
.map(response => response.json());
}
Предыдущий метод Http.get() применяет второй аргумент, являющийся объектом со свойством search для хранения параметров строки запроса. Нетрудно заметить, что ранее в интерфейсе RequestOptionsArgs свойство search могло хранить либо строку, либо экземпляр класса URLSearchParams.
Далее показан код метода ProductService.encodeParams(), превращающего объект JavaScript в экземпляр класса URLSearchParams:
function encodeParams(params: any): URLSearchParams {
return Object.keys(params)
.filter(key => params[key])
.reduce((accum: URLSearchParams, key: string) => {
accum.append(key, params[key]);
return accum;
}, new URLSearchParams());
}
Новый метод ProductService.getAllCategories() используется для заполнения раскрывающегося списка Categories (Категории) в компоненте SearchComponent:
getAllCategories(): string[] {
return ['Books', 'Electronics', 'Hardware'];
}
В классе ProductService также определяется новая переменная searchEvent типа EventEmitter. Ее предназначение будет объяснено ниже.
Изначально компонент HomeComponent отображает на экране все товары путем вызова метода ProductService.getProducts(). Но когда пользователь выполняет поиск по какому-либо критерию, вам нужен запрос к серверу, который может возвратить поднабор товаров или пустой набор данных, если критерию поиска не отвечает ни один из товаров.
Компонент SearchComponent получает результат, который должен быть передан компоненту HomeComponent. Если оба этих элемента были дочерними для общего родителя (например, AppComponent), то родительский компонент может использоваться как посредник (см. главу 6) и вводить-выводить в качестве данных параметры дочерних компонентов. Но HomeComponent добавляется в AppComponent маршрутизатором в динамическом режиме, а текущая версия Angular не поддерживает кросс-маршрутный ввод-вывод параметров. Вам нужен другой посредник; таковым может выступить объект ProductService, поскольку внедрен и в SearchComponent, и в HomeComponent.
Рис. 8.14. Обмен данными между компонентами с помощью событий
В классе ProductService имеется переменная searchEvent, объявляемая следующим образом:
searchEvent: EventEmitter = new EventEmitter();
Компонент SearchComponent использует данную переменную для выдачи события searchEvent, которое в качестве полезной нагрузки переносит объект с параметрами поиска. Как показано на рис. 8.14, на это событие подписывается компонент HomeComponent.
Компонент SearchComponent является формой, и когда пользователь нажимает кнопку Search (Поиск), этот компонент должен уведомить мир о том, какие параметры поиска были введены. ProductService делает это, выдавая событие c параметрами поиска:
onSearch() {
if (this.formModel.valid) {
this.productService.searchEvent.emit(this.formModel.value);
}
}
Компонент HomeComponent подписан на событие searchEvent, которое может прибыть из компонента SearchComponent с полезной нагрузкой в виде параметров поиска. Как только это произойдет, будет вызван метод ProductService.search():
this.productService.searchEvent
.subscribe(
params => this.products = this.productService.search(params),
console.error.bind(console),
() => console.log('DONE')
);
Ограничения поиска В нашем поисковом решении предполагается, что при выполнении пользователем поиска товара компонент HomeComponent показан на экране. Но если пользователь перейдет к представлению Product Detail (Информация о продукте), то данный компонент будет удален из DOM-модели и отслеживателей события searchEvent не станет. Для примера в книге это не является существенным недостатком, и проще всего будет исправить ситуацию, отключив кнопку поиска в случае ухода пользователя с маршрута Home. Можно также внедрить объект Router в компонент SearchComponent и, когда пользователь нажимает кнопку Search (Поиск) при неактивном состоянии главного маршрута (if (!router.isActive(url))), выполнить программный переход путем вызова метода router.navigate('home'), который возвратит промис в виде Promise-объекта. Когда промис будет разрешен, вы можете выдать из него событие searchEvent. |
Следующий фрагмент кода взят из файла auction.ts, в коде которого ведется обработка запроса на поиск товара, отправленного клиентом. Когда клиент попадает в конечную точку сервера со строковыми параметрами запроса, полученные параметры передаются в виде req.query функции getProducts(). Она вводит в действие последовательность фильтров (в соответствии с установками параметров) в отношении массива товаров, чтобы отфильтровать неподходящие товары:
app.get('/products', (req, res) => {
res.json(getProducts(req.query));
});
...
function getProducts(params): Product[] {
let result = products;
if (params.title) {
result = result.filter(
p => p.title.toLowerCase().indexOf(params.title.toLowerCase()) !== -1);
}
if ( result.length > 0 && parseInt(params.price)) {
result = result.filter(
p => p.price <= parseInt(params.price));
}
if ( result.length > 0 && params.category) {
result = result.filter(
p => p.categories.indexOf(params.category.toLowerCase()) !== -1);
}
return result;
}
После краткого обзора кода реализации поиска товаров можно запустить Node-сервер, воспользовавшись командой npm run dev, и открыть в браузере адрес localhost:8000. После загрузки приложения-аукциона введите свой критерий поиска в форму в левой части окна и посмотрите, как компонент HomeComponent заново отобразит свой дочерний элемент (ProductItemComponent), отвечающий критериям поиска.
При проведении реальных аукционов ценовые предложения могут делаться сразу несколькими пользователями. Когда сервер получает такое предложение от пользователя, сервер ценовых предложений должен распространить по сети самое последнее из них среди всех пользователей, заинтересованных в подобных уведомлениях (среди подписавшихся на уведомления). Процесс выдачи ценовых предложений будет имитироваться путем генерации случайных цен от случайных пользователей.
Когда пользователи открывают представление Product Details (Информация о продукте), они должны иметь возможность подписаться на уведомление о ценовых предложениях на выбранный товар, сделанных другими пользователями. Эта функциональная возможность реализуется выдачей с серверной стороны через веб-сокеты. Представление Product Details (Информация о продукте) с кнопкой переключения Watch (???), запускающей и останавливающей текущие уведомления о ценовых предложениях, выдаваемых сервером с помощью сокета, показано на рис. 8.15. Далее мы кратко охарактеризуем изменения в приложении-аукционе, связанные с уведомлениями о ценовых предложениях.
Рис. 8.15. Кнопка-переключатель для отслеживания ценовых предложений
В каталоге client/app/services размещены два новых сервиса: BidService и WebSocketService. Последний является наблюдаемой оболочкой Observable для WebSocket-объекта. Он похож на тот, который создавался ранее в подразделе 8.4.2.
Сервис BidService получает внедренный сервис WebSocketService:
@Injectable()
export class BidService {
constructor(private webSocket: WebSocketService) {}
watchProduct(productId: number): Observable {
let openSubscriber = Subscriber.create(
() => this.webSocket.send({productId: productId}));
return this.webSocket.createObservableSocket('ws://
localhost:8000', openSubscriber)
.map(message => JSON.parse(message));
}
}
Сервис BidService внедрен в компонент ProductDetailComponent. Когда пользователь нажимает кнопку переключения Watch (???), метод BidService.watchProduct() отправляет идентификатор товара на сервер, показывая тем самым, что данный пользователь хочет запустить или остановить наблюдение за выбранным товаром:
toggleWatchProduct() {
if (this.subscription) {
this.subscription.unsubscribe();
this.subscription = null;
this.isWatching = false;
} else {
this.isWatching = true;
this.subscription = this.bidService.watchProduct(this.product.id)
.subscribe(
products => this.currentBid = products.find((p: any) => p.productId
=== this.product.id).bid,
error => console.log(error));
}
}
У шаблона компонента ProductDetailComponent имеется кнопка переключения Watch (???), и самые последние ценовые предложения, получаемые от сервера, отображаются в виде HTML-надписи:
<button class="btn btn-default btn-lg"
[ngClass]="{active: isWatching}"
(click)="toggleWatchProduct()"
role="button">
{{ isWatching ? 'Stop watching' : 'Watch' }}
</button>
<label>Current bid: {{ currentBid | currency }}</label>
Еще есть небольшой новый сценарий client/app/services/services.ts, в котором объявляются все инструкции импортирования и массив сервисов, используемых для внедрения зависимостей:
import {BidService} from './bid-service';
import {ProductService} from './product-service';
import {WebSocketService} from './websocket-service';
export const ONLINE_AUCTION_SERVICES = [
BidService,
ProductService,
WebSocketService
];
Поставщики, объявленные в константе ONLINE_AUCTION_SERVICES, используются в файле main.ts, загружающем ту часть аукциона, которая относится к Angular:
@NgModule({
...
providers:[ProductService,
ONLINE_AUCTION_SERVICES,
{provide: LocationStrategy, useClass: HashLocationStrategy}],
bootstrap:[ ApplicationComponent ]
})
Сценарий server/auction.ts включает код, обслуживающий подписавшихся клиентов и генерирующий случайные ценовые предложения. Каждое сгенерированное ценовое предложение должно быть на пять долларов выше предыдущего предложения. Как только будет сгенерировано новое ценовое предложение, оно тут же распространяется среди всех подписавшихся клиентов.
Обслуживанием запросов на уведомления о ценовых предложениях и распространением ценовых предложений среди всех подписавшихся клиентов занимается следующий код из файла server/auction.ts:
Здесь вам следует протестировать значение свойства readyState WebSocket-объекта, чтобы убедиться в активности подключения клиента. Например, если пользователь закрыл окно аукциона, то надобность в отправке уведомлений о ценовых предложениях отпадает, поэтому данное сокет-подключение из отображения подписок удаляется.
ПРИМЕЧАНИЕ
Обратите внимание на использование в методе subscribeToProductBids() оператора распространения (…). Он применяется для копирования существующего массива идентификаторов товаров и добавления нового идентификатора.
Мы рассмотрели код аукциона, относящийся к веб-сокету, а весь остальной код предлагаем изучить самостоятельно. Для тестирования работы по уведомлениям о ценовых предложениях нужно запустить приложение, щелкнуть на названии товара и в представлении сведений о товаре щелкнуть на кнопке Watch (???). Вы увидите новые ценовые предложения для этого товара, выданные сервером. Откройте приложение-аукцион в более чем одном браузере, чтобы протестировать, должным ли образом включаются и выключаются уведомления в каждом браузере.
Основной темой данной главы было предоставление возможности клиент-серверного взаимодействия, являющегося основанием для существования веб-сред. Angular в сочетании с библиотекой расширений RxJS предлагает унифицированный подход использования данных, получаемых с сервера: клиентский код подписывается на поток данных, поступающий с сервера, независимо от характера взаимодействия, основанного либо на протоколе HTTP, либо на WebSocket. Модель программирования изменилась: вместо запроса данных как в приложениях стиля Ajax, Angular задействует данные, выдаваемые наблюдаемыми потоками.
Вот основные выводы этой главы.
• Angular поставляется с Http-объектом, поддерживающим обмен данными с веб-сервером по протоколу HTTP.
• Поставщики для HTTP-сервисов находятся в модуле HttpModule. Если в вашем приложении используется протокол HTTP, то не забудьте включить его в декоратор @NgModule.
• Открытые методы HttpObject возвращают объект типа Observable, но только когда клиент подпишется на него, выполняется запрос к серверу.
• Протокол WebSocket эффективнее и лаконичнее протокола HTTP. Он двунаправленный, и обмен данными может инициироваться как клиентом, так и сервером.
• Создание веб-сервера с помощью NodeJS и Express представляется относительно несложной задачей, но клиент Angular может вести обмен данными с веб-серверами, реализованными с использованием и других технологий.