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.js
、sw.js
など含めて3パターンのいずれかで作成されるようです。また、今回serviceworker.js
はpublic
フォルダ内に作成しています。
serviceworker.jsの場所については資料によって記述が異なりますが、できるだけトップディレクトリに配置しておくことが推奨されたり、index.thml
と同じディレクトリに配置しているケースが多い印象です。
※Service Workerを作成するツールとして、Googleの開発したWorkboxがありますが、今回は使いませんでした。
それでは、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モデルについて
- PWAを構築するモデルの一つで、webアプリのUIが機能する最小限の3つのファイル(HTML, CSS, JavaScript)のこと
- 参考資料: AppShellモデル (プログレッシブ ウェブアプリ)
2.1.1. addEventListener関数
Service Workerファイルにおいては3つのイベント「install」「activate」「fetch」に対して、それらを処理するファンクションを登録します。
// 基本構文
addEventListener(type, listener, ※省略可能な引数);
-
type
: 対象となるイベントの種類を表す文字列 -
listener
: 指定された種類のイベントが発生するときに通知を受けるオブジェクト - 参考資料: addEventListener関数の公式ドキュメント
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上でのデータの形式を表すもの。
※ スプラッシュ画面:起動時の処理を行っている時に画面上で表示される画面
{
"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>
タグを使って以下の実装を追加してください。
<head>
<link rel="manifest" href="/manifest.json">
</head>
2.3.2. Service Workerについて
<body>
要素の中に、<script>
タグを使って以下の実装を追加してください。
<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機能が実装されています。
しかし、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
プロジェクト作成時の質問は以下のように設定しました。
# 作成したプロジェクト内に移動
% cd pwa-sample
# 起動
% npm run dev
起動すると以下の画面が表示されます。
これでPWA実装環境が整いました。
3.2. Service Workerファイルの作成
まずは基本形と同じようにService Workerファイル(serviceworker.js)を作成します。
キャッシュするリソースのリストはルートパスとglobal.css
とlayout.tsx
のみとなります。リソースのリスト以外のイベント処理(addEventListener)は、基本型と同じものになります。
/* 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の設定を行います。name
、short_name
キーの設定をプロジェクトに合わせて変更すれば、それ以外の記述は基本型のものをそのまま利用しています。
{
"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ディレクトリを作成し、以下のファイルを作成します。
"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>
要素に配置してください。
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実装は成功です。
開発ツールを使えば、より細かく確認することができます。
実際にアプリとしてインストールした結果がこちらです。
さいごに
今回はPWAの基本的な実装方法について書きました。実装方法について知りたい方の一助になれば幸いです。また、間違いなどがありましたらご指導ご鞭撻いただけますと幸いです。
参考資料
記事作成にあたり、以下を参考にさせて頂きました。心より感謝申し上げます。
書籍
- 柴田文彦.プログレッシブウェブアプリ PWA開発入門.インプレスR&D,2018
Webサイト
- wikipedia:プログレッシブウェブアプリ
- PWA対応で実装したことまとめ (実物あり)
- 【Task-RPG】サービスワーカーの追加
- Service Workerによるキャッシュ戦略の種類
- 【JavaScript】addEventListenerの使い方
- 【React】manifest.jsonについて
- キャッシュについて、わかりやすくまとめてみた
- AppShellモデル (プログレッシブ ウェブアプリ)
- MIMEタイプって何者?
- display: standaloneとfullscreenの違いは?【PWA】
- IT用語辞書:スプラッシュ画面
- Next.js14でPWA構築してみた
- Vue.jsで始めるPWA
- Service Worker作成ツール
- manifest作成ツール