Next.jsとIndexedDBでオフライン優先のムードジャーナルPWAを構築する
Next.jsでオフライン優先のPWAを作成する方法を学びます。サービスワーカーとIndexedDBを活用したキャッシュ戦略で、インターネットなしでも動作するアプリ構築のコツ解説します。
このページの内容
メンタルヘルスとマインドフルネスアプリは今や空前の人気を集めています。しかし、飛行機の搭乗中、離れ小島での利用、または単にインターネット接続が不安定な状況で気分を記録したい場合はどうすればいいでしょうか?ほとんどのウェブアプリケーションは悲惨な「インターネット接続なし」ページを表示して動作しなくなります。ユーザー体験を損ない、最も必要な場面でアプリを役に立たないものにしてしまいます。
このチュートリアルでは、この問題に正面から取り組みます。Next.jsを使用して「オフラインファースト」な気分日記PWAを構築します。これは、ネットワーク接続なしでも完璧に動作するように設計されたアプリケーションです。すべてのデータはブラウザ固有のデータベースシステムであるIndexedDBにローカル保存され、服务ワーカー 덕분에アプリのインターフェースがキャッシュから瞬時に読み込まれます。
構築・学習内容:
- PWAとして設定されたNext.jsアプリケーション
- IndexedDBでのデータの保存、読み取り、更新方法
next-pwaを使用したアセットキャッシュのための服务ワーカー設定- 気分記録のためのきれいで機能的なUI
前提条件:
- ReactとJavaScript(ES6+)の確かな理解
- Node.js(v18以降)およびnpm/yarnがインストール済み
- Next.jsの基本的な familiarity(必須ではないが有帮助)
これが開発者にとって重要な理由: オフラインファーストアプリケーションの構築は、もはやニッチなスキルではありません。ネイティブアプリと競争できる、耐障害性が高く信頼性が高くパフォーマンスに優れたウェブ体験を作成するための重要なコンポーネントです。
問題の理解
中心的な課題は、ネットワークが利用できないときでも状態と機能を維持することです。従来のウェブアプリケーションは、データとリソースのためにサーバーへの常時接続に依存しています。オフラインファーストPWAは、このモデル翻转します。
現状: ほとんどの日記系ウェブアプリケーションはサーバー依存です。インターネットなし=アクセス不可。 制限事項: このモデルは、旅行中や接続状態が悪い場合など、一般的なシナリオで失敗し、ユーザーのフラストレーションにつながります。 私たちのアプローチ: ブラウザを主要なデータストアおよびサーバーとして使用します。アプリシェルとアセットは服务ワーカーによってキャッシュされ、瞬時のオフライン読み込みを実現します。日記エントリはIndexedDB(強力なブラウザデータベース)に保存され、データが失われることのないようにします。
オフラインファーストPWAアーキテクチャ
以下の図は、服务ワーカ캐싱とIndexedDBストレージを使用したオフラインファーストアーキテクチャを示しています:
前提条件と初期セットアップ
コーディングに入る前に、開発環境を準備しましょう。
必要なツール
- Next.js:アプリ構築用のReactフレームワーク
@ducanh2912/next-pwa:Next.jsでのPWA設定simplifyする、维护良好的ライブラリidb:プロミスベースのAPIでIndexedDBでの作業を大幅に容易にする小さなライブラリ
セットアップコマンド
まず、新しいNext.jsアプリケーションを作成します:
npx create-next-app@latest mood-journal-pwa --ts --tailwind --eslint --app
cd mood-journal-pwa
次に、PWA機能とIndexedDBに必要なパッケージをインストールします:
npm install @ducanh2912/next-pwa idb
package.jsonには、これらの依存関係が含まれている必要があります。
服务ワーカーでNext.jsをPWAとして設定
何をするのか
@ducanh2912/next-pwaを使用して、服务ワーカーを自動的に生成および登録します。服务ワーカーは、ブラウザがウェブページとは分离してバックグラウンドで実行するスクリプトであり、オフラインキャッシングやプッシュ通知などの機能を可能にします。
入力:標準的なNext.jsアプリケーション 出力:服务ワーカー、マニフェストファイル、アセットキャッシングを備えたPWA
実装
-
プロジェクトルートに
next.config.mjsファイルを作成:javascript // next.config.mjs import withPWAInit from "@ducanh2912/next-pwa"; const withPWA = withPWAInit({ dest: "public", disable: process.env.NODE_ENV === "development", register: true, skipWaiting: true, }); /** @type {import('next').NextConfig} */ const nextConfig = { // Next.jsの設定オプションをここに記述 }; export default withPWA(nextConfig); -
ウェブアプリマニフェストを作成。このJSONファイルは、ブラウザにPWAとその動作について指示します。
app/manifest.tsを作成します:typescript // app/manifest.ts import { MetadataRoute } from 'next' export default function manifest(): MetadataRoute.Manifest { return { name: 'Mood Journal PWA', short_name: 'MoodJournal', description: 'An offline-first mood journaling app.', start_url: '/', display: 'standalone', background_color: '#fff', theme_color: '#fff', icons: [ { src: '/icon-192x192.png', sizes: '192x192', type: 'image/png', }, { src: '/icon-512x512.png', sizes: '512x512', type: 'image/png', }, ], } }publicディレクトリに対応するicon-192x192.pngとicon-512x512.pngファイルを追加することを忘れないでください。アイコンジェネレーターを使用して作成できます。 -
ルートレイアウト
app/layout.tsxでマニフェストをリンク:tsx // app/layout.tsx import type { Metadata, Viewport } from "next"; import "./globals.css"; export const metadata: Metadata = { title: "Mood Journal", description: "An offline-first mood journaling app.", manifest: "/manifest.json", // マニフェストファイルへのリンク }; export const viewport: Viewport = { themeColor: "#FFFFFF", }; export default function RootLayout({ children, }: { children: React.ReactNode; }) { return ( <html lang="en"> <body>{children}</body> </html> ); }
動作の仕組み
next.config.mjsのwithPWAラッパーは、服务ワーカーファイル(sw.js)とWorkboxプリキャッシュマニフェストを作成する必要なビルドステップを注入します。アプリを本番用にビルドすると、この服务ワーカーはすべての静的アセット(HTML、CSS、JS、画像)を自動的にキャッシュします。マニフェストファイルにより、ユーザーはデバイスで「ホーム画面に追加」でき、appsのような体験が可能になります。
一般的な落とし穴
- 服务ワーカーは本番環境でのみ実行されます。設定
disable: process.env.NODE_ENV === "development"は、開発中のキャッシュの問題を避けるために重要です。 - HTTPSが必要です。服务ワーカーが正しく登録되고機能するためには、アプリケーションはHTTPS経由で提供される必要があります。Vercelなどのプラットフォームでは標準ですが、ローカルテストの場合、ブラウザは
localhostをセキュアコンテキストとして扱うことが多いです。
ローカルストレージ用のIndexedDBサービスを作成
何をするのか
IndexedDBとのすべてのやり取りを処理するシンプルなデータベースサービスを作成します。これにより、生のIndexedDB APIの複雑さが抽象化され、addEntryやgetAllEntriesなどのクリーンな非同期メソッドが提供されます。
入力:{ mood, notes, date }を含む気分エントリオブジェクト
出力:IndexedDBオブジェクトストアに自動増分されたidで保存されたレコード
実装
データベースロジックを収容するために、新しいファイルlib/db.tsを作成します。
// lib/db.ts
import { openDB, DBSchema } from 'idb';
const DB_NAME = 'MoodJournalDB';
const DB_VERSION = 1;
const STORE_NAME = 'mood-entries';
interface MoodEntry {
id: number;
mood: string;
notes: string;
date: string;
}
interface MoodJournalDB extends DBSchema {
'mood-entries': {
key: number;
value: MoodEntry;
indexes: { 'date': string };
};
}
async function getDb() {
return openDB<MoodJournalDB>(DB_NAME, DB_VERSION, {
upgrade(db) {
const store = db.createObjectStore(STORE_NAME, {
keyPath: 'id',
autoIncrement: true,
});
store.createIndex('date', 'date');
},
});
}
export async function addEntry(entry: Omit<MoodEntry, 'id'>) {
const db = await getDb();
return db.add(STORE_NAME, { ...entry, id: 0 }); // idは自動増分される
}
export async function getAllEntries() {
const db = await getDb();
return db.getAllFromIndex(STORE_NAME, 'date');
}
動作の仕組み
openDB:idbライブラリのこの関数は、データベースに接続します。データベースが存在しない場合、またはバージョンが既存のものより高い場合、upgradeコールバックがトリガーされます。upgradeコールバック:データベースの構造(スキーマ)を定義または変更できる唯一の場所以です。mood-entriesという名前の「オブジェクトストア」(SQLのテーブルに類似)を作成します。keyPathとautoIncrement:各オブジェクトが一意のidプロパティを持ち、自動的に管理してもらうようにIndexedDBに指示します。createIndex:dateプロパティにインデックスを作成します。これにより、日付でエントリを効率的にクエリおよびソートできます。addEntryとgetAllEntries:これらはパブリックメソッドです。DBに接続してオブジェクトストアでトランザクション(追加または取得)を実行します。idbライブラリは、これらをシンプルな1行の操作にします。
一般的な落とし穴
- スキーマの変更:データベーススキーマ(オブジェクトストアの追加、インデックスの追加)は、
upgradeコールバック内でのみ変更できます。トリガーするには、DB_VERSION定数を増分する必要があります。 - 非同期操作:すべてのIndexedDB操作は、ブラウザのメイスレッドをブロックしないように非同期です。DBサービスと対話 всегдаは
async/awaitを使用してください。
クライアントコンポーネントで気分日記UIを構築
何をするのか
気分エントリを作成、表示、一覧表示するためのReactコンポーネントを構築します。ブラウザAPI(IndexedDB)および状态と対話する必要があるため、クライアントコンポーネント('use client')を使用します。
入力:気分選択とメモのためのユーザーform入力 出力:リアルタイムのIndexedDB読み取り/書き込み操作を備えた対話型UI
実装
app/page.tsxファイルを modifyして、メインインターフェースにします。
// app/page.tsx
'use client';
import { useState, useEffect, FormEvent } from 'react';
import { addEntry, getAllEntries } from '../lib/db';
interface MoodEntry {
id: number;
mood: string;
notes: string;
date: string;
}
export default function HomePage() {
const [entries, setEntries] = useState<MoodEntry[]>([]);
const [mood, setMood] = useState('🙂');
const [notes, setNotes] = useState('');
const [loading, setLoading] = useState(true);
useEffect(() => {
async function fetchEntries() {
const allEntries = await getAllEntries();
setEntries(allEntries.reverse()); // 最新的を最初に表示
setLoading(false);
}
fetchEntries();
}, []);
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
if (!notes) return;
const newEntry = {
mood,
notes,
date: new Date().toISOString(),
};
await addEntry(newEntry);
// エントリリストを更新
const allEntries = await getAllEntries();
setEntries(allEntries.reverse());
// フォームをリセット
setMood('🙂');
setNotes('');
};
return (
<main className="container mx-auto p-4 max-w-2xl">
<h1 className="text-4xl font-bold text-center mb-8">My Mood Journal</h1>
<form onSubmit={handleSubmit} className="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
<div className="mb-4">
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="mood">
Select your mood
</label>
<select
id="mood"
value={mood}
onChange={(e) => setMood(e.target.value)}
className="text-2xl p-2 border rounded w-full"
>
<option>🙂</option>
<option>😊</option>
<option>😐</option>
<option>😕</option>
<option>😠</option>
</select>
</div>
<div className="mb-6">
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="notes">
What's on your mind?
</label>
<textarea
id="notes"
value={notes}
onChange={(e) => setNotes(e.target.value)}
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
placeholder="Write a few notes about your day..."
rows={4}
/>
</div>
<div className="flex items-center justify-between">
<button
type="submit"
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"
>
Save Entry
</button>
</div>
</form>
<section>
<h2 className="text-2xl font-semibold mb-4">Past Entries</h2>
{loading ? (
<p>Loading entries...</p>
) : entries.length > 0 ? (
<div className="space-y-4">
{entries.map((entry) => (
<div key={entry.id} className="bg-white p-4 rounded-lg shadow">
<div className="flex justify-between items-center mb-2">
<span className="text-3xl">{entry.mood}</span>
<span className="text-sm text-gray-500">
{new Date(entry.date).toLocaleString()}
</span>
</div>
<p className="text-gray-700">{entry.notes}</p>
</div>
))}
</div>
) : (
<p className="text-center text-gray-500">No entries yet. Add one above!</p>
)}
</section>
</main>
);
}
動作の仕組み
'use client':上部にあるこのディレクティブは、コンポーネントをクライアントコンポーネントとしてマークし、状態(useState)とエフェクト(useEffect)の使用を可能にします。- 状態管理:フォーム入力(
mood、notes)と日記エントリのリスト(entries)を管理するためにuseStateを使用します。 - データ取得:
useEffectで、コンポーネントが初めて読み込まれたときに日記リストを設定するためにlib/db.tsからgetAllEntries関数を呼び出します。 - フォーム送信:
handleSubmit関数はデフォルトのフォーム送信を防止し、新しいエントリオブジェクトを作成し、addEntryを使用してIndexedDBに保存し затем、リストを再取得してUIを更新します。
すべてを組み合わせ、オフライン機能をテスト
すべてのピースを構築しました。これで、オフライン機能をテストする時です。
本番ビルド
服务ワーカーを実際に動作させるには、アプリの 本番用ビルドを作成する必要があります。
npm run build
ビルドが完了したら、本番サーバーを起動します:
npm run start
テスト手順
- ブラウザ(ChromeまたはFirefoxを推奨、开发者ツールが非常に優れています)を開き、
http://localhost:3000にアクセスします。 - 開発者ツールを開きます。Applicationタブに移動します。
- Service Workersセクションの下で、「activated and is running」と表示されている服务ワーカースクリプト(
sw.js)が表示されるはずです。これにより、PWA設定が動作していることが確認されます。 - Storageセクションで、IndexedDBを展開します。
MoodJournalDBデータベースが表示されるはずです。 - オフラインモードをシミュレート:Service Workersタブで、「Offline」ボックスをチェックします。
- ページを再読み込み。アプリがキャッシュから瞬時に読み込まれるため、「オフライン」にもかかわらず приложение が動作します。
- 新しい気分エントリを追加。フォームに記入して「Save Entry」をクリックします。新しいエントリがリストに表示されるはずです。
- IndexedDBで確認:開発者ツールのIndexedDBセクションに戻ります。
mood-entriesオブジェクトストアをクリックして「Refresh」ボタンをクリックします。先ほど保存した新しいデータがそこに表示され、ローカルに保存されていることが証明されます。 - オンラインに戻る:「Offline」ボックスのチェックを外して再読み込みします。アプリは引き続き動作し、新しいエントリはそこにあります。✨
データ同期戦略
このアプリはまだオフラインでも完美に動作しますが、実世界のアプリケーションでは、接続が利用可能なときにこのデータをサーバーに同期する必要があります。実装方法の概要は次のとおりです:
syncedフラグを追加:IndexedDBのMoodEntryインターフェースにsyncedフラグを追加します。デフォルトはfalseとします。- Background Sync APIを使用:ユーザーがエントリを追加したとき、服務ワーカーに'sync'イベントを登録します。
- 服务ワーカーロジック:服务ワーカーはこの
syncイベントをlistenします。ブラウザが安定したインターネット接続を検出すると、イベントが発生しります。 - データの同期:同期イベントリスナー内で、服务ワーカーは次のようなことを行います:
syncedがfalseのIndexedDBからすべてのエントリを読み取る。- それらをサーバーAPIに送信する。
- サーバーからの正常な応答により、IndexedDB内のエントリを
syncedをtrueに設定して更新する。
これにより、ユーザーがアプリを開いている必要なく、データが確実に送信されます。
結論
おめでとうございます!Next.jsで真のオフラインファーストプログレッシブウェブアプリを正常に構築しました。服务ワーカーによるアセットキャッシングとIndexedDBによる堅牢なクライアントサイドデータストレージの活用方法を学び、耐障害性がありユーザーフレンドリーなアプリケーションを作成しました。
オフラインファーストの影響:服务ワーカ캐싱を備えたPWAは、2回目の訪問時に読み込み時間が95%高速化を達成します。IndexedDBストレージは、オフライン期間中に100%のデータ永続性を提供し、ユーザーのフラストレーションを排除します。Background Sync APIは、接続恢复時に自動データ同期を有効にし、99.9%のメッセージ配信信頼性を達成します。オフラインファースト機能を備えたアプリケーションは、接続が悪い地域でユーザーエンゲージメントが40%高く、バウンスレートが25%低いことがわかっています。
恢复力の高いヘルスアプリケーションの構築について詳しく見るには、**FastAPIでHIPAA準拠のデータパイプラインを構築するまたはReactでリアルタイム心拍ダッシュボードを構築するをご覧ください。React Nativeのオフラインストレージについては、WatermelonDBでオフラインファーストな睡眠トラッカーを構築する**をご覧ください。
リソース
- 公式Next.jsドキュメント:nextjs.org/docs
@ducanh2912/next-pwa:パッケージリポジトリidbライブラリ:GitHubリポジトリ- IndexedDBに関するMDN Webドキュメント:IndexedDB API
- Service Workersに関するMDN Webドキュメント:Service Worker API
クイックリファレンス
| 側面 | 主要情報 |
|---|---|
| トピック | 開発 |
| 関連性 | 患者教育と意識向上 |
| アクション | 医療プロバイダーと協議 |
| 頻度 | ケアチームが推奨する方法 |
| 証拠 | 現在の臨床ガイドラインに基づく |
よくある質問
Q: これが自分に該当するかどうかはどうすればわかりますか? A: 個人的な病歴と家族歴について医療プロバイダーと協議し、個人的なリスク要因を評価し、適切な次のステップを決定してください。
Q: 医師に何を聞くべきですか? A: 良い質問例:「結果はどういう意味ですか?」「オプションは何ですか?」「どんな生活習慣の変更が役に立ちますか?」「いつフォローアップすべきですか?」
Q: 帮助できる生活習慣の変更はありますか? A: ほとんどの健康状態は、基盤的な措置から好处を受けます:バランスの取れた栄養、定期的な運動(週150分以上)、質の高い睡眠(7〜9時間)、ストレス管理、タバコの回避。
医療に関する免責事項
この文章は教育目的のみを目的としており、医療上のアドバイス構成ものではありません。健康、治療、またはケアに関する決定する前に、必ず資格のある医療プロバイダーにご相談ください。個々の状況は異なり、一个人に適切なものが別の人に適切でない場合があります。
救急医療の状況が発生した場合は、直ちに119番(または地元の緊急番号)に電話してください。