mozikeru.com

和歌山在住。2児の父。IoTエンジニア。各種ご相談承ります。

「自分が読んで興味深かったWikipediaの記事をひたすらおすすめするアプリ」を作りましたという話

昔から、読んだWikipdiaの記事のうち、「興味深い」「これはなかなかマニアック」「面白い」と思った記事タイトルをひたすらEvernoteにため込むという事をやっている。

その記事数がついに1400を超えたので何らかの形でこれを世に出したいな、伝えたいなとは思っていたのだが、如何せん自分が面白いと思ったというだけのカテゴライズなのでこれをどう伝えたらいいのよ、と思っていたのだが、書き溜めた一覧を眺めていて「あー、これ中学生が読んだら道を踏み外すヤツだわ」と思うと同時に「オタクとして最低限知っておいて欲しい情報群でもあるな」とも思ったので、これはいっそ「オタクの基礎教養」という名前でくくってリリースしてしまえ。大風呂敷だけど…という事で腹をくくった。中高生あたりに読ませて道を踏み外させたい。

どうせ出すならスマホアプリにしたいな()、という事でChatGPTに相談して作ってみた、、というものです。なのでこの記事は作ったアプリの紹介と、ChatGPTを使った開発の記録というものです。

xyz.plan-and.work

起動することでおすすめ記事のタイトルがランダムで一覧表示され、タイトルをクリックすることでWikipediaへ遷移。戻るボタンを押すと元の一覧に戻り、更新ボタンクリックで一覧を更新します(画面下部のPR欄にはちゃっかりAmazonアフィリエイト貼らせてもらってます)。

オタクの基礎教養一覧画面

実装の詳細はこの後書きますが、GitHub PagesでホスティングしたPWAアプリになってます。なのでスマホ/PCにインストールして気軽に読み応えのある記事群から教養を吸収可能。

肝心のWikipediaのチョイス的には「雑学」「オカルト」「都市伝説」「怪事件」「怪人物」「ヘンテコ」「哲学」「パラドックス」…etcといった感じ。小説や漫画のネタ元としても使えるかも。僕と趣味が合うタイプの人なら無限にリロードして記事を読み耽られると思う。良かったらインストールしてみてください。僕も昔読んだものの普通に内容忘れてるものが多いのでつい自分で起動してひたすら読んでしまい、時間を無限に浪費しています。

では以下実装の話。

スマホで閲覧可能にしたかったというのと、出来ればインストール可能なアプリにしてホーム画面に陣取らせたい、あと、Wikipediaの記事は今後も増やしたりメンテナンスをしたいので記事タイトル群はアプリに内臓するのではなくどこかにホスティングしてAPIで取得する形にしたい、、という要件。

■記事タイトルAPIサーバーの実装

まずAPIサーバーを実装します。ホスティングはCloudflare Workersを使うことにして、 ChatGPTに以下の質問でコード生成してもらった。(この時点ではGPT-3.5を使用)

【質問】

Cloudflare Workersにてjson形式のデータの中から10件ずつランダムな内容を返答するREST APIのコードを作成してください

【回答】

以下はCloudflare WorkersでJSONデータからランダムな10件のコンテンツを返すREST APIの例です。この例では、JSONデータは変数dataに格納されていると仮定しています。

addEventListener('fetch', event => {
  event.respondWith(handleRequest(event.request))
})

async function handleRequest(request) {
  // JSONデータを取得
  const data = // JSONデータを取得する処理

  // データの要素数を取得
  const count = data.length

  // 10件をランダムに選択
  const selected = []
  for (let i = 0; i < 10; i++) {
    const index = Math.floor(Math.random() * count)
    selected.push(data[index])
  }

  // JSON形式でレスポンスを作成
  const response = new Response(JSON.stringify(selected), {
    headers: { 'content-type': 'application/json' },
  })

  return response
}

秒で生成してくれたので、Cloudflare Workersにアカウント作ってデプロイ。試しにアクセスしてみたらちゃんと望みのjsonが返ってきた。爆速開発。

■クライアント側コードの生成

次にクライアント側を生成。(課金したのでGPT-4を使用)

【質問】

WEBAPIからjson形式で取得したデータをリスト形式で提示し、リストをタップすることでjsonの項目に含まれるURLの内容をWEBVIEWの形で表示するPWAアプリのプログラムを作成してください

【回答】

以下のプログラムは、指定された要件に従って、WEBAPIからJSON形式のデータを取得し、リスト形式で表示し、リスト項目をタップすると、JSONの項目に含まれるURLの内容をWebViewの形で表示するPWAアプリを作成します。このアプリは、HTML, CSS, JavaScriptを使用しています。

1.HTMLファイル (index.html):

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <link rel="stylesheet" href="styles.css">
  <title>PWA アプリ</title>
</head>
<body>
  <div class="container">
    <h1>記事リスト</h1>
    <ul id="list"></ul>
  </div>
  <div id="webview-container" class="hidden">
    <button id="close-webview">戻る</button>
    <iframe id="webview" src="" frameborder="0"></iframe>
  </div>

  <script src="app.js"></script>
</body>
</html>

2.CSSファイル (styles.css):

body {
  font-family: Arial, sans-serif;
  margin: 0;
  padding: 0;
}

.container {
  padding: 16px;
}

h1 {
  text-align: center;
}

ul {
  list-style-type: none;
  padding: 0;
}

li {
  border: 1px solid #ccc;
  border-radius: 4px;
  cursor: pointer;
  margin-bottom: 8px;
  padding: 8px;
  transition: background-color 0.3s;
}

li:hover {
  background-color: #f1f1f1;
}

.hidden {
  display: none;
}

#webview-container {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-color: white;
}

#close-webview {
  display: block;
  margin: 16px;
}

iframe {
  width: 100%;
  height: calc(100% - 64px);
  border: none;
}

3.JavaScriptファイル (app.js):

const apiUrl = 'https://your-webapi-url-here.com'; // API URLを設定してください。
const listElement = document.getElementById('list');
const webviewContainer = document.getElementById('webview-container');
const webview = document.getElementById('webview');
const closeWebviewBtn = document.getElementById('close-webview');

// APIからデータを取得
async function fetchData() {
  const response = await fetch(apiUrl);
  const data = await response.json();
  displayList(data);
}

// リスト形式でデータを表示
function displayList(data) {
  data.forEach(item => {
    const listItem = document.createElement('li');
    listItem.textContent = item.title;
    listItem.addEventListener('click', () => {
      openWebview(item.url);
    });
    listElement.appendChild(listItem);
  });
}

// WebViewでURLを表示
function openWebview(url) {
  webview.src = url;
  webviewContainer.classList.remove('hidden');
}

// WebViewを閉じる
function closeWebview() {
  webview.src = '';
  webviewContainer.classList.add

ここで途切れたので「続きをお願いできますか」を入力。

もちろんです。JavaScriptファイル (app.js) の続きを以下に示します。

  webviewContainer.classList.add('hidden');
}

// WebViewを閉じるイベントリスナー
closeWebviewBtn.addEventListener('click', closeWebview);

// アプリを初期化
fetchData();

これで、PWAアプリのプログラムが完成しました。プログラムは、指定されたAPIからJSONデータを取得し、リスト形式で表示します。リスト項目をタップすると、JSONの項目に含まれるURLの内容をWebViewの形で表示します。
各ファイルを適切なディレクトリに保存し、index.htmlをブラウザで開いて、アプリケーションが正しく動作するかを確認してください。また、API URLを実際のURLに置き換えて、正しいデータが表示されることを確認してください。

びゅるびゅるっと生成してくれた。Githubにリポジトリ作ってGithubページとして公開→Pixel6aのChromeから開いてアプリインストールを試そう…とすると動かないので追加で質問。

【質問】

上記のプログラムをPWAとしてAndroid端末にインストールするためにはどうすればよいですか?

【回答】

PWA (Progressive Web App) として Android 端末にインストールするためには、以下の手順を実行してください。
1.manifest.json ファイルの作成: プロジェクトのルートディレクトリに manifest.json ファイルを作成し、以下の内容を追加します。

{
  "name": "PWAアプリ",
  "short_name": "PWA",
  "description": "JSONデータをリスト形式で表示し、リスト項目をタップしてURLをWebViewで表示するアプリ",
  "start_url": "index.html",
  "display": "standalone",
  "background_color": "#ffffff",
  "theme_color": "#3f51b5",
  "orientation": "portrait",
  "icons": [
    {
      "src": "icons/icon-192x192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "icons/icon-512x512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ]
}

2.アイコンの追加: プロジェクトに icons フォルダを作成し、192x192 と 512x512 のサイズの PNG 形式のアイコンを作成して追加します。これらのアイコンは、manifest.json ファイルで指定されたパスに従って配置してください。
3.index.html へのリンクの追加: index.html の タグ内に、以下のリンクタグを追加して、manifest.json を関連付けます。

<link rel="manifest" href="manifest.json">

4.Service Worker の追加と登録: プロジェクトのルートディレクトリに sw.js (Service Worker) ファイルを作成します。この例では、オフライン対応は含まれていませんが、必要に応じて追加できます。

// Service Workerの登録
if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker
      .register('sw.js')
      .then(registration => {
        console.log('Service Worker 登録成功: ', registration);
      })
      .catch(error => {
        console.log('Service Worker 登録失敗: ', error);
      });
  });
}

5.プロジェクトを HTTPS サーバーでホストする: PWA を実行するためには、HTTPS サーバーでホストする必要があります。これをローカルで行うには、Node.js の http-server を利用するか、Python の http.server を利用できます。 app.js の最後に以下のコードを追加して、Service Worker を登録します。

PWAだと言っているんだから最初から教えてよ!と思いつつ、追加で質問しても嫌な顔一つせず答えてくれるのは大変すばらしい。これが人間相手だったら僕の場合、「ひょっとしたらこちらの実装が間違っているのかも…」と、相手に工数を発生させて損害を与える事を考えてしまい。一通りググったり、もう一度実装しなおしたりしてこちらで出来ることは全部試してから聞くことになってしまうのが想像に難くない。何聞いても何とか答えを返してくれるというのは素晴らしい。

で、気を取り直して再デプロイ、再インストール。しかしうまくいかない。デベロッパーツールで見てみたら、以下のメッセージ。

Service Worker 登録失敗: TypeError: Failed to register a ServiceWorker for scope なので、再々質問。

【質問】

作成したプログラムをgithub pagesにデプロイしてアクセスしてみましたが、コンソールに以下のエラーが表示されてしまい動作しません。考えられる問題点を教えてください
Service Worker 登録失敗: TypeError: Failed to register a ServiceWorker for scope

【回答】

エラーメッセージから判断すると、Service Worker の登録に失敗しているようです。この問題はいくつかの原因が考えられますが、以下によくある問題点をいくつか紹介します。
1.Service Worker ファイルが見つからない: sw.js ファイルが正しい場所に配置されていることを確認してください。通常、プロジェクトのルートディレクトリに配置します。
2.Service Worker ファイルが空または不完全: sw.js ファイルが正しく記述されていることを確認してください。ファイルが空または不完全な場合、Service Worker の登録に失敗します。
3.HTTPS接続: Service Worker は HTTPS 上でのみ動作します。GitHub Pages はデフォルトで HTTPS を使用するため、通常は問題ありませんが、何らかの理由で HTTPS が無効になっている場合は、Service Worker の登録に失敗します。
4.ブラウザの互換性: 使用しているブラウザが Service Worker をサポートしていることを確認してください。一部の古いブラウザでは Service Worker がサポートされていません。最新の Google Chrome、Mozilla Firefox、Microsoft Edge などのメジャーブラウザでは問題なく動作するはずです。
5.Service Worker のスコープ: Service Worker のスコープが正しく設定されているか確認してください。通常、プロジェクトのルートディレクトリをスコープとして設定します。register() 関数のオプションでスコープを指定できます。以下の例では、プロジェクトのルートディレクトリがスコープになります。

navigator.serviceWorker.register('/sw.js', { scope: '/' });

これらのチェックポイントを確認して問題が解決しない場合、エラーメッセージの詳細を確認して特定の問題に対処してください。

ん??sw.js??なんやねんそれ。という事で質問。

sw.jsの中には何を記述するべきですか

sw.js ファイルは、Service Worker のコードを含むファイルで、アプリのオフライン対応やキャッシュ機能などを実装するために使用されます。以下に、基本的な Service Worker のコード例を示します。これは、アプリのファイルをキャッシュし、オフラインで利用できるようにする機能を提供します。

const CACHE_NAME = 'your-app-cache-v1';
const urlsToCache = [
  '/',
  '/index.html',
  '/app.js',
  '/styles.css',
  // 他のキャッシュするファイルを追加
];

self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then((cache) => {
        console.log('Opened cache:', CACHE_NAME);
        return cache.addAll(urlsToCache);
      })
  );
});

self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request)
      .then((response) => {
        if (response) {
          return response;
        }
        return fetch(event.request);
      })
  );
});

self.addEventListener('activate', (event) => {
  const cacheWhitelist = [CACHE_NAME];

  event.waitUntil(
    caches.keys().then((cacheNames) => {
      return Promise.all(
        cacheNames.map((cacheName) => {
          if (cacheWhitelist.indexOf(cacheName) === -1) {
            return caches.delete(cacheName);
          }
        })
      );
    })
  );
});

このコードは、以下の 3 つの主要なイベントリスナーを実装しています。
1.install: インストール時にキャッシュを開き、指定されたファイル(urlsToCache 配列内)をキャッシュします。
2.fetch: リクエストが行われるたびに、キャッシュされたリソースがあればそれを返し、そうでなければ通常のネットワークリクエストを実行します。これにより、オフライン時にもキャッシュされたリソースにアクセスできます。
3.activate: サービスワーカーがアクティブになると、新しいキャッシュバージョンが利用可能になり、古いキャッシュが削除されます。
この基本的な Service Worker のコードを sw.js ファイルに記述し、適切な場所に配置してください。これで、アプリはオフライン対応とキャッシュ機能を備えた PWA として機能します。ただし、キャッシュするファイルやキャッシュの管理方法に応じて、このコードをカスタマイズすることができます。

お前、そんなん最初に言うといてくれや、と思いつつも聞いたら答えてくれるのでまあよし。 教えてもらった内容を反映してチャレンジしたら、インストール成功!やったー。…だけどこんどはAPIから記事が降ってこない。再度デベロッパーツールで見てみると、、おっとCORSエラー出てる。そっか、どうやって回避するんだっけ?という事でまたも質問。

app.js内のapiUrlに対してclowdflare workersで作成したAPIのURLを指定しましたが、アプリを実行するとCORSエラーが発生してしまいます。対処方法を教えてください

CORS(Cross-Origin Resource Sharing)エラーは、ブラウザが異なるオリジンからのリソースへのアクセスを制限するために発生します。この問題を解決するには、Cloudflare Workersを使ってAPI側で適切なCORSヘッダーを追加する必要があります。
Cloudflare Workersを使ってCORSヘッダーを追加する方法は次のとおりです。
1.Cloudflare Workersのダッシュボードにアクセスし、新しいWorkerを作成または既存のWorkerを編集します。
2.Workerのスクリプトに以下のコードを追加または変更してください:

addEventListener('fetch', event => {
  event.respondWith(handleRequest(event.request));
});

async function handleRequest(request) {
  // リクエストを転送
  let response = await fetch(request);

  // レスポンスを複製し、CORSヘッダーを追加
  let modifiedResponse = new Response(response.body, response);
  modifiedResponse.headers.set('Access-Control-Allow-Origin', '*');
  modifiedResponse.headers.set('Access-Control-Allow-Methods', 'GET, HEAD, POST, OPTIONS');
  modifiedResponse.headers.set('Access-Control-Allow-Headers', 'Content-Type');

  return modifiedResponse;
}

このコードは、リクエストを転送し、レスポンスにCORSヘッダーを追加しています。Access-Control-Allow-Originヘッダーは、どのオリジンからのリクエストを許可するかを指定します。この例では、* を使用してすべてのオリジンを許可していますが、必要に応じて特定のオリジンに制限できます。 変更を保存してデプロイします。 これで、CORSエラーが解決し、APIに対してクロスオリジンリクエストを行えるようになります。ただし、APIのバックエンドがCORSヘッダーを正しく返すように設定されている場合、Cloudflare Workersを使用せずに問題を解決できることに注意してください。この場合、APIバックエンドの設定でCORSヘッダーを追加または変更してください。

おー記事降ってきて見れるようになった!!

まだこの後もいくつか質問して手直しはやったのですが、基本的にこんな感じで開発進められているので、めちゃめちゃありがたい。おかげで割とサクッとリリースまでこぎつけられました。

ChatGPTで開発質問投げてみての感想ですが、何となくでも当たりのつく問題については質問の投げ方もわかるし、ChatGPTが全くのでたらめ言っているのかそれとも別の問題なのか的なことも雰囲気でわかるけど、知見が極端に少ない問題についてはそのあたりが全然わからないので目的の達成までのハードルはやっぱり高くなる、、という印象。全く知見が無い環境/言語の場合、出されたコードを言われるがままに張り付けても全く動かず、どう聞いたらいいんだろう的な。

現状では知識皆無での丸投げ開発を初手から成功させるのは難しく、若干の自身での調査、繰り返しのトライ&エラーは必要っぽいな、という印象を持ちました(とはいえChatGPTありとなしでは難易度段違いなのは間違いないけども)。