tag:crieit.net,2005:https://crieit.net/tags/PWA/feed
「PWA」の記事 - Crieit
Crieitでタグ「PWA」に投稿された最近の記事
2023-07-29T05:35:55+09:00
https://crieit.net/tags/PWA/feed
tag:crieit.net,2005:PublicArticle/18544
2023-07-29T05:35:55+09:00
2023-07-29T05:35:55+09:00
https://crieit.net/posts/Nuxt3-Cloudflare-GooglePlay-64c426ab215c8
【個人開発】アイデア出しから公開・プロモーションまでまとめ(Nuxt3、Cloudflare、GooglePlay)
<h1 id="1. はじめに"><a href="#1.+%E3%81%AF%E3%81%98%E3%82%81%E3%81%AB">1. はじめに</a></h1>
<p>個人開発の大体の流れがつかめるように、アイデア出しから公開・プロモーションまでをまとめました。</p>
<h2 id="1-1. 作ったアプリ"><a href="#1-1.+%E4%BD%9C%E3%81%A3%E3%81%9F%E3%82%A2%E3%83%97%E3%83%AA">1-1. 作ったアプリ</a></h2>
<p><a target="_blank" rel="nofollow noopener" href="https://lisble.net/about/"><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/906025/2eb38bfc-0efc-d719-9baa-d0967308e507.png" alt="シンプルな買い物リスト-lisble リスブル" width="350"></a></p>
<div class="table-responsive"><table>
<thead>
<tr>
<th>About</th>
<th>List</th>
<th>History</th>
</tr>
</thead>
<tbody>
<tr>
<td><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/906025/01ff7a29-9672-8717-161b-807bfd85f014.png" alt="Aboutページのスクリーンショット.png" width="120" height=""></td>
<td><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/906025/1e53b87e-f6d3-8ef8-8646-f66b8c02dbb2.png" alt="リストページのスクリーンショット" width="120" height=""></td>
<td><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/906025/64e97b97-f950-2f48-e346-63a506643401.png" alt="履歴ページのスクリーンショット" width="120" height=""></td>
</tr>
</tbody>
</table></div>
<div class="table-responsive"><table>
<thead>
<tr>
<th></th>
<th>基本操作</th>
</tr>
</thead>
<tbody>
<tr>
<td>① <strong>リスト作成</strong> ② <strong>買い物完了でチェック</strong> ③ <strong>チェック済みを削除</strong> ※これらの手順だけで簡単操作</td>
<td><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/906025/33bc7e8d-e258-bdbd-85e4-affb6c585cd9.gif" alt="買い物リスト-lisble リスブルの基本操作" width="160" ></td>
</tr>
</tbody>
</table></div>
<p><strong>『<a target="_blank" rel="nofollow noopener" href="https://lisble.net/about/">買い物リスト・メモ - Lisble リスブル</a>』</strong><br />
<div class="table-responsive"><table>
<thead>
<tr>
<th>iOS,PC</th>
<th>Android</th>
</tr>
</thead>
<tbody>
<tr>
<td><a src="https://lisble.net/about/" target="_blank" rel="noopener noreferrer" ><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/906025/992c120a-33bd-48d1-93cd-d97fde42d22c.png" alt="シンプルな買い物リスト-lisble リスブル" width="250"></a></td>
<td><a src="https://play.google.com/store/apps/details?id=net.lisble.twa" target="_blank" rel="noopener noreferrer" ><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/906025/f92cdde6-21d1-2d3f-b8bf-9fb72a74abde.png" width=230 alt="googlePlayバッジ" ></a></td>
</tr>
<tr>
<td>iOS,PCの方は、PWAの為、メニューから <strong>「ホーム画面に追加」</strong> をお願いします。</td>
<td>このアプリは Google Play で入手できます。</td>
</tr>
</tbody>
</table></div></p>
<h1 id="2. 0から公開までの流れ"><a href="#2.+0%E3%81%8B%E3%82%89%E5%85%AC%E9%96%8B%E3%81%BE%E3%81%A7%E3%81%AE%E6%B5%81%E3%82%8C">2. 0から公開までの流れ</a></h1>
<h2 id="2-1. アイデア"><a href="#2-1.+%E3%82%A2%E3%82%A4%E3%83%87%E3%82%A2">2-1. アイデア</a></h2>
<p>1番大事だけど一番頭を悩ませました。<br />
新しいものか、既存のサービスでの差別化の二択で、はじめてということで後者にしてしまいました。<br />
これからもアンテナをはって次につなげたいです。</p>
<p><strong>アイデアの出し方</strong><br />
* とにかく紙に書く<br />
* 不満・面倒・悩み・困っていること<br />
* 人間観察(周りの人)・他業種の業務課題<br />
* 真似 → シンプル化・別分野へ応用・既存のサービスの不満解消<br />
* 海外のサービス調査<br />
* ニッチ・マニアック(自分の強み・趣味)<br />
* 合体・逆</p>
<p>既存のサービスの発展系か、何かの組み合わせか<br />
思いついたらメモ 📝</p>
<p><strong><em>参考サイト</em></strong><br />
<a target="_blank" rel="nofollow noopener" href="https://shuheblog.com/programing-app-idea-creative">アプリ開発でアイデアが浮かばないあなたへ。独創的なアイデアの出し方</a><br />
<a target="_blank" rel="nofollow noopener" href="https://qiita.com/MasatoraAtarashi/items/eec4642fe1e6ce79304d">ポートフォリオや個人開発で使えそうなアイデア</a></p>
<h2 id="2-2. 要件定義"><a href="#2-2.+%E8%A6%81%E4%BB%B6%E5%AE%9A%E7%BE%A9">2-2. 要件定義</a></h2>
<p><strong>①要件(こうだったらいいな)</strong><br />
* 出来る、出来ないを考えずあげていく</p>
<p><strong>②要件の為の機能</strong><br />
* ここで出来る、出来ないを考える<br />
* 最低限必要な機能を絞る<br />
* 実装方法が思いつかない機能は省略</p>
<hr />
<ul>
<li>出先で「あれあったかな❓」を解決したい → 買い物履歴を残して最近買ったものをわかるようにする機能</li>
<li>在庫の把握・重複購入を減らしたい → 買い物履歴から同じ単語入力時に色の変化で知らせる → 入力中に知らせることにより在庫を思い出すきっかけにもなる<br />
etc...</li>
</ul>
<p><strong>③出来ればマネタイズも考える</strong><br />
1. <strong><em>広告</em></strong>:アフィリエイト、AdSense(web)、AdMob(アプリ)<br />
2. <strong><em>課金</em></strong>:広告削除、追加コンテンツ、アイテム<br />
3. <strong><em>寄付</em></strong><br />
4. <strong><em>有料</em></strong>:買い切り<br />
5. <strong><em>サブスク</em></strong>:月額制</p>
<p>とにかく一連の流れを経験したく、マネタイズまでの道のりが見えたのが本アプリ案で、結果Amazonアソシエイト、AdSenseになりました。<br />
AdSenseは受かりませんでした。継続して挑戦します。</p>
<p><strong>④利用技術</strong><br />
🔑 <strong><em>ログイン・ユーザー登録無し</em></strong>、、広告も可でとにかく<strong><em>お金をかけずに</em></strong>を優先して、SPAで制作することにしました。<br />
- GooglePlay Developer 申請・登録25ドル(当時3,352円)<br />
- 独自ドメイン Xserverドメイン 初年度のみ1円<br />
- 更新、移管はCloudflare予定</p>
<p><strong><em>アイデア・デザイン</em></strong><br />
Xmind(マインドマッピング)<br />
Figma<br />
<strong><em>コード</em></strong><br />
Nuxt3(Composition API)・TypeScript・Tailwind CSS<br />
<strong><em>インフラ</em></strong><br />
Cloudflare<br />
<strong><em>開発環境</em></strong><br />
VSCode・Git、GitHub</p>
<p><strong><em>参考サイト</em></strong><br />
<a target="_blank" rel="nofollow noopener" href="https://tenshoku-miti.com/takahiro/create-original-application/">【5ステップ】初めてのオリジナルアプリの作り方!アイデアの出し方も公開</a></p>
<h2 id="2-3. 設計"><a href="#2-3.+%E8%A8%AD%E8%A8%88">2-3. 設計</a></h2>
<p><strong>①画面設計</strong><br />
1. 必要なページ列挙 ➡ 階層・整理<br />
2. ワイヤーフレーム(紙手書き)下書き<br />
3. ワイヤーフレーム(figma)清書</p>
<p><strong><em>参考サイト</em></strong><br />
<a target="_blank" rel="nofollow noopener" href="https://b-risk.jp/blog/2019/10/wireframe/">ワイヤーフレーム(画面設計)の作り方</a><br />
<a target="_blank" rel="nofollow noopener" href="https://aqcg.jp/web_point/">Web画面設計の手順と重要なポイント</a></p>
<p><strong>デザインの4原則</strong><br />
1. 「<strong><em>近接</em></strong>」:関連する要素を<strong><em>グループ化</em></strong><br />
2. 「<strong><em>整列</em></strong>」:関連する要素を<strong><em>見えない線</em></strong>で整列<br />
3. 「<strong><em>反復</em></strong>」:要素の<strong><em>一貫性</em></strong>、デザインの<strong><em>統一</em></strong><br />
4. 「<strong><em>対比</em></strong>」:要素の優先度を<strong><em>強弱</em></strong>で示す</p>
<p><strong><em>参考サイト</em></strong><br />
<a target="_blank" rel="nofollow noopener" href="https://designpartner.jp/principle/#:~:text=%E3%80%8C%E3%83%87%E3%82%B6%E3%82%A4%E3%83%B3%E3%81%AE4%E5%8E%9F%E5%89%87%E3%80%8D%E3%81%AF,%E3%82%B7%E3%83%BC%E3%83%B3%E3%81%A7%E3%82%82%E5%BD%B9%E7%AB%8B%E3%81%A4%E3%81%A7%E3%81%97%E3%82%87%E3%81%86%E3%80%82">お役立ち知識:デザインの4原則 - デザインパートナー</a></p>
<p><strong>②データ設計(構造)</strong><br />
* データの設計図</p>
<h2 id="2-4. 開発"><a href="#2-4.+%E9%96%8B%E7%99%BA">2-4. 開発</a></h2>
<ul>
<li>設計通りに作る</li>
<li>最低限の機能</li>
<li>制作途中で機能の追加案を我慢して、とにかく完成させることに集中する(追加の機能を制作してしまいました。なのでここに残しておきます。)</li>
</ul>
<p><strong>①工夫した機能</strong><br />
- <strong>並べ替え(D&D)・スワイプ機能</strong>をライブラリ(Vue.Draggableは、思うような機能にならなかった)を使用せずに作成<br />
- ↳シンプル化のため、<strong>同じ発火点</strong>で、<strong>参考サイト様</strong>(<a target="_blank" rel="nofollow noopener" href="https://reffect.co.jp/vue/trello-drag-drop-clone/">Trello風タスク並び替えドラッグ&ドロップクローン(Vue.js利用)</a>)を参考に、【<strong>マウス、タッチ、処理回数削減、d&dとswipeの融合、グループ間並べ替え、d&d中の上下の画面端scroll</strong>】ロジックを考え自作しました。</p>
<div class="table-responsive"><table>
<thead>
<tr>
<th>並び替え機能</th>
<th>スワイプ</th>
</tr>
</thead>
<tbody>
<tr>
<td><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/906025/30495dd3-fec4-c791-cb39-a1c09f78003d.gif" alt="買い物リスト-lisble リスブルの並べ替え動作" width="150" height=""></td>
<td><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/906025/cb611a1f-9124-472b-1a26-b7c4f53f8ecc.gif" alt="買い物リスト-lisble リスブルのスワイプ動作" width="150" height=""></td>
</tr>
<tr>
<td></td>
<td>←<strong>色</strong> →<strong>削除</strong>※タッチデバイスのみ</td>
</tr>
</tbody>
</table></div>
<ul>
<li>スマホとPCの<strong>挙動の違い</strong>など(ENTER、終了時の処理...)</li>
<li>日本、世界の<strong>日時の表示形式</strong>(履歴)</li>
<li>Amazonアソシエイトの各国の設定(<strong><em>OneLink</em></strong>)</li>
<li>アニメーション(数字のドラムロール、追加削除時...)etc...</li>
</ul>
<p><strong>②多言語化(i18n)</strong><br />
<strong>@intlify/nuxt3</strong>使用<br />
日/英で単語の長さの違いによる表示崩れに注意</p>
<hr />
<p><strong><em>未解決問題</em></strong><br />
日本語で検索時、検索結果が英語サイトのtitleとdescriptionになってしまう問題(同じURLで 日/英 にしたい)</p>
<p><strong><em>確認</em></strong><br />
meta tagの設定(title、description等)日/英 切替時にきちんと切り替わっている。</p>
<p><strong><em>↓ためしたこと↓</em></strong><br />
- <a target="_blank" rel="nofollow noopener" href="https://zenn.dev/koushikagawa/articles/32ef098e0698c7">Nuxt.jsのi18n多言語対応したら、日本語で検索した時の検索結果が英語になってしまった。解決できたので対応方法を記載します。</a><br />
- <strong>@intlify/nuxt3</strong> で <code>detectBrowserLanguage: false,</code>にあたる設定がない??<br />
- sitemapの設定 (hreflang属性)<br />
- Google Search Consoleに日/英を認識させる方法??...<br />
- (2023年7月) Nuxt3での多言語化はまだ開発中らしいので情報待ち🍥 → <a target="_blank" rel="nofollow noopener" href="https://github.com/intlify/nuxt3">intlify/nuxt3: Nuxt 3 Module for vue-i18n-next</a></p>
<hr />
<p><strong>③プライバシーポリシー</strong><br />
<strong><em>参考サイト</em></strong><br />
<a target="_blank" rel="nofollow noopener" href="https://biz.moneyforward.com/contract/basic/1237/">プライバシーポリシーとは何か?必要性、記載事項をわかりやすく解説</a></p>
<p><strong>④テスト</strong><br />
- 個人情報を扱っているなら、徹底的に安全確認<br />
- 例外処理</p>
<h2 id="2-5. 公開"><a href="#2-5.+%E5%85%AC%E9%96%8B">2-5. 公開</a></h2>
<p><strong>①(SPA SSG) 静的ファイルホスティングサービス【無料】</strong><br />
1. GitHub Pages<br />
2. Firebase Hosting<br />
3. Netlify<br />
4. Vercel<br />
5. <strong><em>Cloudflare Pages</em></strong></p>
<p>無料プランの枠・サイトのパフォーマンス・商用利用可で<strong><em>Cloudflare Pages</em></strong></p>
<p><strong><em>参考サイト</em></strong><br />
<a target="_blank" rel="nofollow noopener" href="https://zenn.dev/catnose99/scraps/6780379210136f">Cloudflare Pages・Vercel ・Netlify の違いや使い分けをまとめる</a><br />
<a target="_blank" rel="nofollow noopener" href="https://jpsern.com/netlify-to-cloudflare-pages/">NetlifyからCloudflare Pagesに引っ越しました</a></p>
<p><strong>②Google Play Store 配信方法</strong><br />
<strong><em>PWA → TWA</em></strong><br />
* TWA変換ツール「<strong><em>Bubblewrap CLI</em></strong>」使用<br />
* Lighthouse で <strong><em>80</em></strong> 以上のパフォーマンス スコアが必要(↓に記述)</p>
<p><strong><em>参考サイト</em></strong><br />
<a target="_blank" rel="nofollow noopener" href="https://flaming.codes/ja/posts/trusted-web-activity-create-pwa-android-app">信頼できるWebアクティビティ</a><br />
<a target="_blank" rel="nofollow noopener" href="https://0115765.com/archives/7932#outline__2_2">【TWA】完全開発ガイド=超簡単にPWAをPlay Storeで配信しよう</a><br />
<a target="_blank" rel="nofollow noopener" href="https://sqripts.com/2022/12/14/22480/">PWA対応サイトをAndroid APK(AAB)に変換する</a></p>
<p><strong>③Lighthouse のパフォーマンス改善(リファクタリング)</strong><br />
- <strong><em>cssの最適化</em></strong> → 読み込み順、削除、animation(<code>transform</code>,<code>opacity</code>...)etc...<br />
- <strong><em>googleFonts, Analytics</em></strong> → GTM使用、読み込むタイミング、読み込み方、文字の限定etc...<br />
- <a target="_blank" rel="nofollow noopener" href="https://deep-space.blue/web/2090">【2022年3月】Google Fontsのパフォーマンス比較&ハリー・ロバーツ方式の勝手に改良版</a><br />
- <a target="_blank" rel="nofollow noopener" href="https://nullnull.dev/blog/google-fonts-subset/#%F0%9F%8F%A9______________________%F0%9F%91%A8%F0%9F%91%A9">Google Fontsを簡単にサブセット化する方法</a><br />
- <strong><em>Web Worker</em></strong> → 並列処理 <a target="_blank" rel="nofollow noopener" href="https://note.com/npaka/n/nc930b61840ac">Web Workerの使い方</a><br />
- <code>requestAnimationFrame()</code> → 「60fps」 <a target="_blank" rel="nofollow noopener" href="https://www.webdesignleaves.com/pr/jquery/requestAnimationFrame.html">requestAnimationFrame の使い方</a><br />
etc...</p>
<p>Mobile :arrow_down:<br />
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/906025/ea2d91d0-76be-ff81-0c8f-ee55669bf1e6.png" alt="Lighthouseモバイルのスコア" width="500" height=""><br />
Desktop :arrow_down:<br />
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/906025/23080b76-e970-9ecb-b92d-3f873b2878e7.png" alt="Lighthouseデスクトップのスコア" width="500" height=""></p>
<h2 id="2-6. リリース後"><a href="#2-6.+%E3%83%AA%E3%83%AA%E3%83%BC%E3%82%B9%E5%BE%8C">2-6. リリース後</a></h2>
<p><strong>①掲載用スクリーンショット、画像作成</strong><br />
* figma制作<br />
* サイトにより、サイズ、比率、枚数、が異なる</p>
<p><strong>②登録サイトに投稿/掲載依頼</strong><br />
当たって砕けろ、チャレンジ精神で色々掲載依頼を出してみた結果です。<br />
<a target="_blank" rel="nofollow noopener" href="https://zenn.dev/yamatake/articles/b93d87ed134f31">個人開発者に告ぐ!「告知=恥ずかしい」を脱却しよう~実例データから重要性を解説</a></p>
<p><strong><em>登録or掲載</em></strong> → 🎉<br />
メール・問い合わせ反応待ち → 📧<br />
更新が止まっているサイトが多い</p>
<p><strong>日本</strong><br />
- <a target="_blank" rel="nofollow noopener" href="https://app-liv.jp/5347459/"><strong><em>Appliv</em></strong></a> → 🎉 <strong><em>レビュー</em></strong>、とても丁寧な対応 :thumbsup:<br />
- <a target="_blank" rel="nofollow noopener" href="https://applion.jp/android/app/net.lisble.twa/"><strong><em>APPLION</em></strong></a> → 🎉 ランキング<br />
- <a target="_blank" rel="nofollow noopener" href="http://houkago-no.appspot.com/app_detail/6278466848686080"><strong><em>放課後アプリ部</em></strong></a> →🎉 登録<br />
- <a target="_blank" rel="nofollow noopener" href="https://applishow.com/detail/DsL3Q4b5hxxm/"><strong><em>Applishow</em></strong></a> → 🎉 登録<br />
- <a target="_blank" rel="nofollow noopener" href="https://www.approom.me/app/01H2J1N7SXF5JSH8ZMMDMGBF2V?platform_id=2"><strong><em>AppRoom</em></strong></a> → 🎉 登録 <a target="_blank" rel="nofollow noopener" href="https://www.approom.me/articles/01H2JGWJ3K3J72WVN7EJT5CKYD"><strong><em>アプリ記事掲載</em></strong></a><br />
- <a target="_blank" rel="nofollow noopener" href="https://seekups.seekgeeks.net/views/detail.html?id=712"><strong><em>SeekUps</em></strong></a> → 🎉 登録<br />
- <a target="_blank" rel="nofollow noopener" href="https://rrws.info/archives/3197"><strong><em>ロケットリリース</em></strong></a> → 🎉 登録<br />
- <a target="_blank" rel="nofollow noopener" href="https://appstimes.jp/posts/2991"><strong><em>Appstimes</em></strong></a> → 🎉 登録<br />
- <a target="_blank" rel="nofollow noopener" href="https://anymake.app/products/kAjQm0f6tswHGaKTd0G5"><strong><em>AnyMake</em></strong></a> → 🎉 登録<br />
- <a target="_blank" rel="nofollow noopener" href="https://www.makepost.net/projects/386"><strong><em>makepost</em></strong></a> → 🎉 登録<br />
- <a target="_blank" rel="nofollow noopener" href="https://www.mitsukarusite.jp/2023/06/550/"><strong><em>MITSUKARU!</em></strong></a> → 🎉 登録<br />
- <a target="_blank" rel="nofollow noopener" href="https://devhaunt.com/product/111"><strong><em>DevHaunt!</em></strong></a> →🎉 登録、とても丁寧な対応 👍<br />
- <a target="_blank" rel="nofollow noopener" href="https://kojin.dev/application/41"><strong><em>個人dev</em></strong></a> → 🎉 登録<br />
- <a target="_blank" rel="nofollow noopener" href="https://appget.com/c/developerpage/createdevpost/"><strong><em>アプリゲット</em></strong></a> → ゲームのみ<br />
- <a target="_blank" rel="nofollow noopener" href="https://www.appbank.net/"><strong><em>AppBank</em></strong></a> → 📧<br />
- <a target="_blank" rel="nofollow noopener" href="https://applision.com/review/"><strong><em>Applision</em></strong></a> → 📧<br />
- <a target="_blank" rel="nofollow noopener" href="https://tsukulog.work/"><strong><em>ツクログ</em></strong></a> → 📧 クリエイター登録済み、まだ作品は掲載されない(2度投稿)<br />
- <a target="_blank" rel="nofollow noopener" href="https://octoba.net/"><strong><em>octoba</em></strong></a> → 📧<br />
- <a target="_blank" rel="nofollow noopener" href="https://newlaun-ch.com/"><strong><em>NewLaun-ch</em></strong></a> → 📧<br />
- <a target="_blank" rel="nofollow noopener" href="https://appllio.com/"><strong><em>appllio</em></strong></a> → 📧<br />
- <a target="_blank" rel="nofollow noopener" href="https://appnavi.info/"><strong><em>appnavi</em></strong></a> → 📧<br />
- <a target="_blank" rel="nofollow noopener" href="https://androlion.org/"><strong><em>Androライオン</em></strong></a> → 📧条件:レビュー<br />
- <a target="_blank" rel="nofollow noopener" href="https://androrank.com/?m=send"><strong><em>AndroRank</em></strong></a> → 📧条件:レビュー<br />
- <a target="_blank" rel="nofollow noopener" href="https://android-apps.org/"><strong><em>Androidアップス</em></strong></a> → 📧<br />
- <a target="_blank" rel="nofollow noopener" href="https://softcollection.org/android/"><strong><em>Androidコレクション</em></strong></a> → 📧<br />
- <a target="_blank" rel="nofollow noopener" href="https://news.mynavi.jp/top/notice/press/"><strong><em>マイナビニュース</em></strong></a> → 📧<br />
- <a target="_blank" rel="nofollow noopener" href="https://corp.itmedia.co.jp/pr/"><strong><em>ITmedia</em></strong></a> → 📧<br />
- <a target="_blank" rel="nofollow noopener" href="https://corp.itmedia.co.jp/media/pr/"><strong><em>ITmedia ねとらぼ</em></strong></a> → 📧<br />
- <a target="_blank" rel="nofollow noopener" href="https://androck.jp/contact/"><strong><em>AndRock</em></strong></a> → 📧<br />
- <a target="_blank" rel="nofollow noopener" href="http://app-vip.jp/posts/add"><strong><em>AppVIP</em></strong></a> → 📧<br />
- <a target="_blank" rel="nofollow noopener" href="https://applink.jp/request"><strong><em>AppLink</em></strong></a> → 📧<br />
- <a target="_blank" rel="nofollow noopener" href="https://www.gapsis.jp/"><strong><em>GAPSIS</em></strong></a> → 📧</p>
<hr />
<p><strong>海外</strong></p>
<ul>
<li><a target="_blank" rel="nofollow noopener" href="https://www.producthunt.com/my/products"><strong><em>Product Hunt</em></strong></a> → 🎉 登録から1週間待つ必要がある</li>
<li><a target="_blank" rel="nofollow noopener" href="https://alternativeto.net/software/shopping-list--lisble/about/"><strong><em>AlternativeTo</em></strong></a> → 🎉 登録</li>
<li><a target="_blank" rel="nofollow noopener" href="https://appstoreapps.com/submit-your-app/"><strong><em>app store apps</em></strong></a> → 📧</li>
<li><a target="_blank" rel="nofollow noopener" href="https://www.appcraver.com/contact-us/"><strong><em>AppCraver</em></strong></a> → 📧</li>
<li><a target="_blank" rel="nofollow noopener" href="https://arstechnica.com/contact-us/"><strong><em>arttechnica</em></strong></a> → 📧</li>
<li><a target="_blank" rel="nofollow noopener" href="https://www.androidappsreview.com/submit-your-android-app/"><strong><em>Apps Review</em></strong></a> → 📧</li>
<li><a target="_blank" rel="nofollow noopener" href="https://androidcommunity.com/send-tips/"><strong><em>community</em></strong></a> → 📧</li>
<li><a target="_blank" rel="nofollow noopener" href="http://www.androidapplog.com/suggest-app/"><strong><em>App Log</em></strong></a> → 📧</li>
<li><a target="_blank" rel="nofollow noopener" href="https://getandroidstuff.com/submit-app/"><strong><em>Get Android Stuff</em></strong></a> → 📧</li>
<li><a target="_blank" rel="nofollow noopener" href="https://www.androidguys.com/request-app-review/"><strong><em>Android Guys</em></strong></a> → 📧</li>
<li><a target="_blank" rel="nofollow noopener" href="https://www.androlib.com/"><strong><em>androlib</em></strong></a> → 📧</li>
<li><a target="_blank" rel="nofollow noopener" href="https://www.apkmirror.com/"><strong><em>APKMirror</em></strong></a> → 📧</li>
<li><a target="_blank" rel="nofollow noopener" href="http://appscout.pcmag.com/"><strong><em>PCMag</em></strong></a> → 📧</li>
<li><a target="_blank" rel="nofollow noopener" href="https://www.newsreports.com/contact/"><strong><em>NEWSREPORTS</em></strong></a> → 📧</li>
<li><a target="_blank" rel="nofollow noopener" href="https://www.ifanzine.com/contact/"><strong><em>iFanzine</em></strong></a> → 📧</li>
</ul>
<hr />
<p>※上記以外で無料で宣伝できるサイト、方法があれば教えていただけると嬉しいです。</p>
<p><strong>③記事を書く</strong><br />
- Qiita<br />
- <a target="_blank" rel="nofollow noopener" href="https://qiita.com/arieight/items/7935afdaf78d31f8b444">【Nuxt3】はじめての個人開発「買い物リストアプリ」開発記録</a><br />
- Zenn<br />
- <a target="_blank" rel="nofollow noopener" href="https://zenn.dev/arieight/articles/55ec7a38edb940">Nuxt3で初めての個人開発記録【買い物リストアプリ】</a><br />
- Crieit<br />
- <a href="https://crieit.net/posts/Lisble-64bfaf0e5e2b7">シンプルな買い物リストアプリ「lisble リスブル 」の紹介</a><br />
- 【個人開発】アイデア出しから公開・プロモーションまでまとめ(Nuxt3、Cloudflare、GooglePlay)<br />
- note<br />
- <a target="_blank" rel="nofollow noopener" href="https://note.com/arieight/n/nc8cace276feb">値上げ対策! 買い物リスト作成で節約</a><br />
- <a target="_blank" rel="nofollow noopener" href="https://note.com/arieight/n/ne70e5e3865f3">シンプルな買い物リストアプリ「Lisble リスブル 」のご紹介</a><br />
- Medium(英語記事)<br />
- <a target="_blank" rel="nofollow noopener" href="https://medium.com/@arieight_8/price-increase-measures-save-money-by-making-a-shopping-list-120cec400b15">Price increase measures! Save money by making a shopping list</a></p>
<p><strong>④Twitter(日本語/英語)</strong><br />
- Twitter日本語ver.:<a target="_blank" rel="nofollow noopener" href="https://twitter.com/arieight_8">@arieight_8</a><br />
- Twitter英語ver.:<a target="_blank" rel="nofollow noopener" href="https://twitter.com/Lisble_en">@Lisble_en</a><br />
- 登録or掲載 → つぶやく<br />
- 機能一覧、設定一覧、動画(gif)でツイート<br />
- 開発中からつぶやいた方が良い</p>
<p><strong><em>参考サイト</em></strong><br />
<a target="_blank" rel="nofollow noopener" href="https://www.producthunt.com/posts/shopping-list-lisble">個人開発したWebサービスをリリースした後にやったこと / やり続けていること</a></p>
<h1 id="3. 気付き・反省点・まとめ"><a href="#3.+%E6%B0%97%E4%BB%98%E3%81%8D%E3%83%BB%E5%8F%8D%E7%9C%81%E7%82%B9%E3%83%BB%E3%81%BE%E3%81%A8%E3%82%81">3. 気付き・反省点・まとめ</a></h1>
<ul>
<li>開発を0からして、初めてコードをさわって新しい概念にふれた時くらい勉強になり、とても知見が広がり良かったです。</li>
<li>プログラミング以外の作業が初だったので「プログラミング:他 = 3:7」くらいの感覚でした。様々なデザインが予想より大変でした。</li>
<li>一番面白いと感じた作業は、ロジックを考えて形になった時でした。<br />
</li>
<li><strong>アイデア</strong> → 紙やXMindに思いつく限りまとめたのですが、総合的な判断で結局既存のサービスに追加機能の方向になってしまいました。マネタイズまで含めたアイデアが難しいです。どうせやるなら一通りやってみたかったので次回以降にしました。常にアンテナをはって知見を広げていいこうと思いました。</li>
<li><strong>要件定義</strong> → 運用費がかからない、個人情報は扱わない、AdSense以外のマネタイズまで考える、などを考慮してアイデアを絞り、考え、すでにあるサービスと見比べ、アイデアと要件定義をぐるぐるしていました。</li>
<li>AdSenseは、コンテンツ不足でwebアプリは受かりづらいらしいです。</li>
<li><strong>利用技術</strong> → 平凡なアイデアだったので初めて挑戦する技術(Nuxt3、TypeScript、etc...)を使用してしまいました。</li>
<li>デプロイ先など、お金をかけずに運用していくための他の効率の良い方法やサービスを引き続き調査していきます。(Firebaseをデータベース等)</li>
<li><strong>設計</strong> → 画面デザインは、UI足したら引き算・データ設計は構造整理</li>
<li><strong>開発</strong> → 追加の機能を制作してしまいました。最低限の機能でまず完成を目指すべき</li>
<li><strong>工夫した機能</strong> → ToDoの変化型アプリで平凡なアプリで非常に多く数があるため、機能にこだわりました。</li>
<li>多言語化して作業が増えた。[Amazon設定、時間形式、表示崩れ、metaタグ、etc...]</li>
<li><strong>テスト</strong> → 多視点が重要</li>
<li><strong>公開</strong> → 運用費をかけないにこだわる</li>
<li><strong>パフォーマンス改善</strong> → 良い学習</li>
<li><strong>マネタイズ</strong> → Amazonまでの導線が弱い 要検討</li>
<li>著作権・プライバシーポリシー・DDoS対策etc...</li>
<li><strong>プロモーション</strong> → Twitterは開発中から・アイデア段階でSNSを考慮したサービス・地道にor金をかけるか・個人の場合限られるので勉強段階からブログや記事など書いておけばまた違ったかも</li>
<li>「告知=恥ずかしい」があったので、経験せずにはわからないからできることは全部しようと思いました。</li>
</ul>
<p>作る前に想像していたよりコード以外の作業が多いとうことに改めて気付かされました。<br />
制作中、何度も宇宙兄弟のセリフが脳内再生してました。ありがとうございました。</p>
<hr />
<blockquote>
<p>「また積めばいいよ」<br />
「次はもっと上手く積めるようになってるよ」<br />
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/906025/54752648-2273-f10c-d125-ea6abc68e562.jpeg" alt="『宇宙兄弟』17巻 小山 宙哉「また積めばいいよ」" width="300"><br />
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/906025/9d577b8b-90ef-2e69-8d2b-2e67ebfb9794.jpeg" alt="『宇宙兄弟』17巻 小山 宙哉「次はもっと上手く積めるようになってるよ」" width="300"><br />
出典: 『宇宙兄弟』17巻 小山 宙哉(講談社、2012年3月23日第1発行)</p>
</blockquote>
<hr />
<p>購入履歴が残せるシンプルな買い物リストアプリ <strong><em>『買い物リスト - Lisble リスブル』</em></strong> です。メモ、ToDoアプリとしても機能します。<br />
購入履歴の保存機能により、無駄な重複購入を防ぎ、在庫の備忘録にもなります。<br />
ログイン・ユーザー登録不要、無料ですぐにご利用いただけます。<br />
もしよかったら、触ってみてください。</p>
<p><strong>『<a target="_blank" rel="nofollow noopener" href="https://lisble.net/about/">買い物リスト・メモ - Lisble リスブル</a>』</strong><br />
<div class="table-responsive"><table>
<thead>
<tr>
<th>iOS,PC</th>
<th>Android</th>
</tr>
</thead>
<tbody>
<tr>
<td><a src="https://lisble.net/about/" target="_blank" rel="noopener noreferrer" ><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/906025/992c120a-33bd-48d1-93cd-d97fde42d22c.png" alt="シンプルな買い物リスト-lisble リスブル" width="250"></a></td>
<td><a src="https://play.google.com/store/apps/details?id=net.lisble.twa" target="_blank" rel="noopener noreferrer" ><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/906025/f92cdde6-21d1-2d3f-b8bf-9fb72a74abde.png" width=230 alt="googlePlayバッジ" ></a></td>
</tr>
<tr>
<td>iOS,PCの方は、PWAの為、メニューから <strong>「ホーム画面に追加」</strong> をお願いします。</td>
<td>このアプリは Google Play で入手できます。</td>
</tr>
</tbody>
</table></div></p>
<p>Twitter:<a target="_blank" rel="nofollow noopener" href="https://twitter.com/arieight_8">@arieight_8</a></p>
<hr />
<p>Google Play および Google Play ロゴは、Google LLC の商標です。</p>
Arieight
tag:crieit.net,2005:PublicArticle/18538
2023-07-25T20:16:30+09:00
2023-07-25T20:16:30+09:00
https://crieit.net/posts/Lisble-64bfaf0e5e2b7
シンプルな買い物リストアプリ「Lisble リスブル 」について
<h1 id="1. はじめに"><a href="#1.+%E3%81%AF%E3%81%98%E3%82%81%E3%81%AB">1. はじめに</a></h1>
<p><strong>シンプルな買い物リスト・メモ - Lisble リスブル</strong><br />
アプリの解説・紹介をするために記事を書きました。</p>
<p><strong>シンプルな買い物リスト</strong>のアプリです。メモ、ToDoアプリとしても機能します。<strong>ログイン・ユーザー登録不要、無料</strong>ですぐにご利用いただけます。</p>
<p>「<strong>買い忘れ、衝動買い、同じものを買ってしまう、出先での在庫確認、ネットショップとの比較など</strong>」考える事が多く、余計な負担だったので、リストをつけることにより改善したいと思いました。<br />
<strong>購入履歴の保存機能</strong>は、<strong>在庫の備忘録</strong>にもなります。また買い物中の操作性を重視し、レイアウトの<strong>左/右切り替え</strong>が可能です。<br />
事前に買うべきものをメモしておくことで、より計画的に買い物をして、結果的に節約につながります。</p>
<h1 id="2. 買い物リストアプリ「Lisble リスブル 」について"><a href="#2.+%E8%B2%B7%E3%81%84%E7%89%A9%E3%83%AA%E3%82%B9%E3%83%88%E3%82%A2%E3%83%97%E3%83%AA%E3%80%8CLisble+%E3%83%AA%E3%82%B9%E3%83%96%E3%83%AB+%E3%80%8D%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6">2. 買い物リストアプリ「Lisble リスブル 」について</a></h1>
<p><a target="_blank" rel="nofollow noopener" href="https://lisble.net/about/"><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/906025/2eb38bfc-0efc-d719-9baa-d0967308e507.png" alt="シンプルな買い物リスト-Lisble リスブル" width="350"></a></p>
<p><strong>『<a target="_blank" rel="nofollow noopener" href="https://lisble.net/about/">買い物リスト・メモ - Lisble リスブル</a>』</strong><br />
<div class="table-responsive"><table>
<thead>
<tr>
<th>iOS,PC</th>
<th>Android</th>
</tr>
</thead>
<tbody>
<tr>
<td><a src="https://lisble.net/about/" target="_blank" rel="noopener noreferrer" ><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/906025/992c120a-33bd-48d1-93cd-d97fde42d22c.png" alt="シンプルな買い物リスト-Lisble リスブル" width="250"></a></td>
<td><a src="https://play.google.com/store/apps/details?id=net.lisble.twa" target="_blank" rel="noopener noreferrer" ><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/906025/f92cdde6-21d1-2d3f-b8bf-9fb72a74abde.png" width=230 alt="googlePlayバッジ" ></a></td>
</tr>
<tr>
<td>iOS,PCの方は、PWAの為、メニューから <strong>「ホーム画面に追加」</strong> をお願いします。</td>
<td>このアプリは Google Play で入手できます。</td>
</tr>
</tbody>
</table></div></p>
<h2 id="2-1. アプリの概要"><a href="#2-1.+%E3%82%A2%E3%83%97%E3%83%AA%E3%81%AE%E6%A6%82%E8%A6%81">2-1. アプリの概要</a></h2>
<p>購入履歴が残せるシンプルな買い物リストのアプリです。メモ、ToDoアプリとしても機能します。<strong>ログイン・ユーザー登録不要、無料</strong>ですぐにご利用できます。</p>
<div class="table-responsive"><table>
<thead>
<tr>
<th>About</th>
<th>List</th>
<th>History</th>
</tr>
</thead>
<tbody>
<tr>
<td><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/906025/01ff7a29-9672-8717-161b-807bfd85f014.png" alt="Aboutページのスクリーンショット.png" width="120" height=""></td>
<td><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/906025/1e53b87e-f6d3-8ef8-8646-f66b8c02dbb2.png" alt="リストページのスクリーンショット" width="120" height=""></td>
<td><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/906025/64e97b97-f950-2f48-e346-63a506643401.png" alt="履歴ページのスクリーンショット" width="120" height=""></td>
</tr>
</tbody>
</table></div>
<h2 id="2-2. アプリの3つの特徴"><a href="#2-2.+%E3%82%A2%E3%83%97%E3%83%AA%E3%81%AE3%E3%81%A4%E3%81%AE%E7%89%B9%E5%BE%B4">2-2. アプリの3つの特徴</a></h2>
<p><strong>① 買い物中の操作性(UI,UX)</strong><br />
片手操作での誤操作防止のため編集機能を片側に寄せ、かつ <strong>左/右 表示切替可能</strong>。<br />
チェックしたメモ、まだしていないメモを切り替え、すぐに確認できるようにしました。</p>
<p><strong>② 購入履歴の保存</strong><br />
<strong>日付時間と共に保存</strong>をして、入力時に同一品目を色の変化でお知らせをします。<br />
購入履歴を残すことにより、直近で購入したものがわかり、無駄な重複買いと共に在庫を思い出すきっかけにもなると思いました。</p>
<p><strong>③ Amazon検索</strong><br />
入力したメモをそのまま<strong>Amazon検索</strong>できます。買い物中の値段の比較や、欲しいものリストとしてご活用してもらえればと思いました。</p>
<h2 id="2-3. アプリの画面案内"><a href="#2-3.+%E3%82%A2%E3%83%97%E3%83%AA%E3%81%AE%E7%94%BB%E9%9D%A2%E6%A1%88%E5%86%85">2-3. アプリの画面案内</a></h2>
<div class="table-responsive"><table>
<thead>
<tr>
<th></th>
<th>スマホ</th>
</tr>
</thead>
<tbody>
<tr>
<td>① <strong>購入履歴へ</strong> ② <strong>メニュー</strong>(移動・設定) ③ <strong>タブ切り替え</strong> ④ <strong>↑↓並べ替え</strong> ④ <strong>←色、→削除</strong> ⑤ <strong>編集</strong> ⑥ <strong>チェック</strong> ⑦ <strong>リスト追加</strong> ⑧ <strong>「チェックあり」のみ削除</strong> ⑨ <strong>すべて削除</strong></td>
<td><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/906025/bb63d0a7-c937-4b2b-e913-8230c118d9fb.png" alt="スクリーンショット" width="350" height=""></td>
</tr>
</tbody>
</table></div>
<div class="table-responsive"><table>
<thead>
<tr>
<th></th>
<th>タブレット・PC</th>
</tr>
</thead>
<tbody>
<tr>
<td>① <strong>編集</strong> ② <strong>色</strong> ③ <strong>削除</strong> ④ ↑↓ <strong>並べ替えのみ</strong></td>
<td><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/906025/a75aec69-901c-d87e-0411-b5c40e23b409.png" alt="スクリーンショット" width="405" height=""></td>
</tr>
</tbody>
</table></div>
<h2 id="2-4. アプリの基本操作"><a href="#2-4.+%E3%82%A2%E3%83%97%E3%83%AA%E3%81%AE%E5%9F%BA%E6%9C%AC%E6%93%8D%E4%BD%9C">2-4. アプリの基本操作</a></h2>
<p>①②③これらの手順だけで簡単操作<br />
<div class="table-responsive"><table>
<thead>
<tr>
<th></th>
<th>基本操作</th>
</tr>
</thead>
<tbody>
<tr>
<td>① <strong>リスト作成</strong> ② <strong>買い物完了でチェック</strong> ③ <strong>チェック済みを削除</strong> ※これらの手順だけで簡単操作</td>
<td><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/906025/33bc7e8d-e258-bdbd-85e4-affb6c585cd9.gif" alt="買い物リスト-lisble リスブルの基本操作" width="160" ></td>
</tr>
</tbody>
</table></div></p>
<h2 id="2-5. アプリの機能一覧"><a href="#2-5.+%E3%82%A2%E3%83%97%E3%83%AA%E3%81%AE%E6%A9%9F%E8%83%BD%E4%B8%80%E8%A6%A7">2-5. アプリの機能一覧</a></h2>
<div class="table-responsive"><table>
<thead>
<tr>
<th>並び替え機能</th>
<th>スワイプ</th>
</tr>
</thead>
<tbody>
<tr>
<td><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/906025/30495dd3-fec4-c791-cb39-a1c09f78003d.gif" alt="買い物リスト-lisble リスブルの並べ替え動作" width="150" height=""></td>
<td><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/906025/cb611a1f-9124-472b-1a26-b7c4f53f8ecc.gif" alt="買い物リスト-lisble リスブルのスワイプ動作" width="150" height=""></td>
</tr>
<tr>
<td></td>
<td>←<strong>色</strong> →<strong>削除</strong>※タッチデバイスのみ</td>
</tr>
</tbody>
</table></div>
<div class="table-responsive"><table>
<thead>
<tr>
<th>タブ切り替え</th>
<th>Amazon検索</th>
</tr>
</thead>
<tbody>
<tr>
<td><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/906025/db8c743b-1789-98a7-0c26-82adbef8733e.gif" alt="買い物リスト-lisble リスブルのタブ動作" width="150" height=""></td>
<td><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/906025/e0d457fa-a1c3-3b28-b90e-fe67f86ffba6.png" alt="買い物リスト-lisble Amazon検索のイメージ" width="170" height=""></td>
</tr>
<tr>
<td><strong>すべて、チェックあり、なし</strong></td>
<td>そのまま<strong>Amazon検索</strong></td>
</tr>
</tbody>
</table></div>
<div class="table-responsive"><table>
<thead>
<tr>
<th>購入履歴の保存</th>
<th>重複買い防止</th>
</tr>
</thead>
<tbody>
<tr>
<td><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/906025/4c03d104-0d05-234f-f56b-ff150451077a.gif" alt="買い物リスト-lisble リスブルの買い物履歴・削除動作" width="150" height=""></td>
<td><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/906025/72a81f65-8c27-f1cb-84c5-74fbf3d8680b.gif" alt="買い物リスト-lisble リスブルの重複購入防止動作" width="150" height=""></td>
</tr>
<tr>
<td><strong>1. チェックあり削除</strong> <strong>2. 購入履歴の保存</strong></td>
<td>履歴と同じ品目を入力時に<strong>色の変化</strong>でお知らせ</td>
</tr>
</tbody>
</table></div>
<div class="table-responsive"><table>
<thead>
<tr>
<th>続けて追加</th>
<th>全削除</th>
</tr>
</thead>
<tbody>
<tr>
<td><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/906025/93b63cdc-c10b-5f6c-c8c6-18bdfe229f2c.png" alt="追加ボタン" width="50" height=""></td>
<td><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/906025/9e640615-6745-b110-9de7-37df2827ba6f.png" alt="全削除ボタン" width="100" height=""></td>
</tr>
<tr>
<td><strong>+ボタン、ENTERキー</strong></td>
<td>チェック済みは<strong>購入履歴に保存</strong></td>
</tr>
</tbody>
</table></div>
<h2 id="2-6. アプリの設定"><a href="#2-6.+%E3%82%A2%E3%83%97%E3%83%AA%E3%81%AE%E8%A8%AD%E5%AE%9A">2-6. アプリの設定</a></h2>
<div class="table-responsive"><table>
<thead>
<tr>
<th>テーマカラー</th>
<th>レイアウト</th>
<th>多言語</th>
</tr>
</thead>
<tbody>
<tr>
<td><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/906025/e1de9ced-0089-d47b-9108-45c7354bf927.png" alt="買い物リスト-lisble リスブルのテーマカラー5種" width="150" height=""></td>
<td><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/906025/31cc5133-da53-b9e0-6419-6fbc7fe6fda6.png" alt="買い物リスト-lisble リスブルのレイアウトの左/右切り替え" width="150" height=""></td>
<td><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/906025/f318e886-f667-3430-e50b-0a36c2eccb27.png" alt="買い物リスト-lisble リスブルの英語表示" width="129" height=""></td>
</tr>
<tr>
<td>5種</td>
<td>片手操作での誤操作防止(<strong>左利き</strong>)</td>
<td><strong>日本語</strong> / <strong>英語</strong></td>
</tr>
</tbody>
</table></div>
<div class="table-responsive"><table>
<thead>
<tr>
<th>自動削除</th>
<th>入力中のEnterキー操作</th>
</tr>
</thead>
<tbody>
<tr>
<td><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/906025/cca8e553-c7bf-fd06-99b6-c5fee6f0fa1b.png" alt="買い物リスト-lisble リスブルの自動削除機能" width="150" height=""></td>
<td><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/906025/63a179fc-2df2-5456-8ae4-d2565ed27ad9.png" alt="買い物リスト-lisble リスブルの入力中のEnterKeyの挙動" width="150" height=""></td>
</tr>
<tr>
<td>終了時に「<strong>チェックあり</strong>」を<strong>自動削除(保存)機能</strong></td>
<td><strong>追加</strong> / <strong>改行</strong> 切り替え</td>
</tr>
</tbody>
</table></div>
<hr />
<h1 id="3. 最後に"><a href="#3.+%E6%9C%80%E5%BE%8C%E3%81%AB">3. 最後に</a></h1>
<p><strong>購入履歴</strong>が残せるシンプルな買い物リストアプリ 『<strong>買い物リスト - Lisble リスブル</strong>』 です。メモ、ToDoアプリとしても機能します。<br />
<strong>購入履歴の保存機能</strong>により、無駄な重複購入を防ぎ、在庫の備忘録にもなります。<br />
<strong>ログイン・ユーザー登録不要、無料</strong>ですぐにご利用いただけます。<br />
もしよかったら、触ってみてください。</p>
<p><strong>『<a target="_blank" rel="nofollow noopener" href="https://lisble.net/about/">買い物リスト・メモ - Lisble リスブル</a>』</strong><br />
<div class="table-responsive"><table>
<thead>
<tr>
<th>iOS,PC</th>
<th>Android</th>
</tr>
</thead>
<tbody>
<tr>
<td><a src="https://lisble.net/about/" target="_blank" rel="noopener noreferrer" ><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/906025/992c120a-33bd-48d1-93cd-d97fde42d22c.png" alt="シンプルな買い物リスト-lisble リスブル" width="250"></a></td>
<td><a src="https://play.google.com/store/apps/details?id=net.lisble.twa" target="_blank" rel="noopener noreferrer" ><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/906025/f92cdde6-21d1-2d3f-b8bf-9fb72a74abde.png" width=230 alt="googlePlayバッジ" ></a></td>
</tr>
<tr>
<td>iOS,PCの方は、PWAの為、メニューから <strong>「ホーム画面に追加」</strong> をお願いします。</td>
<td>このアプリは Google Play で入手できます。</td>
</tr>
</tbody>
</table></div></p>
<hr />
<p>Google Play および Google Play ロゴは、Google LLC の商標です。</p>
Arieight
tag:crieit.net,2005:PublicArticle/18161
2022-04-09T22:40:57+09:00
2022-04-09T22:40:57+09:00
https://crieit.net/posts/metnal
気分が落ち込んだ時に、自動で周囲の人に通知するアプリを作りました
<p>こんにちは!</p>
<p>「<a target="_blank" rel="nofollow noopener" href="https://metnal.stock-stu.com/">metnal</a>」というアプリを作りましたので、ご紹介させてください。</p>
<h2 id="metnalとは"><a href="#metnal%E3%81%A8%E3%81%AF">metnalとは</a></h2>
<p>metnelは、「気分が落ち込んだ時に、自動で家族や友人、パートナーなどに通知することで、周囲の人の助けを受けることをサポートする」アプリです。</p>
<p>特徴として、<br />
1. 手軽に始められるように、LINEでのみログイン可能。メールアドレスやパスワードでアカウントを管理すると、アカウントを紛失したり、ログインが面倒になったりすることで、離脱してしまうリスクがあります(多くの人がアカウントを紛失した経験があると思います笑)。<br />
2. 機能は非常にシンプル。その時の気分をつけるだけで、独自のスコアリングに基づき、自動で気分のスコアが表示されます。<br />
3. 周囲の人に、気分のスコアを共有できます。また周囲の人が登録することで、スコアが極端に低くなった時(気分が落ち込んだ時)にLINEのbotを通じて自動で通知されます。</p>
<p>となっております。<br />
このアプリが十分に機能した場合、周囲の人の落ち込みを即座に発見できることが期待されます。</p>
<h3 id="利用方法"><a href="#%E5%88%A9%E7%94%A8%E6%96%B9%E6%B3%95">利用方法</a></h3>
<p><a target="_blank" rel="nofollow noopener" href="https://stock-stu.com/introduce_metnal/#howtouse">こちら</a>をご覧ください!</p>
<h3 id="利用技術等"><a href="#%E5%88%A9%E7%94%A8%E6%8A%80%E8%A1%93%E7%AD%89">利用技術等</a></h3>
<p><a target="_blank" rel="nofollow noopener" href="https://qiita.com/StStu/items/11dabe66a2f92665d551#使用技術等">qiita</a>にまとめてみました。<br />
一読して頂ければ幸いです!</p>
nyonyataro
tag:crieit.net,2005:PublicArticle/17032
2021-04-30T00:02:35+09:00
2021-04-30T00:02:35+09:00
https://crieit.net/posts/SPA-PWA
【メモ】SPAとPWAのこと。
<h1 id="SPA(Single Page Application)"><a href="#SPA%EF%BC%88Single+Page+Application%EF%BC%89">SPA(Single Page Application)</a></h1>
<ul>
<li>単一ページによるWebアプリケーション。</li>
<li>返された結果をクライアント側でリアルタイムにレンダリングできるようになった。</li>
<li>瞬時に画面を書き換えることができる。(いいね!ボタンとか)</li>
<li>画面遷移はDOM操作。</li>
<li>ページのリフレッシュは不要。</li>
<li>リッチなエクスペリエンス。</li>
<li>クライアントとサーバーのロジックを分けて、保守性を上げることができる。(分業)</li>
</ul>
<h1 id="PWA(Progressive Web Apps)"><a href="#PWA%EF%BC%88Progressive+Web+Apps%EF%BC%89">PWA(Progressive Web Apps)</a></h1>
<ul>
<li>ネイティブアプリのようなUXを提供するWebアプリの概念。</li>
<li>デバイスに合わせた機能提供がWebコンテンツ自体で行われている。</li>
</ul>
<hr />
<h5 id="~これまでのWebになかった機能~"><a href="#%EF%BD%9E%E3%81%93%E3%82%8C%E3%81%BE%E3%81%A7%E3%81%AEWeb%E3%81%AB%E3%81%AA%E3%81%8B%E3%81%A3%E3%81%9F%E6%A9%9F%E8%83%BD%EF%BD%9E">~これまでのWebになかった機能~</a></h5>
<p><strong>Service Worker</strong><br />
* オフラインサポート(Cache API)<br />
* プッシュ通知(Push API)<br />
* バックグラウンド処理(Background API)</p>
<p><strong>Web Application Manifest</strong><br />
* アイコンの追加</p>
<hr />
<h5 id="~PWAのメリット~"><a href="#%EF%BD%9EPWA%E3%81%AE%E3%83%A1%E3%83%AA%E3%83%83%E3%83%88%EF%BD%9E">~PWAのメリット~</a></h5>
<ul>
<li>ブラウザUIなしで起動できてオフラインでも使える。</li>
<li>アプリを公開するためにストアに提出する必要がない。</li>
<li>プラットフォームごとに修正する必要がない。</li>
<li>インストールしなくても使える。</li>
<li>サーバー側から通知をプッシュできる。(タイムセールの開始など)</li>
<li>オフラインでの使用が可能なため、ネットワークの状態に左右されないように作れる。</li>
<li>低機能なブラウザやPWA非サポートのデバイスにもサービスを提供できる。</li>
<li>ユーザの強い権限で動作しない。</li>
<li>HTTPS接続でしか動作しないのでサーバーとのやり取りを安全に行える。</li>
<li>ハイパーリンクがあればサービスに接続できる。<br />
*各メリットを状況に合わせて取捨選択できる。</li>
</ul>
<hr />
<h5 id="Service Worker"><a href="#Service+Worker">Service Worker</a></h5>
<p>(バックグラウンドで動作するプログラミング可能なネットワークプロキシ)<br />
* PWAを実現する上で中心となる機能。<br />
* Webページのネットワークリクエスト全てをインターセプトできる。<br />
* キャッシュ<br />
* リクエストのハンドリング<br />
* Push<br />
* バックグラウンド同期</p>
<hr />
<h5 id="Web Application Manifest"><a href="#Web+Application+Manifest">Web Application Manifest</a></h5>
<p>(ホーム画面のアイコンやアプリ起動時のウィンドウの状態を指定)<br />
* ホーム画面に追加するアイコン、スプラッシュ、背景色、ウィンドウスタイルを定義するもの。</p>
<h1 id="まとめ"><a href="#%E3%81%BE%E3%81%A8%E3%82%81">まとめ</a></h1>
<ul>
<li>SPAは物理的なページ遷移を行わずWebクライアント上で画面が描画されるものでリアクティブな動作が可能。</li>
<li>PWAはWebクライアントの機能に合わせて、Webアプリにネイティブアプリライクな機能を実装したものでデバイスへのインストールやPush通知が可能。</li>
</ul>
しんじ。
tag:crieit.net,2005:PublicArticle/15909
2020-05-24T17:18:35+09:00
2020-05-24T17:18:35+09:00
https://crieit.net/posts/Nuxt-Firebase-Cloud-Messaging-FCM-Web
Nuxt+Firebase Cloud Messaging(FCM)でWebプッシュ通知を送る
<p>WebでもFCMが使えるようになったので、試してみたときの備忘録。<br />
これでSafari以外には、通知が送れるようになる(<em>´ω`</em>)</p>
<h3 id="使い方"><a href="#%E4%BD%BF%E3%81%84%E6%96%B9">使い方</a></h3>
<p>構成は、@nuxtjs/pwaでPWA化している感じ。</p>
<h4 id="コンソール側"><a href="#%E3%82%B3%E3%83%B3%E3%82%BD%E3%83%BC%E3%83%AB%E5%81%B4">コンソール側</a></h4>
<p>Settingsでウェブプッシュ証明書を作成して、鍵ペアを取得する。</p>
<p><img width="973" alt="スクリーンショット_2020-05-24_17_06_33.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/d17f3a0e-e6df-73a7-d57b-a6ff36e7c79f.png"></p>
<p>これを、PUBLIC_VAPID_KEYという環境変数に設定しておく。</p>
<h4 id="クライアント側"><a href="#%E3%82%AF%E3%83%A9%E3%82%A4%E3%82%A2%E3%83%B3%E3%83%88%E5%81%B4">クライアント側</a></h4>
<h5 id="firebaseの初期化"><a href="#firebase%E3%81%AE%E5%88%9D%E6%9C%9F%E5%8C%96">firebaseの初期化</a></h5>
<p><code>~/plugins/firebase.ts</code>というファイルを用意し、firebaseを初期化</p>
<pre><code class="typescript">// ~/plugins/firebase.ts
import * as firebase from "firebase/app";
import "firebase/auth";
import "firebase/firestore";
import "firebase/messaging";
if (!firebase.apps.length) {
// まずは、firebaseの初期化
firebase.initializeApp({
apiKey: process.env.API_KEY,
authDomain: process.env.AUTH_DOMAIN,
databaseURL: process.env.DATABASE_URL,
projectId: process.env.PROJECT_ID,
storageBucket: process.env.STORAGE_BUCKET,
messagingSenderId: process.env.MESSAGING_SENDER_ID,
appId: process.env.APP_ID,
measurementId: process.env.MEASUREMENT_ID
});
// Push通知をサポートしているかをチェック
// サポートしていないと、firebase.messaging()を呼んだときに例外が発生
const isSupported = firebase.messaging.isSupported();
// コンソールで発行した、ウェブプッシュ証明書の鍵ペアを取得
const publicVapidKey = process.env.PUBLIC_VAPID_KEY;
if (!!publicVapidKey && process.client && !!isSupported) {
// FCMの初期化。鍵ペアを設定する
const messaging = firebase.messaging();
messaging.usePublicVapidKey(publicVapidKey);
// @nuxtjs/pwaが生成するsw.jsと、
// 後で作成するFCM受信処理用のsw-firebase-messaging.jsを統合するための設定
navigator.serviceWorker
.register("/sw.js")
.then(registration => messaging.useServiceWorker(registration))
.catch(err => console.error(err));
}
}
export default firebase;
</code></pre>
<h5 id="トークンの取得"><a href="#%E3%83%88%E3%83%BC%E3%82%AF%E3%83%B3%E3%81%AE%E5%8F%96%E5%BE%97">トークンの取得</a></h5>
<p>Push通知を送る際の宛先として、トークンを指定しないといけないので取得。<br />
トークンは端末ごとに取得する必要があるので、注意が必要。<br />
※PCとスマホだとそれぞれトークンが違う</p>
<pre><code class="typescript">import firebase from "~/plugins/firebase";
// トークンの取得
public async getToken(user: User) {
const isSupported = firebase.messaging.isSupported();
if (!isSupported) return;
const token = await firebase.messaging().getToken();
// firestoreにトークンを保存しておく処理(中身は略)
await saveToken(user, token);
}
</code></pre>
<p>Firestoreなどへユーザごとにトークンを保存しておく。</p>
<p><code>firebase.messaging().getToken();</code>を呼んだ際に、<br />
通知の設定が「確認」だと、許可を求めるダイアログが表示される。</p>
<p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/e9976414-8418-09c1-8846-25211a949776.png" alt="スクリーンショット 2020-05-24 16.30.23.png" /></p>
<p>これが許可されていないと、トークンも取得できない。</p>
<p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/c21078c8-38d0-04c7-133a-b5402611443d.png" alt="スクリーンショット 2020-05-24 16.30.35.png" /></p>
<h5 id="メッセージを受け取ったときの処理"><a href="#%E3%83%A1%E3%83%83%E3%82%BB%E3%83%BC%E3%82%B8%E3%82%92%E5%8F%97%E3%81%91%E5%8F%96%E3%81%A3%E3%81%9F%E3%81%A8%E3%81%8D%E3%81%AE%E5%87%A6%E7%90%86">メッセージを受け取ったときの処理</a></h5>
<p>メッセージを受信したときに受け取る関数は2つあり、</p>
<ul>
<li>フォアグラウンド(画面を見ている時) ... onMessage</li>
<li>バックグラウンド(画面を見ていない時) ... setBackgroundMessageHandler</li>
</ul>
<p>・【参考】<a target="_blank" rel="nofollow noopener" href="https://firebase.google.com/docs/cloud-messaging/js/receive#handle_messages_when_your_web_app_is_in_the_background">JavaScript クライアントでメッセージを受信する | Firebase</a></p>
<p>今回はバックグラウンドのときに通知を送りたいので、<br />
<code>setBackgroundMessageHandler</code>を設定していく。</p>
<p>こんな感じ。</p>
<p>ドキュメントを見ると、『<code>firebase-messaging-sw.js</code>というファイル名で作成』と書かれているけど、<br />
その名前にすると、自動で読み込まれてしまう。。</p>
<p>開発用と本番用で切り替えたいときなどもあるので、@nuxtjs/pwaが生成するsw.jsと統合できるように、<br />
<code>sw-firebase-messaging.js</code>という名前でファイルを作成しておく。</p>
<p>ファイル名を変更したので、上で書いている「firebaseの初期化」の部分で、<br />
<code>messaging.useServiceWorker(registration)</code>を呼んでいる形。</p>
<pre><code class="javascript">// ~/static/sw-firebase-messaging.js
importScripts("https://www.gstatic.com/firebasejs/7.14.2/firebase-app.js");
importScripts("https://www.gstatic.com/firebasejs/7.14.2/firebase-messaging.js");
// Firebaseの初期化
firebase.initializeApp({
apiKey: "...",
authDomain: "...",
databaseURL: "...",
projectId: "...",
storageBucket: "...",
messagingSenderId: "...",
appId: "...",
measurementId: "...",
});
// [START background_handler]
const isSupported = firebase.messaging.isSupported();
if (!!isSupported) {
const messaging = firebase.messaging();
// バックグラウンド時の処理
messaging.setBackgroundMessageHandler(function(payload) {
// 受け取ったFCMの内容を取得
const notificationTitle = payload.notification.title;
const notificationOptions = {
body: payload.notification.body,
icon: payload.notification.icon,
};
// 通知を作成する
return self.registration.showNotification(notificationTitle, notificationOptions);
});
}
// [END background_handler]
</code></pre>
<h5 id="nuxt.config.tsでPWA関連の設定をする"><a href="#nuxt.config.ts%E3%81%A7PWA%E9%96%A2%E9%80%A3%E3%81%AE%E8%A8%AD%E5%AE%9A%E3%82%92%E3%81%99%E3%82%8B">nuxt.config.tsでPWA関連の設定をする</a></h5>
<p>作成した<code>sw-firebase-messaging.js</code>を取り込む設定と、<br />
@nuxtjs/pwaが生成するmanifest.jsonに、gcm_sender_idを追加する設定を追加</p>
<pre><code class="typescript">import { Configuration } from "@nuxt/types";
const config: Configuration = {
// 略
modules: [
"@nuxtjs/pwa",
],
workbox: {
// sw-firebase-messaging.jsをimportするように追加
importScripts: [
"sw-firebase-messaging.js"
]
},
pwa: {
manifest: {
// manifest.jsonにgcm_sender_idを追加
gcm_sender_id: process.env.MESSAGING_SENDER_ID || ""
}
},
};
export default config;
</code></pre>
<p>これでクライアント側はOK!</p>
<h4 id="サーバ側: メッセージを送信する"><a href="#%E3%82%B5%E3%83%BC%E3%83%90%E5%81%B4%3A+%E3%83%A1%E3%83%83%E3%82%BB%E3%83%BC%E3%82%B8%E3%82%92%E9%80%81%E4%BF%A1%E3%81%99%E3%82%8B">サーバ側: メッセージを送信する</a></h4>
<p>メッセージの送信は、firebase-adminでできる。<br />
・<a target="_blank" rel="nofollow noopener" href="https://firebase.google.com/docs/cloud-messaging/send-message?hl=ja">アプリサーバーからの送信リクエストを作成する | Firebase</a></p>
<p>firestoreを利用しているので、Cloud Functionsのfirestoreトリガーを使い、<br />
ドキュメントが追加されたら通知するようにしている例。</p>
<pre><code class="typescript">import * as functions from "firebase-functions";
import admin from "../common/firebaseAdmin"; // 初期化済みのfirebase-admin
export default functions
.firestore.document("ドキュメントのパス")
.onCreate(async (snap, context) => {
// getTokenで保存しておいたトークンを取得(中身は略)
const token = getToken();
// 通知の送信
const title = "通知するタイトル";
const body = "通知する本文";
const icon = "通知で表示するアイコン画像のURL"
const link = "通知をタップしたときに開くURL"
await admin.messaging().send({
// 送信先の端末のトークン
token: token,
// 通知する内容
notification: {
title: title,
body: body
},
// Web Push向けの通知内容
webpush: {
notification: {
icon: icon
},
fcmOptions: {
link: link
}
}
});
});
</code></pre>
<p>送信はこれだけ!</p>
<h3 id="ほかの小ネタ"><a href="#%E3%81%BB%E3%81%8B%E3%81%AE%E5%B0%8F%E3%83%8D%E3%82%BF">ほかの小ネタ</a></h3>
<h4 id="トークンを削除する"><a href="#%E3%83%88%E3%83%BC%E3%82%AF%E3%83%B3%E3%82%92%E5%89%8A%E9%99%A4%E3%81%99%E3%82%8B">トークンを削除する</a></h4>
<p><code>firebase.messaging().deleteToken();</code>でトークンを無効化できる。</p>
<pre><code class="typescript">// トークンの削除
public async deleteToken(user: User) {
const isSupported = firebase.messaging.isSupported();
if (isSupported) {
const token = await firebase.messaging().getToken();
await firebase.messaging().deleteToken(token);
}
}
</code></pre>
<h4 id="フォアグラウンドで通知を受け取ったときになにかする"><a href="#%E3%83%95%E3%82%A9%E3%82%A2%E3%82%B0%E3%83%A9%E3%82%A6%E3%83%B3%E3%83%89%E3%81%A7%E9%80%9A%E7%9F%A5%E3%82%92%E5%8F%97%E3%81%91%E5%8F%96%E3%81%A3%E3%81%9F%E3%81%A8%E3%81%8D%E3%81%AB%E3%81%AA%E3%81%AB%E3%81%8B%E3%81%99%E3%82%8B">フォアグラウンドで通知を受け取ったときになにかする</a></h4>
<p><code>onMessage</code>を使うと、通知を受け取ったときに呼び出してくれる。</p>
<pre><code class="typescript">firebase.messaging().onMessage(async (payload) => {
// 受け取ったときの処理
});
</code></pre>
<h4 id="トークンが変更されたときになにかする"><a href="#%E3%83%88%E3%83%BC%E3%82%AF%E3%83%B3%E3%81%8C%E5%A4%89%E6%9B%B4%E3%81%95%E3%82%8C%E3%81%9F%E3%81%A8%E3%81%8D%E3%81%AB%E3%81%AA%E3%81%AB%E3%81%8B%E3%81%99%E3%82%8B">トークンが変更されたときになにかする</a></h4>
<p><code>onTokenRefresh</code>を使うと、トークンが更新されたときに呼び出してくれる。<br />
新しいトークンは再度<code>getToken()</code>を呼ばないといけない。</p>
<pre><code class="typescript">firebase.messaging().onTokenRefresh(async () => {
// トークンが更新されたときの処理
});
</code></pre>
<h4 id="通知の許可状態を確認する"><a href="#%E9%80%9A%E7%9F%A5%E3%81%AE%E8%A8%B1%E5%8F%AF%E7%8A%B6%E6%85%8B%E3%82%92%E7%A2%BA%E8%AA%8D%E3%81%99%E3%82%8B">通知の許可状態を確認する</a></h4>
<p>通知の状態は、<code>Notification.permission</code>で確認できるらしい。<br />
・<a target="_blank" rel="nofollow noopener" href="https://developer.mozilla.org/ja/docs/Web/API/notification/permission">Notification.permission - Web API | MDN</a></p>
<pre><code class="typescript">if (Notification.permission === "default") {
// 確認(デフォルト)
} else if (Notification.permission === "granted") {
// 許可
} else if (Notification.permission === "denied") {
// 拒否
}
</code></pre>
<p>以上!!</p>
<h3 id="【PR】これをつかって、こんなのつくりました!"><a href="#%E3%80%90PR%E3%80%91%E3%81%93%E3%82%8C%E3%82%92%E3%81%A4%E3%81%8B%E3%81%A3%E3%81%A6%E3%80%81%E3%81%93%E3%82%93%E3%81%AA%E3%81%AE%E3%81%A4%E3%81%8F%E3%82%8A%E3%81%BE%E3%81%97%E3%81%9F%EF%BC%81">【PR】これをつかって、こんなのつくりました!</a></h3>
<p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/2f71c56a-0ab2-5910-f203-e1fca191ad69.png" alt="スクリーンショット 2020-05-24 12.44.28.png" /></p>
<p>こんな通知を受け取れます!<br />
<img width="359" alt="スクリーンショット_2020-05-24_13_34_17.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/1d4037bf-e662-a15a-4567-91f845ef5142.png"></p>
<p>1週間でWebサービスを作るイベント <a href="https://crieit.net/boards/web1week-202005">web1week</a>への投稿作品です!<br />
よかったら、遊んでみてください(<em>´ω`</em>)</p>
<p>■エアで投げ銭できるWebサービス「エア銭」<br />
URL: <a target="_blank" rel="nofollow noopener" href="https://air-money.netlify.app/">https://air-money.netlify.app/</a></p>
<h1 id="参考にしたサイトさま"><a href="#%E5%8F%82%E8%80%83%E3%81%AB%E3%81%97%E3%81%9F%E3%82%B5%E3%82%A4%E3%83%88%E3%81%95%E3%81%BE">参考にしたサイトさま</a></h1>
<ul>
<li><a target="_blank" rel="nofollow noopener" href="https://developers.cyberagent.co.jp/blog/archives/9662/">FRESH! における Web プッシュ通知機能 〜実装編〜 | CyberAgent Developers Blog</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://github.com/firebase/quickstart-js/blob/bcce38ebc1e5602560e2b76b20f19b7834b8279e/messaging/firebase-messaging-sw.js#L15-L37">quickstart-js/firebase-messaging-sw.js at bcce38ebc1e5602560e2b76b20f19b7834b8279e · firebase/quickstart-js</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://firebase.google.com/docs/reference/js/firebase.messaging.Messaging#deletetoken">Messaging | JavaScript SDK | Firebase</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://firebase.google.com/docs/cloud-messaging/js/send-multiple#%E3%83%88%E3%83%94%E3%83%83%E3%82%AF-http-post-%E3%83%AA%E3%82%AF%E3%82%A8%E3%82%B9%E3%83%88">複数のデバイスにメッセージを送信する | Firebase</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages#WebpushConfig">REST Resource: projects.messages | Firebase</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://firebase.google.com/docs/cloud-messaging/send-message">アプリサーバーからの送信リクエストを作成する | Firebase</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://firebase.google.com/docs/cloud-messaging/js/first-message">バックグラウンド アプリにテスト メッセージを送信する | Firebase</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://firebase.google.com/docs/cloud-messaging/js/client">JavaScript Firebase Cloud Messaging クライアント アプリを設定する</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/ryo_hisano/items/1171beca22d5a04ed802">Firebase Cloud Messagingで始めるWebプッシュ通知 - Qiita</a></li>
</ul>
きらぷか@積読ハウマッチ/SSSAPIなど
tag:crieit.net,2005:PublicArticle/15687
2020-01-17T09:22:44+09:00
2020-01-17T09:28:03+09:00
https://crieit.net/posts/PWA-Web-TWA-Google-Play
PWA対応WebサービスをTWAとしてGoogle Playに登録してみた
<p>現在PWA対応のWebサイトはTWAとしてGoogle Playストアに登録してAndroidアプリとしてインストールできるようになっています。当サービスCrieitも試しにやってみました。Laravel + Laravel Mix(Vue.js)という構成です。</p>
<h2 id="PWA対応"><a href="#PWA%E5%AF%BE%E5%BF%9C">PWA対応</a></h2>
<p>まずはサイト自体がPWA対応していることが前提ですのでそちらの準備を行います。利用しているフレームワークによっては最初からPWA対応されているものもあるのでその場合は特に設定は不要です。</p>
<p>色々キャッシュしてトラブルになると面倒だと思ったのでホーム画面追加機能だけ利用しました。</p>
<h3 id="Workboxを導入"><a href="#Workbox%E3%82%92%E5%B0%8E%E5%85%A5">Workboxを導入</a></h3>
<p>Service Workerの作成のためWorkboxを導入します。</p>
<pre><code>yarn add -D workbox-sw workbox-webpack-plugin
</code></pre>
<p>デフォルトだとLaravel Mixでビルドしたファイルをキャッシュ設定にしてしまうため、今回はそれを無効にしておきました。webpack.mix.jsに下記を追記します。excludeで全無視しています。</p>
<pre><code class="javascript">const workboxPlugin = require('workbox-webpack-plugin')
mix.webpackConfig({
plugins: [
new workboxPlugin.GenerateSW({
swDest: 'service-worker.js',
clientsClaim: true,
skipWaiting: true,
exclude: [/.*/],
runtimeCaching: []
})
],
output: {
publicPath: ''
}
})
</code></pre>
<p>PWA用のmanifest.jsonを公開ルートフォルダに作成します。</p>
<pre><code class="json">{
"name": "Crieit - プログラマー、クリエイターが何でも気軽に書けるコミュニティ",
"short_name": "Crieit",
"icons": [
{
"src": "/img/card.png",
"sizes": "144x144",
"type": "image/png"
}
],
"start_url": "/",
"display": "standalone",
"theme_color": "#448AFF",
}
</code></pre>
<p>表示するHTMLにmanifest.jsonを位置を記述します。</p>
<pre><code class="html"><link rel="manifest" href="/manifest.json">
</code></pre>
<p>JavaScriptでService Workerの登録を行います。Laravelプロジェクト作成時に自動的に作られていたbootstrap.jsに追記しておきました。</p>
<pre><code class="javascript">if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/service-worker.js')
})
}
</code></pre>
<p>これでビルドしたら完了です。ビルドすると <code>precache-manifest.なんちゃらかんちゃら.js</code> というファイルがされるためそれもコミットします。ただ、これは毎回生成されてしまうので、一度生成した後にwebpack.mix.jsの記述はコメントアウトしておきました。シンプルなPWA対応の場合は以後もう何も変更がありませんので。</p>
<h2 id="TWA対応"><a href="#TWA%E5%AF%BE%E5%BF%9C">TWA対応</a></h2>
<p>次にTWAの対応です。これはAndroid StudioでAndroidアプリケーションを作成します。具体的なやり方は下記に書かれています。</p>
<p><a target="_blank" rel="nofollow noopener" href="https://qiita.com/zprodev/items/181c1c8f19bc0beb1183">【実践】Google Play Store でPWA配信 (TWA) - Qiita</a></p>
<p>ただしちょっとインストールするもののハッシュ値が古かったりするので本家も参考にすると良いでしょう。</p>
<p><a target="_blank" rel="nofollow noopener" href="https://developers.google.com/web/updates/2019/02/using-twa">Using Trusted Web Activities | Google Developers</a></p>
<p>丁度この記事を書いている最中に見つけたできたてほやほやの記事もありました。多分色々情報が最新ですので日本語情報では一番参考になりそうです。</p>
<p><a target="_blank" rel="nofollow noopener" href="https://www.maytry.net/how-to-release-twa/">話題のTWAでPWAをGoogle Play Storeに配信してみた</a></p>
<h3 id="ハマったところ"><a href="#%E3%83%8F%E3%83%9E%E3%81%A3%E3%81%9F%E3%81%A8%E3%81%93%E3%82%8D">ハマったところ</a></h3>
<h4 id="build.gradle"><a href="#build.gradle">build.gradle</a></h4>
<p>似たようなセクションがあったりして追記するところを間違えてうまくいかなかった事がありました。</p>
<h4 id="customtabsだとうまくいかない"><a href="#customtabs%E3%81%A0%E3%81%A8%E3%81%86%E3%81%BE%E3%81%8F%E3%81%84%E3%81%8B%E3%81%AA%E3%81%84">customtabsだとうまくいかない</a></h4>
<p>実は前述リンクでやっているcustomtabsを使った方法ではうまくいきませんでした。ということで、代わりにandroid-browser-helperというものを使いました。なぜうまく行かなかったかというと、多分丁度最近android-browser-helperに変わったばかりで、微妙に各所の情報が古かったのではないかと。</p>
<p>具体的には、app/build.gradleに追記したcustomtabsのimplementationの代わりに下記を追記しました。</p>
<pre><code class="gradle"> implementation 'androidx.browser:browser:1.0.0'
implementation 'com.github.GoogleChrome:android-browser-helper:ff8dfc4ed3d4133aacc837673c88d090d3628ec8'
</code></pre>
<p>僕は試していませんが、実は公式も変わったようなので下記のほうが正しそうです。ハッシュ値からバージョンに変わっているのでもしかしたら丁度ちゃんとリリースしたよ、というところなのかもしれません。</p>
<pre><code class="gradle"> implementation 'com.google.androidbrowserhelper:androidbrowserhelper:1.0.0'
</code></pre>
<p>あとはAndroidManifest.xmlのActivityもこれを使った形に変更が必要です。</p>
<pre><code class="xml"> <activity android:name="com.google.androidbrowserhelper.trusted.LauncherActivity">
</code></pre>
<h2 id="assetlinks.json"><a href="#assetlinks.json">assetlinks.json</a></h2>
<p>あとはassetlinks.jsonを公開フォルダに追加する必要があります。前述のリンクどれでも作り方の説明が書かれています。</p>
<h2 id="アイコンの調整"><a href="#%E3%82%A2%E3%82%A4%E3%82%B3%E3%83%B3%E3%81%AE%E8%AA%BF%E6%95%B4">アイコンの調整</a></h2>
<p>そのままだとデフォルトのアイコンでアプリになってしまいますので、適宜アイコンを置き換えます。最近は最新の方の機種用にXMLのアイコンファイルも作成するようです。容量自体も軽量になり、あとはforegroundとbackgroundがあり、Android側でアイコンを揺らしたりするアニメーションなどで利用されるようです。</p>
<p>今回は作るのが面倒だったので元々のXMLを削除することで画像を使ってもらうことにしました。</p>
<p><a href="https://crieit.now.sh/upload_images/7eaa5757ab431b6429649b8e6c60b2e05e11c99a35548.jpg" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/7eaa5757ab431b6429649b8e6c60b2e05e11c99a35548.jpg?mw=700" alt="" /></a></p>
<h2 id="まとめ"><a href="#%E3%81%BE%E3%81%A8%E3%82%81">まとめ</a></h2>
<p>ということでTWAの対応方法を書きました。詳しい説明と言うよりは主にハマったところをメインに書いているため詳しくは記事中のリンクを参考にしてください。</p>
だら@Crieit開発者
tag:crieit.net,2005:PublicArticle/15656
2019-12-29T19:07:03+09:00
2019-12-29T19:07:50+09:00
https://crieit.net/posts/Nuxt-Firebase-PWA-Service-Worker
Nuxt+Firebaseでセッション管理: PWA(Service Worker)編
<p>FirebaseとSSRなNuxt.jsでアプリを作っていて、<br />
クライアント側で認証チェックするとFirebaseの初期化などでラグが...<br />
サーバ側で認証情報とかを取得してもう少しなんとかできないかなと。</p>
<p>まだベータっぽい?けど、公式の以下の内容を試してみたときの備忘録。</p>
<ul>
<li><a target="_blank" rel="nofollow noopener" href="https://firebase.google.com/docs/auth/web/service-worker-sessions?hl=ja">Service Worker によるセッション管理 | Firebase</a></li>
</ul>
<h3 id="よく出てくる言葉"><a href="#%E3%82%88%E3%81%8F%E5%87%BA%E3%81%A6%E3%81%8F%E3%82%8B%E8%A8%80%E8%91%89">よく出てくる言葉</a></h3>
<p>単語はよく聞くけど、ちゃんと見てなかったので、ざっくりとしたまとめ</p>
<ul>
<li>PWA: ネイティブアプリみたいなUXを提供するWebアプリ
<ul>
<li><a target="_blank" rel="nofollow noopener" href="https://developers.google.com/web/progressive-web-apps/">Progressive Web Apps | Google Developers</a></li>
</ul></li>
<li>Service Worker: PWAを実現するための基盤技術。独自のライフサイクルを持ってる
<ul>
<li><a target="_blank" rel="nofollow noopener" href="https://developers.google.com/web/fundamentals/primers/service-workers?hl=ja">Service Worker の紹介 | Web Fundamentals | Google Developers</a></li>
</ul></li>
<li>Workbox: PWAでよく使うコード(ボイラープレート)やベストプラクティスを提供するライブラリ
<ul>
<li><a target="_blank" rel="nofollow noopener" href="https://developers.google.com/web/tools/workbox/">Workbox | Google Developers</a></li>
</ul></li>
<li>Nuxt PWA: NuxtでPWAするときに使うプラグイン。workboxを使ったService Worker(sw.js)とかを生成してくれる
<ul>
<li><a target="_blank" rel="nofollow noopener" href="https://pwa.nuxtjs.org/">⚡ Nuxt PWA</a></li>
</ul></li>
</ul>
<h3 id="Nuxt PWAを使ってみる"><a href="#Nuxt+PWA%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6%E3%81%BF%E3%82%8B">Nuxt PWAを使ってみる</a></h3>
<h4 id="インストール"><a href="#%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB">インストール</a></h4>
<pre><code class="shell">$ npm install @nuxtjs/pwa
</code></pre>
<h4 id="設定"><a href="#%E8%A8%AD%E5%AE%9A">設定</a></h4>
<p>設定は簡単。modulesに<code>@nuxtjs/pwa</code>を追加するだけ。</p>
<pre><code class="typescript">// nuxt.config.ts
import { Configuration } from "@nuxt/types";
const config: Configuration = {
// ...略
modules: [
// ... 略
"@nuxtjs/pwa",
],
// ...略
};
export default config;
</code></pre>
<h3 id="Firebase Authと組み合わせる"><a href="#Firebase+Auth%E3%81%A8%E7%B5%84%E3%81%BF%E5%90%88%E3%82%8F%E3%81%9B%E3%82%8B">Firebase Authと組み合わせる</a></h3>
<p>このあたりを見つつ、Firebase Authの情報を扱えるようにする。<br />
- <a target="_blank" rel="nofollow noopener" href="https://firebase.google.com/docs/auth/web/service-worker-sessions?hl=ja">Service Worker によるセッション管理 | Firebase</a><br />
- <a target="_blank" rel="nofollow noopener" href="https://qiita.com/daishinkawa/items/915d918aba6bf7849b21#auth%E3%83%A9%E3%82%A4%E3%83%96%E3%83%A9%E3%83%AA%E3%81%AE%E7%B5%84%E3%81%BF%E8%BE%BC%E3%81%BF">Firebase (Hosting × Functions) × Nuxt.js (universal) で ユーザ認証のベストプラクティスを探る旅 その2 - Qiita</a></p>
<p>流れ的には、以下の通り。</p>
<ol>
<li>Firebase Auth用のService Workerの作成して、リクエストにIDトークンを付与するように変更</li>
<li>作成したService Workerを使うよう、nuxt.config.tsに設定を追加</li>
<li></li>
</ol>
<h4 id="Firebase Auth用のService Workerの作成"><a href="#Firebase+Auth%E7%94%A8%E3%81%AEService+Worker%E3%81%AE%E4%BD%9C%E6%88%90">Firebase Auth用のService Workerの作成</a></h4>
<p>長めだけど、ほぼ<a target="_blank" rel="nofollow noopener" href="https://firebase.google.com/docs/auth/web/service-worker-sessions?hl=ja">公式ドキュメント</a>のまま。</p>
<pre><code class="javascript">// ~/static/sw-firebase-auth.js
// firebaseを初期化
firebase.initializeApp({
apiKey: /* API_KEY */,
authDomain: /* AUTH_DOMAIN */,
databaseURL: /* DATABASE_URL */,
projectId: /* PROJECT_ID */,
storageBucket: /* STORAGE_BUCKET */,
messagingSenderId: /* MESSAGING_SENDER_ID */,
appId: /* APP_ID */,
measurementId: /* AUTH_DOMAIN */
});
// onAuthStateChanged()で現在のuserからidTokenを取得
const getIdToken = () => {
return new Promise((resolve, reject) => {
const unsubscribe = firebase.auth().onAuthStateChanged(user => {
unsubscribe();
if (user) {
user.getIdToken().then(
idToken => resolve(idToken),
error => resolve(null)
);
} else {
resolve(null);
}
});
});
};
// URLからルートのURLを取得する処理
const getOriginFromUrl = url => {
// https://stackoverflow.com/questions/1420881/how-to-extract-base-url-from-a-string-in-javascript
const pathArray = url.split("/");
const protocol = pathArray[0];
const host = pathArray[2];
return protocol + "//" + host;
};
/**
* Service Workderのライフサイクルでfetchしたときの処理
*/
self.addEventListener("fetch", event => {
// リクエストをラップして、ヘッダにFirebase AuthのIdTokenを追加する処理
const requestProcessor = idToken => {
let req = event.request;
// URLを取得して、httpsもしくはlocalhostかなどをチェック
if (self.location.origin == getOriginFromUrl(event.request.url) &&
(self.location.protocol == "https:" || self.location.hostname == "localhost") &&
idToken
) {
// ヘッダ情報をクローンする
const headers = new Headers();
for (let entry of req.headers.entries()) {
headers.append(entry[0], entry[1]);
}
// クローンしたヘッダにFirebase AuthのIdTokenを追加
headers.append("Authorization", "Bearer " + idToken);
try {
req = new Request(req.url, {
method: req.method,
headers: headers,
mode: "same-origin",
credentials: req.credentials,
cache: req.cache,
redirect: req.redirect,
referrer: req.referrer,
body: req.body,
bodyUsed: req.bodyUsed,
context: req.context
});
} catch (e) {
console.error(e);
}
}
return fetch(req);
};
// 上の関数を使って、全リクエストでIdTokenの取得し、Firebase AuthのIdTokenを追加ようにする
event.respondWith(getIdToken().then(requestProcessor, requestProcessor));
});
/**
* Service Workderのライフサイクルでactivateしたときの処理
*/
self.addEventListener("activate", event => {
event.waitUntil(clients.claim());
});
</code></pre>
<p>やっていることは、以下のような感じ。</p>
<ol>
<li>リクエストするときに、</li>
<li>現在のユーザからIDトークンを取得して</li>
<li>IDトークンをリクエストヘッダーに追加する</li>
</ol>
<p>ヘッダーにIDトークンが付与されているので、<br />
サーバ側でそれを見て、認証済みかをチェックする。</p>
<h3 id="nuxt.config.tsへの取り込み"><a href="#nuxt.config.ts%E3%81%B8%E3%81%AE%E5%8F%96%E3%82%8A%E8%BE%BC%E3%81%BF">nuxt.config.tsへの取り込み</a></h3>
<p>作成したサービスワーカを使うように、nuxt.config.tsに設定を追加する。<br />
ドキュメントだと<a target="_blank" rel="nofollow noopener" href="https://pwa.nuxtjs.org/modules/workbox.html#adding-custom-service-worker">このあたり</a>を参照。</p>
<pre><code class="typescript">// nuxt.config.ts
import { Configuration } from "@nuxt/types";
const config: Configuration = {
// ...略
modules: [
// ... 略
"@nuxtjs/pwa",
],
workbox: {
// 追加するスクリプトを指定。
// バンドルされないので、CDNのfirebase-appを追加しておく。
importScripts: [
"https://www.gstatic.com/firebasejs/7.6.1/firebase-app.js",
"https://www.gstatic.com/firebasejs/7.6.1/firebase-auth.js",
"sw-firebase-auth.js"
],
// 開発中でもsw.jsが生成されるように設定。
dev: process.env.MODE != "production",
},
// ...略
};
export default config;
</code></pre>
<h4 id="nuxtServerInitなどで認証状態をチェックする"><a href="#nuxtServerInit%E3%81%AA%E3%81%A9%E3%81%A7%E8%AA%8D%E8%A8%BC%E7%8A%B6%E6%85%8B%E3%82%92%E3%83%81%E3%82%A7%E3%83%83%E3%82%AF%E3%81%99%E3%82%8B">nuxtServerInitなどで認証状態をチェックする</a></h4>
<p>IDトークンもJWTデコードすると、UIDを取得できるけれど、<br />
有効かどうかをfirebase-adminでチェックする必要がある。</p>
<p>なので、まずは、firebase-adminのインスタンスを初期化するファイルを用意。</p>
<pre><code class="typescript">// ~/utils/firebaseAdmin.ts
let admin;
if (process.server) {
admin = require("firebase-admin");
if (!admin.apps.length) {
const serviceAccount = require("./path/to/your/key.json");
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
databaseURL: "https://your-database-url.firebaseio.com"
});
}
}
export default admin;
</code></pre>
<p>次にこれを使って、以下をしていく。</p>
<ol>
<li>リクエストヘッダからIDトークンを取得し、</li>
<li>firebase-adminを使ってIDトークンの有効性を確認</li>
</ol>
<p>以下は、<a target="_blank" rel="nofollow noopener" href="https://github.com/championswimmer/vuex-module-decorators">vuex-module-decorators</a>を使ったnuxtServerInitでチェックするサンプル。</p>
<pre><code class="typescript">// ~/store/index.ts
import { Context } from "@nuxt/types";
import { ActionContext } from "vuex/types";
import { ActionTree, Store } from "vuex";
import { initialiseStores } from "~/utils/store-accessor";
export const state = () => ({});
export type RootState = ReturnType<typeof state>;
const initializer = (store: Store<any>) => initialiseStores(store);
export const plugins = [initializer];
export const actions: ActionTree<any, any> = {
async nuxtServerInit(
context: ActionContext<RootState, RootState>,
server: Context
) {
// requestのAuthorizationからIDトークンを取得
const authorizationHeader = req.headers.authorization || "";
const components = authorizationHeader.split(" ");
const token = components.length > 1 ? components[1] : "";
if (!token) return;
// firebase-adminの初期化
const admin = require("~/utils/firebaseAdmin").default;
if (!admin) return;
// IDトークンの検証: 有効期限などをFirebaseでチェック
const decodedClaims = await admin.auth().verifyIdToken(token);
// 検証結果からUIDを取得
const uid = decodedClaims.uid;
// TODO: 認証状態に応じてなにかする
}
};
export * from "~/utils/store-accessor";
</code></pre>
<p>firebase-adminを取得する部分を</p>
<pre><code class="typescript">const admin = require("firebase-admin");
</code></pre>
<p>としていたり、<code>if (process.server)</code>や<code>if (!admin.apps.length)</code>などのチェックをせずにいたら、<br />
クライアント側でもバンドルされていて、うまく動かいない状態に...</p>
<h4 id="注意点"><a href="#%E6%B3%A8%E6%84%8F%E7%82%B9">注意点</a></h4>
<p>これで認証が必要なページでもいい感じSSRできた気がする(<em>´ω`</em>)</p>
<p>ただ、課題が残っていて、スーパーリロード/ハードリロードすると、<br />
Service Workderを介して、リクエストされないので、ヘッダに認証情報が付与されない...</p>
<p>ログを見ていると、スーパーリロード時には、描画されたあとに、Service Workderの登録されているよう。</p>
<p>そういった場合でも、利用したい場合には、従来のセッションCookieを利用する方法がよいかも?</p>
<ul>
<li><a target="_blank" rel="nofollow noopener" href="https://firebase.google.com/docs/auth/admin/manage-cookies?hl=ja">セッション Cookie を管理する | Firebase</a></li>
</ul>
<p>(もし良い方法があれば、教えてほしいです...)</p>
<hr />
<h4 id="おまけ: Service Workerのライフサイクル"><a href="#%E3%81%8A%E3%81%BE%E3%81%91%3A+Service+Worker%E3%81%AE%E3%83%A9%E3%82%A4%E3%83%95%E3%82%B5%E3%82%A4%E3%82%AF%E3%83%AB">おまけ: Service Workerのライフサイクル</a></h4>
<p>ここに書いてあった。<br />
- <a target="_blank" rel="nofollow noopener" href="https://developers.google.com/web/fundamentals/primers/service-workers?hl=ja">Service Worker の紹介 | Web Fundamentals | Google Developers</a></p>
<p><img src="https://developers.google.com/web/fundamentals/primers/service-workers/images/sw-lifecycle.png?hl=ja" width="400px"/></p>
<hr />
<p>以上!!</p>
<h2 id="こんなのつくってます!!"><a href="#%E3%81%93%E3%82%93%E3%81%AA%E3%81%AE%E3%81%A4%E3%81%8F%E3%81%A3%E3%81%A6%E3%81%BE%E3%81%99%21%21">こんなのつくってます!!</a></h2>
<p>積読用の読書管理アプリ 『積読ハウマッチ』をリリースしました!<br />
<a target="_blank" rel="nofollow noopener" href="https://tsundoku.site">積読ハウマッチ</a>は、Nuxt.js+Firebaseで開発してます!</p>
<p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/572d4947-f40b-e4dc-1c9c-bc584cd2a66c.png" width="200"/></p>
<p>もしよかったら、遊んでみてくださいヽ(=´▽`=)ノ</p>
<p>要望・感想・アドバイスなどあれば、<br />
公式アカウント(<a target="_blank" rel="nofollow noopener" href="https://twitter.com/MemoryLoverz">@MemoryLoverz</a>)や開発者(<a target="_blank" rel="nofollow noopener" href="https://twitter.com/kira_puka">@kira_puka</a>)まで♪</p>
<h1 id="参考にしたサイト様"><a href="#%E5%8F%82%E8%80%83%E3%81%AB%E3%81%97%E3%81%9F%E3%82%B5%E3%82%A4%E3%83%88%E6%A7%98">参考にしたサイト様</a></h1>
<ul>
<li><a target="_blank" rel="nofollow noopener" href="https://firebase.google.com/docs/auth/web/service-worker-sessions?hl=ja">Service Worker によるセッション管理 | Firebase</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/daishinkawa/items/915d918aba6bf7849b21#auth%E3%83%A9%E3%82%A4%E3%83%96%E3%83%A9%E3%83%AA%E3%81%AE%E7%B5%84%E3%81%BF%E8%BE%BC%E3%81%BF">Firebase (Hosting × Functions) × Nuxt.js (universal) で ユーザ認証のベストプラクティスを探る旅 その2 - Qiita</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/basho/items/acd6a17bb6e2a2f7a932#section3%E6%96%B0%E8%A6%8F%E7%99%BB%E9%8C%B2google%E3%82%A2%E3%82%AB%E3%82%A6%E3%83%B3%E3%83%88%E3%81%AE%E5%AE%9F%E8%A3%85">SSRモードのNuxtでのFirebase認証 - Qiita</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/y_fujieda/items/f9e765ac9d89ba241154#service-worker">Service Workerの基本とそれを使ってできること - Qiita</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/horo/items/175c8fd7513138308930">ServiceWorkerとCache APIを使ってオフラインでも動くWebアプリを作る - Qiita</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://developers.google.com/web/fundamentals/primers/service-workers/lifecycle?hl=ja">Service Worker のライフサイクル | Web Fundamentals | Google Developers</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://firebase.google.com/docs/auth/admin/manage-sessions">ユーザー セッションの管理 | Firebase</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://firebase.google.com/docs/auth/admin/manage-cookies">Manage Session Cookies | Firebase</a></li>
</ul>
きらぷか@積読ハウマッチ/SSSAPIなど
tag:crieit.net,2005:PublicArticle/15427
2019-09-26T22:08:54+09:00
2019-09-26T22:08:54+09:00
https://crieit.net/posts/React-PWA-Rails-GraphQL-RPG
React PWA + Rails GraphQLで作ったポモドーロRPGに使った技術やその選定理由を書いてみた
<p>先日、『<a target="_blank" rel="nofollow noopener" href="https://www.g-g-g-g.games">g4</a>』というポモドーロ+RPGなサービスをリリースしました。</p>
<p>そのサービスで使った技術について聞かれることがあったので残しておきます。</p>
<h1 id="どんなサービス?"><a href="#%E3%81%A9%E3%82%93%E3%81%AA%E3%82%B5%E3%83%BC%E3%83%93%E3%82%B9%3F">どんなサービス?</a></h1>
<p>ポモドーロ・タイマーを使い25分間集中すると経験値をもらえ、その経験値でレベルが上がる。<br />
って言う感じのやつです。</p>
<p><a target="_blank" rel="nofollow noopener" href="https://www.g-g-g-g.games">https://www.g-g-g-g.games</a></p>
<p><a target="_blank" rel="nofollow noopener" href="https://www.g-g-g-g.games"><a href="https://crieit.now.sh/upload_images/4857e38ac110f7f2ccb9a3a6c9851f425d8c46a290533.jpeg" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/4857e38ac110f7f2ccb9a3a6c9851f425d8c46a290533.jpeg?mw=700" alt="https___qiita-image-store.s3.ap-northeast-1.amazonaws.com_0_51402_6c1fef60-9f24-2233-821b-1b4b16575e21.jpeg" /></a></a></p>
<h2 id="こんな特徴があります。"><a href="#%E3%81%93%E3%82%93%E3%81%AA%E7%89%B9%E5%BE%B4%E3%81%8C%E3%81%82%E3%82%8A%E3%81%BE%E3%81%99%E3%80%82">こんな特徴があります。</a></h2>
<ul>
<li>ポモドーロ・タイマーやRPG的なUIはリッチで動きがある</li>
<li>現在のステータスをOGP画像にしてシェアできる</li>
<li>上昇する能力値や覚えるスキルは登録した文章を解析して決まる</li>
</ul>
<h1 id="構成はこんな感じ"><a href="#%E6%A7%8B%E6%88%90%E3%81%AF%E3%81%93%E3%82%93%E3%81%AA%E6%84%9F%E3%81%98">構成はこんな感じ</a></h1>
<p><a href="https://crieit.now.sh/upload_images/a91960cfcbabfe4677662c7e3a35b9225d8c468610acb.jpeg" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/a91960cfcbabfe4677662c7e3a35b9225d8c468610acb.jpeg?mw=700" alt="https___qiita-image-store.s3.ap-northeast-1.amazonaws.com_0_51402_1f7797cd-59e5-b556-2afe-b0a964acd0ac.jpeg" /></a></p>
<h2 id="フロントエンドの選定理由"><a href="#%E3%83%95%E3%83%AD%E3%83%B3%E3%83%88%E3%82%A8%E3%83%B3%E3%83%89%E3%81%AE%E9%81%B8%E5%AE%9A%E7%90%86%E7%94%B1">フロントエンドの選定理由</a></h2>
<p>フロントエンドはSSRしたReactアプリをfly.ioにおいています</p>
<h3 id="[React]"><a href="#%5BReact%5D">[React]</a></h3>
<p>自分は過去に仕事でNuxt.jsや生Vue.jsを使ったことがあり、個人ではExpoやNext.jsでReactにも触っていました。</p>
<p>今回Reactを選択した理由は以下です。</p>
<ul>
<li>型が欲しかった。Typescriptを使いたかった</li>
<li>Reactのほうが書いていて楽しい(個人の感想です)
<ul>
<li>React Hooksめっちゃいい</li>
</ul></li>
<li>Apolloを使うならVueよりReactのほうが色々揃っている</li>
</ul>
<h3>[<a target="_blank" rel="nofollow noopener" href="https://fly.io">fly.io</a>]</h3>
<p>SSRするためにNode.jsをホスティングするやつが欲しかったので選定。<br />
<a target="_blank" rel="nofollow noopener" href="https://mizchi.hatenablog.com/entry/2019/02/21/235403">mizchiさんのブログ</a>で知り、面白そうだなと思ったのがきっかけ。<br />
べつにfirebase hostingでもよかったですが、以下が決め手</p>
<ul>
<li>firebase hosting同様初期導入費用がほぼ0</li>
<li>CDN</li>
<li>キャッシュコントロール</li>
<li>svgを使ったOGP画像の生成ができる</li>
</ul>
<p>特に、</p>
<blockquote>
<p>svgを使ったOGP画像の生成ができる</p>
</blockquote>
<p>に有用性を感じて選定したんですが、現時点ではカスタムフォントと日本語への対応が十分でなく、別途Cloud Runを使うことになってしまっています(後述)<br />
(サポートに聞いたらリポジトリ教えてもらったので(OSS)対応できるようなら自分で対応してなげたいなぁ)</p>
<h5 id="キャッシュコントロールもよい"><a href="#%E3%82%AD%E3%83%A3%E3%83%83%E3%82%B7%E3%83%A5%E3%82%B3%E3%83%B3%E3%83%88%E3%83%AD%E3%83%BC%E3%83%AB%E3%82%82%E3%82%88%E3%81%84">キャッシュコントロールもよい</a></h5>
<p>キャッシュの操作も面白くて、<br />
g4の公開用ページではAPIからもらった結果をキャッシュして、キャッシュがあればそれをそのまま返すということをしています。</p>
<p>これによってAPIサーバーへの負荷はかなり低くなってると思います。<br />
APIサーバー側からは、データ更新時にfly.ioのキャッシュを削除する操作をしていて、データが更新されるまではfly.ioはキャッシュを使い続けてます。</p>
<p>この辺のAPIが普通に使いやすくて簡単にかけるのでめっちゃ良きです。</p>
<h5 id="fly.io良いよ"><a href="#fly.io%E8%89%AF%E3%81%84%E3%82%88">fly.io良いよ</a></h5>
<p>fly.io自体ははデプロイも早いし、導入コストも安いので<a target="_blank" rel="nofollow noopener" href="https://zeit.co">now</a>とかと並べて、手軽にNode.jsをホスティングするための選択肢にしてもいいんじゃないかなって思います。</p>
<h3 id="Next.jsを選ばなかった理由"><a href="#Next.js%E3%82%92%E9%81%B8%E3%81%B0%E3%81%AA%E3%81%8B%E3%81%A3%E3%81%9F%E7%90%86%E7%94%B1">Next.jsを選ばなかった理由</a></h3>
<p>SSRする予定ではありましたが、OGPだけ生成できればいいなというレベルだったので、通常のReactアプリで作りました。<br />
なるべく継続開発するため、アップデートの手間と依存を減らしたかったのも少しある</p>
<h3 id="[PWA]を選定した理由"><a href="#%5BPWA%5D%E3%82%92%E9%81%B8%E5%AE%9A%E3%81%97%E3%81%9F%E7%90%86%E7%94%B1">[PWA]を選定した理由</a></h3>
<p>最初は、<a target="_blank" rel="nofollow noopener" href="https://expo.io">Expo</a>で作ろうかと思っていました。</p>
<p>もともとSPAでアプリっぽいUIを作る仕事を受けたときに、Nuxt.js + Firebaseで作り、マルチブラウザ対応が辛いと思った経験がありました。</p>
<p>RNならその辺いい感じに作れるかもと思って<a target="_blank" rel="nofollow noopener" href="https://shwld.net/seicho-release/">みんなの成長</a> というアプリでExpoを試しみて、使い勝手が良い事までは確認してました。</p>
<p>ただ今回はモバイルより、デスクトップアプリのほうが欲しいということに気づき、<br />
Electronで作りやすかったり、PWAならそのままインストールもできちゃうのでWebでいいかなって思いPWAにしました。</p>
<p>なのでマルチブラウザ辛いは解決してないです。<br />
ここは個人開発なので修行と割り切って本業に生かしていきます。<br />
Safari対応が結構めんどいけど、ユーザー多くて無視できない...</p>
<p>マルチブラウザと言っても、現状Chrome、Safari以外はほぼ無視してまっす。</p>
<h3>[<a target="_blank" rel="nofollow noopener" href="https://www.apollographql.com">Apollo Client (react-apollo)</a>]</h3>
<p>これもSPAでアプリっぽいUIを作る仕事で、Nuxt.js + Firebaseで作ったときに、Firebaseのデータベース(このときはrealtime database使ってた)をフロントに反映するのが辛くて、同じようにREST APIも辛いだろうなと思って、GraphQLに手をだしたのがきっかけでした。</p>
<p>GraphQLだけでもすごく良いのですが、Apollo Clientがいいのはキャッシュコントールで、<br />
クエリ毎にキャッシュを優先するかサーバーを優先するかを選択できます。</p>
<p>また、キャッシュがあれば再取得しないなどを簡単に書けたり、とにかくこれはSPAのベストプラクティスだなって感じの実装をシンプルに書けます。</p>
<p>Mutationするとキャッシュが書き換わったりするのもめっちゃ良き</p>
<p>正直この辺のこと考えながらSPA実装するのってかなり大変じゃない?ってか大変だった<br />
アプリライクなUIのSPAならApollo Client使おうぜ!</p>
<p>こいつのおかげでこれまたAPIサーバーの負荷はまあまあ抑えられてそうです。</p>
<h2 id="[Firebase Auth, Storage]について"><a href="#%5BFirebase+Auth%2C+Storage%5D%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6">[Firebase Auth, Storage]について</a></h2>
<p>バックエンドはRailsですが、APIサーバー作るのに正直Deviseとか使いたくないです(完)</p>
<p>Twitter連携やら諸々簡単なので選ばない理由がない。<br />
Auth0は選択肢に入るけど今回GCPなのでGoogleでまとめられるし、そこまで込み入った認証必要ないしでこれにしました。</p>
<p>Storageは別に何でもよかったですが、Firebase Authの認証ユーザー単位での権限指定が楽なので使い勝手は良いです。</p>
<h2 id="デザインについて"><a href="#%E3%83%87%E3%82%B6%E3%82%A4%E3%83%B3%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6">デザインについて</a></h2>
<p>g4のデザインはレスポンシブのヘルパーとして<a target="_blank" rel="nofollow noopener" href="https://github.com/twbs/bootstrap/blob/master/dist/css/bootstrap-grid.css">bootstrap-grid</a>だけ使わせてもらってます。<br />
めっちゃ便利なのでおすすめ。</p>
<p>それ以外はcss全部自分で書いてます。</p>
<p>コンポーネントファーストで作成しており、Atomic Designの粒度で、<a target="_blank" rel="nofollow noopener" href="https://storybook.js.org">Storybook</a>作ってコンポーネントを作ってる感じです。<br />
Storybookは割とぶっ壊れやすかったり、他のビルド設定と競合とか重複したり、辛みもありますが、<a target="_blank" rel="nofollow noopener" href="https://storybook.js.org/docs/testing/structural-testing/">storyshots</a>によるスナップショットの差分確認ができるのと、テストの安心感があるので、無いと無いで不安ではあります。</p>
<h2 id="バックエンドの選定理由"><a href="#%E3%83%90%E3%83%83%E3%82%AF%E3%82%A8%E3%83%B3%E3%83%89%E3%81%AE%E9%81%B8%E5%AE%9A%E7%90%86%E7%94%B1">バックエンドの選定理由</a></h2>
<h3>[<a target="_blank" rel="nofollow noopener" href="https://graphql-ruby.org">graphql-ruby</a> & Ruby on Rails]</h3>
<p>Apollo Clientの項で書きましたが、GraphQLを使うことは決まっていました。<br />
GraphQLをマネージドでホスティングしてくれるやつを探したりしたんですが、普段Railsエンジニアなので慣れてるからテストとかも書きやすいしRailsでいっかで、graphql-rubyにしました。</p>
<p>Railsにするとランニングコストは割とかかるんですが、今回は個人開発のメインに据えて長く継続開発したいを目的にしてたので、<br />
稼働してないものにお金払うみたいなことにならない想定(自分が使い続ける)で、コストかける覚悟はしました。</p>
<p>graphql-rubyは仕事でも使ってるので、今後も押していきたい存在。</p>
<h3 id="[GCP/App Engine]"><a href="#%5BGCP%2FApp+Engine%5D">[GCP/App Engine]</a></h3>
<p>App Engineについては<a target="_blank" rel="nofollow noopener" href="https://qiita.com/shwld/items/e86ee3f642c7857dd56e">こちらの記事</a>で書きました。</p>
<p>要約するとHerokuでも良かったけど、使ってみたかったのでGCPにした。です。</p>
<h2 id="[GCP/Natural Language API]"><a href="#%5BGCP%2FNatural+Language+API%5D">[GCP/Natural Language API]</a></h2>
<p>便利。Azureとかにもありますが、今回はGoogle統一で行きました。くらいの選定理由</p>
<h2 id="[GCP/Cloud Run]について"><a href="#%5BGCP%2FCloud+Run%5D%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6">[GCP/Cloud Run]について</a></h2>
<p>Cloud Runは今回使う予定は全くありませんでした。</p>
<p>今利用している理由としては、</p>
<ul>
<li>fly.ioでOGPを生成できるとふんでいたができなかった。</li>
<li>とりあえず手っ取り早くカスタムしたDockerimageでfry.ioをあまりお金をかけずに使えるとこはないか。</li>
</ul>
<p>からの、Cloud Run。</p>
<p>中身は日本語とフォントをインストールした普通のNodeイメージに<br />
fly.ioのサーバーを単純に乗っけて起動しているだけです。(プレビルドして実行したいがやり方が分からずdevサーバー起動しているのでなんとかしたい。誰かやり方知りませんか。)</p>
<p>なので、いずれはfly.ioだけでできるようして、Cloud Runをやめたいです。</p>
<h4 id="Cloud Run超便利"><a href="#Cloud+Run%E8%B6%85%E4%BE%BF%E5%88%A9">Cloud Run超便利</a></h4>
<p>そんな理由で使い始めましたが、驚くくらい簡単かつ手軽にカスタムDockerイメージが使えるので、Cloud Runめっちゃ便利です。<br />
今後活用していきたい。</p>
<h2 id="おわりに"><a href="#%E3%81%8A%E3%82%8F%E3%82%8A%E3%81%AB">おわりに</a></h2>
<p>割と構成的にはガチよりになりました。<br />
自分の技術力総動員してる感じなのでテンション上がってめっちゃ楽しい!</p>
<p>g4の運用を支える技術についても今後書いていきたいです。</p>
<p>みんなでレベル上げしましょう!</p>
<p><a target="_blank" rel="nofollow noopener" href="https://www.g-g-g-g.games">https://www.g-g-g-g.games</a></p>
<p><a target="_blank" rel="nofollow noopener" href="https://www.g-g-g-g.games"><a href="https://crieit.now.sh/upload_images/4857e38ac110f7f2ccb9a3a6c9851f425d8c46a290533.jpeg" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/4857e38ac110f7f2ccb9a3a6c9851f425d8c46a290533.jpeg?mw=700" alt="https___qiita-image-store.s3.ap-northeast-1.amazonaws.com_0_51402_6c1fef60-9f24-2233-821b-1b4b16575e21.jpeg" /></a></a></p>
shwld