Pages - Menu

Pages - Menu

Pages

2024年3月30日土曜日

PWAで注目されるオフラインブラウザーWebアプリをService WorkersとPouchDBで実装しよう。

Reprint ENGLISH Source:



https://www.sitepoint.com/offline-web-apps-service-workers-pouchdb/


Offline web applications are becoming more and more popular. Offline support is so important that it’s now common to talk about the ‘Offline First’ approach, where it becomes a primary consideration. It’s also gathering popularity with the rise of Progressive Web Apps.

In this post, we will look at how to add offline support to a basic contact list web app by implementing asset caching, client-side data storage and synchronization with a remote data store.

The source code of the app is available on GitHub.

Why Support Offline?

Why should we care about offline support?

I myself spend more than one hour on a train every day. I don’t want to waste this time, so I take my laptop to work a bit along the way. I use the cell network to be online. The connection is not reliable, so I lose it from time to time. My user experience is up to the web app I’m using. Only a few apps, with good offline support, behave as expected and connection loss is transparent. Some behave weirdly, so when I refresh the page, I lose data. Most don’t support offline at all, and I have to wait for a stable connection to be able to use them.

An unreliable connection is not the only use case. We can also talk about situations where you might be offline for several hours, for example, while on an airplane.

Another important advantage of offline support is the performance boost. Indeed, the browser doesn’t need to wait for assets to be loaded from the server. The same for data, once stored on the client.

Thus we need offline:

  1. to be able to use apps even with flaky connection (cell network in a train)
  2. to be able to work without network connection (on an airplane)
  3. to boost the performance

Progressive Web Apps

Google’s Progressive Web Apps (PWA) concept is a methodology aimed at delivering web apps that provide the UX of native mobile apps. PWA includes offline support, but it also covers a lot more:

  • Responsiveness – support for different form factors: mobile, tablet, desktop
  • Web App Manifest – to install an app on the home screen
  • App Shell – a design pattern in which the basic UI app shell is separated from the content loaded afterward
  • Push notifications – to get “instant” updates from the server

Addy Osmani wrote a great intro post about PWA.

In this article, we are going to focus only on a single aspect: offline support.

Defining Offline Support

Let’s clarify what it takes to support offline. We need to take care of two aspects:

  1. app assets – caching HTML, JS scripts, CSS style sheets, images
  2. app data – storing data on client-side

App assets

The first solution in HTML5 to cache offline assets was AppCache. The idea is to provide an app manifest describing which resources should be stored in the browser cache. Thus, the next time an app is loaded, these assets will be taken from the browser cache.

Important: While being simple, there are quite a lot of pitfalls with using AppCache. The standard is now deprecated, although it’s still widely supported by browsers.

Service Workers were introduced to replace AppCache. They provide a flexible solution for the offline support. Service Workers give control over outgoing requests, allowing a script intercept them and return the necessary responses. The caching logic is entirely on the developer’s shoulders. The app code itself can check if an asset is saved in the cache and requests it from the server only if needed.

It’s important to note that Service Workers are supported only via HTTPS (HTTP is allowed for localhost) connections. We will look at how to use Service Workers shortly.

App data

App data can be stored in the offline storage provided by browsers.

There are several options introduced by HTML5:

WebStorage is a key-value storage. This is the simplest cross-browser storage, but there are several pitfalls to be aware of. You have to take care of serialization and deserialization of data that you put inside because the values must be plain strings. You may run up against size limits with larger data sets. Also, it’s possible to get into a race condition, meaning if you have two tabs opened at the same time in the browser you could end up with unexpected behavior.

IndexedDB is much more powerful and seems to be the best way to go with offline storage. It has plenty of space available. It supports transactions and can be safely used in several browser tabs at the same time. It’s also supported by all modern browsers.

WebSQL is literally SQLite in the browser. Full-featured relational DB with ACID on the client. Unfortunately, WebSQL has been deprecated by the standards committee and was never supported in non-Blink/Webkit browsers.

There are several libraries out there which provide an abstraction over offline storage:

  • localForage – simple localStorage-like API
  • IDBWrapper – cross-browser IndexedDB wrapper
  • PouchDB – client-side storage solution inspired by CouchDB. It supports automatic sync with the backend if CouchDB is being used.

The ContactBook App

Now, let’s see how to add offline support to a web application. Our sample app is a basic contact book:

contact book screenshot

We have the list of contacts on the left and a details form on the right used for editing contacts. A contact has three fields: first name, last name, and phone.

You can find the app source code on GitHub. To run the app you’ll need Node.js installed. If you’re not sure about this step, you can follow our beginner’s guide to npm.

Learn to Code with JavaScript

Start by downloading the sources and running the following commands from the project folder:

$ npm install
$ npm run serve

What about backend? We are using pouchdb-server to provide a REST API over CouchDB storage, and http-server to serve frontend assets.

Our scripts section of package.json looks like this:

  "scripts": {
    "serve": "npm-run-all -p serve-front serve-backend",
    "serve-front": "http-server -o",
    "serve-backend": "pouchdb-server -d db"
  },

The package npm-run-all allows running several commands in parallel. We start both servers: http-server and pouchdb-server.

Now let’s look at the implementation of offline support for the app assets.

Offline Assets

The directory /public contains all the assets for the application:

  • /css/style.css – the application style sheet
  • /js/ext – directory containing external libs (PouchDB, and Babel to use ES2015 syntax)
  • /js/app.js – the main application script
  • /js/register-service-worker.js – the script that registers the service worker
  • /js/store.js – the adapter class to work with PouchDB storage
  • /contactbook.appcache – the AppCache manifesto
  • /index.html – the application markup
  • /service-worker.js – the source of the service worker

The journey begins with the registration of a service worker. Here is the registration code in register-service-worker.js:

if ('serviceWorker' in navigator) {
    navigator.serviceWorker.register('/service-worker.js', {
        scope: '/'
    }).then(function() {
        // success
    }).catch(function(e) {
        // failed
    });
}

First, we check that serviceWorker is supported by the browser. If yes, we call the register method, providing the URL to the service worker script (in our case /service-worker.js) and extra parameters to specify the scope of the service worker. Parameters are optional and the root / is the scope default value..

Important: to be able to use the root of the app as a scope, the service worker script should be located in the root directory of the app.

The register method returns a Promise.

The life-cycle of a service worker starts with installation. We can handle the install event and put all required resources in the cache:

var CACHE_NAME = 'contact-book-v1';

var resourcesToCache = [
  '/',
  '/css/style.css',
  '/js/ext/babel.min.js',
  '/js/ext/pouchdb.min.js',
  '/js/register-service-worker.js',
  '/js/store.js',
  '/js/app.js'
];

self.addEventListener('install', function(event) {
  event.waitUntil(
    // open the app browser cache
    caches.open(CACHE_NAME)
      .then(function(cache) {
        // add all app assets to the cache
        return cache.addAll(resourcesToCache);
      })
  );
});

The last thing is to handle the fetch event triggered each time a resource from the Service Worker scope is fetched:

self.addEventListener('fetch', function(event) {
  event.respondWith(
    // try to find corresponding response in the cache
    caches.match(event.request)
      .then(function(response) {
        if (response) {
          // cache hit: return cached result
          return response;
        }

        // not found: fetch resource from the server
        return fetch(event.request);
      })
  );
});

That’s it. Let’s test that it’s working:

  1. run the app with npm run serve
  2. open the URL http://127.0.0.1:8080/ in Chrome
  3. stop the web server with Ctrl + C in the console (or use the Chrome dev tools to simulate going offline)
  4. refresh the web page

app assets offline

The app is still available. Awesome!

AppCache

The problem with the solution above is that Service Workers have limited browser support. We can implement a fallback solution using widely supported AppCache. Read more info about AppCache usage here.

The basic usage is straightforward and includes two steps:

  1. Defining the app cache manifest contactbook.appcache:

    CACHE MANIFEST
    # v1 2017-30-01
    
    CACHE:
    index.html
    css/style.css
    js/ext/babel.min.js
    js/ext/pouchdb.min.js
    js/store.js
    js/app.js
    

    For our simple app, we define a single section CACHE and put all the assets there.

  2. Reference the manifest file from the HTML:

    <html manifest="contactbook.appcache" lang="en">
    

That’s it. Let’s open the page in a browser not supporting Service Workers and test it the same way we did before.

app assets offline with appcache

Offline Data

Being able to cache assets is great. But it’s not enough. What makes an app alive is unique data. We are going to use PouchDB as a client-side data storage. It’s powerful, easy-to-use, and provides data sync out-of-box.

If you’re not familiar with it, check out this introduction to PouchDB.

The helper class Store is responsible for the interaction with PouchDB:

class Store {

  constructor(name) {
    this.db = new PouchDB(name);
  }

  getAll() {
    // get all items from storage including details
    return this.db.allDocs({
        include_docs: true
      })
      .then(db => {
        // re-map rows to collection of items
        return db.rows.map(row => {
          return row.doc;
        });
      });
  }

  get(id) {
    // find item by id
    return this.db.get(id);
  }

  save(item) {
    // add or update an item depending on _id
    return item._id ?
      this.update(item) :
      this.add(item);
  }

  add(item) {
    // add new item 
    return this.db.post(item);
  }

  update(item) {
    // find item by id
    return this.db.get(item._id)
      .then(updatingItem => {
        // update item
        Object.assign(updatingItem, item);
        return this.db.put(updatingItem);
      });
  }

  remove(id) {
    // find item by id
    return this.db.get(id)
      .then(item => {
        // remove item
        return this.db.remove(item);
      });
  }
}

The code of the Store class is a typical CRUD implementation, providing a Promise-based API.

Now, our main app component can use the Store:

class ContactBook {

  constructor(storeClass) {
    // create store instance
    this.store = new storeClass('contacts');

    // init component internals
    this.init();

    // refresh the component
    this.refresh();
  }

  refresh() {
    // get all contacts from the store
    this.store.getAll().then(contacts => {
      // render retrieved contacts
      this.renderContactList(contacts);
    });
  }

    ...
}

The Store class is passed to the constructor to decouple the app class from the concrete store. Once the store is created, it’s used in the refresh method to get all the contacts.

The app initialization looks like this:

new ContactBook(Store);

Other app methods interact with the store:

class ContactBook {
  ...

  showContact(event) {
    // get contact id from the clicked element attributes
    var contactId = event.currentTarget.getAttribute(CONTACT_ID_ATTR_NAME);

    // get contact by id
    this.store.get(contactId).then(contact => {
      // show contact details
      this.setContactDetails(contact);

      // turn off editing
      this.toggleContactFormEditing(false);
    })
  }

  editContact() {
    // get id of selected contact
    var contactId = this.getContactId();

    // get contact by id
    this.store.get(this.getContactId()).then(contact => {
      // show contact details
      this.setContactDetails(contact);

      // turn on editing
      this.toggleContactFormEditing(true);
    });
  }

  saveContact() {
    // get contact details from edit form
    var contact = this.getContactDetails();

    // save contact
    this.store.save(contact).then(() => {
      // clear contact details form
      this.setContactDetails({});

      // turn off editing
      this.toggleContactFormEditing(false);

      // refresh contact list
      this.refresh();
    });
  }

  removeContact() {
    // ask user to confirm deletion
    if (!window.confirm(CONTACT_REMOVE_CONFIRM))
      return;

    // get id of selected contact
    var contactId = this.getContactId();

    // remove contact by id
    this.store.remove(contactId).then(() => {
      // clear contact details form
      this.setContactDetails({});

      // turn off editing
      this.toggleContactFormEditing(false);

      // refresh contact list
      this.refresh();
    });
  }

These are the basic operations using the store CRUD methods:

  • showContact – displays contact details once a contact is selected from the list
  • editContact – enables editing of a contact’s details
  • saveContact – saves details of a new or existing contact
  • removeContact – removes the selected contact

Now, if you add contacts while offline and refresh the page, the data won’t be lost.

But, there is a ‘but’…

Data sync

This all works fine, but all data is stored locally in the browser. If we open the app in another browser, we won’t see the changes.

We need to implement data sync with the server. Implementation of the two-way data sync is not a trivial problem. Fortunately, it’s provided by PouchDB if we have CouchDB on the backend.

Let’s change our Store class a bit, so it syncs with the remote data source:

class Store {

  constructor(name, remote, onChange) {
    this.db = new PouchDB(name);

    // start sync in pull mode
    PouchDB.sync(name, `${remote}/${name}`, {
      live: true,
      retry: true
    }).on('change', info => {
      onChange(info);
    });
  }

We’ve added two parameters to the constructor:

  • remote – the URL of the remote server
  • onChange – a callback fired once changes have come from the backend

The PouchDB.sync method does the trick and starts the sync with the backend. The live parameter indicates that it should periodically check for changes, while retry indicates to retry when an error happens (thus if the user goes offline, synchronization won’t stop).

We need to change the app class accordingly and pass required parameters to the Store constructor:

class ContactBook {

  constructor(storeClass, remote) {
    this.store = new storeClass('contacts', remote, () => {
      // refresh contact list when data changed
      this.refresh();
    });

    ...
  }

The main app class constructor now accepts the remote URL which is passed to the store. The onChange callback just calls refresh method to refresh the list of contacts.

The app initialization has to be updated:

new ContactBook(Store, 'http://localhost:5984');

Done! Now our app allows editing the contact list while offline. Once the app has a network connection, the data is synchronized with the backend storage.

Let’s test it:

  1. run the web server with $ npm run serve
  2. open the URL http://127.0.0.1:8080/ in two different browsers
  3. stop the web server clicking Ctrl + C
  4. edit contact list in both browsers
  5. run the web server again with $ npm run serve
  6. check out the contact list in both browsers (it should be up-to-date according to the changes in both browsers)

offline-contactbook-demo

Wonderful, we did it!

Check out the full source code of the app on GitHub.

Conclusion

Providing an offline experience has more and more value today. Being able to use an app with a flaky connection in transport or being offline in the airplane is critical for frequently used apps. It’s also about improving the app performance.

To support offline we needed to take care of:

  • caching app assets – use Service Workers with fallback to AppCache until the former is supported by all modern browsers
  • storing data on the client-side – use browser offline storage, like IndexedDB, with one of the libs available out there

We’ve just looked at how all this can be implemented. I hope you enjoyed reading. Please, share your thoughts about the topic in comments!

This article was peer reviewed by James Kolce and Craig Buckler. Thanks to all of SitePoint’s peer reviewers for making SitePoint content the best it can be!

Frequently Asked Questions (FAQs) about Offline Web Apps, Service Workers, and PouchDB

What is the main advantage of using PouchDB for offline web applications?

PouchDB is a JavaScript database that allows applications to store data locally while offline, then synchronize it with CouchDB and compatible servers when the application is back online, keeping the user’s data in sync no matter where they next go online. This is particularly beneficial for applications that need to provide offline functionality, such as mobile apps or web apps in areas with intermittent internet connectivity.

How does a service worker function in an offline web application?

A service worker is a type of web worker. It’s a JavaScript file that can control the web page/site it is associated with, intercepting and modifying navigation and resource requests, and caching resources in a very granular fashion to complete offline experiences, or to boost performance.

Can PouchDB be used with other databases apart from CouchDB?

Yes, PouchDB can be used with any database that implements the CouchDB Replication Protocol. This includes databases like IBM Cloudant and Apache CouchDB. It can also sync with other PouchDB databases, allowing for flexible and distributed data storage.

How secure is data stored using PouchDB?

PouchDB uses the same security model as the web. If your web application is served over HTTPS, then all data stored and retrieved by PouchDB is also sent over HTTPS. It’s important to note that PouchDB itself does not provide any additional encryption beyond what’s provided by the web platform.

What are the limitations of using service workers?

Service workers require sites to be served over HTTPS for security reasons. They are also asynchronous, meaning they can’t use XHR and localStorage. Another limitation is that they are not available in all browsers.

How does offline servicing work in Windows?

Offline servicing in Windows involves applying updates to a Windows image while it’s offline, i.e., not currently running on a device. This can include adding or removing drivers, enabling or disabling Windows features, or applying updates and patches.

What is the difference between PouchDB and traditional SQL databases?

PouchDB is a NoSQL database that stores data in a flexible, JSON format, whereas traditional SQL databases store data in a rigid, table-like format. This makes PouchDB more adaptable to different types of data and easier to use for developers familiar with JavaScript and JSON.

Can service workers improve the performance of my web application?

Yes, service workers can significantly improve the performance of your web application. By intercepting network requests and serving cached responses, service workers can eliminate the need to retrieve resources from the network every time, resulting in faster load times.

How does PouchDB handle conflict resolution?

PouchDB handles conflicts automatically using a built-in conflict resolution algorithm. When a conflict occurs, PouchDB will choose one version of the document as the “winner” and the other versions will be marked as “conflicts”. Developers can also implement custom conflict resolution strategies if needed.

What are the prerequisites for learning PouchDB?

To learn PouchDB, you should have a basic understanding of HTML, CSS, and JavaScript. Knowledge of JSON (JavaScript Object Notation) and some familiarity with NoSQL databases can also be helpful.

Artem TabalinArtem Tabalin
View Author

Artem is a web developer with 8 years experience in back-end and front-end development. He worked at DevExpress creating DevExtreme and currently works at Criteo. He loves learning new web-related things and believes in open source.



関連情報:

オフラインで使えてサクサク動く、Webフロントエンド開発の新機軸

Articles in this issue reproduced from SitePoint
Copyright © 2017, All rights reserved. SitePoint Pty Ltd. www.sitepoint.com. Translation copyright © 2017, KADOKAWA ASCII Research Laboratories, Inc. Japanese syndication rights arranged with SitePoint Pty Ltd, Collingwood, Victoria,Australia through Tuttle-Mori Agency, Inc., Tokyo
グーグルのPWA(Progressive Web Apps)の登場によって、再び注目されているWebアプリのオフライン機能。Service WorkersとPouchDBを使って実装する実例を紹介。
オフラインWebアプリがどんどん使われるようになっています。オフラインのサポートが重要になった結果、最初にオフラインでの利用を考える「オフライン・ファースト」を話題にするのが普通になりました。また、プログレッシブWebアプリ(PWA)という考え方が現れたのもオフライン・サポートが普及し始めた一因になっています。
この記事では、アセット・キャッシング、クライアント側のデータストレージ、リモート・データストアとの同期などの機能を利用して、基本的な電話帳Webアプリにオフライン・サポートを追加する方法を説明します。
アプリのソースコードはGitHubで入手できます。

なぜオフライン・サポート?

なぜ、オフライン・サポートが必要なのでしょうか?
私は毎日1時間以上、電車の中で過ごします。時間を無駄にしたくないので、途中、少し仕事をするのにノートパソコンを持っていきます。ネットにつなぐのに携帯電話ネットワークを使いますが、接続が安定せず良く切れます。私のこれまでの経験では、優れたオフライン・サポートがあり、思ったとおりに動いてくれて、ネット接続を意識しなくていいアプリは知りません。動きが変になって、ページを更新すると、データが失われてしまうこともあります。オフライン・サポートがまったくないものがほとんどで、きちんと使えるようになるのにネット接続が回復するのを待たなければなりません。
接続が悪いときに限った話ではありません。何時間もオフラインである場合、たとえば、飛行機に乗るときのことも考える必要があります。
オフライン・サポートのもう1つの良いところは、処理速度が飛躍的に向上することです。ブラウザーはサーバーからアセットをロードするのを待つ必要がありません。データについても同様で、クライアント側でいったんセーブしておけば、それで終わりです。
以上、まとめると、オフラインが必要な理由は、次のようになります。
  1. ネット接続が不安定なときでも(電車の中の携帯接続など)アプリが使える
  2. ネット接続がなくても(飛行機の中など)仕事ができる
  3. 動作速度が向上する

Progressive Web Apps

Googleの「Progressive Web Apps(PWA)」コンセプトは、Webアプリでネイティブのモバイルアプリと同等のユーザー・エクスペリエンスの提供を目的とした方法論です。PWAにはオフライン・サポートも含まれますが、それ以外も対象にしています。
  • 応答性:モバイル、タブレット、デスクトップなどさまざまな形態をサポートする
  • Webアプリマニフェスト:ホーム画面にアプリをインストールする
  • アプリシェル:基本的なUIアプリシェルがあとからロードされるコンテンツと分かれているデザインパターンを使用する
  • プッシュ通知:サーバーからただちにアップデートを受け取る
PWAについては、Addy Osmaniが参考になる入門記事を書いています。
この記事で注目するのはそのうちの1つ、オフライン・サポートです。

オフライン・サポートの定義

オフライン・サポートで明らかに必要なのは、次の2点です。
  1. アプリアセット:HTMLキャッシュ、JSスクリプト、CSSスタイルシート、画像
  2. アプリデータ:クライアント側でのデータ保存

アプリアセット

HTML5でオフラインアセットをキャッシュする最初の方法は、AppCacheでした。ブラウザーキャッシュに保存すべきリソースが書かれたマニフェストを用意するという考え方です。次回アプリがロードされるときに、指定されたアセットがブラウザーのキャッシュから読み込まれるということです。
重要:AppCacheを使うのは簡単なのですが、たくさんの落とし穴があるため、現在は余り使われなくなりました。しかし、ブラウザーではいまでも広くサポートされています。
AppCacheに代わってService Workersが使われるようになりました。オフライン・サポートのためにいろいろな使い方を提供します。Service Workersは、発生するリクエストを管理して、スクリプトでインターセプトして必要な応答を返します。キャッシングのロジックはすべて開発者の肩にかかっています。アプリのコードでアセットがキャッシュにセーブされているかをチェックでき、必要な場合だけサーバーにアセットをリクエストします。
Service WorkersはHTTPS経由(HTTPではlocalhostのみ許可)でしかサポートされないことに注意してください。Service Workersの使い方についてはあとで説明します。

アプリデータ

アプリデータはブラウザーが用意しているオフライン・ストレージに保存できます。
HTML5ではいくつかの選択肢があります。
  • WebStorage:key-valueストレージ
  • IndexedDB:NoSQLデータベース
  • WebSQL:ビルトインの SQLite データベース
WebStorageはkey-valueストレージです。もっとも簡単なクロスブラウザー保存ですが、落とし穴があります。セーブする値は文字列でなければならないので、保存するデータを直列化、復元化する必要があります。大きなデータセットにはサイズ制限があります。また、競合の問題もあります。すなわち、ブラウザーでタブを2つ同時に開いた場合、なにが起こるか分からないのです。
IndexedDBはWebStorageに比べてずっと強力で、オフライン・ストレージに使うには最適です。スペースは十分です。トランザクションをサポートし、ブラウザーで同時に複数のタブを使っても安全です。最近のブラウザーならサポートしています。
WebSQLは文字通りブラウザーのSQLiteです。クライアント側でACID特性を備え、完全な機能を持ったリレーショナルDBです。残念なことに標準委員会からは認証されず、non-Blink/Webkitブラウザーでサポートされませんでした。
オフライン・ストレージからデータを抽出する機能を提供するライブラリーは以下のものです。
  • localForage:簡単なlocalStorageライクなAPI
  • IDBWrapper:クロスブラウザーのkdexedDBラッパー
  • PouchDB:CouchDBが提供するクライアント側でのストレージ。CouchDBを使っている場合、バックエンドで自動的に同期をサポートする

電話帳アプリ

Webアプリにオフライン・サポートを加える方法を説明します。例として、基本的な電話帳を取り上げます。
contact book screenshot
左側に連絡先のリストを、右側に連絡先を編集するのに使う詳細フォームを配置します。連絡先には3つのフィールド「名」「姓」「電話番号」があります。
アプリのソースコードはGitHubにあります。アプリを実行するには、Node.jsをインストールしておく必要があります。よく分からなかったら、npmのビギナーズガイドを参考にしてください。
最初に、ソースをダウンロードしてプロジェクトフォルダーから次のコマンドを実行します。
$ npm install
$ npm run serve
バックエンドはどうなのでしょうか。CouchDBストーレジにREST APIを提供するpouchdb-serverとフロントエンド・アセットのためにhttp-serverを使っています。
package.jsonscriptsは次のようになります。
  "scripts": {
    "serve": "npm-run-all -p serve-front serve-backend",
    "serve-front": "http-server -o",
    "serve-backend": "pouchdb-server -d db"
  },
npm-run-allパッケージはコマンドを並列で使えます。http-serverpouchdb-serverの両方を使います。
アプリアセットのオフライン・サポートをインプリメントします。

オフライン・アセット

/publicディレクトリにアプリケーションに必要なアセットがすべてあります。
  • /css/style.css:アプリケーションスタイルシート
  • /js/ext:外部ライブラリー(ES2015で書かれたPouchDBとBabel)を含んだディレクトリ
  • /js/app.js:メインのアプリケーション・スクリプト
  • /js/register-service-worker.js:Service Workersを登録するスクリプト
  • /js/store.js:PouchDBストレージを扱うアダプタークラス
  • /contactbook.appcache:AppCacheのマニフェスト
  • /index.html:アプリケーション・マークアップ
  • /service-worker.js:Service Workersのソース
Service Workersの登録から始めます。以下がregister-service-worker.jsの登録コードです。
if ('serviceWorker' in navigator) {
    navigator.serviceWorker.register('/service-worker.js', {
        scope: '/'
    }).then(function() {
        // success
    }).catch(function(e) {
        // failed
    });
}
最初にserviceWorkerがブラウザーでサポートされているかチェックします。サポートされているならregisterメソッドを呼び出し、Service WorkersスクリプトにURLを渡し(この例では/service-worker.js)、Service Workersのスコープを指定するパラメータを渡します。パラメータはオプションで、デフォルトのscopeの値はルート/です。
重要:スコープとして、アプリのルートディレクトリが使えるようするには、Service Workersスクリプトはアプリのルートに置いておかなければなりません。
registerメソッドはPromiseを返します。
Service Workersのライフサイクルはインストールから始まります。installイベントを操作し、必要なリソースをキャッシュに置けます。
var CACHE_NAME = 'contact-book-v1';

var resourcesToCache = [
  '/',
  '/css/style.css',
  '/js/ext/babel.min.js',
  '/js/ext/pouchdb.min.js',
  '/js/register-service-worker.js',
  '/js/store.js',
  '/js/app.js'
];

self.addEventListener('install', function(event) {
  event.waitUntil(
    // open the app browser cache
    caches.open(CACHE_NAME)
      .then(function(cache) {
        // add all app assets to the cache
        return cache.addAll(resourcesToCache);
      })
  );
});
最後に必要なことは、Service Workersのスコープからリソースがフェッチされるたびにトリガーされるfetchイベントを制御することです。
self.addEventListener('fetch', function(event) {
  event.respondWith(
    // try to find corresponding response in the cache
    caches.match(event.request)
      .then(function(response) {
        if (response) {
          // cache hit: return cached result
          return response;
        }

        // not found: fetch resource from the server
        return fetch(event.request);
      })
  );
});
これで終わりです。ちゃんと動くかテストしてみます。
  1. npm run serveでアプリを起動する
  2. ChromeでURL http://127.0.0.1:8080/を開く
  3. コンソールからCtrl + Cでサーバーを停止する(あるいはChromeのデベロッパーツールsimulate going offlineを使う)
  4. Webページをリフレッシュする
app assets offline
アプリはまだ使えています。すごい!

AppCache

上の方法で問題なのは、Service Workersのブラウザー・サポートが限られていることです。ブラウザー・サポートが優れているAppCacheを使ってフォールバックソリューションを実装します。AppCacheの使い方の詳細はここを読んでください。
使い方は簡単で、2つのステップがあります。
  1. AppCacheのマニフェストcontactbook.appcacheを定義する
    CACHE MANIFEST
    # v1 2017-30-01
    
    CACHE:
    index.html
    css/style.css
    js/ext/babel.min.js
    js/ext/pouchdb.min.js
    js/store.js
    js/app.js
    
    例の簡単なアプリではCACHEセクションを1つ定義し、アセットを全部置いておく
  2. Htmlからマニフェストファイルを参照する
    <html manifest="contactbook.appcache" lang="en">
    
これで終わりです。Service Workersをサポートしていないブラウザのページを開いて、前回と同じようにテストします。
app assets offline with appcache

オフラインデータ

アセットをキャッシュできることはすごいことです。しかし、十分ではありません。アプリがオフラインで動くためにはユニークなデータが扱えるかどうかです。PouchDBをクライアント側のデータストレージとして使います。PouchDBは強力で使いやすく、すぐにデータの同期ができます。
PouchDBのことが良く分からないときは『たった2行でサーバーとも同期できるJSデータベース「PouchDB」がアツい!』を参照してください。
PouchDBを操作するにはヘルパークラスStoreを使います。
class Store {

  constructor(name) {
    this.db = new PouchDB(name);
  }

  getAll() {
    // get all items from storage including details
    return this.db.allDocs({
        include_docs: true
      })
      .then(db => {
        // re-map rows to collection of items
        return db.rows.map(row => {
          return row.doc;
        });
      });
  }

  get(id) {
    // find item by id
    return this.db.get(id);
  }

  save(item) {
    // add or update an item depending on _id
    return item._id ?
      this.update(item) :
      this.add(item);
  }

  add(item) {
    // add new item 
    return this.db.post(item);
  }

  update(item) {
    // find item by id
    return this.db.get(item._id)
      .then(updatingItem => {
        // update item
        Object.assign(updatingItem, item);
        return this.db.put(updatingItem);
      });
  }

  remove(id) {
    // find item by id
    return this.db.get(id)
      .then(item => {
        // remove item
        return this.db.remove(item);
      });
  }
}
Storeクラスのコードは典型的なCRUDを用いるもので、PromiseベースのAPIを提供します。
以下でメインのアプリのコンポーネントがStoreを使えるようになります。
class ContactBook {

  constructor(storeClass) {
    // create store instance
    this.store = new storeClass('contacts');

    // init component internals
    this.init();

    // refresh the component
    this.refresh();
  }

  refresh() {
    // get all contacts from the store
    this.store.getAll().then(contacts => {
      // render retrieved contacts
      this.renderContactList(contacts);
    });
  }

    ...
}
Storeクラスは、具体的なデータをアプリクラスから分離するコンストラクタに渡されます。 いったんストアーが生成されると、すべての連絡先にアクセスするためrefreshメソッドで使われます。
アプリの初期化は次のようになります。
new ContactBook(Store);
このストアーに影響するほかのアプリメソッドは、次のようになります。
class ContactBook {
  ...

  showContact(event) {
    // get contact id from the clicked element attributes
    var contactId = event.currentTarget.getAttribute(CONTACT_ID_ATTR_NAME);

    // get contact by id
    this.store.get(contactId).then(contact => {
      // show contact details
      this.setContactDetails(contact);

      // turn off editing
      this.toggleContactFormEditing(false);
    })
  }

  editContact() {
    // get id of selected contact
    var contactId = this.getContactId();

    // get contact by id
    this.store.get(this.getContactId()).then(contact => {
      // show contact details
      this.setContactDetails(contact);

      // turn on editing
      this.toggleContactFormEditing(true);
    });
  }

  saveContact() {
    // get contact details from edit form
    var contact = this.getContactDetails();

    // save contact
    this.store.save(contact).then(() => {
      // clear contact details form
      this.setContactDetails({});

      // turn off editing
      this.toggleContactFormEditing(false);

      // refresh contact list
      this.refresh();
    });
  }

  removeContact() {
    // ask user to confirm deletion
    if (!window.confirm(CONTACT_REMOVE_CONFIRM))
      return;

    // get id of selected contact
    var contactId = this.getContactId();

    // remove contact by id
    this.store.remove(contactId).then(() => {
      // clear contact details form
      this.setContactDetails({});

      // turn off editing
      this.toggleContactFormEditing(false);

      // refresh contact list
      this.refresh();
    });
  }
ストアーCRUDメソッドを使った基本的なオペレーションを以下に示します。
  • showContact:リストから連絡先が選択されたとき、詳細を表示する
  • editContact:連絡先の詳細を編集できるようにする
  • saveContact:新規または既存の連絡先の詳細を保存する
  • removeContact:選択された連絡先を削除する
これで、オフラインで連絡先を加えてページを更新しても、データは失われません。
ただし、気をつけなければならないことがあります。

データ同期

ここまではすべてうまくいきました。しかし、データはすべてブラウザーにローカル保存されているので、違うブラウザーでアプリを開いても変更は反映されていません。
サーバーにデータ同期機能を実装する必要があります。 2方向のデータ同期を実装するのは簡単ではありません。幸い、バックエンドでCouchDBが動いていればPouchDBの同期機能を使えます。
Storeクラスを変更すれば、リモートのデータソースと同期します。
class Store {

  constructor(name, remote, onChange) {
    this.db = new PouchDB(name);

    // start sync in pull mode
    PouchDB.sync(name, `${remote}/${name}`, {
      live: true,
      retry: true
    }).on('change', info => {
      onChange(info);
    });
  }
コンストラクタに2つのパラメータを追加しました。
  • remote:リモートサーバーのURL
  • onChange:バックエンドから変更がきたときに発生するコールバック
PouchDB.syncメソッドがうまくバックエンドとの同期をします。liveパラメータは定期的に変更があるかどうかをチェックしており、retryはエラーが発生したときにリトライします(したがって、ユーザーがオフラインにしても、同期は止まりません) 。
アプリクラスを変更し、必要なパラメータをStoreコンストラクタに渡す必要があります。
class ContactBook {

  constructor(storeClass, remote) {
    this.store = new storeClass('contacts', remote, () => {
      // refresh contact list when data changed
      this.refresh();
    });

    ...
  }
メインのアプリクラス・コンストラクタはストアーに渡されるリモートのURLを受けとります。onChangeコールバックはrefreshメソッドを呼び出し、連絡先のリストを更新します。
アプリの初期化も変更しなければなりません。
new ContactBook(Store, 'http://localhost:5984');
できました! これでオフラインで電話帳を編集できます。アプリがネットワーク接続したら、データはバックエンド・ストレージと同期されます。
試してみます。
  1. $ npm run serveでWebサーバーを起動する
  2. 2種類のブラウザーでURL http://127.0.0.1:8080/を開く
  3. Ctrl + CをクリックしてWebサーバーを停止する
  4.  両方のブラウザーで電話帳を編集する
  5. $ npm run serveでWebサーバーを再度起動する
  6. 両方のブラウザーで電話帳をチェックする(両方のブラウザーで変更が反映されているはず)
offline-contactbook-demo
すばらしい! やりました!
GitHubにあるアプリのソースコードをチェックしてください。

最後に

オフラインでのアプリの使用は、日に日に重要性を増しています。移動中にネット接続がよく切れたり、飛行機でネット接続ができないときにアプリが使えることは、手放せないアプリを使うのに非常に重要です。アプリの実行速度を上げることにもなります。
オフラインで使えるようにするには次のことに注意する必要があります。
  • アプリアセットをキャッシングする:Service Workersがすべてのブラウザーでサポートされるまでは、AppCacheのフォールバックを利用する
  • クライアント側でデータをストアーする:indexedDBのようなブラウザー・オフライン・ストレージをライブラリーとともに利用する
以上をどのように実装するのか説明してきました。楽しんでいただけたでしょうか。
※本記事はJames KolceCraig Bucklerが査読を担当しています。最高のコンテンツに仕上げるために尽力してくれたSitePointの査読担当者のみなさんに感謝します。
[翻訳:関 宏也/編集:Livit
Copyright © 2017, Artem Tabalin All Rights Reserved.
Artem Tabalin
Artem Tabalin
フロントエンド、バックエンドの開発を8年続けているWeb開発者で、DV expressでDevExtremeを製作しています。現在はCriteoに勤務。Web関連の新しいことを学ぶのが大好きで、オープンソースを支持しています。

ハイパフォーマー
https://www.high-performer.jp/

Daijob.com
https://www.daijob.com/
https://www.daijob.com/en/

skype自動翻訳
検索結果はこちら
コメント:Skypeで音声認識しながら自動翻訳するのでしたら、
ヘッドセットや卓上マイクを使用された方が音声認識の精度は向上致します。

英語でプログラミングレッスン
検索結果はこちら

0 件のコメント:

コメントを投稿