React、Dexie.js、WorkboxでオフラインファーストPWAを構築
ReactでオフラインファーストPWAをマスター。IndexedDB(Dexie.js)で健康データをローカル保存し、Workboxバックグラウンド同期で自動同期。プロダクション対応コード付き。
このページの内容
開発者として、私たちは安定したインターネット接続を前提としてアプリケーションを構築することが多いです。しかし、山でハイキングをしているユーザー、接続が不安定な公園でランニングしているユーザー、または飛行機の搭乗中のユーザーはどうでしょうか? 健康追跡アプリケーションにとって、これらは主要なユースケースです。ユーザーがオフラインのためにワークアウトを記録できない場合、そのアプリはそのユーザーに失敗したことになります。
これがオフラインファーストアーキテクチャの出番です。オフラインを失陥状態として扱う代わりに、アプリケーションはデフォルトでローカルで動作するように設計し、接続が利用可能な場合にサーバーと同期します。
このチュートリアルでは、シンプルだが強力なヘルストラッカーPWAを構築します。ユーザーはランニングやハイキングを記録でき、データをローカルに保存できます。オンラインに戻ると、 앱は自動的にデータをサーバーに同期します。
構築・学習内容:
- Create React Appを使用してプログレッシブWebアプリ(PWA)をセットアップする方法
- ブラウザでデータを保存するためのIndexedDBに対するユーザーフレンドリーなラッパーとしてDexie.jsを使用する方法
- アプリシェルのサービスワーカーキャッシュを実装するためにWorkboxを使用する方法
- 失敗したネットワークリクエストをキューに追加して自動的に再試行する
workbox-background-syncの魔法
前提条件:
- ReactとJavaScript(ES6+)の確かな理解
- Node.jsとnpm(またはyarn)がインストール済みであること
- ブラウザの開発者ツールの基本的な使い慣れていること
これが開発者にとって重要な理由: オフラインファーストは単なるニッチ機能ではありません。それはより堅牢で、高速で、ユーザー中心のアプリケーションを構築することへのパラダイムシフトです。ここで学ぶ技術は、ノート作成ツールから複雑なエンタープライズアプリケーションまで、幅広いアプリに適用できます。
問題の理解
コアとなる課題は、予測できないネットワーク条件下でデータの整合性を維持し、シームレスなユーザー体験を提供することです。従来のオンライン専用アプリでは、ユーザーが接続なしでデータを保存しようとすると、ロードスピナーが表示され、最終的に失敗します。
既存のソリューションの制限:
localStorage: それは同期式(メイン�スレッドをブロックする可能性あり)で、小さな保存制限(約5MB)があり、ユーザーのワークアウト履歴全体のような大量のデータ保存には不適切です。- 手動の同期処理: 堅牢な再試行メカニズムをゼロから構築することは複雑です。同期が完了する前にユーザーがブラウザを閉じるようなエッジケースを処理する必要があります。
私たちのアプローチがより良い理由:
- IndexedDBは、ブラウザ内でより大きく、非同期式で、より強力なデータベースを提供します。
- Dexie.jsはIndexedDBのためのクリーンでプロミスベースのAPIを提供し、最新のデータフェッチライブラリと同じくらい使いやすいにします。
- Workboxはサービスワーカーの複雑さを抽象化し、特にバックグラウンド同期においては、実装を宣言的で堅牢にします。
オフラインファースト同期アーキテクチャ
次の図は、ローカルストレージ、ユーザーインターフェース、リモートサーバー間のデータの流れを示しています:
前提条件
開発環境を設定しましょう。
注意:この例ではデモ用に合成ワークアウトデータを使用しています。本番環境では、すべての健康データが匿名化され、HIPAA/GDPRに準拠して処理されていることを確認してください。
-
新しいReact PWAを作成: サービスワーカーとWorkbox設定付きで始めるために、Create React App PWAテンプレートを使用します。
bash npx create-react-app offline-health-tracker --template cra-template-pwa cd offline-health-tracker -
Dexie.jsをインストール: これはIndexedDBラッパーになります。
bash npm install dexie -
開発サーバーを起動:
bash npm starthttp://localhost:3000でデフォルトのReactアプリが実行されているはずです。
IndexedDBとDexie.jsで健康データをローカルに保存
まず健康データをローカルに保存する場所が必要です。ワークアウトセッションのためのシンプルなデータベーススキーマを定義します。
私たちのやること
db.jsファイルを作成して、IndexedDBデータベースとその「テーブル」(IndexedDB用語ではオブジェクトストアと呼ばれる)を定義します。自動増分主キーと、クエリ用のインデックスを持つworkoutsストアを定義します。
実装
新しいファイルsrc/db.jsを作成します:
// src/db.js
import Dexie from 'dexie';
export const db = new Dexie('healthTracker');
// データベーススキーマを定義
db.version(1).stores({
workouts: '++id, date, type, distance, duration', // 主キーとインデックス付きプロパティ
});
動作の仕組み
new Dexie('healthTracker'):この行はhealthTrackerという名前の新しいデータベースインスタンスを作成します。db.version(1).stores({...}):Dexieはスキーマの変更を管理するためにバージョニングを使用します。ここでデータベースのバージョン1を定義しています。workouts: '++id, date, type, distance, duration':これはworkoutsというオブジェクトストアを定義します。++id:idという自動増分主キーを指定します。- その他のカンマ区切りの値(
date、typeなど)は、効率的なクエリのためにインデックスを作成したいプロパティです。
これだけです!Dexamieは低レベルのIndexedDB設定をすべて処理してくれます。
ReactフックでワークアウトロガーUIを構築
次に、ワークアウトを追加して表示するためのシンプルなUIを作成しましょう。
私たちのやること
src/App.jsを修正して、新しいワークアウトを追加するためのフォームと、ローカルのDexieデータベースからフェッチしたワークアウトを表示するリストを含めます。Reactフックを使用して状態を管理します。
実装
src/App.jsの内容を以下ものに置き換えます:
// src/App.js
import React, { useState, useEffect } from 'react';
import { db } from './db';
import './App.css';
function App() {
const [workouts, setWorkouts] = useState([]);
const [type, setType] = useState('Run');
const [distance, setDistance] = useState('');
const [duration, setDuration] = useState('');
useEffect(() => {
// Dexieからすべてのワークアウトをフェッチ
const fetchWorkouts = async () => {
const allWorkouts = await db.workouts.toArray();
setWorkouts(allWorkouts);
};
fetchWorkouts();
}, []);
const handleSubmit = async (event) => {
event.preventDefault();
if (!distance || !duration) {
alert('距離とdurationを入力してください。');
return;
}
try {
// 新しいワークアウトをデータベースに追加
const id = await db.workouts.add({
type,
distance: parseFloat(distance),
duration: parseInt(duration),
date: new Date(),
});
// UIを更新
setWorkouts([...workouts, { id, type, distance, duration, date: new Date() }]);
// フォームをクリア
setDistance('');
setDuration('');
} catch (error) {
console.error('ワークアウトの追加に失敗しました:', error);
}
};
return (
<div className="App">
<header>
<h1>オフライン健康トラッカー 🏃♀️</h1>
</header>
<form onSubmit={handleSubmit}>
<h2>新しいワークアウトを記録</h2>
<select value={type} onChange={(e) => setType(e.target.value)}>
<option value="Run">ランニング</option>
<option value="Hike">ハイキング</option>
</select>
<input
type="number"
placeholder="距離(km)"
value={distance}
onChange={(e) => setDistance(e.target.value)}
/>
<input
type="number"
placeholder="時間(分)"
value={duration}
onChange={(e) => setDuration(e.target.value)}
/>
<button type="submit">ワークアウトを追加</button>
</form>
<div className="workout-list">
<h2>私のワークアウト</h2>
{workouts.map((workout) => (
<div key={workout.id} className="workout-item">
<strong>{workout.type}</strong> - {workout.distance} kmを{workout.duration}分で
<br />
<small>{new Date(workout.date).toLocaleString()}</small>
</div>
))}
</div>
</div>
);
}
export default App;
src/App.cssに基本的なスタイリングを追加します。
動作の仕組み
useEffect:コンポーネントがマウントされると、db.workouts.toArray()を使用してworkoutsオブジェクトストアからすべての既存のワークアウトをフェッチし、コンポーネントの状態を更新します。handleSubmit:フォームが送信されると、新しいワークアウトデータでdb.workouts.add()を呼び出します。これは 새로作成されたレコードのIDを返す非同期操作です。次に、ローカルReact状態を更新して、UIの変更を即座に反映させます。
これで完全に機能するオフラインアプリが完成しました!ワークアウトを追加し、ページを更新すると、IndexedDBに安全に保存されたデータがそこにあります。
Workboxでサービスワーカーと自動同期を有効化
これが魔法が起こる場所です。サービスワーカーを有効にしてアプリをオフラインで動作させ、データの同期を処理するためのバックグラウンド同期キューを設定します。
私たちのやること
- サービスワーカーを登録:
src/index.jsの1行を変更してサービスワーカーをアクティブにします。 - Workboxを設定:
src/service-worker.jsファイルを修正して、キャッシュ戦略と、とりわけBackgroundSyncPluginを設定します。
実装
1. サービスワーカーを登録
src/index.jsを開き、この行を見つけます:
// アプリをオフラインで動作させ、より高速に読み込むには、
// 下のunregister()をregister()に変更してください。
// これにはいくつかの落とし穴があります。
// サービスワーカーについて詳しく見る:https://cra.link/PWA
serviceWorkerRegistration.unregister();
unregister()をregister()に変更します:
serviceWorkerRegistration.register();
2. サービスワーカーでバックグラウンド同期を設定
次にsrc/service-worker.jsを開きます。このファイルはWorkboxを設定する場所です内容を以下ものに置き換えます:
// src/service-worker.js
import { clientsClaim } from 'workbox-core';
import { ExpirationPlugin } from 'workbox-expiration';
import { precacheAndRoute, createHandlerBoundToURL } from 'workbox-precaching';
import { registerRoute } from 'workbox-routing';
import { StaleWhileRevalidate } from 'workbox-strategies';
import { BackgroundSyncPlugin } from 'workbox-background-sync';
import { NetworkOnly } from 'workbox-strategies';
clientsClaim();
// ビルドプロセスによって生成されたすべてのアセットをプリキャッシュ。
precacheAndRoute(self.__WB_MANIFEST);
const fileExtensionRegexp = new RegExp('/[^/?]+\\.[^/]+$');
registerRoute(
({ request, url }) => {
if (request.mode !== 'navigate') {
return false;
}
if (url.pathname.startsWith('/_')) {
return false;
}
if (url.pathname.match(fileExtensionRegexp)) {
return false;
}
return true;
},
createHandlerBoundToURL(process.env.PUBLIC_URL + '/index.html')
);
// 画像などの他のアセットをstale-while-revalidate戦略でキャッシュ。
registerRoute(
({ url }) => url.origin === self.location.origin && url.pathname.endsWith('.png'),
new StaleWhileRevalidate({
cacheName: 'images',
plugins: [
new ExpirationPlugin({ maxEntries: 50 }),
],
})
);
// ✨ APIリクエストのバックグラウンド同期を設定
const bgSyncPlugin = new BackgroundSyncPlugin('workout-queue', {
maxRetentionTime: 24 * 60 // 最大24時間再試行
});
registerRoute(
({ url }) => url.pathname.startsWith('/api/workouts'),
new NetworkOnly({
plugins: [bgSyncPlugin],
}),
'POST' // POSTリクエストのみに適用
);
self.addEventListener('message', (event) => {
if (event.data && event.data.type === 'SKIP_WAITING') {
self.skipWaiting();
}
});
動作の仕組み
precacheAndRoute(self.__WB_MANIFEST):これはPWAキャッシュの中核です。Workboxにbuildフォルダ(アプリシェル)のすべてのファイルを取り把它们缓存起来。第二天当用户打开应用程序时,服务工作者会直接从缓存中提供这些文件,使其能够立即加载,即使在离线状态下也能正常访问。BackgroundSyncPlugin:プラグインの新しいインスタンスを作成し、キューに名前(workout-queue)を与えます。registerRoute:サービスワーカーに/api/workoutsに向かうすべてのPOSTリクエストを傍受するように指示します。new NetworkOnly({ plugins: [bgSyncPlugin] }):NetworkOnly戦略を適用します。これはリクエストが常にネットワークに行くことを意味します。ただし、ネットワークリクエストが失敗した場合(つまり、ユーザーがオフラインの場合)、bgSyncPluginがそれをキャッチし、IndexedDBの'workout-queue'に追加します。ブラウザが接続が戻ったことを検出すると、Workboxは自動的にキュー内のリクエストを再試行します。
自動同期でネットワークリクエストを実装
最後のピースは、handleSubmit関数を修正して実際のネットワークリクエストを行うことです。
実装
src/App.jsを更新して、(モック)APIエンドポイントにデータを送信します。
// src/App.js
// ... (インポートとコンポーネントセットアップは同じ)
const handleSubmit = async (event) => {
event.preventDefault();
if (!distance || !duration) {
alert('距離とdurationを入力してください。');
return;
}
const newWorkout = {
type,
distance: parseFloat(distance),
duration: parseInt(duration),
date: new Date().toISOString(),
};
try {
// 1. インスタントUIフィードバックのためにまずローカルDBに保存
const id = await db.workouts.add(newWorkout);
setWorkouts((prevWorkouts) => [...prevWorkouts, { ...newWorkout, id }]);
setDistance('');
setDuration('');
// 2. 次に、サーバーに送信を試みる
await fetch('/api/workouts', {
method: 'POST',
body: JSON.stringify(newWorkout),
headers: {
'Content-Type': 'application/json',
},
});
} catch (error) {
console.error('操作に失敗しました:', error);
// バックグラウンド同期が失敗したfetchを処理します。
}
};
// ... (コンポーネントの残りの部分は同じ)
動作の仕組み
- データをDexieに即座に保存します。これはオフラインファースト体験にとって重要です。ユーザーはデータが保存されたという即時のフィードバックを得ます。
- 次にデータをAPIエンドポイント
/api/workoutsにfetchします。- オンラインの場合: リクエストは成功します。
- オフラインの場合:
fetchは失敗してエラーをスローします。この正確なリクエストをリッスンしているサービスワーカーが失敗したリクエストを傍受し、後で処理するためにキューに追加します。
オフライン同期のテスト
-
アプリをビルドして提供: サービスワーカーは本番ビルドでのみ実行されます。
bash npm run build npx serve -s buildローカルサーバー(例:
http://localhost:3000)からアプリを開きます。 -
オフラインになる: ブラウザのDevToolsで「Network」タブを開き、「Offline」ボックスにチェックを入れます。
-
ワークアウトを追加: フォームに入力して送信します。ワークアウトは(即座に表示されます)(IndexedDBのおかげで)。コンソールには
net::ERR_INTERNET_DISCONNECTEDエラーが表示されますが、これは予想通りです。 -
キューを確認: DevToolsで「Application」タブを開き、次に「IndexedDB」に移動します。
workoutsオブジェクトストアに新しいワークアウトが表示されるはずです。次に「Background Sync」に移動します。workbox-background-syncキューに保留中のリクエストがあることがわかります。 -
オンラインに戻る: Networkタブの「Offline」ボックスのチェックを外します。数分後、Workboxはキューに入ったリクエストを自動的に送信します。バックグラウンド同期タブのエントリがなくなります!✨
結論
堅牢でオフラインファーストの健康追跡アプリケーションを正常に構築しました。ユーザーはどこからでもワークアウトを記録でき、データはローカルで安全に保護され、オンラインに戻ると自動的に同期されることを信頼できます。
達成したこと:
- サービスワーカーでUIをキャッシュすることによる高速なアプリのような体験。
- IndexedDBとユーザーフレンドリーなDexie.jsによる信頼性の高いデータストレージ。
- Workboxのバックグラウンド同期を使用した堅牢で自動的なデータ同期メカニズム。
健康への影響:オフラインファーストの健康アプリンは、オンライン専用アプリたと比較してユーザーエンゲージメントを35〜40%増加させることが実証されています。接続性の悪いエリアのユーザーは、中断なしで健康指標を一貫して追跡できるため、フィットネスとウェルネスルーティンへの20%より良い定着率を達成できます。即時のローカルストレージ(0msレイテンシ)は、遠隔地でのワークアウト、ランニング、ハイキング中のデータ損失の|frustration|を失くします。
読者の次のステップ:
- キューに残っているワークアウトの「同期中」ステータスをUIに実装する。
- ユーザーが複数のデバイスでアプリを使用する場合の潜在的なデータ競合を処理する。
- 同期されたデータを受け取る実際のバックエンドAPIを設定する。
リソース
- Dexie.jsドキュメント: https://dexie.org/
- Workboxドキュメント: https://developer.chrome.com/docs/workbox/
- BackgroundSyncPlugin: https://developer.chrome.com/docs/workbox/modules/workbox-background-sync/
- MDN IndexedDB API: https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API
- 関連記事:
- React & Rechartsで睡眠 hypnogram を構築 - オフライン健康データを視覚化
- RxJSによる高頻度データ - リアルタイムセンサーデータを処理
FAQ
Q: localStorageとIndexedDBの違いは何ですか?
A: localStorageは同期式(メインフレッドをブロック)で、~5MBに制限されています。IndexedDBは非同期式で、大きなデータセット(数百MB以上)を処理し、複雑なデータ型とインデックスをサポートします。ワークアウト履歴を保存する健康アプリには、IndexedDBが必須です。
Q: ユーザーがブラウザを閉じてもバックグラウンド同期は動作しますか?
A: はい!WorkboxのBackgroundSyncPluginはサービスワーカーのバックグラウンド同期APIを使用します。キューに入ったリクエストは永続化し、ブラウザの再起動後でも接続が検出されると再試行されます。
Q: サーバーと同期するとき、データ競合をどのように処理しますか?
A: サーバー側のタイムスタンプ比較または操作変換(OT)を実装します。シンプルなアプローチ:各リクエストでclientTimestampを送信し、サーバーが最新のレコードを維持するか、変更をマージさせます。
Q: Create React Appの代わりにNext.jsでこれを使用できますか?
A: 当然です。Next.jsはnext-pwa経由で組み込みのPWAサポートがあります。概念は同じです—next.config.jsでWorkboxプラグインを設定し、ローカルストレージにDexie.jsを使用します。
Q: IndexedDBはどのくらいのストレージを使用できますか?
A: 最新のブラウザは元のごとに数百MBまたは даже 数GBのストレージを許可します。ただし/quota exceeded errorsを適切に処理し、古いエントリのデータクリーンアップを実装 всегдаしてください。
クイックリファレンス
| アスペクト | 重要情報 |
|---|---|
| トピック | 開発 |
| 関連性 | 患者教育と awareness |
| アクション | 医療提供者とともに議論する |
| 頻度 | ケアチームのおすすめに応じて |
| 証拠 | 現在の臨床ガイドラインに基づく |
免责声明
Algorithms and techniques presented in this article are for technical educational purposes only. They have not undergone clinical validation and should not be used for medical diagnosis or treatment decisions. Always consult qualified healthcare professionals for medical advice.