tag:crieit.net,2005:https://crieit.net/tags/web1week/feed
「web1week」の記事 - Crieit
Crieitでタグ「web1week」に投稿された最近の記事
2021-02-15T14:38:44+09:00
https://crieit.net/tags/web1week/feed
tag:crieit.net,2005:PublicArticle/16064
2020-09-20T19:25:58+09:00
2021-02-15T14:38:44+09:00
https://crieit.net/posts/2-5f672e364d13b
ハッカソンイベントで、「2」に近い式をあてるゲームを作ってみた
<p>こんにちは、最近「個人開発ガリガリやって実力つけたいなー」と思っているよしです。<br />
先日、web1week に参加してきました。<br />
色々とてんやわんやしながらも、なんとか投稿までいけたので、レポートを残しておきますー。</p>
<h2 id="はじめに"><a href="#%E3%81%AF%E3%81%98%E3%82%81%E3%81%AB">はじめに</a></h2>
<h3 id="web1week の概要"><a href="#web1week+%E3%81%AE%E6%A6%82%E8%A6%81">web1week の概要</a></h3>
<p>「1週間でお題に沿った Web サービスを作ってみよう」というハッカソン的なイベントです。</p>
<blockquote class="twitter-tweet">
<p lang="ja" dir="ltr">9/7~9/13の1週間でWebサービスを作るイベントです! Hello worldレベルのサービスでもOKですのでぜひご参加をお願いします! <a target="_blank" rel="nofollow noopener" href="https://twitter.com/hashtag/%E9%A7%86%E3%81%91%E5%87%BA%E3%81%97%E3%82%A8%E3%83%B3%E3%82%B8%E3%83%8B%E3%82%A2?src=hash&ref_src=twsrc%5Etfw">#駆け出しエンジニア</a> 仲間が多い方は是非シェアもお願いします~お題は「2」です。<a target="_blank" rel="nofollow noopener" href="https://t.co/cl4XbPFici">https://t.co/cl4XbPFici</a></p>— だら🎄いろいろつくってる (@dala00) <a target="_blank" rel="nofollow noopener" href="https://twitter.com/dala00/status/1302622342951002115?ref_src=twsrc%5Etfw">September 6, 2020</a>
</blockquote>
<h3 id="参加したきっかけ"><a href="#%E5%8F%82%E5%8A%A0%E3%81%97%E3%81%9F%E3%81%8D%E3%81%A3%E3%81%8B%E3%81%91">参加したきっかけ</a></h3>
<p>前回も参加して楽しかったので。</p>
<p>前回参加の時は割と使える時間が多かった一方、今回は仕事が終わった後か休日に開発することになりました。<br />
なので、最初はどうしようかとも思いましたが、他の参加者の皆さんの多くは同じ状況下で開発されているはずなので、まぁどうにかなるだろう...くらいの気持ちで参加してました。</p>
<p>※前回参加時のレポート記事<br />
- <a href="https://crieit.net/posts/ac044ef3dd9b2580f6a86c0ac05881c1">ハッカソンイベントで、React Konva 製のジグソーパズルっぽいパズルを作ってみた</a></p>
<h2 id="自分が作ったもの"><a href="#%E8%87%AA%E5%88%86%E3%81%8C%E4%BD%9C%E3%81%A3%E3%81%9F%E3%82%82%E3%81%AE">自分が作ったもの</a></h2>
<p><a target="_blank" rel="nofollow noopener" href="https://h-yoshikawa44.github.io/close-to-2/">Close to 2</a></p>
<p>今回のお題は「2」ということで、<br />
計算結果が一番「2」に近い式をあてるゲームを作りました。<br />
一応、スマホでも遊べます。</p>
<p><a href="https://crieit.now.sh/upload_images/cebd6c87a2c39e160ba6ee313b71e37d5f672cc56b863.gif" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/cebd6c87a2c39e160ba6ee313b71e37d5f672cc56b863.gif?mw=700" alt="close-to-2-overview.gif" /></a></p>
<p><a href="https://crieit.net/boards/web1week-202009/Close-to-2">イベントボードへの投稿</a>から引用。</p>
<blockquote>
<p>初級、中級、上級で遊べて、難易度が高いほど長い式になり、30秒で何問正答できるかな?というゲームです。<br />
自分で作っておいてなんですが、暗算得意じゃないと無理ゲーじゃねーかと思いました。</p>
</blockquote>
<h3 id="なんでこれを作ろうとしたのか"><a href="#%E3%81%AA%E3%82%93%E3%81%A7%E3%81%93%E3%82%8C%E3%82%92%E4%BD%9C%E3%82%8D%E3%81%86%E3%81%A8%E3%81%97%E3%81%9F%E3%81%AE%E3%81%8B">なんでこれを作ろうとしたのか</a></h3>
<p>こちらもボードへの投稿に書いていたので引用。</p>
<blockquote>
<p>元々は「2」に関するクロスワードを作ろうとしていたのですが、「2」縛りで問題を考えるのがしんどいの極みだったので変えました(しかも土曜...)<br />
その時点ですでに投稿されていた方(特に、きらぷかさんとDE-TEIUさん)の作品を見て、シンプルなゲームでも十分面白いものが作れるんだなぁと思い、今回の作品に至りました。<br />
(他の方の作品を参考にさせていただきましたが、丸パクリレベルになってしまわないよう気を付けたつもりです)</p>
</blockquote>
<h3 id="技術構成"><a href="#%E6%8A%80%E8%A1%93%E6%A7%8B%E6%88%90">技術構成</a></h3>
<p>最初は前回同様、Docker の Node.js コンテナを使おうとしてました。<br />
ただ、yarn コマンドやアプリ自体の動作速度、VSCode で開発する上での環境など色々考えたらめんどくさくなったので、WSL の Node.js を使いました。<br />
(フロント開発でわざわざDocker使う意味ある?という議論も見かけたことがあったので...)</p>
<p>本番は GitHub Pages でホスティングしています(gh-pages でデプロイ)</p>
<p>使用した主なライブラリはこちら<br />
- React:言わずと知れたUI構築ライブラリ(create-react-app で導入)<br />
- Material UI:UI コンポーネント集<br />
- React Share:各種 SNS のシェアボタン集<br />
- React Snap:SPA の OGP設定<br />
- PropTypes:props のバリデーション<br />
- ESLint:静的解析<br />
- Prettier:コードフォーマッター</p>
<p>前回同様、基本的な UI は Material UI で構築。<br />
一部、前回作ったコンポーネントを流用したものもあります。</p>
<h3 id="どんな感じで開発してたのか"><a href="#%E3%81%A9%E3%82%93%E3%81%AA%E6%84%9F%E3%81%98%E3%81%A7%E9%96%8B%E7%99%BA%E3%81%97%E3%81%A6%E3%81%9F%E3%81%AE%E3%81%8B">どんな感じで開発してたのか</a></h3>
<p>大体こんなでしたが、「8日目」があるところからわかるように、1日遅刻投稿です(苦笑)</p>
<h4 id="1日目:9/7(月)"><a href="#1%E6%97%A5%E7%9B%AE%EF%BC%9A9%2F7%28%E6%9C%88%29">1日目:9/7(月)</a></h4>
<p>「2」というお題を目にして、何を作るか悩みました。<br />
前回、React Konva というライブラリを使用したので、今回も何かライブラリを使おうかと Reactのライブラリを調べてました。</p>
<p>そこで <a target="_blank" rel="nofollow noopener" href="https://github.com/JaredReisinger/react-crossword">GitHub - React Crossword</a> という、クロスワードを作れるライブラリを見つけ、これで何かやろうかな?となりました。</p>
<h4 id="2日目:9/8(火)"><a href="#2%E6%97%A5%E7%9B%AE%EF%BC%9A9%2F8%28%E7%81%AB%29">2日目:9/8(火)</a></h4>
<p>クロスワードで何かするのであれば問題を考える必要があります。<br />
とりあえず環境構築だけしておこうと、この日はリポジトリ作って環境構築まで。</p>
<h4 id="3日目:9/9(水)"><a href="#3%E6%97%A5%E7%9B%AE%EF%BC%9A9%2F9%28%E6%B0%B4%29">3日目:9/9(水)</a></h4>
<p>技術構成のところで書いた通り、わざわざ Docker 環境を使うの微妙だなと思ったので WSL を使う方向へ移行。<br />
React Crossword を試してみたかったので試してみてました。</p>
<blockquote class="twitter-tweet">
<p lang="ja" dir="ltr">おおー、ライブラリでホントにクロスワードできたー。(公式のサンプルコード)<a target="_blank" rel="nofollow noopener" href="https://twitter.com/hashtag/web1week?src=hash&ref_src=twsrc%5Etfw">#web1week</a> <a target="_blank" rel="nofollow noopener" href="https://t.co/4dA2snPpzC">pic.twitter.com/4dA2snPpzC</a></p>— よし (@yoshi44_lion) <a target="_blank" rel="nofollow noopener" href="https://twitter.com/yoshi44_lion/status/1303696169445462017?ref_src=twsrc%5Etfw">September 9, 2020</a>
</blockquote>
<h4 id="4日目:9/10(木)"><a href="#4%E6%97%A5%E7%9B%AE%EF%BC%9A9%2F10%28%E6%9C%A8%29">4日目:9/10(木)</a></h4>
<p>「2」をいろんな言語の読み方にして成立するような問題を考えてました。</p>
<h4 id="5日目:9/11(金)"><a href="#5%E6%97%A5%E7%9B%AE%EF%BC%9A9%2F11%28%E9%87%91%29">5日目:9/11(金)</a></h4>
<p>この日も問題を考えるも、あまり進まず...。</p>
<h4 id="6日目:9/12(土)"><a href="#6%E6%97%A5%E7%9B%AE%EF%BC%9A9%2F12%28%E5%9C%9F%29">6日目:9/12(土)</a></h4>
<p>さすがに問題をどうにかしないと...と焦り出しました(遅い)</p>
<p>「2」をいろんな言語の読み方にして成立するような問題を考えていたものの、思いのほか難しい<br />
↓<br />
これ無理じゃね?<br />
↓<br />
読み方でなく「2」が含まれるものに関する問題(例:2月の和風月名 → キサラギ)に移行してみる<br />
↓<br />
縦読み、横読みを成立させるのが難しい<br />
↓<br />
これ無理じゃね?</p>
<p>みたいな感じになった結果、クロスワードは諦めることに...。</p>
<p>じゃあ、何を作ろうか?というところで、すでに投稿されていた他の参加者の方の作品を見て、シンプル路線にすることにしました。<br />
ここからやっと当作品を作り始めます。</p>
<blockquote class="twitter-tweet">
<p lang="ja" dir="ltr">元々、2に関するクロスワード作ろうとしてたけど、問題考えるのがしんどすぎて、他のものに切り替え中。明日、とりあえず公開できる程度になればいいなぁ。<a target="_blank" rel="nofollow noopener" href="https://twitter.com/hashtag/web1week?src=hash&ref_src=twsrc%5Etfw">#web1week</a></p>— よし (@yoshi44_lion) <a target="_blank" rel="nofollow noopener" href="https://twitter.com/yoshi44_lion/status/1304802019585060864?ref_src=twsrc%5Etfw">September 12, 2020</a>
</blockquote>
<h4 id="7日目:9/13(日)"><a href="#7%E6%97%A5%E7%9B%AE%EF%BC%9A9%2F13%28%E6%97%A5%29">7日目:9/13(日)</a></h4>
<p>何とか実装が進み、一旦デプロイしてみようとなりました。<br />
前回は Firebase Hosting を使用しましたが、今回は使うまでもないかなということで GitHub Pages にホスティングすることに。</p>
<p>GitHub Actions でデプロイする記事を見かけたので、それでやろうと試みるもビルドで失敗。<br />
どうも ESLint の react-hooks/exhaustive-deps ルールに引っ掛かっていたようです(.eslintrc 設定に含まれていなくても、ビルド時にチェックされる?)</p>
<p>対応方法を調べて試すも、なかなか解決しなかったので、一旦は無効化で対応(あまりよくないことでしょうが...)<br />
ビルドはパスするようになり、デプロイ自体は成功しているものの、なぜか真っ白なページしか表示されず...。</p>
<p>この時点でもうヘロヘロで瀕死だったので、この日は諦めて終わりました。</p>
<blockquote class="twitter-tweet">
<p lang="ja" dir="ltr">ゲームとしてはできてるんだけど、ESLint の新ルールにビルドを阻まれ、GitHub Pages にデプロイできる GitHub Actions 試そうとしたら、デプロイ自体は成功してるっぽいのに、URL にアクセスしても何も表示されないし。やり方悪いんかな...。もう疲れた...。一旦終わろう。 <a target="_blank" rel="nofollow noopener" href="https://twitter.com/hashtag/web1week?src=hash&ref_src=twsrc%5Etfw">#web1week</a></p>— よし (@yoshi44_lion) <a target="_blank" rel="nofollow noopener" href="https://twitter.com/yoshi44_lion/status/1305160798155673601?ref_src=twsrc%5Etfw">September 13, 2020</a>
</blockquote>
<h4 id="8日目:9/14(月)"><a href="#8%E6%97%A5%E7%9B%AE%EF%BC%9A9%2F14%28%E6%9C%88%29">8日目:9/14(月)</a></h4>
<p>gh-pages を使用して手動デプロイしてみても真っ白ページに。<br />
試しに S3 にデプロイしたら普通に動作したので、GitHub Pages の問題と切り分け。</p>
<p>真っ白ページの原因を特定。<br />
GitHub Pages 特有の注意点を見落としていただけだったようです。<br />
こちらの記事にまさしくなことが書いてありました。<br />
- <a target="_blank" rel="nofollow noopener" href="https://qiita.com/rhirayamaaan/items/cdbda70670157a8fb705">create-react-appとTypeScriptでサラッと作ったSPAをgh-pagesにスルッとデプロイすっぞ!</a></p>
<p>ただ、これでもなぜか GitHub Actions でのデプロイは真っ白<br />
ページのままだったので、以降は手動デプロイでやるように。</p>
<blockquote class="twitter-tweet">
<p lang="ja" dir="ltr">GitHub Pages のデプロイで動いたー。どうも package.json に homepage キーで URL を指定する必要があったらしい。そういえば Jekyll をデプロイするうえでの注意点とかで、前に見たことあったような…。なんか URL がずれておかしくなる的な。<a target="_blank" rel="nofollow noopener" href="https://twitter.com/hashtag/web1week?src=hash&ref_src=twsrc%5Etfw">#web1week</a></p>— よし (@yoshi44_lion) <a target="_blank" rel="nofollow noopener" href="https://twitter.com/yoshi44_lion/status/1305355374346661890?ref_src=twsrc%5Etfw">September 14, 2020</a>
</blockquote>
<p>無事動作確認できたし投稿するかと思いましたが、こういうのはやはり OGP 設定や Twitter シェア機能を付けた方がいいよなぁ...ということで対応することに。<br />
この時点でもヘロヘロ気味だったので、やっつけで対応して、日が変わる直前に何とか投稿しました。</p>
<blockquote class="twitter-tweet">
<p lang="ja" dir="ltr">なんとか形にできたー。計算結果が一番「2」に近い式をあてるゲームを作りました。よろしければ遊んでやってください🙏Close to 2 - (9/7~9/13)1週間でWebサービスを作るイベント - お題「2」 - Boards - Crieit <a target="_blank" rel="nofollow noopener" href="https://t.co/HDSKmfCsWo">https://t.co/HDSKmfCsWo</a> <a target="_blank" rel="nofollow noopener" href="https://twitter.com/hashtag/Crieit?src=hash&ref_src=twsrc%5Etfw">#Crieit</a> <a target="_blank" rel="nofollow noopener" href="https://twitter.com/hashtag/web1week?src=hash&ref_src=twsrc%5Etfw">#web1week</a></p>— よし (@yoshi44_lion) <a target="_blank" rel="nofollow noopener" href="https://twitter.com/yoshi44_lion/status/1305520904642805760?ref_src=twsrc%5Etfw">September 14, 2020</a>
</blockquote>
<h2 id="おおよそどんなことをやっているのか"><a href="#%E3%81%8A%E3%81%8A%E3%82%88%E3%81%9D%E3%81%A9%E3%82%93%E3%81%AA%E3%81%93%E3%81%A8%E3%82%92%E3%82%84%E3%81%A3%E3%81%A6%E3%81%84%E3%82%8B%E3%81%AE%E3%81%8B">おおよそどんなことをやっているのか</a></h2>
<p>特に変わったことはしてないと思いますので、ソースコード見てもらった方が早いかとー。<br />
- <a target="_blank" rel="nofollow noopener" href="https://github.com/h-yoshikawa44/close-to-2">GitHub - close-to-2</a></p>
<p>参考にさせていただいた記事はこちらなど<br />
- <a target="_blank" rel="nofollow noopener" href="https://keizokuma.com/js-array-object-sort/">JavaScriptで要素がオブジェクトの配列を日付や数値でソートする方法</a><br />
- <a target="_blank" rel="nofollow noopener" href="https://qiita.com/netebakari/items/7c1db0b0cea14a3d4419">JavaScriptで重複排除を自分で実装してはいけない(Setを使う)</a></p>
<h3 id="課題"><a href="#%E8%AA%B2%E9%A1%8C">課題</a></h3>
<p>React Hooks のこととか、もうちょっとわかったら、全体的にもっとスマートに書けるんでしょうかね...。なんかごちゃついてる感。</p>
<p>OGP はやっつけで5分くらいで作りました(笑)</p>
<p>Twitter シェアボタンは、公式のものを SPA で使うには少し工夫がいるということで、なんかうまくいきませんでした...。<br />
なので今回は React Share を頼りました。<br />
本当は Twitter アイコンとともにツイートと書かれたボタンにしたかったのですが、React Share だとできないっぽい?ということで妥協することに。</p>
<h2 id="今回参加してどうだったか"><a href="#%E4%BB%8A%E5%9B%9E%E5%8F%82%E5%8A%A0%E3%81%97%E3%81%A6%E3%81%A9%E3%81%86%E3%81%A0%E3%81%A3%E3%81%9F%E3%81%8B">今回参加してどうだったか</a></h2>
<p>楽しかったですが、前回以上にてんやわんやしてたなぁと(笑)<br />
特に最後の方。まぁ、自分が前半悠長にやってたせいなんですが。</p>
<p>Twitter シェアボタンを導入したおかげて、遊んでくださってる方の存在を確認することができました。<br />
確認できるとやはり嬉しいですね!</p>
<p>他の方の投稿を見るのも楽しい + 勉強になりますし、こういったイベントに参加するのはいい経験になりますね。<br />
web1week はほどよくゆるい感じの雰囲気があるので、敷居が低くて参加しやすいなと。<br />
また開催されることがありましたら、余程忙しくない限りはなるべく参加していきたいなーと思います。</p>
<p>ありがとうございました!</p>
<h2 id="参考リンクまとめ"><a href="#%E5%8F%82%E8%80%83%E3%83%AA%E3%83%B3%E3%82%AF%E3%81%BE%E3%81%A8%E3%82%81">参考リンクまとめ</a></h2>
<ul>
<li><a target="_blank" rel="nofollow noopener" href="https://github.com/JaredReisinger/react-crossword">GitHub - React CrossWord</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://keizokuma.com/js-array-object-sort/">JavaScriptで要素がオブジェクトの配列を日付や数値でソートする方法</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/netebakari/items/7c1db0b0cea14a3d4419">JavaScriptで重複排除を自分で実装してはいけない(Setを使う)</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/rhirayamaaan/items/cdbda70670157a8fb705">create-react-appとTypeScriptでサラッと作ったSPAをgh-pagesにスルッとデプロイすっぞ!</a></li>
</ul>
よし
tag:crieit.net,2005:PublicArticle/15920
2020-06-05T19:26:08+09:00
2020-06-05T19:26:08+09:00
https://crieit.net/posts/musico
好きな楽曲について語り合うウェブサービス「musico」を作ってみました。
<p>突然ですが、先々週に行われたweb1weekという1週間でウェブアプリを作って公開するオンラインハッカソンに参加しました。</p>
<p><a href="https://crieit.net/boards/web1week-202005">(5/18~5/24)1週間でWebサービスを作るイベント - お題「Like」</a></p>
<p>と、言いたいところだったのですが、実は2日目以降、家庭内のインシデント対応やそれに起因する自身の体調不良で、全く開発を進めることができませんでした(汗)</p>
<p>昨日あたりから、体調含めて状況が落ち着いてきたので、遅ればせながら開発を進め、ガっと最低限の所まで作り込み、勢いでドメイン取って公開してみました。</p>
<p>以下、今回開発したウェブアプリについて。</p>
<p>お題は「Like」でしたので、好きな楽曲についてあーだこーだ語り合えるサービスを作ってみました。</p>
<p>「<a target="_blank" rel="nofollow noopener" href="https://musi-co.fun">musico | find track you like and talk about it</a>」(何故か英語w)</p>
<p><a href="https://crieit.now.sh/upload_images/cbe4c4b65476b6a84552bce4ea4da34f5eda1a8c5fd28.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/cbe4c4b65476b6a84552bce4ea4da34f5eda1a8c5fd28.png?mw=700" alt="画面" /></a></p>
<p>フリーワードで楽曲を検索して、その楽曲にLikeやコメントをすることができます。</p>
<p>現時点での機能は以下、</p>
<h3 id="未ログイン時"><a href="#%E6%9C%AA%E3%83%AD%E3%82%B0%E3%82%A4%E3%83%B3%E6%99%82">未ログイン時</a></h3>
<ul>
<li>楽曲を検索、またはコメントがある楽曲リストを閲覧できる</li>
<li>楽曲のコメントページを閲覧できる</li>
<li>楽曲のプレビュー再生ができる(ファイルがあるもののみ)</li>
<li>Spotifyで楽曲を聞くことができる(別タブでウェブ版を表示)</li>
</ul>
<h3 id="ログイン時"><a href="#%E3%83%AD%E3%82%B0%E3%82%A4%E3%83%B3%E6%99%82">ログイン時</a></h3>
<p>上記に追加して、<br />
- 楽曲にコメントできる<br />
- 楽曲にLikeできる</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://youtu.be/sDVK6O61_S0">https://youtu.be/sDVK6O61_S0</a></p>
<p>各楽曲、コメントごとのページをSNSにシェアすることも出来ます。</p>
<p><a href="https://crieit.now.sh/upload_images/f3869d86a81591bf66abf5f407661d895eda1b78e57e3.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/f3869d86a81591bf66abf5f407661d895eda1b78e57e3.png?mw=700" alt="image" /></a><br />
<a target="_blank" rel="nofollow noopener" href="https://musi-co.fun/tracks/6K53GM9W6Vle5KBwGFVnZM">https://musi-co.fun/tracks/6K53GM9W6Vle5KBwGFVnZM</a></p>
<p>開発時の様々や動機はなどは下記のハッカソンのボードに書きました。<br />
<a href="https://crieit.net/boards/web1week-202005/musico">https://crieit.net/boards/web1week-202005/musico</a></p>
<h2 id="技術的なこと"><a href="#%E6%8A%80%E8%A1%93%E7%9A%84%E3%81%AA%E3%81%93%E3%81%A8">技術的なこと</a></h2>
<p>技術スタック的にはFirebase+Vuejs(Nuxtjs)という、マネージドなサーバレス環境とコンポーネント思考なリアクティブフレームワークを組み合わせたJAMstackな流行りの構成です。</p>
<p>最初はホスティングにNetlifyを選択したのですが、SSR周りやFirestoreなど他に使ってるサービスとの兼ね合いも考えて、Firebase全乗っかりな構成にしました。</p>
<p>UIは最近良く使ってるBulmaのコンポーネントをほぼほぼそのまま使ってる感じです。</p>
<p>本業ではAWSがっちり使って、コンテナとかサーバレスとかマネージドサービスを組み合わせたサーバサイド寄りの開発が多いので、個人開発ではフロントエンド寄りの技術を選定しがちですね(汗)</p>
<p>あんまり凝ったことはしていないのですが、開発時に得た知見は今後すこしずつ書いていこうと思っています。</p>
<p>それではまた。</p>
<hr />
<h3 id="PS."><a href="#PS.">PS.</a></h3>
<p>祝Crieit初投稿!!</p>
ぷろみつ
tag:crieit.net,2005:PublicArticle/15915
2020-05-29T13:19:06+09:00
2021-02-15T14:36:20+09:00
https://crieit.net/posts/ac044ef3dd9b2580f6a86c0ac05881c1
ハッカソンイベントで、React Konva製のジグソーパズルっぽいパズルを作ってみた
<p>個人開発って何作るか悩んだり、モチベを保つのが難しかったりしますよね。<br />
そんな自分が、先日web1weekというイベントで個人開発にチャレンジしました。<br />
何とかリリースまでできたので、使用した技術やどんなことをやったのかといった内容を残しておきます。</p>
<h2 id="はじめに"><a href="#%E3%81%AF%E3%81%98%E3%82%81%E3%81%AB">はじめに</a></h2>
<h3 id="web1weekって?"><a href="#web1week%E3%81%A3%E3%81%A6%EF%BC%9F">web1weekって?</a></h3>
<p>簡単に言うと「1週間でお題に沿ったWebサービスを作ってみよう」というイベントです。<br />
当コミュニティ Crieit の運営者である、だらさん主催で行われました。</p>
<blockquote class="twitter-tweet">
<p lang="ja" dir="ltr">5/18~5/24の1週間でWebサービスを作るイベントです! Hello worldレベルのサービスでもOKですのでぜひご参加をお願いします!
<a target="_blank" rel="nofollow noopener" href="https://twitter.com/hashtag/%E9%A7%86%E3%81%91%E5%87%BA%E3%81%97%E3%82%A8%E3%83%B3%E3%82%B8%E3%83%8B%E3%82%A2?src=hash&ref_src=twsrc%5Etfw">#駆け出しエンジニア</a>
仲間が多い方は是非シェアもお願いします~お題は「Like」です。
<a target="_blank" rel="nofollow noopener" href="https://t.co/ORZQGb6Yu2">https://t.co/ORZQGb6Yu2</a>
<a target="_blank" rel="nofollow noopener" href="https://twitter.com/hashtag/Crieit?src=hash&ref_src=twsrc%5Etfw">#Crieit</a>
<a target="_blank" rel="nofollow noopener" href="https://twitter.com/hashtag/web1week?src=hash&ref_src=twsrc%5Etfw">#web1week</a>
<a target="_blank" rel="nofollow noopener" href="https://twitter.com/hashtag/StayHome?src=hash&ref_src=twsrc%5Etfw">#StayHome</a>
</p>— だら🎄サービスづくりひたすら (@dala00)
<a target="_blank" rel="nofollow noopener" href="https://twitter.com/dala00/status/1262035532386689025?ref_src=twsrc%5Etfw">May 17, 2020</a>
</blockquote>
<h3 id="参加しようと思ったきっかけ"><a href="#%E5%8F%82%E5%8A%A0%E3%81%97%E3%82%88%E3%81%86%E3%81%A8%E6%80%9D%E3%81%A3%E3%81%9F%E3%81%8D%E3%81%A3%E3%81%8B%E3%81%91">参加しようと思ったきっかけ</a></h3>
<p>主な背景としては<br />
- 個人開発に使える時間があった<br />
- 前回の開催時も面白そうと気になっていた(今回は2回目の開催)<br />
- 個人開発をやってみたかった(+こういったイベントならモチベも保てるかなと思った)<br />
といった感じです。</p>
<p>個人開発は以前からやりたいと思うことはありつつ、結局モチベが続かなくて止めてしまったりということが多かったのです...。<br />
もし何か作ることができたら、1つの実績にできて自分の財産になるかなーと思いました。</p>
<p>これは...やるしかない。</p>
<p>ということで参加。</p>
<h2 id="自分が作ったもの"><a href="#%E8%87%AA%E5%88%86%E3%81%8C%E4%BD%9C%E3%81%A3%E3%81%9F%E3%82%82%E3%81%AE">自分が作ったもの</a></h2>
<p><a target="_blank" rel="nofollow noopener" href="https://jigsaw-like-puzzle.web.app/">Jigsaw Like Puzzle</a></p>
<p>ちょっとしたパズルを作りました。<br />
PC向け。スマホ対応は現状してません。</p>
<p><a href="https://crieit.now.sh/upload_images/d57cb2f3cdd1b7e92ccbbb0fa0d4565e5ed0aab957fa9.gif" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/d57cb2f3cdd1b7e92ccbbb0fa0d4565e5ed0aab957fa9.gif?mw=700" width="80%" alt="パズルのプレイ画面GIF"></a></p>
<h3 id="なんでパズル?"><a href="#%E3%81%AA%E3%82%93%E3%81%A7%E3%83%91%E3%82%BA%E3%83%AB%EF%BC%9F">なんでパズル?</a></h3>
<p><a href="https://crieit.net/boards/web1week-202005/41152da9fccdaae73e45967feced2e2a">イベント記事への投稿</a>にも書いていたので引用します。</p>
<blockquote>
<p>「Like」ということで<br />
好きなもの → 趣味とかかな? → 絵を描くこと(最近全然描いてないけど) → Canvasでお絵描き実装ができるらしい → でも、ただ絵を描くだけじゃつまらない → もしかしてジグソーパズル作れそう? といった感じで行きつきました。</p>
<p>ジグソーパズル特有の形は再現できてないので、あくまで「ジグソーパズルっぽいもの」ですね。<br />
完全に後付け理由ですが、Like って「~ようなもの」って意味もありますし、意図せずテーマに沿ったものになりました(笑)<br />
こんなことあるんですねー。</p>
</blockquote>
<h3 id="構成"><a href="#%E6%A7%8B%E6%88%90">構成</a></h3>
<p>構成図描こうかなとも思ったんですが、大した構成でもないのでざっくり文面で書きます。</p>
<p>開発はDockerのNode.jsコンテナで。<br />
本番はFirebase Hostingでホスティングしています。</p>
<p>使用した主なライブラリはこちら(正確には他にもあります)<br />
- React:言わずと知れたUI構築ライブラリ(create-react-appで導入)<br />
- React Konva:Canvasを扱うライブラリであるKonva.jsのReact版<br />
- React Router:ルーティング<br />
- Material UI:UIコンポーネント集<br />
- PropTypes:propsのバリデーション<br />
- ESLint:静的解析<br />
- Prettier:コードフォーマッター</p>
<p>基本的なUIはMaterial UIで構築しました。<br />
Boxコンポーネントがすごい便利でしたね。おかげであまりCSSを書かずにすみました。divがその分増えましたが。<br />
パズル部分はReact Konvaで構築しています。</p>
<p>React KonvaとFirebaseについては初めて使ったので、まだまだちゃんとわかってないことも多いです。</p>
<h3 id="どんな感じで開発してたのか"><a href="#%E3%81%A9%E3%82%93%E3%81%AA%E6%84%9F%E3%81%98%E3%81%A7%E9%96%8B%E7%99%BA%E3%81%97%E3%81%A6%E3%81%9F%E3%81%AE%E3%81%8B">どんな感じで開発してたのか</a></h3>
<p>ざっくりこんな感じでした。</p>
<h4 id="1日目:5/18(月)"><a href="#1%E6%97%A5%E7%9B%AE%EF%BC%9A5%2F18%28%E6%9C%88%29">1日目:5/18(月)</a></h4>
<p>お題に沿って何を作るか案がなかなか出てこなかったです。<br />
React Konvaに行きついてからドキュメントをひたすら読んで、夕方くらいにやっとパズルいけるかも?ってなりました。</p>
<h4 id="2日目:5/19(火)"><a href="#2%E6%97%A5%E7%9B%AE%EF%BC%9A5%2F19%28%E7%81%AB%29">2日目:5/19(火)</a></h4>
<p>午後からやっと手元の環境で動かしてみました。<br />
色々試してみて、なんとなくいけそうかな?という手ごたえがありました。</p>
<blockquote class="twitter-tweet">
<p lang="ja" dir="ltr">パズルっぽいものを作成チャレンジ。
<a target="_blank" rel="nofollow noopener" href="https://twitter.com/hashtag/web1week?src=hash&ref_src=twsrc%5Etfw">#web1week</a>
<a target="_blank" rel="nofollow noopener" href="https://t.co/JL4D2noURE">pic.twitter.com/JL4D2noURE</a>
</p>
— よし (@yoshi44_lion)
<a target="_blank" rel="nofollow noopener" href="https://twitter.com/yoshi44_lion/status/1262635512889610241?ref_src=twsrc%5Etfw">May 19, 2020</a>
</blockquote>
<p>ただ、そこからパズルの元となる画像の適切なサイズ割り出しに時間かかりました...。3パターンできれいに割れて、適度な大きさのサイズがいいよねってなりまして。</p>
<h4 id="3日目:5/20(水)"><a href="#3%E6%97%A5%E7%9B%AE%EF%BC%9A5%2F20%28%E6%B0%B4%29">3日目:5/20(水)</a></h4>
<p>ようやく画像サイズを決定。</p>
<p>720 * 480<br />
初級:120 * 120 → 6 * 4 = 24<br />
中級:80 * 80 → 9 * 6 = 54<br />
上級:60 * 60 → 12 * 8 = 96</p>
<p>その後、ストップウォッチの実装をどうやるかめっちゃ悩みました...。<br />
記事を参考にしながら試すも、うまくいかずドはまり。<br />
時間かかりつつも一応実装できました。</p>
<p>この時点でいまだにリポジトリを作っていなかったので、とりあえず作成だけ。</p>
<h4 id="4日目:5/21(木)"><a href="#4%E6%97%A5%E7%9B%AE%EF%BC%9A5%2F21%28%E6%9C%A8%29">4日目:5/21(木)</a></h4>
<p>ルーティングやOGP、画像の取り扱いをどうしようか悩みました。<br />
結果的には、最低限遊べるレベルのリリースができればいいやということで、一旦は妥協することにしました。</p>
<p>午後からやっとプロジェクトのセットアップ。<br />
Issueやプルリクのテンプレ、Dockerで開発環境構築ともろもろ必要な準備を整えました。</p>
<p>ピース位置チェック(正解位置に置かれたらはまる)のやり方がなんとなくわかって、よーし作っていくぞーという流れへ。</p>
<h4 id="5日目:5/22(金)"><a href="#5%E6%97%A5%E7%9B%AE%EF%BC%9A5%2F22%28%E9%87%91%29">5日目:5/22(金)</a></h4>
<p>パズル画面に背景テクスチャをいれようとしましたが、迷ったので結局止めました。</p>
<p>黙々と進めて、ぼんやりとしたイメージで作っていった画面のモックがおおよそできました。</p>
<blockquote class="twitter-tweet">
<p lang="ja" dir="ltr">やっとモック的なものができた
<a target="_blank" rel="nofollow noopener" href="https://twitter.com/hashtag/web1week?src=hash&ref_src=twsrc%5Etfw">#web1week</a>
<a target="_blank" rel="nofollow noopener" href="https://t.co/r9GK4fwEuM">pic.twitter.com/r9GK4fwEuM</a>
</p>
— よし (@yoshi44_lion)
<a target="_blank" rel="nofollow noopener" href="https://twitter.com/yoshi44_lion/status/1263709728640610305?ref_src=twsrc%5Etfw">May 22, 2020</a>
</blockquote>
<p>モックに続いて、難易度選択モーダル、ピース生成ロジックを作成。</p>
<h4 id="6日目:5/23(土)"><a href="#6%E6%97%A5%E7%9B%AE%EF%BC%9A5%2F23%28%E5%9C%9F%29">6日目:5/23(土)</a></h4>
<p>ポーズモーダル、クリアモーダルを作成。</p>
<p>ここまでで、とりあえず最低限遊べることは確認できたのでリリースすることに。<br />
コンポーネントを多少分けてはいましたが、ファイル分割とか全然できてなかったので、そこまでリファクタやってからリリースするかとも考えたのですが、もうやっちゃえとなりました。</p>
<p>ホスティングに関しては、多少慣れてるNetlifyでやる手もあるなと思いました。<br />
ただ、今後機能拡張していくとしたらFirebaseの方が色々やれてよさそうと思い、こちらにすることにしました。</p>
<p>リリース(v0.1.0)して投稿。<br />
リリースと言いつつ、色々と足りてないものがまだあるのでプレリリースみたいなものですね。</p>
<blockquote class="twitter-tweet">
<p lang="ja" dir="ltr">パズルリリースしましたー。
<a target="_blank" rel="nofollow noopener" href="https://t.co/028WAbS7W9">https://t.co/028WAbS7W9</a>
よかったら遊んでみてください。
PC向け。簡易的なものなので、クオリティはご容赦ください🙏(5/18~5/24)1週間でWebサービスを作るイベント - お題「Like」 - Boards の投稿 - Crieit
<a target="_blank" rel="nofollow noopener" href="https://t.co/FldwX4odRN">https://t.co/FldwX4odRN</a>
<a target="_blank" rel="nofollow noopener" href="https://twitter.com/hashtag/Crieit?src=hash&ref_src=twsrc%5Etfw">#Crieit</a> <a target="_blank" rel="nofollow noopener" href="https://twitter.com/hashtag/web1week?src=hash&ref_src=twsrc%5Etfw">#web1week</a>
<a target="_blank" rel="nofollow noopener" href="https://twitter.com/hashtag/StayHome?src=hash&ref_src=twsrc%5Etfw">#StayHome</a>
</p>— よし (@yoshi44_lion)
<a target="_blank" rel="nofollow noopener" href="https://twitter.com/yoshi44_lion/status/1264144419818573825?ref_src=twsrc%5Etfw">May 23, 2020</a>
</blockquote>
<h2 id="おおよそどんなことをやっているのか"><a href="#%E3%81%8A%E3%81%8A%E3%82%88%E3%81%9D%E3%81%A9%E3%82%93%E3%81%AA%E3%81%93%E3%81%A8%E3%82%92%E3%82%84%E3%81%A3%E3%81%A6%E3%81%84%E3%82%8B%E3%81%AE%E3%81%8B">おおよそどんなことをやっているのか</a></h2>
<p>ソースコードはリファクタやったりして変わる可能性があるので、リポジトリ見ていただいた方が確実かもしれません(あまりきれいなコードではないですが...)<br />
一応、Issue書いたりしながら進めました。<br />
- <a target="_blank" rel="nofollow noopener" href="https://github.com/h-yoshikawa44/jigsaw-like-puzzle">GitHub - jigsaw-like-puzzle</a></p>
<p>以下の内容は執筆時点(v0.1.4)での実装のものとなります。<br />
有識者の方からすると、この実装イケてないとかあると思いますがご容赦ください。</p>
<h3 id="ストップウォッチ"><a href="#%E3%82%B9%E3%83%88%E3%83%83%E3%83%97%E3%82%A6%E3%82%A9%E3%83%83%E3%83%81">ストップウォッチ</a></h3>
<p><a href="https://crieit.now.sh/upload_images/7296a1e13c3ba9f86e222e09f2c2e8c85ed0abacd6412.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/7296a1e13c3ba9f86e222e09f2c2e8c85ed0abacd6412.png?mw=700" width="80%" alt="パズル画面のストップウォッチ部分画像"></a></p>
<h4 id="概要"><a href="#%E6%A6%82%E8%A6%81">概要</a></h4>
<p><code>setInterval()</code>と<code>clearInterval()</code>を使って実装。<br />
恥ずかしながら自分はこの関数を使用したことがなかったこともあり、最初はストップウォッチってどうやって実装したらいいんだろう?という状態でした。</p>
<p>実装のやり方についてはこちらの記事をとても参考にさせていただきました。<br />
- <a target="_blank" rel="nofollow noopener" href="https://blitzgate.co.jp/blog/805/">【1から始めるReact】ストップウォッチを作る</a></p>
<p>1秒ごとに秒数カウントを+1して、その秒数カウントをもとに時、分、秒を計算して更新していくというものです。</p>
<h4 id="問題点"><a href="#%E5%95%8F%E9%A1%8C%E7%82%B9">問題点</a></h4>
<p>同画面で開始と停止を行う上では問題なかったのですが、今回の場合は<br />
- パズル画面の「一時停止」ボタンを押す → ストップウォッチを停止してポーズモーダルを開く<br />
- ポーズモーダルの「復帰」ボタンを押す → ポーズモーダルを閉じて、ストップウォッチ再開</p>
<p>という仕様だったので、再開時に時間が最初からになってしまう問題が起きました。</p>
<p>秒数カウントはstateで管理してない変数だったので、再レンダリング時に値がリセットされていたんだろうなと。<br />
そのため、秒数カウントの値をバックアップを取っておく感じでstateでも保持するようにしました。<br />
1秒ごとの更新処理の際に、秒数カウント(変数)が0 だったらstateを確認して、バックアップがあればそこから復元するイメージです。</p>
<p>秒数カウントを最初からstateで管理すればいいのでは?となるかもしれませんが、stateでやるとうまく動いてくれなかったので、こういった形をとりました。</p>
<h3 id="パズル"><a href="#%E3%83%91%E3%82%BA%E3%83%AB">パズル</a></h3>
<p><a href="https://crieit.now.sh/upload_images/a753b71f782fdb5bf171c87c58829a4d5ed0ac2d7633b.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/a753b71f782fdb5bf171c87c58829a4d5ed0ac2d7633b.png?mw=700" width="80%" alt="パズル画面のパズル部分画像"></a></p>
<h4 id="概要"><a href="#%E6%A6%82%E8%A6%81">概要</a></h4>
<p>React Konvaで実装していますが、その実体はCanvasです。<br />
Canvasを扱う<code>Konva.js</code>というライブラリがあり、そのReact版だそうです。</p>
<p>Canvasを扱うには<code>Konva.js</code>が便利らしいみたいな記事は複数見かけたんですが、自分はCanvas自体を使ったことがなかったため、いまいちピンとこず...。<br />
なので、最初はひたすらドキュメントを読んで、おおよそどんなことができるものなのかを見ていきました。<br />
その結果、パズルいけそうだなという目途がついたので使ってみたという背景があります。</p>
<p>Konva.jsの構造としては、以下のようになっています(<a target="_blank" rel="nofollow noopener" href="https://konvajs.org/docs/overview.html">公式</a>より引用)</p>
<p><a href="https://crieit.now.sh/upload_images/d4558c875c94cb3594a9cd556d3393435ed0ac8deeece.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/d4558c875c94cb3594a9cd556d3393435ed0ac8deeece.png?mw=700" width="60%" alt="Konva.jsの構造画像"></a></p>
<p>Shapeは複数種類があり<br />
- Rect(長方形)<br />
- Circle(円)<br />
- Ellipse(楕円)<br />
- Line(線)<br />
- Image(画像)<br />
- Text(テキスト)<br />
- Star(星)</p>
<p>などがあてはまります。</p>
<p>これらは上位の要素を基準とした x、y座標であったり、横幅、縦幅、色、影などを指定することで、Canvasに描画をしていけるようになっています。</p>
<h4 id="パズルの元画像"><a href="#%E3%83%91%E3%82%BA%E3%83%AB%E3%81%AE%E5%85%83%E7%94%BB%E5%83%8F">パズルの元画像</a></h4>
<p>Imageコンポーネントを使用。<br />
普通に画像パスを渡すのではダメらしく、<code>use-image</code>ライブラリの<code>useImage</code>フックを使って生成した、DOM画像を渡すようにしています。<br />
- <a target="_blank" rel="nofollow noopener" href="https://github.com/konvajs/use-image">GitHub - use-image</a></p>
<p>ちなみにピース数の計算との兼ね合いで、画像サイズおよびこのImageコンポーネントのサイズは 720 * 480 で固定しています。</p>
<h4 id="パズルの額縁"><a href="#%E3%83%91%E3%82%BA%E3%83%AB%E3%81%AE%E9%A1%8D%E7%B8%81">パズルの額縁</a></h4>
<p>Lineコンポーネントを使用。<br />
4つの点の座標を指定して繋ぐことで図形を描画。これを上下左右で4つ作成しています。<br />
ただの塗りつぶしだと安っぽくなるので、グラデーション指定にしてみました。</p>
<h4 id="パズルのピース"><a href="#%E3%83%91%E3%82%BA%E3%83%AB%E3%81%AE%E3%83%94%E3%83%BC%E3%82%B9">パズルのピース</a></h4>
<p>Imageコンポーネントを使用。<br />
<code>useImage</code>フックによる DOM 画像を渡しているのは同様ですが、cropを指定することで画像の切り抜きをしています。</p>
<p>例として初級の場合であれば、ピースサイズは 120 * 120 なので<br />
1行目<br />
- {x:0 y:0 width:120 height:120}<br />
- {x:120 y:0 width:120 height:120}<br />
- {x:240 y:0 width:120 height:120}<br />
.<br />
.<br />
.</p>
<p>2行目<br />
- {x:0 y:120 width:120 height:120}<br />
- {x:120 y:120 width:120 height:120}<br />
- {x:240 y:120 width:120 height:120}<br />
.<br />
.<br />
.</p>
<p>みたいな感じです。<br />
合わせてコンポーネント自体のサイズも 120 * 120 を指定になります。</p>
<p>draggableを有効化してドラッグアンドドロップができるように。<br />
そのうえ、onDragStartとonDragEndでイベント処理を実装しています。</p>
<h4 id="ピースドラッグ時の挙動"><a href="#%E3%83%94%E3%83%BC%E3%82%B9%E3%83%89%E3%83%A9%E3%83%83%E3%82%B0%E6%99%82%E3%81%AE%E6%8C%99%E5%8B%95">ピースドラッグ時の挙動</a></h4>
<p>scaleを変えて、少しだけピースが大きくなるようになっています。<br />
(公式デモのコードそのまま持ってきた感じです)</p>
<p>それに加えて、ドラッグしているピースが必ず最前面に来るような処理をしています。<br />
Canvas要素は、あとに定義したものが前面に来るようになっているようです。<br />
そのため、この処理をやらないと場合によっては、はめ込まれたピースの背面にドラッグ中のピースが隠れてしまい操作不能になってしまうことがあります。<br />
そんなことなったら一気に萎えちゃいますよね。</p>
<h4 id="ピースドロップ時の挙動"><a href="#%E3%83%94%E3%83%BC%E3%82%B9%E3%83%89%E3%83%AD%E3%83%83%E3%83%97%E6%99%82%E3%81%AE%E6%8C%99%E5%8B%95">ピースドロップ時の挙動</a></h4>
<p>scaleを元に戻します。durationも設定してるので、ポヨンと大きさが戻るような見た目になってます。<br />
(これも公式デモのコードをそのまま持ってきた感じです)</p>
<p>加えて、ドロップされた座標と正解位置の座標を比較。<br />
誤差の範囲内であれば、draggableを無効 + ピースの座標を正解位置の座標に更新 することで、ピースがはめこまれるような挙動を実現しています。<br />
この処理はこちらの公式デモを参考にしました。<br />
- <a target="_blank" rel="nofollow noopener" href="https://konvajs.org/docs/sandbox/Animals_on_the_Beach_Game.html">Konva.js - Animals on the Beach Game</a></p>
<h3 id="ゲームの流れ"><a href="#%E3%82%B2%E3%83%BC%E3%83%A0%E3%81%AE%E6%B5%81%E3%82%8C">ゲームの流れ</a></h3>
<p>これまでの内容を踏まえて、ゲームの流れとしてはおおまかにこんな感じです。<br />
(並列で処理しているところもあります)</p>
<p>難易度選択<br />
↓<br />
難易度に応じたピース数(縦、横)、ピースサイズの値をセット<br />
↓<br />
この3つの値の変更を検知して、初期化ロジック実行<br />
ピースの情報を持ったオブジェクトの配列を生成後、その順番をシャッフル<br />
↓<br />
ピースの情報を持ったオブジェクトの配列をもとにピースのコンポーネントが描画される<br />
↓<br />
ゲーム開始(ストップウォッチ開始)<br />
↓<br />
ピースのドラッグアンドドロップ<br />
ドロップ座標が正解位置の座標の誤差範囲であればはめこまれ、正解ピース数が+1される<br />
(これを全てのピースがはめ込まれるまで繰り返す)<br />
↓<br />
正解ピース数の値の変更を検知して、総ピース数と一致すればクリア(ストップウォッチ停止)</p>
<h2 id="今回参加してみてどうだったか"><a href="#%E4%BB%8A%E5%9B%9E%E5%8F%82%E5%8A%A0%E3%81%97%E3%81%A6%E3%81%BF%E3%81%A6%E3%81%A9%E3%81%86%E3%81%A0%E3%81%A3%E3%81%9F%E3%81%8B">今回参加してみてどうだったか</a></h2>
<p>楽しかったです!ただ、疲れました(笑)<br />
お題があったとはいえ、ほぼ一から自分で考えて作る必要があったので、普段よりもいっぱい頭使ったからかなと思います。</p>
<p>とはいえ、個人開発として無事にリリースまでできたのはこれが初めてなので素直に嬉しいですね。<br />
他の方の投稿を見るのも楽しいですし、学ばせていただく機会にもなりました。<br />
こういった機会を設けてくださり、ありがとうございました!</p>
<hr />
<p>ちょっとしたレポート記事を書くつもりがすっかり長くなってしまいました...。<br />
ここまで読んでくださった方、ありがとうございます!</p>
<p>パズルの方は今後も合間を見つけて改修していこうかなと思ってます。<br />
ちなみに最初に導入したGoogle Analyticsがちゃんと動いておらず、投稿時のアクセス数を見られなかったというヘマをやらかしていましたが、修正して現在は無事に動いてます(冷や汗)</p>
<p>もしお暇な時があれば、パズル部屋を覗いてみてください。</p>
<h2 id="参考リンクまとめ"><a href="#%E5%8F%82%E8%80%83%E3%83%AA%E3%83%B3%E3%82%AF%E3%81%BE%E3%81%A8%E3%82%81">参考リンクまとめ</a></h2>
<ul>
<li><a target="_blank" rel="nofollow noopener" href="https://blitzgate.co.jp/blog/805/">【1から始めるReact】ストップウォッチを作る</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://konvajs.org/docs/react/">Konva.js</a></li>
</ul>
よし
tag:crieit.net,2005:PublicArticle/15911
2020-05-25T01:34:59+09:00
2020-05-25T01:35:56+09:00
https://crieit.net/posts/a-is-like-b
とっさのスピーチ力とか説得力を培うことのできる、A is like Bゲームを作りました
<p>とっさに誰かを説得しなければならなくなったり、何かを伝えなくてはいけなくなるような状況って結構ありますよね。<br />
そんな力を培うことのできる練習ができるようなサービスを作りました!</p>
<p>お題をランダムに出題させ、それの回答を書いて、<br />
Twitterにシェアしてみてください!(140文字以内じゃないとツイートできないと思います!)</p>
<p>ゲームはこれです↓↓<br />
<a target="_blank" rel="nofollow noopener" href="https://a-is-like-b.netlify.app">A is like B ゲーム</a></p>
<p>初の #web1week チャレンジです! 完成できてよかった。</p>
<p><a href="https://crieit.now.sh/upload_images/cddcda62b0dd18f5b2d61216700383da5eca9fadc06e2.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/cddcda62b0dd18f5b2d61216700383da5eca9fadc06e2.png?mw=700" alt="image.png" /></a></p>
<h1 id="このゲームはなに?"><a href="#%E3%81%93%E3%81%AE%E3%82%B2%E3%83%BC%E3%83%A0%E3%81%AF%E3%81%AA%E3%81%AB%EF%BC%9F">このゲームはなに?</a></h1>
<p>とっさの場面、上司やお客さんからの新しい話を聞いたとき、さっと主張しないといけない場面なのに、<br />
何を言えばいいのか思いつかなかったり、考えるのに時間がかかってしまったり。。。<br />
そんなことって、よくありますよね。</p>
<p>この「A is like B ゲーム」は、英語ディベートをやっていた学生の頃、私が先輩に教えてもらった方法です。<br />
「A is like B」、つまり「AはBに似ている」、もしくは「AはまるでBだ」、という主張を、とっさに考えて言葉にする練習ができます。<br />
よく大学への行き帰りの自転車をこぎながら、ぶつぶつとこの練習をしていたことを覚えています。</p>
<p>私が学生時代にやっていた英語ディベートは「パーラメンタリーディベート」といって、<br />
いわゆる長期間文献を調べて臨むディベートとは違い、その日、その場でお題が出題されます。<br />
20分ほどの準備時間で何を主張するのかを準備して臨むのです。</p>
<p>そのため、思考の瞬発力と、豊かな表現力が求められます。<br />
真面目に練習しなかった私はとても弱く全然勝てませんでしたが、練習は楽しかったです。</p>
<h1 id="意外と知られてない方法?"><a href="#%E6%84%8F%E5%A4%96%E3%81%A8%E7%9F%A5%E3%82%89%E3%82%8C%E3%81%A6%E3%81%AA%E3%81%84%E6%96%B9%E6%B3%95%EF%BC%9F">意外と知られてない方法?</a></h1>
<p>ネットで検索してみると、「A is like B」というワードはおそらくなさそうでしたので、<br />
もしかすると、くだんの先輩が作った練習方法なのかもしれませんが、<br />
この練習は非常に基礎練習として有効だなぁと思っていました。</p>
<h1 id="開発について"><a href="#%E9%96%8B%E7%99%BA%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6">開発について</a></h1>
<p>今回、 #web1week のお題が「like」ということもあり、<br />
天の邪鬼ワタクシ、シンプルにいいねやスキの意味のlikeではなく、<br />
「~のようだ」「まるで~だ」という前置詞のlikeを採用してみました。</p>
<p>今回は使い慣れたNuxtを利用し、またNetlifyにホスティングしてます。<br />
本当はデータベースと連動させていろいろやりたかったのですが<br />
Firebaseの設定がどうにもうまくできなかったので、こちらも使い慣れているNetlifyにしちゃいました。</p>
<p>今後、まだまだ直したい改修予定としては、<br />
ログインできたり、記録できたり、また時間制限などを追加したいと思っています。</p>
<p>まずはTwitterに投稿できるシステムだけつけてみました。<br />
楽しみながらスピーチの練習や、説得力の向上をしていただけたらと思います。<br />
ありがとうございました。</p>
<p>りんご</p>
<p><a target="_blank" rel="nofollow noopener" href="https://a-is-like-b.netlify.app">A is like B ゲーム</a></p>
りんご🍎@エンジニア/個人開発がんばりんご
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/15766
2020-03-14T22:03:11+09:00
2020-03-14T22:04:12+09:00
https://crieit.net/posts/1bbf25a7cba4095f0fd1afe817b553dd
(改良版)パワプロ風画面ジェネレータを作ってみた
<p><a href="https://crieit.now.sh/upload_images/102e92a702e89059033b1dff5b0f87c55e6cd23db0d46.jpg" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/102e92a702e89059033b1dff5b0f87c55e6cd23db0d46.jpg?mw=700" alt="" /></a></p>
<h1 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></h1>
<p><a target="_blank" rel="nofollow noopener" href="https://pawapro-gen.netlify.com/">https://pawapro-gen.netlify.com/</a></p>
<h1 id="一旦書いたボード"><a href="#%E4%B8%80%E6%97%A6%E6%9B%B8%E3%81%84%E3%81%9F%E3%83%9C%E3%83%BC%E3%83%89">一旦書いたボード</a></h1>
<p><a href="https://crieit.net/boards/web1week-202003/6fbd2929361eb9caa53fc33d51d31f36">web1week - パワプロ風画面ジェネレータを作ってみた</a></p>
<p>時間的に残りの機能間に合わないかと思っていたので、最低限動く機能で出したのですが、結論から言うと間に合いました。</p>
<h1 id="インライン編集機能"><a href="#%E3%82%A4%E3%83%B3%E3%83%A9%E3%82%A4%E3%83%B3%E7%B7%A8%E9%9B%86%E6%A9%9F%E8%83%BD">インライン編集機能</a></h1>
<p>当初はフォームとプレビュー機能を分けて作っていたのですが、<br />
ほとんど全てインラインで編集できるように変更しました。</p>
<h1 id="twitterシェア機能"><a href="#twitter%E3%82%B7%E3%82%A7%E3%82%A2%E6%A9%9F%E8%83%BD">twitterシェア機能</a></h1>
<h2 id="OGPサーバの構築"><a href="#OGP%E3%82%B5%E3%83%BC%E3%83%90%E3%81%AE%E6%A7%8B%E7%AF%89">OGPサーバの構築</a></h2>
<p>以前<a href="https://crieit.net/posts/slack">slack流量計</a>を作った際に、web表彰のスクリーンショットを撮っていたので、OGP作成にはそれほど抵抗はありませんでした。</p>
<p>あとはだらさんの作成されてる<a target="_blank" rel="nofollow noopener" href="https://github.com/dala00/puppeteer-ogp">OGPサーバ</a>を参考にさせていただきました。</p>
<h3 id="OGPサーバの構築に使った技術"><a href="#OGP%E3%82%B5%E3%83%BC%E3%83%90%E3%81%AE%E6%A7%8B%E7%AF%89%E3%81%AB%E4%BD%BF%E3%81%A3%E3%81%9F%E6%8A%80%E8%A1%93">OGPサーバの構築に使った技術</a></h3>
<ul>
<li>NodeJS</li>
<li>Express</li>
<li>TypeScript</li>
<li>puppeteer</li>
<li>VPS</li>
<li>docker-compose</li>
</ul>
<h2 id="ロジック"><a href="#%E3%83%AD%E3%82%B8%E3%83%83%E3%82%AF">ロジック</a></h2>
<ul>
<li>ジェネレータからDBにデータを登録する</li>
<li>登録されたデータをもとにpuppeteerでスクリーンショットを撮る</li>
<li>DBに画像を格納する</li>
<li>OGPのサーバ問い合わせにはDBに保存してある画像を使用する
<ul>
<li>OGPサーバに直接アクセスさせると負荷が集中してサーバが落ちるため</li>
</ul></li>
</ul>
<h3 id="DB登録処理は既存のAPサーバに追加"><a href="#DB%E7%99%BB%E9%8C%B2%E5%87%A6%E7%90%86%E3%81%AF%E6%97%A2%E5%AD%98%E3%81%AEAP%E3%82%B5%E3%83%BC%E3%83%90%E3%81%AB%E8%BF%BD%E5%8A%A0">DB登録処理は既存のAPサーバに追加</a></h3>
<h3 id="OGPサーバは別コンテナ"><a href="#OGP%E3%82%B5%E3%83%BC%E3%83%90%E3%81%AF%E5%88%A5%E3%82%B3%E3%83%B3%E3%83%86%E3%83%8A">OGPサーバは別コンテナ</a></h3>
<h2 id="twitter投稿"><a href="#twitter%E6%8A%95%E7%A8%BF">twitter投稿</a></h2>
<p>アプリ起動時<code>componentDidMount</code>のタイミングでUUIDを生成するようにしました。<br />
<a target="_blank" rel="nofollow noopener" href="https://www.npmjs.com/package/react-share">react-share</a>というライブラリを使い、<br />
<code>beforeOnClick</code>メソッドでDBへの登録を行っています。<br />
ローディング画面も作りました(同時にバグ生成)。</p>
<pre><code class="html"><TwitterShareButton
url={'https://pawapro-gen.netlify.com/view/'+store.getState().uuid}
hashtags={['パワプロ風画面ジェネレータで作ってみた']}
beforeOnClick={async()=>{
store.dispatch(switchLoading(true));
let isSuccess = await postData();
store.dispatch(switchLoading(false));
if(!isSuccess){
alert('データの保存に失敗しました。時間を空けるか、リロードして再度作成してください。')
return new Error();
}else{
return Promise.resolve();
}
<span>}</span><span>}</span>
>
保存してシェアする!
</TwitterShareButton>
</code></pre>
<h1 id="苦労したこと"><a href="#%E8%8B%A6%E5%8A%B4%E3%81%97%E3%81%9F%E3%81%93%E3%81%A8">苦労したこと</a></h1>
<h2 id="画像のキャッシュ"><a href="#%E7%94%BB%E5%83%8F%E3%81%AE%E3%82%AD%E3%83%A3%E3%83%83%E3%82%B7%E3%83%A5">画像のキャッシュ</a></h2>
<p>ヘッダでキャッシュコントロールしたことがなかったので若干はまりました。</p>
<h2 id="OGPのトリミング問題"><a href="#OGP%E3%81%AE%E3%83%88%E3%83%AA%E3%83%9F%E3%83%B3%E3%82%B0%E5%95%8F%E9%A1%8C">OGPのトリミング問題</a></h2>
<p>twitterやFacebookでOGPの上下左右がトリミングされる問題があり、<br />
Jimpなどで余白の拡張をしようと思ったのですが、時間がなかったこともあり、<br />
React側でマージンをとってスクリーンショットを撮る際に余白ができるように修正しました。</p>
<h1 id="できなかったこと"><a href="#%E3%81%A7%E3%81%8D%E3%81%AA%E3%81%8B%E3%81%A3%E3%81%9F%E3%81%93%E3%81%A8">できなかったこと</a></h1>
<h2 id="チーム・選手画像のアップロード"><a href="#%E3%83%81%E3%83%BC%E3%83%A0%E3%83%BB%E9%81%B8%E6%89%8B%E7%94%BB%E5%83%8F%E3%81%AE%E3%82%A2%E3%83%83%E3%83%97%E3%83%AD%E3%83%BC%E3%83%89">チーム・選手画像のアップロード</a></h2>
<p>OGPの生成で手一杯だったので、base64化してDBに格納するところまで考えられませんでした。後々追加する予定。</p>
ckoshien