【実践】PWAで作るクロスプラットフォーム対応のメモ帳アプリ

# オフライン対応のPWAメモ帳を作ってみよう


https://github.com/yonaka15/pwa-simple-notepad


モバイルファーストの時代において、ネイティブアプリケーションの代替として注目を集めているのがProgressive Web App（PWA）です。PWAは、Webの利点を活かしながらネイティブアプリに近い体験を提供する技術です。今回は、PWAの基本的な実装方法を学ぶために、オフライン対応のメモ帳アプリケーションを作成していきます。

![Screenshot 2025-01-21 at 23.35.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/79562/549774a2-b43f-c983-bff8-9728301b2c61.png)

## 想定する開発環境

- モダンブラウザ（Chrome, Edge, Safari, Firefox最新版）
- テキストエディタまたはIDE
- ローカル開発サーバー（Node.jsのhttp-serverやPythonの組み込みサーバーなど）

## プロジェクトの準備

まず、以下のようなフォルダ構造でプロジェクトを作成します：

```
notepad-pwa/
├── index.html          # メインのHTMLファイル
├── manifest.json       # PWAマニフェスト
├── sw.js              # Service Worker
├── app.js             # アプリケーションのメインロジック
├── styles.css         # スタイルシート
├── icon-192.png       # 192x192 サイズのアイコン（ホーム画面用）
└── icon-512.png       # 512x512 サイズのアイコン（アプリインストール用）
```

このフォルダ構造は、PWAの基本要件を満たすために必要な最小限のファイル構成となっています。各ファイルの役割は以下の通りです：

- `index.html`: アプリケーションのエントリーポイントとなるHTML
- `manifest.json`: PWAとしての設定を定義するマニフェストファイル
- `sw.js`: オフライン機能を実現するService Worker
- `app.js`: アプリケーションの主要な機能を実装するJavaScript
- `styles.css`: アプリケーションのスタイリングを定義するCSS
- `icons/`: ホーム画面に追加する際などに使用されるアプリケーションアイコン

## メモ帳アプリケーションの特徴

このメモ帳アプリケーションは、PWAの主要な機能を活用して、以下のような特徴を持つWebアプリケーションとして実装します：

- インターネット接続がない環境でも利用可能
- デバイスにインストールして単独のアプリケーションとして起動可能
- 入力内容の自動保存機能
- ダークモードとライトモードの切り替え対応
- スマートフォンからデスクトップまで、様々な画面サイズに対応

それでは、実装の詳細を見ていきましょう。

## アプリケーションの基本構造

まず、アプリケーションの基本となるHTML構造を作成します。PWAとして動作させるために必要なマニフェストファイルの参照やメタ情報を適切に設定します。

```html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta name="theme-color" content="#4a90e2">
    <link rel="apple-touch-icon" href="icon-192.png">
    <title>Simple Notepad</title>
    <link rel="manifest" href="/manifest.json">
    <link rel="stylesheet" href="styles.css">
</head>
<body>
    <div class="container">
        <div class="controls">
            <button id="toggleTheme">🌓</button>
            <button id="downloadNote">💾</button>
        </div>
        <textarea id="notepad" 
                  placeholder="メモを入力してください..." 
                  autofocus></textarea>
    </div>
    <script src="app.js"></script>
</body>
</html>
```

このHTMLでは、アプリケーションの基本的なユーザーインターフェースを定義しています。テーマの切り替えボタンと保存ボタン、そしてメインとなるテキストエリアを配置しています。また、PWAとして必要なマニフェストファイルの参照やアイコン設定も含まれています。

## レスポンシブなスタイリング

次に、アプリケーションのスタイリングを行います。CSSカスタムプロパティ（CSS変数）を活用することで、ダークモードへの対応を効率的に実装します。

```css
:root {
    --bg-color: #ffffff;
    --text-color: #333333;
    --control-bg: #f5f5f5;
}

[data-theme="dark"] {
    --bg-color: #1a1a1a;
    --text-color: #ffffff;
    --control-bg: #333333;
}

body {
    margin: 0;
    padding: 0;
    background-color: var(--bg-color);
    color: var(--text-color);
    transition: background-color 0.3s, color 0.3s;
}

.container {
    max-width: 800px;
    margin: 0 auto;
    padding: 20px;
}

.controls {
    margin-bottom: 20px;
    display: flex;
    gap: 10px;
}

button {
    padding: 8px 16px;
    background-color: var(--control-bg);
    border: none;
    border-radius: 4px;
    cursor: pointer;
    font-size: 1.2em;
}

textarea {
    width: 100%;
    min-height: 70vh;
    padding: 15px;
    border: 1px solid #ccc;
    border-radius: 4px;
    background-color: var(--bg-color);
    color: var(--text-color);
    font-size: 16px;
    line-height: 1.6;
    resize: vertical;
}

@media (max-width: 600px) {
    .container {
        padding: 10px;
    }
}
```

このCSSでは、カラーテーマの切り替えをカスタムプロパティで管理し、スムーズな遷移アニメーションを実装しています。また、モバイルデバイスでの使用を考慮したレスポンシブなデザインも取り入れています。

## アプリケーションロジックの実装

アプリケーションの中核となるJavaScriptの実装を行います。ここでは、クラスベースのアプローチを採用し、機能をモジュール化して管理しやすい構造を目指します。

```javascript
class Notepad {
    constructor() {
        this.textarea = document.getElementById('notepad');
        this.themeToggle = document.getElementById('toggleTheme');
        this.downloadBtn = document.getElementById('downloadNote');
        
        this.setupEventListeners();
        this.loadSavedContent();
        this.loadThemePreference();
    }

    setupEventListeners() {
        // テキストの自動保存
        this.textarea.addEventListener('input', () => {
            localStorage.setItem('noteContent', this.textarea.value);
        });

        // テーマの切り替え
        this.themeToggle.addEventListener('click', () => {
            document.documentElement.dataset.theme = 
                document.documentElement.dataset.theme === 'dark' 
                ? 'light' 
                : 'dark';
            localStorage.setItem('theme', document.documentElement.dataset.theme);
        });

        // メモのダウンロード
        this.downloadBtn.addEventListener('click', () => {
            const text = this.textarea.value;
            const blob = new Blob([text], { type: 'text/plain' });
            const url = URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.href = url;
            a.download = 'note.txt';
            a.click();
            URL.revokeObjectURL(url);
        });
    }

    loadSavedContent() {
        const savedContent = localStorage.getItem('noteContent');
        if (savedContent) {
            this.textarea.value = savedContent;
        }
    }

    loadThemePreference() {
        const savedTheme = localStorage.getItem('theme') || 'light';
        document.documentElement.dataset.theme = savedTheme;
    }
}

// PWAのセットアップ
if ('serviceWorker' in navigator) {
    window.addEventListener('load', () => {
        navigator.serviceWorker.register('./sw.js')
            .then(registration => {
                console.log('ServiceWorker登録成功:', registration);
            })
            .catch(error => {
                console.log('ServiceWorker登録失敗:', error);
            });
    });
}

// アプリケーションの初期化
const notepad = new Notepad();
```

このJavaScriptコードでは、LocalStorageを使用してメモの内容とテーマの設定を永続化し、ユーザーの好みを維持します。また、Service Workerの登録処理も含まれており、PWAとしての機能を有効にしています。

## オフライン対応の実装

PWAの重要な特徴の一つであるオフライン対応を実現するため、Service Workerを実装します。

```javascript
const CACHE_NAME = 'notepad-v1';
const urlsToCache = [
    '/',
    '/index.html',
    '/styles.css',
    '/app.js',
    '/manifest.json'
];

// インストール時にリソースをキャッシュ
self.addEventListener('install', event => {
    event.waitUntil(
        caches.open(CACHE_NAME)
            .then(cache => cache.addAll(urlsToCache))
    );
});

// フェッチ時のキャッシュ戦略
self.addEventListener('fetch', event => {
    event.respondWith(
        caches.match(event.request)
            .then(response => {
                if (response) {
                    return response;
                }
                return fetch(event.request)
                    .then(response => {
                        if (!response || response.status !== 200 || response.type !== 'basic') {
                            return response;
                        }
                        const responseToCache = response.clone();
                        caches.open(CACHE_NAME)
                            .then(cache => {
                                cache.put(event.request, responseToCache);
                            });
                        return response;
                    });
            })
    );
});
```

このService Workerは、アプリケーションのリソースをキャッシュし、オフライン時でもアプリケーションが動作するようにします。キャッシュ戦略として、「キャッシュファーストフォールバックトゥネットワーク」を採用しています。

## PWAの設定

アプリケーションをPWAとして認識させるため、マニフェストファイルを作成します。

```json
{
    "name": "Simple Notepad",
    "short_name": "Notepad",
    "start_url": "./",
    "display": "standalone",
    "background_color": "#ffffff",
    "theme_color": "#4a90e2",
    "icons": [
        {
            "src": "icon-192.png",
            "sizes": "192x192",
            "type": "image/png"
        },
        {
            "src": "icon-512.png",
            "sizes": "512x512",
            "type": "image/png"
        }
    ]
}
```

このマニフェストファイルにより、アプリケーションをデバイスにインストールした際の表示名やアイコン、表示モードなどが定義されます。

## 開発時の注意点

### セキュリティの考慮

1. HTTPSの利用
   - PWAの機能を完全に活用するためには、HTTPSでの提供が必要です
   - 開発時はlocalhostでも動作しますが、本番環境ではHTTPSは必須です

2. コンテンツセキュリティポリシー
   - 必要に応じて適切なCSPヘッダーを設定し、XSSなどの攻撃を防ぐ必要があります

### ブラウザ対応

- iOS Safari特有の対応
  - PWAのインストールプロンプトが自動で表示されない
  - `apple-touch-icon`の設定が必要

- 古いブラウザへの対応
  - Service WorkerやPWA機能が利用できない環境でも、基本的な機能は動作するようにフォールバック処理を実装することを推奨

## デバッグとテスト

アプリケーションの動作確認には、以下の手順を実行します：

1. ローカルサーバーの起動
```bash
# Node.jsの場合
npx http-server

# Pythonの場合
python -m http.server
```

2. ブラウザの開発者ツールでの確認
   - Application > Service Workers: 登録状態の確認
   - Application > Manifest: PWA設定の確認
   - Network > Offline: オフライン動作の確認

#### 重要な視覚的な確認ポイント

Chrome/Edgeブラウザでは、PWAが正しく構成されている場合、以下の表示が確認できます：

- アドレスバーの右側に「インストール」アイコンが表示されます
  - PCの場合：🔽のようなアイコンが表示され、クリックするとアプリをインストール可能
  - モバイルの場合：「ホーム画面に追加」のオプションが表示

このインストールアイコンの表示は、アプリケーションが以下の要件を満たしている証です：
- 有効なmanifest.jsonが存在する
- Service Workerが正しく登録されている
- 必要なアイコンが適切なサイズで提供されている
- HTTPSまたはlocalhostでアクセスされている

インストールアイコンが表示されない場合は、以下を確認してください：
1. manifest.jsonのパスが正しいか
2. アイコンファイル（icon-192.png, icon-512.png）が存在するか
3. Service Workerが正しく登録されているか（DevToolsのConsoleで確認）

## まとめ

このチュートリアルでは、PWAの基本機能を活用したメモ帳アプリケーションの実装を通じて、以下の点について学びました：

- PWAの基本概念と実装方法
- Service Workerによるオフライン対応の実現
- LocalStorageを使用したデータの永続化
- レスポンシブデザインとダークモードの実装

PWAは、Webの利点を活かしながらネイティブアプリに近い体験を提供できる優れた技術です。今回の実装例をベースに、さらなる機能の追加や改善を行うことで、より実用的なアプリケーションを作ることができます。

## 参考資料

- [MDN Progressive Web Apps](https://developer.mozilla.org/ja/docs/Web/Progressive_web_apps)
- [Service Worker API](https://developer.mozilla.org/ja/docs/Web/API/Service_Worker_API)
- [Web App Manifest](https://developer.mozilla.org/ja/docs/Web/Manifest)

※ コードの動作確認は各自の環境で行い、必要に応じて調整してください。また、本番環境へのデプロイ時には、適切なセキュリティ対策を実施することを忘れないようにしましょう。