- 投稿日:2020-03-29T20:50:07+09:00
よく使われるHTTPステータスコードをNode.jsの実例で勉強する
はじめに
簡単なアプリを作りながらNode.jsと基本的なサーバーサイドプログラミングについて勉強していました。
また、ステータスコードの扱いについて本で読んでいると、こういう場合にはこのステータスコードを返せばいいよと書かれてあったのですが、実際のコードでそれをどう表現すればいいのか本だけではイマイチ理解できませんでした。
今回はNode.jsの復習を兼ねて手を動かしながら実例を作ってみました。
対象のステータスコード
200
: OK301
: Moved Permanently303
: See Other400
: Bad Request401
: Unauthorized404
: Not Foundこれ以外のステータスコードについてはまた別の記事でやるかも...?
例題
このような勤怠表を作ります。(見た目素のHTMLですが。。。)
basic認証でのログイン機能と、ログアウト、出勤退勤ボタンを押すと、ユーザ名と勤怠のログが生成される簡単なWebアプリです。
簡単に機能を下記にまとめました。
パス メソッド 機能 /top GET 勤怠表の表示 /top/kintai POST 勤怠表への勤怠情報の追加 /logout GET ログアウト機能 /old-url GET 古いURLなので/topへリダイレクトさせる 準備
依存モジュールはこのようになっております。
package.json{ "dependencies": { "http": "^0.0.1-security", "http-auth": "^4.1.2", "pug": "^2.0.4" } }basic認証には
http-auth
, テンプレートエンジンはpug
を使用します。
次に、node.jsの雛形を記述します。index.js'use strict'; const http = require('http'); const auth = require('http-auth'); const pug = require('pug'); const basic = auth.basic({ realm: 'Enter username and password', file: './password' }); const server = http.createServer(basic.check((req, res) => { switch (req.url) { default: break; } })); const port = 8000; server.listen(port, () => { console.info('Listening on ' + port); });ちなみにこのコードはこのままでは動きません。
また、認証に使用するユーザ名とパスワードはこのようなテキストファイルに格納しておきます。passwordguest1:2222 guest2:3333ステータスコード : 200 OK
このステータスコードはリクエストが正常に完了したことを表します。GET,PUT,POSTメソッドで正常なリクエストが来たらまずこれでいいのではないでしょうか。
今回の例ではGET
メソッドで/top
にリクエストが来たらステータスコード200
とHTMLファイルを返します。index.js'use strict'; const http = require('http'); const auth = require('http-auth'); const pug = require('pug'); const basic = auth.basic({ realm: 'Enter username and password', file: './password' }); //勤怠履歴 var history = []; const server = http.createServer(basic.check((req, res) => { switch (req.url) { case '/top': if(req.method === 'GET'){ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); res.end(pug.renderFile('./views/index.pug', { history: history, user: req.user })); } break; default: break; } })); const port = 8000; server.listen(port, () => { console.info('Listening on ' + port); });
res.writeHead
でHTTPレスポンスヘッダに200のステータスコードを書き込んでいます。
また、res.end
でpugテンプレートからHTMLに変換したものをレスポンスボディに書き込んでいます。
pugテンプレートは以下のようになります。views/index.pugdoctype html html(lang="ja") head meta(charset="utf-8") title 勤怠表 body h1 勤怠表 h2 あなたは #{user}
node index.js
でプログラムを実行し、localhost:8000/top
をブラウザで開くとページが見れます。
この時点でログインするとユーザ名だけが表示されるページができました。
index.jsでpugにhistory
とuser.name
を渡していますが、history
は現段階では使用していません。ステータスコード : 400 Bad Request
このステータスコードはリクエストの構文やパラメータが間違っている時に返します。
例えば、ユーザの入力からパスワードの設定を行う時に指定した要件を満たしていなかった場合など400
を返します。
またリクエストのメソッドが指定されたURLには実装されていないときなどもこのコードを返します。
リクエストのメソッドが指定されたURLには実装されていない場合の処理をindex.jsに書き加えます。index.jscase '/top': if(req.method === 'GET'){ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); res.end(pug.renderFile('./views/index2.pug', { history: history, user: req.user })); } + else{ + handleBadRequest(req, res); + } break; default: break; } })); +function handleBadRequest(req, res) { + res.writeHead(400, { + 'Content-Type': 'text/plain; charset=utf-8' + }); + const message = "未対応のリクエストです。"; + res.end('status code :' + res.statusCode + " " + message); +} + const port = 8000;この例ではtop/にリクエストが来たものの、メソッドがGET以外ならばhandleBadRequest関数でレスポンスヘッダに400を書いています。
これでプログラムを再起動して、別のターミナルからcurl -X POST -u guest1:2222 -d "hoge=huga" localhost:8000/topなどと実装されていないPOSTメソッドでリクエストを送ると、
status code :400 未対応のリクエストです。と帰ってくるはずです。
ステータスコード : 301 Moved Permanently
こちらはリクエストで指定したURLが新しいURLに移行されている時に返します。古いURLにアクセスされると新しいURLにリダイレクトされます。新しいURLはレスポンスヘッダのLocationに絶対URLとして格納します。
ここでは古いURL('/old-url')にアクセスがくると新しいURL(/top)へリダイレクトを行います。
このコードを追加したものが以下の差分になります。index.jshandleBadRequest(req, res); } + case '/old-url' : + handleMovedPermanently(req, res); + break default: break; } })); function handleBadRequest(req, res) { res.writeHead(400, { 'Content-Type': 'text/plain; charset=utf-8' }); const message = "未対応のリクエストです。"; res.end('status code :' + res.statusCode + " " + message); } +function handleMovedPermanently(req, res) { + res.writeHead(301, { + 'Content-Type': 'text/html; charset=utf-8', + 'Location' : '/top' + }); + res.end('<!DOCTYPE html><html lang="jp"><body>' + + '<h1>新しいURLに移動しました</h1>' + + '<a href="/top">新しいほう</h1>' + + '</body></html>'); +} const port = 8000; server.listen(port, () => { console.info('Listening on ' + port);Chromeで
localhost:8000/old-url
を開くと/top
にリダイレクトされます。
一方curlではレスポンスボディに新しいURLに移動した旨を知らせるHTMLが帰ってくると思います。curl -X GET -u guest1:2222 localhost:8000/old-url<!DOCTYPE html><html lang="jp"><body><h1>新しいURLに移動しました</h1><a href="/top">新しいほう</h1></body></html>%ステータスコード : 401 Unauthorized
このステータスコードは認証に失敗したときに返します。
またユーザがログアウトするときはこのステータスコードを返せばOKです。
この例では、server
がbasic認証が通らなかったら自動的に401を返してくれるのでログアウト処理部分だけ対応することにします。index.jscase '/old-url' : handleMovedPermanently(req, res); break; + case '/logout' : + handleUnauthorized(req, res); + break; default: break; } }));
index.jsfunction handleMovedPermanently(req, res) { res.writeHead(301, { 'Content-Type': 'text/html; charset=utf-8', 'Location' : '/top' }); res.end('<!DOCTYPE html><html lang="jp"><body>' + '<h1>新しいURLに移動しました</h1>' + '<a href="/top">新しいほう</h1>' + '</body></html>'); } +function handleUnauthorized(req, res) { + res.writeHead(401, { + 'Content-Type': 'text/html; charset=utf-8' + }); + res.end('<!DOCTYPE html><html lang="jp"><body>' + + '<h1>ログアウトしました</h1>' + + '<a href="/top">ログイン</h1>' + + '</body></html>'); +}次にログアウトボタンをviewに追加します。
views/index.pugdoctype html html(lang="ja") head meta(charset="utf-8") title 勤怠表 body h1 勤怠表 h2 あなたは #{user} a(href="/logout") ログアウトブラウザで
localhost:8000/top
を開き、ログアウトを押すと、ログアウトした旨のHMLが表示され、ログインを押すとまたユーザ名とパスワードが要求されます。ステータスコード : 404 Not Found
このステータスコードは指定したリソースが存在しない場合に返します。
またこのときのレスポンスボディには理由を記述します。
"status code :404 指定したURLは見つかりません" という記述をレスポンスボディに追加しています。index.jscase '/logout' : handleUnauthorized(req, res); break; default: + handleNotFound(req, res); break;
index.jsfunction handleUnauthorized(req, res) { res.writeHead(401, { 'Content-Type': 'text/html; charset=utf-8' }); res.end('<!DOCTYPE html><html lang="jp"><body>' + '<h1>ログアウトしました</h1>' + '<a href="/top">ログイン</h1>' + '</body></html>'); } +function handleNotFound(req, res){ + res.writeHead(404, { + 'Content-Type': 'text/plain; charset=utf-8' + }); + const message = "指定したURLが見つかりません。"; + res.end('status code :' + res.statusCode + " " +message); +}ブラウザで
localhost:8000/hoge
などと存在しないURLを開くと、ページが存在しない旨が表示されます。ステータスコード : 303 See Other
このステータスコードはリクエストに対する処理結果が別のURLで取得できる時に返します。例えばブラウザからPOSTで何かリソースを操作するリクエストの処理結果やそれを反映した結果をGETで取得する時に使います。
ここの例では、出勤ボタンを押すと、出勤
のログを出し、退勤ボタンを押すと、退勤
のログをユーザ名と一緒に流す機能をPOSTで実装します。コードは以下の通りとなります。
index.jshandleBadRequest(req, res); } break; + case '/top/kintai': + let data = []; + if(req.method === 'POST'){ + req.on('data', (chunk) => { + data.push(chunk); + }).on('end', () => { + data = data.toString(); + const decoded = decodeURIComponent(data); + let kintai = decoded ? decoded.split('kintai-button=')[1] : ''; + kintai = req.user + ':' + kintai + history.unshift(kintai); + res.writeHead(303, { + 'Location': '/top' + }); + res.end(); + }) + } + else{ + handleBadRequest(req, res); + } case '/old-url' : handleMovedPermanently(req, res); break;
viewは以下の通りとなります。
views/index.pugdoctype html html(lang="ja") head meta(charset="utf-8") title 勤怠表 body h1 勤怠表 h2 あなたは #{user} a(href="/logout") ログアウト form(method="post" action="/top/kintai") button(type="submit" name="kintai-button" value="出勤") 出勤 button(type="submit" name="kintai-button" value="退勤") 退勤 each element in history div.history <hr> p #{element} <hr>コードの説明をします。
まず、localhost:8000/top
をブラウザで開くと出勤ボタンと退勤ボタンが出現します。
出勤ボタンを押すとkintai-button="出勤"
、退勤ボタンを押すとkintai-button="退勤"
という情報(厳密には違いますが)が、index.jsのcase 'top/kintai':
以下で処理されることになります。data
にはURLエンコーディングされた文字列が入ってくるので、これをdecodeすることによって人間が読める文字列をdecoded
に取り出します。ここから、decoded
の中(例;kintai-button="退勤"
)から=の右部分("退勤"
)だけを取り出したものとユーザネームをつなげたものをkintai
に格納します。そしてログ全体を格納する配列であるhistory
に前から追加していきます。なぜ普通のpush(後ろから追加)しないのかというと、新しいものをログの上部分に表示したかったからです。
最後に303のステータスコードと、リダイレクト先を指定('Location':'/top')をレスポンスヘッダに記述します。出勤ボタンを押されると、見かけ上はページの移動が行われていないように見え、そのページ上で次々とログが出てくるはずです。最後に
この例題では他のステータスコードの具体例を思いつかなかったので、他のステータスコードはまた別の記事でやるかもしれません。
自分の学習ログ兼ねているので読みづらい文章だったかもしれませんがここまで読んでいただきありがとうございました。
- 投稿日:2020-03-29T19:49:00+09:00
常に動くLINEBOTにお引っ越し(レンタルサーバ+PHP編)
今回のモチベーション
前回、こちらの記事を参考にWikipedia APIを使った、調べものLINE botを作った。
前回の記事
https://qiita.com/shima-07/items/2322598ca5a40cfee47bだが、
- ngrokを立ち上げている時しか使えないから普段使えない。
- いざ、ngrokを立ち上げるとアドレスが変わってしまうため、Messaging API settingsのwebhook URLを毎度変えないと動かない。
うーん。。。
ngrok立ち上げるのめんどくさい! 常に使えるようにしないと意味ないじゃん!
と思ったわけです。だから『常に動くようにしよう!』が今回の動機です。
今回やったこと
- 1. まずは now を試してみた
- 2. さくらのレンタルサーバでやることにした
- 3. jsで書いていたものをPHPに書き直した
最終的にはPHP化してさくらサーバに載っけました。
1. まずは now を試してみた
このあたりを参考に進めてみる。
さすが良記事!サクサク進むぜと思いながら最後までいきデプロイ完了!
簡単だったなあと思いながら、Webhook URLに入れて「Verify」をクリック・・・・う。。まあよくある。
(1時間ほど立ち向かう)
色々と試すが私の手におえないと判断して諦める。2. さくらのレンタルサーバでやることにした
この記事を発見!
http://blog.hetabun.com/line-bot-php-sakuraこれ通りやることで、「こんにちは」に対して「こんにちは!」と元気よく返してくれるボットができました。
3. jsで書いていたものをPHPに書き直した
ここからが本番。前回あげた下記jsのコードと同じような振る舞いをPHPで書いていく。
PHPももちろん初心者である。再掲
server.js'use strict'; const express = require('express'); const line = require('@line/bot-sdk'); const PORT = process.env.PORT || 3000; // 追加 const axios = require('axios'); const config = { channelSecret: '作成したBOTのチャンネルシークレット', channelAccessToken: '作成したBOTのチャンネルアクセストークン' }; const app = express(); app.get('/', (req, res) => res.send('Hello LINE BOT!(GET)')); //ブラウザ確認用(無くても問題ない) app.post('/webhook', line.middleware(config), (req, res) => { //ここのif文はdeveloper consoleの"接続確認"用なので後で削除して問題ないです。 if(req.body.events[0].replyToken === '00000000000000000000000000000000' && req.body.events[1].replyToken === 'ffffffffffffffffffffffffffffffff'){ res.send('Hello LINE BOT!(POST)'); console.log('疎通確認用'); return; } Promise .all(req.body.events.map(handleEvent)) .then((result) => res.json(result)); }); const client = new line.Client(config); function handleEvent(event) { if (event.type !== 'message' || event.message.type !== 'text') { return Promise.resolve(null); } let mes = '' // console.log(event.message.text); if(event.message.text.indexOf('?') > -1){ // ?を含んでいる場合にはwikiで検索したものを出して、含んでない場合はurlを返す var str = event.message.text; var result = str.split( '?' ).join( '' ); //?を取り除く処理 mes = result + 'の説明:'; //wikiのbodyの前の一言 getBody(event.source.userId,result); //wiki APIで取得できたらプッシュメッセージ }else{ var result = event.message.text; mes = result + 'のURL:'; //wikiのurlの前の一言 getUrl(event.source.userId,result); //wiki APIで取得できたらプッシュメッセージ } return client.replyMessage(event.replyToken, { type: 'text', text : mes }); } const getBody = async (userId,word) => { const res = await axios.get('http://wikipedia.simpleapi.net/api?keyword='+ encodeURIComponent(word) + '&output=json'); const item = res.data; // console.log(item); await client.pushMessage(userId, { type: 'text', text: item[0].body, }); } const getUrl = async (userId,word) => { const res = await axios.get('http://wikipedia.simpleapi.net/api?keyword='+ encodeURIComponent(word) + '&output=json'); const item = res.data; // console.log(item); await client.pushMessage(userId, { type: 'text', text: item[0].url, }); } app.listen(PORT); console.log(`Server running at ${PORT}`);処理の整理
- LINEからのメッセージを受け取る
- そのメッセージをWikipedia APIに渡して結果を受け取る
- メッセージによってLINE側に返却するものを変える
- ?があるときはurlを返す
- ?がないときはbodyを返す
LINEからのメッセージを受け取る
参考にした記事中にあった下記の
$text
で取れているからそれはOK。LINEからのメッセージ.php//ユーザーからのメッセージ取得 $json_string = file_get_contents('php://input'); $jsonObj = json_decode($json_string); $type = $jsonObj->{"events"}[0]->{"message"}->{"type"}; //メッセージ取得 $text = $jsonObj->{"events"}[0]->{"message"}->{"text"}; //ReplyToken取得 $replyToken = $jsonObj->{"events"}[0]->{"replyToken"};そのメッセージをWikipedia APIに渡して結果を受け取る
ここが一番ハマった。
jsではres.data.item[0].body
の構造で取れていたので、
同じノリで$value = $res->{"data"}->{"item"}[0]->{"body"};
のような書き方をして、、当然何も取れず。結論、下記のような取り方でできた。
wikipediaAPIからURLやbody取得部分.php$keyword = mb_convert_encoding($text, "UTF-8", "auto"); $res = file_get_contents('http://wikipedia.simpleapi.net/api?keyword=' . $keyword . '&output=json'); $jsonwiki_decode = json_decode($res,true); // 0番目の物だけを抽出する。もっとたくさん抽出したいときはここを変更する $jsonwiki = $jsonwiki_decode[0]; //欲しい項目だけの配列にする $wikidata = array( 'url' => $jsonwiki["url"], 'body' => $jsonwiki["body"] ); $URL = $wikidata["url"]; $body = $wikidata["body"];流れとしては、
- LINEから受け取ったキーワードをAPIのkeywordとして渡せるようにエンコードする
- それをAPIに渡し
$res
として取得する- json_decodeして配列にする
- そのキーワードに対しての一番先頭の回答を取得するため[0]を取得する
- それに対して
$wikidata
として必要な項目だけ取得する- LINEに返したいものは、
$URL = $wikidata["url"]
や$body = $wikidata["body"]
として取得できる補足
上記
$res
にどんなものが入っているか見るために下記のようなものをページを作って見てました。( jsはconsole.log()で気軽に見えたけどphpではどうやってみたらいいかわからずわざわざこんなことしました。。。 )
check_data.php<html> <head> <title> test </title> </head> <body> <form method="POST" action="show.php"> キーワード: <input type="text" name="name" size="15" /> <input type="submit" name="submit" value="送信" /> </form> <?php if($_REQUEST['submit'] != null){ $input = $_REQUEST[name]; //$textのなかに'?'が含まれている場合 $text = str_replace('?', '', $input); $keyword = mb_convert_encoding($text, "UTF-8", "auto"); $res = file_get_contents('http://wikipedia.simpleapi.net/api?keyword=' . $keyword . '&output=json'); $jsonwiki_decode = json_decode($res,true); $jsonwiki = $jsonwiki_decode[0]; $wikidata = array( 'url' => $jsonwiki["url"], 'body' => $jsonwiki["body"] ); $URL = $wikidata["url"]; $body = $wikidata["body"]; print('$input: '.$input.'-----'); print('$text: '.$text.'-----'); print('$keyword: '.$keyword.'-----'); print('$res: '.$res.'-----'); print('$jsonwiki_decode :'.$jsonwiki_decode.'-----'); print('$jsonwiki :'.$jsonwiki .'-----'); print('$wikidata :'.$wikidata .'-----'); print('URL: '.$URL.'-----'); print('body: '.$body.'-----'); } ?> </body> </html>メッセージによってLINE側に返却するものを変える
- ?がある場合はURLをLINEに返す。また、Wikipedia APIに渡すときには?を取り除く。
- ?がない場合はBodyをLINEに返す。
分岐部分.phpif(strpos($text,'?') !== false){ //$textのなかに'?'が含まれている場合 $text = str_replace('?', '', $text); $keyword = mb_convert_encoding($text, "UTF-8", "auto"); $res = file_get_contents('http://wikipedia.simpleapi.net/api?keyword=' . $keyword . '&output=json'); $jsonwiki_decode = json_decode($res,true); // 0番目の物だけを抽出する。もっとたくさん抽出したいときはここを変更する $jsonwiki = $jsonwiki_decode[0]; //欲しい項目だけの配列にする $wikidata = array( 'url' => $jsonwiki["url"], 'body' => $jsonwiki["body"] ); $URL = $wikidata["url"]; $body = $wikidata["body"]; // メッセージ部分 $response_format_text = [ "type" => "text", "text" => $URL ]; }else{ // ?が含まれないときの処理 $keyword = mb_convert_encoding($text, "UTF-8", "auto"); $res = file_get_contents('http://wikipedia.simpleapi.net/api?keyword=' . $keyword . '&output=json'); $jsonwiki_decode = json_decode($res,true); // 0番目の物だけを抽出する。もっとたくさん抽出したいときはここを変更する $jsonwiki = $jsonwiki_decode[0]; //欲しい項目だけの配列にする $wikidata = array( 'url' => $jsonwiki["url"], 'body' => $jsonwiki["body"] ); $URL = $wikidata["url"]; $body = $wikidata["body"]; // メッセージ部分 $response_format_text = [ "type" => "text", "text" => $body ]; } }サンプル
できたー レンタルサーバ上で動いたー!
— yuta kawashima (@y_kawashima_) March 29, 2020
前回のはngrokでやってたから普段使えなかったけど、これでいつでも使える!
#protoout pic.twitter.com/HznZSciExJ全ソースコード
linebot.php<?php $accessToken = 'アクセストークン'; //ユーザーからのメッセージ取得 $json_string = file_get_contents('php://input'); $jsonObj = json_decode($json_string); $type = $jsonObj->{"events"}[0]->{"message"}->{"type"}; //メッセージ取得 $text = $jsonObj->{"events"}[0]->{"message"}->{"text"}; //ReplyToken取得 $replyToken = $jsonObj->{"events"}[0]->{"replyToken"}; //メッセージ以外のときは何も返さず終了 if($type != "text"){ exit; } if($type == "text"){ if(strpos($text,'?') !== false){ //$textのなかに'?'が含まれている場合 $text = str_replace('?', '', $text); $keyword = mb_convert_encoding($text, "UTF-8", "auto"); $res = file_get_contents('http://wikipedia.simpleapi.net/api?keyword=' . $keyword . '&output=json'); $jsonwiki_decode = json_decode($res,true); // 0番目の物だけを抽出する。もっとたくさん抽出したいときはここを変更する $jsonwiki = $jsonwiki_decode[0]; //欲しい項目だけの配列にする $wikidata = array( 'url' => $jsonwiki["url"], 'body' => $jsonwiki["body"] ); $URL = $wikidata["url"]; $body = $wikidata["body"]; // メッセージ部分 $response_format_text = [ "type" => "text", "text" => $URL ]; }else{ // ?が含まれないときの処理 $keyword = mb_convert_encoding($text, "UTF-8", "auto"); $res = file_get_contents('http://wikipedia.simpleapi.net/api?keyword=' . $keyword . '&output=json'); $jsonwiki_decode = json_decode($res,true); // 0番目の物だけを抽出する。もっとたくさん抽出したいときはここを変更する $jsonwiki = $jsonwiki_decode[0]; //欲しい項目だけの配列にする $wikidata = array( 'url' => $jsonwiki["url"], 'body' => $jsonwiki["body"] ); $URL = $wikidata["url"]; $body = $wikidata["body"]; // メッセージ部分 $response_format_text = [ "type" => "text", "text" => $body ]; } } $post_data = [ "replyToken" => $replyToken, "messages" => [$response_format_text] ]; $ch = curl_init("https://api.line.me/v2/bot/message/reply"); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST'); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($post_data)); curl_setopt($ch, CURLOPT_HTTPHEADER, array( 'Content-Type: application/json; charser=UTF-8', 'Authorization: Bearer ' . $accessToken )); $result = curl_exec($ch); curl_close($ch);おわりに
APIから返ってきた値をいい感じで取ってくるところでだいぶハマりました。
JSONデータの扱い方にもっと慣れないとなあー次はHerokuを使ったお引っ越しもやってみようと思います。
- 投稿日:2020-03-29T19:48:37+09:00
Catalinaのバージョンアップしたらrails sできなくなったけど、Node.jsのインストールで解決!
筆者の環境
macOS Catalina バージョン 10.15.4
使用言語:Ruby、JavaScriptエラー内容
不注意により、macOSがCatalina バージョン10.15.4に上がってしまった。
それから実装中のアプリでrails sすると、以下のエラーメッセージが表示されサーバーが起動しなくなった。
Could not find a JavaScript runtime. See https://github.com/rails/execjs for a list of available runtimes. (ExecJS::RuntimeUnavailable)
これで解決!
Node.jsをインストール
左側の12.16.1の方はインストールしても開けず、
右側(最新版)をインストールしたらrails s成功しました!番外編
他にも対処法はいくつかあり、therubyracerというGemのインストールでも解決できるようです。(参考リンクご参照ください)
現在、チーム開発中だったので、自分以外の複数端末への影響を考え今回はGem以外の方法を選択しました。
参考
にさせていただきました。ありがとうございます。
https://qiita.com/azusanakano/items/771dc9919f347de061d7
- 投稿日:2020-03-29T17:38:47+09:00
RSSで取得した記事を日付順にし、表示形式を変更する(Javascript・node.js)
解決する問題
RSSで取得した記事を、日付順にして日付の表示形式を変更する。
今回はRSSでコロナウイルスに関しての記事を取得しました。
対象読者
・node.jsでxmlを取得する事ができる方。
・express-generatorを触った事がある方。
・xmlの表示形式の知識がある方。express-generatorを使ってXMLを取得しているところから進めていきます。
環境
OS: macOS
Node.js: v13.5.0
npm: 6.14.3
express: ~4.16.1
ejs: ~2.6.1,RSSで記事を取得した時の状況
記事を取得し表示させると日付順にならず、見づらい
hello.js↓
router.get('/',(req, res, next) => { var opt = { host: 'news.google.com', port: 443, path: '/rss/search?q=corona&q=korona&hl=ja&gl=JP&ceid=JP:ja' }; http.get(opt, (res2) => { var body = ''; res2.on('data',(data) => { body += data; }); res2.on('end', () => { parseString(body.trim(), (err, result) => { var data = { title: 'コロナウイルスの最新情報を表示します', content: result.rss.channel[0].item }; res.render('hello', data); }) }); }); });hello.ejs↓
<!DOCTYPE html> <html> <head> <meta http-equiv="content-type" content="text/html; charset=UTF-8"> <title><%= title %></title> <link rel='stylesheet' href="/stylesheets/style.css" /> </head> <body> <header> <h1><%= title %></h1> </header> <div role="main"> <% if (content != null) { %> <ol> <% for (var i in content) { %> <% var obj = content[i]; %> <li><%= obj.pubDate %><a href="<%=obj.link %>"><%= obj.title %></a></li> </tr> <% } %> </ol> <% } %> </div> </body> </html>(今回取得したxml
https://news.google.com/rss/search?q=corona&q=korona&hl=ja&gl=JP&ceid=JP:ja)解決策
①xmlの記事の中のitemを配列に入れていく。
for in を使ってxmlのitemを配列に入れる処理をループさせます。
配列を作る、ループを作る、配列へpushという順番です。
※ソートする為に配列へ入れます。
<!-- 配列を作成 --> <% var hash = new Array %> <!-- xmlのitemをループ処理する --> <% for (var i in content) { %> <% var obj = content[i]; %> <!-- itemを配列へ追加 --> <% hash.push(obj); %> <% } %>※ejsの中にjsを書いています。
※content = result.rss.channel[0].item②配列をソートして日付順に並べる
<!-- 配列をソートして日付順に変更 --> <% hash.sort(function(a,b) { return (a.pubDate < b.pubDate ? 1 : -1); }); %>これで配列の中のitemが、xmlのpubDateの日付順に並びました。
※こちらもejsの中にjsを書いています。
※参考にした記事は下に載っています。③日付の表示形式を変更
1、配列をfor inでループさせる
2、ループの中にDateオブジェクトを作成
3、日付の形式を変更
4、(西暦2020年を削除)任意
5、ループ処理の中で記事を表示する<!-- 配列をループ処理 --> <% for (var i in hash) { %> <% var obj2 = hash[i] %> <!-- 日付の表示形式を変更 --> <% var date2 = new Date(obj2.pubDate); var b = date2.toLocaleString('ja-JP', {era:'long'}); obj2.pubDate = b; %> <!-- 西暦を削除 --> <% var obj3_pubDate = obj2.pubDate.replace('西暦2020年', ''); %> <!-- 表示 --> <div class="center"> <ul> <li> <a href="<%=obj2.link %>"> <div class="Date"><%=obj3_pubDate %></div> <div class="title"><%=" " + obj2.title %></div> </a> </li> </ul> </div> <% } %>表示結果
変更後のコード
hello.ejs↓
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta http-equiv="content-type" content="text/html; charset=UTF-8"> <title><%= title %></title> <link rel='stylesheet' href="/stylesheets/style.css" /> </head> <div class="style"> <!-- 更新日時を日本語へ変更する --> <% var update2 = new Date(update); var update3 = update2.toLocaleString('ja-JP', {era:'long'}); update = update3; %> <!-- 更新日時の西暦を消す --> <% var update4 = update.replace('西暦2020年', ''); %> </div> <body> <header> <h1><%= title %>更新(<%= update4 %>)</h1> </header> <div role="main"> <% if (content != null) { %> <!-- 配列を作成 --> <% var hash = new Array %> <!-- xmlのitemをループ処理する --> <% for (var i in content) { %> <% var obj = content[i]; %> <!-- itemを配列へ追加 --> <% hash.push(obj); %> <% } %> <!-- 配列をソートして日付順に変更 --> <% hash.sort(function(a,b) { return (a.pubDate < b.pubDate ? 1 : -1); }); %> <!-- 配列をループ処理 --> <% for (var i in hash) { %> <% var obj2 = hash[i] %> <!-- 日付の表示形式を変更 --> <% var date2 = new Date(obj2.pubDate); var b = date2.toLocaleString('ja-JP', {era:'long'}); obj2.pubDate = b; %> <!-- 西暦を削除 --> <% var obj3_pubDate = obj2.pubDate.replace('西暦2020年', ''); %> <!-- 表示 --> <div class="center"> <ul> <li> <a href="<%=obj2.link %>"> <div class="Date"><%=obj3_pubDate %></div> <div class="title"><%=" " + obj2.title %></div> </a> </li> </ul> </div> <% } %> <% } %> </div> </body> </html>hello.js↓
var express = require('express'); var router = express.Router(); var http = require('https'); var parseString = require('xml2js').parseString; router.get('/',(req, res, next) => { var opt = { host: 'news.google.com', port: 443, path: '/rss/search?q=corona&q=Coronavirus&hl=ja&gl=JP&ceid=JP:ja' }; http.get(opt, (res2) => { var body = ''; res2.on('data',(data) => { body += data; }); res2.on('end', () => { parseString(body.trim(), (err, result) => { var data = { title: 'コロナウイルスの最新情報を表示します', content: result.rss.channel[0].item, update: result.rss.channel[0].lastBuildDate }; res.render('hello', data); }) }); }); }); module.exports = router;参考
配列を日付順にソートする方法
https://infoteck-life.com/a0107-js-array-sort-date/
https://www.p-nt.com/technicblog/archives/58Dateオブジェクトの使い方入門
https://www.sejuku.net/blog/30171Dateオブジェクトのプロパティ(日付の表示形式を参考にしました。)
https://so-zou.jp/web-app/tech/programming/javascript/grammar/object/date.htmループ処理(今回はfor inでループしています。)
https://qiita.com/endam/items/808a084859e3a101ab8f文字列から指定した文字を削除する(日付の西暦2020年を削除する為に参考にしました。)
https://zukucode.com/2017/04/javascript-string-remove.html
- 投稿日:2020-03-29T17:38:47+09:00
【JS】RSSで取得した記事を日付順にし、表示形式を変更する(Javascript・node.js)
解決する問題
RSSで取得した記事を、日付順にして日付の表示形式を変更する。
今回はRSSでコロナウイルスに関しての記事を取得しました。
対象読者
・node.jsでxmlを取得する事ができる方。
・express-generatorを触った事がある方。
・xmlの表示形式の知識がある方。express-generatorを使ってXMLを取得しているところから進めていきます。
環境
OS: macOS
Node.js: v13.5.0
npm: 6.14.3
express: ~4.16.1
ejs: ~2.6.1,RSSで記事を取得した時の状況
記事を取得し表示させると日付順にならず、見づらい
hello.js↓
router.get('/',(req, res, next) => { var opt = { host: 'news.google.com', port: 443, path: '/rss/search?q=corona&q=korona&hl=ja&gl=JP&ceid=JP:ja' }; http.get(opt, (res2) => { var body = ''; res2.on('data',(data) => { body += data; }); res2.on('end', () => { parseString(body.trim(), (err, result) => { var data = { title: 'コロナウイルスの最新情報を表示します', content: result.rss.channel[0].item }; res.render('hello', data); }) }); }); });hello.ejs↓
<!DOCTYPE html> <html> <head> <meta http-equiv="content-type" content="text/html; charset=UTF-8"> <title><%= title %></title> <link rel='stylesheet' href="/stylesheets/style.css" /> </head> <body> <header> <h1><%= title %></h1> </header> <div role="main"> <% if (content != null) { %> <ol> <% for (var i in content) { %> <% var obj = content[i]; %> <li><%= obj.pubDate %><a href="<%=obj.link %>"><%= obj.title %></a></li> </tr> <% } %> </ol> <% } %> </div> </body> </html>(今回取得したxml
https://news.google.com/rss/search?q=corona&q=korona&hl=ja&gl=JP&ceid=JP:ja)解決策
①xmlの記事の中のitemを配列に入れていく。
for in を使ってxmlのitemを配列に入れる処理をループさせます。
配列を作る、ループを作る、配列へpushという順番です。
※ソートする為に配列へ入れます。
<!-- 配列を作成 --> <% var hash = new Array %> <!-- xmlのitemをループ処理する --> <% for (var i in content) { %> <% var obj = content[i]; %> <!-- itemを配列へ追加 --> <% hash.push(obj); %> <% } %>※ejsの中にjsを書いています。
※content = result.rss.channel[0].item②配列をソートして日付順に並べる
<!-- 配列をソートして日付順に変更 --> <% hash.sort(function(a,b) { return (a.pubDate < b.pubDate ? 1 : -1); }); %>これで配列の中のitemが、xmlのpubDateの日付順に並びました。
※こちらもejsの中にjsを書いています。
※参考にした記事は下に載っています。③日付の表示形式を変更
1、配列をfor inでループさせる
2、ループの中にDateオブジェクトを作成
3、日付の形式を変更
4、(西暦2020年を削除)任意
5、ループ処理の中で記事を表示する<!-- 配列をループ処理 --> <% for (var i in hash) { %> <% var obj2 = hash[i] %> <!-- 日付の表示形式を変更 --> <% var date2 = new Date(obj2.pubDate); var b = date2.toLocaleString('ja-JP', {era:'long'}); obj2.pubDate = b; %> <!-- 西暦を削除 --> <% var obj3_pubDate = obj2.pubDate.replace('西暦2020年', ''); %> <!-- 表示 --> <div class="center"> <ul> <li> <a href="<%=obj2.link %>"> <div class="Date"><%=obj3_pubDate %></div> <div class="title"><%=" " + obj2.title %></div> </a> </li> </ul> </div> <% } %>表示結果
変更後のコード
hello.ejs↓
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta http-equiv="content-type" content="text/html; charset=UTF-8"> <title><%= title %></title> <link rel='stylesheet' href="/stylesheets/style.css" /> </head> <div class="style"> <!-- 更新日時を日本語へ変更する --> <% var update2 = new Date(update); var update3 = update2.toLocaleString('ja-JP', {era:'long'}); update = update3; %> <!-- 更新日時の西暦を消す --> <% var update4 = update.replace('西暦2020年', ''); %> </div> <body> <header> <h1><%= title %>更新(<%= update4 %>)</h1> </header> <div role="main"> <% if (content != null) { %> <!-- 配列を作成 --> <% var hash = new Array %> <!-- xmlのitemをループ処理する --> <% for (var i in content) { %> <% var obj = content[i]; %> <!-- itemを配列へ追加 --> <% hash.push(obj); %> <% } %> <!-- 配列をソートして日付順に変更 --> <% hash.sort(function(a,b) { return (a.pubDate < b.pubDate ? 1 : -1); }); %> <!-- 配列をループ処理 --> <% for (var i in hash) { %> <% var obj2 = hash[i] %> <!-- 日付の表示形式を変更 --> <% var date2 = new Date(obj2.pubDate); var b = date2.toLocaleString('ja-JP', {era:'long'}); obj2.pubDate = b; %> <!-- 西暦を削除 --> <% var obj3_pubDate = obj2.pubDate.replace('西暦2020年', ''); %> <!-- 表示 --> <div class="center"> <ul> <li> <a href="<%=obj2.link %>"> <div class="Date"><%=obj3_pubDate %></div> <div class="title"><%=" " + obj2.title %></div> </a> </li> </ul> </div> <% } %> <% } %> </div> </body> </html>hello.js↓
var express = require('express'); var router = express.Router(); var http = require('https'); var parseString = require('xml2js').parseString; router.get('/',(req, res, next) => { var opt = { host: 'news.google.com', port: 443, path: '/rss/search?q=corona&q=Coronavirus&hl=ja&gl=JP&ceid=JP:ja' }; http.get(opt, (res2) => { var body = ''; res2.on('data',(data) => { body += data; }); res2.on('end', () => { parseString(body.trim(), (err, result) => { var data = { title: 'コロナウイルスの最新情報を表示します', content: result.rss.channel[0].item, update: result.rss.channel[0].lastBuildDate }; res.render('hello', data); }) }); }); }); module.exports = router;参考
配列を日付順にソートする方法
https://infoteck-life.com/a0107-js-array-sort-date/
https://www.p-nt.com/technicblog/archives/58Dateオブジェクトの使い方入門
https://www.sejuku.net/blog/30171Dateオブジェクトのプロパティ(日付の表示形式を参考にしました。)
https://so-zou.jp/web-app/tech/programming/javascript/grammar/object/date.htmループ処理(今回はfor inでループしています。)
https://qiita.com/endam/items/808a084859e3a101ab8f文字列から指定した文字を削除する(日付の西暦2020年を削除する為に参考にしました。)
https://zukucode.com/2017/04/javascript-string-remove.html
- 投稿日:2020-03-29T15:17:40+09:00
npmでReactのプロジェクト作成
npx create-react-app プロジェクト名プロジェクト作成すると下記画面が表示される
Success! Created react-app at /Users/xxx/Myapp/React/learning/react-app Inside that directory, you can run several commands: npm start Starts the development server. npm run build Bundles the app into static files for production. npm test Starts the test runner. npm run eject Removes this tool and copies build dependencies, configuration files and scripts into the app directory. If you do this, you can’t go back! We suggest that you begin by typing: cd react-app npm start上記の画面にはコマンド毎の操作説明が表示されており、こちらのコマンドは
npm
の他にyarn
でも実行可能1.
yarn start
又はnpm start
・webブラウザを起動
2.yarn build
又はnpm run build
・プロジェクトのビルド(プロジェクトのファイルから実際のWebサーバーにアップロードして利用するファイル類を生成する)
3.yarn test
又はnpm test
・テストプログラムを実行して、アプリケーションのテストを行う
4.yarn eject
又はnpm run eject
・プロジェクトのインジェクトを行う
- 投稿日:2020-03-29T12:04:36+09:00
位置情報を送ると最寄りのラーメン店を教えてくれるLINE botをつくってみた?(2020年版)
概要
この記事はLINE bot・javascrptの初心者が、「LINE bot 面白い!」という熱意で作ったものの制作記です。何をつくったか...。
「LINE botに位置情報を送ると、その近辺のラーメン屋さんを教えてくれるbot」を作りました?!【デモ】
位置情報を入力すると最寄りのラーメンやさんを教えてくれるLINE botを作ってみた!
— まえぷー@出窓菜園 BWG (@kmaepu) March 28, 2020
後でQiitaの記事を出します。 pic.twitter.com/ZHCvzj6nqT本記事ではこのLINE botのサーバプログラムと、ラーメン店情報を検索するためにぐるなび APIを解説しています。基礎的な機能のみ実装しているので、店舗の星の多さ、複数件表示などは未実装です。
位置情報を送ると、その最寄りのラーメン屋さんを一軒返すところまで動かしました。
以下、2020年3月29日時点の情報です。開発環境
OS:Windows 10
Node.js:v10.15.3【ライブラリバージョン】
@line/bot-sdk:6.8.4
express:4.17.1
axios:0.19.2構成
構成は次のようになっています。ローカルPC上でNode.jsを動かしているので、グローバルネットワークとのつながりをngrokでトンネリングしています。
位置情報をNode.jsサーバが受け取ると、ぐるなび APIを利用して最寄りののラーメン店情報を取得します。このとき、ユーザから送られた位置情報をぐるなび APIのリクエストURLに含めています。
実装
ぐるなび APIの調査
ぐるなび APIはAPIの使用説明だけでなく、APIテストツールまで用意されていました。手始めに、freewordクエリを追加してリクエストしてみると、レスポンスが下の方に表示されます。
(アクセスキーはテスト用になっています)どんなクエリがあるかは、API仕様のレストラン検索 APIで公開されています。
プログラムからアクセスしてみる
LINE botとのやり取りと、APIアクセスはこちらのソースコードをもとに進めていきます。
APIテストツールを使うと、リクエストURLが生成されるのでコピーしてaxisos.getに引数に与えます。
こんな感じです。axios.get('https://api.gnavi.co.jp/RestSearchAPI/v3/?keyid=b17d2f36873123b0d0f09e9139556c79&freeword=ラーメン');この状態でアクセスしたら、次のようなエラーが発生しました。
Request path contains unescaped charactersコーディングミスというよりは、URLとして認識されていないようなエラーメッセージですね。試しにフリーワードをローマ字(ra-men)にしたらエラーが解消されて、レスポンスが返るようになりました!
↓レスポンスのログ一先ず、これでレスポンスが返るので先に進みます。日本語対応は後述の「後回しにしていた日本語対応」で解説しています。
最寄りの店に絞る
フリーワード検索のみだと全国の店が検索されてしまうので、最寄りの店に絞ります。これはぐるなび APIのクエリに経緯を追加することで絞ることが可能です。
最初は経緯を固定で動かします。経緯を調べるにはGoogleマップが便利です。今回はProtoOut Studioの本拠地であるgranicaにしてみました。
【リクエストURL」
https://api.gnavi.co.jp/RestSearchAPI/v3/?keyid=アクセスキー&latitude=35.704607&longitude=139.772416&range=2&freeword=ラーメンこのURLで問題なくレスポンスが来ればOKです。
ラーメン店に絞る
ラーメンに絞りたいので、レスポンスデータの中のcategoryでフィルタリングしました。
for(var num = 0; num <= response.data.rest.length; num++){ if(response.data.rest[num].category === 'ラーメン'){ console.log(response.data.rest[num]); ramen_url = response.data.rest[num].url_mobile; break; } }これで最寄りのラーメン店に絞ることができました!と、思いきや最寄りにラーメン店が見つからなかった場合、エラーが発生します。ちなみに、最寄りとは半径500m圏内です。今回は正常系のみ作ることを目的とし、エラーハンドリングをスルーします...。
後回しにしていた日本語対応
フリーワードに日本語が入っていることが原因なので、URLにエンコードしてくれるencodeURI関数がありました。こちらの記事の通りになおすと、フリーワードに日本語が入っていても通るようになりました!
こんなコードです。url = 'https://api.gnavi.co.jp/RestSearchAPI/v3/?keyid=アクセスキー&latitude=35.704607&longitude=139.772416&range=2&freeword=ラーメン'; const encodeUrl = encodeURI(url); const response = await axios.get(encodeUrl);LINE botへのレスポンスを位置情報にする
このままテキストで返してもよいのですが、LINE botには位置情報を返す機能がありまので、位置情報で返してみました。ちなみに、テキストでURLを返すと次のようになります。
これを位置情報にすると、
こっちの方が見栄え良さそうですね イェーイ(/・ω・)/
LINE botで位置情報を返すコードは次の通りです。経緯だけではなく住所も返す必要があります。
コード中のhitnumは、レスポンスデータの中で、category”ラーメン”にヒットした位置が格納されているとします。ぐるなび APIの良いところはこれらの情報が全てそろっていることです?return client.replyMessage(event.replyToken, { type: 'location', title: response.data.rest[hitnum].name, address: response.data.rest[hitnum].address, latitude: response.data.rest[hitnum].latitude, longitude: response.data.rest[hitnum].longitude });位置情報入力を追加
最後に位置情報入力に対応させます。LINEから位置情報を受け取ってログに出し、URLに加えるコードは次の通りです。入力が位置情報のみとなるようにtypeで判定しています。
if (event.type !== 'message' || event.message.type !== 'location') { return Promise.resolve(null); } console.log(event.message.latitude + ' : ' + event.message.longitude); url = 'https://api.gnavi.co.jp/RestSearchAPI/v3/?keyid=アクセスキー&latitude=' + event.message.latitude + '&longitude=' + event.message.longitude + '&range=2&freeword=ラーメン';余談
今回、正常系で動くところまで作れたのですが、ラーメン店が見つからないかった場合のエラーハンドリングを入れたかったなと思います。例えば、かわいい動物画像を出すとか。もう一つ思ったのは、jsonデータのコンソールログデバッグは時間がかかったなと。APIの解説がされているサイトをみて一気にコーディングできたらよいのですが...。まずは経験して慣れることが大切ですね。
ちなみに、ProtoOut Studioの授業後から制作を開始し、Qiita公開まで6時間程度かかりました。
javascriptのデバッグ不慣れなのと、Qiita記事執筆をもう少し効率化できそうなので、もう少し頑張ってプロトアウトのサイクルを短くしてきたいなと思っています。ほんとに余談ですが、ProtoOut Studioの一環で進めているので、よくわからないエラーが発生して困っていると講師の方々に相談できます。校長である菅原のびすけさんに直接コーディング指南していただいてものすごく緊張していました(;'∀')
今回はぐるなびAPIを利用しましたが、他にも猫の種類検索APIこの猫なに猫?やHarry Potter APIなど気になるAPIが沢山あります(^^♪
今後はこのLINE botをベースに、connpass APIやGoogle MAP APIなどと連携して、勉強会やイベントのお知らせと夕食を提案してくれるようなサービスにしていけたら面白そうかなと思っています。
参考
ソースコード
'use strict'; const axios = require('axios'); const express = require('express'); const line = require('@line/bot-sdk'); const PORT = process.env.PORT || 3000; const config = { channelSecret: 'LINE botのチャンネルシークレット', channelAccessToken: 'LINE botのアクセストークン' }; const app = express(); app.get('/', (req, res) => res.send('Hello LINE BOT!(GET)')); //ブラウザ確認用(無くても問題ない) app.post('/webhook', line.middleware(config), (req, res) => { console.log(req.body.events); //ここのif文はdeveloper consoleの"接続確認"用なので後で削除して問題ないです。 if(req.body.events[0].replyToken === '00000000000000000000000000000000' && req.body.events[1].replyToken === 'ffffffffffffffffffffffffffffffff'){ res.send('Hello LINE BOT!(POST)'); console.log('疎通確認用'); return; } Promise .all(req.body.events.map(handleEvent)) .then((result) => res.json(result)); }); const client = new line.Client(config); async function handleEvent(event) { var url var ramen_url; var hitnum; // 位置情報のみに入力制限 if (event.type !== 'message' || event.message.type !== 'location') { return Promise.resolve(null); } // 取得した位置情報をログに表示 console.log(event.message.latitude + ' : ' + event.message.longitude); // ぐるなびAPIを使うためのURLに経緯を加える url = 'https://api.gnavi.co.j p/RestSearchAPI/v3/?keyid=af2b4862cbe801c5e2e1b87ef52881f6&latitude=' + event.message.latitude + '&longitude=' + event.message.longitude + '&range=2&freeword=ラーメン'; const encodeUrl = encodeURI(url); // ぐるなびAPIに問い合わせ const response = await axios.get(encodeUrl); //レスポンスの中からcategory"ラーメン"を探索 for(var num = 0; num <= response.data.rest.length; num++){ if(response.data.rest[num].category === 'ラーメン'){ // console.log(response.data.rest[num]); hitnum = num; ramen_url = response.data.rest[num].url_mobile; break; } } // ヒットしたラーメン店の住所をLINE botに返す return client.replyMessage(event.replyToken, { type: 'location', title: response.data.rest[hitnum].name, address: response.data.rest[hitnum].address, latitude: response.data.rest[hitnum].latitude, longitude: response.data.rest[hitnum].longitude }); } app.listen(PORT); console.log(`Server running at ${PORT}`);
- 投稿日:2020-03-29T12:04:36+09:00
位置情報を送ると最寄りのラーメン店を教えてくれるLINE botをつくってみた?
概要
この記事はLINE bot・javascrptの初心者が、「LINE bot 面白い!」という熱意で作ったものの制作記です。何をつくったか...。
「LINE botに位置情報を送ると、その近辺のラーメン屋さんを教えてくれるbot」を作りました?!【デモ】
位置情報を入力すると最寄りのラーメンやさんを教えてくれるLINE botを作ってみた!
— まえぷー@出窓菜園 BWG (@kmaepu) March 28, 2020
後でQiitaの記事を出します。 pic.twitter.com/ZHCvzj6nqT本記事ではこのLINE botのサーバプログラムと、ラーメン店情報を検索するためにぐるなび APIを解説しています。基礎的な機能のみ実装しているので、店舗の星の多さ、複数件表示などは未実装です。
位置情報を送ると、その最寄りのラーメン屋さんを一軒返すところまで動かしました。
以下、2020年3月29日時点の情報です。開発環境
OS:Windows 10
Node.js:v10.15.3【ライブラリバージョン】
@line/bot-sdk:6.8.4
express:4.17.1
axios:0.19.2構成
構成は次のようになっています。ローカルPC上でNode.jsを動かしているので、グローバルネットワークとのつながりをngrokでトンネリングしています。
位置情報をNode.jsサーバが受け取ると、ぐるなび APIを利用して最寄りののラーメン店情報を取得します。このとき、ユーザから送られた位置情報をぐるなび APIのリクエストURLに含めています。
実装
ぐるなび APIの調査
ぐるなび APIはAPIの使用説明だけでなく、APIテストツールまで用意されていました。手始めに、freewordクエリを追加してリクエストしてみると、レスポンスが下の方に表示されます。
(アクセスキーはテスト用になっています)どんなクエリがあるかは、API仕様のレストラン検索 APIで公開されています。
プログラムからアクセスしてみる
LINE botとのやり取りと、APIアクセスはこちらのソースコードをもとに進めていきます。
APIテストツールを使うと、リクエストURLが生成されるのでコピーしてaxisos.getに引数に与えます。
こんな感じです。axios.get('https://api.gnavi.co.jp/RestSearchAPI/v3/?keyid=b17d2f36873123b0d0f09e9139556c79&freeword=ラーメン');この状態でアクセスしたら、次のようなエラーが発生しました。
Request path contains unescaped charactersコーディングミスというよりは、URLとして認識されていないようなエラーメッセージですね。試しにフリーワードをローマ字(ra-men)にしたらエラーが解消されて、レスポンスが返るようになりました!
↓レスポンスのログ一先ず、これでレスポンスが返るので先に進みます。日本語対応は後述の「後回しにしていた日本語対応」で解説しています。
最寄りの店に絞る
フリーワード検索のみだと全国の店が検索されてしまうので、最寄りの店に絞ります。これはぐるなび APIのクエリに経緯を追加することで絞ることが可能です。
最初は経緯を固定で動かします。経緯を調べるにはGoogleマップが便利です。今回はProtoOut Studioの本拠地であるgranicaにしてみました。
【リクエストURL」
https://api.gnavi.co.jp/RestSearchAPI/v3/?keyid=アクセスキー&latitude=35.704607&longitude=139.772416&range=2&freeword=ラーメンこのURLで問題なくレスポンスが来ればOKです。
ラーメン店に絞る
ラーメンに絞りたいので、レスポンスデータの中のcategoryでフィルタリングしました。
for(var num = 0; num <= response.data.rest.length; num++){ if(response.data.rest[num].category === 'ラーメン'){ console.log(response.data.rest[num]); ramen_url = response.data.rest[num].url_mobile; break; } }これで最寄りのラーメン店に絞ることができました!と、思いきや最寄りにラーメン店が見つからなかった場合、エラーが発生します。ちなみに、最寄りとは半径500m圏内です。今回は正常系のみ作ることを目的とし、エラーハンドリングをスルーします...。
後回しにしていた日本語対応
フリーワードに日本語が入っていることが原因なので、URLにエンコードしてくれるencodeURI関数がありました。こちらの記事の通りになおすと、フリーワードに日本語が入っていても通るようになりました!
こんなコードです。url = 'https://api.gnavi.co.jp/RestSearchAPI/v3/?keyid=アクセスキー&latitude=35.704607&longitude=139.772416&range=2&freeword=ラーメン'; const encodeUrl = encodeURI(url); const response = await axios.get(encodeUrl);LINE botへのレスポンスを位置情報にする
このままテキストで返してもよいのですが、LINE botには位置情報を返す機能がありまので、位置情報で返してみました。ちなみに、テキストでURLを返すと次のようになります。
これを位置情報にすると、
こっちの方が見栄え良さそうですね イェーイ(/・ω・)/
LINE botで位置情報を返すコードは次の通りです。経緯だけではなく住所も返す必要があります。
コード中のhitnumは、レスポンスデータの中で、category”ラーメン”にヒットした位置が格納されているとします。ぐるなび APIの良いところはこれらの情報が全てそろっていることです?return client.replyMessage(event.replyToken, { type: 'location', title: response.data.rest[hitnum].name, address: response.data.rest[hitnum].address, latitude: response.data.rest[hitnum].latitude, longitude: response.data.rest[hitnum].longitude });位置情報入力を追加
最後に位置情報入力に対応させます。LINEから位置情報を受け取ってログに出し、URLに加えるコードは次の通りです。入力が位置情報のみとなるようにtypeで判定しています。
if (event.type !== 'message' || event.message.type !== 'location') { return Promise.resolve(null); } console.log(event.message.latitude + ' : ' + event.message.longitude); url = 'https://api.gnavi.co.jp/RestSearchAPI/v3/?keyid=アクセスキー&latitude=' + event.message.latitude + '&longitude=' + event.message.longitude + '&range=2&freeword=ラーメン';余談
今回、正常系で動くところまで作れたのですが、ラーメン店が見つからないかった場合のエラーハンドリングを入れたかったなと思います。例えば、かわいい動物画像を出すとか。もう一つ思ったのは、jsonデータのコンソールログデバッグは時間がかかったなと。APIの解説がされているサイトをみて一気にコーディングできたらよいのですが...。まずは経験して慣れることが大切ですね。
ちなみに、ProtoOut Studioの授業後から制作を開始し、Qiita公開まで6時間程度かかりました。
javascriptのデバッグ不慣れなのと、Qiita記事執筆をもう少し効率化できそうなので、もう少し頑張ってプロトアウトのサイクルを短くしてきたいなと思っています。ほんとに余談ですが、ProtoOut Studioの一環で進めているので、よくわからないエラーが発生して困っていると講師の方々に相談できます。校長である菅原のびすけさんに直接コーディング指南していただいてものすごく緊張していました(;'∀')
今回はぐるなびAPIを利用しましたが、他にも猫の種類検索APIこの猫なに猫?やHarry Potter APIなど気になるAPIが沢山あります(^^♪
今後はこのLINE botをベースに、connpass APIやGoogle MAP APIなどと連携して、勉強会やイベントのお知らせと夕食を提案してくれるようなサービスにしていけたら面白そうかなと思っています。
参考
ソースコード
'use strict'; const axios = require('axios'); const express = require('express'); const line = require('@line/bot-sdk'); const PORT = process.env.PORT || 3000; const config = { channelSecret: 'LINE botのチャンネルシークレット', channelAccessToken: 'LINE botのアクセストークン' }; const app = express(); app.get('/', (req, res) => res.send('Hello LINE BOT!(GET)')); //ブラウザ確認用(無くても問題ない) app.post('/webhook', line.middleware(config), (req, res) => { console.log(req.body.events); //ここのif文はdeveloper consoleの"接続確認"用なので後で削除して問題ないです。 if(req.body.events[0].replyToken === '00000000000000000000000000000000' && req.body.events[1].replyToken === 'ffffffffffffffffffffffffffffffff'){ res.send('Hello LINE BOT!(POST)'); console.log('疎通確認用'); return; } Promise .all(req.body.events.map(handleEvent)) .then((result) => res.json(result)); }); const client = new line.Client(config); async function handleEvent(event) { var url var ramen_url; var hitnum; // 位置情報のみに入力制限 if (event.type !== 'message' || event.message.type !== 'location') { return Promise.resolve(null); } // 取得した位置情報をログに表示 console.log(event.message.latitude + ' : ' + event.message.longitude); // ぐるなびAPIを使うためのURLに経緯を加える url = 'https://api.gnavi.co.j p/RestSearchAPI/v3/?keyid=af2b4862cbe801c5e2e1b87ef52881f6&latitude=' + event.message.latitude + '&longitude=' + event.message.longitude + '&range=2&freeword=ラーメン'; const encodeUrl = encodeURI(url); // ぐるなびAPIに問い合わせ const response = await axios.get(encodeUrl); //レスポンスの中からcategory"ラーメン"を探索 for(var num = 0; num <= response.data.rest.length; num++){ if(response.data.rest[num].category === 'ラーメン'){ // console.log(response.data.rest[num]); hitnum = num; ramen_url = response.data.rest[num].url_mobile; break; } } // ヒットしたラーメン店の住所をLINE botに返す return client.replyMessage(event.replyToken, { type: 'location', title: response.data.rest[hitnum].name, address: response.data.rest[hitnum].address, latitude: response.data.rest[hitnum].latitude, longitude: response.data.rest[hitnum].longitude }); } app.listen(PORT); console.log(`Server running at ${PORT}`);
- 投稿日:2020-03-29T12:03:28+09:00
新型コロナウイルス感染症相談ボットの作成
概要
普段は耳鼻咽喉科の開業医をしています。
新型コロナウイルス感染患者数が増えていますね。発熱や風邪症状が出ると心配になるかと思います。各自治体に「帰国者・接触者相談センターセンター」があり相談の目安が政府から公表されているのは皆さんもご存じかと思います。それとは別に新型コロナウイルス感染症が流行している国や地域に行った方などが発熱や風邪症状が出た場合は医療機関を受診する前にまず「帰国者・接触者相談センター」に連絡することになっています。
このあたりが少しややこしいので『ボットの質問に答えていくと「帰国者・接触者相談センター」に連絡すべきかどうかがわかるLINE Bot」を作成しました。
適切な電話相談のタイミングが分かり、感染の拡大や重症化の防止、相談センターの方の負担軽減につながればいいなと思っています。
完成動画
新型コロナウイルス感染症相談ボット pic.twitter.com/YIQ6SosOMe
— 病気のセルフチェック (@Selfcheckhealt1) March 29, 2020作成方法
政府や自治体からの新型コロナウイルスに関する発表、所轄の保健所や学会からの連絡を基にプログラムを作成しました。開発者(医師)自身の考えはプログラムには組み込んでいません。
新型コロナウイルスの感染が認められている国・地域の情報は外務省のホームページに、
国内のクラスターマップは厚生労働省のホームページ(地方自治体の報道発表等に基づき新型コロナウイルス厚生労働省対策本部が集計した速報値に基づくもので、随時更新される)にリンクさせました。
電話相談の基準を満たしているならば連絡先が表示され、そうでない場合は自宅安静やかかりつけ医への相談が推奨されます。
※このボットは「発熱や風邪症状がある方」を対象としています。こちらのBotのコードをいじって作ったので変数名が一部そのままになってます。
耳年齢を判定するLINE Bot×Iotの作成index.js'use strict'; require('dotenv').config(); const express = require('express'); const line = require('@line/bot-sdk'); const PORT = process.env.PORT || 3000; const config = { channelSecret: "自分のchannelSecret", channelAccessToken: "自分のchannelAccessToken" }; const app = express(); app.use(express.static('public')); app.get('/', (req, res) => res.send('Hello LINE BOT!(GET)')); //ブラウザ確認用(無くても問題ない) app.post('/webhook', line.middleware(config), (req, res) => { console.log(req.body.events); //ここのif分はdeveloper consoleの"接続確認"用なので削除して問題ないです。 if(req.body.events[0].replyToken === '00000000000000000000000000000000' && req.body.events[1].replyToken === 'ffffffffffffffffffffffffffffffff'){ res.send('Hello LINE BOT!(POST)'); console.log('疎通確認用'); return; } Promise .all(req.body.events.map(event=>handleEvent(event,req))) .then((result) => res.json(result)); }); const client = new line.Client(config); function handleEvent(event, req) { // console.log(req); if (event.type !== 'message' || event.message.type !== 'text') { return Promise.resolve(null); } // LINE botのプログラム let ans = ""; let question = event.message.text; let hz; let yes = ""; let no = ""; if (question == "開始") { hz = 1; ans = "発熱または風邪症状がありますか?"; yes = "発熱:はい"; no = "発熱:いいえ"; } else if (question == "発熱:はい") { hz = 2; ans = "新型コロナウイルス感染症であることが確定した人と濃厚接触歴がありますか?"; yes = "感染者濃厚接触:はい"; no = "感染者濃厚接触:いいえ"; } else if (question == "感染者濃厚接触:いいえ") { hz = 3; ans = "発症前14日以内に新型コロナウイルス感染症の流行が確認されている国や地域に行きましたか?"; yes = "流行地域:はい"; no = "流行地域:いいえ"; } else if (question == "流行地域:いいえ") { hz = 4; ans = "発症前14日以内に新型コロナウイルス感染症の流行が確認されている国や地域に行った人と濃厚接触歴がありますか?"; yes = "流行地域、濃厚接触:はい"; no = "流行地域、濃厚接触:いいえ"; } else if (question == "流行地域、濃厚接触:いいえ") { hz = 5; ans = "発症前14日以内にライブハウス、スポーツクラブ、介護施設、老人ホーム、お祭り、クルーズ船など感染者が多発した施設あるいはイベントに行きましたか?"; yes = "施設、イベント:はい"; no = "施設、イベント:いいえ"; } else if (question == "施設、イベント:いいえ") { hz = 4; ans = "発症前14日以内に感染者が多発した施設あるいはイベントに行った人と濃厚接触歴がありますか?"; yes = "施設、濃厚接触:はい"; no = "施設、濃厚接触:いいえ"; } else if (question == "施設、濃厚接触:いいえ") { ans = "以下の(1)または(2)の症状が2日以上続いていますか?(1)風邪の症状や37.5度以上の発熱(解熱剤を飲み続けなければならない場合も同様)、(2)強いだるさや息苦しさ"; yes = "2日以上:はい"; no = "2日以上:いいえ"; } else if (question == "2日以上:はい") { ans = "以下のいずれかに当てはまりますか?高齢である、糖尿病・心不全・呼吸器疾患(COPD等)の基礎疾患がある、透析を受けている、免疫抑制剤や抗がん剤等を用いている、妊娠中である"; yes = "基礎疾患:はい"; no = "基礎疾患:いいえ"; } else if (question == "基礎疾患:いいえ") { ans = "以下の(1)または(2)の症状が4日以上続いていますか?(1)風邪の症状や37.5度以上の発熱(解熱剤を飲み続けなければならない場合も同様)、(2)強いだるさや息苦しさ"; yes = "4日以上:はい"; no = "4日以上:いいえ"; } else if (question == "発熱:いいえ" || question == "2日以上:いいえ" || question == "4日以上:いいえ") { hz = 0; ans = "自宅で安静にするか、かかりつけ医にご相談ください。"; } else if (question == "感染者濃厚接触:はい" || question == "流行地域:はい" || question == "流行地域濃厚接触:はい" || question == "流行地域、濃厚接触:はい" || question == "施設、イベント:はい" || question == "施設、濃厚接触:はい") { hz = 6; ans="医療機関は受診せず、「船橋市新型コロナウイルス感染症相談センター(電話:047-409-3127)【聴覚障害などにより電話での相談が難しい方は、FAX:047-409-2952】」に連絡してください。"; } else if (question == "基礎疾患:はい" || question == "4日以上:はい") { hz = 7; ans = "「船橋市新型コロナウイルス感染症相談センター(電話:047-409-3127)【聴覚障害などにより電話での相談が難しい方は、FAX:047-409-2952】」に相談してください。"; } else if (question == "電話") { hz = 7; ans = "「船橋市新型コロナウイルス感染症相談センター(電話:047-409-3127)【聴覚障害などにより電話での相談が難しい方は、FAX:047-409-2952】」に相談してください。"; } let title; let url; if (hz == 1) { url = "「はい」か「いいえ」どちらかをお選びください"; } else if (hz == 2) { url = "※濃厚接触とは、以下のような状況です : 感染者と同居、車内や航空機内等で、2メートル以内での長い時間の会話 / マスクや手袋などの個人防護具なしでの感染者の看護・介護 / 感染者の唾や鼻水、体液等に直接触れた"; } else if (hz == 3) { title = "流行国・地域はこちら" url = "https://www.anzen.mofa.go.jp/covid19/country_count.html"; } else if (hz == 4) { url = "※濃厚接触とは、以下のような状況です : 同居、車内や航空機内等で、2メートル以内での長い時間の会話 / マスクや手袋などの個人防護具なしで看護・介護 / 唾や鼻水、体液等に直接触れた"; } else if (hz == 5) { title = "クラスターマップはこちら" url = "https://www.mhlw.go.jp/content/10900000/000609647.pdf"; } else if (hz == 6 || hz == 7) { title = "全国の相談センターはこちら" url = "https://www.mhlw.go.jp/stf/seisakunitsuite/bunya/kenkou_iryou/covid19-kikokusyasessyokusya.html"; } if (hz == 0) { return client.replyMessage(event.replyToken, { type: 'text', text: ans }); } if (hz==1||hz == 2||hz==4) { return client.replyMessage(event.replyToken, [ { "type": "template", "altText": "this is a confirm template", "template": { "type": "confirm", "text": ans, "actions": [ { "type": "message", "label": "はい", "text": yes }, { "type": "message", "label": "いいえ", "text": no } ] } }, { type: "flex", altText: "追加情報", contents: { type: "bubble", body: { type: "box", layout: "vertical", spacing: "md", contents: [ { type: "text", text:url, wrap: true, color: "#aaaaaa", size: "lg" } ] } } } ]); } if (hz==3||hz==5) { return client.replyMessage(event.replyToken, [ { "type": "template", "altText": "this is a confirm template", "template": { "type": "confirm", "text": ans, "actions": [ { "type": "message", "label": "はい", "text": yes }, { "type": "message", "label": "いいえ", "text": no } ] } }, { type: "flex", altText: "Flex Message", contents: { type: "bubble", footer: { type: "box", layout: "vertical", spacing: "sm", contents: [ { type: "button", action: { type: "uri", label: title, uri: url }, height: "sm", style: "link" } ] } } } ]); } if (hz==6||hz == 7) { console.log("+++++++++++++電話+++++++++++") return client.replyMessage(event.replyToken, [ { type: 'text', text: ans }, { type: "flex", altText: "Flex Message", contents: { type: "bubble", footer: { type: "box", layout: "vertical", spacing: "sm", contents: [ { type: "button", action: { type: "uri", label: title, uri: url }, height: "sm", style: "link" } ] } } } ]); } else { return client.replyMessage(event.replyToken, [ { "type": "template", "altText": "this is a confirm template", "template": { "type": "confirm", "text": ans, "actions": [ { "type": "message", "label": "はい", "text": yes }, { "type": "message", "label": "いいえ", "text": no } ] } }, ]); } } (process.env.NOW_REGION) ? module.exports = app : app.listen(PORT); console.log(`Server running at ${PORT}`);考察
現在、連絡先の電話やFaxの番号は医院がある自治体(船橋市)の「帰国者・接触者相談センターセンター」のものになっています。船橋市在住でない方はこちらには電話しないようお願いいたします。他の地域のユーザーのため全国の相談センターが出ている厚生労働省のホームページをリンクしていますが、ユーザーの位置情報や自宅住所を入力するとユーザー居住地の「帰国者・接触者相談センター」の電話番号などが分かるようにできれば他の自治体の方も使いやすいものができるのではないかと思っています。やり方ご存じの方は教えていただけると嬉しいです。
ボットのURLとQRコード
- 投稿日:2020-03-29T09:26:25+09:00
Node.jsを用いて、ORMにSequelize, DBはRDS(MySQL)という構成でApollo Serverを用いて、GraphQL Serverを構成する【勉強メモ】
最初に
これはNode.js環境で, ORMにはSequelize, DBにはMySQLを使った構成で、Apollo Serverを用いてGraphQLサーバを構築してみた際の備忘録となります。
と言っても自身でイチから、これらの構成を構築していったわけではなく、下記のチュートリアルを参照しながらの勉強メモとなります。
(ちょうど同じ構成での例を探していたところ、丁寧に書かれていた下記のドキュメントを見つけました。ありがたい)How To Set Up a GraphQL Server in Node.js with Apollo Server and Sequelize
ちなみにこのドキュメントの中では、sqlite3を用いているので、そこはこちらでMySQLに置き換えて実践しています。
また細かなところで適宜アレンジを施しています。大枠自体は変わらないので、この構成(
Node.js
,Sequelize
などをApollo Server
と組み合わせる)に興味ある方は、直接ドキュメントを読まれることをおすすめします。
(というかこのポストを読み進めていく場合は、上のDigitalOcean
のチュートリアル記事とセットで読んでいくことをおすすめします)自身で実際に実装したコードはGithubに置いています。
shinshin86/graphql-recipe-serverSequelizeを用いたDB関連のセットアップ
まずはDB関連のセットアップから行っていきます。
(ここらへんApollo Server
というよりはSequelize
の基本的なセットアップの流れになります)インストール&初期化
必要なライブラリをインストール
yarn add sequelize sequelize-cli mysql2Sequelize関連の初期化処理を実施
yarn sequelize initテーブルの作成
次に必要なmodelとmigrationを作成・実施していきます。
まずはUserテーブルから作成
yarn sequelize model:create --name User --attributes name:string,email:string,password:string作成した項目は空の入力を許可しないようにするなどの設定を行います。
※ここについては参照先の記述(Step 2 — Creating Models and Migrations
)を参照してください。次にRecipeテーブル
yarn sequelize model:create --name Recipe --attributes title:string,ingredients:text,direction:textこちらも同じ用にmodelとmigrationの内容を編集していきます。
また、ここでuserId
の追加も実施しています。
※ここについても参照先の記述(Step 2 — Creating Models and Migrations
)を参照してください。userId: { allowNull: false, type: Sequelize.INTEGER.UNSIGNED, },ここにはレシピを作成したユーザIDが格納され、後々レシピを作成したユーザ情報を取得するために使われます。
associateの設定
modelも編集したら、UserとRecipeでそれぞれassociateの設定を行っていきます。
// models/user.js User.associate = function(models) { User.hasMany(models.Recipe) };// models/recipe.jsから抜粋 Recipe.associate = function(models) { Recipe.belongsTo(models.User, { foreignKey: 'userId' }) };DBに対する文字コード関連の設定を記述する
また、上記
migration
とmodels
にはDBに対する文字コードの設定も忘れないように記述します。
これを忘れると、日本語で入力した場合に文字コード関連でエラーになります。
(実はすっかり忘れていて、日本語を使ってエラーになったりしていました)
migration
ファイルの場合、queryInterface.createTable
の第3引数に下記の内容をセットします。{ charset: 'utf8', collate: 'utf8_general_ci', }また
model
ファイルの場合は、sequelize.define
の第3引数に下記の記述をセットします。{ charset: 'utf8', collate: 'utf8_general_ci', }migrationの実施
すでにローカルにMySQLは立ち上がっているものとします。
Sequelizeはセットアップしたデフォルトの状態だと
password: null
でアクセスするようになっているかと思いますが、流石にそれは現実味がない気がしたので、一応形だけですが、
root
というユーザ名、password
というパスワードで接続するように
config/config.json
に記述しました。
なので、DB自体もそのような設定で動かしています。
(これも現実味ないといえばないですが...)ちなみに自身はローカルで動くDocker上に、最新のMySQLを立ち上げて、そちらを使っていきます。
下記のコマンドでDBの作成・migrationを実施していきます
yarn sequelize db:create yarn sequelize db:migrateGraphQL Serverを作成する
ここからが本番です。
必要なライブラリをインストールしていきます。
apollo-server
はgraphql
に依存するため、こちらも併せてインストールしています。
またbcryptjs
はユーザのパスワードをハッシュ化するために使用します。yarn add apollo-server graphql bcryptjs次に
src
ディレクトリを作成し、必要なファイルを作成していきます。mkdir src nv src/index.js
ソース自体は参照元の
Step 3 — Creating the GraphQL Server
を参照してもらうとして、下記のcontext: { models }
でmodels側とのつなぎ込みを行っているようでした。const server = new ApolloServer({ typeDefs, resolvers, context: { models } })ちなみにGraphQLには
Query
,Mutations
,Subscriptions
がありますが、このチュートリアルではQuery
,Mutations
に焦点が当てられています。schemaを作成していく(GraphQL)
次に
src/schema.js
を作成していきます。
記述を見ると、設定したassociateを反映させた構成になっているのが分かります。type User { id: Int! name: String! email: String! recipes: [Recipe!]! } type Recipe { id: Int! title: String! ingredients: String! direction: String! user: User! }queryは3つ設定されているようです。
- user IDを引数にしてユーザ情報を取得するもの
- すべてのレシピを取得するもの
- recipe IDを引数にしてレシピ情報を取得するもの
type Query { user(id: Int!): User allRecipes: [Recipe!]! recipe(id: Int!): Recipe }
mutation
は2つ設定されています。
name, email, password
を引数にしてユーザを作成するものuserId, title, ingredients, direction
を引数にしてレシピを作成するものtype Mutation { createUser(name: String!, email: String!, password: String!): User! createRecipe( userId: Int! title: String! ingredients: String! direction: String! ): Recipe! }resolverを設定していく(GraphQL)
上に書いたqueryに対応する実際の処理がresolverには書かれています。
実際にどういうロジックが動くのかはこちらを見れば、大体イメージがつくかと思います。
resolver
内に実際に書かれるロジックは、特にGraphQL
的なものというのはそれほどなく、実際の取得ロジックなどが書かれる形となるので(今回で言えば、Sequelizeを用いたデータ作成・取得処理など)、結構すぐに馴染める印象でした。例えば
MutationのcreateUser
の場合ならば、下記のように実装されています。async createUser(root, { name, email, password }, { models }) { return models.User.create({ name, email, password: await bcrypt.hash(password, 10), }); },Sequelize v5ではfindByIdではなくfindByPkとなる(余談)
ちなみに現時点で
sequelize
の最新のversionをインストールした場合、sequelizeのv5系
がインストールされるかと思います。
v5
ではsequelizeに実装されていたfindById
は廃止されfindByPk
に移行しています。
参照しているドキュメントではfindById
で書かれているので、 ここはfindByPk
に書き換える必要があります。
(sequelize v4
系をインストールした場合は書き換える必要はありません)diff --git a/src/resolvers.js b/src/resolvers.js index 7372e74..0cacc9b 100644 --- a/src/resolvers.js +++ b/src/resolvers.js @@ -3,13 +3,13 @@ const bcrypt = require('bcryptjs'); const resolvers = { Query: { async user(root, { id }, { models }) { - return models.User.findById(id); + return models.User.findByPk(id); }, async allRecipes(root, args, { models }) { return models.Recipe.findAll(); }, async recipe(root, { id }, { models }) { - return models.Recipe.findById(id); + return models.Recipe.findByPk(id); }, }, Mutation: {ApolloServerのcontextについて
第3引数として
models
が渡っていますが、これはsrc/index
でcontextにmodels
を指定すると渡せるようです。const models = require('../models'); const server = new ApolloServer({ typeDefs, resolvers, context: { models }, });例えば下記のようなコードを書いて、該当の箇所のコードを動かすと、
{ hoge: 'hogehoge' }
がログとして出力されます。diff --git a/src/index.js b/src/index.js index e1049b7..133f156 100644 --- a/src/index.js +++ b/src/index.js @@ -6,7 +6,7 @@ const models = require('../models'); const server = new ApolloServer({ typeDefs, resolvers, - context: { models }, + context: { models, hoge: 'hogehoge' }, }); server diff --git a/src/resolvers.js b/src/resolvers.js index 0cacc9b..3bc1d24 100644 --- a/src/resolvers.js +++ b/src/resolvers.js @@ -8,7 +8,8 @@ const resolvers = { async allRecipes(root, args, { models }) { return models.Recipe.findAll(); }, - async recipe(root, { id }, { models }) { + async recipe(root, { id }, { models, hoge }) { + console.log({hoge}) return models.Recipe.findByPk(id); }, },他に書き残しとくべき箇所というのも、あまりないのですが、
下記のuser.getRecipes()
,recipe.getUser()
などはSequelize
側での処理になります。
associate
で設定しているゆえに、こういう形で取得ができます。User: { async recipes(user) { return user.getRecipes(); }, }, Recipe: { async user(recipe) { return recipe.getUser(); }, },これで、動かすための必要な実装はすべて完了です。
Apollo Playgroundで実際に試してみる
下記コマンドでsevrerを起動します。
node src/index.js # もしくは "yarn start"
http://localhost:4000/
にアクセスすると、親しみやすいApolloのPlaygroundが表示されます。とりあえず
user
を作成して見ようと思います。mutation { createUser ( name: "テストユーザ1" email: "text@example.com" password: "password" ) { id name email } }すると下記のような反応が返ってきます。
{ "data": { "createUser": { "id": 1, "name": "テストユーザ1", "email": "text@example.com" } } }サーバのログを見ると、SQLが発行されているのも確認できます。
Executing (default): INSERT INTO `Users` (`id`,`name`,`email`,`password`,`createdAt`,`updatedAt`) VALUES (DEFAULT,?,?,?,?,?);次にレシピを作成します。
先ほど作成したテストユーザ1に紐づくレシピを作成します。mutation { createRecipe( userId: 1 title: "サンプルレシピ1" ingredients: "Salt, Pepper" direction: "Add salt, Add pepper" ) { id title ingredients direction user { id name email } } }下記のようなレスポンスが返ります。
{ "data": { "createRecipe": { "id": 1, "title": "サンプルレシピ1", "ingredients": "Salt, Pepper", "direction": "Add salt, Add pepper", "user": { "id": 1, "name": "テストユーザ1", "email": "text@example.com" } } } }サーバのログには下記のようなSQLが発行されているのが確認できます。
INSERT INTO `Recipes` (`id`,`title`,`ingredients`,`direction`,`createdAt`,`updatedAt`,`userId`) VALUES (DEFAULT,?,?,?,?,?,?);また、同時レスポンス時に必要となるSQLが発行されているのも分かります。
SELECT `id`, `name`, `email`, `password`, `createdAt`, `updatedAt` FROM `Users` AS `User` WHERE `User`.`id` = 1;User作成時は
SELECT
は発行されていませんでしたが、今回は紐づくユーザ情報も返す必要があるため、SELECT
クエリを発行する必要があったということかと想像します
(ソースはまだ読んでいません)queryについてはあまりここに書かなくても、結構情報はある気がしたので、ざっくりと。
query { allRecipes { id title user { name } } }ちなみに上のようなqueryを発行した場合、SQL的には下記のように返ってくるようです。
SELECT `id`, `title`, `ingredients`, `direction`, `createdAt`, `updatedAt`, `userId`, `UserId` FROM `Recipes` AS `Recipe`; SELECT `id`, `name`, `email`, `password`, `createdAt`, `updatedAt` FROM `Users` AS `User` WHERE `User`.`id` = 1;テーブルjoinして取得するような動きではありませんが、別にそういうオプションがあるのか、実装的にそういう形になっているのかは今後調べていくこととします。
結果は下記の通り。{ "data": { "allRecipes": [ { "id": 1, "title": "サンプルレシピ1", "user": { "name": "テストユーザ1" } } ] } }ざっくりとではありますが、以上勉強メモとなります。
参照
How To Set Up a GraphQL Server in Node.js with Apollo Server and Sequelize
- 投稿日:2020-03-29T00:10:28+09:00
Mac Nodebrewを用いてNode.jsをインストールした時にエラーが出た話
目的
- Nodebrewを用いたNode.jsインストール時に出たエラーを解決した話をまとめる
実施環境
- ハードウェア環境
項目 情報 備考 OS macOS Catalina(10.15.3) ハードウェア MacBook Air (11-inch ,2012) プロセッサ 1.7 GHz デュアルコアIntel Core i5 メモリ 8 GB 1600 MHz DDR3 グラフィックス Intel HD Graphics 4000 1536 MB エラー内容
- Nodebrewを使用してNode.jsの最新の安定バージョンをインストールしようとした時に下記のエラーが発生した。
実行コマンドをとエラー内容を下記に記載する。
$ nodebrew install-binary stable Fetching: https://nodejs.org/dist/v12.16.1/node-v12.16.1-darwin-x64.tar.gz Warning: Failed to create the file Warning: /Users/ユーザ名/.nodebrew/src/v12.16.1/node-v12.16.1-darwin-x64.tar.gz: Warning: No such file or directory curl: (23) Failed writing body (0 != 1016) download failed: https://nodejs.org/dist/v12.16.1/node-v12.16.1-darwin-x64.tar.gzエラー原因
- 原因は単純でインストール先のディレクトリがみなさんのPCに存在しないためインストールできないと言われている。
解決方法
下記コマンドを実行してインストール用のディレクトリを作成する。
$ mkdir ~/.nodebrew/srcもう一度問題のコマンドを実行する。
$ nodebrew install-binary stable Fetching: https://nodejs.org/dist/v12.16.1/node-v12.16.1-darwin-x64.tar.gz ###################################################################################################################### 100.0% Installed successfully正常に実行された。