はじめに
縦書きの名言画像を作成するPWAアプリ「ことのはっと」に、新しい機能を追加しました。今回実装したのは、名言に作者名を添える機能と、前回の入力内容を自動復元する機能です。さらに、開発中に発生したバージョン管理の課題も解決しました。
この記事では、実際の開発過程で直面した問題と解決策を、コードと一緒に記録します。
実装した機能の概要
今回追加した主な機能は以下の通りです。
- 名言に作者名を添える機能では、モーダルに新しい入力欄を追加し、縦書きレイアウトで控えめに表示します。
- 自動復元機能では、アプリを閉じても前回の入力内容が保持され、次回起動時に自動的に復元されます。
- また、開発中にPWAのキャッシュが更新されない問題が発生したため、バージョン管理システムも改善しました。
作者欄機能の実装
モーダルのHTML構造の変更とスタイルの調整
まずモーダル内に作者名の入力欄を追加しました。
<div class="input-section">
<textarea id="textInput" class="text-input"
placeholder="ここに名言を入力してください 改行で縦の列が分かれます"></textarea>
<input id="authorInput" class="author-input"
placeholder="作者名(省略可)"
maxlength="20">
</div>
Code language: JavaScript (javascript)
作者名は省略可能とし、文字数制限を20文字に設定しました。これは縦書きレイアウトでの表示バランスを考慮した設定です。
作者入力欄には、テキスト入力欄と統一感のあるスタイルを適用しました。
.text-input {
width: 100%;
min-height: 200px;
border: 2px solid var(--color-border);
border-radius: var(--border-radius);
padding: var(--spacing-md);
font-family: inherit;
font-size: 1rem;
line-height: 1.6;
resize: vertical;
transition: border-color 0.2s ease;
margin-bottom: var(--spacing-sm);
}
.author-input {
width: 100%;
border: 2px solid var(--color-border);
border-radius: var(--border-radius);
padding: var(--spacing-sm);
font-family: inherit;
font-size: 1rem;
line-height: 1.4;
transition: border-color 0.2s ease;
}
Code language: CSS (css)
作者入力欄のフォントサイズを1remに設定したのは、iOS Safariの自動ズーム機能を無効化するためです。16px未満のフォントサイズのinput要素をタップすると、iOS Safariが自動的にズームしてしまいます。これを防ぐには、フォントサイズを16px以上にしました。
アプリケーション側の制御
JavaScriptでは、現在のテキストと作者名を管理する変数を追加します。
constructor() {
this.currentText = '';
this.currentAuthor = '';
this.currentGradient = '純白';
this.currentColor = '#1a1a1a';
// ...
}
Code language: JavaScript (javascript)
DOM要素の取得処理にも作者入力欄を追加します。
this.elements = {
// 既存の要素...
authorInput: document.getElementById('authorInput'),
// ...
};
Code language: JavaScript (javascript)
リアルタイム更新のイベントリスナーも設定します。
// 作者入力リアルタイム更新
this.elements.authorInput.addEventListener('input', () => {
this.currentAuthor = this.elements.authorInput.value;
this.saveLastInput(); // 自動保存
this.render();
});
Code language: JavaScript (javascript)
縦書きでの作者表示
作者名は名言本文の最後の列に、控えめに表示します。Canvas描画エンジンに作者専用の描画メソッドを追加しました。
drawAuthorLine(line, x, fontSize, availableHeight) {
if (!line || line.length === 0) return;
const authorFontSize = fontSize * 0.8; // 0.8em
const authorCharHeight = authorFontSize * (this.charSpacing + 0.2); // 行間を0.2em追加
this.ctx.save();
this.ctx.font = `500 ${authorFontSize}px ${this.fontFamily}`; // フォントウェイト500
this.ctx.globalAlpha = 0.8; // アルファ値0.8
// 下揃え配置
const totalHeight = line.length * authorCharHeight;
const startY = this.height - this.padding.bottom - totalHeight + authorFontSize / 2;
line.forEach((char, charIndex) => {
const y = startY + charIndex * authorCharHeight;
if (y >= this.padding.top && y <= this.height - this.padding.bottom) {
this.drawCharacter(char, x, y, authorFontSize);
}
});
this.ctx.restore();
}
Code language: JavaScript (javascript)
作者名は本文より小さく(0.8倍)、薄く(透明度0.8)、フォントウェイトも控えめ(500)に設定しています。また、下揃えで配置することで、名言の最後に署名のように表示されます。
自動復元機能の実装
LocalStorageを使った状態保存と起動時の自動復元
前回の入力内容を使うことも多いので、LocalStorageに保存することにしました。JSON形式でテキストと作者名をセットで保存します。
saveLastInput() {
try {
const data = {
text: this.currentText,
author: this.currentAuthor,
timestamp: new Date().toISOString()
};
localStorage.setItem('kotono_hat_last_input', JSON.stringify(data));
} catch (error) {
console.warn('入力内容の保存に失敗しました:', error);
}
}
Code language: JavaScript (javascript)
LocalStorageの容量制限やブラウザの設定によってエラーが発生する可能性があるため、try-catch文でエラーハンドリングを行います。
アプリ起動時に、保存されたデータを取得して復元します。
restoreLastInput() {
const lastInput = this.getLastInput();
if (lastInput) {
this.currentText = lastInput.text;
this.currentAuthor = lastInput.author;
}
}
getLastInput() {
try {
const stored = localStorage.getItem('kotono_hat_last_input');
if (stored) {
const data = JSON.parse(stored);
if (data && typeof data.text === 'string') {
return {
text: data.text,
author: data.author || ''
};
}
}
} catch (error) {
console.warn('前回の入力内容の復元に失敗しました:', error);
}
return null;
}
Code language: JavaScript (javascript)
データの妥当性チェックを行い、不正なデータの場合は無視します。作者名が存在しない古いデータとの互換性も考慮しています。
リアルタイム保存の実装
ユーザーが入力中にブラウザを閉じてしまっても内容が失われないよう、複数のタイミングで自動保存を実行します。
// ページ離脱時の自動保存
window.addEventListener('beforeunload', () => {
this.saveLastInput();
});
// ページの可視性変更時の自動保存
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
this.saveLastInput();
}
});
Code language: JavaScript (javascript)
beforeunloadイベントはページを離れる前に発火し、visibilitychangeイベントはタブが非アクティブになった時に発火します。この二重の仕組みで、様々な状況での自動保存を実現しています。
履歴管理機能の拡張
データ構造の変更
既存の履歴管理システムに作者情報を追加します。後方互換性を保つため、作者情報は省略可能な設計にしました。
addToHistory(text, author = '') {
if (!text || typeof text !== 'string' || !text.trim()) {
return;
}
const trimmedText = text.trim();
const trimmedAuthor = author ? author.trim() : '';
const history = this.getHistory();
const newItem = {
id: this.generateId(),
text: trimmedText,
author: trimmedAuthor,
timestamp: new Date().toISOString()
};
history.unshift(newItem);
// ...
}
Code language: JavaScript (javascript)
既存データとの互換性
古いバージョンで保存されたデータには作者情報が含まれていません。データ移行処理で、古いデータに空の作者情報を追加します。
migrateOldData() {
try {
const stored = localStorage.getItem(this.storageKey);
if (!stored) return;
const data = JSON.parse(stored);
const migratedData = data.map(item => {
if (typeof item === 'string') {
return {
id: this.generateId(),
text: item,
author: '',
timestamp: new Date().toISOString()
};
}
if (item && typeof item === 'object' && item.id && item.text && item.timestamp) {
if (!item.hasOwnProperty('author')) {
item.author = '';
}
return item;
}
return null;
}).filter(item => item !== null);
this.saveHistory(migratedData);
} catch (error) {
console.warn('データ移行に失敗しました:', error);
this.clearHistory();
}
}
Code language: JavaScript (javascript)
データ移行に失敗した場合は、安全のため履歴をクリアします。これにより、アプリが正常に動作することを保証します。
PWAのキャッシュ更新問題と解決策
問題の発生
機能追加後にCSSファイルを更新しても、既存ユーザーのブラウザでスタイルが反映されない問題が発生しました。これはPWAのService Workerがファイルをキャッシュしているためです。
PWAでは、一度キャッシュされたファイルは、Service Workerのキャッシュ名が変更されるまで更新されません。つまり、CSSやJavaScriptファイルを更新しても、ユーザーには古いバージョンが表示され続けます。
バージョン管理の課題と一元管理システムの構築
当初のバージョン管理では、複数のファイルにバージョン情報が散らばっていました。
- Service Worker(sw.js)のキャッシュ名
- manifest.jsonのアプリバージョン
- HTMLファイルのCSSクエリパラメータ
バージョンアップの度に、これらすべてを手動で更新する必要があり、更新漏れが発生しやすい状況でした。この問題を解決するため、バージョン情報を一元管理するシステムを構築しました。
まず、バージョン設定専用のファイルを作成します。
// version-config.js
const VERSION_CONFIG = {
APP_VERSION: '1.6.6.22', // アプリ全体のバージョン
CACHE_VERSION: '22', // キャッシュ用(シンプルな連番)
CSS_VERSION: '22', // CSS用
BUILD_DATE: '2025-06-08' // ビルド日付
};
if (typeof window !== 'undefined') {
window.VERSION_CONFIG = VERSION_CONFIG;
}
if (typeof module !== 'undefined') {
module.exports = VERSION_CONFIG;
}
Code language: JavaScript (javascript)
Service Workerでは、importScriptsを使ってバージョン設定を読み込みます。
// sw.js
importScripts('version-config.js');
const CACHE_NAME = `kotono-hat-v${VERSION_CONFIG.CACHE_VERSION}`;
const STATIC_CACHE_NAME = `kotono-hat-static-v${VERSION_CONFIG.CACHE_VERSION}`;
const DYNAMIC_CACHE_NAME = `kotono-hat-dynamic-v${VERSION_CONFIG.CACHE_VERSION}`;
const STATIC_FILES = [
'/',
'/index.html',
`/styles.css?v=${VERSION_CONFIG.CSS_VERSION}`,
'/js/app.js',
// ...
];
Code language: JavaScript (javascript)
HTMLファイルでは、動的にCSSファイルのクエリパラメータを設定します。
<!-- index.html -->
<script src="version-config.js"></script>
<script>
document.write(`<link rel="stylesheet" href="styles.css?v=${VERSION_CONFIG.CSS_VERSION}">`);
</script>
Code language: HTML, XML (xml)
このシステムにより、version-config.jsの数値を更新するだけで、すべてのキャッシュが無効化され、新しいファイルが配信されます。
UI改善の細かな工夫
あとは細かな気付いたUIの不自然さを修正しました。
ヘッダーデザインの調整
アプリの用途をより明確にするため、ヘッダーにサブタイトルを追加しました。
<header class="app-header">
<div class="app-icon"></div>
<div class="app-subtitle">縦書きメーカー</div>
<h1 class="app-title">ことのはっと</h1>
<button class="header-share-button" id="shareButton">
<!-- SVGアイコン -->
</button>
</header>
Code language: HTML, XML (xml)
サブタイトルは小さく控えめに表示し、メインタイトルのサイズも調整してバランスを改善しました。
モバイルでのスクロール制御
スマートフォンでテキスト入力時に画面が自動スクロールした後、モーダルを閉じても元の位置に戻らない問題がありました。この問題は、モーダルを閉じる際にスクロール位置を上に戻す処理で解決しました。
closeModal() {
this.elements.modalOverlay.classList.remove('active');
this.scrollToTop();
}
scrollToTop() {
window.scrollTo({
top: 0,
left: 0,
behavior: 'smooth'
});
// iOS Safari対応
document.body.scrollTop = 0;
document.documentElement.scrollTop = 0;
}
Code language: JavaScript (javascript)
スムーススクロールを使用することで、自然な動きでヘッダーが見える位置まで戻ります。
開発で得られた知見
PWAのキャッシュ戦略
PWAにおけるキャッシュ管理は、ユーザー体験に直結する重要な要素です。適切なバージョン管理システムなしには、ユーザーが最新の機能を利用できない状況が発生します。
今回構築したバージョン管理システムは、以下の利点があります。単一ファイルの更新で全体のキャッシュを制御でき、更新漏れによるバグを防げます。また、バージョン番号の管理が簡潔になり、デバッグ時にどのバージョンが動作しているかを容易に確認できます。
iOS Safariの特殊な動作
モバイルWebアプリを開発する際は、iOS Safariの独特な動作に注意が必要です。16px未満のフォントサイズの入力欄をタップすると自動ズームが発生する問題や、キーボード表示時のビューポート変更など、様々な特殊ケースがあります。
これらの問題は、適切な対処法を知っていれば比較的簡単に解決できます。
まとめ
今回の開発では、作者欄追加と自動復元機能の実装を通じて、PWAアプリの機能拡張手法を実践しました。特にバージョン管理システムの一元化により、開発効率と保守性が大幅に向上しました。
実装した主要な機能は、作者名を縦書きで控えめに表示する機能、前回の入力内容を自動復元する機能、そしてバージョン情報を一元管理するシステムです。また、iOS Safariの自動ズーム防止やスクロール位置制御など、モバイルユーザビリティの改善も行いました。
PWAにおけるキャッシュ管理とデータ移行の重要性、そしてモバイル環境での特殊な動作への対処法についても、実践的な知見を得ることができました。