tag:crieit.net,2005:https://crieit.net/tags/TypeScript/feed 「TypeScript」の記事 - Crieit Crieitでタグ「TypeScript」に投稿された最近の記事 2023-04-11T23:36:31+09:00 https://crieit.net/tags/TypeScript/feed tag:crieit.net,2005:PublicArticle/18423 2023-04-11T23:29:51+09:00 2023-04-11T23:36:31+09:00 https://crieit.net/posts/simple-hr-reporter SmartHRと連携して人事データを可視化するツールを作ってみた <p>個人開発でSmartHRと連携したレポーティングツールを作成してみました。</p> <p><a target="_blank" rel="nofollow noopener" href="https://simple-hr-reporter.saikicks.net/report"><strong>人事データ簡易分析ツール</strong></a></p> <h2 id="概要"><a href="#%E6%A6%82%E8%A6%81">概要</a></h2> <p>このツールはSmartHRと連携してAPI経由でデータを取得して可視化を行うためのレポーティングツールです。</p> <p>SmartHRではフリープラン、労務管理プランでは分析レポートの利用が有料プランに切り替えるなどの対応を行わないと利用できません。</p> <p>オプション機能として提供されていますが、そこまで細かく分析するほどでは無く現状の人事情報を可視化したい、みたいな緩い使い方でも許容できるユーザーさんを対象に簡易的なグラフ出力を提供します。</p> <p>利用にはテナントIDとアクセストークンを利用しますが、サイト内にも記載がある通り<strong>このツールはテナントIDやアクセストークンを保存したりはしません。データ取得時のみに利用します</strong>。</p> <p>そのため画面をリロードしたりするともちろん情報は失われます。</p> <p>再度表示したい場合は都度テナントID、アクセストークンはをもう一度入力していただく手間はありますが…</p> <h2 id="技術構成"><a href="#%E6%8A%80%E8%A1%93%E6%A7%8B%E6%88%90">技術構成</a></h2> <ul> <li>React</li> <li>rebass</li> <li>styled-component</li> <li>recharts</li> <li>TypeScript</li> <li>Vite</li> <li>VItest</li> <li>Axios</li> <li>CloudFlare Pages</li> </ul> <h2 id="レポート出力結果"><a href="#%E3%83%AC%E3%83%9D%E3%83%BC%E3%83%88%E5%87%BA%E5%8A%9B%E7%B5%90%E6%9E%9C">レポート出力結果</a></h2> <p>出力結果は以下のような感じでシンプルに出力しています。</p> <p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/y_saiki/20230324/20230324202730.png" alt="image" /></p> <p>もしご興味あれば是非使ってみてください。</p> <p>次にテナントIDとアクセストークンについて簡単に説明します。</p> <h2 id="テナントIDについて"><a href="#%E3%83%86%E3%83%8A%E3%83%B3%E3%83%88ID%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6">テナントIDについて</a></h2> <p>テナントIDはSmartHRのURLに含まれている以下の部分の文字列です。</p> <pre><code>[https://[テナントID].smarthr.jp/](https://[テナントID].smarthr.jp/) </code></pre> <h2 id="アクセストークンについて"><a href="#%E3%82%A2%E3%82%AF%E3%82%BB%E3%82%B9%E3%83%88%E3%83%BC%E3%82%AF%E3%83%B3%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6">アクセストークンについて</a></h2> <p>アクセストークンは管理画面より作成を行うため以下に手順を載せておきます。</p> <p>SmartHRにログインした後、右上のユーザー名の部分をクリックするとメニューが表示されます。</p> <p>その中の「共通設定」をクリックしてください。</p> <p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/y_saiki/20230324/20230324201148.png" alt="image" /></p> <p>次に左側メニューから「外部システム連携」をクリックします。</p> <p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/y_saiki/20230324/20230324201203.png" alt="image" /></p> <p>API連携より「アクセストークン」をクリックします。</p> <p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/y_saiki/20230324/20230324201159.png" alt="image" /></p> <p>「新規発行」をクリックします</p> <p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/y_saiki/20230324/20230324201155.png" alt="image" /></p> <p>「アクセストークン名」を入力します。利用する用途を名前にしておくと良いです。</p> <p>例えば「分析レポート」とか…</p> <p>必須項目の入力が埋まると「登録」ボタンがクリックできるようになるので登録するとアクセストークンが払い出されます。</p> <p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/y_saiki/20230324/20230324201151.png" alt="image" /></p> <p>これらのテナントIDとアクセストークンを利用して本ツールにて入力して「出力」ボタンをクリックしてもらうとレポートが出力されます。</p> <p>まだまだ表示がおかしい部分があったり、追加したい機能等があるので引き続き開発を進めていきたいと思います!</p> <p>採用計画や人事データを活用して分析を行いたい方は是非活用してみてください。</p> Sassy_1123 tag:crieit.net,2005:PublicArticle/16763 2021-03-22T13:21:27+09:00 2021-03-22T13:21:27+09:00 https://crieit.net/posts/sync-zenn-with-dev-action Zenn の記事を DEV に自動的に同期させる GitHub Action 作ってみた <h1 id="はじめに"><a href="#%E3%81%AF%E3%81%98%E3%82%81%E3%81%AB">はじめに</a></h1> <p>去年 <a target="_blank" rel="nofollow noopener" href="https://dev.to/">DEV</a> のアカウントを作成したものの、今まで全く有効活用出来ていませんでした。</p> <p>DEV には <a target="_blank" rel="nofollow noopener" href="https://dev.to/michaelburrows/comment/125j0">カノニカル URL</a> を設定出来るので、常々 Zenn の記事を投稿する際にクロスポストしたいなと考えておりました。そこで、<strong>Zenn に記事を投稿したら、自動的に DEV にも記事を投稿 & 同期する GitHub Action を作ってみました。</strong><br /> <a target="_blank" rel="nofollow noopener" href="https://github.com/nikaera/sync-zenn-with-dev-action">sync-zenn-with-dev-action</a></p> <p>今回初めて GitHub Action を自作したのですが、その中で得た知見を残す形で記事を書くことにしました。また、GitHub Action は TypeScript で作成しました。</p> <h1 id="開発した GitHub Action の概要"><a href="#%E9%96%8B%E7%99%BA%E3%81%97%E3%81%9F+GitHub+Action+%E3%81%AE%E6%A6%82%E8%A6%81">開発した GitHub Action の概要</a></h1> <p>まずはザッとどのような GitHub Action を作成したのか、概要について説明いたします。</p> <p><strong>GitHub リポジトリで管理している Zenn の記事を DEV に同期して投稿する GitHub Action を作成しました。</strong> その際に DEV へ投稿する記事には Zenn の該当記事へのカノニカル URL も自動で設定できます。これにより DEV と Zenn へ記事をシームレスにクロスポストすることが可能となります。</p> <p>今回作成した GitHub Action を利用するワークフローファイルの一例は下記となります。</p> <pre><code class="yml">name: "Sync all Zenn articles to DEV" on: push: branches: - main jobs: build: runs-on: ubuntu-latest steps: - name: checkout my project uses: actions/checkout@v2 - name: dev.to action step uses: nikaera/sync-zenn-with-dev-action@v1 # id を設定することで、後のジョブで Output で指定した値が参照可能になる id: dev-to with: # DEV の API キーを指定する api_key: $<span>{</span><span>{</span> secrets.api_key <span>}</span><span>}</span> # (オプション) DEV に記事を投稿した際に Zenn のカノニカル URL を設定したい場合に指定する # username: nikaera # (オプション) 改行区切りで指定した articles フォルダ内のファイルパスを記載した txt ファイルを指定することで、記載された記事のみを同期するようになる。 # 他プラグインと組み合わせることで差分のみを txt ファイルに載せることが可能。詳細については後述の Outputs の項目に記載。 # added_modified_filepath: ./added_modified.txt # (オプション) Zenn の articles 以下全ての記事を常に DEV に同期するか指定する # update_all が true のときは added_modified_filepath は無視される。 # update_all: false # 上記アクション実行時に DEV に新規で同期する記事に関しては Zenn のマークダウンヘッダに # 該当する DEV の記事の ID が dev_article_id として記載されるようになる。 # 今後はその ID を元に同期するようになるため、該当する Zenn の記事をコミットする。 # 新規で同期する記事が無ければ、このジョブは実行しない。 - name: write article id of DEV to articles of Zenn. run: | git config user.name github-actions git config user.email [email protected] git add $<span>{</span><span>{</span> steps.dev-to.outputs.newly-sync-articles <span>}</span><span>}</span> git commit -m "sync: Zenn with DEV [skip ci]" git push if: steps.dev-to.outputs.newly-sync-articles # Outputs には DEV の記事情報 (title, url) が含まれるようになるため、 # 最後に出力して実行結果の内容を確認することもできる - name: Get the output articles. # dev-to という id が紐付いたジョブの Outputs を取得して echo で内容を出力する run: echo "$<span>{</span><span>{</span> steps.dev-to.outputs.articles <span>}</span><span>}</span>" </code></pre> <p>簡単に <code>nikaera/sync-zenn-with-dev-action@v1</code> というジョブの内部処理について説明いたします。</p> <ol> <li>Inputs の <code>update_all</code> 及び <code>added_modified_filepath</code> で受け取った情報を元に、<br /> どの記事を DEV に同期させるか判定する</li> <li>DEV に同期する記事のファイルパス一覧を取得して、<br /> それぞれの記事のヘッダに <code>dev_article_id</code> の記載があるか判定する</li> <li>Inputs の <code>api_key</code> を利用して、Zenn の記事に <code>dev_article_id</code> が含まれていれば、<br /> 該当する DEV の記事を更新する。含まれていなければ DEV に新規で記事を作成する</li> <li>Inputs の <code>username</code> を利用して、<br /> DEV の記事に該当する Zenn 記事のカノニカル URL を設定する</li> <li>DEV に新規で記事を作成した場合は、<br /> Zenn の該当記事のヘッダに <code>dev_article_id</code> を書き込む</li> <li>DEV に記事を投稿する際、Zenn は対応しているが、<br /> DEV では対応していない記述は削除する (<code>:::</code> 記法や一部のコード記法)</li> <li>記事の公開ステータス及びタグなどについても DEV の記事に反映する<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup></li> <li>新規で DEV に記事を同期した Zenn 記事のファイルパスを、<br /> Outputs の <code>newly-sync-articles</code> に設定する<br /> (後のジョブで <code>dev_article_id</code> の含まれた記事をコミットしたいため)</li> <li>ワークフローで同期された記事情報は Outouts の <code>articles</code> に設定する</li> </ol> <p>Inputs と Outputs の内容一覧については下記になります。</p> <p><strong>Inputs</strong></p> <div class="table-responsive"><table> <thead> <tr> <th align="left">キー</th> <th align="left">説明</th> <th align="center">必須</th> </tr> </thead> <tbody> <tr> <td align="left">api_key</td> <td align="left">DEV の <a target="_blank" rel="nofollow noopener" href="https://docs.forem.com/api/#section/Authentication">API Key</a> を設定する</td> <td align="center">o</td> </tr> <tr> <td align="left">username</td> <td align="left">Zenn の <strong>自分のアカウント名</strong> を設定する (DEV に同期する記事に Zenn のカノニカル URL を設定したい場合のみ)</td> <td align="center">x</td> </tr> <tr> <td align="left">added_modified_filepath</td> <td align="left">改行区切りで指定した articles フォルダ内のファイルパスを記載した txt ファイルを指定することで、記載された記事のみを同期するようになる。<strong>PR やコミット差分のファイルのみを取得するための GitHub Action <a target="_blank" rel="nofollow noopener" href="https://github.com/jitterbit/get-changed-files">jitterbit/get-changed-files@v1</a> と組み合わせることで、更新差分のあった記事のみを随時同期することも可能。<sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup></strong> 更新差分のあった記事のみを随時同期するための<a target="_blank" rel="nofollow noopener" href="https://github.com/nikaera/zenn.dev/blob/main/.github/workflows/sync-zenn-with-dev.yml">実際のワークフローファイルはこちら</a></td> <td align="center">x</td> </tr> <tr> <td align="left">update_all</td> <td align="left">Zenn の全ての記事をどうきするかどうかを設定する。GitHub Action 初回実行時のみ true にする使い方を想定している。デフォルトは true。<strong><code>added_modified_filepath</code> よりも <code>update_all</code> が優先されるため <code>added_modified_filepath</code> を設定する場合は false を設定する必要あり`</strong></td> <td align="center">x</td> </tr> </tbody> </table></div> <p><strong>Outputs</strong></p> <div class="table-responsive"><table> <thead> <tr> <th align="left">キー</th> <th align="left">説明</th> </tr> </thead> <tbody> <tr> <td align="left">articles</td> <td align="left">同期された DEV の記事のタイトル及び URL が格納された配列</td> </tr> <tr> <td align="left">newly-sync-articles</td> <td align="left">DEV で新たに新規作成された Zenn 記事のファイルパスが格納された配列。<strong><a target="_blank" rel="nofollow noopener" href="https://github.com/nikaera/sync-zenn-with-dev-action/blob/main/.github/workflows/test.yml#L31-L38">実際のワークフローファイルの該当する記述</a>のように、必ずコミットに含めるようにする必要がある (理由は後述)</strong></td> </tr> </tbody> </table></div> <p>Inputs 及び Outputs については<a target="_blank" rel="nofollow noopener" href="https://docs.github.com/ja/actions/creating-actions/metadata-syntax-for-github-actions#">公式サイトの説明</a>をご参照ください。</p> <h2 id="Zenn の記事を DEV に同期するための仕組み"><a href="#Zenn+%E3%81%AE%E8%A8%98%E4%BA%8B%E3%82%92+DEV+%E3%81%AB%E5%90%8C%E6%9C%9F%E3%81%99%E3%82%8B%E3%81%9F%E3%82%81%E3%81%AE%E4%BB%95%E7%B5%84%E3%81%BF">Zenn の記事を DEV に同期するための仕組み</a></h2> <p>Zenn の記事を新規で DEV に同期する際は、DEV に記事を新規作成する必要があります。<strong>その際に Zenn の記事と DEV の記事を紐付けるための何らかの仕組みが必要となります。そうしないと、今後 Zenn の記事内容を更新した際に、DEV のどの記事に内容を同期させればよいかが不明なためです。</strong></p> <p>そこで、記事を同期するための仕組みとして、<strong><code>dev_article_id</code> というフィールドを Zenn のマークダウンヘッダに追記することで DEV の同期すべき記事との紐付けを行うことにしました。</strong><code>dev_article_id</code> には <a target="_blank" rel="nofollow noopener" href="https://docs.forem.com/api/#operation/createArticle">DEV の記事作成 API</a> 実行時の返り値である <code>id</code> を設定します。</p> <p>一度 <code>id</code> を <code>dev_article_id</code> として Zenn の記事に紐付けてしまえば、次回以降に記事の同期を行う際は <a target="_blank" rel="nofollow noopener" href="https://docs.forem.com/api/#operation/updateArticle">DEV の記事更新 API</a> を利用できます。</p> <p>また、<strong>Outputs の <code>newly-sync-articles</code> には新規で作成された DEV 記事の <code>id</code> である <code>dev_article_id</code> が追記された Zenn 記事のファイルパスが格納されています。そのため、<code>nikaera/sync-zenn-with-dev-action@v1</code> 実行後は、下記のように <code>steps.dev-to.outputs.newly-sync-articles</code> 内に格納されたファイル群をコミットに反映させる必要があります。</strong></p> <pre><code class="yml"># `nikaera/sync-zenn-with-dev-action@v1` 実行後に必ず定義すべきジョブ # DEV に新規に作成した記事がなければ実行しない (if: steps.dev-to.outputs.newly-sync-articles) - name: write article id of DEV to articles of Zenn. run: | git config user.name github-actions git config user.email [email protected] git add $<span>{</span><span>{</span> steps.dev-to.outputs.newly-sync-articles <span>}</span><span>}</span> git commit -m "sync: Zenn with DEV [skip ci]" git push if: steps.dev-to.outputs.newly-sync-articles </code></pre> <p>上記のジョブで <code>newly-sync-articles</code> に格納された <code>dev_article_id</code> が追記された Zenn 記事は随時コミットに反映しないと、<strong>Zenn の全ての記事が同期毎 DEV に新規作成され続けるという不具合を引き起こしてしまうので、ご注意ください</strong></p> <h1 id="GitHub Action を開発する手順"><a href="#GitHub+Action+%E3%82%92%E9%96%8B%E7%99%BA%E3%81%99%E3%82%8B%E6%89%8B%E9%A0%86">GitHub Action を開発する手順</a></h1> <p>サクッと開発に取り組みたかったため、<a target="_blank" rel="nofollow noopener" href="https://docs.github.com/ja/actions/creating-actions/creating-a-docker-container-action">Docker コンテナを利用する方法</a> ではなく、<a target="_blank" rel="nofollow noopener" href="https://docs.github.com/ja/actions/creating-actions/creating-a-javascript-action">JavaScript を利用する方法</a> で開発を進めていくことにしました。</p> <h2 id="TypeScript で GitHub Action を作る"><a href="#TypeScript+%E3%81%A7+GitHub+Action+%E3%82%92%E4%BD%9C%E3%82%8B">TypeScript で GitHub Action を作る</a></h2> <p>GitHub 公式が TypeScript で GitHub Action を作るための <a target="_blank" rel="nofollow noopener" href="https://github.com/actions/typescript-action">テンプレートプロジェクト</a> を用意してくれています。今回はこのテンプレートプロジェクトを利用する形でプロジェクトを作成しました。</p> <p><em>(余談) GitHub Action では <a target="_blank" rel="nofollow noopener" href="https://docs.github.com/ja/actions/creating-actions/creating-a-docker-container-action">Docker コンテナ</a> を用いてワークフローを実行可能です。<strong>そのため、実行環境は自由に設定出来ます。(Go, Python, Ruby, etc.)</strong></em></p> <p>早速 TypeScript のテンプレートプロジェクトを元に自分の GitHub Action プロジェクトを作成します。</p> <p><img src="https://i.gyazo.com/359f6f795bab9807c9f480c0f922973a.png" alt="スクリーンショット 2021-03-21 13.25.54.png" /><br /> <strong>1. テンプレートプロジェクトを元に GitHub Action の TypeScript プロジェクトを作成する</strong></p> <p><img src="https://i.gyazo.com/76aecd3d63db95d34c086774ded122d4.png" alt="スクリーンショット 2021-03-21 13.29.43.png" /><br /> <strong>2. プロジェクトの作成後 <code>git clone</code> してきて開発する準備を整える</strong></p> <h2 id="GitHub Action プロジェクトの開発を進めるための準備を行う"><a href="#GitHub+Action+%E3%83%97%E3%83%AD%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%81%AE%E9%96%8B%E7%99%BA%E3%82%92%E9%80%B2%E3%82%81%E3%82%8B%E3%81%9F%E3%82%81%E3%81%AE%E6%BA%96%E5%82%99%E3%82%92%E8%A1%8C%E3%81%86">GitHub Action プロジェクトの開発を進めるための準備を行う</a></h2> <p>テンプレートプロジェクトを <code>git clone</code> したら、まずは <code>action.yml</code> の内容を変更します。<br /> 今回作成した GitHub Action の <code>action.yml</code> は下記となっております。</p> <pre><code class="yml"># action.yml # GitHub Action のプロジェクト名 name: 'Sync Zenn articles to DEV' # GitHub Action のプロジェクト説明文 description: 'Just sync Zenn articles to DEV.' # GitHub Action の作者 author: 'nikaera' # GitHub Action に渡せる引数の値定義 inputs: api_key: # フィールドの指定が必須であれば true、必須でなければ false を設定する # DEV の API キーは同期を行う際に必須なため、true を設定している required: true # フィールドの説明文 description: 'The api_key required to use the DEV API (https://docs.forem.com/api/#section/Authentication)' username: required: false description: "Zenn user's account name. (Fields to be filled in if canonical url is set.)" articles: required: false description: "The directory where Zenn articles are stored." # フィールドにはデフォルト値を指定することも可能 # Zenn の記事がデフォで格納されているフォルダ名を指定している default: articles update_all: require: false description: "Whether to synchronize all articles." default: true added_modified_filepath: required: false description: | Synchronize only the articles in the file path divided by line breaks. You can use jitterbit/get-changed-files@v1 to get only the file paths of articles that have changed in the correct format. (https://github.com/jitterbit/get-changed-files) # GitHub Action 実行後に参照可能になる値定義 outputs: articles: description: 'A list of URLs of dev.to articles that have been created or updated' newly-sync-articles: description: 'File path list of newly synchronized articles.' # GitHub Action の実行環境 runs: using: 'node12' # テンプレートプロジェクトでは コンパイル先が dist になるため `dist/index.js` を指定している main: 'dist/index.js' </code></pre> <p>TypeScript のテンプレートプロジェクトでは、バンドルツールとして <a target="_blank" rel="nofollow noopener" href="https://github.com/vercel/ncc"><code>ncc</code></a> が採用されています。<strong>GitHub Action 実行時に使用されるのは ncc によりコンパイルされた単一の JavaScript ファイル (<code>dist/index.js</code>) になります。</strong></p> <p>あとは <code>src</code> フォルダ内でプログラムを書いて、<code>npm run all && node dist/index.js</code> のようにコマンド実行しながら開発を進めていくだけです。</p> <p><em>(余談) GitHub Action の開発ツールとして Docker を利用した <a target="_blank" rel="nofollow noopener" href="https://github.com/nektos/act"><code>act</code></a> というものが存在するようです。ローカル環境で検証する際は <a target="_blank" rel="nofollow noopener" href="https://github.com/nektos/act#known-issues">既知の問題</a> に対応する必要がありそうですが、GitHub Action の開発で非常に有効活用できそうで気になっております。</em></p> <p>今回の開発では利用しなかったのですが、今後開発を進めていく中で利用する機会も出てきそうなので、その際は本記事内容を更新する形で知見を追記したいと考えております。</p> <p>:::</p> <h2 id="GitHub Action を実装する際に利用した機能"><a href="#GitHub+Action+%E3%82%92%E5%AE%9F%E8%A3%85%E3%81%99%E3%82%8B%E9%9A%9B%E3%81%AB%E5%88%A9%E7%94%A8%E3%81%97%E3%81%9F%E6%A9%9F%E8%83%BD">GitHub Action を実装する際に利用した機能</a></h2> <p>GitHub Action を実装する際に利用した機能を、実際のコード内容を抜粋して簡単に説明していきます。下記で紹介する内容は <a target="_blank" rel="nofollow noopener" href="https://github.com/actions/toolkit">GitHub Actions Toolkit</a> の機能です。</p> <pre><code class="typescript">/** 下記の yml の with で指定した値は core.getInput で受け取ることが可能。 - name: dev.to action step uses: nikaera/sync-zenn-with-dev-action@v1 id: dev-to with: # DEV の API キーを指定する api_key: $<span>{</span><span>{</span> secrets.api_key <span>}</span><span>}</span> */ core.getInput("api_key", { required: true }); core.getInput("update_all", { required: false }); /** 下記の yml の steps.<ジョブで指定した id>.outputs で参照可能な値をセットすることが可能。 セットする内容は文字列である必要がある。 - name: dev.to action step uses: nikaera/sync-zenn-with-dev-action@v1 id: dev-to - name: Get the output articles. run: echo "$<span>{</span><span>{</span> steps.dev-to.outputs.articles <span>}</span><span>}</span>" */ core.setOutput("articles", JSON.stringify(devtoArticles, undefined, 2)); core.setOutput("newly-sync-articles", newlySyncedArticles.join(" ")); /** GitHub Action 実行時に出力されるログをレベルごとに出力することが可能 core.debug はローカル実行時のみに出力内容を確認することができる */ core.debug("debug"); core.info(`update_all: ${updateAll}`); core.error(JSON.stringify(error)); </code></pre> <p>上記だけ把握してれば GitHub Action の開発は問題なく行うことができました。</p> <h1 id="作成した GitHub Action を実際に GitHub 上で実行可能にする"><a href="#%E4%BD%9C%E6%88%90%E3%81%97%E3%81%9F+GitHub+Action+%E3%82%92%E5%AE%9F%E9%9A%9B%E3%81%AB+GitHub+%E4%B8%8A%E3%81%A7%E5%AE%9F%E8%A1%8C%E5%8F%AF%E8%83%BD%E3%81%AB%E3%81%99%E3%82%8B">作成した GitHub Action を実際に GitHub 上で実行可能にする</a></h1> <p>ローカル環境で一通り開発が完了したら、GitHub リポジトリに push した後タグ付けを行います。<strong>GitHub Action はタグを設定しないと実行できないため必要な作業となります。</strong> 今回タグ付けは GitHub 上で行いました。</p> <p><img src="https://i.gyazo.com/252514cca20470154475db75b133e9b3.png" alt="スクリーンショット 2021-03-22 8.11.58.png" /><br /> <strong>1. タグの項目をクリックする</strong></p> <p><img src="https://i.gyazo.com/a8c145f8d6ae7b1f6253eb4866bcbb51.png" alt="スクリーンショット 2021-03-22 8.14.47.png" /><br /> <strong>2. <code>Create a new release</code> ボタンをクリックしてタグ作成画面に遷移する</strong></p> <p><img src="https://i.gyazo.com/25e019e42227ad7e0b0b634e68bb1873.png" alt="スクリーンショット 2021-03-22 8.20.00.png" /><br /> <strong>3. <code>Publish release</code> ボタンをクリックしてタグの作成を完了する</strong></p> <p><strong>上記の例では <code>v1</code> というタグを作成したので <code>nikaera/sync-zenn-with-dev-action@v1</code> のような記述で GitHub Action を利用可能になりました。</strong> 私は Zenn の記事を <a target="_blank" rel="nofollow noopener" href="https://github.com/nikaera/zenn.dev/"><code>zenn.dev</code></a> というリポジトリで管理しているため、早速このリポジトリに GitHub Action を導入してみます。</p> <h2 id="Zenn の全ての記事を DEV に同期するためのワークフロー"><a href="#Zenn+%E3%81%AE%E5%85%A8%E3%81%A6%E3%81%AE%E8%A8%98%E4%BA%8B%E3%82%92+DEV+%E3%81%AB%E5%90%8C%E6%9C%9F%E3%81%99%E3%82%8B%E3%81%9F%E3%82%81%E3%81%AE%E3%83%AF%E3%83%BC%E3%82%AF%E3%83%95%E3%83%AD%E3%83%BC">Zenn の全ての記事を DEV に同期するためのワークフロー</a></h2> <p>本記事の GitHub Action では DEV の API キーを使用するため、<strong>事前にシークレットへ <code>API_KEY</code> という名称で値を登録しておきます。</strong><a target="_blank" rel="nofollow noopener" href="https://docs.github.com/ja/actions/reference/encrypted-secrets#creating-encrypted-secrets-for-a-repository">公式サイトの手順</a> に従い シークレットの登録が完了したら、該当リポジトリに <code>.github/workflows/sync-zenn-with-dev-all.yml</code> というワークフローファイルを作成します。</p> <pre><code class="yml"># .github/workflows/sync-zenn-with-dev-all.yml name: "Sync-All Zenn with DEV" on: workflow_dispatch: jobs: build: runs-on: ubuntu-latest if: contains(github.event.head_commit.message, '[skip ci]') == false steps: - name: setup node project uses: actions/checkout@v2 - name: dev.to action step uses: nikaera/sync-zenn-with-dev-action@v1 id: dev-to with: api_key: $<span>{</span><span>{</span> secrets.api_key <span>}</span><span>}</span> # Zenn の自分のアカウント名を指定すると # DEV 記事のカノニカル URL に Zenn 記事の URL を指定できる # username: nikaera update_all: true - name: write article id of DEV to articles of Zenn. run: | git config user.name github-actions git config user.email [email protected] git add $<span>{</span><span>{</span> steps.dev-to.outputs.newly-sync-articles <span>}</span><span>}</span> git commit -m "sync: Zenn with DEV [skip ci]" git push if: steps.dev-to.outputs.newly-sync-articles - name: Get the output articles. run: echo "$<span>{</span><span>{</span> steps.dev-to.outputs.articles <span>}</span><span>}</span>" </code></pre> <p>上記は手動実行が可能な Zenn の記事を全て DEV に同期するためのワークフローファイルになります。作成が完了したらワークフローファイルを実行して、記事が正常に同期できているか確認してみます。</p> <p><img src="https://i.gyazo.com/e0740fe369752db67514e0ab50e88772.png" alt="スクリーンショット 2021-03-22 8.35.38.png" /><br /> <strong>1. <code>Actions</code> タブをクリックする</strong></p> <p><img src="https://i.gyazo.com/2aeadddccf08c0dddc54dddab464edee.png" alt="スクリーンショット 2021-03-22 8.37.24.png" /><br /> <strong>2. 作成した <code>Sync-All Zenn with DEV</code> ワークフローファイルを選択する</strong></p> <p><img src="https://i.gyazo.com/d9e138de1f6e0baac8b0c3bc6742f4f2.png" alt="スクリーンショット 2021-03-22 8.40.11.png" /><br /> <strong>3. <code>Run workflow</code> ボタンを実行して、ワークフローを実行する</strong></p> <p><img src="https://i.gyazo.com/e67df0f6e33d1f57a46000d20ef2c824.png" alt="スクリーンショット 2021-03-22 8.45.23.png" /><br /> <strong>4. ワークフローの実行が正常に完了していれば、ログに DEV の記事情報が出力される</strong></p> <p><img src="https://i.gyazo.com/4acc0b0de3acd6731918a9347e7baea8.png" alt="スクリーンショット 2021-03-22 8.50.37.png" /><br /> <strong>5. <code>dev.to</code> にアクセスして Zenn の記事情報が正常に同期されていることを確認する</strong></p> <p>正常に Zenn の全ての記事が DEV に同期されていることが確認できたら、次は Zenn の記事が新規作成されたり、更新されたときのみに DEV に同期するためのワークフローファイルを作成します。</p> <h2 id="更新差分のあった Zenn 記事のみを DEV に同期するためのワークフロー"><a href="#%E6%9B%B4%E6%96%B0%E5%B7%AE%E5%88%86%E3%81%AE%E3%81%82%E3%81%A3%E3%81%9F+Zenn+%E8%A8%98%E4%BA%8B%E3%81%AE%E3%81%BF%E3%82%92+DEV+%E3%81%AB%E5%90%8C%E6%9C%9F%E3%81%99%E3%82%8B%E3%81%9F%E3%82%81%E3%81%AE%E3%83%AF%E3%83%BC%E3%82%AF%E3%83%95%E3%83%AD%E3%83%BC">更新差分のあった Zenn 記事のみを DEV に同期するためのワークフロー</a></h2> <p><code>main</code> ブランチが更新されたときのみ更新された記事のみを DEV に同期するためのワークフローファイルを作成します。該当リポジトリに <code>.github/workflows/sync-zenn-with-dev.yml</code> というワークフローファイルを作成します。</p> <pre><code class="yml"># .github/workflows/sync-zenn-with-dev.yml name: "Sync Zenn with DEV" on: push: branches: - main jobs: build: runs-on: ubuntu-latest if: contains(github.event.head_commit.message, '[skip ci]') == false steps: - name: setup node project uses: actions/checkout@v2 - name: get modified files id: files uses: jitterbit/get-changed-files@v1 - name: output modified files to text run: | for changed_file in $<span>{</span><span>{</span> steps.files.outputs.added_modified <span>}</span><span>}</span>; do echo "${changed_file}" >> added_modified.txt done - name: dev.to action step uses: nikaera/sync-zenn-with-dev-action@v1 id: dev-to with: api_key: $<span>{</span><span>{</span> secrets.api_key <span>}</span><span>}</span> # Zenn の自分のアカウント名を指定すると # DEV 記事のカノニカル URL に Zenn 記事の URL を指定できる # username: nikaera added_modified_filepath: ./added_modified.txt update_all: false - name: write article id of DEV to articles of Zenn. run: | git config user.name github-actions git config user.email [email protected] git add $<span>{</span><span>{</span> steps.dev-to.outputs.newly-sync-articles <span>}</span><span>}</span> git commit -m "sync: Zenn with DEV [skip ci]" git push if: steps.dev-to.outputs.newly-sync-articles - name: Get the output articles. run: echo "$<span>{</span><span>{</span> steps.dev-to.outputs.articles <span>}</span><span>}</span>" </code></pre> <p>上記ワークフローファイルの作成が完了したら、早速動作確認のために、<strong>まさに今執筆中の本記事内容をリポジトリに push してみます。</strong></p> <p><img src="https://i.gyazo.com/36512e7874b49edc1e48f0ef88af5d89.png" alt="スクリーンショット 2021-03-22 9.07.55.png" /><br /> <strong>1. <code>git push</code> 後に該当するワークフローの実行結果を確認する</strong></p> <p><img src="https://i.gyazo.com/cbb0a1cf7b94605680e39630dded0096.png" alt="Image from Gyazo" /><br /> <strong>2. <code>dev.to</code> に執筆途中の内容で記事が同期されていることを確認する</strong></p> <p>これまで説明してきた 2 つのワークフローを利用することで、大抵のユースケースはカバーできるはずです。</p> <h1 id="おわりに"><a href="#%E3%81%8A%E3%82%8F%E3%82%8A%E3%81%AB">おわりに</a></h1> <p>GitHub Action の勉強のために取り組んだプロジェクトですが、思いの外楽しくて他にも色々な機能のアイデアがあるので随時実装していきたいと考えています。(英訳, タイトルフォーマット変更, etc.)</p> <p>DEV に Zenn の記事をクロスポストする GitHub Action を公開することで、いつもお世話になっている Zenn というプラットフォームを海外の方に認知していただける機会を創出できたのかもと考えたらテンションが上がってきました。</p> <p>それはさておき、Zenn の記事を他でも有効活用するための GitHub Action を開発する際には、恐らく本記事で紹介した GitHub Action のコードが参考になるはずです。</p> <p>また、<a target="_blank" rel="nofollow noopener" href="https://docs.github.com/ja/actions/creating-actions/publishing-actions-in-github-marketplace">GitHub Action の Marketplace</a> というものが用意されているようなので、開発がある程度完了次第、こちらに申請するのも試してみたいと考えております。</p> <p>ところで、<em>Crieit さんにも記事投稿 API ができたらクロスポストできるようにしたいな...という個人的願望がありますw</em></p> <h1 id="参考リンク"><a href="#%E5%8F%82%E8%80%83%E3%83%AA%E3%83%B3%E3%82%AF">参考リンク</a></h1> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://dev.to/">DEV Community 👩‍💻👨‍💻</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://dev.to/michaelburrows/comment/125j0">dev.to supports canonical URLs so you can share content without impacting SEO... - DEV Community</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://github.com/nikaera/sync-zenn-with-dev-action">nikaera/sync-zenn-with-dev-action: Just sync Zenn articles to DEV.</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://docs.forem.com/api/#section/Authentication">DEV API (beta)</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://github.com/jitterbit/get-changed-files">jitterbit/get-changed-files: Get all of the files changed/modified in a pull request or push's commits.</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://docs.github.com/ja/actions/creating-actions/creating-a-docker-container-action">Docker コンテナのアクションを作成する - GitHub Docs</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://docs.github.com/ja/actions/creating-actions/creating-a-javascript-action">JavaScript アクションを作成する - GitHub Docs</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://github.com/actions/typescript-action">actions/typescript-action: Create a TypeScript Action with tests, linting, workflow, publishing, and versioning</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://github.com/vercel/ncc">vercel/ncc: Compile a Node.js project into a single file. Supports TypeScript, binary addons, dynamic requires.</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://github.com/nektos/act">nektos/act: Run your GitHub Actions locally 🚀</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://github.com/actions/toolkit">actions/toolkit: The GitHub ToolKit for developing GitHub Actions.</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://docs.github.com/ja/actions/reference/encrypted-secrets#creating-encrypted-secrets-for-a-repository">暗号化されたシークレット - GitHub Docs</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://www.deepl.com/ja/pro#developer">DeepL Pro:テキストの他、Word などの文書をセキュアに翻訳</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/">Qiita</a></li> <li><a href="https://crieit.net/advent-calendars/2020/crieit">Crieit - プログラマー、クリエイターが何でも気軽に書けるコミュニティ</a></li> </ul> <div class="footnotes" role="doc-endnotes"> <hr /> <ol> <li id="fn:1" role="doc-endnote"> <p>DEV (Forem) の仕様上、<a target="_blank" rel="nofollow noopener" href="https://dev.to/p/editor_guide#front-matter">タグは最大でも 4 つまで</a>しか設定できないため、Zenn で設定したタグの先頭 4 つまで DEV の記事には設定しています <a href="#fnref:1" class="footnote-backref" role="doc-backlink">↩︎</a></p> </li> <li id="fn:2" role="doc-endnote"> <p>当然といえば当然ですが <a target="_blank" rel="nofollow noopener" href="https://github.com/jitterbit/get-changed-files">jitterbit/get-changed-files@v1</a> は <code>workflow_dispatch</code> でワークフローを手動実行した際や、<code>force push</code> 等でファイル差分を正確に特定できない操作には対応しておりませんので、その場合はエラーが発生します <a href="#fnref:2" class="footnote-backref" role="doc-backlink">↩︎</a></p> </li> </ol> </div> nikaera tag:crieit.net,2005:PublicArticle/16727 2021-03-10T05:00:58+09:00 2021-03-10T07:30:31+09:00 https://crieit.net/posts/TypeScript-Deno 素朴な自作言語のコンパイラをTypeScript(Deno)に移植した <p><a target="_blank" rel="nofollow noopener" href="https://memo88.hatenablog.com/entry/2020/08/30/132314">移植一覧に戻る</a></p> <hr /> <p>TypeScript 入門というか、とりあえず何か書いて慣れようと思って書いてみました。やっつけなので汚いです。TypeScript まだよく分からないけどなんか動いたのでヨシ、というレベルのものです。<br /> (※ <a target="_blank" rel="nofollow noopener" href="https://memo88.hatenablog.com/entry/2020/08/15/114754">2020-08-15 に書いた記事</a>のクロス投稿です)</p> <p><a target="_blank" rel="nofollow noopener" href="https://github.com/sonota88/vm2gol-v2-typescript">https://github.com/sonota88/vm2gol-v2-typescript</a></p> <h1 id="移植元"><a href="#%E7%A7%BB%E6%A4%8D%E5%85%83">移植元</a></h1> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://github.com/sonota88/vm2gol-v2">https://github.com/sonota88/vm2gol-v2</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://memo88.hatenablog.com/entry/2020/05/04/155425">Rubyで素朴な自作言語のコンパイラを作った</a></li> </ul> <p>ベースになっているバージョン: <a target="_blank" rel="nofollow noopener" href="https://github.com/sonota88/vm2gol-v2/tree/45">tag:45</a> のあたり</p> <p>(201-03-06 追記: <a target="_blank" rel="nofollow noopener" href="https://memo88.hatenablog.com/entry/20210206_vm2gol_v2_step55_delete_set_reg_a_b">step 55 の修正</a>まで適用しました。それに伴い、アセンブラ・VM 関連のコードを <code>old_version</code> ディレクトリに移動しました。全部修正すると大変なので)</p> <h1 id="メモ"><a href="#%E3%83%A1%E3%83%A2">メモ</a></h1> <ul> <li>YAML/JSON ライブラリへの依存をなくしてみた <ul> <li>vge コードは YAML ではなく簡単な独自フォーマットにして自力でパースするようにした</li> <li>単に行ごとに読んで文字列か数かを判別できればよい</li> <li>vgt コード(JSON)も自力でパースしてみた</li> </ul></li> <li>Ruby 版で作った出力データがすでにあるので、<br /> それに一致するように未実装部分を潰していくだけ。<br /> ゼロから作っていた Ruby 版のときに比べたら遥かに楽。</li> <li>今回もなんとなく VM → アセンブラ → コード生成器 → パーサ<br /> の順で作ってしまって、インクリメンタルな作り方を試せばかったな……と作り終わってから思った</li> </ul> sonota486 tag:crieit.net,2005:PublicArticle/16723 2021-03-08T01:51:05+09:00 2021-03-08T01:51:05+09:00 https://crieit.net/posts/jest-private-readonly-mock 📝 Jest で private readonly な値をモックする方法 <h1 id="はじめに"><a href="#%E3%81%AF%E3%81%98%E3%82%81%E3%81%AB">はじめに</a></h1> <p>Jest でクラスの <code>private readonly</code> な変数を差し替えたい時に若干引っかかったのでメモっておきます。タイトルでは Jest とありますが、本記事の内容は JavaScript でモックする際の有効な手法の 1 つとして利用することが可能です。</p> <h1 id="Object.defineProperty を利用して値を差し替える"><a href="#Object.defineProperty+%E3%82%92%E5%88%A9%E7%94%A8%E3%81%97%E3%81%A6%E5%80%A4%E3%82%92%E5%B7%AE%E3%81%97%E6%9B%BF%E3%81%88%E3%82%8B">Object.defineProperty を利用して値を差し替える</a></h1> <p>結論から言うと変数を差し替えたい場合は下記のような記述になります。</p> <pre><code class="javascript">const mockValue = ""; Object.defineProperty(service, "privateReadOnlyValue", { value: mockValue, }); </code></pre> <p>ちなみに関数を差し替えたい場合は下記のような記述になります。</p> <pre><code class="javascript">Object.defineProperty(service, "privateSumFunction", { value: jest.fn((a, b) => a + b), }); </code></pre> <p>各種テストケースで使いまわしているインスタンスの <code>private readonly</code> な変数をモックした場合、値をリストアしたいケースも出てきました。その場合の記述としては、下記が有効でした。</p> <pre><code class="javascript">// tmpService 変数に service インスタンスを clone して利用する const tmpService = Object.create(service); Object.defineProperty(tmpService, "privateReadOnlyValue", { value: "", }); </code></pre> <h1 id="おわりに"><a href="#%E3%81%8A%E3%82%8F%E3%82%8A%E3%81%AB">おわりに</a></h1> <p><code>Object.defineProperty</code> と <code>Object.create</code> を駆使すれば大体のケースでは事足りそうです。</p> <h1 id="参考リンク"><a href="#%E5%8F%82%E8%80%83%E3%83%AA%E3%83%B3%E3%82%AF">参考リンク</a></h1> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Object/create">Object.create() - JavaScript | MDN</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://github.com/facebook/jest/issues/2227#issuecomment-265005782">Mocking read-only properties for a class · Issue #2227 · facebook/jest</a></li> </ul> nikaera tag:crieit.net,2005:PublicArticle/16704 2021-03-01T01:31:55+09:00 2021-03-01T01:31:55+09:00 https://crieit.net/posts/Next-js-Vercel-Recoil-Material-Table-AWS Next.jsとVercelとRecoilとMaterial Tableを使ってAWSのステータスダッシュボードを作ってみた話 <h2 id="AWSのステータス確認難しいよね"><a href="#AWS%E3%81%AE%E3%82%B9%E3%83%86%E3%83%BC%E3%82%BF%E3%82%B9%E7%A2%BA%E8%AA%8D%E9%9B%A3%E3%81%97%E3%81%84%E3%82%88%E3%81%AD">AWSのステータス確認難しいよね</a></h2> <p>AWSを使ったことのある人ならばわかると思いますが、公式がAWSの障害情報を掲載する<a target="_blank" rel="nofollow noopener" href="https://status.aws.amazon.com/">AWS Service Health Dashboard</a>があまり使いやすくないです。</p> <p><img src="https://i.imgur.com/XghDulZ.png" alt="img" /></p> <p>それぞれのリージョンの障害がRSSで配信される形式になっているのですが、わざわざRSSを登録するのもめんどくさいし、Slackとかの連携に乗っけるのもそれはそれで便利なのですが、そもそもSlackを見ていないほかの人でも障害情報を共有したいです。</p> <p>実は、AWS Service Health Dashboardの情報はJSONで取得することができます。</p> <p><a target="_blank" rel="nofollow noopener" href="https://status.aws.amazon.com/data.json">https://status.aws.amazon.com/data.json</a></p> <p>こちらのJSONを活用して勉強がてら使いやすいダッシュボードを作っていきます。</p> <h2 id="クビになるぞ!"><a href="#%E3%82%AF%E3%83%93%E3%81%AB%E3%81%AA%E3%82%8B%E3%81%9E%EF%BC%81">クビになるぞ!</a></h2> <p>最近、これといった新しい技術に触れておらず、このままだとクビになりそうなので、そろそろ重い腰を上げてNext.jsを勉強することにしました。</p> <p>また、Next.jsを使う場合はVercelが便利だよーとのことですので、こちらも使っていきます。</p> <h2 id="Next.js"><a href="#Next.js">Next.js</a></h2> <p>Next.jsではpages/api配下に格納したコードについては、サーバーサイドとして振る舞います。</p> <p>クライアントから直接status情報がかかれたJSONを読みとってもよかったのですが、HTMLの面倒なサニタイジング処理やら、値の補完など面倒なことはサーバーサイドに持ってこようということで、<br /> statusJSONを取得して、フロントに返却するサーバーコードを書いていきます。</p> <p>次のようなコードになりました。</p> <pre><code class="typescript">import { NextApiRequest, NextApiResponse } from 'next' import axios from 'axios' export interface AwsStatusResp { archive: AwsStatusArchive[] } export interface AwsStatusArchive { service_name: string summary: string date: string status: string details: string description: string service: string } const handler = (req: NextApiRequest, res: NextApiResponse) => { axios .get<AwsStatusResp>('https://status.aws.amazon.com/data.json') .then((resp) => { const handlerResp = resp.data.archive.map((x) => ({ // eslint-disable-next-line @typescript-eslint/camelcase service_name: x.service_name, summary: x.summary, region: x.service.includes('management-console') ? 'global' : x.service.split('-').slice(1).join('-') === '' ? 'global' : x.service.split('-').slice(1).join('-'), date: x.date, status: x.status, details: x.details, service: x.service.includes('management-console') ? 'management-console' : x.service.split('-')[0], description: x.description .replace(/<("[^"]*"|'[^']*'|[^'">])*>/g, '') .replace(/&nbsp;/g, '\n'), })) res.statusCode = 200 // eslint-disable-next-line no-console console.log(handlerResp) res.json(handlerResp) }) .catch((error) => { console.error(error.response) res.statusCode = error.response.status || 500 res.statusMessage = error.response.statusText || 'InternalServerError' res.json({ error: error.response.statusText || 'InternalServerError' }) }) } export default handler </code></pre> <p>注意点として、必ずハンドラーはexport defaultを指定してあげないこと以外はいたって直感的なコードとなっております。</p> <p>Vercelに載っけるとわかるのですが、こちらのコード、Lambdaにデプロイされることになります。たしかに見覚えある感じですね。</p> <p>また、Next.jsと関係ないのですが、axiosのレスポンスに型がつけられるって知ってましたか?</p> <pre><code class="typescript">export interface AwsStatusResp { archive: AwsStatusArchive[] } export interface AwsStatusArchive { service_name: string summary: string date: string status: string details: string description: string service: string } axios .get<AwsStatusResp>('https://status.aws.amazon.com/data.json') .then((resp) => { .......... </code></pre> <h2 id="Material Table"><a href="#Material+Table">Material Table</a></h2> <p>Material UI準拠のテーブルとして、Material Tableなるものがありましたので今回採用することにしました。</p> <pre><code class="typescript">import MaterialTable from 'material-table' import tableIcons from '../components/tableIcons' <MaterialTable icons={tableIcons} columns={[ { title: 'Service Name', field: 'service_name' }, { title: 'Service', field: 'service', width: 10 }, { title: 'Region', field: 'region', lookup: regionNameMapping }, { title: 'Summary', field: 'summary' }, { title: 'Date (' + dayjs.tz.guess() + ')', field: 'date', render: (rowData) => ( <div> {dayjs .unix(Number(rowData.date)) .format('YYYY-MM-DDTHH:mm:ssZ[Z]')} </div> ), defaultSort: 'desc', type: 'string', }, { title: 'Status', field: 'status', lookup: statusMapping, }, ]} data={aws} detailPanel={[ { tooltip: 'Details', render: (rowData) => { return ( <> <div className="title">{rowData.summary}</div> <div className="description"> {dayjs .unix(Number(rowData.date)) .format('YYYY-MM-DDTHH:mm:ss')}{' '} {rowData.service_name} </div> <div className="code">{rowData.description}</div> </> ) }, }, ]} options=<span>{</span><span>{</span> filtering: true, grouping: true, exportButton: true, exportFileName: 'exported', headerStyle: { backgroundColor: '#e77f2f', color: '#FFF', }, <span>}</span><span>}</span> isLoading={loading} actions={[ { // Issue: https://github.com/mbrn/material-table/issues/51 //@ts-ignore icon: tableIcons.BarChartIcon, tooltip: 'Show Bar Chart', isFreeAction: true, disabled: loading, onClick: async () => { setShowGraph(!showG) }, }, { // Issue: https://github.com/mbrn/material-table/issues/51 //@ts-ignore icon: tableIcons.Refresh, tooltip: 'Refresh Data', isFreeAction: true, disabled: loading, onClick: async () => { setLoading(true) await getAwsStatus() }, }, ]} title={ <div className="header"> <img src="/awslogo.png" /> <a href="https://aws-health-dashboard.vercel.app/"> AWS Health Dashboard </a> </div> } /> </code></pre> <p>使い方もシンプルかつ比較的高機能でいい感じです。</p> <p>いい感じですが後述するRecoilとの相性問題とDatetimeの扱いが微妙なのがツラミでした。</p> <p>本当はDate型を渡してあげるとSearchableの際、カレンダーでの絞り込みができるのかなと思ったのですが、こちらがうまくいきませんでした。</p> <p>あと、微妙に型もおかしく例えば、actionsはactionを複数指定することができるはずですが、型チェックで怒られるので、仕方なくts-ignoreしてます。</p> <p>あなたが直せばいいじゃんアゼルバイジャンって言われそうですが、めんどくさくなってしまいIssueだけあげてしまいました。申し訳ねぇ...。</p> <p><a target="_blank" rel="nofollow noopener" href="https://github.com/mbrn/material-table/issues/2762">https://github.com/mbrn/material-table/issues/2762</a></p> <h2 id="Recoil"><a href="#Recoil">Recoil</a></h2> <p>RecoilとはReactの新しい状態管理ライブラリで、いわゆるReact HooksでGlobal Storeを作ろうというものです。</p> <p>基本的な使い方はまず、storeとしてatomという共有ステートを作成します。</p> <p>atomのkeyはプロジェクトで一意にする必要がありますが、今回はそこまで大規模なプロジェクトではないのでawsとかいうクソ名をつけてます。</p> <p>storeなので、store/aws.ts として格納します。</p> <pre><code class="typescript">import { atom } from 'recoil' const awsState = atom({ key: 'aws', default: [ { // eslint-disable-next-line @typescript-eslint/camelcase service_name: 'Auto Scaling (N. Virginia)', summary: '[RESOLVED] Example Error', date: '1542849575', status: '1', details: '', description: 'The issue has been resolved and the service is operating normally.', service: 'autoscaling', region: 'us-east-1', }, ], dangerouslyAllowMutability: true, }) export default awsState </code></pre> <p>次にステートを共有したいコンポーネントのルートにRecoilRootを設置します。</p> <p>Next.jsの場合、_app.tsxが全ページのルートにあたるのでここに置けばいいですね。</p> <pre><code class="typescript">import { AppProps } from 'next/app' import Head from 'next/head' import { RecoilRoot } from 'recoil' import React from 'react' const App = ({ Component, pageProps }: AppProps) => ( <> <RecoilRoot> <Head> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" /> <title>AWS Health Dashboard</title> </Head> <Component {...pageProps} /> </RecoilRoot> </> ) export default App </code></pre> <p>そして、利用するときはuseRecoilStateをReact Hooksのように利用するだけです。簡単ですね。</p> <pre><code class="typescript">import React, { useState, useEffect } from 'react' import { useRecoilState } from 'recoil' import awsState from '../store/aws' import showGraph from '../store/showGraph' import axios from 'axios' import dayjs from 'dayjs' dayjs.extend(utc) dayjs.extend(timezone) const Alert = (props: AlertProps) => { return <MuiAlert elevation={6} variant="filled" {...props} /> } export const Table = (): JSX.Element => { // 20200112: dangerouslyAllowMutabilityでできた const [aws, setAws] = useRecoilState(awsState) const [showG, setShowGraph] = useRecoilState(showGraph) const [loading, setLoading] = useState(true) const [slackBarOpen, setSlackBarOpen] = React.useState(false) const [apiErrorMsg, setApiErrorMsg] = React.useState('') useEffect(() => { getAwsStatus() setLoading(false) }, []) const getAwsStatus = () => { axios .get('/api/aws') .then((resp) => { setAws(resp.data) setLoading(false) }) .catch((error) => { console.error(error.response) setSlackBarOpen(true) setApiErrorMsg(error.response.statusText || 'Error') setAws([]) setLoading(false) }) } </code></pre> <p>stateの読み込みはgetterから、書き込みはsetterから行います。</p> <p>React Hooksに慣れていれば簡単ですね。</p> <h2 id="思わぬ落とし穴 Material TablesでRecoilが使えない"><a href="#%E6%80%9D%E3%82%8F%E3%81%AC%E8%90%BD%E3%81%A8%E3%81%97%E7%A9%B4+Material+Tables%E3%81%A7Recoil%E3%81%8C%E4%BD%BF%E3%81%88%E3%81%AA%E3%81%84">思わぬ落とし穴 Material TablesでRecoilが使えない</a></h2> <p>Recoilのatomは基本値の書き換えはset stateを使うことが求められます。ですが、material tablesはテーブルを作るときにdataにIDの書き込みが発生するようでそのままだと怒られてしまいます。</p> <pre><code>Cannot add property tableData, object is not extensible </code></pre> <p>これの解決策はRecoilにstateへの直接的な書き換えを許可することです。こちらはatomのoptionでdangerouslyAllowMutabilityを有効にすることで解決できます。</p> <pre><code class="typescript">import { atom } from 'recoil' const awsState = atom({ key: 'aws', default: [ { }, ], dangerouslyAllowMutability: true, }) </code></pre> <p>これがわかるのに半日くらい使っちまいました。</p> <h2 id="Chart.js"><a href="#Chart.js">Chart.js</a></h2> <p>さて、無事にRecoilでstateの共有ができたのでせっかくなので別コンポーネントも作ります。</p> <p>意味があるかどうか不明ですが、AWSの障害発生状況を可視化してみようと思います。</p> <p>ということで、採用したのがChart.js。</p> <p>次のようにデータを渡すだけできれいめなグラフを書いてくれます。</p> <pre><code class="typescript">import { useRecoilValue } from 'recoil' import awsState from '../store/aws' import React from 'react' import { regionNameMapping, } from './const' import BarGraph from './barGraph' export const AlertPerRegion = (): JSX.Element => { const aws = useRecoilValue(awsState) const labels = Array.from( new Set(aws.map((data) => regionNameMapping[data.region])) ) const data = [] for (const r of labels) { data.push( aws .map((data) => regionNameMapping[data.region]) .reduce((total, x) => { return x === r ? total + 1 : total }, 0) ) } return ( <div className="container"> <BarGraph labels={labels} data={data} title="Alert per region" /> </div> ) } </code></pre> <p>どうでもいい実装ですが、各グラフを一覧で見れる画面を用意し、実際のグラフは遷移先で表示するようにしてます。</p> <p><img src="https://i.imgur.com/tfnpq4w.png" alt="img" /></p> <p><img src="https://i.imgur.com/hpJ70fR.png" alt="img" /></p> <h2 id="Vercelにデプロイ"><a href="#Vercel%E3%81%AB%E3%83%87%E3%83%97%E3%83%AD%E3%82%A4">Vercelにデプロイ</a></h2> <p>さて、実装ができたので後はVercelにあげるだけです。</p> <p>もうここはほとんど書くことがないのですが、Next.jsで作ったアプリケーションはVercelでレポジトリと使っているフレームワークを設定するだけで簡単にデプロイ出来てしまいます。</p> <p>これはすごい。</p> <h2 id="完成"><a href="#%E5%AE%8C%E6%88%90">完成</a></h2> <p>ということで、AWS Health Dashboardが完成しました。</p> <p>アクセスすると、Next.jsのapiをコールし、AWSのstatusを取得加工したものを返却します。</p> <p>フロントでは受け取ったデータをRecoilのatomに格納しつつ、material tableで表として描画します。</p> <p>また右上のグラフボタンを押すことで色々な切り口の可視化を行うことができます。</p> <p><a target="_blank" rel="nofollow noopener" href="https://aws-health-dashboard.vercel.app/">https://aws-health-dashboard.vercel.app/</a></p> <p>できれば使う場面にならないことを祈りつつ、ご活用いただければとおもいます。</p> <h2 id="まとめ"><a href="#%E3%81%BE%E3%81%A8%E3%82%81">まとめ</a></h2> <p>食わず嫌いでやらなかったNext.js+Recoilをやってみましたが、楽しく実装ができました。</p> <h2 id="2021/02/20追記"><a href="#2021%2F02%2F20%E8%BF%BD%E8%A8%98">2021/02/20追記</a></h2> <p>2021/02/19~20にかけて起きた<a target="_blank" rel="nofollow noopener" href="https://status.aws.amazon.com/rss/ec2-ap-northeast-1.rss">AWS EC2障害</a>ですが、本ダッシュボードでは更新がされませんでした。</p> <p>どうやら、data.jsonはRSSとは違い、同期的に更新されないようです。</p> <p>大変ご迷惑をおかけしました。改めて、改修しRSS更新にも対応できるように頑張ります。</p> tubone24 tag:crieit.net,2005:PublicArticle/16407 2020-12-22T16:15:41+09:00 2020-12-22T16:15:41+09:00 https://crieit.net/posts/hugo-react-dev Hugo で React + TypeScript を利用してサクッとウェブサイトに RSS リーダーを追加する <p>この記事は <a target="_blank" rel="nofollow noopener" href="https://qiita.com/advent-calendar/2020/static-site-generator">Static Site Generator Advent Calendar 2020</a> 22日目の記事です。</p> <h1 id="はじめに"><a href="#%E3%81%AF%E3%81%98%E3%82%81%E3%81%AB">はじめに</a></h1> <p>Hugo のウェブサイトに組み込む RSS リーダーを TypeScript で開発してみたいと思い調査したところ、Hugo の最新版には <a target="_blank" rel="nofollow noopener" href="https://github.com/evanw/esbuild">ESBuild</a> が組み込まれていて、<strong>非常に手厚く JavaScript の開発環境がサポートされていることが分かりました。</strong> 本記事では紹介していませんが <a target="_blank" rel="nofollow noopener" href="https://gohugo.io/hugo-pipes/babel/">Babel</a> も利用できるようです。</p> <p>また、NPM パッケージも利用できるため、普段のウェブ開発と同様の流れで開発ができ、各種ライブラリを用いた開発も非常に楽でした。<br /> 今回は Hugo で JavaScript 開発する方法を RSS リーダーの開発を例に上げ、そこで得た知見についても交える形で記事として残しておくことにしました。</p> <p><strong>ちなみに本記事内容は Hugo で JavaScript 開発する方法に焦点を絞ったものなのですが、ウェブサイトに RSS リーダーを組み込むことに焦点を絞って見たい方は <a href="#(%E4%BD%99%E8%AB%87)-rss-%E3%83%AA%E3%83%BC%E3%83%80%E3%83%BC%E3%82%92-hugo-%E3%81%AE-data-templates-%E3%81%A7%E5%AE%9F%E8%A3%85%E3%81%99%E3%82%8B"><code>RSS リーダーを Hugo の Data Templates で実装する</code></a> から見ていただくことをオススメします。</strong></p> <h1 id="Hugo で JavaScript (React + TypeScript) の開発環境を整える"><a href="#Hugo+%E3%81%A7+JavaScript+%28React+%2B+TypeScript%29+%E3%81%AE%E9%96%8B%E7%99%BA%E7%92%B0%E5%A2%83%E3%82%92%E6%95%B4%E3%81%88%E3%82%8B">Hugo で JavaScript (React + TypeScript) の開発環境を整える</a></h1> <p>まず、<strong>TypeScript のビルドは ESBuild に任せることができるため何も行う必要はありません。</strong> そのため React 開発用パッケージのインストールのみ行えば大丈夫です。</p> <p>Hugo プロジェクトのルートディレクトリで下記コマンドを実行し、<code>package.json</code> を作成してから、React の開発に必要なパッケージをインストールします。</p> <pre><code class="bash">npm init -y npm install --save react react-dom </code></pre> <p>無事パッケージのインストールが完了したら、早速 TSX ファイルを <code>assets/js/App.tsx</code> に作成してしまいます。</p> <pre><code class="javascript">// assets/js/App.tsx import * as React from "react"; import * as ReactDOM from "react-dom"; function App() { return ( <> Hello React! </> ); } ReactDOM.render( <App />, document.getElementById("react") ); </code></pre> <p>上記のコードを見てもらえば分かる通り、レンダリング先に <code>id</code> が <code>react</code> の DOM ノードを指定しています。そのため Hugo 側で該当する DOM ノードを用意する必要があります。その際の HTML テンプレートは下記になります。</p> <pre><code class="html"><!-- ... --> <!-- 利用するリソースを指定する --> <span>{</span><span>{</span> with resources.Get "js/App.tsx" <span>}</span><span>}</span> <!-- id が react の div 要素を用意する --> <div id="react"></div> <!-- TSX を ESBuild でビルドする際の Hugo のオプションを指定する --> <span>{</span><span>{</span> $options := dict "targetPath" "js/app.js" "minify" true "defines" (dict "process.env.NODE_ENV" "\"development\"") <span>}</span><span>}</span> <!-- TSX のビルドを Hugo のオプションで指定した内容で実行する --> <span>{</span><span>{</span> $js := resources.Get . | js.Build $options <span>}</span><span>}</span> <!-- 一応 SRI を有効化した状態でビルドした JS を読み込む --> <span>{</span><span>{</span> $secureJS := $js | resources.Fingerprint "sha512" <span>}</span><span>}</span> <script src="<span>{</span><span>{</span> $secureJS.Permalink <span>}</span><span>}</span>" integrity="<span>{</span><span>{</span> $secureJS.Data.Integrity <span>}</span><span>}</span>"></script> <span>{</span><span>{</span> end <span>}</span><span>}</span> <!-- ... --> </code></pre> <p>ちなみに <code>$options</code> で指定している ESBuild でビルド時に指定可能なオプションは <a target="_blank" rel="nofollow noopener" href="https://gohugo.io/hugo-pipes/js/">Hugo の公式ページ</a> に記載されています。</p> <p>上記 HTML の記述を RSS リーダーを埋め込みたいページに追加します。<br /> この状態で該当ページにアクセスすると下記のような表示が確認できるはずです。</p> <p><img src="https://i.gyazo.com/7e196a2a52f492771deb5dd6913bbe60.png" alt="Hello React! と画面に表示される" /><br /> <strong>App.tsx で定義した内容が画面に表示される</strong></p> <p>これで React + TypeScript の開発環境が整いました。</p> <h1 id="RSS リーダーを実装する"><a href="#RSS+%E3%83%AA%E3%83%BC%E3%83%80%E3%83%BC%E3%82%92%E5%AE%9F%E8%A3%85%E3%81%99%E3%82%8B">RSS リーダーを実装する</a></h1> <p>あとは一般的な Web フロントエンド開発の流れで RSS リーダーの開発を進めていくだけです。</p> <h2 id="ウェブサイトで読み込みたい RSS フィードを準備する"><a href="#%E3%82%A6%E3%82%A7%E3%83%96%E3%82%B5%E3%82%A4%E3%83%88%E3%81%A7%E8%AA%AD%E3%81%BF%E8%BE%BC%E3%81%BF%E3%81%9F%E3%81%84+RSS+%E3%83%95%E3%82%A3%E3%83%BC%E3%83%89%E3%82%92%E6%BA%96%E5%82%99%E3%81%99%E3%82%8B">ウェブサイトで読み込みたい RSS フィードを準備する</a></h2> <p>RSS フィードを利用する際は必ず提供しているサービスの利用規約をご確認ください。<br /> <a target="_blank" rel="nofollow noopener" href="https://qiita.com/terms">Qiita</a> 及び <a target="_blank" rel="nofollow noopener" href="https://zenn.dev/terms">Zenn</a> については個人利用かつ自分の情報のみを扱う範囲内であれば利用が許可されているように見受けられました。</p> <p>下準備としてウェブサイトで読み込みたい RSS フィードを事前にダウンロードするためのバッチを作成します。バッチは NPM を利用して作成していきます。<strong>NPM を導入したので Hugo で利用する簡易なバッチは JavaScript でサクッと作成していきます。</strong></p> <p>まずはスクリプト作成の際に必要となるパッケージを事前にいくつかインストールします。</p> <pre><code class="bash"># html をテキスト変換にするパッケージと RSS フィードのパーサーをインストールする npm i -D --save html-to-text rss-parser </code></pre> <p>実際のコードは下記になります。ファイル名末尾が <code>.mjs</code> なのは <a target="_blank" rel="nofollow noopener" href="https://dev.to/mikeesto/top-level-await-in-node-2jad">Top-Level Await</a> を使用したいからです。</p> <pre><code class="javascript">// scripts/update-rss.mjs import { writeFileSync } from 'fs'; import pkg from 'html-to-text'; const { htmlToText } = pkg; import Parser from 'rss-parser'; const parser = new Parser(); // 自ブログで読み込みたい RSS フィードの情報を設定する const rssFeed = { Zenn: { rss_url: 'https://zenn.dev/nikaera/feed', profile_url: 'https://zenn.dev/nikaera', }, Qiita: { rss_url: 'https://qiita.com/nikaera/feed.atom', profile_url: 'https://qiita.com/nikaera', } } try { const jsonFeed = {} // RSS フィード内の description を 73字で切り取り末尾に ... を付与する関数 const spliceContent = (content) => `${htmlToText(content).slice(0, 73)}...` // rssFeed 変数で定義されてる情報を繰り返し処理する for (const [site, info] of Object.entries(rssFeed)) { // RSS フィードの URL から必要な情報を取得する const feed = await parser.parseURL(info.rss_url); // RSS フィードに登録されている項目で必要な情報のみを取得する const items = feed.items.map((i) => { return { title: i.title, content: spliceContent(i.content), url: i.link, date: i.pubDate } }) // 取得内容は jsonFeed に格納する const { rss_url, profile_url } = info jsonFeed[site] = { rss_url, profile_url, items }; } // 最後に jsonFeed に格納された内容を JSON 文字列として static/rss.json に出力する writeFileSync('./static/rss.json', JSON.stringify(jsonFeed)); } catch(err) { console.error(err); } </code></pre> <p>次に <code>package.json</code> の <code>scripts</code> に登録してコマンドとして実行可能にします。</p> <pre><code class="json">{ "scripts": { "update-rss": "node ./scripts/update-rss.mjs" } } </code></pre> <p>これで <code>npm run update-rss</code> を実行すれば自ブログで表示する際に用いる JSON ファイルとして RSS フィードの内容を <code>static/rss.json</code> に出力できます。また、JSON ファイルは <code>static</code> フォルダに出力しているため <code>http://localhost:1313/rss.json</code> でアクセスできます。</p> <p><img src="https://i.gyazo.com/508ba87c41f1c1e410b89ff1bb56be4e.png" alt="npm run update-rss を実行して出力した rss.json" /><br /> <strong>npm run update-rss を実行して出力した rss.json</strong></p> <p><img src="https://i.gyazo.com/9b7ebeedce1cb69b6b3ab8acacb0b1d1.png" alt="npm run update-rss を実行して出力した rss.json にブラウザからアクセスする" /><br /> <strong><code>http://localhost:1313/rss.json</code> にアクセスして出力した rss.json が参照可能なことを確認する</strong></p> <h2 id="RSS リーダーを React + TypeScript で実装する"><a href="#RSS+%E3%83%AA%E3%83%BC%E3%83%80%E3%83%BC%E3%82%92+React+%2B+TypeScript+%E3%81%A7%E5%AE%9F%E8%A3%85%E3%81%99%E3%82%8B">RSS リーダーを React + TypeScript で実装する</a></h2> <p>準備が整ったので、早速 RSS リーダーを作成していきます。</p> <p>下記は Hugo のテーマの 1つである <a target="_blank" rel="nofollow noopener" href="https://themes.gohugo.io/hugo-papermod/">hugo-PaperMod</a> の <code>archives</code> テンプレートを利用してページに埋め込むことを想定した RSS リーダーのコードです。</p> <pre><code class="typescript">// assets/js/Rss.tsx import React, { useMemo, useState } from 'react' import * as superagent from 'superagent'; const Rss = (props) => { const [feed, setFeed] = useState({}); const { name } = props; useMemo(() => { (async () => { try { const res = await superagent.get('/rss.json'); setFeed(res.body[name]); } catch (err) { console.error(err); } })() }, [name]); if (!("items" in feed)) return null return ( <div className="archive-month"> <h3 className="archive-month-header"> <a href={feed.profile_url} target="_blank" rel="noopener noreferrer">{name}</a> - <a href={feed.rss_url} target="_blank" rel="noopener noreferrer">RSS</a> </h3> <div className="archive-posts"> {feed.items.map((item) => { return <div className="archive-entry" key={item.url}> <h3 className="archive-entry-title">{item.title}</h3> <div className="archive-meta">{item.date} - {item.content}</div> <a className="entry-link" href={item.url} target="_blank" rel="noopener noreferrer">&nbsp;</a> </div> })} </div> </div> ) } export default Rss </code></pre> <p>次に <code>assets/js/App.tsx</code> で <code>assets/js/Rss.tsx</code> を読み込み画面に表示できるよう改修します。</p> <pre><code class="javascript">// assets/js/App.tsx import Rss from './Rss'; import * as React from "react"; import * as ReactDOM from "react-dom"; function App() { return ( <> <div class="archive-year"> <h2 class="archive-year-header"> Tech 🦾 </h2> <Rss name="Zenn" /> <Rss name="Qiita" /> </div> </> ); } ReactDOM.render( <App />, document.getElementById("react") ); </code></pre> <p>これで RSS リーダーを埋め込んだページを閲覧すると下記のような画面が表示されるはずです。</p> <p><img src="https://i.gyazo.com/0a6b8923d141ae70f5e298637f5acc69.png" alt="hugo-PaperMod で archives テンプレートを用いて RSS リーダーを表示する" /><br /> <strong>hugo-PaperMod で <code>archives</code> テンプレートを用いて RSS リーダーを表示したときの画面</strong></p> <p>もし他の RSS フィードを追加したい場合は <code>scripts/update-rss.mjs</code> の <code>rssFeed</code> 変数に情報を追加して、<code>App.tsx</code> に <code><Rss name="<rssFeed 変数で定義した RSS Feed 名>" /></code> を定義することで対応できます。</p> <h1 id="RSS フィードの内容を自動で更新する"><a href="#RSS+%E3%83%95%E3%82%A3%E3%83%BC%E3%83%89%E3%81%AE%E5%86%85%E5%AE%B9%E3%82%92%E8%87%AA%E5%8B%95%E3%81%A7%E6%9B%B4%E6%96%B0%E3%81%99%E3%82%8B">RSS フィードの内容を自動で更新する</a></h1> <p><code>npm run update-rss</code> を手元で実行して <code>static/rss.json</code> を更新して公開すれば、最新の RSS フィードの内容をページに反映できる状態ですが、都度手動で更新するのは面倒な作業です。</p> <p>そこで今回は GitHub Actions の <code>schedule</code> を用いて <code>static/rss.json</code> の更新を自動化します。</p> <h2 id="GitHub Actions のワークフローファイルを作成する"><a href="#GitHub+Actions+%E3%81%AE%E3%83%AF%E3%83%BC%E3%82%AF%E3%83%95%E3%83%AD%E3%83%BC%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%82%92%E4%BD%9C%E6%88%90%E3%81%99%E3%82%8B">GitHub Actions のワークフローファイルを作成する</a></h2> <p>実際のワークフローファイルは下記になります。<code>schedule</code> の項目で設定している内容がワークフローの実行スケジュールになります。今回は半日毎に更新が走るようにしました。</p> <pre><code class="yml"># .github/workflows/update-rss.yml name: update rss json file on: push: branches: - main # Set a branch name to trigger deployment schedule: - cron: '0 */12 * * *' # 今回は半日に 1回のタイミングで更新するようにした jobs: build: runs-on: ubuntu-18.04 steps: - uses: actions/checkout@v2 with: ref: main submodules: true # Fetch Hugo themes (true OR recursive) fetch-depth: 0 # Fetch all history for .GitInfo and .Lastmod - name: Use Node.js 14.10.1 uses: actions/setup-node@v1 with: node-version: 14.10.1 - name: Install dependencies run: npm install - name: Update RSS Feeds run: npm run update-rss - name: Commit files run: | git config --local user.email "[email protected]" git config --local user.name "GitHub Action" git add static/rss.json STATUS=$(git status -s) if [ -n "$STATUS" ]; then git commit -m "Update rss.json `date +'%Y-%m-%d %H:%M:%S'`" -a git push origin main fi </code></pre> <p>上記ワークフローファイルをプロジェクトに追加して、リモートリポジトリにプッシュした後は、ワークフローが実行されるタイミングを待ちます。</p> <p>無事にワークフローの実行が完了すると下記のようなコミットが追加されているはずです。</p> <p><img src="https://i.gyazo.com/ebb7cb2e64b13e4a1e1a592836f511f5.png" alt="GitHub Actions が JSON ファイルを更新してコミットしている" /><br /> <strong>GitHub Actions が JSON ファイルを更新してコミットしている</strong></p> <p><img src="https://i.gyazo.com/1a74399a3cf1053e0480a01590086fbe.png" alt="コミットの詳細を見ると正常に JSON ファイルが更新されていることを確認できる" /><br /> <strong>コミットの詳細を見ると正常に JSON ファイルが更新されていることが確認できる</strong></p> <p><img src="https://i.gyazo.com/bf86668d5fc32ca09b6d2cfcf71262ce.png" alt="コミット後 Hugo をビルド & デプロイするとページが更新されていることを確認できる" /><br /> <strong>コミット後 Hugo をビルド & デプロイするとページが更新されていることを確認できる</strong></p> <p>これで Zenn や Qiita 等に記事を書いた際に、都度手動で <code>static/rss.json</code> を更新してページに最新の内容を反映させる作業は必要なくなりました。</p> <h1 id="(余談) RSS リーダーを Hugo の Data Templates で実装する"><a href="#%28%E4%BD%99%E8%AB%87%29+RSS+%E3%83%AA%E3%83%BC%E3%83%80%E3%83%BC%E3%82%92+Hugo+%E3%81%AE+Data+Templates+%E3%81%A7%E5%AE%9F%E8%A3%85%E3%81%99%E3%82%8B">(余談) RSS リーダーを Hugo の Data Templates で実装する</a></h1> <p>ちなみに Hugo には <a target="_blank" rel="nofollow noopener" href="https://gohugo.io/templates/data-templates/">Data Templates</a> という仕組みがあり、これを用いることで実は JavaScript を利用しなくても HTML テンプレートで RSS リーダーを実現できるということを後から知りました。</p> <p>そこで最後に Data Template での RSS リーダーの実装方法について記載します。</p> <p>まずは、<code>scripts/update-rss.mjs</code> の内容を書き換えます。</p> <pre><code class="typescript">// scripts/update-rss.mjs import { writeFileSync } from 'fs'; import pkg from 'html-to-text'; const { htmlToText } = pkg; import Parser from 'rss-parser'; const parser = new Parser(); const rssFeed = { Zenn: { rss_url: 'https://zenn.dev/nikaera/feed', profile_url: 'https://zenn.dev/nikaera' }, Qiita: { rss_url: 'https://qiita.com/nikaera/feed.atom', profile_url: 'https://qiita.com/nikaera' } } try { const jsonFeed = {} const spliceContent = (content) => `${htmlToText(content).slice(0, 73)}...` for (const [site, info] of Object.entries(rssFeed)) { const feed = await parser.parseURL(info.rss_url); const items = feed.items.map((i) => { console.log(i); return { title: i.title, content: spliceContent(i.content), url: i.link, date: i.pubDate } }) const { rss_url, profile_url } = info jsonFeed[site] = { rss_url, profile_url, items }; /* 最終的な JSON ファイルの出力先は data フォルダとなり、RSS フィード毎に出力する 例: ./data/Qiita.json, ./data/Zenn.json, etc. */ writeFileSync(`./data/${site}.json`, JSON.stringify(jsonFeed[site])); } } catch(err) { console.error(err); } </code></pre> <p>上記を実行することで <code>data/Qiita.json</code> や <code>data/Zenn.json</code> にファイルが出力されます。</p> <p>Hugo の Data Template を用いると <code>data</code> フォルダ内に配置した <code>json</code>, <code>yaml</code>, <code>toml</code> 形式のファイルは Go の HTML テンプレートで読み込めるようになります。</p> <p>例えば、<strong><code>data/Qiita.json</code> に配置された JSON ファイルを読み込みたい場合は Go のテンプレートで <code>$Qiita := $.Site.Data.Qiita</code> のような記述でできます。</strong></p> <p>次に RSS リーダーを埋め込んでいたページを下記のように書き換えます。</p> <pre><code class="html"><!-- ... --> <!-- React 関連の記述を全て削除する --> <!-- <span>{</span><span>{</span> with resources.Get "js/App.tsx" <span>}</span><span>}</span> <div id="react"></div> <span>{</span><span>{</span> $options := dict "targetPath" "js/app.js" "minify" true "defines" (dict "process.env.NODE_ENV" "\"development\"") <span>}</span><span>}</span> <span>{</span><span>{</span> $js := resources.Get . | js.Build $options <span>}</span><span>}</span> <span>{</span><span>{</span> $secureJS := $js | resources.Fingerprint "sha512" <span>}</span><span>}</span> <script src="<span>{</span><span>{</span> $secureJS.Permalink <span>}</span><span>}</span>" integrity="<span>{</span><span>{</span> $secureJS.Data.Integrity <span>}</span><span>}</span>"></script> <span>{</span><span>{</span> end <span>}</span><span>}</span> --> <div class="archive-year"> <h2 class="archive-year-header"> Tech 🦾 </h2> <div class="archive-month"> <!-- data/Zenn.json の内容を読み込む --> <span>{</span><span>{</span> $Zenn := $.Site.Data.Zenn <span>}</span><span>}</span> <h3 class="archive-month-header"> <a href="<span>{</span><span>{</span> $Zenn.profile_url <span>}</span><span>}</span>" target="_blank" rel="noopener noreferrer">Zenn</a> - <a href="<span>{</span><span>{</span> $Zenn.rss_url <span>}</span><span>}</span>" target="_blank" rel="noopener noreferrer">RSS</a> </h3> <div class="archive-posts"> <!-- 配列で格納されている記事情報を繰り返し処理で取得する --> <span>{</span><span>{</span>- range $Zenn.items <span>}</span><span>}</span> <div class="archive-entry" key="<span>{</span><span>{</span> .url <span>}</span><span>}</span>"> <h3 class="archive-entry-title"><span>{</span><span>{</span> .title <span>}</span><span>}</span></h3> <div class="archive-meta"><span>{</span><span>{</span> .date <span>}</span><span>}</span> - <span>{</span><span>{</span> .content <span>}</span><span>}</span></div> <a class="entry-link" aria-label="<span>{</span><span>{</span> .content <span>}</span><span>}</span>" href="<span>{</span><span>{</span> .url <span>}</span><span>}</span>" target=" _blank" rel="noopener noreferrer"></a> </div> <span>{</span><span>{</span>- end <span>}</span><span>}</span> </div> </div> <div class="archive-month"> <!-- data/Qiita.json の内容を読み込む --> <span>{</span><span>{</span> $Qiita := $.Site.Data.Qiita <span>}</span><span>}</span> <h3 class="archive-month-header"> <a href="<span>{</span><span>{</span> $Qiita.profile_url <span>}</span><span>}</span>" target="_blank" rel="noopener noreferrer">Qiita</a> - <a href="<span>{</span><span>{</span> $Qiita.rss_url <span>}</span><span>}</span>" target="_blank" rel="noopener noreferrer">RSS</a> </h3> <div class="archive-posts"> <!-- 配列で格納されている記事情報を繰り返し処理で取得する --> <span>{</span><span>{</span>- range $Qiita.items <span>}</span><span>}</span> <div class="archive-entry" key="<span>{</span><span>{</span> .url <span>}</span><span>}</span>"> <h3 class="archive-entry-title"><span>{</span><span>{</span> .title <span>}</span><span>}</span></h3> <div class="archive-meta"><span>{</span><span>{</span> .date <span>}</span><span>}</span> - <span>{</span><span>{</span> .content <span>}</span><span>}</span></div> <a class="entry-link" aria-label="<span>{</span><span>{</span> .content <span>}</span><span>}</span>" href="<span>{</span><span>{</span> .url <span>}</span><span>}</span>" target=" _blank" rel="noopener noreferrer"></a> </div> <span>{</span><span>{</span>- end <span>}</span><span>}</span> </div> </div> </div> <!-- ... --> </code></pre> <p>また GitHub Actions のワークフローを用いて RSS フィードの情報を更新していた場合は、<code>.github/workflows/update-rss.yml</code> ファイルの更新も必要になります。</p> <pre><code class="yml"># .github/workflows/update-rss.yml name: update rss json file on: push: branches: - main # Set a branch name to trigger deployment schedule: - cron: '0 */12 * * *' jobs: build: runs-on: ubuntu-18.04 steps: - uses: actions/checkout@v2 with: ref: main submodules: true # Fetch Hugo themes (true OR recursive) fetch-depth: 0 # Fetch all history for .GitInfo and .Lastmod - name: Use Node.js 14.10.1 uses: actions/setup-node@v1 with: node-version: 14.10.1 - name: Install dependencies run: npm install - name: Update RSS Feeds run: npm run update-rss # Git で追加する内容を data フォルダに変更する # git add static/rss.json -> git add data/ - name: Commit files run: | git config --local user.email "[email protected]" git config --local user.name "GitHub Action" git add data/ STATUS=$(git status -s) if [ -n "$STATUS" ]; then git commit -m "Update data folder `date +'%Y-%m-%d %H:%M:%S'`" -a git push origin main fi </code></pre> <p>これで JavaScript で作成した RSS リーダーから、Hugo の Data Templates を用いて作成した RSS リーダーへ移行できました。</p> <h1 id="おわりに"><a href="#%E3%81%8A%E3%82%8F%E3%82%8A%E3%81%AB">おわりに</a></h1> <p>Hugo で React + TypeScript 開発を楽にできそうなことが分かり、テンションが上がってしまい、そのままのノリで実際に RSS リーダーを自ブログ向けに作成してみました。</p> <p>しかし、本記事内容で RSS リーダーを実装するのであれば、Hugo の Data Templates を利用することがベストなことに後から気づきました。ただ Hugo での JavaScript を用いた開発手法が理解でき勉強になったので結果ヨシとしました。</p> <p>Hugo での JavaScript 開発環境は相当充実していることが分かったので、また何かアイデアを思いついたら気軽に作って自ブログに取り込んでいきます。今はザックリ WebGL/WebVR とかで何か面白いもの作れそうだなと考えています。</p> <h1 id="参考リンク"><a href="#%E5%8F%82%E8%80%83%E3%83%AA%E3%83%B3%E3%82%AF">参考リンク</a></h1> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://esbuild.github.io/">esbuild - An extremely fast JavaScript bundler</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://gohugo.io/templates/data-templates/">Data Templates | Hugo</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://gohugo.io/functions/">Functions Quick Reference | Hugo</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://gohugo.io/hugo-pipes/js/">JavaScript Building | Hugo</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://reactjs.org/docs/hooks-intro.html">Introducing Hooks – React</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://github.com/rbren/rss-parser">rbren/rss-parser: A lightweight RSS parser, for Node and the browser</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://github.com/html-to-text/node-html-to-text">html-to-text/node-html-to-text: Advanced html to text converter</a></li> </ul> nikaera tag:crieit.net,2005:PublicArticle/16172 2020-10-25T17:10:29+09:00 2020-10-25T17:22:39+09:00 https://crieit.net/posts/TypeScript-any-unknown TypeScriptのanyとunknown、object使い分け <p>any型:なんでも良い場合<br /> unknown型:型が特定できない場合<br /> object型:オブジェクトであることが重要で中身は問わない場合</p> nabe tag:crieit.net,2005:PublicArticle/16071 2020-09-25T02:54:21+09:00 2020-09-25T02:56:46+09:00 https://crieit.net/posts/Javascript-PDF JavascriptでPDFを作成するライブラリまとめと比較 <p><a href="https://crieit.now.sh/upload_images/72b6800e676458962e78520f80fde1905f6cdddc140c5.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/72b6800e676458962e78520f80fde1905f6cdddc140c5.png?mw=700" alt="image" /></a><br /> はじめまして。<br /> <a target="_blank" rel="nofollow noopener" href="https://labelmake.jp/">PDFの作成サービス</a>を個人で運用しており、数万ページのPDFを作成しており、<a target="_blank" rel="nofollow noopener" href="https://github.com/hand-dot/labelmake">JavascriptでのPDFの作成ライブラリ</a>を作ったりでそれなりに詳しくなってきたのでこの記事を作成しました。笑</p> <p>Javascriptで扱えるPDFのライブラリーはいくつかあります。しかし、実際どれを使えばいいのかわかりにくいので、それらを比較しながら紹介していきます。</p> <p>UMDモジュールとして提供されていて、近年のフロントエンド(TypeScript/Webpack/React/etc.)で扱いやすい?Nodeとブラウザーで動く?型はあるか?日本語フォントは使えるのか?という観点でも比較していきます。</p> <p><strong>この記事ではJavascriptで扱えるPDF作成ライブラリーで「どれを採用するか」ということで悩んでいる時に、用途に合ったライブラリを発見することができるようにします。</strong></p> <h2 id="PDFKit"><a href="#PDFKit">PDFKit</a></h2> <p><a href="https://crieit.now.sh/upload_images/287fd18c57a76a8f56e19136fce9d4bb5f6cda03acf7b.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/287fd18c57a76a8f56e19136fce9d4bb5f6cda03acf7b.png?mw=700" alt="image" /></a></p> <p><a target="_blank" rel="nofollow noopener" href="https://pdfkit.org/">公式サイト:https://pdfkit.org/</a></p> <p><strong>PDFKitはJavascriptでPDFを作成するライブラリの元祖的存在。</strong> フォントの埋め込みや画像の埋め込みもできます。開発が2012年から行われており古いですが、今でもメンテナンスが行われております。多少複雑ですがNodeだけでなく、 <a target="_blank" rel="nofollow noopener" href="https://github.com/foliojs/pdfkit#browser-usage">Webpackを利用してブラウザで動かすことも可能です。</a></p> <p>下記に紹介するライブラリでもPDFKitのラッパーとして作成されたものや、プログラムを参考にしているものもあり、フォントの埋め込みも含めてJavascriptでPDF作成を行うというパワープレーを最初に成し遂げたライブラリと言うだけあってすごいライブラリです。</p> <p>よくない点をあげるなら、高レベルのAPIを提供していないで操作に慣れるまでに一定の学習が必要です。最初はデザインがしづらく、記述が複雑になりがちです。</p> <div class="table-responsive"><table> <thead> <tr> <th>ポイント</th> <th>評価</th> </tr> </thead> <tbody> <tr> <td>Nodeとブラウザーで動く</td> <td>△(若干手間)</td> </tr> <tr> <td>型はあるか</td> <td>○(DefinitelyTyped)</td> </tr> <tr> <td>日本語フォントは使えるのか</td> <td>○(ブラウザで使う場合は要注意)</td> </tr> <tr> <td>使いやすさ</td> <td>△プログラムの手続きが複雑</td> </tr> </tbody> </table></div> <hr /> <h2 id="pdfmake"><a href="#pdfmake">pdfmake</a></h2> <p><a href="https://crieit.now.sh/upload_images/2647ea287bd70147a7e7f1bc0ef4fe4e5f6cda73ce101.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/2647ea287bd70147a7e7f1bc0ef4fe4e5f6cda73ce101.png?mw=700" alt="image" /></a></p> <p><a target="_blank" rel="nofollow noopener" href="http://pdfmake.org/">公式サイト:http://pdfmake.org/</a></p> <p><strong>上記で紹介したPDFKitのラッパーとして作成されたのがpdfmakeです。 PDFKitとの違いはPDFKitが命令的なプログラミング操作のAPIを提供しているのに対して、pdfmakeは宣言的でレイアウトによる計算をpdfmakeが行ってくれるためプログラムが比較的少なくなります。</strong></p> <p>PDFKitの改良版としてGithubでのスター数はpdfmakeの方が多く、Web上でも様々な使い方や例があり、人気のライブラリということがわかります。また、画像の埋め込みはもちろん、QRコード,SVGのレンダリングなどの機能もあります。</p> <p>実際にPDFkitと比較した時にその使いやすさから本サービスのlabelmake.jpも当初はpdfmakeを利用してPDFファイルを作成しておりました。</p> <p>しかし、</p> <ul> <li>独自フォントの作成し、利用する時にWebpack利用時にハマりどころがある</li> <li>独自フォントはbase64エンコードされたttfが利用できるが日本語フォントの場合に巨大になり、メモリを大量に消費する</li> </ul> <p>と言う点がネックになり、途中で後で紹介するpdf-libというものに切り替えています。</p> <p>Web上には様々な例があるため、トライアンドエラーを繰り返しながらなんとかなるとは思いますが、Webpackやtypescriptを使った最新のフロントエンドではハマりどころが多く、その手の記事が無かったりするので注意が必要です。 また、日本語フォントを利用する際は一度gitのrepositoryをクローンしてからフォント作成スクリプトを使ってビルドする必要があります。</p> <div class="table-responsive"><table> <thead> <tr> <th>ポイント</th> <th>評価</th> </tr> </thead> <tbody> <tr> <td>Nodeとブラウザーで動く</td> <td>△(Webpackなどを利用している場合はハマる)</td> </tr> <tr> <td>型はあるか</td> <td>○(DefinitelyTyped)</td> </tr> <tr> <td>日本語フォントは使えるのか</td> <td>△(自前でビルドが必要)</td> </tr> <tr> <td>使いやすさ</td> <td>○(宣言的でpdfmakeのレイアウトエンジンが利用可能)</td> </tr> </tbody> </table></div> <hr /> <h2 id="jsPDF"><a href="#jsPDF">jsPDF</a></h2> <p><a href="https://crieit.now.sh/upload_images/cfed57cc5b84c8c2d1910334c10275fd5f6cda89bd93d.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/cfed57cc5b84c8c2d1910334c10275fd5f6cda89bd93d.png?mw=700" alt="image" /></a></p> <p><a target="_blank" rel="nofollow noopener" href="https://github.com/MrRio/jsPDF">公式サイト:https://github.com/MrRio/jsPDF</a></p> <p><strong>jsPDFは上記のPDFKitとは別に独自にPDF作成ができるライブラリです。 この記事で紹介するライブラリの中で一番スター数が多く、比較的安定してメンテナンスされています。</strong></p> <p>node,ブラウザーで利用できるUMDモジュールも提供されています。(nodeで保存する場合はカレントディレクトリに保存されるので使い勝手悪いかもです)</p> <p>命令的な操作ができるAPIが提供されており、複雑な帳票をプログラムする場合は難易度が高い印象です。</p> <p>また、日本語を扱う場合は日本語のフォントを読み込ませる必要がありますが、ここにハマりどころがあるようです。<br /> 詳細は下記の記事にまとまっています</p> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/JunichiWatanuki/items/07bcb842e5532068fd62">jsPDFで、無理やり日本語出力を行ってみる </a></li> <li><a target="_blank" rel="nofollow noopener" href="https://blog.ver001.com/javascript-jspdf-japanese/">JavaScriptでjsPDFを使ってPDFファイルを生成する(日本語対応)</a></li> </ul> <p>上記を参考にしていただければわかりますが、日本語フォントを扱うための方法はありますが、一手間必要になります。(ttfファイルの変換が必要)</p> <p>できることは多いのですが、上記で説明したように操作が命令的でなため、使いこなすには一定のインプットが必要になります。(幸いにもリッチなドキュメントがあります <a target="_blank" rel="nofollow noopener" href="https://rawgit.com/MrRio/jsPDF/master/docs/index.html">https://rawgit.com/MrRio/jsPDF/master/docs/index.html</a>)<br /> また、英語の記事も多く人気なのは間違いないと思います。</p> <p>ドキュメントを参考にして自分の用途に合った操作ができる場合はおすすめできます。(ドキュメントみた感じだとAcroFormとかできちゃいます。ヤバイな)</p> <div class="table-responsive"><table> <thead> <tr> <th>ポイント</th> <th>評価</th> </tr> </thead> <tbody> <tr> <td>Nodeとブラウザーで動く</td> <td>○(<a target="_blank" rel="nofollow noopener" href="https://github.com/MrRio/jsPDF#typescriptangularwebpackreactetc-configuration">参考</a>)</td> </tr> <tr> <td>型はあるか</td> <td>○</td> </tr> <tr> <td>日本語フォントは使えるのか</td> <td>○(ttfファイルの変換が必要)</td> </tr> <tr> <td>使いやすさ</td> <td>△プログラムの手続きが複雑</td> </tr> </tbody> </table></div> <hr /> <h2 id="pdf-lib"><a href="#pdf-lib">pdf-lib</a></h2> <p><a href="https://crieit.now.sh/upload_images/aeb78cf46f1fadacd773efbc17dc76e35f6cdaa3bde4f.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/aeb78cf46f1fadacd773efbc17dc76e35f6cdaa3bde4f.png?mw=700" alt="image" /></a></p> <p><a target="_blank" rel="nofollow noopener" href="https://pdf-lib.js.org/">公式サイト:https://pdf-lib.js.org/</a></p> <p><strong>pdf-libはPDFKitを参考にTypescriptで実装されたPDFの作成、編集ライブラリです。 他のライブラリに比べて後発でリリースされたライブラリですが、非常によくできており、もちろんnode,ブラウザで動作します。また、PDFの結合、分割や埋め込みをサポートし、他のライブラリにない機能を持っていますが、ライブラリとしてシンプルで強力です。</strong></p> <p><strong>他のライブラリと比較して大きく違う点がフォントファイルを埋め込む際に<code>Uint8Array</code>,<code>ArrayBuffer</code>が使用できる</strong>ので、nodeの場合は<code>fs</code>で、ブラウザの場合は<code>xhr</code>を使ってフォントファイルを読み込むことができます。日本語の馬鹿でかいbase64のファイルを読み込む必要がなくなり、パフォーマンス的にも優れています。</p> <p>もちろん、Webpackなどで利用でき、ドキュメントも充実しています。 本サービスのlabelmake.jpも当初はpdfmakeを利用していましたが、途中からパフォーマンスの観点でPDF作成処理をpdf-libに行わせるよう変更しました。</p> <p>欠点としては提供されているAPIが命令的なため複雑な帳票の場合、プログラムが複雑になってしまうという点と考えています。また<strong>レイアウトについてはpdf-libは責務を持たず、ユーザーに計算させるという方針ですので、中央揃えや右揃えなどのレイアウトはできません。</strong>あと地味ですが、位置情報の単位が<code>pt</code>なので、位置を指定する時に若干面倒です。</p> <p>個人的には後に紹介するlabelmakeを除くライブラリに比べて一番扱いやすく、パフォーマンスの観点でも優秀だと考えています。</p> <p>まだ日本語での記事数はpdfmake,jsPDFと比べると少ないのですが、帳票が比較的シンプルでモダンな環境での開発の場合は参考にしてみてください。おすすめです。</p> <div class="table-responsive"><table> <thead> <tr> <th>ポイント</th> <th>評価</th> </tr> </thead> <tbody> <tr> <td>Nodeとブラウザーで動く</td> <td>○</td> </tr> <tr> <td>型はあるか</td> <td>○</td> </tr> <tr> <td>日本語フォントは使えるのか</td> <td>○</td> </tr> <tr> <td>使いやすさ</td> <td>△プログラムの手続きが複雑+レイアウトの計算は自分で行う必要がある</td> </tr> </tbody> </table></div> <hr /> <h2 id="labelmake"><a href="#labelmake">labelmake</a></h2> <p><a href="https://crieit.now.sh/upload_images/c2fcbd38933cf75273dc378858f1da235f6cdab902f0f.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/c2fcbd38933cf75273dc378858f1da235f6cdab902f0f.png?mw=700" alt="image" /></a></p> <p><a target="_blank" rel="nofollow noopener" href="https://labelmake.jp/javascript-pdf-generator-library">公式サイト:https://labelmake.jp/javascript-pdf-generator-library</a></p> <p><strong>labelmakeはpdf-libのラッパーとして筆者が開発しました。 特徴としてはpdf-libを使っている人はレイアウトは自分で計算する必要がありましたが、位置情報の単位をmmにし、alignment,line-heightなどのレイアウトの計算を行い、宣言的にデータでPDFを作成できるようにAPIをデザインしました。</strong> また既存のPDFをベースとして読み込んでそこに入力項目を足すことができます。</p> <p>コードの例は<a target="_blank" rel="nofollow noopener" href="https://labelmake.jp/javascript-pdf-generator-library/example">こちらからご覧になれます。</a><br /> スキーマがあるので、テンプレート的に帳票生成などに利用ができます。</p> <p>帳票のデザインデータをJSONで管理できる点も複数のテンプレートを利用する際には読み込みがシンプルになり利用しやすいです。</p> <p>pdf-libのメリットのフォントのデータで<code>Uint8Array</code>,<code>ArrayBuffer</code>が使用できる点や、PDFファイルの埋め込みができる点を利用して効率的に複雑な帳票が作成できるようにしています。</p> <p><strong>また、<a target="_blank" rel="nofollow noopener" href="https://labelmake.jp/javascript-pdf-generator-library/template-design">こちらのTemplate Design & Code Generator</a>を使い、帳票のデザインと、実行可能コードの生成を行うことができます。</strong></p> <p><a href="https://crieit.now.sh/upload_images/fa948ee1c0bf1cf7c5b8b90d55e0e6f15f6cdad97418f.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/fa948ee1c0bf1cf7c5b8b90d55e0e6f15f6cdad97418f.png?mw=700" alt="image" /></a></p> <p>ポジショントークにならないようにしたいと思いますが、既存のPDFファイルに対して入力項目を設定する形で帳票が作成でき、実際に本サービスでは現時点で様々なテンプレートをつかってPDFを作成しています。</p> <p>デメリットとしてはまだ利用者も少なく、有名なライブラリではありません。<br /> 日本語フォントで利用していますが、他にも自分のサービスだけでは見つけられなかったバグがあるかもしれません。</p> <p><strong>そして、自由に0から帳票すると言うよりは既存のPDFをもとに入力項目を設置するという使い方がハマります。(俗に言うバリアブル印刷というやつです)<br /> 他のJavascriptのPDF作成ライブラリと比較しても、用途が合っている場合は、我ながらよくできていると思っていますので、ぜひスターやシェアをしていただけると嬉しいです 笑</strong></p> <div class="table-responsive"><table> <thead> <tr> <th>ポイント</th> <th>評価</th> </tr> </thead> <tbody> <tr> <td>Nodeとブラウザーで動く</td> <td>○</td> </tr> <tr> <td>型はあるか</td> <td>○</td> </tr> <tr> <td>日本語フォントは使えるのか</td> <td>○</td> </tr> <tr> <td>使いやすさ</td> <td>○</td> </tr> </tbody> </table></div> <hr /> <h2 id="番外編"><a href="#%E7%95%AA%E5%A4%96%E7%B7%A8">番外編</a></h2> <h3 id="PDF.js"><a href="#PDF.js">PDF.js</a></h3> <p><a href="https://crieit.now.sh/upload_images/d4c455428e1bc1bcb7f22658c09c4adb5f6cdaf202c18.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/d4c455428e1bc1bcb7f22658c09c4adb5f6cdaf202c18.png?mw=700" alt="image" /></a></p> <p><a target="_blank" rel="nofollow noopener" href="https://mozilla.github.io/pdf.js/">公式サイト:https://mozilla.github.io/pdf.js/</a></p> <p>よく間違えられるのですが、非常に有名なライブラリなので紹介します。<br /> PDF.jsはPDFのレンダリングライブラリで、PDFの作成ライブラリではありません。<br /> PDFを表示するだけでなく、PDFのレンダリング結果をcanvasに転写し、PDFのレンダリング画像を取得することができます。</p> <p>Webpack用のビルドもバンドルされており、最近のフロントエンドの環境でも利用可能です。</p> <hr /> <h3 id="Puppeteer"><a href="#Puppeteer">Puppeteer</a></h3> <p><a href="https://crieit.now.sh/upload_images/6fc4effa9a96db29ebd3ba9a1351a1ae5f6cdb0ba9c4e.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6fc4effa9a96db29ebd3ba9a1351a1ae5f6cdb0ba9c4e.png?mw=700" alt="image" /></a></p> <p><a target="_blank" rel="nofollow noopener" href="https://github.com/puppeteer/puppeteer">公式サイト:https://github.com/puppeteer/puppeteer</a></p> <p>GoogleのPuppeteerでもPDFファイルの作成が可能です。<br /> テンプレートはhtmlとして管理する必要がありますが、Webの開発者なら装飾も行いやすいかと思います。</p> <p>実装は下記の記事が参考になります。<br /> - <a target="_blank" rel="nofollow noopener" href="https://dev.to/damcosset/generate-a-pdf-from-html-and-back-on-the-front-5ff5">Generate a PDF from HTML with puppeteer<br /> </a><br /> - <a target="_blank" rel="nofollow noopener" href="https://uyamazak.hatenablog.com/entry/2018/02/08/192304">Headless Chrome + puppeteerを使ったHTML→PDF変換サーバーを作る puppeteer編</a></p> <p>バックエンドの実装が必要になるのと、PDF作成するまでにPuppeteerが起動するオーバーヘッドがありますが、それらが問題ない場合で、htmlでデザインしたいと言う場合には選択肢になるかもしれません。(可変のテーブルなどはデザインしやすいかと思います。)</p> <hr /> <h2 id="まとめ"><a href="#%E3%81%BE%E3%81%A8%E3%82%81">まとめ</a></h2> <p>番外編を除く下記の5つのライブラリを紹介させていただきました。</p> <ol> <li>PDFKit</li> <li>pdfmake</li> <li>jsPDF</li> <li>pdf-lib</li> <li>labelmake</li> </ol> <p>参考になったでしょうか?<br /> 筆者はpdf-lib押しです。パフォーマンスも良く、レイアウトの計算が面倒ですがモダンなフロントエンド環境でも使いやすいです。</p> <p>あとは、遊びでも構わないのでlabelmakeを触っていただけると嬉しいです!</p> <h4><a target="_blank" rel="nofollow noopener" href="https://labelmake.jp/javascript-pdf-generator-library">Webページはこちらからご覧いただけます!</a></h4> <p>それでは!</p> hand-dot tag:crieit.net,2005:PublicArticle/15975 2020-06-23T09:37:52+09:00 2020-06-23T09:43:27+09:00 https://crieit.net/posts/game1week 【1週間】Web ゲームを爆速で開発するために実践したこと🐧個人開発向け <p>(内心)<br /> 作ったゲームを沢山の人に触ってもらってリアクションが欲しいなぁ~<br /> そうだ、記事投稿して宣伝しよう!</p> <p>といった<strong>下心で書いた記事です</strong>。すみませんでした 遊んでください😆</p> <blockquote class="twitter-tweet"><p lang="ja" dir="ltr">Web Game 処女作🎉シンプルでストレス発散になるゲームが爆誕!敵さんを吹っ飛ばして 1,000 G 以上を目指してみてね😆<a target="_blank" rel="nofollow noopener" href="https://t.co/h8w574aGGQ">https://t.co/h8w574aGGQ</a>★アドバイス急募★『こうしたらもっと面白くなる』など、ご意見ご感想を頂けると嬉しいっす(〃´∪`〃)ゞ<a target="_blank" rel="nofollow noopener" href="https://twitter.com/hashtag/%E4%BB%8A%E6%97%A5%E3%81%AE%E7%A9%8D%E3%81%BF%E4%B8%8A%E3%81%92?src=hash&ref_src=twsrc%5Etfw">#今日の積み上げ</a> <a target="_blank" rel="nofollow noopener" href="https://twitter.com/hashtag/100DaysOfCode?src=hash&ref_src=twsrc%5Etfw">#100DaysOfCode</a> DAY 21 <a target="_blank" rel="nofollow noopener" href="https://t.co/HJvFj87zTf">pic.twitter.com/HJvFj87zTf</a></p>— hikaru🐧#100DaysOfCode! (@hikaru_firecamp) <a target="_blank" rel="nofollow noopener" href="https://twitter.com/hikaru_firecamp/status/1273289660186292224?ref_src=twsrc%5Etfw">June 17, 2020</a></blockquote> <blockquote> <p>ペンギンがサメさんを倒すゲーム<br /> 敵さんを吹っ飛ばして 1,000 G 以上を目指してみてね😆<br /> <a target="_blank" rel="nofollow noopener" href="https://games.westa.io/">https://games.westa.io/</a></p> </blockquote> <h2 id="0⃣ 結論 / 爆速で開発するために実践したこと"><a href="#0%E2%83%A3+%E7%B5%90%E8%AB%96+%2F+%E7%88%86%E9%80%9F%E3%81%A7%E9%96%8B%E7%99%BA%E3%81%99%E3%82%8B%E3%81%9F%E3%82%81%E3%81%AB%E5%AE%9F%E8%B7%B5%E3%81%97%E3%81%9F%E3%81%93%E3%81%A8">0⃣ 結論 / 爆速で開発するために実践したこと</a></h2> <ul> <li>1週間のカウントダウンタイマーを設置する</li> <li>適当な企画書を作って全体を把握しならが作業する</li> <li>新しいことは1個までに制限して知っているものを使う</li> <li>こだわりたい部分を絞り他を捨てる覚悟を持つ</li> </ul> <h2 id="1️⃣ この記事の対象読者"><a href="#1%EF%B8%8F%E2%83%A3+%E3%81%93%E3%81%AE%E8%A8%98%E4%BA%8B%E3%81%AE%E5%AF%BE%E8%B1%A1%E8%AA%AD%E8%80%85">1️⃣ この記事の対象読者</a></h2> <p>この記事では<strong>1週間で Web ゲームを作る為に何を考え何をしたのか</strong>を書きました。<br /> 以下に当てはまる人達に参考になれば嬉しいです。</p> <ul> <li>好きなことへのこだわりが強く<strong>いつまでも作品が完成しない</strong>人</li> <li>いつも<strong>ダラダラ期限を伸ばしてしまう</strong>人</li> <li>このゲームがどんなライブラリ使っているか知りたい人</li> <li>奇特な人</li> </ul> <h2 id="2️⃣ なぜ1週間なの?"><a href="#2%EF%B8%8F%E2%83%A3+%E3%81%AA%E3%81%9C%EF%BC%91%E9%80%B1%E9%96%93%E3%81%AA%E3%81%AE%EF%BC%9F">2️⃣ なぜ1週間なの?</a></h2> <p>個人開発の一番の敵って<strong>いかにモチベーションを保つか</strong>だと思いませんか?</p> <p>私の場合1ヶ月もたてば他に興味あることが出てきてしまうのでモチベーションが移ってしまいます。<br /> でも、<strong>1週間なら全力で頑張れるちょうどよい長さ</strong>かな~と考えました。</p> <p>(それと、1週間ならクオリティ低くても言い訳になるかな と)</p> <h2 id="3⃣ 1週間という短い期間で終わらせるために意識したこと"><a href="#3%E2%83%A3+%EF%BC%91%E9%80%B1%E9%96%93%E3%81%A8%E3%81%84%E3%81%86%E7%9F%AD%E3%81%84%E6%9C%9F%E9%96%93%E3%81%A7%E7%B5%82%E3%82%8F%E3%82%89%E3%81%9B%E3%82%8B%E3%81%9F%E3%82%81%E3%81%AB%E6%84%8F%E8%AD%98%E3%81%97%E3%81%9F%E3%81%93%E3%81%A8">3⃣ 1週間という短い期間で終わらせるために意識したこと</a></h2> <h3 id="(1) 1週間のカウントダウンタイマーを設置する"><a href="#%281%29+%EF%BC%91%E9%80%B1%E9%96%93%E3%81%AE%E3%82%AB%E3%82%A6%E3%83%B3%E3%83%88%E3%83%80%E3%82%A6%E3%83%B3%E3%82%BF%E3%82%A4%E3%83%9E%E3%83%BC%E3%82%92%E8%A8%AD%E7%BD%AE%E3%81%99%E3%82%8B">(1) 1週間のカウントダウンタイマーを設置する</a></h3> <p>いつもズルズル伸ばしてしまう癖があるので <strong>Twitter で公開日を宣言</strong>しました。<br /> そして公開日までの<strong>カウントダウンタイマーを常に目に入る位置に設置</strong>しておきました。</p> <blockquote class="twitter-tweet"><p lang="ja" dir="ltr">次に出す Web アプリのリリース日を 6/17 12:00 頃に決めました🎉メリハリ付けるために中途半端でもこのタイミングで出します写真はちょうどいいカウントダウンタイマーが無かったので作ってみました▪コードはこちら<a target="_blank" rel="nofollow noopener" href="https://t.co/HiGjiROP4R">https://t.co/HiGjiROP4R</a>(8,9行目を編集すれば自分用に使えるよ!) <a target="_blank" rel="nofollow noopener" href="https://t.co/HnjXxktDLb">pic.twitter.com/HnjXxktDLb</a></p>— hikaru🐧#100DaysOfCode! (@hikaru_firecamp) <a target="_blank" rel="nofollow noopener" href="https://twitter.com/hikaru_firecamp/status/1269807592533979138?ref_src=twsrc%5Etfw">June 8, 2020</a></blockquote> <p>このカウントダウンタイマー、<strong>すっごく効果があった</strong>ように思います。度々目に入るので<strong>程よい緊張感と無機質な圧力</strong>をくれます。<br /> さらにシレっと<strong>周囲の人たちに公開日を宣言</strong>できるのでかなりお勧めです。</p> <blockquote> <p>⚠ 補足 ⚠<br /> さも予定を守ったように言ってますが結局半日ほど遅く公開してしまいました<br /> ほ ん と す み ま せ ん で し た 😗</p> </blockquote> <h3 id="(2) 適当な企画書を作って全体を把握しならが作業する"><a href="#%282%29+%E9%81%A9%E5%BD%93%E3%81%AA%E4%BC%81%E7%94%BB%E6%9B%B8%E3%82%92%E4%BD%9C%E3%81%A3%E3%81%A6%E5%85%A8%E4%BD%93%E3%82%92%E6%8A%8A%E6%8F%A1%E3%81%97%E3%81%AA%E3%82%89%E3%81%8C%E4%BD%9C%E6%A5%AD%E3%81%99%E3%82%8B">(2) 適当な企画書を作って全体を把握しならが作業する</a></h3> <p>企画書などが無い状態でコーディングすると目に付いた場所から手を付けてしまいがちで<strong>視野が局所的になりやすく作業の優先度決めが難しく</strong>なります。</p> <p>簡単なものでも<strong>全体像を把握できるもの</strong>があると、次にどの部分を実装するか考えるときに『最低でもここを実装しないとだから優先度高めで』みたいな<strong>スケジュール管理が容易</strong>になります。</p> <p>今回は以下の様な簡易仕様書をあらかじめ書いておりました。</p> <blockquote class="twitter-tweet"><p lang="ja" dir="ltr">これから作るミニゲームの簡易仕様書!よくあるクリックゲームですが、ちゃんと作りきれるか不安😇1週間がんばります〜(あ、奇しくも一人 web1week みたいになってる)全然関係ないけど最近 iPad でお絵描きの練習始めました!スマブラやったことあればピンとくるはず😎 <a target="_blank" rel="nofollow noopener" href="https://t.co/oVo5xAUeBe">pic.twitter.com/oVo5xAUeBe</a></p>— hikaru🐧#100DaysOfCode! (@hikaru_firecamp) <a target="_blank" rel="nofollow noopener" href="https://twitter.com/hikaru_firecamp/status/1270280818116583425?ref_src=twsrc%5Etfw">June 9, 2020</a></blockquote> <p>これのおかげで実装段階での仕様変更が減り、<strong>一直線にゴールに向かって実装できるので結構大切</strong>なものだと思います。</p> <h3 id="(3) 新しいことは1個までに制限して知っているものを使う"><a href="#%283%29+%E6%96%B0%E3%81%97%E3%81%84%E3%81%93%E3%81%A8%E3%81%AF%EF%BC%91%E5%80%8B%E3%81%BE%E3%81%A7%E3%81%AB%E5%88%B6%E9%99%90%E3%81%97%E3%81%A6%E7%9F%A5%E3%81%A3%E3%81%A6%E3%81%84%E3%82%8B%E3%82%82%E3%81%AE%E3%82%92%E4%BD%BF%E3%81%86">(3) 新しいことは1個までに制限して知っているものを使う</a></h3> <p>初めて使う道具が多いと勉強することが増えたり些細なことにはまったりして時間を消費しがちなので、<strong>使い慣れた開発環境</strong>と<strong>使った事のあるライブラリ</strong>を中心に選定しました。</p> <h4 id="使い慣れたもの"><a href="#%E4%BD%BF%E3%81%84%E6%85%A3%E3%82%8C%E3%81%9F%E3%82%82%E3%81%AE">使い慣れたもの</a></h4> <ul> <li><strong>VS-Code</strong> ... 開発環境</li> <li><strong>TypeScript</strong> ... 開発言語</li> <li><strong>UIKit</strong> ... UI コンポーネント</li> </ul> <h4 id="以前から実験レベルで遊んでいたもの"><a href="#%E4%BB%A5%E5%89%8D%E3%81%8B%E3%82%89%E5%AE%9F%E9%A8%93%E3%83%AC%E3%83%99%E3%83%AB%E3%81%A7%E9%81%8A%E3%82%93%E3%81%A7%E3%81%84%E3%81%9F%E3%82%82%E3%81%AE">以前から実験レベルで遊んでいたもの</a></h4> <ul> <li><strong>Three.js</strong> ... 3D 描画エンジン</li> <li><strong>Cannon.js</strong> ... 3D 物理エンジン</li> </ul> <h4 id="初めて使うもの"><a href="#%E5%88%9D%E3%82%81%E3%81%A6%E4%BD%BF%E3%81%86%E3%82%82%E3%81%AE">初めて使うもの</a></h4> <ul> <li><strong>Audio API</strong> ... BGM や効果音の再生</li> </ul> <p>今回は遊べる Web ゲームを<strong>短期間で作ることが目的</strong>だったので冒険をしない制約を設けましたが、完成度が低くても<strong>未知なる技術を沢山学びたいならどんどん新しい技術を使ってみたらよい</strong>と思います。</p> <p><strong>目的次第でやり方を臨機応変にする</strong>ことが大事なのかなと😎</p> <h3 id="(4) こだわりたい部分を絞り他を捨てる覚悟を持つ"><a href="#%284%29+%E3%81%93%E3%81%A0%E3%82%8F%E3%82%8A%E3%81%9F%E3%81%84%E9%83%A8%E5%88%86%E3%82%92%E7%B5%9E%E3%82%8A%E4%BB%96%E3%82%92%E6%8D%A8%E3%81%A6%E3%82%8B%E8%A6%9A%E6%82%9F%E3%82%92%E6%8C%81%E3%81%A4">(4) こだわりたい部分を絞り他を捨てる覚悟を持つ</a></h3> <p>こだわりが強いと味のあるイイものができるけど、その代わり<strong>完成が遅くなる</strong>傾向にあると思います。</p> <p>今回は『ローポリキャラ達が<strong>物理演算で予想外の挙動</strong>をする』部分だけこだわりましたが、操作 UI の見た目などはブラウザ標準のプログレスバーを使っていたりと<strong>大部分はかなり適当</strong>です。</p> <p>個人的にはゲームの中に<strong>ブラウザの DOM を混ぜることに違和感</strong>とアレルギーを感じるのですが『<strong>こだわる部分を絞って他は適当</strong>』と大胆に割り切っちゃいました。<br /> <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67192/ef433b3e-8d54-62b9-94b9-83f98f528ade.png" alt="image.png" /></p> <p>他にも、ペンギン/サメ/コイン/木 の当たり判定はすべて立方体(正六面体)で手抜きをしましたが、これは予想に反して『<strong>サメさんやコインが地面に刺さってる</strong>』ように見えたり『木々があらぶってる』感じになったり<strong>ヘンテコな世界観の演出</strong>に一役買ったように思います。<br /> <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67192/a1a1764b-8e99-17a7-de03-a410ae195d96.png" alt="2020-06-18_00h32_18.png" /></p> <h2 id="4⃣ おまけ: 実装 Tips"><a href="#4%E2%83%A3+%E3%81%8A%E3%81%BE%E3%81%91%3A+%E5%AE%9F%E8%A3%85+Tips">4⃣ おまけ: 実装 Tips</a></h2> <p>唐突なおまけの実装 Tips です。</p> <h3 id="(1) Cannon.js / サメさんの Z 位置(奥行)を固定"><a href="#%281%29+Cannon.js+%2F+%E3%82%B5%E3%83%A1%E3%81%95%E3%82%93%E3%81%AE+Z+%E4%BD%8D%E7%BD%AE%28%E5%A5%A5%E8%A1%8C%29%E3%82%92%E5%9B%BA%E5%AE%9A">(1) Cannon.js / サメさんの Z 位置(奥行)を固定</a></h3> <p>Cannon.js は 3 次元物理演算なので XYZ 軸分の動きがあるのですが、サメさん(敵)に関しては奥行方向に動かれると攻撃を当てられなくなるので Z 軸を固定しています。</p> <pre><code class="ts">// CannonJs: サメさんの物理演算用の剛体を生成 const sharkBody = new CANNON.Body({ <省略> }); // ThreeJs: アニメーションループを開始 renderer.setAnimationLoop(() => { // CannonJs: 1フレーム分の物理演算を実行 world.step(<省略>); // ★★ サメさんのZ位置を固定する ★★ sharkBody.position.set( sharkBody.position.x, sharkBody.position.y, 0); // Z軸を0に変更 }); </code></pre> <h3 id="(2) Cannon.js / 摩擦設定・反発設定"><a href="#%282%29+Cannon.js+%2F+%E6%91%A9%E6%93%A6%E8%A8%AD%E5%AE%9A%E3%83%BB%E5%8F%8D%E7%99%BA%E8%A8%AD%E5%AE%9A">(2) Cannon.js / 摩擦設定・反発設定</a></h3> <p>初期値だとほとんど滑ることは無く、滑らせた方が面白そうだったので摩擦と反発の設定をいじっています。</p> <p>モデルごとに摩擦係数や反発係数を設定できれば直感的だったのですが、Cannon.js では『モデル1とモデル2に対しての摩擦・反発を設定』といった具合に設定が必要でした。</p> <pre><code class="ts">// // 物理演算ワールドを初期化 // const world = new CANNON.World(); world.gravity.set(0, -9.82, 0); // m/s² const floorBodyMaterial = new CANNON.Material(`FloorModel`); const penguinBodyMaterial = new CANNON.Material(`PenguinModel`); const sharkBodyMaterial = new CANNON.Material(`SharkModel`); // 摩擦反発設定: 床とペンギン world.addContactMaterial(new CANNON.ContactMaterial( floorBodyMaterial, penguinBodyMaterial, { friction: 0.01, // 摩擦設定 (ペンギンが床を滑るように) restitution: 0.8, // 反発設定 } )); // 摩擦反発設定: 床とサメ world.addContactMaterial(new CANNON.ContactMaterial( floorBodyMaterial, sharkBodyMaterial, { friction: 0.05, // 摩擦設定 restitution: 0.3, // 反発設定 contactEquationStiffness: 1e8, contactEquationRelaxation: 3, } )); // 摩擦反発設定: ペンギンとサメ world.addContactMaterial(new CANNON.ContactMaterial( penguinBodyMaterial, sharkBodyMaterial, { friction: 0.01, // 摩擦設定 restitution: 2.0, // 反発設定 (攻撃を受けたサメが吹っ飛びやすいように) } )); </code></pre> <h3 id="(3) Three.js / 同じモデルは使いまわして効率化"><a href="#%283%29+Three.js+%2F+%E5%90%8C%E3%81%98%E3%83%A2%E3%83%87%E3%83%AB%E3%81%AF%E4%BD%BF%E3%81%84%E3%81%BE%E3%82%8F%E3%81%97%E3%81%A6%E5%8A%B9%E7%8E%87%E5%8C%96">(3) Three.js / 同じモデルは使いまわして効率化</a></h3> <p>今回ゲーム内では<strong>同じモデルが大量に出現</strong>します。その際に<strong>毎回モデルをロードしていると実行効率が悪い</strong>ので、モデル管理クラスが必要になります。</p> <p>こういった目的の管理クラス系にはシングルトン実装が最適かと思いますが、TypeScript では <strong><code>module</code> を使うと簡単にシングルトン実装が可能</strong>です。</p> <pre><code class="ts">/** モデル名の型定義 */ export type ModelName = `PENGUIN` | `SHARK` | `TREE` | `COIN`; /** * ゲーム内のモデルを管理するモジュール *  シングルトン実装 *  モデルをあらかじめロードして、ロード済みのモデルを使いまわすことで効率化 */ export module ModelManager { /** ロード済みのモデルを保持 */ const modelMap = new Map<ModelName, THREE.Object3D>(); /** 各種モデルをロードする */ export async function load() { await loadModel(`PENGUIN`, `models/PenguinJumping.glb`); await loadModel(`SHARK`, `models/Shark.glb`); await loadModel(`TREE`, `models/Tree.glb`); await loadModel(`COIN`, `models/Coin.glb`); } /** モデルを複製して取得する */ export function getModel(modelName: ModelName) { return modelMap.get(modelName)!.clone(); } /** ロード済みモデルを開放する */ export function dispose() { // ThreeJs: ロードしたモデルをすべて解放 modelMap.forEach((obj3D, key) => { GameUtils.disposeObject3D(obj3D); }); } async function loadModel(modelName: ModelName, path: string) { // ThreeJs: モデル読み込み const obj3D = await GameUtils.loadGltfModel(path); // リストに追加 modelMap.set(modelName, obj3D); } } </code></pre> <p>使い方</p> <pre><code class="ts">// あらかじめすべてのモデルをロードする await ModelManager.load(); // ペンギンモデルを取得する (内部的にはロード済みのモデルを複製しているので効率的) const penguin1 = ModelManager.getModel(`PENGUIN`); const penguin2 = ModelManager.getModel(`PENGUIN`); const penguin3 = ModelManager.getModel(`PENGUIN`); </code></pre> <h2 id="最後に"><a href="#%E6%9C%80%E5%BE%8C%E3%81%AB">最後に</a></h2> <p>ゲーム開発中に Twitter でいいねやコメントなどでリアクションをくれた方、開発中のゲームを試してヒントをくれた友人方、本当にありがとうございました。<br /> 大変励みになりモチベーションになりました、重ねてお礼申し上げます😆</p> <p>あと、ここまで読んでまだプレイしていない人!!<br /> ↓ やってからリアクションをクレクレ厨😗</p> <blockquote> <p>ペンギンがサメさんを倒すゲーム<br /> 敵さんを吹っ飛ばして 1,000 G 以上を目指してみてね😆<br /> <a target="_blank" rel="nofollow noopener" href="https://games.westa.io/">https://games.westa.io/</a></p> </blockquote> <hr /> <p>P.S. crieit.net で初カノニカル投稿してみました🎉これ書く側としてはイイ仕組みですね</p> hikaru🐧 tag:crieit.net,2005:PublicArticle/15652 2019-12-29T11:22:37+09:00 2019-12-29T11:22:37+09:00 https://crieit.net/posts/Node-js-Twitter Node.jsで画像/動画つきツイートをTwitterに投稿すると大変だった... <p>JavaScriotでツイートしたいなと思って、いろいろ試していたら、<br /> 30秒以上動画つきツイートが結構めんどくさかったので、その時の備忘録。</p> <h3 id="Node.jsでTwitter APIを使う"><a href="#Node.js%E3%81%A7Twitter+API%E3%82%92%E4%BD%BF%E3%81%86">Node.jsでTwitter APIを使う</a></h3> <p>Node.jsでTwitter APIを使うときは、<a target="_blank" rel="nofollow noopener" href="https://github.com/desmondmorris/node-twitter">desmondmorris/node-twitter</a>を使うのが良さそう</p> <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 twitter </code></pre> <h4 id="ツイートしてみる"><a href="#%E3%83%84%E3%82%A4%E3%83%BC%E3%83%88%E3%81%97%E3%81%A6%E3%81%BF%E3%82%8B">ツイートしてみる</a></h4> <p>文字だけをツイートするのは、こんな感じ。</p> <pre><code class="typescript">import Twitter from "twitter"; // 初期化 const client = new Twitter({ consumer_key: TWITTER_CONSUMER_KEY, consumer_secret: TWITTER_CONSUMER_SECLET, access_token_key: ACCESS_TOKEN_KEY, access_token_secret: ACCESS_TOKEN_SECRET }); // 文字だけをツイート async function tweet(text: string) { const tweet = await client.post("statuses/update", { status: text }); } tweet("ツイート").then(); </code></pre> <p>Twitterクラスに<code>.post()</code>や、<code>.get()</code>が用意されているので、<br /> <a target="_blank" rel="nofollow noopener" href="https://developer.twitter.com/en/docs">Twitter APIのドキュメント</a>を見ながら、呼び出していく感じ。</p> <p>ツイートするのは<a target="_blank" rel="nofollow noopener" href="https://developer.twitter.com/en/docs/tweets/post-and-engage/api-reference/post-statuses-update">POST statuses/update</a>なのでドキュメントを参照。</p> <h3 id="画像つきでツイートしてみる"><a href="#%E7%94%BB%E5%83%8F%E3%81%A4%E3%81%8D%E3%81%A7%E3%83%84%E3%82%A4%E3%83%BC%E3%83%88%E3%81%97%E3%81%A6%E3%81%BF%E3%82%8B">画像つきでツイートしてみる</a></h3> <p>画像とか動画とかメディアつきだとちょっとめんどくさく...</p> <p>ツイートと一緒に画像をアップロードできないので、</p> <ol> <li>最初に画像をアップロードしてからmediaIdを取得し、</li> <li>mediaIdと一緒に<code>statuses/update</code>でツイート</li> </ol> <p>という段階的な感じになる。</p> <pre><code class="typescript">import Twitter from "twitter"; const client = // 略 async function tweetWithImage(text: string, filePath: string) { const data = require('fs').readFileSync(filePath); // 画像をアップロード const media = await client.post('media/upload', {media: data}); // mediaIdをパラメタに追加して、ツイート const params = { status: text, media_ids: media.media_id_string }; const tweet = await client.post("statuses/update", params); } tweetWithImage("ツイート", "./imange.jpg").then(); </code></pre> <p>複数の画像をつけたい場合は、それぞれアップロードして、<br /> <code>media_ids</code>にカンマ区切りでmediaIdを指定する。</p> <p>ただ、この<code>media/upload</code>を1度だけ呼び出すシンプルな方法には制限があり、<br /> GIFや動画はアップロードできない...</p> <h3 id="30秒以下の動画付きツイートをしてみる"><a href="#30%E7%A7%92%E4%BB%A5%E4%B8%8B%E3%81%AE%E5%8B%95%E7%94%BB%E4%BB%98%E3%81%8D%E3%83%84%E3%82%A4%E3%83%BC%E3%83%88%E3%82%92%E3%81%97%E3%81%A6%E3%81%BF%E3%82%8B">30秒以下の動画付きツイートをしてみる</a></h3> <p>動画やGIFをアップロードしたい場合は、<a target="_blank" rel="nofollow noopener" href="https://developer.twitter.com/en/docs/media/upload-media/uploading-media/chunked-media-upload">Chunked media upload</a>という形でアップロードする必要がある。</p> <p>この方法は、大きく3ステップに分かれている</p> <ol> <li>初期化: command=INIT</li> <li>アップロード: command=APPEND</li> <li>完了: commaind=FINALIZE</li> </ol> <pre><code class="typescript">import Twitter from "twitter"; const client = // 略 async function tweetWithChunkedMedia(text: string, filePath: string) { const mediaType = 'video/mp4'; const mediaData = require('fs').readFileSync(filePath); const mediaSize = require('fs').statSync(filePath).size; // 動画をアップロード: INIT const media = await client.post('media/upload', { command : 'INIT', total_bytes: mediaSize, media_type : mediaType }); // INITでmediaIdが発行されるので、取得しておく const mediaId = media.media_id_string; // 動画をアップロード: UPLOAD await client.post('media/upload', { command : 'APPEND', media_id : mediaId, media : mediaData, segment_index: 0 }); // 動画をアップロード: FINALIZE await client.post('media/upload', { command : 'FINALIZE', media_id: mediaId }); // mediaIdをパラメタに追加して、ツイート const params = { status: text, media_ids: mediaId }; const tweet = await client.post("statuses/update", params); } tweetWithChunkedMedia("ツイート", "./video.mp4").then(); </code></pre> <p>動画やGIFのような大きいサイズのメディアは、分割してアップロードできるこの仕組みを使うっぽい。</p> <p>ただ、30秒以上の動画や1MB(チャンクサイズ上限)を超える場合は、<br /> INIT時に<code>media_category</code>を指定して、非同期アップロードをしないといけない。</p> <h3 id="30秒を超える動画付きツイートをしてみる"><a href="#30%E7%A7%92%E3%82%92%E8%B6%85%E3%81%88%E3%82%8B%E5%8B%95%E7%94%BB%E4%BB%98%E3%81%8D%E3%83%84%E3%82%A4%E3%83%BC%E3%83%88%E3%82%92%E3%81%97%E3%81%A6%E3%81%BF%E3%82%8B">30秒を超える動画付きツイートをしてみる</a></h3> <p>30秒を超える動画は、media_categoryをつけ、非同期アップロードで対応しないといけない。</p> <p>media_categoryは、<code>tweet_image</code>, <code>tweet_gif</code>, <code>tweet_video</code>を指定できるので、<br /> アップロードするメディアに合わせて指定する。</p> <p>また、チャンクサイズの上限が1MBなので、APPENDでデータをPOSTする際には注意。<br /> 1MB以上の場合は、1MB以下になるように分割し、segment_indexでindexを指定する。</p> <pre><code class="typescript">import Twitter from "twitter"; const client = // 略 async function tweetWithChunkedMedia(text: string, filePath: string) { const mediaType = 'video/mp4'; const mediaData = require('fs').readFileSync(filePath); const mediaSize = require('fs').statSync(filePath).size; // 動画をアップロード: INIT const media = await client.post('media/upload', { command : 'INIT', total_bytes: mediaSize, media_type : mediaType, media_category: "tweet_video" // media_categoryを指定 }); const mediaId = media.media_id_string; // 動画をアップロード: UPLOAD await client.post('media/upload', { command : 'APPEND', media_id : mediaId, media : mediaData, segment_index: 0 }); // 動画をアップロード: FINALIZE await client.post('media/upload', { command : 'FINALIZE', media_id: mediaId }); // 動画をアップロード: STATUS while(true) { // アップロードのステータスをポーリング const status = await client.get('media/upload', { command : 'STATUS', media_id: mediaId }); if (status.processing_info.state == "succeeded") { // 完了したら、ポーリングを終了 break; } else if (status.processing_info.state == "failed") { // エラーになったら、例外を投げる throw new Error(status.processing_info.error.message); } else { // 処理中(in_progress)の場合は、指定された秒数分待つ await sleep(status.processing_info.check_after_secs + 1); } } // mediaIdをパラメタに追加して、ツイート const params = { status: text, media_ids: mediaId }; const tweet = await client.post("statuses/update", params); } function sleep(time: number) { return new Promise((resolve, reject) => { setTimeout(() => resolve(), time * 1000); }); } tweetWithChunkedMedia("ツイート", "./video.mp4").then(); </code></pre> <p>かなりハマったのが以下の2点。</p> <ol> <li>STATUSはFINALIZEしてからじゃないと、404が返ってくる</li> <li>FINALIZEのレスポンスにもprocessing_infoがあるが、<br /> STATUSをしないと永遠にpending状態。(STATUSを呼ぶと処理が始まる)</li> </ol> <p>このあたり、ドキュメントに詳しい説明がなくて、かなりハマった...</p> <h3 id="axiosを使って外部URLのメディアをツイートする"><a href="#axios%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6%E5%A4%96%E9%83%A8URL%E3%81%AE%E3%83%A1%E3%83%87%E3%82%A3%E3%82%A2%E3%82%92%E3%83%84%E3%82%A4%E3%83%BC%E3%83%88%E3%81%99%E3%82%8B">axiosを使って外部URLのメディアをツイートする</a></h3> <p>Cloud Storageにある画像/動画を含めてツイートしたかったので、<br /> axiosを使って外部URLを取得する処理を加えてみたのがこれ。</p> <p><code>new TwitterApi().postTweet("ツイート", ["https://..."]);</code><br /> みたいに呼び出すと、ダウンロード/アップロード/ツイートできる。(はず...)</p> <pre><code class="typescript">import Twitter from "twitter"; import axios from "axios"; /** * スリープ処理 * @param time スリープする秒数 */ function sleep(time: number) { return new Promise((resolve, reject) => { setTimeout(() => resolve(), time * 1000); }); } export default class TwitterApi { private client: Twitter; constructor() { this.client = new Twitter({ consumer_key: // TWITTER_CONSUMER_KEY, consumer_secret: // TWITTER_CONSUMER_SECLET, access_token_key: // ACCESS_TOKEN_KEY, access_token_secret: // ACCESS_TOKEN_SECRET }); } /** * ツイートするメイン処理 * @param text ツイート文 * @param medias 添付する外部URLのリスト */ public async postTweet(text: string, medias: string[] = []) { let mediaIds: string[] = []; if (medias.length > 0) { // メディアファイルがあれば、アップロードしてmediaIdを取得 mediaIds = await Promise.all( medias.map(async v => await this.uploadMedia(v.url)) ); } const res = await this.tweet(text, mediaIds); } /** * メディアのアップロード処理 * @param url メディアのURL */ private async uploadMedia(url: string) { // axiosを使って、メディアのデータを取得 const res = await axios.create({ responseType: "arraybuffer" }).get(url); const mediaData: ArrayBuffer = res.data; const mediaSize = res.headers["content-length"]; const mediaType = res.headers["content-type"]; // INIT: mp4かgifなら、media_categoryを指定する const initParams = { command: "INIT", total_bytes: mediaSize, media_type: mediaType }; if (mediaType == "video/mp4") { initParams["media_category"] = "tweet_video"; } else if (mediaType == "image/gif") { initParams["media_category"] = "tweet_gif"; } const data = await this.client.post("media/upload", initParams); const mediaId = data.media_id_string; // APPEND: 500Bくらいにチャンクを分けてアップロードする const chunkSize = 500000; const chunkNum = Math.ceil(mediaSize / chunkSize); for (let index = 0; index < chunkNum; index++) { const chunk = mediaData.slice(chunkSize * index, chunkSize * (index + 1)); const resAppend = await this.client.post("media/upload", { command: "APPEND", media_id: mediaId, media: mediaData.slice(chunkSize * index, chunkSize * (index + 1)), segment_index: index }); } // FINALIZE const resFinalize = await this.client.post("media/upload", { command: "FINALIZE", media_id: mediaId }); if (!resFinalize.processing_info) { // media_categoryをしていないと、processing_infoがない return mediaId; } else if (resFinalize.processing_info.state == "succeeded") { return mediaId; } else if (resFinalize.processing_info.state == "failed") { throw new Error(resFinalize.processing_info.error.message); } // STATUS while (true) { const resStatus = await this.client.get("media/upload", { command: "STATUS", media_id: mediaId }); if (resStatus.processing_info.state == "succeeded") { return mediaId; } else if (resStatus.processing_info.state == "failed") { throw new Error(resStatus.processing_info.error.message); } else { await sleep(resStatus.processing_info.check_after_secs + 1); } } } /** * ツイート処理 * @param text ツイート文 * @param mediaIds メディアIDのリスト */ private async tweet(text: string, mediaIds: string[] = []) { const params = { status: text }; if (mediaIds.length > 0) params["media_ids"] = mediaIds.join(","); const tweet = await this.client.post("statuses/update", params); return tweet; } } </code></pre> <p>若干ハマったのが、以下の2点</p> <ol> <li>axiosで取得する場合は、<code>{ responseType: "arraybuffer" }</code>でcreateしないといけない</li> <li>cloudStrageでデータを取得できないので、downloadURLを取得しておかないといけない</li> </ol> <p>Twitter APIむずい...</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://qiita.com/mpyw/items/7bedf8c23de286cef0f9">TwitterAPIのアップロード系エンドポイントまとめ (140秒動画対応) - Qiita</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://developer.twitter.com/en/docs/tutorials/uploading-media">Uploading media — Twitter Developers</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://developer.twitter.com/en/docs/media/upload-media/uploading-media/media-best-practices">Media best practices — Twitter Developers</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://developer.twitter.com/en/docs/media/upload-media/uploading-media/chunked-media-upload">Chunked media upload — Twitter Developers</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://stackoverflow.com/questions/12740659/downloading-images-with-node-js">Downloading images with node.js - Stack Overflow</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://developer.twitter.com/en/docs/media/upload-media/api-reference/get-media-upload-status">GET media/upload (STATUS) — Twitter Developers</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/ksh-fthr/items/ba7c80252edad0e7c66c">[axios] 画像データのレスポンスを取得する際にハマった話 - Qiita</a></li> </ul> きらぷか@積読ハウマッチ/SSSAPIなど tag:crieit.net,2005:PublicArticle/15649 2019-12-29T11:18:43+09:00 2019-12-29T11:18:43+09:00 https://crieit.net/posts/Twitter-URL Twitterでツイートできる文字数を正確に数える(絵文字もURLも) <p>Nuxt.jsでツイートするアプリを作りたいなと思い、<br /> 文字数ってどうやって計算するんだろ?って思ったら、<br /> 公式でライブラリ(<a target="_blank" rel="nofollow noopener" href="https://github.com/twitter/twitter-text">twitter-text</a>)が用意されているらしいので、使ってみたときの備忘録</p> <p>Java版/Ruby版/JavaScript版/Objective-C版などいろいろあるらしい。</p> <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 twitter-text </code></pre> <h4 id="使い方"><a href="#%E4%BD%BF%E3%81%84%E6%96%B9">使い方</a></h4> <pre><code class="javascript">const twitter = require('twitter-text'); // ツイートするテキスト const tweetText = "This is a test tweet"; // twitter-textで計算 const result = twitter.parseTweet(tweet); console.log(result) /* Returns: { weightedLength: 20, permillage: 71, valid: true, displayRangeEnd: 19, displayRangeStart: 0, validRangeEnd: 19, validRangeStart: 0 } */ // 日本語版の場合、文字数を2で割るとツイッターと同じになる。 const textLength = result.weightedLength / 2; </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">参考にしたサイト</a></h1> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/PND/items/17e87b8839c9099d2e70#twitter-text-%E3%83%A9%E3%82%A4%E3%83%96%E3%83%A9%E3%83%AA">ツイートの文字数を <strong>厳密に</strong> 数える方法 - Qiita</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://github.com/twitter/twitter-text/tree/master/js">twitter-text/js at master · twitter/twitter-text</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://developer.twitter.com/en/docs/developer-utilities/twitter-text">twitter-text Parser — Twitter Developers</a></li> </ul> きらぷか@積読ハウマッチ/SSSAPIなど tag:crieit.net,2005:PublicArticle/15555 2019-11-19T15:58:44+09:00 2019-11-19T15:58:44+09:00 https://crieit.net/posts/TypeScript-Enum-values TypeScriptのEnumでvaluesみたいなリストを取得する <p>TypeScriptのEnumは便利で好きだけど、valuesみたいなenumのリストを取得できない...<br /> でも、<code>Object.entries()</code>を使えばいいらしい。<br /> 毎回検索するので、自分用の備忘用。</p> <pre><code class="typescript">enum FOO_TYPE { TYPE_FOO = "foo", TYPE_BAR = "bar" } const fooTypes: FOO_TYPE[] = Object.entries(FOO_TYPE).map(([_, value]) => value); console.log(fooTypes); // => [ 'foo', 'bar' ] </code></pre> <p>便利(<em>´ω`</em>)</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/wildmouse/items/96ec2a5892e88ea4aec6">TypeScriptのEnumのループはObject.entries()で実現可能 - Qiita</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> きらぷか@積読ハウマッチ/SSSAPIなど tag:crieit.net,2005:PublicArticle/15414 2019-09-22T12:01:25+09:00 2019-09-22T12:01:25+09:00 https://crieit.net/posts/TypeScript-Nuxt-js-vue-chartjs-chart-js いい感じのグラフをTypeScriptなNuxt.jsとvue-chartjs(chart.js)で書いてみた <p>Nuxt.jsで開発している<a target="_blank" rel="nofollow noopener" href="https://tsundoku.site">Webサービス</a>で、<br /> 棒グラフとか線グラフを使いたいなと思い、いろいろ調べた備忘録。</p> <p><a target="_blank" rel="nofollow noopener" href="https://vue-chartjs.org/">vue-chartjs</a>を使うと、いい感じのグラフが簡単にできた(<em>´ω`</em>)</p> <h2 id="こんな感じで使ってます!!"><a href="#%E3%81%93%E3%82%93%E3%81%AA%E6%84%9F%E3%81%98%E3%81%A7%E4%BD%BF%E3%81%A3%E3%81%A6%E3%81%BE%E3%81%99%21%21">こんな感じで使ってます!!</a></h2> <p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/061cfd7c-d18e-bbc5-37bc-77e191d3f034.png" width="720" /></p> <p>登録されている本の総額と総冊数の推移を棒グラフと線グラフで表示!!<br /> 色も文字も変えられて、いい感じに(<em>´ω`</em>)</p> <hr /> <h2 id="vue-chartjsの使い方"><a href="#vue-chartjs%E3%81%AE%E4%BD%BF%E3%81%84%E6%96%B9">vue-chartjsの使い方</a></h2> <p>全体の流れはこんな感じ。<br /> 1. インストール<br /> 2. コンポーネントを作る<br /> 3. コンポーネントを配置する</p> <h3 id="1. インストール"><a href="#1.+%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB">1. インストール</a></h3> <p>まずはインストール</p> <pre><code class="console">$ npm install vue-chartjs chart.js --save </code></pre> <h3 id="2. コンポーネントを作る"><a href="#2.+%E3%82%B3%E3%83%B3%E3%83%9D%E3%83%BC%E3%83%8D%E3%83%B3%E3%83%88%E3%82%92%E4%BD%9C%E3%82%8B">2. コンポーネントを作る</a></h3> <pre><code class="html"><!-- Template Tag can not be merged... --> <script lang="ts"> import { Component, Vue, Prop, Watch, mixins } from "nuxt-property-decorator"; import Chart from "chart.js"; import VueChart from "vue-chartjs"; const Line = VueChart.Line; const reactiveProp = VueChart.mixins.reactiveProp; @Component export default class ChartLine extends mixins(Line, reactiveProp) { @Prop({ default: {} }) chartData: Chart.ChartData; @Prop({ default: {} }) options: Chart.ChartOptions; mounted() { this.renderChart(this.chartData, this.options); } } </script> </code></pre> <p><strong>注意点としては、以下の3つ</strong></p> <ol> <li><code>vue-chartjs</code>が生成するので、<code><template></code>タグをつけない</li> <li><code>mixins</code>が重複するので、<code>import Chart from "chart.js";</code></li> <li>データのProp名はreactivePropで決まってるので<code>chartData</code>にする<br /> 参考: <a target="_blank" rel="nofollow noopener" href="https://github.com/apertureless/vue-chartjs/blob/18f11350a2e505014c2c4d1a9b68a92bfe8cd394/src/mixins/index.js#L89-L94">src/mixins/index.js | Github</a></li> </ol> <p>この例は折れ線グラフのため、<code>Line</code>をextendsしてるけど、<br /> ほかのチャートは、それぞれコンポーネントを用意すればOK</p> <p>棒グラフの場合はこんな感じ。</p> <pre><code class="html"><!-- Template Tag can not be merged... --> <script lang="ts"> import { Component, Vue, Prop, Watch, mixins } from "nuxt-property-decorator"; import Chart from "chart.js"; import VueChart from "vue-chartjs"; // 棒グラフの場合は、Barを使う const Bar = VueChart.Bar; const reactiveProp = VueChart.mixins.reactiveProp; @Component // mixinsもBarに変更 export default class ChartLine extends mixins(Bar, reactiveProp) { @Prop({ default: {} }) chartData: Chart.ChartData; @Prop({ default: {} }) options: Chart.ChartOptions; mounted() { this.renderChart(this.chartData, this.options); } } </script> </code></pre> <h3 id="3. コンポーネントを配置する"><a href="#3.+%E3%82%B3%E3%83%B3%E3%83%9D%E3%83%BC%E3%83%8D%E3%83%B3%E3%83%88%E3%82%92%E9%85%8D%E7%BD%AE%E3%81%99%E3%82%8B">3. コンポーネントを配置する</a></h3> <p>2.で作成したコンポーネントChartLineとChartBarを配置する。<br /> チャートのデータ(chartData)やオプション、スタイルをpropで設定。</p> <pre><code class="html"><template> <div class="chart-container"> <ChartLine :chartData="chartData" :options="chartOption" :styles="chartStyles" /> </div> </template> <script lang="ts"> import { Component, Vue, Prop } from "nuxt-property-decorator"; import { ChartData, ChartOptions } from "chart.js"; import ChartLine from "~/components/ChartLine.vue"; import ChartBar from "~/components/ChartBar.vue"; @Component({ components: { ChartLine, ChartBar } }) export default class ChartPage extends Vue { // チャートのデータ private chartData: ChartData = { // 横軸のラベル labels: ["A", "B", "C", "D", "E"], // データのリスト datasets: [ { label: "Data One", // データのラベル data: [1, 5, 3, 4, 3] // データの値。labelsと同じサイズ }, { label: "Data Two", data: [10, 50, 30, 40, 30] } ] }; // チャートのオプション private chartOption: ChartOptions = { // アスペクト比を固定しないように変更 maintainAspectRatio: false }; // チャートのスタイル: <canvas>のstyle属性として設定 private chartStyles = { height: "100%", width: "100%" }; } </script> <style lang="scss"> .chart-container { /** * vue-chartjsで生成する<canvas>がabsoluteのため、 * relateveを設定 */ position: relative; /** * chartStylesを設定しているので、 * height/wightが有効になる */ height: 40vh; width: 80vw; margin: 0 auto; } </style> </code></pre> <p>こんな感じでチャートデータを渡すと、こんな感じに(<em>´ω`</em>)</p> <p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/038debbe-9a52-876a-a28c-d8d9de6cfb08.png" width="720"/></p> <p><strong>注意: ハマったポイント</strong></p> <p>グラフ描画だけであれば、データを渡すだけなので簡単(<em>´ω`</em>)<br /> ただ、サイズを変えるのは一苦労...</p> <p>注意するのは3つ。</p> <ol> <li><code>chartOption</code>で<code>maintainAspectRatio: false</code>を指定</li> <li><code>chartStyles</code>でチャートのheight/widthを100%に設定</li> <li>親要素(<code>.chart-container</code>)に<code>position: relative;</code></li> </ol> <p>vue-chartjsには<code>chartStyles</code>以外にもpropがあるので、<br /> ほかは<a target="_blank" rel="nofollow noopener" href="https://vue-chartjs.org/api/#props">公式ドキュメント</a>を参照</p> <hr /> <h2 id="いろいろ設定を変えてみる"><a href="#%E3%81%84%E3%82%8D%E3%81%84%E3%82%8D%E8%A8%AD%E5%AE%9A%E3%82%92%E5%A4%89%E3%81%88%E3%81%A6%E3%81%BF%E3%82%8B">いろいろ設定を変えてみる</a></h2> <p>色や形を変えるなどチャート自体の設定は、<br /> chart.jsの<a target="_blank" rel="nofollow noopener" href="https://www.chartjs.org/docs/latest/">公式ドキュメント</a>を見ていく感じっぽい。</p> <p>困ったら、公式ドキュメントをみれば、だいたい行けた感じ(<em>´ω`</em>)<br /> ここからは、実際に使ってみた設定例を紹介。</p> <p>長いので、目次を見て好きなのを見るのがオススメ。</p> <h3 id="dataset関連"><a href="#dataset%E9%96%A2%E9%80%A3">dataset関連</a></h3> <p>参考: <a target="_blank" rel="nofollow noopener" href="https://www.chartjs.org/docs/latest/charts/">Charts · Chart.js documentation</a><br /> ここに載ってる、それぞれのグラフを見るといい感じ♪</p> <h4>線の色を変える: <code>borderColor</code></h4> <pre><code class="javascript"> private chartData: ChartData = { labels: ["A", "B", "C", "D", "E"], datasets: [ { label: "Data One", data: [1, 5, 3, 4, 3], borderColor: "green" // 線の色 }, { label: "Data Two", data: [10, 50, 30, 40, 30], borderColor: "red" } ] }; </code></pre> <p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/9c199225-d605-26fa-8035-64f9d6a7f46e.png" alt="スクリーンショット 2019-09-21 18.15.50.png" /></p> <p>線の色を変えても、棒グラフの色は変わらない。。</p> <h4>塗りつぶしの色を変える: <code>backgroundColor</code></h4> <pre><code class="javascript"> private chartData: ChartData = { labels: ["A", "B", "C", "D", "E"], datasets: [ { label: "Data One", data: [1, 5, 3, 4, 3], borderColor: "green", // 線の色 backgroundColor: "rgba(0, 255, 0, 0.4)" // 塗りつぶしの色 }, { label: "Data Two", data: [10, 50, 30, 40, 30], borderColor: "red", backgroundColor: "rgba(255, 0, 0, 0.4)" } ] }; </code></pre> <p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/7419cb4a-19f5-47a9-e2c0-1222c7a2dd70.png" alt="スクリーンショット 2019-09-21 18.18.09.png" /></p> <p>いい感じになったけど、折れ線グラフの場合は、点の背景色も変わるので、透過しないほうが良いかも。</p> <p>色関連の値には、rgbaも使える。</p> <h4>塗りつぶしをしない: <code>fill</code></h4> <pre><code class="javascript"> private chartData: ChartData = { labels: ["A", "B", "C", "D", "E"], datasets: [ { label: "Data One", data: [1, 5, 3, 4, 3], borderColor: "green", backgroundColor: "rgba(0, 255, 0, 0.4)", fill: false // 折れ線グラフの塗りつぶしを無効化 }, { label: "Data Two", data: [10, 50, 30, 40, 30], borderColor: "red", backgroundColor: "rgba(255, 0, 0, 0.4)", fill: false } ] }; </code></pre> <p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/5965d143-2f58-f876-c84a-8c904448431a.png" alt="スクリーンショット 2019-09-21 18.19.33.png" /></p> <h4>線を真っ直ぐにする: <code>tension</code></h4> <pre><code class="javascript"> private chartData: ChartData = { labels: ["A", "B", "C", "D", "E"], datasets: [ { label: "Data One", data: [1, 5, 3, 4, 3], borderColor: "green", backgroundColor: "rgba(0, 255, 0, 0.4)", fill: false, tension: 0 // 線を真っ直ぐにする }, { label: "Data Two", data: [10, 50, 30, 40, 30], borderColor: "red", backgroundColor: "rgba(255, 0, 0, 0.4)", fill: false, tension: 0 } ] }; </code></pre> <p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/75f69804-de7c-6996-5723-9a0a6368d035.png" alt="スクリーンショット 2019-09-21 18.20.41.png" /></p> <p>折れ線ぽっくなった(<em>´ω`</em>)</p> <h4>折れ線グラフの点の大きさを変える: <code>radius</code> / <code>hoverRadius</code></h4> <pre><code class="javascript"> private chartData: ChartData = { labels: ["A", "B", "C", "D", "E"], datasets: [ { label: "Data One", data: [1, 5, 3, 4, 3], borderColor: "green", backgroundColor: "rgba(0, 255, 0, 0.4)", fill: false, tension: 0, radius: 8, // 点の大きさ hoverRadius: 20 // 点の大きさ(マウスホバー時) }, { label: "Data Two", data: [10, 50, 30, 40, 30], borderColor: "red", backgroundColor: "rgba(255, 0, 0, 0.4)", fill: false, tension: 0, radius: 8, hoverRadius: 20 } ] }; </code></pre> <p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/362e525a-bf91-1fe9-cfe1-e58235dc739c.png" alt="スクリーンショット 2019-09-21 18.23.48.png" /></p> <h4>グラフを組み合わせる(複合グラフ): <code>type</code></h4> <pre><code class="javascript"> private chartData: ChartData = { labels: ["A", "B", "C", "D", "E"], datasets: [ { label: "Data One", data: [1, 5, 3, 4, 3], borderColor: "green", backgroundColor: "rgba(0, 255, 0, 0.4)", fill: false, tension: 0, radius: 8, hoverRadius: 20, }, { label: "Data Two", data: [10, 50, 30, 40, 30], borderColor: "red", backgroundColor: "rgba(255, 0, 0, 0.4)", fill: false, tension: 0, radius: 8, hoverRadius: 20, type: "line" // 折れ線グラフで表示 } ] }; </code></pre> <p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/c191c6ea-341f-be05-127c-ee9ec8c0ac83.png" alt="スクリーンショット 2019-09-21 18.29.23.png" /></p> <p>Barのときにチャートの<code>type</code>を指定すると変更できる。</p> <p>参考: <a target="_blank" rel="nofollow noopener" href="https://www.chartjs.org/docs/latest/charts/mixed.html">Mixed · Chart.js documentation</a><br /> 参考: <a target="_blank" rel="nofollow noopener" href="https://qiita.com/kd9951/items/aece80abe0bd42b3b5d3">chart.js で複数軸の複合グラフを描く - Qiita</a></p> <h3 id="options関連"><a href="#options%E9%96%A2%E9%80%A3">options関連</a></h3> <p>参考: <a target="_blank" rel="nofollow noopener" href="https://www.chartjs.org/docs/latest/configuration/">Configuration · Chart.js documentation</a><br /> このあたりを見ていくといい感じ。</p> <p>それぞれの例がわかりくくいけど、このあたりに<code>option</code>のどこに書けばいいか書いてある。</p> <p><img width="800" alt="スクリーンショット_2019-09-21_19_15_19.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/169fd02c-5828-18de-2f65-f5af4a4f7668.png"></p> <p>また、Y軸関連は、こっち。</p> <p>参考: <a target="_blank" rel="nofollow noopener" href="https://www.chartjs.org/docs/latest/axes/">Axes · Chart.js documentation</a></p> <h4>凡例の位置を変える: <code>legend</code></h4> <pre><code class="javascript"> private chartOption: ChartOptions = { maintainAspectRatio: false, legend: { position: "bottom" }, // 凡例の位置を変える } </code></pre> <p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/40c5924f-89e3-f999-9c26-c6ea1a3644f3.png" alt="スクリーンショット 2019-09-21 18.34.32.png" /></p> <p>参考: <a target="_blank" rel="nofollow noopener" href="https://www.chartjs.org/docs/latest/configuration/legend.html">Legend · Chart.js documentation</a></p> <h4>折れ線グラフの点の大きさなどを全体で設定: <code>elements</code></h4> <pre><code class="javascript"> private chartData: ChartData = { labels: ["A", "B", "C", "D", "E"], datasets: [ { label: "Data One", data: [1, 5, 3, 4, 3], borderColor: "green", backgroundColor: "rgba(0, 255, 0, 0.4)" }, { label: "Data Two", data: [10, 50, 30, 40, 30], borderColor: "red", backgroundColor: "rgba(255, 0, 0, 0.4)", type: "line" } ] }; // チャートのオプション private chartOption: ChartOptions = { maintainAspectRatio: false, legend: { position: "bottom" }, elements: { point: { radius: 8, // 点の大きさ hoverRadius: 20 // 点の大きさ(マウスホバー時) }, line: { tension: 0, // 線を真っ直ぐにする fill: false // 折れ線グラフの塗りつぶしを無効化 } } }; </code></pre> <p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/4c258ed8-30fd-a507-a765-f5d61b7375df.png" alt="スクリーンショット 2019-09-21 18.38.40.png" /></p> <p>参考: <a target="_blank" rel="nofollow noopener" href="https://www.chartjs.org/docs/latest/configuration/elements.html">Elements · Chart.js documentation</a></p> <h4>Y軸を2つにする: <code>scales.yAxes</code></h4> <pre><code class="javascript"> private chartData: ChartData = { labels: ["A", "B", "C", "D", "E"], datasets: [ { label: "Data One", data: [1, 5, 3, 4, 3], borderColor: "green", backgroundColor: "rgba(0, 255, 0, 0.4)" }, { label: "Data Two", data: [10, 50, 30, 40, 30], borderColor: "red", backgroundColor: "rgba(255, 0, 0, 0.4)", type: "line", yAxisID: "y-axis-2" // Y軸のIDを指定 } ] }; // チャートのオプション private chartOption: ChartOptions = { maintainAspectRatio: false, legend: { position: "bottom" }, elements: { point: { radius: 8, hoverRadius: 20 }, line: { tension: 0, fill: false } }, scales: { yAxes: [ { // 左の軸。設定はデフォルトのまま }, { id: "y-axis-2", position: "right" // 右に表示する } ] } }; </code></pre> <p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/d43c6ab7-0ae1-0fc1-3d4d-01a74b71bd78.png" alt="スクリーンショット 2019-09-21 18.43.06.png" /></p> <p>参考: <a target="_blank" rel="nofollow noopener" href="https://www.chartjs.org/docs/latest/axes/">Axes · Chart.js documentation</a></p> <h3>Y軸のラベルを変える: <code>scales.yAxes.ticks</code></h3> <pre><code class="javascript"> private chartOption: ChartOptions = { maintainAspectRatio: false, legend: { position: "bottom" }, elements: { point: { radius: 8, hoverRadius: 20 }, line: { tension: 0, fill: false } }, scales: { yAxes: [ { ticks: { // 左の軸は、先頭に'¥'をつける callback: (label, index, labels) => "¥" + label.toLocaleString() } }, { id: "y-axis-2", position: "right", ticks: { // 右の軸は、末尾に'冊'をつける callback: (label, index, labels) => label.toLocaleString() + "冊" } } ] } }; </code></pre> <p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/b9918ea8-982f-b453-ae63-424243606006.png" alt="スクリーンショット 2019-09-21 18.46.00.png" /></p> <p>ただ、このままだとホバーしたときのツールチップの値は変わらない。。<br /> 後述する<code>tooltips.callbacks.label</code>を使って変更が必要。</p> <p>参考: <a target="_blank" rel="nofollow noopener" href="https://www.chartjs.org/docs/latest/axes/">Axes · Chart.js documentation</a><br /> 参考: <a target="_blank" rel="nofollow noopener" href="https://blog.capilano-fw.com/?p=235">コピペでOK!Chart.js の数字を3桁カンマで表示する方法 – console dot log</a></p> <h3>横軸を非表示にする: <code>scales.yAxes.display</code></h3> <pre><code class="javascript"> private chartOption: ChartOptions = { maintainAspectRatio: false, legend: { position: "bottom" }, elements: { point: { radius: 8, hoverRadius: 20 }, line: { tension: 0, fill: false } }, scales: { yAxes: [ { ticks: { callback: (label, index, labels) => "¥" + label.toLocaleString() } }, { id: "y-axis-2", position: "right", display: false, // 右の軸を非表示にする ticks: { callback: (label, index, labels) => label.toLocaleString() + "冊" } } ] }, } </code></pre> <p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/d4ec8fe1-73ca-27a1-5efd-690faefd13bc.png" alt="スクリーンショット 2019-09-21 18.57.45.png" /></p> <p>参考: <a target="_blank" rel="nofollow noopener" href="https://www.chartjs.org/docs/latest/axes/">Axes · Chart.js documentation</a></p> <h3>ホバー時のツールチップで両方表示にする: <code>tooltips.mode</code></h3> <pre><code class="javascript"> private chartOption: ChartOptions = { maintainAspectRatio: false, legend: { position: "bottom" }, elements: { point: { radius: 8, hoverRadius: 20 }, line: { tension: 0, fill: false } }, scales: { yAxes: [ { ticks: { callback: (label, index, labels) => "¥" + label.toLocaleString() } }, { id: "y-axis-2", position: "right", ticks: { callback: (label, index, labels) => label.toLocaleString() + "冊" } } ] }, tooltips: { // ツールチップの設定 mode: "index" // 全データを表示する } }; </code></pre> <p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/57b8249c-2389-5860-a1b9-9904f733ae41.png" alt="スクリーンショット 2019-09-21 18.50.18.png" /></p> <p>参考: <a target="_blank" rel="nofollow noopener" href="https://www.chartjs.org/docs/latest/configuration/tooltip.html">Tooltip · Chart.js documentation</a></p> <h3>ツールチップの表示を変える: <code>tooltips.callbacks.label</code></h3> <pre><code class="javascript"> private chartOption: ChartOptions = { maintainAspectRatio: false, legend: { position: "bottom" }, elements: { point: { radius: 8, hoverRadius: 20 }, line: { tension: 0, fill: false } }, scales: { yAxes: [ { ticks: { callback: (label, index, labels) => "¥" + label.toLocaleString() } }, { id: "y-axis-2", position: "right", ticks: { callback: (label, index, labels) => label.toLocaleString() + "冊" } } ] }, tooltips: { // ツールチップの設定 mode: "index", callbacks: { label: function(tooltipItem, data) { // ラベルの表示変更 // データが'Data One'か'Data Two'かのインデックスを取得 const index = tooltipItem.datasetIndex; if (index === 0) { // 凡例にあるラベルを取得 var label = data.datasets[index].label || ""; if (label) label += ": "; // 該当データを取得して、先頭に'¥'をつける label += "¥" + tooltipItem.yLabel.toLocaleString(); return label; } else { var label = data.datasets[index].label || ""; if (label) label += ": "; label += tooltipItem.yLabel.toLocaleString() + "冊"; return label; } } } } }; </code></pre> <p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/cec3e36f-e681-9f8f-9085-e8d9b0428036.png" alt="スクリーンショット 2019-09-21 18.53.48.png" /></p> <p>参考: <a target="_blank" rel="nofollow noopener" href="https://www.chartjs.org/docs/latest/configuration/tooltip.html">Tooltip · Chart.js documentation</a><br /> 参考: <a target="_blank" rel="nofollow noopener" href="https://blog.capilano-fw.com/?p=235">コピペでOK!Chart.js の数字を3桁カンマで表示する方法 – console dot log</a></p> <h2 id="その他の便利関数"><a href="#%E3%81%9D%E3%81%AE%E4%BB%96%E3%81%AE%E4%BE%BF%E5%88%A9%E9%96%A2%E6%95%B0">その他の便利関数</a></h2> <h3 id="画面サイズを判定する"><a href="#%E7%94%BB%E9%9D%A2%E3%82%B5%E3%82%A4%E3%82%BA%E3%82%92%E5%88%A4%E5%AE%9A%E3%81%99%E3%82%8B">画面サイズを判定する</a></h3> <p><code>window.matchMedia().matches</code>でメディアクエリがJavaScriptで使えるらしい。。<br /> 以下のような関数を作っておけば、</p> <pre><code class="javascript">private isDesktop() { return window.matchMedia("screen and (min-width:768px)").matches; } </code></pre> <p>画面サイズなどによって、</p> <ol> <li>横軸ラベルの表示/非表示を切り替えたり、</li> <li>適用するスタイルを変えたり、</li> <li>点の大きさを変えたり</li> <li>できる。</li> </ol> <p>参考: <a target="_blank" rel="nofollow noopener" href="https://obel.hatenablog.jp/entry/20160626/1466937585">Chart.js でレスポンシブ指定をするとサイズが自由に変更できなくなる - 約束の地</a></p> <h3 id="カラーコードのHEXをRGBAに変換する"><a href="#%E3%82%AB%E3%83%A9%E3%83%BC%E3%82%B3%E3%83%BC%E3%83%89%E3%81%AEHEX%E3%82%92RGBA%E3%81%AB%E5%A4%89%E6%8F%9B%E3%81%99%E3%82%8B">カラーコードのHEXをRGBAに変換する</a></h3> <p>こんな感じで、HEXカラーコードとopacityからRGBAに変換できる。</p> <pre><code class="javascript">private convertHex(hex: string, opacity: number) { const arr = hex.replace("#", ""); const r = parseInt(arr.substring(0, 2), 16); const g = parseInt(arr.substring(2, 4), 16); const b = parseInt(arr.substring(4, 6), 16); return "rgba(" + r + "," + g + "," + b + "," + opacity + ")"; } </code></pre> <p>RGBで入力するのは大変なので、これがあると便利(<em>´ω`</em>)<br /> こんな感じで使えます(<em>´ω`</em>)</p> <pre><code class="javascript">{ label: "Data Two", data: [10, 50, 30, 40, 30], borderColor: "red", backgroundColor: this.convertHex("#FF0000", 0.4), } </code></pre> <p>参考: <a target="_blank" rel="nofollow noopener" href="https://jsfiddle.net/subodhghulaxe/t568u/">Hex 2 rgba converter - JSFiddle</a></p> <h2 id="おわりに"><a href="#%E3%81%8A%E3%82%8F%E3%82%8A%E3%81%AB">おわりに</a></h2> <p><a target="_blank" rel="nofollow noopener" href="https://vue-chartjs.org/">vue-chartjs</a>をつかえば、<br /> TypeScriptなNuxt.jsでも簡単にいろんなグラフが表示できる(<em>´ω`</em>)</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>こんな感じで、積読本・読了本の総数でどこまでいけるか表示したりしています♪</p> <blockquote class="twitter-tweet"><p lang="ja" dir="ltr">なんかつけてる(*´ω`*)月までは遠いので、まずは近いところから(*´ω`*) <a target="_blank" rel="nofollow noopener" href="https://t.co/JMHoFbv5cg">pic.twitter.com/JMHoFbv5cg</a></p>— 積読ハウマッチ📚きらぷか (@kira_puka) <a target="_blank" rel="nofollow noopener" href="https://twitter.com/kira_puka/status/1175076203599785984?ref_src=twsrc%5Etfw">September 20, 2019</a></blockquote> <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 target="_blank" rel="nofollow noopener" href="https://vue-chartjs.org/">📈 vue-chartjs</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://www.chartjs.org/docs/latest/">Chart.js · Chart.js documentation</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/kattoon/items/a76caa684261956c900b">NuxtでChart.jsを利用する - Qiita</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/mitsudaman/items/b30b5e905ff348bf35bc">Nuxt.js使って個人開発やってみた 〜時間割を円グラフで表示するサービスをリリースした話〜 - Qiita</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/muramasawani/items/6086ecfcb92034774599">vue-chartjs馴れ初め - Qiita</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/is_ryo/items/1609dcee76aa5df93e2a">Vue.js(ts)でvue-chartjsを使う - Qiita</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/kd9951/items/aece80abe0bd42b3b5d3">chart.js で複数軸の複合グラフを描く - Qiita</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://obel.hatenablog.jp/entry/20160626/1466937585">Chart.js でレスポンシブ指定をするとサイズが自由に変更できなくなる - 約束の地</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://jsfiddle.net/subodhghulaxe/t568u/">Hex 2 rgba converter - JSFiddle</a></li> </ul> きらぷか@積読ハウマッチ/SSSAPIなど tag:crieit.net,2005:PublicArticle/15338 2019-08-20T17:41:42+09:00 2019-08-20T17:41:42+09:00 https://crieit.net/posts/HTML-JSX HTMLのノードを直接生成するJSXファクトリを簡単に書いてみた <p>JSXって知っていますか? JSXとは、Reactなどに使われているXML(HTML)とjavascriptの混在記法です。<br /> そして、JSXを内部的に処理する関数をJSXファクトリといいます。<br /> これは、BabelやTypeScriptコンパイラの設定で自由に変更可能です。<br /> ところでWeb Componentsという、新しいHTMLエレメントを自由に作れるjavascriptのAPIが存在するのはご存じでしょうか。</p> <p>これを使えば、Reactのようなことがネイティブのjavascriptでできるのではないだろうか?そう考えたのでやってみることにしました。</p> <p>まずはJSXファクトリの仕様から説明します。<br /> JSXファクトリは以下のようなシグネチャを持つ関数であれば何でも構いません。</p> <pre><code class="javascript">/** * @param element 文字列または関数またはクラス。文字列の場合はタグの名前、クラス・関数の場合はタグが表すコンポーネント。 * @param props 要素の属性。オブジェクトで渡す。存在しない場合はnull。 * @param children 可変長引数で、現在の要素の子要素を渡す。 */ function JSXFactory(element, props, ...children) {} </code></pre> <p>そしてこれは重要なことなのですが、この関数が返す値や動作は何でもよいのです。<br /> JSXとは、単なる糖衣構文でしかありません。つまり、その動作は実装した人にゆだねられます。<br /> 以下に、JSXの変換例を示します。</p> <pre><code class="jsx">import React from 'react'; let node = <div className="hoge">This is child text.</div>; </code></pre> <p>これは以下のようなjavascriptに変換されます。</p> <pre><code class="javascript">import React from 'react'; let node = React.createElement('div', {'className': 'hoge'}, 'This is child text'); </code></pre> <p>そしてこの、React.createElementにあたる関数はtsconfig.json内で以下のように設定できます。</p> <pre><code class="json">{ "compilerOptions": { "jsx": "react", "jsxFactory": "myCreateElement" } } </code></pre> <p>この場合はjsxFactoryにmyCreateElementを設定しているため、React.createElement関数の代わりにmyCreateElementが使われます。<br /> さて、これらの仕様を使って、普通のHTMLノードを生成するJSXファクトリを作ってみたいと思います。<br /> まずは型の宣言から。</p> <pre><code class="typescript">declare namespace JSX { export interface JSXElement<P = {}> {} export interface IntrinsicElements { [elemName: string]: any; } export type GlobalAttributes = EventAttributes | ElementAttributes; export type ElementAttributes = /* HTMLノードのグローバル属性(略) */ export type EventAttributes = /* イベント属性(略) */ } </code></pre> <p>シンプルに行きましょう。<br /> 組み込みの要素の型をいちいち定義していたら日が暮れます。<br /> なのでanyで誤魔化しました。</p> <p>続いては本題のJSXファクトリの実装です。</p> <pre><code class="typescript">import './types'; type PropType<K> = { [P in JSX.GlobalAttributes | keyof K]: any }; export function createElement<K, E extends JSX.JSXElement<K>>( element: E | string, props: PropType<K>, ...children: any[] ): Element { let elem: Element; if (typeof element === 'string') { elem = document.createElement(element); } else { elem = document.createElement( (element as any).name.replace(/(?!^)([A-Z0-9])/g, '-$1').toLowerCase() ); } Object.keys(props || {}) .filter(it => props.hasOwnProperty(it)) .forEach(it => elem.setAttribute(convertPropName(it), props[it as keyof PropType<K>]) ); children.forEach(it => !(it instanceof Element) ? it.hasOwnProperty('render') && typeof it.render === 'function' ? elem.appendChild(it.render()) : elem.append(it.toString()) : elem.appendChild(it) ); return elem; } function convertPropName(name: string): string { switch (name) { case 'className': return 'class'; case 'htmlFor': return 'for'; default: return name; } } </code></pre> <p>やってること自体はきわめて単純。<br /> <code>document.createElement</code>関数でノードを作り、引数に従って属性や子要素を追加しているだけです。<br /> JSXではclassとforがそれぞれclassNameとhtmlForになるため、convertPropName関数はそれを変換します。</p> <p>にしてもJSXファクトリ、かなり面白いですね。<br /> これ使えばいろいろできそうな予感。<br /> React以外でも活用の目はありそうです。</p> frodo821 tag:crieit.net,2005:PublicArticle/15334 2019-08-19T08:27:39+09:00 2019-08-19T08:58:52+09:00 https://crieit.net/posts/TypeScript-GIF TypeScriptの開発をGIFアニメで見てみる <p>TypeScriptを使っている風景をGIFアニメで説明します。TypeScript良いって聞くけど良くわからないのでスルーしてしまっているという方は多いのではないでしょうか? 使ってみてはじめて感じるものもありますが、文字で記事を見るだけではなかなか想像しづらいと思いますのでGIF動画でいくつか見られるようにしてみました。</p> <p>今回はVSCode上でNext.jsのTypeScriptテンプレートを使って例をあげていきます。理由としてはプロジェクトを生成すればすぐ動かせるのと、ReactはJSXを使っていますが基本的には素の形式のためTypeScriptの恩恵を受けやすいためです。Vue.jsのvueファイルだとTypeScriptの恩恵を受けにくい場合が多いため、今回はそちらを選択しました。</p> <p>せっかくですので実際にコードをいじりつつ見てみると面白いと思います。リポジトリは下記です。</p> <p><a target="_blank" rel="nofollow noopener" href="https://github.com/zeit/next.js/tree/canary/examples/with-typescript">next.js/examples/with-typescript at canary · zeit/next.js</a></p> <p>下記でcloneし、</p> <pre><code class="sh">yarn create next-app --example with-typescript with-typescript-app </code></pre> <p>下記で実行です。</p> <pre><code class="sh">yarn dev </code></pre> <h2 id="厳密な型の無い言語で困ること"><a href="#%E5%8E%B3%E5%AF%86%E3%81%AA%E5%9E%8B%E3%81%AE%E7%84%A1%E3%81%84%E8%A8%80%E8%AA%9E%E3%81%A7%E5%9B%B0%E3%82%8B%E3%81%93%E3%81%A8">厳密な型の無い言語で困ること</a></h2> <p>まず比較として、TypeScriptのような厳密な型の無い言語の場合に困ることをあげてみます。</p> <p>例えばnameという氏名が入っているカラムを無理やり半角スペースで区切って姓と名を分けて表示したい、という場合、例としてPHPで書くと下記のようになります。(例のためあまりきれいな形ではないです)</p> <p>まずは分割するメソッドを作成します。</p> <pre><code class="php">public function splitName() { $parts = explode(' ', $this->name); return ['sei' => $parts[0], 'mei' => $parts[1]]; } </code></pre> <p>こんな感じで、勝手に独自の配列や、Rubyとかでもハッシュを作成してreturnするパターンを作ることもあるのではないかと思います。そしてそれをコントローラでビューに渡したりします。</p> <pre><code class="php">$this->set('seimei', $user->splitName()); </code></pre> <p>そしてそれをテンプレート側で表示します。</p> <pre><code class="php"><div> 姓:<?= $seimei['sei'] ?> </div> <div> 名:<?= $seimei['mei'] ?> </div> </code></pre> <p>これで実装は可能です。でもこれですが、splitNameメソッドが「sei、meiはかっこ悪いからlastName、firstNameに変えて」となってしまった場合。テンプレート側では存在しないキーを指定する形になってしまうためエラーになってしまいます。上記のように1箇所だけ書いている場合は問題ないかもしれませんが、色々なページでいくつも書いていると抜けが出る恐れもあります。</p> <h2 id="TypeScriptで書いてみる"><a href="#TypeScript%E3%81%A7%E6%9B%B8%E3%81%84%E3%81%A6%E3%81%BF%E3%82%8B">TypeScriptで書いてみる</a></h2> <p>これをTypeScriptで書いてみます。</p> <pre><code class="typescript">function splitName(user: User) { const parts = user.name.split(/ /) return { sei: parts[0], mei: parts[1] } } </code></pre> <p>そして実際に表示している箇所に追記します。</p> <pre><code class="jsx"> const seimei = splitName(user) return ( <div> <h1>Detail for {user.name}</h1> <p>ID: {user.id}</p> <p>Last Name: {seimei.sei}</p> <p>First Name: {seimei.mei}</p> </div> ) </code></pre> <p>処理の流れ的は先程のPHPと全く同じです。ただ、ここからがTypeScriptの便利なところです。</p> <h2 id="勝手に型を定義してくれる"><a href="#%E5%8B%9D%E6%89%8B%E3%81%AB%E5%9E%8B%E3%82%92%E5%AE%9A%E7%BE%A9%E3%81%97%E3%81%A6%E3%81%8F%E3%82%8C%E3%82%8B">勝手に型を定義してくれる</a></h2> <p>まず、適当に作ったsplitNameという関数ですが、暗黙で返り値の型が定義されるためカーソルを合わせるとこんな感じに固定された返り値の型が表示されます。</p> <p><a href="https://crieit.now.sh/upload_images/a0b376c790262ade844136c1d38142335d4ede4fbeacc.gif" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/a0b376c790262ade844136c1d38142335d4ede4fbeacc.gif?mw=700" alt="" /></a></p> <h2 id="補完も効く"><a href="#%E8%A3%9C%E5%AE%8C%E3%82%82%E5%8A%B9%E3%81%8F">補完も効く</a></h2> <p>もちろん補完も効きますので入力ミスによる不具合も発生しません。もちろんjsx上で入力している場合もです。</p> <p><a href="https://crieit.now.sh/upload_images/031bf31fe270d8f91da7adf2c35f8a8f5d4edf48e68f9.gif" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/031bf31fe270d8f91da7adf2c35f8a8f5d4edf48e68f9.gif?mw=700" alt="" /></a></p> <p>Vue.jsの場合は独自のファイル形式のため、テンプレート上等では補完等は効いていないと思います。(2019/8現在)(なにか方法はあるのかもですが)</p> <h2 id="返り値を変えたら即エラーになる"><a href="#%E8%BF%94%E3%82%8A%E5%80%A4%E3%82%92%E5%A4%89%E3%81%88%E3%81%9F%E3%82%89%E5%8D%B3%E3%82%A8%E3%83%A9%E3%83%BC%E3%81%AB%E3%81%AA%E3%82%8B">返り値を変えたら即エラーになる</a></h2> <p>返り値を変えると型が変わり、表示している箇所が間違いになるのですぐにエラーになり気づくことが出来ます。</p> <p><a href="https://crieit.now.sh/upload_images/dd01a8a4b251166921e2a12453917c5e5d4ee06692c58.gif" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/dd01a8a4b251166921e2a12453917c5e5d4ee06692c58.gif?mw=700" alt="" /></a></p> <p>もちろんwatchしている場合はすぐにエラーになりビルドに失敗しますので決してこの原因で本番にて問題が発生することはありません。</p> <p><a href="https://crieit.now.sh/upload_images/8bc23d58c9f7d0f0de702cbe5bb687745d4ee1649db08.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/8bc23d58c9f7d0f0de702cbe5bb687745d4ee1649db08.png?mw=700" alt="" /></a></p> <h2 id="自分で型を定義したほうが親切"><a href="#%E8%87%AA%E5%88%86%E3%81%A7%E5%9E%8B%E3%82%92%E5%AE%9A%E7%BE%A9%E3%81%97%E3%81%9F%E3%81%BB%E3%81%86%E3%81%8C%E8%A6%AA%E5%88%87">自分で型を定義したほうが親切</a></h2> <p>とはいえ本来は自分でちゃんと定義してあげたほうが良いと思います。そもそも返り値を間違えてしまっていても気づくことができますし、型は別ファイルにしておけば別のファイルからも参照することが出来ます。</p> <pre><code class="typescript">type SeiMei = { lastName: string firstName: string } function splitName(user: User): SeiMei { const parts = user.name.split(/ /) return { lastName: parts[0], firstName: parts[1] } } </code></pre> <p>typeかinterfaceで同じような事ができるため、適宜使い分けてください。</p> <h2 id="オリジナルの型も作れる"><a href="#%E3%82%AA%E3%83%AA%E3%82%B8%E3%83%8A%E3%83%AB%E3%81%AE%E5%9E%8B%E3%82%82%E4%BD%9C%E3%82%8C%E3%82%8B">オリジナルの型も作れる</a></h2> <p>例えばユーザーにステータスのようなものがあったとします。<code>status</code>というカラムで、enumや文字列でステータスを定義している場合などです。例えば通常ユーザーであれば <code>user.status = 'normal'</code>、停止中ユーザーであれば<code>user.status = 'disabled</code>、上客であれば<code>user.status = 'special'</code>など。</p> <p>こういうものも一文字間違えるだけで不具合の原因になりますし、もしDBにそれを保存してしまうと様々な箇所で影響が出てとんでもないことになってしまいます。TypeScriptだとこういうものも型(文字リテラル型)にすることができます。</p> <pre><code class="typescript"><br />type UserStatus = 'normal' | 'disabled' | 'special' interface User { status: UserStatus } </code></pre> <p>実際に操作するとこのようにエラーを出したり補完してくれたりします。</p> <p><a href="https://crieit.now.sh/upload_images/b9d54e11dd10c51967b47e873842e9c45d502adb146f0.gif" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/b9d54e11dd10c51967b47e873842e9c45d502adb146f0.gif?mw=700" alt="" /></a></p> <p>文字リテラル型ではなく列挙型(Enums)を使うこともできます。DBの型との関連によって適切なものを使い分けていきましょう。</p> <p><a target="_blank" rel="nofollow noopener" href="https://typescript-jp.gitbook.io/deep-dive/type-system/enums">Enums - TypeScript Deep Dive 日本語版</a></p> <h2 id="自動でimportしてくれる"><a href="#%E8%87%AA%E5%8B%95%E3%81%A7import%E3%81%97%E3%81%A6%E3%81%8F%E3%82%8C%E3%82%8B">自動でimportしてくれる</a></h2> <p>importも自動で行ってくれます。型の補完をする際に、importされていなければ自動的に追加してくれます。</p> <p><a href="https://crieit.now.sh/upload_images/9bb28439e70f6d0c07d95b4843348d4e5d502ce2dd8ae.gif" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/9bb28439e70f6d0c07d95b4843348d4e5d502ce2dd8ae.gif?mw=700" alt="" /></a></p> <p>自分で作った定義だけでなく、node_modules内のパッケージなども型情報があれば補完&importしてくれます。(これもVue.jsの場合はうまくいったりいかなかったりする場合があります)</p> <h2 id="まとめ"><a href="#%E3%81%BE%E3%81%A8%E3%82%81">まとめ</a></h2> <p>TypeScriptは他にも便利な機能がたくさんありますが、とりあえず使ってみて最初に感じるであろう便利な部分だけをまとめてみました。</p> <p>TwitterのTLを見ていると時々「TypeScript使い始めたら他の言語が怖い」という発言を見かけます。正直今は僕もPHPやRubyで配列やハッシュを書くのが怖いです。とはいえ全部classで書くのかと言うとそれも冗長すぎる気もしますしでもそうでなければテストを書かなきゃいけないですしなかなか悩ましいです。やはり言語自体で型を持ってくれているというのはありがたいし楽しいし楽です。</p> <p>まあとはいえ全部TypeScriptにするわけにはいかないので今後もテストを書きつつやっていくのでしょう。その分TypeScriptを使って開発する時の楽しさが増すのだと思います。</p> <h3 id="オススメの始め方"><a href="#%E3%82%AA%E3%82%B9%E3%82%B9%E3%83%A1%E3%81%AE%E5%A7%8B%E3%82%81%E6%96%B9">オススメの始め方</a></h3> <p>ということで是非使ってみてください。コツとしては、自分で設定せず今回のように最初から設定済みの公式テンプレートのようなものを使ったりするのがおすすめです。既存のプロジェクトがあるから難しい、という場合も、そういったプロジェクトの設定を参考にすると良いと思います。</p> だら@Crieit開発者 tag:crieit.net,2005:PublicArticle/15163 2019-06-24T19:48:11+09:00 2019-06-25T09:12:57+09:00 https://crieit.net/posts/TypeScript-A-Frame TypeScriptでA-Frameのコンポーネントを書く <p>TypeScriptを使っているプロジェクトでA-Frameのコンポーネントを書く時の方法。まだざっと試したところなのでおかしなところがあれば適宜調整が必要かも。</p> <p>とりあえず型を入れる。</p> <pre><code>yarn add @types/aframe </code></pre> <p>これで適当なtsファイル内で<code>AFRAME.registerComponent</code>とかを実行しても未定義エラー等が発生しなくなるため定義できるようになる。</p> <p>また、下記のようにimportして実行する方法もある。</p> <pre><code class="typescript">import { registerComponent } from 'aframe' registerComponent('my-component', { </code></pre> <p>コンポーネント内部で使うパラメータはオブジェクト内に定義すればいい。JavaScriptの場合だと、initメソッド内で初期化したりするが、それだと型エラーになるので要定義。</p> <pre><code class="typescript">registerComponent('my-component', { hoge: true, fuga: 100, </code></pre> <p>というか、型定義のテストファイルに一通りのものが書かれている。</p> <p><a target="_blank" rel="nofollow noopener" href="https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/aframe/test/aframe-tests.ts">https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/aframe/test/aframe-tests.ts</a></p> <pre><code class="typescript">const Component = registerComponent('test-component', { schema: { myProperty: { default: [], parse() { return [true]; } }, string: { type: 'string' }, num: 0 }, init() { this.data.num = 0; this.el.setAttribute('custom-attribute', 'custom-value'); }, update() {}, tick() {}, remove() {}, pause() {}, play() {}, multiply(f: number) { // Reference to system because both were registered with the same name. return f * this.data.num * this.system!.data.counter; } }); </code></pre> <p>他にも色々書かれているのでこちらを参考にしていけばなんとかなるのではないかと思う。</p> <h2 id="プロパティの型を明示的に指定する場合は?"><a href="#%E3%83%97%E3%83%AD%E3%83%91%E3%83%86%E3%82%A3%E3%81%AE%E5%9E%8B%E3%82%92%E6%98%8E%E7%A4%BA%E7%9A%84%E3%81%AB%E6%8C%87%E5%AE%9A%E3%81%99%E3%82%8B%E5%A0%B4%E5%90%88%E3%81%AF%EF%BC%9F">プロパティの型を明示的に指定する場合は?</a></h2> <p>事前にinterfaceを定義する感じ? ただこれだとメソッドも再定義しないといけないので非常に面倒。何か良い方法は無いのだろうか。</p> だら@Crieit開発者 tag:crieit.net,2005:PublicArticle/15026 2019-05-26T15:21:43+09:00 2019-05-26T15:21:43+09:00 https://crieit.net/posts/TypeScript-setTimeout-NodeJS-Timer-window-setTimeout TypeScriptでsetTimeout()がNodeJS.Timerになる理由から、window.setTimeout()との違いを理解する <p>AngularのSPA(Single Page Application)のプログラムを書いてたら、以下の問題に遭遇しました。</p> <ul> <li>TypeScriptのコード内でのsetInterval()の返り値には罠がある</li> <li><a target="_blank" rel="nofollow noopener" href="https://blog.kubosho.com/entry/setinterval-trap-on-typescript/">https://blog.kubosho.com/entry/setinterval-trap-on-typescript/</a></li> </ul> <p>内容としては<strong>「Type 'Timer' is not assignable to type 'number'.」</strong>というエラーメッセージが表示されます。<br /> VSCode(Visual Studio Code)で関数の戻り値を確認すると、戻り値の型はNodeJS.Timerになってます。</p> <p>私の場合は、Angularで「setTimeout()」を記述しただけです。なぜそれが「NodeJS.Timer」となってしまうのか気になったので、調べてみました。</p> <h2 id="window.setTimeout()にするとnumberになる"><a href="#window.setTimeout%28%29%E3%81%AB%E3%81%99%E3%82%8B%E3%81%A8number%E3%81%AB%E3%81%AA%E3%82%8B">window.setTimeout()にするとnumberになる</a></h2> <p>次のstack overflowを見るとわかるのですが、「setTimeout()」を「window.setTimeout()」に変えると、戻り値は数値型で通用します。</p> <ul> <li>TypeScript - use correct version of setTimeout (node vs window)</li> <li><a target="_blank" rel="nofollow noopener" href="https://stackoverflow.com/questions/45802988/typescript-use-correct-version-of-settimeout-node-vs-window">https://stackoverflow.com/questions/45802988/typescript-use-correct-version-of-settimeout-node-vs-window</a></li> </ul> <pre><code class="js">// Another workaround that does not affect variable declaration: let n: number; n = <any>setTimeout(function () { /* snip */ }, 500); // Also, it should be possible to use the window object explicitly without any: let n: number; n = window.setTimeout(function () { /* snip */ }, 500); </code></pre> <p>この仕様はMDNにも記載されており、「window.setTimeout()」のドキュメントを確認すると、確かに戻り値は数値型です。</p> <blockquote> <p>戻り値 timeoutID は、setTimeout() を呼び出して作成したタイマーを識別する正の整数値です。<br /> <br /> <em>引用:<a target="_blank" rel="nofollow noopener" href="https://developer.mozilla.org/ja/docs/Web/API/WindowTimers/setTimeout">https://developer.mozilla.org/ja/docs/Web/API/WindowTimers/setTimeout</a></em></p> </blockquote> <h2 id="setTimeout()とwindow.setTimeout()の違い"><a href="#setTimeout%28%29%E3%81%A8window.setTimeout%28%29%E3%81%AE%E9%81%95%E3%81%84">setTimeout()とwindow.setTimeout()の違い</a></h2> <p>ではなぜ「window.setTimeout()」にするとTypeScriptの戻り値の解釈が変わるのかと申しますと、これは「setTimeout()」との違いが関係するようです。</p> <blockquote> <p>For JavaScript that does not run in a browser, the window object is not defined, so window.setTimeout() will fail. setTimeout() however, will work.</p> <p>引用: <a target="_blank" rel="nofollow noopener" href="https://stackoverflow.com/questions/20420429/what-the-difference-between-window-settimeout-and-settimeout">https://stackoverflow.com/questions/20420429/what-the-difference-between-window-settimeout-and-settimeout</a></p> </blockquote> <pre><code class="js">function myF() { function setTimeout(callback,seconds) { // call the native setTimeout function return window.setTimeout(callback,seconds*1000); } // call your own setTimeout function (with seconds instead of milliseconds) setTimeout(function() {console.log("hi"); },3); } myF(); </code></pre> <p><strong>要約すると「window.setTimeout()」が明示的にブラウザの「setTimeout()」を使うよう指示していることに対し、<br /> 単なる「setTimeout()」ではブラウザ外(例えばNode.jsのサーバーサイド)のJSや、自分たちで用意した関数も考慮されます。</strong></p> <p>その結果「setTimeout()」のTypeScriptにおける戻り値の型が、NodeJS.Timerになっているようです。</p> <p><a href="https://crieit.now.sh/upload_images/daf22b8f77bbc56e08e56f34d510f5795cea30563fb22.jpg" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/daf22b8f77bbc56e08e56f34d510f5795cea30563fb22.jpg?mw=700" alt="20190526144410.jpg" /></a></p> <h2 id="「window.」の省略について"><a href="#%E3%80%8Cwindow.%E3%80%8D%E3%81%AE%E7%9C%81%E7%95%A5%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6">「window.」の省略について</a></h2> <p>JavaScriptでは、グローバルオブジェクトの「window.」の記述を省略できます。</p> <ul> <li>「window.location.href」を「location.href」と書けます</li> <li>「window.alert()」は「alert()」と書けます</li> </ul> <p>ただし私が遭遇した今回の事例は、まさに「window.」を省略したことによって起こりました。<br /> 調べてみると、確かにNode.jsにはTimerのAPIで「setTimeout()」関数があります。</p> <ul> <li>setTimeout(callback, delay[, ...args])</li> <li><a target="_blank" rel="nofollow noopener" href="https://nodejs.org/api/timers.html">https://nodejs.org/api/timers.html</a></li> </ul> <p><strong>「window.」を省略したことで、TypeScriptがNode.jsの「setTimeout()」を使うと判断したという理屈になります。</strong></p> <h2 id="さいごに"><a href="#%E3%81%95%E3%81%84%E3%81%94%E3%81%AB">さいごに</a></h2> <p>色々と調べてみた結果ですが、私の中では以下の結論に落ち着きました。</p> <ul> <li>確実にブラウザの「setTimeout()」を動かしたいなら、「window.」は省略しないほうが良さそう</li> <li>ブラウザとサーバーサイドの両方で動作を想定するなら、「window.」は省略して記述する</li> </ul> このすみ tag:crieit.net,2005:PublicArticle/14906 2019-04-07T21:09:34+09:00 2019-04-08T23:33:53+09:00 https://crieit.net/posts/NodeJS-GraphQL NodeJSでGraphQLのサーバ側処理を実装してみる <h1 id="背景"><a href="#%E8%83%8C%E6%99%AF">背景</a></h1> <p>先週はクライアントからGraphQLへのアクセス方法を学習しました。<br /> - <a href="https://crieit.net/posts/React-GraphQL-API">ReactでGraphQL APIにアクセスする</a></p> <p>今週は「サーバサイドはどう実装するのか?」という部分を調査しました。</p> <h1 id="パッケージインストール"><a href="#%E3%83%91%E3%83%83%E3%82%B1%E3%83%BC%E3%82%B8%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB">パッケージインストール</a></h1> <p>今回は既に実装しているアプリケーションサーバにgraphqlを組み込みます。</p> <pre><code>$ npm install graphql express-graphql -save </code></pre> <h1 id="スキーマ定義"><a href="#%E3%82%B9%E3%82%AD%E3%83%BC%E3%83%9E%E5%AE%9A%E7%BE%A9">スキーマ定義</a></h1> <p>jsonでもtypescriptでもないので一旦外部ファイル(<strong>graphql/schema.graphql</strong>)として定義しました。<br /> graphqlに対応しているlintとかあるみたいなのですが、<br /> 今回は調査が追いついていません。</p> <h2 id="Queryタイプに型とクエリを定義する"><a href="#Query%E3%82%BF%E3%82%A4%E3%83%97%E3%81%AB%E5%9E%8B%E3%81%A8%E3%82%AF%E3%82%A8%E3%83%AA%E3%82%92%E5%AE%9A%E7%BE%A9%E3%81%99%E3%82%8B">Queryタイプに型とクエリを定義する</a></h2> <p>id(選手ID)でフィルタリングしてPlayer(選手)を返すクエリと<br /> players(選手リスト)を返すクエリを想定します。<br /> Playerオブジェクトは以下のようなプロパティを持つとします。<br /> DBからデータを取得して返却する際にプロパティ同士がマッピングされます。</p> <pre><code class="ts">type Query { player(id: Int!): Player players(name: String): [Player] }, type Player { id: Int name: String team_name :String team_id: Int } </code></pre> <h1 id="エンドポイントの作成"><a href="#%E3%82%A8%E3%83%B3%E3%83%89%E3%83%9D%E3%82%A4%E3%83%B3%E3%83%88%E3%81%AE%E4%BD%9C%E6%88%90">エンドポイントの作成</a></h1> <ul> <li>スキーマを外部ファイルから読み込む</li> <li>buildSchemaに渡してスキーマを生成する</li> <li>express_graphqlに <ul> <li>第一引数にスキーマ</li> <li>第二引数にメソッド定義</li> <li>第三引数はAPIエンドポイントでGraphiQL(GUI)を動かせるようにするかどうか</li> </ul></li> </ul> <p>を渡す。</p> <p><strong>App.ts</strong></p> <pre><code class="typescript">import { graphql} from './graphql' import * as express_graphql from 'express-graphql'; import { buildSchema } from 'graphql'; (中略) let schemaStr = readFileSync('./graphql/schema.graphql',{encoding:'utf8'}) var schema = buildSchema(schemaStr); let root = { player:(args)=>{ let result = new graphql().getPlayer(args) console.log(result) return result } }; app.use('/graphql', express_graphql({ schema: schema, rootValue: root, graphiql: true })); </code></pre> <h1 id="メソッド実装"><a href="#%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89%E5%AE%9F%E8%A3%85">メソッド実装</a></h1> <p>GraphQLの処理部分は分けて実装したかったので、別途graphql.tsを作ってそちらに実装します。</p> <ul> <li>App.tsで定義したGraphQLのメソッド定義を書く</li> </ul> <p><strong>graphql.ts</strong></p> <pre><code class="typescript">import { playerService } from "../services/playerService"; export class graphql{ public getPlayer(args:any):Promise<any>{ return new Promise((resolve,reject)=>{ (async()=>{ //既存のDBアクセスメソッドにリクエストパラメータを渡す let player = await new playerService().findId(args.id) console.log(player) resolve(player[0]) })() .catch((err)=>{ console.log(err) reject(err) }) }) } } </code></pre> <h1 id="完成イメージ(動画)"><a href="#%E5%AE%8C%E6%88%90%E3%82%A4%E3%83%A1%E3%83%BC%E3%82%B8%28%E5%8B%95%E7%94%BB%29">完成イメージ(動画)</a></h1> <div class="iframe-wrapper"><iframe width="560" height="315" src="https://www.youtube.com/embed/m4oMPWFn24A" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></div> <h1 id="参考リンク"><a href="#%E5%8F%82%E8%80%83%E3%83%AA%E3%83%B3%E3%82%AF">参考リンク</a></h1> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://dev.classmethod.jp/server-side/node-js-server-side/graphql-tutorial-nodejsexpress/">GraphQLをNode.jsとexpressでためしてみる</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://www.m3tech.blog/entry/graphql-apollo-react-express-nodejs">GraphQL入門 - React.js & Express.js & Apollo の簡単チュートリアル</a></li> </ul> ckoshien tag:crieit.net,2005:PublicArticle/14861 2019-03-08T00:17:46+09:00 2019-03-08T00:19:10+09:00 https://crieit.net/posts/Express-TypeORM-Wercker-CI Express+TypeORMの環境にてWerckerでCI <p>ExpressとTypeORMを使ったアプリケーションのWerckerでのCIがとりあえず完成したのでメモ。</p> <pre><code class="yaml">box: node services: - id: mysql tag: 5.7 env: MYSQL_ALLOW_EMPTY_PASSWORD: true MYSQL_DATABASE: dbname build: steps: - script: name: yarn code: yarn - script: name: Fill mysql env code: |- cp .env.example .env sudo sed -i -e "s/TYPEORM_HOST=localhost/TYPEORM_HOST=$MYSQL_PORT_3306_TCP_ADDR/g" .env sudo sed -i -e "s/TYPEORM_PORT=3306/TYPEORM_PORT=$MYSQL_PORT_3306_TCP_PORT/g" .env cp .env test/.env - script: name: migration code: yarn run migrate:run - script: name: yarn test code: yarn test </code></pre> <p>migrationとtestのコマンドは適宜自分で設定。</p> <p><code>test/.env</code>はテスト用に読むこむやつを設定しているというだけなので別のやり方をしている場合は不要。</p> <p>TypeORMは特にテスト用のDB管理方法があるわけではないようなので、事前にマイグレーションしている。(ローカル環境も考慮すると全部自分で準備しなければならないのが非常に面倒)</p> <p>ちなみにテストは下記でつくったもの。</p> <p><a href="https://crieit.net/posts/Mocha-SuperTest-async-await">Mocha+SuperTestでasync/awaitを使ってテスト</a></p> だら@Crieit開発者 tag:crieit.net,2005:PublicArticle/14740 2019-01-22T09:15:07+09:00 2019-01-22T09:15:07+09:00 https://crieit.net/posts/TypeScript-Vue TypeScriptでVueのクラスを継承する <p>TypeScriptでVue.jsの単一コンポーネントを作る場合、Vueクラスを基底とした継承クラスを作る。ただ、似たようなコンポーネントは同じメソッドなどが並ぶため使いまわしたいと思う時がある。</p> <p>試してみると特に問題なく継承用の共通クラスを作って使うことができたようなのでメモ。(適当にやってみただけなので問題があれば教えてください)</p> <h2 id="ベースとなるクラスを作成する"><a href="#%E3%83%99%E3%83%BC%E3%82%B9%E3%81%A8%E3%81%AA%E3%82%8B%E3%82%AF%E3%83%A9%E3%82%B9%E3%82%92%E4%BD%9C%E6%88%90%E3%81%99%E3%82%8B">ベースとなるクラスを作成する</a></h2> <p>まずはベースとなる、共通メソッドを含めたクラスを定義する。テンプレートは書かないのでtsファイルに書く。</p> <pre><code class="typescript">import { Vue, Component } from 'vue-property-decorator' @Component export default class BaseEditor extends Vue { isiPhone() { if (window.navigator.userAgent.match(/iPhone/)) { return true } return false } } </code></pre> <h2 id="継承する"><a href="#%E7%B6%99%E6%89%BF%E3%81%99%E3%82%8B">継承する</a></h2> <p>あとは継承して使うだけ。</p> <pre><code class="html"><script lang="ts"> import { Vue, Component, Prop } from 'vue-property-decorator' import BaseEditor from '../../lib/BaseEditor' @Component export default class BoardEditor extends BaseEditor { </code></pre> <h2 id="Mixinは?"><a href="#Mixin%E3%81%AF%EF%BC%9F">Mixinは?</a></h2> <p>一応Vue.jsにはMixinという仕組みもあるのだが、TypeScriptだと結構面倒っぽい。色々方法はありそうだが、継承したコンポーネント側でもメソッドの定義を書かないといけなかったり、複数のMixinを使うために独自に関数を用意しなければならなかったりと、今のところ厳しそうな感じがある。今後ちゃんと使えるようになるのだろうか…?</p> <h2 id="テンプレートも共通化したい場合は"><a href="#%E3%83%86%E3%83%B3%E3%83%97%E3%83%AC%E3%83%BC%E3%83%88%E3%82%82%E5%85%B1%E9%80%9A%E5%8C%96%E3%81%97%E3%81%9F%E3%81%84%E5%A0%B4%E5%90%88%E3%81%AF">テンプレートも共通化したい場合は</a></h2> <p>テンプレートも使いまわしたい場合は継承するのではなく、共通のコンポーネントを書いてそれを各コンポーネントに配置したほうが良さそう。</p> だら@Crieit開発者