Korzystanie z Google Base i Google Gears w celu zapewnienia wydajności i działania w trybie offline

Pierwszy artykuł z serii „Tworzenie lepszych aplikacji Ajax za pomocą interfejsów API Google”.

Dion Almaer i Pamela Fox, Google
Czerwiec 2007 r.

Uwaga redakcji: interfejs Google Gears API nie jest już dostępny.

Wprowadzenie

Łącząc Google Base z Google Gears, pokazujemy, jak utworzyć aplikację, której można używać w trybie offline. Po przeczytaniu tego artykułu dowiesz się więcej o interfejsie Google Base API oraz o tym, jak używać Google Gears do przechowywania preferencji i danych użytkownika oraz uzyskiwania do nich dostępu.

Poznawanie aplikacji

Aby zrozumieć tę aplikację, musisz najpierw zapoznać się z Google Base, czyli dużą bazą danych zawierającą elementy z różnych kategorii, takich jak produkty, opinie, przepisy, wydarzenia i inne.

Każdy element jest opatrzony tytułem, opisem, linkiem do oryginalnego źródła danych (jeśli istnieje) oraz dodatkowymi atrybutami, które różnią się w zależności od typu kategorii. Google Base wykorzystuje fakt, że produkty w tej samej kategorii mają wspólny zestaw atrybutów – na przykład wszystkie przepisy mają składniki. Produkty z Google Base mogą się nawet czasami pojawiać w wynikach wyszukiwania w wyszukiwarce Google lub w wyszukiwarce produktów Google.

Nasza aplikacja demonstracyjna Base with Gears umożliwia przechowywanie i wyświetlanie typowych wyszukiwań, które możesz przeprowadzać w Google Base, np. wyszukiwanie przepisów z hasłem „czekolada” (pyszne!) lub ogłoszeń osobistych z hasłem „spacery po plaży” (romantyczne!). Można ją traktować jako „czytnik Google Base”, który umożliwia subskrybowanie wyszukiwań i wyświetlanie zaktualizowanych wyników po ponownym otwarciu aplikacji lub gdy aplikacja co 15 minut wyszukuje zaktualizowane kanały.

Deweloperzy, którzy chcą rozszerzyć aplikację, mogą dodać więcej funkcji, np. wizualne powiadamianie użytkownika o nowych wynikach wyszukiwania, możliwość dodawania ulubionych elementów do zakładek (offline i online) oraz wyszukiwanie atrybutów w określonych kategoriach, np. w Google Base.

Korzystanie z plików danych interfejsu Google Base Data API

Google Base można wysyłać zapytania programowo za pomocą interfejsu Google Base Data API, który jest zgodny z platformą Google Data API. Protokół Google Data API to prosty protokół do odczytywania i zapisywania danych w internecie, który jest używany w wielu usługach Google, takich jak Picasa, Arkusze, Blogger, Kalendarz, Notatnik i inne.

Format Google Data API jest oparty na XML i protokole publikowania Atom, więc większość interakcji odczytu i zapisu odbywa się w XML.

Przykład pliku danych Google Base opartego na interfejsie Google Data API:
http://www.google.com/base/feeds/snippets/-/products?bq=digital+camera

snippets Typ pliku danych udostępnia nam publicznie dostępny plik danych o produktach. Symbol -/products pozwala nam ograniczyć plik danych do kategorii produktów. Parametr bq= pozwala nam dodatkowo ograniczyć plik danych tylko do wyników zawierających słowo kluczowe „aparat cyfrowy”. Jeśli wyświetlisz ten plik danych w przeglądarce, zobaczysz kod XML zawierający węzły <entry> z pasującymi wynikami. Każdy wpis zawiera typowe elementy: autora, tytuł, treść i link, ale także dodatkowe atrybuty specyficzne dla kategorii (np. „cena” w przypadku produktów).

Ze względu na ograniczenie dotyczące żądań XMLHttpRequest w przeglądarce nie możemy bezpośrednio odczytać pliku danych XML z www.google.com w naszym kodzie JavaScript. Możemy skonfigurować serwer proxy, aby odczytywał kod XML i zwracał go w lokalizacji w tej samej domenie co nasza aplikacja, ale chcemy całkowicie uniknąć programowania po stronie serwera. Na szczęście istnieje alternatywa.

Podobnie jak inne interfejsy Google Data API, interfejs Google Base Data API ma opcję danych wyjściowych w formacie JSON, oprócz standardowego formatu XML. Dane wyjściowe pliku danych, który widzieliśmy wcześniej, w formacie JSON znajdziesz pod tym adresem URL:
http://www.google.com/base/feeds/snippets/-/products?bq=digital+camera&alt=json

JSON to lekki format wymiany danych, który umożliwia hierarchiczne zagnieżdżanie oraz różne typy danych. Co ważniejsze, dane wyjściowe JSON to natywny kod JavaScript, więc można go wczytać na stronę internetową, po prostu odwołując się do niego w tagu skryptu, co pozwala ominąć ograniczenie dotyczące wielu domen.

Interfejsy Google Data API umożliwiają też określenie danych wyjściowych „json-in-script” z funkcją wywołania zwrotnego, która ma zostać wykonana po wczytaniu kodu JSON. Dzięki temu praca z danymi wyjściowymi JSON jest jeszcze łatwiejsza, ponieważ możemy dynamicznie dołączać tagi skryptu do strony i określać dla każdego z nich różne funkcje wywołania zwrotnego.

Aby dynamicznie wczytać do strony plik JSON interfejsu Base API, możemy użyć tej funkcji, która tworzy tag skryptu z adresem URL pliku (z wartościami altcallback) i dodaje go do strony.

function getJSON() {
  var script = document.createElement('script');

  var url = "http://www.google.com/base/feeds/snippets/-/products?bq=digital+camera";
  script.setAttribute('src', url + "&alt=json-in-script&callback=listResults");
  script.setAttribute('type', 'text/JavaScript');
  document.documentElement.firstChild.appendChild(script);
}

Nasza funkcja wywołania zwrotnego listResults może teraz iterować po pliku JSON przekazanym jako jedyny parametr i wyświetlać informacje o każdym znalezionym wpisie na liście punktowanej.

  function listTasks(root) {
    var feed = root.feed;
    var html = [''];
    html.push('<ul>');
    for (var i = 0; i < feed.entry.length; ++i) {
      var entry = feed.entry[i];
      var title = entry.title.$t;
      var content = entry.content.$t;
      html.push('<li>', title, ' (', content, ')</li>');
    }
    html.push('</ul>');

    document.getElementById("agenda").innerHTML = html.join("");
  }

Dodawanie Google Gears

Teraz, gdy mamy aplikację, która może komunikować się z Google Base za pomocą interfejsu Google Data API, chcemy umożliwić jej działanie w trybie offline. Właśnie tu do gry wchodzi Google Gears.

Podczas pisania aplikacji, która może działać w trybie offline, masz do wyboru różne architektury. Zadawaj sobie pytania o to, jak aplikacja powinna działać online i offline (np. czy działa dokładnie tak samo? Czy niektóre funkcje, np. wyszukiwanie, są wyłączone? Jak będzie wyglądać synchronizacja?)

W naszym przypadku chcieliśmy mieć pewność, że użytkownicy przeglądarek bez Gears będą mogli nadal korzystać z aplikacji, a użytkownicy, którzy mają wtyczkę, będą mogli korzystać z niej w trybie offline i z bardziej responsywnego interfejsu.

Nasza architektura wygląda tak:

  • Mamy obiekt JavaScript, który przechowuje Twoje zapytania i zwraca wyniki wyszukiwania.
  • Jeśli masz zainstalowany Google Gears, otrzymasz wersję Gears, która przechowuje wszystko w lokalnej bazie danych.
  • Jeśli nie masz zainstalowanego Google Gears, otrzymasz wersję, która przechowuje zapytania w pliku cookie i w ogóle nie przechowuje pełnych wyników (stąd nieco wolniejsza reakcja), ponieważ wyniki są zbyt duże, aby można je było przechowywać w pliku cookie.
Zaletą tej architektury jest to, że nie musisz przeprowadzać weryfikacji if (online) {} w całym sklepie. Zamiast tego aplikacja ma jeden test Gears, a następnie używany jest odpowiedni adapter.


Korzystanie z lokalnej bazy danych Gears

Jednym z komponentów Gears jest lokalna baza danych SQLite, która jest wbudowana i gotowa do użycia. Jest to prosty interfejs API bazy danych, który będzie Ci znany, jeśli wcześniej korzystałeś(-aś) z interfejsów API baz danych po stronie serwera, takich jak MySQL czy Oracle.

Korzystanie z lokalnej bazy danych jest bardzo proste:

  • Inicjowanie obiektów Google Gears
  • Pobieranie obiektu fabryki bazy danych i otwieranie bazy danych
  • Rozpocznij wykonywanie żądań SQL

Przyjrzyjmy się im pokrótce.


Inicjowanie obiektów Google Gears

Aplikacja powinna odczytywać zawartość /gears/samples/gears_init.js bezpośrednio lub przez wklejenie kodu do własnego pliku JavaScript. Gdy <script src="..../gears_init.js" type="text/JavaScript"></script> działa, masz dostęp do przestrzeni nazw google.gears.


Pobieranie obiektu fabryki baz danych i otwieranie bazy danych
var db = google.gears.factory.create('beta.database', '1.0');
db.open('testdb');

To jedno wywołanie zwróci obiekt bazy danych, który umożliwia otwarcie schematu bazy danych. Gdy otwierasz bazy danych, są one objęte zakresem tych samych reguł zasad dotyczących tego samego pochodzenia, więc Twoja baza „testdb” nie będzie kolidować z moją bazą „testdb”.


Rozpocznij wykonywanie żądań SQL

Teraz możemy wysyłać do bazy danych żądania SQL. Gdy wysyłamy żądania „select”, otrzymujemy zbiór wyników, po którym możemy iterować w celu uzyskania żądanych danych:

var rs = db.execute('select * from foo where name = ?', [ name ]);

Na zwróconym zbiorze wyników możesz wykonywać te działania:

booleanisValidRow()
voidnext()
voidclose()
intfieldCount()
stringfieldName(int fieldIndex)
variantfield(int fieldIndex)
variantfieldByName(string fieldname)

Więcej informacji znajdziesz w dokumentacji interfejsu API modułu bazy danych. (Uwaga redakcyjna: interfejs Google Gears API nie jest już dostępny).


Używanie GearsDB do hermetyzacji interfejsu API niskiego poziomu

Chcieliśmy uprościć i ułatwić wykonywanie niektórych typowych zadań związanych z bazami danych. Na przykład

  • Chcieliśmy mieć wygodny sposób rejestrowania kodu SQL generowanego podczas debugowania aplikacji.
  • Chcieliśmy obsługiwać wyjątki w jednym miejscu, zamiast używać try{}catch(){} w wielu miejscach.
  • Podczas odczytywania lub zapisywania danych chcieliśmy mieć do czynienia z obiektami JavaScript zamiast z zestawami wyników.

Aby rozwiązać te problemy w ogólny sposób, utworzyliśmy GearsDB, bibliotekę open source, która opakowuje obiekt bazy danych. Teraz pokażemy, jak korzystać z GearsDB.

Konfiguracja początkowa

W kodzie window.onload musimy się upewnić, że tabele bazy danych, na których polegamy, są na swoim miejscu. Jeśli użytkownik ma zainstalowany Gears, gdy uruchomi się ten kod, utworzy obiekt GearsBaseContent:

content = hasGears() ? new GearsBaseContent() : new CookieBaseContent();

Następnie otwieramy bazę danych i tworzymy tabele, jeśli jeszcze nie istnieją:

db = new GearsDB('gears-base'); // db is defined as a global for reuse later!

if (db) {
  db.run('create table if not exists BaseQueries' +
         ' (Phrase varchar(255), Itemtype varchar(100))');
  db.run('create table if not exists BaseFeeds' + 
         ' (id varchar(255), JSON text)');
}

Mamy już pewność, że mamy tabelę do przechowywania zapytań i plików danych. Kod new GearsDB(name) będzie zawierać otwarcie bazy danych o podanej nazwie. Metoda run jest opakowaniem metody niższego poziomu execute, ale obsługuje też dane wyjściowe debugowania w konsoli i wyjątki.


Dodawanie wyszukiwanego hasła

Gdy uruchomisz aplikację po raz pierwszy, nie będzie w niej żadnych wyszukiwań. Jeśli spróbujesz wyszukać Nintendo Wii w produktach, zapiszemy to wyszukiwane hasło w tabeli BaseQueries.

Wersja Gears metody addQuery robi to, pobierając dane wejściowe i zapisując je za pomocą insertRow:

var searchterm = { Phrase: phrase, Itemtype: itemtype };
db.insertRow('BaseQueries', searchterm); 

insertRow przyjmuje obiekt JavaScriptu (searchterm) i wstawia go do tabeli. Umożliwia też określanie ograniczeń (np. unikalności – blokowanie wstawiania więcej niż jednego elementu „Bob”). Większość tych ograniczeń będziesz jednak obsługiwać w samej bazie danych.


Pobieranie wszystkich wyszukiwanych haseł

Aby wypełnić listę poprzednich wyszukiwań, używamy ładnego selektora o nazwie selectAll:

GearsBaseContent.prototype.getQueries = function() {
  return this.db.selectAll('select * from BaseQueries');
}

Zwróci to tablicę obiektów JavaScriptu, które pasują do wierszy w bazie danych (np. [ { Phrase: 'Nintendo Wii', Itemtype: 'product' }, { ... }, ...]).

W takim przypadku możesz zwrócić pełną listę. Jeśli jednak masz dużo danych, prawdopodobnie będziesz chcieć użyć wywołania zwrotnego w wywołaniu select, aby móc wykonywać operacje na każdym zwróconym wierszu w miarę jego pojawiania się:

 db.selectAll('select * from BaseQueries where Itemtype = ?', ['product'], function(row) {
  ... do something with this row ...
});

Oto inne przydatne metody wyboru w GearsDB:

selectOne(sql, args)Zwróć pierwszy/jeden pasujący obiekt JavaScript
selectRow(table, where, args, select)Zwykle używane w prostych przypadkach do ignorowania SQL.
selectRows(table, where, args, callback, select)Podobnie jak selectRow, ale w przypadku wielu wyników.

Wczytywanie pliku danych

Gdy otrzymamy plik danych z Google Base, musimy zapisać go w bazie danych:

content.setFeed({ id: id, JSON: json.toJSONString() });

... which calls ...

GearsBaseContent.prototype.setFeed = function(feed) {
  this.db.forceRow('BaseFeeds', feed);
}

Najpierw pobieramy plik danych JSON i zwracamy go jako ciąg znaków za pomocą metody toJSONString. Następnie tworzymy obiekt feed i przekazujemy go do metody forceRow. forceRow WSTAWIA wpis, jeśli jeszcze nie istnieje, lub AKTUALIZUJE istniejący wpis.


Wyświetlanie wyników wyszukiwania

Nasza aplikacja wyświetla wyniki danego wyszukiwania w panelu po prawej stronie. Oto jak pobieramy kanał powiązany z wyszukiwanym hasłem:

GearsBaseContent.prototype.getFeed = function(url) {
  var row = this.db.selectRow('BaseFeeds', 'id = ?', [ url ]);
  return row.JSON;
}

Teraz, gdy mamy JSON wiersza, możemy go eval(), aby odzyskać obiekty:

eval("var json = " + jsonString + ";");

Możemy już zacząć wstawiać treści z pliku JSON do naszej strony za pomocą metody innerHTML.


Korzystanie z magazynu zasobów w celu uzyskania dostępu offline

Treści pochodzą z lokalnej bazy danych, więc ta aplikacja powinna działać też offline, prawda?

Nie. Problem polega na tym, że aby uruchomić tę aplikację, musisz wczytać jej zasoby internetowe, takie jak JavaScript, CSS, HTML i obrazy. Obecnie, jeśli użytkownik wykona te czynności, aplikacja może nadal działać: uruchomi online, przeprowadzi kilka wyszukiwań, nie zamknie przeglądarki i przejdzie w tryb offline. Może to zadziałać, ponieważ elementy będą nadal znajdować się w pamięci podręcznej przeglądarki. A co, jeśli tak nie jest? Chcemy, aby użytkownicy mogli uzyskać dostęp do aplikacji od zera, po ponownym uruchomieniu itp.

W tym celu używamy komponentu LocalServer i przechwytujemy zasoby. Gdy przechwycisz zasób (np. kod HTML i JavaScript wymagany do uruchomienia aplikacji), Gears zapisze te elementy i będzie też przechwytywać żądania przeglądarki dotyczące ich zwrócenia. Serwer lokalny będzie działać jak kontroler ruchu i zwracać zapisane treści ze sklepu.

Korzystamy też z komponentu ResourceStore, który wymaga ręcznego wskazania systemowi, które pliki chcesz przechwycić. W wielu przypadkach warto wersjonować aplikację i umożliwiać uaktualnienia w sposób transakcyjny. Zestaw zasobów definiuje wersję. Gdy udostępnisz nowy zestaw zasobów, użytkownicy powinni mieć możliwość bezproblemowego uaktualnienia plików. Jeśli to Twój model, będziesz używać interfejsu ManagedResourceStore API.

Aby przechwycić nasze zasoby, obiekt GearsBaseContent:

  1. Skonfiguruj tablicę plików, które wymagają przechwycenia.
  2. Tworzenie serwera lokalnego
  3. Otwieranie lub tworzenie nowego elementu ResourceStore
  4. Wywołanie w celu przechwycenia stron w sklepie
// Step 1
this.storeName = 'gears-base';
this.pageFiles = [
  location.pathname,
  'gears_base.js',
  '../scripts/gears_db.js',
  '../scripts/firebug/firebug.js',
  '../scripts/firebug/firebug.html',
  '../scripts/firebug/firebug.css',
  '../scripts/json_util.js',    'style.css',
  'capture.gif' ];

// Step 2
try {
  this.localServer = google.gears.factory.create('beta.localserver', '1.0');
} catch (e) {
  alert('Could not create local server: ' + e.message);
  return;
}

// Step 3
this.store = this.localServer.openStore(this.storeName) || this.localServer.createStore(this.storeName);

// Step 4
this.capturePageFiles();

... which calls ...

GearsBaseContent.prototype.capturePageFiles = function() {
  this.store.capture(this.pageFiles, function(url, success, captureId) {
    console.log(url + ' capture ' + (success ? 'succeeded' : 'failed'));
  });
}

Ważne jest to, że możesz przechwytywać tylko zasoby w swojej domenie. Natrafiliśmy na to ograniczenie, gdy próbowaliśmy uzyskać dostęp do pliku JavaScript GearsDB bezpośrednio z oryginalnego pliku „gears_db.js” w jego gałęzi SVN. Rozwiązanie jest oczywiście proste: musisz pobrać wszystkie zasoby zewnętrzne i umieścić je w swojej domenie. Pamiętaj, że przekierowania 302 lub 301 nie będą działać, ponieważ LocalServer akceptuje tylko kody serwera 200 (Success) lub 304 (Not Modified).

Ma to pewne konsekwencje. Jeśli umieścisz obrazy w domenie images.yourdomain.com, nie będziesz w stanie ich przechwycić. Domeny www1 i www2 nie widzą się nawzajem. Możesz skonfigurować serwery proxy po stronie serwera, ale to zniweczyłoby cel podziału aplikacji na wiele domen.

Debugowanie aplikacji offline

Debugowanie aplikacji offline jest nieco bardziej skomplikowane. Obecnie możesz testować więcej scenariuszy:

  • Jestem online, a aplikacja działa w pełni w pamięci podręcznej
  • Jestem online, ale nie mam dostępu do aplikacji i niczego w pamięci podręcznej.
  • Jestem offline, ale mam dostęp do aplikacji
  • Jestem offline i nigdy nie miałem(-am) dostępu do aplikacji (to nie jest dobra sytuacja).

Aby ułatwić sobie pracę, zastosowaliśmy ten wzór:

  • Wyłączamy pamięć podręczną w Firefoxie (lub w wybranej przez Ciebie przeglądarce), gdy musimy mieć pewność, że przeglądarka nie pobiera informacji z pamięci podręcznej.
  • Do debugowania używamy Firebuga (i Firebuga Lite do testowania w innych przeglądarkach). W wielu miejscach używamy console.log() i wykrywamy konsolę na wszelki wypadek.
  • Dodajemy pomocniczy kod JavaScript, aby:
    • umożliwić nam wyczyszczenie bazy danych i rozpoczęcie od nowa.
    • usuwać przechwycone pliki, aby po ponownym wczytaniu były pobierane z internetu (przydatne podczas iteracyjnego procesu tworzenia);

Widżet debugowania pojawia się po lewej stronie tylko wtedy, gdy masz zainstalowany Gears. Zawiera wywołania do oczyszczania kodu:

GearsBaseContent.prototype.clearServer = function() {
  if (this.localServer.openStore(this.storeName)) {
    this.localServer.removeStore(this.storeName);
    this.store = null;
  }
}

GearsBaseContent.prototype.clearTables = function() {
  if (this.db) {
    this.db.run('delete from BaseQueries');
    this.db.run('delete from BaseFeeds');
  }
  displayQueries();
}

Podsumowanie

Jak widać, praca z Google Gears jest dość prosta. Użyliśmy GearsDB, aby jeszcze bardziej uprościć komponent bazy danych, a także ręcznego ResourceStore, który w naszym przykładzie sprawdził się doskonale.

Obszar, w którym spędzasz najwięcej czasu, to określanie strategii dotyczącej tego, kiedy pobierać dane online i jak przechowywać je offline. Warto poświęcić czas na zdefiniowanie schematu bazy danych. Jeśli w przyszłości zajdzie potrzeba zmiany schematu, musisz zarządzać tą zmianą, ponieważ obecni użytkownicy będą już mieli wersję bazy danych. Oznacza to, że przy każdej aktualizacji bazy danych musisz dostarczyć kod skryptu. Oczywiście warto to zminimalizować. Możesz wypróbować GearShift, małą bibliotekę, która może pomóc w zarządzaniu zmianami.

Do śledzenia plików moglibyśmy też użyć klasy ManagedResourceStore, co miałoby następujące konsekwencje:

  • Będziemy postępować zgodnie z zasadami i wersjonować pliki, aby umożliwić bezproblemowe przyszłe aktualizacje.
  • W usłudze ManagedResourceStore jest dostępna funkcja, która umożliwia przypisanie aliasu do adresu URL innej treści. Prawidłowym wyborem architektury byłoby utworzenie wersji gears_base.js bez Gears i utworzenie aliasu, aby Gears pobierał plik gears_base_withgears.js, który zawierałby wszystkie funkcje obsługi offline.
W przypadku naszej aplikacji uznaliśmy, że łatwiej będzie mieć jeden interfejs i wdrożyć go na 2 sposoby.

Mamy nadzieję, że przygotowywanie aplikacji było dla Ciebie przyjemne i łatwe. Jeśli masz pytania lub chcesz udostępnić aplikację, dołącz do nas na forum Google Gears.