tag:crieit.net,2005:https://crieit.net/users/kira_puka/feed
きらぷか@積読ハウマッチ/SSSAPIなどの投稿 - Crieit
Crieitでユーザーきらぷか@積読ハウマッチ/SSSAPIなどによる最近の投稿
2022-12-05T10:40:53+09:00
https://crieit.net/users/kira_puka/feed
tag:crieit.net,2005:PublicArticle/18330
2022-12-05T05:35:35+09:00
2022-12-05T10:40:53+09:00
https://crieit.net/posts/SaaS-12-1
初のSaaSが12月で1周年なので振り返ってみた
<p><a href="https://crieit.now.sh/upload_images/17513f79bbd637dff441e7cab57d2ace638d03de62032.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/17513f79bbd637dff441e7cab57d2ace638d03de62032.png?mw=700" alt="OGP_お知らせ (1).png" /></a></p>
<p>※これは<a target="_blank" rel="nofollow noopener" href="https://qiita.com/advent-calendar/2022/individual-developers">個人開発 Advent Calendar 2022</a>のカレンダー2/5日目の記事です。</p>
<p>個人開発でいろいろ作ってきたけど、<br />
去年の12月にはじめてSaaSをリリースしました。</p>
<p>ちょうど1年経ったので、少し振り返ってみる。<br />
12月はいろんな記事が出てくるので、少し触発されました(<em>´ω`</em>)</p>
<p>GoogleスプレッドシートのJSON API化サービス</p>
<p><a target="_blank" rel="nofollow noopener" href="https://sssapi.app/">https://sssapi.app/</a></p>
<h2 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></h2>
<p>スプレッドシートのJSON APIをつくれるごくごくシンプルなサービス。</p>
<p>スプレッドシートのURLを登録すると、<br />
中身をJSONで受け取れるAPIを作れます。</p>
<p>検索・絞り込み・ページングなどAPIでよくつかう機能や、<br />
ネストオブジェクトへの変換などいくつか変換機能、<br />
外部API呼び出しやGitHub Actionsとの連携機能などなども用意。</p>
<h2 id="どうして作ったの?"><a href="#%E3%81%A9%E3%81%86%E3%81%97%E3%81%A6%E4%BD%9C%E3%81%A3%E3%81%9F%E3%81%AE%EF%BC%9F">どうして作ったの?</a></h2>
<p><a target="_blank" rel="nofollow noopener" href="https://zenn.dev/kira_puka/articles/f9496a6a847799">β版リリース時の記事</a>でもいろいろ書いたけど、<br />
スプレッドシートをJSON化する方法はいくつか既存のサービスでは少し使いにくいな。。と思うことがあった。</p>
<h3 id="既存のサービスでの課題"><a href="#%E6%97%A2%E5%AD%98%E3%81%AE%E3%82%B5%E3%83%BC%E3%83%93%E3%82%B9%E3%81%A7%E3%81%AE%E8%AA%B2%E9%A1%8C">既存のサービスでの課題</a></h3>
<p>ちょっとしたJSON APIを作る方法はいくつかあるけど、<br />
実際のサービスで利用しようとした場合、すこし気になることが。。</p>
<ul>
<li>GAS
<ul>
<li>レスポンスが遅い/同時実行数などの制限がある</li>
<li>シートごとに準備が必要/どこで使ってるかわからなくなる</li>
<li>リッチな検索の実装がめんどくさい</li>
</ul></li>
<li>ヘッドレスCMS
<ul>
<li>個人・小規模・開発初期では手を出しにくい価格</li>
<li>機能がリッチだけど、初期ではなくていいものも多い</li>
</ul></li>
<li>スプレッドシートDB関連サービス
<ul>
<li>必要な権限が多い</li>
<li>プライベートなAPIは作れない</li>
</ul></li>
</ul>
<p>資金が潤沢であればヘッドレスCMSがよい選択だけど、<br />
試作段階で、いきなり手を出すのはなかなか厳しいお値段。。</p>
<p>GASやスプレッドシートDB関連サービスは、<br />
個人利用なサービスや学習用ではよいけど、<br />
実サービスではなかなか厳しい制約。。</p>
<p>もう少しお手軽で実サービスでも利用できるものがほしいと思い、<br />
このサービスを作りはじめることに。</p>
<h3 id="スプレッドシートをDBにするときの懸念"><a href="#%E3%82%B9%E3%83%97%E3%83%AC%E3%83%83%E3%83%89%E3%82%B7%E3%83%BC%E3%83%88%E3%82%92DB%E3%81%AB%E3%81%99%E3%82%8B%E3%81%A8%E3%81%8D%E3%81%AE%E6%87%B8%E5%BF%B5">スプレッドシートをDBにするときの懸念</a></h3>
<p>多くの競合サービスは直接スプレッドシートに接続しているけど、<br />
実際にスプレッドシートをDBにすることを考えたときに、<br />
いくつか気になる点が。。</p>
<ul>
<li>トランザクションがない -> データ壊れる可能性</li>
<li>編集中のリクエストも受け付ける -> 不正なJSONを返すの可能性</li>
<li>Sheets APIが必要 -> 実サービスだとつらい遅さ</li>
</ul>
<p>作ったサービスが(万が一でも)大ヒットしたときに、<br />
データが壊れたり、意図しないレスポンスになるのは、<br />
かなりつらい。。</p>
<p>そのため、任意のタイミングのスプレッドシートでAPIを更新でき、<br />
読み込み専用のAPIのみとする、いまの方針になりました。</p>
<h2 id="1年やってみてどうだった?"><a href="#1%E5%B9%B4%E3%82%84%E3%81%A3%E3%81%A6%E3%81%BF%E3%81%A6%E3%81%A9%E3%81%86%E3%81%A0%E3%81%A3%E3%81%9F%EF%BC%9F">1年やってみてどうだった?</a></h2>
<ul>
<li>予想以上に利用されてた!!</li>
<li>エンジニア以外の人の利用が多い!!</li>
<li>企業での利用が多い!!</li>
</ul>
<p>自分と同じような個人開発者をイメージしていたので、<br />
かなりびっくり。。うれしい悲鳴。。(*´ω`*)<br />
年プランの購入もあり、黒字で運営できてる。ありがたい。。(*´ω`*)</p>
<p>CloudFlareの障害のときは気が気じゃなかった。。</p>
<h3 id="マッチするユーザ"><a href="#%E3%83%9E%E3%83%83%E3%83%81%E3%81%99%E3%82%8B%E3%83%A6%E3%83%BC%E3%82%B6">マッチするユーザ</a></h3>
<p>アンケートやインタビュー、利用データを見ていると、こんな感じに(*´ω`*)</p>
<ul>
<li>スプレッドシートを介したコラボレーション
<ul>
<li>エンジニア以外がデータを操作しやすい</li>
</ul></li>
<li>お知らせなどを極小のコンテンツをあとづけしたい
<ul>
<li>単純なものを素早く作れる</li>
</ul></li>
<li>スキーマが決まってない開発初期
<ul>
<li>データからスキーマが作成できる</li>
</ul></li>
</ul>
<p>特に、スプレッドシートは多くの人が見慣れているので、<br />
データを作る人に管理画面の操作方法を説明しなくてもよく、<br />
共有も共同編集も簡単にできるというのが強みのよう。</p>
<p>また、スキーマをあらかじめ用意しなくてよく、<br />
列を追加すると自由に変更できるのでプロトも素早くできる。</p>
<h3 id="マッチしないユーザ"><a href="#%E3%83%9E%E3%83%83%E3%83%81%E3%81%97%E3%81%AA%E3%81%84%E3%83%A6%E3%83%BC%E3%82%B6">マッチしないユーザ</a></h3>
<p>逆にマッチしないユーザとしては、<br />
スプレッドシートをDBとして使いたいエンジニア。</p>
<p>やはり、書き込みができない、すぐに反映されないのは、<br />
かなりネックなため、刺さらない感じだった。。</p>
<p>とはいえ、DBとして使う懸念があるので、<br />
妙案が出るまで少し難しいことを思っている。。</p>
<h2 id="振り返り(というなの後悔&反省)"><a href="#%E6%8C%AF%E3%82%8A%E8%BF%94%E3%82%8A%28%E3%81%A8%E3%81%84%E3%81%86%E3%81%AA%E3%81%AE%E5%BE%8C%E6%82%94%EF%BC%86%E5%8F%8D%E7%9C%81%29">振り返り(というなの後悔&反省)</a></h2>
<p>うれしかったことも多かったけど、<br />
「こうしとけばよかった。。」の日々(´・ω・`)</p>
<h3 id="ユーザとのコミュニケーション"><a href="#%E3%83%A6%E3%83%BC%E3%82%B6%E3%81%A8%E3%81%AE%E3%82%B3%E3%83%9F%E3%83%A5%E3%83%8B%E3%82%B1%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3">ユーザとのコミュニケーション</a></h3>
<p>機能の開発はずっとやってきたけど、<br />
マーケティングは考えていなかった。。</p>
<p>リリースしたはいいものの、フィードバックを受けたり、<br />
何かを個別に伝えたりというのが難しい状態に。。</p>
<p>以下のようなものが必要だと感じた。<br />
さらにサイト上とメールなどのサイト外でやり取りできるとよい。</p>
<ul>
<li>運営 -> 全体
<ul>
<li>サイト内: お知らせ・更新履歴のページ</li>
<li>サイト外: メーリングリスト</li>
</ul></li>
<li>運営
<ul>
<li>サイド内: フィードバックフォーム</li>
<li>サイト外: お問い合わせフォーム/メール</li>
</ul></li>
<li>運営 -> 個別
<ul>
<li>サイト内: (現状なし。例:個別のお知らせ)</li>
<li>サイト外: メール</li>
</ul></li>
<li>運営 個別
<ul>
<li>サイト内: (現状なし。例:チャット)</li>
<li>サイト外: メール</li>
</ul></li>
</ul>
<p>当たり前のことだけど、機能の開発が優先になってしまっていた。。<br />
機能は最低限でもいいけど、<br />
ユーザとやり取りできる部分はあらかじめ用意できていると良かった。。</p>
<h3 id="統計データの収集"><a href="#%E7%B5%B1%E8%A8%88%E3%83%87%E3%83%BC%E3%82%BF%E3%81%AE%E5%8F%8E%E9%9B%86">統計データの収集</a></h3>
<p>これもマーケティング関連。</p>
<p>リリースして終わりではなく、なにを改善すべきかを把握する必要があるが、<br />
こんなに利用される想定ではなかったので、かなり後回しに。。</p>
<p>最低限でも仕込んでおけば、かなり改善を早く回せた気がする。</p>
<p>特に、Twitterでのコメントと、実際の利用データとはかなりの乖離があるので、<br />
もっとはやく実データを見て判断できる基盤を用意しておけばよかった。。</p>
<h3 id="個人利用の想定"><a href="#%E5%80%8B%E4%BA%BA%E5%88%A9%E7%94%A8%E3%81%AE%E6%83%B3%E5%AE%9A">個人利用の想定</a></h3>
<p>想定ユーザが自分自身であったので、<br />
複数人でがっつり利用されるのを想定しなかった。。</p>
<p>フタを開けてみると、企業での利用や、<br />
エンジニア/非エンジニアとのコラボレーションが多い。</p>
<p>そういった場合、プロジェクトやワークスペースみたいな、<br />
複数人で共同編集できる構成が必要。</p>
<p>そうしておくことで、メンバーを招待したりもできるので、<br />
バイラル効果も期待できる。</p>
<p>特にSaaSの場合はマルチテナントな構成で組んでおけばよかった。。<br />
(新機能として実装中...)</p>
<h3 id="料金プラン設計"><a href="#%E6%96%99%E9%87%91%E3%83%97%E3%83%A9%E3%83%B3%E8%A8%AD%E8%A8%88">料金プラン設計</a></h3>
<p>個人利用の想定も絡んでいるけど、<br />
アカウント単位のシンプルな3プランだけにしていた。<br />
(しかもかなり手を出しやすい価格...)</p>
<p>この設計の場合、なかなか収益を増やすのが難しい。。<br />
ランニングコストがかかるリッチな機能も開発しにくい。</p>
<p>また、APIがたくさん作りたい人と行数をたくさん増やしたい人がいて、<br />
ニーズにあうプランがないことも多い。</p>
<p>今回のサービスの場合は、ある程度、好きなプランにできるよう、<br />
ユーザがカスタマイズできる構成で組んでおけばよかったと思う。</p>
<p>とはいえ、ユーザが無駄に費用をかけなくていいように、<br />
無料で試せて、小さくはじめて、大きく伸ばせる、<br />
みたいな仕組みにしておくと個人的にはとっつきやすい。</p>
<h3 id="その他もろもろ"><a href="#%E3%81%9D%E3%81%AE%E4%BB%96%E3%82%82%E3%82%8D%E3%82%82%E3%82%8D">その他もろもろ</a></h3>
<p>書きはじめるときりがないけど、ほかにもいろいろと。。</p>
<ul>
<li>より積極的なインタビュー/事例収集</li>
<li>大きな更新よりも小さくても定期的な更新&お知らせ</li>
<li>UIフレームワークは利用を控える/移行つらい</li>
<li>広告は少額だと微妙かも/ある程度の金額なら効果的</li>
<li>記事募集やクーポン配布は効果薄いかも</li>
</ul>
<p>はやく小さくリリースするのも大事だけど、<br />
こういったサービスの場合には、<br />
マーケティング関連や共同利用の想定ももっと想定しておけばよかった。。</p>
<h2 id="今後は?"><a href="#%E4%BB%8A%E5%BE%8C%E3%81%AF%EF%BC%9F">今後は?</a></h2>
<p>まずは後悔している部分の対応と使い勝手を向上していく。</p>
<p>あとはNuxt3がリリースされたので、Buetfyを剥がしつつ、<br />
すこしずつ移行している感じ。</p>
<p>シンプルさを保ちつつ、よりサクッと便利なサービスになるよう、<br />
少しずつでも長く運営できるようがんばります(*´ω`*)!!</p>
<p>ご興味がある方も無い方も一度はぜひぜひ〜(*´ω`*)</p>
<p><a href="https://crieit.now.sh/upload_images/f4d68ff5b3ff8494cd994b7c45ad4257638d046a36eb3.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/f4d68ff5b3ff8494cd994b7c45ad4257638d046a36eb3.png?mw=700" alt="ogp_1syuunen_2.png" /></a></p>
<p>GoogleスプレッドシートのJSON API化サービス<br />
<a target="_blank" rel="nofollow noopener" href="https://sssapi.app/">https://sssapi.app/</a></p>
<h2 id="参考になった本"><a href="#%E5%8F%82%E8%80%83%E3%81%AB%E3%81%AA%E3%81%A3%E3%81%9F%E6%9C%AC">参考になった本</a></h2>
<p>マーケティングの本が多め。どう理解してもらい、どう広めていくかの学びが多い。。</p>
<ul>
<li><a target="_blank" rel="nofollow noopener" href="https://www.amazon.co.jp/exec/obidos/ASIN/4799327844/tsundoku-hm-22">PLG プロダクト・レッド・グロース「セールスがプロダクトを売る時代」から「プロダクトでプロダクトを売る時代」へ</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://www.amazon.co.jp/exec/obidos/ASIN/479815816X/tsundoku-hm-22">THE MODEL(MarkeZine BOOKS) マーケティング・インサイドセールス・営業・カスタマーサクセスの共業プロセス</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://www.amazon.co.jp/exec/obidos/ASIN/462108786X/tsundoku-hm-22">Fearless Change アジャイルに効く アイデアを組織に広めるための48のパターン</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://www.amazon.co.jp/exec/obidos/ASIN/4408650056/tsundoku-hm-22">スモールビジネスの教科書</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://www.amazon.co.jp/exec/obidos/ASIN/4320023684/tsundoku-hm-22">ライト,ついてますか: 問題発見の人間学</a></li>
</ul>
きらぷか@積読ハウマッチ/SSSAPIなど
tag:crieit.net,2005:PublicArticle/17821
2021-12-05T00:06:11+09:00
2021-12-05T00:06:11+09:00
https://crieit.net/posts/ver-2021
個人開発ふりかえり ver.2021年
<p>この記事は<a target="_blank" rel="nofollow noopener" href="https://qiita.com/advent-calendar/2021/personal-project">個人開発 Advent Calendar 2021</a>の5日目の記事です。</p>
<p>今年ずっと<a target="_blank" rel="nofollow noopener" href="https://sssapi.app">つくってたサービス</a>も無事正式リリースできたので、<br />
個人開発について振り返ってみようかなと。</p>
<p>はじめたころからすると目的もだいぶ変わってきてるけど、<br />
こんなに長く続けられる個人開発ってたのしい(*´ω`*)</p>
<h2 id="2021年につくったもの"><a href="#2021%E5%B9%B4%E3%81%AB%E3%81%A4%E3%81%8F%E3%81%A3%E3%81%9F%E3%82%82%E3%81%AE">2021年につくったもの</a></h2>
<p>振り返ってみると4つも作ってた(*´ω`*)</p>
<ul>
<li><strong>4月: <a target="_blank" rel="nofollow noopener" href="https://umaiku.app/">ウマ育ノート</a>をリリース</strong><br />
ゲーム・ウマ娘のお助けアプリ</li>
<li><strong>7月: <a target="_blank" rel="nofollow noopener" href="https://coco-gacha.memory-lovers.com/">CoCoガチャ</a>をリリース</strong><br />
カレーのトッピングをガチャで決めるネタアプリ<br />
Crieitの<a href="https://crieit.net/boards/web1week-202107">web1week</a>への投稿作</li>
<li><strong>10月: <a target="_blank" rel="nofollow noopener" href="https://press-done.memory-lovers.com/">PRESS-DONE</a>をリリース</strong><br />
プレスリリースの送付記録サービス</li>
<li><strong>12月: <a target="_blank" rel="nofollow noopener" href="https://sssapi.app">SSSAPI</a>を正式リリース</strong><br />
GoogleスプレッドシートのAPI化サービス</li>
</ul>
<p>あと、<a target="_blank" rel="nofollow noopener" href="https://tsundoku.site/">積読ハウマッチ</a>もちょこちょこアップデート</p>
<p>ほぼ、<a target="_blank" rel="nofollow noopener" href="https://sssapi.app">SSSAPI</a>をがんばっていた年。<br />
ほかの3つのアプリもSSSAPIを使ってるので、ほぼ一色に。</p>
<p>SSSAPIについてはこちらの記事にまとめたので、もしよければ!<br />
・<a target="_blank" rel="nofollow noopener" href="https://qiita.com/kira_puka/items/42fa53dd2fa381a742d2">スプレッドシートをWeb APIにできるサービスを作ってみた - Qiita</a></p>
<h2 id="目的の変化"><a href="#%E7%9B%AE%E7%9A%84%E3%81%AE%E5%A4%89%E5%8C%96">目的の変化</a></h2>
<p>かれこれ8年くらい個人開発をしてるけど、<br />
目的がだいぶ変わってきた気がする。</p>
<h3 id="はじめはストレス発散"><a href="#%E3%81%AF%E3%81%98%E3%82%81%E3%81%AF%E3%82%B9%E3%83%88%E3%83%AC%E3%82%B9%E7%99%BA%E6%95%A3">はじめはストレス発散</a></h3>
<p>いま思うと、ストレス発散だったとかも。</p>
<p>だんだんコードを書く機会がなくなって、<br />
コードを書けないストレスの発散する場所だったかもしれない。</p>
<p>なので、「コード書ける〜たのし〜」という感じで、<br />
コードを書くこと自体が目的だった。</p>
<h3 id="新しい技術を試す場所"><a href="#%E6%96%B0%E3%81%97%E3%81%84%E6%8A%80%E8%A1%93%E3%82%92%E8%A9%A6%E3%81%99%E5%A0%B4%E6%89%80">新しい技術を試す場所</a></h3>
<p>新しい技術とかはフィードを見ているくらいだったけど、<br />
いろんなライブラリや言語などを個人開発で試すように。</p>
<p>記事を読んだり、サンプル動かすだけだと身につかないけど、<br />
実プロジェクトで試せるので、理解も深くなる。</p>
<p>「個人開発で試す」→「本業で導入する」<br />
という、このサイクルがとてもよくて、<br />
新しめ技術でも自信を持って提案できる気がする。</p>
<p>「個人開発=新しいものを試したり、訓練する場所」<br />
という感じだったかも。</p>
<h3 id="新しい領域へのチャレンジ"><a href="#%E6%96%B0%E3%81%97%E3%81%84%E9%A0%98%E5%9F%9F%E3%81%B8%E3%81%AE%E3%83%81%E3%83%A3%E3%83%AC%E3%83%B3%E3%82%B8">新しい領域へのチャレンジ</a></h3>
<p>このサイクルにも慣れてきて、自由に作れるようになると<br />
「あわよくば、これで暮らしていきたい。。」<br />
という気持ちが湧くように。。ほんと、人間は欲深い。。</p>
<p>そうなると、広報とか営業とかQAとか<br />
いままでの開発以外にも力を入れていくことが大事。</p>
<p>なので、今は開発以外も含めて、<br />
どう運営していくのがよいかを考えることが多くなってきた感じ。</p>
<p>本業だと別の領域にチャレンジできる機会は、<br />
なかなかないので、これも個人開発のいいところ。</p>
<h2 id="個人開発はたのしい"><a href="#%E5%80%8B%E4%BA%BA%E9%96%8B%E7%99%BA%E3%81%AF%E3%81%9F%E3%81%AE%E3%81%97%E3%81%84">個人開発はたのしい</a></h2>
<p>なんでこんなに長く続けてるんだろう?<br />
と考えてみると、この3つなんじゃないだろうか。</p>
<ul>
<li>なにもかも自由</li>
<li>フィードバックがダイレクト</li>
<li>個人でもひとりじゃない</li>
</ul>
<p>(もちろん、個人開発ドリームもあるけど(*´ω`*))</p>
<h3 id="なにもかも自由"><a href="#%E3%81%AA%E3%81%AB%E3%82%82%E3%81%8B%E3%82%82%E8%87%AA%E7%94%B1">なにもかも自由</a></h3>
<p>独立してフリーランスになったときも、<br />
自由だと感じたけれど、それ以上に自由。</p>
<p>作りたいものも、使いたい技術も自由に選べるのがいい。</p>
<p>仕事となると、作るもののテーマは大体決まっているし、<br />
安定している枯れた技術やチームにあう言語など、<br />
いろんな制約を考えて決めたりする。</p>
<p>個人開発なら、そんな制約も無いので、<br />
「ぼくがかんがえたさいきょうの構成」<br />
なんてのもOK。</p>
<p>上で書いたとおり、目的すらも自由なので、<br />
心のままに楽しめるのがいい(*´ω`*)</p>
<h3 id="フィードバックがダイレクト"><a href="#%E3%83%95%E3%82%A3%E3%83%BC%E3%83%89%E3%83%90%E3%83%83%E3%82%AF%E3%81%8C%E3%83%80%E3%82%A4%E3%83%AC%E3%82%AF%E3%83%88">フィードバックがダイレクト</a></h3>
<p>なにもかも自由である反面、<br />
すべての責任があるのも醍醐味の一つ。</p>
<p>いいこともわるいこともあるけど、<br />
ストレートにフィードバックが来るのは刺激的。</p>
<p>本業だとほめられることが少ないけど、<br />
9対1くらいでいいフィードバックなのでたのしい。</p>
<p>わるいフィードバックも改善点の指摘なので、<br />
どちらのフィードバックもとってもありがたい(*´ω`*)</p>
<h3 id="個人でもひとりじゃない"><a href="#%E5%80%8B%E4%BA%BA%E3%81%A7%E3%82%82%E3%81%B2%E3%81%A8%E3%82%8A%E3%81%98%E3%82%83%E3%81%AA%E3%81%84">個人でもひとりじゃない</a></h3>
<p>自分でそういう情報を追っていっているというのもあるけど、<br />
意外と個人開発者はたくさんいて、コミュニティや勉強会/もくもく会も多くある。</p>
<p>ぼくも<a target="_blank" rel="nofollow noopener" href="https://qiita.com/organizations/admin-guild">運営者ギルド</a>というコミュニティに参加させてもらってるけど、<br />
・ リリース前のサービスにバグ出し/アイデア出しする<strong>バグバッシュ</strong>や<br />
・ 1ヶ月の活動内容を報告する<strong>月報</strong><br />
なんてのも開催されていて、それをみると「がんばるぞ!」って気持ちになる。</p>
<p>コードを書くのは一人だけど、コミュニティやTwitterを見てても、<br />
がんばっている同志がたくさんいる感じなのが、続けられるのかも。</p>
<p>それと、フィードバックがダイレクトな効果なのか、<br />
<strong>ユーザさんと一緒に作ってる感じがすごく強い</strong>。</p>
<p>最初のプロトくらいまでは自分だけだけど、<br />
いろんなフィードバックを取り込んでいくうちに、<br />
「自分だけで作ったサービスじゃない感」が出てくる。</p>
<p>ユーザさんが増えていくたびに、<br />
気づかなかったことや自分にない視点が増えていくのがたのしいし、<br />
より喜んでもらえるようにとやる気が出るからかもしれない(*´ω`*)</p>
<h2 id="おわりに"><a href="#%E3%81%8A%E3%82%8F%E3%82%8A%E3%81%AB">おわりに</a></h2>
<p>振り返ってみて思ったけど、目的すら自由な個人開発ってすごい(*´ω`*)</p>
<h3 id="【PR】SSSAPIをリリースしました!!"><a href="#%E3%80%90PR%E3%80%91SSSAPI%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%21">【PR】SSSAPIをリリースしました!!</a></h3>
<p>スプレッドシートをWeb APIにできるサービスをリリースしました🎉<br />
・ 1件取得/フィルタリング/ページング/ソートもできるAPIを簡単作成<br />
・ ドメインやアクセストークンによるアクセス制限もOK</p>
<p>「お知らせとかリリースノートを作りたいけど、<br />
毎回ビルドするのも、管理画面をつくるのもめんどくさい。。」</p>
<p>ってときに便利です!! スプシが管理画面になっちゃいます!!<br />
無料枠もあるので、ぜひぜひ遊んでみてください(<em>´ω`</em>)</p>
<p>GoogleスプレッドシートのAPI化サービス<br />
「<a target="_blank" rel="nofollow noopener" href="https://sssapi.app/">SSSAPI</a>」</p>
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">\㊗️正式リリース㊗️/<a target="_blank" rel="nofollow noopener" href="https://twitter.com/hashtag/SSSAPI?src=hash&ref_src=twsrc%5Etfw">#SSSAPI</a> の正式版を公開しました🎉開発開始から9ヶ月...無事リリースを迎えられました😭β版でのご協力、ありがとうございます🙇♂️まだまだ不便なところもございますが、より一層便利になるよう改善がんばります💪💪💪<a target="_blank" rel="nofollow noopener" href="https://twitter.com/hashtag/%E6%8B%A1%E6%95%A3%E5%B8%8C%E6%9C%9BRT%E3%81%8A%E9%A1%98%E3%81%84%E3%81%97%E3%81%BE%E3%81%99?src=hash&ref_src=twsrc%5Etfw">#拡散希望RTお願いします</a> <a target="_blank" rel="nofollow noopener" href="https://t.co/znp6CJGZEs">https://t.co/znp6CJGZEs</a></p>— 【公式】SSSAPI / GoogleスプレッドシートのAPI化サービス (@sssapi_app) <a target="_blank" rel="nofollow noopener" href="https://twitter.com/sssapi_app/status/1465802829906558984?ref_src=twsrc%5Etfw">November 30, 2021</a></blockquote>
きらぷか@積読ハウマッチ/SSSAPIなど
tag:crieit.net,2005:PublicArticle/17698
2021-10-05T17:05:54+09:00
2021-10-05T17:05:54+09:00
https://crieit.net/posts/Firebase-SSSAPI-Web
FirebaseとSSSAPIでプレスリリースを送ったサイトを記録できるWebサービスを作ってみた
<p>開発しているGoogleスプレッドシートのAPI化サービス「<a target="_blank" rel="nofollow noopener" href="https://sssapi.app">SSSAPI</a>」をβ版リリースしたときに、<br />
プレスリリースをいろんなメディアに送ってみてた。</p>
<p>せっかく調べてまとめたし、次も使う機会があるだろうと思ったので、<br />
Nuxt/Firebase/SSSAPIの構成でWebサービス化してみた。</p>
<p>プレスリリースを受け付けてるサイトを調べるのは大変なので、<br />
どこに送れるのかな〜と思った方は使ってみてもらえるとうれしいです(<em>´ω`</em>)</p>
<h3 id="作ったサービス"><a href="#%E4%BD%9C%E3%81%A3%E3%81%9F%E3%82%B5%E3%83%BC%E3%83%93%E3%82%B9">作ったサービス</a></h3>
<p><a target="_blank" rel="nofollow noopener" href="https://press-done.memory-lovers.com">https://press-done.memory-lovers.com</a></p>
<p>送付するプレスリリースごとに、どのサイトへ送ったかの状態管理ができます。<br />
それだけのシンプルなWebサービス。</p>
<p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/b52d95a8-788f-5625-c536-4ecd6cc147b5.png" alt="スクリーンショット 2021-10-04 23.35.26.png" /></p>
<h3 id="システム構成"><a href="#%E3%82%B7%E3%82%B9%E3%83%86%E3%83%A0%E6%A7%8B%E6%88%90">システム構成</a></h3>
<p>基本構成はこんな感じ。</p>
<ul>
<li>フロントエンド: Nuxt+Buefy</li>
<li>認証: Firebase Authentication</li>
<li>ユーザデータ: Firestore</li>
<li>マスターデータ: <a target="_blank" rel="nofollow noopener" href="https://sssapi.app">SSSAPI</a></li>
</ul>
<p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/d9f069c4-7d32-5ea8-aea5-91300fea7030.png" alt="スクリーンショット 2021-10-04 23.40.41.png" /></p>
<p>ユーザが更新しないマスターデータはSSSAPIを利用している感じ。</p>
<p>メディアサイトのデータをFirestoreに入れておく形もあるけど、<br />
Firebaseコンソールで追加や編集するより、<br />
スプレッドシートを更新するほうが楽なので、この形にしている。</p>
<p>スプレッドシートの中身はこんな感じ。</p>
<p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/e130e233-915d-e105-0e75-48249f002a13.png" alt="スクリーンショット 2021-10-04 23.43.50.png" /></p>
<p>SSSAPIはこんなJSONを返してくれる。</p>
<pre><code class="json">[
{
"id": 1,
"サイト名": "TechCrunch Japan",
"サイトURL": "https://jp.techcrunch.com",
"送付方法": "メール",
"isActive": true
},
{
"id": 2,
"サイト名": "日経クロステック",
"サイトURL": "https://xtech.nikkei.com",
"送付方法": "メール",
"isActive": true
},
{
"id": 3,
"サイト名": "ITmedia",
"サイトURL": "https://www.itmedia.co.jp/",
"送付方法": "メール",
"isActive": true
},
]
</code></pre>
<p>※「行数をIDとして追加」オプションを使ってるので、idが自動で付与されています。</p>
<p>このスプレッドシートは登録依頼のGoogleフォームと連動しているので、<br />
追加依頼があったら内容を確認して<code>isActive</code>を<code>true</code>にすれば反映されます。</p>
<p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/39ce3a05-cd7b-2ed8-81e1-af76025e6175.png" alt="スクリーンショット 2021-10-04 23.56.34.png" /></p>
<p>こういった構成は割と多く、本の貸し出し管理と備品管理とかでも使える。<br />
Webサービスからスプレッドシートを編集できないので、<br />
管理者権限とか不要なので、その分フロント側も楽に作れた(<em>´ω`</em>)</p>
<p>プレスリリースってどこに送れるのかな〜と思った方は、<br />
ぜひ使ってみてもらえるとうれしいです(<em>´ω`</em>)</p>
<p><a target="_blank" rel="nofollow noopener" href="https://press-done.memory-lovers.com">https://press-done.memory-lovers.com</a></p>
<h3 id="SSSAPIはβ版ユーザを募集しています!!"><a href="#SSSAPI%E3%81%AF%CE%B2%E7%89%88%E3%83%A6%E3%83%BC%E3%82%B6%E3%82%92%E5%8B%9F%E9%9B%86%E3%81%97%E3%81%A6%E3%81%84%E3%81%BE%E3%81%99%21%21">SSSAPIはβ版ユーザを募集しています!!</a></h3>
<p>β期間中はプレミアムプランの内容を無料で使えるのでお得!<br />
ぜひぜひ、この機会にお試しいただければ!</p>
<p>わからないところやこんなのあったらいいなぁ〜などあれば、<br />
Twitterやお問い合わせなどからお気軽にご連絡ください〜</p>
<p>GoogleスプレッドシートのAPI化サービス<br />
『<a target="_blank" rel="nofollow noopener" href="https://sssapi.app">SSSAPI</a>』<br />
Twitter: <a target="_blank" rel="nofollow noopener" href="https://twitter.com/sssapi_app">@sssapi_app</a></p>
<p><a target="_blank" rel="nofollow noopener" href="https://sssapi.app">https://sssapi.app</a></p>
<p>開発の励みや記事を書くモチベにもなるので、<br />
いいねやシェア、はてブなどしてもらえるとかなりよろこびます!!</p>
きらぷか@積読ハウマッチ/SSSAPIなど
tag:crieit.net,2005:PublicArticle/17624
2021-08-29T17:21:16+09:00
2021-08-29T17:26:06+09:00
https://crieit.net/posts/Web-15-5
つくってるWebサービスのユーザ数が15倍になったときにやった5つのこと。
<p><a href="https://crieit.now.sh/upload_images/e1987b7820c75d7857fbfe4ca21013da612b3b60d79bd.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/e1987b7820c75d7857fbfe4ca21013da612b3b60d79bd.png?mw=700" alt="OGP_お知らせ (2).png" /></a></p>
<p>先日、<a target="_blank" rel="nofollow noopener" href="https://sssapi.app">開発中のサービス</a>のオープンβ版を公開したら、<strong>4日でユーザ数が15倍に</strong>。。<br />
想像以上の反響だったので、どんなことをやってたかをまとめてみました。</p>
<p>リリース後のプロモーションはいつも困っているので、、<br />
これから何かを作ろう、リリースしようとしている人の一助になれば。</p>
<p>※注意※<br />
・元のユーザ数が少ないのですごく増えた感あり。運要素が強めです。<br />
・実際のユーザ数やデータはちょっとはずかしいので有料にしてます<br />
(Crieitの有料記事機能を使ってみたかったって理由も)</p>
<h2 id="実際にやったこと"><a href="#%E5%AE%9F%E9%9A%9B%E3%81%AB%E3%82%84%E3%81%A3%E3%81%9F%E3%81%93%E3%81%A8">実際にやったこと</a></h2>
<ol>
<li>LPや事前登録サイトを公開</li>
<li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/organizations/admin-guild">運営者ギルド</a>でバグバッシュしてもらう</li>
<li>PR TIMES等でプレスリリースを公開&ツイートする</li>
<li>Zenn/Crieit/Qiita等で記事を公開&ツイートする</li>
<li>RTされやすそうな時間帯にセルフRTなどする</li>
</ol>
<h5 id="実際のやったときの流れ"><a href="#%E5%AE%9F%E9%9A%9B%E3%81%AE%E3%82%84%E3%81%A3%E3%81%9F%E3%81%A8%E3%81%8D%E3%81%AE%E6%B5%81%E3%82%8C">実際のやったときの流れ</a></h5>
<ul>
<li>7/6(火): 事前登録サイトの公開&PR TIMESで<a target="_blank" rel="nofollow noopener" href="https://prtimes.jp/main/html/rd/p/000000006.000056359.html">プレスリリース</a>を公開</li>
<li>7/10(土)-7/12(月): <a target="_blank" rel="nofollow noopener" href="https://qiita.com/organizations/admin-guild">運営者ギルド</a>でバグバッシュ</li>
<li>8/24(火): PR TIMESで<a target="_blank" rel="nofollow noopener" href="https://prtimes.jp/main/html/rd/p/000000007.000056359.html">プレスリリース</a>を公開
<ul>
<li>→ <a target="_blank" rel="nofollow noopener" href="https://digital-shift.jp/flash_news/FN210824_2">プレスを元にした記事</a>をデジタル シフト タイムズさまから公開</li>
<li>→ この記事がはてなブックマークのテクノロジーでホッテントリに!!</li>
<li>→ ホッテントリに入ったので、Discoverで表示(Google砲)!!</li>
<li>→ つられて、SSSAPI自体もホッテントリに!!</li>
</ul></li>
<li>8/25(水): Zennで記事を公開
<ul>
<li><a target="_blank" rel="nofollow noopener" href="https://zenn.dev/kira_puka/articles/f9496a6a847799">Firestore最高だけど、初期データとかマスタデータ管理がめんどくさいので、スプレッドシートをAPI化サービスを作ってみた</a></li>
<li>→ トレンド入りし、トップページに表示される!!</li>
</ul></li>
<li>8/26(木): Qiitaに記事を投稿
<ul>
<li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/kira_puka/items/b61f8cd682161afb55a9">GASでもできるけど、スプシをAPI化するサービスを作ってみた。そして、そのときハマったこと。 - Qiita</a></li>
<li>→ トレンド入りし、トップページに表示される!!</li>
</ul></li>
</ul>
<p>それぞれ記事を投稿したら、朝/昼/夜くらいでセルフRTなどしてました。</p>
<p>(フィードバックやいいね/RTで拡散で協力してくれた方、ほんとありがとうございます!!)</p>
<h3 id="1. LPや事前登録サイトを公開"><a href="#1.+LP%E3%82%84%E4%BA%8B%E5%89%8D%E7%99%BB%E9%8C%B2%E3%82%B5%E3%82%A4%E3%83%88%E3%82%92%E5%85%AC%E9%96%8B">1. LPや事前登録サイトを公開</a></h3>
<p>まずは、トップページのみの事前登録サイトを作って公開してました。<br />
事前登録の受付は、Googleフォームの簡易的なサイト。</p>
<p>事前登録のプレスリリースを打つためもありますが、<br />
<strong>サービス名を検索するとヒットするように、あらかじめ用意していました</strong>。</p>
<p>ツイートへの反応や登録者数なども、ウケそうかのどうかの判断材料に。</p>
<h3 id="2. 運営者ギルドでバグバッシュ"><a href="#2.+%E9%81%8B%E5%96%B6%E8%80%85%E3%82%AE%E3%83%AB%E3%83%89%E3%81%A7%E3%83%90%E3%82%B0%E3%83%90%E3%83%83%E3%82%B7%E3%83%A5">2. 運営者ギルドでバグバッシュ</a></h3>
<p><a target="_blank" rel="nofollow noopener" href="https://qiita.com/organizations/admin-guild">運営者ギルド</a>は、Webサービス運営者が集まっているコミュニティ。<br />
すごい人ばかりで、Slack内も知見がすごいですが、<br />
その中のイベントの一つ、バグバッシュをしてもらいました。</p>
<p>バグバッシュは、「リリース前にみんなにさわってもらってバグを見つけよう!」<br />
という活動なのですが、バグだけでなく、どこがわかりにくい?なども聞けることも。</p>
<p>みんなそれぞれ運営しているので、いろんな視点のフィードバックが集まり、<br />
自分では気づけないポイントのコメントを多くもらえました。。!</p>
<p>そのフィードバックを踏まえ、改善&ブラッシュアップ。</p>
<h3 id="3. PR TIMES等でプレスリリースを投稿"><a href="#3.+PR+TIMES%E7%AD%89%E3%81%A7%E3%83%97%E3%83%AC%E3%82%B9%E3%83%AA%E3%83%AA%E3%83%BC%E3%82%B9%E3%82%92%E6%8A%95%E7%A8%BF">3. PR TIMES等でプレスリリースを投稿</a></h3>
<p>PR TIMESにプレスリリースを投稿すると、<br />
各種メディアに掲載され、露出が増えるので、活用しています。</p>
<p>配信のタイミングは、以下の記事を参考にしていて、<br />
火曜日の朝10時10分にしました。<br />
・<a target="_blank" rel="nofollow noopener" href="https://prtimes.jp/magazine/time-to-pressrelease/">プレスリリースの配信タイミングは?曜日・時間を戦略的に決定して効果を最大化させよう</a></p>
<p>また、会社設立日から2年は無料枠がもらえる、<br />
スタートアップチャレンジを活用しています。</p>
<p>・<a target="_blank" rel="nofollow noopener" href="https://prtimes.jp/startup_free/">スタートアップチャレンジ|PR TIMESプレスリリース無料配信プログラム</a></p>
<h3 id="4. Zenn/Crieit/Qiita等で記事を投稿"><a href="#4.+Zenn%2FCrieit%2FQiita%E7%AD%89%E3%81%A7%E8%A8%98%E4%BA%8B%E3%82%92%E6%8A%95%E7%A8%BF">4. Zenn/Crieit/Qiita等で記事を投稿</a></h3>
<p>なにか作ったら各種サイトに記事を書いてます。<br />
今回は<a target="_blank" rel="nofollow noopener" href="https://zenn.dev/">Zenn</a>と<a href="https://crieit.net/">Crieit</a>と<a target="_blank" rel="nofollow noopener" href="https://qiita.com/">Qiita</a>。<br />
(Zennはずっと投稿したかったけど、ネタがなかったけど、初Zennできてよかった(<em>´ω`</em>))</p>
<p>それぞれトレンドに掲載されると露出が増えるのもあるけど、<br />
LPやトップページには書ききれないことを記事で補足している感じ。</p>
<p>今回は、以下のテーマでそれぞれ書きました。<br />
・Zenn ... <strong>なんで作ろうと思ったかの理由と経緯</strong><br />
・Qiita ... <strong>システム構成や技術要素</strong><br />
・Crieit ... <strong>PRのためになにをしたか</strong>(この記事)</p>
<p>特に、Crieit独自の機能である、カノニカルが設定できるのが好きで、<br />
Zennなどに投稿するときは合わせてクロス投稿しています。</p>
<p>いいね等が集計時に5〜10くらいつくとトレンドに乗れるっぽいので、<br />
記事書いて、ツイートして、RTして、たくさん祈ります🙏</p>
<p>Qiitaは毎日5時/17時に更新されるので、<br />
集計対象の時間が長くなるよう、その時間くらいに投稿がよさそうです。</p>
<h3 id="5. RTされやすそうな時間帯にセルフRT"><a href="#5.+RT%E3%81%95%E3%82%8C%E3%82%84%E3%81%99%E3%81%9D%E3%81%86%E3%81%AA%E6%99%82%E9%96%93%E5%B8%AF%E3%81%AB%E3%82%BB%E3%83%AB%E3%83%95RT">5. RTされやすそうな時間帯にセルフRT</a></h3>
<p>プレスリリースや記事を投稿したときもそうですが、<br />
Twitterで伸びる曜日と時間帯というツイートを見てから、<br />
なるべくこのときに投稿するように意識しています。</p>
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">【データから見るTwitterのツイートが伸びる曜日と時間帯🔥】◼︎平日・5:00、11:00、15:00、20:00◼︎休日・8:00、12:00、14:00、16:00→多くの方が通勤時間帯や夜間がユーザー数が多い為、良いと考えているが、実はタイムラインの流速が早くツイートが流れてしまう可能性が高い。<a target="_blank" rel="nofollow noopener" href="https://twitter.com/hashtag/%E3%83%87%E3%82%B8%E3%83%97%E3%83%AD?src=hash&ref_src=twsrc%5Etfw">#デジプロ</a> <a target="_blank" rel="nofollow noopener" href="https://t.co/JHIT9Nv6QK">pic.twitter.com/JHIT9Nv6QK</a></p>— デジプロ🔥@デジタル広告のプロを育てる学習サービス (@digimapro) <a target="_blank" rel="nofollow noopener" href="https://twitter.com/digimapro/status/1161375718611140608?ref_src=twsrc%5Etfw">August 13, 2019</a></blockquote>
<p>◼︎<strong>平日</strong> ... 5:00、11:00、15:00、20:00<br />
◼︎<strong>休日</strong> ... 8:00、12:00、14:00、16:00</p>
<p>一日に何回かあるので、他の時間帯にセルフRTを活用。</p>
<p>セルフRTは結構抵抗があったけど、<br />
昔、とあるマンガ家さんが</p>
<blockquote>
<p>ツイートしただけではダメ。<br />
何度もセルフRTしない届かないし、見てもらえないよ。</p>
</blockquote>
<p>的なことをいってたツイートを見てから、<br />
見てほしいツイートはなるべくRTするようがんばってる。<br />
(イラスト系だと昼あげ/夜あげとかみるので同じ感じ。)</p>
<p>運要素多いけど、やさしいフォロワーさんが<br />
いいね/RTで拡散してくれるので、ほんとありがたすぎる。。</p>
<h2 id="まとめ"><a href="#%E3%81%BE%E3%81%A8%E3%82%81">まとめ</a></h2>
<p>こんな感じで、なるべく露出が増えるよう、<br />
サービスを知ってもらえるような活動をしていました。</p>
<ol>
<li>LPや事前登録サイトを公開</li>
<li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/organizations/admin-guild">運営者ギルド</a>でバグバッシュしてもらう</li>
<li>PR TIMES等でプレスリリースを公開&ツイートする</li>
<li>Zenn/Crieit/Qiita等で記事を公開&ツイートする</li>
<li>RTされやすそうな時間帯にセルフRTなどする</li>
</ol>
<p>トレンド→はてブのホッテントリ→Google砲、<br />
などのコンボが決まると広く拡散しますが、<br />
0いいねなどよくあるので、いつも運ゲー。。</p>
<p>地道にサービスを改善しつつ、<br />
記事などのコンテンツを増やしつつ、<br />
最後は天にまかせて、がんばっています!</p>
<p>リリース後のプロモーションはいつも困っているので、<br />
これから何かを作ろうとしている人の参考になれたらうれしいです😊</p>
<h3 id="SSSAPIでは、β版ユーザを募集中です!"><a href="#SSSAPI%E3%81%A7%E3%81%AF%E3%80%81%CE%B2%E7%89%88%E3%83%A6%E3%83%BC%E3%82%B6%E3%82%92%E5%8B%9F%E9%9B%86%E4%B8%AD%E3%81%A7%E3%81%99%EF%BC%81">SSSAPIでは、β版ユーザを募集中です!</a></h3>
<p>β期間中はプレミアムプランの内容を無料で使えるのでお得!<br />
ぜひぜひ、この機会にお試しいただければ〜!</p>
<p>GoogleスプレッドシートのAPI化サービス<br />
『<a target="_blank" rel="nofollow noopener" href="https://sssapi.app">SSSAPI</a>』<br />
URL: <a target="_blank" rel="nofollow noopener" href="https://sssapi.app">https://sssapi.app</a><br />
Twitter: <a target="_blank" rel="nofollow noopener" href="https://twitter.com/sssapi_app">@sssapi_app</a></p>
<p><a href="https://crieit.now.sh/upload_images/661a1f43523cb2f246e88ee6362c26f8612b3d4166323.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/661a1f43523cb2f246e88ee6362c26f8612b3d4166323.png?mw=700" style="max-width: 500px;" /></a></p>
<p>記事を書く励みにもなるので、<br />
よかったら、いいねやシェア、はてブなどもよろしくおねがいします!</p>
<p><a target="_blank" rel="nofollow noopener" href="https://b.hatena.ne.jp/entry/s/crieit.net/posts/Web-15-5">はてなブックマークはこちらから</a></p>
<p><strong>※以下、有料コンテンツです※</strong></p>
<p>以下のデータを公開しています。<br />
ちょっとはずかしいので、おもしろいと思ってくれた方は、<br />
コーヒー一杯差し入れるくらいの気持ちで見てもらえれば〜</p>
<ul>
<li>Google AnaliticsのPV数/流入元</li>
<li>PR TIMESの統計情報</li>
<li>SSSAPIの登録者数とAPI作成数</li>
<li>(おまけ) Twitter広告を試してみた結果</li>
</ul>
きらぷか@積読ハウマッチ/SSSAPIなど
tag:crieit.net,2005:PublicArticle/17614
2021-08-25T05:47:03+09:00
2021-08-25T05:47:03+09:00
https://crieit.net/posts/Firestore-API
Firestore最高だけど、初期データとかマスタデータ管理がめんどくさいので、スプレッドシートをAPI化サービスを作ってみた
<p>開発しているGoogleスプレッドシートのAPI化サービス「<a target="_blank" rel="nofollow noopener" href="https://sssapi.app">SSSAPI</a>」。<br />
無事にオープンβ版を公開できたので、なんで作ろうと思ったかを書いてみた。</p>
<p><a href="https://crieit.now.sh/upload_images/7622e92a81d522c24c95246b0656b8e3612559a688a84.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/7622e92a81d522c24c95246b0656b8e3612559a688a84.png?mw=700" style="max-width: 600px;"></a></img><br />
<a target="_blank" rel="nofollow noopener" href="http://sssapi.app">http://sssapi.app</a></p>
<h2 id="なぜ作ろうと思ったか"><a href="#%E3%81%AA%E3%81%9C%E4%BD%9C%E3%82%8D%E3%81%86%E3%81%A8%E6%80%9D%E3%81%A3%E3%81%9F%E3%81%8B">なぜ作ろうと思ったか</a></h2>
<p>個人開発が好きで、いろんなものをちょこちょこ作ってる。<br />
今は、Nuxt.jsとFirebaseがお気に入りで、<br />
小さなものを含めると20サービスくらい作ったと思う。</p>
<p>ちょっとしたものを作ることも多く、<br />
「<strong>もっと楽に作ることはできないかなぁ?</strong>」<br />
といろいろ考えたりしていた。</p>
<p>その時同時に考えていたのが、<a target="_blank" rel="nofollow noopener" href="https://umaiku.app">ゲーム攻略のサービス</a>。<br />
マスタデータが多く、更新も早いので、どうしようかなと。</p>
<p>とりあえず、初期データが必要なので、<br />
スプレッドシートにまとめていた。</p>
<p>データを集め終わって、いざつくろうとしたときに、<br />
「<strong>このままJSON APIにできれば楽なのにな〜</strong>」<br />
というのが最初だった。</p>
<h2 id="最初はJSONファイル"><a href="#%E6%9C%80%E5%88%9D%E3%81%AFJSON%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB">最初はJSONファイル</a></h2>
<p>ゲームの攻略サービスを作っている途中だったので、<br />
まずは、スプレッドシートAPIでJSON化して、<br />
JSONファイルをソースに含める形で開発。</p>
<p>これだけでも割とよく、<br />
とりあえずJSONファイルで開発するのは良い感じだった。</p>
<p>データの項目が足りなくても、<br />
スプレッドシートなら列を追加するだけなので、<br />
<strong>データ構造を検討している初期はスムーズに進んだ</strong>。</p>
<p>ただ、JSONファイルをインポートしてるので、<br />
<strong>ビルドサイズは大きくなる</strong>し、<br />
<strong>データを更新したいときはビルドしないといけない</strong>ので、<br />
めんどくさいな思うようになり、API化は必要だなと作りはじめた。</p>
<h2 id="Firestoreで初期データをめんどくさい"><a href="#Firestore%E3%81%A7%E5%88%9D%E6%9C%9F%E3%83%87%E3%83%BC%E3%82%BF%E3%82%92%E3%82%81%E3%82%93%E3%81%A9%E3%81%8F%E3%81%95%E3%81%84">Firestoreで初期データをめんどくさい</a></h2>
<p>今まで作ってきたものを振り返ると、<br />
割とこういうことが多い感じだった。</p>
<p>過去に作ったものでも、カテゴリやお知らせなど、<br />
こちらが用意するデータは、JSONファイルで用意していたり、<br />
<strong>JSONファイルの内容をFirestoreに登録するスクリプト</strong>を書いていた。</p>
<p>Firestoreは便利なんだけど、<br />
<strong>管理画面は自分で用意しないといけない</strong>ので、<br />
マスターデータを扱いたいときは、めんどうな部分があった。</p>
<p>1つ項目を追加するのにも、JSONファイル&ビルドだったり、<br />
追加用のスクリプトを書かないといけない。。</p>
<p><strong>SSSAPIであれば、スプレッドシートを管理画面にでき、<br />
そのままAPI化できるので、かなり楽になるな〜と。</strong></p>
<p>実際、楽になって、<a href="https://crieit.net">crieit</a>の<a href="https://crieit.net/boards/web1week-202107/CoCo">web1week</a>の企画へ<br />
参加したときに作ったWebサービスは1~2日で開発できた。</p>
<h2 id="他のじゃダメだった?"><a href="#%E4%BB%96%E3%81%AE%E3%81%98%E3%82%83%E3%83%80%E3%83%A1%E3%81%A0%E3%81%A3%E3%81%9F%EF%BC%9F">他のじゃダメだった?</a></h2>
<p>いざ作るとなると、時間が必要なので、<br />
既存のものはないかな〜と見てみた。</p>
<p>見てみたのは、GAS、ヘッドレスCMS、競合サービスの3種類。</p>
<h4 id="GASは?"><a href="#GAS%E3%81%AF%EF%BC%9F">GASは?</a></h4>
<p>一番よく見るのはこれかなと思う。<br />
Google Action ScriptでWebサービスとして公開するパターン。</p>
<p>記事も多くコードもあるので、便利で昔使っていた。</p>
<p>ただ、<strong>シートごとにGASの設定が必要だったり、<br />
どれが設定したスプレッドシートか忘れてしまう</strong>ので、<br />
一元管理ができればな〜と思ったりする。</p>
<p>SSSAPIであれば、<strong>毎シートにGASを設定する必要はない</strong>し、<br />
登録してあるスプレッドシート=API化しているなので、<br />
<strong>一元管理も簡単にできる</strong>ようにした。</p>
<h3 id="ヘッドレスCMSは?"><a href="#%E3%83%98%E3%83%83%E3%83%89%E3%83%AC%E3%82%B9CMS%E3%81%AF%EF%BC%9F">ヘッドレスCMSは?</a></h3>
<p>次によくみるのはヘッドレスCMSかなと思う。<br />
スキーマも柔軟に設定できて、管理画面もリッチ。</p>
<p>ただ、内容がリッチすぎたり、<br />
<strong>個人開発だとお高めの価格設定なので、手が出せず。。</strong></p>
<p>「多くのサービスを楽に作りたい!!」と思うと、<br />
無料枠の制限では心もとないなと。。<br />
また、<strong>アクセス制限などは有料プラン</strong>ぽいので、断念。</p>
<p>SSSAPIでは、機能は限定的だけど、<br />
<strong>個人開発でも使いやすい価格帯</strong>になるようにがんばってる。<br />
無料のプランでもドメイン制限を使えるようにしている。</p>
<h3 id="競合サービスは?"><a href="#%E7%AB%B6%E5%90%88%E3%82%B5%E3%83%BC%E3%83%93%E3%82%B9%E3%81%AF%EF%BC%9F">競合サービスは?</a></h3>
<p>今まで聞いたことがなかったけど、調べてみたら、<br />
6つくらい競合のサービスがあった。</p>
<p>もちろん、ヘッドレスCMSよりも安めだけど、<br />
<strong>Googleドライブやスプレッドシートへの全権限を許可しないといけない。。</strong><br />
もしくは、全員がアクセスできるように共有設定を変更する必要がある。</p>
<p>Googleドライブやスプレッドシートへの全権限を許可は、<br />
割とこれが強くて、<strong>個人情報や機密情報を入れているアカウントだと使えない</strong>。</p>
<p>API化したいスプレッドシートのデータ自体も財産なので、<br />
<strong>オープンにはしたくない</strong>ときも多い。</p>
<p>SSSAPIでは、権限を共有するのではなく、<br />
システムアカウントを共有設定に追加してもらう形にし、<br />
<strong>共有設定されているシートのみ参照できる方法</strong>にしている。</p>
<p>すこし手間ではあるけど、安心・安全のため。</p>
<p>ドメイン制限やアクセストークンによる認証も用意しているので、<br />
<strong>APIのURLがわかっていても、アクセスできない仕組み</strong>を用意している。</p>
<h2 id="使ってみてどうだったか?"><a href="#%E4%BD%BF%E3%81%A3%E3%81%A6%E3%81%BF%E3%81%A6%E3%81%A9%E3%81%86%E3%81%A0%E3%81%A3%E3%81%9F%E3%81%8B%EF%BC%9F">使ってみてどうだったか?</a></h2>
<p>利便性・価格・セキュリティの面から、<br />
作ったほうがいいなぁと思って、開発を進めてみた。</p>
<p>使っているのが、マスターデータが多いものに偏っているけど、</p>
<ul>
<li><strong>スプシが管理画面になる</strong></li>
<li><strong>ビルドしないで更新できる</strong></li>
<li><strong>ドメイン制限できるので守れる</strong></li>
</ul>
<p>と、かなり便利に。</p>
<p>他のサービスのお知らせやリリースノートなども、<br />
順次、SSSAPIに置き換えている。</p>
<p>ユーザが作成するデータは、今までどおりFirestoreを使っている感じだけど、<br />
プロトタイプ開発のときやテストデータ作成なんかでも活躍してるので、<br />
<strong>最初に思っていた「ちょっとしたものを早く楽に作れる」ようになった</strong>気がする。</p>
<p>スプレッドシートで管理するのつらいな。。という規模になってきたら、<br />
<strong>JSON APIの結果をFirestoreに突っ込めばよい</strong>のもいい。</p>
<p>β版なので、まだまだ足りないところはあるけど、<br />
ユーザさんからのフィードバックとドックフーディングをして、<br />
よりよいものにしていければと思う。</p>
<h2 id="SSSAPIでは、β版ユーザを募集中です!"><a href="#SSSAPI%E3%81%A7%E3%81%AF%E3%80%81%CE%B2%E7%89%88%E3%83%A6%E3%83%BC%E3%82%B6%E3%82%92%E5%8B%9F%E9%9B%86%E4%B8%AD%E3%81%A7%E3%81%99%EF%BC%81">SSSAPIでは、β版ユーザを募集中です!</a></h2>
<p>β期間中はプレミアムプランの内容を無料で使えるのでお得!<br />
ぜひぜひ、この機会にお試しいただければ〜!</p>
<p>GoogleスプレッドシートのAPI化サービス<br />
『SSSAPI』<br />
Twitter: <a target="_blank" rel="nofollow noopener" href="https://twitter.com/sssapi_app">@sssapi_app</a></p>
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">🌟🌟オープンβ版の公開開始🌟🌟フィルタリングやページングなど機能を強化し、より便利になりました😊β版はプレミアムプランの内容を無料で利用できます🙌ぜひ、この機会にお試しください‼️‼️GoogleスプレッドシートのAPI化サービス<a target="_blank" rel="nofollow noopener" href="https://t.co/fKUGVFeQR7">https://t.co/fKUGVFeQR7</a><a target="_blank" rel="nofollow noopener" href="https://twitter.com/hashtag/%E6%8B%A1%E6%95%A3%E5%B8%8C%E6%9C%9BRT%E5%8D%94%E5%8A%9B%E3%81%8A%E9%A1%98%E3%81%84%E8%87%B4%E3%81%97%E3%81%BE%E3%81%99?src=hash&ref_src=twsrc%5Etfw">#拡散希望RT協力お願い致します</a> <a target="_blank" rel="nofollow noopener" href="https://t.co/O2rjs35BZD">pic.twitter.com/O2rjs35BZD</a></p>— 【公式】SSSAPI / GoogleスプレッドシートのAPI化サービス (@sssapi_app) <a target="_blank" rel="nofollow noopener" href="https://twitter.com/sssapi_app/status/1429986819047792662?ref_src=twsrc%5Etfw">August 24, 2021</a></blockquote>
<p><a target="_blank" rel="nofollow noopener" href="http://sssapi.app">http://sssapi.app</a><br />
<a href="https://crieit.now.sh/upload_images/7622e92a81d522c24c95246b0656b8e3612559a688a84.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/7622e92a81d522c24c95246b0656b8e3612559a688a84.png?mw=700" style="max-width: 600px;"></a></img></p>
きらぷか@積読ハウマッチ/SSSAPIなど
tag:crieit.net,2005:PublicArticle/16256
2020-12-04T07:00:09+09:00
2020-12-04T07:00:09+09:00
https://crieit.net/posts/2020-5fc95fe99fa00
2020年の個人開発を振り返ってみた
<p>どうも<a target="_blank" rel="nofollow noopener" href="https://twitter.com/kira_puka">きらぷか</a>です。<br />
この記事は、<a href="https://crieit.net">Crieit</a>のアドベントカレンダー、<a href="https://crieit.net/advent-calendars/2020/crieit">なんでも</a>の4日目の記事です。</p>
<p>今年はなんかいろいろ作ったなぁ〜と思い、振り返ってみた。<br />
切りが悪いので、2019年12月からの1年分を見てみる。</p>
<h3 id="新作を6つも作ってた"><a href="#%E6%96%B0%E4%BD%9C%E3%82%926%E3%81%A4%E3%82%82%E4%BD%9C%E3%81%A3%E3%81%A6%E3%81%9F">新作を6つも作ってた</a></h3>
<ul>
<li>1月: <a target="_blank" rel="nofollow noopener" href="http://hen-ai.net/">へんあいマップ</a></li>
<li>5月: <a target="_blank" rel="nofollow noopener" href="https://air-money.netlify.app/">エア銭</a> / <a target="_blank" rel="nofollow noopener" href="https://kabu-reco.app/">カブれこ</a></li>
<li>9月: <a target="_blank" rel="nofollow noopener" href="https://find-two.vercel.app/">FIND TWO</a> / <a target="_blank" rel="nofollow noopener" href="https://nakama-count.vercel.app/">仲間カウント</a> / <a target="_blank" rel="nofollow noopener" href="http://zutsuu.memory-lovers.com/">頭痛仲間</a></li>
</ul>
<p>エア銭、FIND TWOは<a href="https://crieit.net/boards/web1week-202009">#web1week</a>のなので毛色が違うけど、<br />
新作作りたい意欲が刺激されて新作作ってるのがよくわかる(<em>´ω`</em>)</p>
<p>思いつくのがCGM系のアイデアが多く、いろいろ作ってみてるけど、<br />
やっぱり難しい。。でも思いついたから作りたい。。を繰り返してる感じ。</p>
<p>この中だと、ツール系のカブれこが一番がんばっているのがおもしろい。<br />
もうすこし稼働をかけてもいいかもしれない。</p>
<h3 id="積読ハウマッチは31回アップデート"><a href="#%E7%A9%8D%E8%AA%AD%E3%83%8F%E3%82%A6%E3%83%9E%E3%83%83%E3%83%81%E3%81%AF31%E5%9B%9E%E3%82%A2%E3%83%83%E3%83%97%E3%83%87%E3%83%BC%E3%83%88">積読ハウマッチは31回アップデート</a></h3>
<div class="table-responsive"><table>
<thead>
<tr>
<th align="right"></th>
<th align="right">更新回数</th>
<th align="right">新作</th>
</tr>
</thead>
<tbody>
<tr>
<td align="right">12月</td>
<td align="right">2</td>
<td align="right">1</td>
</tr>
<tr>
<td align="right">1月</td>
<td align="right">4</td>
<td align="right"></td>
</tr>
<tr>
<td align="right">2月</td>
<td align="right">4</td>
<td align="right"></td>
</tr>
<tr>
<td align="right">3月</td>
<td align="right">8</td>
<td align="right"></td>
</tr>
<tr>
<td align="right">4月</td>
<td align="right">2</td>
<td align="right"></td>
</tr>
<tr>
<td align="right">5月</td>
<td align="right">3</td>
<td align="right">2</td>
</tr>
<tr>
<td align="right">6月</td>
<td align="right">3</td>
<td align="right"></td>
</tr>
<tr>
<td align="right">7月</td>
<td align="right">2</td>
<td align="right"></td>
</tr>
<tr>
<td align="right">8月</td>
<td align="right">1</td>
<td align="right"></td>
</tr>
<tr>
<td align="right">9月</td>
<td align="right">0</td>
<td align="right">3</td>
</tr>
<tr>
<td align="right">10月</td>
<td align="right">2</td>
<td align="right"></td>
</tr>
<tr>
<td align="right">11月</td>
<td align="right">0</td>
<td align="right"></td>
</tr>
</tbody>
</table></div>
<p>更新内容の大小はあれど、前半はがんばってたっぽい。<br />
4月頃から新作とか本業とかで稼働が少なくなってるのが顕著に。</p>
<p>とはいえ、結構継続的に開発できているのでよい。</p>
<h3 id="積読ハウマッチの数字を見てみる"><a href="#%E7%A9%8D%E8%AA%AD%E3%83%8F%E3%82%A6%E3%83%9E%E3%83%83%E3%83%81%E3%81%AE%E6%95%B0%E5%AD%97%E3%82%92%E8%A6%8B%E3%81%A6%E3%81%BF%E3%82%8B">積読ハウマッチの数字を見てみる</a></h3>
<p>今までためてた数字をまとめてみた。<br />
上下はあるけど、横ばい or ちょっと成長という感じ。</p>
<div class="table-responsive"><table>
<thead>
<tr>
<th align="right"></th>
<th align="right">MAU</th>
<th align="right">PV</th>
<th align="right">ユーザ数</th>
<th align="right">書籍数</th>
<th align="right">収支</th>
<th align="right">更新回数</th>
<th align="right">新作</th>
</tr>
</thead>
<tbody>
<tr>
<td align="right">12月</td>
<td align="right">1,244</td>
<td align="right">7,526</td>
<td align="right">91</td>
<td align="right">2,779</td>
<td align="right">1,087</td>
<td align="right">2</td>
<td align="right">1</td>
</tr>
<tr>
<td align="right">1月</td>
<td align="right">1,348</td>
<td align="right">11,081</td>
<td align="right">92</td>
<td align="right">2,375</td>
<td align="right">1,568</td>
<td align="right">4</td>
<td align="right"></td>
</tr>
<tr>
<td align="right">2月</td>
<td align="right">1,393</td>
<td align="right">10,125</td>
<td align="right">102</td>
<td align="right">2,820</td>
<td align="right">2,653</td>
<td align="right">4</td>
<td align="right"></td>
</tr>
<tr>
<td align="right">3月</td>
<td align="right">1,655</td>
<td align="right">12,385</td>
<td align="right">90</td>
<td align="right">3,190</td>
<td align="right">437</td>
<td align="right">8</td>
<td align="right"></td>
</tr>
<tr>
<td align="right">4月</td>
<td align="right">1,422</td>
<td align="right">11,780</td>
<td align="right">92</td>
<td align="right">2,645</td>
<td align="right">371</td>
<td align="right">2</td>
<td align="right"></td>
</tr>
<tr>
<td align="right">5月</td>
<td align="right">1,389</td>
<td align="right">10,770</td>
<td align="right">70</td>
<td align="right">1,947</td>
<td align="right">847</td>
<td align="right">3</td>
<td align="right">2</td>
</tr>
<tr>
<td align="right">6月</td>
<td align="right">1,888</td>
<td align="right">13,486</td>
<td align="right">137</td>
<td align="right">3,399</td>
<td align="right">323</td>
<td align="right">3</td>
<td align="right"></td>
</tr>
<tr>
<td align="right">7月</td>
<td align="right">1,778</td>
<td align="right">18,104</td>
<td align="right">104</td>
<td align="right">9,367</td>
<td align="right">567</td>
<td align="right">2</td>
<td align="right"></td>
</tr>
<tr>
<td align="right">8月</td>
<td align="right">1,667</td>
<td align="right">14,304</td>
<td align="right">85</td>
<td align="right">3,919</td>
<td align="right">2,278</td>
<td align="right">1</td>
<td align="right"></td>
</tr>
<tr>
<td align="right">9月</td>
<td align="right">1,886</td>
<td align="right">18,827</td>
<td align="right">104</td>
<td align="right">10,769</td>
<td align="right">1,601</td>
<td align="right">0</td>
<td align="right">3</td>
</tr>
<tr>
<td align="right">10月</td>
<td align="right">1,432</td>
<td align="right">15,569</td>
<td align="right">70</td>
<td align="right">10,152</td>
<td align="right">2,873</td>
<td align="right">2</td>
<td align="right"></td>
</tr>
<tr>
<td align="right">11月</td>
<td align="right">1,086</td>
<td align="right">10,845</td>
<td align="right">57</td>
<td align="right">2,111</td>
<td align="right">1,215</td>
<td align="right">0</td>
<td align="right"></td>
</tr>
</tbody>
</table></div>
<h4 id="PVと更新回数"><a href="#PV%E3%81%A8%E6%9B%B4%E6%96%B0%E5%9B%9E%E6%95%B0">PVと更新回数</a></h4>
<p>ここからPVといろんな数字を比較してみる。</p>
<p>更新が少ないので、PVも少ないのかなと思ったら、そんなことなかった。</p>
<p><a href="https://crieit.now.sh/upload_images/4aa4de18c0e570b74253c2caee51ecd55fc83a65d2a91.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/4aa4de18c0e570b74253c2caee51ecd55fc83a65d2a91.png?mw=700" alt="スクリーンショット 2020-12-03 10.07.07.png" /></a></p>
<p>7月は新機能を追加したりプレスリリースを出したりしてPVが上がってるのと、<br />
9月はバズったツイートで宣伝してもらえたり、Vtuberの方に遊んでもらったりで上がってる感じ。</p>
<p>知ってくれた人が増えたので、全体的に少しずつPVが上がってる状態。</p>
<h4 id="PVと書籍増加数"><a href="#PV%E3%81%A8%E6%9B%B8%E7%B1%8D%E5%A2%97%E5%8A%A0%E6%95%B0">PVと書籍増加数</a></h4>
<p><a href="https://crieit.now.sh/upload_images/1b3882590306543fcd69f0422433fda65fc83a6e6c929.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/1b3882590306543fcd69f0422433fda65fc83a6e6c929.png?mw=700" alt="スクリーンショット 2020-12-03 10.07.14.png" /></a></p>
<p>そのまんま比例している感じ。登録するからPV増えるので予想通り。</p>
<h4 id="PVと収支"><a href="#PV%E3%81%A8%E5%8F%8E%E6%94%AF">PVと収支</a></h4>
<p><a href="https://crieit.now.sh/upload_images/8a5f17fb86f8a96c6b0ffe7353567eac5fc83a77e7123.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/8a5f17fb86f8a96c6b0ffe7353567eac5fc83a77e7123.png?mw=700" alt="スクリーンショット 2020-12-03 10.07.24.png" /></a></p>
<p>更新回数と同じで、あまり関係なかった。<br />
アフィリエイト型なので、Amazonで買わないと売上にならないので、たしかに。</p>
<p>買うまでの動線をなんとかしないといけないなと思いつつ、まだあまり整備できてない。。</p>
<h3 id="作ったアプリの技術要素の移り変わり"><a href="#%E4%BD%9C%E3%81%A3%E3%81%9F%E3%82%A2%E3%83%97%E3%83%AA%E3%81%AE%E6%8A%80%E8%A1%93%E8%A6%81%E7%B4%A0%E3%81%AE%E7%A7%BB%E3%82%8A%E5%A4%89%E3%82%8F%E3%82%8A">作ったアプリの技術要素の移り変わり</a></h3>
<p>なにをつくるかも変わってきたけど、どう作るかも結構変わった気がする。</p>
<p>Nuxt+TypeScript+Firebaseの基本セットは確定だけど、<br />
どこにデプロイするかは試行錯誤中。</p>
<p>作ってきたWebアプリのデプロイ先はこんな感じ。上から新しい順。</p>
<div class="table-responsive"><table>
<thead>
<tr>
<th>アプリ名</th>
<th align="center">Cloud Run</th>
<th align="center">GAE</th>
<th align="center">Vercel</th>
<th align="center">Netlify</th>
<th align="center">Heroku</th>
<th align="center">Firebase</th>
</tr>
</thead>
<tbody>
<tr>
<td>積読ハウマッチ(新)</td>
<td align="center">○</td>
<td align="center"></td>
<td align="center"></td>
<td align="center"></td>
<td align="center"></td>
<td align="center"></td>
</tr>
<tr>
<td>頭痛仲間</td>
<td align="center"></td>
<td align="center">○</td>
<td align="center"></td>
<td align="center"></td>
<td align="center"></td>
<td align="center"></td>
</tr>
<tr>
<td>仲間カウント</td>
<td align="center"></td>
<td align="center"></td>
<td align="center">○</td>
<td align="center"></td>
<td align="center"></td>
<td align="center"></td>
</tr>
<tr>
<td>FIND TWO</td>
<td align="center"></td>
<td align="center"></td>
<td align="center">○</td>
<td align="center"></td>
<td align="center"></td>
<td align="center"></td>
</tr>
<tr>
<td>カブれこ</td>
<td align="center"></td>
<td align="center"></td>
<td align="center"></td>
<td align="center">○</td>
<td align="center"></td>
<td align="center"></td>
</tr>
<tr>
<td>エア銭</td>
<td align="center"></td>
<td align="center"></td>
<td align="center"></td>
<td align="center">○</td>
<td align="center"></td>
<td align="center"></td>
</tr>
<tr>
<td>へんあいマップ</td>
<td align="center"></td>
<td align="center"></td>
<td align="center"></td>
<td align="center"></td>
<td align="center">○</td>
<td align="center"></td>
</tr>
<tr>
<td>積読ハウマッチ(旧)</td>
<td align="center"></td>
<td align="center"></td>
<td align="center"></td>
<td align="center">○</td>
<td align="center"></td>
<td align="center"></td>
</tr>
<tr>
<td>便利サイト</td>
<td align="center"></td>
<td align="center"></td>
<td align="center">○</td>
<td align="center"></td>
<td align="center"></td>
<td align="center"></td>
</tr>
<tr>
<td>トランスノート</td>
<td align="center"></td>
<td align="center"></td>
<td align="center"></td>
<td align="center"></td>
<td align="center"></td>
<td align="center">○</td>
</tr>
</tbody>
</table></div>
<p>2年前くらいはFirebase Hostingを使っていたけど、<br />
Netlify→Vercel→GAE or Cloud Runという感じで移り変わってる。</p>
<p>というより、今までSPAだったけどSSRしたい、というのが主な理由。<br />
それぞれの構成はこんな感じ。</p>
<div class="table-responsive"><table>
<thead>
<tr>
<th>アプリ名</th>
<th>デプロイ先</th>
<th align="center">SSR</th>
<th align="center">SSG</th>
<th align="center">SPA</th>
</tr>
</thead>
<tbody>
<tr>
<td>積読ハウマッチ(新)</td>
<td>Cloud Run</td>
<td align="center">○</td>
<td align="center"></td>
<td align="center"></td>
</tr>
<tr>
<td>頭痛仲間</td>
<td>GAE</td>
<td align="center">○</td>
<td align="center"></td>
<td align="center"></td>
</tr>
<tr>
<td>仲間カウント</td>
<td>Vercel</td>
<td align="center">○</td>
<td align="center"></td>
<td align="center"></td>
</tr>
<tr>
<td>FIND TWO</td>
<td>Vercel</td>
<td align="center">○</td>
<td align="center"></td>
<td align="center"></td>
</tr>
<tr>
<td>カブれこ</td>
<td>Netlify</td>
<td align="center"></td>
<td align="center"></td>
<td align="center">○</td>
</tr>
<tr>
<td>エア銭</td>
<td>Netlify</td>
<td align="center"></td>
<td align="center"></td>
<td align="center">○</td>
</tr>
<tr>
<td>へんあいマップ</td>
<td>Heroku</td>
<td align="center">○</td>
<td align="center"></td>
<td align="center"></td>
</tr>
<tr>
<td>積読ハウマッチ(旧)</td>
<td>Netlify</td>
<td align="center"></td>
<td align="center"></td>
<td align="center">○</td>
</tr>
<tr>
<td>便利サイト</td>
<td>Vercel</td>
<td align="center"></td>
<td align="center">○</td>
<td align="center"></td>
</tr>
<tr>
<td>トランスノート</td>
<td>Firebase</td>
<td align="center"></td>
<td align="center"></td>
<td align="center">○</td>
</tr>
</tbody>
</table></div>
<ul>
<li>Netlify最高だけど、SPAの初期ロードがやっぱりおそい。。</li>
<li>Vercelが簡単にSSRできるようになってよくなってきた
<ul>
<li>デプロイしたらメールで通知してくれるので楽</li>
<li>でも東京リージョンじゃないし、リクエストごとに起動なので遅い</li>
<li>規模が大きくなると、ファイルサイズの制限でデプロイできない</li>
</ul></li>
<li>Cloud RunやGAEだと、ファイル制限やリージョンの設定もできる
<ul>
<li>デプロイ時のメール通知やSlack通知も自分で設定可能</li>
</ul></li>
</ul>
<p>という経緯を経て、今はCloud RunやGAEかなという気持ち。<br />
いろんなWebサービスを触ってみて、初期表示の速さをなんとかしたい感が強まってる。</p>
<h3 id="いろいろ作ってみて"><a href="#%E3%81%84%E3%82%8D%E3%81%84%E3%82%8D%E4%BD%9C%E3%81%A3%E3%81%A6%E3%81%BF%E3%81%A6">いろいろ作ってみて</a></h3>
<p>今年は割といろいろ作ってみたけど、至らなさを痛感する。</p>
<p>雛形のプロジェクトを作ったことで立ち上げ早くなったけど、<br />
「最低限の機能を作って出す」というところ考え直すのがよいと感じがしてる。</p>
<p>たとえ、最低限の機能がある動くものがあっても、<br />
遅い / バグが多い / さわり心地が悪い<br />
など、残念な部分が目立つと一瞬来てくるけど、継続しない。</p>
<p>また、お知らせや通知などの部分も初期では外しがちだけど、<br />
継続に繋がる部分なので、『最低限の機能』に含めるべきだったと思う。</p>
<p>リリース時やアップデート時は、最初のピークなので、<br />
来てもらった人にまた来てもらえる部分は最初に掴んでおけばよかったなと思う。</p>
<p>一発ネタならいいけど、そうじゃないのはちゃんと抑えておきたいところ。</p>
<h3 id="来年はどうしようか"><a href="#%E6%9D%A5%E5%B9%B4%E3%81%AF%E3%81%A9%E3%81%86%E3%81%97%E3%82%88%E3%81%86%E3%81%8B">来年はどうしようか</a></h3>
<p>積読ハウマッチは引き続き、がんばるとして、<br />
たくさん作りすぎて、よくわからなくなるので、<br />
開発や管理を楽にするのを作りたいなぁとか思ってる。</p>
<p>なんだかんだ昔作ったAndroidアプリは着実に伸びてるので、<br />
PUSH通知もできるFlutterにも手を出していきたい。</p>
<p>振り返ってみて、いろいろ思うところはあるけれど、<br />
やっぱり、作りたい欲が高まるとなにか作っちゃうよね(<em>´ω`</em>)</p>
<p>来年もゆるゆるがんばろう(<em>´ω`</em>)</p>
きらぷか@積読ハウマッチ/SSSAPIなど
tag:crieit.net,2005:PublicArticle/15909
2020-05-24T17:18:35+09:00
2020-05-24T17:18:35+09:00
https://crieit.net/posts/Nuxt-Firebase-Cloud-Messaging-FCM-Web
Nuxt+Firebase Cloud Messaging(FCM)でWebプッシュ通知を送る
<p>WebでもFCMが使えるようになったので、試してみたときの備忘録。<br />
これでSafari以外には、通知が送れるようになる(<em>´ω`</em>)</p>
<h3 id="使い方"><a href="#%E4%BD%BF%E3%81%84%E6%96%B9">使い方</a></h3>
<p>構成は、@nuxtjs/pwaでPWA化している感じ。</p>
<h4 id="コンソール側"><a href="#%E3%82%B3%E3%83%B3%E3%82%BD%E3%83%BC%E3%83%AB%E5%81%B4">コンソール側</a></h4>
<p>Settingsでウェブプッシュ証明書を作成して、鍵ペアを取得する。</p>
<p><img width="973" alt="スクリーンショット_2020-05-24_17_06_33.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/d17f3a0e-e6df-73a7-d57b-a6ff36e7c79f.png"></p>
<p>これを、PUBLIC_VAPID_KEYという環境変数に設定しておく。</p>
<h4 id="クライアント側"><a href="#%E3%82%AF%E3%83%A9%E3%82%A4%E3%82%A2%E3%83%B3%E3%83%88%E5%81%B4">クライアント側</a></h4>
<h5 id="firebaseの初期化"><a href="#firebase%E3%81%AE%E5%88%9D%E6%9C%9F%E5%8C%96">firebaseの初期化</a></h5>
<p><code>~/plugins/firebase.ts</code>というファイルを用意し、firebaseを初期化</p>
<pre><code class="typescript">// ~/plugins/firebase.ts
import * as firebase from "firebase/app";
import "firebase/auth";
import "firebase/firestore";
import "firebase/messaging";
if (!firebase.apps.length) {
// まずは、firebaseの初期化
firebase.initializeApp({
apiKey: process.env.API_KEY,
authDomain: process.env.AUTH_DOMAIN,
databaseURL: process.env.DATABASE_URL,
projectId: process.env.PROJECT_ID,
storageBucket: process.env.STORAGE_BUCKET,
messagingSenderId: process.env.MESSAGING_SENDER_ID,
appId: process.env.APP_ID,
measurementId: process.env.MEASUREMENT_ID
});
// Push通知をサポートしているかをチェック
// サポートしていないと、firebase.messaging()を呼んだときに例外が発生
const isSupported = firebase.messaging.isSupported();
// コンソールで発行した、ウェブプッシュ証明書の鍵ペアを取得
const publicVapidKey = process.env.PUBLIC_VAPID_KEY;
if (!!publicVapidKey && process.client && !!isSupported) {
// FCMの初期化。鍵ペアを設定する
const messaging = firebase.messaging();
messaging.usePublicVapidKey(publicVapidKey);
// @nuxtjs/pwaが生成するsw.jsと、
// 後で作成するFCM受信処理用のsw-firebase-messaging.jsを統合するための設定
navigator.serviceWorker
.register("/sw.js")
.then(registration => messaging.useServiceWorker(registration))
.catch(err => console.error(err));
}
}
export default firebase;
</code></pre>
<h5 id="トークンの取得"><a href="#%E3%83%88%E3%83%BC%E3%82%AF%E3%83%B3%E3%81%AE%E5%8F%96%E5%BE%97">トークンの取得</a></h5>
<p>Push通知を送る際の宛先として、トークンを指定しないといけないので取得。<br />
トークンは端末ごとに取得する必要があるので、注意が必要。<br />
※PCとスマホだとそれぞれトークンが違う</p>
<pre><code class="typescript">import firebase from "~/plugins/firebase";
// トークンの取得
public async getToken(user: User) {
const isSupported = firebase.messaging.isSupported();
if (!isSupported) return;
const token = await firebase.messaging().getToken();
// firestoreにトークンを保存しておく処理(中身は略)
await saveToken(user, token);
}
</code></pre>
<p>Firestoreなどへユーザごとにトークンを保存しておく。</p>
<p><code>firebase.messaging().getToken();</code>を呼んだ際に、<br />
通知の設定が「確認」だと、許可を求めるダイアログが表示される。</p>
<p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/e9976414-8418-09c1-8846-25211a949776.png" alt="スクリーンショット 2020-05-24 16.30.23.png" /></p>
<p>これが許可されていないと、トークンも取得できない。</p>
<p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/c21078c8-38d0-04c7-133a-b5402611443d.png" alt="スクリーンショット 2020-05-24 16.30.35.png" /></p>
<h5 id="メッセージを受け取ったときの処理"><a href="#%E3%83%A1%E3%83%83%E3%82%BB%E3%83%BC%E3%82%B8%E3%82%92%E5%8F%97%E3%81%91%E5%8F%96%E3%81%A3%E3%81%9F%E3%81%A8%E3%81%8D%E3%81%AE%E5%87%A6%E7%90%86">メッセージを受け取ったときの処理</a></h5>
<p>メッセージを受信したときに受け取る関数は2つあり、</p>
<ul>
<li>フォアグラウンド(画面を見ている時) ... onMessage</li>
<li>バックグラウンド(画面を見ていない時) ... setBackgroundMessageHandler</li>
</ul>
<p>・【参考】<a target="_blank" rel="nofollow noopener" href="https://firebase.google.com/docs/cloud-messaging/js/receive#handle_messages_when_your_web_app_is_in_the_background">JavaScript クライアントでメッセージを受信する | Firebase</a></p>
<p>今回はバックグラウンドのときに通知を送りたいので、<br />
<code>setBackgroundMessageHandler</code>を設定していく。</p>
<p>こんな感じ。</p>
<p>ドキュメントを見ると、『<code>firebase-messaging-sw.js</code>というファイル名で作成』と書かれているけど、<br />
その名前にすると、自動で読み込まれてしまう。。</p>
<p>開発用と本番用で切り替えたいときなどもあるので、@nuxtjs/pwaが生成するsw.jsと統合できるように、<br />
<code>sw-firebase-messaging.js</code>という名前でファイルを作成しておく。</p>
<p>ファイル名を変更したので、上で書いている「firebaseの初期化」の部分で、<br />
<code>messaging.useServiceWorker(registration)</code>を呼んでいる形。</p>
<pre><code class="javascript">// ~/static/sw-firebase-messaging.js
importScripts("https://www.gstatic.com/firebasejs/7.14.2/firebase-app.js");
importScripts("https://www.gstatic.com/firebasejs/7.14.2/firebase-messaging.js");
// Firebaseの初期化
firebase.initializeApp({
apiKey: "...",
authDomain: "...",
databaseURL: "...",
projectId: "...",
storageBucket: "...",
messagingSenderId: "...",
appId: "...",
measurementId: "...",
});
// [START background_handler]
const isSupported = firebase.messaging.isSupported();
if (!!isSupported) {
const messaging = firebase.messaging();
// バックグラウンド時の処理
messaging.setBackgroundMessageHandler(function(payload) {
// 受け取ったFCMの内容を取得
const notificationTitle = payload.notification.title;
const notificationOptions = {
body: payload.notification.body,
icon: payload.notification.icon,
};
// 通知を作成する
return self.registration.showNotification(notificationTitle, notificationOptions);
});
}
// [END background_handler]
</code></pre>
<h5 id="nuxt.config.tsでPWA関連の設定をする"><a href="#nuxt.config.ts%E3%81%A7PWA%E9%96%A2%E9%80%A3%E3%81%AE%E8%A8%AD%E5%AE%9A%E3%82%92%E3%81%99%E3%82%8B">nuxt.config.tsでPWA関連の設定をする</a></h5>
<p>作成した<code>sw-firebase-messaging.js</code>を取り込む設定と、<br />
@nuxtjs/pwaが生成するmanifest.jsonに、gcm_sender_idを追加する設定を追加</p>
<pre><code class="typescript">import { Configuration } from "@nuxt/types";
const config: Configuration = {
// 略
modules: [
"@nuxtjs/pwa",
],
workbox: {
// sw-firebase-messaging.jsをimportするように追加
importScripts: [
"sw-firebase-messaging.js"
]
},
pwa: {
manifest: {
// manifest.jsonにgcm_sender_idを追加
gcm_sender_id: process.env.MESSAGING_SENDER_ID || ""
}
},
};
export default config;
</code></pre>
<p>これでクライアント側はOK!</p>
<h4 id="サーバ側: メッセージを送信する"><a href="#%E3%82%B5%E3%83%BC%E3%83%90%E5%81%B4%3A+%E3%83%A1%E3%83%83%E3%82%BB%E3%83%BC%E3%82%B8%E3%82%92%E9%80%81%E4%BF%A1%E3%81%99%E3%82%8B">サーバ側: メッセージを送信する</a></h4>
<p>メッセージの送信は、firebase-adminでできる。<br />
・<a target="_blank" rel="nofollow noopener" href="https://firebase.google.com/docs/cloud-messaging/send-message?hl=ja">アプリサーバーからの送信リクエストを作成する | Firebase</a></p>
<p>firestoreを利用しているので、Cloud Functionsのfirestoreトリガーを使い、<br />
ドキュメントが追加されたら通知するようにしている例。</p>
<pre><code class="typescript">import * as functions from "firebase-functions";
import admin from "../common/firebaseAdmin"; // 初期化済みのfirebase-admin
export default functions
.firestore.document("ドキュメントのパス")
.onCreate(async (snap, context) => {
// getTokenで保存しておいたトークンを取得(中身は略)
const token = getToken();
// 通知の送信
const title = "通知するタイトル";
const body = "通知する本文";
const icon = "通知で表示するアイコン画像のURL"
const link = "通知をタップしたときに開くURL"
await admin.messaging().send({
// 送信先の端末のトークン
token: token,
// 通知する内容
notification: {
title: title,
body: body
},
// Web Push向けの通知内容
webpush: {
notification: {
icon: icon
},
fcmOptions: {
link: link
}
}
});
});
</code></pre>
<p>送信はこれだけ!</p>
<h3 id="ほかの小ネタ"><a href="#%E3%81%BB%E3%81%8B%E3%81%AE%E5%B0%8F%E3%83%8D%E3%82%BF">ほかの小ネタ</a></h3>
<h4 id="トークンを削除する"><a href="#%E3%83%88%E3%83%BC%E3%82%AF%E3%83%B3%E3%82%92%E5%89%8A%E9%99%A4%E3%81%99%E3%82%8B">トークンを削除する</a></h4>
<p><code>firebase.messaging().deleteToken();</code>でトークンを無効化できる。</p>
<pre><code class="typescript">// トークンの削除
public async deleteToken(user: User) {
const isSupported = firebase.messaging.isSupported();
if (isSupported) {
const token = await firebase.messaging().getToken();
await firebase.messaging().deleteToken(token);
}
}
</code></pre>
<h4 id="フォアグラウンドで通知を受け取ったときになにかする"><a href="#%E3%83%95%E3%82%A9%E3%82%A2%E3%82%B0%E3%83%A9%E3%82%A6%E3%83%B3%E3%83%89%E3%81%A7%E9%80%9A%E7%9F%A5%E3%82%92%E5%8F%97%E3%81%91%E5%8F%96%E3%81%A3%E3%81%9F%E3%81%A8%E3%81%8D%E3%81%AB%E3%81%AA%E3%81%AB%E3%81%8B%E3%81%99%E3%82%8B">フォアグラウンドで通知を受け取ったときになにかする</a></h4>
<p><code>onMessage</code>を使うと、通知を受け取ったときに呼び出してくれる。</p>
<pre><code class="typescript">firebase.messaging().onMessage(async (payload) => {
// 受け取ったときの処理
});
</code></pre>
<h4 id="トークンが変更されたときになにかする"><a href="#%E3%83%88%E3%83%BC%E3%82%AF%E3%83%B3%E3%81%8C%E5%A4%89%E6%9B%B4%E3%81%95%E3%82%8C%E3%81%9F%E3%81%A8%E3%81%8D%E3%81%AB%E3%81%AA%E3%81%AB%E3%81%8B%E3%81%99%E3%82%8B">トークンが変更されたときになにかする</a></h4>
<p><code>onTokenRefresh</code>を使うと、トークンが更新されたときに呼び出してくれる。<br />
新しいトークンは再度<code>getToken()</code>を呼ばないといけない。</p>
<pre><code class="typescript">firebase.messaging().onTokenRefresh(async () => {
// トークンが更新されたときの処理
});
</code></pre>
<h4 id="通知の許可状態を確認する"><a href="#%E9%80%9A%E7%9F%A5%E3%81%AE%E8%A8%B1%E5%8F%AF%E7%8A%B6%E6%85%8B%E3%82%92%E7%A2%BA%E8%AA%8D%E3%81%99%E3%82%8B">通知の許可状態を確認する</a></h4>
<p>通知の状態は、<code>Notification.permission</code>で確認できるらしい。<br />
・<a target="_blank" rel="nofollow noopener" href="https://developer.mozilla.org/ja/docs/Web/API/notification/permission">Notification.permission - Web API | MDN</a></p>
<pre><code class="typescript">if (Notification.permission === "default") {
// 確認(デフォルト)
} else if (Notification.permission === "granted") {
// 許可
} else if (Notification.permission === "denied") {
// 拒否
}
</code></pre>
<p>以上!!</p>
<h3 id="【PR】これをつかって、こんなのつくりました!"><a href="#%E3%80%90PR%E3%80%91%E3%81%93%E3%82%8C%E3%82%92%E3%81%A4%E3%81%8B%E3%81%A3%E3%81%A6%E3%80%81%E3%81%93%E3%82%93%E3%81%AA%E3%81%AE%E3%81%A4%E3%81%8F%E3%82%8A%E3%81%BE%E3%81%97%E3%81%9F%EF%BC%81">【PR】これをつかって、こんなのつくりました!</a></h3>
<p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/2f71c56a-0ab2-5910-f203-e1fca191ad69.png" alt="スクリーンショット 2020-05-24 12.44.28.png" /></p>
<p>こんな通知を受け取れます!<br />
<img width="359" alt="スクリーンショット_2020-05-24_13_34_17.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/1d4037bf-e662-a15a-4567-91f845ef5142.png"></p>
<p>1週間でWebサービスを作るイベント <a href="https://crieit.net/boards/web1week-202005">web1week</a>への投稿作品です!<br />
よかったら、遊んでみてください(<em>´ω`</em>)</p>
<p>■エアで投げ銭できるWebサービス「エア銭」<br />
URL: <a target="_blank" rel="nofollow noopener" href="https://air-money.netlify.app/">https://air-money.netlify.app/</a></p>
<h1 id="参考にしたサイトさま"><a href="#%E5%8F%82%E8%80%83%E3%81%AB%E3%81%97%E3%81%9F%E3%82%B5%E3%82%A4%E3%83%88%E3%81%95%E3%81%BE">参考にしたサイトさま</a></h1>
<ul>
<li><a target="_blank" rel="nofollow noopener" href="https://developers.cyberagent.co.jp/blog/archives/9662/">FRESH! における Web プッシュ通知機能 〜実装編〜 | CyberAgent Developers Blog</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://github.com/firebase/quickstart-js/blob/bcce38ebc1e5602560e2b76b20f19b7834b8279e/messaging/firebase-messaging-sw.js#L15-L37">quickstart-js/firebase-messaging-sw.js at bcce38ebc1e5602560e2b76b20f19b7834b8279e · firebase/quickstart-js</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://firebase.google.com/docs/reference/js/firebase.messaging.Messaging#deletetoken">Messaging | JavaScript SDK | Firebase</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://firebase.google.com/docs/cloud-messaging/js/send-multiple#%E3%83%88%E3%83%94%E3%83%83%E3%82%AF-http-post-%E3%83%AA%E3%82%AF%E3%82%A8%E3%82%B9%E3%83%88">複数のデバイスにメッセージを送信する | Firebase</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages#WebpushConfig">REST Resource: projects.messages | Firebase</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://firebase.google.com/docs/cloud-messaging/send-message">アプリサーバーからの送信リクエストを作成する | Firebase</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://firebase.google.com/docs/cloud-messaging/js/first-message">バックグラウンド アプリにテスト メッセージを送信する | Firebase</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://firebase.google.com/docs/cloud-messaging/js/client">JavaScript Firebase Cloud Messaging クライアント アプリを設定する</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/ryo_hisano/items/1171beca22d5a04ed802">Firebase Cloud Messagingで始めるWebプッシュ通知 - Qiita</a></li>
</ul>
きらぷか@積読ハウマッチ/SSSAPIなど
tag:crieit.net,2005:PublicArticle/15887
2020-05-06T15:52:28+09:00
2020-05-06T15:52:28+09:00
https://crieit.net/posts/Canvas-Konva-js-OGP-Nuxt
Canvasのライブラリ「Konva.js」でOGP生成(Nuxtアプリ)
<p>Nuxt.jsとFirebaseで作っていた<a target="_blank" rel="nofollow noopener" href="https://tsundoku.site">Webサービス</a>を7月末にリリースして、はや10ヶ月。。<br />
登録総額1億円を突破したので、記念企画として<a target="_blank" rel="nofollow noopener" href="https://tsundoku.site/review/">積読レビュー</a>機能をリリースしました!!</p>
<p>そこで<a target="_blank" rel="nofollow noopener" href="https://konvajs.org/">Konva.js</a>というライブラリを使ってOGP画像を生成したので、<br />
その時に調べたことをまとめてみました。</p>
<h3 id="積読・読書前でも書ける『積読レビュー』"><a href="#%E7%A9%8D%E8%AA%AD%E3%83%BB%E8%AA%AD%E6%9B%B8%E5%89%8D%E3%81%A7%E3%82%82%E6%9B%B8%E3%81%91%E3%82%8B%E3%80%8E%E7%A9%8D%E8%AA%AD%E3%83%AC%E3%83%93%E3%83%A5%E3%83%BC%E3%80%8F">積読・読書前でも書ける『積読レビュー』</a></h3>
<p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/fc0f21b7-ee49-f01c-34a1-0d44ed3022d2.png" width="600px"/></p>
<p>読書のレビューというと、読書後の感想を書くものですが、<br />
積ん読が多いとなかなか書く機会がないです。。</p>
<p><strong>「感想は読書後だけではないのでは?」</strong><br />
ということで、読書前でも書けるレビューを2つ用意してみました。</p>
<h4 id="妄想で書く『妄想レビュー』"><a href="#%E5%A6%84%E6%83%B3%E3%81%A7%E6%9B%B8%E3%81%8F%E3%80%8E%E5%A6%84%E6%83%B3%E3%83%AC%E3%83%93%E3%83%A5%E3%83%BC%E3%80%8F">妄想で書く『妄想レビュー』</a></h4>
<p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/8e8a9968-c44f-64c8-93d2-0f8eb1f3172a.png" width="600px"/></p>
<p><strong>まだ読んでないけど、本の表紙や帯の印象から妄想で書いてみる『妄想レビュー』</strong></p>
<p>「中身じゃなく外観だけで、読書後のレビューを書いてみるとどうだろう。。?」<br />
というネタ的なレビューです。</p>
<p>いかに読んでる醸し出せるかをチャレンジしてみるとおもしろいかも(<em>´ω`</em>)</p>
<p>他の使い方としては、<strong>読む前に書いておいて、読んだ後との感想を比較してみる</strong>のもたのしいかも?<br />
読書前後で感想が同じになっても違っていても、新たな発見があるかもです。</p>
<h4 id="きっかけを書く『きっかけレビュー』"><a href="#%E3%81%8D%E3%81%A3%E3%81%8B%E3%81%91%E3%82%92%E6%9B%B8%E3%81%8F%E3%80%8E%E3%81%8D%E3%81%A3%E3%81%8B%E3%81%91%E3%83%AC%E3%83%93%E3%83%A5%E3%83%BC%E3%80%8F">きっかけを書く『きっかけレビュー』</a></h4>
<p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/e740910f-2687-0484-e3d2-bebdd0d95bf6.png" width="600px"/></p>
<p><strong>買ったときのきっかけや意気込み・ワクワク感を書いてみる『きっかけレビュー』</strong></p>
<p>「『なぜ買うのか、どうして買いたいのか』という気持ちを残しておくのもどうだろう。。?」<br />
と思いつきつくってみました。</p>
<p>買ったときの気持ちも大事で、<br />
* 技術書やビジネス書なら「こうなりたい!ここを強くしたい!」<br />
* 小説やマンガなら「ひさびさの新刊!たのしみ!」<br />
という思いがあるはず・・</p>
<p><strong>それを記録として残しておくと、いつか見返したときにたのしいかもしれません。</strong></p>
<h2 id="なぜKonva.js?"><a href="#%E3%81%AA%E3%81%9CKonva.js%EF%BC%9F">なぜKonva.js?</a></h2>
<p>この企画では、本を選んで、レビューを書いていくのですが、<br />
本の表紙など画像を埋め込む必要があります。</p>
<p>以前、<a target="_blank" rel="nofollow noopener" href="http://hen-ai.net/">別のサービス</a>で、<a target="_blank" rel="nofollow noopener" href="https://www.memory-lovers.blog/entry/2020/01/24/110000">SVGでOGP用の画像を生成</a>してみたのですが、端末によってはうまくいかなかったりと、画像を埋め込むのがかなり大変でした。。</p>
<p>なので、SVGではなく、Canvasで試してみようと調べたところ、<br />
<a target="_blank" rel="nofollow noopener" href="https://konvajs.org/">Konva.js</a>と<a target="_blank" rel="nofollow noopener" href="http://fabricjs.com/">Fabric.js</a>が見つけ、<br />
文字の折返しなどのガイドがあった<a target="_blank" rel="nofollow noopener" href="https://konvajs.org/">Konva.js</a>を選んでみました。</p>
<h2 id="Konvaで画像生成"><a href="#Konva%E3%81%A7%E7%94%BB%E5%83%8F%E7%94%9F%E6%88%90">Konvaで画像生成</a></h2>
<p>ここからが本題。やっと本題。。</p>
<h3 id="準備"><a href="#%E6%BA%96%E5%82%99">準備</a></h3>
<h4 id="インストール"><a href="#%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB">インストール</a></h4>
<pre><code class="shell">$ npm install vue-konva konva --save
</code></pre>
<p>今回は、Nuxt/Vueで使うので、KonvaのVueライブラリ(<a target="_blank" rel="nofollow noopener" href="https://konvajs.org/docs/vue/index.html">vue-konva</a>)もインストール</p>
<h4 id="プラグインの作成"><a href="#%E3%83%97%E3%83%A9%E3%82%B0%E3%82%A4%E3%83%B3%E3%81%AE%E4%BD%9C%E6%88%90">プラグインの作成</a></h4>
<p>vue-konvaを使えるようプラグインを作成。</p>
<pre><code class="ts">// ~/plugins/vue-konva.ts
import Vue from "vue";
import VueKonva from "vue-konva";
Vue.use(VueKonva);
</code></pre>
<p>そして、作ったプラグインをnuxt.config.tsに追加。</p>
<pre><code class="ts">// nuxt.config.js
import { Configuration } from "@nuxt/types";
const config: Configuration = {
mode: "spa",
// 略
plugins: [
{ src: "~/plugins/vue-konva", ssr: false },
// 略
],
// 略
};
</code></pre>
<p>これで準備はOK</p>
<h3 id="KonvaでCanvasを描いてみる"><a href="#Konva%E3%81%A7Canvas%E3%82%92%E6%8F%8F%E3%81%84%E3%81%A6%E3%81%BF%E3%82%8B">KonvaでCanvasを描いてみる</a></h3>
<h4 id="画像を表示する(v-image)"><a href="#%E7%94%BB%E5%83%8F%E3%82%92%E8%A1%A8%E7%A4%BA%E3%81%99%E3%82%8B%28v-image%29">画像を表示する(v-image)</a></h4>
<p>まずは、フレームとなる以下の背景画像を表示してみます。</p>
<p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/9033133e-55f5-17eb-a8bc-85ce000a68ef.png" width="600px"/></p>
<pre><code class="html"><template>
<div>
<v-stage :config="configKonva">
<v-layer>
<v-image :config="configBg"></v-image>
</v-layer>
</v-stage>
</div>
</template>
<script lang="ts">
import { Component, Vue, Prop, Watch } from "nuxt-property-decorator";
const IMAGE_WIDTH = 1200;
const IMAGE_HEIGHT = 630;
@Component
export default class ReviewPreviewSample extends Vue {
// konva全体の設定(v-stageに対応)
private configKonva = { width: IMAGE_WIDTH, height: IMAGE_HEIGHT };
// 背景画像の設定(v-imageに対応)
private configBg: { image: HTMLImageElement | null } = { image: null };
async mounted() {
// マウントされたら、背景画像を読み込んで設定する
this.setupBg().then();
}
// 背景画像の設定
private async setupBg() {
const image = await this.getImage("/img/review_paple.png");
this.configBg.image = image;
}
// 共通処理: 画像の読み込み
private async getImage(src: string): Promise<HTMLImageElement> {
return new Promise((resolve, reject) => {
const image = new window.Image();
image.onload = () => resolve(image);
image.src = src;
});
}
}
</script>
</code></pre>
<p>こんな感じ。</p>
<h5 id="基本的な構成"><a href="#%E5%9F%BA%E6%9C%AC%E7%9A%84%E3%81%AA%E6%A7%8B%E6%88%90">基本的な構成</a></h5>
<p>基本的な構成は、こんな感じで、<br />
<code>v-stage > v-layer > その他諸々</code><br />
で配置していく。</p>
<pre><code class="html"><v-stage :config="configKonva">
<v-layer>
<v-image :config="configBg"></v-image>
</v-layer>
</v-stage>
</code></pre>
<p>必ずrootが<code>v-stage</code>で、その直下が<code>v-layer</code>。<br />
<code>v-layer</code>はいくつも作れるけど、3〜4までがよいっぽい。(waringがでた)</p>
<p>位置やサイズ、表示する画像は<code>:config</code>を介して、Kanva.js自体の設定値を付与していく。</p>
<p>どんな設定値があるかは、<a target="_blank" rel="nofollow noopener" href="https://konvajs.org/api/Konva.html">ApiDoc</a>を見ていく感じで、<br />
Imageだと<a target="_blank" rel="nofollow noopener" href="https://konvajs.org/api/Konva.Image.html">ここ(Class: Image)をみる</a></p>
<p><a target="_blank" rel="nofollow noopener" href="https://konvajs.org/docs/">チュートリアル</a>もあるので、それを見つつ、<br />
具体的な設定値は<a target="_blank" rel="nofollow noopener" href="https://konvajs.org/api/Konva.html">ApiDoc</a>参照という流れで進めた。</p>
<h5 id="画像の表示には読み込みが必要"><a href="#%E7%94%BB%E5%83%8F%E3%81%AE%E8%A1%A8%E7%A4%BA%E3%81%AB%E3%81%AF%E8%AA%AD%E3%81%BF%E8%BE%BC%E3%81%BF%E3%81%8C%E5%BF%85%E8%A6%81">画像の表示には読み込みが必要</a></h5>
<p>画像の表示は少しめんどくさくて、</p>
<ol>
<li>HTMLImageElementをつくって</li>
<li>HTMLImageElementで画像をロードして</li>
<li>ロード後のHTMLImageElementをconfigに設定</li>
</ol>
<p>という流れ。。</p>
<p>それをしているのがこのあたり。<br />
画像読み込みは何度も使うけど、Promiseじゃないので、<br />
ロードが終わったらHTMLImageElementを返す処理を共通化</p>
<pre><code class="ts">async mounted() {
// マウントされたら、背景画像を読み込んで設定する
this.setupBg().then();
}
// 背景画像の設定
private async setupBg() {
const image = await this.getImage("/img/review_paple.png");
this.configBg.image = image;
}
// 共通処理: 画像の読み込み
private async getImage(src: string): Promise<HTMLImageElement> {
return new Promise((resolve, reject) => {
const image = new window.Image();
image.onload = () => resolve(image);
image.src = src;
});
}
</code></pre>
<p>これでできるのがこんな感じ。</p>
<p><img width="600px" alt="スクリーンショット 2020-05-06 12.46.54.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/b295f2ff-7b56-5ab4-1e9c-055ef7ff4314.png"></p>
<p>でかい。。<br />
CanvasのサイズをOGP画像のサイズ(1200x630)にしているもんね。。</p>
<h5 id="画像を画面サイズにあわせる"><a href="#%E7%94%BB%E5%83%8F%E3%82%92%E7%94%BB%E9%9D%A2%E3%82%B5%E3%82%A4%E3%82%BA%E3%81%AB%E3%81%82%E3%82%8F%E3%81%9B%E3%82%8B">画像を画面サイズにあわせる</a></h5>
<p>ほんとはこんな感じにしたい。。</p>
<p><img width="600px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/e3210e3f-c4b0-6e6c-1a11-e791dc90dd05.gif" /></p>
<p>ので、Windowサイズの変更を検知して、CSSのscaleで調整してみる。</p>
<p>こんな感じ。</p>
<pre><code class="html"><template>
<!-- サイズ計算用にidが必要なので追加 -->
<div id="preview-wrapper" class="preview-wrapper">
<v-stage :config="configKonva" class="preview-content" :style="style">
<v-layer>
<v-image :config="configBg"></v-image>
</v-layer>
</v-stage>
</div>
</template>
<script lang="ts">
import { Component, Vue, Prop, Watch } from "nuxt-property-decorator";
const IMAGE_WIDTH = 1200;
const IMAGE_HEIGHT = 630;
@Component
export default class ReviewPreviewSample extends Vue {
private configKonva = { width: IMAGE_WIDTH, height: IMAGE_HEIGHT };
private configBg: { image: HTMLImageElement | null } = { image: null };
private scale: number = 0;
async mounted() {
this.setupBg().then();
// 初回のスケール計算処理を走らせる
this.$nextTick(() => this.handleResize());
// イベントリスナーにスケール計算処理を登録
window.addEventListener("resize", this.handleResize);
}
beforeDestroy() {
// Destroy前に解放
window.removeEventListener("resize", this.handleResize);
}
private get style() {
// transformのscaleを使って、サイズ調整
return {
transform: `scale(${this.scale})`,
"-webkit-transform": `scale(${this.scale})`,
"transform-origin": "0 0"
};
}
// スケール計算用の処理: 対象IDのサイズを取得して、scaleを計算
private handleResize() {
const elm = document.getElementById("preview-wrapper");
if (!elm) return;
const rect = elm.getBoundingClientRect();
this.scale = rect.width / IMAGE_WIDTH;
}
private async setupBg() {
// 略
}
private async getImage(src: string): Promise<HTMLImageElement> {
// 略
}
}
</script>
<style lang="scss" scoped>
/* 画像のアスペクト比は固定なので、あらかじめ高さをCSSで調整 */
.preview-wrapper {
position: relative;
width: 100%;
height: auto;
}
.preview-wrapper {
&:before {
content: "";
display: block;
padding-top: 52.5%; /* 630 / 1200 x 100 */
}
}
.preview-content {
position: absolute;
top: 0;
left: 0;
}
</style>
</code></pre>
<p>これでOK。Konva.jsの話に戻る</p>
<h4 id="画像を加工する(サイズ変更)"><a href="#%E7%94%BB%E5%83%8F%E3%82%92%E5%8A%A0%E5%B7%A5%E3%81%99%E3%82%8B%28%E3%82%B5%E3%82%A4%E3%82%BA%E5%A4%89%E6%9B%B4%29">画像を加工する(サイズ変更)</a></h4>
<p>次は書影を読み込んで、配置する部分。<br />
書影は本ごとにサイズが違うので、加工が必要。。</p>
<p><img width="600" alt="スクリーンショット_2020-05-06_13_06_55.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/733c10bc-58e3-ffd4-45cd-09d344c9eb4f.png"></p>
<pre><code class="html"><template>
<div id="preview-wrapper" class="preview-wrapper">
<v-stage :config="configKonva" class="preview-content" :style="style">
<v-layer>
<v-image :config="configBg"></v-image>
</v-layer>
<v-layer>
<!-- 書影の画像 -->
<v-image :config="configBookImg"></v-image>
</v-layer>
</v-stage>
</div>
</template>
<script lang="ts">
import { Component, Vue, Prop, Watch } from "nuxt-property-decorator";
const IMAGE_WIDTH = 1200;
const IMAGE_HEIGHT = 630;
const BOOK_IMAGE_WIDTH = 464;
const BOOK_IMAGE_HEIGHT = 470;
@Component
export default class ReviewPreviewSample extends Vue {
// 略
private configBookImg: any | null = null;
async mounted() {
this.setupBg().then();
// 略
}
// 略
private async setupBookImage() {
// 画像の読み込み
const imageBook = await this.getImage("/img/notfound_paple.png");
// v-imageのconfig
const scale = BOOK_IMAGE_WIDTH / imageBook.width;
this.configBookImg = {
image: imageBook,
x: 48, // 左上からの位置(x)
y: 112, // 左上からの位置(y)
width: BOOK_IMAGE_WIDTH, // 画像のサイズ(横)
height: BOOK_IMAGE_HEIGHT, // 画像のサイズ(縦)
crop: { // 切り取り処理
x: 0, // 切り取り位置(x)
y: 0, // 切り取り位置(y)
width: imageBook.width, // 切り取りサイズ(横)
height: BOOK_IMAGE_HEIGHT / scale // 切り取りサイズ(縦)
}
};
}
// 略
}
</script>
<style lang="scss" scoped>
/* 略 */
</style>
</code></pre>
<p>こんな感じ。切り取りは、v-imageのcropを指定すればOK。</p>
<p>ここで若干ハマった。。</p>
<h5 id="【ハマり1】cropとscaleを同時に指定するとうまくいかない。。"><a href="#%E3%80%90%E3%83%8F%E3%83%9E%E3%82%8A1%E3%80%91crop%E3%81%A8scale%E3%82%92%E5%90%8C%E6%99%82%E3%81%AB%E6%8C%87%E5%AE%9A%E3%81%99%E3%82%8B%E3%81%A8%E3%81%86%E3%81%BE%E3%81%8F%E3%81%84%E3%81%8B%E3%81%AA%E3%81%84%E3%80%82%E3%80%82">【ハマり1】cropとscaleを同時に指定するとうまくいかない。。</a></h5>
<p>サイズを調整しようと、いろいろしていたときに、<br />
スケールを変化させる<code>scale</code>と切り取り処理の<code>crop</code>を同時に指定してみたけど、<br />
あまりうまくいかず、cropのみで対応した。。</p>
<h5 id="【ハマり2】computedだとうまくいかない。。"><a href="#%E3%80%90%E3%83%8F%E3%83%9E%E3%82%8A2%E3%80%91computed%E3%81%A0%E3%81%A8%E3%81%86%E3%81%BE%E3%81%8F%E3%81%84%E3%81%8B%E3%81%AA%E3%81%84%E3%80%82%E3%80%82">【ハマり2】computedだとうまくいかない。。</a></h5>
<p>最初は、以下のような感じで、computedにしてたけど、<br />
cropなど、配下のobjectが変わっても変更が反映されなかったので、<br />
dataのconfigBookImgを用意する形にしてみた。</p>
<pre><code class="ts">get configBookImg() {
// 画像の読み込み
const imageBook = await this.getImage("/img/notfound_paple.png");
// v-imageのconfig
const scale = BOOK_IMAGE_WIDTH / imageBook.width;
return {
image: imageBook,
x: 48, // 左上からの位置(x)
y: 112, // 左上からの位置(y)
width: BOOK_IMAGE_WIDTH, // 画像のサイズ(横)
height: BOOK_IMAGE_HEIGHT, // 画像のサイズ(縦)
crop: { // 切り取り処理
x: 0, // 切り取り位置(x)
y: 0, // 切り取り位置(y)
width: imageBook.width, // 切り取りサイズ(横)
height: BOOK_IMAGE_HEIGHT / scale // 切り取りサイズ(縦)
}
};
}
</code></pre>
<h4 id="画像を加工する(丸く切り取る)"><a href="#%E7%94%BB%E5%83%8F%E3%82%92%E5%8A%A0%E5%B7%A5%E3%81%99%E3%82%8B%28%E4%B8%B8%E3%81%8F%E5%88%87%E3%82%8A%E5%8F%96%E3%82%8B%29">画像を加工する(丸く切り取る)</a></h4>
<p>次はこれ。ユーザのアイコン画像。</p>
<p><img width="600" alt="スクリーンショット_2020-05-06_14_08_51.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/80f51e08-b4f4-5fd5-fa3e-35ef560b7197.png"></p>
<p>もともとは、こんな四角い画像だけど、アイコンっぽく丸く切り取りたい。</p>
<p><img width="200" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/62f0de4a-a307-82be-9755-a1a0963b9e2e.png" /></p>
<p><a target="_blank" rel="nofollow noopener" href="https://konvajs.org/docs/clipping/Clipping_Function.html">チュートリアル</a>を見てみると、<code>v-group</code>を使うとできるっぽい。</p>
<pre><code class="html"><template>
<div id="preview-wrapper" class="preview-wrapper">
<v-stage :config="configKonva" class="preview-content" :style="style">
<!-- 略 -->
<v-layer>
<v-group :config="configUserImageGroup">
<v-image :config="configUserImg"></v-image>
</v-group>
</v-layer>
</v-stage>
</div>
</template>
<script lang="ts">
import { Component, Vue, Prop, Watch } from "nuxt-property-decorator";
const IMAGE_WIDTH = 1200;
const IMAGE_HEIGHT = 630;
const BOOK_IMAGE_WIDTH = 464;
const BOOK_IMAGE_HEIGHT = 470;
const USER_IMAGE_SIZE = 76;
@Component
export default class ReviewPreviewSample extends Vue {
// 略
private configUserImg: any | null = null;
async mounted() {
// 略
this.setupUserImage().then();
// 略
}
// 略
private get configUserImageGroup() {
return {
// 複雑な切り取り処理: CanvasRenderingContext2Dのarcを使う
clipFunc: ctx => {
const r = USER_IMAGE_SIZE / 2;
ctx.arc(1092 + r, 538 + r, r, 0, Math.PI * 2, false);
},
// ドラッグを無効にする
draggable: false
};
}
// ユーザ画像の読み込み処理: 本の画像とかとだいたい同じ
private async setupUserImage() {
const image = await this.getImage("/img/kira_puka.png");
this.configUserImg = {
image: image,
x: 1092,
y: 538,
width: USER_IMAGE_SIZE,
height: USER_IMAGE_SIZE
};
}
// 略
}
</script>
<style lang="scss" scoped>
/* 略 */
</style>
</code></pre>
<p><code>v-group</code>の<code>clipFunc</code>を使うと、Canvaを操作できるようで、<br />
<a target="_blank" rel="nofollow noopener" href="https://developer.mozilla.org/ja/docs/Web/API/CanvasRenderingContext2D/arc">CanvasRenderingContext2Dのarc</a>を使って、丸く切り取り。</p>
<p>注意点は、原点がCanvas(v-stage)の左上であること。<br />
画像の左上が原点だと思っていたので、全然切り取られなかった。。</p>
<h4 id="1行の文字を表示する(省略あり)"><a href="#1%E8%A1%8C%E3%81%AE%E6%96%87%E5%AD%97%E3%82%92%E8%A1%A8%E7%A4%BA%E3%81%99%E3%82%8B%28%E7%9C%81%E7%95%A5%E3%81%82%E3%82%8A%29">1行の文字を表示する(省略あり)</a></h4>
<p>画像はここまで。次は文字。本のタイトルを表示したい。</p>
<p><img width="600" alt="スクリーンショット_2020-05-06_14_20_00.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/5042be5a-3901-780e-34eb-3616558ed1e8.png"></p>
<pre><code class="html"><template>
<div id="preview-wrapper" class="preview-wrapper">
<v-stage :config="configKonva" class="preview-content" :style="style">
<!-- 略 -->
<v-layer>
<v-text :config="configBookTitle"></v-text>
</v-layer>
</v-stage>
</div>
</template>
<script lang="ts">
import { Component, Vue, Prop, Watch } from "nuxt-property-decorator";
// 略
@Component
export default class ReviewPreviewSample extends Vue {
// 略
private get configBookTitle() {
return {
// 表示する文字
text: "選んだ本のタイトル 選んだ本のタイトル 選んだ本のタイトル 選んだ本のタイトル",
x: 200,
y: 28,
width: IMAGE_WIDTH - 400,
fill: "#FFFFFF", // 文字の色
fontSize: 36, // フォントサイズ
fontFamily: "Noto Sans JP", // フォントの種類
fontStyle: "bold", // フォントのスタイル
align: "center", // 揃え位置(中央揃え)
// 折り返しなし(wrap)を指定すると、省略(ellipsis)を設定可能に
wrap: "none",
ellipsis: true
};
}
// 略
}
</script>
<style lang="scss" scoped>
/* 略 */
</style>
</code></pre>
<p>文字を表示するには、<code>v-text</code>を使えばOK</p>
<p>CSSのtext-overflowと同じ感じで、折返しをnoneにすれば、省略記号の表示もできる。</p>
<h4 id="複数行の文字を表示する(省略不可)"><a href="#%E8%A4%87%E6%95%B0%E8%A1%8C%E3%81%AE%E6%96%87%E5%AD%97%E3%82%92%E8%A1%A8%E7%A4%BA%E3%81%99%E3%82%8B%28%E7%9C%81%E7%95%A5%E4%B8%8D%E5%8F%AF%29">複数行の文字を表示する(省略不可)</a></h4>
<p>次はレビューの文字を表示。</p>
<p><img width="600" alt="スクリーンショット_2020-05-06_14_30_22.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/75fc126e-1179-e8d1-a2ab-6aa1aaf44974.png"></p>
<pre><code class="html"><template>
<div id="preview-wrapper" class="preview-wrapper">
<v-stage :config="configKonva" class="preview-content" :style="style">
<!-- 略 -->
<v-layer>
<v-text :config="configReview"></v-text>
</v-layer>
</v-stage>
</div>
</template>
<script lang="ts">
import { Component, Vue, Prop, Watch } from "nuxt-property-decorator";
// 略
@Component
export default class ReviewPreviewSample extends Vue {
// 略
private get configReview() {
return {
text: "レビューの内容れびゅーのないよう\n\nレビューの内容れびゅーのないようレビューの内容れびゅーのないようレビューの内容れびゅーのないようレビューの内容れびゅーのないようレビューの内容れびゅーのないようレビューの内容れびゅーのないようレビューの内容れびゅーのないようレビューの内容れびゅーのないよう",
x: 596,
y: 188,
width: 542,
height: 270,
fill: "#443a33",
fontSize: 30,
fontFamily: "Noto Sans JP"
// 行の高さ
lineHeight: 1.2,
};
}
// 略
}
</script>
<style lang="scss" scoped>
/* 略 */
</style>
</code></pre>
<p>同じく<code>v-text</code>を使う感じ。<br />
<code>wrap</code>を指定しなければ、そのまま複数行に。</p>
<p>改行文字を入れれば改行もしてくれて、widthを指定すれば折返しも自動。</p>
<p>ただ、残念ながら、<code>-webkit-line-clamp</code>みたいに複数行の省略はないみたい。。</p>
<h3 id="Canvasを画像にエクスポート"><a href="#Canvas%E3%82%92%E7%94%BB%E5%83%8F%E3%81%AB%E3%82%A8%E3%82%AF%E3%82%B9%E3%83%9D%E3%83%BC%E3%83%88">Canvasを画像にエクスポート</a></h3>
<p>最後に作った画像をエクスポート!!</p>
<p><img width="500" alt="スクリーンショット_2020-05-06_14_41_16.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/57be76de-b8c2-3a34-31f8-5b85079b621c.png"></p>
<pre><code class="html"><template>
<div>
<div id="preview-wrapper" class="preview-wrapper">
<!-- v-stageが取得できるようにrefを追加-->
<v-stage
ref="stage"
:config="configKonva"
class="preview-content"
:style="style"
>
<!-- 略 -->
</v-stage>
</div>
<!-- ボタンを追加 -->
<div class="has-text-centered">
<b-button type="is-primary" size="is-medium" @click="download">
<span>作成する</span>
</b-button>
</div>
</div>
</template>
<script lang="ts">
import { Component, Vue, Prop, Watch } from "nuxt-property-decorator";
// 略
@Component
export default class ReviewPreviewSample extends Vue {
// 略
// ダウンロード処理
private async download() {
// stageの取得: 大きいサイズを取得するときは、'pixelRatio'で倍率を指定
const stage = (this.$refs.stage as any).getStage({ pixelRatio: 1 });
// stageからDataURLを取得
const dataURL = stage.toDataURL();
// ダウンロード処理: プログラムでリンクを作ってクリック
const link = document.createElement("a");
link.download = "ogp.png";
link.href = dataURL;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
}
</script>
<style lang="scss" scoped>
/** 略 **/
</style>
</code></pre>
<p>こんな感じ。Konvaでエクスポートするときには、<code>v-stage</code>から取得する。<br />
DataURL以外にJSONとかでもエクスポートできるので、保存したりもOK</p>
<ul>
<li><a target="_blank" rel="nofollow noopener" href="https://konvajs.org/docs/data_and_serialization/Stage_Data_URL.html">HTML5 Canvas Stage Data URL Tutorial | Konva - JavaScript 2d canvas library</a></li>
</ul>
<p>開発しているサービスでは、FirebaseのCloud Storageに保存しているので、DataURL形式でOK。</p>
<p>ハマリポイントは、外部URLの画像を使っているとき。。エクスポートに失敗する。。<br />
Cavans自体の仕様で外部URLの画像がある場合、汚染されたとみなされエラーになるとのこと。。</p>
<p>対処法は以下に書かれている感じで、CORS対応をすればOK<br />
・<a target="_blank" rel="nofollow noopener" href="https://konvajs.org/docs/posts/Tainted_Canvas.html">Resolving "Tainted canvases may not be exported" with Konva | Konva - JavaScript 2d canvas library</a></p>
<h2 id="おわり"><a href="#%E3%81%8A%E3%82%8F%E3%82%8A">おわり</a></h2>
<p>SVGでも外部URLは問題があったけど、Konva.jsでも。。<br />
でも、サクサクできたのと、ドラッグで位置を変えれるみたいなので、いろいろできそう(<em>´ω`</em>)</p>
<p>積ん読レビューはできたての機能なので、ぜひぜひさわってみてくださいヽ(=´▽`=)ノ<br />
■<a target="_blank" rel="nofollow noopener" href="https://tsundoku.site/review/">どっちでレビューする?妄想レビュ・きっかけレビュー</a></p>
<p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/fc0f21b7-ee49-f01c-34a1-0d44ed3022d2.png" width="600px"/></p>
きらぷか@積読ハウマッチ/SSSAPIなど
tag:crieit.net,2005:PublicArticle/15775
2020-03-20T13:51:56+09:00
2020-03-22T06:54:01+09:00
https://crieit.net/posts/Nuxt-5-Firebase-GAE-Netlify-Heroku
Nuxtアプリを無料で公開するときに試した5つの環境まとめ(Firebase/GAE/Netlify/Heroku)
<p>最近Nuxtでいろいろ作っているけど、無料で使える環境をいろいろ試してる。<br />
いろいろメリデメあるけど、SPAならNetlify/SSRならHerokuがよさそう。<br />
いままで試したものをまとめてみた。</p>
<h3 id="ほしかったもの"><a href="#%E3%81%BB%E3%81%97%E3%81%8B%E3%81%A3%E3%81%9F%E3%82%82%E3%81%AE">ほしかったもの</a></h3>
<p>主に開発してるのが<a target="_blank" rel="nofollow noopener" href="https://tsundoku.site">CGM系のWebサービス</a>なので、</p>
<ol>
<li>動的なOGP画像などが設定できる(OGP芸)</li>
<li>カスタムドメインが使える</li>
<li>日次のランキング集計などの定期実行ができる</li>
</ol>
<p>が、無料でできて、なるべく実装が楽で、そこまで遅くないのがうれしい。</p>
<h3 id="試した5つのパターン"><a href="#%E8%A9%A6%E3%81%97%E3%81%9F5%E3%81%A4%E3%81%AE%E3%83%91%E3%82%BF%E3%83%BC%E3%83%B3">試した5つのパターン</a></h3>
<p>試したのは以下の5パターン。試してみた順で記載。</p>
<ol>
<li>Nuxt(SSR) + Cloud Function<br />
起動がかなり遅かった。。実装も大変なのでNG</li>
<li>Nuxt(SPA) + Firebase Hosting<br />
構築はかなり楽。ただ、OGP芸が大変でFunctionsが必要</li>
<li>Nuxt(SPA) + Netlify<br />
プレレンダリングでOGP芸が楽。定期実行はFunctionsでできる</li>
<li>Nuxt(SSR) + GAE(f1:256M)<br />
メモリの制限きつく、たまに落ちる。。定期実行はcron.yamlでできる</li>
<li>Nuxt(SSR) + Heroku(free:512M) + Cloudflare<br />
メモリ多くてよい。SSLはないのでCloudflareを併用。定期実行はHeroku Scheduler</li>
</ol>
<p>SPAで十分であれば、「3.Nuxt(SPA) + Netlify」が構築も簡単で良かった。<br />
<a target="_blank" rel="nofollow noopener" href="https://tsundoku.site">積読ハウマッチ</a>は、現在この構成。</p>
<p>SSRの場合、「5. Nuxt(SSR) + Heroku(free:512M) + CloudFlare」が良い感じ。<br />
<a target="_blank" rel="nofollow noopener" href="https://hen-ai.net/">へんあいマップ</a>がこの構成。</p>
<h3 id="各パターンについて"><a href="#%E5%90%84%E3%83%91%E3%82%BF%E3%83%BC%E3%83%B3%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6">各パターンについて</a></h3>
<h4 id="1. Nuxt(SSR) + Cloud Function"><a href="#1.+Nuxt%28SSR%29+%2B+Cloud+Function">1. Nuxt(SSR) + Cloud Function</a></h4>
<p>一番はじめに<a target="_blank" rel="nofollow noopener" href="https://tsundoku.site">積読ハウマッチ</a>でやろうとした構成。</p>
<p><img width="870" alt="スクリーンショット 2020-03-20 11.43.40.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/7fa5a956-cec1-9117-1d5d-f769eae53a32.png"></p>
<p>細かいやり方は、以下の記事の「Firebase Cloud Functionsの設定してSSRできるようにする」あたりに。<br />
・<a target="_blank" rel="nofollow noopener" href="https://www.memory-lovers.blog/entry/2019/04/02/115149">Nuxt.jsではじめるときのやることリスト(SSRも国際化も自動デプロイも) - くらげになりたい。</a></p>
<p>当時は動的ページのSEO対応するには、この方法が多く検索に出ていたので試したけど、<br />
Cloud Functionのコールドスタートがつらすぎてお蔵入り。。SPAでいくことに。。</p>
<h4 id="2. Nuxt(SPA) + Firebase Hosting"><a href="#2.+Nuxt%28SPA%29+%2B+Firebase+Hosting">2. Nuxt(SPA) + Firebase Hosting</a></h4>
<p><a target="_blank" rel="nofollow noopener" href="https://tsundoku.site">積読ハウマッチ</a>をリリースしたときの構成。</p>
<p><img width="910" alt="スクリーンショット 2020-03-20 11.37.12.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/37459683-872a-5047-a383-71c33f0c640e.png"></p>
<p>課題だった動的ページのSEOは、Cloud Functionsを経由して返す方法で対応。<br />
こちらも当時よく検索に出てたけど、実装が大変そうだったので2番手に。</p>
<p>細かいやり方は、以下の記事に。(2020/03/22追記: OGP画像生成系の記事を追加)<br />
・<a target="_blank" rel="nofollow noopener" href="https://www.memory-lovers.blog/entry/2019/08/07/150000">Nuxt(SPA)+FirebaseでSEO!OGP!: 特定のパスだけheadだけ返すやつ - くらげになりたい。</a><br />
・<a target="_blank" rel="nofollow noopener" href="https://qiita.com/kira_puka/items/8c1d1240c3aa200cbec0">Nuxt.js(SPA)+Firebaseで積読用の読書管理サービスを作ってみたときにハマったこと - Qiita</a><br />
・<a target="_blank" rel="nofollow noopener" href="https://www.memory-lovers.blog/entry/2019/06/26/194500">Cloud Functions + ImageMagickでOPG画像の動的生成してCloud Storageにアップロードする - くらげになりたい。</a><br />
・<a target="_blank" rel="nofollow noopener" href="https://www.memory-lovers.blog/entry/2020/02/19/150000">Cloud FunctionとSVGでOGP画像生成を試行錯誤したまとめ - くらげになりたい。</a></p>
<p>カスタムドメインについては、Firebase HostingでSSLと合わせて無料で設定可能。</p>
<p>定期実行は、Cloud Functionsでできるのでそれを利用した。<br />
・<a target="_blank" rel="nofollow noopener" href="https://firebase.google.com/docs/functions/schedule-functions?hl=ja">関数のスケジュール設定 | Firebase</a><br />
・<a target="_blank" rel="nofollow noopener" href="https://qiita.com/kira_puka/items/1f164dd8d1a5a281d9c1">Cloud Functions for Firebaseのcronみたいな定期実行を試したら簡単だった - Qiita</a></p>
<p>ただ、Cloud Functionsの定期実行は、<br />
「<strong>Googleアカウントごとに3つのジョブ</strong>を無料で使用できる」<br />
なので、注意が必要。<strong>プロジェクトごとじゃない</strong>。。</p>
<h4 id="3. Nuxt(SPA) + Netlify"><a href="#3.+Nuxt%28SPA%29+%2B+Netlify">3. Nuxt(SPA) + Netlify</a></h4>
<p><a target="_blank" rel="nofollow noopener" href="https://tsundoku.site">積読ハウマッチ</a>の現在の構成。</p>
<p><img width="775" alt="スクリーンショット 2020-03-20 11.47.02.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/5010c13a-98a9-c9e0-ae3b-767d18cd8006.png"></p>
<p><a target="_blank" rel="nofollow noopener" href="https://docs.netlify.com/site-deploys/post-processing/prerendering/#set-up-prerendering">NetlifyのPrerendering</a>が無料化されて使えるようになり、よいという話を聞くように。。</p>
<p>実装が複雑で、変更もしにくかったので、この方法に。<br />
Netlify自体がCDNも提供していているので、すこしはやくなった(気がする)</p>
<p>カスタムドメインも、NetlifyのDNSを設定して無料で対応できる。</p>
<p>定期実行は、2.と同じ感じで、Cloud Functionsのまま。</p>
<p>コード量も減って変更もしやすくなったので、OGP画像の改善とかが楽にできるように。。(<em>´ω`</em>)</p>
<h4 id="4. Nuxt(SSR) + GAE(f1:256M)"><a href="#4.+Nuxt%28SSR%29+%2B+GAE%28f1%3A256M%29">4. Nuxt(SSR) + GAE(f1:256M)</a></h4>
<p>SPAの課題として、初期表示が遅いのでなんとかしたいなと、SSRの環境を模索しはじめ。。<br />
公開していないけど、1つ作ってみた。</p>
<p><img width="798" alt="スクリーンショット 2020-03-20 13.17.06.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/c24ce0b2-64b3-1ea4-9bd2-e2150ba13d14.png"></p>
<p>試してみたところ、起動するだけでメモリ上限すれすれで、複数アクセスがあったりすると、落ちる場合も。。<br />
(ts-nodeで動かしているのも悪い気がしている。。)</p>
<p><img width="561" alt="スクリーンショット 2020-03-20 12.22.00.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/e3940627-4141-4c17-3c82-b2ee13ca1e14.png"></p>
<p>ただ、定期実行はcron.yamlを用意すればURLに送信でき、無料範囲も広い。<br />
料金も「<a target="_blank" rel="nofollow noopener" href="https://cloud.google.com/appengine/pricing?hl=ja#%E3%81%9D%E3%81%AE%E4%BB%96%E3%81%AE%E3%83%AA%E3%82%BD%E3%83%BC%E3%82%B9">その他のリソース</a>」をみるとcronは無料のよう。<br />
・<a target="_blank" rel="nofollow noopener" href="https://cloud.google.com/appengine/docs/flexible/nodejs/scheduling-jobs-with-cron-yaml?hl=ja">cron.yaml を使用したジョブのスケジューリング</a></p>
<p>カスタムドメインやSSLも無料で利用できる感じ。(試してないので未確認)</p>
<h4 id="5. Nuxt(SSR) + Heroku(free:512M) + Cloudflare"><a href="#5.+Nuxt%28SSR%29+%2B+Heroku%28free%3A512M%29+%2B+Cloudflare">5. Nuxt(SSR) + Heroku(free:512M) + Cloudflare</a></h4>
<p>GAEでうまくいかなかったので、無料でメモリも多いHerokuを利用。<br />
<a target="_blank" rel="nofollow noopener" href="https://hen-ai.net/">へんあいマップ</a>がこの構成で稼働中。</p>
<p><img width="885" alt="スクリーンショット 2020-03-20 13.24.02.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/60f2ab5c-3f4a-9b4b-ac45-341ed99c4667.png"></p>
<p>定期実行は無料のアドオン(<a target="_blank" rel="nofollow noopener" href="https://elements.heroku.com/addons/scheduler">Heroku Scheduler</a>)が利用できる。<br />
Nuxt側で<a target="_blank" rel="nofollow noopener" href="https://ja.nuxtjs.org/api/configuration-servermiddleware/">serverMiddleware</a>を用意すればOK。</p>
<p>ただ、メモリは多く定期実行もが、Herokuの無料枠だと制限も多い。</p>
<ol>
<li>SSLは非対応</li>
<li>30分アクセスがないとスリープする</li>
<li>クレジットカードの登録で1000時間/月xアカウント<br />
(未認証だと550時間/月xアカウント)</li>
</ol>
<h5 id="1. SSLは非対応"><a href="#1.+SSL%E3%81%AF%E9%9D%9E%E5%AF%BE%E5%BF%9C">1. SSLは非対応</a></h5>
<p>単体だけだとSSLに対応していないので、<a target="_blank" rel="nofollow noopener" href="https://www.cloudflare.com/ja-jp/">Cloudflare</a>を併用。<br />
CloudflareがCDNも提供してくれるので良い感じに。</p>
<h5 id="2. 30分アクセスがないとスリープする"><a href="#2.+30%E5%88%86%E3%82%A2%E3%82%AF%E3%82%BB%E3%82%B9%E3%81%8C%E3%81%AA%E3%81%84%E3%81%A8%E3%82%B9%E3%83%AA%E3%83%BC%E3%83%97%E3%81%99%E3%82%8B">2. 30分アクセスがないとスリープする</a></h5>
<p>これは、Heroku Schedulerを使えばOK。<br />
任意のURLを叩けるため、15分毎などスリープしないようにしておく。</p>
<h5 id="3. 無料枠は1000時間/月xアカウント"><a href="#3.+%E7%84%A1%E6%96%99%E6%9E%A0%E3%81%AF1000%E6%99%82%E9%96%93%2F%E6%9C%88x%E3%82%A2%E3%82%AB%E3%82%A6%E3%83%B3%E3%83%88">3. 無料枠は1000時間/月xアカウント</a></h5>
<p>1000時間あれば、24時間x31日でも744時間なので、大丈夫な感じ。<br />
ただ、アカウント単位での無料枠なので、複数アプリを無料で稼働はできない。。</p>
<h5 id="実際のやりかたとかは、"><a href="#%E5%AE%9F%E9%9A%9B%E3%81%AE%E3%82%84%E3%82%8A%E3%81%8B%E3%81%9F%E3%81%A8%E3%81%8B%E3%81%AF%E3%80%81">実際のやりかたとかは、</a></h5>
<p>以下の記事に。<br />
・<a target="_blank" rel="nofollow noopener" href="https://www.memory-lovers.blog/entry/2020/01/25/120000">Heroku+CloudflareなNuxtでSSRしてみる - くらげになりたい。</a></p>
<h3 id="まとめ"><a href="#%E3%81%BE%E3%81%A8%E3%82%81">まとめ</a></h3>
<p>いろいろ試してみたけど、Nuxt.jsを無料で公開するときは、</p>
<ul>
<li>SPAならNetlify
<ul>
<li>prerenderingで動的ページのSEOも対応</li>
<li>カスタムドメインやSSLもNetlifyでOK</li>
<li>定期実行はCloud Functionsで。</li>
<li>ただし、定期実行は1アカウントにつき、3ジョブまで</li>
</ul></li>
<li>SSRならHeroku
<ul>
<li>SSRで動的ページのSEOも対応</li>
<li>定期実行はHeroku Schedulerで</li>
<li>Cloudflareを併用し、SSL対応</li>
<li>無料枠は1アカウントにつき、1アプリが限界かも</li>
</ul></li>
</ul>
<p>SSRの方は他に<a target="_blank" rel="nofollow noopener" href="https://cloud.google.com/run?hl=ja">Cloud Run</a>や<a target="_blank" rel="nofollow noopener" href="https://zeit.co/">ZEIT now</a>がある。<br />
Cloud Runは立ち上げがGAEより遅いときので後手だけど、<br />
ZEIT nowは気になってるので試してみたい(<em>´ω`</em>)</p>
<p>以上!!</p>
<h2 id="こんなのつくってます!!"><a href="#%E3%81%93%E3%82%93%E3%81%AA%E3%81%AE%E3%81%A4%E3%81%8F%E3%81%A3%E3%81%A6%E3%81%BE%E3%81%99%21%21">こんなのつくってます!!</a></h2>
<p>積読用の読書管理アプリ 『積読ハウマッチ』をリリースしました!<br />
<a target="_blank" rel="nofollow noopener" href="https://tsundoku.site">積読ハウマッチ</a>は、Nuxt.js+Firebaseで開発してます!</p>
<p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/572d4947-f40b-e4dc-1c9c-bc584cd2a66c.png" width="200"/></p>
<p>もしよかったら、遊んでみてくださいヽ(=´▽`=)ノ</p>
<p>要望・感想・アドバイスなどあれば、<br />
公式アカウント(<a target="_blank" rel="nofollow noopener" href="https://twitter.com/MemoryLoverz">@MemoryLoverz</a>)や開発者(<a target="_blank" rel="nofollow noopener" href="https://twitter.com/kira_puka">@kira_puka</a>)まで!</p>
<h3 id="へんあいマップもリリースしました!"><a href="#%E3%81%B8%E3%82%93%E3%81%82%E3%81%84%E3%83%9E%E3%83%83%E3%83%97%E3%82%82%E3%83%AA%E3%83%AA%E3%83%BC%E3%82%B9%E3%81%97%E3%81%BE%E3%81%97%E3%81%9F%EF%BC%81">へんあいマップもリリースしました!</a></h3>
<p>「偏愛マップ」を簡単に作れるWebアプリです!<br />
<a target="_blank" rel="nofollow noopener" href="https://hen-ai.net">へんあいマップ</a>も、Nuxt.js+Firebaseで開発してます!</p>
<p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/63686f5c-390b-0b09-3f9a-0ce6676c3cce.png" width="600"/></p>
<p>偏愛マップは人見知りや口下手な人にも優しいコミュニケーションツールで、<br />
勉強会、懇親会、オフ会などの余興・アイスブレイクや自分のプロフィールにも!</p>
<p>よかったら遊んでみてください(<em>´ω`</em>)</p>
きらぷか@積読ハウマッチ/SSSAPIなど
tag:crieit.net,2005:PublicArticle/15771
2020-03-16T14:04:14+09:00
2020-03-16T14:04:14+09:00
https://crieit.net/posts/Flutter-Navigator-PageRouteBuilder
Flutterで画面遷移するときのいろいろ(Navigator / PageRouteBuilder)
<p>FlutterでWebアプリを作ってみたときに調べたときの備忘録。<br />
画面遷移まわりをまとめてみた。</p>
<h4 id="複数画面をルーティングする"><a href="#%E8%A4%87%E6%95%B0%E7%94%BB%E9%9D%A2%E3%82%92%E3%83%AB%E3%83%BC%E3%83%86%E3%82%A3%E3%83%B3%E3%82%B0%E3%81%99%E3%82%8B">複数画面をルーティングする</a></h4>
<ul>
<li><a target="_blank" rel="nofollow noopener" href="https://flutter.dev/docs/cookbook/navigation/named-routes">Navigate with named routes - Flutter</a></li>
</ul>
<p>ルーティングは、こんな感じででAppに設定する。</p>
<pre><code class="dart">void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(primarySwatch: Colors.blue),
// デフォルトのルーティング
initialRoute: '/',
// ルーティングの一覧を設定
routes: {
'/': (context) => TopPage(title: "TopPage"),
'/second': (context) => SecondPage(title: "SecondPage"),
},
);
}
}
</code></pre>
<p>Navigator.pushNamedを使うと、パスで遷移できる</p>
<pre><code class="dart">// Within the `FirstScreen` widget
onPressed: () {
// Navigate to the second screen using a named route.
Navigator.pushNamed(context, '/second');
}
</code></pre>
<p>戻るときは、popでできる。</p>
<pre><code class="dart">// Within the SecondScreen widget
onPressed: () {
// Navigate back to the first screen by popping the current route off the stack.
Navigator.pop(context);
}
</code></pre>
<h4 id="named routeでパラメタを渡す"><a href="#named+route%E3%81%A7%E3%83%91%E3%83%A9%E3%83%A1%E3%82%BF%E3%82%92%E6%B8%A1%E3%81%99">named routeでパラメタを渡す</a></h4>
<ul>
<li><a target="_blank" rel="nofollow noopener" href="https://flutter.dev/docs/cookbook/navigation/navigate-with-arguments">Pass arguments to a named route - Flutter</a></li>
</ul>
<p>オブジェクトで渡すことができるよう。<br />
まずは、引数のクラスを作成。</p>
<pre><code class="dart">class ScreenArguments {
final String title;
final String message;
ScreenArguments(this.title, this.message);
}
</code></pre>
<p>引数を受け取るクラスはこんな感じ。<br />
<code>ModalRoute.of(context).settings.arguments;</code>で取得する。</p>
<pre><code class="dart">class ExtractArgumentsScreen extends StatelessWidget {
static const routeName = '/extractArguments';
@override
Widget build(BuildContext context) {
// 引数の受け取り
final ScreenArguments args = ModalRoute.of(context).settings.arguments;
return Scaffold(/* 略 */);
}
}
</code></pre>
<p>引数ありで画面を呼び出すのはこんな感じ。</p>
<pre><code class="dart">onPressed: () {
// argumentsに渡したい引数オブジェクトを設定する
Navigator.pushNamed(
context,
ExtractArgumentsScreen.routeName,
arguments: ScreenArguments(
'Extract Arguments Screen',
'This message is extracted in the build method.',
),
);
},
</code></pre>
<h4 id="画面遷移の操作いろいろ"><a href="#%E7%94%BB%E9%9D%A2%E9%81%B7%E7%A7%BB%E3%81%AE%E6%93%8D%E4%BD%9C%E3%81%84%E3%82%8D%E3%81%84%E3%82%8D">画面遷移の操作いろいろ</a></h4>
<p>画面遷移のヒストリを操作する処理はいろいろある。</p>
<ul>
<li>進む(push)
<ul>
<li>push / pushNamed ... 進む</li>
</ul></li>
<li>戻る(pop)
<ul>
<li>pop ... 戻る</li>
<li>popUntil .. 指定Routeまで戻る</li>
</ul></li>
<li>組み合わせ
<ul>
<li>popAndPushNamed ... popしてpushする</li>
</ul></li>
<li>置き換え系(replace)
<ul>
<li>replace ... 指定したRouteを置き換え</li>
<li>replaceRouteBelow .. 指定したRouteの一つ前を置き換え</li>
<li>pushReplacement / pushReplacementNamed ... 現在のRouteを置き換え</li>
</ul></li>
<li>削除系(remove)
<ul>
<li>removeRoute ... 指定したRouteを削除</li>
<li>removeRouteBelow ... 指定したRouteの一つ前を削除</li>
<li>pushAndRemoveUntil / pushNamedAndRemoveUntil ... pushしつつ、指定条件を満たすRouteを削除</li>
</ul></li>
</ul>
<p>一気に戻ったり、置き換えたりするときに使うことがある。</p>
<h4 id="画面遷移時のアニメーション"><a href="#%E7%94%BB%E9%9D%A2%E9%81%B7%E7%A7%BB%E6%99%82%E3%81%AE%E3%82%A2%E3%83%8B%E3%83%A1%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3">画面遷移時のアニメーション</a></h4>
<p>画面遷移するときは、こんな感じだけど、</p>
<pre><code class="dart">Navigator.pushNamed(
context,
'/second',
arguments=ScreenArguments("title", "message"),
);
</code></pre>
<p><code>MaterialApp</code>を使ってる場合は<code>MaterialPageRoute</code>を使ってこんな感じになってる。</p>
<pre><code class="dart">Navigator.push(
context,
MaterialPageRoute(
settings: RouteSettings(
name: '/second',
arguments: ScreenArguments("title", "message"),
),
builder: (context) => SecondPage(),
),
);
</code></pre>
<p>画面遷移だけ、iOSっぽくしたい場合は、<code>CupertinoPageRoute</code>に変えればOK</p>
<pre><code class="dart">Navigator.push(
context,
CupertinoPageRoute(
settings: RouteSettings(
name: '/second',
arguments: ScreenArguments("title", "message"),
),
builder: (context) => SecondPage(),
),
);
</code></pre>
<p>このPageRouteをカスタマイズすれば、画面遷移のアニメーションを変えられる。<br />
<code>PageRouteBuilder</code>を継承すると作成できるらしい。</p>
<p><a target="_blank" rel="nofollow noopener" href="https://itome.team/blog/2019/12/flutter-advent-calendar-day10/">この記事</a>で紹介されているスライドで遷移する例はこんな感じ。</p>
<pre><code class="dart">class SlidePageRoute extends PageRouteBuilder {
final Widget page;
final RouteSettings settings;
SlidePageRoute({this.page, this.settings})
: super(
pageBuilder: (
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
) {
return page;
},
transitionsBuilder: (
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget page,
) {
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(1, 0),
end: Offset.zero,
).animate(animation),
child: page,
);
},
);
}
</code></pre>
<p>呼び出すときはこんな感じに。<code>builder</code>から<code>page</code>に変わるので注意。</p>
<pre><code class="dart">Navigator.push(
context,
SlidePageRoute(
page: SecondPage()
settings: RouteSettings(
name: '/second',
arguments: ScreenArguments("title", "message"),
),
),
);
</code></pre>
<p>以下のサイトにいろんなアニメーション/トランジションの例があるので、<br />
参考にPageRouteBuilderを作っていけば、いろいろできる。</p>
<ul>
<li><a target="_blank" rel="nofollow noopener" href="https://medium.com/flutter-community/everything-you-need-to-know-about-flutter-page-route-transition-9ef5c1b32823">Everything you need to know about Flutter page route transition</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://itome.team/blog/2019/12/flutter-advent-calendar-day10/">FlutterのNavigationとRoutingを理解する</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://medium.com/@agungsurya/create-custom-router-transition-in-flutter-using-pageroutebuilder-73a1a9c4a171">Create Custom Router Transition in Flutter using PageRouteBuilder</a></li>
</ul>
<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://api.flutter.dev/flutter/widgets/Navigator-class.html">Navigator class - widgets library - Dart API</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://flutter.dev/docs/cookbook/navigation">Cookbook Navigation - Flutter</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://itome.team/blog/2019/12/flutter-advent-calendar-day10/">FlutterのNavigationとRoutingを理解する</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://medium.com/flutter-community/everything-you-need-to-know-about-flutter-page-route-transition-9ef5c1b32823">Everything you need to know about Flutter page route transition</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://medium.com/@agungsurya/create-custom-router-transition-in-flutter-using-pageroutebuilder-73a1a9c4a171">Create Custom Router Transition in Flutter using PageRouteBuilder</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://flutter.dev/docs/cookbook/animation/page-route-animation">Animate a page route transition - Flutter</a></li>
</ul>
きらぷか@積読ハウマッチ/SSSAPIなど
tag:crieit.net,2005:PublicArticle/15770
2020-03-16T12:26:54+09:00
2020-03-16T12:26:54+09:00
https://crieit.net/posts/Flutter-MediaQuery-OrientationBuilder
Flutterで画面サイズや向きを取得する(MediaQuery/OrientationBuilder)
<h3 id="画面のサイズ(MediaQuery/Size)"><a href="#%E7%94%BB%E9%9D%A2%E3%81%AE%E3%82%B5%E3%82%A4%E3%82%BA%28MediaQuery%2FSize%29">画面のサイズ(MediaQuery/Size)</a></h3>
<p>Flutterで画面サイズを取得するときは、<a target="_blank" rel="nofollow noopener" href="https://api.flutter.dev/flutter/widgets/MediaQuery-class.html">MediaQuery</a>を使うらしい。<br />
<a target="_blank" rel="nofollow noopener" href="https://api.flutter.dev/flutter/dart-ui/Size-class.html">Size</a>を取得できるので、そこからいろいろする感じ。</p>
<pre><code class="dart">@override
Widget build(BuildContext context) {
// 画面サイズの取得
final Size size = MediaQuery.of(context).size;
// 横幅
double width = size.width;
// 高さ
double height = size.height;
// アスペクト比
double aspectRatio = size.aspectRatio;
// 長い方
double longestSide = size.longestSide;
// 短い方
double shortestSide = size.shortestSide;
return new Container();
}
</code></pre>
<h3 id="画面の向き(OrientationBuilder/Orientation)"><a href="#%E7%94%BB%E9%9D%A2%E3%81%AE%E5%90%91%E3%81%8D%28OrientationBuilder%2FOrientation%29">画面の向き(OrientationBuilder/Orientation)</a></h3>
<p>画面の向きによってレイアウトとかを変えたい場合は、<a target="_blank" rel="nofollow noopener" href="https://api.flutter.dev/flutter/widgets/OrientationBuilder-class.html">OrientationBuilder</a>をつかうよう。</p>
<p>公式Cookbookの<a target="_blank" rel="nofollow noopener" href="https://flutter.dev/docs/cookbook/design/orientation">Update the UI based on orientation</a>に書いてあった。</p>
<pre><code class="dart">@override
Widget build(BuildContext context) {
return OrientationBuilder(
builder: (context, orientation) {
return GridView.count(
// 縦長の場合は2列、横長の場合は3列のGridView
crossAxisCount: orientation == Orientation.portrait ? 2 : 3,
children: [/*...*/]),
);
},
);
}
</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://qiita.com/najeira/items/c98c5fec9c71104f8263">Flutter でデバイスの画面サイズを得る MediaQuery - Qiita</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://flutter.dev/docs/cookbook/design/orientation">Update the UI based on orientation - Flutter</a></li>
</ul>
きらぷか@積読ハウマッチ/SSSAPIなど
tag:crieit.net,2005:PublicArticle/15769
2020-03-16T11:29:49+09:00
2020-03-16T11:29:49+09:00
https://crieit.net/posts/Flutter-Web
初めてFlutterでWebアプリをつくってみたときに思ったこと
<p>はじめてFlutterをつかったみたときの備忘録というか所感。<br />
Crieitの1週間でWebサービスをつくる企画 <a href="https://crieit.net/boards/web1week-202003">web1week</a>があったので、<br />
なかなかやる機会がなかったFlutterに手を付けてみた(<em>´ω`</em>)</p>
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr"><a target="_blank" rel="nofollow noopener" href="https://twitter.com/hashtag/web1week?src=hash&ref_src=twsrc%5Etfw">#web1week</a> 間に合った...(*´ω`*)たし算ゲーム「たすたすたしざん」をつくりました!! - (3/9~3/15)1週間でWebサービスを作るイベント - お題「Home」 - Boards - Crieit <a target="_blank" rel="nofollow noopener" href="https://t.co/9EhCCepYsm">https://t.co/9EhCCepYsm</a></p>— 積読ハウマッチ📚きらぷか (@kira_puka) <a target="_blank" rel="nofollow noopener" href="https://twitter.com/kira_puka/status/1238879746185777156?ref_src=twsrc%5Etfw">March 14, 2020</a></blockquote>
<p>やってみるといろいろ気になるところが出てきたので、整理がてらメモ。</p>
<h3 id="はじめるときやったこと"><a href="#%E3%81%AF%E3%81%98%E3%82%81%E3%82%8B%E3%81%A8%E3%81%8D%E3%82%84%E3%81%A3%E3%81%9F%E3%81%93%E3%81%A8">はじめるときやったこと</a></h3>
<p>ここの書き方とかは個別に記事にする予定なので、ここはざっくり全体のことだけ。<br />
シンプルなFlutter WebをGitHub Pageで公開した感じ。</p>
<p>Flutter自体やAndroidStudioのインストールは済んでいたので、その後から。</p>
<h4 id="構成を考える"><a href="#%E6%A7%8B%E6%88%90%E3%82%92%E8%80%83%E3%81%88%E3%82%8B">構成を考える</a></h4>
<p>とりあえず、どういうパッケージ構成が良いのかなと、ざっくりいろいろ見てみた。</p>
<ul>
<li>参考にしたサイト
<ul>
<li><a target="_blank" rel="nofollow noopener" href="https://github.com/mono0926/flutter_navigation_example">mono0926/flutter_navigation_example</a></li>
<li>https://twitter.com/ampersand000/status/1225344767115976705</li>
<li><a target="_blank" rel="nofollow noopener" href="https://github.com/flutter/samples">flutter/samples: A collection of Flutter examples and demos.</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/tanakeiQ/items/2c4a7fcb8e95b9aa55ad">管理しやすい(と思う)Flutterプロジェクトのディレクトリ構造 - Qiita</a></li>
</ul></li>
</ul>
<p>シンプルなのだったので、とりあえず、以下の感じ。</p>
<pre><code>- configs ... 定数とか設定関連
- models ... モデルとかコアの処理とか
- pages ... ページなどUI関連
- widgets ... ページ内で使うWidget群
- main.dart ... メイン。テーマのカスタムやルーティングも。
</code></pre>
<p>処理も少なく、DBもFirebaseもないので、最小構成な感じ。</p>
<h4 id="Flutter Webを有効化する"><a href="#Flutter+Web%E3%82%92%E6%9C%89%E5%8A%B9%E5%8C%96%E3%81%99%E3%82%8B">Flutter Webを有効化する</a></h4>
<p>まずはFlutter Webはベータ版なので、<br />
<a target="_blank" rel="nofollow noopener" href="https://flutter.dev/docs/get-started/web">公式サイト</a>みながら有効化する。</p>
<pre><code class="shell">$ flutter channel beta
$ flutter upgrade
$ flutter config --enable-web
</code></pre>
<h4 id="プロジェクトを作る"><a href="#%E3%83%97%E3%83%AD%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%82%92%E4%BD%9C%E3%82%8B">プロジェクトを作る</a></h4>
<p>Android Studioから。「Start a new Flutter project」を選んで、<br />
ポチポチしていけばOK。アプリと変わらない。</p>
<p><a href="https://crieit.now.sh/upload_images/4e53b17ab99d5d84f65aa1d17d8689655e6ee4168ea25.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/4e53b17ab99d5d84f65aa1d17d8689655e6ee4168ea25.png?mw=700" alt="スクリーンショット_2020-03-16_10_06_50.png" /></a></p>
<h4 id="実行"><a href="#%E5%AE%9F%E8%A1%8C">実行</a></h4>
<p>エミュレータの選択する部分で「Chrome(web)」とかが増えているので、<br />
「Chrome(web)」を選んで実行すると、Chromeの新しいウィンドウで起動してくれる。</p>
<p><a href="https://crieit.now.sh/upload_images/a55683393ce5e223fa18c4980cdb366e5e6ee3cbcb736.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/a55683393ce5e223fa18c4980cdb366e5e6ee3cbcb736.png?mw=700" alt="スクリーンショット 2020-03-16 10.10.12.png" /></a></p>
<p>もちろん、HotReloadも効くけど、ガラッと変えるとリロードが必要になる。</p>
<h4 id="Web向けの変更"><a href="#Web%E5%90%91%E3%81%91%E3%81%AE%E5%A4%89%E6%9B%B4">Web向けの変更</a></h4>
<p>Android/iOSごとの設定と同様に、Web版も個別設定ができる。<br />
<code>web/</code>配下のファイルを修正すればOK。</p>
<p><a href="https://crieit.now.sh/upload_images/cfd7f9ef7291d3f0c0fc4b4d1d85d1ba5e6ee424d564c.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/cfd7f9ef7291d3f0c0fc4b4d1d85d1ba5e6ee424d564c.png?mw=700" alt="スクリーンショット 2020-03-16 10.16.54.png" /></a></p>
<p>今回のでは主に、以下を変更。</p>
<ul>
<li>メタタグの変更: index.htmlにdescriptionやOGタグを変更</li>
<li>ファビコンなどの配置: icons配下にfavicon.icoなどを配置</li>
<li>マニフェストの修正: PWAとして公開されるっぽいので、アプリ名などを設定</li>
</ul>
<h4 id="ビルド"><a href="#%E3%83%93%E3%83%AB%E3%83%89">ビルド</a></h4>
<p>リリース用にビルドする方法は以下の感じ。</p>
<pre><code class="shell"># Web版のビルド
$ flutter build web
</code></pre>
<p>実行すると、<code>build/web/</code>配下にファイルが生成される。</p>
<p><a href="https://crieit.now.sh/upload_images/ce8635c160f65c18705506b278040d9b5e6ee42f201a2.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/ce8635c160f65c18705506b278040d9b5e6ee42f201a2.png?mw=700" alt="スクリーンショット 2020-03-16 10.21.13.png" /></a></p>
<p>あとは、生成されたファイルをアップロードすればOK。</p>
<h4 id="GitHub Pageで公開するときの注意点"><a href="#GitHub+Page%E3%81%A7%E5%85%AC%E9%96%8B%E3%81%99%E3%82%8B%E3%81%A8%E3%81%8D%E3%81%AE%E6%B3%A8%E6%84%8F%E7%82%B9">GitHub Pageで公開するときの注意点</a></h4>
<p>GitHub Pageに公開するときにちょっとハマった。。</p>
<p>web/配下のindex.htmlやmanifest.jsonを修正した際、<br />
<code>/manifest.json</code>としていたが、</p>
<pre><code class="html"><!-- manifest -->
<link rel="manifest" href="/manifest.json">
</code></pre>
<p>リポジトリ名もパスにいれないとだめだった。。</p>
<pre><code class="html"><!-- manifest -->
<link rel="manifest" href="/tasu2tashizan/manifest.json">
</code></pre>
<h3 id="Flutter Webを使ってみた所感など"><a href="#Flutter+Web%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6%E3%81%BF%E3%81%9F%E6%89%80%E6%84%9F%E3%81%AA%E3%81%A9">Flutter Webを使ってみた所感など</a></h3>
<p>調べて制限事項だったり、迷ったり、こうすればよかったかもという所感とか。<br />
初Flutterなので、解消していきたい疑問とかを列挙してみた。</p>
<h4 id="【制限】動的にメタ情報を変更できない"><a href="#%E3%80%90%E5%88%B6%E9%99%90%E3%80%91%E5%8B%95%E7%9A%84%E3%81%AB%E3%83%A1%E3%82%BF%E6%83%85%E5%A0%B1%E3%82%92%E5%A4%89%E6%9B%B4%E3%81%A7%E3%81%8D%E3%81%AA%E3%81%84">【制限】動的にメタ情報を変更できない</a></h4>
<p>上記で修正したように、Flutter上でメタ情報を設定できず、<br />
web/index.htmlを修正しなければならない。</p>
<p>以下のissueにも上がっている。<br />
・<a target="_blank" rel="nofollow noopener" href="https://github.com/flutter/flutter/issues/47600">Server-side rendering for Flutter web · Issue #47600 · flutter/flutter</a></p>
<p>なので、CGM系サービスで投稿ごとにOGP画像を変えたりすることは現状難しいっぽい。</p>
<p>(以下の<a target="_blank" rel="nofollow noopener" href="https://github.com/flutter/flutter/issues/33245#issuecomment-502800039">Issue #33245</a>にあるようなdart:htmlをつかってゴリゴリ書き換えればいけるかも?だけど未確認。。)</p>
<h4>【制限】URLに<code>#</code>がつく</h4>
<p>namedRouteでパスを設定できるがURLに<code>#</code>がつく。</p>
<p><a href="https://crieit.now.sh/upload_images/2e2f7f2a8c14f6e28b6e019c1d490dd65e6ee43b824c1.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/2e2f7f2a8c14f6e28b6e019c1d490dd65e6ee43b824c1.png?mw=700" alt="スクリーンショット 2020-03-16 10.33.43.png" /></a></p>
<p>以下のissueにワークアラウンドが紹介されているが、まだっぽい。。?<br />
・<a target="_blank" rel="nofollow noopener" href="https://github.com/flutter/flutter/issues/33245#issuecomment-502800039">Flutter_web navigation should provide a way to remove hash symbol(#) · Issue #33245 · flutter/flutter</a></p>
<h4 id="【反省】UIを全部Dartで書くので意識しないとカオスになる。。"><a href="#%E3%80%90%E5%8F%8D%E7%9C%81%E3%80%91UI%E3%82%92%E5%85%A8%E9%83%A8Dart%E3%81%A7%E6%9B%B8%E3%81%8F%E3%81%AE%E3%81%A7%E6%84%8F%E8%AD%98%E3%81%97%E3%81%AA%E3%81%84%E3%81%A8%E3%82%AB%E3%82%AA%E3%82%B9%E3%81%AB%E3%81%AA%E3%82%8B%E3%80%82%E3%80%82">【反省】UIを全部Dartで書くので意識しないとカオスになる。。</a></h4>
<p>Webみたいに、構造/装飾/動作がHTML/CSS/JavaScriptのように分かれていないため、<br />
すべてがごちゃまぜになったFatなdartファイルが簡単に作れてしまう。。</p>
<p>VueのSFCのようにひとまとまりのコンポーネントにするのが良いのか?</p>
<p>役割ごとにかき分けるのが良いのか、単一ファイルコンポーネントが良いのかを含め、<br />
どう書き分けるのがよいかなども気になるので、いろいろ見ていきたい。。</p>
<h5 id="【反省】最初はStatelessだけでモックアップをつくるのがよいかも?"><a href="#%E3%80%90%E5%8F%8D%E7%9C%81%E3%80%91%E6%9C%80%E5%88%9D%E3%81%AFStateless%E3%81%A0%E3%81%91%E3%81%A7%E3%83%A2%E3%83%83%E3%82%AF%E3%82%A2%E3%83%83%E3%83%97%E3%82%92%E3%81%A4%E3%81%8F%E3%82%8B%E3%81%AE%E3%81%8C%E3%82%88%E3%81%84%E3%81%8B%E3%82%82%EF%BC%9F">【反省】最初はStatelessだけでモックアップをつくるのがよいかも?</a></h5>
<p>とりあえず、StatefulWidgetをつかって、部分的にStatelessWidgetにしていたけど、<br />
いくつか本を読みながら、学びながらやっていたこともあり、ぐちゃぐちゃに。。</p>
<p>まずはUIを作って、あとから状態が必要な箇所をStatefulにしていくほうがよかったかもしれない。<br />
BLoCパターンなどはまだ試せていないので、そのあたりも見ていきたい。</p>
<h5 id="【疑問】パラメタに応じてリダイレクトするみたいな処理はどう書く?"><a href="#%E3%80%90%E7%96%91%E5%95%8F%E3%80%91%E3%83%91%E3%83%A9%E3%83%A1%E3%82%BF%E3%81%AB%E5%BF%9C%E3%81%98%E3%81%A6%E3%83%AA%E3%83%80%E3%82%A4%E3%83%AC%E3%82%AF%E3%83%88%E3%81%99%E3%82%8B%E3%81%BF%E3%81%9F%E3%81%84%E3%81%AA%E5%87%A6%E7%90%86%E3%81%AF%E3%81%A9%E3%81%86%E6%9B%B8%E3%81%8F%EF%BC%9F">【疑問】パラメタに応じてリダイレクトするみたいな処理はどう書く?</a></h5>
<p>「ログインが必要な画面で未ログインだと、ログインページに飛ばす」<br />
みたいな処理を書きたかったけど、conextがないとNavigatorを呼べない。。</p>
<p>今回はそこまで重要じゃない&時間がないので省略したけど、<br />
どうするのがいいか気になる。。</p>
<h5 id="【疑問】デプロイの自動化とかどうするんだろう?"><a href="#%E3%80%90%E7%96%91%E5%95%8F%E3%80%91%E3%83%87%E3%83%97%E3%83%AD%E3%82%A4%E3%81%AE%E8%87%AA%E5%8B%95%E5%8C%96%E3%81%A8%E3%81%8B%E3%81%A9%E3%81%86%E3%81%99%E3%82%8B%E3%82%93%E3%81%A0%E3%82%8D%E3%81%86%EF%BC%9F">【疑問】デプロイの自動化とかどうするんだろう?</a></h5>
<p>Flutterをインストールしないとビルドできないので、<br />
現状ローカルでビルドしている感じ。</p>
<p>今後CIサービスでFlutterのビルドができるようになっていくのだろうか?</p>
<h3 id="まとめ"><a href="#%E3%81%BE%E3%81%A8%E3%82%81">まとめ</a></h3>
<p>簡単にWebアプリを作れてHotReloadもいい感じだけど、<br />
まだまだissueがあり、思ってるWebサービスとはギャップがある感じ。</p>
<p>個別の処理とかはよく見るけど、全体的な方法論とか気になるところがまだまだあるので、<br />
いろいろ見ながら、サクサク作れるようになりたい( ゚д゚)!</p>
<p>以上!!</p>
<h3 id="作ったアプリはこちら"><a href="#%E4%BD%9C%E3%81%A3%E3%81%9F%E3%82%A2%E3%83%97%E3%83%AA%E3%81%AF%E3%81%93%E3%81%A1%E3%82%89">作ったアプリはこちら</a></h3>
<p><a target="_blank" rel="nofollow noopener" href="https://memory-lovers.github.io/tasu2tashizan/#/">たすたすたしざん</a></p>
<p><a href="https://crieit.now.sh/upload_images/3caa1c9eee3d15a865ec6443fca901245e6ee4485540c.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/3caa1c9eee3d15a865ec6443fca901245e6ee4485540c.png?mw=700" alt="ogp_tashizan.png" /></a></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>
きらぷか@積読ハウマッチ/SSSAPIなど
tag:crieit.net,2005:PublicArticle/15750
2020-03-07T17:25:02+09:00
2020-03-07T17:25:02+09:00
https://crieit.net/posts/PAAPI-v5-Node-js
PAAPI v5に移行してみた: Node.js版
<p><a target="_blank" rel="nofollow noopener" href="https://tsundoku.site">積読ハウマッチ</a>でAmazonのPAAPIを使っているけど、<br />
v4が2020年3月9日に利用できなくなるため、移行してみたときの備忘録。</p>
<p><a href="https://crieit.now.sh/upload_images/e5449de49d37c0a93ce3ada90eb537335e635a1adaf4b.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/e5449de49d37c0a93ce3ada90eb537335e635a1adaf4b.png?mw=700" alt="スクリーンショット 2020-03-07 17.12.50.png" /></a></p>
<p>以前は、<a target="_blank" rel="nofollow noopener" href="https://github.com/dmcquay/node-apac">node-apac</a>を使ってたけど、<a target="_blank" rel="nofollow noopener" href="https://github.com/dmcquay/node-apac/issues/97">対応はなさそう</a>。</p>
<h2 id="Amazon側の移行手順"><a href="#Amazon%E5%81%B4%E3%81%AE%E7%A7%BB%E8%A1%8C%E6%89%8B%E9%A0%86">Amazon側の移行手順</a></h2>
<p><a target="_blank" rel="nofollow noopener" href="https://affiliate.amazon.co.jp/help/node/topic/GZH32YX29UH5GACM">公式の移行手順</a>を参考に進める感じ。</p>
<h3 id="ステップ1: 新しい認証情報を取得する"><a href="#%E3%82%B9%E3%83%86%E3%83%83%E3%83%971%EF%BC%9A+%E6%96%B0%E3%81%97%E3%81%84%E8%AA%8D%E8%A8%BC%E6%83%85%E5%A0%B1%E3%82%92%E5%8F%96%E5%BE%97%E3%81%99%E3%82%8B">ステップ1: 新しい認証情報を取得する</a></h3>
<p>認証情報の形式が古いと払い出しが必要らしい<br />
いままで使っていたアクセスキーとシークレットキーでOKだったのでスキップ</p>
<h3 id="ステップ2: PA-API 5.0を試してみる"><a href="#%E3%82%B9%E3%83%86%E3%83%83%E3%83%972%EF%BC%9A+PA-API+5.0%E3%82%92%E8%A9%A6%E3%81%97%E3%81%A6%E3%81%BF%E3%82%8B">ステップ2: PA-API 5.0を試してみる</a></h3>
<p>以前と同様、Scratchpadが用意されている。<br />
アクセスキーがOKだったかも、ここで確認した。<br />
・<a target="_blank" rel="nofollow noopener" href="https://webservices.amazon.com/paapi5/scratchpad/index.html">Product Advertising API 5.0 Scratchpad</a></p>
<h3 id="ステップ3: 必要な知識を得る"><a href="#%E3%82%B9%E3%83%86%E3%83%83%E3%83%973%EF%BC%9A+%E5%BF%85%E8%A6%81%E3%81%AA%E7%9F%A5%E8%AD%98%E3%82%92%E5%BE%97%E3%82%8B">ステップ3: 必要な知識を得る</a></h3>
<p>公式ドキュメントにも書いてあるとおり、レスポンスがXMLからJSONになったらしい。</p>
<p>変更点は、<a target="_blank" rel="nofollow noopener" href="https://webservices.amazon.com/paapi5/documentation/migration-guide/whats-new-in-paapi5.html">ここ</a>にまとまっている。<br />
主には、セキュリティ強化、レスポンス速度の改善、新しいAPIの追加など。</p>
<p>また、レスポンスに関してv4からv5へのマッピングも用意されてる。<br />
・<a target="_blank" rel="nofollow noopener" href="https://webservices.amazon.com/paapi5/documentation/migration-guide/pa-api-40-to-50-mapping.html">PA-API 4.0 to 5.0 Mapping · Product Advertising API 5.0</a></p>
<h3 id="ステップ4: PA-API 5.0を導入する"><a href="#%E3%82%B9%E3%83%86%E3%83%83%E3%83%974%EF%BC%9A+PA-API+5.0%E3%82%92%E5%B0%8E%E5%85%A5%E3%81%99%E3%82%8B">ステップ4: PA-API 5.0を導入する</a></h3>
<p>従来どおり、Web APIを直接叩けるけど、SDKも提供されている。<br />
言語は、Java / Node.js / PHP / Pythonの4つ。サンプルもある。<br />
・<a target="_blank" rel="nofollow noopener" href="https://webservices.amazon.com/paapi5/documentation/quick-start/using-sdk.html">Using SDK · Product Advertising API 5.0</a></p>
<p>ただ、残念なことに、Node.jsのSDKは、zipで公開されている...</p>
<h2 id="ソース側の移行手順"><a href="#%E3%82%BD%E3%83%BC%E3%82%B9%E5%81%B4%E3%81%AE%E7%A7%BB%E8%A1%8C%E6%89%8B%E9%A0%86">ソース側の移行手順</a></h2>
<h3 id="SDKを配置"><a href="#SDK%E3%82%92%E9%85%8D%E7%BD%AE">SDKを配置</a></h3>
<p>zipをダウンロードして展開するとこんな感じになってる。</p>
<p><a href="https://crieit.now.sh/upload_images/07628c2b1f8ec3ae587e9d119d743d6a5e635a2951165.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/07628c2b1f8ec3ae587e9d119d743d6a5e635a2951165.png?mw=700" alt="スクリーンショット 2020-03-07 17.11.09.png" /></a></p>
<p>src配下がAPIなので、自分のソースに配置。</p>
<p><a href="https://crieit.now.sh/upload_images/35f1b70ec8ce6466124c642bb991527e5e635a37ecdf3.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/35f1b70ec8ce6466124c642bb991527e5e635a37ecdf3.png?mw=700" alt="スクリーンショット 2020-03-07 6.06.05.png" /></a></p>
<p>dependencyがあるので、それもコピペして、<code>npm i</code>する</p>
<pre><code class="json">"dependencies": {
"superagent": "3.5.2",
"crypto-js": "^3.1.9-1"
},
</code></pre>
<h3 id="APIの呼び出し"><a href="#API%E3%81%AE%E5%91%BC%E3%81%B3%E5%87%BA%E3%81%97">APIの呼び出し</a></h3>
<p>解答したzipにサンプルがあるので、それを見ながら実装していく。<br />
使っているのは検索なので、<code>sampleSearchItemsApi.js</code>を参照。</p>
<p>以下は、サンプルの抜粋</p>
<pre><code class="javascript">// ** API Clientの準備
var ProductAdvertisingAPIv1 = require('./src/index');
var defaultClient = ProductAdvertisingAPIv1.ApiClient.instance;
// アクセスキーなどを設定。ホストとかリージョンは、以下を参照
// https://webservices.amazon.com/paapi5/documentation/common-request-parameters.html#host-and-region
defaultClient.accessKey = '<YOUR ACCESS KEY>';
defaultClient.secretKey = '<YOUR SECRET KEY>';
defaultClient.host = 'webservices.amazon.co.jp';
defaultClient.region = 'us-west-2';
// ** リクエストの生成
var searchItemsRequest = new ProductAdvertisingAPIv1.SearchItemsRequest();
// アソシエイトタグを設定
searchItemsRequest['PartnerTag'] = '<YOUR PARTNER TAG>';
searchItemsRequest['PartnerType'] = 'Associates';
// 検索リクエストを設定。SearchIndexは以下を参照
// https://webservices.amazon.com/paapi5/documentation/use-cases/organization-of-items-on-amazon/search-index.html
searchItemsRequest['Keywords'] = 'Harry Potter';
searchItemsRequest['SearchIndex'] = 'Books';
searchItemsRequest['ItemCount'] = 2;
// 取得するレスポンスの指定
// https://webservices.amazon.com/paapi5/documentation/search-items.html#resources-parameter
searchItemsRequest['Resources'] = ['Images.Primary.Medium', 'ItemInfo.Title', 'Offers.Listings.Price'];
// ** コールバックの設定
var callback = function (error, data, response) {
if (error) {
// エラーの場合
console.log('Status Code: ' + error['status']);
} else {
// レスポンスを取得
var searchItemsResponse = ProductAdvertisingAPIv1.SearchItemsResponse.constructFromObject(data);
if (searchItemsResponse['SearchResult'] !== undefined) {
// 1件目の結果を取得
var item_0 = searchItemsResponse['SearchResult']['Items'][0];
if (item_0 !== undefined && item_0['ASIN'] !== undefined) {
console.log('ASIN: ' + item_0['ASIN']);
}
}
// エラーが返ってきた場合
if (searchItemsResponse['Errors'] !== undefined) {
var error_0 = searchItemsResponse['Errors'][0];
console.log('Error Code: ' + error_0['Code']);
console.log('Error Message: ' + error_0['Message']);
}
}
};
// ** APIの実行
var api = new ProductAdvertisingAPIv1.DefaultApi();
try {
api.searchItems(searchItemsRequest, callback);
} catch (ex) {
console.log('Exception: ' + ex);
}
</code></pre>
<h2 id="対応が必要だった点"><a href="#%E5%AF%BE%E5%BF%9C%E3%81%8C%E5%BF%85%E8%A6%81%E3%81%A0%E3%81%A3%E3%81%9F%E7%82%B9">対応が必要だった点</a></h2>
<p>基本的にパラメタがわかったため、一致してるものを探していけばOK。<br />
変更自体はそこまでだけど、できることが変わっているので注意...</p>
<h3 id="リクエスト関連"><a href="#%E3%83%AA%E3%82%AF%E3%82%A8%E3%82%B9%E3%83%88%E9%96%A2%E9%80%A3">リクエスト関連</a></h3>
<ul>
<li>MerchantIdがMerchantになっていた。設定値は変わらず</li>
<li>SortがSortyByになり、設定値も変更</li>
<li>PowerSearchが廃止に...つまり、除外や期間指定ができない</li>
</ul>
<h3 id="レスポンス関連"><a href="#%E3%83%AC%E3%82%B9%E3%83%9D%E3%83%B3%E3%82%B9%E9%96%A2%E9%80%A3">レスポンス関連</a></h3>
<ul>
<li>全体的に変更。XMLからJSONになっており、構成も変わっている</li>
<li>発売日がUTCになり、時刻まで記載</li>
<li>エラーメッセージも全体的に変わっているので対応が必要</li>
<li>バリエーションが除外され、GetVariations APIが新設(リクエスト数が増...)</li>
<li>ただ、電子書籍の価格が取得できるようになった</li>
</ul>
<h2 id="よく参照したURL"><a href="#%E3%82%88%E3%81%8F%E5%8F%82%E7%85%A7%E3%81%97%E3%81%9FURL">よく参照したURL</a></h2>
<p>とにかく、以下を読みまくった。。</p>
<ul>
<li>Operations関連: リクエストパラメタとか。指定できるレスポンスもここ
<ul>
<li><a target="_blank" rel="nofollow noopener" href="https://webservices.amazon.com/paapi5/documentation/search-items.html">SearchItems</a> ... 検索</li>
<li><a target="_blank" rel="nofollow noopener" href="https://webservices.amazon.com/paapi5/documentation/get-items.html">GetItems</a> ... ASINから商品情報取得</li>
<li><a target="_blank" rel="nofollow noopener" href="https://webservices.amazon.com/paapi5/documentation/get-variations.html">GetVariations</a> ... ASINからバリエーション情報取得</li>
</ul></li>
<li>Resource関連: レスポンスの内容
<ul>
<li><a target="_blank" rel="nofollow noopener" href="https://webservices.amazon.com/paapi5/documentation/item-info.html">ItemInfo</a> ... 商品情報</li>
<li><a target="_blank" rel="nofollow noopener" href="https://webservices.amazon.com/paapi5/documentation/images.html">Images</a> ... 画像関連</li>
<li><a target="_blank" rel="nofollow noopener" href="https://webservices.amazon.com/paapi5/documentation/offers.html">Offers</a> ... 価格関連</li>
</ul></li>
<li>共通のリクエストパラメタ関連
<ul>
<li><a target="_blank" rel="nofollow noopener" href="https://webservices.amazon.com/paapi5/documentation/locale-reference/japan.html">日本のMarketplaceとかの情報</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://webservices.amazon.com/paapi5/documentation/common-request-parameters.html#host-and-region">共通のリクエストパラメタ</a> ... HostやRegionもここ</li>
</ul></li>
<li>共通のレスポンス関連
<ul>
<li><a target="_blank" rel="nofollow noopener" href="https://webservices.amazon.com/paapi5/documentation/troubleshooting/error-messages.html">エラーメッセージ</a></li>
</ul></li>
<li>マイグレーション関連
<ul>
<li><a target="_blank" rel="nofollow noopener" href="https://affiliate.amazon.co.jp/help/node/topic/GZH32YX29UH5GACM">v5.0への移行ガイドの日本語版</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://webservices.amazon.com/paapi5/documentation/migration-guide/pa-api-40-to-50-mapping.html">v4.0からv5.0へのマッピング</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://webservices.amazon.com/paapi5/documentation/play-around-using-scratchpad.html">Scratchpadの使い方</a></li>
</ul></li>
</ul>
<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>
きらぷか@積読ハウマッチ/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/15724
2020-02-19T19:55:15+09:00
2020-02-19T19:55:15+09:00
https://crieit.net/posts/Retrofit2
Retrofit2で自己証明書のサーバへのアクセスできるようにする
<p><a target="_blank" rel="nofollow noopener" href="https://square.github.io/retrofit/">Retrofit2</a>でオレオレ証明書なサーバにアクセスすると、<br />
こんなエラーが出る。その時の対処方法の備忘録。</p>
<pre><code>java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.
javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.
at com.android.org.conscrypt.OpenSSLSocketImpl.startHandshake(OpenSSLSocketImpl.java:361)
</code></pre>
<h3 id="ソース"><a href="#%E3%82%BD%E3%83%BC%E3%82%B9">ソース</a></h3>
<p>こんな感じ。Retrofit2の内部で利用しているOkHttpClientのカスタマイズし、<br />
安全でない証明書でも許可するように変更する。</p>
<p>開発中など一時的な感じで使う。</p>
<pre><code class="java">import com.google.gson.FieldNamingPolicy;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import java.security.cert.CertificateException;
import java.util.concurrent.TimeUnit;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import okhttp3.OkHttpClient;
import okhttp3.logging.HttpLoggingInterceptor;
import retrofit2.Retrofit;
public class ApiClientFactory {
// ApiClientを生成する処理
public ApiClient getApiClient(String baseUrl) {
return new Retrofit.Builder()
.baseUrl(baseUrl)
.client(getHttpClient())
.build()
.create(ApiClient.class);
}
private OkHttpClient getHttpClient() {
HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor();
interceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
// return new OkHttpClient.Builder()
// 通常のBuilderの代わりに、カスタマイズしたBuilderを使う
return getUnsafeOkHttpClient()
.readTimeout((15 * 1000), TimeUnit.MILLISECONDS)
.writeTimeout((20 * 1000), TimeUnit.MILLISECONDS)
.connectTimeout((20 * 1000), TimeUnit.MILLISECONDS)
.addInterceptor(interceptor)
.build();
}
private static OkHttpClient.Builder getUnsafeOkHttpClient() {
try {
// Create a trust manager that does not validate certificate chains
final TrustManager[] trustAllCerts = new TrustManager[]{
new X509TrustManager() {
@Override
public void checkClientTrusted(java.security.cert.X509Certificate[] chain, String authType) throws CertificateException {
}
@Override
public void checkServerTrusted(java.security.cert.X509Certificate[] chain, String authType) throws CertificateException {
}
@Override
public java.security.cert.X509Certificate[] getAcceptedIssuers() {
return new java.security.cert.X509Certificate[]{};
}
}
};
// Install the all-trusting trust manager
final SSLContext sslContext = SSLContext.getInstance("SSL");
sslContext.init(null, trustAllCerts, new java.security.SecureRandom());
// Create an ssl socket factory with our all-trusting manager
final SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
OkHttpClient.Builder builder = new OkHttpClient.Builder();
builder.sslSocketFactory(sslSocketFactory, (X509TrustManager) trustAllCerts[0]);
builder.hostnameVerifier(new HostnameVerifier() {
@Override
public boolean verify(String hostname, SSLSession session) {
return true;
}
});
return builder;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
</code></pre>
<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://mobikul.com/android-retrofit-handling-sslhandshakeexception/">Retrofit handle the SSLHandshakeException - Mobikul</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://square.github.io/okhttp/https/#customizing-trusted-certificates">HTTPS - OkHttp</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://github.com/square/okhttp/issues/5700">SSLHandshakeException on Android 5 (API 21) · Issue #5700 · square/okhttp</a></li>
</ul>
きらぷか@積読ハウマッチ/SSSAPIなど
tag:crieit.net,2005:PublicArticle/15723
2020-02-19T19:53:57+09:00
2020-02-19T19:53:57+09:00
https://crieit.net/posts/Cloud-Function-SVG-OGP
Cloud FunctionとSVGでOGP画像生成を試行錯誤したまとめ
<p>Nuxt+Firebaseで<a target="_blank" rel="nofollow noopener" href="https://tsundoku.site">開発してるサービス</a>のOGP画像を改善しようと、<br />
いろいろ試してみたときの備忘録。</p>
<p>OGP画像の生成はクライアント側とサーバ側かがあるが、<br />
Firestoreの変更に合わせて生成したいので、<br />
Cloud Function上で利用できる方法を考えてた。</p>
<p>結果的に、</p>
<ol>
<li><a target="_blank" rel="nofollow noopener" href="https://sharp.pixelplumbing.com/">sharp</a>で画像を合成してベースをつくり</li>
<li>ImageMagickで文字を追記していく</li>
</ol>
<p>という構成になった。その試行錯誤の備忘録です。</p>
<h3 id="やりたかったこと"><a href="#%E3%82%84%E3%82%8A%E3%81%9F%E3%81%8B%E3%81%A3%E3%81%9F%E3%81%93%E3%81%A8">やりたかったこと</a></h3>
<p>背景画像+本の画像+文字みたいなOGP画像にしたい。</p>
<p><a href="https://crieit.now.sh/upload_images/8b51de5926bba4fd12714eb7d06f4d8b5e4d1387cde01.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/8b51de5926bba4fd12714eb7d06f4d8b5e4d1387cde01.png?mw=700" alt="tsundoku_book_start_4873115655.png" /></a></p>
<p>あと、座標の指定はめんどくさいので、楽な方法を探してみた。<br />
SVGだとCSSも使えて、ブラウザ上で書くにできるので良さそう。<br />
(と思ったけど、結果、だめだった...)</p>
<h3 id="ためした方法とまとめ"><a href="#%E3%81%9F%E3%82%81%E3%81%97%E3%81%9F%E6%96%B9%E6%B3%95%E3%81%A8%E3%81%BE%E3%81%A8%E3%82%81">ためした方法とまとめ</a></h3>
<p>試したのは以下の4パターン。<br />
SVG+<a target="_blank" rel="nofollow noopener" href="https://sharp.pixelplumbing.com/">sharp</a>で試した感じ。<br />
node-canvasはおまけな感じ程度。</p>
<ol>
<li>sharp+SVG(style+foreginObject)
<ul>
<li>SVGですべて構成する方法。</li>
<li>フォントとか位置、サイズはstyleで設定</li>
<li>外部URLの画像が取得できずにNG</li>
</ul></li>
<li>sharp+SVG+画像は別で合成
<ul>
<li>1.の画像部分を別で合成するパターン</li>
<li>カスタムフォントが設定できずNG</li>
</ul></li>
<li>sharp+SVG+ImageMagick
<ul>
<li>画像の合成部分のみsharpをつかい</li>
<li>枠線をSVG、文字はImageMagickで書き出し</li>
<li>これを採用</li>
</ul></li>
<li>node-canvas+SVG
<ul>
<li>node-canvasもSVGを入力にできるようなので試した</li>
<li>1.や2.と同様の理由でNG</li>
</ul></li>
</ol>
<p>内部で利用しているSVGライブラリ自体で未対応っぽいので、<br />
SVGのstyleや外部URLの画像、カスタムフォントなどは、<br />
ブラウザ上以外の画像生成ではまだ未対応っぽい。</p>
<p>ほかにも、CloudFunction上でpuppeteerを使えるようだけど、<br />
メモリをすごい使うっぽい記事を見てしまい、まだためしてない。。</p>
<hr />
<p>以下試したコード。</p>
<h4 id="SVG(style+foreginObject)"><a href="#SVG%28style%2BforeginObject%29">SVG(style+foreginObject)</a></h4>
<ul>
<li>sharpが、foreignObject+sytleに未対応...のためNG</li>
<li>imageに外部URLを使っているとダメっぽい...</li>
</ul>
<pre><code class="typescript">import sharp from "sharp";
await sharp("./input.svg")
.png()
.toFile("./output.png");
</code></pre>
<pre><code class="xml"><?xml version="1.0"?>
<svg xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" viewbox="0 0 1200 630" width="1200" height="630">
<style>
@import url("https://fonts.googleapis.com/css?family=Noto+Sans+JP:500&display=swap&subset=japanese");
.contents {
display: flex;
justify-content: center;
align-items: flex-end;
width: 1200px;
height: 630px;
position: relative;
}
.book {
height: calc(100% - 100px);
}
.text-contents {
position: absolute;
left: 0;
right: 0;
bottom: 58px;
width: 1200px;
background-color: rgba(0, 0, 0, 0.6);
font-family: "Noto Sans JP", sans-serif;
color: white;
padding-bottom: 8px;
text-align: center;
}
.text-label {
font-size: 60px;
font-weight: 500;
padding-left: 0.5em;
}
.title-label {
font-size: 28px;
font-weight: 500;
padding-top: 0.2em;
}
</style>
<image xlink:href="https://.../background.png" width="1200" height="630" />
<foreignObject requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" width="1200" height="630">
<div class="contents">
<img class="book" src="https://.../thumbnail.png" />
<div class="text-contents">
<div class="text-label">
<span>読みます!!</span>
</div>
<div class="title-label">
<span>リーダブルコード</span>
</div>
<div class="title-label">
<span>ダスティン・ボズウェル/トレバー・フォシェ</span>
</div>
</div>
</div>
</foreignObject>
</svg>
</code></pre>
<h4 id="SVG+外部URLの画像"><a href="#SVG%2B%E5%A4%96%E9%83%A8URL%E3%81%AE%E7%94%BB%E5%83%8F">SVG+外部URLの画像</a></h4>
<ul>
<li>SVGには文字とかだけにして、画像はファイル読み込みで対応</li>
<li>カスタムフォントが使えない...のNG</li>
<li>GAEかlamdaだと、fontconfigに関する環境変数を設定できるが、Cloud Functionでは無理そう</li>
<li>fonts.confを設定してもダメだった...</li>
</ul>
<pre><code class="typescript">import axios from "axios";
import sharp from "sharp";
// 埋め込む画像のURL
const bookURL = "https://.../thumbnail.png";
const bookBuffer = await axios.get(bookURL, { responseType: "arraybuffer" });
// URLから取得した画像を加工(リサイズ)
const bookImage = await sharp(bookBuffer.data)
.resize(520, 454, { position: "top" })
.toBuffer();
// sharpで結合
await sharp("./background.png") // 背景画像を読み込み
.composite([
{ input: bookImage, gravity: "south" }, 本の画像を書き出し
{ input: "./input.svg" } // SVGの書き出し
])
.png()
.toFile(tempFile);
</code></pre>
<pre><code class="xml"><?xml version="1.0"?>
<svg xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" viewbox="0 0 1200 630" width="1200" height="630">
<defs>
<!-- Fontを外部URLで指定 -->
<font-face font-family="Noto Sans JP">
<font-face-src>
<font-face-uri xlink:href="https://fonts.googleapis.com/css?family=Noto+Sans+JP:400,700|Roboto:400,700&display=swap&subset=japanese" />
</font-face-src>
</font-face>
<!-- フォントをローカルファイルで指定 -->
<font-face font-family="Noto Sans JP" font-weight="500">
<font-face-src>
<font-face-uri xlink:href="./font/NotoSansJP-Medium.otf">
<font-face-format string="opentype"/>
</font-face-uri>
</font-face-src>
</font-face>
<font-face font-family="Noto Sans JP" font-weight="700">
<font-face-src>
<font-face-uri xlink:href="./font/NotoSansJP-Bold.otf">
<font-face-format string="opentype"/>
</font-face-uri>
</font-face-src>
</font-face>
<!-- ローカルファイルを@font-faceで指定 -->
<style type="text/css">
@font-face {
font-family: 'Noto Sans JP';
font-style: normal;
font-weight: 500;
src: url('./font/NotoSansJP-Medium.otf') format("opentype");
}
@font-face {
font-family: 'Noto Sans JP';
font-style: normal;
font-weight: 700;
src: url('./font/NotoSansJP-Bold.otf') format("opentype");
}
</style>
</defs>
<rect x="0" y="530" width="1200" height="100" fill="#000000" fill-opacity="0.6" />
<text x="610" y="156" font-size="60" fill="#FFFFFF" text-anchor="middle">
<tspan font-weight="700">読みます!!</tspan>
</text>
<text x="600" y="574" font-size="28" fill="#FFFFFF" text-anchor="middle">
<tspan font-weight="700">リーダブルコード</tspan>
</text>
<text x="600" y="610" font-size="24" fill="#FFFFFF" text-anchor="middle">
<tspan font-weight="500">ダスティン・ボズウェル/トレバー・フォシェ</tspan>
</text>
</svg>
</code></pre>
<h3 id="SVG+ImageMagick"><a href="#SVG%2BImageMagick">SVG+ImageMagick</a></h3>
<ul>
<li>画像の加工や合成はsharpを使い、文字の書き出しだけImageMagickを使う</li>
<li>画像の加工・合成部分は、上の「SVG+外部URLの画像」と同じ感じ</li>
<li>ImageMagickで文字を書くのは以下の記事を参照
<ul>
<li><a target="_blank" rel="nofollow noopener" href="https://www.memory-lovers.blog/entry/2019/06/26/194500" target="_blank" rel="noopener">Cloud Functions + ImageMagickでOPG画像の動的生成してCloud Storageにアップロードする - くらげになりたい。</a></li>
</ul></li>
</ul>
<h3 id="node-canvas+SVG"><a href="#node-canvas%2BSVG">node-canvas+SVG</a></h3>
<ul>
<li>同じくダメ。imageタグの外部URLがNG+foreignObject未対応...</li>
<li><a target="_blank" rel="nofollow noopener" href="https://github.com/Automattic/node-canvas">Automattic/node-canvas: Node canvas is a Cairo backed Canvas implementation for NodeJS.</a></li>
</ul>
<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://blog.kozakana.net/2019/04/sharp-resize/">Node.jsのライブラリsharpでリサイズを試してみる | Simple is Beautiful.</a></li>
<li><a target="_blank" rel="nofollow noopener" href="http://cream-worker.blog.jp/archives/1073895056.html">くりーむわーかー : axiosで取得した画像データを保存とか表示とかする</a></li>
</ul>
きらぷか@積読ハウマッチ/SSSAPIなど
tag:crieit.net,2005:PublicArticle/15695
2020-01-24T15:25:45+09:00
2020-01-24T15:25:45+09:00
https://crieit.net/posts/SVG-OGP-PNG-PNG
SVGでOGP用のPNG画像を生成してみる(折り返し文字、画像埋め込み、PNG化)
<p>SVGでOGP画像を作りたいなと思い、いろいろ調べたときの備忘録。</p>
<p>SVGはまるのも多いけど、いろいろできるので、<br />
SVGからOGP画像をつくるのいいかもしれない。</p>
<h2 id="とりあえず、SVGを書いてみる"><a href="#%E3%81%A8%E3%82%8A%E3%81%82%E3%81%88%E3%81%9A%E3%80%81SVG%E3%82%92%E6%9B%B8%E3%81%84%E3%81%A6%E3%81%BF%E3%82%8B">とりあえず、SVGを書いてみる</a></h2>
<p>ちょろっとなるなら、HTMLでベタ書きしてみるのもいい。<br />
動作確認とか楽ちん。</p>
<pre><code class="html"><html>
<body>
<svg viewbox="0 0 1200 630" width="1200" height="630" style="background-color: lightgray;">
<rect x="550" y="265" width="100" height="100" fill="blue" />
<circle cx="550" cy="265" r="30" fill="none" stroke="red" stroke-width="5" />
</svg>
</body>
</html>
</code></pre>
<p>こんな感じ。</p>
<p><a href="https://crieit.now.sh/upload_images/0996c11f41d0943bf90efa5baebbc3af5e2a8d8a9963d.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/0996c11f41d0943bf90efa5baebbc3af5e2a8d8a9963d.png?mw=700" alt="スクリーンショット 2020-01-23 16.15.07.png" /></a></p>
<p><code><rect></code>で四角を書いて、<code><circle></code>で丸を書いてる。</p>
<p>上から順に描画されるので、下にある方が前面にくる感じ。<br />
なので、四角の上に丸が描画されてる。</p>
<p><code><rect></code>の座標(x,y)は左上の場所なので、<br />
横幅と縦幅分の半分だけ、全体の中心からずらしてる。</p>
<h2 id="画像化する"><a href="#%E7%94%BB%E5%83%8F%E5%8C%96%E3%81%99%E3%82%8B">画像化する</a></h2>
<p>最終的にOGPで使うPNG画像にしたい。</p>
<p>画像化する方法について、ローカルとブラウザ上の2つを試した。</p>
<h4 id="1. ローカルで画像化する"><a href="#1.+%E3%83%AD%E3%83%BC%E3%82%AB%E3%83%AB%E3%81%A7%E7%94%BB%E5%83%8F%E5%8C%96%E3%81%99%E3%82%8B">1. ローカルで画像化する</a></h4>
<p>いろんな画像フォーマットに対応しているsharpがよさそう。<br />
・<a target="_blank" rel="nofollow noopener" href="https://sharp.pixelplumbing.com/">sharp - High performance Node.js image processing</a></p>
<p>.svgファイルを.pngに簡単にできる。</p>
<h5 id="sharpでsvgをpngに変換してみる"><a href="#sharp%E3%81%A7svg%E3%82%92png%E3%81%AB%E5%A4%89%E6%8F%9B%E3%81%97%E3%81%A6%E3%81%BF%E3%82%8B">sharpでsvgをpngに変換してみる</a></h5>
<p>まずは、.svgとして扱えるように、<br />
xmlnsとかをちゃんとつけたsample.svgを用意する。</p>
<pre><code class="xml"><!-- sample.svg -->
<?xml version="1.0"?>
<svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 1200 630" width="1200" height="630" fill="lightgray">
<rect x="550" y="265" width="100" height="100" fill="blue" />
<circle cx="550" cy="265" r="30" fill="none" stroke="red" stroke-width="5" />
</svg>
</code></pre>
<p>パッケージをインストール</p>
<pre><code class="shell">$ npm i sharp
</code></pre>
<p>変換するコードはこんな感じ。</p>
<pre><code class="javascript">// generate.js
const sharp = require("sharp");
async function main() {
await sharp("sample.svg")
.png()
.toFile("output.png");
}
main().then();
</code></pre>
<p>そして、実行。</p>
<pre><code class="shell">$ node generate.js
</code></pre>
<p>すると、output.pngというPNGファイルを作成してくれる。</p>
<p>ただ、<strong><code>svg</code>のstyleは反映してくれないので注意</strong>。</p>
<pre><code class="javascript">// generate.js
const sharp = require("sharp");
async function main() {
await sharp("sample.svg")
.png()
.toFile("output.png");
}
main().then();
</code></pre>
<p>背景色をつける場合は、sharp側で設定すればOK</p>
<pre><code class="javascript">// generate.js
const sharp = require("sharp");
async function main() {
await sharp("sample.svg")
.flatten({ background: "lightgray" }) // 背景色
.png()
.toFile("output.png");
}
main().then();
</code></pre>
<h3 id="2. ブラウザ上のSVGを画像化する"><a href="#2.+%E3%83%96%E3%83%A9%E3%82%A6%E3%82%B6%E4%B8%8A%E3%81%AESVG%E3%82%92%E7%94%BB%E5%83%8F%E5%8C%96%E3%81%99%E3%82%8B">2. ブラウザ上のSVGを画像化する</a></h3>
<p>ユーザに情報を入力してもらった内容をSVGで表示して、<br />
OGP画像にするという感じが多いので、こっちがメイン。</p>
<p>保存先のFirebase StorageがData URL形式に対応しているので、<br />
PNGのData URLが取得できればOK。</p>
<p>大まかな流れは、こんな感じ。</p>
<ol>
<li>Canvasを用意する</li>
<li>SVGを読み込む<code><image></code>を用意する</li>
<li><code><svg></code>を文字列に変換</li>
<li>作成した<code><image></code>にDataURL形式でSVGをセットして、読み込み開始</li>
<li>読み込みが完了したら、Canvasに書き出して</li>
<li>CanvasからDataURLを取得する</li>
</ol>
<pre><code class="typescript">// svg2DataURL.ts
/**
* svgをpngに変換
* @param svgElement <svg>のHTML要素
*/
export default function svg2DataURL(
svgElement: HTMLElement
): Promise<HTMLCanvasElement> {
return new Promise((resolve, reject) => {
// 1. Canvasを用意する
const canvas = document.createElement("canvas");
canvas.width = 1200;
canvas.height = 630;
const ctx = canvas.getContext("2d");
if (!ctx) {
reject(Error("Create Canvas Error..."));
return;
}
// 2. SVGを読み込む<image>を用意する
const image = new Image();
image.decoding = "async";
image.onload = () => {
// 5. 読み込みが完了したら、Canvasに書き出して、
ctx.drawImage(image, 0, 0, 1200, 630);
// 6. CanvasからDataURLを取得する
resolve(canvas.toDataURL());
};
image.onerror = e => reject(e);
// 3. <svg>を文字列に変換
const svgXml = new XMLSerializer().serializeToString(svgElement);
const svgData = btoa(unescape(encodeURIComponent(svgXml)));
// 4. 作成した<image>にDataURL形式でセットして、読み込み開始
image.src = `data:image/svg+xml;charset=utf-8;base64,${svgData}`;
});
}
</code></pre>
<p>あとは、好きなタイミングで呼び出せばOK。</p>
<p>Nuxt.jsでの例はこんな感じ。</p>
<pre><code class="html"><template>
<div>
<!-- document.getElementByIdできるように、idをつけておく -->
<svg id="svg" viewbox="0 0 1200 630" width="1200" height="630" style="background-color: lightgray;">
<rect x="550" y="265" width="100" height="100" fill="blue" />
<circle cx="550" cy="265" r="30" fill="none" stroke="red" stroke-width="5" />
</svg>
</div>
<div>
<a class="button" @click="saveSVG">画像を保存</a>
</div>
</template>
<script lang="ts">
import { Component, Vue } from "nuxt-property-decorator";
// 初期化済みのfirebaseインスタンス。詳細は略
import firebase from "~/plugins/firebase";
import svg2DataURL from "./svg2DataURL";
@Component()
export default class SaveSvgPage extends Vue {
// 画像を保存する処理
async saveSVG() {
// svgのHTML要素を取得
const elm = document.getElementById("svg");
if (!elm) return;
// さっきの処理: HTML要素からDataURLを取得
const dataURL = await svg2DataURL(elm);
// Cloud Storage for Firebaseへ保存
const filePath = "...保存する先のパス..."
const storage = firebase.storage();
const fileRef = storage.ref().child(filePath);
await fileRef.putString(dataURL, "data_url");
}
</script>
</code></pre>
<p>これでSVGがCloud Storage for FirebaseにPNG画像で保存できる(<em>´ω`</em>)</p>
<h4 id="ハマったポイント"><a href="#%E3%83%8F%E3%83%9E%E3%81%A3%E3%81%9F%E3%83%9D%E3%82%A4%E3%83%B3%E3%83%88">ハマったポイント</a></h4>
<h5 id="1. 画像化するときにhtml2canvasを使うといいかんじにならない"><a href="#1.+%E7%94%BB%E5%83%8F%E5%8C%96%E3%81%99%E3%82%8B%E3%81%A8%E3%81%8D%E3%81%ABhtml2canvas%E3%82%92%E4%BD%BF%E3%81%86%E3%81%A8%E3%81%84%E3%81%84%E3%81%8B%E3%82%93%E3%81%98%E3%81%AB%E3%81%AA%E3%82%89%E3%81%AA%E3%81%84">1. 画像化するときにhtml2canvasを使うといいかんじにならない</a></h5>
<p>画像化する方法で<a target="_blank" rel="nofollow noopener" href="https://html2canvas.hertzen.com/">html2canvas</a>もあるけど、うまくいかなかった。。</p>
<p>スクロール位置や画面サイズによって、<br />
うまく撮れるときと撮れない時があって、この方法に。。</p>
<h5 id="2. ブラウザ上と保存した画像が違う"><a href="#2.+%E3%83%96%E3%83%A9%E3%82%A6%E3%82%B6%E4%B8%8A%E3%81%A8%E4%BF%9D%E5%AD%98%E3%81%97%E3%81%9F%E7%94%BB%E5%83%8F%E3%81%8C%E9%81%95%E3%81%86">2. ブラウザ上と保存した画像が違う</a></h5>
<p>この後出てくる小ネタ集でスタイルで装飾する方法を使ったところ、<br />
うまくいかない感じに。。</p>
<p>svg内にsytleをもたせたらうまくいった(<em>´ω`</em>)</p>
<p>ブラウザ上 = 全体のCSSが適用<br />
保存画像 = SVG配下のCSSのみ適用</p>
<p>なので、SVGだけで完結しないといけない...</p>
<p>フォントとかも指定しないと、見た目が変わってしまう。。</p>
<h5 id="3. 背景画像など外部リンクがあるとうまく保存できない..."><a href="#3.+%E8%83%8C%E6%99%AF%E7%94%BB%E5%83%8F%E3%81%AA%E3%81%A9%E5%A4%96%E9%83%A8%E3%83%AA%E3%83%B3%E3%82%AF%E3%81%8C%E3%81%82%E3%82%8B%E3%81%A8%E3%81%86%E3%81%BE%E3%81%8F%E4%BF%9D%E5%AD%98%E3%81%A7%E3%81%8D%E3%81%AA%E3%81%84...">3. 背景画像など外部リンクがあるとうまく保存できない...</a></h5>
<p>Canvasの仕様っぽく、外部リンクでCORSで引っかかると画像が表示されないよう...</p>
<p>SVGで完結するように、読み込んだ画像をDataURL形式で指定するようにしたらうまくいった(<em>´ω`</em>)</p>
<p>「<strong>SVGで完結</strong>」が大事らしい。。</p>
<h2 id="小ネタ集"><a href="#%E5%B0%8F%E3%83%8D%E3%82%BF%E9%9B%86">小ネタ集</a></h2>
<h3 id="svg内でstyleが使える"><a href="#svg%E5%86%85%E3%81%A7style%E3%81%8C%E4%BD%BF%E3%81%88%E3%82%8B">svg内でstyleが使える</a></h3>
<p><code><style></code>タグがあるらしい。<br />
classを設定して、いろいろできる。便利。</p>
<pre><code class="html"><html>
<body>
<svg viewbox="0 0 1200 630" width="1200" height="630" style="background-color: lightgray;" >
<style>
.rect {
fill: green;
}
</style>
<rect class="rect" x="600" y="315" width="100" height="100"/>
</svg>
</body>
</html>
</code></pre>
<h3 id="折り返し文字を表示してみる(foreignObject)"><a href="#%E6%8A%98%E3%82%8A%E8%BF%94%E3%81%97%E6%96%87%E5%AD%97%E3%82%92%E8%A1%A8%E7%A4%BA%E3%81%97%E3%81%A6%E3%81%BF%E3%82%8B%28foreignObject%29">折り返し文字を表示してみる(foreignObject)</a></h3>
<p>SVGにも<code><text></code>があるけど、自動折り返しに対応していない。<br />
文字数を計算して自分で分割すればできるけど、めんどくさい。。</p>
<p>文字の折り返ししたい場合、<code><foreignObject></code>という<br />
HTMLを追加できるのがあるので、それを使うといいらしい。</p>
<p>styleでfont-sizeとかも設定できるのでいろいろできそう(<em>´ω`</em>)</p>
<pre><code class="html"><html>
<body>
<svg viewbox="0 0 1200 630" width="1200" height="630" style="background-color: lightgray;" >
<style>
@import url("https://fonts.googleapis.com/css?family=Noto+Sans+JP:500&display=swap&subset=japanese");
.item {
font-family: "Noto Sans JP", sans-serif;
font-size: 60px;
border: 2px solid green;
}
</style>
<foreignObject
requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"
width="200"
height="200"
x="500"
y="215"
>
<div class="item">
こんにちは
</div>
</foreignObject>
</svg>
</body>
</html>
</code></pre>
<p>こんな感じ</p>
<p><a href="https://crieit.now.sh/upload_images/c4318acbc1f8d438ed7eea4a3e11257a5e2a8da3252f2.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/c4318acbc1f8d438ed7eea4a3e11257a5e2a8da3252f2.png?mw=700" alt="スクリーンショット 2020-01-23 17.44.58.png" /></a></p>
<p>ただ、foreignObjectはサイズを自動計算してくれるわけではないので、<br />
widthとheightを設定しないといけない。</p>
<h3 id="画像を表示する(image)"><a href="#%E7%94%BB%E5%83%8F%E3%82%92%E8%A1%A8%E7%A4%BA%E3%81%99%E3%82%8B%28image%29">画像を表示する(image)</a></h3>
<p>SVGで画像を使うときは、<code><image></code>を使う。<br />
<code><img></code>とは違う。</p>
<pre><code class="html"><html>
<body>
<svg viewbox="0 0 1200 630" width="1200" height="630" style="background-color: lightgray;" >
<image
xlink:href="https://mdn.mozillademos.org/files/6457/mdn_logo_only_color.png"
width="200"
height="200"
x="500"
y="215"
/>
</svg>
</body>
</html>
</code></pre>
<p>こんな感じ。</p>
<p><a href="https://crieit.now.sh/upload_images/a6f51a0f42f98ba2aa818a0116e59f605e2a8db81f639.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/a6f51a0f42f98ba2aa818a0116e59f605e2a8db81f639.png?mw=700" alt="スクリーンショット 2020-01-23 17.48.11.png" /></a></p>
<p>ハマったポイントにも書いたとおり、このままCanvasに書き出すと、<br />
保存時に画像が表示されないので、動的にDataURLを取得してセットするといい感じ。</p>
<pre><code class="html"><html>
<body>
<svg viewbox="0 0 1200 630" width="1200" height="630" style="background-color: lightgray;" >
<image
xlink:href="data:image/svg+xml;charset=utf-8;base64,...略"
width="200"
height="200"
x="500"
y="215"
/>
</svg>
</body>
</html>
</code></pre>
<p>URLからDataURLに変換するのは、こんな感じ。</p>
<pre><code class="typescript">// svg2DataURL.ts
/**
* URLをDataURLに変換
* @param 変換したい画像のURL
*/
export default function url2DataURL(
url: string
): Promise<HTMLCanvasElement> {
return new Promise((resolve, reject) => {
// 1. Canvasを用意する
const canvas = document.createElement("canvas");
canvas.width = 1200;
canvas.height = 630;
const ctx = canvas.getContext("2d");
if (!ctx) {
reject(Error("Create Canvas Error..."));
return;
}
// 2. SVGを読み込む<image>を用意する
const image = new Image();
image.decoding = "async";
image.onload = () => {
// 4. 読み込みが完了したら、Canvasに書き出して、
ctx.drawImage(image, 0, 0, 1200, 630);
// 5. CanvasからDataURLを取得する
resolve(canvas.toDataURL());
};
image.onerror = e => reject(e);
// 3. 作成した<image>にURLでセットして、読み込み開始
image.src = url;
});
}
</code></pre>
<h3 id="画面サイズに合うように表示する(resize対応)"><a href="#%E7%94%BB%E9%9D%A2%E3%82%B5%E3%82%A4%E3%82%BA%E3%81%AB%E5%90%88%E3%81%86%E3%82%88%E3%81%86%E3%81%AB%E8%A1%A8%E7%A4%BA%E3%81%99%E3%82%8B%28resize%E5%AF%BE%E5%BF%9C%29">画面サイズに合うように表示する(resize対応)</a></h3>
<p>OGP画像としていい感じのサイズで保存したいけど、<br />
表示するときは画面サイズにあった感じにしたい。。</p>
<p>svgにstyleが使えるので、リサイズ時にスケールを計算して、<br />
<code>transform: scale();</code>で縮小する感じにしてみた。</p>
<pre><code class="html"><template>
<div id="svg-wrapper" class="svg-wrapper">
<svg class="svg-content" :viewbox="`0 0 ${svgWidth} ${svgHeight}`"
:width="svgWidth" :height="svgHeight" :style="style">
<rect x="550" y="265" width="100" height="100" fill="blue" />
<circle cx="550" cy="265" r="30" fill="none" stroke="red" stroke-width="5" />
</svg>
</div>
</template>
<script lang="ts">
import { Component, Vue } from "nuxt-property-decorator";
@Component()
export default class SvgPage extends Vue {
private svgWidth: number = 1200;
private svgHeight: number = 630;
private scale: number = 1;
mounted() {
// マウント時にリサイズする
this.$nextTick(() => this.handleResize());
// windowのresizeイベントのリスナーに登録して、
// 画面サイズが変わったら、スケールを再計算するようにする
window.addEventListener("resize", this.handleResize);
}
beforeDestroy() {
// 破棄されるときに、リスナーの登録を解除する
window.removeEventListener("resize", this.handleResize);
}
// リサイズ用のスケールを計算する処理
private handleResize() {
const elm = document.getElementById("svg-wrapper");
if (!elm) return;
this.rect = elm.getBoundingClientRect();
this.scale = this.rect.width / this.svgWidth;
}
// ****************************************************
// * computed
// ****************************************************
private get style() {
// 計算したスケールで縮小するようにtransformを設定する
return { transform: `scale(${this.scale})` };
}
}
</script>
<style>
svg {
transform-origin: 0 0;
}
.svg-wrapper {
position: relative;
width: 100%;
height: auto;
}
.svg-wrapper:before {
content: "";
display: block;
padding-top: 52.5%; /* 630 / 1200 x 100 */
}
.svg-content {
position: absolute;
top: 0;
left: 0;
}
</style>
</code></pre>
<p>ただ、このまま保存すると縮小されたままになるので、<br />
deepコピーでクローンして、transformをクリアしてから書き出すようにする。</p>
<pre><code class="typescript">// svg2DataURL.ts
/**
* svgをpngに変換
* @param svgElement <svg>のHTML要素
*/
export default function svg2DataURL(
svgElement: HTMLElement
): Promise<HTMLCanvasElement> {
return new Promise((resolve, reject) => {
// deepコピーでクローン
const elm = svgElement.cloneNode(true) as HTMLElement;
// transformをクリア
elm.style.transform = "";
// ... 略
// 3. <svg>を文字列に変換
// ※ transformを削除したelmでsvgの文字列を取得
const svgXml = new XMLSerializer().serializeToString(elm);
// ... 略
});
}
</code></pre>
<p>以上!!</p>
<h2 id="こんなのつくってます!!"><a href="#%E3%81%93%E3%82%93%E3%81%AA%E3%81%AE%E3%81%A4%E3%81%8F%E3%81%A3%E3%81%A6%E3%81%BE%E3%81%99%21%21">こんなのつくってます!!</a></h2>
<p>積読用の読書管理アプリ 『積読ハウマッチ』をリリースしました!<br />
<a target="_blank" rel="nofollow noopener" href="https://tsundoku.site">積読ハウマッチ</a>は、Nuxt.js+Firebaseで開発してます!</p>
<p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/572d4947-f40b-e4dc-1c9c-bc584cd2a66c.png" width="200"/></p>
<p>もしよかったら、遊んでみてくださいヽ(=´▽`=)ノ</p>
<p>要望・感想・アドバイスなどあれば、<br />
公式アカウント(<a target="_blank" rel="nofollow noopener" href="https://twitter.com/MemoryLoverz">@MemoryLoverz</a>)や開発者(<a target="_blank" rel="nofollow noopener" href="https://twitter.com/kira_puka">@kira_puka</a>)まで♪</p>
<h1 id="参考にしたサイト様"><a href="#%E5%8F%82%E8%80%83%E3%81%AB%E3%81%97%E3%81%9F%E3%82%B5%E3%82%A4%E3%83%88%E6%A7%98">参考にしたサイト様</a></h1>
<ul>
<li><a target="_blank" rel="nofollow noopener" href="https://developer.mozilla.org/ja/docs/Web/SVG/Element/image"> - SVG: Scalable Vector Graphics | MDN</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://developer.mozilla.org/ja/docs/Web/HTML/CORS_enabled_image">画像とキャンバスをオリジン間で利用できるようにする - HTML: HyperText Markup Language | MDN</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://developer.mozilla.org/ja/docs/Web/SVG/Element/foreignObject"> - SVG: Scalable Vector Graphics | MDN</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://kamoqq.info/post/nodejs-image-processor-sharp/">Node.js向け画像編集ライブラリSharp - kamoqq.info</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://sharp.pixelplumbing.com/">sharp - High performance Node.js image processing</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://forum.vuejs.org/t/does-vue-2-0-break-svg-foreignobject/2566">Does vue 2.0 break SVG foreignObject? - Get Help - Vue Forum</a></li>
<li><a target="_blank" rel="nofollow noopener" href="http://nakajmg.hatenablog.com/entry/2019/08/30/133330">Vueでsvgファイルをいい感じに扱う - じまろぐ</a></li>
<li><a target="_blank" rel="nofollow noopener" href="http://cocu.hatenablog.com/entry/2014/01/14/214917">svgにhtmlを組み込んで、テキストを折り返したりcanvasを使ったり - cocuh's note</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://inside.pixiv.blog/subal/7123">文字レイヤーを支える技術 - pixiv inside</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/yuukive/items/ede7c087843d2f7ef979">VueコンポーネントでWindowサイズ変更検知&値取得 - Qiita</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/haribote/items/b17d46b9679ce2fb2712">一発芸!SVGでHTMLを画像化する - Qiita</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://developer.mozilla.org/ja/docs/Web/SVG/Tutorial/SVG_Fonts">SVG Fonts - SVG: Scalable Vector Graphics | MDN</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://blanche-toile.com/web/css-font-face">CSSの@font-faceでGoogle Fontsのwebフォントを利用する方法 | Free Style</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://developer.mozilla.org/ja/docs/Web/API/Window/onresize">window.onresize - Web API | MDN</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/ryounagaoka/items/a98f59347ed758743b8d">CSSだけでアスペクト比を固定するテク - Qiita</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/serinuntius/items/3017fb6ef51cd47352f6">Vue.jsとFirebaseでOGP画像生成系のサービスを爆速で作ろう - Qiita</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/you8/items/bc5cbe887101863b242b">画像生成してOGPに設定する - Qiita</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://creive.me/archives/17653/#word-breakoverflow-wrap">【CSS】 テキストを折り返す方法!自動で改行・レスポンシブにも対応 | creive【クリーブ】</a></li>
</ul>
きらぷか@積読ハウマッチ/SSSAPIなど
tag:crieit.net,2005:PublicArticle/15666
2020-01-07T21:31:01+09:00
2020-01-07T21:31:01+09:00
https://crieit.net/posts/SPA-Nuxt-Firebase-Hosting-Netlify
SPAなNuxtアプリをFirebase HostingからNetlifyに移行してみた
<p>Netlifyがいいと聞いて、Firebase Hostingから移行してみたときの備忘録。</p>
<p>NetfilyのプレレンダリングがあるのでOGP芸が楽。<br />
昔、<a target="_blank" rel="nofollow noopener" href="https://www.memory-lovers.blog/entry/2019/08/07/150000">こんな記事</a>を書いたけど、Functionsで頑張らなくてもよくなった。</p>
<p>使ってみてよかったので、移行したときにやったことをまとめてみる。</p>
<h2 id="移行するときにやったこと"><a href="#%E7%A7%BB%E8%A1%8C%E3%81%99%E3%82%8B%E3%81%A8%E3%81%8D%E3%81%AB%E3%82%84%E3%81%A3%E3%81%9F%E3%81%93%E3%81%A8">移行するときにやったこと</a></h2>
<h4 id="nuxt.config.tsなどの変更"><a href="#nuxt.config.ts%E3%81%AA%E3%81%A9%E3%81%AE%E5%A4%89%E6%9B%B4">nuxt.config.tsなどの変更</a></h4>
<h5 id="1. Nuxtなどの公式サイトを見て設定"><a href="#1.+Nuxt%E3%81%AA%E3%81%A9%E3%81%AE%E5%85%AC%E5%BC%8F%E3%82%B5%E3%82%A4%E3%83%88%E3%82%92%E8%A6%8B%E3%81%A6%E8%A8%AD%E5%AE%9A">1. Nuxtなどの公式サイトを見て設定</a></h5>
<p>Firebase Hostingで設定したのとかぶっているところもあり、nuxt.config.tsは変更なし。</p>
<p>参考: <a target="_blank" rel="nofollow noopener" href="https://ja.nuxtjs.org/faq/netlify-deployment/">Netlify へデプロイするには? - NuxtJS</a></p>
<h5 id="2. _redirectsファイルの作成"><a href="#2.+_redirects%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%81%AE%E4%BD%9C%E6%88%90">2. _redirectsファイルの作成</a></h5>
<p>Netlifyでは、index.html以外は404になるので、<br />
SPAの場合は<code>static/_redirecs</code>というファイルを作成して、<br />
以下の内容を記載しておく。</p>
<pre><code>/* /index.html 200
</code></pre>
<p>参考] <a target="_blank" rel="nofollow noopener" href="https://docs.netlify.com/routing/redirects/rewrites-proxies/#history-pushstate-and-single-page-apps">Rewrites and proxies | Netlify Docs</a></p>
<h5 id="3. Netlify側の設定"><a href="#3.+Netlify%E5%81%B4%E3%81%AE%E8%A8%AD%E5%AE%9A">3. Netlify側の設定</a></h5>
<p>あとは、Netlifyにアクセスして、 「<a target="_blank" rel="nofollow noopener" href="https://ja.nuxtjs.org/faq/netlify-deployment/#spa-%E3%83%A2%E3%83%BC%E3%83%89%E3%81%A7%E7%94%9F%E6%88%90%E3%81%95%E3%82%8C%E3%81%9F%E3%82%B5%E3%82%A4%E3%83%88%E3%81%AE%E5%A0%B4%E5%90%88">SPA モードで生成されたサイトの場合</a>」にも書いてあるとおりの内容をNetlifyの設定に入力すればOK。</p>
<p>迷わなければ、5分くらいでできて、masterにpushと更新される(<em>´ω`</em>)</p>
<h4 id="プレレンダリング"><a href="#%E3%83%97%E3%83%AC%E3%83%AC%E3%83%B3%E3%83%80%E3%83%AA%E3%83%B3%E3%82%B0">プレレンダリング</a></h4>
<p>ベータ版だけど、Netlifyでプレレンダリングしてくれる機能がある。</p>
<p><a target="_blank" rel="nofollow noopener" href="https://www.memory-lovers.blog/entry/2019/08/07/150000">この記事</a>に書いてある感じで、<br />
「FunctionsでheadだけのHTMLを生成→リダイレクト」<br />
としていたのが不要になる感じ。</p>
<h5 id="Netlifyの画面で設定"><a href="#Netlify%E3%81%AE%E7%94%BB%E9%9D%A2%E3%81%A7%E8%A8%AD%E5%AE%9A">Netlifyの画面で設定</a></h5>
<p>設定は簡単で、Netlify上で設定画面で有効化すればOK。<br />
参考: <a target="_blank" rel="nofollow noopener" href="https://docs.netlify.com/site-deploys/post-processing/prerendering/#set-up-prerendering">Prerendering | Netlify Docs</a></p>
<h5 id="その他の修正点"><a href="#%E3%81%9D%E3%81%AE%E4%BB%96%E3%81%AE%E4%BF%AE%E6%AD%A3%E7%82%B9">その他の修正点</a></h5>
<p>あとは、以下の修正。</p>
<ol>
<li>OPGのためにrouterでリダイレクトしていた設定を削除</li>
<li>各ページでメタ情報を適切になるように変更</li>
</ol>
<p>また、上で書いたとおり、<code>static/_redirecs</code>で</p>
<pre><code>/* /index.html 200
</code></pre>
<p><strong>これを設定しておかないとボットがアクセスしたときに404になってOGPがでない</strong>。。</p>
<p>(これに気づかずかなりハマった。。)</p>
<h5 id="小ネタ: プレレンダリングの結果を確認する"><a href="#%E5%B0%8F%E3%83%8D%E3%82%BF%3A+%E3%83%97%E3%83%AC%E3%83%AC%E3%83%B3%E3%83%80%E3%83%AA%E3%83%B3%E3%82%B0%E3%81%AE%E7%B5%90%E6%9E%9C%E3%82%92%E7%A2%BA%E8%AA%8D%E3%81%99%E3%82%8B">小ネタ: プレレンダリングの結果を確認する</a></h5>
<p>プレレンダリングするかどうかは、UAを見て判断しているらしく、</p>
<ul>
<li>UAを指定してすると、プレレンダリングされた結果を確認でき、</li>
<li>UAを指定してないと、プレレンダリングなしの結果になる</li>
</ul>
<p>curlだとこんな感じで、<code>-A twitterbot</code>を指定するとよい。</p>
<pre><code class="shell">$ curl -A twitterbot https://example.com/hoge/fuga -o fuga.html
</code></pre>
<p>参考: <a target="_blank" rel="nofollow noopener" href="https://community.netlify.com/t/common-issue-understanding-and-debugging-prerendering/150">[Common Issue] Understanding and debugging prerendering</a></p>
<p>毎回、<a target="_blank" rel="nofollow noopener" href="https://cards-dev.twitter.com/validator">Twitter Card Validator</a>で確認するのはめんどくさいので、この方法だと便利。</p>
<h3 id="ページが存在しないURLの対応"><a href="#%E3%83%9A%E3%83%BC%E3%82%B8%E3%81%8C%E5%AD%98%E5%9C%A8%E3%81%97%E3%81%AA%E3%81%84URL%E3%81%AE%E5%AF%BE%E5%BF%9C">ページが存在しないURLの対応</a></h3>
<p>積読ハウマッチでは、少し特殊なことをしていて、<br />
一部のURLはheadだけのHTMLしか存在しないページがいくつかある。</p>
<p>なので、その部分を修正していく。方法としては以下の感じ。</p>
<ol>
<li>nuxt.config.tsでrouterに、クエリパラメタ付きでリダイレクトを追加する
<ul>
<li><a target="_blank" rel="nofollow noopener" href="https://router.vuejs.org/ja/guide/essentials/redirect-and-alias.html">リダイレクトとエイリアス | Vue Router</a></li>
</ul></li>
<li><code>asyncData()</code>でクエリパラメタを受け取り、<code>created()</code>で削除
<ul>
<li><code>this.$router.push({ query: {} });</code></li>
<li><a target="_blank" rel="nofollow noopener" href="https://www.kimullaa.com/entry/2018/01/27/183743">Vuejs vue-router クエリパラメータの一部だけを取り除く - SIerだけど技術やりたいブログ</a></li>
</ul></li>
</ol>
<h4 id="動的なサイトマップの対応"><a href="#%E5%8B%95%E7%9A%84%E3%81%AA%E3%82%B5%E3%82%A4%E3%83%88%E3%83%9E%E3%83%83%E3%83%97%E3%81%AE%E5%AF%BE%E5%BF%9C">動的なサイトマップの対応</a></h4>
<p>書籍やユーザのページなどは日々増えていくので、<br />
サイトマップも動的に生成するようにしている。</p>
<p>Hostingの場合はFunctionsを呼び出せたけれど、<br />
Netlifyに移行すると、それができないので対応が必要。</p>
<p><strong>netlifyの_redirectsを使って直接Functionsへリダイレクトする方法で対応した</strong>。</p>
<p>こんな感じで各URLを該当のFunctionsにリダイレクトさせておく。</p>
<pre><code>/sitemap https://us-central1-<project-id>.cloudfunctions.net/sitemap
/* /index.html 200
</code></pre>
<h5 id="sitemap-moduleだとダメだった..."><a href="#sitemap-module%E3%81%A0%E3%81%A8%E3%83%80%E3%83%A1%E3%81%A0%E3%81%A3%E3%81%9F...">sitemap-moduleだとダメだった...</a></h5>
<p>sitemap-moduleで動的なページの対応方法も乗っているが、<br />
SPAなのでgenerateされてしまう...</p>
<ul>
<li>参考
<ul>
<li><a target="_blank" rel="nofollow noopener" href="https://github.com/nuxt-community/sitemap-module#readme">nuxt-community/sitemap-module: Sitemap Module for Nuxt.js</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/bucchi49/items/d271c4010a3f6c900926">Nuxt.jsで静的ファイル生成時にサイトマップも自動生成する方法 - Qiita</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/cortyuming/items/b4640b371e4ceb37ae47">Nuxt.js + Firestore で動的サイトマップ - Qiita</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/sauzar18/items/2ea958043eb6758c4f83">Nuxt.jsでサイトマップを動的に設定する方法 - Qiita</a></li>
</ul></li>
</ul>
<h4 id="カスタムドメインの設定"><a href="#%E3%82%AB%E3%82%B9%E3%82%BF%E3%83%A0%E3%83%89%E3%83%A1%E3%82%A4%E3%83%B3%E3%81%AE%E8%A8%AD%E5%AE%9A">カスタムドメインの設定</a></h4>
<p>Netlifyの設定画面に従い、カスタムドメインを設定していく。</p>
<p>ALIASを設定するようにいわれるが、<br />
お名前ドットコムはALIASを設定できないらしいので、<br />
NetlifyのDNSを使う形で対応</p>
<p><a target="_blank" rel="nofollow noopener" href="https://qiita.com/hitochan777/items/7acce5b4398af7f4c358">参考にした記事</a>でも書いてあるとおり、<br />
Aレコードでもできるが、CDNを活用できないらしいので同様の対応</p>
<p>あとは、設定画面にも書いてあるとおり、<br />
<code>*.netlify.com</code>から設定したドメインへリダイレクトを追加しておく。</p>
<pre><code>[https://tsundoku.netlify.com/*](https://tsundoku.netlify.com/*) https://tsundoku.site/:splat 301!
/sitemap https://us-central1-<project-id>.cloudfunctions.net/sitemap
/* /index.html 200
</code></pre>
<p>これでリダイレクトされるようになるので、あとはDNSの反映を待てばOK!</p>
<p>以上!!</p>
<h2 id="こんなのつくってます!!"><a href="#%E3%81%93%E3%82%93%E3%81%AA%E3%81%AE%E3%81%A4%E3%81%8F%E3%81%A3%E3%81%A6%E3%81%BE%E3%81%99%21%21">こんなのつくってます!!</a></h2>
<p>積読用の読書管理アプリ 『積読ハウマッチ』をリリースしました!<br />
<a target="_blank" rel="nofollow noopener" href="https://tsundoku.site">積読ハウマッチ</a>は、Nuxt.js+Firebaseで開発してます!</p>
<p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/572d4947-f40b-e4dc-1c9c-bc584cd2a66c.png" width="200"/></p>
<p>もしよかったら、遊んでみてくださいヽ(=´▽`=)ノ</p>
<p>要望・感想・アドバイスなどあれば、<br />
公式アカウント(<a target="_blank" rel="nofollow noopener" href="https://twitter.com/MemoryLoverz">@MemoryLoverz</a>)や開発者(<a target="_blank" rel="nofollow noopener" href="https://twitter.com/kira_puka">@kira_puka</a>)まで♪</p>
<h1 id="参考にしたサイト"><a href="#%E5%8F%82%E8%80%83%E3%81%AB%E3%81%97%E3%81%9F%E3%82%B5%E3%82%A4%E3%83%88">参考にしたサイト</a></h1>
<ul>
<li><a href="https://crieit.net/posts/Netlify-404">Netlifyを使ってたらルートパス以外が404になった話とその解決方法 - Crieit</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://docs.netlify.com/routing/redirects/rewrites-proxies/#limitations">Rewrites and proxies | Netlify Docs</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://prerender.io/documentation/best-practices">Prerender - Dynamic Rendering for JavaScript Website SEO</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://community.netlify.com/t/common-issue-understanding-and-debugging-prerendering/150">[Common Issue] Understanding and debugging prerendering - Support / Common Issues - Netlify Community</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/NaokiIshimura/items/64e060ccc244e38d0c15#%E3%82%AB%E3%82%B9%E3%82%BF%E3%83%A0%E3%83%89%E3%83%A1%E3%82%A4%E3%83%B3%E3%81%AE%E8%A8%AD%E5%AE%9A">【Netlify】カスタムドメインを設定する - Qiita</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/don-bu-rakko/items/8eb30c7e9a3f9531ba16">Netlifyにお名前.comで取得した独自ドメインを設定する - Qiita</a></li>
</ul>
きらぷか@積読ハウマッチ/SSSAPIなど
tag:crieit.net,2005:PublicArticle/15656
2019-12-29T19:07:03+09:00
2019-12-29T19:07:50+09:00
https://crieit.net/posts/Nuxt-Firebase-PWA-Service-Worker
Nuxt+Firebaseでセッション管理: PWA(Service Worker)編
<p>FirebaseとSSRなNuxt.jsでアプリを作っていて、<br />
クライアント側で認証チェックするとFirebaseの初期化などでラグが...<br />
サーバ側で認証情報とかを取得してもう少しなんとかできないかなと。</p>
<p>まだベータっぽい?けど、公式の以下の内容を試してみたときの備忘録。</p>
<ul>
<li><a target="_blank" rel="nofollow noopener" href="https://firebase.google.com/docs/auth/web/service-worker-sessions?hl=ja">Service Worker によるセッション管理 | Firebase</a></li>
</ul>
<h3 id="よく出てくる言葉"><a href="#%E3%82%88%E3%81%8F%E5%87%BA%E3%81%A6%E3%81%8F%E3%82%8B%E8%A8%80%E8%91%89">よく出てくる言葉</a></h3>
<p>単語はよく聞くけど、ちゃんと見てなかったので、ざっくりとしたまとめ</p>
<ul>
<li>PWA: ネイティブアプリみたいなUXを提供するWebアプリ
<ul>
<li><a target="_blank" rel="nofollow noopener" href="https://developers.google.com/web/progressive-web-apps/">Progressive Web Apps | Google Developers</a></li>
</ul></li>
<li>Service Worker: PWAを実現するための基盤技術。独自のライフサイクルを持ってる
<ul>
<li><a target="_blank" rel="nofollow noopener" href="https://developers.google.com/web/fundamentals/primers/service-workers?hl=ja">Service Worker の紹介 | Web Fundamentals | Google Developers</a></li>
</ul></li>
<li>Workbox: PWAでよく使うコード(ボイラープレート)やベストプラクティスを提供するライブラリ
<ul>
<li><a target="_blank" rel="nofollow noopener" href="https://developers.google.com/web/tools/workbox/">Workbox | Google Developers</a></li>
</ul></li>
<li>Nuxt PWA: NuxtでPWAするときに使うプラグイン。workboxを使ったService Worker(sw.js)とかを生成してくれる
<ul>
<li><a target="_blank" rel="nofollow noopener" href="https://pwa.nuxtjs.org/">⚡ Nuxt PWA</a></li>
</ul></li>
</ul>
<h3 id="Nuxt PWAを使ってみる"><a href="#Nuxt+PWA%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6%E3%81%BF%E3%82%8B">Nuxt PWAを使ってみる</a></h3>
<h4 id="インストール"><a href="#%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB">インストール</a></h4>
<pre><code class="shell">$ npm install @nuxtjs/pwa
</code></pre>
<h4 id="設定"><a href="#%E8%A8%AD%E5%AE%9A">設定</a></h4>
<p>設定は簡単。modulesに<code>@nuxtjs/pwa</code>を追加するだけ。</p>
<pre><code class="typescript">// nuxt.config.ts
import { Configuration } from "@nuxt/types";
const config: Configuration = {
// ...略
modules: [
// ... 略
"@nuxtjs/pwa",
],
// ...略
};
export default config;
</code></pre>
<h3 id="Firebase Authと組み合わせる"><a href="#Firebase+Auth%E3%81%A8%E7%B5%84%E3%81%BF%E5%90%88%E3%82%8F%E3%81%9B%E3%82%8B">Firebase Authと組み合わせる</a></h3>
<p>このあたりを見つつ、Firebase Authの情報を扱えるようにする。<br />
- <a target="_blank" rel="nofollow noopener" href="https://firebase.google.com/docs/auth/web/service-worker-sessions?hl=ja">Service Worker によるセッション管理 | Firebase</a><br />
- <a target="_blank" rel="nofollow noopener" href="https://qiita.com/daishinkawa/items/915d918aba6bf7849b21#auth%E3%83%A9%E3%82%A4%E3%83%96%E3%83%A9%E3%83%AA%E3%81%AE%E7%B5%84%E3%81%BF%E8%BE%BC%E3%81%BF">Firebase (Hosting × Functions) × Nuxt.js (universal) で ユーザ認証のベストプラクティスを探る旅 その2 - Qiita</a></p>
<p>流れ的には、以下の通り。</p>
<ol>
<li>Firebase Auth用のService Workerの作成して、リクエストにIDトークンを付与するように変更</li>
<li>作成したService Workerを使うよう、nuxt.config.tsに設定を追加</li>
<li></li>
</ol>
<h4 id="Firebase Auth用のService Workerの作成"><a href="#Firebase+Auth%E7%94%A8%E3%81%AEService+Worker%E3%81%AE%E4%BD%9C%E6%88%90">Firebase Auth用のService Workerの作成</a></h4>
<p>長めだけど、ほぼ<a target="_blank" rel="nofollow noopener" href="https://firebase.google.com/docs/auth/web/service-worker-sessions?hl=ja">公式ドキュメント</a>のまま。</p>
<pre><code class="javascript">// ~/static/sw-firebase-auth.js
// firebaseを初期化
firebase.initializeApp({
apiKey: /* API_KEY */,
authDomain: /* AUTH_DOMAIN */,
databaseURL: /* DATABASE_URL */,
projectId: /* PROJECT_ID */,
storageBucket: /* STORAGE_BUCKET */,
messagingSenderId: /* MESSAGING_SENDER_ID */,
appId: /* APP_ID */,
measurementId: /* AUTH_DOMAIN */
});
// onAuthStateChanged()で現在のuserからidTokenを取得
const getIdToken = () => {
return new Promise((resolve, reject) => {
const unsubscribe = firebase.auth().onAuthStateChanged(user => {
unsubscribe();
if (user) {
user.getIdToken().then(
idToken => resolve(idToken),
error => resolve(null)
);
} else {
resolve(null);
}
});
});
};
// URLからルートのURLを取得する処理
const getOriginFromUrl = url => {
// https://stackoverflow.com/questions/1420881/how-to-extract-base-url-from-a-string-in-javascript
const pathArray = url.split("/");
const protocol = pathArray[0];
const host = pathArray[2];
return protocol + "//" + host;
};
/**
* Service Workderのライフサイクルでfetchしたときの処理
*/
self.addEventListener("fetch", event => {
// リクエストをラップして、ヘッダにFirebase AuthのIdTokenを追加する処理
const requestProcessor = idToken => {
let req = event.request;
// URLを取得して、httpsもしくはlocalhostかなどをチェック
if (self.location.origin == getOriginFromUrl(event.request.url) &&
(self.location.protocol == "https:" || self.location.hostname == "localhost") &&
idToken
) {
// ヘッダ情報をクローンする
const headers = new Headers();
for (let entry of req.headers.entries()) {
headers.append(entry[0], entry[1]);
}
// クローンしたヘッダにFirebase AuthのIdTokenを追加
headers.append("Authorization", "Bearer " + idToken);
try {
req = new Request(req.url, {
method: req.method,
headers: headers,
mode: "same-origin",
credentials: req.credentials,
cache: req.cache,
redirect: req.redirect,
referrer: req.referrer,
body: req.body,
bodyUsed: req.bodyUsed,
context: req.context
});
} catch (e) {
console.error(e);
}
}
return fetch(req);
};
// 上の関数を使って、全リクエストでIdTokenの取得し、Firebase AuthのIdTokenを追加ようにする
event.respondWith(getIdToken().then(requestProcessor, requestProcessor));
});
/**
* Service Workderのライフサイクルでactivateしたときの処理
*/
self.addEventListener("activate", event => {
event.waitUntil(clients.claim());
});
</code></pre>
<p>やっていることは、以下のような感じ。</p>
<ol>
<li>リクエストするときに、</li>
<li>現在のユーザからIDトークンを取得して</li>
<li>IDトークンをリクエストヘッダーに追加する</li>
</ol>
<p>ヘッダーにIDトークンが付与されているので、<br />
サーバ側でそれを見て、認証済みかをチェックする。</p>
<h3 id="nuxt.config.tsへの取り込み"><a href="#nuxt.config.ts%E3%81%B8%E3%81%AE%E5%8F%96%E3%82%8A%E8%BE%BC%E3%81%BF">nuxt.config.tsへの取り込み</a></h3>
<p>作成したサービスワーカを使うように、nuxt.config.tsに設定を追加する。<br />
ドキュメントだと<a target="_blank" rel="nofollow noopener" href="https://pwa.nuxtjs.org/modules/workbox.html#adding-custom-service-worker">このあたり</a>を参照。</p>
<pre><code class="typescript">// nuxt.config.ts
import { Configuration } from "@nuxt/types";
const config: Configuration = {
// ...略
modules: [
// ... 略
"@nuxtjs/pwa",
],
workbox: {
// 追加するスクリプトを指定。
// バンドルされないので、CDNのfirebase-appを追加しておく。
importScripts: [
"https://www.gstatic.com/firebasejs/7.6.1/firebase-app.js",
"https://www.gstatic.com/firebasejs/7.6.1/firebase-auth.js",
"sw-firebase-auth.js"
],
// 開発中でもsw.jsが生成されるように設定。
dev: process.env.MODE != "production",
},
// ...略
};
export default config;
</code></pre>
<h4 id="nuxtServerInitなどで認証状態をチェックする"><a href="#nuxtServerInit%E3%81%AA%E3%81%A9%E3%81%A7%E8%AA%8D%E8%A8%BC%E7%8A%B6%E6%85%8B%E3%82%92%E3%83%81%E3%82%A7%E3%83%83%E3%82%AF%E3%81%99%E3%82%8B">nuxtServerInitなどで認証状態をチェックする</a></h4>
<p>IDトークンもJWTデコードすると、UIDを取得できるけれど、<br />
有効かどうかをfirebase-adminでチェックする必要がある。</p>
<p>なので、まずは、firebase-adminのインスタンスを初期化するファイルを用意。</p>
<pre><code class="typescript">// ~/utils/firebaseAdmin.ts
let admin;
if (process.server) {
admin = require("firebase-admin");
if (!admin.apps.length) {
const serviceAccount = require("./path/to/your/key.json");
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
databaseURL: "https://your-database-url.firebaseio.com"
});
}
}
export default admin;
</code></pre>
<p>次にこれを使って、以下をしていく。</p>
<ol>
<li>リクエストヘッダからIDトークンを取得し、</li>
<li>firebase-adminを使ってIDトークンの有効性を確認</li>
</ol>
<p>以下は、<a target="_blank" rel="nofollow noopener" href="https://github.com/championswimmer/vuex-module-decorators">vuex-module-decorators</a>を使ったnuxtServerInitでチェックするサンプル。</p>
<pre><code class="typescript">// ~/store/index.ts
import { Context } from "@nuxt/types";
import { ActionContext } from "vuex/types";
import { ActionTree, Store } from "vuex";
import { initialiseStores } from "~/utils/store-accessor";
export const state = () => ({});
export type RootState = ReturnType<typeof state>;
const initializer = (store: Store<any>) => initialiseStores(store);
export const plugins = [initializer];
export const actions: ActionTree<any, any> = {
async nuxtServerInit(
context: ActionContext<RootState, RootState>,
server: Context
) {
// requestのAuthorizationからIDトークンを取得
const authorizationHeader = req.headers.authorization || "";
const components = authorizationHeader.split(" ");
const token = components.length > 1 ? components[1] : "";
if (!token) return;
// firebase-adminの初期化
const admin = require("~/utils/firebaseAdmin").default;
if (!admin) return;
// IDトークンの検証: 有効期限などをFirebaseでチェック
const decodedClaims = await admin.auth().verifyIdToken(token);
// 検証結果からUIDを取得
const uid = decodedClaims.uid;
// TODO: 認証状態に応じてなにかする
}
};
export * from "~/utils/store-accessor";
</code></pre>
<p>firebase-adminを取得する部分を</p>
<pre><code class="typescript">const admin = require("firebase-admin");
</code></pre>
<p>としていたり、<code>if (process.server)</code>や<code>if (!admin.apps.length)</code>などのチェックをせずにいたら、<br />
クライアント側でもバンドルされていて、うまく動かいない状態に...</p>
<h4 id="注意点"><a href="#%E6%B3%A8%E6%84%8F%E7%82%B9">注意点</a></h4>
<p>これで認証が必要なページでもいい感じSSRできた気がする(<em>´ω`</em>)</p>
<p>ただ、課題が残っていて、スーパーリロード/ハードリロードすると、<br />
Service Workderを介して、リクエストされないので、ヘッダに認証情報が付与されない...</p>
<p>ログを見ていると、スーパーリロード時には、描画されたあとに、Service Workderの登録されているよう。</p>
<p>そういった場合でも、利用したい場合には、従来のセッションCookieを利用する方法がよいかも?</p>
<ul>
<li><a target="_blank" rel="nofollow noopener" href="https://firebase.google.com/docs/auth/admin/manage-cookies?hl=ja">セッション Cookie を管理する | Firebase</a></li>
</ul>
<p>(もし良い方法があれば、教えてほしいです...)</p>
<hr />
<h4 id="おまけ: Service Workerのライフサイクル"><a href="#%E3%81%8A%E3%81%BE%E3%81%91%3A+Service+Worker%E3%81%AE%E3%83%A9%E3%82%A4%E3%83%95%E3%82%B5%E3%82%A4%E3%82%AF%E3%83%AB">おまけ: Service Workerのライフサイクル</a></h4>
<p>ここに書いてあった。<br />
- <a target="_blank" rel="nofollow noopener" href="https://developers.google.com/web/fundamentals/primers/service-workers?hl=ja">Service Worker の紹介 | Web Fundamentals | Google Developers</a></p>
<p><img src="https://developers.google.com/web/fundamentals/primers/service-workers/images/sw-lifecycle.png?hl=ja" width="400px"/></p>
<hr />
<p>以上!!</p>
<h2 id="こんなのつくってます!!"><a href="#%E3%81%93%E3%82%93%E3%81%AA%E3%81%AE%E3%81%A4%E3%81%8F%E3%81%A3%E3%81%A6%E3%81%BE%E3%81%99%21%21">こんなのつくってます!!</a></h2>
<p>積読用の読書管理アプリ 『積読ハウマッチ』をリリースしました!<br />
<a target="_blank" rel="nofollow noopener" href="https://tsundoku.site">積読ハウマッチ</a>は、Nuxt.js+Firebaseで開発してます!</p>
<p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/572d4947-f40b-e4dc-1c9c-bc584cd2a66c.png" width="200"/></p>
<p>もしよかったら、遊んでみてくださいヽ(=´▽`=)ノ</p>
<p>要望・感想・アドバイスなどあれば、<br />
公式アカウント(<a target="_blank" rel="nofollow noopener" href="https://twitter.com/MemoryLoverz">@MemoryLoverz</a>)や開発者(<a target="_blank" rel="nofollow noopener" href="https://twitter.com/kira_puka">@kira_puka</a>)まで♪</p>
<h1 id="参考にしたサイト様"><a href="#%E5%8F%82%E8%80%83%E3%81%AB%E3%81%97%E3%81%9F%E3%82%B5%E3%82%A4%E3%83%88%E6%A7%98">参考にしたサイト様</a></h1>
<ul>
<li><a target="_blank" rel="nofollow noopener" href="https://firebase.google.com/docs/auth/web/service-worker-sessions?hl=ja">Service Worker によるセッション管理 | Firebase</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/daishinkawa/items/915d918aba6bf7849b21#auth%E3%83%A9%E3%82%A4%E3%83%96%E3%83%A9%E3%83%AA%E3%81%AE%E7%B5%84%E3%81%BF%E8%BE%BC%E3%81%BF">Firebase (Hosting × Functions) × Nuxt.js (universal) で ユーザ認証のベストプラクティスを探る旅 その2 - Qiita</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/basho/items/acd6a17bb6e2a2f7a932#section3%E6%96%B0%E8%A6%8F%E7%99%BB%E9%8C%B2google%E3%82%A2%E3%82%AB%E3%82%A6%E3%83%B3%E3%83%88%E3%81%AE%E5%AE%9F%E8%A3%85">SSRモードのNuxtでのFirebase認証 - Qiita</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/y_fujieda/items/f9e765ac9d89ba241154#service-worker">Service Workerの基本とそれを使ってできること - Qiita</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/horo/items/175c8fd7513138308930">ServiceWorkerとCache APIを使ってオフラインでも動くWebアプリを作る - Qiita</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://developers.google.com/web/fundamentals/primers/service-workers/lifecycle?hl=ja">Service Worker のライフサイクル | Web Fundamentals | Google Developers</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://firebase.google.com/docs/auth/admin/manage-sessions">ユーザー セッションの管理 | Firebase</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://firebase.google.com/docs/auth/admin/manage-cookies">Manage Session Cookies | Firebase</a></li>
</ul>
きらぷか@積読ハウマッチ/SSSAPIなど