tag:crieit.net,2005:https://crieit.net/tags/Nuxt.js/feed
「Nuxt.js」の記事 - Crieit
Crieitでタグ「Nuxt.js」に投稿された最近の記事
2021-12-27T23:20:34+09:00
https://crieit.net/tags/Nuxt.js/feed
tag:crieit.net,2005:PublicArticle/17895
2021-12-27T23:20:34+09:00
2021-12-27T23:20:34+09:00
https://crieit.net/posts/Web-61c9cbb273b27
【個人開発】踏破した都道府県を日本地図でシェアできるWebサービス「日本踏破図🗾」を作りました
<h2 id="はじめに"><a href="#%E3%81%AF%E3%81%98%E3%82%81%E3%81%AB">はじめに</a></h2>
<p>「日本踏破図🗾」というWebサービスをNuxt × Vuetifyで個人開発しました!</p>
<p><a target="_blank" rel="nofollow noopener" href="https://traverse-japan.dev/">日本踏破図🗾</a></p>
<h2 id="どんなWebサービスか"><a href="#%E3%81%A9%E3%82%93%E3%81%AAWeb%E3%82%B5%E3%83%BC%E3%83%93%E3%82%B9%E3%81%8B">どんなWebサービスか</a></h2>
<p>踏破した都道府県を日本地図でチェックすることができ、自分が今までに日本をどれだけ踏破したかをみんなにシェアできます👍<br />
以下のように日本地図上で都道府県をチェックできます。これをシェアボタンでシェアすると・・・<br />
<a href="https://crieit.now.sh/upload_images/c21a19f689e6b03b8fd32ed155a13cc561c9c9f546005.gif" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/c21a19f689e6b03b8fd32ed155a13cc561c9c9f546005.gif?mw=700" alt="cap3.gif" /></a></p>
<p>以下のようなOGP画像でシェア可能です!地図のカラーも好きな色に変更することができます。<br />
<a href="https://crieit.now.sh/upload_images/c96a5c5d12e327a1c82680dd58a2f4c261c9c899adde1.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/c96a5c5d12e327a1c82680dd58a2f4c261c9c899adde1.png?mw=700" alt="cap2.png" /></a></p>
<h2 id="使用したフレームワーク・インフラなど"><a href="#%E4%BD%BF%E7%94%A8%E3%81%97%E3%81%9F%E3%83%95%E3%83%AC%E3%83%BC%E3%83%A0%E3%83%AF%E3%83%BC%E3%82%AF%E3%83%BB%E3%82%A4%E3%83%B3%E3%83%95%E3%83%A9%E3%81%AA%E3%81%A9">使用したフレームワーク・インフラなど</a></h2>
<ul>
<li>NuxtJS(Nuxt Bridge)</li>
<li>Vuetify</li>
<li>vuex(vuex-module-decorators)</li>
<li>PWA</li>
<li>Vercel(Serverless Functions)</li>
</ul>
<p>主なものになります。多機能なWebサービスではないのでそんなに色々使ってはいません。<br />
よく使うNuxtとVuetifyでフロントを構築して、Nuxtの<a target="_blank" rel="nofollow noopener" href="https://v3.nuxtjs.org/concepts/server-engine/">Nitroエンジン</a>を使ってVercelのServerless Functionsへデプロイしています。</p>
<p>当初はSSRにするつもりはなかったのですが、動的OGPを実装するために色々試行錯誤した結果SSRにしました。ただ、Nuxt3/Nuxt Bridgeから使えるようになったNitroエンジンのおかげで高速&ゼロコンフィグでVercelを利用できます。</p>
<h2 id="苦労したところ"><a href="#%E8%8B%A6%E5%8A%B4%E3%81%97%E3%81%9F%E3%81%A8%E3%81%93%E3%82%8D">苦労したところ</a></h2>
<h3 id="動的OGPをどう実装するか"><a href="#%E5%8B%95%E7%9A%84OGP%E3%82%92%E3%81%A9%E3%81%86%E5%AE%9F%E8%A3%85%E3%81%99%E3%82%8B%E3%81%8B">動的OGPをどう実装するか</a></h3>
<p>正確に言うと「いかに簡単かつ無料で実装するか」に苦労しました。<br />
検索するとfirebaseを使ってmetaだけSSRしたものを返してリダイレクトさせる等々、実現方法自体は色々でてきます。</p>
<p>ただ、正直なところこの規模・機能のWebサービスにそこまでしたくなかったんですね(できるだけシンプルな作りにするということも最初に決めていました)</p>
<p>最終的に実装した方法は以下です。</p>
<ol>
<li>OGP画像を生成して画像を返すServerless Functionsを用意する</li>
<li>Functionsへ画像生成に必要な情報(チェック済みの都道府県、地図カラー)を渡す</li>
</ol>
<p>必要な情報をURLパラメータに持ち、それを利用してmetaタグのog:imageのエンドポイントを作っています。<br />
そのためサービス自体はSSRする必要がありますがVercelを使えば十分な無料枠でServerless FunctionsでSSRできます。</p>
<p>個人的には下手にhackyな方法をとるよりも、SSRが必要な場面ではおとなしくSSRしておいた方が良い気がします🤔<br />
(結局のところTwitter・Facebookのクローラーのためだけにあれこれやりたくない)</p>
<p>ちなみにNetlifyに<a target="_blank" rel="nofollow noopener" href="https://docs.netlify.com/site-deploys/post-processing/prerendering/">Prerendering</a>という機能があるのですが、これを有効にすることで事前レンダリングされたHTMLを返すことができます。<br />
SPA構成にしてPrerenderingを試してみたのですが、どうもPrerenderingの対象はUserAgentを見ているだけらしくTwitterクローラーであればNetlify Functionsのエンドポイントも問答無用でPrerenderingされるみたいでした。<br />
なのでOGP画像を返すはずのFunctionsがTwitterクローラーの時はPrerenderingされてHTMLを返すようになってしまい、OGP画像が表示されず使えませんでした。。</p>
<p>まぁ、NetlifyのPrerenderingはキャッシュ時間(24〜48時間)もコントロールできないし、なによりBETAなのでまだ実用的ではないですね。。</p>
<h3 id="サイトデザイン"><a href="#%E3%82%B5%E3%82%A4%E3%83%88%E3%83%87%E3%82%B6%E3%82%A4%E3%83%B3">サイトデザイン</a></h3>
<p>デザインというのはセンスだと思っています。つまるところ、それを磨くためには多大な時間・努力が必要ということです😉<br />
私はデザイナーではないのでそこについて時間をかけて勉強するのは得策ではありません。<br />
悩み・試行した結果、全体的にカードベースのデザインになりました。</p>
<p>また、根本のデザインはいわゆるUIフレームワークに頼るわけですが今回はよく使うVuetifyを利用しました。Vuetifyのコンポーネントを使うことで何も考えずそこそこきれいなサイトができあがります。</p>
<p><a target="_blank" rel="nofollow noopener" href="https://vuetifyjs.com/ja/">https://vuetifyjs.com/ja/</a></p>
<p>ただ、今回は個人的にやってみたいと思っていたGlassmorphism(グラスモーフィズム)チックなデザインを実装しました。<br />
これが間違いで、Vuetifyはマテリアルデザインを実装するフレームワークなんですね。それに対してマテリアルデザインからかけ離れるようなものを実装すると色々と歪みが生じます🙃</p>
<p>importantを使ってVuetifyのcssを上書きして実装しましたが、こういうことをする場合はTailwind CSSを利用するべきでした。。</p>
<p><a target="_blank" rel="nofollow noopener" href="https://tailwindcss.com/">https://tailwindcss.com/</a></p>
<h2 id="運営コストについて"><a href="#%E9%81%8B%E5%96%B6%E3%82%B3%E3%82%B9%E3%83%88%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6">運営コストについて</a></h2>
<p>現状、ドメイン代オンリーです。<br />
上の方でも少し書いていますが個人開発においては<strong>無料で運営できること</strong>にこだわっています。</p>
<p>まぁ、お金はかからないに越したことはないし、お金がかかってくるとちゃんとメンテしなくちゃとか、マネタイズ考えないと!とか思ってしまうわけですが無料であればひとまず放置で問題ないですからね。</p>
<p>ちなみに、このWebサービスでは最初からマネタイズは一切考えていません。機能的にサービス自体がマネタイズにつながるものでもないですがアドセンス等の広告等も一切はらないと決めて開発をスタートしました。</p>
<h2 id="サイトパフォーマンスについて"><a href="#%E3%82%B5%E3%82%A4%E3%83%88%E3%83%91%E3%83%95%E3%82%A9%E3%83%BC%E3%83%9E%E3%83%B3%E3%82%B9%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6">サイトパフォーマンスについて</a></h2>
<p>使っている技術からもわかるかと思いますが、パフォーマンスは問題なく良いです。<br />
特にSSRするとやっぱり初期描画のパフォーマンスがすごいです(コールドスタート時以外)</p>
<p>いくら無料のインフラにこだわっているかといってパフォーマンスを犠牲にしたらお話にならないですからね😉</p>
<p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271387/b46f5426-3ef1-c2d9-327f-071178f7d3b2.png" alt="image.png" /></p>
<h2 id="機能の説明"><a href="#%E6%A9%9F%E8%83%BD%E3%81%AE%E8%AA%AC%E6%98%8E">機能の説明</a></h2>
<p>日本踏破図🗾でできることを改めて説明させていただきます。</p>
<h3 id="日本地図で踏破した都道府県をチェック"><a href="#%E6%97%A5%E6%9C%AC%E5%9C%B0%E5%9B%B3%E3%81%A7%E8%B8%8F%E7%A0%B4%E3%81%97%E3%81%9F%E9%83%BD%E9%81%93%E5%BA%9C%E7%9C%8C%E3%82%92%E3%83%81%E3%82%A7%E3%83%83%E3%82%AF">日本地図で踏破した都道府県をチェック</a></h3>
<p><a href="https://crieit.now.sh/upload_images/8bd592eed3b0970775e45c8493e1361161c9ca82b4032.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/8bd592eed3b0970775e45c8493e1361161c9ca82b4032.png?mw=700" alt="image.png" /></a></p>
<p>日本地図上の都道府県を選択することができます。選択状況した数によって日本をどれくらい踏破したかがわかります。<br />
また、シェアボタンから踏破した都道府県をシェアすることができます❗</p>
<h3 id="日本地図のメニュー"><a href="#%E6%97%A5%E6%9C%AC%E5%9C%B0%E5%9B%B3%E3%81%AE%E3%83%A1%E3%83%8B%E3%83%A5%E3%83%BC">日本地図のメニュー</a></h3>
<p><a href="https://crieit.now.sh/upload_images/8bd592eed3b0970775e45c8493e1361161c9ca9f1b976.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/8bd592eed3b0970775e45c8493e1361161c9ca9f1b976.png?mw=700" alt="image.png" /></a></p>
<p>地図メニューから、それぞれの機能を利用できます。</p>
<ul>
<li>地図をリセットする(都道府県の選択状態・色をリセットする)</li>
<li>地図の色を変える(日本地図の色を変更できます)</li>
<li>日本を踏破する(すべての都道府県を選択状態にします)</li>
<li>コピー(日本地図をクリップボードへコピーします)</li>
<li>ダウンロード(日本地図をpng画像としてダウンロードします)</li>
</ul>
<h3 id="チェックボックス"><a href="#%E3%83%81%E3%82%A7%E3%83%83%E3%82%AF%E3%83%9C%E3%83%83%E3%82%AF%E3%82%B9">チェックボックス</a></h3>
<p><a href="https://crieit.now.sh/upload_images/8bd592eed3b0970775e45c8493e1361161c9cab5eba93.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/8bd592eed3b0970775e45c8493e1361161c9cab5eba93.png?mw=700" alt="image.png" /></a></p>
<p>日本地図上から都道府県を選択する以外に、チェックボックスでも選択することができます。チェックボックスで選択すれば連動して日本地図上の都道府県も選択状態になります。</p>
<h2 id="ソースコード"><a href="#%E3%82%BD%E3%83%BC%E3%82%B9%E3%82%B3%E3%83%BC%E3%83%89">ソースコード</a></h2>
<p>日本踏破図🗾のソースコードはMITライセンスで公開しています👉</p>
<p><a target="_blank" rel="nofollow noopener" href="https://github.com/k-urtica/traverse-japan">https://github.com/k-urtica/traverse-japan</a></p>
<h2 id="さいごに"><a href="#%E3%81%95%E3%81%84%E3%81%94%E3%81%AB">さいごに</a></h2>
<p>登録ログイン不要で使えるのでぜひ使ってみてください。<br />
よろしくおねがいします!</p>
<p>🌐 サイト:<a target="_blank" rel="nofollow noopener" href="https://traverse-japan.dev/">https://traverse-japan.dev/</a><br />
👀 開発:<a target="_blank" rel="nofollow noopener" href="https://twitter.com/k_urtica">K</a><br />
🚩 ソース:<a target="_blank" rel="nofollow noopener" href="https://github.com/k-urtica/traverse-japan">https://github.com/k-urtica/traverse-japan</a></p>
<h2 id="おまけ"><a href="#%E3%81%8A%E3%81%BE%E3%81%91">おまけ</a></h2>
<p>Web ToolBoxというブラウザで無料で使えるツール集を公開しています。<br />
こちらもよろしくおねがいします。</p>
<p><a target="_blank" rel="nofollow noopener" href="https://web-toolbox.dev/">https://web-toolbox.dev/</a></p>
K@個人開発バックエンドエンジニア
tag:crieit.net,2005:PublicArticle/17721
2021-10-25T15:29:45+09:00
2021-10-25T15:31:36+09:00
https://crieit.net/posts/Nuxt-js-Rails-OSS-GitHub-web
【個人開発】(Nuxt.js + Rails)OSSやプロジェクトの、ソースコードを管理するGitHubのように、web上で結合テストをチームで同時に管理・実行・記録出来るプラットフォームを開発しました!
<h1 id="はじめに"><a href="#%E3%81%AF%E3%81%98%E3%82%81%E3%81%AB">はじめに</a></h1>
<p>今回、個人で開発したWebサービス「Itamaster」の本番稼働版をリリースすることが出来ました!<br />
こちらのサービスを開発しようと思った経緯は、ある日会社で、統合テストの管理・実行を行っていたときに、<strong>「何故ソースコードの管理プラットフォーム(GitHub等)はあるのに、結合テストの管理プラットフォームは無いんだろう」</strong>と思った事がきっかけです。<br />
開発期間は八ヶ月ほど、作業工数は大体700時間です。</p>
<p>弊社、私の所属しているチームは結合テストはExcelによって管理しており、実行記録もExcelを利用しています。<br />
Excelでの管理・実行には、以下のような不便な点がありました。</p>
<ul>
<li><p>Excelでの作業は、PCにかかる負荷が大きかったこと。<br />
会社のPCは自宅で扱うようなハイスペックなものではないため、Excelで作業を行っていると、セルをコピーしたりした時に<strong>PCがフリーズしたり、突然Excelが落ちて、保存していないデータが復旧しなかったり</strong>と、作業中に感じる<strong>ストレス</strong>がとても大きかったです。</p></li>
<li><p>Excelだと、複数人で一つのファイルに対する作業が出来ないこと。<br />
共有設定にして複数人で同時に作業すると、Excelが一層重くなったりして、とても実用出来るものではありませんでした。<br />
Excelだと、<strong>一つのファイルは同時に一人でしか作業が出来ず、待ち時間の発生</strong>するメンバーがいました。</p></li>
<li><p>ローカルで作業しているメンバーの作業状況が把握出来ないこと。<br />
私は管理者経験はありませんが、毎日の定時報告会や、Slackでのメッセージを見る限り、作業状況の把握が少しアナログに感じました。<br />
Redmine等の進捗管理ツールも利用していますが、結局は自己申告であり、<strong>「どのくらいの作業が終わっていて、残っているのか」</strong>を管理者が一目で把握することが出来れば、課題を解決できると考えました。</p></li>
<li><p>サマリーレポートや、結果一覧表の作成が面倒<br />
Excelから情報を整理して、Excelに表を作って、割合を求めて、グラフ化して......<br />
テストが追加されたり、状況が変わると、また、情報を整理して、Excelに表を作って、割合を求めて、グラフ化して......<br />
<strong>「あ"あ"あ"あ"あ"あ"あ"あ"あ"あ"あ"あ"あ"あ"あ"あ"あ"!!!!!!!」</strong><br />
ってなりました。</p></li>
</ul>
<p>開発告知記事を書くのは初めてで、至らない点が多々あるかと思いますが<br />
サービスの紹介、開発時の苦労や反省、所感を統括して書きたいと思います。<br />
最後まで読んでいただければ幸いです。</p>
<p>【Twitterリンク】<br />
<a target="_blank" rel="nofollow noopener" href="https://twitter.com/@itamaster_">https://twitter.com/@itamaster_</a></p>
<p>【Itamaster】<br />
<a target="_blank" rel="nofollow noopener" href="https://itamaster.work">https://itamaster.work</a><br />
PC推奨です。</p>
<p>【Itamaster 操作ガイド】(Itamasterのヘッダーと、フッターにも操作ガイドリンクを設置しています。)<br />
<a target="_blank" rel="nofollow noopener" href="https://qiita.com/Itamaster/items/3c271181e0c9c06d1d08">https://qiita.com/Itamaster/items/3c271181e0c9c06d1d08</a></p>
<h1 id="使用技術"><a href="#%E4%BD%BF%E7%94%A8%E6%8A%80%E8%A1%93">使用技術</a></h1>
<ul>
<li>npm 7.6.3</li>
<li>nuxtjs 2.15.3 (SPA モード)
<ul>
<li>サクサクと、Excelのようなもっさり感やストレスの無いUXにしたかったため、SPAによって構築しました。</li>
</ul></li>
<li>@nuxtjs/storybook 4.0.2
<ul>
<li>コンポーネントのレイアウト・動作を開発中にリアルタイムで確認出来る、コンポーネントカタログ作成ツール。<br />
フロントエンドの単体テストの役割も果たします。</li>
</ul></li>
<li>vuetify (@nuxtjs/vuetify 1.12.1)
<ul>
<li>MaterialDesignベースのデザインライブラリです。<br />
少し挙動が不安定な点が目立ちました。</li>
</ul></li>
<li>ruby 2.6.8</li>
<li>Ruby on Rails 6.1.4 (APIモード)</li>
<li>Device
<ul>
<li>ログイン周り。フロントエンドにはトークンを渡します。フロントエンドではトークンをsession storageに一時保存し、axiosのインターセプターに噛ませることで、ログイン機能付きの認証認可を実現しました。</li>
</ul></li>
<li>EC2(t2.small)</li>
<li>S3</li>
<li>ALB</li>
<li>ACM</li>
<li>RDS (mysql)</li>
<li>Route53</li>
<li>nginx</li>
<li>Stripe
<ul>
<li>クレジットカード決済機能をAPI利用できます。<br />
顧客情報や、クレジットカード情報を自サーバーで持たなくてよいため、セキュアなシステムの構築が可能です。<br />
また、同様に、決済機能のAPI利用はリスク管理の面からも有効です。</li>
</ul></li>
</ul>
<h1 id="アプリ概要"><a href="#%E3%82%A2%E3%83%97%E3%83%AA%E6%A6%82%E8%A6%81">アプリ概要</a></h1>
<p><strong>結合テストの自動化を行わないチームや、手動でのテストを行いたい状況が対象のWebサービス</strong>です。<br />
ユーザーをメールアドレスで招待し、チームを結成。<br />
プロジェクトを立ち上げて、テストスイート毎にテストケースを管理・実行が出来ます。</p>
<p>⇓チームの情報を表示するダイアログ<br />
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1869061/865d4a70-6ed5-c5ba-391f-ced31d721833.png" alt="image.png" /></p>
<p>⇓プロジェクト、プロジェクト内のテストスイートの管理を行う画面<br />
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1869061/b55c1892-1c76-b9aa-37aa-b0eabc7842b4.png" alt="image.png" /></p>
<p>⇓テストスイートの情報を表示するダイアログ<br />
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1869061/c64f28f2-eab8-c765-a40e-284b299735bc.png" alt="image.png" /></p>
<p>⇓テストケースの情報を表示するダイアログ<br />
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1869061/e43628c2-0dec-5a4d-c14c-fe672a860915.png" alt="image.png" /></p>
<p>⇓テストスイート内のテストケースを実行する画面<br />
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1869061/002c37d0-8c55-ab2d-6415-e45e3d331894.png" alt="image.png" /></p>
<p>また、テスト結果一覧表や、テストサマリーレポートも出力可能です。<br />
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1869061/5ce63e70-6a27-8f4c-65a9-f1da499f672f.png" alt="image.png" /></p>
<p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1869061/c230b738-0ed5-be27-1e32-2983c6c48d98.png" alt="image.png" /><br />
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1869061/d80d35c5-4b9e-204f-d960-f9f4a9ececad.png" alt="image.png" /></p>
<h1 id="こだわった箇所"><a href="#%E3%81%93%E3%81%A0%E3%82%8F%E3%81%A3%E3%81%9F%E7%AE%87%E6%89%80">こだわった箇所</a></h1>
<h2 id="全体的に、明るい色を起用すること"><a href="#%E5%85%A8%E4%BD%93%E7%9A%84%E3%81%AB%E3%80%81%E6%98%8E%E3%82%8B%E3%81%84%E8%89%B2%E3%82%92%E8%B5%B7%E7%94%A8%E3%81%99%E3%82%8B%E3%81%93%E3%81%A8">全体的に、明るい色を起用すること</a></h2>
<p>テストという地味な作業は、設計書作成の次に気が滅入り易くなります。<br />
暗い気持ちになってしまわぬよう、明るい色を起用しつつ、<br />
緑や薄めの青色をメインに使う事で、ユーザーのモチベーションを維持します。</p>
<h2 id="アイコンを多用し、直感的な操作を可能にすること"><a href="#%E3%82%A2%E3%82%A4%E3%82%B3%E3%83%B3%E3%82%92%E5%A4%9A%E7%94%A8%E3%81%97%E3%80%81%E7%9B%B4%E6%84%9F%E7%9A%84%E3%81%AA%E6%93%8D%E4%BD%9C%E3%82%92%E5%8F%AF%E8%83%BD%E3%81%AB%E3%81%99%E3%82%8B%E3%81%93%E3%81%A8">アイコンを多用し、直感的な操作を可能にすること</a></h2>
<p>画面にはアイコンを多用して、文字を読まなくても色とアイコンから、ボタンの機能を推測することが可能です。<br />
直感的に操作することが可能で、作業の効率を向上します。</p>
<h2 id="進捗バーをプロジェクトと、テストスイート毎に表示可能にし、一目で状況を確認可能に"><a href="#%E9%80%B2%E6%8D%97%E3%83%90%E3%83%BC%E3%82%92%E3%83%97%E3%83%AD%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%81%A8%E3%80%81%E3%83%86%E3%82%B9%E3%83%88%E3%82%B9%E3%82%A4%E3%83%BC%E3%83%88%E6%AF%8E%E3%81%AB%E8%A1%A8%E7%A4%BA%E5%8F%AF%E8%83%BD%E3%81%AB%E3%81%97%E3%80%81%E4%B8%80%E7%9B%AE%E3%81%A7%E7%8A%B6%E6%B3%81%E3%82%92%E7%A2%BA%E8%AA%8D%E5%8F%AF%E8%83%BD%E3%81%AB">進捗バーをプロジェクトと、テストスイート毎に表示可能にし、一目で状況を確認可能に</a></h2>
<p>色分けされた進捗バーは、ホバーすることでそれぞれのステータスの件数を確認可能です。<br />
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1869061/7046daf8-49c5-af8c-1ab4-6df7b1d35f1c.png" alt="image.png" /><br />
チームの管理者が、「現在プロジェクト全体で、どれだけのテストが完了しているのか」を一目で確認することが可能です。</p>
<h2 id="自分独自のアイコンを設定可能にすること"><a href="#%E8%87%AA%E5%88%86%E7%8B%AC%E8%87%AA%E3%81%AE%E3%82%A2%E3%82%A4%E3%82%B3%E3%83%B3%E3%82%92%E8%A8%AD%E5%AE%9A%E5%8F%AF%E8%83%BD%E3%81%AB%E3%81%99%E3%82%8B%E3%81%93%E3%81%A8">自分独自のアイコンを設定可能にすること</a></h2>
<p><strong>Itamasterは、GitHubのように、OSSのテストをオープンに行う事の出来るプラットフォームを目指します。</strong><br />
アイコンを設定可能にし、画面に多用することで、<strong>今後実装するSNS的な機能が違和感なく利用できます。</strong><br />
(Publicなプロジェクトを表示・検索し、運営しているチームとダイレクトにメッセージをやり取りし、テストを世界中のメンバーと共同して行うような)<br />
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1869061/ffa5c1bf-3579-4550-5ba6-0f913b99be1d.png" alt="image.png" /><br />
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1869061/875d80d2-7653-19ce-0beb-9829b3f94ab6.png" alt="image.png" /></p>
<h1 id="サービス改善告知"><a href="#%E3%82%B5%E3%83%BC%E3%83%93%E3%82%B9%E6%94%B9%E5%96%84%E5%91%8A%E7%9F%A5">サービス改善告知</a></h1>
<p style="color:red;">
2021/10/17
サービスに、操作ガイドページ・Demoページを追加実装しました!
</p>
<p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1869061/bacb5f22-edac-0a85-d19d-26cf94088f9a.png" alt="image.png" /><br />
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1869061/a4f28d96-928e-6da4-52be-6153e3a6488f.png" alt="image.png" /></p>
<p>操作ガイドページでは、サービス内の全てのボタンや、項目の意味、操作方法やユースケースを記述しております。<br />
Demoページでは、実際の画面にモックデーターを流し込んだものを使い、操作感や使用イメージを持っていただくことを目的としています。<br />
どちらも無料でご利用いただける機能ですので、よろしければお楽しみください!</p>
<p style="color:red;">
2021/10/20
トップページのレイアウトを変更しました!
</p>
<p>旧:<br />
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1869061/d16ed7d0-f360-5ebf-1ae2-76809f111587.png" alt="image.png" /><br />
新:<br />
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1869061/5752c7cd-dd2e-784d-1ac9-fec76e29f3a8.png" alt="image.png" /></p>
<p><strong>変更にあたって、具体的に意識した点</strong></p>
<ul>
<li>設計思想をメインコンセプトとして表示</li>
<li>注目線を意識し、印象付けたい文言を左側に配置する</li>
</ul>
<p><strong>アニメーション</strong>が少しだけ配置されているので、実際に見ていただければ、と存じます!</p>
<h1 id="サービスのこれから"><a href="#%E3%82%B5%E3%83%BC%E3%83%93%E3%82%B9%E3%81%AE%E3%81%93%E3%82%8C%E3%81%8B%E3%82%89">サービスのこれから</a></h1>
<h2 id="トップページのレイアウト改善"><a href="#%E3%83%88%E3%83%83%E3%83%97%E3%83%9A%E3%83%BC%E3%82%B8%E3%81%AE%E3%83%AC%E3%82%A4%E3%82%A2%E3%82%A6%E3%83%88%E6%94%B9%E5%96%84">トップページのレイアウト改善</a></h2>
<p>とりあえず、第一課題です。<br />
現状注目線があっちこっちに行ってしまっており、とてもわかりにくいページになっています。<br />
デザインを勉強しつつ、わかりやすく、「使ってみたい」と思っていただけるようなトップページにしたいと思っております。</p>
<h2 id="チーム公開機能の実装"><a href="#%E3%83%81%E3%83%BC%E3%83%A0%E5%85%AC%E9%96%8B%E6%A9%9F%E8%83%BD%E3%81%AE%E5%AE%9F%E8%A3%85">チーム公開機能の実装</a></h2>
<p>public設定のチームをいくつか表示し、興味の出たOSSのテスト等に世界中から参画希望を出せるようにします。<br />
本来の設計思想である、「何故OSSをGitで管理するように、テストを世界中のエンジニアと管理実行するプラットフォームは無いのか」という疑問を解決する機能なので、じっくり作り込みたいと思っています。<br />
現状は、<br />
・Topics<br />
・Search<br />
・Explore<br />
の三画面を開発して、世界中のチームを見られるようにしたいと思っています。</p>
ふぁる@個人開発
tag:crieit.net,2005:PublicArticle/17698
2021-10-05T17:05:54+09:00
2021-10-05T17:05:54+09:00
https://crieit.net/posts/Firebase-SSSAPI-Web
FirebaseとSSSAPIでプレスリリースを送ったサイトを記録できるWebサービスを作ってみた
<p>開発しているGoogleスプレッドシートのAPI化サービス「<a target="_blank" rel="nofollow noopener" href="https://sssapi.app">SSSAPI</a>」をβ版リリースしたときに、<br />
プレスリリースをいろんなメディアに送ってみてた。</p>
<p>せっかく調べてまとめたし、次も使う機会があるだろうと思ったので、<br />
Nuxt/Firebase/SSSAPIの構成でWebサービス化してみた。</p>
<p>プレスリリースを受け付けてるサイトを調べるのは大変なので、<br />
どこに送れるのかな〜と思った方は使ってみてもらえるとうれしいです(<em>´ω`</em>)</p>
<h3 id="作ったサービス"><a href="#%E4%BD%9C%E3%81%A3%E3%81%9F%E3%82%B5%E3%83%BC%E3%83%93%E3%82%B9">作ったサービス</a></h3>
<p><a target="_blank" rel="nofollow noopener" href="https://press-done.memory-lovers.com">https://press-done.memory-lovers.com</a></p>
<p>送付するプレスリリースごとに、どのサイトへ送ったかの状態管理ができます。<br />
それだけのシンプルなWebサービス。</p>
<p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/b52d95a8-788f-5625-c536-4ecd6cc147b5.png" alt="スクリーンショット 2021-10-04 23.35.26.png" /></p>
<h3 id="システム構成"><a href="#%E3%82%B7%E3%82%B9%E3%83%86%E3%83%A0%E6%A7%8B%E6%88%90">システム構成</a></h3>
<p>基本構成はこんな感じ。</p>
<ul>
<li>フロントエンド: Nuxt+Buefy</li>
<li>認証: Firebase Authentication</li>
<li>ユーザデータ: Firestore</li>
<li>マスターデータ: <a target="_blank" rel="nofollow noopener" href="https://sssapi.app">SSSAPI</a></li>
</ul>
<p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/d9f069c4-7d32-5ea8-aea5-91300fea7030.png" alt="スクリーンショット 2021-10-04 23.40.41.png" /></p>
<p>ユーザが更新しないマスターデータはSSSAPIを利用している感じ。</p>
<p>メディアサイトのデータをFirestoreに入れておく形もあるけど、<br />
Firebaseコンソールで追加や編集するより、<br />
スプレッドシートを更新するほうが楽なので、この形にしている。</p>
<p>スプレッドシートの中身はこんな感じ。</p>
<p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/e130e233-915d-e105-0e75-48249f002a13.png" alt="スクリーンショット 2021-10-04 23.43.50.png" /></p>
<p>SSSAPIはこんなJSONを返してくれる。</p>
<pre><code class="json">[
{
"id": 1,
"サイト名": "TechCrunch Japan",
"サイトURL": "https://jp.techcrunch.com",
"送付方法": "メール",
"isActive": true
},
{
"id": 2,
"サイト名": "日経クロステック",
"サイトURL": "https://xtech.nikkei.com",
"送付方法": "メール",
"isActive": true
},
{
"id": 3,
"サイト名": "ITmedia",
"サイトURL": "https://www.itmedia.co.jp/",
"送付方法": "メール",
"isActive": true
},
]
</code></pre>
<p>※「行数をIDとして追加」オプションを使ってるので、idが自動で付与されています。</p>
<p>このスプレッドシートは登録依頼のGoogleフォームと連動しているので、<br />
追加依頼があったら内容を確認して<code>isActive</code>を<code>true</code>にすれば反映されます。</p>
<p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/39ce3a05-cd7b-2ed8-81e1-af76025e6175.png" alt="スクリーンショット 2021-10-04 23.56.34.png" /></p>
<p>こういった構成は割と多く、本の貸し出し管理と備品管理とかでも使える。<br />
Webサービスからスプレッドシートを編集できないので、<br />
管理者権限とか不要なので、その分フロント側も楽に作れた(<em>´ω`</em>)</p>
<p>プレスリリースってどこに送れるのかな〜と思った方は、<br />
ぜひ使ってみてもらえるとうれしいです(<em>´ω`</em>)</p>
<p><a target="_blank" rel="nofollow noopener" href="https://press-done.memory-lovers.com">https://press-done.memory-lovers.com</a></p>
<h3 id="SSSAPIはβ版ユーザを募集しています!!"><a href="#SSSAPI%E3%81%AF%CE%B2%E7%89%88%E3%83%A6%E3%83%BC%E3%82%B6%E3%82%92%E5%8B%9F%E9%9B%86%E3%81%97%E3%81%A6%E3%81%84%E3%81%BE%E3%81%99%21%21">SSSAPIはβ版ユーザを募集しています!!</a></h3>
<p>β期間中はプレミアムプランの内容を無料で使えるのでお得!<br />
ぜひぜひ、この機会にお試しいただければ!</p>
<p>わからないところやこんなのあったらいいなぁ〜などあれば、<br />
Twitterやお問い合わせなどからお気軽にご連絡ください〜</p>
<p>GoogleスプレッドシートのAPI化サービス<br />
『<a target="_blank" rel="nofollow noopener" href="https://sssapi.app">SSSAPI</a>』<br />
Twitter: <a target="_blank" rel="nofollow noopener" href="https://twitter.com/sssapi_app">@sssapi_app</a></p>
<p><a target="_blank" rel="nofollow noopener" href="https://sssapi.app">https://sssapi.app</a></p>
<p>開発の励みや記事を書くモチベにもなるので、<br />
いいねやシェア、はてブなどしてもらえるとかなりよろこびます!!</p>
きらぷか@積読ハウマッチ/SSSAPIなど
tag:crieit.net,2005:PublicArticle/16651
2021-01-26T20:25:54+09:00
2021-01-26T20:25:54+09:00
https://crieit.net/posts/Bulma-Nuxt-Content-Prism-js
BulmaとNuxt/Content(というかPrism.js)の相性が結構辛い
<h2 id="環境"><a href="#%E7%92%B0%E5%A2%83">環境</a></h2>
<ul>
<li>nuxt: 2.14.6</li>
<li>nuxt/content: 1.9.0</li>
<li>prism-themes: 1.5.0</li>
<li>nuxt-buefy: 0.4.3</li>
</ul>
<h2 id="デザイン大崩れ"><a href="#%E3%83%87%E3%82%B6%E3%82%A4%E3%83%B3%E5%A4%A7%E5%B4%A9%E3%82%8C">デザイン大崩れ</a></h2>
<p>Nuxt/Contentを使っていて、かなり苦戦した。特にNuxt/Contentが内包している、Prism.jsでコードのハイライトを出そうとしたときが辛かった。<br />
<a href="https://crieit.now.sh/upload_images/8ab28d65442b113bed0f81d1e532434a5ff697044862b.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/8ab28d65442b113bed0f81d1e532434a5ff697044862b.png?mw=700" alt="image" /></a><br />
これ、酷くない?</p>
<p>最初、何か設定をミスったのかと思って、prismの導入あたりのドキュメントを見ても問題ないし、じゃあ、何かのバグかっていうと、突拍子もなさすぎて訳わからん。<br />
しばらく悩んで気がついた。<br />
Bulmaのスタイルが当たってるんだ。<br />
どうやらnumberというクラスが競合しているようだ。<br />
(正直、このnumberクラス、ドキュメントにもないし、初めて見た。そして、何というか、今後使うことなさそう……)<br />
他にもtagというクラスが競合するらしい(未確認)。</p>
<p>cssに関しては大の苦手なので、具体的な施策を提示する自信がないのですが、<br />
とりあえず、Bulmaのスタイルより優先度上のセレクタで上書きする感じで無理矢理やりました。</p>
<h2 id="結構いました。"><a href="#%E7%B5%90%E6%A7%8B%E3%81%84%E3%81%BE%E3%81%97%E3%81%9F%E3%80%82">結構いました。</a></h2>
<p>調べてみたら同じ理由で困った人がちらほら。<br />
<a target="_blank" rel="nofollow noopener" href="https://papadays.com/post/5inybk0imz6r2ycmeoquwn/">Bulmaをやめてしまった人までいる。</a><br />
<a target="_blank" rel="nofollow noopener" href="https://stackoverrun.com/ja/q/9978069">みんな困ってるんだね。</a></p>
<h2 id="他にも"><a href="#%E4%BB%96%E3%81%AB%E3%82%82">他にも</a></h2>
<p>Nuxt/Content側の話で、見出しがやけに右に寄るなあ、てのもあった(画像2行目)<br />
<a href="https://crieit.now.sh/upload_images/7878ba53b27b21fcc3ce1b5c3b49da2e5ff6971d71552.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/7878ba53b27b21fcc3ce1b5c3b49da2e5ff6971d71552.png?mw=700" alt="image" /></a><br />
見出しのリンク(h2 > a > span)にiconというクラスが割り当てられていて、そこにBulmaのクラスが当たって幅を持ってしまっていた。<br />
まあ、そもそもNuxt/Contentは「<a target="_blank" rel="nofollow noopener" href="https://content.nuxtjs.org/ja/displaying#style">こういう感じで生成するから自分でデザインしてね</a>」なので良いのだが(自分ではなくBulmaが勝手にやってしまった、というのはあるけど)</p>
<h2 id="感想"><a href="#%E6%84%9F%E6%83%B3">感想</a></h2>
<p>Nuxt/Contentもprismも自分の預かり知らないところでHTMLを構成するので、意図せずCSSフレームワークと競合するのはしんどいなあという感想です。<br />
Nuxt/Contentは<a href="#他にも">上記の通り</a>なんだけど、prismは元々のデザインがあるから辛いなあと。<br />
あと、これでBulmaを辞めるという決断になると、同時にbuefyを辞めるという事になるので、結構辛いなあ。</p>
hammhiko
tag:crieit.net,2005:PublicArticle/16543
2021-01-06T08:13:26+09:00
2021-01-06T08:13:26+09:00
https://crieit.net/posts/Nuxt-Content-5ff4f2969eba2
Nuxt/Contentで日付による範囲検索をしたい
<p>ハマりにハマった。</p>
<h2 id="Nuxt/Content"><a href="#Nuxt%2FContent">Nuxt/Content</a></h2>
<p>最近、Nuxt/Contentというものをお試しで触っている。<br />
Markdownとかでコンテンツをゴリゴリ書いていける、GitベースのヘッドレスCMSとかいうものになるらしい。「GitベースのヘッドレスCMS」とか言われてもよくわからないが、DBではなくGitで管理するということだと思う。<br />
検索とか、なんなら全文検索もできるし結構便利そう。<br />
ただ、<a target="_blank" rel="nofollow noopener" href="https://content.nuxtjs.org/ja">公式ドキュメント</a>以外に日本語の記事が乏しくて、ちょっと苦戦する。<br />
ちょっと、というか、どハマりしてしまった。</p>
<h2 id="ハマった環境"><a href="#%E3%83%8F%E3%83%9E%E3%81%A3%E3%81%9F%E7%92%B0%E5%A2%83">ハマった環境</a></h2>
<ul>
<li>Nuxt: 2.14.6</li>
<li>nuxt/content: 1.9.0</li>
</ul>
<h2 id="日付でコンテンツを絞りたい"><a href="#%E6%97%A5%E4%BB%98%E3%81%A7%E3%82%B3%E3%83%B3%E3%83%86%E3%83%B3%E3%83%84%E3%82%92%E7%B5%9E%E3%82%8A%E3%81%9F%E3%81%84">日付でコンテンツを絞りたい</a></h2>
<p>作成日でコンテンツを絞りたかったので、<a target="_blank" rel="nofollow noopener" href="https://content.nuxtjs.org/ja/fetching#wherequery">ドキュメント</a>を参考にクエリを書いた。<br />
ただ、日付の扱い方がよくわからなかったので嫌な予感はしていた。</p>
<pre><code class="javascript">// 例)2020/11の記事を検索したい
const monthStart = new Date("2020-11-01")
const monthEnd = new Date("2020-11-30")
const articles = await $content('articles')
.sortBy('createdAt', 'desc')
.limit(5)
.where({
createdAt: { $and: [{ $gte: monthStart }, { $lte: monthEnd }] },
})
.fetch()
</code></pre>
<p>これで11月に作成した記事の最新5件が取れるはずだった。</p>
<h2 id="けど取れない"><a href="#%E3%81%91%E3%81%A9%E5%8F%96%E3%82%8C%E3%81%AA%E3%81%84">けど取れない</a></h2>
<p>変数articlesの中身は空でした。なんで?<br />
もしかしてDateじゃないのかなとか思って日付形式っぽい文字列にしてみてもダメ。<br />
公式ドキュメントを探し直したけど、特に記述はなく……。<br />
なんなら</p>
<blockquote>
<p>createdAt: DateTime</p>
</blockquote>
<p>とか<a target="_blank" rel="nofollow noopener" href="https://content.nuxtjs.org/ja/writing#%E3%83%95%E3%83%AD%E3%83%B3%E3%83%88%E3%83%9E%E3%82%BF%E3%83%BC">書いてある</a>し、どう考えたってDateでしょ。</p>
<h2 id="調べた"><a href="#%E8%AA%BF%E3%81%B9%E3%81%9F">調べた</a></h2>
<p>先述の通り、日本語の記事が乏しいうえ、公式ドキュメントに記載がないので苦戦したのだけど、調べて、見つけた。<br />
あったよ。<br />
ていうか、<a target="_blank" rel="nofollow noopener" href="https://github.com/nuxt/content/issues/437">GitHubのissue</a>だったよ!<br />
まあ、内容としてはissueそのまんまなんだけど、</p>
<pre><code class="js">// 例)2020/11の記事を検索したい
const monthStart = new Date("2020-11-01").valueOf()
const monthEnd = new Date("2020-11-30").valueOf()
const articles = await $content('articles')
.sortBy('createdAt', 'desc')
.limit(5)
.where({
createdAt: { $and: [{ $gte: monthStart }, { $lte: monthEnd }] },
})
.fetch()
</code></pre>
<p>valueOf()なんて初めて使ったよ。てか、初めて知ったよ。<br />
と思って<a target="_blank" rel="nofollow noopener" href="https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Date/valueOf">調べてみたら</a>、getTime()の方が正しい気がしてきたぞ?</p>
<blockquote>
<p>このメソッドは、Date.prototype.getTime() メソッドと機能的に同等です。<br />
このメソッドは、JavaScript によって内部的に呼ばれ、コード内で明示的に呼ばれることはありません。</p>
</blockquote>
<pre><code class="js">// 例)2020/11の記事を検索したい
const monthStart = new Date("2020-11-01").getTime()
const monthEnd = new Date("2020-11-30").getTime()
const articles = await $content('articles')
.sortBy('createdAt', 'desc')
.limit(5)
.where({
createdAt: { $and: [{ $gte: monthStart }, { $lte: monthEnd }] },
})
.fetch()
</code></pre>
<p>うん、これでも動いた。</p>
<p>あー、UNIXタイムスタンプだったかー。ドキュメントに書いてて欲しいなー。</p>
<h2 id="おわりに"><a href="#%E3%81%8A%E3%82%8F%E3%82%8A%E3%81%AB">おわりに</a></h2>
<p>issuesにはあったけど、日本語の記事があっても良いと思ったので書きました。<br />
探し方が悪いだけかもしれませんが。</p>
hammhiko
tag:crieit.net,2005:PublicArticle/16070
2020-09-24T10:25:23+09:00
2020-09-24T10:25:23+09:00
https://crieit.net/posts/unlock-bank
ハッカー専用パズルゲームを作ったので全てネタバレする
<blockquote>
<p>当記事は <a target="_blank" rel="nofollow noopener" href="https://zenn.dev/kinmi/articles/b6646b4902dbda585c0b">ハッカー専用パズルゲームを作ったので全てネタバレする</a> のクロス投稿です</p>
</blockquote>
<h1 id="💰作ったもの💰"><a href="#%F0%9F%92%B0%E4%BD%9C%E3%81%A3%E3%81%9F%E3%82%82%E3%81%AE%F0%9F%92%B0">💰作ったもの💰</a></h1>
<p><a href="https://crieit.now.sh/upload_images/aa565ccbf6a415ffff3d41f0f20fe93f5f6bf02787aff.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/aa565ccbf6a415ffff3d41f0f20fe93f5f6bf02787aff.png?mw=700" alt="UNLOCK BANK" /></a><br />
<a target="_blank" rel="nofollow noopener" href="https://unlock-bank.vercel.app/">UNLOCK BANK</a></p>
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">@銀行口座をHackしたくてウズウズしている皆さん「違法行為はしたくないけどハッキングはしたい…」そんな貴方の悩みを解決するゲームをご用意しました。このゲームでは何をやっても合法です。是非、不正ログインしてみてください💰<a target="_blank" rel="nofollow noopener" href="https://t.co/LyIpYLCRrP">https://t.co/LyIpYLCRrP</a> <a target="_blank" rel="nofollow noopener" href="https://twitter.com/hashtag/UNLOCK_BANK?src=hash&ref_src=twsrc%5Etfw">#UNLOCK_BANK</a></p>— きんみ | ツイッター大喜利サイト🎍ついぎり🎍作りました🙄 (@_kinmi) <a target="_blank" rel="nofollow noopener" href="https://twitter.com/_kinmi/status/1307988548591611904?ref_src=twsrc%5Etfw">September 21, 2020</a></blockquote>
<h2 id="仕様"><a href="#%E4%BB%95%E6%A7%98">仕様</a></h2>
<ul>
<li>Account Number(数字8桁) と Password(数字4桁) によるログイン認証</li>
<li>Account Number は全て存在する(8桁なので全口座数は1億口)</li>
<li>Password を3回間違えるとロック(再試行不可)される</li>
<li><em>[裏設定]</em> 銀行口座を模している為、もちろん<strong>パスワードが流出したら他者もログイン可能</strong></li>
</ul>
<p>ユーザーはどんな攻撃手法を用いても良い。<br />
最近話題の1段階認証を突破してみようというパズルゲーム。</p>
<h2 id="システム構成"><a href="#%E3%82%B7%E3%82%B9%E3%83%86%E3%83%A0%E6%A7%8B%E6%88%90">システム構成</a></h2>
<ul>
<li>JSフレームワーク: Nuxt.js 2.14.5(Vue.js 2.x)</li>
<li>トランスパイル: TypeScript 4.0</li>
<li>CSSフレームワーク: TailwindCSS</li>
<li>ホスティング: Vercel</li>
</ul>
<p>お馴染みの構成を選択したが、Vue.jsの使用については少し後悔している(後述)</p>
<h1 id="以下ネタバレ"><a href="#%E4%BB%A5%E4%B8%8B%E3%83%8D%E3%82%BF%E3%83%90%E3%83%AC">以下ネタバレ</a></h1>
<h2 id="想定する解法"><a href="#%E6%83%B3%E5%AE%9A%E3%81%99%E3%82%8B%E8%A7%A3%E6%B3%95">想定する解法</a></h2>
<p>基本的には以下の2つと考えている。<br />
- <strong>☠️リバースブルートフォース攻撃☠️</strong><br />
件の金融事件で脚光を浴びた攻撃手法。<br />
暗証番号の総当たり(ブルートフォース攻撃)は一般的なシステムだと試行回数に制限を設けているが、ユーザーIDの様な公開情報については秘匿性が低いため無制限に試行できるシステムが多い。そこを突いてパスワードは固定のままIDの方を総当たりするという攻撃。<br />
特に当ゲームのような、認証情報に数値しか許容していないシステムだと少ない試行回数で突破できるため致命的な脆弱性となる。</p>
<ul>
<li><strong>😈リバースエンジニアリング😈</strong><br />
JavaScriptのみで構成している為、コードを解析してパスワードの取得が可能。<br />
当初はNode.jsサーバー等を用意して認証処理を隠蔽するつもりだったがガチ攻撃された場合、サーバーか私の財布が死ぬと思い断念。<br />
しかし「ハッカー専用」と銘打っている以上、コードを覗くだけで突破されては面白くないので悪足掻きとして難読化を施している。</li>
</ul>
<h2 id="仕組みについて"><a href="#%E4%BB%95%E7%B5%84%E3%81%BF%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6">仕組みについて</a></h2>
<h3 id="🔐パスワード生成"><a href="#%F0%9F%94%90%E3%83%91%E3%82%B9%E3%83%AF%E3%83%BC%E3%83%89%E7%94%9F%E6%88%90">🔐パスワード生成</a></h3>
<p>仕様として、口座番号とパスワードの紐付きを作らないといけない(パスワードが流出したら誰でもログインできるように)<br />
最初は愚直に1億口座分のパスワードマッピングを定数で用意しようと考えた。<br />
しかし上述のコードを隠蔽できない事情によりリテラル値で持つのは避けたかった為、下記のライブラリを採用した。<br />
<a target="_blank" rel="nofollow noopener" href="https://github.com/davidbau/seedrandom">seedrandom.js</a><br />
これを用いてシード値から再現性のある乱数を生成(※1)する。</p>
<p>下記のように口座番号をシード値とした乱数を生成した後、整形してパスワードとしている。</p>
<pre><code class="ts">const seedrandom = require('seedrandom')
function authenticated(account: string, password: string): boolean {
const rng = seedrandom(account)
const _pass = String(Math.round(rng() * 10000)).padStart(4, '0')
return _pass === password
}
</code></pre>
<p>これで口座番号とパスワードの紐付きを再現している。</p>
<h4 id="seedrandom.jsの使用感"><a href="#seedrandom.js%E3%81%AE%E4%BD%BF%E7%94%A8%E6%84%9F">seedrandom.jsの使用感</a></h4>
<ul>
<li>🙆♂️ <strong>良かった所</strong>
<ul>
<li>直感的に使える</li>
<li>複数の乱数アルゴリズムに対応している</li>
<li><code>Math.random()</code>をラップできる為、テストにも使えそう</li>
</ul></li>
<li>🙅♂️ <strong>頑張って〜</strong>
<ul>
<li>型定義がない(2020/09現在、<a target="_blank" rel="nofollow noopener" href="https://github.com/davidbau/seedrandom/pull/70">issue</a>にて対応中)</li>
</ul></li>
</ul>
<p>個人的には「再現性のある乱数を生成する処理」というのは<a target="_blank" rel="nofollow noopener" href="https://qiita.com/kinmi/items/ddd213bf7a09f67f68ee">バーコードバトラーライクのゲーム</a>を作る際に重宝するので、有難いライブラリ。</p>
<h3 id="状態管理, 認証処理"><a href="#%E7%8A%B6%E6%85%8B%E7%AE%A1%E7%90%86%2C+%E8%AA%8D%E8%A8%BC%E5%87%A6%E7%90%86">状態管理, 認証処理</a></h3>
<p>本筋と外れるが、クライアントで認証処理を行う為に口座番号と暗証番号をグローバル管理する必要があった。<br />
Vuexはオーバースペックなので状態を<a target="_blank" rel="nofollow noopener" href="https://ja.nuxtjs.org/guide/plugins/#%E7%B5%B1%E5%90%88%E3%81%95%E3%82%8C%E3%81%9F%E6%B3%A8%E5%85%A5">Inject</a>するプラグインを自作してstoreとして使用した。</p>
<blockquote>
<p>~/plugins/auth.ts</p>
</blockquote>
<pre><code class="ts">import Vue from 'vue'
import { Plugin } from '@nuxt/types'
import { Seedrandom } from '../types/seedrandom'
const seedrandom = require('seedrandom') as Seedrandom
type InjectTypeAuth = {
/**
* 口座番号
*/
accountNumber: string
/**
* 暗証番号
*/
password: string
/**
* 認証処理
* seedから4桁の乱数(先頭0埋め)を生成し、passwordとの比較結果を返却する
* @param {string} seed シード値となる値
* @param {string} password パスワード
*/
authenticated(seed: string, password: string): boolean
}
declare module '@nuxt/types' {
interface Context {
$auth: InjectTypeAuth
}
interface NuxtAppOptions {
$auth: InjectTypeAuth
}
}
declare module 'vue/types/vue' {
interface Vue {
$auth: InjectTypeAuth
}
}
type State = {
accountNumber: string
password: string
}
/**********************************************
* 認証情報プラグイン
* @param {Context} ctx
* @param {(key: string, value: any) => void} inject
*/
const AuthPlugin: Plugin = (_ctx, inject) => {
/**
* Observable properties
*/
const state = Vue.observable({
accountNumber: '',
password: '',
} as State)
function authenticated(seed: string, password: string): boolean {
const rng = seedrandom(seed)
const _pass = String(Math.round(rng() * 10000)).padStart(4, '0')
if (_pass === password) state.password = _pass
return _pass === password
}
/**
* Injection
*/
inject('auth', {
get accountNumber() {
return state.accountNumber
},
set accountNumber(accountNumber: string) {
state.accountNumber = accountNumber
},
get password() {
return state.password
},
authenticated,
})
}
export default AuthPlugin
</code></pre>
<p>※ 実際の乱数生成処理はもう少しノイズを入れてるので、このまま動かしても同じパスワードは生成されません</p>
<p>この<code>authenticated</code>をEnterボタン押下時と、直アクセスを防ぐ目的でログイン後ページ内の<code>validate()</code>hookでも呼び出して認証処理としている。</p>
<p>⚠️注意<br />
<em>当然ですが、フロントエンドでパスワードの一致チェック等の認証処理を行ってはいけません。</em></p>
<p>Vue3のRCが外れて正式リリースとなったが、<a target="_blank" rel="nofollow noopener" href="https://v3.vuejs.org/api/basic-reactivity.html">Reactivity API</a>等を活用したVuex(ver 5)のリリースはまだ先の様なので暫く状態管理はこの手法に落ち着きそう。</p>
<h2 id="攻撃方法の紹介"><a href="#%E6%94%BB%E6%92%83%E6%96%B9%E6%B3%95%E3%81%AE%E7%B4%B9%E4%BB%8B">攻撃方法の紹介</a></h2>
<p>以上を踏まえ、解法の一つであるリバースブルートフォース攻撃の実施方法を紹介していく。<br />
尚、当ゲームでは外部からブラウザ操作しやすいように主要な要素に対してid属性を付与している。<br />
<a href="https://crieit.now.sh/upload_images/8e2ef469b048d0e22a0b6e053a8a9f7d5f6bf3fec95cd.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/8e2ef469b048d0e22a0b6e053a8a9f7d5f6bf3fec95cd.png?mw=700" alt="振っているID一覧" /></a></p>
<h3 id="前提知識: JavaScript の場合"><a href="#%E5%89%8D%E6%8F%90%E7%9F%A5%E8%AD%98%3A+JavaScript+%E3%81%AE%E5%A0%B4%E5%90%88">前提知識: JavaScript の場合</a></h3>
<p>「<strong>システム構成</strong>」の節でも触れたが、Vue.jsの採用を後悔したのはここ。</p>
<p>システムがHTML+PureJSの構成であれば、ブラウザの開発者コンソールから下記の様に実行して入力値を動的に与える事が可能。</p>
<pre><code class="js">const digit1 = document.getElementById('digit1')
digit1.value = "1"
</code></pre>
<p>これは一般的な方法だし、私も当初このやり方を想定していた。<br />
しかしVue.jsの場合、inputイベントを検知してVueインスタンス内に保有するデータを更新する。<br />
直接input要素のvalue値を書き換えてもイベントは発火せず、データは更新されない。<br />
この挙動を想定しておらず、ユーザーに無駄なハードルを与えることとなってしまった。</p>
<p>ではどうするのか。<br />
当ゲームには数値をカウントアップ/カウントダウンするボタンを実装している。<br />
<a href="https://crieit.now.sh/upload_images/ffc2f4e46d2b2c361e83cb2dd2287bde5f6bf42460c65.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/ffc2f4e46d2b2c361e83cb2dd2287bde5f6bf42460c65.png?mw=700" alt="これ" /></a></p>
<p><strong>金融システムって誰得UI多いよな</strong> という遊び心で実装したボタンだが、Vue内のデータを書き換えるイベントに直結している。<br />
このボタンをプログラムからクリックすることが出来れば口座番号入力の自動化が可能となる。</p>
<p>ここから先の解説は、既にUNLOCK成功された<a target="_blank" rel="nofollow noopener" href="https://twitter.com/yoneapp">@yoneapp</a>さんが記事を書かれている為、そちらに任せることとする。<br />
<a target="_blank" rel="nofollow noopener" href="https://zenn.dev/yoneapp/articles/ec2892c7e2e5c499684d">リバースブルートフォース攻撃を使ってUNLOCK BANKの口座に不正ログインして優勝する</a>(Zenn)<br />
(改めて解説記事の執筆ありがとうございます🙇♂️)</p>
<h3 id="前提知識: Vue.js の場合"><a href="#%E5%89%8D%E6%8F%90%E7%9F%A5%E8%AD%98%3A+Vue.js+%E3%81%AE%E5%A0%B4%E5%90%88">前提知識: Vue.js の場合</a></h3>
<p>Vue.jsには <a target="_blank" rel="nofollow noopener" href="https://github.com/vuejs/vue-devtools">vue-devtools</a> というデバッグ用のブラウザ拡張が存在する。<br />
これを使えばコンポーネント構成を覗きつつ、各種データの書き換えやイベント発火が可能となるが通常はProduction環境で開けない。<br />
しかし、開発者ツールを使いこれをこじ開ける方法がある。</p>
<p><a target="_blank" rel="nofollow noopener" href="https://qiita.com/POPOPON/items/60010faae1eb4e4e67ac">本番公開されているサイトで Vue devtools を使う裏技</a>(Qiita)</p>
<p>厳密にはソースの難読化を施してるのため上記記事と全く同じではないが、ソースを検索すれば<code>devtool</code>というキーワードは見つかるはず。<br />
あとはその後ろにブレークポイントを張り、<code>_0x1bd719.devtools = true</code> を実行すれば開発者ツールを開けることが可能。</p>
<p><img src="https://storage.googleapis.com/zenn-user-upload/y04xwot067stonsyjc0dxwtwrbj6" alt="ProductionでDevtoolsを開いた図" /></p>
<p>図の状態(Devtoolsでコンポーネントを選択した状態)だと、開発者コンソール上で<code>$vm0</code>というオブジェクトが使用出来る。これは選択したコンポーネントのVueインスタンスであり、コンポーネント内に存在するデータやメソッドが全て内包されている。<br />
あとはそこからアタリを付けて、自動化プログラムを組めば良い。<br />
実際には下記をイジれば入力操作の自動化が可能となる。</p>
<pre><code class="js">$vm0.accountNumbers // 口座番号(1桁ずつ格納した配列)
$vm0.password // パスワード
$vm0.enter() // Enterボタン押下時の処理を実行
</code></pre>
<p>⚠️注意<br />
<em>初めてこれ(Productionでdevtoolsが使えること)を知った方はセキュリティ面に不安を覚えるかもしれませんが、それはお門違いです。</em><br />
<em>フロントエンドで保持するデータはユーザーから自由に改ざんされる前提であるべきです。</em></p>
<h3 id="前提知識: Nuxt.js の場合"><a href="#%E5%89%8D%E6%8F%90%E7%9F%A5%E8%AD%98%3A+Nuxt.js+%E3%81%AE%E5%A0%B4%E5%90%88">前提知識: Nuxt.js の場合</a></h3>
<p>Nuxt.jsで作られたアプリケーションは<code>window</code>直下に<code>$nuxt</code>というオブジェクトが作られる。<br />
そこには全ての情報が含まれており、上記のようにDevtoolsを開かずとも開発者コンソールからデータの書き換えやメソッドの実行が可能。<br />
しかし内包する情報量が膨大な為、開発者ではない第三者が操作したい対象を探すのは一苦労かもしれない。<br />
ちなみに当ゲームでは下記が自動化に必要な対象となる。</p>
<pre><code class="js">window.$nuxt.$children[1].$children[0].$children[0].accountNumbers
window.$nuxt.$children[1].$children[0].$children[0].password
window.$nuxt.$children[1].$children[0].$children[0].enter()
</code></pre>
<h3 id="その他の手法"><a href="#%E3%81%9D%E3%81%AE%E4%BB%96%E3%81%AE%E6%89%8B%E6%B3%95">その他の手法</a></h3>
<p><a target="_blank" rel="nofollow noopener" href="https://www.selenium.dev/documentation/ja/">Selenium</a> や <a target="_blank" rel="nofollow noopener" href="https://github.com/puppeteer/puppeteer">Puppeteer</a> 等を用いたブラウザ操作の自動化があげられる。<br />
今のところ、これらを使ってUNLOCKしたという報告は観測していない。ブラウザの開発者ツールが万能すぎる。</p>
<p>⚠️注意<br />
<em>悪意が無くともサーバーに高負荷をかける行為は法律により罰せられる可能性があります。</em><br />
<em>しかし、当ゲームで行う分には問題ありません。存分に攻撃してください。</em></p>
<h3 id="更にネタバレ"><a href="#%E6%9B%B4%E3%81%AB%E3%83%8D%E3%82%BF%E3%83%90%E3%83%AC">更にネタバレ</a></h3>
<p>もしproduction環境でログイン後のページを確認する必要が出た場合を考慮して、<br />
- <strong>Account Number</strong>: 1145-1419<br />
- <strong>Password</strong>: 1919</p>
<p>と入力したら開発コンソールに上記口座のパスワードが出力されるようになっている。<br />
攻撃するのは面倒くさいけどログインしてみたい、という方はどうぞ。</p>
<h2 id="確率について"><a href="#%E7%A2%BA%E7%8E%87%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6">確率について</a></h2>
<p>数値4桁のパスワードというのは 0000 ~ 9999 の1万パターンしかない。<br />
パスワードを固定にして、口座番号を総当たりした場合の確率は下記で求められる。</p>
<p><em>1 - ( 9999 / 10000 )^x</em><br />
x=試行回数</p>
<p>(間違ってたらご指摘ください)<br />
確率を表にするとこうなる。<br />
<div class="table-responsive"><table>
<thead>
<tr>
<th>試行回数</th>
<th>HITする確率</th>
</tr>
</thead>
<tbody>
<tr>
<td>3回</td>
<td>0.03%未満</td>
</tr>
<tr>
<td>10回</td>
<td>約0.1%</td>
</tr>
<tr>
<td>100回</td>
<td>約1%</td>
</tr>
<tr>
<td>1,000回</td>
<td>約10%</td>
</tr>
<tr>
<td>10,000回</td>
<td>約63%</td>
</tr>
<tr>
<td>100,000回</td>
<td>約99%</td>
</tr>
</tbody>
</table></div></p>
<p>つまり、口座番号8桁(1億パターン)全て試行せずとも、10万回程度で1口座はUNLOCKできてしまう。<br />
下5桁総当たりすればいい計算で、実際にTwitterでUNLOCKされた方の反応も「思ったより早かった」という声が多かった。<br />
当ゲームはパスワードを乱数によって生成しているが、これが本当の銀行口座の場合、パスワードの偏りが生じるはずなので更にUNLOCKは容易となるだろう。</p>
<h1 id="おわり"><a href="#%E3%81%8A%E3%82%8F%E3%82%8A">おわり</a></h1>
<p>ネタのつもりで作ったんですが思いのほか反響があって、解説記事を書いて頂いたり、難読化をデコードしてリバースエンジニアリングまでやって下さってる方もいて、個人開発冥利に尽きるなと思いました。<br />
他にも「こんなやり方あるよ」という方がいましたらご連絡ください。</p>
<h1 id="注釈"><a href="#%E6%B3%A8%E9%87%88">注釈</a></h1>
<p>※1: JavaScriptにはシード値から乱数を生成する機能が備わっていない</p>
きんみ
tag:crieit.net,2005:PublicArticle/15924
2020-06-07T19:47:07+09:00
2020-06-07T20:04:24+09:00
https://crieit.net/posts/d18d57017a47b6f0774f4e7cbcea2b94
週末の時間を使って「みんなのきょうしつ」をアップデートしました。
<p>promitsuです。</p>
<p>今週の土日の空いた時間を使って、先日リリースした「みんなのきょうしつ」というウェブサービスのアップデート作業を行いました。</p>
<p><a href="https://crieit.now.sh/upload_images/8ae825a52f93537d282260b5dff649f25edcc4d28a7d5.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/8ae825a52f93537d282260b5dff649f25edcc4d28a7d5.png?mw=700" alt="image" /></a></p>
<p><a target="_blank" rel="nofollow noopener" href="http://class-room.fun/"><strong>http://class-room.fun/</strong></a></p>
<p>このアプリは、リアルタイム同期な共同編集機能により、オンライン上の別々の場所やデバイスから、友達と一緒に好き勝手に教室(という名の座席表)を作成し、その画像をシェアできるサービスです。</p>
<p>開発の経緯などについては下記の記事を参照ください。</p>
<p><a href="https://crieit.net/posts/620b85ee075fd93545b20ccfb92d3491">https://crieit.net/posts/620b85ee075fd93545b20ccfb92d3491</a></p>
<h2 id="アップデート"><a href="#%E3%82%A2%E3%83%83%E3%83%97%E3%83%87%E3%83%BC%E3%83%88">アップデート</a></h2>
<p>先日のリリースは、複数人でリアルタイムに「教室」(座席表)を作成できるという目的のもとに、最低限の機能のみに絞った仕様になっていました。その後、友人などに使ってもらい、フィードバックをもらった内容について、以下のような機能追加や修正などを行いました。</p>
<p>※ 前提としてこのサービスはNuxtjs(Vuejs)とFirebaseを使って開発・運用されています。</p>
<h2 id="主な内容"><a href="#%E4%B8%BB%E3%81%AA%E5%86%85%E5%AE%B9">主な内容</a></h2>
<h3 id="ステータスのタイプライター表示"><a href="#%E3%82%B9%E3%83%86%E3%83%BC%E3%82%BF%E3%82%B9%E3%81%AE%E3%82%BF%E3%82%A4%E3%83%97%E3%83%A9%E3%82%A4%E3%82%BF%E3%83%BC%E8%A1%A8%E7%A4%BA">ステータスのタイプライター表示</a></h3>
<p>黒板に表示される生徒のつぶやきをタイプライター風に1文字ずつ表示するようにしました。<br />
普通に文字列を配列化して、一定間隔おきにv-modelの配列にpushする感じでJavascriptで実装しました。</p>
<h3 id="教室ページのデザイン・レイアウト修正"><a href="#%E6%95%99%E5%AE%A4%E3%83%9A%E3%83%BC%E3%82%B8%E3%81%AE%E3%83%87%E3%82%B6%E3%82%A4%E3%83%B3%E3%83%BB%E3%83%AC%E3%82%A4%E3%82%A2%E3%82%A6%E3%83%88%E4%BF%AE%E6%AD%A3">教室ページのデザイン・レイアウト修正</a></h3>
<p>VuejsもFirebase(firestore)もまともに使ったことがなかった3月ぐらいに一日間の突貫作業で作った時のままだったので、HTML/CSSの構造から、Vueコンポーネントの構成などもリファクタリングしました。</p>
<h3 id="いいね機能"><a href="#%E3%81%84%E3%81%84%E3%81%AD%E6%A9%9F%E8%83%BD">いいね機能</a></h3>
<p>現状ユーザー認証がないので、SessionStorageに自分がいいねした教室の情報を保持し、多重いいねを防ぐようにしました。<br />
実装にはvuex-persistedstateを使っています。<br />
これ以外のデータの読み込みなどはvuexfireを使って、vuexのstoreとfirestoreをシームレスにつないでいるので、moduleとして分けてindex.jsから読み込む構成にしています。</p>
<pre><code>```jsx
store/
|
+-- index.js
|
+-- like.js
```
</code></pre>
<pre><code class="jsx">store/index.js
import like from './like'
import localStorage from 'vuex-persistedstate'
...
export const plugins = [
localStorage({
key: 'like',
paths: ['like'],
storage: window.sessionStorage
})
]
</code></pre>
<p>vuex-persistedstate<br />
<a target="_blank" rel="nofollow noopener" href="https://github.com/robinvdvleuten/vuex-persistedstate">https://github.com/robinvdvleuten/vuex-persistedstate</a><br />
vuex-persistedstate<br />
<a target="_blank" rel="nofollow noopener" href="https://github.com/posva/vuexfire">https://github.com/posva/vuexfire</a></p>
<h3 id="公開教室のトップページでのリスト表示"><a href="#%E5%85%AC%E9%96%8B%E6%95%99%E5%AE%A4%E3%81%AE%E3%83%88%E3%83%83%E3%83%97%E3%83%9A%E3%83%BC%E3%82%B8%E3%81%A7%E3%81%AE%E3%83%AA%E3%82%B9%E3%83%88%E8%A1%A8%E7%A4%BA">公開教室のトップページでのリスト表示</a></h3>
<p>これがないと他にどんな教室があるかわからんし、アクセスもできないという指摘。ごもっともですね😓</p>
<h3 id="座席削除"><a href="#%E5%BA%A7%E5%B8%AD%E5%89%8A%E9%99%A4">座席削除</a></h3>
<p>作成・編集ができるのに、削除できないとは何事か!?というご指摘。さーせん。</p>
<h3 id="公開・非公開設定"><a href="#%E5%85%AC%E9%96%8B%E3%83%BB%E9%9D%9E%E5%85%AC%E9%96%8B%E8%A8%AD%E5%AE%9A">公開・非公開設定</a></h3>
<p>トップページにリストが表示される様になったので、他の人にアクセスしてほしくない人は非公開設定にできるようにしました。秘密の言葉による簡易認証を入れようかとも思ったのですが、ユーザーから見た複雑度が増す気がしたので一旦ペンディングにしました。</p>
<h3 id="教室背景変更機能"><a href="#%E6%95%99%E5%AE%A4%E8%83%8C%E6%99%AF%E5%A4%89%E6%9B%B4%E6%A9%9F%E8%83%BD">教室背景変更機能</a></h3>
<p>だだっ白い無味乾燥な背景が寂しかったので、教室の背景画像を変更できるようにしました。アップロード時の画像リサイズ・クロップなどはまだ実装できていないのでIssue積んどきました。</p>
<p><a href="https://crieit.now.sh/upload_images/9dbfdbf890225ceaff9757a8e6fe561f5edcc4accecfa.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/9dbfdbf890225ceaff9757a8e6fe561f5edcc4accecfa.png?mw=700" alt="image" /></a></p>
<h3 id="ドラッグ&ドロップ編集機能"><a href="#%E3%83%89%E3%83%A9%E3%83%83%E3%82%B0%26amp%3B%E3%83%89%E3%83%AD%E3%83%83%E3%83%97%E7%B7%A8%E9%9B%86%E6%A9%9F%E8%83%BD">ドラッグ&ドロップ編集機能</a></h3>
<p>vue-draggableを使って実装しました。(Vuejsは便利なコンポーネント・ライブラリが充実してるので、個人開発者にはとても嬉しい😭)<br />
もちろん編集結果はリアルタイムに他のデバイスに反映されるようになっています。(挙動をもうちょっとかっこよくしたい😓)</p>
<h4 id="動作ムービー"><a href="#%E5%8B%95%E4%BD%9C%E3%83%A0%E3%83%BC%E3%83%93%E3%83%BC">動作ムービー</a></h4>
<p><a target="_blank" rel="nofollow noopener" href="https://www.youtube.com/watch?v=2RCYmpFSae8">https://www.youtube.com/watch?v=2RCYmpFSae8</a></p>
<p>vue-draggable<br />
<a target="_blank" rel="nofollow noopener" href="https://github.com/SortableJS/Vue.Draggable">https://github.com/SortableJS/Vue.Draggable</a></p>
<h3 id="その他の修正(レイアウト変更、バグフィックス等)"><a href="#%E3%81%9D%E3%81%AE%E4%BB%96%E3%81%AE%E4%BF%AE%E6%AD%A3%EF%BC%88%E3%83%AC%E3%82%A4%E3%82%A2%E3%82%A6%E3%83%88%E5%A4%89%E6%9B%B4%E3%80%81%E3%83%90%E3%82%B0%E3%83%95%E3%82%A3%E3%83%83%E3%82%AF%E3%82%B9%E7%AD%89%EF%BC%89">その他の修正(レイアウト変更、バグフィックス等)</a></h3>
<p>背面黒板を廃止して、全面の黒板につぶやきを表示するようにしたり、その他微妙なデザイン・レイアウト調整、及びバグ修正を行いました。</p>
<hr />
<p>まだまだバグも多いし、実装したい機能や修正したいUX、インタラクションなどもたくさんありますが、地道に少しずつ良いプロダクトにしていこうと思います。</p>
<p>ではまた。</p>
ぷろみつ
tag:crieit.net,2005:PublicArticle/15923
2020-06-05T22:29:21+09:00
2020-06-05T22:31:45+09:00
https://crieit.net/posts/620b85ee075fd93545b20ccfb92d3491
自分の理想の教室を、友達とリアルタイムで一緒に作れるウェブサービス「みんなのきょうしつ」をリリースしました。
<p>さっき投稿した記事で、4日前に「musico」というウェブアプリをリリースしたことについて書きましたが、</p>
<p>好きな楽曲について語り合うウェブサービス「musico」を作ってみました。<br />
<a href="https://crieit.net/posts/musico">https://crieit.net/posts/musico</a><br />
<a target="_blank" rel="nofollow noopener" href="https://musi-co.fun">musico | find track you like and talk about it</a></p>
<p>実は昨日、もう一つウェブアプリをリリースしましたw<br />
その名も「みんなのきょうしつ」。</p>
<p><a href="https://crieit.now.sh/upload_images/8ae825a52f93537d282260b5dff649f25eda3eaa27764.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/8ae825a52f93537d282260b5dff649f25eda3eaa27764.png?mw=700" alt="image" /></a></p>
<p><a target="_blank" rel="nofollow noopener" href="https://class-room.fun">みんなのきょうしつ</a></p>
<h2 id="開発の動機"><a href="#%E9%96%8B%E7%99%BA%E3%81%AE%E5%8B%95%E6%A9%9F">開発の動機</a></h2>
<p>このアプリは、もともと3月のコロナ禍による一斉休校をきっかけに、いきなり会えなくなった友達とオンラインで再開して貰い、小中高生のみなさんに、社会の混乱からのストレスを一時的にでも忘れてもらえたら良いなという思いで、二日間ぐらいでガッと基本的な機能を作って公開しようとしていたものでした。ですが、その後なかなか着手する時間が取れず、限定的な公開にとどめて、ほぼ動いていない状態が続いていました。</p>
<h2 id="開発頓挫、からの突貫リリースへ"><a href="#%E9%96%8B%E7%99%BA%E9%A0%93%E6%8C%AB%E3%80%81%E3%81%8B%E3%82%89%E3%81%AE%E7%AA%81%E8%B2%AB%E3%83%AA%E3%83%AA%E3%83%BC%E3%82%B9%E3%81%B8">開発頓挫、からの突貫リリースへ</a></h2>
<p>そんななか、先日の「musico」の開発と公開をきっかけに、テンションとモチベーションがひさびさに爆上がりしたので、この機を逃す手はないと思い、一念発起して昨日・一昨日をかけて最低限の機能を実装し、公開していみました。</p>
<h2 id="どんなアプリ?"><a href="#%E3%81%A9%E3%82%93%E3%81%AA%E3%82%A2%E3%83%97%E3%83%AA%EF%BC%9F">どんなアプリ?</a></h2>
<p>このアプリでは、教室を模した座席表的なものに、自分の好きな人やモノの名前と写真を登録していき、自分の理想の教室が作れます。</p>
<p><a href="https://crieit.now.sh/upload_images/3cd6bc3848f85dbd1abedf7a1cfeb3cc5eda46c7d816b.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/3cd6bc3848f85dbd1abedf7a1cfeb3cc5eda46c7d816b.png?mw=700" alt="image" /></a></p>
<p>さらに、URLを共有することで、別の場所にいる友だちとオンラインでリアルタイムで共同編集することが出来ます。(動画は開発中に撮影したものです)</p>
<p><a target="_blank" rel="nofollow noopener" href="https://www.youtube.com/watch?v=N7qo5popZ3Q">動作ムービー</a></p>
<p>また、「記念撮影」機能で作った教室の画像を作成・ダウンロードしたり、各種SNSに手軽にシェアすることが出来ます。(写真は友人が作った「あつまれ どうぶつの森」のキャラクターが集まる教室です)</p>
<p><a href="https://crieit.now.sh/upload_images/43103ea48631f57b3237c5df0b282b575eda46f1109e2.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/43103ea48631f57b3237c5df0b282b575eda46f1109e2.png?mw=700" alt="image" /></a></p>
<p><a target="_blank" rel="nofollow noopener" href="https://class-room.fun/classrooms/8jVBr7suU0In1vbE3Ze8">https://class-room.fun/classrooms/8jVBr7suU0In1vbE3Ze8</a></p>
<p>地味にインスタ映えしたりしますw</p>
<p><a href="https://crieit.now.sh/upload_images/1780742f62e146dcfd91693883f4363d5eda47b04c5d4.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/1780742f62e146dcfd91693883f4363d5eda47b04c5d4.png?mw=700" alt="image" /></a></p>
<h3 id="技術的なこと"><a href="#%E6%8A%80%E8%A1%93%E7%9A%84%E3%81%AA%E3%81%93%E3%81%A8">技術的なこと</a></h3>
<p>技術スタック的には「musico」と同じくFirebase+Vuejs(Nuxtjs)という、構成です。「musico」はUniversalモード(SSR)で作りましたが、こっちはSPAとして開発し、OGP取得部分のみCloud Functionsで作りました。</p>
<p>しかし、Firebaseめっちゃ便利ですね〜。</p>
<p>こっちもあんまり凝ったことはしていないのですが、OGPの動的生成あたりとか、記念写真機能あたりについて、あとで技術的なことを書こうかなと思っています。</p>
<p>それではまた。</p>
ぷろみつ
tag:crieit.net,2005:PublicArticle/15917
2020-06-01T21:52:34+09:00
2020-06-01T21:53:26+09:00
https://crieit.net/posts/nuxt-content-5ed4fa12e42ad
@nuxt/contentで画像を最適化する
<p>先日導入した@nuxt/content で画像をどうやって最適化するか模索して、nuxt-optimized-images が良さそうだったので紹介します。</p>
<h2 id="今回使用した主な環境"><a href="#%E4%BB%8A%E5%9B%9E%E4%BD%BF%E7%94%A8%E3%81%97%E3%81%9F%E4%B8%BB%E3%81%AA%E7%92%B0%E5%A2%83">今回使用した主な環境</a></h2>
<ul>
<li>@aceforth/nuxt-optimized-images: 1.0.1</li>
<li>@nuxt/content: 1.2.0</li>
<li>nuxt: 2.12.2</li>
</ul>
<h2 id="nuxt-optimized-images のインストール"><a href="#nuxt-optimized-images+%E3%81%AE%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB">nuxt-optimized-images のインストール</a></h2>
<p>こちらは<a target="_blank" rel="nofollow noopener" href="https://aceforth.com/docs/nuxt-optimized-images/">公式ドキュメント</a>に従って、インストールしました。</p>
<p>今回は png 画像だけできれば良かったので、最適化に使うパッケージは 1 つだけにしています。</p>
<pre><code class="sh">yarn add -D @aceforth/nuxt-optimized-images imagemin-pngquant
</code></pre>
<p>もし他の種類も最適化したい場合は、追加でインストールします。</p>
<p>詳細は<a target="_blank" rel="nofollow noopener" href="https://aceforth.com/docs/nuxt-optimized-images/#optimization-packages">公式ドキュメント</a>で確認してください。</p>
<h3 id="nuxt の設定に追加する"><a href="#nuxt+%E3%81%AE%E8%A8%AD%E5%AE%9A%E3%81%AB%E8%BF%BD%E5%8A%A0%E3%81%99%E3%82%8B">nuxt の設定に追加する</a></h3>
<p>次に nuxt.config.ts にモジュールを追加します。</p>
<pre><code class="ts">{
buildModules: [
'@aceforth/nuxt-optimized-images',
],
optimizedImages: {
// モジュールのオプション
optimizeImages: true
}
}
</code></pre>
<h2 id="マークダウンで画像を表示する"><a href="#%E3%83%9E%E3%83%BC%E3%82%AF%E3%83%80%E3%82%A6%E3%83%B3%E3%81%A7%E7%94%BB%E5%83%8F%E3%82%92%E8%A1%A8%E7%A4%BA%E3%81%99%E3%82%8B">マークダウンで画像を表示する</a></h2>
<p> 一般的なマークダウン記法であれば<code>![画像](/images/sample.png)</code>のようにすれば表示することができます。</p>
<p>しかし、今回のパッケージで最適化するには別の方法で行います。</p>
<h3 id="components に画像読み込み用のコンポーネントを追加"><a href="#components+%E3%81%AB%E7%94%BB%E5%83%8F%E8%AA%AD%E3%81%BF%E8%BE%BC%E3%81%BF%E7%94%A8%E3%81%AE%E3%82%B3%E3%83%B3%E3%83%9D%E3%83%BC%E3%83%8D%E3%83%B3%E3%83%88%E3%82%92%E8%BF%BD%E5%8A%A0">components に画像読み込み用のコンポーネントを追加</a></h3>
<pre><code class="vue"><!-- ~/components/ImageLoader.vue -->
<template>
<img :src="require(`~/assets/images/${this.file}`)" alt="" />
</template>
<script lang="ts">
import { Vue, Component, Prop } from 'nuxt-property-decorator'
@Component
export default class ImageLoader extends Vue {
@Prop({ required: true })
public file!: string
}
</script>
</code></pre>
<p>props で使用する画像ファイル名を指定するようにしました。</p>
<p>このように、require('画像パス')とすることで画像の最適化が自動的に行われるようになります。</p>
<h3 id="マークダウンを呼び出すコンポーネントを修正"><a href="#%E3%83%9E%E3%83%BC%E3%82%AF%E3%83%80%E3%82%A6%E3%83%B3%E3%82%92%E5%91%BC%E3%81%B3%E5%87%BA%E3%81%99%E3%82%B3%E3%83%B3%E3%83%9D%E3%83%BC%E3%83%8D%E3%83%B3%E3%83%88%E3%82%92%E4%BF%AE%E6%AD%A3">マークダウンを呼び出すコンポーネントを修正</a></h3>
<pre><code class="vue"><!-- ~/pages/index.md -->
<script lang="ts">
import { Vue, Component } from 'nuxt-property-decorator'
import { ImageLoader } from '~/components/ImageLoader.vue'
@Component({ components: { ImageLoader } })
export default class Index extends Vue {}
</script>
</code></pre>
<p>マークダウンを処理するコンポーネントに先ほど作成した、ImageLoader.vue を追加しておきます。</p>
<h3 id="マークダウンファイルで使う"><a href="#%E3%83%9E%E3%83%BC%E3%82%AF%E3%83%80%E3%82%A6%E3%83%B3%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%81%A7%E4%BD%BF%E3%81%86">マークダウンファイルで使う</a></h3>
<pre><code class="md">---
title: '画像を表示する`
---
## ここに画像を表示します
<image-loader file="ファイル名" />
</code></pre>
<p>これで<code>nuxt generate</code>することでハッシュ化された、ファイルが出力されるようになります。</p>
<h2 id="画像のリサイズ"><a href="#%E7%94%BB%E5%83%8F%E3%81%AE%E3%83%AA%E3%82%B5%E3%82%A4%E3%82%BA">画像のリサイズ</a></h2>
<p>画像のリサイズはさらにパッケージの追加が必要です。</p>
<pre><code class="sh">yarn add -D responsive-loader sharp
</code></pre>
<p>sharp は jimp でもいいようですが、jimp は遅いみたいです。</p>
<h3 id="ImageLoader.vue を変更"><a href="#ImageLoader.vue+%E3%82%92%E5%A4%89%E6%9B%B4">ImageLoader.vue を変更</a></h3>
<p>リサイズのアクションは require するときに、サイズを決めなくてはいけません。</p>
<p>size を動的にしたくて props を追加してやってみたのですが、うまくいかなかったので決め打ちで設定することにしました。</p>
<p>以下のように ImageLoader.vue を変更します。</p>
<pre><code class="vue"><template>
<img :src="resizeImage.src" alt="" />
</template>
<script lang="ts">
import { Vue, Component, Prop } from 'nuxt-property-decorator'
@Component
export default class ImageLoader extends Vue {
@Prop({ required: true })
public file!: string
public resizeImage = require(`~/assets/images/${this.file}?resize&size=600`)
}
</script>
</code></pre>
<h3 id="マークダウンで画像のサイズを指定する"><a href="#%E3%83%9E%E3%83%BC%E3%82%AF%E3%83%80%E3%82%A6%E3%83%B3%E3%81%A7%E7%94%BB%E5%83%8F%E3%81%AE%E3%82%B5%E3%82%A4%E3%82%BA%E3%82%92%E6%8C%87%E5%AE%9A%E3%81%99%E3%82%8B">マークダウンで画像のサイズを指定する</a></h3>
<pre><code class="md">---
title: '画像を表示する`
---
## ここに画像を表示します
<image-loader file="ファイル名" />
</code></pre>
<p>これで任意のサイズ、今回は<code>width: 400px</code>にリサイズされた画像を出力できるようになりました。</p>
<h2 id="終わりに"><a href="#%E7%B5%82%E3%82%8F%E3%82%8A%E3%81%AB">終わりに</a></h2>
<p>マークダウンに vue コンポーネントが混じってしまうのが気持ち悪いですが、これで画像も自由なサイズで表示できました。</p>
<p>次回はレスポンシブの表示にも対応していきたいです。</p>
naoki0719
tag:crieit.net,2005:PublicArticle/15916
2020-05-31T20:37:48+09:00
2020-06-05T13:29:34+09:00
https://crieit.net/posts/nuxt-content
@nuxt/contentでブログを構築してみました
<p>これまで nuxt と microCMS を組み合わせて JAMStack なブログを Netlify で公開していましたが、コンテンツ自体も一緒に管理できたほうが効率的かつ外部サービスに依存したくないと考え、作り直しのため markdown-it を採用つもりでした。</p>
<p>しかし<a target="_blank" rel="nofollow noopener" href="https://content.nuxtjs.org/">@nuxt/content</a> が公開されたことを知り、早速導入をしてみました。</p>
<h2 id="@nuxt/content の機能について"><a href="#%40nuxt%2Fcontent+%E3%81%AE%E6%A9%9F%E8%83%BD%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6">@nuxt/content の機能について</a></h2>
<ul>
<li>ホットリロード</li>
<li>Markdown の Vue コンポーネント</li>
<li>全文検索</li>
<li><code>nuxt generate</code>で静的サイトの生成</li>
<li>MongoDB のような QueryBuilder</li>
<li>PrismJS によるマークダウンファイルのコードブロックをスタイリングする</li>
<li>目次の生成</li>
<li>Markdown、CSV、YAML、JSON を処理する</li>
<li>hooks による拡張</li>
<li>types による Typescript のサポート (v1.0.1)</li>
</ul>
<h2 id="インストール時の環境"><a href="#%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB%E6%99%82%E3%81%AE%E7%92%B0%E5%A2%83">インストール時の環境</a></h2>
<ul>
<li>MacOS Catalina 10.15.4</li>
<li>yarn 1.22.4</li>
<li>NuxtJS 2.12.2 (TypeScript)</li>
<li>@nuxt/content v1.2.0</li>
</ul>
<h2 id="インストール"><a href="#%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB">インストール</a></h2>
<p>まずはパッケージをインストールします。</p>
<p><code>yarn add @nuxt/content</code></p>
<p>次に nuxt.config.ts に以下を追加します。</p>
<pre><code># nuxt.config.ts
{
modules: [
'@nuxt/content',
],
content: {
// デフォルト以外の設定
}
}
</code></pre>
<h2 id="TypeScript の対応"><a href="#TypeScript+%E3%81%AE%E5%AF%BE%E5%BF%9C">TypeScript の対応</a></h2>
<p><code>tsconfig.json</code>に以下を追加します。</p>
<pre><code class="json">{
"compilerOptions": {
"types": ["@nuxt/types", "@nuxt/content"]
}
}
</code></pre>
<p>これを追加することで Context を拡張して、後に出てくる<code>$content</code>の参照が行われるようにします。</p>
<h2 id="コンテンツの管理"><a href="#%E3%82%B3%E3%83%B3%E3%83%86%E3%83%B3%E3%83%84%E3%81%AE%E7%AE%A1%E7%90%86">コンテンツの管理</a></h2>
<p>コンテンツの管理はデフォルトでは<code>~/content</code>以下で行います。</p>
<p>それぞれのファイルはサブディレクトリでも管理することができます。</p>
<p>今回は<code>~/content/articles/sample.md</code>として作成してみます。</p>
<pre><code class="md">---
title: '日本語タイトルを使えます'
date: 2020-05-23
tags: [Markdown, NuxtJS]
---
# 記事タイトル
コンテンツの内容
</code></pre>
<p><code>@nuxt/content</code>で slug は、ファイル名の拡張子を除く部分となっています。</p>
<p>YAML Front Matte の機能として記事のメタデータを管理することができ、<code>tags</code>のように配列で記述できます。</p>
<p>これらのメタデータはクエリを使って絞り込みに利用できます。</p>
<p>注意点として<code>title</code>を<code>title: 1</code>のように文字列以外を入力すると、文字列の関数がないことでエラーが発生します。この場合はシングルクォートで括るようにすれば回避できます。</p>
<h2 id="コンテンツの表示"><a href="#%E3%82%B3%E3%83%B3%E3%83%86%E3%83%B3%E3%83%84%E3%81%AE%E8%A1%A8%E7%A4%BA">コンテンツの表示</a></h2>
<p><code>asyncData</code>を使ってサーバーサイドで記事を取得します。</p>
<pre><code class="vue"><template>
<div>
<div><span>{</span><span>{</span> page.title <span>}</span><span>}</span></div>
<div><span>{</span><span>{</span> page.date <span>}</span><span>}</span></div>
<nuxt-content :document="page" />
</div>
</template>
<script lang="ts">
import { Vue, Component } from 'nuxt-property-decorator'
import { Context } from '@nuxt/types'
@Component
export default class Sample extends Vue {
async asyncData({ $content, params, error }: Context) {
const page = await $content('articles/sample').fetch()
return { page }
}
}
</script>
</code></pre>
<p><code>$content('articles/sample')</code>とすると、<code>content/articles/sample</code>を記事として取得します。</p>
<p>複数の記事をまとめて取得するには<code>$content('articles')</code>として、<code>content/articles</code>以下にある全ての記事を配列で取得することができます。</p>
<p>サーバーサイドで取得後、<code>page</code>変数には記事ファイルのメタデータとコンテンツが読み込まれ、<br />
<code><nuxt-content :document="page"></code>で HTML にマークアップされた状態で表示されます。</p>
<h2 id="コードブロックのテーマを変更する"><a href="#%E3%82%B3%E3%83%BC%E3%83%89%E3%83%96%E3%83%AD%E3%83%83%E3%82%AF%E3%81%AE%E3%83%86%E3%83%BC%E3%83%9E%E3%82%92%E5%A4%89%E6%9B%B4%E3%81%99%E3%82%8B">コードブロックのテーマを変更する</a></h2>
<p><code>@nuxt/content</code>でコードブロックのハイライトは<code>prismjs</code>で行われているので、<code>prism-themes</code>をインストールすれば好きなテーマへ変更することができます。</p>
<pre><code class="shell">yarn add prism-themes
</code></pre>
<p>nuxt.config.ts でテーマの変更を設定します。</p>
<pre><code class="ts"># nuxt.config.ts
{
modules: [
'@nuxt/content',
],
content: {
markdown: {
prism: {
// ここに使いたいcssのテーマを設定します
theme: 'prism-themes/themes/prism-vsc-dark-plus.css'
}
}
}
}
</code></pre>
<h2 id="タグの検索をする"><a href="#%E3%82%BF%E3%82%B0%E3%81%AE%E6%A4%9C%E7%B4%A2%E3%82%92%E3%81%99%E3%82%8B">タグの検索をする</a></h2>
<p>メタデータが配列の場合は、単純な where では取得できません。</p>
<p><code>@nuxt/content</code>のクエリは<a target="_blank" rel="nofollow noopener" href="https://github.com/techfort/LokiJS/wiki">LokiJS</a>を使っているので、公式マニュアルを参照して<code>$contains</code>を使えばいいことがわかりました。</p>
<p>具体的には以下の方法で、特定のタグを持つ記事を取得することができます。</p>
<pre><code class="ts">await $content('articles')
.where({ tags: { $contains: 'Markdown' } })
.fetch()
</code></pre>
<h2 id="サブディレクトリの記事も取得するには"><a href="#%E3%82%B5%E3%83%96%E3%83%87%E3%82%A3%E3%83%AC%E3%82%AF%E3%83%88%E3%83%AA%E3%81%AE%E8%A8%98%E4%BA%8B%E3%82%82%E5%8F%96%E5%BE%97%E3%81%99%E3%82%8B%E3%81%AB%E3%81%AF">サブディレクトリの記事も取得するには</a></h2>
<p>v1.3.0で実装されました。TypeScriptを使っている場合は<code>$content</code>の引数に問題があるので、v1.3.1を使いましょう。</p>
<p>これでフラットな記事管理から解放されます。</p>
<pre><code class="ts">const articles = $content('articles', { deep: true })
</code></pre>
<h2 id="終わりに"><a href="#%E7%B5%82%E3%82%8F%E3%82%8A%E3%81%AB">終わりに</a></h2>
<p>開発速度が速く、どんどん機能追加がされていて、シンプルで使いやすい印象です。</p>
<p>実際に導入までの作業も簡単で、公式ドキュメントを見ながら行うことができました。</p>
<p>目的としていた記事の一元管理を短時間で構築することができました。今後ますます注目されそうな気がします。</p>
naoki0719
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/15887
2020-05-06T15:52:28+09:00
2020-05-06T15:52:28+09:00
https://crieit.net/posts/Canvas-Konva-js-OGP-Nuxt
Canvasのライブラリ「Konva.js」でOGP生成(Nuxtアプリ)
<p>Nuxt.jsとFirebaseで作っていた<a target="_blank" rel="nofollow noopener" href="https://tsundoku.site">Webサービス</a>を7月末にリリースして、はや10ヶ月。。<br />
登録総額1億円を突破したので、記念企画として<a target="_blank" rel="nofollow noopener" href="https://tsundoku.site/review/">積読レビュー</a>機能をリリースしました!!</p>
<p>そこで<a target="_blank" rel="nofollow noopener" href="https://konvajs.org/">Konva.js</a>というライブラリを使ってOGP画像を生成したので、<br />
その時に調べたことをまとめてみました。</p>
<h3 id="積読・読書前でも書ける『積読レビュー』"><a href="#%E7%A9%8D%E8%AA%AD%E3%83%BB%E8%AA%AD%E6%9B%B8%E5%89%8D%E3%81%A7%E3%82%82%E6%9B%B8%E3%81%91%E3%82%8B%E3%80%8E%E7%A9%8D%E8%AA%AD%E3%83%AC%E3%83%93%E3%83%A5%E3%83%BC%E3%80%8F">積読・読書前でも書ける『積読レビュー』</a></h3>
<p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/fc0f21b7-ee49-f01c-34a1-0d44ed3022d2.png" width="600px"/></p>
<p>読書のレビューというと、読書後の感想を書くものですが、<br />
積ん読が多いとなかなか書く機会がないです。。</p>
<p><strong>「感想は読書後だけではないのでは?」</strong><br />
ということで、読書前でも書けるレビューを2つ用意してみました。</p>
<h4 id="妄想で書く『妄想レビュー』"><a href="#%E5%A6%84%E6%83%B3%E3%81%A7%E6%9B%B8%E3%81%8F%E3%80%8E%E5%A6%84%E6%83%B3%E3%83%AC%E3%83%93%E3%83%A5%E3%83%BC%E3%80%8F">妄想で書く『妄想レビュー』</a></h4>
<p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/8e8a9968-c44f-64c8-93d2-0f8eb1f3172a.png" width="600px"/></p>
<p><strong>まだ読んでないけど、本の表紙や帯の印象から妄想で書いてみる『妄想レビュー』</strong></p>
<p>「中身じゃなく外観だけで、読書後のレビューを書いてみるとどうだろう。。?」<br />
というネタ的なレビューです。</p>
<p>いかに読んでる醸し出せるかをチャレンジしてみるとおもしろいかも(<em>´ω`</em>)</p>
<p>他の使い方としては、<strong>読む前に書いておいて、読んだ後との感想を比較してみる</strong>のもたのしいかも?<br />
読書前後で感想が同じになっても違っていても、新たな発見があるかもです。</p>
<h4 id="きっかけを書く『きっかけレビュー』"><a href="#%E3%81%8D%E3%81%A3%E3%81%8B%E3%81%91%E3%82%92%E6%9B%B8%E3%81%8F%E3%80%8E%E3%81%8D%E3%81%A3%E3%81%8B%E3%81%91%E3%83%AC%E3%83%93%E3%83%A5%E3%83%BC%E3%80%8F">きっかけを書く『きっかけレビュー』</a></h4>
<p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/e740910f-2687-0484-e3d2-bebdd0d95bf6.png" width="600px"/></p>
<p><strong>買ったときのきっかけや意気込み・ワクワク感を書いてみる『きっかけレビュー』</strong></p>
<p>「『なぜ買うのか、どうして買いたいのか』という気持ちを残しておくのもどうだろう。。?」<br />
と思いつきつくってみました。</p>
<p>買ったときの気持ちも大事で、<br />
* 技術書やビジネス書なら「こうなりたい!ここを強くしたい!」<br />
* 小説やマンガなら「ひさびさの新刊!たのしみ!」<br />
という思いがあるはず・・</p>
<p><strong>それを記録として残しておくと、いつか見返したときにたのしいかもしれません。</strong></p>
<h2 id="なぜKonva.js?"><a href="#%E3%81%AA%E3%81%9CKonva.js%EF%BC%9F">なぜKonva.js?</a></h2>
<p>この企画では、本を選んで、レビューを書いていくのですが、<br />
本の表紙など画像を埋め込む必要があります。</p>
<p>以前、<a target="_blank" rel="nofollow noopener" href="http://hen-ai.net/">別のサービス</a>で、<a target="_blank" rel="nofollow noopener" href="https://www.memory-lovers.blog/entry/2020/01/24/110000">SVGでOGP用の画像を生成</a>してみたのですが、端末によってはうまくいかなかったりと、画像を埋め込むのがかなり大変でした。。</p>
<p>なので、SVGではなく、Canvasで試してみようと調べたところ、<br />
<a target="_blank" rel="nofollow noopener" href="https://konvajs.org/">Konva.js</a>と<a target="_blank" rel="nofollow noopener" href="http://fabricjs.com/">Fabric.js</a>が見つけ、<br />
文字の折返しなどのガイドがあった<a target="_blank" rel="nofollow noopener" href="https://konvajs.org/">Konva.js</a>を選んでみました。</p>
<h2 id="Konvaで画像生成"><a href="#Konva%E3%81%A7%E7%94%BB%E5%83%8F%E7%94%9F%E6%88%90">Konvaで画像生成</a></h2>
<p>ここからが本題。やっと本題。。</p>
<h3 id="準備"><a href="#%E6%BA%96%E5%82%99">準備</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 vue-konva konva --save
</code></pre>
<p>今回は、Nuxt/Vueで使うので、KonvaのVueライブラリ(<a target="_blank" rel="nofollow noopener" href="https://konvajs.org/docs/vue/index.html">vue-konva</a>)もインストール</p>
<h4 id="プラグインの作成"><a href="#%E3%83%97%E3%83%A9%E3%82%B0%E3%82%A4%E3%83%B3%E3%81%AE%E4%BD%9C%E6%88%90">プラグインの作成</a></h4>
<p>vue-konvaを使えるようプラグインを作成。</p>
<pre><code class="ts">// ~/plugins/vue-konva.ts
import Vue from "vue";
import VueKonva from "vue-konva";
Vue.use(VueKonva);
</code></pre>
<p>そして、作ったプラグインをnuxt.config.tsに追加。</p>
<pre><code class="ts">// nuxt.config.js
import { Configuration } from "@nuxt/types";
const config: Configuration = {
mode: "spa",
// 略
plugins: [
{ src: "~/plugins/vue-konva", ssr: false },
// 略
],
// 略
};
</code></pre>
<p>これで準備はOK</p>
<h3 id="KonvaでCanvasを描いてみる"><a href="#Konva%E3%81%A7Canvas%E3%82%92%E6%8F%8F%E3%81%84%E3%81%A6%E3%81%BF%E3%82%8B">KonvaでCanvasを描いてみる</a></h3>
<h4 id="画像を表示する(v-image)"><a href="#%E7%94%BB%E5%83%8F%E3%82%92%E8%A1%A8%E7%A4%BA%E3%81%99%E3%82%8B%28v-image%29">画像を表示する(v-image)</a></h4>
<p>まずは、フレームとなる以下の背景画像を表示してみます。</p>
<p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/9033133e-55f5-17eb-a8bc-85ce000a68ef.png" width="600px"/></p>
<pre><code class="html"><template>
<div>
<v-stage :config="configKonva">
<v-layer>
<v-image :config="configBg"></v-image>
</v-layer>
</v-stage>
</div>
</template>
<script lang="ts">
import { Component, Vue, Prop, Watch } from "nuxt-property-decorator";
const IMAGE_WIDTH = 1200;
const IMAGE_HEIGHT = 630;
@Component
export default class ReviewPreviewSample extends Vue {
// konva全体の設定(v-stageに対応)
private configKonva = { width: IMAGE_WIDTH, height: IMAGE_HEIGHT };
// 背景画像の設定(v-imageに対応)
private configBg: { image: HTMLImageElement | null } = { image: null };
async mounted() {
// マウントされたら、背景画像を読み込んで設定する
this.setupBg().then();
}
// 背景画像の設定
private async setupBg() {
const image = await this.getImage("/img/review_paple.png");
this.configBg.image = image;
}
// 共通処理: 画像の読み込み
private async getImage(src: string): Promise<HTMLImageElement> {
return new Promise((resolve, reject) => {
const image = new window.Image();
image.onload = () => resolve(image);
image.src = src;
});
}
}
</script>
</code></pre>
<p>こんな感じ。</p>
<h5 id="基本的な構成"><a href="#%E5%9F%BA%E6%9C%AC%E7%9A%84%E3%81%AA%E6%A7%8B%E6%88%90">基本的な構成</a></h5>
<p>基本的な構成は、こんな感じで、<br />
<code>v-stage > v-layer > その他諸々</code><br />
で配置していく。</p>
<pre><code class="html"><v-stage :config="configKonva">
<v-layer>
<v-image :config="configBg"></v-image>
</v-layer>
</v-stage>
</code></pre>
<p>必ずrootが<code>v-stage</code>で、その直下が<code>v-layer</code>。<br />
<code>v-layer</code>はいくつも作れるけど、3〜4までがよいっぽい。(waringがでた)</p>
<p>位置やサイズ、表示する画像は<code>:config</code>を介して、Kanva.js自体の設定値を付与していく。</p>
<p>どんな設定値があるかは、<a target="_blank" rel="nofollow noopener" href="https://konvajs.org/api/Konva.html">ApiDoc</a>を見ていく感じで、<br />
Imageだと<a target="_blank" rel="nofollow noopener" href="https://konvajs.org/api/Konva.Image.html">ここ(Class: Image)をみる</a></p>
<p><a target="_blank" rel="nofollow noopener" href="https://konvajs.org/docs/">チュートリアル</a>もあるので、それを見つつ、<br />
具体的な設定値は<a target="_blank" rel="nofollow noopener" href="https://konvajs.org/api/Konva.html">ApiDoc</a>参照という流れで進めた。</p>
<h5 id="画像の表示には読み込みが必要"><a href="#%E7%94%BB%E5%83%8F%E3%81%AE%E8%A1%A8%E7%A4%BA%E3%81%AB%E3%81%AF%E8%AA%AD%E3%81%BF%E8%BE%BC%E3%81%BF%E3%81%8C%E5%BF%85%E8%A6%81">画像の表示には読み込みが必要</a></h5>
<p>画像の表示は少しめんどくさくて、</p>
<ol>
<li>HTMLImageElementをつくって</li>
<li>HTMLImageElementで画像をロードして</li>
<li>ロード後のHTMLImageElementをconfigに設定</li>
</ol>
<p>という流れ。。</p>
<p>それをしているのがこのあたり。<br />
画像読み込みは何度も使うけど、Promiseじゃないので、<br />
ロードが終わったらHTMLImageElementを返す処理を共通化</p>
<pre><code class="ts">async mounted() {
// マウントされたら、背景画像を読み込んで設定する
this.setupBg().then();
}
// 背景画像の設定
private async setupBg() {
const image = await this.getImage("/img/review_paple.png");
this.configBg.image = image;
}
// 共通処理: 画像の読み込み
private async getImage(src: string): Promise<HTMLImageElement> {
return new Promise((resolve, reject) => {
const image = new window.Image();
image.onload = () => resolve(image);
image.src = src;
});
}
</code></pre>
<p>これでできるのがこんな感じ。</p>
<p><img width="600px" alt="スクリーンショット 2020-05-06 12.46.54.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/b295f2ff-7b56-5ab4-1e9c-055ef7ff4314.png"></p>
<p>でかい。。<br />
CanvasのサイズをOGP画像のサイズ(1200x630)にしているもんね。。</p>
<h5 id="画像を画面サイズにあわせる"><a href="#%E7%94%BB%E5%83%8F%E3%82%92%E7%94%BB%E9%9D%A2%E3%82%B5%E3%82%A4%E3%82%BA%E3%81%AB%E3%81%82%E3%82%8F%E3%81%9B%E3%82%8B">画像を画面サイズにあわせる</a></h5>
<p>ほんとはこんな感じにしたい。。</p>
<p><img width="600px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/e3210e3f-c4b0-6e6c-1a11-e791dc90dd05.gif" /></p>
<p>ので、Windowサイズの変更を検知して、CSSのscaleで調整してみる。</p>
<p>こんな感じ。</p>
<pre><code class="html"><template>
<!-- サイズ計算用にidが必要なので追加 -->
<div id="preview-wrapper" class="preview-wrapper">
<v-stage :config="configKonva" class="preview-content" :style="style">
<v-layer>
<v-image :config="configBg"></v-image>
</v-layer>
</v-stage>
</div>
</template>
<script lang="ts">
import { Component, Vue, Prop, Watch } from "nuxt-property-decorator";
const IMAGE_WIDTH = 1200;
const IMAGE_HEIGHT = 630;
@Component
export default class ReviewPreviewSample extends Vue {
private configKonva = { width: IMAGE_WIDTH, height: IMAGE_HEIGHT };
private configBg: { image: HTMLImageElement | null } = { image: null };
private scale: number = 0;
async mounted() {
this.setupBg().then();
// 初回のスケール計算処理を走らせる
this.$nextTick(() => this.handleResize());
// イベントリスナーにスケール計算処理を登録
window.addEventListener("resize", this.handleResize);
}
beforeDestroy() {
// Destroy前に解放
window.removeEventListener("resize", this.handleResize);
}
private get style() {
// transformのscaleを使って、サイズ調整
return {
transform: `scale(${this.scale})`,
"-webkit-transform": `scale(${this.scale})`,
"transform-origin": "0 0"
};
}
// スケール計算用の処理: 対象IDのサイズを取得して、scaleを計算
private handleResize() {
const elm = document.getElementById("preview-wrapper");
if (!elm) return;
const rect = elm.getBoundingClientRect();
this.scale = rect.width / IMAGE_WIDTH;
}
private async setupBg() {
// 略
}
private async getImage(src: string): Promise<HTMLImageElement> {
// 略
}
}
</script>
<style lang="scss" scoped>
/* 画像のアスペクト比は固定なので、あらかじめ高さをCSSで調整 */
.preview-wrapper {
position: relative;
width: 100%;
height: auto;
}
.preview-wrapper {
&:before {
content: "";
display: block;
padding-top: 52.5%; /* 630 / 1200 x 100 */
}
}
.preview-content {
position: absolute;
top: 0;
left: 0;
}
</style>
</code></pre>
<p>これでOK。Konva.jsの話に戻る</p>
<h4 id="画像を加工する(サイズ変更)"><a href="#%E7%94%BB%E5%83%8F%E3%82%92%E5%8A%A0%E5%B7%A5%E3%81%99%E3%82%8B%28%E3%82%B5%E3%82%A4%E3%82%BA%E5%A4%89%E6%9B%B4%29">画像を加工する(サイズ変更)</a></h4>
<p>次は書影を読み込んで、配置する部分。<br />
書影は本ごとにサイズが違うので、加工が必要。。</p>
<p><img width="600" alt="スクリーンショット_2020-05-06_13_06_55.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/733c10bc-58e3-ffd4-45cd-09d344c9eb4f.png"></p>
<pre><code class="html"><template>
<div id="preview-wrapper" class="preview-wrapper">
<v-stage :config="configKonva" class="preview-content" :style="style">
<v-layer>
<v-image :config="configBg"></v-image>
</v-layer>
<v-layer>
<!-- 書影の画像 -->
<v-image :config="configBookImg"></v-image>
</v-layer>
</v-stage>
</div>
</template>
<script lang="ts">
import { Component, Vue, Prop, Watch } from "nuxt-property-decorator";
const IMAGE_WIDTH = 1200;
const IMAGE_HEIGHT = 630;
const BOOK_IMAGE_WIDTH = 464;
const BOOK_IMAGE_HEIGHT = 470;
@Component
export default class ReviewPreviewSample extends Vue {
// 略
private configBookImg: any | null = null;
async mounted() {
this.setupBg().then();
// 略
}
// 略
private async setupBookImage() {
// 画像の読み込み
const imageBook = await this.getImage("/img/notfound_paple.png");
// v-imageのconfig
const scale = BOOK_IMAGE_WIDTH / imageBook.width;
this.configBookImg = {
image: imageBook,
x: 48, // 左上からの位置(x)
y: 112, // 左上からの位置(y)
width: BOOK_IMAGE_WIDTH, // 画像のサイズ(横)
height: BOOK_IMAGE_HEIGHT, // 画像のサイズ(縦)
crop: { // 切り取り処理
x: 0, // 切り取り位置(x)
y: 0, // 切り取り位置(y)
width: imageBook.width, // 切り取りサイズ(横)
height: BOOK_IMAGE_HEIGHT / scale // 切り取りサイズ(縦)
}
};
}
// 略
}
</script>
<style lang="scss" scoped>
/* 略 */
</style>
</code></pre>
<p>こんな感じ。切り取りは、v-imageのcropを指定すればOK。</p>
<p>ここで若干ハマった。。</p>
<h5 id="【ハマり1】cropとscaleを同時に指定するとうまくいかない。。"><a href="#%E3%80%90%E3%83%8F%E3%83%9E%E3%82%8A1%E3%80%91crop%E3%81%A8scale%E3%82%92%E5%90%8C%E6%99%82%E3%81%AB%E6%8C%87%E5%AE%9A%E3%81%99%E3%82%8B%E3%81%A8%E3%81%86%E3%81%BE%E3%81%8F%E3%81%84%E3%81%8B%E3%81%AA%E3%81%84%E3%80%82%E3%80%82">【ハマり1】cropとscaleを同時に指定するとうまくいかない。。</a></h5>
<p>サイズを調整しようと、いろいろしていたときに、<br />
スケールを変化させる<code>scale</code>と切り取り処理の<code>crop</code>を同時に指定してみたけど、<br />
あまりうまくいかず、cropのみで対応した。。</p>
<h5 id="【ハマり2】computedだとうまくいかない。。"><a href="#%E3%80%90%E3%83%8F%E3%83%9E%E3%82%8A2%E3%80%91computed%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%E3%80%82%E3%80%82">【ハマり2】computedだとうまくいかない。。</a></h5>
<p>最初は、以下のような感じで、computedにしてたけど、<br />
cropなど、配下のobjectが変わっても変更が反映されなかったので、<br />
dataのconfigBookImgを用意する形にしてみた。</p>
<pre><code class="ts">get configBookImg() {
// 画像の読み込み
const imageBook = await this.getImage("/img/notfound_paple.png");
// v-imageのconfig
const scale = BOOK_IMAGE_WIDTH / imageBook.width;
return {
image: imageBook,
x: 48, // 左上からの位置(x)
y: 112, // 左上からの位置(y)
width: BOOK_IMAGE_WIDTH, // 画像のサイズ(横)
height: BOOK_IMAGE_HEIGHT, // 画像のサイズ(縦)
crop: { // 切り取り処理
x: 0, // 切り取り位置(x)
y: 0, // 切り取り位置(y)
width: imageBook.width, // 切り取りサイズ(横)
height: BOOK_IMAGE_HEIGHT / scale // 切り取りサイズ(縦)
}
};
}
</code></pre>
<h4 id="画像を加工する(丸く切り取る)"><a href="#%E7%94%BB%E5%83%8F%E3%82%92%E5%8A%A0%E5%B7%A5%E3%81%99%E3%82%8B%28%E4%B8%B8%E3%81%8F%E5%88%87%E3%82%8A%E5%8F%96%E3%82%8B%29">画像を加工する(丸く切り取る)</a></h4>
<p>次はこれ。ユーザのアイコン画像。</p>
<p><img width="600" alt="スクリーンショット_2020-05-06_14_08_51.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/80f51e08-b4f4-5fd5-fa3e-35ef560b7197.png"></p>
<p>もともとは、こんな四角い画像だけど、アイコンっぽく丸く切り取りたい。</p>
<p><img width="200" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/62f0de4a-a307-82be-9755-a1a0963b9e2e.png" /></p>
<p><a target="_blank" rel="nofollow noopener" href="https://konvajs.org/docs/clipping/Clipping_Function.html">チュートリアル</a>を見てみると、<code>v-group</code>を使うとできるっぽい。</p>
<pre><code class="html"><template>
<div id="preview-wrapper" class="preview-wrapper">
<v-stage :config="configKonva" class="preview-content" :style="style">
<!-- 略 -->
<v-layer>
<v-group :config="configUserImageGroup">
<v-image :config="configUserImg"></v-image>
</v-group>
</v-layer>
</v-stage>
</div>
</template>
<script lang="ts">
import { Component, Vue, Prop, Watch } from "nuxt-property-decorator";
const IMAGE_WIDTH = 1200;
const IMAGE_HEIGHT = 630;
const BOOK_IMAGE_WIDTH = 464;
const BOOK_IMAGE_HEIGHT = 470;
const USER_IMAGE_SIZE = 76;
@Component
export default class ReviewPreviewSample extends Vue {
// 略
private configUserImg: any | null = null;
async mounted() {
// 略
this.setupUserImage().then();
// 略
}
// 略
private get configUserImageGroup() {
return {
// 複雑な切り取り処理: CanvasRenderingContext2Dのarcを使う
clipFunc: ctx => {
const r = USER_IMAGE_SIZE / 2;
ctx.arc(1092 + r, 538 + r, r, 0, Math.PI * 2, false);
},
// ドラッグを無効にする
draggable: false
};
}
// ユーザ画像の読み込み処理: 本の画像とかとだいたい同じ
private async setupUserImage() {
const image = await this.getImage("/img/kira_puka.png");
this.configUserImg = {
image: image,
x: 1092,
y: 538,
width: USER_IMAGE_SIZE,
height: USER_IMAGE_SIZE
};
}
// 略
}
</script>
<style lang="scss" scoped>
/* 略 */
</style>
</code></pre>
<p><code>v-group</code>の<code>clipFunc</code>を使うと、Canvaを操作できるようで、<br />
<a target="_blank" rel="nofollow noopener" href="https://developer.mozilla.org/ja/docs/Web/API/CanvasRenderingContext2D/arc">CanvasRenderingContext2Dのarc</a>を使って、丸く切り取り。</p>
<p>注意点は、原点がCanvas(v-stage)の左上であること。<br />
画像の左上が原点だと思っていたので、全然切り取られなかった。。</p>
<h4 id="1行の文字を表示する(省略あり)"><a href="#1%E8%A1%8C%E3%81%AE%E6%96%87%E5%AD%97%E3%82%92%E8%A1%A8%E7%A4%BA%E3%81%99%E3%82%8B%28%E7%9C%81%E7%95%A5%E3%81%82%E3%82%8A%29">1行の文字を表示する(省略あり)</a></h4>
<p>画像はここまで。次は文字。本のタイトルを表示したい。</p>
<p><img width="600" alt="スクリーンショット_2020-05-06_14_20_00.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/5042be5a-3901-780e-34eb-3616558ed1e8.png"></p>
<pre><code class="html"><template>
<div id="preview-wrapper" class="preview-wrapper">
<v-stage :config="configKonva" class="preview-content" :style="style">
<!-- 略 -->
<v-layer>
<v-text :config="configBookTitle"></v-text>
</v-layer>
</v-stage>
</div>
</template>
<script lang="ts">
import { Component, Vue, Prop, Watch } from "nuxt-property-decorator";
// 略
@Component
export default class ReviewPreviewSample extends Vue {
// 略
private get configBookTitle() {
return {
// 表示する文字
text: "選んだ本のタイトル 選んだ本のタイトル 選んだ本のタイトル 選んだ本のタイトル",
x: 200,
y: 28,
width: IMAGE_WIDTH - 400,
fill: "#FFFFFF", // 文字の色
fontSize: 36, // フォントサイズ
fontFamily: "Noto Sans JP", // フォントの種類
fontStyle: "bold", // フォントのスタイル
align: "center", // 揃え位置(中央揃え)
// 折り返しなし(wrap)を指定すると、省略(ellipsis)を設定可能に
wrap: "none",
ellipsis: true
};
}
// 略
}
</script>
<style lang="scss" scoped>
/* 略 */
</style>
</code></pre>
<p>文字を表示するには、<code>v-text</code>を使えばOK</p>
<p>CSSのtext-overflowと同じ感じで、折返しをnoneにすれば、省略記号の表示もできる。</p>
<h4 id="複数行の文字を表示する(省略不可)"><a href="#%E8%A4%87%E6%95%B0%E8%A1%8C%E3%81%AE%E6%96%87%E5%AD%97%E3%82%92%E8%A1%A8%E7%A4%BA%E3%81%99%E3%82%8B%28%E7%9C%81%E7%95%A5%E4%B8%8D%E5%8F%AF%29">複数行の文字を表示する(省略不可)</a></h4>
<p>次はレビューの文字を表示。</p>
<p><img width="600" alt="スクリーンショット_2020-05-06_14_30_22.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/75fc126e-1179-e8d1-a2ab-6aa1aaf44974.png"></p>
<pre><code class="html"><template>
<div id="preview-wrapper" class="preview-wrapper">
<v-stage :config="configKonva" class="preview-content" :style="style">
<!-- 略 -->
<v-layer>
<v-text :config="configReview"></v-text>
</v-layer>
</v-stage>
</div>
</template>
<script lang="ts">
import { Component, Vue, Prop, Watch } from "nuxt-property-decorator";
// 略
@Component
export default class ReviewPreviewSample extends Vue {
// 略
private get configReview() {
return {
text: "レビューの内容れびゅーのないよう\n\nレビューの内容れびゅーのないようレビューの内容れびゅーのないようレビューの内容れびゅーのないようレビューの内容れびゅーのないようレビューの内容れびゅーのないようレビューの内容れびゅーのないようレビューの内容れびゅーのないようレビューの内容れびゅーのないよう",
x: 596,
y: 188,
width: 542,
height: 270,
fill: "#443a33",
fontSize: 30,
fontFamily: "Noto Sans JP"
// 行の高さ
lineHeight: 1.2,
};
}
// 略
}
</script>
<style lang="scss" scoped>
/* 略 */
</style>
</code></pre>
<p>同じく<code>v-text</code>を使う感じ。<br />
<code>wrap</code>を指定しなければ、そのまま複数行に。</p>
<p>改行文字を入れれば改行もしてくれて、widthを指定すれば折返しも自動。</p>
<p>ただ、残念ながら、<code>-webkit-line-clamp</code>みたいに複数行の省略はないみたい。。</p>
<h3 id="Canvasを画像にエクスポート"><a href="#Canvas%E3%82%92%E7%94%BB%E5%83%8F%E3%81%AB%E3%82%A8%E3%82%AF%E3%82%B9%E3%83%9D%E3%83%BC%E3%83%88">Canvasを画像にエクスポート</a></h3>
<p>最後に作った画像をエクスポート!!</p>
<p><img width="500" alt="スクリーンショット_2020-05-06_14_41_16.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/57be76de-b8c2-3a34-31f8-5b85079b621c.png"></p>
<pre><code class="html"><template>
<div>
<div id="preview-wrapper" class="preview-wrapper">
<!-- v-stageが取得できるようにrefを追加-->
<v-stage
ref="stage"
:config="configKonva"
class="preview-content"
:style="style"
>
<!-- 略 -->
</v-stage>
</div>
<!-- ボタンを追加 -->
<div class="has-text-centered">
<b-button type="is-primary" size="is-medium" @click="download">
<span>作成する</span>
</b-button>
</div>
</div>
</template>
<script lang="ts">
import { Component, Vue, Prop, Watch } from "nuxt-property-decorator";
// 略
@Component
export default class ReviewPreviewSample extends Vue {
// 略
// ダウンロード処理
private async download() {
// stageの取得: 大きいサイズを取得するときは、'pixelRatio'で倍率を指定
const stage = (this.$refs.stage as any).getStage({ pixelRatio: 1 });
// stageからDataURLを取得
const dataURL = stage.toDataURL();
// ダウンロード処理: プログラムでリンクを作ってクリック
const link = document.createElement("a");
link.download = "ogp.png";
link.href = dataURL;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
}
</script>
<style lang="scss" scoped>
/** 略 **/
</style>
</code></pre>
<p>こんな感じ。Konvaでエクスポートするときには、<code>v-stage</code>から取得する。<br />
DataURL以外にJSONとかでもエクスポートできるので、保存したりもOK</p>
<ul>
<li><a target="_blank" rel="nofollow noopener" href="https://konvajs.org/docs/data_and_serialization/Stage_Data_URL.html">HTML5 Canvas Stage Data URL Tutorial | Konva - JavaScript 2d canvas library</a></li>
</ul>
<p>開発しているサービスでは、FirebaseのCloud Storageに保存しているので、DataURL形式でOK。</p>
<p>ハマリポイントは、外部URLの画像を使っているとき。。エクスポートに失敗する。。<br />
Cavans自体の仕様で外部URLの画像がある場合、汚染されたとみなされエラーになるとのこと。。</p>
<p>対処法は以下に書かれている感じで、CORS対応をすればOK<br />
・<a target="_blank" rel="nofollow noopener" href="https://konvajs.org/docs/posts/Tainted_Canvas.html">Resolving "Tainted canvases may not be exported" with Konva | Konva - JavaScript 2d canvas library</a></p>
<h2 id="おわり"><a href="#%E3%81%8A%E3%82%8F%E3%82%8A">おわり</a></h2>
<p>SVGでも外部URLは問題があったけど、Konva.jsでも。。<br />
でも、サクサクできたのと、ドラッグで位置を変えれるみたいなので、いろいろできそう(<em>´ω`</em>)</p>
<p>積ん読レビューはできたての機能なので、ぜひぜひさわってみてくださいヽ(=´▽`=)ノ<br />
■<a target="_blank" rel="nofollow noopener" href="https://tsundoku.site/review/">どっちでレビューする?妄想レビュ・きっかけレビュー</a></p>
<p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/fc0f21b7-ee49-f01c-34a1-0d44ed3022d2.png" width="600px"/></p>
きらぷか@積読ハウマッチ/SSSAPIなど
tag:crieit.net,2005:PublicArticle/15876
2020-04-27T23:20:13+09:00
2020-04-27T23:20:13+09:00
https://crieit.net/posts/Vercel-Zeit-Now-Nuxt-js-2020
Vercel(元ZeitのNow)でNuxt.jsアプリをデプロイ 2020年最新版
<p><code>now</code> というコマンドを使わないVercel(元ZeitのNow)へのデプロイ方法を解説します。今回の手順ではNuxt.jsを使います。</p>
<h2 id="背景"><a href="#%E8%83%8C%E6%99%AF">背景</a></h2>
<p><code>now</code> というコマンドで簡単にデプロイできるということで一世を風靡したZeitのNowですが、先日Vercelというサービス名に変更されました。その前からちょこちょこ新たな機能をリリースしており、nowというコマンドを使わなくてもGitHub連携によってpushするだけでデプロイできるようになったり、環境変数も独自のsecretsという設定を使わず、管理画面で設定できるようになったりと、かなり手軽になってきているので実際にそれらを使ったデプロイ方法を説明します。</p>
<h2 id="Nuxt.jsアプリを作成"><a href="#Nuxt.js%E3%82%A2%E3%83%97%E3%83%AA%E3%82%92%E4%BD%9C%E6%88%90">Nuxt.jsアプリを作成</a></h2>
<p>まずはNuxt.jsのアプリケーションを作成します。</p>
<pre><code>npx create-nuxt-app nuxt-vercel-sample
# or
yarn create nuxt-app nuxt-vercel-sample
</code></pre>
<p>いくつか設定項目が現れるため下記のように設定しました。適当です。</p>
<pre><code class="text">✨ Generating Nuxt.js project in nuxt-vercel-sample
? Project name nuxt-vercel-sample
? Project description My astonishing Nuxt.js project
? Author name dala00
? Choose programming language TypeScript
? Choose the package manager Yarn
? Choose UI framework Buefy
? Choose custom server framework None (Recommended)
? Choose the runtime for TypeScript Default
? Choose Nuxt.js modules Axios, Progressive Web App (PWA) Support, DotEnv
? Choose linting tools (Press <space> to select, <a> to toggle all, <i> to invert selection)
? Choose test framework Jest
? Choose rendering mode Universal (SSR)
? Choose development tools (Press <space> to select, <a> to toggle all, <i> to invert selection)
</code></pre>
<p>あとは出力されるメッセージの通り、下記のようにして実行できます。</p>
<pre><code>cd nuxt-vercel-sample
yarn dev
</code></pre>
<p>見た目は利用しているUIフレームワークによって変わります。</p>
<p><a href="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea6e12b13314.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea6e12b13314.png?mw=700" alt="" /></a></p>
<h2 id="GitHubへ登録"><a href="#GitHub%E3%81%B8%E7%99%BB%E9%8C%B2">GitHubへ登録</a></h2>
<p>今回はnowコマンドは一切使わずGitHub連携でデプロイを行います。ですのでまずはいま作成したアプリケーションをGitHubに登録します。その前にとりあえずcommitしておきます。<code>git init</code>だけは勝手にやってくれています。</p>
<pre><code>git add .
git commit -m "Initial commit"
</code></pre>
<p>VSCode等でやってもよいでしょう。</p>
<p>まずは適当にGitHubでリポジトリを作成します。</p>
<p><a href="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea6e189f259c.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea6e189f259c.png?mw=700" alt="" /></a></p>
<p>表示されているとおりに設定してpushします(SSHキーの登録などは事前に行っておいてください)。</p>
<p><a href="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea6e1ac15622.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea6e1ac15622.png?mw=700" alt="" /></a></p>
<h2 id="Vercelにデプロイする"><a href="#Vercel%E3%81%AB%E3%83%87%E3%83%97%E3%83%AD%E3%82%A4%E3%81%99%E3%82%8B">Vercelにデプロイする</a></h2>
<p>続いてVercel側に今pushしたものをデプロイします。……がその前に、Nuxt.jsをVercelで使うための <code>@nuxtjs/now-builder</code> というものがNuxt.js公式から用意されていますので、それをnow.jsonに設定します。</p>
<pre><code class="json">{
"version": 2,
"builds": [
{
"src": "nuxt.config.js",
"use": "@nuxtjs/now-builder",
"config": {}
}
]
}
</code></pre>
<p>これをcommit & pushしておきましょう。</p>
<p>続いてVercelのダッシュボードにImport Projectボタンがあるためそちらをクリックします。</p>
<p><a href="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea459d4063ee.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea459d4063ee.png?mw=700" alt="" /></a></p>
<p>From Git Repositoryを選択します。</p>
<p><a href="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea459fd1c53f.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea459fd1c53f.png?mw=700" alt="" /></a></p>
<p>Importボタンをクリックします。</p>
<p><a href="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea45a3fb370b.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea45a3fb370b.png?mw=700" alt="" /></a></p>
<p>プロジェクトの選択画面が開きますので適宜選択します。僕の場合は個別に権限を与えたものだけ選択できるようにしているため、同様の場合は下記のようにEdit your repository access settings on GitHub.を実行してGitHub側で権限を与えてから選択します。</p>
<p><a href="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea45abb2e10f.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea45abb2e10f.png?mw=700" alt="" /></a></p>
<p>選択できたらSelectしてImportを実行です。プロジェクト名の入力画面が開くので入力してContinueします。</p>
<p>引き続きROOT DIRECTORYやBUILD COMMANDの設定画面なども開きますが、Nuxt.jsアプリケーションの場合は一切何も設定しなくてもデプロイが可能のためひたすらContinueして最後のDeployボタンを押せば完了です(さきほどのnow.jsonの設定が抜けているとビルドに失敗します)。</p>
<p><a href="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea6e54ca8f75.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea6e54ca8f75.png?mw=700" alt="" /></a></p>
<p>Buildingステータスになっているのでしばし待ちます。</p>
<p><a href="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea45bdf2d264.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea45bdf2d264.png?mw=700" alt="" /></a></p>
<p>Readyとなったらデプロイ完了です。</p>
<p><a href="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea6e59b5216c.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea6e59b5216c.png?mw=700" alt="" /></a></p>
<p>URLがいくつかありますが、どれでも閲覧できます。DEPLOYMENTというURLは一意なデプロイURLです。DOMAINSはそのプロジェクトのURLで、いつも使えるURLです。どれでも良いので開くとローカルで実行したアプリケーションと同じ画面が表示されます。これでデプロイは完了です!</p>
<p>ちなみに下記は今回作ったサンプルです。</p>
<p><a target="_blank" rel="nofollow noopener" href="https://nuxt-vercel-sample.now.sh/">https://nuxt-vercel-sample.now.sh/</a><br />
<a target="_blank" rel="nofollow noopener" href="https://github.com/dala00/nuxt-vercel-sample">https://github.com/dala00/nuxt-vercel-sample</a></p>
<h2 id="更新したい時"><a href="#%E6%9B%B4%E6%96%B0%E3%81%97%E3%81%9F%E3%81%84%E6%99%82">更新したい時</a></h2>
<p>プログラムを調整して更新したいときは、GitHubにpushするだけで勝手にデプロイされます。</p>
<p>ちなみに先ほどの画面はProduction Deploymentで、masterブランチのものがProductionとしてデプロイされます。別のブランチを作ってそちらでpushしておくと、その製品版のURLは利用されず、別ブランチ用のURLが作成されるため安全にステージング環境として利用することが可能です。下記のようにDeploymentsメニューで全てのブランチのデプロイを確認できます。</p>
<p><a href="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea6e68aa6335.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea6e68aa6335.png?mw=700" alt="" /></a></p>
<p>それをクリックすると下記のようにテスト用のURLを利用することが出来ます。</p>
<p><a href="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea6e6e256a15.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea6e6e256a15.png?mw=700" alt="" /></a></p>
<p>プルリクエストを作るとブランチのマージもGitHub上だけで行えます。</p>
<p>ちなみにGitHub連携しているとこんな感じでnowちゃんが甲斐甲斐しくコメントをしてくれます(そういやこちらはまだ名前が変わってないですね)。これで一人ぼっちで友達がいなくても寂しくありません。</p>
<p><a href="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea6e70fa5c7a.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea6e70fa5c7a.png?mw=700" alt="" /></a></p>
<h2 id="環境変数を設定する"><a href="#%E7%92%B0%E5%A2%83%E5%A4%89%E6%95%B0%E3%82%92%E8%A8%AD%E5%AE%9A%E3%81%99%E3%82%8B">環境変数を設定する</a></h2>
<p>Nowではずっとsecretsという機能を利用してプライベートな値を利用できるようにしていましたが、最近Vercelになったのと同じくらいの時期に管理画面で直接環境変数を設定できるようになりました。下記のように設定ができます。</p>
<p><a href="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea594c111ec1.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea594c111ec1.png?mw=700" alt="" /></a></p>
<p>ちなみにProductionは本番、つまり独自ドメインを設定していればそちらのURLや、<code>プロジェクト名.now.sh</code> というシンプルなエイリアスのものなどです。Previewは別のブランチでpushした時に作成されるURLです。DevelopmentはローカルPCにて、<code>now dev</code>を実行した時のものです。</p>
<p>設定後は見えなくなります。</p>
<p><a href="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea594dc71d71.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea594dc71d71.png?mw=700" alt="" /></a></p>
<p>Nuxt.js上では、nuxt.config.jsにて例えば下記のように設定します。ローカルでの環境変数は直接指定するか、dotenv等を利用しましょう。</p>
<pre><code class="javascript">// require('dotenv').config()
export default {
mode: "universal",
env: {
TEST_TEXT: process.env.TEST_TEXT
},
</code></pre>
<p>index.vueにてscript側に値を取得処理を作成し、</p>
<pre><code class="javascript"> methods: {
getText() {
return process.env.TEST_TEXT
}
}
</code></pre>
<p>表示側でそれを使います。</p>
<pre><code class="html"> <span>{</span><span>{</span> getText() <span>}</span><span>}</span>
</code></pre>
<p>問題なければ下記のように表示されます。</p>
<p><a href="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea6e9d8ecc0e.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea6e9d8ecc0e.png?mw=700" alt="" /></a></p>
<h2 id="nowコマンドは?"><a href="#now%E3%82%B3%E3%83%9E%E3%83%B3%E3%83%89%E3%81%AF%EF%BC%9F">nowコマンドは?</a></h2>
<p>こちらも使えます。今回のパターンだと不要でしたが、 複数のbuildersを使う場合などはローカルで <code>now dev</code> コマンドで開発ができます。この場合は事前に <code>now</code> コマンドを実行してVercel側のプロジェクトと紐付けておく必要があります。</p>
<h2 id="まとめ"><a href="#%E3%81%BE%E3%81%A8%E3%82%81">まとめ</a></h2>
<p>ということで2020年に入り、Vercelは進化を遂げ続けており非常に使いやすくなりました。GitHubや自動デプロイに慣れている方はGitHub連携で、GitHubを使ったことがなくてちょっと怖い、という方は今まで通りnowコマンドを使って、柔軟に利用できます。非常に便利ですので使い倒していきましょう。</p>
だら@Crieit開発者
tag:crieit.net,2005:PublicArticle/15776
2020-03-22T17:10:52+09:00
2020-03-22T20:38:57+09:00
https://crieit.net/posts/web1week
敗者のweb1week
<p>先週行われた <a href="https://crieit.net/boards/web1week-202003">web1week</a>。<br />
初開催にも関わらず50を超えるサービスが登場し、大盛況のうちに幕を閉じた。</p>
<p>経験者ならば理解を得られると思うが、個人開発という舞台には魔物が棲んでいる。<br />
着手時点で思い描いた予定などペロリと喰われてしまう。</p>
<p>不意に削られる作業時間。<br />
理想と妥協の狭間で揺れる要件。<br />
技術仕様の落とし穴。<br />
サービスの意義を自問自答し続ける日々。<br />
リリース直後に訪れる無慈悲な障害。</p>
<p>これらを乗り越えローンチされた個人サービスの奥には、語られることのない物語が潜んでいる。<br />
そんなサービスが50以上も並ぶweb1weekのボードは圧巻で、たとえHello Worldの様なサービスでも私の心を踊らせてくれる。<br />
とても甲乙など付けられない。</p>
<p>しかし「敗者」は存在する。<br />
「web1week楽しみ!」と方方で意気込みを語り、「やってみませんか?」と様々な人を勧誘した挙句、あろうことか最小限の機能でさえ1weekに間に合わず、最終的には要件すら満たせなかった。<br />
紛うことなき「敗者」だ。<br />
個人開発界隈ではよく「リリースすることが大事」と言われている。その通りだ。リリースさえすれば勝ちなのだ。</p>
<p>これは、そんな勝負に敗北を喫した男の記録である。</p>
<h1 id="立案"><a href="#%E7%AB%8B%E6%A1%88">立案</a></h1>
<p>月曜の午前0時、お題「Home」が発表された。<br />
こじつけでも何でも「Home」を絡ませたWebサービスならクリアとのこと。<br />
初学者でも参加し易いうえに、CGMやIoTを絡めたサービスも構想でき、かつ疫病の脅威に怯える時勢に寄り添った素晴らしいお題だと思った。<br />
それと同時に頭を抱えた。<br />
1週間で作り上げるとなると出来る事は限られている。ピンと来るアイディアが何も思い浮かばない。</p>
<p>「自分が実装してみたい機能」というアプローチで考えてみる。<br />
我が家には<a target="_blank" rel="nofollow noopener" href="https://store.google.com/jp/product/google_nest_hub">Google Nest Hub</a>というガジェットが存在する。ディスプレイ付きスマートスピーカーというイロモノだ。<br />
スマート家電のような贅沢品は存在しない我が家だが、昨年の私の誕生日に妻から贈ってもらっていた。しかし、動画を見るにはタブレットやテレビに転送で事足りるし、BGMをかける風習も無い。なかなか使う場面が訪れなかった。<br />
これを何とか活用できないか。</p>
<p>玄関や冷蔵庫に貼ってるホワイトボード。これをNest Hubで代用できないかと考えた。<br />
「いってらっしゃい」「おやつにわらび餅を冷蔵庫に入れてます」の様な、家族間のメッセージは手書きの方が嬉しかったりする。<br />
しかし、ホワイトボードのデメリットはホワイトボードの場所へ行かないと書けないことだ。<br />
仕事中に「冷蔵庫にわらび餅入れてるの忘れてた!」となった場合、LINEに頼るしかない。<br />
スマホから手書きでメッセージを書き、玄関やリビングに置かれたNest Hubに表示されたらエモいのでは。</p>
<p>Nest Hubで表示するWebアプリについて調べてみる。<br />
どうやら<a target="_blank" rel="nofollow noopener" href="https://developers.google.com/assistant/interactivecanvas">Interactive Canvas</a>というフレームワークで実装するらしい。<br />
しかし、基本的にVUI(音声ユーザーインターフェース)が前提となる作りで、ストアへ並べるにはGoogleの承認も必要となる。<br />
今回の要件・納期では厳しい、、</p>
<p>方針を変える。<br />
Nest HubにはGoogle Photosにあるアルバムを選択して、フォトフレームとして表示する機能がある。<br />
Nest Hub側でメッセージ画像を入れたアルバムを選択して、Webサービス側ではそのメッセージ画像を更新すれば良いのはないだろうか。<br />
調べてみた。さすがGoogle様。ちゃんと<a target="_blank" rel="nofollow noopener" href="https://developers.google.com/photos">Google PhotosのAPI</a>も存在する。</p>
<p>これでいこう。</p>
<ol>
<li>WebサービスにGoogleアカウントでログイン</li>
<li>手書きUIのCanvasでメッセージを書く。</li>
<li>Nest Hub表示用のアルバムをGoogle Photosに作成</li>
<li>Canvasを画像化して3.のアルバムにアップロード</li>
<li>既にメッセージ画像が存在する場合は削除(アルバム内の画像は1枚のみとする)</li>
<li>Nest Hubで表示</li>
</ol>
<p>OAuth2認証をクライアントで完結できれば、DBすら不要。<br />
この程度なら1週間でいける。</p>
<p>市場調査もしてみた。</p>
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">スマートディスプレイ(ディスプレイ付きスマートスピーカー)がどれぐらい普及してるのか気になるのでアンケお願いします!持ってるか持ってないか。押し入れで眠っててもOKです!</p>— きんみ | ツイッター大喜利サイト🎍ついぎり🎍作りました🙄 (@_kinmi) <a target="_blank" rel="nofollow noopener" href="https://twitter.com/_kinmi/status/1236827457027592192?ref_src=twsrc%5Etfw">March 9, 2020</a></blockquote>
<p>うん、Nest Hub専用サービスは良い感じのスキマ産業のようだ(強がり)</p>
<p>Nuxt.jsでプロジェクトを作成して今日の作業は終了。<br />
この時点で月曜の夜。順調だ。</p>
<h1 id="裏の目的"><a href="#%E8%A3%8F%E3%81%AE%E7%9B%AE%E7%9A%84">裏の目的</a></h1>
<p>常々思っていたのが「もっと手軽に画像アップロード機能を作れないか」という事だった。<br />
画像アップロードというのは、ストレージを用意して、アップされる画像の最適化処理を組み、著作権の侵害等を考慮して規約や同意UIの設計をしなければならない。<br />
リリース後は不適切な画像が上がってないかウォッチする義務が発生するし、ストレージの残容量も気にしなければならない。<br />
更に、増え続ける画像の管理方法(一定期間で削除するか、お金で解決するか)も検討しなければならない。<br />
個人開発において、割とコストが高めの機能だ。</p>
<p>この課題はGoogle Photos等のユーザーが保有するクラウドストレージに公開状態で保存すれば解決するのではないかと考えた。<br />
このご時世、Googleアカウントは誰でも持っている。アカウント登録時に自動で付与されるGoogle Photosの容量は高画質モードなら無制限だし、あくまで「画像を公開しているのはユーザー」という体裁を繕えるので著作権問題もグレーゾーンとなる(漫画村方式)<br />
サービス側で保有するのは画像の静的URLのみだ。</p>
<p>今回、Google PhotosのAPIを使いこなせる様になれば今後の個人開発に幅を持たせることが出来るかもしれない。</p>
<h1 id="ハマる"><a href="#%E3%83%8F%E3%83%9E%E3%82%8B">ハマる</a></h1>
<p>コロナ自粛の影響により勤務時間が削減され、個人開発の時間は比較的多めに取れた。<br />
それでも間に合わなかったのは全てにハマったからだ。<br />
全てだ。<br />
時系列でハマりポイントを紹介していく。</p>
<h2 id="JS ClientSDKが使いづらい"><a href="#JS+ClientSDK%E3%81%8C%E4%BD%BF%E3%81%84%E3%81%A5%E3%82%89%E3%81%84">JS ClientSDKが使いづらい</a></h2>
<p>Google APIをWebで利用する場合、<a target="_blank" rel="nofollow noopener" href="https://github.com/google/google-api-javascript-client">Google API Client Library for JavaScript</a>(GAPI)というライブラリを利用する。<br />
Initial Commitは2011年。それなりに年季の入ったライブラリだ。</p>
<h3 id="NPMには登録されていない。"><a href="#NPM%E3%81%AB%E3%81%AF%E7%99%BB%E9%8C%B2%E3%81%95%E3%82%8C%E3%81%A6%E3%81%84%E3%81%AA%E3%81%84%E3%80%82">NPMには登録されていない。</a></h3>
<p>scriptタグで読み込む必要がある。<br />
Nuxt.jsで外部スクリプトを使用する場合、<code>$nextTicks()</code>を用いてもスクリプトのロードが完了していない場合がある。<br />
ロード状態を監視する必要があるので少し手間だ。</p>
<h3 id="DiscoveryDocs"><a href="#DiscoveryDocs">DiscoveryDocs</a></h3>
<p>GAPIのスクリプトを読み込んだ時点では<code>init()</code>等、最低限の機能しか保有していない。<br />
<a target="_blank" rel="nofollow noopener" href="https://github.com/google/google-api-javascript-client/blob/master/docs/start.md">Getting Started</a>に記載されているが、API Key等を設定して初期化する際に<code>DiscoveryDocs</code>というURLを設定する。<br />
DiscoveryDocsはAPI単位で用意されており、URLの先にあるJSONをGAPIでロードすれば機能として利用できる。<br />
Google Photos APIのDiscoveryDocsは下記となる。<br />
<a target="_blank" rel="nofollow noopener" href="https://content.googleapis.com/discovery/v1/apis/photoslibrary/v1/rest"><code>https://content.googleapis.com/discovery/v1/apis/photoslibrary/v1/rest</code></a><br />
スクリプトの肥大化を防ぐための機構だろう。<br />
ここまではまだ良い。手間を感じるがまだ許せる。</p>
<h3 id="同期処理"><a href="#%E5%90%8C%E6%9C%9F%E5%87%A6%E7%90%86">同期処理</a></h3>
<p>ドキュメントのサンプルはthenチェーンで記載されている。<br />
じゃあ・・・と思って<code>async / await</code>で書き直してみる。動かない。<br />
そう、返却値は<strong>ES6 Promiseじゃない</strong>。<br />
<a target="_blank" rel="nofollow noopener" href="https://google.github.io/closure-library/api/goog.Thenable.html">goog.Thenable</a>という独自インターフェースを継承したオブジェクトだ。<br />
<code>init().then(()=>{ })</code> の中でしかGAPIは動かない。同期的にいくつものAPIを使いたければthenのネストを深め続けるしかない。F*ck。</p>
<p>ES6が標準化されたのが2015年。このライブラリの開発当初ならスマートな仕様だったのだろう。<br />
しかし、こちとら去年からJSを学び始めた身。<br />
終始、この仕様に慣れず生産性が低下した。</p>
<h2 id="認証で躓く"><a href="#%E8%AA%8D%E8%A8%BC%E3%81%A7%E8%BA%93%E3%81%8F">認証で躓く</a></h2>
<p>ユーザー管理には<a target="_blank" rel="nofollow noopener" href="https://firebase.google.com/docs/auth?hl=ja">Firebase Authentication</a>を採用した。<br />
手軽に導入でき、「特定のユーザーには使わせない」といったセキュリティ対策も出来る。<br />
Twitter API等、他社のAPIと連携する場合はトークンの保管といった一手間が必要になってくるが、今回はGoogle Platform内で連携するだけ。<br />
簡単だろうと思った。</p>
<h3 id="🙅 Firebase → Google API"><a href="#%F0%9F%99%85+Firebase+%E2%86%92+Google+API">🙅 Firebase → Google API</a></h3>
<p>以下の記事を参考に、Firebase UIでログイン→GAPIのイニシャライズを試みる。<br />
<a target="_blank" rel="nofollow noopener" href="https://medium.com/google-cloud/using-google-apis-with-firebase-auth-and-firebase-ui-on-the-web-46e6189cf571">Using Google APIs with Firebase Auth and Firebase UI on the Web</a></p>
<p><strong>できない。</strong><br />
この記事ではFirebaseでログインした時点で <em>GAPIの認証状態</em> : <code>gapi.auth2.getAuthInstance().isSignedIn.get()</code>が <code>true</code> になる想定だが <code>false</code> だ。<br />
GAPIでもログインする必要があるのか?と思って試したがFirebaseとGAPIで二重ログインされてしまった。</p>
<p>記事のコメント欄に<a target="_blank" rel="nofollow noopener" href="https://medium.com/@slashdrew/this-appears-to-be-no-longer-functional-cc2ca49db640">記載がされている</a>が、現在はFirebaseでログインしてもGAPIでログイン状態を検知しない。<br />
下記のissueにもその事が記載されており、ステータスはopenのままだ。<br />
<a target="_blank" rel="nofollow noopener" href="https://github.com/google/google-api-javascript-client/issues/561">https://github.com/google/google-api-javascript-client/issues/561</a></p>
<h3 id="🙆 Google API → Firebase"><a href="#%F0%9F%99%86+Google+API+%E2%86%92+Firebase">🙆 Google API → Firebase</a></h3>
<p>つまり現状、FirebaseとGAPIを連携するには認証順を逆にしないといけない。<br />
<em>GoogleAPIのサインイン</em> : <code>gapi.auth2.signIn()</code>でログインした後、認証情報を取得して <em>Firebaseの認証情報を使用したサインイン</em> : <code>firebase.auth().signInWithCredential()</code>でもログインするフローとなる。<br />
下記の記事を参考に実装した。<br />
<a target="_blank" rel="nofollow noopener" href="https://fireship.io/snippets/how-to-use-google-apis-or-gapi-with-firebase-auth/">How to Use Google APIs on the Web</a></p>
<h3 id="進捗:だめです"><a href="#%E9%80%B2%E6%8D%97%EF%BC%9A%E3%81%A0%E3%82%81%E3%81%A7%E3%81%99">進捗:だめです</a></h3>
<p>この時点で既に平日は終わっていた。残すところは土日のみ。<br />
対応が長期化した要因は「出来ると書いてあったから」。<br />
Firebase→GAPIの認証は不可能という可能性を考慮していなかった。<br />
この考慮漏れが発生するとプログラマーの思考はどうなるか。<br />
<strong>ひたすらタイポを探し続ける</strong>のである。</p>
<h2 id="画像アップロードが用意されてない"><a href="#%E7%94%BB%E5%83%8F%E3%82%A2%E3%83%83%E3%83%97%E3%83%AD%E3%83%BC%E3%83%89%E3%81%8C%E7%94%A8%E6%84%8F%E3%81%95%E3%82%8C%E3%81%A6%E3%81%AA%E3%81%84">画像アップロードが用意されてない</a></h2>
<p>まだ間に合う。<br />
土曜にアップロード機能を実装。<br />
日曜にデザインを微修正&実機で動作確認。<br />
ここまで盛大に躓いて尚、根拠なく「出来る」と盲信する。<br />
進捗が遅れてるプログラマーにありがちな<strong>逆算スケジュール</strong>である。</p>
<h3 id="🙅 GAPIで画像アップロード"><a href="#%F0%9F%99%85+GAPI%E3%81%A7%E7%94%BB%E5%83%8F%E3%82%A2%E3%83%83%E3%83%97%E3%83%AD%E3%83%BC%E3%83%89">🙅 GAPIで画像アップロード</a></h3>
<p>ガイドラインを読んでみる。<br />
<a target="_blank" rel="nofollow noopener" href="https://developers.google.com/photos/library/guides/upload-media">https://developers.google.com/photos/library/guides/upload-media</a><br />
どうやら画像をアップロードするには、二段階の手順を踏まないといけないようだ。</p>
<ol>
<li>画像のバイナリデータをGoogleにアップロードする</li>
<li>上記返却値に含まれるアップロードトークンを用いてアルバムに追加する。</li>
</ol>
<p>Google Photosの<a target="_blank" rel="nofollow noopener" href="https://content.googleapis.com/discovery/v1/apis/photoslibrary/v1/rest">DiscoveryDocs</a>から該当のメソッドを探してみる。<br />
<strong>無い。</strong><br />
何度検索しても「uploads」という処理は無い。<br />
「なぜ無いのか」なんて考える余裕も無い。<br />
(現在、不安になって再度探してみたがやっぱり無い)</p>
<p>GAPIじゃGoogleに画像をアップロード出来ない・・・?</p>
<h3 id="🙅 axiosでリクエスト"><a href="#%F0%9F%99%85+axios%E3%81%A7%E3%83%AA%E3%82%AF%E3%82%A8%E3%82%B9%E3%83%88">🙅 axiosでリクエスト</a></h3>
<p>悩んでいる暇は無い。もうクライアントSDKは捨てる。<br />
ドキュメントにはエンドポイントが記載されているので、そこに対して<code>axios</code>でPOSTリクエストを投げてみる。</p>
<p><strong>Network Error.</strong></p>
<p>・・・久し振りに見たぜ。<br />
<code>has been blocked by CORS policy: No 'Access-Control-Allow-Origin'</code><br />
<a target="_blank" rel="nofollow noopener" href="https://developer.mozilla.org/ja/docs/Web/HTTP/CORS/Errors">CORSエラー</a>だ。<br />
おいおい、嘘だろ。CORSだと?<br />
浅学だが、基本的にはAPI側での対応が必須だったと記憶している。詰んだか?</p>
<h3 id="🙅 XMLHttpRequestでリクエスト"><a href="#%F0%9F%99%85+XMLHttpRequest%E3%81%A7%E3%83%AA%E3%82%AF%E3%82%A8%E3%82%B9%E3%83%88">🙅 XMLHttpRequestでリクエスト</a></h3>
<p>ドキュメントには「CORSもサポートしているよ」と書かれている。<br />
<a target="_blank" rel="nofollow noopener" href="https://github.com/google/google-api-javascript-client/blob/master/docs/cors.md">How to use CORS to access Google APIs</a><br />
クライアントSDKを用いれば回避できるよ。と。<br />
<strong>使えねぇんだよチクショウが。</strong><br />
記事の最後に<a target="_blank" rel="nofollow noopener" href="https://developer.mozilla.org/ja/docs/Web/API/XMLHttpRequest"><code>XMLHttpRequest</code></a>を用いた例も記載されている。<br />
存在は知っていたが初めて使うなこれ。<br />
愚直に書いてある通り組んでみる。</p>
<p><code>has been blocked by CORS policy: No 'Access-Control-Allow-Origin'</code></p>
<p>泣く。</p>
<h3 id="🙅 gapi.client.request()"><a href="#%F0%9F%99%85+gapi.client.request%28%29">🙅 gapi.client.request()</a></h3>
<p>まだだ。諦めない。<br />
俺は長男だから我慢できたけど次男だったら我慢できなかった。</p>
<p>ドキュメントを漁り、もう1つの可能性を見つける。<br />
クライアントSDKには標準で <em>APIを叩く為のメソッド</em> : <a target="_blank" rel="nofollow noopener" href="https://github.com/google/google-api-javascript-client/blob/master/docs/reference.md#api-requests">gapi.client.request()</a>が備わっている。<br />
GoogleAPIのエンドポイントをベタ書きして引数に渡せば叩けるらしい。<br />
クライアントSDKならCORSを回避できるんだろ?信じるぞ?<br />
叩いてみる。</p>
<p><strong>404</strong></p>
<p>???<br />
タイポ探しフェーズ(数時間)に入るも、やっぱり問題は無い。<br />
リクエスト内容を確認した。POST先のURLが変わっている。<br />
画像アップロードのエンドポイントは<code>https://photoslibrary.googleapis.com/v1/uploads</code>だが、ドメイン部が<code>content.googleapis.com</code>に変わってる。<br />
なんだこれは。<br />
恐らく、クライアントSDKが書き換えていると推測。M*ther F*cker。</p>
<p>諦めた。</p>
<p>クライアントで完結することを。</p>
<h3 id="🙆 Netlify Functions"><a href="#%F0%9F%99%86+Netlify+Functions">🙆 Netlify Functions</a></h3>
<p>ホスト先にNetlifyを利用している。<br />
そこでクラウド関数を作れるNetlify Functionsを使ってみようと思い立った。<br />
使った経験が無い機能だ。下記の記事を参考にHello Worldから始めてみる。<br />
<a target="_blank" rel="nofollow noopener" href="https://qiita.com/Sr_Bangs/items/7867853f5e71bd4ada56">【入門】Netlify Functionsコトハジメ</a><br />
そして「Googleに画像をアップロードしてアップロードトークン返すクラウド関数」を立ててみた。<br />
うまくいった。<br />
初めて使う機能なのに、ハマらず動く。ローカル環境もすぐ出来た。<br />
感動した。モダン最高。</p>
<h3 id="進捗:納期は昨日なので大丈夫です"><a href="#%E9%80%B2%E6%8D%97%EF%BC%9A%E7%B4%8D%E6%9C%9F%E3%81%AF%E6%98%A8%E6%97%A5%E3%81%AA%E3%81%AE%E3%81%A7%E5%A4%A7%E4%B8%88%E5%A4%AB%E3%81%A7%E3%81%99">進捗:納期は昨日なので大丈夫です</a></h3>
<p>この時点で日曜の夜。<br />
軸となる機能は一通り完成したものの、「同名アルバムが存在していた場合」などのシチュエーションに応じた分岐は作れてないし、要件として「既にメッセージ画像が存在していた場合は更新する」といった機能も存在するが未実装だ。実機確認もやれてない。</p>
<p>「ごめんなさい」しつつ、月曜に出そう。<br />
そう決めて床についた。</p>
<h2 id="画像の削除/更新はできない"><a href="#%E7%94%BB%E5%83%8F%E3%81%AE%E5%89%8A%E9%99%A4%2F%E6%9B%B4%E6%96%B0%E3%81%AF%E3%81%A7%E3%81%8D%E3%81%AA%E3%81%84">画像の削除/更新はできない</a></h2>
<p>ベットに入った直後、悪寒がした。</p>
<p><strong>見覚えがない。</strong></p>
<p>散々GoogleAPIのドキュメントと格闘し続けたが、Google Photos内の写真を<strong>削除/更新するエンドポイントは見た記憶がない</strong>。<br />
スマホを取り出し確認してみるも、やはり無い。</p>
<p>冗談だろ?クラウドストレージを操作するAPIのくせに削除/更新は出来ない?そんな馬鹿な話があるか。<br />
調べてみる。<br />
<a target="_blank" rel="nofollow noopener" href="https://issuetracker.google.com/issues/109759781">https://issuetracker.google.com/issues/109759781</a><br />
結論:<strong>出来ないらしい</strong></p>
<p>詰みです。お疲れ様でした。</p>
<p>過去画像を含めてランダム表示されるなんてホワイトボードじゃない。</p>
<p>「お手数ですがGoogle Photosを開いて、ご自身で過去の画像を削除してください。」<br />
しょーもな。<br />
サービスとして成立していない。</p>
<p>・・・いや、まだだ。</p>
<p>よし、譲ろう。</p>
<p>百歩譲ろう。</p>
<p>Google Driveなら削除/更新もAPIで可能じゃなかろうか。<br />
Photosなんかより歴史あるクラウドストレージだ。<br />
Google Driveへアップした写真をNest Hubで表示できれば問題無い。</p>
<p><a target="_blank" rel="nofollow noopener" href="https://support.google.com/googlenest/thread/671055">Can I connect my Google Drive to my Google home hub? </a><br />
結論:<strong>出来ないらしい</strong></p>
<p>こうして私は、web1weekの敗者となった。</p>
<h2 id="Safari対応(おまけ)"><a href="#Safari%E5%AF%BE%E5%BF%9C%EF%BC%88%E3%81%8A%E3%81%BE%E3%81%91%EF%BC%89">Safari対応(おまけ)</a></h2>
<p>負け犬でも遠吠えぐらいは出来る。<br />
「手書き画像をGoogle Photosへアップするサービス」としてローンチしよう。<br />
そう決めて実機確認に入った。</p>
<h3 id="CORSぞ"><a href="#CORS%E3%81%9E">CORSぞ</a></h3>
<p>MacのChrome、Android Chromeは問題なく動作完了。<br />
妻に「iPhoneでこれ使えるか確認してほしい」と依頼する。<br />
返信は「画面が驚きの白さ」。<br />
ここで初めてMacのSafariでも動作確認してみる。</p>
<p><code>has been blocked by CORS policy: No 'Access-Control-Allow-Origin'</code></p>
<p><strong>CORSぞ</strong>。</p>
<p>何度立ちはだかれば気が済むのだCORSよ。<br />
まぁ思い当たる節はあった。<br />
GAPIが使い辛いので、下記のようにクラアントSDKを動的に読み込んでplugin化していたのだ。</p>
<pre><code class="javascript">export default async ({ $axios, store }, inject) => {
// GAPIの読み込み
const gapiScript = document.createElement('script')
const src = await $axios.$get('https://apis.google.com/js/api.js')
gapiScript.appendChild(document.createTextNode(src))
document.head.appendChild(gapiScript)
const gapi = window.gapi
// ロード処理(Promise化)
const clientLoad = new Promise((resolve, reject) => {
gapi.load('client:auth2', () => {
resolve()
})
})
// GAPI初期化処理
const init = () => {
return clientLoad.then(() => {
return gapi.client.init({
apiKey: authConfig.Google.apiKey,
clientId: authConfig.Google.clientId,
discoveryDocs: authConfig.Google.discoveryDocs,
scope: authConfig.Google.scopes.join(' ')
})
})
}
inject('gapi', gapi)
inject('gapiInit', init)
}
</code></pre>
<p>不安はあったが、動いたのでそのままにしていた。<br />
外部スクリプトを動的に読み込む場合、普通はCORSの問題が発生する。<br />
素直にscriptタグで読み込むように変更した。</p>
<p>というか、なぜChromeでは読み込めたのか分からない。<br />
同じGoogle製品だから?<br />
プラットフォーマー恐るべし。</p>
<h3 id="クロスサイトトラッキング"><a href="#%E3%82%AF%E3%83%AD%E3%82%B9%E3%82%B5%E3%82%A4%E3%83%88%E3%83%88%E3%83%A9%E3%83%83%E3%82%AD%E3%83%B3%E3%82%B0">クロスサイトトラッキング</a></h3>
<p>さて、初期表示までは問題なくSafariで動作した。<br />
しかしGoogleへログインしても認証状態を検知しない。<br />
プログラムが「ログイン中」と判断しないのだ。<br />
こればっかりは思い当たる節も無い。</p>
<p>issueが上がっていた。<br />
<a target="_blank" rel="nofollow noopener" href="https://github.com/google/google-api-javascript-client/issues/503">gapi auth2 issue on safari</a><br />
同様の事象、かつオープンのままだ。<br />
Safariの場合、Cookieを全削除するか、プライベートブラウズなら問題無いらしい。<br />
マジかよ・・・<br />
頑張れば何か対策できるのかもしれないが、負け犬にそんな根性は無い。<br />
UserAgentから「Safari、もしくはiPhone/iPad」を判定して、該当する場合は長ったらしい注意文言を表示するようにした。</p>
<h1 id="敗因"><a href="#%E6%95%97%E5%9B%A0">敗因</a></h1>
<p>次回以降のweb1weekで勝つための敗因分析を行う。</p>
<h2 id="PoC(概念実証)不足"><a href="#PoC%EF%BC%88%E6%A6%82%E5%BF%B5%E5%AE%9F%E8%A8%BC%EF%BC%89%E4%B8%8D%E8%B6%B3">PoC(概念実証)不足</a></h2>
<p>一番の原因はこれ。<br />
事前に「画像の削除/更新は出来ない」と知っていれば別案を採用していた。<br />
とはいえ、1週間という過密スケジュールで検証工程を取れるかは疑問が残る。<br />
少なくとも、</p>
<ol>
<li>機能の洗い出し</li>
<li>実装に必要な外部インターフェースのドキュメントはちゃんと読む</li>
</ol>
<p>まぁ、、当たり前のことはちゃんとしよう。ということ。</p>
<h2 id="「出来ない」の判断が遅い"><a href="#%E3%80%8C%E5%87%BA%E6%9D%A5%E3%81%AA%E3%81%84%E3%80%8D%E3%81%AE%E5%88%A4%E6%96%AD%E3%81%8C%E9%81%85%E3%81%84">「出来ない」の判断が遅い</a></h2>
<p>認証や画像アップロード等、ハマった際に「出来ない」という判断が出来ていない。<br />
昨今の潤沢な開発環境に甘え、「出来ない事は無い」と思い込んでいる節がある。<br />
確かに大抵の場合、ハマる原因は外部インターフェースの仕様変更、もしくはタイポが多い。<br />
塩梅が難しいが、もっと早いタイミングでissueを探そう。</p>
<h1 id="Google Photosを利用した画像アップローダーは作れるのか"><a href="#Google+Photos%E3%82%92%E5%88%A9%E7%94%A8%E3%81%97%E3%81%9F%E7%94%BB%E5%83%8F%E3%82%A2%E3%83%83%E3%83%97%E3%83%AD%E3%83%BC%E3%83%80%E3%83%BC%E3%81%AF%E4%BD%9C%E3%82%8C%E3%82%8B%E3%81%AE%E3%81%8B">Google Photosを利用した画像アップローダーは作れるのか</a></h1>
<p>冒頭に記した「裏の目的」について。<br />
そもそもGoogle Photos APIの<a target="_blank" rel="nofollow noopener" href="https://developers.google.com/photos/library/guides/acceptable-use">利用規約</a>には「ホスティングサービスとして使うならCloud Storageを使え」と明記してある。<br />
開発するサービスを「Google Photosへアップした写真の共有ギャラリー」という位置付けにするならグレーかな?と思ったが、<br />
結論から言うと「多分、不可」だ。<br />
少なくとも、そう言うサービスを作れるだけのAPIが提供されていない。</p>
<p>まず前述の通り、API経由での画像の削除/更新は出来ない。<br />
あくまで画像のアップロードのみで、アルバム間の移動すら出来ない。</p>
<p>そして、更に致命的なことに<strong>静的なURLは取得できない</strong>。<br />
写真のURLを取得するAPIは存在するが、一定期間が過ぎると無効化されるらしい。<br />
これはGoogle Photosの「URLで共有する」と同じURLなのだろう。<br />
URL先には静的な画像が埋め込まれているらしいが、それを取得するAPIは存在しない。<br />
スクレイピングを駆使すれば取得できるかもしれないが、実用的では無いだろう。</p>
<p>「ユーザーのプライベートストレージを用いた画像アップローダー」は別サービスを検討した方が良さそうだ。</p>
<h1 id="ゴミを公開する"><a href="#%E3%82%B4%E3%83%9F%E3%82%92%E5%85%AC%E9%96%8B%E3%81%99%E3%82%8B">ゴミを公開する</a></h1>
<p>これはweb1weekじゃない。<br />
私が作りたかったものでもない。<br />
ゴミだ。</p>
<p><a target="_blank" rel="nofollow noopener" href="https://nest-board.netlify.com/">Nest Board</a><br />
<a target="_blank" rel="nofollow noopener" href="https://nest-board.netlify.com/">https://nest-board.netlify.com/</a></p>
<p>Google APIは申請無しで使用できるが、Googleから承認されていない場合、ログイン時に警告画面が出る。<br />
利用する際は注意文言を読んで頂きたい。</p>
<p>需要あるか分からないが、Nest Hub側で表示する際の手順も明記しておく。</p>
<blockquote>
<p><strong>1. スマホ(iPhone/Android)でGoogle Homeアプリを開く</strong><br />
Nest Hubの初期設定に必要なアプリなのでインストールされているはず。<br />
<br />
<strong>2. 表示したいNest Hub > フォトフレームを編集 > Google フォト の順で選択</strong></p>
<p><strong>3. Nest Boardで作成したアルバムを選択する</strong><br />
初期値は「Nest Board」<br />
<br />
<strong>4. 「フォトフレーム」画面の下部にある「個人的な写真の整理」を「リアルタイム共有アルバムのみ」にする</strong><br />
これを設定しないとNest Hubがフォトフレームに不向きな写真と判定して表示されない。<br />
ハマりポイント。</p>
</blockquote>
<p>以上の手順を踏めば、Nest Hubに手書きのメッセージ画像が表示される。</p>
<p><a href="https://crieit.now.sh/upload_images/e5d58ba13a25c14fac70e13548eff8d95e770af16b1cc.jpg" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/e5d58ba13a25c14fac70e13548eff8d95e770af16b1cc.jpg?mw=700" alt="敗北者じゃけぇ" /></a></p>
<p>普段はソースコードを公開したりしないのだが、今回はリポジトリをpublicにした。<br />
なぜなら敗者だからだ。<br />
<a target="_blank" rel="nofollow noopener" href="https://github.com/kin-mi/nest-board">https://github.com/kin-mi/nest-board</a></p>
<h1 id="おわり"><a href="#%E3%81%8A%E3%82%8F%E3%82%8A">おわり</a></h1>
<p>次は勝つ。</p>
きんみ
tag:crieit.net,2005:PublicArticle/15775
2020-03-20T13:51:56+09:00
2020-03-22T06:54:01+09:00
https://crieit.net/posts/Nuxt-5-Firebase-GAE-Netlify-Heroku
Nuxtアプリを無料で公開するときに試した5つの環境まとめ(Firebase/GAE/Netlify/Heroku)
<p>最近Nuxtでいろいろ作っているけど、無料で使える環境をいろいろ試してる。<br />
いろいろメリデメあるけど、SPAならNetlify/SSRならHerokuがよさそう。<br />
いままで試したものをまとめてみた。</p>
<h3 id="ほしかったもの"><a href="#%E3%81%BB%E3%81%97%E3%81%8B%E3%81%A3%E3%81%9F%E3%82%82%E3%81%AE">ほしかったもの</a></h3>
<p>主に開発してるのが<a target="_blank" rel="nofollow noopener" href="https://tsundoku.site">CGM系のWebサービス</a>なので、</p>
<ol>
<li>動的なOGP画像などが設定できる(OGP芸)</li>
<li>カスタムドメインが使える</li>
<li>日次のランキング集計などの定期実行ができる</li>
</ol>
<p>が、無料でできて、なるべく実装が楽で、そこまで遅くないのがうれしい。</p>
<h3 id="試した5つのパターン"><a href="#%E8%A9%A6%E3%81%97%E3%81%9F5%E3%81%A4%E3%81%AE%E3%83%91%E3%82%BF%E3%83%BC%E3%83%B3">試した5つのパターン</a></h3>
<p>試したのは以下の5パターン。試してみた順で記載。</p>
<ol>
<li>Nuxt(SSR) + Cloud Function<br />
起動がかなり遅かった。。実装も大変なのでNG</li>
<li>Nuxt(SPA) + Firebase Hosting<br />
構築はかなり楽。ただ、OGP芸が大変でFunctionsが必要</li>
<li>Nuxt(SPA) + Netlify<br />
プレレンダリングでOGP芸が楽。定期実行はFunctionsでできる</li>
<li>Nuxt(SSR) + GAE(f1:256M)<br />
メモリの制限きつく、たまに落ちる。。定期実行はcron.yamlでできる</li>
<li>Nuxt(SSR) + Heroku(free:512M) + Cloudflare<br />
メモリ多くてよい。SSLはないのでCloudflareを併用。定期実行はHeroku Scheduler</li>
</ol>
<p>SPAで十分であれば、「3.Nuxt(SPA) + Netlify」が構築も簡単で良かった。<br />
<a target="_blank" rel="nofollow noopener" href="https://tsundoku.site">積読ハウマッチ</a>は、現在この構成。</p>
<p>SSRの場合、「5. Nuxt(SSR) + Heroku(free:512M) + CloudFlare」が良い感じ。<br />
<a target="_blank" rel="nofollow noopener" href="https://hen-ai.net/">へんあいマップ</a>がこの構成。</p>
<h3 id="各パターンについて"><a href="#%E5%90%84%E3%83%91%E3%82%BF%E3%83%BC%E3%83%B3%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6">各パターンについて</a></h3>
<h4 id="1. Nuxt(SSR) + Cloud Function"><a href="#1.+Nuxt%28SSR%29+%2B+Cloud+Function">1. Nuxt(SSR) + Cloud Function</a></h4>
<p>一番はじめに<a target="_blank" rel="nofollow noopener" href="https://tsundoku.site">積読ハウマッチ</a>でやろうとした構成。</p>
<p><img width="870" alt="スクリーンショット 2020-03-20 11.43.40.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/7fa5a956-cec1-9117-1d5d-f769eae53a32.png"></p>
<p>細かいやり方は、以下の記事の「Firebase Cloud Functionsの設定してSSRできるようにする」あたりに。<br />
・<a target="_blank" rel="nofollow noopener" href="https://www.memory-lovers.blog/entry/2019/04/02/115149">Nuxt.jsではじめるときのやることリスト(SSRも国際化も自動デプロイも) - くらげになりたい。</a></p>
<p>当時は動的ページのSEO対応するには、この方法が多く検索に出ていたので試したけど、<br />
Cloud Functionのコールドスタートがつらすぎてお蔵入り。。SPAでいくことに。。</p>
<h4 id="2. Nuxt(SPA) + Firebase Hosting"><a href="#2.+Nuxt%28SPA%29+%2B+Firebase+Hosting">2. Nuxt(SPA) + Firebase Hosting</a></h4>
<p><a target="_blank" rel="nofollow noopener" href="https://tsundoku.site">積読ハウマッチ</a>をリリースしたときの構成。</p>
<p><img width="910" alt="スクリーンショット 2020-03-20 11.37.12.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/37459683-872a-5047-a383-71c33f0c640e.png"></p>
<p>課題だった動的ページのSEOは、Cloud Functionsを経由して返す方法で対応。<br />
こちらも当時よく検索に出てたけど、実装が大変そうだったので2番手に。</p>
<p>細かいやり方は、以下の記事に。(2020/03/22追記: OGP画像生成系の記事を追加)<br />
・<a target="_blank" rel="nofollow noopener" href="https://www.memory-lovers.blog/entry/2019/08/07/150000">Nuxt(SPA)+FirebaseでSEO!OGP!: 特定のパスだけheadだけ返すやつ - くらげになりたい。</a><br />
・<a target="_blank" rel="nofollow noopener" href="https://qiita.com/kira_puka/items/8c1d1240c3aa200cbec0">Nuxt.js(SPA)+Firebaseで積読用の読書管理サービスを作ってみたときにハマったこと - Qiita</a><br />
・<a target="_blank" rel="nofollow noopener" href="https://www.memory-lovers.blog/entry/2019/06/26/194500">Cloud Functions + ImageMagickでOPG画像の動的生成してCloud Storageにアップロードする - くらげになりたい。</a><br />
・<a target="_blank" rel="nofollow noopener" href="https://www.memory-lovers.blog/entry/2020/02/19/150000">Cloud FunctionとSVGでOGP画像生成を試行錯誤したまとめ - くらげになりたい。</a></p>
<p>カスタムドメインについては、Firebase HostingでSSLと合わせて無料で設定可能。</p>
<p>定期実行は、Cloud Functionsでできるのでそれを利用した。<br />
・<a target="_blank" rel="nofollow noopener" href="https://firebase.google.com/docs/functions/schedule-functions?hl=ja">関数のスケジュール設定 | Firebase</a><br />
・<a target="_blank" rel="nofollow noopener" href="https://qiita.com/kira_puka/items/1f164dd8d1a5a281d9c1">Cloud Functions for Firebaseのcronみたいな定期実行を試したら簡単だった - Qiita</a></p>
<p>ただ、Cloud Functionsの定期実行は、<br />
「<strong>Googleアカウントごとに3つのジョブ</strong>を無料で使用できる」<br />
なので、注意が必要。<strong>プロジェクトごとじゃない</strong>。。</p>
<h4 id="3. Nuxt(SPA) + Netlify"><a href="#3.+Nuxt%28SPA%29+%2B+Netlify">3. Nuxt(SPA) + Netlify</a></h4>
<p><a target="_blank" rel="nofollow noopener" href="https://tsundoku.site">積読ハウマッチ</a>の現在の構成。</p>
<p><img width="775" alt="スクリーンショット 2020-03-20 11.47.02.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/5010c13a-98a9-c9e0-ae3b-767d18cd8006.png"></p>
<p><a target="_blank" rel="nofollow noopener" href="https://docs.netlify.com/site-deploys/post-processing/prerendering/#set-up-prerendering">NetlifyのPrerendering</a>が無料化されて使えるようになり、よいという話を聞くように。。</p>
<p>実装が複雑で、変更もしにくかったので、この方法に。<br />
Netlify自体がCDNも提供していているので、すこしはやくなった(気がする)</p>
<p>カスタムドメインも、NetlifyのDNSを設定して無料で対応できる。</p>
<p>定期実行は、2.と同じ感じで、Cloud Functionsのまま。</p>
<p>コード量も減って変更もしやすくなったので、OGP画像の改善とかが楽にできるように。。(<em>´ω`</em>)</p>
<h4 id="4. Nuxt(SSR) + GAE(f1:256M)"><a href="#4.+Nuxt%28SSR%29+%2B+GAE%28f1%3A256M%29">4. Nuxt(SSR) + GAE(f1:256M)</a></h4>
<p>SPAの課題として、初期表示が遅いのでなんとかしたいなと、SSRの環境を模索しはじめ。。<br />
公開していないけど、1つ作ってみた。</p>
<p><img width="798" alt="スクリーンショット 2020-03-20 13.17.06.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/c24ce0b2-64b3-1ea4-9bd2-e2150ba13d14.png"></p>
<p>試してみたところ、起動するだけでメモリ上限すれすれで、複数アクセスがあったりすると、落ちる場合も。。<br />
(ts-nodeで動かしているのも悪い気がしている。。)</p>
<p><img width="561" alt="スクリーンショット 2020-03-20 12.22.00.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/e3940627-4141-4c17-3c82-b2ee13ca1e14.png"></p>
<p>ただ、定期実行はcron.yamlを用意すればURLに送信でき、無料範囲も広い。<br />
料金も「<a target="_blank" rel="nofollow noopener" href="https://cloud.google.com/appengine/pricing?hl=ja#%E3%81%9D%E3%81%AE%E4%BB%96%E3%81%AE%E3%83%AA%E3%82%BD%E3%83%BC%E3%82%B9">その他のリソース</a>」をみるとcronは無料のよう。<br />
・<a target="_blank" rel="nofollow noopener" href="https://cloud.google.com/appengine/docs/flexible/nodejs/scheduling-jobs-with-cron-yaml?hl=ja">cron.yaml を使用したジョブのスケジューリング</a></p>
<p>カスタムドメインやSSLも無料で利用できる感じ。(試してないので未確認)</p>
<h4 id="5. Nuxt(SSR) + Heroku(free:512M) + Cloudflare"><a href="#5.+Nuxt%28SSR%29+%2B+Heroku%28free%3A512M%29+%2B+Cloudflare">5. Nuxt(SSR) + Heroku(free:512M) + Cloudflare</a></h4>
<p>GAEでうまくいかなかったので、無料でメモリも多いHerokuを利用。<br />
<a target="_blank" rel="nofollow noopener" href="https://hen-ai.net/">へんあいマップ</a>がこの構成で稼働中。</p>
<p><img width="885" alt="スクリーンショット 2020-03-20 13.24.02.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/60f2ab5c-3f4a-9b4b-ac45-341ed99c4667.png"></p>
<p>定期実行は無料のアドオン(<a target="_blank" rel="nofollow noopener" href="https://elements.heroku.com/addons/scheduler">Heroku Scheduler</a>)が利用できる。<br />
Nuxt側で<a target="_blank" rel="nofollow noopener" href="https://ja.nuxtjs.org/api/configuration-servermiddleware/">serverMiddleware</a>を用意すればOK。</p>
<p>ただ、メモリは多く定期実行もが、Herokuの無料枠だと制限も多い。</p>
<ol>
<li>SSLは非対応</li>
<li>30分アクセスがないとスリープする</li>
<li>クレジットカードの登録で1000時間/月xアカウント<br />
(未認証だと550時間/月xアカウント)</li>
</ol>
<h5 id="1. SSLは非対応"><a href="#1.+SSL%E3%81%AF%E9%9D%9E%E5%AF%BE%E5%BF%9C">1. SSLは非対応</a></h5>
<p>単体だけだとSSLに対応していないので、<a target="_blank" rel="nofollow noopener" href="https://www.cloudflare.com/ja-jp/">Cloudflare</a>を併用。<br />
CloudflareがCDNも提供してくれるので良い感じに。</p>
<h5 id="2. 30分アクセスがないとスリープする"><a href="#2.+30%E5%88%86%E3%82%A2%E3%82%AF%E3%82%BB%E3%82%B9%E3%81%8C%E3%81%AA%E3%81%84%E3%81%A8%E3%82%B9%E3%83%AA%E3%83%BC%E3%83%97%E3%81%99%E3%82%8B">2. 30分アクセスがないとスリープする</a></h5>
<p>これは、Heroku Schedulerを使えばOK。<br />
任意のURLを叩けるため、15分毎などスリープしないようにしておく。</p>
<h5 id="3. 無料枠は1000時間/月xアカウント"><a href="#3.+%E7%84%A1%E6%96%99%E6%9E%A0%E3%81%AF1000%E6%99%82%E9%96%93%2F%E6%9C%88x%E3%82%A2%E3%82%AB%E3%82%A6%E3%83%B3%E3%83%88">3. 無料枠は1000時間/月xアカウント</a></h5>
<p>1000時間あれば、24時間x31日でも744時間なので、大丈夫な感じ。<br />
ただ、アカウント単位での無料枠なので、複数アプリを無料で稼働はできない。。</p>
<h5 id="実際のやりかたとかは、"><a href="#%E5%AE%9F%E9%9A%9B%E3%81%AE%E3%82%84%E3%82%8A%E3%81%8B%E3%81%9F%E3%81%A8%E3%81%8B%E3%81%AF%E3%80%81">実際のやりかたとかは、</a></h5>
<p>以下の記事に。<br />
・<a target="_blank" rel="nofollow noopener" href="https://www.memory-lovers.blog/entry/2020/01/25/120000">Heroku+CloudflareなNuxtでSSRしてみる - くらげになりたい。</a></p>
<h3 id="まとめ"><a href="#%E3%81%BE%E3%81%A8%E3%82%81">まとめ</a></h3>
<p>いろいろ試してみたけど、Nuxt.jsを無料で公開するときは、</p>
<ul>
<li>SPAならNetlify
<ul>
<li>prerenderingで動的ページのSEOも対応</li>
<li>カスタムドメインやSSLもNetlifyでOK</li>
<li>定期実行はCloud Functionsで。</li>
<li>ただし、定期実行は1アカウントにつき、3ジョブまで</li>
</ul></li>
<li>SSRならHeroku
<ul>
<li>SSRで動的ページのSEOも対応</li>
<li>定期実行はHeroku Schedulerで</li>
<li>Cloudflareを併用し、SSL対応</li>
<li>無料枠は1アカウントにつき、1アプリが限界かも</li>
</ul></li>
</ul>
<p>SSRの方は他に<a target="_blank" rel="nofollow noopener" href="https://cloud.google.com/run?hl=ja">Cloud Run</a>や<a target="_blank" rel="nofollow noopener" href="https://zeit.co/">ZEIT now</a>がある。<br />
Cloud Runは立ち上げがGAEより遅いときので後手だけど、<br />
ZEIT nowは気になってるので試してみたい(<em>´ω`</em>)</p>
<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>
<h3 id="へんあいマップもリリースしました!"><a href="#%E3%81%B8%E3%82%93%E3%81%82%E3%81%84%E3%83%9E%E3%83%83%E3%83%97%E3%82%82%E3%83%AA%E3%83%AA%E3%83%BC%E3%82%B9%E3%81%97%E3%81%BE%E3%81%97%E3%81%9F%EF%BC%81">へんあいマップもリリースしました!</a></h3>
<p>「偏愛マップ」を簡単に作れるWebアプリです!<br />
<a target="_blank" rel="nofollow noopener" href="https://hen-ai.net">へんあいマップ</a>も、Nuxt.js+Firebaseで開発してます!</p>
<p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/63686f5c-390b-0b09-3f9a-0ce6676c3cce.png" width="600"/></p>
<p>偏愛マップは人見知りや口下手な人にも優しいコミュニケーションツールで、<br />
勉強会、懇親会、オフ会などの余興・アイスブレイクや自分のプロフィールにも!</p>
<p>よかったら遊んでみてください(<em>´ω`</em>)</p>
きらぷか@積読ハウマッチ/SSSAPIなど
tag:crieit.net,2005:PublicArticle/15749
2020-03-06T09:34:49+09:00
2020-03-07T09:31:54+09:00
https://crieit.net/posts/netlify-redirect-nuxt
nuxt.jsプロジェクトをnetlifyで公開してredirectを使うときの罠
<hr />
<p>3/6 11:15追記</p>
<p>サブディレクトリにあるときだけ発生する問題ぽい。問題をまとめてサポートに報告しました https://community.netlify.com/t/subdirectory-and-publish-directory-redirection/10120</p>
<hr />
<p>netlifyにはnetlity.tomlという設定ファイルを書くといろいろ設定ができることになっていて、従来nginxでやっていたような各種リダイレクトも、ここに書くことで実行してくれることになっている。<br />
<a target="_blank" rel="nofollow noopener" href="https://docs.netlify.com/configure-builds/file-based-configuration/#redirects">Syntax for the Netlify configuration file</a></p>
<p>これと、nuxt.jsみたいにビルド結果をサブディレクトリに出力するツールが組み合わさるとちょっと予想しない対応が必要になる話。</p>
<hr />
<p>ルートディレクトリに配置したnetlify.tomlにビルドとリダイレクトの設定を書く。</p>
<pre><code>[build]
base = "front/"
publish = "dist/"
command = "npm run generate"
[[redirects]]
from = "https://gisyohub.netlify.com/"
to = "https://gisyohub.com/"
status = 301
force = true
</code></pre>
<p>結果、ビルドはしてくれるのだけれど、リダイレクトは動かない。</p>
<p>ぼくの実験結果だと、同じファイルをコピーして、staticディレクトリにもnetlify.tomlを配置してあげると、ビルドする時dist/にコピーされて、その結果redirectが動作するようになる。</p>
<p>おそらくnetlifyは、ビルド時はルートディレクトリにあるnetlify.tomlを見るけど、リダイレクトのときはpublishディレクトリにあるnetlify.tomlを見ているらしい。実装の都合は何となく分かるんだけど、せめてドキュメントに明示してほしい感じ。</p>
<p>こことかここで話題に上がってるんだけど、バグとしては認めてもらえなかったぽい。<br />
<a target="_blank" rel="nofollow noopener" href="https://community.netlify.com/t/redirects-within-netlify-toml-not-working/1731">https://community.netlify.com/t/redirects-within-netlify-toml-not-working/1731</a><br />
<a target="_blank" rel="nofollow noopener" href="https://community.netlify.com/t/netlify-toml-folder-with-nuxtjs-and-netlify-cli/4659/3">https://community.netlify.com/t/netlify-toml-folder-with-nuxtjs-and-netlify-cli/4659/3</a></p>
<p>シンプル再現環境を作ればバグだと認めてもらえそうな気がするんだけど、この問題のミニマム環境って何で作るのがいいのだろう。nuxtとかの開発環境なしに、index.htmlだけをnetlifyで公開するような構成って作れるのかな?</p>
daisuke furukawa
tag:crieit.net,2005:PublicArticle/15726
2020-02-19T19:58:48+09:00
2020-02-19T19:58:48+09:00
https://crieit.net/posts/Vue-Nuxt
Vue/Nuxtのアニメーションするときに参考にしたところ
<p>VueとNuxtでアニメーションをするときに調べたところのまとめ。<br />
公式サイトを読みながら、関連するとこをまとめた感じの備忘録。</p>
<h4 id="トランジションクラスってのがあるらしい"><a href="#%E3%83%88%E3%83%A9%E3%83%B3%E3%82%B8%E3%82%B7%E3%83%A7%E3%83%B3%E3%82%AF%E3%83%A9%E3%82%B9%E3%81%A3%E3%81%A6%E3%81%AE%E3%81%8C%E3%81%82%E3%82%8B%E3%82%89%E3%81%97%E3%81%84">トランジションクラスってのがあるらしい</a></h4>
<p>参考: <a target="_blank" rel="nofollow noopener" href="https://jp.vuejs.org/v2/guide/transitions.html">Enter/Leave とトランジション一覧 — Vue.js</a></p>
<p><code><transition></code>で囲むと、いくつかのクラスを生成してくれるらしい。<br />
そのクラスに対して、</p>
<ol>
<li>CSSトランジションやアニメーションをつけたり、</li>
<li>Animate.css のようなサードパーティの CSS アニメーションライブラリと連携</li>
</ol>
<p>することで、アニメーションをつけていくぽい。</p>
<pre><code class="html"><transition>
<p v-if="show">hello</p>
</transition>
</code></pre>
<ul>
<li><code>v-enter-active</code> ... 表示/挿入のトランジション
<ul>
<li><code>v-enter</code> ... 表示/挿入のトランジションの開始</li>
<li><code>v-enter-to</code> ... 表示/挿入のトランジションの終了</li>
</ul></li>
<li><code>v-leave-active</code> ... 非表示/削除のトランジション
<ul>
<li><code>v-leave</code> ... 非表示/削除のトランジションの開始</li>
<li><code>v-leave-to</code> ... 非表示/削除のトランジションの終了</li>
</ul></li>
</ul>
<p>refs: <a target="_blank" rel="nofollow noopener" href="https://jp.vuejs.org/v2/guide/transitions.html#%E3%83%88%E3%83%A9%E3%83%B3%E3%82%B8%E3%82%B7%E3%83%A7%E3%83%B3%E3%82%AF%E3%83%A9%E3%82%B9">トランジションクラス | Enter/Leave とトランジション一覧 — Vue.js</a></p>
<p><img src="https://jp.vuejs.org/images/transition.png" alt="トランジションクラス" title="トランジションクラス" /></p>
<p><code><transition name="fade"></code>にすると、<code>v-</code>の部分が<code>fade-</code>のように指定したクラス名にできる。</p>
<h4 id="CSSトランジション/CSSアニメーション"><a href="#CSS%E3%83%88%E3%83%A9%E3%83%B3%E3%82%B8%E3%82%B7%E3%83%A7%E3%83%B3%2FCSS%E3%82%A2%E3%83%8B%E3%83%A1%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3">CSSトランジション/CSSアニメーション</a></h4>
<h5 id="フェード"><a href="#%E3%83%95%E3%82%A7%E3%83%BC%E3%83%89">フェード</a></h5>
<pre><code class="css">.fade-enter-active, .fade-leave-active {
transition: opacity .5s;
}
.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
opacity: 0;
}
</code></pre>
<h5 id="フェードしながらスライド"><a href="#%E3%83%95%E3%82%A7%E3%83%BC%E3%83%89%E3%81%97%E3%81%AA%E3%81%8C%E3%82%89%E3%82%B9%E3%83%A9%E3%82%A4%E3%83%89">フェードしながらスライド</a></h5>
<pre><code class="css">/* enter、 leave アニメーションで異なる間隔やタイミング関数を利用することができます */
.slide-fade-enter-active {
transition: all .3s ease;
}
.slide-fade-leave-active {
transition: all .8s cubic-bezier(1.0, 0.5, 0.8, 1.0);
}
.slide-fade-enter, .slide-fade-leave-to
/* .slide-fade-leave-active below version 2.1.8 */ {
transform: translateX(10px);
opacity: 0;
}
</code></pre>
<h5 id="バウンスしてイン"><a href="#%E3%83%90%E3%82%A6%E3%83%B3%E3%82%B9%E3%81%97%E3%81%A6%E3%82%A4%E3%83%B3">バウンスしてイン</a></h5>
<pre><code class="css">.bounce-enter-active {
animation: bounce-in .5s;
}
.bounce-leave-active {
animation: bounce-in .5s reverse;
}
@keyframes bounce-in {
0% {
transform: scale(0);
}
50% {
transform: scale(1.5);
}
100% {
transform: scale(1);
}
}
</code></pre>
<h4 id="カスタムトランジションクラスでAnimate.cssと連携する"><a href="#%E3%82%AB%E3%82%B9%E3%82%BF%E3%83%A0%E3%83%88%E3%83%A9%E3%83%B3%E3%82%B8%E3%82%B7%E3%83%A7%E3%83%B3%E3%82%AF%E3%83%A9%E3%82%B9%E3%81%A7Animate.css%E3%81%A8%E9%80%A3%E6%90%BA%E3%81%99%E3%82%8B">カスタムトランジションクラスでAnimate.cssと連携する</a></h4>
<p>参考: <a target="_blank" rel="nofollow noopener" href="https://jp.vuejs.org/v2/guide/transitions.html#%E3%82%AB%E3%82%B9%E3%82%BF%E3%83%A0%E3%83%88%E3%83%A9%E3%83%B3%E3%82%B8%E3%82%B7%E3%83%A7%E3%83%B3%E3%82%AF%E3%83%A9%E3%82%B9">カスタムトランジションクラス | Enter/Leave とトランジション一覧 — Vue.js</a></p>
<p><code>enter-active-class</code>や<code>leave-active-class</code>などを利用すると、<br />
該当するクラスを指定したものに置き換えることができるらしい。</p>
<p>Animate.cssのクラスを指定すれば、該当のアニメーションを設定できる。</p>
<pre><code><link href="https://cdn.jsdelivr.net/npm/[email protected]" rel="stylesheet" type="text/css">
<div id="example-3">
<button @click="show = !show">
Toggle render
</button>
<transition
name="custom-classes-transition"
enter-active-class="animated tada"
leave-active-class="animated bounceOutRight"
>
<p v-if="show">hello</p>
</transition>
</div>
</code></pre>
<h4 id="コンポーネント間のトランジション"><a href="#%E3%82%B3%E3%83%B3%E3%83%9D%E3%83%BC%E3%83%8D%E3%83%B3%E3%83%88%E9%96%93%E3%81%AE%E3%83%88%E3%83%A9%E3%83%B3%E3%82%B8%E3%82%B7%E3%83%A7%E3%83%B3">コンポーネント間のトランジション</a></h4>
<p>参考: <a target="_blank" rel="nofollow noopener" href="https://jp.vuejs.org/v2/guide/transitions.html#%E3%82%B3%E3%83%B3%E3%83%9D%E3%83%BC%E3%83%8D%E3%83%B3%E3%83%88%E9%96%93%E3%81%AE%E3%83%88%E3%83%A9%E3%83%B3%E3%82%B8%E3%82%B7%E3%83%A7%E3%83%B3">コンポーネント間のトランジション — Vue.js</a></p>
<h4 id="リストのトランジション"><a href="#%E3%83%AA%E3%82%B9%E3%83%88%E3%81%AE%E3%83%88%E3%83%A9%E3%83%B3%E3%82%B8%E3%82%B7%E3%83%A7%E3%83%B3">リストのトランジション</a></h4>
<p>参考: <a target="_blank" rel="nofollow noopener" href="https://jp.vuejs.org/v2/guide/transitions.html#%E3%83%AA%E3%82%B9%E3%83%88%E3%83%88%E3%83%A9%E3%83%B3%E3%82%B8%E3%82%B7%E3%83%A7%E3%83%B3">リストトランジション — Vue.js</a></p>
<h4 id="状態のトランジション"><a href="#%E7%8A%B6%E6%85%8B%E3%81%AE%E3%83%88%E3%83%A9%E3%83%B3%E3%82%B8%E3%82%B7%E3%83%A7%E3%83%B3">状態のトランジション</a></h4>
<p>参考: <a target="_blank" rel="nofollow noopener" href="https://jp.vuejs.org/v2/guide/transitioning-state.html">状態のトランジション — Vue.js</a></p>
<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://jp.vuejs.org/v2/guide/transitions.html">Enter/Leave とトランジション一覧 — Vue.js</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://ja.nuxtjs.org/api/configuration-transition/#transition-%E3%83%97%E3%83%AD%E3%83%91%E3%83%86%E3%82%A3">API: transition プロパティ - Nuxt.js</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/Nexus0831/items/9dbbac367a653fbd8ba4">VueでイケてるTransition! fade, slide編 - Qiita</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/masaakikunsan/items/8ff141ebdcdd52c762fb#transition-group">CSS transitionで頑張らないVue.js transition - Qiita</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://www.ritolab.com/entry/177#aj_3">Vue.jsのtransitionアニメーションとSassの導入</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://medium.com/nyle-engineering-blog/vue-js%E3%81%AE%E3%83%88%E3%83%A9%E3%83%B3%E3%82%B8%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%A8css%E3%81%A7%E4%BD%9C%E3%82%8B%E3%82%A2%E3%83%8B%E3%83%A1%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%AE%E5%9F%BA%E6%9C%AC%E3%82%92%E3%82%B5%E3%83%B3%E3%83%97%E3%83%AB%E3%81%A7%E3%82%8F%E3%81%8B%E3%82%8A%E3%82%84%E3%81%99%E3%81%8F%E8%A7%A3%E8%AA%AC-d594a263497d">Vue.jsのトランジションとCSSで作るアニメーションの基本をサンプルでわかりやすく解説</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/Nexus0831/items/9dbbac367a653fbd8ba4#fade">VueでイケてるTransition! fade, slide編 - Qiita</a></li>
</ul>
きらぷか@積読ハウマッチ/SSSAPIなど
tag:crieit.net,2005:PublicArticle/15725
2020-02-19T19:57:45+09:00
2020-02-19T19:57:45+09:00
https://crieit.net/posts/Nuxt-js-5e4d14a9d61f4
Nuxt.jsで編集中に、ページ移動やページを閉じるときに警告を出すやつ
<p>よくある編集中に、ページを離れると、<br />
「編集中のデータが消えちゃうよ」<br />
と教えてしてくれるやつを調べたときの備忘録。</p>
<p>ページを閉じるときに出るこんなのや</p>
<p><a href="https://crieit.now.sh/upload_images/7a311dc945e4b02e674c6ac135927b435e4d144cbcdfd.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/7a311dc945e4b02e674c6ac135927b435e4d144cbcdfd.png?mw=700" alt="スクリーンショット 2020-02-19 15.18.53.png" /></a></p>
<p>戻るや別のページに移動しようとするときに出るこんなの</p>
<p><a href="https://crieit.now.sh/upload_images/365938af5e847d960e78a9bcd81601375e4d1457433bf.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/365938af5e847d960e78a9bcd81601375e4d1457433bf.png?mw=700" alt="スクリーンショット 2020-02-19 15.19.33.png" /></a></p>
<h3 id="ソースコード"><a href="#%E3%82%BD%E3%83%BC%E3%82%B9%E3%82%B3%E3%83%BC%E3%83%89">ソースコード</a></h3>
<p>中身はこんな感じ。Mixinで実装してるけど、pageでもOK。</p>
<p>ページを閉じるときと、ページを移動するときとで処理が異なり、</p>
<ol>
<li>ページを閉じるとき ... windowのbeforeunloadイベントでハンドリング</li>
<li>ページを移動するとき ... vue-routerのbeforeRouteLeaveでハンドリング</li>
</ol>
<p>という感じになる。</p>
<p>なにも編集してないときに出るのが嫌なので、<br />
<code>isBlockUnload</code>というフラグを使って制御できるようにしてる。</p>
<pre><code class="typescript">import Vue from "vue";
import Component from "vue-class-component";
@Component
export default class BlockUnloadMixin extends Vue {
// 表示するメッセージ
protected blockUnloadMessage = "編集した内容が消えちゃうけど、移動する?";
// アラートを出すかのフラグ
protected isBlockUnload: boolean = false;
// ****************************
// * ページを閉じるときの処理: beforeunloadイベント
// ****************************
created() {
if (process.client) {
// 作成時にイベントリスナーに登録
window.addEventListener("beforeunload", this.checkWindow);
}
}
beforeDestroy() {
// 破棄される前にイベントリスナーから削除
window.removeEventListener("beforeunload", this.checkWindow);
}
// 実際の処理
protected checkWindow(event: BeforeUnloadEvent) {
if (!!this.isBlockUnload) {
// unloadをキャンセルして、メッセージを表示
event.preventDefault();
event.returnValue = this.blockUnloadMessage;
}
}
// ****************************
// * ページを移動するときの処理: v-router
// ****************************
beforeRouteLeave(to, from, next) {
if (!!this.isBlockUnload) {
// アラートダイアログを表示して、結果に応じて遷移
const msg = this.blockUnloadMessage;
const answer = window.confirm(msg);
next(answer);
} else {
// next()は必ず呼び出さないといけない
next();
}
}
}
</code></pre>
<h3 id="Mixinsを使う例"><a href="#Mixins%E3%82%92%E4%BD%BF%E3%81%86%E4%BE%8B">Mixinsを使う例</a></h3>
<p>使うときはこんな感じ。</p>
<pre><code class="html"><template>
<div><!-- 略... --></div>
</template>
<script lang="ts">
import { Component, Vue, mixins } from "nuxt-property-decorator";
// minins
import BlockUnloadMixin from "~/mixins/BlockUnloadMixin";
// ※ mixinsでBlockUnloadMixinを追加
@Component
export default class CreateMap extends mixins(BlockUnloadMixin) {
// なんかあたりが更新されるときによばれるなにか
private changeValue(val: string) {
// 略...
// ※ 変更があったら、アラートを出すフラグをONにする
this.isBlockUnload = true;
}
}
</script>
</code></pre>
<p>以上!!</p>
<h3 id="へんあいマップをリリースしました!"><a href="#%E3%81%B8%E3%82%93%E3%81%82%E3%81%84%E3%83%9E%E3%83%83%E3%83%97%E3%82%92%E3%83%AA%E3%83%AA%E3%83%BC%E3%82%B9%E3%81%97%E3%81%BE%E3%81%97%E3%81%9F%EF%BC%81">へんあいマップをリリースしました!</a></h3>
<p>「偏愛マップ」を簡単に作れるWebアプリです!</p>
<p><a href="https://crieit.now.sh/upload_images/8031402d59242e760178c357223a16245e4d146e01def.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/8031402d59242e760178c357223a16245e4d146e01def.png?mw=700" alt="ogp_top.png" /></a></p>
<p>偏愛マップは人見知りや口下手な人にも優しいコミュニケーションツールで、<br />
勉強会、懇親会、オフ会などの余興・アイスブレイクや自分のプロフィールにも!</p>
<p>よかったら遊んでみてください(<em>´ω`</em>)</p>
<p><a target="_blank" rel="nofollow noopener" href="https://hen-ai.net/">好きを集めた自己紹介マップ|へんあいマップ</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://blog.fukata.org/archives/9613">Vue.jsでbeforeunloadを正しく実装する | blog.fukata.org</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/issan/items/7facec98aebfc7f6c326">vue.jsでフォームを離脱するときに警告をだすやつを実装する - Qiita</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://gist.github.com/saneatsu/975f13109cde46763fedbfdebd10098d">nuxtjs_memo.md</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://webbibouroku.com/Blog/Article/js-unload-confirm">[Javascript] Webページからの離脱時に確認メッセージを表示する方法 │ Web備忘録</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://developer.mozilla.org/ja/docs/Web/API/Window/beforeunload_event">Window: beforeunload イベント - Web API | MDN</a></li>
</ul>
きらぷか@積読ハウマッチ/SSSAPIなど
tag:crieit.net,2005:PublicArticle/15695
2020-01-24T15:25:45+09:00
2020-01-24T15:25:45+09:00
https://crieit.net/posts/SVG-OGP-PNG-PNG
SVGでOGP用のPNG画像を生成してみる(折り返し文字、画像埋め込み、PNG化)
<p>SVGでOGP画像を作りたいなと思い、いろいろ調べたときの備忘録。</p>
<p>SVGはまるのも多いけど、いろいろできるので、<br />
SVGからOGP画像をつくるのいいかもしれない。</p>
<h2 id="とりあえず、SVGを書いてみる"><a href="#%E3%81%A8%E3%82%8A%E3%81%82%E3%81%88%E3%81%9A%E3%80%81SVG%E3%82%92%E6%9B%B8%E3%81%84%E3%81%A6%E3%81%BF%E3%82%8B">とりあえず、SVGを書いてみる</a></h2>
<p>ちょろっとなるなら、HTMLでベタ書きしてみるのもいい。<br />
動作確認とか楽ちん。</p>
<pre><code class="html"><html>
<body>
<svg viewbox="0 0 1200 630" width="1200" height="630" style="background-color: lightgray;">
<rect x="550" y="265" width="100" height="100" fill="blue" />
<circle cx="550" cy="265" r="30" fill="none" stroke="red" stroke-width="5" />
</svg>
</body>
</html>
</code></pre>
<p>こんな感じ。</p>
<p><a href="https://crieit.now.sh/upload_images/0996c11f41d0943bf90efa5baebbc3af5e2a8d8a9963d.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/0996c11f41d0943bf90efa5baebbc3af5e2a8d8a9963d.png?mw=700" alt="スクリーンショット 2020-01-23 16.15.07.png" /></a></p>
<p><code><rect></code>で四角を書いて、<code><circle></code>で丸を書いてる。</p>
<p>上から順に描画されるので、下にある方が前面にくる感じ。<br />
なので、四角の上に丸が描画されてる。</p>
<p><code><rect></code>の座標(x,y)は左上の場所なので、<br />
横幅と縦幅分の半分だけ、全体の中心からずらしてる。</p>
<h2 id="画像化する"><a href="#%E7%94%BB%E5%83%8F%E5%8C%96%E3%81%99%E3%82%8B">画像化する</a></h2>
<p>最終的にOGPで使うPNG画像にしたい。</p>
<p>画像化する方法について、ローカルとブラウザ上の2つを試した。</p>
<h4 id="1. ローカルで画像化する"><a href="#1.+%E3%83%AD%E3%83%BC%E3%82%AB%E3%83%AB%E3%81%A7%E7%94%BB%E5%83%8F%E5%8C%96%E3%81%99%E3%82%8B">1. ローカルで画像化する</a></h4>
<p>いろんな画像フォーマットに対応しているsharpがよさそう。<br />
・<a target="_blank" rel="nofollow noopener" href="https://sharp.pixelplumbing.com/">sharp - High performance Node.js image processing</a></p>
<p>.svgファイルを.pngに簡単にできる。</p>
<h5 id="sharpでsvgをpngに変換してみる"><a href="#sharp%E3%81%A7svg%E3%82%92png%E3%81%AB%E5%A4%89%E6%8F%9B%E3%81%97%E3%81%A6%E3%81%BF%E3%82%8B">sharpでsvgをpngに変換してみる</a></h5>
<p>まずは、.svgとして扱えるように、<br />
xmlnsとかをちゃんとつけたsample.svgを用意する。</p>
<pre><code class="xml"><!-- sample.svg -->
<?xml version="1.0"?>
<svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 1200 630" width="1200" height="630" fill="lightgray">
<rect x="550" y="265" width="100" height="100" fill="blue" />
<circle cx="550" cy="265" r="30" fill="none" stroke="red" stroke-width="5" />
</svg>
</code></pre>
<p>パッケージをインストール</p>
<pre><code class="shell">$ npm i sharp
</code></pre>
<p>変換するコードはこんな感じ。</p>
<pre><code class="javascript">// generate.js
const sharp = require("sharp");
async function main() {
await sharp("sample.svg")
.png()
.toFile("output.png");
}
main().then();
</code></pre>
<p>そして、実行。</p>
<pre><code class="shell">$ node generate.js
</code></pre>
<p>すると、output.pngというPNGファイルを作成してくれる。</p>
<p>ただ、<strong><code>svg</code>のstyleは反映してくれないので注意</strong>。</p>
<pre><code class="javascript">// generate.js
const sharp = require("sharp");
async function main() {
await sharp("sample.svg")
.png()
.toFile("output.png");
}
main().then();
</code></pre>
<p>背景色をつける場合は、sharp側で設定すればOK</p>
<pre><code class="javascript">// generate.js
const sharp = require("sharp");
async function main() {
await sharp("sample.svg")
.flatten({ background: "lightgray" }) // 背景色
.png()
.toFile("output.png");
}
main().then();
</code></pre>
<h3 id="2. ブラウザ上のSVGを画像化する"><a href="#2.+%E3%83%96%E3%83%A9%E3%82%A6%E3%82%B6%E4%B8%8A%E3%81%AESVG%E3%82%92%E7%94%BB%E5%83%8F%E5%8C%96%E3%81%99%E3%82%8B">2. ブラウザ上のSVGを画像化する</a></h3>
<p>ユーザに情報を入力してもらった内容をSVGで表示して、<br />
OGP画像にするという感じが多いので、こっちがメイン。</p>
<p>保存先のFirebase StorageがData URL形式に対応しているので、<br />
PNGのData URLが取得できればOK。</p>
<p>大まかな流れは、こんな感じ。</p>
<ol>
<li>Canvasを用意する</li>
<li>SVGを読み込む<code><image></code>を用意する</li>
<li><code><svg></code>を文字列に変換</li>
<li>作成した<code><image></code>にDataURL形式でSVGをセットして、読み込み開始</li>
<li>読み込みが完了したら、Canvasに書き出して</li>
<li>CanvasからDataURLを取得する</li>
</ol>
<pre><code class="typescript">// svg2DataURL.ts
/**
* svgをpngに変換
* @param svgElement <svg>のHTML要素
*/
export default function svg2DataURL(
svgElement: HTMLElement
): Promise<HTMLCanvasElement> {
return new Promise((resolve, reject) => {
// 1. Canvasを用意する
const canvas = document.createElement("canvas");
canvas.width = 1200;
canvas.height = 630;
const ctx = canvas.getContext("2d");
if (!ctx) {
reject(Error("Create Canvas Error..."));
return;
}
// 2. SVGを読み込む<image>を用意する
const image = new Image();
image.decoding = "async";
image.onload = () => {
// 5. 読み込みが完了したら、Canvasに書き出して、
ctx.drawImage(image, 0, 0, 1200, 630);
// 6. CanvasからDataURLを取得する
resolve(canvas.toDataURL());
};
image.onerror = e => reject(e);
// 3. <svg>を文字列に変換
const svgXml = new XMLSerializer().serializeToString(svgElement);
const svgData = btoa(unescape(encodeURIComponent(svgXml)));
// 4. 作成した<image>にDataURL形式でセットして、読み込み開始
image.src = `data:image/svg+xml;charset=utf-8;base64,${svgData}`;
});
}
</code></pre>
<p>あとは、好きなタイミングで呼び出せばOK。</p>
<p>Nuxt.jsでの例はこんな感じ。</p>
<pre><code class="html"><template>
<div>
<!-- document.getElementByIdできるように、idをつけておく -->
<svg id="svg" viewbox="0 0 1200 630" width="1200" height="630" style="background-color: lightgray;">
<rect x="550" y="265" width="100" height="100" fill="blue" />
<circle cx="550" cy="265" r="30" fill="none" stroke="red" stroke-width="5" />
</svg>
</div>
<div>
<a class="button" @click="saveSVG">画像を保存</a>
</div>
</template>
<script lang="ts">
import { Component, Vue } from "nuxt-property-decorator";
// 初期化済みのfirebaseインスタンス。詳細は略
import firebase from "~/plugins/firebase";
import svg2DataURL from "./svg2DataURL";
@Component()
export default class SaveSvgPage extends Vue {
// 画像を保存する処理
async saveSVG() {
// svgのHTML要素を取得
const elm = document.getElementById("svg");
if (!elm) return;
// さっきの処理: HTML要素からDataURLを取得
const dataURL = await svg2DataURL(elm);
// Cloud Storage for Firebaseへ保存
const filePath = "...保存する先のパス..."
const storage = firebase.storage();
const fileRef = storage.ref().child(filePath);
await fileRef.putString(dataURL, "data_url");
}
</script>
</code></pre>
<p>これでSVGがCloud Storage for FirebaseにPNG画像で保存できる(<em>´ω`</em>)</p>
<h4 id="ハマったポイント"><a href="#%E3%83%8F%E3%83%9E%E3%81%A3%E3%81%9F%E3%83%9D%E3%82%A4%E3%83%B3%E3%83%88">ハマったポイント</a></h4>
<h5 id="1. 画像化するときにhtml2canvasを使うといいかんじにならない"><a href="#1.+%E7%94%BB%E5%83%8F%E5%8C%96%E3%81%99%E3%82%8B%E3%81%A8%E3%81%8D%E3%81%ABhtml2canvas%E3%82%92%E4%BD%BF%E3%81%86%E3%81%A8%E3%81%84%E3%81%84%E3%81%8B%E3%82%93%E3%81%98%E3%81%AB%E3%81%AA%E3%82%89%E3%81%AA%E3%81%84">1. 画像化するときにhtml2canvasを使うといいかんじにならない</a></h5>
<p>画像化する方法で<a target="_blank" rel="nofollow noopener" href="https://html2canvas.hertzen.com/">html2canvas</a>もあるけど、うまくいかなかった。。</p>
<p>スクロール位置や画面サイズによって、<br />
うまく撮れるときと撮れない時があって、この方法に。。</p>
<h5 id="2. ブラウザ上と保存した画像が違う"><a href="#2.+%E3%83%96%E3%83%A9%E3%82%A6%E3%82%B6%E4%B8%8A%E3%81%A8%E4%BF%9D%E5%AD%98%E3%81%97%E3%81%9F%E7%94%BB%E5%83%8F%E3%81%8C%E9%81%95%E3%81%86">2. ブラウザ上と保存した画像が違う</a></h5>
<p>この後出てくる小ネタ集でスタイルで装飾する方法を使ったところ、<br />
うまくいかない感じに。。</p>
<p>svg内にsytleをもたせたらうまくいった(<em>´ω`</em>)</p>
<p>ブラウザ上 = 全体のCSSが適用<br />
保存画像 = SVG配下のCSSのみ適用</p>
<p>なので、SVGだけで完結しないといけない...</p>
<p>フォントとかも指定しないと、見た目が変わってしまう。。</p>
<h5 id="3. 背景画像など外部リンクがあるとうまく保存できない..."><a href="#3.+%E8%83%8C%E6%99%AF%E7%94%BB%E5%83%8F%E3%81%AA%E3%81%A9%E5%A4%96%E9%83%A8%E3%83%AA%E3%83%B3%E3%82%AF%E3%81%8C%E3%81%82%E3%82%8B%E3%81%A8%E3%81%86%E3%81%BE%E3%81%8F%E4%BF%9D%E5%AD%98%E3%81%A7%E3%81%8D%E3%81%AA%E3%81%84...">3. 背景画像など外部リンクがあるとうまく保存できない...</a></h5>
<p>Canvasの仕様っぽく、外部リンクでCORSで引っかかると画像が表示されないよう...</p>
<p>SVGで完結するように、読み込んだ画像をDataURL形式で指定するようにしたらうまくいった(<em>´ω`</em>)</p>
<p>「<strong>SVGで完結</strong>」が大事らしい。。</p>
<h2 id="小ネタ集"><a href="#%E5%B0%8F%E3%83%8D%E3%82%BF%E9%9B%86">小ネタ集</a></h2>
<h3 id="svg内でstyleが使える"><a href="#svg%E5%86%85%E3%81%A7style%E3%81%8C%E4%BD%BF%E3%81%88%E3%82%8B">svg内でstyleが使える</a></h3>
<p><code><style></code>タグがあるらしい。<br />
classを設定して、いろいろできる。便利。</p>
<pre><code class="html"><html>
<body>
<svg viewbox="0 0 1200 630" width="1200" height="630" style="background-color: lightgray;" >
<style>
.rect {
fill: green;
}
</style>
<rect class="rect" x="600" y="315" width="100" height="100"/>
</svg>
</body>
</html>
</code></pre>
<h3 id="折り返し文字を表示してみる(foreignObject)"><a href="#%E6%8A%98%E3%82%8A%E8%BF%94%E3%81%97%E6%96%87%E5%AD%97%E3%82%92%E8%A1%A8%E7%A4%BA%E3%81%97%E3%81%A6%E3%81%BF%E3%82%8B%28foreignObject%29">折り返し文字を表示してみる(foreignObject)</a></h3>
<p>SVGにも<code><text></code>があるけど、自動折り返しに対応していない。<br />
文字数を計算して自分で分割すればできるけど、めんどくさい。。</p>
<p>文字の折り返ししたい場合、<code><foreignObject></code>という<br />
HTMLを追加できるのがあるので、それを使うといいらしい。</p>
<p>styleでfont-sizeとかも設定できるのでいろいろできそう(<em>´ω`</em>)</p>
<pre><code class="html"><html>
<body>
<svg viewbox="0 0 1200 630" width="1200" height="630" style="background-color: lightgray;" >
<style>
@import url("https://fonts.googleapis.com/css?family=Noto+Sans+JP:500&display=swap&subset=japanese");
.item {
font-family: "Noto Sans JP", sans-serif;
font-size: 60px;
border: 2px solid green;
}
</style>
<foreignObject
requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"
width="200"
height="200"
x="500"
y="215"
>
<div class="item">
こんにちは
</div>
</foreignObject>
</svg>
</body>
</html>
</code></pre>
<p>こんな感じ</p>
<p><a href="https://crieit.now.sh/upload_images/c4318acbc1f8d438ed7eea4a3e11257a5e2a8da3252f2.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/c4318acbc1f8d438ed7eea4a3e11257a5e2a8da3252f2.png?mw=700" alt="スクリーンショット 2020-01-23 17.44.58.png" /></a></p>
<p>ただ、foreignObjectはサイズを自動計算してくれるわけではないので、<br />
widthとheightを設定しないといけない。</p>
<h3 id="画像を表示する(image)"><a href="#%E7%94%BB%E5%83%8F%E3%82%92%E8%A1%A8%E7%A4%BA%E3%81%99%E3%82%8B%28image%29">画像を表示する(image)</a></h3>
<p>SVGで画像を使うときは、<code><image></code>を使う。<br />
<code><img></code>とは違う。</p>
<pre><code class="html"><html>
<body>
<svg viewbox="0 0 1200 630" width="1200" height="630" style="background-color: lightgray;" >
<image
xlink:href="https://mdn.mozillademos.org/files/6457/mdn_logo_only_color.png"
width="200"
height="200"
x="500"
y="215"
/>
</svg>
</body>
</html>
</code></pre>
<p>こんな感じ。</p>
<p><a href="https://crieit.now.sh/upload_images/a6f51a0f42f98ba2aa818a0116e59f605e2a8db81f639.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/a6f51a0f42f98ba2aa818a0116e59f605e2a8db81f639.png?mw=700" alt="スクリーンショット 2020-01-23 17.48.11.png" /></a></p>
<p>ハマったポイントにも書いたとおり、このままCanvasに書き出すと、<br />
保存時に画像が表示されないので、動的にDataURLを取得してセットするといい感じ。</p>
<pre><code class="html"><html>
<body>
<svg viewbox="0 0 1200 630" width="1200" height="630" style="background-color: lightgray;" >
<image
xlink:href="data:image/svg+xml;charset=utf-8;base64,...略"
width="200"
height="200"
x="500"
y="215"
/>
</svg>
</body>
</html>
</code></pre>
<p>URLからDataURLに変換するのは、こんな感じ。</p>
<pre><code class="typescript">// svg2DataURL.ts
/**
* URLをDataURLに変換
* @param 変換したい画像のURL
*/
export default function url2DataURL(
url: string
): Promise<HTMLCanvasElement> {
return new Promise((resolve, reject) => {
// 1. Canvasを用意する
const canvas = document.createElement("canvas");
canvas.width = 1200;
canvas.height = 630;
const ctx = canvas.getContext("2d");
if (!ctx) {
reject(Error("Create Canvas Error..."));
return;
}
// 2. SVGを読み込む<image>を用意する
const image = new Image();
image.decoding = "async";
image.onload = () => {
// 4. 読み込みが完了したら、Canvasに書き出して、
ctx.drawImage(image, 0, 0, 1200, 630);
// 5. CanvasからDataURLを取得する
resolve(canvas.toDataURL());
};
image.onerror = e => reject(e);
// 3. 作成した<image>にURLでセットして、読み込み開始
image.src = url;
});
}
</code></pre>
<h3 id="画面サイズに合うように表示する(resize対応)"><a href="#%E7%94%BB%E9%9D%A2%E3%82%B5%E3%82%A4%E3%82%BA%E3%81%AB%E5%90%88%E3%81%86%E3%82%88%E3%81%86%E3%81%AB%E8%A1%A8%E7%A4%BA%E3%81%99%E3%82%8B%28resize%E5%AF%BE%E5%BF%9C%29">画面サイズに合うように表示する(resize対応)</a></h3>
<p>OGP画像としていい感じのサイズで保存したいけど、<br />
表示するときは画面サイズにあった感じにしたい。。</p>
<p>svgにstyleが使えるので、リサイズ時にスケールを計算して、<br />
<code>transform: scale();</code>で縮小する感じにしてみた。</p>
<pre><code class="html"><template>
<div id="svg-wrapper" class="svg-wrapper">
<svg class="svg-content" :viewbox="`0 0 ${svgWidth} ${svgHeight}`"
:width="svgWidth" :height="svgHeight" :style="style">
<rect x="550" y="265" width="100" height="100" fill="blue" />
<circle cx="550" cy="265" r="30" fill="none" stroke="red" stroke-width="5" />
</svg>
</div>
</template>
<script lang="ts">
import { Component, Vue } from "nuxt-property-decorator";
@Component()
export default class SvgPage extends Vue {
private svgWidth: number = 1200;
private svgHeight: number = 630;
private scale: number = 1;
mounted() {
// マウント時にリサイズする
this.$nextTick(() => this.handleResize());
// windowのresizeイベントのリスナーに登録して、
// 画面サイズが変わったら、スケールを再計算するようにする
window.addEventListener("resize", this.handleResize);
}
beforeDestroy() {
// 破棄されるときに、リスナーの登録を解除する
window.removeEventListener("resize", this.handleResize);
}
// リサイズ用のスケールを計算する処理
private handleResize() {
const elm = document.getElementById("svg-wrapper");
if (!elm) return;
this.rect = elm.getBoundingClientRect();
this.scale = this.rect.width / this.svgWidth;
}
// ****************************************************
// * computed
// ****************************************************
private get style() {
// 計算したスケールで縮小するようにtransformを設定する
return { transform: `scale(${this.scale})` };
}
}
</script>
<style>
svg {
transform-origin: 0 0;
}
.svg-wrapper {
position: relative;
width: 100%;
height: auto;
}
.svg-wrapper:before {
content: "";
display: block;
padding-top: 52.5%; /* 630 / 1200 x 100 */
}
.svg-content {
position: absolute;
top: 0;
left: 0;
}
</style>
</code></pre>
<p>ただ、このまま保存すると縮小されたままになるので、<br />
deepコピーでクローンして、transformをクリアしてから書き出すようにする。</p>
<pre><code class="typescript">// svg2DataURL.ts
/**
* svgをpngに変換
* @param svgElement <svg>のHTML要素
*/
export default function svg2DataURL(
svgElement: HTMLElement
): Promise<HTMLCanvasElement> {
return new Promise((resolve, reject) => {
// deepコピーでクローン
const elm = svgElement.cloneNode(true) as HTMLElement;
// transformをクリア
elm.style.transform = "";
// ... 略
// 3. <svg>を文字列に変換
// ※ transformを削除したelmでsvgの文字列を取得
const svgXml = new XMLSerializer().serializeToString(elm);
// ... 略
});
}
</code></pre>
<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://developer.mozilla.org/ja/docs/Web/SVG/Element/image"> - SVG: Scalable Vector Graphics | MDN</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://developer.mozilla.org/ja/docs/Web/HTML/CORS_enabled_image">画像とキャンバスをオリジン間で利用できるようにする - HTML: HyperText Markup Language | MDN</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://developer.mozilla.org/ja/docs/Web/SVG/Element/foreignObject"> - SVG: Scalable Vector Graphics | MDN</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://kamoqq.info/post/nodejs-image-processor-sharp/">Node.js向け画像編集ライブラリSharp - kamoqq.info</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://sharp.pixelplumbing.com/">sharp - High performance Node.js image processing</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://forum.vuejs.org/t/does-vue-2-0-break-svg-foreignobject/2566">Does vue 2.0 break SVG foreignObject? - Get Help - Vue Forum</a></li>
<li><a target="_blank" rel="nofollow noopener" href="http://nakajmg.hatenablog.com/entry/2019/08/30/133330">Vueでsvgファイルをいい感じに扱う - じまろぐ</a></li>
<li><a target="_blank" rel="nofollow noopener" href="http://cocu.hatenablog.com/entry/2014/01/14/214917">svgにhtmlを組み込んで、テキストを折り返したりcanvasを使ったり - cocuh's note</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://inside.pixiv.blog/subal/7123">文字レイヤーを支える技術 - pixiv inside</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/yuukive/items/ede7c087843d2f7ef979">VueコンポーネントでWindowサイズ変更検知&値取得 - Qiita</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/haribote/items/b17d46b9679ce2fb2712">一発芸!SVGでHTMLを画像化する - Qiita</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://developer.mozilla.org/ja/docs/Web/SVG/Tutorial/SVG_Fonts">SVG Fonts - SVG: Scalable Vector Graphics | MDN</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://blanche-toile.com/web/css-font-face">CSSの@font-faceでGoogle Fontsのwebフォントを利用する方法 | Free Style</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://developer.mozilla.org/ja/docs/Web/API/Window/onresize">window.onresize - Web API | MDN</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/ryounagaoka/items/a98f59347ed758743b8d">CSSだけでアスペクト比を固定するテク - Qiita</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/serinuntius/items/3017fb6ef51cd47352f6">Vue.jsとFirebaseでOGP画像生成系のサービスを爆速で作ろう - Qiita</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/you8/items/bc5cbe887101863b242b">画像生成してOGPに設定する - Qiita</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://creive.me/archives/17653/#word-breakoverflow-wrap">【CSS】 テキストを折り返す方法!自動で改行・レスポンシブにも対応 | creive【クリーブ】</a></li>
</ul>
きらぷか@積読ハウマッチ/SSSAPIなど
tag:crieit.net,2005:PublicArticle/15666
2020-01-07T21:31:01+09:00
2020-01-07T21:31:01+09:00
https://crieit.net/posts/SPA-Nuxt-Firebase-Hosting-Netlify
SPAなNuxtアプリをFirebase HostingからNetlifyに移行してみた
<p>Netlifyがいいと聞いて、Firebase Hostingから移行してみたときの備忘録。</p>
<p>NetfilyのプレレンダリングがあるのでOGP芸が楽。<br />
昔、<a target="_blank" rel="nofollow noopener" href="https://www.memory-lovers.blog/entry/2019/08/07/150000">こんな記事</a>を書いたけど、Functionsで頑張らなくてもよくなった。</p>
<p>使ってみてよかったので、移行したときにやったことをまとめてみる。</p>
<h2 id="移行するときにやったこと"><a href="#%E7%A7%BB%E8%A1%8C%E3%81%99%E3%82%8B%E3%81%A8%E3%81%8D%E3%81%AB%E3%82%84%E3%81%A3%E3%81%9F%E3%81%93%E3%81%A8">移行するときにやったこと</a></h2>
<h4 id="nuxt.config.tsなどの変更"><a href="#nuxt.config.ts%E3%81%AA%E3%81%A9%E3%81%AE%E5%A4%89%E6%9B%B4">nuxt.config.tsなどの変更</a></h4>
<h5 id="1. Nuxtなどの公式サイトを見て設定"><a href="#1.+Nuxt%E3%81%AA%E3%81%A9%E3%81%AE%E5%85%AC%E5%BC%8F%E3%82%B5%E3%82%A4%E3%83%88%E3%82%92%E8%A6%8B%E3%81%A6%E8%A8%AD%E5%AE%9A">1. Nuxtなどの公式サイトを見て設定</a></h5>
<p>Firebase Hostingで設定したのとかぶっているところもあり、nuxt.config.tsは変更なし。</p>
<p>参考: <a target="_blank" rel="nofollow noopener" href="https://ja.nuxtjs.org/faq/netlify-deployment/">Netlify へデプロイするには? - NuxtJS</a></p>
<h5 id="2. _redirectsファイルの作成"><a href="#2.+_redirects%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%81%AE%E4%BD%9C%E6%88%90">2. _redirectsファイルの作成</a></h5>
<p>Netlifyでは、index.html以外は404になるので、<br />
SPAの場合は<code>static/_redirecs</code>というファイルを作成して、<br />
以下の内容を記載しておく。</p>
<pre><code>/* /index.html 200
</code></pre>
<p>参考] <a target="_blank" rel="nofollow noopener" href="https://docs.netlify.com/routing/redirects/rewrites-proxies/#history-pushstate-and-single-page-apps">Rewrites and proxies | Netlify Docs</a></p>
<h5 id="3. Netlify側の設定"><a href="#3.+Netlify%E5%81%B4%E3%81%AE%E8%A8%AD%E5%AE%9A">3. Netlify側の設定</a></h5>
<p>あとは、Netlifyにアクセスして、 「<a target="_blank" rel="nofollow noopener" href="https://ja.nuxtjs.org/faq/netlify-deployment/#spa-%E3%83%A2%E3%83%BC%E3%83%89%E3%81%A7%E7%94%9F%E6%88%90%E3%81%95%E3%82%8C%E3%81%9F%E3%82%B5%E3%82%A4%E3%83%88%E3%81%AE%E5%A0%B4%E5%90%88">SPA モードで生成されたサイトの場合</a>」にも書いてあるとおりの内容をNetlifyの設定に入力すればOK。</p>
<p>迷わなければ、5分くらいでできて、masterにpushと更新される(<em>´ω`</em>)</p>
<h4 id="プレレンダリング"><a href="#%E3%83%97%E3%83%AC%E3%83%AC%E3%83%B3%E3%83%80%E3%83%AA%E3%83%B3%E3%82%B0">プレレンダリング</a></h4>
<p>ベータ版だけど、Netlifyでプレレンダリングしてくれる機能がある。</p>
<p><a target="_blank" rel="nofollow noopener" href="https://www.memory-lovers.blog/entry/2019/08/07/150000">この記事</a>に書いてある感じで、<br />
「FunctionsでheadだけのHTMLを生成→リダイレクト」<br />
としていたのが不要になる感じ。</p>
<h5 id="Netlifyの画面で設定"><a href="#Netlify%E3%81%AE%E7%94%BB%E9%9D%A2%E3%81%A7%E8%A8%AD%E5%AE%9A">Netlifyの画面で設定</a></h5>
<p>設定は簡単で、Netlify上で設定画面で有効化すればOK。<br />
参考: <a target="_blank" rel="nofollow noopener" href="https://docs.netlify.com/site-deploys/post-processing/prerendering/#set-up-prerendering">Prerendering | Netlify Docs</a></p>
<h5 id="その他の修正点"><a href="#%E3%81%9D%E3%81%AE%E4%BB%96%E3%81%AE%E4%BF%AE%E6%AD%A3%E7%82%B9">その他の修正点</a></h5>
<p>あとは、以下の修正。</p>
<ol>
<li>OPGのためにrouterでリダイレクトしていた設定を削除</li>
<li>各ページでメタ情報を適切になるように変更</li>
</ol>
<p>また、上で書いたとおり、<code>static/_redirecs</code>で</p>
<pre><code>/* /index.html 200
</code></pre>
<p><strong>これを設定しておかないとボットがアクセスしたときに404になってOGPがでない</strong>。。</p>
<p>(これに気づかずかなりハマった。。)</p>
<h5 id="小ネタ: プレレンダリングの結果を確認する"><a href="#%E5%B0%8F%E3%83%8D%E3%82%BF%3A+%E3%83%97%E3%83%AC%E3%83%AC%E3%83%B3%E3%83%80%E3%83%AA%E3%83%B3%E3%82%B0%E3%81%AE%E7%B5%90%E6%9E%9C%E3%82%92%E7%A2%BA%E8%AA%8D%E3%81%99%E3%82%8B">小ネタ: プレレンダリングの結果を確認する</a></h5>
<p>プレレンダリングするかどうかは、UAを見て判断しているらしく、</p>
<ul>
<li>UAを指定してすると、プレレンダリングされた結果を確認でき、</li>
<li>UAを指定してないと、プレレンダリングなしの結果になる</li>
</ul>
<p>curlだとこんな感じで、<code>-A twitterbot</code>を指定するとよい。</p>
<pre><code class="shell">$ curl -A twitterbot https://example.com/hoge/fuga -o fuga.html
</code></pre>
<p>参考: <a target="_blank" rel="nofollow noopener" href="https://community.netlify.com/t/common-issue-understanding-and-debugging-prerendering/150">[Common Issue] Understanding and debugging prerendering</a></p>
<p>毎回、<a target="_blank" rel="nofollow noopener" href="https://cards-dev.twitter.com/validator">Twitter Card Validator</a>で確認するのはめんどくさいので、この方法だと便利。</p>
<h3 id="ページが存在しないURLの対応"><a href="#%E3%83%9A%E3%83%BC%E3%82%B8%E3%81%8C%E5%AD%98%E5%9C%A8%E3%81%97%E3%81%AA%E3%81%84URL%E3%81%AE%E5%AF%BE%E5%BF%9C">ページが存在しないURLの対応</a></h3>
<p>積読ハウマッチでは、少し特殊なことをしていて、<br />
一部のURLはheadだけのHTMLしか存在しないページがいくつかある。</p>
<p>なので、その部分を修正していく。方法としては以下の感じ。</p>
<ol>
<li>nuxt.config.tsでrouterに、クエリパラメタ付きでリダイレクトを追加する
<ul>
<li><a target="_blank" rel="nofollow noopener" href="https://router.vuejs.org/ja/guide/essentials/redirect-and-alias.html">リダイレクトとエイリアス | Vue Router</a></li>
</ul></li>
<li><code>asyncData()</code>でクエリパラメタを受け取り、<code>created()</code>で削除
<ul>
<li><code>this.$router.push({ query: {} });</code></li>
<li><a target="_blank" rel="nofollow noopener" href="https://www.kimullaa.com/entry/2018/01/27/183743">Vuejs vue-router クエリパラメータの一部だけを取り除く - SIerだけど技術やりたいブログ</a></li>
</ul></li>
</ol>
<h4 id="動的なサイトマップの対応"><a href="#%E5%8B%95%E7%9A%84%E3%81%AA%E3%82%B5%E3%82%A4%E3%83%88%E3%83%9E%E3%83%83%E3%83%97%E3%81%AE%E5%AF%BE%E5%BF%9C">動的なサイトマップの対応</a></h4>
<p>書籍やユーザのページなどは日々増えていくので、<br />
サイトマップも動的に生成するようにしている。</p>
<p>Hostingの場合はFunctionsを呼び出せたけれど、<br />
Netlifyに移行すると、それができないので対応が必要。</p>
<p><strong>netlifyの_redirectsを使って直接Functionsへリダイレクトする方法で対応した</strong>。</p>
<p>こんな感じで各URLを該当のFunctionsにリダイレクトさせておく。</p>
<pre><code>/sitemap https://us-central1-<project-id>.cloudfunctions.net/sitemap
/* /index.html 200
</code></pre>
<h5 id="sitemap-moduleだとダメだった..."><a href="#sitemap-module%E3%81%A0%E3%81%A8%E3%83%80%E3%83%A1%E3%81%A0%E3%81%A3%E3%81%9F...">sitemap-moduleだとダメだった...</a></h5>
<p>sitemap-moduleで動的なページの対応方法も乗っているが、<br />
SPAなのでgenerateされてしまう...</p>
<ul>
<li>参考
<ul>
<li><a target="_blank" rel="nofollow noopener" href="https://github.com/nuxt-community/sitemap-module#readme">nuxt-community/sitemap-module: Sitemap Module for Nuxt.js</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/bucchi49/items/d271c4010a3f6c900926">Nuxt.jsで静的ファイル生成時にサイトマップも自動生成する方法 - Qiita</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/cortyuming/items/b4640b371e4ceb37ae47">Nuxt.js + Firestore で動的サイトマップ - Qiita</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/sauzar18/items/2ea958043eb6758c4f83">Nuxt.jsでサイトマップを動的に設定する方法 - Qiita</a></li>
</ul></li>
</ul>
<h4 id="カスタムドメインの設定"><a href="#%E3%82%AB%E3%82%B9%E3%82%BF%E3%83%A0%E3%83%89%E3%83%A1%E3%82%A4%E3%83%B3%E3%81%AE%E8%A8%AD%E5%AE%9A">カスタムドメインの設定</a></h4>
<p>Netlifyの設定画面に従い、カスタムドメインを設定していく。</p>
<p>ALIASを設定するようにいわれるが、<br />
お名前ドットコムはALIASを設定できないらしいので、<br />
NetlifyのDNSを使う形で対応</p>
<p><a target="_blank" rel="nofollow noopener" href="https://qiita.com/hitochan777/items/7acce5b4398af7f4c358">参考にした記事</a>でも書いてあるとおり、<br />
Aレコードでもできるが、CDNを活用できないらしいので同様の対応</p>
<p>あとは、設定画面にも書いてあるとおり、<br />
<code>*.netlify.com</code>から設定したドメインへリダイレクトを追加しておく。</p>
<pre><code>[https://tsundoku.netlify.com/*](https://tsundoku.netlify.com/*) https://tsundoku.site/:splat 301!
/sitemap https://us-central1-<project-id>.cloudfunctions.net/sitemap
/* /index.html 200
</code></pre>
<p>これでリダイレクトされるようになるので、あとはDNSの反映を待てばOK!</p>
<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">参考にしたサイト</a></h1>
<ul>
<li><a href="https://crieit.net/posts/Netlify-404">Netlifyを使ってたらルートパス以外が404になった話とその解決方法 - Crieit</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://docs.netlify.com/routing/redirects/rewrites-proxies/#limitations">Rewrites and proxies | Netlify Docs</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://prerender.io/documentation/best-practices">Prerender - Dynamic Rendering for JavaScript Website SEO</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://community.netlify.com/t/common-issue-understanding-and-debugging-prerendering/150">[Common Issue] Understanding and debugging prerendering - Support / Common Issues - Netlify Community</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/NaokiIshimura/items/64e060ccc244e38d0c15#%E3%82%AB%E3%82%B9%E3%82%BF%E3%83%A0%E3%83%89%E3%83%A1%E3%82%A4%E3%83%B3%E3%81%AE%E8%A8%AD%E5%AE%9A">【Netlify】カスタムドメインを設定する - Qiita</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/don-bu-rakko/items/8eb30c7e9a3f9531ba16">Netlifyにお名前.comで取得した独自ドメインを設定する - Qiita</a></li>
</ul>
きらぷか@積読ハウマッチ/SSSAPIなど