Tworzenie interfejsu wyszukiwania za pomocą widżetu wyszukiwania

Widżet wyszukiwania zawiera konfigurowalny interfejs wyszukiwania dla aplikacji internetowych. Implementacja tego widżetu wymaga niewielkiej ilości kodu HTML i JavaScriptu i umożliwia korzystanie z popularnych funkcji wyszukiwania, takich jak aspekty i podział na strony. Możesz też dostosować części interfejsu za pomocą CSS i JavaScriptu.

Jeśli potrzebujesz większej elastyczności niż widżet, rozważ użycie interfejsu Query API. Informacje o tworzeniu interfejsu wyszukiwania za pomocą interfejsu Query API znajdziesz w artykule Tworzenie interfejsu wyszukiwania przy użyciu interfejsu Query API.

Tworzenie interfejsu wyszukiwania

Utworzenie interfejsu wyszukiwania składa się z kilku kroków:

  1. Konfigurowanie wyszukiwarki
  2. Wygeneruj identyfikator klienta dla aplikacji
  3. Dodaj znaczniki HTML pola wyszukiwania i wyników
  4. Wczytaj widżet na stronie
  5. Zainicjuj widżet

Konfigurowanie wyszukiwarki

Każdy interfejs wyszukiwania musi mieć zdefiniowaną wyszukiwarkę w konsoli administracyjnej. Wyszukiwarka udostępnia dodatkowe informacje związane z zapytaniem, np. źródła danych, aspekty i ustawienia jakości wyszukiwania.

Instrukcje tworzenia wyszukiwarki znajdziesz w sekcji Tworzenie niestandardowego środowiska wyszukiwania.

Wygeneruj identyfikator klienta dla aplikacji

Oprócz wykonania czynności opisanych w artykule Konfigurowanie dostępu do interfejsu Google Cloud Search API musisz też wygenerować identyfikator klienta dla aplikacji internetowej.

Konfigurowanie projektu

Podczas konfigurowania projektu:

  • Wybierz typ klienta Przeglądarka internetowa.
  • Podaj identyfikator URI źródła aplikacji.
  • Zanotuj utworzony identyfikator klienta. Identyfikator klienta będzie potrzebny do wykonania kolejnych czynności. Tajny klucz klienta nie jest wymagany dla widżetu.

Więcej informacji znajdziesz w artykule OAuth 2.0 w przypadku aplikacji internetowej po stronie klienta.

Dodaj znaczniki HTML

Widżet wymaga do działania niewielkiej ilości kodu HTML. Musisz podać:

  • Element input pola wyszukiwania.
  • Element, do którego zostanie zakotwiczone wyskakujące okienko z sugestią.
  • Element do przechowywania wyników wyszukiwania.
  • (Opcjonalnie) Podaj element zawierający elementy sterujące aspektów.

Ten fragment kodu HTML zawiera kod HTML widżetu wyszukiwania. Elementy do powiązania są oznaczone atrybutem id:

serving/widget/public/with_css/index.html
<div id="search_bar">
  <div id="suggestions_anchor">
    <input type="text" id="search_input" placeholder="Search for...">
  </div>
</div>
<div id="facet_results"></div>
<div id="search_results"></div>

Wczytaj widżet

Widżet jest ładowany dynamicznie przez skrypt wczytujący. Aby uwzględnić komponent wczytujący, użyj tagu <script> w ten sposób:

serving/widget/public/with_css/index.html
<!-- Google API loader -->
<script src="https://apis.google.com/js/api.js?mods=enable_cloud_search_widget&onload=onLoad" async defer></script>

Musisz podać wywołanie zwrotne onload w tagu skryptu. Funkcja jest wywoływana, gdy moduł ładowania jest gotowy. Gdy moduł wczytywania będzie gotowy, kontynuuj ładowanie widżetu przez wywołanie metody gapi.load() w celu załadowania modułów klienta API, logowania przez Google i Cloud Search.

serving/widget/public/with_css/app.js
/**
* Load the cloud search widget & auth libraries. Runs after
* the initial gapi bootstrap library is ready.
*/
function onLoad() {
  gapi.load('client:auth2:cloudsearch-widget', initializeApp)
}

Funkcja initializeApp() jest wywoływana po wczytaniu wszystkich modułów.

Zainicjuj widżet

Najpierw zainicjuj bibliotekę klienta, wywołując metodę gapi.client.init() lub gapi.auth2.init() za pomocą wygenerowanego identyfikatora klienta i zakresu https://www.googleapis.com/auth/cloud_search.query. Następnie użyj klas gapi.cloudsearch.widget.resultscontainer.Builder i gapi.cloudsearch.widget.searchbox.Builder, aby skonfigurować widżet i powiązać go z elementami HTML.

Poniższy przykład pokazuje, jak uruchomić widżet:

serving/widget/public/with_css/app.js
/**
 * Initialize the app after loading the Google API client &
 * Cloud Search widget.
 */
function initializeApp() {
  // Load client ID & search app.
  loadConfiguration().then(function() {
    // Set API version to v1.
    gapi.config.update('cloudsearch.config/apiVersion', 'v1');

    // Build the result container and bind to DOM elements.
    var resultsContainer = new gapi.cloudsearch.widget.resultscontainer.Builder()
      .setSearchApplicationId(searchApplicationName)
      .setSearchResultsContainerElement(document.getElementById('search_results'))
      .setFacetResultsContainerElement(document.getElementById('facet_results'))
      .build();

    // Build the search box and bind to DOM elements.
    var searchBox = new gapi.cloudsearch.widget.searchbox.Builder()
      .setSearchApplicationId(searchApplicationName)
      .setInput(document.getElementById('search_input'))
      .setAnchor(document.getElementById('suggestions_anchor'))
      .setResultsContainer(resultsContainer)
      .build();
  }).then(function() {
    // Init API/oauth client w/client ID.
    return gapi.auth2.init({
        'clientId': clientId,
        'scope': 'https://www.googleapis.com/auth/cloud_search.query'
    });
  });
}

W przykładzie powyżej podano 2 zmienne konfiguracji zdefiniowane w taki sposób:

serving/widget/public/with_css/app.js
/**
* Client ID from OAuth credentials.
*/
var clientId = "...apps.googleusercontent.com";

/**
* Full resource name of the search application, such as
* "searchapplications/<your-id>".
*/
var searchApplicationName = "searchapplications/...";

Dostosowywanie procesu logowania

Domyślnie widżet prosi użytkowników o zalogowanie się i autoryzowanie aplikacji w chwili, gdy zaczynają wpisywać zapytanie. Możesz używać logowania się przez Google w witrynach, aby oferować użytkownikom bardziej spersonalizowane logowanie.

Bezpośrednie autoryzację użytkowników

Użyj funkcji Zaloguj się przez Google, aby monitorować stan logowania użytkowników i w razie potrzeby także wylogowywanych i logowanych użytkowników. Na przykład w tym przykładzie obserwujemy stan isSignedIn w celu monitorowania zmian dotyczących logowania i używamy metody GoogleAuth.signIn() do inicjowania logowania kliknięciem przycisku:

serving/widget/public/with_signin/app.js
// Handle sign-in/sign-out.
let auth = gapi.auth2.getAuthInstance();

// Watch for sign in status changes to update the UI appropriately.
let onSignInChanged = (isSignedIn) => {
  // Update UI to switch between signed in/out states
  // ...
}
auth.isSignedIn.listen(onSignInChanged);
onSignInChanged(auth.isSignedIn.get()); // Trigger with current status.

// Connect sign-in/sign-out buttons.
document.getElementById("sign-in").onclick = function(e) {
  auth.signIn();
};
document.getElementById("sign-out").onclick = function(e) {
  auth.signOut();
};

Więcej informacji znajdziesz w artykule Logowanie się przez Google.

Automatyczne logowanie użytkowników

Możesz jeszcze bardziej uprościć logowanie, wstępnie autoryzując aplikację w imieniu użytkowników w organizacji. Ta technika jest też przydatna, gdy do ochrony aplikacji używasz Cloud Identity Aware Proxy.

Więcej informacji znajdziesz w artykule Korzystanie z logowania przez Google w aplikacjach IT.

Dostosowywanie interfejsu

Wygląd interfejsu wyszukiwania możesz zmieniać, korzystając z kilku technik:

  • Zastępowanie stylów CSS
  • Ozdabiaj elementy za pomocą adaptera
  • Tworzenie elementów niestandardowych za pomocą adaptera

Zastępowanie stylów CSS

Widżet wyszukiwania ma własny kod CSS do określania stylu sugestii i elementów wyników oraz elementów sterujących podziałem na strony. W razie potrzeby możesz zmienić styl tych elementów.

Podczas wczytywania widżet wyszukiwania dynamicznie wczytuje swój domyślny arkusz stylów. Dzieje się tak po wczytaniu arkuszy stylów aplikacji, co zwiększa priorytet reguł. Aby mieć pewność, że Twoje style będą miały pierwszeństwo przed stylami domyślnymi, zwiększ szczegółowość reguł domyślnych za pomocą selektorów elementów nadrzędnych.

Na przykład ta reguła nie działa, gdy dokument jest wczytywany w statycznym tagu link lub style.

.cloudsearch_suggestion_container {
  font-size: 14px;
}

Zamiast tego kwalifikuj regułę na podstawie identyfikatora lub klasy kontenera nadrzędnego zadeklarowanego na stronie.

#suggestions_anchor .cloudsearch_suggestion_container {
  font-size: 14px;
}

Listę klas obsługi i przykładowy kod HTML wygenerowany przez widżet znajdziesz w dokumentacji obsługiwanych klas CSS.

Ozdabiaj elementy za pomocą adaptera

Aby dekorować element przed renderowaniem, utwórz i ponownie skonfiguruj adapter, który obsługuje jedną z metod dekorowania, np. decorateSuggestionElement lub decorateSearchResultElement.

Na przykład te adaptery dodają klasę niestandardową do elementów sugestii i wyników.

serving/widget/public/with_decorated_element/app.js
/**
 * Search box adapter that decorates suggestion elements by
 * adding a custom CSS class.
 */
function SearchBoxAdapter() {}
SearchBoxAdapter.prototype.decorateSuggestionElement = function(element) {
  element.classList.add('my-suggestion');
}

/**
 * Results container adapter that decorates suggestion elements by
 * adding a custom CSS class.
 */
function ResultsContainerAdapter() {}
ResultsContainerAdapter.prototype.decorateSearchResultElement = function(element) {
  element.classList.add('my-result');
}

Aby zarejestrować adapter podczas inicjowania widżetu, użyj metody setAdapter() odpowiedniej klasy Builder:

serving/widget/public/with_decorated_element/app.js
// Build the result container and bind to DOM elements.
var resultsContainer = new gapi.cloudsearch.widget.resultscontainer.Builder()
  .setAdapter(new ResultsContainerAdapter())
  // ...
  .build();

// Build the search box and bind to DOM elements.
var searchBox = new gapi.cloudsearch.widget.searchbox.Builder()
  .setAdapter(new SearchBoxAdapter())
  // ...
  .build();

Dekoratory mogą modyfikować atrybuty elementu kontenera, a także wszelkie elementy podrzędne. Podczas dekorowania można dodawać i usuwać elementy podrzędne. Jeśli jednak wprowadzasz zmiany strukturalne elementów, rozważ tworzenie elementów bezpośrednio, a nie dekorowanie.

Tworzenie elementów niestandardowych za pomocą adaptera

Aby utworzyć element niestandardowy dla sugestii, kontenera aspektu lub wyniku wyszukiwania, utwórz i zarejestruj adapter, który implementuje createSuggestionElement, createFacetResultElement lub createSearchResultElement w sposób reprezentatywny.

Poniższe adaptery pokazują tworzenie niestandardowych elementów sugestii i wyników wyszukiwania za pomocą tagów HTML <template>.

serving/widget/public/with_custom_element/app.js
/**
 * Search box adapter that overrides creation of suggestion elements.
 */
function SearchBoxAdapter() {}
SearchBoxAdapter.prototype.createSuggestionElement = function(suggestion) {
  let template = document.querySelector('#suggestion_template');
  let fragment = document.importNode(template.content, true);
  fragment.querySelector('.suggested_query').textContent = suggestion.suggestedQuery;
  return fragment.firstElementChild;
}

/**
 * Results container adapter that overrides creation of result elements.
 */
function ResultsContainerAdapter() {}
ResultsContainerAdapter.prototype.createSearchResultElement = function(result) {
  let template = document.querySelector('#result_template');
  let fragment = document.importNode(template.content, true);
  fragment.querySelector('.title').textContent = result.title;
  fragment.querySelector('.title').href = result.url;
  let snippetText = result.snippet != null ?
    result.snippet.snippet : '';
  fragment.querySelector('.query_snippet').innerHTML = snippetText;
  return fragment.firstElementChild;
}

Aby zarejestrować adapter podczas inicjowania widżetu, użyj metody setAdapter() odpowiedniej klasy Builder:

serving/widget/public/with_custom_element/app.js
// Build the result container and bind to DOM elements.
var resultsContainer = new gapi.cloudsearch.widget.resultscontainer.Builder()
  .setAdapter(new ResultsContainerAdapter())
  // ...
  .build();

// Build the search box and bind to DOM elements.
var searchBox = new gapi.cloudsearch.widget.searchbox.Builder()
  .setAdapter(new SearchBoxAdapter())
  // ...
  .build();

Tworzenie elementów aspektów niestandardowych za pomocą createFacetResultElement podlega kilku ograniczeniom:

  • Musisz dołączyć klasę CSS cloudsearch_facet_bucket_clickable do elementu, który użytkownicy klikają, aby przełączyć zasobnik.
  • Każdy zasobnik musisz opakować element zawierającym za pomocą klasy CSS cloudsearch_facet_bucket_container.
  • Nie możesz wyrenderować zasobników w innej kolejności niż występują w odpowiedzi.

Na przykład ten fragment kodu renderuje aspekty za pomocą linków, a nie pól wyboru.

serving/widget/public/with_custom_facet/app.js
/**
 * Results container adapter that intercepts requests to dynamically
 * change which sources are enabled based on user selection.
 */
function ResultsContainerAdapter() {
  this.selectedSource = null;
}

ResultsContainerAdapter.prototype.createFacetResultElement = function(result) {
  // container for the facet
  var container = document.createElement('div');

  // Add a label describing the facet (operator/property)
  var label = document.createElement('div')
  label.classList.add('facet_label');
  label.textContent = result.operatorName;
  container.appendChild(label);

  // Add each bucket
  for(var i in result.buckets) {
    var bucket = document.createElement('div');
    bucket.classList.add('cloudsearch_facet_bucket_container');

    // Extract & render value from structured value
    // Note: implementation of renderValue() not shown
    var bucketValue = this.renderValue(result.buckets[i].value)
    var link = document.createElement('a');
    link.classList.add('cloudsearch_facet_bucket_clickable');
    link.textContent = bucketValue;
    bucket.appendChild(link);
    container.appendChild(bucket);
  }
  return container;
}

// Renders a value for user display
ResultsContainerAdapter.prototype.renderValue = function(value) {
  // ...
}

Dostosowywanie działania wyszukiwania

Ustawienia wyszukiwarki reprezentują domyślną konfigurację interfejsu wyszukiwania i są statyczne. Aby wdrożyć filtry lub aspekty dynamiczne, na przykład umożliwić użytkownikom przełączanie źródeł danych, możesz zastąpić ustawienia wyszukiwarki, przechwytując żądanie wyszukiwania za pomocą adaptera.

Zaimplementuj adapter z metodą interceptSearchRequest, aby modyfikować żądania przesyłane do interfejsu Search API przed wykonaniem tej czynności.

Na przykład ten adapter przechwytuje żądania, aby ograniczyć zapytania do źródła wybranego przez użytkownika:

serving/widget/public/with_request_interceptor/app.js
/**
 * Results container adapter that intercepts requests to dynamically
 * change which sources are enabled based on user selection.
 */
function ResultsContainerAdapter() {
  this.selectedSource = null;
}
ResultsContainerAdapter.prototype.interceptSearchRequest = function(request) {
  if (!this.selectedSource || this.selectedSource == 'ALL') {
    // Everything selected, fall back to sources defined in the search
    // application.
    request.dataSourceRestrictions = null;
  } else {
    // Restrict to a single selected source.
    request.dataSourceRestrictions = [
      {
        source: {
          predefinedSource: this.selectedSource
        }
      }
    ];
  }
  return request;
}

Aby zarejestrować adapter podczas inicjowania widżetu, użyj metody setAdapter() podczas tworzenia komponentu ResultsContainer

serving/widget/public/with_request_interceptor/app.js
var resultsContainerAdapter = new ResultsContainerAdapter();
// Build the result container and bind to DOM elements.
var resultsContainer = new gapi.cloudsearch.widget.resultscontainer.Builder()
  .setAdapter(resultsContainerAdapter)
  // ...
  .build();

Poniższy kod HTML służy do wyświetlania pola wyboru umożliwiającego filtrowanie według źródeł:

serving/widget/public/with_request_interceptor/index.html
<div>
  <span>Source</span>
  <select id="sources">
    <option value="ALL">All</option>
    <option value="GOOGLE_GMAIL">Gmail</option>
    <option value="GOOGLE_DRIVE">Drive</option>
    <option value="GOOGLE_SITES">Sites</option>
    <option value="GOOGLE_GROUPS">Groups</option>
    <option value="GOOGLE_CALENDAR">Calendar</option>
    <option value="GOOGLE_KEEP">Keep</option>
  </select>
</div>

Poniższy kod nasłuchuje zmiany, ustawia wybór i w razie potrzeby ponownie wykonuje zapytanie.

serving/widget/public/with_request_interceptor/app.js
// Handle source selection
document.getElementById('sources').onchange = (e) => {
  resultsContainerAdapter.selectedSource = e.target.value;
  let request = resultsContainer.getCurrentRequest();
  if (request.query) {
    // Re-execute if there's a valid query. The source selection
    // will be applied in the interceptor.
    resultsContainer.resetState();
    resultsContainer.executeRequest(request);
  }
}

Możesz też przechwytywać odpowiedź wyszukiwania, implementując w adapterze obiekt interceptSearchResponse.

Przypnij wersję interfejsu API

Domyślnie widżet używa najnowszej stabilnej wersji interfejsu API. Aby zablokować konkretną wersję, przed zainicjowaniem widżetu ustaw parametr konfiguracji cloudsearch.config/apiVersion na preferowaną wersję.

serving/widget/public/basic/app.js
gapi.config.update('cloudsearch.config/apiVersion', 'v1');

Jeśli zasada jest nieskonfigurowana lub ustawiona na nieprawidłową wartość, wersja interfejsu API przyjmuje domyślnie wartość 1.0.

Przypnij wersję widżetu

Aby uniknąć nieoczekiwanych zmian w interfejsach wyszukiwania, ustaw parametr konfiguracji cloudsearch.config/clientVersion w następujący sposób:

gapi.config.update('cloudsearch.config/clientVersion', 1.1);

Jeśli nie jest skonfigurowana lub zostanie ustawiona na nieprawidłową wartość, wersja widżetu będzie domyślnie ustawiona na 1,0.

Zabezpieczanie interfejsu wyszukiwania

Wyniki wyszukiwania zawierają informacje poufne. Postępuj zgodnie ze sprawdzonymi metodami zabezpieczania aplikacji internetowych, w szczególności przed atakami typu clickjacking.

Więcej informacji znajdziesz w przewodniku OWASP Guide (w języku angielskim).

Włącz debugowanie

Użyj narzędzia interceptSearchRequest, aby włączyć debugowanie widżetu wyszukiwania. Na przykład:

  if (!request.requestOptions) {
  // Make sure requestOptions is populated
  request.requestOptions = {};
  }
  // Enable debugging
  request.requestOptions.debugOptions = {enableDebugging: true}

  return request;