Руководство по Создание одностраничного веб-приложения
Предупреждение
30 октября 2020 г. API-интерфейсы Поиск Bing перемещены из служб ИИ Azure в службы Поиск Bing. Эта документация приводится только для справки. Обновленную информацию см. в документации по API Поиска Bing. Инструкции по созданию ресурсов Azure для Поиска Bing приведены в статье Создание ресурса для Поиска Bing с помощью Azure Marketplace.
API Bing для поиска новостей позволяет выполнять поиск новостей и получать результаты различных типов, релевантных поисковому запросу. В этом руководстве мы создаем одностраничное веб-приложение, использующее API Bing для поиска новостей для отображения результатов поиска прямо на странице. Приложение включает в себя компоненты HTML, CSS и JavaScript. Исходный код этого примера доступен на GitHub.
Примечание
При нажатии заголовков JSON и HTTP в нижней части страницы отображаются сведения об ответе JSON и HTTP-запросе. Эти данные позволяют лучше изучить службу.
На примере учебного приложения показано, как выполнить такие задачи:
- Вызов API Bing для поиска новостей из JavaScript.
- Передача параметров поиска в API Bing для поиска новостей.
- Отображение результатов поиска новостей по четырем категориям: любой тип, деловые, здравоохранение или политика; за определенные периоды: 24 часа, за прошедшую неделю, месяц или за любое время.
- Переход по страницам результатов поиска.
- Обработка идентификатора клиента Bing и ключа подписки API.
- Обработка ошибок, которые могут возникнуть.
Страница руководства является полностью автономной; на ней не используется никаких внешних платформ, таблиц стилей и даже файлов изображений. Она использует только широко распространенные функции языка JavaScript и работает с текущими версиями всех основных браузеров.
Предварительные требования
Чтобы выполнить задания, описанные в этом руководстве, требуются ключи подписок для API "Поиск Bing". Если у вас их нет, вам потребуется следующее:
- подписка Azure — создайте бесплатную учетную запись.
- Получив подписку Azure, создайте ресурс Поиск Bing в портал Azure, чтобы получить ключ и конечную точку. После развертывания щелкните Перейти к ресурсам.
Компоненты приложения
Как и любое другое одностраничное веб-приложение, это учебное приложение состоит из трех частей:
- HTML — определяет структуру и содержимое страницы;
- CSS — определяет внешний вид страницы;
- JavaScript — определяет поведение страницы.
Большинство компонентов HTML и CSS являются обычными, поэтому в руководстве это не обсуждается. HTML-код содержит форму поиска, в которой пользователь вводит запрос и выбирает параметры поиска. Форма связана с JavaScript, который выполняет поиск, используя атрибут onsubmit
тега <form>
:
<form name="bing" onsubmit="return newBingNewsSearch(this)">
Обработчик onsubmit
возвращает значение false
, что препятствует отправке формы на сервер. Код JavaScript выполняет сбор необходимой информации из формы и выполняет поиск.
HTML также содержит разделы (HTML-теги <div>
), где отображаются результаты поиска.
Управление ключом подписки
Чтобы не включать ключ подписки для API поиска Bing прямо в код, мы используем для его хранения постоянное хранилище браузера. Перед сохранением ключа мы запрашиваем ключ пользователя. Если на дальнейшем этапе ключ отклоняется API, мы аннулируем сохраненный ключ и снова выводим запрос пользователю.
Мы определяем функции storeValue
и retrieveValue
, которые используют объект localStorage
(если браузер поддерживает его) или файл cookie. Наша функция getSubscriptionKey()
использует эти функции для хранения и извлечения ключа пользователя. Вы можете использовать указанную ниже глобальную конечную точку или конечную точку пользовательского поддомена, отображаемого на портале Azure для вашего ресурса.
// Cookie names for data we store
API_KEY_COOKIE = "bing-search-api-key";
CLIENT_ID_COOKIE = "bing-search-client-id";
// Bing Search API endpoint
BING_ENDPOINT = "https://api.cognitive.microsoft.com/bing/v7.0/news";
// ... omitted definitions of storeValue() and retrieveValue()
// Browsers differ in their support for persistent storage by
// local HTML files. See the source code for browser-specific
// options.
// Get stored API subscription key, or
// prompt if it's not found.
function getSubscriptionKey() {
var key = retrieveValue(API_KEY_COOKIE);
while (key.length !== 32) {
key = prompt("Enter Bing Search API subscription key:", "").trim();
}
// always set the cookie in order to update the expiration date
storeValue(API_KEY_COOKIE, key);
return key;
}
В теге HTML <form>
атрибут onsubmit
вызывает функцию bingWebSearch
, чтобы возвращать результаты поиска. Функция bingWebSearch
использует getSubscriptionKey()
для аутентификации каждого запроса. Как показано в предыдущем определении, getSubscriptionKey
запрашивает у пользователя ключ, если он еще не был введен. Затем ключ сохраняется для последующего использования приложением.
<form name="bing" onsubmit="this.offset.value = 0; return bingWebSearch(this.query.value,
bingSearchOptions(this), getSubscriptionKey())">
Выбор параметров поиска
На следующем изображении показано текстовое поле для запроса и параметры, которые определяют поиск новостей о финансировании школ.
HTML-форма включает элементы со следующими именами:
Элемент | Описание |
---|---|
where |
Раскрывающееся меню для выбора рынка (расположения и языка), который используется для поиска. |
query |
Текстовое поле для ввода условий поиска. |
category |
Флажки для повышения уровня определенных видов результатов. Например, повышение уровня здравоохранения повышает положение новостей о здравоохранении в списке результатов. |
when |
Раскрывающееся меню для ограничения поиска результатами за последний день, неделю или месяц (необязательный параметр). |
safe |
Флажок, указывающий, следует ли использовать функцию безопасного поиска Bing для фильтрации результатов с материалами для взрослых. |
count |
Скрытое поле. Количество результатов поиска для каждого запроса. Измените значение, чтобы на странице отображалось меньше или больше результатов. |
offset |
Скрытое поле. Смещение первого результата поиска в запросе. Используется для разбиения по страницам. Сбрасывается на 0 при новом запросе. |
Примечание
Поиск в Интернете Bing поддерживает дополнительные параметры запроса. Мы используем только некоторые из них.
// build query options from the HTML form
function bingSearchOptions(form) {
var options = [];
options.push("mkt=" + form.where.value);
options.push("SafeSearch=" + (form.safe.checked ? "strict" : "off"));
if (form.when.value.length) options.push("freshness=" + form.when.value);
for (var i = 0; i < form.category.length; i++) {
if (form.category[i].checked) {
category = form.category[i].value;
break;
}
}
if (category.valueOf() != "all".valueOf()) {
options.push("category=" + category);
}
options.push("count=" + form.count.value);
options.push("offset=" + form.offset.value);
return options.join("&");
}
Например, параметр SafeSearch
в реальном вызове API может иметь значение strict
, moderate
или off
, при этом moderate
является значением по умолчанию. Тем не менее в форме используется флажок, который имеет только два состояния. Код JavaScript преобразует это значение в strict
или off
(moderate
не используется).
Выполнение запроса
Получив на вход запрос, строку параметров и ключ API, функция BingNewsSearch
применяет объект XMLHttpRequest
для выполнения запроса к конечной точке Поиска новостей Bing.
// perform a search given query, options string, and API key
function bingNewsSearch(query, options, key) {
// scroll to top of window
window.scrollTo(0, 0);
if (!query.trim().length) return false; // empty query, do nothing
showDiv("noresults", "Working. Please wait.");
hideDivs("results", "related", "_json", "_http", "paging1", "paging2", "error");
var request = new XMLHttpRequest();
if (category.valueOf() != "all".valueOf()) {
var queryurl = BING_ENDPOINT + "/search?" + "?q=" + encodeURIComponent(query) + "&" + options;
}
else
{
if (query){
var queryurl = BING_ENDPOINT + "?q=" + encodeURIComponent(query) + "&" + options;
}
else {
var queryurl = BING_ENDPOINT + "?" + options;
}
}
// open the request
try {
request.open("GET", queryurl);
}
catch (e) {
renderErrorMessage("Bad request (invalid URL)\n" + queryurl);
return false;
}
// add request headers
request.setRequestHeader("Ocp-Apim-Subscription-Key", key);
request.setRequestHeader("Accept", "application/json");
var clientid = retrieveValue(CLIENT_ID_COOKIE);
if (clientid) request.setRequestHeader("X-MSEdge-ClientID", clientid);
// event handler for successful response
request.addEventListener("load", handleBingResponse);
// event handler for erorrs
request.addEventListener("error", function() {
renderErrorMessage("Error completing request");
});
// event handler for aborted request
request.addEventListener("abort", function() {
renderErrorMessage("Request aborted");
});
// send the request
request.send();
return false;
}
После успешного выполнения HTTP-запроса JavaScript вызывает обработчик событий load
, функцию handleBingResponse()
, чтобы предать успешный запрос HTTP GET в API.
// handle Bing search request results
function handleBingResponse() {
hideDivs("noresults");
var json = this.responseText.trim();
var jsobj = {};
// try to parse JSON results
try {
if (json.length) jsobj = JSON.parse(json);
} catch(e) {
renderErrorMessage("Invalid JSON response");
}
// show raw JSON and HTTP request
showDiv("json", preFormat(JSON.stringify(jsobj, null, 2)));
showDiv("http", preFormat("GET " + this.responseURL + "\n\nStatus: " + this.status + " " +
this.statusText + "\n" + this.getAllResponseHeaders()));
// if HTTP response is 200 OK, try to render search results
if (this.status === 200) {
var clientid = this.getResponseHeader("X-MSEdge-ClientID");
if (clientid) retrieveValue(CLIENT_ID_COOKIE, clientid);
if (json.length) {
if (jsobj._type === "News") {
renderSearchResults(jsobj);
} else {
renderErrorMessage("No search results in JSON response");
}
} else {
renderErrorMessage("Empty response (are you sending too many requests too quickly?)");
}
}
// Any other HTTP response is an error
else {
// 401 is unauthorized; force re-prompt for API key for next request
if (this.status === 401) invalidateSubscriptionKey();
// some error responses don't have a top-level errors object, so gin one up
var errors = jsobj.errors || [jsobj];
var errmsg = [];
// display HTTP status code
errmsg.push("HTTP Status " + this.status + " " + this.statusText + "\n");
// add all fields from all error responses
for (var i = 0; i < errors.length; i++) {
if (i) errmsg.push("\n");
for (var k in errors[i]) errmsg.push(k + ": " + errors[i][k]);
}
// also display Bing Trace ID if it isn't blocked by CORS
var traceid = this.getResponseHeader("BingAPIs-TraceId");
if (traceid) errmsg.push("\nTrace ID " + traceid);
// and display the error message
renderErrorMessage(errmsg.join("\n"));
}
}
Важно!
Успешный HTTP-запрос не обязательно означает, что сам поиск был успешным. Если во время выполнения операции поиска возникает ошибка, то API Bing для поиска новостей возвращает код состояния, отличный от "HTTP: 200", и включает сведения об ошибке в ответ JSON. Кроме того, если запрос имел ограничение по скорости, то API возвращает пустой ответ.
Большая часть кода в обеих описанных выше функциях предназначена для обработки ошибок. Ошибки могут возникать на следующих этапах:
Этап | Возможные ошибки | Чем обрабатывается |
---|---|---|
Построение объекта запроса JavaScript | Недопустимый URL-адрес | Блок try /catch |
Выполнение запроса | Ошибки сети, прерванные соединения | Обработчики событий error и abort |
Выполнение поиска | Недопустимый запрос, недопустимый JSON-файл, ограничения скорости | Тесты в обработчике событий load |
Ошибки обрабатываются путем вызова функции renderErrorMessage()
со всеми имеющимися сведениями об ошибке. Если ответ передает полный набор тестов ошибок, то мы вызываем функцию renderSearchResults()
для отображения результатов поиска на странице.
Отображение результатов поиска
Основная функция отображения результатов поиска — renderSearchResults()
. Эта функция принимает JSON, возвращенный службой поиска новостей Bing, и отображает результаты новостей и связанные с ними поисковые запросы, если они есть.
// render the search results given the parsed JSON response
function renderSearchResults(results) {
// add Prev / Next links with result count
var pagingLinks = renderPagingLinks(results);
showDiv("paging1", pagingLinks);
showDiv("paging2", pagingLinks);
showDiv("results", renderResults(results.value));
if (results.relatedSearches)
showDiv("sidebar", renderRelatedItems(results.relatedSearches));
}
Основные результаты поиска возвращаются как объекты верхнего уровня value
в ответе JSON. Мы передаем их в нашу функцию renderResults()
, которая поочередно вызывает для них функцию отображения в формате HTML. Полученный HTML-код возвращается в renderSearchResults()
, где он вставляется в раздел results
на странице.
function renderResults(items) {
var len = items.length;
var html = [];
if (!len) {
showDiv("noresults", "No results.");
hideDivs("paging1", "paging2");
return "";
}
for (var i = 0; i < len; i++) {
html.push(searchItemRenderers.news(items[i], i, len));
}
return html.join("\n\n");
}
API Bing для поиска новостей возвращает до четырех различных видов связанных результатов, каждый в своем собственном объекте верхнего уровня. К ним относятся:
Тип связи | Описание |
---|---|
pivotSuggestions |
Запросы, которые заменяют сводное слово в исходном поиске другим. Например, если вы ищете "красные цветы", сводным словом может быть "красные", а сводным предложением может быть "желтые цветы". |
queryExpansions |
Запросы, которые сужают исходный поиск, добавляя больше условий. Например, если вы ищете "Microsoft Surface", расширением запроса может быть "Microsoft Surface Pro". |
relatedSearches |
Запросы, которые вводились другими пользователями, использовавшими ранее исходный поиск. Например, если вы ищете "гора Рейнир", может применяться связанный поиск "гора Святой Елены". |
similarTerms |
Запросы, похожие по смыслу на исходный запрос. Например, если вы ищете "школы", аналогичным термином может быть "образование". |
Как ранее было показано в renderSearchResults()
, мы преобразовываем для просмотра только предложения relatedItems
и размещаем результирующие ссылки на боковой панели страницы.
Отображение элементов результата
В коде JavaScript есть объект searchItemRenderers
, который содержит функции-обработчики, создающие HTML-код для каждого типа результатов поиска.
searchItemRenderers = {
news: function(item) { ... },
webPages: function (item) { ... },
images: function(item, index, count) { ... },
relatedSearches: function(item) { ... }
}
Функция-обработчик может принимать следующие параметры:
Параметр | Описание |
---|---|
item |
Объект JavaScript, содержащий свойства элемента, такие как URL-адрес и его описание. |
index |
Индекс элемента результата в коллекции. |
count |
Число элементов в коллекции результатов поиска. |
Параметры index
и count
можно использовать для нумерации результатов, создания специального HTML-кода для начала или конца коллекции, вставки разрывов строки после определенного числа элементов и т. д. Если от отрисовщика не требуется выполнять эту функцию, то ему не нужно принимать эти два параметра.
Отрисовщик news
показан в следующем фрагменте JavaScript:
// render news story
news: function (item) {
var html = [];
html.push("<p class='news'>");
if (item.image) {
width = 60;
height = Math.round(width * item.image.thumbnail.height / item.image.thumbnail.width);
html.push("<img src='" + item.image.thumbnail.contentUrl +
"&h=" + height + "&w=" + width + "' width=" + width + " height=" + height + ">");
}
html.push("<a href='" + item.url + "'>" + item.name + "</a>");
if (item.category) html.push(" - " + item.category);
if (item.contractualRules) { // MUST display source attributions
html.push(" (");
var rules = [];
for (var i = 0; i < item.contractualRules.length; i++)
rules.push(item.contractualRules[i].text);
html.push(rules.join(", "));
html.push(")");
}
html.push(" (" + getHost(item.url) + ")");
html.push("<br>" + item.description);
return html.join("");
},
Функция отрисовщика новостей выполняет следующее.
- Создает тег параграфа, назначает его классу
news
и передает его в массив html. - Вычисляет размер эскиза изображения (с фиксированной шириной 60 пикселей, высота рассчитывается пропорционально).
- создает HTML-тег
<img>
для отображения эскиза изображения; - создает HTML-теги
<a>
со ссылкой на изображение и страницей, которая его содержит; - создает описание со сведениями об этом изображении и о сайте, на котором оно размещено.
Размер эскиза указывается в теге <img>
и в полях h
и w
URL-адреса эскиза. Затем служба эскизов Bing предоставляет эскиз точно такого размера.
Сохранение идентификатора клиента
Ответы от интерфейсов API поиска Bing могут содержать заголовок X-MSEdge-ClientID
, который необходимо отправить обратно в API с последующими запросами. Если используется несколько API-интерфейсов поиска Bing, то по возможности со всеми ими необходимо использовать один идентификатор клиента.
Наличие заголовка X-MSEdge-ClientID
позволяет API-интерфейсам Bing связывать все поисковые запросы пользователя, а это дает два важных преимущества.
Во-первых, поисковая система Bing может применять к операциям поиска прошлый контекст и находить результаты, которые лучше соответствуют требованиям пользователя. Если пользователь ранее уже вводил поисковые запросы, например, связанные с мореплаванием под парусом, то при последующем поиске по слову "морские узлы" в приоритетном порядке могут возвращаться сведения об узлах, которые используются в мореплавании.
Во-вторых, Bing может случайным образом выбирать пользователей для опробования новых возможностей, прежде чем делать их общедоступными. Предоставление одинакового идентификатора клиента при каждом запросе гарантирует, что пользователи, которым был открыт доступ к определенному компоненту, всегда будут иметь к нему доступ. Без идентификатора клиента такой компонент может появляться и исчезать в результатах поиска пользователя как бы случайным образом.
Из-за политик безопасности браузера (CORS) заголовок X-MSEdge-ClientID
может быть недоступным для кода JavaScript. Это ограничение возникает, когда ответ поиска и страница, его запросившая, имеют разные источники. В рабочей среде такая проблема с политикой решается путем размещения серверного скрипта, который выполняет вызов API, на одном домене с веб-страницей. Так как скрипт будет иметь тот же источник, что и веб-страница, заголовок X-MSEdge-ClientID
станет доступным для JavaScript.
Примечание
В рабочем веб-приложении запрос следует выполнять на стороне сервера. В противном случае вам придется включать в веб-страницу ключ API поиска Bing, где он будет доступен для всех, кто просматривает исходный код. С вас будет взиматься плата за любое использование ресурсов в рамках вашего ключа подписки API, включая запросы, выполненные неавторизованными сторонами, поэтому важно не предоставлять доступ к своему ключу.
В целях разработки запрос к API Bing для поиска в Интернете можно выполнить через прокси-сервер CORS. Ответ от прокси-сервера содержит заголовок Access-Control-Expose-Headers
, который поддерживает заголовки ответов и делает их доступными для JavaScript.
Установить прокси-сервер CORS довольно просто. Это позволит нашему учебному приложению иметь доступ к заголовку идентификатора клиента. Для начала установите платформу Node.js, если она еще не установлена. Затем введите следующую команду в командном окне:
npm install -g cors-proxy-server
После этого в HTML-файле измените конечную точку службы "Поиск в Интернете Bing" на следующую:
http://localhost:9090/https://api.cognitive.microsoft.com/bing/v7.0/search
И наконец, запустите прокси-сервер CORS с помощью следующей команды:
cors-proxy-server
Не закрывайте командное окно, пока используете учебное приложение. Это приведет к остановке прокси-сервера. Теперь в развертываемом разделе заголовков HTTP под результатами поиска можно увидеть заголовок X-MSEdge-ClientID
(среди прочих) и убедиться, что он одинаковый для всех запросов.