20210906のNode.jsに関する記事は12件です。

OSSのライブラリを作ったので、立ち上げからリリースまでを時系列に書いていく

まえがき ちょうど3年前に「初心者3人でwebサービス(webアプリ)を作ったので、立ち上げからリリースまでを時系列に書いていく」なる記事を書いたことがあり、いまだにいいねをつけてもらえることがあります。そこで、3年の時を経て似たような記事を書いてみようと思いました。 Webアプリの開発経験はあるものの、npmパッケージを公開したことはありませんでした。手探りからどのようにリリースまで、残したドキュメントをもとに振り返っていきたいと思います。 これを読んで「OSSを作ってみたいけどどう進めていけばいいか分からない」という人の一助になれば嬉しいです。誰か0から10までの流れを書いててくれないかな〜と思っていたので、きっと誰かには届く気がします。 作ったOSSについて Realtimelyという、Reactアプリに楽しいリアルタイム要素を簡単に導入できるライブラリを開発しました。 Realtimelyの裏側ではGraphQLとWebsocketでAWSクラウドと通信していますが、プログラマはそれを意識することなくRealtimelyの提供するhooks関数を使うだけでwebsocketベースのリアルタイムな体験を実装することができるようになります。 例えば、下のgifのようにマウスカーソルの位置を共有する機能を10行程度のコードで実現できます。 import { useRealtimeCursor } from 'realtimely'; export default () => { const { onMouseMove, renderCursors } = useRealtimeCursor() return ( <div onMouseMove={onMouseMove}> {renderCursors()} </div> ) } デモページを作っているので、よかったら触ってみてください。 立ち上げからリリースまで さて、ここからは実際にどのように企画を立ててリリースしていったかを時系列に書いていきたいと思います。 人数 1人 制作日数 18日 かかったお金 AWS無料枠 1日にかけた時間 平日1時間 休日3時間 8/19(開始から0日目) 計画 初日に決めたことは以下になります。 ユースケース → どのような場面でこのライブラリを使うのかイメージ プロダクトイメージ → プログラマがどのようなインターフェイスでこのライブラリを使うかをイメージ アーキテクチャ → 何を使って実現するか。 今回は可能な限り(楽をするため)ローコードでスケーラブルにしたいという思いがありました。 必要なタスク洗い出しとスケジュール 何をやらないといけないか列挙 何にどれくらい時間がかかりそうか見積もり(正直個人開発で見積もりすることに意味はないですが、どれくらいかかりそうか把握するためにやりました) 今回はかなりプロダクトアウト的な発想でユースケースを検討しましたが、実際には以下の手順でやるのが理想かと思います。 ① 実際の開発で困っている課題を考える ② 誰かがすでに実現していないか調べる ③ 実現していたとして、それが本当に便利か、より使いやすいプロダクトイメージがないか考える OSSは誰にでも公開できますが、すでに実現されていたり、さほど困っていないことをベースにしたものを作って公開しても検索の邪魔になるプロダクトができてしまう恐れがあります。 8/20 (開始から1日目) 競合(?)調査 自分がやりたいことをやれそうなライブラリを調べ、ドキュメントを読み、実際に使ってみました。 そのライブラリは「パフォーマンスの高いリアルタイムアプリ」を作るためにRESTベースで作られたライブラリでした。 高いパフォーマンスが目的だったのでキャッシュ等を利用した状態管理も機能の中に入っておりtoo muchであることや、バックエンドもフルスクラッチで開発しなければならず簡単に使えるものではないことがわかりました。 ここで自分のプロダクトイメージを手軽に使えるようにするところに尖らせるなど方向修正したりしました。 また方向性は違えど、実現することは似ているので参考にできることがたくさんあります。HTTPレスポンスなども見て、どういったデータ構造が良さそうかなどの参考にしました。 8/21-8/26 (開始から6日目) First iteration 「リアルタイムにカーソルの動きを共有する」というスプリントゴールを最初の7日で実装しました。 AWS AppSyncを触る AppSyncはGraphQLサーバのマネージドサービスです。 DynamoDBのテーブルと接続することで、自動でGraphQLスキーマとリゾルバを生成する機能があります。 今回はローコードでなるべく楽しようと考えていたので、AppSyncを採用してみました。 ノーコード、ローコード系あるあるですが、どうしても限界というかサービスの特徴があり、そこからはみ出すと一気に実装が難しくなります。AppSyncもその例に漏れず、AppSyncの特徴を捉えながら設計をする必要がありました。 触ってみて感じたことはこんな感じ。 自分で作ってないものは挙動を理解するのに時間がかかる → DynamoDBを手動で更新してもSubscriptionが更新してくれなくてハテナとなった → DynamoDBを監視しているわけではなくMutaitonを監視していた カスタムロジックを書こうとしたらVTLを書かないといけない → 基本的なコードはネットから拾えるが、自分で一からVTLでロジックを書くのはしんどすぎる できることの範囲に限界があり、追加要件が走った瞬間に破綻するのではないかという不安から実務で採用は難しそう。 自動生成されたリゾルバに勝手にロジックが書かれていてハマる → 自動生成系は何が実装されているか分からない 不可思議な挙動にも身体で慣れていくしかない。このノウハウにあまり再利用性がない。 AppSyncでweb socket通信をするためのハンドシェイクプロトコルが独特 Subscriptionの挙動が奇妙(StackOverflow) 設計する 「リアルタイムにカーソルを移動させる」を実装させるために必要なAPIとフロントでの処理を書き出し、実現できそうか検討していきました。この辺は「実現したいこと」と「AppSyncでできること」を両睨みして考えていく形でした。 ユーザがURL(Host + Path)にアクセス 誰かが入室したら通知(onCreateRealtimeUser) 誰かの所在更新を通知(onUpdateRealtimeUser) → 所在が古いものはローカルで削除する そのURLへアクセスしていることを報告(CreateRealtimeUser) カーソルの移動に合わせて位置を報告(createRealtimeCursor, updateRealtimeCursor) 他の人がカーソル移動したら通知(onUpdateRealtimeCursor) 自分が存在していることをpoke (updateRealtimeUser) DB設計 DynamoDBはキーバリューストアのスケーラブルなNoSQLデータベースです。 DynamoDBのキーはHashKeyとSortKeyがあり、この組み合わせが一意であるようにDB設計を行います。 RealtimeCursorは次のようなスキーマにしました。 HashKey SortKey URL#{URL} UserId#{UserId} {URL}と{UserId}には実際の値が入ります。 このキーに対して、マウスのカーソル位置(X,Y)やユーザ情報をAttributeとして保存していきます。 高速実装 僕は実装は何よりもまず最初はスピードだけを意識してやります。 どれだけ汚くてもいいので、とにかくやりたいことができればOKです。 なぜかというと綺麗な設計というのは全てを書き終えてからスクラップアンドビルドをするのが一番効率的だと思っているからです。 最初から何が共通化できるかなんて、僕はわかりません。 リファクタリング ざっくりと汚いコードでやりたいことが実現できたらリファクタリングをしました。 Webサービスであれば最悪リファクタリングをしなくても動くものを先にリリースするという判断もありかと思います。 今回はOSSライブラリとして使いやすくなければいけないため、責務とインターフェイスをよく考えて再設計しました。 8/27-8/30 (開始から11日目) Second iteration 「ユーザアクションの可視化を実装する(ボタンを押すと他の人にも押されたように見えるとか、テキストを入力するとそれが他の人の画面に見えるとか)」というスプリントゴールに2回目のイテレーションを回します。 リファクタリングされて整理されているのと、おおよそAppSyncで何ができるかわかっているのでスムーズに開発が進みました。 8/31 (開始から12日目) デモページを作る 開発に使っていたNext.jsプロジェクトを多少整理してそのままデプロイします。 Serverless Frameworkを使うことで5分でAWSにデプロイできました。 (デモページ)[https://d2vfno2gco8009.cloudfront.net/] 9/1-9/4 (開発から16日目) ドキュメントを作る docusaurusというライブラリを使ってドキュメントを作成しました。docusaurusはfacebookがメンテしており、めちゃくちゃよくできてます。 まず、大項目で書くことを列挙してからゴリゴリと書いていきました。 書くことを考える Introduction Getting Started Installation Demo Realtime Cursor Realtime User Presence Realtime User Action API Guides useXXX How it works Architecture Self hosted Backend RoadMap ゴリゴリ書く ゴリゴリ書きます。一番しんどかったところ。 英語にする docusaurusにi18n機能があるので、英語のドキュメントも作成しました。 基本的にgoogle translateに翻訳させて、ざっと見直しするだけです。 Github Pagesにデプロイする これもdocusaurusに機能としてあったので簡単にできました。 その他のタスク ロゴを作る これは適当にロゴgeneratorを活用しました。 寄付の窓口を作る ko-fiを使って寄付の窓口も作ってみました。 Stripeと接続してクレジットカードで寄付ができるようになります。 9/5-6 (開発から18日目) npm公開 いよいよ公開です。詳細なnpmへの公開方法は別の記事を参照ください。今回は荒く書いていきます。 npmアカウントの作成 https://www.npmjs.com/ でアカウントを作成 TypeScriptをコンパイルして.jsファイルにする tsconfig.jsonを書いてから tsc -d yarn publish npmのログインを求められるのでEmail, Passwordを入力するだけ、、 公開 これでnpmに公開することができました。 公開後の姿 めちゃくちゃ簡単でびびります。npmのライブラリって今まで利用させて頂くばかりで、その土俵に僕なんかがあがっていいのか不安になるような、土俵に汚い土足で乗り上げたような、そんな罪悪感がありました。 デバッグ 意気揚々と別リポジトリでrealtimelyをインストールして使ってみると、エラーが出て使えない・・・ この辺のデバッグで1日溶けました。 ライブラリの依存関係 CSS読み込めないエラー 依存しているライブラリのエラー これから 公開へのスピード優先で開発を進めたので、現在は到底Production Readyなライブラリになっていません。あくまでもPoCレベルで、アプリにリアルタイム要素を入れるとどんな感じになるのか確かめて頂く用途で使っていただければと思います。 これからはProduction Readyにするための課題の解決と、より使いやすい機能の開発を細々とやっていこうと思います。温かい目で見守ってください。 Document Demo
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

リアルタイムなWebアプリを簡単に作れるOSSを作ったので、立ち上げからリリースまでを時系列に書いていく

まえがき ちょうど3年前に「初心者3人でwebサービス(webアプリ)を作ったので、立ち上げからリリースまでを時系列に書いていく」なる記事を書いたことがあり、いまだにいいねをつけてもらえることがあります。そこで、3年の時を経て似たような記事を書いてみようと思いました。 Webアプリの開発経験はあるものの、npmパッケージを公開したことはありませんでした。手探りからどのようにリリースまで、残したドキュメントをもとに振り返っていきたいと思います。 これを読んで「OSSを作ってみたいけどどう進めていけばいいか分からない」という人の一助になれば嬉しいです。誰か0から10までの流れを書いててくれないかな〜と思っていたので、きっと誰かには届く気がします。 作ったOSSについて Realtimelyという、Reactアプリに楽しいリアルタイム要素を簡単に導入できるライブラリを開発しました。 Realtimelyの裏側ではGraphQLとWebsocketでAWSクラウドと通信していますが、プログラマはそれを意識することなくRealtimelyの提供するhooks関数を使うだけでwebsocketベースのリアルタイムな体験を実装することができるようになります。 例えば、下のgifのようにマウスカーソルの位置を共有する機能を10行程度のコードで実現できます。 import { useRealtimeCursor } from 'realtimely'; export default () => { const { onMouseMove, renderCursors } = useRealtimeCursor() return ( <div onMouseMove={onMouseMove}> {renderCursors()} </div> ) } デモページを作っているので、よかったら触ってみてください。 立ち上げからリリースまで さて、ここからは実際にどのように企画を立ててリリースしていったかを時系列に書いていきたいと思います。 人数 1人 制作日数 18日 かかったお金 AWS無料枠 1日にかけた時間 平日1時間 休日3時間 8/19(開始から0日目) 計画 初日に決めたことは以下になります。 ユースケース → どのような場面でこのライブラリを使うのかイメージ プロダクトイメージ → プログラマがどのようなインターフェイスでこのライブラリを使うかをイメージ アーキテクチャ → 何を使って実現するか。 今回は可能な限り(楽をするため)ローコードでスケーラブルにしたいという思いがありました。 必要なタスク洗い出しとスケジュール 何をやらないといけないか列挙 何にどれくらい時間がかかりそうか見積もり(正直個人開発で見積もりすることに意味はないですが、どれくらいかかりそうか把握するためにやりました) 今回はかなりプロダクトアウト的な発想でユースケースを検討しましたが、実際には以下の手順でやるのが理想かと思います。 ① 実際の開発で困っている課題を考える ② 誰かがすでに実現していないか調べる ③ 実現していたとして、それが本当に便利か、より使いやすいプロダクトイメージがないか考える OSSは誰にでも公開できますが、すでに実現されていたり、さほど困っていないことをベースにしたものを作って公開しても検索の邪魔になるプロダクトができてしまう恐れがあります。 8/20 (開始から1日目) 競合(?)調査 自分がやりたいことをやれそうなライブラリを調べ、ドキュメントを読み、実際に使ってみました。 そのライブラリは「パフォーマンスの高いリアルタイムアプリ」を作るためにRESTベースで作られたライブラリでした。 高いパフォーマンスが目的だったのでキャッシュ等を利用した状態管理も機能の中に入っておりtoo muchであることや、バックエンドもフルスクラッチで開発しなければならず簡単に使えるものではないことがわかりました。 ここで自分のプロダクトイメージを手軽に使えるようにするところに尖らせるなど方向修正したりしました。 また方向性は違えど、実現することは似ているので参考にできることがたくさんあります。HTTPレスポンスなども見て、どういったデータ構造が良さそうかなどの参考にしました。 8/21-8/26 (開始から6日目) First iteration 「リアルタイムにカーソルの動きを共有する」というスプリントゴールを最初の7日で実装しました。 AWS AppSyncを触る AppSyncはGraphQLサーバのマネージドサービスです。 DynamoDBのテーブルと接続することで、自動でGraphQLスキーマとリゾルバを生成する機能があります。 今回はローコードでなるべく楽しようと考えていたので、AppSyncを採用してみました。 ノーコード、ローコード系あるあるですが、どうしても限界というかサービスの特徴があり、そこからはみ出すと一気に実装が難しくなります。AppSyncもその例に漏れず、AppSyncの特徴を捉えながら設計をする必要がありました。 触ってみて感じたことはこんな感じ。 自分で作ってないものは挙動を理解するのに時間がかかる → DynamoDBを手動で更新してもSubscriptionが更新してくれなくてハテナとなった → DynamoDBを監視しているわけではなくMutaitonを監視していた カスタムロジックを書こうとしたらVTLを書かないといけない → 基本的なコードはネットから拾えるが、自分で一からVTLでロジックを書くのはしんどすぎる できることの範囲に限界があり、追加要件が走った瞬間に破綻するのではないかという不安から実務で採用は難しそう。 自動生成されたリゾルバに勝手にロジックが書かれていてハマる → 自動生成系は何が実装されているか分からない 不可思議な挙動にも身体で慣れていくしかない。このノウハウにあまり再利用性がない。 AppSyncでweb socket通信をするためのハンドシェイクプロトコルが独特 Subscriptionの挙動が奇妙(StackOverflow) 設計する 「リアルタイムにカーソルを移動させる」を実装させるために必要なAPIとフロントでの処理を書き出し、実現できそうか検討していきました。この辺は「実現したいこと」と「AppSyncでできること」を両睨みして考えていく形でした。 ユーザがURL(Host + Path)にアクセス 誰かが入室したら通知(onCreateRealtimeUser) 誰かの所在更新を通知(onUpdateRealtimeUser) → 所在が古いものはローカルで削除する そのURLへアクセスしていることを報告(CreateRealtimeUser) カーソルの移動に合わせて位置を報告(createRealtimeCursor, updateRealtimeCursor) 他の人がカーソル移動したら通知(onUpdateRealtimeCursor) 自分が存在していることをpoke (updateRealtimeUser) DB設計 DynamoDBはキーバリューストアのスケーラブルなNoSQLデータベースです。 DynamoDBのキーはHashKeyとSortKeyがあり、この組み合わせが一意であるようにDB設計を行います。 RealtimeCursorは次のようなスキーマにしました。 HashKey SortKey URL#{URL} UserId#{UserId} {URL}と{UserId}には実際の値が入ります。 このキーに対して、マウスのカーソル位置(X,Y)やユーザ情報をAttributeとして保存していきます。 高速実装 僕は実装は何よりもまず最初はスピードだけを意識してやります。 どれだけ汚くてもいいので、とにかくやりたいことができればOKです。 なぜかというと綺麗な設計というのは全てを書き終えてからスクラップアンドビルドをするのが一番効率的だと思っているからです。 最初から何が共通化できるかなんて、僕はわかりません。 リファクタリング ざっくりと汚いコードでやりたいことが実現できたらリファクタリングをしました。 Webサービスであれば最悪リファクタリングをしなくても動くものを先にリリースするという判断もありかと思います。 今回はOSSライブラリとして使いやすくなければいけないため、責務とインターフェイスをよく考えて再設計しました。 8/27-8/30 (開始から11日目) Second iteration 「ユーザアクションの可視化を実装する(ボタンを押すと他の人にも押されたように見えるとか、テキストを入力するとそれが他の人の画面に見えるとか)」というスプリントゴールに2回目のイテレーションを回します。 リファクタリングされて整理されているのと、おおよそAppSyncで何ができるかわかっているのでスムーズに開発が進みました。 8/31 (開始から12日目) デモページを作る 開発に使っていたNext.jsプロジェクトを多少整理してそのままデプロイします。 Serverless Frameworkを使うことで5分でAWSにデプロイできました。 (デモページ)[https://d2vfno2gco8009.cloudfront.net/] 9/1-9/4 (開発から16日目) ドキュメントを作る docusaurusというライブラリを使ってドキュメントを作成しました。docusaurusはfacebookがメンテしており、めちゃくちゃよくできてます。 まず、大項目で書くことを列挙してからゴリゴリと書いていきました。 書くことを考える Introduction Getting Started Installation Demo Realtime Cursor Realtime User Presence Realtime User Action API Guides useXXX How it works Architecture Self hosted Backend RoadMap ゴリゴリ書く ゴリゴリ書きます。一番しんどかったところ。 英語にする docusaurusにi18n機能があるので、英語のドキュメントも作成しました。 基本的にgoogle translateに翻訳させて、ざっと見直しするだけです。 Github Pagesにデプロイする これもdocusaurusに機能としてあったので簡単にできました。 その他のタスク ロゴを作る これは適当にロゴgeneratorを活用しました。 寄付の窓口を作る ko-fiを使って寄付の窓口も作ってみました。 Stripeと接続してクレジットカードで寄付ができるようになります。 9/5-6 (開発から18日目) npm公開 いよいよ公開です。詳細なnpmへの公開方法は別の記事を参照ください。今回は荒く書いていきます。 npmアカウントの作成 https://www.npmjs.com/ でアカウントを作成 TypeScriptをコンパイルして.jsファイルにする tsconfig.jsonを書いてから tsc -d yarn publish npmのログインを求められるのでEmail, Passwordを入力するだけ、、 公開 これでnpmに公開することができました。 公開後の姿 めちゃくちゃ簡単でびびります。npmのライブラリって今まで利用させて頂くばかりで、その土俵に僕なんかがあがっていいのか不安になるような、土俵に汚い土足で乗り上げたような、そんな罪悪感がありました。 デバッグ 意気揚々と別リポジトリでrealtimelyをインストールして使ってみると、エラーが出て使えない・・・ この辺のデバッグで1日溶けました。 ライブラリの依存関係 CSS読み込めないエラー 依存しているライブラリのエラー これから 公開へのスピード優先で開発を進めたので、現在は到底Production Readyなライブラリになっていません。あくまでもPoCレベルで、アプリにリアルタイム要素を入れるとどんな感じになるのか確かめて頂く用途で使っていただければと思います。 これからはProduction Readyにするための課題の解決と、より使いやすい機能の開発を細々とやっていこうと思います。温かい目で見守ってください。 Document Demo
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Reactをexpressにホストする

やり方を聞かれて調べたのでメモ。 react-router-domでルーティングされているページをbuildしてexpressにホストしたい。 Reactでの作業 とりあえず何でもいいのですが、react-router-domで最低限のルーティングを設定します。 簡易仕様 / → Home.js /about → About.js それ以外だと、Page not found.を表示。 プロジェクトの作成 とりあえずcreate-react-appでプロジェクトを作成。 npx create-react-app react-test cd react-test 最低限のルーティングを設定したいのでreact-router-domのインストール。 npm install react-router-dom Home.jsとAbout.jsを生成。 touch src/Home.s src/About.js Home.js Home.jsの実装。Homeと表示し、About.jsへのリンクがあるだけ。 Home.js import { Link } from "react-router-dom"; const Home = () => { return ( <div> Home<br/> <Link to="/about">Aboutへ</Link> </div> ); } export default Home; About.js Aboutと表示しHome.jsへのリンクがあるだけ。 About.js import { Link } from "react-router-dom"; const About = () => { return ( <div> About<br/> <Link to="/">Homeへ</Link> </div> ); } export default About; App.js ルーティングの設定。/でも/aboutでもなければPage not foundを表示。 App.js import { BrowserRouter, Switch, Route } from "react-router-dom"; import Home from "./Home"; import About from "./About"; const App = () => { return ( <BrowserRouter> <Switch> <Route path="/" exact component={Home} /> <Route path="/about" exact component={About} /> <Route render={()=><p>Page not found.</p>}/> </Switch> </BrowserRouter> ); } export default App; 動作確認 npm startで動作確認。 http://localhost:3000。 npm start build(本番用ファイル生成) 今回の肝はbuildしたファイルがexpressで動くかなのでbuild。 npm run build buildファイルが作成され、中にbuild結果が出力される。index.htmlを始めとする各種ファイルが生成される。 Express側での作業 上記で生成されたファイル群をexpressの静的ファイルホスティングで正常に動作させるのがゴール。 expressのインストールと作業場の作成 expressをインストールし、reactのプロジェクトフォルダ中にnodeフォルダを作る(わけてもいい。めんどいのでそうしているだけ)。そして、index.jsを生成。 npm install express mkdir node touch node/index.js cd node 実装 index.jsを実装していく。 index.js const path = require("path"); const express = require("express"); const app = express(); //ミドルウエアでstaticパスを追加(ただ、これだけだと直アクセスや無いpathだと動かない) app.use(express.static(path.join(__dirname, "..", "build"))); //これを追加(全てをindex.htmlにリダイレクト。いわゆるrewrite設定) app.use((req, res, next) => { res.sendFile(path.join(__dirname, "..", "build", "index.html")); }); app.listen(3001, () => { console.log("server started on port 3001"); }); ポイント 動作のポイントは2つ。 静的ファイルの場所 まず、静的フォルダを設定する。 これだけで実は http://localhsot:3001にアクセスすると動きます。が、これだけでは不十分。 app.use(express.static(path.join(__dirname, "..", "build"))); /を経由せず、ダイレクトに/aboutや/xxxとかにアクセスするとエラーとなる(表示されない or Not foundとならない)。 全てのリクエストをbuild/index.htmlへ これらを処理するには全てのリクエストをindex.htmlに処理させる必要がある。 一般的なWebサーバではRewirte処理などを行うがExpressでは以下のようにする。 app.use((req, res, next) => { res.sendFile(path.join(__dirname, "..", "build", "index.html")); }); 実行 では動作確認してみる。 node index.js 確認
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

新たな力ーJLCPCBの紫のプリント基板

紫のプリント基板今日からロールアウト! いつでも発注できる 新たな色は君たちに刺激を与え創造力を与える。 pcbの変革は今から始まる口を開けた笑顔
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

新たなJLCPCBの紫のプリント基板

紫のプリント基板今日からロールアウト! いつでも発注できる 新たな色は君たちに刺激を与え創造力を与える。 pcbの変革は今から始まる
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

nvmを利用して、Node.jsをセットアップする方法のチートシート

サッとコピペして進められる様にチートシートを作ろうと思いました。 前提条件 Node.jsが入っていること プロジェクトフォルダがある階層でnode -vしてみた時、きちんとバージョンが表示されればOK 表示されない時は,Node.jsを入れよう。 macOS Mojave 多分macならコマンドは基本的に同じだと思う。でも私はbashを使っているので、zshの人は少し違うのかもしれない。 nvmってなんだ nvmはすーぐ新しいバージョンが出てしまうNode.jsのバージョン管理をしてくれるもの。 バージョンの切り替えなんかを簡単にしてくれます。 公式Readmeによると、ユーザごとにインストールされて、シェルごとに呼び出される様に設計されているとのこと。 sh、dash、ksh、zsh、bash、特にUNIX、macOS、WindowsWSLで動作するそうです。 Gitからnvmをインストール インストールスクリプトを使用する方法もある様ですが、注意書きが長くて面倒そうだと思ったので、今回はGithubからインストールする方法で行きます。 cdでプロジェクトフォルダへ移動しましょう。 移動できたら、そこで以下のコマンドを入力。 $ git clone https://github.com/creationix/nvm.git ~/.nvm 今度はパスを通さないとコマンドが使えないので、以下を実行します。 $ source ~/.nvm/nvm.sh 早速、使いたいバージョンのNode.jsを入れます。 $ nvm install 入れたいバージョン番号 以下の様なメッセージが出れば完了です。 Now using node 入れたいバージョン番号 (npm ほにゃらら) Creating default alias: default -> 入れたいバージョン番号
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

node-webrtcでWebRTCを入門

環境 Ubuntu 20.04 (AWS) node v10.19.0 (npm 6.14.4) npm packages wrtc 0.4.7 (node-webrtc) mkdirp 1.0.4 chokidar 3.5.2 dateformat 4.5.1 作ったもの WebRTCのデータチャンネルを使って文字列データのやり取りをするサンプル オファー側、アンサー側、共に node.js で実装、コマンドで実行 P2Pを始めるまでの情報のやり取りをファイルシステム経由で行っている WebRTCでP2P通信をする流れ オファー側とアンサー側の2つあったり、イベントハンドラが複数あったりして混乱しがちだが、 setLocalDescription()、setRemoteDescription()、createOffer()、createAnswer()の4つのメソッドと onicecandidate イベントハンドラ をおさえれば、通信確立の全体像をつかみやすい 以下はフローを図にまとめたもの コード 上記のフロー図について、コードのコメントで処理の内容をまとめる オファー側とアンサー側でP2P通信を確立する部分に焦点を絞った(全体の流れを俯瞰して理解できる)コードになるように努めた イベントハンドラの処理を隠蔽している イベントハンドラなどの処理については、、githubのコードで確認できる オファー側 const RTCPeerConnection = require('wrtc').RTCPeerConnection; const RTCPeerConnectionUtils = require('./libs/RTCPeerConnectionUtils') const DataChannelEventListenerForOffer = require('./libs/DataChannelEventListenerForOffer') const SDPFileForOffer = require('./libs/SDPFileForOffer') const rtcPeerConnection = new RTCPeerConnection({}) const dataChannelEventListenr = new DataChannelEventListenerForOffer("channel", rtcPeerConnection) const sdpFile = new SDPFileForOffer(); (async () => { console.log("This is OFFERER.") // 通信の確立に焦点を当てているため、データチャンネル関連のイベントを隠蔽 dataChannelEventListenr.listen(); // シグナリングをファイルでやりとりするための前処理 await sdpFile.setUp(); // 経路の候補(ICE Candidates)一覧を取得するプロミスオブジェクトを生成 const candidates = []; const waitAllCandidates = RTCPeerConnectionUtils.createPromiseWaitAllCandidates(rtcPeerConnection, candidates); // オファーを作成 const offer = await rtcPeerConnection.createOffer(); // オファーを設定(設定するとICE Candidatesのイベントが発火する) await rtcPeerConnection.setLocalDescription(offer); // 経路の候補(ICE Candidates)一覧がすべて取得できるまで待つ await waitAllCandidates; // 経路の候補(ICE Candidates)一覧をファイルに書きだす(※1) sdpFile.writeOffer(JSON.stringify(candidates)); // アンサーがファイルに書かれるのを待つ(answer.js側の処理)(※2の処理を待つ) const answer = await sdpFile.waitAnswer(); // アンサー情報を設定(オファーからみてリモート側) rtcPeerConnection.setRemoteDescription(answer[0]); })(); アンサー側 const RTCPeerConnection = require('wrtc').RTCPeerConnection; const RTCPeerConnectionUtils = require('./libs/RTCPeerConnectionUtils') const SDPFileForAnswer = require('./libs/SDPFileForAnswer') const DataChannelEventListenerForAnswer = require('./libs/DataChannelEventListenerForAnswer') const rtcPeerConnection = new RTCPeerConnection({}) const sdpFileForAnswer = new SDPFileForAnswer(); const dataChannelEventListener = new DataChannelEventListenerForAnswer(rtcPeerConnection); (async () => { console.log("This is ANSWERER.") // 通信の確立に焦点を当てているため、データチャンネル関連のイベントを隠蔽 dataChannelEventListener.listen() // シグナリングをファイルでやりとりするための前処理 await sdpFileForAnswer.setUp(); // オファーがファイルに書かれるのを待つ(offer.jsの処理)(※1の処理を待つ) const offer = await sdpFileForAnswer.waitOffer(); // オファー情報を設定(アンサーからみてリモート側) await rtcPeerConnection.setRemoteDescription(offer[0]); // 経路の候補(ICE Candidates)一覧を取得するプロミスオブジェクトを生成 const candidates = []; const waitAllCandidates = RTCPeerConnectionUtils.createPromiseWaitAllCandidates(rtcPeerConnection, candidates); // アンサーを作成 const answer = await rtcPeerConnection.createAnswer(); // アンサーを設定(設定するとICE Candidatesのイベントが発火する) await rtcPeerConnection.setLocalDescription(answer); // 経路の候補(ICE Candidates)一覧がすべて取得できるまで待つ await waitAllCandidates; // アンサーをファイルに書く(※2) sdpFileForAnswer.writeAnswer(JSON.stringify(candidates)); })(); ソースコード
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AzureにNode.jsから画像を保存する

AzureにNode.jsから画像を保存する 準備編の続きですが、Azure Blob Storageに画像をアップロード、逆にダウンロードしてローカルに保存を試してみます。 まずは利用する画像を用意してみました。n0bisuke.jpg 前回試したコードでhelloという文字列を送っていたので、そのコードをそのまま使ってbase64変換して送ってみます。 とりあえず、Blob Storage内での概念として、コンテナ=フォルダ的な概念、blob=ファイル的な概念でコンテナ内に作成されると現状思っています。 画像をアップロード uploadimage.js 'use strict'; const { BlobServiceClient, StorageSharedKeyCredential, ContainerClient } = require("@azure/storage-blob"); //ファイルを利用 const fs = require(`fs`); const FILE_PATH = `n0bisuke.jpg`; //画像ファイルをbase64文字列へ const image_data = fs.readFileSync(FILE_PATH); const base64_data = "data:image/jpeg;base64," + image_data.toString('base64'); async function main() { // Enter your storage account name and shared key const account = process.env.ACCOUNT_NAME || "ストレージアカウント名"; const accountKey = process.env.ACCOUNT_KEY || "キー"; const sharedKeyCredential = new StorageSharedKeyCredential(account, accountKey); const blobServiceClient = new BlobServiceClient( `https://${account}.blob.core.windows.net`, sharedKeyCredential ); //コンテナを作成した名前に指定 const containerName = `n0bisuke-container1630888214169`; const containerClient = blobServiceClient.getContainerClient(containerName); // Create a blob const content = base64_data; //変換したbase64文字列を使う const blobName = "newblob" + new Date().getTime(); const blockBlobClient = containerClient.getBlockBlobClient(blobName); const uploadBlobResponse = await blockBlobClient.upload(content, Buffer.byteLength(content)); console.log(`Upload block blob ${blobName} successfully`, uploadBlobResponse.requestId); } main().catch((err) => console.error("Error running sample:", err.message)); $ node uploadimage.js Upload block blob newblob1630891763317 successfully 37db3c29-201e-004e-52c6-a28313000000 newblob1630894950109というblob名でアップロードされました。 ポータル上でも確認できました。6.93KiBということでしっかり保存されてそうです。 画像をダウンロードして保存 今後は逆にDLを試してみます。テキストだけでは仕方ないので画像ファイルとして保存する処理まで書いていきます。 参考: Node.jsで画像をダウンロードして保存する(axios利用) 参考: [Node.js] Base64エンコードされたファイルデータをデコードして、S3にputObjectする この辺りが参考になりました。 先程アップロードしたnewblob1630891763317のblob名を直指定で使います。 公式サンプルにあるstreamToBuffer()を利用して文字列にエンコードして、base64文字列からデータの中身を取り出してwriteFileSync()でファイル保存しています。 dl.js 'use strict'; const { BlobServiceClient, StorageSharedKeyCredential, ContainerClient } = require("@azure/storage-blob"); const fs = require(`fs`); const FILE_PATH = `n0bisuke.jpg`; async function main() { // Enter your storage account name and shared key const account = process.env.ACCOUNT_NAME || "ストレージアカウント名"; const accountKey = process.env.ACCOUNT_KEY || "キー"; const sharedKeyCredential = new StorageSharedKeyCredential(account, accountKey); const blobServiceClient = new BlobServiceClient( `https://${account}.blob.core.windows.net`, sharedKeyCredential ); //コンテナを作成した名前に指定 const containerName = `n0bisuke-container1630888214169`; const containerClient = blobServiceClient.getContainerClient(containerName); //blobにアクセス const targetBlobName = `newblob1630891763317`; const blockBlobClient = containerClient.getBlockBlobClient(targetBlobName); //blobからデータをダウンロード const downloadBlockBlobResponse = await blockBlobClient.download(0); //データを文字列に const encodedData = (await streamToBuffer(downloadBlockBlobResponse.readableStreamBody)).toString(); //今回は元が画像ファイルだったのでbase64形式のテキストがDLされるので中身を抽出 const fileData = encodedData.replace(/^data:\w+\/\w+;base64,/, ''); //ローカルファイルに書き込み fs.writeFileSync(`./dl/${FILE_PATH}`, fileData, 'base64'); } // A helper method used to read a Node.js readable stream into a Buffer async function streamToBuffer(readableStream) { return new Promise((resolve, reject) => { const chunks = []; readableStream.on("data", (data) => { chunks.push(data instanceof Buffer ? data : Buffer.from(data)); }); readableStream.on("end", () => { resolve(Buffer.concat(chunks)); }); readableStream.on("error", reject); }); } main().catch((err) => console.error("Error running sample:", err.message)); 実行すると... 無事に保存された! まとめ "画像をAzure Blob Storageにアップロードとダウンロードする"をNode.jsから試してみました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AzureにNode.jsから画像を保存したりDLしたりする

何かと画像を保存したいってシーンは出てきますよね。 AzureにNode.jsから画像を保存する 準備編の続きですが、Azure Blob Storageに画像をアップロード、逆にダウンロードしてローカルに保存を試してみます。 とりあえず、Blob Storage内での概念として、コンテナ=フォルダ的な概念、blob=ファイル的な概念でコンテナ内に作成されると現状思っています。 画像用意など まずは利用する画像を用意してみました。n0bisuke.jpg 前回試したコードでhelloという文字列を送っていたので、そのコードをそのまま使ってbase64変換して送ってみます。 画像をアップロード uploadimage.js 'use strict'; const { BlobServiceClient, StorageSharedKeyCredential, ContainerClient } = require("@azure/storage-blob"); //ファイルを利用 const fs = require(`fs`); const FILE_PATH = `n0bisuke.jpg`; //画像ファイルをbase64文字列へ const image_data = fs.readFileSync(FILE_PATH); const base64_data = "data:image/jpeg;base64," + image_data.toString('base64'); async function main() { // Enter your storage account name and shared key const account = process.env.ACCOUNT_NAME || "ストレージアカウント名"; const accountKey = process.env.ACCOUNT_KEY || "キー"; const sharedKeyCredential = new StorageSharedKeyCredential(account, accountKey); const blobServiceClient = new BlobServiceClient( `https://${account}.blob.core.windows.net`, sharedKeyCredential ); //コンテナを作成した名前に指定 const containerName = `n0bisuke-container1630888214169`; const containerClient = blobServiceClient.getContainerClient(containerName); // Create a blob const content = base64_data; //変換したbase64文字列を使う const blobName = "newblob" + new Date().getTime(); const blockBlobClient = containerClient.getBlockBlobClient(blobName); const uploadBlobResponse = await blockBlobClient.upload(content, Buffer.byteLength(content)); console.log(`Upload block blob ${blobName} successfully`, uploadBlobResponse.requestId); } main().catch((err) => console.error("Error running sample:", err.message)); $ node uploadimage.js Upload block blob newblob1630891763317 successfully 37db3c29-201e-004e-52c6-a28313000000 newblob1630894950109というblob名でアップロードされました。 ポータル上でも確認できました。6.93KiBということでしっかり保存されてそうです。 画像をダウンロードして保存 今後は逆にDLを試してみます。テキストだけでは仕方ないので画像ファイルとして保存する処理まで書いていきます。 参考: Node.jsで画像をダウンロードして保存する(axios利用) 参考: [Node.js] Base64エンコードされたファイルデータをデコードして、S3にputObjectする この辺りが参考になりました。 先程アップロードしたnewblob1630891763317のblob名を直指定で使います。 公式サンプルにあるstreamToBuffer()を利用して文字列にエンコードして、base64文字列からデータの中身を取り出してwriteFileSync()でファイル保存しています。 dl.js 'use strict'; const { BlobServiceClient, StorageSharedKeyCredential, ContainerClient } = require("@azure/storage-blob"); const fs = require(`fs`); const FILE_PATH = `n0bisuke.jpg`; async function main() { // Enter your storage account name and shared key const account = process.env.ACCOUNT_NAME || "ストレージアカウント名"; const accountKey = process.env.ACCOUNT_KEY || "キー"; const sharedKeyCredential = new StorageSharedKeyCredential(account, accountKey); const blobServiceClient = new BlobServiceClient( `https://${account}.blob.core.windows.net`, sharedKeyCredential ); //コンテナを作成した名前に指定 const containerName = `n0bisuke-container1630888214169`; const containerClient = blobServiceClient.getContainerClient(containerName); //blobにアクセス const targetBlobName = `newblob1630891763317`; const blockBlobClient = containerClient.getBlockBlobClient(targetBlobName); //blobからデータをダウンロード const downloadBlockBlobResponse = await blockBlobClient.download(0); //データを文字列に const encodedData = (await streamToBuffer(downloadBlockBlobResponse.readableStreamBody)).toString(); //今回は元が画像ファイルだったのでbase64形式のテキストがDLされるので中身を抽出 const fileData = encodedData.replace(/^data:\w+\/\w+;base64,/, ''); //ローカルファイルに書き込み fs.writeFileSync(`./dl/${FILE_PATH}`, fileData, 'base64'); } // A helper method used to read a Node.js readable stream into a Buffer async function streamToBuffer(readableStream) { return new Promise((resolve, reject) => { const chunks = []; readableStream.on("data", (data) => { chunks.push(data instanceof Buffer ? data : Buffer.from(data)); }); readableStream.on("end", () => { resolve(Buffer.concat(chunks)); }); readableStream.on("error", reject); }); } main().catch((err) => console.error("Error running sample:", err.message)); 実行すると... 無事に保存された! encodedData.replace(/^data:\w+\/\w+;base64,/, '');の処理を忘れると、うまくファイルが表示出来なくなってしまうので注意って感じでした。 まとめ "画像をAzure Blob Storageにアップロードとダウンロードする"をNode.jsから試してみました。 終わってみるとあっさりでしたが、最初が発狂しそうでした。笑
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AzureにNode.jsから画像を保存する 準備編

画像の保存などをやりたいなと思ってAzureでやってみよ〜と思った時の話です。 まずは情報に行き着くまでが大変でした Azureに限らずAWSもGCPもそんな感じだとは思いますが、サービス多いので概念理解をするのに時間がかかるといったやつです。 Azureに画像を保存したいけどBlobストレージとかFilesとか調べるとごちゃごちゃ... って感じです。 調べるとAzureのBlob Storageというサービスが画像とか保存できるっぽい話が出てきます。ただAzure Filesってのも出てきたり、なんかもっと色々出てきます。 少しイラつき。笑 Azule Filesとはとかを調べてみて雰囲気こんなかなぁという印象が Azure Blob Storage => API経由で利用想定 Azure Files => NASとか社内共有フォルダ的な利用想定(APIもある) といった印象。 とりあえずBlob Storageの方を使ってみます。 ポータルからblobって調べるとストレージアカウントというまた新ワードが出てくる 割と分からんけど、これかなぁ、、という感じで進めてみたけど合ってたみたい。 今だによく分かってはないけど使えたのでよしとします。 (きっとこの記事をみた詳しい人が教えてくれるはず!) と言うかうんじでストレージアカウントなるものを作成します。この時の気持ち的にはストレージを使いたいのにアカウント作るのか?という雰囲気。 こんな感じで作ってみました。 プレミアムって表記もありますが、とりあえずStandardで。 作成してリソースグループに移動するとこんな感じの画面になります。 Azure Blob StorageをNode.jsから利用する とりあえずリソースは作ったっぽいけど、どう使うんだということで調べてたらSDKを見つけました。 Azure/azure-sdk-for-jsこちらのサンプルを触ってみます。 どのキーを使うのか basic.jsを見ると、ACCOUNT_NAMEとACCOUNT_KEYを入れたら使えるっぽい雰囲気があるので調べてみます。 どうやらこれらっぽいですね。キーの表示を押すとキーが表示されます。 basic.jsを試してみました。 $ npm i @azure/storage-blob dotenvは使わないので11行目は削除して試します。 $ node basic.js Create container newcontainer1630887559036 successfully e5e72525-101e-0037-7eb4-a27f37000000 Upload block blob newblob1630887559208 successfully e5e72597-101e-0037-6bb4-a27f37000000 Blob 1: newblob1630887559208 Downloaded blob content hello deleted container ほうほう... サンプルコードで何行われてたかコード読まずに実行(危ないw)したけどどうやら、コンテナってのを作る -> データをアップロードする -> データのダウンロードをする -> コンテナを消す みたいな雰囲気で処理をとりあえず実行しましたよってサンプルっぽいですね。 とりあえずsuccessっぽい挙動なのでとりあえずキーなどは合ってた模様。 Azure Blob Storageにコンテナを作成する とりあえず先程のコードだとコンテナ作成して削除してまでをやってるので、とりあえずコンテナ作成部分だけ抜き出して見ました。 名前もn0bisuke-container~~的な名前にしてみます。 cc.js 'use strict'; const { BlobServiceClient, StorageSharedKeyCredential } = require("@azure/storage-blob"); async function main() { // Enter your storage account name and shared key const account = process.env.ACCOUNT_NAME || "ストレージアカウント名"; const accountKey = process.env.ACCOUNT_KEY || "キー"; const sharedKeyCredential = new StorageSharedKeyCredential(account, accountKey); const blobServiceClient = new BlobServiceClient( `https://${account}.blob.core.windows.net`, sharedKeyCredential ); //コンテナの作成 const containerName = `n0bisuke-container${new Date().getTime()}`; const containerClient = blobServiceClient.getContainerClient(containerName); const createContainerResponse = await containerClient.create(); console.log(`Create container ${containerName} successfully`, createContainerResponse.requestId); } main().catch((err) => console.error("Error running sample:", err.message)); 実行してみます。 $ node cc.js Create container n0bisuke-container1630888214169 successfully 947cbed6-101e-0027-7bb6-a2ba5f000000 お、成功したっぽい。 管理画面を見ると指定した名前のコンテナーが表示されてました。 まだコンテナーが何なのか分かってないですが多分大枠のフォルダみたいな概念だと思っておきます。 作成したコンテナにアクセス cc.jsを一部変更してshow.jsを作ってみました。 show.js 省略 //コンテナを作成した名前に指定 const containerName = `n0bisuke-container1630888214169`; const containerClient = blobServiceClient.getContainerClient(containerName); console.log(containerClient); 省略 実行するとコンテナの情報が表示されました。 $ node show.js ContainerClient { url: 'https://xxxxxxxxxxxx.blob.core.windows.net/n0bisuke-container1630888214169', accountName: 'xxxxxxxxxxxxxx', pipeline: Pipeline { factories: [ [Object], 省略 コンテナにデータをアップロードしてみる この辺までくると割とイメージ掴めてきました。 basic.jsにあったコードを持ってきてアップロード出来ました。 basic.jsで既にやってた処理ですけどね。 参考: https://github.com/Azure/azure-sdk-for-js/blob/main/sdk/storage/storage-blob/samples/javascript/basic.js#L62-L66 upload.js 省略 //コンテナを作成した名前に指定 const containerName = `n0bisuke-container1630888214169`; const containerClient = blobServiceClient.getContainerClient(containerName); // Create a blob const content = "hello"; const blobName = "newblob" + new Date().getTime(); const blockBlobClient = containerClient.getBlockBlobClient(blobName); const uploadBlobResponse = await blockBlobClient.upload(content, Buffer.byteLength(content)); console.log(`Upload block blob ${blobName} successfully`, uploadBlobResponse.requestId); 省略 実行するとこんな感じでポータル上にも表示されます。 何か出来てますね。このNode.jsのコード見る限りだとhelloって文字列が入ってそうです。 5Bって書いてるしそれっぽいですね。 いよいよ画像を投稿 長くなってきたのと、この辺は分かってる人は見なくてもよさそうということで、記事を分けて次回に続く...
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

手を動かして GraphQL を順番に理解していく

はじめに 最終的には AWS AppSync を使いたいため、なんとなく GraphQL や AppSync の使い方を流し読みしたが、やっぱり実際に使ってみないとよくわからない。 そこで、まずは第一歩としてローカルに Apollo サーバー を立てて、手作業でクエリを発行したりしながら GraphQL の理解を試みる。 なお、この記事では「GraphQLのメリットや良さ」については取り扱わない。 簡単なサンプルを動かしながら GraphQL を使う手順の理解をすることに注力する。 GraphQL とは Wikipedia では GraphQL(グラフQL)はAPI向けに作られたクエリ言語およびランタイムである と説明されている。 クエリ言語およびランタイムである、という説明から分かるとおり、複数の項目を含む概念であるため、簡潔に説明するのは難しい。 そこで、GraphQL を理解するために関連項目を以下の通り図で表した。 単に GraphQL とだけ行った場合、この図のすべての範囲、あるいはその一部を示している。  GraphQL は効率良くデータを取得(と変更)するために、利用者と実データの間を結ぶ実装を提供するもの と理解するのが良いと思う。 一般的なデータベースサービス (RDBMS, つまりは Oracle Database とか MySQL とかのサーバー側) と比較すると、RDBMS は右側の黄色い枠全体を包括していて、データの保存にもサービス独自の規格が用いられている。 SQL や DDL によって、データベースおよびそこで管理されるデータの操作を行うことができる。 この時、利用者・DB管理者の視点では、データ操作のために SQL などを記載する必要があるが、書かれた SQL に対してどのようにデータを操作するかの具体的なプログラムを書くことはない。 一方、GraphQL のサービスではこれを分離できる。 その代わり、外部から与えられたコマンドに対して、どのような処理を行ったデータを返すかというマッピングというプログラム実装を resolver として行う必要がある。 この記事では、GraphQL Service として Apollo を利用する。 説明ではデータを分離しているが、GraphQL Service の Resolver で固定値、あるいはサーバーのメモリ上の値を返すように実装すれば、Dataset 部分を作らなくても GraphQL Service を動かすことができる。 利用した環境とバージョンなど OS: Ubuntu 20.04 LTS Node.js: v15.8.0 yarn: 1.22.10 apollo-server: 2.25.2 graphql: 15.5.1 Apollo セットアップと Hello World 最初は Github の README を参考にセットアップする。 $ cd [プロジェクトディレクトリ] $ yarn init # ... 手を動かすだけなので今回は yarn init の入力内容は適当に $ yarn add apollo-server graphql # サーバーサイドスクリプトを記載 $ vim index.js index.js も README に書かれている内容をそのまま保存する。 index.js const { ApolloServer, gql } = require('apollo-server'); // The GraphQL schema const typeDefs = gql` type Query { "A simple type for getting started!" hello: String } `; // A map of functions which return data for the schema. const resolvers = { Query: { hello: () => 'world', }, }; const server = new ApolloServer({ typeDefs, resolvers, }); server.listen().then(({ url }) => { console.log(`? Server ready at ${url}`); }); これを記述した後、ローカルでサーバーを立ち上げてから http://localhost:4000 にアクセスする。 $ node index.js すると、以下のようなコンソールが立ち上がる。 ここで左側に GraphQL のクエリを入力して中央の実行ボタンを押せば、右側にその結果が出てくる。 ここではクエリを投げて、GraphQL からのレスポンスを受け取ってみる。 query { hello } というクエリを発行すると、 { "data": { "hello": "world" } } というレスポンスが返ってくる。  これで、まずは最初のステップが実施できた。 ここでは query { hello } と書いているが、以下で説明されている通り条件を満たす場合は query は省略できる(以後、適時省略を用いる)。 Specifying the query keyword and an operation name is only required when a GraphQL document defines multiple operations. We therefore could have written the previous query with the query shorthand: https://github.com/graphql/graphql-spec/blob/main/README.md から引用 GraphQLの CRUD 操作 データを追加してからクエリを発行する index.js を以下の通り修正して、簡単なユーザーデータを取得できるようにする。 index.js // The GraphQL schema const typeDefs = gql` type User { name: String! age: Int! } type Query { hello: String user(name: String!): User users: [User] } `; const users = [ { name: 'Sample User1', age: 20 }, { name: 'Sample User2', age: 30 }, { name: 'Sample User3', age: 40 }, ] // A map of functions which return data for the schema. const resolvers = { Query: { hello: () => 'world', user: (parent, args, context, info) => { return users.find(u => u.name === args.name); }, users: () => users, }, }; typeDefs にデータ型 User を定義し、この User 型の値を取得できるようにしている。 ここで gql を使っていて、この構文を全く見たことがなかったのだが、テンプレートリテラルの「タグ付きテンプレート」と言うらしい。 仕組み的には関数 fx を fx(args) ではなく fx`args` という形で書いても呼び出すことができる。 ただし後半の `args` 部分はテンプレート文字列。 なので、 fx`my name is ${name}` などの形で文字列を渡す時のみ利用できる。 より詳細な説明は以下を参考。 実際にクエリを発行してみる。 入力1 { hello } 出力1 { "data": { "hello": "world" } } 入力2 { users { name, age } } 出力2 { "data": { "users": [ { "name": "Sample User1", "age": 20 }, { "name": "Sample User2", "age": 30 }, { "name": "Sample User3", "age": 40 } ] } } 入力3 { user(name: "Sample User1") { age } } 出力3 { "data": { "user": { "age": 20 } } } このように、新しく定義した type Query 内に定義した users や user を利用できている。 ところで、SQL になれている人間からすると、例えば User の全項目を取得したい場合に逐一フィールドを記述したくないと考えると思う。 そのため、以下のようなクエリを打ちたくなる。 ダメな例1 # 全ユーザーの全属性を取得したい { users } ダメな例2 # 全ユーザーの全属性を取得したい { users { * } } しかし、これらのクエリは期待通りの値を返さない。 例1の場合 Field \"users\" of type \"[User]\" must have a selection of subfields. Did you mean \"users { ... }\"?" というエラーメッセージが返される。 エラーメッセージに記載がある通り、子要素(属性)を持つ値(プリミティブではない値)を問い合わせる場合、明確にどの属性が必要かを問い合わせ時に明記する必要がある ( hello はプリミティブな文字列を返すため、{ hello } は有効)。 例1が失敗するなら、例2のようにワイルドカードで表記を簡略化したいと考える私と同じ思考の人もいるだろう。 しかし、これは構文エラーとなる。 ワイルドカードの導入は GraphQL 実装では議論されてはいるが予定はないそうだ。 後述する Fragment を利用することである程度の共通化は可能だが、その場合も必要な項目を一度は記載する必要がある。 条件付きのデータを取得する 先の例3では、name が一致するユーザーを取得した。 もちろん、これも条件付きのデータ取得( name が完全一致するユーザーを取得する条件付きのデータ取得) なのだが、もっと複雑な条件を持つデータ、例えば「年齢が一定以下/以上のユーザーを取得する」にはどうすれば良いだろうか? 最も簡単なのは「利用者側ですべて取得して、利用者側でフィルターをかけて利用するデータを取捨選択する」だが、データ通信量が増えてしまうため好ましくない。 そのため、こういった場合は検索条件用のデータ構造を作るなどの方法を取る。 ここでは input UserAgeFilter が検索用のデータ構造になる。 type UserAgeFilter のように type で定義したものは出力用の型定義であり、input で定義したものは入力用の型定義である。 Query の引数として input は利用できるが type は利用できない。 // The GraphQL schema const typeDefs = gql` input UserAgeFilter { gt: Int lt: Int } type User { name: String! age: Int! } type Query { hello: String users(name: String, ageFilter: UserAgeFilter): [User] } `; const users = [ { name: 'Sample User1', age: 20 }, { name: 'Sample User2', age: 30 }, { name: 'Sample User3', age: 40 }, ] // A map of functions which return data for the schema. const resolvers = { Query: { hello: () => 'world', users: (parent, args, context, info) => { let us = users; if (args.name) { us = us.filter(u => u.name === args.name); } if (args.ageFilter) { if (typeof args.ageFilter.gt !== 'undefined') { us = us.filter(u => u.age > args.ageFilter.gt); } if (typeof args.ageFilter.lt !== 'undefined') { us = us.filter(u => u.age < args.ageFilter.lt); } } return us; }, }, }; コードを見れば分かるが、users に渡す引数の name と ageFilter を使ってデータのフィルタリングを実施している。 users の利用例は以下の通り。 年齢が29歳より大きいユーザーを取得するクエリ { users (ageFilter: {gt: 29}) { name, age } } # 取得結果 # { # "data": { # "users": [ # { # "name": "Sample User2", # "age": 30 # }, # { # "name": "Sample User3", # "age": 40 # } # ] # } # } 名前が一致し、年齢が19歳より大きく、38歳未満のユーザーを取得するクエリ { users (name: "Sample User1", ageFilter: {gt: 19, lt: 38}) { name, age } } # 取得結果 # { # "data": { # "users": [ # { # "name": "Sample User1", # "age": 20 # } # ] # } # } 結論としては、入力に合わせて適切なデータを取得して返す resolver を実装する ということになる。 ここでは簡易的に lt , gt のみを実装したが、例えば lte や gte の実装や、文字列を渡して解析し、その結果によってフィルタをかけるような汎用実装を行うことも考えられる。 データの変更 (Create / Update / Delete) これまではデータの取得 (Read) に焦点を当ててきたが、CRUD 操作の Read 以外であるデータの生成・更新・削除についてをどのようにすれば実現できるのかを見ていく。 データの変更には GraphQL の mutation を利用する。 ここではデータを追加する場合の Mutation である add を実装する。 // The GraphQL schema const typeDefs = gql` type User { name: String! age: Int! } type Query { users: [User] } type Mutation { add(name: String!, age: Int!): String } `; const users = [ { name: 'Sample User1', age: 20 }, { name: 'Sample User2', age: 30 }, { name: 'Sample User3', age: 40 }, ] // A map of functions which return data for the schema. const resolvers = { Query: { users: (parent, args, context, info) => { return users; }, }, Mutation: { add: (parent, args, context) => { const u = { name: args.name, age: args.age }; users.push(u); return `added ${u.name}`; } } }; add する場合の mutation 発行方法は以下の通り。 query の場合は省略可、と先に説明したが mutation は省略不可なので、問い合わせ文は mutation から始める。 データ追加の問い合わせ mutation { add(name: "Hoge", age: 10) } データ追加後の結果 { "data": { "add": "added Hoge" } } この状態で users を取得すると、データの末尾に {name: "Hoge", age: 10} となるユーザーが追加されている。 query { users { name, age } } # { # "data": { # "users": [ # { # "name": "Sample User1", # "age": 20 # }, # { # "name": "Sample User2", # "age": 30 # }, # { # "name": "Sample User3", # "age": 40 # }, # { # "name": "Hoge", # "age": 10 # } # ] # } # } 今回の実装では User データは永続化されていないので、アプリケーションを再起動すると追加したデータは元に戻る。 しかし、このデータを外部のファイルやデータベースなどに保存し永続化すれば、アプリケーションを再起動しても追加したデータは残り続ける。 更新・削除の場合も mutation の resolver 実装が異なるだけでやり方は同じ。 発展的なデータの取得方法 ここでは 実際に GraphQL で遭遇しそうな、少し複雑な問い合わせについてどうなるかを見ていく。 1回のクエリで複数のデータを取得する 問い合わせ query 内に複数の query を列挙する場合、それらを1回のリクエストで取得できる。 複数のクエリの同時発行 query { hello, users { name }, users { age } } 3クエリの結果を得られるが、usersの結果がマージされている { "data": { "hello": "world", "users": [ { "name": "Sample User1", "age": 20 }, { "name": "Sample User2", "age": 30 }, { "name": "Sample User3", "age": 40 } ] } } 戻り値のフィールド名を指定する (エイリアス) ここまで、戻り値の結果は data というオブジェクト内にあるクエリ名と同じフィールドに設定されていた。 しかし、GraphQLではフィールド名を呼び出し側が指定することができる。 GraphQLの文脈ではこれを Alias と呼ぶ。 具体的な利用例は以下の通り。 呼び出し例 { names: users { AreYou: name }, ages: users { Age: age } } aliasとしてnames,agesがdata内のフィールドとして設定されている { "data": { "names": [ { "AreYou": "Sample User1" }, { "AreYou": "Sample User2" }, { "AreYou": "Sample User3" } ], "ages": [ { "Age": 20 }, { "Age": 30 }, { "Age": 40 } ] } } 同様の方法で得られるフィールド内部の値まで別のエイリアスを貼ることもできる。 利用方法としては、findUser というクエリがあった場合、loginUser: findUser(...) のようにして利用者が都合の良い名前を設定することができるため、利用者側での命名の自由が増える。 同一型の値を取得する (Fragment) ここでは以下のようなクエリを考える。 { user: user (name: "Sample User1") { name, age }, bestFriend: user(name: "Sample User2") {name, age} } # { # "data": { # "user": { # "name": "Sample User1", # "age": 20 # }, # "bestFriend": { # "name": "Sample User2", # "age": 30 # } # } # } ここでは同一の user という resolver から値を取得しているのだが、その戻り値の指定に name , age と記載している。 ここでは2個なのでまだよいかもしれないが、フィールドと項目が増えた場合、同じフィールドを取得しようとしているかどうかがひと目で分からないし、もし修正があった場合は大変になる。 こういった場合の共通化の方法として Fragment と呼ばれる方法が用意されている。 上記のクエリは以下のように置き換えられる。 fragmentを利用して引数の共通化を行う(結果は同じなので省略) { user: user (name: "Sample User1") { ...userField }, bestFriend: user(name: "Sample User2") { ...userField } } fragment userField on User { name, age } クエリ変数と変数展開 これまでクエリの呼び出し時には具体的な値を含むクエリ (例: user(name: "Sample User1") ) を発行してきた。 しかし、クライアントアプリケーションからクエリ呼び出しを行おうとしてユーザーの入力を元に 単純連結で クエリを作ると、以下のような問題が生じる。 入力次第でクエリが成立しなくなる 入力内容により既存の構文を改ざんし 関係ないクエリ/ミューテーションが発行できる可能性がある (=SQL Injectionと同じようなことが起こる) これらの解消方法として、GraphQL では「構文」と「変数」を分割して問い合わせを行う方法がある。 構文内には $ から始まる変数を定義し、実行時に同時に渡した変数を展開してくれる。 SQLに馴染みのある人にはプレースホルダ構文(変数が入る場所に $? などを埋め込み、関数実行時にSQLのテンプレートとは別に展開用の引数を渡す仕組み)だと思えば分かりやすいと思う。 Apollo Server で実施する場合、画面の左下に Query Variables を入力するところがあり、ここには JSON 形式で入力する。 上記コンソール内に書かれている内容を改めて書くと、 具体的な値を書かず、変数化したクエリ query ($name: String!){ user(name: $name) { name, age } } クエリ呼び出し時に渡される変数ディクショナリ { "name": "Sample User1" } 呼び出し結果 { "data": { "user": { "name": "Sample User1", "age": 20 } } } データ型の再帰定義について GraphQL では型の再帰定義はできない。 そのため、例えば以下の friends のようにある型の中に循環依存を作ることはできない。 再帰定義は不可なのでこの定義は失敗する type User { name: String! age: Int! friends: [User] } まとめ Apollo の resolver 実装を行い、実際にクエリを発行しながら CRUD 操作の実装の方法および実践的な構文 (Fragrment, Variables) についてを説明した。 実際に手を動かすことで、基本的な GraphQL の利用・実装方法は理解できたように思う。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Nest.jsでGuardからControllerへデータを渡す方法

Nest.jsでは公式ドキュメントにも書いてあるとおり、Guardを作成する際には CanActivateインターフェースを継承する canActivateメソッド中でbooleanを返却する という実装をすることで対象のリクエストの呼び出し可否をコントロールするというのが基本的な使い方です。 しかし、実務で色々なGuardを実装していると、Guardで取得したデータをControllerでも使いたくなるケースに時々出くわします。 GuardとControllerとで同じデータを取得する処理を重複して実装するのは無駄なので、今回はこのGuard内で取得したデータをControllerへと渡す実装をしてみようと思います。 1. Guardのベースとなる抽象化クラス「BaseGuard」を実装する 公式ドキュメントのPassportの実装を参考にします。 認証用モジュールである@nestjs/passportを利用する際には、Guard内のhandleRequestメソッドの中で任意の検証をしつつ、ユーザー情報を返却するような実装をする必要があります。 例えば、公式ドキュメントにあるjwtの認証を行うGuardでは、下記の様な実装をしています。 jwt-auth.guard.ts import { ExecutionContext, Injectable, UnauthorizedException, } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; @Injectable() export class JwtAuthGuard extends AuthGuard('jwt') { canActivate(context: ExecutionContext) { // Add your custom authentication logic here // for example, call super.logIn(request) to establish a session. return super.canActivate(context); } handleRequest(err, user, info) { // You can throw an exception based on either "info" or "err" arguments if (err || !user) { throw err || new UnauthorizedException(); } return user; } } このJwtAuthGuardを対象のControllerにセットすると、requestからuserの情報が取得できる様になります。 このuserの値が、先ほどJwtAuthGuardのhandleRequestメソッドで返却していたユーザーの情報になります。 import { Controller, Request, Post, UseGuards } from '@nestjs/common'; import { JwtAuthGuard } from './auth/jwt-auth.guard'; @Controller() export class AppController { @UseGuards(JwtAuthGuard) @Post('auth/login') async login(@Request() req) { return req.user; } } これと似たような実装をすべくAuthGuardの実装を参考にしてみます。 AuthGuardでは、canActivateに以下のような実装をしています。 https://github.com/nestjs/passport/blob/master/lib/auth.guard.ts async canActivate(context: ExecutionContext): Promise<boolean> { const options = { ...defaultOptions, ...this.options, ...await this.getAuthenticateOptions(context) }; const [request, response] = [ this.getRequest(context), this.getResponse(context) ]; const passportFn = createPassportContext(request, response); const user = await passportFn( type || this.options.defaultStrategy, options, (err, user, info, status) => this.handleRequest(err, user, info, context, status) ); request[options.property || defaultOptions.property] = user; return true; } これはつまり、handleRequestをcanActivate内で呼び出し、handleRequestの戻り値をrequestに設定しているだけということです。 この実装を踏まえ、下記のようなベースとなる抽象化クラスを実装します。 import { CanActivate, Injectable, ExecutionContext } from '@nestjs/common'; import { GqlExecutionContext } from '@nestjs/graphql'; @Injectable() export abstract class BaseGuard implements CanActivate { async canActivate(context: ExecutionContext): Promise<boolean> { const data = await this.handleRequest(context); const request = context.switchToHttp().getRequest(); request['guard'] = data; return !!data; } abstract handleRequest(context: ExecutionContext): any; } BaseGuardでは抽象メソッドhandleRequestを用意し、その戻り値をcanActivateメソッド内でguardという名称でrequestに格納しています。 2. Guardからのデータを取得するデコレーターを実装する。 BaseGuardのhandleRequestメソッドでrequestに格納された値を取得するデコレーターを作成します。 request['guard']からデータを取得するだけなので、下記の様な実装になります。 import { createParamDecorator, ExecutionContext } from '@nestjs/common'; export const GuardResponse = createParamDecorator((property: string, context: ExecutionContext) => { const request = context.switchToHttp().getRequest(); const data = request['guard']; return property ? data?.[property] : data; }); 3. BaseGuardを継承したGuardを実装する サンプルとして、Httpヘッダーにdemo-idというデータが存在するかチェックをするGuardを作成します。 import { Injectable, ExecutionContext } from '@nestjs/common'; import { HttpException, HttpStatus } from '@nestjs/common'; import { Demo, DemoDocument } from '../schemas/demo.schema'; import { BaseGuard } from './base.guard'; @Injectable() export class DemoIdGuard extends BaseGuard { constructor( @InjectModel(Demo.name) private readonly demoModel: Model<DemoDocument>, ) { super(); } async handleRequest(context: ExecutionContext): Promise<Demo> { const { headers } = context.switchToHttp().getRequest(); const { 'demo-id': demoId } = headers; if (!demoId) { throw new HttpException('http header must have a "demo-id" property', HttpStatus.BAD_REQUEST); } return await this.demoModel.findOne({ _id: demoId }).exec(); } } このGuardではヘッダーから取得したdemo-idの値をkeyに、handleRequestでDemoというデータを取得してreturnしています。 Controllerではこのreturnした値を取得することができます。 4. 作成したGuardとデコレーターを使用したAPIを作成する 作成したGuardとデコレーターを使用し、Guardより取得した値を返却するだけのGET APIを作成します。 import { Controller, Get } from '@nestjs/common'; import { GuardResponse } from '../decorators'; import { DemoIdGuard } from '../guards'; import { Demo } from '../schemas/demo.schema'; @Controller('demo') export class DemoController { @UseGuards(DemoIdGuard) @Get() find(@GuardResponse() demo: Demo): Demo { return demo; } } まとめ 今回はGuardで取得した値をControllerで返却するだけのシンプルな実装であったため、GuardからControllerに値を渡すメリットは薄かったですが、Guardで複雑なバリデーションを実施し、その結果取得した値をControllerでも使いたい場合には、2度同じデータを取得する必要がなく、またGuardとContorollerとで役割を分割するNest.jsらしい実装も守ることができるので、とても見通しの良い実装ができると思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む