In this codelab, you'll start with a web application that displays the local weather, and use the <platinum-sw> Polymer elements to make the web application work even when there isn't an Internet connection. This type of "offline-first" web application is great for users who are frequently in locations without a network, and loading your web application's resources from a local cache also means that it will start up faster than it otherwise would if it had to be fetched via the network.

What you'll learn

What you'll need

How will you use this tutorial?

Read it through only Read it and complete the exercises

What is your current level of experience working with Polymer?

Novice Intermediate Proficient

While you're free to use your own editor, there are a number of instructions in this code lab that are specific to the Chrome Dev Editor, a Chrome app IDE. If you don't have it installed yet, you can install it from Chrome Web Store.

Download Chrome Dev Editor

Use the Chrome Dev Editor's "Git Clone..." command to get a copy of the source code from it's repository, https://github.com/googlecodelabs/polymer-offline-weather.git

The repository contains directories corresponding to the web application at various stages of functionality, with the completed version in the final/ directory, and the various in-progress versions in the step1/, etc. directories. There is also a common/ directory which contains helper resources used at every stage.

First, let's see what the final version of the sample web app looks like. With the code imported into the Chrome Dev Editor, select the final/ directory and hit the button in the top toolbar. Chrome Dev Editor fires up a web server and navigates to the final/index.html page.

Alternatively, a live version of the same code is deployed at https://googlecodelabs.github.io/polymer-offline-weather/final/index.html

By default, you'll see the current weather for Mountain View, CA:

The button to will use the Geolocation API to determine your device's current location, and show you weather for there instead.

Inspecting network traffic

Thanks to the service worker controlling the sample web app, it will be available even if the web page itself would otherwise be unavailable. You can test this out by either quitting the Chrome Dev Editor or stopping your alternative local web server, and then reloading the page.

If you'd like to confirm that the service worker is intercepting requests, you can visit the Network panel of Chrome's Developer Tools. Requests originating from the page that the service worker responds to will be attributed (from ServiceWorker) in the Size column. Requests that originate via fetch() calls from within the service worker will be attributed (from cache) or with an actual size in the Size column, and will have a icon in the Name column.

Starting fresh

During the course of this codelab, and when developing with service workers in general, you can take advantage of Chrome's incognito mode to view up a particular page without any involvement from a previously installed service worker. Incognito mode windows start fresh without any service workers installed when they're opened, and they will clear out any service workers that do get installed when they're closed. Note that you will need to close the entire incognito window to start fresh, including all tabs.

Now that you've tried out the final version of the weather web app, including offline support via a service worker, let's backtrack and build it ourselves. In this section, we'll provide an overview of the code in the step1/ directory, which contains a functional weather web app, though one that won't work offline.

Web Components/Polymer concepts

Let's go over some Web Components and Polymer concepts you'll need for this codelab. Here's links to those concepts followed by representative examples from the code found in step1/index.html:

<link rel="import" href="../common/elements.html">
<body class="fit vertical layout center-center">
<template is="dom-bind" id="t">
<template is="dom-if" if="[[coordinates]]">
Weather for <span>[[_calculateLocationString(weatherResponse.query.results.channel.location)]]</span>
<iron-ajax id="weather-ajax"
           auto
           loading="{{activeRequest}}"
           url="[[weatherApiUrl]]"
           params="[[_calculateWeatherParams(coordinates.latitude, coordinates.longitude)]]"
           handle-as="json"
           last-response="{{weatherResponse}}">
</iron-ajax>

If you're familiar with all of those concepts and snippets, you should have a good idea as to what's going on in the step1/ code.

Web app structure

Let's focus for a minute on the structure of the web app, and how it relates to the resources it needs in order to function. From the start, we're using a common pattern that will lend itself well to adding offline support later.

The core of our web app, everything needed to display the basic "shell", consists of its HTML, CSS, and JavaScript. Some of this shell is provided inline in the index.html file, and other parts of the shell are loaded from external web component definitions, via HTML Imports. But together, they're all that's needed to show a basic, static web site. It's crucial to keep the shell of your web app lean, to ensure that some initial static structural content can be displayed as soon as the web app is opened, regardless of whether there's a network available or not.

Of course, a static page that always showed the same values isn't what users expect from a weather web app. The app needs to fetch data specific to the current user's state. In this case, that data is the current weather for their location. This dynamic data, fetched via the Yahoo! Weather API, is logically separate from the app's shell.

When it comes time to think about offline support, structuring our app so that there's a clear distinction between the "shell" and the dynamic or state-specific resources will come in handy. But before we get that far, let's take a detour to cover something crucial to the offline experience: service workers!

Service workers! We've mentioned them throughout this codelab, but so far, we haven't said what they're all about, and what they have to do with working offline. Before we go any further, let's fix that.

At its most basic, you can think of a service worker as a "background thread" for the web. It's JavaScript code that runs outside of the context of your web pages, and responds to events, including network requests made from web pages under its control.

A service worker has a deliberately short lifetime. It only runs as long as it's needed to service a an event, , and revived the next time it needs to handle an event.

Service workers also have a limited set of APIs compared to JavaScript code run from the context of a web page. Service workers can't access the DOM, for instance. But service workers do have access to the Cache Storage API, and they can make network requests using the Fetch API. The IndexedDB API and postMessage() are also available to use for data persistence and messaging between the service worker and pages it controls.

A service worker can intercept network requests made from an web page (which triggers a fetch event on the service worker) and return a response retrieved from the network, or retrieved from a local cache, or even constructed programmatically! The neat part is that, regardless of where the response comes from, once the service worker returns it to the web page, the response looks just like it would if there had been no service worker involvement. Nothing has to change from the perspective of your web page, even if the responses are coming from a local cache because your browser is offline.

Now that some of the theory behind service workers is out of the way, let's see how we can use the <platinum-sw> elements to register a service worker for our web app, and implement a basic caching strategy that will allow our app to function offline. We'll be looking through the code in the step2/ directory.

The major change between step1/ and step2/ was the addition of the following elements:

index.html

<platinum-sw-register skip-waiting
                      clients-claim
                      auto-register
                      reload-on-install>
  <platinum-sw-cache default-cache-strategy="fastest"></platinum-sw-cache>
</platinum-sw-register>

There's a lot going on here, so let's dive into the new elements and the configuration attributes:

&lt;platinum-sw-register>

This is a parent element that handles registering the service worker to control the page, based on the configuration provided via its <platinum-sw-*> children. Under the hood, the sw-import.js file, located in the same directory as our index.html, contains the boilerplate code for our service worker's implementation.

We use the following attributes to configure the registration behavior:

&lt;platinum-sw-cache>

This element needs to be a child of <platinum-sw-register>, and is used to configure the caching behavior. Under the hood, <platinum-sw-cache> makes use of the sw-toolbox library, and that project's documentation goes into detail about the specific behavior.

In this case, we're making use of a cache strategy called fastest, which will cause our service worker to intercept all requests and return either a cached response or a network response, whichever comes back first. While it will almost always be the case that the cached response comes back first, the benefit of simultaneously making a network request is that the cache will be automatically updated with the newest response, resulting in a cache that is kept relatively current.

The following diagram illustrates this caching strategy, with the assumption that the cached version is returned to the page and the network response updates the cache:

The corresponding section of The Offline Cookbook explains the theory further.

Remember how our web app has a "shell," composed of its static HTML/JavaScript/CSS, distinct from the requests for the current weather? We're going to take advantage of that structure now and implement different caching strategies for the "shell" and the dynamic requests.

By virtue of the <platinum-sw-cache default-cache-strategy="fastest">, added in the previous step, we have caching that applies to all of our resource requests by default, but this caching relies on the URLs for the resources staying the same. For our static resources, that's not a problem—our HTML file will always be called index.html. But our web app makes Yahoo! Weather API requests that have URLs which change based on the current latitude and longitude of the user, like the 40.741 and -74.002 values in following:

https://query.yahooapis.com/v1/public/yql?format=json&q=select%20*%20from%20weather.forecast%20where%20woeid%20in%20(SELECT%20woeid%20FROM%20geo.placefinder%20WHERE%20text%3D%2240.741%2C-74.002%22%20and%20gflags%3D%22R%22)

Caching each one of those distinct URLs independently won't lead to the experience that your users expect. Instead, our application should store and retrieve the last API response independent of the location encoded in the search parameters of the URL. That way, if a user is offline, the application will still be able to show something (the most recent weather response), even if it's not the most up-to-date response for the current location.

To implement this custom behavior, we'll use two additional Polymer elements, and write a sw-toolbox-style request handler. The code for this section corresponds to what's in the final/ directory, since this is our last step.

&lt;platinum-sw-import-script>

First, we'll use the <platinum-sw-import-script> element to load the definition of our custom request handler, using the href attribute to specify the underlying file.

<platinum-sw-import-script href="weather-fetch-handler.js">

Here's the handler in its entirety, along with some inline comments:

weather-fetch-handler.js

(function(global) {
  // Removes the search/query portion from a URL.
  // E.g. stripSearchParameters("http://example.com/index.html?a=b&c=d")
  //      ➔ "http://example.com/index.html"
  function stripSearchParameters(url) {
    var strippedUrl = new URL(url);
    strippedUrl.search = '';
    return strippedUrl.toString();
  }

  global.weatherFetchHandler = function(request) {
    // Attempt to fetch(request). This will always make a network request, and will include the
    // full request URL, including the search parameters.
    return global.fetch(request).then(function(response) {
      if (response.ok) {
        // If we got back a successful response, great!
        return global.caches.open(global.toolbox.options.cacheName).then(function(cache) {
          // First, store the response in the cache, stripping away the search parameters to
          // normalize the URL key.
          return cache.put(stripSearchParameters(request.url), response.clone()).then(function() {
            // Once that entry is written to the cache, return the response to the controlled page.
            return response;
          });
        });
      }

      // If we got back an error response, raise a new Error, which will trigger the catch().
      throw new Error('A response with an error status code was returned.');
    }).catch(function(error) {
      // This code is executed when there's either a network error or a response with an error
      // status code was returned.
      return global.caches.open(global.toolbox.options.cacheName).then(function(cache) {
        // Normalize the request URL by stripping the search parameters, and then return a
        // previously cached response as a fallback.
        return cache.match(stripSearchParameters(request.url));
      });
    });
  }
})(self);

&lt;platinum-sw-fetch>

Now that we've imported the definition of our custom weatherFetchHandler, we're ready to use it to handle requests. The <platinum-sw-fetch> element takes care of associating a request handler with a URL host and path pattern. In our case, we want the custom request handler to apply to Yahoo! Weather API URLs matching https://query.yahooapis.com/v1/public/yql, so we use

<platinum-sw-fetch handler="weatherFetchHandler"
                   path="/v1/public/yql"
                   origin="https://query.yahooapis.com">
</platinum-sw-fetch>

You now have a web app that displays the current weather, and thanks to the <platinum-sw> elements, works offline. It uses a custom caching strategy for dynamic API requests, and a default "fastest" caching strategy for the web app's static content.

What we've covered

Next Steps

Learn More