- 投稿日:2019-08-22T17:38:14+09:00
チャットガチャにテーマガチャを実装したときの話
技術でこの世から寂しさを消すことは可能か?
匿名でランダムな相手と1対1のチャットが気軽に楽しめるwebサービスを開発し運営している。
だが、いくら寂しいからといって誰とも分からない相手と話すことが可能だろうか?
どちらかが話題を振り、相手が答えるというのは何気に難しい。
的外れなことを聞いてしまえば、それだけで切断されることもあるだろう。
その問題を解決するべく、テーマガチャというお題をシステム側が提供する機能を実装した。どうやって実装するか
スマホからのアクセスが多いと予想している。
スマホからみた画面
この画面に「お題を出すボタン」を追加するのは正直しんどい。
デザインが崩れるし、テーマガチャなんぞ使わずに会話したい人達にとって邪魔でしょうがない。ゆえにこういう感じにした。
実装済み画面
フォームに「@話題」とだけ入れて送信すると話題が表示される。
無駄なアニメーションはない。
話題が盛り上がらなさそうなら、何度でも送信するとお題をくれる。
お題は全部で100近くある(数えてない)。少しドキッとするお題も入っている。
これによって「永久に」暇をつぶすことが可能になった。
あえて、同じお題を連続で出すようにしている。(重複されることも稀にある)
システム側がボケることで匿名の二人に共通の敵のようなものが誕生し仲良くなれるかもしれない。ダメなUI
最悪なのはフォームの横幅を小さくして「お題ボタン」を追加すること。
さらに、ボタンを押したらルーレットのようなものが表示され無駄なアニメーションがでてしまうこと。
そういうことは、二人の会話にとって邪魔でしょうがない。
チャットをしにきているのであって、ルーレットを楽しみにしにきているわけではない。
また、テーマガチャを追加したことを知らない実装する前に遊びに来ていたユーザーもそのままの形で使えるようにしておいたほうがいい。
コロコロとUIが変更されるのは、ストレスになるだろうとも思う。既に稼働しているシステムにコードを追加する話
こういったファイルを1つ新規で作成する。(javascriptだがnode.jsなのでサーバー側)
theme.jsconst wadai_array = new Array( "最近見た映画", "YouTube", "ハマったゲーム", "アニメ", "好きな漫画", "旅行の思い出", "友達", "最近読んだ本", "好きな食べ物" //これがずらずらと100件ぐらい続く。 //もしもシリーズや きのこVSたけのこのようなVSテーマもある } exports.func = function(){ return wadai_array[Math.floor(Math.random() * wadai_array.length)]; };上のファイルを読み込む
app.js//トークテーマを送信 socket.on('theme_make', function(data) { var f = require('./theme.js'); //実際は圧縮されているので.min.jsになる io.sockets.in(data.roomid).emit("theme_get", f.func()); });このファイルを「@話題」と送信されたときに呼び出すようにすると簡単に実装できた。
何事も工夫次第
今回は話題を提供するテーマガチャを追加した。
@クイズと送信すればクイズがランダムに出てくるようにも作れるだろう。
良かった点は、フォームと送信を使って新機能を追加できたこと。
無駄なUIを作らなくて良かったことと、ルーレットのような無駄な演出を入れなくてよかったこと。
無駄なUIを作ってしまうと次の新機能のときに困ることになる。
見えないところで新しい何かが実装できるとシンプルで良いと思った。
また、実装するプログラムも当然だが別ファイルをつくって読み込ませるといい。
新しいお題を追加するときは、お題が書かれているファイル(theme.js)だけを開けばいいからだ。
プログラムもデザインも全て工夫なんだなと改めて思った。おわり
テーマガチャがあれば無限に話すことができると思うので一度遊んで見て欲しい。
効果音をONにすれば、LINEのようにポン!ポン!と音が出る。
かなり気持ちいいサービスになっていると思う。さて、技術でこの世から寂しさを消すことは可能なのだろうか?
チャットガチャ
https://chatgacha.com/
https://twitter.com/ryuuga_h
- 投稿日:2019-08-22T15:14:37+09:00
PCの画面をリアルタイム配信 WebRTC 1日目
きっかけ
以前に私がベースを作って友人にソースごと渡したアプリケーションについて、友人から勉強のために色々教えてくれと電話がきた。
「ほにゃららってパッケージはこういうことができてー」
「何行目から何行目はこういうことをしてー」
「何行目で定義している配列はこういう時のためにデータを保存しておくためでー」
なんだかんだ 1 時間半かかった。画面共有で相手の PC を見ながらならさぞ早かったであろう。
画面共有とまで行かずとも相手の PC 画面さえ見られれば簡単だと思い、世の中に出回る素敵なアプリケーションに目もくれず自分で作れないか調べたところ WebRTC を発見。
最低限の機能をとりあえず作ってみる。開発環境
- macOS Mojave
- Node v12.7.0
- npm 6.10.0
- vscode
最終的に作るもの
- PC 画面を配信
- 他の PC で配信されている動画を受信する
とりあえず PC 画面を取得したい
まずはこれができないとお話にならないので、PC 画面を取得し video タグで再生までをやってみた。
PC 画面を video タグで再生するコード
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Test</title> <style> html, body { padding: 0; margin: 0; } .v { width: 100%; } </style> </head> <body> <video class="v" autoplay></video> <script> const init = async () => { /** * video取得 * @type {HTMLVideoElement} */ const video = document.querySelector('.v') try { // PC画面のstream取得 const stream = await navigator.mediaDevices.getDisplayMedia({ video: true }) // streamをvideoにつなげる video.srcObject = stream } catch (e) { return } } init() </script> </body> </html>案外簡単にいけました。
この html にアクセスすると「どの画面にする?全体?特定のウィンドウ?」って聞かれます。
着目すべき点は以下の部分。const stream = await navigator.mediaDevices.getDisplayMedia({ video: true })いままで web サイトとか作ってても出会ったことがない
navigatorさんが登場しました。
ちょっとこれを調べます。Navigator インターフェース
Navigator インターフェイスは、ユーザーエージェントの状態や身元情報を表します。スクリプトからその情報を問い合わる、および活動を続けるためにそれら自体を登録することができます。
Navigator オブジェクトは、読み取り専用の window.navigator プロパティを使用して取得できます。
位置情報とか OS とか、ブラウザの外側にある情報にアクセスできる的な?
IE の頃からあったらしい。全然知らなかった。
バッテリー情報も見れるらしいですよ。
ただ、ブラウザによって実装が結構ぐちゃぐちゃみたい。navigator.mediaDevices (MediaDevices オブジェクト)
Navigator.mediaDevices - Web API | MDN
さて、Navigator がどんなものかちょっと知ったところでお次は
navigator.mediaDevicesですね。
これは MediaDevices オブジェクトを返すらしいです。MediaDevices はカメラ映像・マイクの音声・PC 画面映像とかメディア入力装置を紹介してくれる案内役っぽいですね。
例えば
navigator.mediaDevices.getUserMedia(constraints)とすればconstraintsで指定したメディア入力装置からのストリームが取れるそうです。メディア入力装置の指定ってどうやるねんって思ったらちゃんとありました。
mediaDevices.enumerateDevices()で PC に接続されている全てのメディア入力装置を取得でき、その中から指定したい装置の ID を使用するそうです。
constraintsは他にも映像のサイズ(width, height)とかも設定できるそうで。ブラウザってそういうのできるんだなーってびっくりしました(無知
navigator.mediaDevices.getDisplayMedia(constraints)
先ほどは
navigator.mediaDevices.getUserMedia(constraints)の説明をしましたが、
今回使用するのはnavigator.mediaDevices.getDisplayMedia(constraints)です。これは PC 画面のストリームを取得できるものです。
constraintsにどの画面にするか、あるいはどのウィンドウにするかを設定できます。
ウィンドウ単位の映像までとれるなんて!
ちなみに指定しなかったらブラウザがどの画面にする?って聞いてくれます。
音声はまだとれないっぽい?ですね。さっきからストリームストリームって言ってるけど何それ?って方へ
navigator.mediaDevices.getUserMedia(constraints)やnavigator.mediaDevices.getDisplayMedia(constraints)で得られるオブジェクトは MediaStream というもので、ざっくりいうと映像・音声データのことです。これを video タグに渡せば映像をブラウザ内で見れます。
配信したい
配信するには WebRTC っていう技術?API?を利用するそうです。
WebRTC - Wikipedia
Web RTC API - Web API | MDN根幹には Peer to Peer(以下 P2P)が使われているそうです。
P2P はその昔 Winny で一躍有名になったことだけは覚えています。せっかくなので P2P についてちょっと調べてみました。
P2P
Web 界隈の通信というとサーバー・クライアントの通信を自然と考えます。
サーバー・クライアント方式で
- サーバーが 1 つあり、そこに複数のクライアントがアクセスしてサーバーとやりとりする。
- クライアント同士でやりとりするにはサーバーに仲介をお願いする
のようにクライアントは基本的にサーバーとしかやりとりしない中央集権型の通信方式です。
しかし P2P は地方分散型の通信方式で、それぞれがサーバーになったりクライアントになったりします。
例えば
- A さんがとあるファイルを要求した場合、それを持っている B さんがサーバー役になってファイルを供給する
- また別のファイルを C さんが要求した場合、A さんが偶然持っていたのでサーバー役になってくれる
みたいな感じだそうです。
P2P についてだけで余裕で本が書けるし、ブロックチェーンだとかそういったものにも使用されていて
この界隈は今けっこう盛り上がってるそうです。
IPv6 が主流になって各端末がそれぞれ IP を持つようになったら爆発的に機能しそうだなーっと素人目で見てました。さて、問題はどうやってブラウザで P2P 通信をするのかです。
ブラウザは基本的にただのクライアントアプリケーションでしかないと思っていたので、実装方法が想像もつきません。ということでそれを可能にしている WebRTC の技術を見てみます。
WebRTC
P2P を用いてブラウザ間でのデータのやりとりをする仕組みだそうで。
以下の 2 つが重要らしいです。
- SessionDescriptionProtocol(SDP)
- Interactive Connectivity Establishment(ICE)
SDP はデータの情報とか IP アドレスとかポート番号が書いてある文字列だそうです。
ICE は通信経路情報だそうです。SDP をブラウザ間で交換して、お互い ICE を追加すれば通信できる経路が見つかってコネクションが成立するそうです。
ようわからんので、頑張って雰囲気をつかみたいところです。
通信の雰囲気をつかむ
まずは通信をしたいブラウザ同士でお互いのことを知る必要がありそうです。
IP アドレスとかポート番号とかですね。
PC が自分の IP アドレスとして認識しているのはルーターが割り当てるプライベート IP アドレスです。
(ブラウザ A の PC: 192.168.1.2、ブラウザ B の PC: 192.168.10.5 みたいな)
こんなものを知ったところで相手にデータを送ることはできなさそうです。これを解決する方法はルーターの外側にいる人から教えてもらうのが手っ取り早いです。
ブラウザ A がルーターの外側の人に自分がどう見えるかを要求すれば、
192.168.1.10(プライベート IP) > ルーター(NAT) > 123.123.123.123(グローバル IP) > 外側の人
通信は適当な空いているポートを開いてレスポンスを受け取れるようにしているのでポートもわかります。
外側の人から「君は 123.123.123.123 でポート 60000 番って見えるよ!」って教えてもらえます。通常 http はステートレスなのでデータの受信が終了したらこのポートを閉じてしまいますが、これを開けっぱにしておけば通信ができそうなことはわかります。
ブラウザ B でも同様にして外側の人から「君は 111.111.111.111 でポート 55555 番って見えるよ!」と教えてもらいます。
この外側の人のことを STUN サーバーと呼ぶそうです。
実際にはそこにたどり着くまでの ICE を STUN サーバーは教えてくれるそうです。こんなような情報をお互い交換したら、P2P 通信ができそうな雰囲気はつかめました。
でもおかしくないです?
通信を可能にするための情報の交換はどうやるんですかね?シグナリングサーバー
P2P 通信が確立する前段階の通信をサポートするサーバーだそうです。
WebRTC のためにはサーバークライアント方式でデータをやりとりする必要が出てきちゃうそうです。上述の SDP をやりとりします。
これにうってつけなのが WebSocket です。
リアルタイム双方向通信でブラウザ同士の SDP なり ICE なりを相互に送りあってもらいましょう。処理の流れを考える
配信側 受信側 使う API PC 画面のストリームを取得しておく MediaStream 配信要求を送信する 配信要求を受ける コネクションを作成する RTCPeerConnection コネクションにストリームを設定する RTCPeerConnection.addTrack() コネクションのオファーを作成する RTCPeerConnection.createOffer() オファーを自身に設定する RTCPeerConnection.setLocalDescription() STUN サーバーへのアクセス開始 ICE の候補(ICE candidate)を順次取得する RTCPeerConnection.onicecandidate ICE candidate を全て取得したら SessionDescription を送信 オファーを受ける コネクションを作成する RTCPeerConnection コネクションの通信先としてオファーを設定する RTCPeerConnection.setRemoteDescription() コネクションのアンサーを作成する RTCPeerConnection.createAnswer() アンサーを自身に設定する RTCPeerConnection.setLocalDescription() STUN サーバーへのアクセス開始 ICE の候補(ICE candidate)を順次取得する RTCPeerConnection.onicecandidate ICE candidate を全て取得したら SessionDescription を送信 アンサーを受ける コネクションの通信先としてアンサーを設定する RTCPeerConnection.setRemoteDescription() こんな感じでやっていきます
実装開始!
出来上がりを git にアップいたしました。
kotazuck/webrtc-testシグナリングサーバー
/** * シグナリングサーバー(WebSocketサーバー) + Webサーバー */ // Express const express = require('express') const app = express() // publicディレクトリを公開 app.use(express.static(__dirname + '/public')) const http = require('http') const server = http.createServer(app) // WebSocketサーバーにはsocket.ioを採用 const io = require('socket.io')(server) // 接続要求 io.on('connect', socket => { console.log('io', 'connect') console.log('io', 'socket: ', socket.id) // 受信側からの配信要求を配信側へ渡す socket.on('request', () => socket.broadcast.emit('request', { cid: socket.id }) ) // 配信側からのオファーを受信側へ渡す socket.on('offer', ({ offer }) => { socket.broadcast.emit('offer', { offer }) // 配信側の接続が切れた場合にそれを受信側へ通知する socket.on('disconnect', () => socket.broadcast.emit('close')) }) // 受信側からのアンサーを配信側へ渡す socket.on('answer', ({ answer }) => socket.broadcast.emit('answer', { cid: socket.id, answer }) ) }) server.listen(55555)配信側
;(async () => { // シグナリングサーバーであるWebSocketサーバーに接続 // 今回はsocket.ioを採用 const socket = require('socket.io-client')('http://localhost:55555') /** * RTCPeerConnectionをクライアントごとに格納する変数 * keyをクライアントID(ソケットID)として保存する */ const connections = {} /** * PC映像streamを取得 * @type {MediaStream} */ const stream = await navigator.mediaDevices.getDisplayMedia({ video: true }) // ソケットサーバー疎通確認 socket.on('connect', () => console.log('socket', 'connected')) // 配信要求を受ける // Client ID (cid)を受け取りコネクションを作成する socket.on('request', ({ cid }) => sendOffer(cid)) // アンサーを受ける socket.on('answer', async ({ cid, answer }) => { if (cid in connections) connections[cid].setRemoteDescription(answer) }) /** * オファーを送信する * * @param {string} cid Client ID * @return {void} */ async function sendOffer(cid) { // コネクションの設定 const pcConfig = { // STUNサーバーはGoogle様のものを利用させていただく iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] } // コネクションの作成 const peer = new RTCPeerConnection(pcConfig) // cidをキーとしてコネクションを保存 connections[cid] = peer // コネクションにストリームを設定 stream.getTracks().forEach(track => peer.addTrack(track, stream)) // ICE candidateを取得イベントハンドラ peer.onicecandidate = evt => { // evt.candidateがnullならICE Candidateを全て取得したとみなしてオファーを送信 if (!evt.candidate) socket.emit('offer', { offer: peer.localDescription, cid }) } // オファーを作成 const offer = await peer.createOffer() // オファーを自身に設定 // STUNサーバーへアクセスが始まり、onicecandidateが呼ばれるようになる await peer.setLocalDescription(offer) } })()受信側
;() => { // シグナリングサーバーであるWebSocketサーバーに接続 // 今回はsocket.ioを採用 const socket = require('socket.io-client')('http://localhost:55555') /** * @type {HTMLVideoElement} */ const video = document.querySelector('video') video.addEventListener('click', evt => { if (video.paused) video.play() else video.pause() }) /** * コネクションを保存しておく用 * * @type {RTCPeerConnection} */ let connection = null // ソケット接続で配信要求する socket.on('connect', () => socket.emit('request')) // アンサーを受ける socket.on('offer', async ({ offer }) => sendAnswer(offer)) // closeがきたらコネクションを切ってvideoも止める socket.on('close', () => { if (connection) { video.pause() video.srcObject = null connection.close() connection = null } }) /** * アンサーを送信する * * @param {RTCSessionDescription} offer * @return {void} */ async function sendAnswer(offer) { // コネクションの設定 const pcConfig = { // STUNサーバーはGoogle様のものを利用させていただく iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] } // コネクションの作成 const peer = new RTCPeerConnection(pcConfig) // コネクションを保存 connection = peer // 配信イベントハンドラ的な? peer.ontrack = evt => { console.log('ontrack') // streamを設定 video.srcObject = evt.streams[0] } // ICE candidateを取得イベントハンドラ peer.onicecandidate = evt => { // evt.candidateがnullならICE Candidateを全て取得したとみなしてアンサーを送信 if (!evt.candidate) socket.emit('answer', { answer: peer.localDescription }) } // コネクションの通信先としてオファーを設定 await peer.setRemoteDescription(offer) // アンサーを作成 const answer = await peer.createAnswer() // アンサーを自身に設定 await peer.setLocalDescription(answer) } }やっていることは全てコメントに書いているからお分かりかとは思います。
感想
無事、画面の配信が完了しました。
案外と手軽なサイズ感でできました。ちなみに今回のシグナリングは Vanilla ICE という方式を用いています。
- Vanilla ICE
- 全ての ICE Candidate(経路情報の候補)を取得してから SDP の交換を行う方式
- 多分こっちの方が記述がスッキリします
- バニラアイスって読み方でいいのかな
- Trikle ICE
- 先に初期状態の SDP を交換し、ICE を取得するたびにそれを送信し追加していきます。
- 接続できる経路が見つかった時点で接続されるので、Vanilla ICE より早く繋がるらしいです。
- 記述が複雑というか面倒っぽいです
配信・受信でコネクション周りは共通した処理もあるので、まとめちゃえばもっと簡単かも。
WebSocket で room 分ければ簡単な生配信くらいはできそう。
受信側の ontrack で video.play()が効かなかったのが悲しみ。oncanplay でやったりしてみたけどダメでした。
なんでだっけ?前にも引っかかったような気がする。あとは文字のチャット機能、画面配信+マイク音声にして声をいれたりすればそこそこ面白いんじゃね?
P2P ってサーバー負荷をあまり考えなくて良いのが幸せ。
2 日目は何をやろう
- 投稿日:2019-08-22T10:34:53+09:00
できるエンジニアはconsole.log()を上手く使いこなしている。(初心者向け)
はじめに
できるエンジニアはconsole.log()を使いこなしているとのこと。
未経験から上がったような人向けです。
console.log()の配置場所、中身の書き方など参考すべき記事やアドバイスありましたら教えてください。個人でのやりやすさが肝だと思うのでいろんな人の意見を聞きたいです。
node.jsを触っていて、フロントはさっぱりです。デバック時のconsole.logの書き方
// DBからデータ取得時にはエラーが起きにくいので、データ取得の関数の後に`console.log()`を。 // DB更新時のエラーは起きやすいから、DB変更の関数の前後にconsole.log()を仕込む。 result1 = getDate(); console.log('getDate: ' result); console.log('addDate'); result2 = addDate(); console.log('addedDate: ' result2); //簡潔な書き方 console.log(`checkdata: ${result}`) → console.log('checkdata: ', result) // 変数の型がわかる。 console.log(type of 変数) //stringなど。 //result1,2みたいな書き方は意図が読めず関数としては最悪なので実際には書かないでください。
- 投稿日:2019-08-22T09:15:08+09:00
package.json について概要だったり、プロパティだったり
package.jsonってなんだ?となったので、備忘録。随時更新します。
関連用語
- Node.js・・・サーバサイドで動く javascript
- npm・・・javascript のモジュールを管理するツール。ruby でいう bundler のようなもの。
- yarn・・・npm と同じ javascript のモジュールを管理するツール。npm よりインストール速度が早いらしい
概要
パッケージの依存関係を記した json ファイル。rails で使用する Gemfile のようなもの
作り方
npm init でカレントディレクトリに作成される。
結果
カレントディレクトリ jstest で npm init を打って、何も入力せずに Enter 連打すると以下のようになる
コマンド結果$ npm init ・・・略・・・ name: (jstest) version: (1.0.0) description: entry point: (index.js) test command: git repository: keywords: author: /package.json: { "name": "jstest", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC" } Is this ok? (yes) yespackage.json{ "name": "jstest", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC" }中身
name
パッケージ名。必須。name と version で一意にする。他のライブラリと被ったらダメ
"name": "jstest"version
パッケージのバージョン。必須。パッケージ更新時は version も更新する
"version": "1.0.0"description
パッケージについての説明
"description": ""main
パッケージ内で最初に呼ばれるモジュール。今回で言えば、パッケージ jstest を require した時に、index.js 内で export しているオブジェクトが返る
"main": "index.js"scripts
シェルスクリプト、エイリアスコマンドを指定できる
test、start などの予約語を key 名に指定した場合、npm key 名でコマンド実行できる。(他の予約語はなにがあるか分からない・・・)"scripts": { "test": "echo test" }予約語以外を key 名に指定した場合、npm run key 名でコマンド実行できる。devDependencies にモジュール指定しておけば、PATH が自動的に通る
"scripts": { "webpack": "node_modules/.bin/webpack -d" }と、書かなくてもモジュール指定しておけば以下のように書ける
"scripts": { "webpack": "webpack -d" } "devDependencies": { "webpack": "^4.34.0" }author
著者を一人書く。プロジェクト開発ではあまり気にしなくていいかも
author: "Barney Rubble <b@rubble.com> (http://barnyrubble.tumblr.com/)"license
パッケージのライセンス。これも気にしなくていいかも。ISC はゆるいライセンスで、コピー、改変、配布などを許可するものらしい
"license": "ISC"参考
- 投稿日:2019-08-22T09:15:08+09:00
package.json について概要だったり、詳細だったり
package.jsonってなんだ?となったので、備忘録。随時更新します。
関連用語
- Node.js・・・サーバサイドで動く javascript
- npm・・・javascript のモジュールを管理するツール。ruby でいう bundler のようなもの。
- yarn・・・npm と同じ javascript のモジュールを管理するツール。npm よりインストール速度が早いらしい
概要
パッケージの依存関係を記した json ファイル。rails で使用する Gemfile のようなもの
作り方
npm init でカレントディレクトリに作成される。
結果
カレントディレクトリ jstest で npm init を打って、何も入力せずに Enter 連打すると以下のようになる
コマンド結果$ npm init ・・・略・・・ name: (jstest) version: (1.0.0) description: entry point: (index.js) test command: git repository: keywords: author: /package.json: { "name": "jstest", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC" } Is this ok? (yes) yespackage.json{ "name": "jstest", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC" }中身
name
パッケージ名。必須。name と version で一意にする。他のライブラリと被ったらダメ
"name": "jstest"version
パッケージのバージョン。必須。パッケージ更新時は version も更新する
"version": "1.0.0"description
パッケージについての説明
"description": ""main
パッケージ内で最初に呼ばれるモジュール。今回で言えば、パッケージ jstest を require した時に、index.js 内で export しているオブジェクトが返る
"main": "index.js"scripts
シェルスクリプト、エイリアスコマンドを指定できる
test、start などの予約語を key 名に指定した場合、npm key 名でコマンド実行できる。(他の予約語はなにがあるか分からない・・・)"scripts": { "test": "echo test" }予約語以外を key 名に指定した場合、npm run key 名でコマンド実行できる。devDependencies にモジュール指定しておけば、PATH が自動的に通る
"scripts": { "webpack": "node_modules/.bin/webpack -d" }と、書かなくてもモジュール指定しておけば以下のように書ける
"scripts": { "webpack": "webpack -d" } "devDependencies": { "webpack": "^4.34.0" }author
著者を一人書く。プロジェクト開発ではあまり気にしなくていいかも
author: "Barney Rubble <b@rubble.com> (http://barnyrubble.tumblr.com/)"license
パッケージのライセンス。これも気にしなくていいかも。ISC はゆるいライセンスで、コピー、改変、配布などを許可するものらしい
"license": "ISC"参考
- 投稿日:2019-08-22T08:02:11+09:00
5分で組み上げる DB ドリブン REST API コール
5分で組み上げる DB ドリブン REST API コール
はじめに
API を呼び出す際、下記のように DB 中のデータとパラメータ設定を連動させるようなロジックを組み上げることよくあると思います。
- DB へクエリを投げる
- DB から取得した値を RESTful API のパラメータにセットして呼び出す
最近、API を使うことが多々あり、どうしても冗長化してしまう同ロジックをなんとか簡潔にできないかと
調べていたところ、RapidQL なる DB のクエリと RESTful API の呼び出しをチェーンできるライブラリを見つけました。
使ってみたら確かに DB, RESTful API 呼び出しの連動を楽にしてくれました。
今回はこれを使って遊んでみた結果を記事共有したいと思います。
ある程度試行錯誤して慣れた後ではありますが、本当に5分くらいでコーディングできました!RapidQL とは
こちらの公式サイトを見ますとデータベースや RESTful Web API のクエリを1つにまとめることができるクエリ言語のようです。
REST API コールにおいては GraphQL のように任意のフィールドのみ取得するよう指定もできます。
SQL のようなクエリ言語ですので、基本的にはそれ単体で言語にはなっており、埋め込み SQL 文のようにアプリケーションの処理を行う親プログラム中に埋め込んで使用します。
現行は、Node.js のみのサポートになりますが、今後は Python 等、他の言語へも埋め込めるようにするようです。
RapidQL で書かれたクエリは親の Node.js の実行時に RapidQL の実装が RapidQL のクエリをハンドルするという仕組みのようです。
埋め込み SQL でプリコンパイル --> コンパイルとやるところを内部で全て実行してくれているようなイメージだと思います。RapidQL で DB 連動 API 呼び出しロジックを組み上げてみる
シナリオ
顧客 DB に登録された Email アドレスを API のパラメータにセットし、API 呼び出しをする。ここでは、ZeroBounce というベンダーのメールバウンスチェック(メールアドレスの生存状態をチェックする API)を呼び出してみます。(無料枠を利用)
この一連の流れを1クエリで簡潔に書いてみます。
イメージとしては下図のような感じです。
事前準備
API をサブスクライブ
- Rakuten RapidAPI へ登録(無料)
- Zerobounce の BASIC プランをサブスクライブ(月100コールまで無料で使用可)
RapidQL をインストール
$ npm install rapidql : + rapidql@0.0.6 added 514 packages in 8.628s $DB を用意
今回は Posgresql を利用。RapidQL としては、Posgresql 以外に MySQL や Redis もサポートしているもようです。
DB がインストールされていない場合は、こちら等を参考。
今回用意した postgres 環境はこちらです。
DB 名:postgres
Scheme 名: public
ここに下記のような DDL で customers テーブルを作成し上図のようなデータを入れ込みました。
(桁数や中身は基本的には何でも構いません。)CREATE TABLE public.customers ( customer_id integer NOT NULL, customer_name character varying(50), email character varying(255)" )DB呼び出しロジックを組み上げる
プログラムを用意
rapidqltest.jsconst RapidQL = require('RapidQL'); //RapidQL のインスタンス化と同時にDB 接続情報を指定 const rql = new RapidQL({ PostgreSQL: { postgres: { user: 'postgres', database: 'postgres', password: 'postgres', host: 'localhost', port: '5432' } } }); //WHERE 句で渡す顧客ID const wkCustId = 2222; //RapidQL rql.query( `{ PostgreSQL.postgres.public.customers.find(customer_id: {"=": customerId}){ email } }`, { "customerId": wkCustId } ) .then((res) => console.log(JSON.stringify(res))) .catch((err) => console.log(err));実行してみる
$ node rapidqltest.js {"PostgreSQL.postgres.public.users.find":[{"email":"muto@moonsault.com"}]} $API呼び出しロジック取得する
- ZeroBounce API のページへ移動
- アカウントに紐づいた自分の API キーを確認
- email 欄に何か入れて API の挙動を確認
- 「コードスニペット」から「RapidQL」を確認 イメージとしてはこんな感じです。
![]()
API呼び出しロジックを加えてみる
前のステップで確認したコードスニペットを DB クエリした RapidQL クエリにチェーンしてみます。
チェーンは下記のように親のクエリ(ここでは postgresql へのクエリ)から取得する項目にハイフンを付けてクエリを追加するようです。<中略> PostgreSQL.postgres.public.customers.find(customer_id: {"=": customerId){ email, - Http.get( url:"https://zerobounce1.p.rapidapi.com/v2/validate", headers : { <中略>クエリパラメータを動的に埋め込みたいは下記のように param セクションを追加して指定できるようです。
<中略> - Http.get( url:"https://zerobounce1.p.rapidapi.com/v2/validate", headers : { "X-RapidAPI-Host": "zerobounce1.p.rapidapi.com", "X-RapidAPI-Key": rapidKey }, params:{ 'ip_address' : 'dummy', 'email' : email } <中略>この要領でチェーンしてみた最終形がこちらです。
ZeroBounce からは status, sub_status, free_email, smtp_provider のフィールドのみ取得します。rapidqltest.jsconst RapidQL = require('RapidQL'); //RapidQL のインスタンス化と同時にDB 接続情報を指定 const rql = new RapidQL({ PostgreSQL: { postgres: { user: 'postgres', database: 'postgres', password: 'postgres', host: 'localhost', port: '5432' } } }); //WHERE 句に渡す顧客ID const wkCustId = 2222; //API のキー const rakutenRapidAPIKey = '<上で確認した API キー>'; //RapidQL rql.query( `{ PostgreSQL.postgres.public.customers.find(customer_id: {"=": customerId}){ email, - Http.get( url:"https://zerobounce1.p.rapidapi.com/v2/validate", headers : { "X-RapidAPI-Host": "zerobounce1.p.rapidapi.com", "X-RapidAPI-Key": rapidKey }, params:{ 'ip_address' : 'dummy', 'email' : email } ) { status, sub_status, free_email, smtp_provider } } }`, { "rapidKey": rakutenRapidAPIKey, "customerId": wkCustId } ) .then((res) => console.log(JSON.stringify(res))) .catch((err) => console.log(err));実行結果
$ node rapidqltest.js {"PostgreSQL.postgres.public.users.find":[{"email":"muto@moonsault.com","status":"invalid","sub_status":"does_not_accept_mail","free_email":false,"smtp_provider":""}]} $今回は DB からクエリしたのが適当なメールアドレスでしたのでちゃんと invalid と判定してくれています。
ちゃんと生きてるアドレスを入れたら staus=valid と判定してくれました。さいごに
今回やってみて思ったこととしては、もともと GraphQL に詳しいわけではないので記法は少しとっつきにくかったです。
ただ、一般的に複数のメソッドを用意して処理する DB と API のクエリをまとめて1文で書けるのは個人的には画期的でした。
また、いずれのクエリもある程度抽象化でき、慣れてきたら可読性も上がるのかという印象でした。RESTful API 同士のチェーンもこれを使えばできるようなので、次回はそれを試してみたいと思います。



