1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

0. はじめに

この記事は、初学者を対象にPWAの基本的な実装についてまとめています。

1. PWAとは

「PWAとはGoogleが提唱するアークテクチャの一種で、Webアプリでネイティブアプリと同等またはそれ以上のユーザ体験を提供することを目的としています。」(@syunyamaさんのQiita記事「PWAとは?」より)

2. PWA実装:基本型

このPWA実装において、想定するディレクトリを以下のように仮定します。

ディレクトリ構成
// 他のディレクトリとファイルは省略
app
├─ public
│  ├─ index.html
│  ├─ manifest.json     ←この後作成
│  └─ serviceworker.js  ←この後作成
├─ src
│  └─ scss
│     └─ style.scss
└─ App.js

PWA実装において、最も重要なのはService WorkerファイルとManifestファイルの作成となります。

2.1. Service Workerファイルの作成

この記事ではService Workerファイルをserviceworker.jsという名前で作成します。本質的にはファイル名は自由に設定できますが、慣習でservice_worker.jssw.jsなど含めて3パターンのいずれかで作成されるようです。また、今回serviceworker.jspublicフォルダ内に作成しています。

serviceworker.jsの場所については資料によって記述が異なりますが、できるだけトップディレクトリに配置しておくことが推奨されたり、index.thmlと同じディレクトリに配置しているケースが多い印象です。

※Service Workerを作成するツールとして、Googleの開発したWorkboxがありますが、今回は使いませんでした。

それでは、serviceworker.jsを作成していきます。以下のように実装します。
※無駄にコメントが多いので、用いる際は不要なコメントを削除してください。

serviceworker.js
/* eslint-disable no-restricted-globals */

// キャッシュ名の定義(バージョン管理)
var cacheName = 'sample-cache-v1';

// キャッシュするリソースのリスト
var filesToCache = [
  '/',
  // App Shellモデル(ルート相対パスで記述)
  '/index.html',  // エントリーポイント(HTML)
  '/style.scss',  // プロジェクト全体のスタイル(CSS)
  '/App.js'       // ルートコンポーネント(JavaScript)
];

// installイベント
self.addEventListener('install', function (event) {
  console.log('Service Worker installing.');
  // キャッシュを作成
  event.waitUntil(          // インストール処理完了までService Workerのインストールを待機
    caches.open(cacheName)  // 指定したキャッシュ名でキャッシュを開く
      .then(function (cache) {  
        console.log('Opened cache');
        return cache.addAll(filesToCache);  // filesToCacheをキャッシュに保存する
      })
  );
});

// activateイベント
self.addEventListener('activate', function (event) {
  console.log('Service Worker activating.');
  // 古いキャッシュの削除
  event.waitUntil(  // アクティベート処理完了までService Workerのインストールを待機
    caches.keys()   // ブラウザに保存されているすべてのキャッシュ名のリストを取得
      .then(function (cacheNames) {
        return Promise.all(
          cacheNames.map(function (thisCacheName) {
            // 現在のキャッシュ名と異なる場合
            if (thisCacheName !== cacheName) {
              console.log('Service Worker removing old cache.', thisCacheName);
              return caches.delete(thisCacheName);  // 現在のキャッシュを削除
            }
          })
        );
      })
  );
});

// fetchイベント
self.addEventListener('fetch', function (event) {
  console.log('Service Worker fetching.', event.request.url);
  // リクエスト処理(オフライン・オンライン両方)
  event.respondWith(  // 指定したレスポンスをブラウザに返すように指示
    caches.match(event.request)  // リクエストされたリソースがキャッシュに存在するか調べる
      .then(function (response) {
        // キャッシュがあればそれを返し、なければネットワークから取得
        return response || fetch(event.request);
      })
  );
});

App Shellモデルについて

2.1.1. addEventListener関数

Service Workerファイルにおいては3つのイベント「install」「activate」「fetch」に対して、それらを処理するファンクションを登録します。

// 基本構文
addEventListener(type, listener, 省略可能な引数);

2.1.1.1. installイベント

installイベントは、Service Worker自体をブラウザー環境へインストールする際に発生するイベントです。これによってService Workerは一種の常駐プログラムとなり、オフラインでも動作するようになります。また、同じイベント処理の中でキャッシュすべきアプリ本体のファイルもキャッシュに格納します。それによってオフラインでも起動可能となります。

2.1.1.2. activateイベント

activateイベントは、Service Workerがアクティブな状態になったとき(起動した時)に発生するイベントです。Service Workerが古いバージョンから新しものに更新された後で起動した場合には、それに伴って古いキャッシュの内容が不要となるため、キャッシュのキーを調べて、それが最新のService Workerのキャッシュの名前と一致していなければ、そのキーのキャッシュを削除します。

2.1.1.3. fetchイベント

fetchイベントは、PWAがウェブ上のリソースをリクエストすると発生するイベントです。これはPWAがオフラインでも動作するための中核となる仕組みです。ただ今回紹介しているこの基本型では、特に外部からデータを読み込むことはないため、アップシェルの構成要素であるHTML, JavaScript, CSSリクエストに対する処理に割り込んで、リクエストされているファイルがキャッシュにあれば、とりあえずそれを返すようにしています。キャッシュにないリソースは、通常通りネットワークからフェッチします。

2.2. Manifestファイルの作成

Manifestファイル(manifest.json)を作成するにあたり、ファイル内にて以下のキーを設定します。

キー 説明
short_name アプリケーションの略称
name アプリケーションの正式名称
icons アプリケーションのアイコン
src : アイコン画像のパス
sizes : アイコンのピクセルサイズ(幅x高さ)
type : アイコン画像のMIMEタイプ
start_url アプリケーション起動時に最初にロードされるURL
通常ルートディレクトリを指定
display アプリケーションの表示UIを指定
standalone : ネイティブアプリのように表示する
fullscreen : 全画面で表示する
browser : ブラウザー表示と同じ
minimal-ui : standaloneのようにネイティブアプリのように表示されるが、最低限のナビゲーションUI表示となる
background_color アプリケーションのUI要素のテーマカラーを指定
theme_color スプラッシュ画面などで使用される背景色を指定

※MIMEタイプ:Web上でのデータの形式を表すもの。
※ スプラッシュ画面:起動時の処理を行っている時に画面上で表示される画面

manifest.json
{
  "short_name": "PWA sample",
  "name": "React PWA sample",
  "icons": [
    {
      "src": "./assets/img/favicon.png",
      "sizes": "100x100",
      "type": "image/png"
    }
  ],
  "start_url": ".",
  "display": "standalone",
  "theme_color": "#000000",
  "background_color": "#ffffff"
}

2.3. index.htmlの修正

Service WorkerファイルとManifestファイルの作成が完了したら、最後にindex.htmlにそれぞれのファイルに対応する実装を追加します。

2.3.1. Manifestについて

<head>要素の中に<link>タグを使って以下の実装を追加してください。

index.html
<head>
  <link rel="manifest" href="/manifest.json">
</head>

2.3.2. Service Workerについて

<body>要素の中に、<script>タグを使って以下の実装を追加してください。

index.html
<body>
  <script type="application/javascript">
    if ('serviceWorker' in navigator) {
      navigator.serviceWorker
        // 指定したJavaScriptファイルをブラウザにインストールおよび登録する
        .register('./serviceworker.js')
        .then(function () {
          console.log('Service Worker Registered');
        });
    }
  </script>
</body>

以上より、ターミナルで開発環境を立ち上げれば次のようにインストール項目が表示されるようになります。
※以下の画像は上記の実装による結果ではなくQiitaページを表示したときのものですが、ご覧の通りQiitaはPWA機能が実装されています。

48DD4780-EA7C-41C6-8004-209EC20E1830_1_201_a.jpeg

しかし、Service Workerファイルのキャッシュに記載するリソースが、必ずしもプロジェクト内に揃っているとは限りません。次はそういった別のパターンでの実装について紹介します。

3. その他のPWA実装(next.js)

ReactベースのフルスタックフレームワークであるNext.jsでは、プロジェクト作成時だと、上記と同じApp Shellモデルに基づくファイル(HTML, CSS, JavaScript)がありませんが、それと同等のファイルがあります。以下ではそれらのファイルを用いたPWAの実装を試みます。

※Next.jsにはPWAを実装するためのパッケージnext-pwaがありますが、今回は使わずに実装します。next-pwaを用いた実装方法が知りたい方は、@Coa3さんの記事「Next.js14でPWA構築してみた」をご確認ください。

3.1. プロジェクト作成

まずはPWAを実装するためのプロジェクトを用意します。

3.1.1. 環境準備

  • OS: macOS Sequoia 15.5
  • react: 19.1.0
  • next: 15.5.4

以下のコマンドでプロジェクトを作成します。

ターミナル
% npx npx create-next-app@latest --ts pwa-sample

プロジェクト作成時の質問は以下のように設定しました。

982BF170-380B-4695-A02D-5D9FA74E4DA8_4_5005_c.jpeg

# 作成したプロジェクト内に移動
% cd pwa-sample
# 起動
% npm run dev

起動すると以下の画面が表示されます。

スクリーンショット 2025-10-08 11.50.22.png

これでPWA実装環境が整いました。

3.2. Service Workerファイルの作成

まずは基本形と同じようにService Workerファイル(serviceworker.js)を作成します。
キャッシュするリソースのリストはルートパスとglobal.csslayout.tsxのみとなります。リソースのリスト以外のイベント処理(addEventListener)は、基本型と同じものになります。

public/serviceworker.js
/* eslint-disable no-restricted-globals */

// キャッシュ名の定義
var cacheName = 'next-sample-cache-v1';

// キャッシュするリソースのリスト
var filesToCache = [
  '/',            // ルートパス(page.tsx)
  '/global.css',  // プロジェクト全体のスタイル
  '/layout.tsx',  // ルートレイアウト
];

// installイベント
self.addEventListener('install', function (event) {
  console.log('Service Worker installing.');
  // キャッシュを作成
  event.waitUntil(
    caches.open(cacheName).then(function (cache) {
      console.log('Opened cache');
      return cache.addAll(filesToCache);
    })
  );
});

// activateイベント
self.addEventListener('activate', function (event) {
  console.log('Service Worker activating.');
  // 古いキャッシュを削除
  event.waitUntil(
    caches.keys().then(function (cacheNames) {
      return Promise.all(
        cacheNames.map(function (thisCacheName) {
          if (thisCacheName !== cacheName) {
            console.log('Service Worker removing old cache.', thisCacheName);
            return caches.delete(thisCacheName);
          }
        })
      );
    })
  );
});

// fetchイベント
self.addEventListener('fetch', function (event) {
  console.log('Service Worker fetching.', event.request.url);
  // リクエスト処理
  event.respondWith(
    caches.match(event.request)
      .then(function (response) {
        // キャッシュがあればそれを返し、なければネットワークから取得
        return response || fetch(event.request);
      })
  );
});

3.3. Manifestファイルの作成

次に、Manifestファイル(manifest.json)の作成を行います。こちらはプロジェクト作成時の情報を参考にiconの設定を行います。nameshort_nameキーの設定をプロジェクトに合わせて変更すれば、それ以外の記述は基本型のものをそのまま利用しています。

public/manifest.json
{
  "short_name": "Next-sample",
  "name": "Next-sample-PWA",
  "icons": [
    {
      "src": "../src/app/favicon.ico",
      "sizes": "100x100",
      "type": "image/x-icon"
    }
  ],
  "start_url": ".",
  "display": "standalone",
  "theme_color": "#000000",
  "background_color": "#ffffff"
}

3.4. layout.tsx

ここからが基本型と大きく異なるポイントです。

基本型のindex.htmlの代わりに、layout.tsxにService WorkerファイルとManifestファイルに対応する実装をしますが、Service Workerに対応する実装の<script>タグは、TSX内に書けないためコンポーネント化します。

src直下にcomponentsディレクトリを作成し、以下のファイルを作成します。

src/components/ServiceWorkerRegistrar.tsx
"use client"; // クライアントコンポーネントであることを宣言

import { useEffect } from "react";

export default function ServiceWorkerRegistrar() {
  useEffect(() => {
    // コンポーネントがマウントされた後にブラウザでのみ実行される
    if ("serviceWorker" in navigator) {
      navigator.serviceWorker
        .register("/serviceworker.js")
        .then(() => {
          console.log("Service Worker Registered");
        })
        .catch((error) => {
          console.error("Service Worker registration failed:", error);
        });
    }
  }, []);

  return null;
}

そしてlayout.tsxに作成したServiceWorkerRegistrarコンポーネント<ServiceWorkerRegistrar />を実装します。なお、Manifestファイルに対応する実装<link rel="manifest" href="/manifest.json"></link><head>要素に配置してください。

layout.tsx
import ServiceWorkerRegistrar from "@/components/ServiceWorkerRegistrar";

// 間の記述は省略

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <head>
        <link rel="manifest" href="/manifest.json"></link>
      </head>
      <body
        className={`${geistSans.variable} ${geistMono.variable} antialiased`}
      >
        {children}
        <ServiceWorkerRegistrar />
      </body>
    </html>
  );
}

以上より、開発画面で以下のようにインストール項目が表示されていればPWA実装は成功です。

B9F9080E-771B-40C9-8056-5C6C9C162D9E_1_201_a.jpeg

開発ツールを使えば、より細かく確認することができます。

スクリーンショット 2025-10-09 0.45.08.png

実際にアプリとしてインストールした結果がこちらです。

スクリーンショット 2025-10-09 0.46.28.png

さいごに

今回はPWAの基本的な実装方法について書きました。実装方法について知りたい方の一助になれば幸いです。また、間違いなどがありましたらご指導ご鞭撻いただけますと幸いです。

参考資料

記事作成にあたり、以下を参考にさせて頂きました。心より感謝申し上げます。

書籍

  • 柴田文彦.プログレッシブウェブアプリ PWA開発入門.インプレスR&D,2018

Webサイト

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?