tag:crieit.net,2005:https://crieit.net/tags/Vue/feed 「Vue」の記事 - Crieit Crieitでタグ「Vue」に投稿された最近の記事 2022-08-31T09:39:51+09:00 https://crieit.net/tags/Vue/feed tag:crieit.net,2005:PublicArticle/18288 2022-08-31T09:35:31+09:00 2022-08-31T09:39:51+09:00 https://crieit.net/posts/Web-ankeyto 【個人開発】作るのも、答えるのも簡単なWebアンケート「ankeyto」を作りました! <h1 id="2つ目の個人開発サービス公開"><a href="#2%E3%81%A4%E7%9B%AE%E3%81%AE%E5%80%8B%E4%BA%BA%E9%96%8B%E7%99%BA%E3%82%B5%E3%83%BC%E3%83%93%E3%82%B9%E5%85%AC%E9%96%8B">2つ目の個人開発サービス公開</a></h1> <p>アプリケーションのモダン化を勉強しようと個人開発を始めて、以下の記事のように公開していました。もう振り返ると1年経つんですね… こういった個人開発活動を続けることにより、少しずつ知識も身についてきて、開発スピードも上がってきていました。</p> <ul> <li><p>Github Issuesをキレイに外部公開するサービス「2go」作ってみた</p> <ul> <li>https://qiita.com/nice2have/items/28449ae4ef45fef2c671</li> </ul></li> <li><p>海外進出を目指して、ProductHuntへ個人開発サービスを投稿するまでにやったこと&やった結果を全面的にシェアする</p> <ul> <li>https://qiita.com/nice2have/items/f59a27c266efb9b8821c</li> </ul></li> </ul> <p>実際には公開していないのですが、atodyという「Twitterでいいねしたツイートに含まれるリンク・画像だけを自動でまとめてブックマーク化できるサービス」を開発したのですが、いまいちサービスの完成度に納得がいっていなく、開発したサービスとしては3つとなったのですが、公開したサービスは2つとなります。</p> <p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1997184/3f2dbeed-c2cb-8437-164e-78a8797ea9df.png" alt="ogp.png" /></p> <h1 id="簡単Webアンケート"><a href="#%E7%B0%A1%E5%8D%98Web%E3%82%A2%E3%83%B3%E3%82%B1%E3%83%BC%E3%83%88">簡単Webアンケート</a></h1> <p>今回は会社の一定の範囲で簡単にアンケートを取りたいときに、Google FormsやMicrosoft Formsを作るのは手間だな、もっとライトな感じで反応を知りたいなと思ったのがきっかけです。</p> <p>また、アンケートの回答を得るまでのハードルが結構高いと思ったんです。アンケートURLにアクセスして、回答を入力して、、、となると、答えてくれる人が少なくなってしまって、アンケートの価値が薄まってしまうような気がしたんですよね。</p> <h1 id="機能を最小限に絞ったアンケートをリリースしよう"><a href="#%E6%A9%9F%E8%83%BD%E3%82%92%E6%9C%80%E5%B0%8F%E9%99%90%E3%81%AB%E7%B5%9E%E3%81%A3%E3%81%9F%E3%82%A2%E3%83%B3%E3%82%B1%E3%83%BC%E3%83%88%E3%82%92%E3%83%AA%E3%83%AA%E3%83%BC%E3%82%B9%E3%81%97%E3%82%88%E3%81%86">機能を最小限に絞ったアンケートをリリースしよう</a></h1> <p>これらの課題を解決しようと、ankeytoというアンケート機能を最小限に絞ったサービスを開発しようということに至りました。ankeytoはアンケートのモジりで「アンキート」と名付けることにしました。ちなみに、先程のatodyも「あとで」のモジりで「アトディ」と名付けています。</p> <p><a target="_blank" rel="nofollow noopener" href="https://ankeyto.com">https://ankeyto.com</a></p> <p>開発する前の大まかなコンセプトは、A3ノートにまとめる用にしているのですが、汚い字で申し訳ないですが、公開してみます。<br /> <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1997184/8871aa10-08b9-76f6-447b-42611f8a02a4.jpeg" alt="IMG_1337.jpg" /><br /> サービスのコンセプトは以下の2つと設定しました。<br /> + 最大3つまでの選択肢<br /> + 選択肢ごとのURLにアクセスすればもう回答済みになる<br /> <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1997184/ec5dcfde-7952-6f6b-2aa7-679b603afbd2.png" alt="image.png" /><br /> そのため、選択肢を表示して回答ができるページをSNSでシェアするのではなく、アクセスしたら回答できるURLをSNSでシェアするだけで良くなるのです。例えば、「これが良いと思ったら、このURLにアクセスして!」と言えば良くなり、アンケート回答のステップとハードルを減らすことができるのです。つまり、個人個人で対象への「いいね」を調査・獲得できるイメージです。<br /> <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1997184/29d7fd65-e416-566f-3b3c-9345e146f755.png" alt="image.png" /></p> <h1 id="画面イメージ"><a href="#%E7%94%BB%E9%9D%A2%E3%82%A4%E3%83%A1%E3%83%BC%E3%82%B8">画面イメージ</a></h1> <p>こちらのようにアンケート回答がリアルタイムにグラフやUIに反映されるので、回答が多いと変化を楽しめるかもしれません。例えば、プレゼン最中の意見を軽く聞きたいときや、勉強会のフィードバックを簡単に得たいときなどに利用できるかもしれません。<br /> <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1997184/12cb36a5-f89b-8aad-dded-dffcc5f7c223.gif" alt="demo.gif" /></p> <p>アンケートを作るときの画面も極力シンプルにしています。こちらもログインが必要だと手間が増えてしまったり、入力項目数が多いと途中で離脱してしまう可能性が高くなってしまうので、最小限にすることを意識しています。<br /> <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1997184/737aa90d-06ef-4ebc-a439-1dcfc31e9e66.png" alt="image.png" /></p> <h1 id="利用技術"><a href="#%E5%88%A9%E7%94%A8%E6%8A%80%E8%A1%93">利用技術</a></h1> <p>個人開発者としてはまだまだペーペーなので、利用技術も運用負荷が最小限になることを意識しています。<br /> - Firebase(Firestore/Functions/Hosting)<br /> - Vue.js<br /> - Tailwindcss<br /> - Slack</p> <p>サービス自体がとてもシンプルなので、HostingでWebページを表示し、質問作成や回答をFunctionsで処理し、結果をFirestoreでリアルタイムに反映しているだけです。一応、モチベーション維持のために、質問が作成されたり、回答されたりしたら、FuncitonsからSlackに通知されるようにしています。</p> <h1 id="将来に向けて"><a href="#%E5%B0%86%E6%9D%A5%E3%81%AB%E5%90%91%E3%81%91%E3%81%A6">将来に向けて</a></h1> <p>最後に、将来は以下のロードマップを考えています。<br /> - URLにアクセスするよりもさらにハードルを下げるために、URLをQRコード化できるように。<br /> - Cookie等を利用した重複回答者の防止機能<br /> - 質問IDをランダムでなく、わかりやすいIDを提供できる機能<br /> - API機能(UIだけでなく、いいねAPIを提供)<br /> - 英語対応</p> <h1 id="最後に。"><a href="#%E6%9C%80%E5%BE%8C%E3%81%AB%E3%80%82">最後に。</a></h1> <p>まさにこのankeytoを利用して、このWebサービスってどうですか?とアンケートを取って終われればと思います。以下が質問ページから、SNS共有用にコピーできる文章になります。「いい!」か「まあまあ」のURLにアクセスすれば回答が完了しますので、ぜひよろしくお願いします!</p> <blockquote> <p>Q. このWebサービスってどうですか?<br /> https://ankeyto.com/q/BbDJA3P11</p> <p>いい! : https://ankeyto.com/BbDJA3P11/1<br /> まあまあ : https://ankeyto.com/BbDJA3P11/2</p> </blockquote> <p>また、個人開発の進捗などを垂れ流すTwitterもやっていますので、こちらもよろしければフォローお願いします。<br /> <a target="_blank" rel="nofollow noopener" href="https://twitter.com/jnakajima1982">https://twitter.com/jnakajima1982</a></p> jnakajima1982 tag:crieit.net,2005:PublicArticle/18231 2022-06-26T14:43:49+09:00 2022-06-26T14:52:21+09:00 https://crieit.net/posts/Subreco-3 サブスクの解約忘れ・無駄遣いを防止するサービス「Subreco」を開発して得た3つの学び <h2 id="筆者について"><a href="#%E7%AD%86%E8%80%85%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6">筆者について</a></h2> <p>「多くのユーザーが実際に使って、価値があるものはなんだろう?」と考えながら、個人開発を趣味ではなく「<strong>収益を出す</strong>」ために行なっています。</p> <p>技術だけでなく、デザインやグロースハックも好きで、色々な視点からWebサービスを作り上げていくのが好きです。</p> <p>今回ご紹介する「<strong><a target="_blank" rel="nofollow noopener" href="https://bit.ly/3niPqoN">Subreco</a></strong>」は個人開発を始めて3つ目のサービスとなります。</p> <p><strong><a target="_blank" rel="nofollow noopener" href="https://bit.ly/3niPqoN">Subrecoの公式サイトはこちら</a></strong></p> <h2 id="なぜ、サブスク管理サービス「Subreco」を作ったのか?"><a href="#%E3%81%AA%E3%81%9C%E3%80%81%E3%82%B5%E3%83%96%E3%82%B9%E3%82%AF%E7%AE%A1%E7%90%86%E3%82%B5%E3%83%BC%E3%83%93%E3%82%B9%E3%80%8CSubreco%E3%80%8D%E3%82%92%E4%BD%9C%E3%81%A3%E3%81%9F%E3%81%AE%E3%81%8B%EF%BC%9F">なぜ、サブスク管理サービス「Subreco」を作ったのか?</a></h2> <p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326850/ea6daaa9-5fae-ecc7-c8f1-3a99368d62e8.png" alt="スクリーンショット 2022-06-26 12.44.09.png" /><br /> 自分自身、何度もサブスクの「<strong>解約忘れ</strong>」で失敗した経験があったからです。</p> <p>あなたも、サブスクなどで使っていないサービスを解約し忘れて"<strong>無駄な支払いをしてしまった経験</strong>"ありませんか?</p> <p>家計簿アプリ、クレジットカード会社が提供しているアプリは時々見ていたものの、「<strong>あっこのサービス使ってないのに解約し忘れたぁ!(絶望…)</strong>」ということが何回かあったんですね。</p> <p>1回だけならいいのですが。何回も経験したので「さすがにどうにかしたいなぁ」と思って、次はメモアプリとカレンダーアプリを併用して使っていました。</p> <p>これでサブスクと契約更新日を両方管理できる!対策バッチリ!</p> <p><strong>、、、と思いきや、だんだんメモとカレンダーアプリで二重管理するのが面倒くさくなり、結果的に続かなかったんです。。</strong></p> <p>メモアプリやカレンダーアプリだと「<strong>毎回今月は〇〇を使っていたっけ…?</strong>」とわざわざ自分が使っているサービスを"<strong>意識</strong>"して見直さなければなりません。</p> <p>でも、そんなの毎回意識できないし、面倒ですよね?</p> <p>この経験から、使っているサブスクを「<strong>定期的に見直す仕組みを自動化</strong>」しようと生まれたのがSubrecoです。</p> <h2 id="Subrecoの特徴"><a href="#Subreco%E3%81%AE%E7%89%B9%E5%BE%B4">Subrecoの特徴</a></h2> <p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326850/9498e705-0014-ad3a-e72c-12981a941dc5.png" alt="スクリーンショット 2022-06-23 13.28.50.png" /><br /> Subrecoの使い方はシンプルに3STEPで完結します。</p> <pre><code>①今使っている全てのサブスクを一括管理して把握する ②各サブスクの契約更新日があるたびに自動通知する ③通知があるたびに「利用継続」or 「利用停止」を判断する </code></pre> <p>契約更新日はメールで自動通知するため、わざわざメモを見直したり、毎回カレンダーで契約更新日を設定し直さなくて済むのがポイント。</p> <p>使っているサブスクを登録しておくだけで、自動でSubrecoが契約更新日をお知らせします。</p> <p><strong><a target="_blank" rel="nofollow noopener" href="https://bit.ly/3niPqoN">Subrecoの公式サイトはこちら</a></strong></p> <h2 id="「Subreco」を作って得た学び"><a href="#%E3%80%8CSubreco%E3%80%8D%E3%82%92%E4%BD%9C%E3%81%A3%E3%81%A6%E5%BE%97%E3%81%9F%E5%AD%A6%E3%81%B3">「Subreco」を作って得た学び</a></h2> <pre><code>①慣れないことはしない ②コア機能を徹底的に絞る ③自分は使うのかを考える </code></pre> <p>ここからは実際に<a target="_blank" rel="nofollow noopener" href="https://bit.ly/3niPqoN">Subreco</a>を開発して、得た学びを3つ紹介します。</p> <h3 id="①慣れないことはしない"><a href="#%E2%91%A0%E6%85%A3%E3%82%8C%E3%81%AA%E3%81%84%E3%81%93%E3%81%A8%E3%81%AF%E3%81%97%E3%81%AA%E3%81%84">①慣れないことはしない</a></h3> <p>実は、当初SubrecoはNuxt.jsで作っていました。(ちなみにNuxt.jsを使うのが今回が始めて)</p> <p>最初は順調だったものの、徐々にハマるポイントが多く、開発スピードが落ちてきたので、思い切ってLaravel×Vue.jsでリプレイスしました。</p> <p>結果、開発スピードは<strong>3倍</strong>以上にUP。</p> <p>ここから分かったのは、収益目的で個人開発をする場合、<strong>絶対に自分が慣れているフレームワークで開発すべき</strong>だということ。</p> <p>ただでさえ開発以外にもすべきこと(デザイン、マーケetc.)が多いのに、開発に負荷がかかる進め方は合理的でないと痛感しました。</p> <h3 id="②コア機能を徹底的に絞る"><a href="#%E2%91%A1%E3%82%B3%E3%82%A2%E6%A9%9F%E8%83%BD%E3%82%92%E5%BE%B9%E5%BA%95%E7%9A%84%E3%81%AB%E7%B5%9E%E3%82%8B">②コア機能を徹底的に絞る</a></h3> <p>テレビのリモコンを想像してみましょう。</p> <p><code>・電源の入/切ができる</code><br /> <code>・チャンネルで主要な局が切り替えられる</code></p> <p>一番使う機能が多いのは上記だと思います。</p> <p>しかし、実際にテレビのリモコンを見ると他にもボタンがありますよね。<br /> で、実際にほとんどのボタンを使っていないという方も多いのではないでしょうか?</p> <p>そう。<strong>実際にユーザーが求めている機能はシンプル</strong>であることが多いです。</p> <p>これはWebサービスも同じ。</p> <p><a target="_blank" rel="nofollow noopener" href="https://bit.ly/3niPqoN">Subreco</a>の場合、</p> <pre><code>・サブスクの管理が一覧で見れること ・カレンダー形式で契約更新日を確認できること ・契約更新日を自動でメール通知してくれること </code></pre> <p>これさえあれば、<strong>Subrecoが提供したい価値の必要条件は満たせている</strong>と判断して実装していました。</p> <p>実装前に大事なのは、Webサービスの機能として「<strong>必要条件</strong>」と「<strong>十分条件</strong>」を分けて考えること。</p> <p>これは過去の自分に何回も言い聞かせたいです。</p> <h3 id="③自分は使うのかを考える"><a href="#%E2%91%A2%E8%87%AA%E5%88%86%E3%81%AF%E4%BD%BF%E3%81%86%E3%81%AE%E3%81%8B%E3%82%92%E8%80%83%E3%81%88%E3%82%8B">③自分は使うのかを考える</a></h3> <p>個人開発でアイディアを出す際に、</p> <pre><code>①自分起点・・・自分の課題を解決するアイディア ②相手起点・・・自分以外の人が抱えている課題を解決するアイディア </code></pre> <p>の2パターンがあります。</p> <p>今回思ったのが、個人開発をするという前提に立って言えば、圧倒的に<strong>自分起点</strong>で開発するのが良いと思いました。</p> <p>なぜなら、スタート地点で需要が少なくとも「<strong>1つ</strong>」は存在しているから。<br /> 需要が0の場合はそこからスケールさせようがないですが、1であればスケールさせられる可能性は残っています。</p> <p>また、1番のヘビーユーザーは自分になるため、あった方が良い機能や操作する中で潜在的なバグに気付くことができます。<br /> 最悪、誰も使わなくても自分を助けてくれるツールとして機能するので"<strong>一石三鳥</strong>"ですね。</p> <h2 id="まとめ"><a href="#%E3%81%BE%E3%81%A8%E3%82%81">まとめ</a></h2> <p><img src="https://storage.googleapis.com/zenn-user-upload/62663bd93952-20220626.png" alt="" /></p> <p>収益目的で個人開発をすると考えることは膨大にあります。</p> <p>もしかしたら、あなたも「<strong>個人開発で収益を出したい!</strong>」と、もがいて、あがいている途中かもしれません。</p> <p>僕もあなたと一緒です...!</p> <p>ただ、辛いことや苦しいことがつきものの個人開発ですが、その過程で得られる</p> <p><strong>「うひょ〜!」という小さな成功体験</strong>(考えていた機能が完成した時etc.)<br /> <strong>「よし進んでるな」という実感(Todoリストにチェックを入れる瞬間)</strong></p> <p>これらは<strong>圧倒的な揺るぎない自信、資産</strong>として必ず残っていきます。</p> <p>だからこそ、個人開発を続けている方は諦めないで、一緒に頑張りましょう!</p> <p>僕は引き続き<a target="_blank" rel="nofollow noopener" href="https://bit.ly/3niPqoN">Subreco</a>の改善を続けていきます!ではでは!</p> <p><strong><a target="_blank" rel="nofollow noopener" href="https://bit.ly/3niPqoN">Subreco</a>を実際に利用してみて、ご要望・感想などありましたら、メッセージ頂けると嬉しいです!^^</strong></p> サイゼン 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/17693 2021-09-30T07:19:35+09:00 2021-09-30T09:25:05+09:00 https://crieit.net/posts/ProductHunt 海外進出を目指して、ProductHuntへ個人開発サービスを投稿するまでにやったこと&やった結果を全面的にシェアする <h1 id="初めて作った個人開発サービスで海外進出しようという無謀な挑戦"><a href="#%E5%88%9D%E3%82%81%E3%81%A6%E4%BD%9C%E3%81%A3%E3%81%9F%E5%80%8B%E4%BA%BA%E9%96%8B%E7%99%BA%E3%82%B5%E3%83%BC%E3%83%93%E3%82%B9%E3%81%A7%E6%B5%B7%E5%A4%96%E9%80%B2%E5%87%BA%E3%81%97%E3%82%88%E3%81%86%E3%81%A8%E3%81%84%E3%81%86%E7%84%A1%E8%AC%80%E3%81%AA%E6%8C%91%E6%88%A6">初めて作った個人開発サービスで海外進出しようという無謀な挑戦</a></h1> <p>以下の記事の通り、9月頭に2goというサービスを作って公開しました。サービスの詳細は以下のURLから見てもらえればと思いますが、いくつかの理由から世界での公開を元から視野に入れていました。<br /> <a href="https://crieit.net/posts/Github-Issues-2go">https://crieit.net/posts/Github-Issues-2go</a></p> <p><a href="https://crieit.now.sh/upload_images/2ccd9128457814ab80f6aa50fae47a4b615488e83f814.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/2ccd9128457814ab80f6aa50fae47a4b615488e83f814.png?mw=700" alt="スクリーンショット 2021-09-26 16.27.05.png" /></a></p> <ol> <li>英語圏のユーザーも含めたほうがサービスの規模感が大きくなる</li> <li>サービス内容が日本だけに留まらない共通の需要があると思った</li> <li>どこかで挑戦しなくてはならないのなら、ここで挑戦しようと思った(無謀)</li> </ol> <p>こういった背景があり、全てのインターフェースは英語で作ってきました。そして、MVPが9月に完成したので、日本でのアナウンスをしました。海外を意識していたので、日本での進め方はあくまでフィードバックを貰えればくらいに思っていましたが、今思えばここからが既に問題なんだなと考えられます。<br /> <a href="https://crieit.now.sh/upload_images/f1969ee50f2c22b287264f3cd5afceb8615488f2e271d.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/f1969ee50f2c22b287264f3cd5afceb8615488f2e271d.png?mw=700" alt="スクリーンショット 2021-09-07 3.13.35.png" /></a></p> <p>私の経験をまとめましたので、自分への戒めと今後トライされる方の参考になれば。</p> <h1 id="日本でやったことと、その結果"><a href="#%E6%97%A5%E6%9C%AC%E3%81%A7%E3%82%84%E3%81%A3%E3%81%9F%E3%81%93%E3%81%A8%E3%81%A8%E3%80%81%E3%81%9D%E3%81%AE%E7%B5%90%E6%9E%9C">日本でやったことと、その結果</a></h1> <p>日本でリリースしたときにやったことを書いていきます。</p> <h2 id="Twitterでの開発進捗の共有による露出"><a href="#Twitter%E3%81%A7%E3%81%AE%E9%96%8B%E7%99%BA%E9%80%B2%E6%8D%97%E3%81%AE%E5%85%B1%E6%9C%89%E3%81%AB%E3%82%88%E3%82%8B%E9%9C%B2%E5%87%BA">Twitterでの開発進捗の共有による露出</a></h2> <p>私のTwitterアカウントはコチラですが、元々は現在働いている会社のAwarenessを上げるために、自分自身の考えやnoteなどの記事を共有するために使っていたものでした。そのため、インフラやクラウドなどのトピックが中心のため、100強のフォロワーもそういった傾向がありました。ただし、インフラやクラウドもこれからはアプリケーションレイヤーがわかっていないと語れないなと思い、個人開発をすることで開発者の気持ちを理解していこうとしている途中でした。<br /> <a target="_blank" rel="nofollow noopener" href="https://twitter.com/jnakajima1982">https://twitter.com/jnakajima1982</a></p> <h3 id="Twitterでの取り組み内容"><a href="#Twitter%E3%81%A7%E3%81%AE%E5%8F%96%E3%82%8A%E7%B5%84%E3%81%BF%E5%86%85%E5%AE%B9">Twitterでの取り組み内容</a></h3> <p>そのため、絶対数としてこういったアプリレイヤーに興味がある人が少ないということもあり、開発進捗を共有したとしても反応がほぼもらえないという状況が続きました。そこで、私は以下を取り組みました。<br /> 1. 「#個人開発」や「#今日の積み上げ」といったハッシュタグを使った投稿を増やした<br /> 2. 上記ハッシュタグにまつわる個人開発者を中心にフォローを増やした<br /> 3. 自分自身で話題を作るより、フォローしている人のTweetに引用RTするようにした<br />  </p> <h3 id="結果と考察"><a href="#%E7%B5%90%E6%9E%9C%E3%81%A8%E8%80%83%E5%AF%9F">結果と考察</a></h3> <p>「#個人開発」に関しては普段よりはインプレッションが増えましたが、反応が劇的に増えた感じはしませんでした。これには2つの原因があると考えています。「どんなサービスかわかりにくかった(プロフィールもわかりにくかった)」のと、「作っているサービスが英語」だったため、ハードルが高かったのだと思っています。それでも少しずつフォロワーは増え、最終的には今回の取り組みで50フォロワーくらい増えたので、コツコツやっていくことが重要で、指数関数的に上がることになるのではないかなと思っていますので、継続が必要です。</p> <p>ただし、私のアカウントが公共分野(デジタル庁など)のインフラ・クラウドと、個人開発が混ざっているので、アカウントを分けるべきか非常に迷っています。フォロワー0から作り直すリスクと、複数アカウントを運用することの大変さに応えられるのかに解がないため、一旦は1つのアカウントとしています。</p> <p><a href="https://crieit.now.sh/upload_images/89697e0fc27d4936e961e842ff9c0e1861548905416a3.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/89697e0fc27d4936e961e842ff9c0e1861548905416a3.png?mw=700" alt="twitter (1).png" /></a></p> <p>また、 「#今日の積み上げ」は、インプレッションが非常に増え、リアクションも同時に増えるのですが、フォロワーが増えません。これは個人開発に留まらないユーザーが大量に流入しているせいだと思いますが、どちらかと言うとお互いに励まし合おうというハッシュタグの位置づけになっていると思われるので、このような結果になっているのだと思います。自分自身の精神安定の観点で、たまに投稿して安心するくらいが丁度いいと思っています。</p> <p>3については、私自身が有益なことをTweetすることができればいいのですが、いざTwitterの画面を開いてなにか書こうと思ったときに、思いつくことには限界があったんですよね。特に、何がウケるのかみたいなことを考えだしたのもいけなかったんだと思います。なので、フォローしている人は私が興味があってフォローしているわけで、その人が話題にしていることに対して引用RTすることにして、自分の意見を言っていこうと考えました。</p> <p>これは意外と効果があります。Twitter上での表示のされ方も、元Tweetが強ければ同時に表示されることもあります。また、何より引用RTした相手からのコメントを頂ける確率も高く、それがインプレッションの増加に繋がる結果が多かったと思います。</p> <h3 id="反省と今後"><a href="#%E5%8F%8D%E7%9C%81%E3%81%A8%E4%BB%8A%E5%BE%8C">反省と今後</a></h3> <p>いずれにしても、Twitterでの認知度向上は一朝一夕ではいきませんので継続した活動が必要ということが身に沁みました。後ほど触れますが、ProductHuntなどは、このようなフォロワーの数が少ない開発者を掘り出して世に出すためのサービスという大前提があるのですが、現実問題として規模の戦いは存在すると実感しましたので、Twitter等のSNSでコミュニティを形成する必要を感じさせられました。</p> <p>特に英語圏での情報発信を考えると、英語用Twitterアカウントを作ったのですが、日本語用Twitterアカウントだけでも苦労しているのに、英語で書いて運用していくというのはとても大変なことです。そのため、英語用Twitterアカウントはほとんど活動ができていません。</p> <h2 id="運営者ギルドへの参加"><a href="#%E9%81%8B%E5%96%B6%E8%80%85%E3%82%AE%E3%83%AB%E3%83%89%E3%81%B8%E3%81%AE%E5%8F%82%E5%8A%A0">運営者ギルドへの参加</a></h2> <p>Twitterを徘徊していると、運営者ギルドという存在を知りました。まさに私が陥っているような状況が冒頭に書かれていました。<br /> <a target="_blank" rel="nofollow noopener" href="https://qiita.com/organizations/admin-guild">https://qiita.com/organizations/admin-guild</a></p> <blockquote> <p>作ったWebサービス、全然使われない……<br /> どうやってマネタイズしよう<br /> どこまで作り込んでからリリースすればいいんだろう<br /> デザイン難しすぎ<br /> 利用規約はどうすれば?<br /> この先どうやってサービスを伸ばしていけばいいのか……<br /> Slackでの運営形態になっており、中の活動が全く見えなかったので、入れていただくかを非常に悩んだのですが、何事もチャレンジだと思ったので、連絡させて頂き、無事入らせて頂きました。</p> </blockquote> <p><a href="https://crieit.now.sh/upload_images/9a13068ab299538fc8dcc14cfa4026f4615489143c8f5.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/9a13068ab299538fc8dcc14cfa4026f4615489143c8f5.png?mw=700" alt="image (28).png" /></a></p> <h3 id="結果と考察"><a href="#%E7%B5%90%E6%9E%9C%E3%81%A8%E8%80%83%E5%AF%9F">結果と考察</a></h3> <p>結果から言うと、個人開発をしている人は入ったほうが良いです。下手な質問をしたら、ぶっ飛ばされるのではないかとビクビクしていましたが、そんな事は全くありませんでした。唯一あるとするならば、自分自身で発信したことには多くの意見がもらえてモチベーションに繋がりますが、自分自身で発信しない方はここで何をしていいかわからなくなってしまうと思います。</p> <p>私は、実運用ではどのようにしているかなどは、こういった限定されたユーザーだけが参加するコミュニティだからこそ聞ける生の声が本当に貴重だと思いました。また、今回のProductHuntのチャレンジなどを好意的に捉えてくださるだけでなく、普段の自分自身の開発の進捗にコメントいただける方もいらっしゃり、個人開発の課題となりがちなモチベーション維持にもなりました。</p> <p>だからこそ、私自身がコミュニティの一部として、他の方が同様な状況にいる場合には、積極的に自分の経験を還元する必要があると思い、いろいろな方のチャンネルに入るようにしました。</p> <h2 id="技術コミュニティメディアへのサービス開発記事の投稿"><a href="#%E6%8A%80%E8%A1%93%E3%82%B3%E3%83%9F%E3%83%A5%E3%83%8B%E3%83%86%E3%82%A3%E3%83%A1%E3%83%87%E3%82%A3%E3%82%A2%E3%81%B8%E3%81%AE%E3%82%B5%E3%83%BC%E3%83%93%E3%82%B9%E9%96%8B%E7%99%BA%E8%A8%98%E4%BA%8B%E3%81%AE%E6%8A%95%E7%A8%BF">技術コミュニティメディアへのサービス開発記事の投稿</a></h2> <p>Twitterや運営者ギルド以外では、とにかくサービスを知ってもらえるチャネルを増やそうとしていました。そこで以下の技術コミュニティメディアに記事を投稿してみました。<br /> - Zenn:https://zenn.dev/nice2have/articles/aa15eccd13a23c<br /> - Qiita:https://qiita.com/nice2have/items/28449ae4ef45fef2c671<br /> - Crieit: https://crieit.net/posts/Github-Issues-2go<br /> - ツクログ:https://creators.eightbit.jp/service/item4309.html</p> <p>以下にそれぞれのメディアの現時点での結果をまとめます(Google Analytics調べ)。Zennは正直きちんと取れているのかわかりません。ツクログは投稿して間もないのと、どこからデータが取れるのかわからずなので、外しています。<br /> <div class="table-responsive"><table> <thead> <tr> <th>メディア</th> <th>記事閲覧数</th> <th>サービス流入数</th> <th>LGTM</th> </tr> </thead> <tbody> <tr> <td>Zenn</td> <td>34</td> <td>10</td> <td>4</td> </tr> <tr> <td>Qiita</td> <td>1468</td> <td>261</td> <td>7</td> </tr> <tr> <td>Crieit</td> <td>1198</td> <td>66</td> <td>1</td> </tr> </tbody> </table></div></p> <h3 id="結果と考察"><a href="#%E7%B5%90%E6%9E%9C%E3%81%A8%E8%80%83%E5%AF%9F">結果と考察</a></h3> <p>いずれにせよ、どのメディアにも投稿することは確実に認知度とサービス流入に繋がっていると言えますので、サービスローンチのタイミングだけでなく、書く内容がある限り、投稿し続けると良いと思います、、、が、やっぱり書くのって大変なんですよね。さらに、メディアによってトレンドがあるから、書く内容を変えたほうがよいというのを見たのですが、全く同じ記事を上げているのが現状です。まずは続けることが大事なので、しばらくはそれを継続しようと思っています。</p> <h3 id="ダイレクトアプローチ"><a href="#%E3%83%80%E3%82%A4%E3%83%AC%E3%82%AF%E3%83%88%E3%82%A2%E3%83%97%E3%83%AD%E3%83%BC%E3%83%81">ダイレクトアプローチ</a></h3> <p><a href="https://crieit.now.sh/upload_images/d3971adefd981bac418520bb891203cb61548925cc9ef.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/d3971adefd981bac418520bb891203cb61548925cc9ef.png?mw=700" alt="lp_issues.png" /></a><br /> 追加でした作業についても記録しておきます。2goというサービスの特性上、Github Issueを利用している/利用見込みのユーザーがターゲットになります。そのため、メディア上にGithub Issueについて記事を書いている人へ直接連絡して実際に利用していただくことで、フィードバックを受けて改善に生かしていこうと思いました。</p> <p>Google検索からたどり着いた5件にコメントをしてコミュニケーションを図ってみたのですが、どこからも未だ返事がない状況です。Twitter上でキーワード検索してアプローチしたこともありましたが、ちょっと利用用途が異なったようで、試してもらうまでに至りませんでした。</p> <p>以上のアプローチで、面白いですとか、良いサービスですねなど言われるものの、今思えば、やはり利用されるだけの魅力やユースケースが見出してもらえなかったのだと思いました。</p> <h2 id="まとめ:日本での取り組み"><a href="#%E3%81%BE%E3%81%A8%E3%82%81%EF%BC%9A%E6%97%A5%E6%9C%AC%E3%81%A7%E3%81%AE%E5%8F%96%E3%82%8A%E7%B5%84%E3%81%BF">まとめ:日本での取り組み</a></h2> <p>海外進出を前提に置いていたため、以下の2点の理由で、Githubアカウントでログインしてまで試してくれた人は、2桁にすら行きませんでした。<br /> - 日本での取り組みを正直本気で取り組んだとは言えない<br /> - 英語のインターフェースのため、伝わりづらかった</p> <p>そこで、Githubアカウントでログインすることのハードルが高いと思い、ログイン不要で誰にでも触れるサンプルを追加開発して提供してみたのですが、こちらも時たま試す人が現れるくらいで、試すことすらしてもらえないのかと打ちひしがれましたorz</p> <p>ほとんどのユーザーアクティビティ(サービス登録/ログイン/公開設定)が発生すると、Slackに通知が飛んでくるようにしていたのですが、たまにしか飛んでこず、こちらも現時点では自分のログイン動作だけが通知される閑古鳥が鳴いている状態です。</p> <h1 id="海外編:Product Hunt"><a href="#%E6%B5%B7%E5%A4%96%E7%B7%A8%EF%BC%9AProduct+Hunt">海外編:Product Hunt</a></h1> <p>つらつらと書いてきましたが、これからが本編です。当時、海外進出しか見えていなかった私は、海外での認知度を上げるための方策を調査していました。調べていくと以下の2つのサービスがマッチすると思いました。</p> <ol> <li>Product Hunt : https://www.producthunt.com/</li> <li>Indie Hackers : https://www.indiehackers.com/</li> </ol> <p>これらのサービスでは、個人開発者やスタートアップが自分の作ったプロダクトを投稿して、認知度を上げることが毎日のように行われています。ただ、Indie Hackersの方は招待制でしか入れず、Redditなどを徘徊してもInvitation Codeを求める人はいるものの、なかなか何もない状態でそれがもらえる人は見られなかったので、一旦Product Huntにフォーカスすることに決めました。</p> <h2 id="Product Huntへの投稿を決心"><a href="#Product+Hunt%E3%81%B8%E3%81%AE%E6%8A%95%E7%A8%BF%E3%82%92%E6%B1%BA%E5%BF%83">Product Huntへの投稿を決心</a></h2> <p>まずは経験者のまとめを読み漁りました。具体的には以下の記事を読みました。この中でも、本当に有効だったこと、今では有効ではないこと、役に立たないことがあるので、それを自分なりに整理していきました。とにかくやれることはやりきらないと、次につながらないと思い計画していきました。</p> <p><a href="https://crieit.net/posts/7">https://crieit.net/posts/7</a><br /> <a target="_blank" rel="nofollow noopener" href="https://jp.taishikato.com/posts/i-want-to-prove-that-japanese-can-compete-in-english-speaking-countries">https://jp.taishikato.com/posts/i-want-to-prove-that-japanese-can-compete-in-english-speaking-countries</a><br /> <a target="_blank" rel="nofollow noopener" href="https://blog.notsobad.jp/entry/2019/12/23/232807">https://blog.notsobad.jp/entry/2019/12/23/232807</a><br /> <a target="_blank" rel="nofollow noopener" href="https://blog.notsobad.jp/entry/bungo-search-product-hunt">https://blog.notsobad.jp/entry/bungo-search-product-hunt</a></p> <h3 id="Product Hunt攻略計画"><a href="#Product+Hunt%E6%94%BB%E7%95%A5%E8%A8%88%E7%94%BB">Product Hunt攻略計画</a></h3> <p>先述の記事を読んで私が立てた当日の計画は以下のとおりです。<br /> 1. 太平洋時間 0:01(日本時間 16:01)からその日のランキングレース開始。<br /> 2. 1時間くらい様子をみて、Google や Facebook からのプロダクトなどのビッグサービスが投稿されていないのを確認してから投稿。<br /> 3. Twitter で Product Hunt への投稿を英語でアナウンス。 @ProductHuntをメンションする。<br /> 4. Product Hunt 上の紹介ページ URL が取得できるので、それを基に Product Hunt Card 作成(Medium の場合は URL を貼り付けるだけで OK)し、Medium 投稿。<br /> 5. ProductHunt の URL を使って Landing Page に反映し、導線を作る。<br /> 6. Medium の記事を、Product Hunt 上のプロダクト紹介ページに関連記事として貼り付け。<br /> 7. Medium の記事を、Hacker News に投稿(Show HN:をタイトルにつけて投稿)<br /> 8. Reddit に投稿<br /> 9. コメントが入ったらそれに返信していく。</p> <p>また、投稿当日までの計画は以下のとおりです。<br /> 1. 投稿用のランディングページを作る<br /> 2. 投稿用の動画(英語)を作る<br /> 3. Medium/HackersNews/Redditの記事を準備する<br /> 4. Product Huntのupcoming Productに登録する<br /> 5. Product Hunt投稿内容の準備をする<br /> 6. 最低限必要な機能の整備(退会機能・規約)<br /> 7. 著名なProduct HuntersにReview Requestをして、認知してもらう</p> <p>簡単に言ってしまうと、以下の2点に集約されます。すごくシンプルです。<br /> - ProductHuntの投稿を魅力的なものにする<br /> - ProductHuntのupvoteを得るための導線をできるだけ多く作る</p> <h4 id="投稿当日までの準備と結果"><a href="#%E6%8A%95%E7%A8%BF%E5%BD%93%E6%97%A5%E3%81%BE%E3%81%A7%E3%81%AE%E6%BA%96%E5%82%99%E3%81%A8%E7%B5%90%E6%9E%9C">投稿当日までの準備と結果</a></h4> <p>順番は前後しますが、投稿当日までの計画について共有します。</p> <h5 id="ランディングページ"><a href="#%E3%83%A9%E3%83%B3%E3%83%87%E3%82%A3%E3%83%B3%E3%82%B0%E3%83%9A%E3%83%BC%E3%82%B8">ランディングページ</a></h5> <p>ランディングページはたいして手間もかからなかったのですが、当日投稿しなければ、貼るURLがわからないため、ボタンだけ作ってVueの機能で表示しない状態で準備しておきました。実際のトップページの見えるところにボタンを置いた程度でした。はっきり言ってダサいですね。<br /> <a href="https://crieit.now.sh/upload_images/a66b3274000d3cb3eea1c45bdc3c27aa6154893919d2d.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/a66b3274000d3cb3eea1c45bdc3c27aa6154893919d2d.png?mw=700" alt="image (27).png" /></a></p> <p>改めて投稿後にいろいろ研究してみましたが、以下のような形で誘導するのはとても有効だと感じました。後にわかりますが、こんなレベルの問題ではないことがわかります。<br /> <a href="https://crieit.now.sh/upload_images/55b3b19e99f9362c582a1295c888addd61548947627a7.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/55b3b19e99f9362c582a1295c888addd61548947627a7.png?mw=700" alt="image (26).png" /></a></p> <hr /> <h5 id="投稿用の動画"><a href="#%E6%8A%95%E7%A8%BF%E7%94%A8%E3%81%AE%E5%8B%95%E7%94%BB">投稿用の動画</a></h5> <p>ProductHuntの投稿にYoutube動画を付けると、Product紹介の一番最初に表示され、自動的に再生される仕組みになっています。そのため、インパクトがあると思ったのと、これをやらねばやらなかったときの効果が今後わからなくなると思い、動画制作に手を付け始めました。しかしながら、以下の問題にぶつかります。</p> <ul> <li>動画編集に知見がない</li> <li>動画ナレーションは英語の必要性がある</li> </ul> <p>動画編集に知見がないので、いろいろ調べたところ、ダビンチリゾルブという動画編集ソフトが良さそうで試してみたところ、操作が全くわからず、説明記事をウロウロしていたのですが、わからなすぎて挫折しそうになっていました。ところが、原点に戻って動画のことは動画で、と思ってYoutubeの解説動画を探しました。</p> <p>そして以下の動画を10分で見たら、一気に進み、こんな私でも動画編集ができるようになりました。基本操作と、動画編集に最低限必要そうな動画を紹介しておきます。</p> <p><a target="_blank" rel="nofollow noopener" href="https://youtu.be/duxtN8ixpLo">https://youtu.be/duxtN8ixpLo</a><br /> <a target="_blank" rel="nofollow noopener" href="https://www.youtube.com/watch?v=0J7lqAnP_64&t=194s">https://www.youtube.com/watch?v=0J7lqAnP_64&t=194s</a></p> <p>ただ、計画もなしに動画編集するとうまく行かなったので、Figmaで整理してみました。ただし、これで一旦動画を作ったのですが、文字だらけでなかなか理解しにくいものになってしまいました。<br /> <a href="https://crieit.now.sh/upload_images/7ef5b8ff4b8585d89aea6aae136e0560615488982a3bd.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/7ef5b8ff4b8585d89aea6aae136e0560615488982a3bd.png?mw=700" alt="image (25).png" /></a></p> <p>ここあら紆余曲折があって、ようやく完成した最終版はコチラです。<br /> <a target="_blank" rel="nofollow noopener" href="https://youtu.be/YvcyiAccCBo">https://youtu.be/YvcyiAccCBo</a></p> <p>先にナレーションの英文を考えて、Google Text to Speechに喋らせて、それに合わせて映像を作成するという手順を踏みました。しかしながら、結果的に動画以前の問題であることも発覚します。<br /> <a href="https://crieit.now.sh/upload_images/1854cd2cbe6840ac117d5ae95762703d615488842c9e4.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/1854cd2cbe6840ac117d5ae95762703d615488842c9e4.png?mw=700" alt="image (24).png" /></a></p> <h5 id="Medium/HackersNews/Redditの記事を準備する"><a href="#Medium%2FHackersNews%2FReddit%E3%81%AE%E8%A8%98%E4%BA%8B%E3%82%92%E6%BA%96%E5%82%99%E3%81%99%E3%82%8B">Medium/HackersNews/Redditの記事を準備する</a></h5> <p>ここから導線となる記事の準備についてお話しいたします。</p> <p><strong>Medium</strong><br /> <a target="_blank" rel="nofollow noopener" href="https://medium.com/@nice2have/2go-road-to-my-first-webservice-5b38666a7dd8">https://medium.com/@nice2have/2go-road-to-my-first-webservice-5b38666a7dd8</a><br /> <a href="https://crieit.now.sh/upload_images/c76742bc401d73b46336fe6b13cfa4c161548878085d9.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/c76742bc401d73b46336fe6b13cfa4c161548878085d9.png?mw=700" alt="image (23).png" /></a><br /> - 特に制限もないので、自由に書きましたが、タグについては迷いましたね。サービスの特性上、割り当てるべき適切なタグかどれか未だにわかっていません。<br /> - しかしながら現時点でもView数は2です。そのため、こちらもMediumのフォロワーを増やしておくか、単なるプロダクト説明の置き場と割り切るかのどちらかが今後の戦略となりそうです。<br /> <a href="https://crieit.now.sh/upload_images/36006e156388e53b6d55afc31fa83f0a6154883e1dcc6.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/36006e156388e53b6d55afc31fa83f0a6154883e1dcc6.png?mw=700" alt="image (22).png" /></a></p> <hr /> <p><strong>HackersNews</strong><br /> <a href="https://crieit.now.sh/upload_images/1be5ea94b6dfb0a28f3b04305009befe6154880ca815b.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/1be5ea94b6dfb0a28f3b04305009befe6154880ca815b.png?mw=700" alt="image (21).png" /></a><br /> - Show HNを付けるというルールを守って投稿<br /> - 付けるURLはMediumのページか、サービス自体のURLかは迷いましたが、サービスにProductHuntへの誘導を用意していたので、後者を選択しました。<br /> - しかし残念ながら、GoogleAnalytics上は、HackersNewsからの流入は0でしたので、もう少し工夫が必要か、流れるのが早すぎて流入が期待できないかもしれません。</p> <hr /> <p><strong>Reddit</strong><br /> <a target="_blank" rel="nofollow noopener" href="https://www.reddit.com/r/ProductHunters/comments/px15lq/i_created_my_first_web_site_2go_2go_can_generate/">https://www.reddit.com/r/ProductHunters/comments/px15lq/i_created_my_first_web_site_2go_2go_can_generate/</a><br /> <a href="https://crieit.now.sh/upload_images/210f64e0ce8b159071e2738e1bd35776615487f13bade.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/210f64e0ce8b159071e2738e1bd35776615487f13bade.png?mw=700" alt="image (20).png" /></a><br /> - Karmaというポイントがないと記事が投稿できないという話を見たので、事前に記事投稿やコメントで参加してみました。特にGithub Issueの必要性などを投稿したりして、反応を窺うなどもトライしましたが、慣れていないせいもあって反応も薄かったです。<br /> - どのSubredditに投稿するかは検討しました。ProductHunt / Github / webdev / SaaS / startups / indieHackers / indieDevelopers等、色々調査しました。ProductHuntのSubredditが最も適切かと思いましたが、あまり活況がないのが心配でした。ただ、それ以外のところでは厳しい反応も予想されたので、シンプルにProductHuntのSubredditを投稿先と決定しました。<br /> - その他細かいことですが、RedditのProfileなどもきちんと整備しました。<br /> - しかしながら、RedditのSpamFilterに引っかかって削除されてしまったみたいです…</p> <hr /> <h5 id="Product Huntのupcoming Product"><a href="#Product+Hunt%E3%81%AEupcoming+Product">Product Huntのupcoming Product</a></h5> <p>あまり認知されていませんが、以下のようにサービスローンチ前にユーザーとコミュニケーションができるPre-Postサイトを作ることが出来ます。ただここからSubsriberは1人も登録されませんでした。おそらくProductHuntの著名なユーザーが、次期サービスを立ち上げる前にユーザーとコミュニケーションするためのプラットフォームだと感じましたので、私には当分必要ないと思いました。<br /> <a target="_blank" rel="nofollow noopener" href="https://www.producthunt.com/upcoming/2go">https://www.producthunt.com/upcoming/2go</a></p> <hr /> <h6 id="Product Hunt投稿内容の準備をする"><a href="#Product+Hunt%E6%8A%95%E7%A8%BF%E5%86%85%E5%AE%B9%E3%81%AE%E6%BA%96%E5%82%99%E3%82%92%E3%81%99%E3%82%8B">Product Hunt投稿内容の準備をする</a></h6> <p><a href="https://crieit.now.sh/upload_images/76ebfc15ab748145d665f1c4a4986b7e615487ca134e3.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/76ebfc15ab748145d665f1c4a4986b7e615487ca134e3.png?mw=700" alt="image (19).png" /></a><br /> Product Huntの投稿ページは入力したものが記録されるようになっているので、Submitボタンの直前までを入力しておいて事前に準備しておくことが可能です。今回の画像は、動画作成時に作っていたFigmaデータを画像データに変換して利用することとしました。特に動画は視覚的に、画像は落ち着いて読むように文字を入れるようにしました。</p> <p><a href="https://crieit.now.sh/upload_images/7c82e16b607550ccb13639965cdedb976154879728e68.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/7c82e16b607550ccb13639965cdedb976154879728e68.png?mw=700" alt="スクリーンショット 2021-09-26 16.20.56.png" /></a><br /> <a href="https://crieit.now.sh/upload_images/51240628b6d64bac3d7d4b51f2ba477e615487a283dfe.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/51240628b6d64bac3d7d4b51f2ba477e615487a283dfe.png?mw=700" alt="スクリーンショット 2021-09-26 16.03.43.png" /></a><br /> <a href="https://crieit.now.sh/upload_images/4cb7adb70db8bd9afc4ec8e3c63bc1ed615487a8a64c5.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/4cb7adb70db8bd9afc4ec8e3c63bc1ed615487a8a64c5.png?mw=700" alt="スクリーンショット 2021-09-26 16.04.35.png" /></a><br /> <a href="https://crieit.now.sh/upload_images/4937eca85eebe0d18e1ce6a5b2637f91615487ae2d3bc.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/4937eca85eebe0d18e1ce6a5b2637f91615487ae2d3bc.png?mw=700" alt="スクリーンショット 2021-09-26 16.04.11.png" /></a><br /> ここで用意しにくいのは、投稿ロゴとして利用できるGIFファイルだと思います。動画や画像イメージは比較的簡単に用意できますが、GIFファイルはどんなものにするところから微妙に悩ましい位置づけです。私は今回動画ファイルを作って出力した後、MovファイルからGIFに変換して利用しました。<br /> <a href="https://crieit.now.sh/upload_images/07acf05940475d852b84873e9766827461548726e8258.gif" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/07acf05940475d852b84873e9766827461548726e8258.gif?mw=700" alt="titleGIF.gif" /></a><br /> 目立たせるために、アニメーションというよりは、全体の色が大きく変わることで目につくように意識して作りました。</p> <hr /> <h5 id="著名なProduct HuntersにReview Request"><a href="#%E8%91%97%E5%90%8D%E3%81%AAProduct+Hunters%E3%81%ABReview+Request">著名なProduct HuntersにReview Request</a></h5> <p>先程の記事の中には著名なProductHunterにお願いして、彼らに周知してProductHuntで彼らをフォローしているユーザーに通知されるようにしたほうが良いという記述がありました。一方で、その機能はなくなったのでやる必要ないよというのも見受けられました。ということは、やってみるしかありません。まずは以下のサイトからTop Huntersを探しました。</p> <p><a target="_blank" rel="nofollow noopener" href="https://yvoschaap.com/producthunt/">https://yvoschaap.com/producthunt/</a></p> <p>それぞれがどういった方なのかまで抑えられなかったので、上から順番にTwitterを見て、直接DMを送れる人にていねいな英文を書いて送りました。…が、結果的に1人からも返信は返ってきませんでした。おそらく、こういった依頼が定常的に来るので、そもそもDMを開いていない可能性が高いです。既読にすらなりませんでしたので。</p> <p>では、このアプローチは不要なんでしょうか。その答えは、私のProductHuntの通知ページにありました。以下のようにフォローしているHunterがUpvoteすると、その通知がフォロワーには届きます。つまり、TopHuntersにUpvoteされると知られる確率が高くなるという理解をしています。ただ、彼らからUpvoteされるためにどうしたらよいかは未だにわかりません。Twitterでフォローされるわけもないので、ProductHuntでDiscussionに参加するなどして自分自身の露出が必要なのかもしれません。</p> <p><a href="https://crieit.now.sh/upload_images/93dd81709da263bd35dc9bdcde62bc5b615486ed95e33.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/93dd81709da263bd35dc9bdcde62bc5b615486ed95e33.png?mw=700" alt="tophunters.png" /></a></p> <p>その他に可能性があるとすると、以下は検討してみてもよいかと思いました。開発者のクローズドなコミュニティがあり、そこで繋がりが作れるかもしれません。なぜなら、$2,000/yearの有料会員制だからです。だからこそ、人数が爆発的に多いわけでもないですし、コミュニケーションが活発なのかもしれません。ただし、個人開発者がここに投資するのは、英語とお金が必要なので、ハードルが高いとは思います。</p> <p><a target="_blank" rel="nofollow noopener" href="https://megamaker.co/club/">https://megamaker.co/club/</a></p> <hr /> <h4 id="投稿当日の結果"><a href="#%E6%8A%95%E7%A8%BF%E5%BD%93%E6%97%A5%E3%81%AE%E7%B5%90%E6%9E%9C">投稿当日の結果</a></h4> <p>改めて、投稿当日の計画を振り返る前に、投稿曜日を検討していました。最初は準備ができた翌日に投稿しようかと思っていましたが、ちょうど週末でした。週末がどう影響するかがわからず、Active User数が減少するのではないかと推測して候補から外しました。また、週明けの月曜日も同様の理由で除外し、現地時間火曜日0時に投稿することとしました。</p> <ol> <li>太平洋時間 0:01(日本時間 16:01)からその日のランキングレース開始。</li> <li>1時間くらい様子をみて、Google や Facebook からのプロダクトなどのビッグサービスが投稿されていないのを確認してから投稿。</li> <li>Twitter で Product Hunt への投稿を英語でアナウンス。 @ProductHuntをメンションする。</li> <li>Product Hunt 上の紹介ページ URL が取得できるので、それを基に Product Hunt Card 作成(Medium の場合は URL を貼り付けるだけで OK)し、Medium 投稿。</li> <li>ProductHunt の URL を使って Landing Page に反映し、導線を作る。</li> <li>Medium の記事を、Product Hunt 上のプロダクト紹介ページに関連記事として貼り付け。</li> <li>Medium の記事を、Hacker News に投稿(Show HN:をタイトルにつけて投稿)</li> <li>Reddit に投稿</li> <li>コメントが入ったらそれに返信していく</li> </ol> <p>まず、予約投稿機能があるので、試してみましたが、正常に動作しませんでした。もしかすると私の現地時間の入力時間が間違っているかもしれませんが、念の為日本時間16時にはPCの前にいたほうがいいかもしれません。</p> <hr /> <h5 id="投稿後の動きの感想"><a href="#%E6%8A%95%E7%A8%BF%E5%BE%8C%E3%81%AE%E5%8B%95%E3%81%8D%E3%81%AE%E6%84%9F%E6%83%B3">投稿後の動きの感想</a></h5> <ul> <li>現地時間0時のタイミングでは、少しでもUpvoteを稼いだProductが上位に行きやすいです。そのため、初速を確保する知り合いやコミュニティへの事前声掛けは重要だと思いました(ただし、Upvoteを直接的に促してはいけません)</li> <li>必要初速Upvoteは10くらいだと思います。トップページに表示できるProductにも限りがあり、そこに載れなければ閲覧数も獲得できないです。</li> <li>必ずUpvote順にソートされるわけではありませんでした。コメントなどの他の要素も見ていると思われます。コメントはUpvote同様に初速に影響する可能性があるので、できれば事前に確保しておきたいところです。ただし、日本人のコミュニティにおいて、Upvoteはしてくれても、コメントまでしてくれる人を確保するのは至難の業だと思います。</li> <li>コメントがないと盛り上がりに欠けるのか、追加のコメントが得られにくいと思います。私はただの1つもコメントを最後まで得られませんでした。他のProductは、共同開発者などがコメントし合うことで盛り上がりを演出している印象を受けました。</li> <li>コメントが来たら返信しながら、投稿スレッドを盛り上げる準備をして待っていましたが、杞憂に終わってしまいました。</li> </ul> <hr /> <h5 id="結果と感想"><a href="#%E7%B5%90%E6%9E%9C%E3%81%A8%E6%84%9F%E6%83%B3">結果と感想</a></h5> <p><a href="https://crieit.now.sh/upload_images/74934dd4ad26684204b3a8d132c39ae6615486b229512.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/74934dd4ad26684204b3a8d132c39ae6615486b229512.png?mw=700" alt="スクリーンショット 2021-09-29 17.18.24.png" /></a></p> <p>最終的な結果はUpvote x15となりました。結果をまとめていきます。<br /> - Upvote x15(しかし、運営者ギルドから半数くらいはUpvoteしてくれた方がいたと思います)<br /> - 流入トラフィック x5(ほとんど流入が見込めませんでした)<br /> - 新規登録ユーザー数 x0(残念ながら一人も使ってくれませんでした)<br /> - サンプルを試したユーザー数 x2(流入トラフィックの割に確率が高かった)</p> <p>重要なのは…</p> <ul> <li>とにかく初速Upvoteを増やして、トップページに載らないと何も始まらない</li> <li>流入さえしてくれれば試してもらえる率はそこそこある</li> <li>投稿内容(文章・動画・画像)の良し悪しが影響するのかはわからなかった</li> <li>導線はほとんど影響しなかった(影響するように仕立て上げるには継続的な活動が必要)</li> <li>TopHuntersに認知される方法があれば、Upvoteを得るための近道になるかも。</li> </ul> <h2 id="ProductHuntへの挑戦は惨敗"><a href="#ProductHunt%E3%81%B8%E3%81%AE%E6%8C%91%E6%88%A6%E3%81%AF%E6%83%A8%E6%95%97">ProductHuntへの挑戦は惨敗</a></h2> <p>多くの方にご協力を頂きましたが、残念ながら私の個人開発サービスのProductHunt挑戦は惨敗に終わったと言って良いでしょう。ただし、考えうる限りのことはやり切ったので、次に繋がる敗戦だと考えています。</p> <p>海外進出のサービスの難しさを以下の点で痛感しています。<br /> - 海外でウケるサービスとかユースケースが、真の意味で理解できない可能性<br /> - 海外で求められるデザインやマーケティング・言い回しが適切かどうかわからない<br /> - 海外でのコミュニティ参加は更に負荷がかかるため、労力に見合うかが不安<br /> - 番外編)ユーザーに提供する規約の英文精査が難しい</p> <p>英語で開発すれば、非常に多くのユーザーに対する市場へのアプローチが可能ですが、このような深刻な悩みがついてまわることになります。いくつかの記事にもありましたが、英語圏の感覚を持つ人にヘルプしてもらわないと難しいかもしれません。</p> <h1 id="最後に…"><a href="#%E6%9C%80%E5%BE%8C%E3%81%AB%E2%80%A6">最後に…</a></h1> <p>想像してよりもずっとずっと個人開発は難しいです。プログラミングの世界だけでなく、デザイン・マーケティングまで1人で取り組まなければならず、かつアイデアの捻出とその中からの選定も全部自分の責任です。だからこそ楽しみがあるわけですが、大変さも同時に存在しています。</p> <p>企業よりも個人開発が気楽な分、企業よりもマーケティングや作業分担にお金がかけられません。特に個人開発はサービスが知られることがなければ、提供するユースケースが知られていないから失敗しているのか、知られていても失敗しているのか判断が難しくなります。</p> <p>そこでやはり重要となってくるのが、コミュニティだと思います。自分の作るプロダクトに意見を言ってくれる、試してみてくれる、拡散して届けてくれる人たちとともに歩まねば成功はあり得ません。そしてそれは継続的な努力によってのみ得られるものだと感じました。</p> <p>私にとって今回の開発は本当に作りたいものに対する準備の意味合いも兼ねていますが、コミュニティとともにモチベーションを維持し、成功の模索をしていきたいと思います。</p> <p>ということで、ぜひ気になる方はTwitterのフォローと、2goの利用をしてみて、フィードバックをいただければと思いますので、最後にリンクを貼って終わりにさせていただきます。</p> <p><a target="_blank" rel="nofollow noopener" href="https://twitter.com/jnakajima1982">https://twitter.com/jnakajima1982</a><br /> <a target="_blank" rel="nofollow noopener" href="https://2go.plus/">https://2go.plus/</a></p> <p>ありがとうございました!</p> jnakajima1982 tag:crieit.net,2005:PublicArticle/17641 2021-09-10T06:00:03+09:00 2021-09-10T06:00:03+09:00 https://crieit.net/posts/Github-Issues-2go Github Issuesをキレイに外部公開するサービス「2go」作ってみた <h1 id="はじめに"><a href="#%E3%81%AF%E3%81%98%E3%82%81%E3%81%AB">はじめに</a></h1> <p>今回初めて個人開発で作ったサービスを公開します。今までも何回かトライしていたのですが、なかなか公開までに至らないうちに、なにか問題にぶちあたったり、時間がかかりすぎて情熱が冷めてしまったりしていたので、今回はまずはスモールスタートで公開して改善していくことを心がけました。大体今回の公開まで、開発を始めてから1ヶ月程度になります。毎日朝4時半に起きて、子どもたちが起きる7時過ぎまでを開発時間として取り組んできました。<br /> <img src="https://storage.googleapis.com/zenn-user-upload/1463631d38d985beee6c9289.png" alt="" /></p> <h1 id="どんな人向け?"><a href="#%E3%81%A9%E3%82%93%E3%81%AA%E4%BA%BA%E5%90%91%E3%81%91%EF%BC%9F">どんな人向け?</a></h1> <p>自分で作っているサービスの開発ロードマップをユーザーに公開するために、都度ブログを書いたりするのも大変ですよね。もし、自分のサービスのソースコードをGithubで管理していたとして、Github Issuesを見せるとしてもGithubに馴染みのない人にとっては読みにくいですし、外に出すサービスであればあるほど、Githubのレポジトリは非公開になっていると思います。これをなんとかできないかなと思いました。<br /> <img src="https://storage.googleapis.com/zenn-user-upload/4aad80e2913adc2dac582847.png" alt="" /></p> <h1 id="どんなサービス?"><a href="#%E3%81%A9%E3%82%93%E3%81%AA%E3%82%B5%E3%83%BC%E3%83%93%E3%82%B9%EF%BC%9F">どんなサービス?</a></h1> <p>まずはこちらを見てください。<br /> <a target="_blank" rel="nofollow noopener" href="https://2go.plus/nice2h/2go/roadmap">https://2go.plus/nice2h/2go/roadmap</a><br /> このようにGithubのレポジトリ内のIssueをMilestone区切りにして、Githubに馴染みのない人にも見やすいロードマップサイトを公開するサービスです。もちろん、非公開のレポジトリを公開設定にすることなく、ロードマップだけを外部公開することができます。<br /> <img src="https://storage.googleapis.com/zenn-user-upload/18ca977fd4aa86432338179e.png" alt="" /><br /> Githubアカウントでログインすると、以下の設定画面になります。レポジトリを選択して、どのMilestoneを共有するかをチェックして保存します。すると、自動的にロードマップサイトのURLが生成されるので、こちらにアクセスすれば常に最新のロードマップを見ることができます。このURLをシェアすれば、多くの人にあなたのサービスのロードマップを簡単にキレイに公開できるわけです。<br /> <img src="https://storage.googleapis.com/zenn-user-upload/d3bf728edcb2592f63689133.png" alt="" /><br /> Milestoneを今まで使っていな方は、Milestoneを作成して、Issueを割り当てればロードマップを整理することができます。また、Labelを活用している方もいらっしゃると思いますので、Milestoneの中のLabelでIssueをフィルタすることもできます。これらを駆使することで、Bugは見せずにenhancementだけ表示するなどのことも可能です。<br /> 今後の予定としては、今はMilestone基点で表示しているところを、Labelベースで表示できるようにもしたいと思っています。その他は、まさに上記のURLを参考にしていただければ、いつも最新です。</p> <h1 id="どんな技術を使ってる?"><a href="#%E3%81%A9%E3%82%93%E3%81%AA%E6%8A%80%E8%A1%93%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6%E3%82%8B%3F">どんな技術を使ってる?</a></h1> <p>企業での開発経験はないので、全て自己流です。書籍やWebサイトを中心に勉強して開発しながら学んできました。念の為、利用している技術は以下のとおりです。<br /> - Laravel : APIとBlade(SSRはやってみたことあり)<br /> - Vue3 : フロントエンド(今回初挑戦)<br /> - Tailwindcss : デザイン(Bootstrapからの脱却)<br /> - Docker : これらの開発環境と、本番でもDocker利用(いずれ分割予定)<br /> - Cloud : Conoha VPS(インフラに時間を掛けたくなかった)</p> <h1 id="気になっていること(随時追加予定)"><a href="#%E6%B0%97%E3%81%AB%E3%81%AA%E3%81%A3%E3%81%A6%E3%81%84%E3%82%8B%E3%81%93%E3%81%A8%EF%BC%88%E9%9A%8F%E6%99%82%E8%BF%BD%E5%8A%A0%E4%BA%88%E5%AE%9A%EF%BC%89">気になっていること(随時追加予定)</a></h1> <ul> <li>開発環境はLaravel sailを使いましたが、本番は自前でdocker-compose.ymlを使いました。このあたりのコンテナの構成をどのようにしているか気になりました(本番でもsail使う?)</li> <li>本番環境にコードをpullしてからnpm run prodしてbuildが終わるまでにラグがあり、この間は使えない機能が出てしまったりするが、このあたりをみなさんはどのように工夫しているか?</li> <li>DBについては、どこまでレコードを暗号化するか。検索機能などとのトレードオフになると思われるが。。</li> </ul> <h1 id="今後は?"><a href="#%E4%BB%8A%E5%BE%8C%E3%81%AF%EF%BC%9F">今後は?</a></h1> <p>現在alphaフェーズですが、alphaとしてもう1段階リリースの予定があります。多くの人に使っていただき、Feedbackをいただきながら改善して、開発経験を増していきたいと思います。また、今回の個人開発は最初から海外も同様の問題を抱えていると思ったので、最初から英語圏も視野に入っており、今の所すべてのUIは英語にしています。以下の英語Twitterアカウントも作成して、情報発信していこうと思っています。</p> <p><a target="_blank" rel="nofollow noopener" href="https://twitter.com/2go_plus">https://twitter.com/2go_plus</a></p> <p>もちろん、日本語のTwitterは私の今まで通りのアカウントを利用していく予定です。<br /> <a target="_blank" rel="nofollow noopener" href="https://twitter.com/jnakajima1982">https://twitter.com/jnakajima1982</a><br /> ぜひご感想・ご指摘をいただければと思います。</p> jnakajima1982 tag:crieit.net,2005:PublicArticle/17546 2021-07-30T02:39:33+09:00 2021-07-30T02:39:33+09:00 https://crieit.net/posts/QnQ QnQ開発日誌 単体テストなしで大丈夫か? <p>デプロイしたらサインインしていない状態でサイトが見られなくなった、QnQの開発日誌ですこんにちは。今は修正している。</p> <p>Firebaseのセキュリティルールをミスっていたせいなんだが、Seleniumを使ったUIテストしかなく、匿名ユーザーの挙動がテストケースから漏れていたために気づかなかった。</p> <h2 id="単体テストがない"><a href="#%E5%8D%98%E4%BD%93%E3%83%86%E3%82%B9%E3%83%88%E3%81%8C%E3%81%AA%E3%81%84">単体テストがない</a></h2> <p>この程度のことは単体テストでやるのが良いのだろうと思うのだが、開発メンバーで真面目にフロントエンドをやってきた者がおらず、なかなかハードルが高い。まずVue.jsという時点で経験がないのに、そのうえFirebaseが絡むので、どうにも手を付けられていない。せめて、新しい知識がどちらか片方ならまだなんとかなったと思うのだが……。</p> <h2 id="UIテストだけ"><a href="#UI%E3%83%86%E3%82%B9%E3%83%88%E3%81%A0%E3%81%91">UIテストだけ</a></h2> <p>そんなわけで、せめてこれくらいは……とSelenium IDEを使ってUIテストだけなんとか実行している、というのが現状である。</p> <p>正直これがあるだけでも実は頑張った。さすがに何のテストもないのはまず過ぎる、と思って作ったのだが、大きな修正のたびに思いもよらないところでテストで引っかかってくれているので、実際役に立っている。</p> <p>ランスルーテストを流すと普通に数分かかるが、まぁその間はコーヒーでも入れて飲めばいいのだ。</p> <h2 id="単体テスト入れたいというより"><a href="#%E5%8D%98%E4%BD%93%E3%83%86%E3%82%B9%E3%83%88%E5%85%A5%E3%82%8C%E3%81%9F%E3%81%84%E3%81%A8%E3%81%84%E3%81%86%E3%82%88%E3%82%8A">単体テスト入れたいというより</a></h2> <p>しかしまぁ、さすがにそろそろ単体テスト入れなあかんよなぁと思うのだが、それにしてもハードルが高い……。</p> <p>というかコードがスパゲッティで非常に汚いし、そもそも抜本的にこのUIどうなの?と非常に思うところあるので、ぶっちゃけフルスクラッチで最初から書き直したほうがいいのかもしれない、とか思う。</p> <h2 id="ダイエットとバグ修正の話を一緒にできる、そうQnQならね"><a href="#%E3%83%80%E3%82%A4%E3%82%A8%E3%83%83%E3%83%88%E3%81%A8%E3%83%90%E3%82%B0%E4%BF%AE%E6%AD%A3%E3%81%AE%E8%A9%B1%E3%82%92%E4%B8%80%E7%B7%92%E3%81%AB%E3%81%A7%E3%81%8D%E3%82%8B%E3%80%81%E3%81%9D%E3%81%86QnQ%E3%81%AA%E3%82%89%E3%81%AD">ダイエットとバグ修正の話を一緒にできる、そうQnQならね</a></h2> <p>ちなみにこのバグ修正は、友人とWebサービス上でやりとりしていた。GitHubでもやりとりしたけど。<strong>俺の日記帳状態だったが、再び友人との交換日記状態に復帰している</strong>。やっぱり複数で使うと楽しい。</p> <p><a target="_blank" rel="nofollow noopener" href="https://qnqtree.com/tree/O65tm6rVyeu0rLPVuXTo">もくもく22 2021-07-28 | QnQ</a></p> <p>で、上記のスレなのだが、このサービスの特徴が出ている。</p> <p><img src="https://hack-le.com/wp-content/uploads/2021/07/9bf24c3324495cb8d1a1835f2e053603.png" alt="" /></p> <p>上図のように、<strong>「ダイエットの話」と「バグ修正」の話を並行している</strong>。こういう体験は他ではないはずで、自分たちでは「面白いと思うんだけどなぁ」と言いつつ、閑古鳥が鳴いている僕らのサービスである。</p> <p>なんか面白そうだなと思ったら、覗いてやってください。</p> tama tag:crieit.net,2005:PublicArticle/16695 2021-02-20T20:03:15+09:00 2021-02-20T20:10:54+09:00 https://crieit.net/posts/Vue-CLI-Stylus Vue CLIの環境変数でStylusのグローバル変数を入れ替えるし <h1 id="Vue CLIの環境変数でStylusのグローバル変数を入れ替えるし"><a href="#Vue+CLI%E3%81%AE%E7%92%B0%E5%A2%83%E5%A4%89%E6%95%B0%E3%81%A7Stylus%E3%81%AE%E3%82%B0%E3%83%AD%E3%83%BC%E3%83%90%E3%83%AB%E5%A4%89%E6%95%B0%E3%82%92%E5%85%A5%E3%82%8C%E6%9B%BF%E3%81%88%E3%82%8B%E3%81%97">Vue CLIの環境変数でStylusのグローバル変数を入れ替えるし</a></h1> <p>米:この記事の有効期限:このコードは今は動くが、いずれ動かなくなる。2021年2月20日記す</p> <h2 id="まず背景として"><a href="#%E3%81%BE%E3%81%9A%E8%83%8C%E6%99%AF%E3%81%A8%E3%81%97%E3%81%A6">まず背景として</a></h2> <p>うう... おれ、JavaScript、わからん。</p> <p>ぜんぜん、わからん。</p> <h3 id="何がしたいのか?"><a href="#%E4%BD%95%E3%81%8C%E3%81%97%E3%81%9F%E3%81%84%E3%81%AE%E3%81%8B%EF%BC%9F">何がしたいのか?</a></h3> <p>Stylusのグローバル変数にVue CLIの環境変数が使えたら便利やろなあ...</p> <h2 id="結論"><a href="#%E7%B5%90%E8%AB%96">結論</a></h2> <p>うう... おれ、なにも書けない、なにも出来ない。</p> <p>出来た事だけ書く。</p> <ul> <li>Stylusのグローバル変数が書かれたファイルを2つ用意した。ビルド用と、開発環境用にだ!</li> <li>Vue CLIの環境変数が書かれたファイルを2つ用意した。ビルド用と、開発環境用にだ!</li> <li>Vue CLIの環境変数にStylusのグローバル変数が書かれたファイルのパスを記述した</li> <li>vue.config.jsでStylusのグローバル変数が書かれたファイルをインポートした</li> <li>yarn serveしたら開発環境用が、yarn buildしたらビルド用が! やったぜ!!</li> </ul> <h2 id="コード"><a href="#%E3%82%B3%E3%83%BC%E3%83%89">コード</a></h2> <h3 id="Stylusのグローバル変数が書かれたファイル"><a href="#Stylus%E3%81%AE%E3%82%B0%E3%83%AD%E3%83%BC%E3%83%90%E3%83%AB%E5%A4%89%E6%95%B0%E3%81%8C%E6%9B%B8%E3%81%8B%E3%82%8C%E3%81%9F%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB">Stylusのグローバル変数が書かれたファイル</a></h3> <p>これらはプロジェクトフォルダ内の任意の場所に置く</p> <p><strong>src/assets/stylus-global.styl</strong></p> <pre><code><br />// .env.productionで呼び出されるファイル HaikeiShoku = #cccccc </code></pre> <p><strong>src/assets/stylus-global-development.styl</strong></p> <pre><code><br />// .env.developmentで呼び出されるファイル HaikeiShoku = #999999 </code></pre> <h3 id="Vue CLIの環境変数が書かれたファイル"><a href="#Vue+CLI%E3%81%AE%E7%92%B0%E5%A2%83%E5%A4%89%E6%95%B0%E3%81%8C%E6%9B%B8%E3%81%8B%E3%82%8C%E3%81%9F%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB">Vue CLIの環境変数が書かれたファイル</a></h3> <p>これらはプロジェクトフォルダのルートに置く</p> <p><strong>.env.production</strong></p> <pre><code><br />// yarn build したときに呼び出されるファイル VUE_APP_STYLUS_GLOBAL='src/assets/stylus-global.styl' </code></pre> <p><strong>.env.development</strong></p> <pre><code><br />// yarn serve したときに呼び出されるファイル VUE_APP_STYLUS_GLOBAL='src/assets/stylus-global-development.styl' </code></pre> <h3 id="vue.config.jsでStylusのグローバル変数が書かれたファイルをインポートする"><a href="#vue.config.js%E3%81%A7Stylus%E3%81%AE%E3%82%B0%E3%83%AD%E3%83%BC%E3%83%90%E3%83%AB%E5%A4%89%E6%95%B0%E3%81%8C%E6%9B%B8%E3%81%8B%E3%82%8C%E3%81%9F%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%82%92%E3%82%A4%E3%83%B3%E3%83%9D%E3%83%BC%E3%83%88%E3%81%99%E3%82%8B">vue.config.jsでStylusのグローバル変数が書かれたファイルをインポートする</a></h3> <p>これはプロジェクトフォルダのルートに置く</p> <p><strong>vue.config.js</strong></p> <pre><code>const path = require("path"); module.exports = { css: { loaderOptions: { stylus: { stylusOptions: { import: [path.join(__dirname, process.env.VUE_APP_STYLUS_GLOBAL)], }, }, }, }, }; </code></pre> <h3 id="App.vueとかどこかにグローバル変数を書いて試す"><a href="#App.vue%E3%81%A8%E3%81%8B%E3%81%A9%E3%81%93%E3%81%8B%E3%81%AB%E3%82%B0%E3%83%AD%E3%83%BC%E3%83%90%E3%83%AB%E5%A4%89%E6%95%B0%E3%82%92%E6%9B%B8%E3%81%84%E3%81%A6%E8%A9%A6%E3%81%99">App.vueとかどこかにグローバル変数を書いて試す</a></h3> <p>yarnでserveしたりbuildしたりして余韻に浸る。</p> <pre><code><br /><style lang="stylus"> #app font-family Avenir, Helvetica, Arial, sans-serif -webkit-font-smoothing antialiased -moz-osx-font-smoothing grayscale text-align center color #2c3e50 margin-top 60px background-color HaikeiShoku </style> </code></pre> <h3 id="vue.config.jsの補足"><a href="#vue.config.js%E3%81%AE%E8%A3%9C%E8%B6%B3">vue.config.jsの補足</a></h3> <h4 id="Yes! Good Look"><a href="#Yes%21+Good+Look">Yes! Good Look</a></h4> <p>このコードは動く。人類に未来が訪れる。</p> <pre><code>const path = require("path"); module.exports = { css: { loaderOptions: { stylus: { stylusOptions: { import: [path.join(__dirname, process.env.VUE_APP_STYLUS_GLOBAL)], }, }, }, }, }; </code></pre> <h4 id="Oh! Not Work"><a href="#Oh%21+Not+Work">Oh! Not Work</a></h4> <p>これらのコードは動かない。人類は滅亡する。</p> <p><strong>FAIL</strong></p> <pre><code>const path = require("path"); module.exports = { css: { loaderOptions: { stylus: { import: [path.join(__dirname, process.env.VUE_APP_STYLUS_GLOBAL)], }, }, }, }; </code></pre> <p><strong>FAIL</strong></p> <pre><code>const path = require("path"); module.exports = { css: { loaderOptions: { stylusOptions: { import: [path.join(__dirname, process.env.VUE_APP_STYLUS_GLOBAL)], }, }, }, }; </code></pre> <p><strong>VERDAMMT!!</strong></p> <pre><code>module.exports = { css: { loaderOptions: { stylus: { stylusOptions: { import: [process.env.VUE_APP_STYLUS_GLOBAL], }, }, }, }, }; </code></pre> <h2 id="参考文献"><a href="#%E5%8F%82%E8%80%83%E6%96%87%E7%8C%AE">参考文献</a></h2> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://cli.vuejs.org/guide/mode-and-env.html#modes">Modes and Environment Variables | Vue CLI</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://cli.vuejs.org/config/">Configuration Reference | Vue CLI</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://github.com/webpack-contrib/stylus-loader">GitHub - webpack-contrib/stylus-loader: A stylus loader for webpack.</a></li> </ul> <h2 id="検証環境"><a href="#%E6%A4%9C%E8%A8%BC%E7%92%B0%E5%A2%83">検証環境</a></h2> <p><strong>package.json</strong></p> <pre><code>{ "name": "try-globalval", "version": "0.1.0", "private": true, "scripts": { "serve": "vue-cli-service serve", "build": "vue-cli-service build", "lint": "vue-cli-service lint" }, "dependencies": { "core-js": "^3.8.1", "vue": "^3.0.4" }, "devDependencies": { "@babel/core": "^7.12.10", "@babel/eslint-parser": "^7.12.1", "@vue/cli-plugin-babel": "~5.0.0", "@vue/cli-plugin-eslint": "~5.0.0", "@vue/cli-service": "~5.0.0", "@vue/compiler-sfc": "^3.0.4", "@vue/eslint-config-prettier": "^6.0.0", "eslint": "^7.15.0", "eslint-plugin-prettier": "^3.2.0", "eslint-plugin-vue": "^7.2.0", "prettier": "^2.2.1", "stylus": "^0.54.8", "stylus-loader": "^4.3.1" } } </code></pre> fkuMnk tag:crieit.net,2005:PublicArticle/16226 2020-11-18T11:41:16+09:00 2020-11-19T13:59:08+09:00 https://crieit.net/posts/Vue-Draggable Vue.Draggable をスマホでも使う時のアレコレ <p>Vueでドラッグ&ドロップの何かしらを作ろうとした時に便利な<a target="_blank" rel="nofollow noopener" href="https://github.com/SortableJS/Vue.Draggable">Vue.Draggable</a>ですが、<br /> これまたスマホ対応している最中に困ったことがあったのでメモ。</p> <h2 id="困ったこと1:スクロールしようとするとドラッグ&ドロップされてしまう"><a href="#%E5%9B%B0%E3%81%A3%E3%81%9F%E3%81%93%E3%81%A8%EF%BC%91%EF%BC%9A%E3%82%B9%E3%82%AF%E3%83%AD%E3%83%BC%E3%83%AB%E3%81%97%E3%82%88%E3%81%86%E3%81%A8%E3%81%99%E3%82%8B%E3%81%A8%E3%83%89%E3%83%A9%E3%83%83%E3%82%B0%EF%BC%86%E3%83%89%E3%83%AD%E3%83%83%E3%83%97%E3%81%95%E3%82%8C%E3%81%A6%E3%81%97%E3%81%BE%E3%81%86">困ったこと1:スクロールしようとするとドラッグ&ドロップされてしまう</a></h2> <p>特にオプションなどの設定をせずデフォルトのままやると、リスト内の要素全体がドラッグ&ドロップできてしまうので、<br /> スマホだとうまくスクロールができない。</p> <pre><code class="html"><div id="app"> <draggable tag="ul"> <li v-for="item, index in items" :key="item.id"> <span>{</span><span>{</span>item.name<span>}</span><span>}</span> </li> </draggable> </div> </code></pre> <pre><code class="javascript">new Vue({ el: "#app", components: { 'draggable': draggable, }, data: { items:[] }, created() { for (var i = 0; i <= 30; i++) { this.items.push({id: i+1, name: `リスト${i + 1}`}) } } }); </code></pre> <p><a href="https://crieit.now.sh/upload_images/fef09829d50da55327f090d7279500b25fb48b1bb6ebc.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/fef09829d50da55327f090d7279500b25fb48b1bb6ebc.png?mw=700" alt="image" /></a></p> <h2 id="困ったこと2:要素の中のクリックイベントが動かない(Androidのみ?)"><a href="#%E5%9B%B0%E3%81%A3%E3%81%9F%E3%81%93%E3%81%A8%EF%BC%92%EF%BC%9A%E8%A6%81%E7%B4%A0%E3%81%AE%E4%B8%AD%E3%81%AE%E3%82%AF%E3%83%AA%E3%83%83%E3%82%AF%E3%82%A4%E3%83%99%E3%83%B3%E3%83%88%E3%81%8C%E5%8B%95%E3%81%8B%E3%81%AA%E3%81%84%EF%BC%88Android%E3%81%AE%E3%81%BF%EF%BC%9F%EF%BC%89">困ったこと2:要素の中のクリックイベントが動かない(Androidのみ?)</a></h2> <p>例えば、<br /> <a href="https://crieit.now.sh/upload_images/ad0586aa1a4628d79b6cd4caa62361935fb48bb64e05a.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/ad0586aa1a4628d79b6cd4caa62361935fb48bb64e05a.png?mw=700" alt="image" /></a></p> <p>のように、削除ボタンをリストの中に入れて、<br /> ボタンクリックで削除処理が行われる…みたいなときに、<br /> ボタンをクリックしても何も起こらない。<br /> ※自分はAndroidのみで確認。PC、iosは動く</p> <h2 id="解決策"><a href="#%E8%A7%A3%E6%B1%BA%E7%AD%96">解決策</a></h2> <p>2つとも同じ方法で対処。</p> <p>draggableタグにoptionsを設定して、<br /> ドラッグ&ドロップができる領域を指定してあげる。</p> <p>まずは以下のようにドラッグ&ドロップ可能にする要素を追加。<br /> 任意の名前でclassを付けておく。 今回は<code>class="handle"</code>とした。</p> <pre><code class="html"><div id="app"> <draggable tag="ul"> <li v-for="item, index in items" :key="item.id"> <!-- ↓追加 --> <span class="handle">●</span> <span>{</span><span>{</span>item.name<span>}</span><span>}</span> <button> 削除 </button> </li> </draggable> </div> </code></pre> <p><a href="https://crieit.now.sh/upload_images/5d2729a3176f2e2df0f3e56c859c01fa5fb48e1bf0315.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/5d2729a3176f2e2df0f3e56c859c01fa5fb48e1bf0315.png?mw=700" alt="image" /></a><br /> この●をクリックしているときだけドラッグ&ドロップ出来るようにする。</p> <p>draggableのoptionsを以下のように設定</p> <pre><code class="html"><draggable tag="ul" :options="{handle: '.handle'}"> ... </draggable> </code></pre> <p>これで、.handle要素をクリックorタップしたときのみドラッグ&ドロップが可能になり、<br /> スマホでのスクロールやボタンのクリックも問題なく動くようになった。</p> <p>いちおうここまでのサンプルを置いておきます。<br /> <a target="_blank" rel="nofollow noopener" href="https://jsfiddle.net/mimimimimi/vx1ksm8c/8/">https://jsfiddle.net/mimimimimi/vx1ksm8c/8/</a></p> <h2 id="参考"><a href="#%E5%8F%82%E8%80%83">参考</a></h2> <p><a target="_blank" rel="nofollow noopener" href="https://qiita.com/tobita0000/items/867d25e7287ddb5b9cc3">Vue.Draggableでドラッグ領域を指定する方法</a><br /> <a target="_blank" rel="nofollow noopener" href="https://www.programmersought.com/article/55644805280/">The click event bound to the drag element using vue-draggable is invalid on Android phones</a></p> <h3 id="どうでもいい余談"><a href="#%E3%81%A9%E3%81%86%E3%81%A7%E3%82%82%E3%81%84%E3%81%84%E4%BD%99%E8%AB%87">どうでもいい余談</a></h3> <p>解決した日に気づいたけど、Youtubeのプレイリストの中身の順番を変える時とかも、同じ様にアイコンをタップしないとドラッグできないようになっていた!<br /> こういう普段何気なく使っているところにヒントが隠されてたりするんだな~と思ったり…。</p> みみみみみ tag:crieit.net,2005:PublicArticle/16210 2020-11-10T18:19:54+09:00 2020-11-10T18:22:30+09:00 https://crieit.net/posts/Apache-Laravel-Vue-js-Vuetify-web 【Apache Laravel Vue.js Vuetify】webアプリをスマホに対応させるためにやったこと <p>Apache+Laravel+Vue.js+Vuetify で作った社内ツールをスマホ対応したときに行った作業内容メモ。<br /> <del>ほんとにiosが嫌いになりそうだった</del></p> <p>対応ブラウザは、</p> <ul> <li>chrome(PC, Android, ios)</li> <li>safari(ios)</li> </ul> <h2 id="まずはiPhone、Androidのデバッグが出来るように準備する"><a href="#%E3%81%BE%E3%81%9A%E3%81%AFiPhone%E3%80%81Android%E3%81%AE%E3%83%87%E3%83%90%E3%83%83%E3%82%B0%E3%81%8C%E5%87%BA%E6%9D%A5%E3%82%8B%E3%82%88%E3%81%86%E3%81%AB%E6%BA%96%E5%82%99%E3%81%99%E3%82%8B">まずはiPhone、Androidのデバッグが出来るように準備する</a></h2> <p>それぞれの端末を持っていれば、PCに繋いでデバッグができる!なんて素晴らしい。<br /> iosはiPhone以外にもiPad、iPod touch でも問題なし。<br /> それぞれ以下を参考に。</p> <p>Android:<a target="_blank" rel="nofollow noopener" href="https://qiita.com/hojishi/items/12b726f8b02ef3d713e4">https://qiita.com/hojishi/items/12b726f8b02ef3d713e4</a><br /> ios:<a target="_blank" rel="nofollow noopener" href="https://webkatu.com/archives/ios-safari-debug-on-windows/">https://webkatu.com/archives/ios-safari-debug-on-windows/</a></p> <h2 id="Polyfillを入れる"><a href="#Polyfill%E3%82%92%E5%85%A5%E3%82%8C%E3%82%8B">Polyfillを入れる</a></h2> <p><a target="_blank" rel="nofollow noopener" href="https://v2.vuetifyjs.com/ja/getting-started/browser-support/">Vuetifyの公式ドキュメント</a>に則って入れればOK。</p> <pre><code class="bash">npm install babel-polyfill --save npm install @babel/preset-env --save </code></pre> <p>インストール後、<br /> <code>import Vue from 'vue'</code> とか<br /> <code>import vuetify from '@/plugins/vuetify</code><br /> を書いているファイルに、<br /> <code>import 'babel-polyfill'</code> を追加。<br /> (自分の場合は <code>/resources/assets/js/app.js</code>)</p> <p>ルートディレクトリの .babelrcに、</p> <pre><code> { "presets": ["@babel/preset-env"] } </code></pre> <h2 id="対応していないjsを地道に直す"><a href="#%E5%AF%BE%E5%BF%9C%E3%81%97%E3%81%A6%E3%81%84%E3%81%AA%E3%81%84js%E3%82%92%E5%9C%B0%E9%81%93%E3%81%AB%E7%9B%B4%E3%81%99">対応していないjsを地道に直す</a></h2> <p>特に問題なければ、↑を行えば画面は表示されるはず…と思っていたのだが、画面が真っ白だった。</p> <p>エラーを調べると、どうやら正規表現で後読みを使っているのが原因みたいだったので、該当箇所を修正。<br /> (safariは後読みに対応していない)</p> <p>※書いている最中に気づいたけど、それってつまりうまくPolyfillが使えていなかったのでは…??</p> <h2 id="「Error: Network Error POST 応答を解析できません」を直す"><a href="#%E3%80%8CError%3A+Network+Error%E3%80%80POST++%E5%BF%9C%E7%AD%94%E3%82%92%E8%A7%A3%E6%9E%90%E3%81%A7%E3%81%8D%E3%81%BE%E3%81%9B%E3%82%93%E3%80%8D%E3%82%92%E7%9B%B4%E3%81%99">「Error: Network Error POST 応答を解析できません」を直す</a></h2> <p>iosのみ起きる謎現象。これに2日かかった…<br /> 自分は、Apacheの設定を変更で直った。</p> <pre><code><Directory "/var/www/html/public"> AllowOverride All # Allow open access: Header unset Upgrade ←これを追加 </Directory> </code></pre> <p>とりあえずここまでで画面が開けるようになりました。<br /> 以降は細かい部分です。</p> <h2 id="スクロールを入れたくないのにスマホだと入ってしまう"><a href="#%E3%82%B9%E3%82%AF%E3%83%AD%E3%83%BC%E3%83%AB%E3%82%92%E5%85%A5%E3%82%8C%E3%81%9F%E3%81%8F%E3%81%AA%E3%81%84%E3%81%AE%E3%81%AB%E3%82%B9%E3%83%9E%E3%83%9B%E3%81%A0%E3%81%A8%E5%85%A5%E3%81%A3%E3%81%A6%E3%81%97%E3%81%BE%E3%81%86">スクロールを入れたくないのにスマホだと入ってしまう</a></h2> <p>webアプリ全体にスクロールを入れたくない場合、bodyに<code>height:100%</code>とか入れればPCでは大丈夫だったりしたんですが、<br /> スマホだとどうにも謎のスクロールが入ってしまうので、以下のように対応。</p> <pre><code class="html"><html id="content">  … </code></pre> <p>※jsでhtmlのところにstyleを指定する方法がわからなかったのでid属性を無理やり、、、</p> <pre><code class="js">const windowHeight = window.innerHeight; // windowの高さを取得 // <html>の高さを、windowsの高さいっぱいに設定 document.getElementById("content").style.height = windowHeight + "px"; // bodyはheight:100% document.body.style.height = "100%"; // スクロールバー非表示 // スマホのときはbodyに、PCのときはhtmlにかけないと反映されないっぽいので分ける const isSP = /iPhone|iPod|iPad|Android/i.test(navigator.userAgent); if (isSP) { document.body.style.overflowY = "hidden"; } else { document.getElementById("content").style.overflowY = "hidden"; } </code></pre> <p>…無理矢理感半端ないけど、ひとまずこれでPCでもスマホでも画面いっぱいでコンテンツが収まるようになった。</p> <h2 id="iosだけ、キーボードのenterでボタンクリック判定されてしまう"><a href="#ios%E3%81%A0%E3%81%91%E3%80%81%E3%82%AD%E3%83%BC%E3%83%9C%E3%83%BC%E3%83%89%E3%81%AEenter%E3%81%A7%E3%83%9C%E3%82%BF%E3%83%B3%E3%82%AF%E3%83%AA%E3%83%83%E3%82%AF%E5%88%A4%E5%AE%9A%E3%81%95%E3%82%8C%E3%81%A6%E3%81%97%E3%81%BE%E3%81%86">iosだけ、キーボードのenterでボタンクリック判定されてしまう</a></h2> <p>PCやAndroidでは大丈夫なのに、iosだけ起きる謎現象。<br /> 改行する度にボタンが押されて送信されるという、、、、</p> <p>※単純にv-formのsubmitイベントが発火してしまっている場合は、<a target="_blank" rel="nofollow noopener" href="https://riotz.works/articles/lopburny/2019/07/31/page-reload-issue-by-implicit-submission/">こちら</a>を参考に。</p> <p>根本的な原因はわからなかったため、<br /> 入力文字が空欄・改行のみの場合は、ボタンクリックで呼ばれる処理が走らないようにした。</p> <pre><code class="html"><textarea v-model="inputText"></textarea> <v-btn @click="submit()">送信</v-btn> </code></pre> <pre><code class="js"><br />data: () => ({ inputText: "", }), computed: { /** * 空文字判定 */ isTextEmpty() { return !this.inputText || !this.inputText.match(/\S/g); }, }, methods: { submit() { if (this.isTextEmpty) return; // 以下送信処理 } } </code></pre> <h2 id="iosだけファイルアップロードができない"><a href="#ios%E3%81%A0%E3%81%91%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%82%A2%E3%83%83%E3%83%97%E3%83%AD%E3%83%BC%E3%83%89%E3%81%8C%E3%81%A7%E3%81%8D%E3%81%AA%E3%81%84">iosだけファイルアップロードができない</a></h2> <p>ファイル選択のダイアルボックスを開く→ファイルを選択→アップロード<br /> という流れのとき、iosだけダイアルボックスが開かないという謎現象。<br /> エラーも出ない…</p> <p>調べてみたら、どうやら<code>input.onchange</code>が動いていないようだった。</p> <pre><code class="javascript">function uplod() { return new Promise(resolve => { const input = document.createElement('input'); input.type = "file"; input.multiple = false; input.accept = "image/gif,image/jpeg,image/png"; input.onchange = event => { var file = event.target.files[0]; resolve(file); }; input.click(); }); } </code></pre> <p>ios以外は↑でも動いたが、どうやらiosではちゃんと<code><input></code>要素をDOMに追加してあげないといけないらしい。<br /> 参考:<a target="_blank" rel="nofollow noopener" href="https://stackoverflow.com/questions/47664777/javascript-file-input-onchange-not-working-ios-safari-only">https://stackoverflow.com/questions/47664777/javascript-file-input-onchange-not-working-ios-safari-only</a></p> <p>というわけで、以下のように修正</p> <pre><code class="javascript">function uplod() { return new Promise(resolve => { const input = document.createElement('input'); input.type = 'file'; input.multiple = false; input.accept = "image/gif,image/jpeg,image/png"; document.body.appendChild(input); // ←DOMに追加 input.onchange = event => { var file = event.target.files[0]; document.body.removeChild(input); // ←DOMから削除 resolve(file); }; input.click(); }); } </code></pre> <h2 id="iosだけ画像が縦に伸びる時"><a href="#ios%E3%81%A0%E3%81%91%E7%94%BB%E5%83%8F%E3%81%8C%E7%B8%A6%E3%81%AB%E4%BC%B8%E3%81%B3%E3%82%8B%E6%99%82">iosだけ画像が縦に伸びる時</a></h2> <p>これは別記事にもメモしてあるが、<br /> <code>display:flex;</code>を指定している箇所に、<code>align-items: flex-start;</code>を追記するだけ。<br /> 参考:<a target="_blank" rel="nofollow noopener" href="https://nichiyogogo.com/image-looks-stretched/">https://nichiyogogo.com/image-looks-stretched/</a></p> <h2 id="iosだけ、heightを指定した要素内の「overflow-y: visible scroll;」が効かない"><a href="#ios%E3%81%A0%E3%81%91%E3%80%81height%E3%82%92%E6%8C%87%E5%AE%9A%E3%81%97%E3%81%9F%E8%A6%81%E7%B4%A0%E5%86%85%E3%81%AE%E3%80%8Coverflow-y%3A+visible+scroll%3B%E3%80%8D%E3%81%8C%E5%8A%B9%E3%81%8B%E3%81%AA%E3%81%84">iosだけ、heightを指定した要素内の「overflow-y: visible scroll;」が効かない</a></h2> <p>これもiosの一部で起きる現象。<br /> safariのバージョンの問題かもしれない。</p> <p>ますは<a target="_blank" rel="nofollow noopener" href="https://shanabrian.com/web/html-css-js-technics/css-ios-safari-02.php">ここ</a>を参考に修正。<br /> 自分はこのときにも頑なに<code>overflow-y: visible scrol</code>と書き続けていたら直らず、<br /> 素直に <code>overflow-y:auto;</code> にするだけで解決…。</p> <h2 id="おわり"><a href="#%E3%81%8A%E3%82%8F%E3%82%8A">おわり</a></h2> <p>ほとんどios対応ですね。<br /> Androidはグリッドをちゃんと設定して、表示崩れしないようにだけしてあげれば殆ど動きました。<br /> マジでiosとsafariが嫌いになりそうだった…。</p> みみみみみ tag:crieit.net,2005:PublicArticle/16051 2020-09-07T14:22:14+09:00 2020-09-07T23:02:38+09:00 https://crieit.net/posts/Nuxt-js-lint Nuxt.jsにlintを導入してみた <h2 id="動機"><a href="#%E5%8B%95%E6%A9%9F">動機</a></h2> <p>以前からなんとなくeslintは導入してあーだこーだしてたのですが、ポートフォリオサイトやブログサイトをNetlifyからFirebaseに移植する際に、github actions(CI)を使うようになり、せっかくだからもう少し本格的にlintやtestを導入してみようということで、色々調べて導入してみました。</p> <p>その手順を初心者なりに備忘録としてまとめてみました。<br /> (だいぶ我流な部分もありますが多めに見ていただければ幸いです)</p> <p>アドバイスなどは大歓迎です。</p> <p>知識が増えたら随時更新します。</p> <p><a target="_blank" rel="nofollow noopener" href="https://blog.taka1156.site">https://blog.taka1156.site</a></p> <p><a target="_blank" rel="nofollow noopener" href="https://github.com/taka1156/nuxt-blog">https://github.com/taka1156/nuxt-blog</a> - アトミックデザインの形にプロジェクトを見直し中</p> <p>↑ブログのプロジェクトで導入してみました。(Nuxt.js + Firebase + microCMS 構成)</p> <h2 id="環境"><a href="#%E7%92%B0%E5%A2%83">環境</a></h2> <ul> <li>mac OS Catalina</li> <li>Node.js v12.16.1</li> <li>npm v6.14.3</li> <li>yarn v1.22.0</li> </ul> <h2 id="手順"><a href="#%E6%89%8B%E9%A0%86">手順</a></h2> <ol> <li><p>これらをインストール</p> <ul> <li><p>eslint関連<br /> <code>yarn add eslint eslint-config-prettier eslint-plugin-prettier eslint-plugin-vue babel-eslint</code></p></li> <li><p>stylelint関連<br /> <code>yarn add stylelint stylelint-config-standard stylelint-config-prettier stylelint-prettier</code></p></li> <li><p>prettier<br /> <code>yarn add prettier</code></p></li> </ul> <p>※ なんでprettierを入れたか<br /> eslintだけだとエラーは出すが修正はしない項目(コード整形関連)もあるので併用する。<br /> (逆にprettierは細かい命名規則や処理に関するエラーは出さない?)</p></li> <li><p>設定ファイルを書く</p> <ul> <li>.eslintrc.json</li> </ul> <pre><code class="json">{ "parserOptions": { "parser": "babel-eslint", "sourceType": "module" }, "extends": [ "eslint:recommended", "plugin:vue/recommended", "plugin:prettier/recommended", "prettier/vue" ], "plugins": [ "vue" ], "env": { "jest": true, "browser": true, "es6": true, "node": true }, "globals": {}, "rules": { "camelcase": [2,{"properties": "always"}], "quotes": [2,"single", { "avoidEscape": true } ], "eqeqeq": [2, "always", {"null": "ignore"}], "prefer-const": 2, "vue/component-name-in-template-casing": "off", "vue/no-v-html": "off" } } </code></pre> <ul> <li>.stylelintrc.json</li> </ul> <pre><code class="json">{ "extends": ["stylelint-config-standard", "stylelint-prettier/recommended"], "rules": { "color-no-invalid-hex": true, "color-hex-case": "lower", "selector-class-pattern": "^(?:(?:o|c|u|t|s|is|has|_|js|qa)-)?[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*(?:__[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*)?(?:--[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*)?(?:\\[.+\\])?$" } } </code></pre> <ul> <li>.eslintignore, .stylelintignore<br /> ※ storybook-static、coverage は storybookやjestを導入していると生成されるファイル</li> </ul> <pre><code class="txt"># ビルドファイル dist storybook-static coverage # 設定、モジュール .nuxt .storybook node_modules </code></pre> <ul> <li>.prtteirrc.json</li> </ul> <pre><code class="json">{ "printWidth": 85, "tabWidth": 2, "singleQuote": true, "semi": true, "bracketSpacing": true, "arrowParens": "avoid" } </code></pre></li> <li><p>scriptsを追加</p> <pre><code class="json">"scripts": { "lint-js": "eslint --ext .js,.vue .", "lint-style": "stylelint \"**/*.vue\" \"**/*.css\"", "lint-js:fix": "eslint --ext .js,.vue . --fix", "lint-style:fix": "stylelint \"**/*.vue\" \"**/*.css\" --fix", "lint:check": "yarn lint-js & yarn lint-style", "lint:fix": "yarn lint-js && yarn lint-style", ... } </code></pre></li> <li><p>お好みでvscode設定を追加<br /> 保存時に勝手にlint処理が走るようになるので便利<br /> (vscode/setting.jsonに記述)</p> <pre><code class="json">{ "comments": "保存時にeslint、stylelintを自動実行", "eslint.alwaysShowStatus": true, "editor.codeActionsOnSave": { "source.fixAll.eslint": true, "source.fixAll.stylelint": true }, } </code></pre></li> </ol> <h2 id="まとめ"><a href="#%E3%81%BE%E3%81%A8%E3%82%81">まとめ</a></h2> <p>eslint、stylelintについてざっくりと使い方やルールの指定方法についてわかってきたので、<br /> 積極的に活用して読みやすいコードを意識していきたいですね。</p> <p>また、eslintルールについても、気になるものがあればまとめてみようと思います。</p> taka1156 tag:crieit.net,2005:PublicArticle/15953 2020-06-14T15:24:10+09:00 2021-05-17T21:59:25+09:00 https://crieit.net/posts/introducing-portaldots 大学祭の参加団体向けウェブシステムをOSS化してみた <p><a href="https://crieit.now.sh/upload_images/658e61a355af5e8c729368deafa1ffd35ee268532719f.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/658e61a355af5e8c729368deafa1ffd35ee268532719f.png?mw=700" alt="main_screenshot.png" /></a></p> <h2 id="はじめに"><a href="#%E3%81%AF%E3%81%98%E3%82%81%E3%81%AB">はじめに</a></h2> <p>大学祭では、たくさんの参加団体(サークル・部活)が模擬店などの企画を出店し、盛り上げています。</p> <p>大学祭を成功させるには、実行委員会と参加団体の間の連携が欠かせません。連携するための方法として、実行委員会では、参加団体からの「各種申請」を受け付けています。例えば、大学祭当日に配布されるパンフレットに掲載する内容を参加団体から募集する必要があります。また、実行委員会が貸し出す備品の申請を受け付けることもあります。</p> <p>こうした申請受付業務は、多くの大学祭では紙による受付であったり、メールや Google フォームでの受付を行っているところが多いかと思います。</p> <p>紙やメールによる受付の場合、紙・メールに書かれた内容を1枚1枚Excelに入力していく手間がかかります。受付方法によっては対面での対応が必要となり、昨今の状況下では厳しいものがあります。<br /> (そもそも、今年の秋冬に開催される学園祭でも、予定通り開催できるかどうか怪しいところではありますが……)</p> <p>そのような中、私は<strong>大学祭の各種申請などを受け付けるウェブシステムを開発し、今年になってそれを OSS 化</strong>してみました。</p> <ul> <li>GitHub : <a target="_blank" rel="nofollow noopener" href="https://github.com/portal-dots/PortalDots">https://github.com/portal-dots/PortalDots</a></li> <li>PortalDots 公式ウェブサイト(2020/11/30 公開) : <a target="_blank" rel="nofollow noopener" href="https://dots.soji.dev">https://dots.soji.dev</a></li> </ul> <h2 id="私は誰?"><a href="#%E7%A7%81%E3%81%AF%E8%AA%B0%EF%BC%9F">私は誰?</a></h2> <p>私は、東京理科大学の野田キャンパスに通う大学生です。大学名こそ「東京」とついていますが、「野田」は「千葉県」にあります。そんな野田キャンパスで開催される学園祭「野田地区理大祭」の実行委員をしていました。</p> <p>実行委員時代は、PortalDots の開発のほか、公式ウェブサイト・パンフレットのデザイン・実装なども行っていました。</p> <ul> <li>野田地区理大祭公式ウェブサイト : <a target="_blank" rel="nofollow noopener" href="https://nodaridaisai.com/">https://nodaridaisai.com/</a></li> </ul> <h2 id="参加団体向けウェブシステム「PortalDots」"><a href="#%E5%8F%82%E5%8A%A0%E5%9B%A3%E4%BD%93%E5%90%91%E3%81%91%E3%82%A6%E3%82%A7%E3%83%96%E3%82%B7%E3%82%B9%E3%83%86%E3%83%A0%E3%80%8CPortalDots%E3%80%8D">参加団体向けウェブシステム「PortalDots」</a></h2> <p><a href="https://crieit.now.sh/upload_images/21e25532d98d0eaccd44964220ac8f765ee5e5b35aaeb.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/21e25532d98d0eaccd44964220ac8f765ee5e5b35aaeb.png?mw=700" height="40" alt="PortalDotsのロゴ"></a></p> <ul> <li>GitHub : <a target="_blank" rel="nofollow noopener" href="https://github.com/portal-dots/PortalDots">https://github.com/portal-dots/PortalDots</a> <ul> <li>よろしければぜひ Star お願いします…!</li> </ul></li> </ul> <h3 id="開発環境の動かし方"><a href="#%E9%96%8B%E7%99%BA%E7%92%B0%E5%A2%83%E3%81%AE%E5%8B%95%E3%81%8B%E3%81%97%E6%96%B9">開発環境の動かし方</a></h3> <p>開発環境を動かすには Git、PHP(7.4以上)、Node.js、Yarn、Docker がセットアップ済みである必要があります。</p> <p><strong>2020/06/27 追記</strong> : コマンドの実行順序が間違っていたので修正しました。<br /> <strong>2021/05/17 追記</strong> : 開発環境の起動方法の変更を反映しました。</p> <pre><code class="bash">$ git clone [email protected]:portal-dots/PortalDots.git $ cd PortalDots/ # 必要な Node.js パッケージをインストール # ※ エラーが表示される場合は、Node.js を最新バージョンにアップグレードした上で、再度 yarn install を実行してください。 $ yarn install # 設定ファイルを作成 $ cp .env.example .env $ php artisan key:generate # 開発環境を起動する $ yarn docker # マイグレーション(データベースのセットアップ) $ yarn migrate # Docker コンテナ内で必要な PHP パッケージをインストール $ yarn docker-bash $ composer install $ exit # フロントエンド開発サーバーの起動 $ yarn hot # → ブラウザで http://localhost にアクセスすると、PortalDots の開発環境が起動する # → フロントエンド開発サーバーを終了するには Ctrl + C を押す # 開発環境を停止する $ yarn docker-stop </code></pre> <h3 id="トップページ"><a href="#%E3%83%88%E3%83%83%E3%83%97%E3%83%9A%E3%83%BC%E3%82%B8">トップページ</a></h3> <p>トップページでは、参加団体向け説明会の次回日程の表示機能や、各種お知らせの閲覧、配布資料のダウンロードなどができるようになっています。</p> <p>企画参加登録の受付期間中は、提出している参加登録の受理状況も確認できます。</p> <p><a href="https://crieit.now.sh/upload_images/42a053619c899ae5db5c6cbc04f3d5eb5ee266d90c32e.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/42a053619c899ae5db5c6cbc04f3d5eb5ee266d90c32e.png?mw=700" alt="screenshot_home.png" /></a></p> <h3 id="企画参加登録"><a href="#%E4%BC%81%E7%94%BB%E5%8F%82%E5%8A%A0%E7%99%BB%E9%8C%B2">企画参加登録</a></h3> <p>学園祭への企画エントリーもウェブから可能です。</p> <p><a href="https://crieit.now.sh/upload_images/942b1908f49e4c9db2e9466e90b229eb5ee267d059776.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/942b1908f49e4c9db2e9466e90b229eb5ee267d059776.png?mw=700" alt="screenshot_circle_register.png" /></a></p> <h3 id="申請フォーム"><a href="#%E7%94%B3%E8%AB%8B%E3%83%95%E3%82%A9%E3%83%BC%E3%83%A0">申請フォーム</a></h3> <p>大学祭の参加団体は、このようなフォームからパンフレット掲載内容などの情報を委員会へ提出することができます。</p> <p>受付期間を設定することも可能です。</p> <p><a href="https://crieit.now.sh/upload_images/98cb9cc9bf6a8e93ec300a84080c92345ee2679b7ac94.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/98cb9cc9bf6a8e93ec300a84080c92345ee2679b7ac94.png?mw=700" alt="image.png" /></a></p> <h3 id="フォームエディター(Vue.js 製)"><a href="#%E3%83%95%E3%82%A9%E3%83%BC%E3%83%A0%E3%82%A8%E3%83%87%E3%82%A3%E3%82%BF%E3%83%BC%28Vue.js+%E8%A3%BD%29">フォームエディター(Vue.js 製)</a></h3> <p>「フォームエディター」は <strong>PortalDots の目玉機能(?)</strong> です。</p> <p>実行委員は、各種申請を受け付けるフォームを Google フォームのようなノリで作成することができるようになっています。</p> <p>「じゃあ、Google フォーム使えば良いのでは?」と思われるかもしれませんが、Google フォームではログイン・新規登録機能(※1)は利用できない上、回答内容の編集が容易でなかったり(※2)、<strong>1団体あたり1回答までに制限できなかったり</strong>(※3)するなど、大学祭の申請フォームとしては不便なところもあります。</p> <p>PortalDots の「フォームエディター」で作成できるフォームは、回答受付期間を設定できたり、1企画につき1回まで回答可能という設定ができたり、あとから回答を簡単に修正できたりします。</p> <p>※1 : Google フォームでも、一応 Google アカウントでのログインを必須にすることはできます</p> <p>※2 : Google フォームでも、回答者に回答の編集を許可することはできます。ただ、編集用の URL を紛失してしまうと編集できなくなってしまいます</p> <p>※3 : Google フォームでは、<strong>1ユーザーあたり</strong>の回答数を制限できます。<strong>1団体あたり</strong>のような制限をかけるのは難しいと思われます</p> <p><a href="https://crieit.now.sh/upload_images/fd82398a4a47ec43ce8edc68a27147cd5ee2676b1a9f0.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/fd82398a4a47ec43ce8edc68a27147cd5ee2676b1a9f0.png?mw=700" alt="screenshot_form_editor.png" /></a></p> <blockquote class="twitter-tweet" data-conversation="none"><p lang="ja" dir="ltr">参加企画に提出してもらう各種申請もウェブフォームで受け付けられます。動画のように設問ぐりぐりできます。 <a target="_blank" rel="nofollow noopener" href="https://t.co/5rVFRVnUPd">pic.twitter.com/5rVFRVnUPd</a></p>— Soji — PortalDots (@sofpyon) <a target="_blank" rel="nofollow noopener" href="https://twitter.com/sofpyon/status/1271122648928403456?ref_src=twsrc%5Etfw">June 11, 2020</a></blockquote> <h2 id="CodeIgniter から Laravel への移行途中です → 完了しました!"><a href="#CodeIgniter+%E3%81%8B%E3%82%89+Laravel+%E3%81%B8%E3%81%AE%E7%A7%BB%E8%A1%8C%E9%80%94%E4%B8%AD%E3%81%A7%E3%81%99+%E2%86%92+%E5%AE%8C%E4%BA%86%E3%81%97%E3%81%BE%E3%81%97%E3%81%9F%EF%BC%81">CodeIgniter から Laravel への移行途中です → 完了しました!</a></h2> <p><strong>2021/05/17追記 : 先日、CodeIgniter のコードを全て削除し、Laravel へ完全移行しました!</strong></p> <blockquote class="twitter-tweet" data-lang="ja" data-dnt="true"><p lang="ja" dir="ltr">ついに <a target="_blank" rel="nofollow noopener" href="https://twitter.com/hashtag/PortalDots?src=hash&ref_src=twsrc%5Etfw">#PortalDots</a> のコードが、悲願の 100% Laravel 化達成しました…!CodeIgniter 消します! <a target="_blank" rel="nofollow noopener" href="https://t.co/vt1RKYDfcL">pic.twitter.com/vt1RKYDfcL</a></p>— Soji (@sofpyon) <a target="_blank" rel="nofollow noopener" href="https://twitter.com/sofpyon/status/1388511050224472066?ref_src=twsrc%5Etfw">2021年5月1日</a></blockquote> <hr /> <p>(以下、追記前)</p> <p>現在、「CodeIgniter」と「Laravel」という、2つのウェブフレームワークを混在して使用しています。</p> <p>元々 PortalDots は、私がウェブフレームワーク初心者のころ(大学1年の夏)に開発を始めた物でした。それ以前はフレームワークという物自体を使ったことがなく、プレーンな PHP コードでしか書いたことがなかったのですが、「CodeIgniter というフレームワークは簡単」だという話を聞き、試しに PortalDots の開発で使ってみたのでした。</p> <p>しかし、CodeIgniter の機能は貧弱である点や、なるべくメインストリームにあるフレームワークを使ったほうが今後のメンテナンスがしやすいだろうということで、Laravel への移行をはじめました。</p> <p>2020年6月現在、CodeIgniter が使われているのは「スタッフモード」(実行委員用のページ)のみとなっており、それ以外のページは Laravel に移行済みとなっています。</p> <h2 id="おわりに"><a href="#%E3%81%8A%E3%82%8F%E3%82%8A%E3%81%AB">おわりに</a></h2> <p>OSS 化したので PortalDots はどなたでも自由にお使いいただけるようになりました。</p> <p>学園祭実行委員会に所属しており、かつプログラミング経験のある方がいましたら、ぜひ使っていただきたいです。また、学園祭関係者でなくても、お試しとして実物を触っていただけると嬉しいです!</p> <ul> <li>GitHub : <a target="_blank" rel="nofollow noopener" href="https://github.com/portal-dots/PortalDots">https://github.com/portal-dots/PortalDots</a></li> <li>PortalDots 公式ウェブサイト(2020/11/30 公開) : <a target="_blank" rel="nofollow noopener" href="https://dots.soji.dev">https://dots.soji.dev</a></li> </ul> SofPyon 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="ドラッグ&amp;ドロップ編集機能"><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/15895 2020-05-10T20:43:39+09:00 2020-05-10T22:30:35+09:00 https://crieit.net/posts/electron-vue-5eb7e8ebc5bbc electron-vueでマークダウンエディターを作ってみた。 <p>はい、タイトルのまんまです。</p> <p>先ほど、オーディオプレイヤー作ってまたかと言われそうですが、とりあえず書いていきます。<br /> (開発順的にはこっちが先)</p> <h2 id="完成品"><a href="#%E5%AE%8C%E6%88%90%E5%93%81">完成品</a></h2> <p><a href="https://crieit.now.sh/upload_images/c087cafce5f65e0e671c10b2d59f02a95eb7e7c4c75ed.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/c087cafce5f65e0e671c10b2d59f02a95eb7e7c4c75ed.png?mw=700" alt="image" /></a></p> <p>フルスクリーンモード<br /> <a href="https://crieit.now.sh/upload_images/ee09f54b34ee23dcb012c900c3f3278f5eb7e7e20f94c.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/ee09f54b34ee23dcb012c900c3f3278f5eb7e7e20f94c.png?mw=700" alt="image" /></a></p> <h2 id="環境"><a href="#%E7%92%B0%E5%A2%83">環境</a></h2> <ul> <li>Mac Book Pro (Catalina)</li> <li>Node.js(v12.16.1)</li> <li>vue-cli(3系)</li> </ul> <p><a target="_blank" rel="nofollow noopener" href="https://simulatedgreg.gitbooks.io/electron-vue/ja/">ドキュメント: electron-vue 日本語</a><br /> <a target="_blank" rel="nofollow noopener" href="https://www.electronjs.org">ドキュメント: electron</a></p> <p>先ほどと変わってないですね(^^;</p> <h2 id="使用したモジュール"><a href="#%E4%BD%BF%E7%94%A8%E3%81%97%E3%81%9F%E3%83%A2%E3%82%B8%E3%83%A5%E3%83%BC%E3%83%AB">使用したモジュール</a></h2> <ul> <li>BootStrap4</li> <li>markdown-it-vue</li> <li>fs(Node.jsに最初から組み込まれている)</li> </ul> <h2 id="仕様など"><a href="#%E4%BB%95%E6%A7%98%E3%81%AA%E3%81%A9">仕様など</a></h2> <p>プレビューと入力エリアのスクロール同期、保存済みファイルの(差分検知後10~20sの)<br /> オートセーブなど、最低限マークダウンエディターに必要なものは、追加しました。</p> <p>それに加えて、Ctrl + S などデスクトップアプリならではの、ショートカットも実装し、Qiitaのスライドモード(<code>---</code>や<code>***</code>でsplitしてスライド用の配列を作る)のようなものも作成しました。</p> <p>また、デスクトップである利点を生かして、フルスクリーン表示で共有できたら良いと思いwebkitのフルスクリーンの処理について調べて、実装してみました。<br /> (electronは、Chromiumベースなので、どのOSの特殊な処理でもWebKitだけカバー、もしくは、Chromeを想定して開発すればいいのも利点?)</p> <h2 id="振り返り"><a href="#%E6%8C%AF%E3%82%8A%E8%BF%94%E3%82%8A">振り返り</a></h2> <p>今後の改良では、使用者がトークンを設定して、QiitaやmicroCMSに気軽に投稿しやすくなる機能も作成したいと思っています。</p> <p>また、fsモジュールの書き方が少し読みにくいので、もう少し、node.jsのコアな機能について学ぶ必要があると感じました。</p> <p>その他、electronに触れたことで、細かいブラウザのレンダリングエンジンや、JavaScriptのエンジンなど、単なるウェブ開発を少し超えたところの歴史や文化にも興味を持てたのは良かったと思います。</p> <p>最近だと、Chromium版のedgeなどでChromiumが使われる機会が増えているので、今後どんな発展をしていくか、またそれらに対してユーザー、エンジニアの両面の立場から関わって行けることに楽しさを感じている今日この頃です。</p> taka1156 tag:crieit.net,2005:PublicArticle/15894 2020-05-10T17:41:44+09:00 2020-05-11T05:55:49+09:00 https://crieit.net/posts/electron-vue electron-vueでオーディオプレイヤーを作ってみた <h1 id="Electron-Vueについて"><a href="#Electron-Vue%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6">Electron-Vueについて</a></h1> <h2 id="なにそれ"><a href="#%E3%81%AA%E3%81%AB%E3%81%9D%E3%82%8C">なにそれ</a></h2> <p>electronというフロントエンド技術で、デスクトップアプリを作れるフレームワークがあり<br /> それをvueで記述してビルドできるようにしてくれたもの(+ cliによる雛形作成)</p> <p>(electronが使われている例: Atom、VScode、SlackとDiscordのデスクトップ版など)</p> <h2 id="環境"><a href="#%E7%92%B0%E5%A2%83">環境</a></h2> <ul> <li>Mac Book Pro (Catalina)</li> <li>Node.js(v12.16.1)</li> <li>vue-cli(3系)</li> </ul> <p><a target="_blank" rel="nofollow noopener" href="https://simulatedgreg.gitbooks.io/electron-vue/ja/">ドキュメント: electron-vue 日本語</a><br /> <a target="_blank" rel="nofollow noopener" href="https://www.electronjs.org">ドキュメント: electron</a></p> <h2 id="始め方"><a href="#%E5%A7%8B%E3%82%81%E6%96%B9">始め方</a></h2> <p><code>vue init simulatedgreg/electron-vue プロジェクト名</code><br /> で対話形式で、勝手にプロジェクトが出来上がるので、src/renderer以下にゴリゴリとコンポーネントを書く。</p> <p><a href="https://crieit.now.sh/upload_images/5408febd77362bd4ff6c8635bfc643dc5eb7b982723f7.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/5408febd77362bd4ff6c8635bfc643dc5eb7b982723f7.png?mw=700" alt="image" /></a><br /> ※ 灰色はテストで使用した音源[<a target="_blank" rel="nofollow noopener" href="https://dova-s.jp">DOVA-SYNDROME</a>]</p> <ul> <li><p>mainは、主にウィンドウ関連の設定や開発時のプレビューの設定などが入っている。</p></li> <li><p>rendererが主にいじる部分で、vuex、vue-ruterなどは普通のvueと同じ感覚で書くことができる。</p></li> </ul> <h2 id="詰まったところ"><a href="#%E8%A9%B0%E3%81%BE%E3%81%A3%E3%81%9F%E3%81%A8%E3%81%93%E3%82%8D">詰まったところ</a></h2> <ul> <li><p>multispinnerがメンテされてない(githubのセキュリティ警告を気にする人)<br /> listrを使おう<br /> <a target="_blank" rel="nofollow noopener" href="https://github.com/SimulatedGREG/electron-vue/pull/971#issue-3581364">こちら</a>を<del>パクッ</del>参考に作ったもの<a target="_blank" rel="nofollow noopener" href="https://gist.github.com/taka1156/40236fb0afde56205b97814c2ba9ae41">electron-vue build設定</a></p> <p>(※<a target="_blank" rel="nofollow noopener" href="https://github.com/codekirei/node-multispinner">node-multispinner</a>は最後の更新が4~5年前になっている。)</p></li> <li><p>Mac系でプレビューもビルドも失敗する。<br /> index.ejsの<code>if (!process.browser) {</code>を<br /> <code>if (!htmlWebpackPlugin.options.isBrowser && !htmlWebpackPlugin.options.isDevelopment) {</code>に書き換え</p></li> </ul> <p>それ以外は、ドキュメント通りにやればプレビューが見れるはず?<br /> <a href="https://crieit.now.sh/upload_images/ce1d4f6ed1f2654825a853f3a7ec3c895eb7c09515ff4.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/ce1d4f6ed1f2654825a853f3a7ec3c895eb7c09515ff4.png?mw=700" alt="image" /></a></p> <h1 id="オーディオプレイヤーについて"><a href="#%E3%82%AA%E3%83%BC%E3%83%87%E3%82%A3%E3%82%AA%E3%83%97%E3%83%AC%E3%82%A4%E3%83%A4%E3%83%BC%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6">オーディオプレイヤーについて</a></h1> <h2 id="完成品"><a href="#%E5%AE%8C%E6%88%90%E5%93%81">完成品</a></h2> <p><a href="https://crieit.now.sh/upload_images/c5f50ef0eb56466c74f96294005543705eb7b7bfb65e7.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/c5f50ef0eb56466c74f96294005543705eb7b7bfb65e7.png?mw=700" alt="image" /></a><br /> ※<a target="_blank" rel="nofollow noopener" href="https://gist.github.com/taka1156/05d39982f98659565403b786a5fac31c">現物のサイズはこちら</a></p> <h2 id="使用したモジュール"><a href="#%E4%BD%BF%E7%94%A8%E3%81%97%E3%81%9F%E3%83%A2%E3%82%B8%E3%83%A5%E3%83%BC%E3%83%AB">使用したモジュール</a></h2> <ul> <li>electron-vue</li> <li>JsMediaTags</li> </ul> <h2 id="仕様など"><a href="#%E4%BB%95%E6%A7%98%E3%81%AA%E3%81%A9">仕様など</a></h2> <ul> <li><p>基本的には、JavaScriptのAudioオブジェクトを利用して曲の再生、停止を制御しています。</p></li> <li><p>アーティストや曲のアートワークは、<a target="_blank" rel="nofollow noopener" href="https://github.com/aadsm/jsmediatags">JsMediaTags</a>というものを利用して、トラックが遷移するたびに、 ファイルパスを渡して穿り出すという形にしています。</p></li> <li><p>設定はLocalStrageを使用して行う予定(開発中)</p></li> </ul> <h2 id="反省"><a href="#%E5%8F%8D%E7%9C%81">反省</a></h2> <p>vuexの使い方も含めて、課題が色々残っていますが、electron、vue, electron-vueそれぞれのドキュメントを行ったり来たりしながら、自分なりに動けたところは、良かったかなと思っています。</p> <h2 id="最後に"><a href="#%E6%9C%80%E5%BE%8C%E3%81%AB">最後に</a></h2> <p>今後もこまめに成果物を公開していくので、コメント、マサカリなど色々ください〜</p> taka1156 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/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など