- 投稿日:2020-02-16T23:20:09+09:00
htmlを介さずにjsonから自動でコンテンツを作って更新するスクリプト
こんにちは、wattak777 です。
最近、必要に迫られてNode.jsを嗜むようになったのですが、とある時に「jsonで記載された元データをロードしてコンテンツとして表示し、ユーザがそのコンテンツを更新するとそのjsonを更新する」みたいなことをしようとしてNode.jsでソース一本で出来ないか?と考えたサンプルです。
ファイル構成は以下。
+
├ server.js
└ db.jsonserver.jsvar express = require('express') ; var url = require('url') ; var app = express() ; var json_write = { param1_label: '', param1_value: '', param2_label: '', param2_value: '' } app.get('/update_param', function(req, res) { console.log(" GET" ) ; var url_parse = url.parse(req.url, true) ; console.log(" search:%s", url_parse.search) ; json_write.param1_label = url_parse.query.param1_label ; json_write.param1_value = url_parse.query.param1_value ; json_write.param2_label = url_parse.query.param2_label ; json_write.param2_value = url_parse.query.param2_value ; var json = JSON.stringify(json_write, null, ' ' ) ; var fs = require('fs') ; fs.writeFile('./db.json', json, err => { if (err) { res.sendStatus(500) ; } else { res.sendStatus(200) ; } }) ; }) ; const setJson_JS = '<script type="text/javascript">\n' + ' function setJson() {\n' + ' var sethttp = new XMLHttpRequest() ;\n' + ' var url_str = "http://自らのIPアドレス:50000/update_param?" ;\n' + ' var elem = document.getElementById("param1_label") ;\n' + ' var url_str = url_str + "param1_label=" + encodeURI(elem.innerText) + "&" ;\n' + ' var elem = document.getElementById("param1_value") ;\n' + ' var url_str = url_str + "param1_value=" + encodeURI(elem.value) + "&" ;\n' + ' var elem = document.getElementById("param2_label") ;\n' + ' var url_str = url_str + "param2_label=" + encodeURI(elem.innerText) + "&" ;\n' + ' var elem = document.getElementById("param2_value") ;\n' + ' var url_str = url_str + "param2_value=" + encodeURI(elem.value) ;\n' + '\n' + ' sethttp.open("GET", url_str) ;\n' + ' sethttp.send() ;\n' + ' }\n' + '</script>\n' ; const templ_input = '<p><input type="button" value="close" onClick="Javasctipt:history.back()"></p>\n' + '<p><input type="button" value="send" onClick="setJson()"></p>\n' ; app.get('/view_param', function(req, res) { console.log(" GET" ) ; var fs = require('fs') ; var readJson = fs.readFileSync('jsondb/db.json') ; const jsonObj = JSON.parse( readJson ) ; var html_text = '<p><span id="param1_label">' ; html_text += jsonObj.param1_label ; html_text += '</span>:<input type="text" size="100" name="param1" id="param1_value" value="' ; html_text += jsonObj.param1_value + '"></p>\n' ; html_text += '<p><span id="param2_label">' ; html_text += jsonObj.param2_label ; html_text += '</span>:<input type="text" size="100" name="param2" id="param2_value" value="' ; html_text += jsonObj.param2_value + '"></p>\n' ; html_text += templ_input ; res.writeHead(200, {'Content-Type' : 'text/html'}) ; res.write(setJson_JS + html_text) ; res.end() ; }) ; var server = app.listen(50000, function() { console.log("listening at port %s", server.address().port) ; });db.json{ "param1_label": "param1_label", "param1_value": "param1 def value.", "param2_label": "param2_label", "param2_value": "param2 def value." }これを使ってnodeで起動させ、ブラウザより、
http://立てたIPアドレス:50000/view_param
とすると画面が表示され、更新後にjsonが更新されることが出来ました。
- 投稿日:2020-02-16T23:17:17+09:00
TypeError: validator is not a functionが解決しない
とりあえず初心者向けの書籍を一冊購入。
とにかく写経でもいいから一通りのアプリを作成したいと動きをメモしつつとりあえず書き上げた。
さあ動かしてみようしかしコンソールログに出たのは・・・
TypeError: validator is not a functionいやいや読み込んでますけど汗
var validator = require('express-validator'); // ~~~~~~ // (中略) // ~~~~~~ app.use(validator());※本当はvar使っちゃいけないけどとりあえず写経なので、
あとサンプルのソースと比較しやすいためこのままにしてますどこが違うのか比較しよう
ソースは同じ汗
となると、インストールしたモジュールのバージョンがちがうのか?
以下自分のpackage.json
package.json{ "name": "mini-board-2", "version": "0.0.0", "private": true, "scripts": { "start": "node ./bin/www" }, "dependencies": { "bookshelf": "^1.1.0", "cookie-parser": "~1.4.4", "debug": "~2.6.9", "ejs": "~2.6.1", "express": "~4.16.1", "express-session": "^1.17.0", "express-validator": "^6.4.0", "http-errors": "~1.6.3", "knex": "^0.20.9", "morgan": "~1.9.1", "mysql": "^2.18.1" } }以下サンプルのpackage.json
package.json{ "name": "mini-board-2", "version": "0.0.0", "private": true, "scripts": { "start": "node ./bin/www" }, "dependencies": { "bookshelf": "^0.13.3", "cookie-parser": "~1.4.3", "debug": "~2.6.9", "ejs": "~2.5.7", "express": "~4.16.0", "express-session": "^1.15.6", "express-validator": "^5.2.0", "http-errors": "~1.6.2", "knex": "^0.14.6", "morgan": "~1.9.0", "mysql": "^2.15.0" } }全然違う
バージョンをそろえて、npm install
通りました
$ npm start > mini-board-2@0.0.0 start C:\Users\user\Desktop\mini_board_2 > node ./bin/www GET / 302 118.593 ms - 56 GET /users 200 179.510 ms - 1242 GET /stylesheets/style.css 200 22.919 ms - 753 GET /favicon.ico 302 5.750 ms - 28 GET /users 304 9.479 ms - -原因は?
最初にいろんなパッケージをインストールする際、最新のパッケージをインストールしたためではないか。
本自体は昨年の5月に改訂されたがいろいろググるうちに自分と同じエラーを質問している方がいて、書き方が古いですよーみたいなことを言われている。
たしかに、ほかに出していたpaginationのエラーも今はこういう書き方ですよーみたいな内容の文章があった。最新のNode.js情報を追いかけなければ。
参考文献・サイト
・掌田 津耶乃 (2019/5/10)Node.js超入門[第2版] 秀和システム
・https://qiita.com/maitake9116/items/7825d90c09f3e2f87dea
・https://stackoverflow.com/questions/56711444/typeerror-validator-is-not-a-function-after-installing-and-requiring-express追伸
terat●ilで同じエラー質問したやりとりみてて安定のググレカス。
このサイトで回答するひとってこういうタイプ多くない?
- 投稿日:2020-02-16T22:28:03+09:00
node.js(express)のパーシャルとは何なのか
パーシャルとは
レンプレート内から更に読み込んで使われる、テンプレート内の小さな部品を”パーシャル”と呼びます。
node.jsのテンプレートはejsなどがあります。パーシャルの例
<tr> <th><%= key %></th><td><%= val %></td> </tr>これはテーブル内の一部の例で
これをテンプレートで読み込んで使います。パーシャルの使い方
テンプレート内で下のように書いて読み込みます。
<%- incluede('パーシャルのファイル名',{受け渡す値}) %>実際の例
<%- incluede('data_item',{key:key, val:data[key]) %>
- 投稿日:2020-02-16T21:43:18+09:00
node.js(express)のbody-parserを理解する
bodyParserとは、
HTML(ejs)のformのinputに入力された値を受け取れるようにするものです。
例↓
<form action="/" method="post"> <p> <input type="text" name="message"> <input type="submit" value="送信"> </p> </form>この例の場合は、
<input type="text" name="message">の値を取得します。
inputの値をどう取得するの?
inputにnameを指定して、javascriptで
req.body.[inputのnameに指定した値]というように書く事でinputの値を受け取れるようになります。
上の例のnameにmessageを指定した例ですと、下のように書けば値を受け取れます。
req.body.messagebody-parserをインストールしなければ、上のコードを書いてもエラーになります。
body-parserを使った簡単なアプリの例
html(ejs)でフォームを作成します。
index.html<!DOCTYPE html> <html lang="ja"> <head> <meta http-equiv="content-type" content="text/html" charset="UTF-8"> <title><%=title %></title> <link type="text/css" href="./style.css" rel="stylesheet"> </head> <body> <head> <h1><%=title %></h1> </head> <div role="main"> <p><%-content %></p> <form action="/" method="post"> <p><input type="text" name="message"> <input type="submit" value="送信"></p> </form> </div> </body> </html>このフォームを送信したら(input type="submitのボタンを押したら)、actionへ移動します。
index.jsvar express = require('express'); var ejs = require("ejs"); var app = express(); app.engine('ejs', ejs.renderFile); app.use(express.static('public')); var bodyParser = require('body-parser'); app.use(bodyParser.urlencoded({extended: false})); // ※トップページ app.get('/', (req, res) => { var msg = 'This is Express Page!<br>' + '※メッセージを書いて送信して下さい。'; res.render('index.ejs', { title: 'Index', content: msg, }); }); // ※POST送信の処理 app.post('/', (req, res) => { var msg = 'This is Posted Page!<br>' + 'あなたは「<b>' + req.body.message + '</b>」と送信しました。'; res.render('index.ejs', { title: 'Posted', content: msg, }); }); var server = app.listen(3000,() => { console.log('Server is runnning!'); })javascriptでpost送信されたあと、
body-parserを使ってreq.body.messageで値を受け取り、
それを表示させています。作ったアプリ↓
”ありがとう”という値を送信しました。
それを受け取っていることが分かります。終わりに
の212ページ
- 投稿日:2020-02-16T20:29:46+09:00
2020年版 Node.js+Reactのインストール
1.概要
MacbookにNode.jsとReactをインストールし、アプリを開発するための環境を構築するための手順について。
2.前提条件
事前作業
- MacのOSを最新にする
- Xcodeのコマンドラインツールをインストールする
作業日時
- 2020年2月
環境
- MacBook Pro
- macOS Catalina
ソフトウェアのバージョン
ソフトウェア バージョン nodebrew 1.0.1 Homebrew 2.1.11 yarn 1.21.1 3.インストール手順
Node.jsをインストールする
以下の流れでインストールする。
- Homebrewのインストール
- nodebrewのインストール
- Node.jsのインストール
Homebrewのインストール
以下のコマンドでHomebrewをインストールする。
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"nodebrewのインストール
Homebrewを使用してインストールする。
brew install nodebrew
インストール後は以下コマンドでnodebrewのバージョンが確認できます。
nodebrew -v nodebrew 1.0.1
node.jsのインストール
$ mkdir -p ~/.nodebrew/src最新版を取得する際は
$ nodebrew install-binary latest安定版を取得する際は
$ nodebrew install-binary stableインストールされたnodeを有効化
$ nodebrew ls上記、コマンドでインストールされたバージョンが一覧できる。
v7.1.0 current: none
インストール直後はcurrent: noneとなっているため、必要なバージョンを有効化する。
$ nodebrew use v7.1.0もう一度nodebrew lsを試すと
v7.1.0 current: v7.1.0
v7.1.0が設定されました。
参考記事
- MacにNode.jsをインストール
ちなみに、Windowsの場合は、以下のインストーラでインストール可能。
https://nodejs.org/en/download/
yarnのインストール
npmの代わりのパッケージマネージャ。
yarn add
でパッケージをインストールできる。$ npm install -g yarnReactのアプリの作成
以下コマンドでReactアプリのフォルダが作成される。
Typescriptで開発を行うため、--template typescript
のオプションを設定する。npx create-react-app <app name> --template typescript各種ライブラリのインストール
作成したフォルダに移動して、各種ライブラリをインストールする。
$ cd my-react-appReduxのインストール
プロジェクトルートフォルダで以下コマンドを実行する。
reduxはreduxそのもの、react-reduxはreactとreduxをつなぐライブラリです$ yarn add redux react-redux typescript-fsa typescript-fsa-reducerstypescript-fsaはAction側、typescript-fsa-reducersがReducer側で利用するライブラリです。
- typescript-fsa → ActionCreatorを簡単に生成するライブラリ
- typescript-fsa-reducers → Reducerを簡単に作成するためのライブラリ
開発ツール(redux-devtools)のインストール
redux-devtoolsは、Reduxで開発する際に利用できる便利な開発ツールで、アクション実行時のstoreの状態を確認するのに利用する。
これに加えてChromeの拡張機能でRedux DevToolsを追加するとツールでRedux上のStoreの状態が確認でき、非常にデバッグしやすくなります。yarn add --dev redux-devtoolsPrettierのインストール
整形ツールのPrettier(-Dで開発版のみにインストールされる)
設定はこちら
https://qiita.com/awakia/items/3a05edfa135762d7952c# yarn add -D prettierEslintのインストール
# yarn add -D eslint \ @typescript-eslint/eslint-plugin \ eslint-plugin-prettier \ eslint-config-prettierMaterial-ui
Materialデザインを利用するためのパッケージ。
タッチ、タップ、クリックなどのイベントを使うためにreact-tap-event-pluginのインストールも必要。# yarn add material-ui react-tap-event-plugin # yarn add @material-ui/core @material-ui/iconsreact-router-dom
react-routerの代わりにConnected React Routerを使う。
URLに応じて、表示するコンテンツを変更するルーティングを行うためにインストールする。# yarn add react-router-dom connected-react-router # yarn add -D @types/react-router-domdate-fns
dateオブジェクトのライブラリ。
momentより軽いらしい。$ yarn add date-fns4.コマンド
ローカルでサーバーを起動する
以下コマンドでローカルでサーバーを起動できる。
http://localhost:8080/でアクセスできる。$ yarn startビルドする
$ yarn build5.終わりに
- firebaseの設定は別途。
以上。
- 投稿日:2020-02-16T20:23:53+09:00
Node.js + Express + passport.jsでローカルサーバーにBasic認証ページを立てる
経緯
FlutterアプリのWebViewからBasic認証ページの接続テストをしたかったので、サンプルとしてローカルでBasic認証ページを作ることにした。
FlutterのWebViewからBasic認証ページにアクセスする - Qiitaローカルサーバーを立てるだけならVSCodeのLive Serverでサクッとできたのだが、Basic認証のかけ方がいまいちわからなかったため、Node.js + Express構成を使うことにした。
環境はMacでnpmコマンドが使用できる前提。
Passport-HTTPのインストール
認証まわりが簡単に実装できるpassport.jsというライブラリを使ってみた。
Passport-HTTPをインストールする。
$ npm i express passport passport-httpview engine(テンプレートエンジン)には
ejs
を使うので、こちらもインストール。$ npm install --save ejs認証ページの準備
プロジェクトフォルダ直下に
app.js
ファイルを作成。Basic認証をかけ、成功時にそれ用の表示を出すだけの簡素なつくり。app.jsconst express = require('express'); const passport = require('passport'); const passportHttp = require('passport-http'); passport.use(new passportHttp.BasicStrategy( function(username, password, done) { if (username === 'user' && password == 'pass') { return done(null, true); } else { return done(null, false); } } )); const app = express(); app.set("view engine", "ejs"); app.get('/', passport.authenticate('basic', { session: false, }), (req, res) => { res.render("index", {content:"Success"}); }); app.listen(3000, (err, res) => { console.log('server is launched'); });
views
フォルダを作成し、その直下にindex.ejs
を作成する。/views/index.ejs<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title><%= content %></title> </head> <body> <h1><%= content %></h1> </body> </html>今回の目的はWebViewからBasic認証ページの接続確認をしたいだけなので、認証に使う
username
は「user 」、password
は「pass」というガバガバセキュリティで行く。ブラウザで表示確認
ローカルサーバーを起動。
$ node app.js server is launchedブラウザで
localhost:3000
にアクセスすると、ユーザー名&パスワードを聞かれる。
http://localhost:3000/
正しいユーザー名&パスワードを入力して「OK」を押すと
「Success」と表示される。
終わり。
curlコマンドで確認
ちなみにターミナルでcurlコマンド叩いても確認できる。
$ curl -u user:pass http://localhost:3000今回はビューをレンダリングしてるのでhtmlが吐かれる。
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>Success</title> </head> <body> <h1>Success</h1> </body> </html>誤ったユーザー名、パスワードを指定したり、userオプション(
-u
)を指定しなかった場合はUnauthorized
が返ってくるだけ。Unauthorized
- 投稿日:2020-02-16T18:08:09+09:00
雑に知ってしまったDockerを知り直す ~アプリケーション持ち込み編~
おさらい
第2回です。
前回の投稿はこちら
前回はnginxのイメージをpullして来てコンテナ起動。
ポートフォワードしてウェルカムページを表示させるまでやりました。今回やりたいこと
- ローカルで適当にアプリケーションを用意する
- どうにかしてそのアプリケーションをコンテナの中にぶちこむ
- ホスト側から起動したアプリケーションへアクセスできるようにする
アプリケーションを用意
Nodeとgithubを勉強したときに作った、first-stepっていうオシャレ気取った名前のリポジトリが放置されていたので、これを再利用します。
せっかくなので触ったこと無いReactへ転生。この辺のステップは本題と外れてしまうので割愛。
$ npm start
これをどうにかしてコンテナへ持っていきたい。
コンテナを用意
前回はnginxのイメージを利用しましたが、今回はnodeのイメージを利用します。
現在のnodeの最新バージョンはv13.8.0
なんですが、ホストにインストールしてるのは安定版のv12.16.0
なのでこれをインストールしたい。バージョン指定してイメージをpullするのどうやるんだろう・・と思いつつとりあえずイメージを探します。
コマンドでも探せるみたいなんですが、初めてなのでGUIに頼ります。なるほどなるほど、なんかそれっぽいですね。
調べてみるとこれはTagと呼ばれるもので、古いのバージョンのイメージが欲しい場合はこのTagを指定してpullする必要があると。ただ、同じバージョン(v12.16.0)でもstretchとかslimとか更に違いがあるのが気になる。。
で更に調べると、例えばnodeの場合だとバージョンは同じでも無駄なファイルを省いて最適化されたslimバージョンがあったり、Debianのバージョンが異なるもの(stretchとかbusterとかはDebianのコードネーム)があったりと結構細かい違いがあるということが判明。
このあたりもちゃんと意識して適切なイメージを選択する必要があるってことか・・
で、更に気になったのがTag指定しない場合はどうなるんだ?とかlatest指定すると結局どれを引っ張ってくるのよとか、そもそもこれ誰が作ったイメージなん?安全なん?とか・・
TODO: これも色々調べてみたんですけど、今回の趣旨からは外れそうなのとボリューム膨らみそうなので別記事でまたやります。今回はとりあえず名前がかっこいいのでbusterでやります。
$ docker pull node:12.16.0-buster 12.16.0-buster: Pulling from library/node dc65f448a2e2: Pull complete 346ffb2b67d7: Pull complete dea4ecac934f: Pull complete 8ac92ddf84b3: Pull complete a3ca60abc08a: Pull complete ab849ba5abe0: Pull complete b9e0c215e971: Pull complete 766774022603: Pull complete f199c0426b2a: Pull complete Digest: sha256:66d5994de29952fe982729ef7c7f8d4c50d528279db386efbf373451f534fa16 Status: Downloaded newer image for node:12.16.0-buster $ docker image ls REPOSITORY TAG IMAGE ID CREATED SIZE node 12.16.0-buster ce43ce61c1de 28 hours ago 882MB $ docker run [イメージ名:TAG] $ docker run -d --name node-react node:12.16.0-buster 505ba1fdefd2f705f90b083cee3653445256a0917e054147f8251d4e0eeef122 #前回はrunする前にイメージをpullしてなかったのでこのタイミングで色々ダウンロードしてましたが、 今回は事前にpull済なのでローカルのイメージを利用。カシコイ $ docker ps -a CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 8a126cad2450 node:12.16.0-buster "docker-entrypoint.s…" 5 seconds ago Exited (0) 4 seconds ago node-reactで、ここで作成したコンテナのSTATUSを見るとUp(起動中)ではなく、Exitedになっちゃってる。
前回のnginxの場合サーバを立ち上げてUp状態になっていたけど、今回のイメージはあくまでnodeの実行環境を用意するだけなのでコンテナが立ち上がりっぱなしにはならないのかな、となんとなく推測。
環境を構築して後続の命令が無いと、プロセスが死ぬようになっているらしい。起動するアプリケーションも無いのに立ち上げておく意味ないもんね。
-itd
ってオプションつけるとコンテナが立ち上がりっぱなしになるらしいのでお試し。$ docker run -itd -i ホストの入力をコンテナの標準出力につなげる。コンテナ側へ正しくコマンドを渡せるようにする。 -t コンテナの標準出力をホストの標準出力につなげる。ttyをコンテナに割り当てて、対話できるようにする。 -d コンテナに入らずバックグランドで起動する $ docker run -itd --name node-react node:12.16.0-buster 10235f0996b7c1b1e5f47c5b1c36651ff4fe20cd72b14f47dc5a11f0e8607780 $ docker ps -a CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 10235f0996b7 node:12.16.0-buster "docker-entrypoint.s…" 3 seconds ago Up 2 seconds node-reactうんうん、STATUSがUpになってる。
コンテナに入って作業したい場合は、とりあえず-itdって覚えても良いかもしれない。
TODO: 1個づつオプション外して試してみたけど、ここの挙動が上手く理解できていないので別の機会にちゃんと調べる。# コンテナの中に入る $ docker exec -it node-react /bin/bash root@8bf7a6704bb9:/# node -v v12.16.0ちゃんと指定したバージョンで入ってますね
コンテナにアプリケーションをぶちこむ
最初は「ファイル転送でもすれば良いんかなぁ」とかかなり脳筋なこと考えていたんですけど、考えてみたらgithubからcloneしてきて起動すれば良いんじゃね?と思ったのでお試し。
root@8bf7a6704bb9:/# git --version git version 2.20.1git入ってる!勝った
# 適当なディレクトリへ移動して root@8bf7a6704bb9:/# cd /usr/src/ # cloneして root@8bf7a6704bb9:/usr/src# git clone https://github.com/wol-827/first-step.git Cloning into 'first-step'... remote: Enumerating objects: 22, done. remote: Total 22 (delta 0), reused 0 (delta 0), pack-reused 22 Unpacking objects: 100% (22/22), done. root@8bf7a6704bb9:/usr/src# cd first-step/ # アプリ起動に必要なモジュールインストールして root@8bf7a6704bb9:/usr/src/first-step# npm i found 0 vulnerabilities # アプリケーション起動 root@8bf7a6704bb9:/usr/src/first-step# npm start Compiled successfully! You can now view sample-app in the browser. Local: http://localhost:3000/ On Your Network: http://172.17.0.2:3000/ Note that the development build is not optimized. To create a production build, use npm run build.起動できてそう!良い感じ
コンテナ起動時にポートフォワード設定し忘れてしまったので、改めて設定。
アプリケーションは3000番で起動していて、8080へ飛ばしたいのでこんな感じ。$ docker run -itd -p 8080:3000 --name node-react node:12.16.0-busterできた。ウレシイ。
まとめ
Dockerについて調べていると、環境を汚すことなくアプリケーションの実行環境を用意できる、なんでことが書かれているのをよく見るんですが身を持ってそれを実感しました。
ホスト側にDockerイメージだけ用意してあれば、nodeやらReact用のモジュールやらはコンテナの中にだけ入れるのでホスト側はキレイな体を保てると。専用のコンテナを立てるのでnodeのバージョンの切り替えも不要。
イメージはこれで、githubのリポジトリはこれ、コマンド実行手順はこれ、っていう風に準備すれば、とりあえず環境は手に入る。便利便利。とはいえ今回の場合、コンテナの中に入ってcloneしたり色々コマンド実行しているのが冗長。
次はこの辺りをコマンド1発で出来るようにしたいと思います。たぶん出来る。出来ろ。
- 投稿日:2020-02-16T15:52:24+09:00
anyenv&nodenvの環境で、Node.jsのバージョンアップする
Node.jsのバージョンを複数保持し、コマンドひとつでバージョンを切り替えるanyenvを現場で使用しています。
厳密には、anyenv
の中でnodenv
というソフトウェアを動かすことで、Node.js
のバージョンを切り替えているのですが、今回初めての案件で新規作成を手掛けたので、忘れないようにメモとして残しておこうと思います。ついでに、gulpを使用できるようになるところまで、解説します。
開発環境
・anyenvとnodenvでNode.jsのバージョン管理をしている。
・gulp
を使うためにNode.jsを使用。
・既にanyenvとnodenvは導入済み。新規作成のおおまかな手順
・anyenvやnodenvを使用して、Node.jsのバージョンを最新にする
・npmのバージョンを最新にする
・gulpってコマンドを使用できるようにする
↓ 確認
・npm init -y
・npm install -D gulp(ここでnpm auit fixが出なかったら素敵)
・gulp -vやってみる
実際にファイルを新規作成して作ってみます。
Node.jsのバージョンを最新にする
nodenvのバージョンは自動的にアップデートされないので、anyenvを使用して、アップデートが可能なファイルがあるかを確認します。
(node.jsに最新のバージョンがあったとしても、nodenvに反映されない)以下のコマンドで、nodenvを最新のバージョンにします。
anyenv updatenodenvを最新にすることで、新しいバージョンのNode.jsも使用することができるようになっているかと思います。
以下のコマンドで、使用できるNode.jsのバージョンを確認してみましょう。nodenv install —listNode.jsの公式サイトをみたら、最新のバージョン・推奨バージョンを確認できます。
nodenv install {バージョン} 例)nodenv install 12.14.1これで、anyenvとnodenvによる、Node.jsのバージョンアップが終わりました。
お疲れ様です!このままでは、gulpのコマンドが使用できない(以下のようなエラーが出てしまう)ので、gulpのコマンドを使用できるように設定します。
nodenv: gulp: command not found The `gulp' command exists in these Node versions: 8.10.0 9.2.0 10.9.0↓
npm i -g gulp-cliこのコマンドを打ち込めば、gulpコマンドが使えるようになります◎
あとは、確認のために、package.jsonファイルの作成
npm init -ygulpをインストールして
npm install -D gulpgulpのバージョンを確認すれば終了です!
gulp -vここ以降は、自分で好きなgulpの設定をして、サクサク動かしていきましょう!
おまけ
nodenv local現在のプロジェクトのnodenvのバージョンがわかる。
nodenv local {バージョン}これで、node.jsのバージョンの指定が出来ます◎
まとめ
Qiitaの記事を参考にしました!と連絡してくれた方がいて、とてもうれしかったです。この記事も誰かのお役に立てたら嬉しいです。
参考サイト
anyenvとnodenvの導入方法:https://www.to-r.net/media/anyenv/
- 投稿日:2020-02-16T15:05:11+09:00
どこ住み?てかSlackやってる?生後10ヶ月のエンジニアが爆速10時間でChatbot制作!
[前置き]
閲覧ありがとうございます!エンジニア歴10ヶ月のひよっこwebでべろぱーです。
本投稿はエンジニア歴3年未満程度の初学者向け投稿になります。
個人で何か作りたいけど、丁度いい規模感のアイデアが思い付かない...
そんなあなたに!お手軽なChatbotの制作をオススメします!
というわけで作ってみましたー!↓↓作ったもの -What I made-
??地域の飲食店検索Slackチャットボット「GourmetNav」 ??
Slack上で動作するチャットボット。
「駅名」+ 「空白」 + 「店名に関わるキーワード」を入力すると、その地域の飲食店名とお店のURLを飛ばしてくれる(σ・ω・)σ
おもしろいところ
- 地図を見ずに、駅名となんとなくの店名だけで検索できる。Slackで!!
- チェーン店も、もちろん対象。目指せ、地域のスタバマスター!٩(。˃ ᵕ ˂ )وイェーィ♪
なにができるの
- 「GourmetNav」の検索結果から、ワンクリックで各飲食店のサイトに飛べる。
- わざわざブラウザで色んなページを見に行ったりしなくていい。
- 検索結果にたまーに混じっている別の地域のお店、とかがない(←ここ重要!)
技術的な話
TL;DR (大まかなデータの流れ)
Slack × GAS × 駅情報取得API × ぐるなびAPI を使用しています。
- Slackのチャンネル「GourmetNav」から、「駅名」+ 「半角(または全角)空白」 + 「店名に関わるキーワード」を入力、送信。
- Slackから受け取った入力情報をGAS上で参照。
- 駅情報取得APIを用いて、入力情報内の「駅名」から緯度・経度を取得。
- ぐるなびAPIを用いて、上記の緯度・経度からその地域の飲食店を検索、一覧にしてGoogle Spreadsheetに出力(2度目以降はシートを初期化して更新するのでデータは保持しない)
- 生成されたSpreadsheet内の飲食店一覧から、「店名に関わるキーワード」と部分一致する「飲食店名」とその「サイトURL」を取得。
- 取得した「飲食店名」とその「サイトURL」をSlack側に出力。
- Slackのデータ容量圧迫しないように投稿を毎日自動削除するようにトリガーを設定(任意)
具体的な面倒くさいところ
1. まず、ローカルでの開発環境構築(任意)
ある程度のコード量/ロジックを書くならば、GASをブラウザ上のスクリプトエディタでベタ書きするのは、間違いなくNonsenseなんじゃないかと思います。(知らんけど)
(参考): Google Apps Script をローカル環境で快適に開発するためのテンプレートを作りました
(著者): @howdy39※型定義ないとデバックしづらいのでTypeScriptも入れました(任意)
今回デバッグしていて初めて気付いたんですけど、Javascriptって配列にtypeofかけても「object」って返されるんですね。なんて暗黒言語!!
競技プログラミング(Atcoder)でC#愛好家の私には拒絶反応が出たので、typescript導入しました。
(参考): clasp + TypeScriptで課題改善botを作った
(著者): @mochisuna
(参考): JavaScriptの型などの判定いろいろ
(著者): @amamamaou2. GASでSlackからの入力情報を受け取り、参照し、出力する(必須)
開発環境を整備したら、まずはSlackとBotによる簡単な入出力を実装する。
下記の記事を参考にすると、以下のようなことができる。
✔️GASでPOSTリクエストを受け取る
✔️SlackでOutgoing WebHookの設定を行う
✔️GASでSlackから渡された値を参照する
(参考): Slack上のメッセージをGoogleAppsScriptで受け取ってよしなに使う
(著者): @kyo_nanba※このとき、SlackAppというライブラリを使用することで設定が少し楽になる
世の中のSlack愛好家の皆さんに圧倒的感謝!
SlackApp作成者@soundTricker さんに三跪九叩頭しましょう。
(参考): Slack BotをGASでいい感じで書くためのライブラリを作った3. 駅情報取得API, ぐるなびAPIを上記の中に組み込んで出力内容を工夫する
この部分に関しては、記事読んで実践するだけですんなり実装できました。
(参考): GoogleAppsScriptでぐるなびAPIから取得した駅周辺のお店をスプレッドシートに書き出す
(著者): @kouheidev4. 上記参考記事を悪魔合体させながら論理構造を調整
今回は社内Slack上で作成したこともあり、一旦ソースコードは開示しません。
(まだまだ生後10ヶ月のエンジニア、トークンの扱いとかGASの仕様とか、セキュリティ面に関しては完璧に理解してるわけじゃないので!)
ただまあ、ベテランの皆さんなら多分苦もなく実装できると思います。(・ω・。)
苦労したところ
無限ループ??
GAS上でのテストでは問題ないのに、
Slackで実際に入出力をテストすると無限ループが頻繁に発生しました。(恐ろしい...)
暫くして気付いたのですが、下記のロジックによるものでした。
ユーザの「A」という検索ワードに対して、「GourmetNav」は該当する検索結果を見つけてきます。
そして、「Aが見つかりました!」とSlackに出力してくれるわけです。
この出力を、GAS側は「ユーザによる新しい入力」として検知し、再び検索/出力処理を繰り返すわけです。
なるほど〜〜それはそうだわ! ということで、
doPost内の記述に下記を加えることで無限ループは解消します!if (e.parameter.user_name == "slackbot"){ throw new Error("this is bot."); //入力者がbotだった場合、エラーを返します }まとめ
Slackからのデータ入力をSpreadsheetに出力だとか、
Spreadsheet側の入力をSlackに出力するだとか、
こうした一方通行なBotならQiita上にも数多くあります。ただ、今回作成したのはInteractiveなChatBot。
ユーザ入力に対して、GAS側で入力を検知、
各API内を検索、Spleadsheetに検索結果を出力、
そして、ユーザの求めている情報にマッチする情報のみをユーザに提供する。こうしたInteractiveなSlackチャットボット制作記事は、決して多くはないかと思います。
GASでSlack向けに開発している方にとって、何かの一助となれば幸いです。ついったフォローしてね。(・ω・。)
@NadjaHarold
- 投稿日:2020-02-16T15:05:11+09:00
どこ住み?てかSlackやってる?生後10ヶ月のエンジニアによる爆速10時間Chatbot制作総まとめ!!
作ったもの -What I made-
??地域の飲食店検索Slackチャットボット「GourmetNav」 ??
Slack上で動作するチャットボット。
「駅名」+ 「空白」 + 「店名に関わるキーワード」を入力するとがその地域の飲食店名とお店のURLを飛ばしてくれる(σ・ω・)σ
おもしろいところ
- 地図を見ずに、駅名となんとなくの店名だけで検索できる。Slackで!!
- チェーン店も、もちろん対象。目指せ、地域のスタバマスター!٩(。˃ ᵕ ˂ )وイェーィ♪
なにができるの
- 「GourmetNav」の検索結果から、ワンクリックで各飲食店のサイトに飛べる。
- わざわざブラウザで色んなページを見に行ったりしなくていい。
- 検索結果にたまーに混じっている別の地域のお店、とかがない(←ここ重要!)
技術的な話
TL;DR (大まかなデータの流れ)
Slack × GAS × 駅情報取得API × ぐるなびAPI を使用しています。
- Slackのチャンネル「GourmetNav」から、「駅名」+ 「半角(または全角)空白」 + 「店名に関わるキーワード」を入力、送信。
- Slackから受け取った入力情報をGAS上で参照。
- 駅情報取得APIを用いて、入力情報内の「駅名」から緯度・経度を取得。
- ぐるなびAPIを用いて、上記の緯度・経度からその地域の飲食店を検索、一覧にしてGoogle Spreadsheetに出力(2度目以降はシートを初期化して更新するのでデータは保持しない)
- 生成されたSpreadsheet内の飲食店一覧から、「店名に関わるキーワード」と部分一致する「飲食店名」とその「サイトURL」を取得。
- 取得した「飲食店名」とその「サイトURL」をSlack側に出力。
- Slackのデータ容量圧迫しないように投稿を毎日自動削除するようにトリガーを設定(任意)
具体的な面倒くさいところ
1. まず、ローカルでの開発環境構築(任意)
ある程度のコード量/ロジックを書くならば、GASをブラウザ上のスクリプトエディタでベタ書きするのは、間違いなくNonsenseなんじゃないかと思います。(知らんけど)
(参考): Google Apps Script をローカル環境で快適に開発するためのテンプレートを作りました
(著者): @howdy39※型定義ないとデバックしづらいのでTypeScriptも入れました(任意)
今回デバッグしていて初めて気付いたんですけど、Javascriptって配列にtypeofかけても「object」って返されるんですね。なんて暗黒言語!!
競技プログラミング(Atcoder)でC#愛好家の私には拒絶反応が出たので、typescript導入しました。
(参考): clasp + TypeScriptで課題改善botを作った
(著者): @mochisuna
(参考): JavaScriptの型などの判定いろいろ
(著者): @amamamaou2. GASでSlackからの入力情報を受け取り、参照し、出力する(必須)
開発環境を整備したら、まずはSlackとBotによる簡単な入出力を実装する。
下記の記事を参考にすると、以下のようなことができる。
✔️GASでPOSTリクエストを受け取る
✔️SlackでOutgoing WebHookの設定を行う
✔️GASでSlackから渡された値を参照する
(参考): Slack上のメッセージをGoogleAppsScriptで受け取ってよしなに使う
(著者): @kyo_nanba※このとき、SlackAppというライブラリを使用することで設定が少し楽になる
世の中のSlack愛好家の皆さんに圧倒的感謝!
SlackApp作成者@soundTricker さんに三跪九叩頭しましょう。
(参考): Slack BotをGASでいい感じで書くためのライブラリを作った3. 駅情報取得API, ぐるなびAPIを上記の中に組み込んで出力内容を工夫する
この部分に関しては、記事読んで実践するだけですんなり実装できました。
(参考): GoogleAppsScriptでぐるなびAPIから取得した駅周辺のお店をスプレッドシートに書き出す
(著者): @kouheidev4. 上記参考記事を悪魔合体させながら論理構造を調整
今回は社内Slack上で作成したこともあり、一旦ソースコードは開示しません。
(まだまだ生後10ヶ月のエンジニア、トークンの扱いとかGASの仕様とか、セキュリティ面に関しては完璧に理解してるわけじゃないので!)
ただまあ、ベテランの皆さんなら多分苦もなく実装できると思います。(・ω・。)
苦労したところ
無限ループ??
GAS上でのテストでは問題ないのに、
Slackで実際に入出力をテストすると無限ループが頻繁に発生しました。(恐ろしい...)
暫くして気付いたのですが、下記のロジックによるものでした。
ユーザの「A」という検索ワードに対して、「GourmetNav」は該当する検索結果を見つけてきます。
そして、「Aが見つかりました!」とSlackに出力してくれるわけです。
この出力を、GAS側は「ユーザによる新しい入力」として検知し、再び検索/出力処理を繰り返すわけです。
なるほど〜〜それはそうだわ! ということで、
doPost内の記述に下記を加えることで無限ループは解消します!if (e.parameter.user_name == "slackbot"){ throw new Error("this is bot."); //入力者がbotだった場合、エラーを返します }まとめ
Slackからのデータ入力をSpreadsheetに出力だとか、
Spreadsheet側の入力をSlackに出力するだとか、
こうした一方通行なBotならQiita上にも数多くあります。ただ、今回作成したのはInteractiveなChatBot。
ユーザ入力に対して、GAS側で入力を検知、
各API内を検索、Spleadsheetに検索結果を出力、
そして、ユーザの求めている情報にマッチする情報のみをユーザに提供する。こうしたInteractiveなSlackチャットボット制作記事は、決して多くはないかと思います。
GASでSlack向けに開発している方にとって、何かの一助となれば幸いです。ついったフォローしてね。(・ω・。)
@NadjaHarold
- 投稿日:2020-02-16T11:30:57+09:00
Expressを習得するためにやったこと(随時更新)
Node.jsのフレームワーク、Expressを習得するためにやったことをここにまとめていきます。(随時更新!)
前提知識
Expressを使うためにはNode.jsのインストールが必要
※参考:初めてのNode.js:インストール確認、REPL、Hello worldまで
REST APIとは何か
※参考:REST APIとは何かを調べまくったらようやくイメージができてきたのでまとめた
Express事始め
まず、Expressとはなんなのか。JSフレームワークとの役割の違い。
※参考:https://www.i-ryo.com/entry/2020/02/13/083521
次はExpressでハロワ(Hello World)する予定。
随時更新していきます!
- 投稿日:2020-02-16T11:30:57+09:00
Expressを習得するためにやったことまとめ(随時更新)
Node.jsのフレームワーク、Expressを習得するためにやったことをここにまとめていきます。(随時更新!)
前提知識
Expressを使うためにはNode.jsのインストールが必要
※参考:初めてのNode.js:インストール確認、REPL、Hello worldまで
REST APIとは何か
※参考:REST APIとは何かを調べまくったらようやくイメージができてきたのでまとめた
Express事始め
まず、Expressとはなんなのか。JSフレームワークとの役割の違い。
※参考:https://www.i-ryo.com/entry/2020/02/13/083521
次はExpressでハロワ(Hello World)する予定。
随時更新していきます!
- 投稿日:2020-02-16T10:52:47+09:00
THE TYPESCRIPT WORKSHOPをやる時のハマりどころ・注意どころ
概要
THE TYPESCRIPT WORKSHOPをやった時に感じた、ハマりどころ・注意どころをメモしました。
Node.jsの相互互換性問題
JavaScriptでlambdaを書く部分を以下のように修正しないと
SyntaxError
がでます。
lambdaの設定で、runtime: lambda.Runtime.NODEJS_10_X
としているので、Node.jsが`ES Moduleのsyntaxを理解できないことが原因です。Before
hitcounter.jsimport { DynamoDB, Lambda } from 'aws-sdk';After
hitcounter.jsconst { DynamoDB, Lambda } = require('aws-sdk');DynamoDBは
cdk destroy
で削除されない気をつけましょう。
これはデフォルトの挙動で、RemovalPolicy
がRETAIN
になっているためです。
(確かにデフォルトの挙動で削除されたら恐いですし)以下のようにすることで
cdk destroy
でDnynamoDBも削除できるようになります。cdk-workshop-stack.tsconst table = new dynamodb.Table(this, 'Hits', { partitionKey: { name: 'path', type: dynamodb.AttributeType.STRING }, removalPolicy: cdk.RemovalPolicy.DESTROY });
- 投稿日:2020-02-16T09:37:03+09:00
Mac で nodebrew が動かない ( #node )
brewが使えないというひどい罠が。
Error
nodebrew list No such file or directory at /usr/local/bin/nodebrew line 575.Resolve
https://github.com/hokaccha/nodebrew#install
brew uninstall nodebrewcurl -L git.io/nodebrew | perl - setupexport PATH=$HOME/.nodebrew/current/bin:$PATHnot installed current: none nodebrew lsOriginal by Github issue
- 投稿日:2020-02-16T05:17:43+09:00
Alexaスキルの開発 0から公開まで
初めて Alexaスキル の開発を始めてから公開するまでの流れについて。
フローチャートを作成する
事前に Alexaにどう発話したらどう分岐するかをまとめたフローチャートを作っておくとフローが整理できて開発が捗るので以下のような感じで作成します。(黒塗り多くてすみません)
alexa developer console への登録
amazon alexaにアクセスし、アカウントを作成してログインします。
「スキルの作成」ボタンを押し、好きなスキル名を入力して作成。スキルを作成した後、ビルドタブの左メニューから「エンドポイント」を選択し、表示される スキルID を覚えておきましょう。
AWS Lambdaの利用
作成
AWSにログインしてLambdaにアクセスし、関数の作成ボタンを押します。
関数名を入力し、ランタイムは今回は「Node.js」を選択し、作成。
(自分が開発していた当時は東京リージョンだと必要な機能が揃っておらず、オレゴンリージョンを利用しました)Alexa Skill Kit の追加
次にLambdaの作成した関数の画面にて、「トリガーを追加」で「Alexa Skill Kit」を選択し、スキルIDに先程 alexa deevloper console でコピーしたスキルIDを貼り付けて追加します。
ARNをメモ
画面右上の ARN 値を覚えておきます。
再び alexa developer consoleに戻ってスキル設定
alexa developer console と Lambdaの紐付け
alexa developer console にて、スキルを選択したあとのビルドタブの左メニューから「エンドポイント」を選択し、
「Aws LambdaのARN」を選択して、「デフォルトの地域」に先程メモした ARN を貼り付けて「エンドポイントを保存」。呼び出し名の設定
ビルドタブの「呼び出し名」をクリックし、なんという呼びかけでスキルが起動するかを設定します。
インテントの追加
ビルドタブのインテントの追加をクリックし、今回のスキルでAlexaが受け付ける可能性のある全発話をインテントとして作成していきます。例えば、
インテント名 発話例 RecommendIntent 「おすすめを教えて」「おすすめ見せて」「おすすめ開いて」 SelectNumberIntent 「{number}で」「{number}でお願い」「{number}がいい」 SelectCategoryIntent 「{category}にする」「{category}が見たい」「{category}を見せて」 といった感じ。編集が終わったらモデルのビルドをすることで反映されます。
- 各インテントにて、できる限り発話の揺らぎを網羅するようにします。足りていないとAmazonからのレビューで指摘されます。
- 上記
{number}
や{category}
のように、一部を変数化して、複数の発話を受け付けることも可能。変数のパターンは別途 スロットタイプ に登録します。{number}など一部の変数は既に用意されています。- 肯定/否定の返事をするインテントや「前へ」「次へ」などのインテントは既にビルトインとして用意されているものがあるので、独自に追加せずにそれをビルトインインテントとして追加します。
- CancelIntent (取り消し)、HelpIntent (ヘルプ)、StopIntent (中止)などいくつかのインテントはルール上、必ず実装しないといけない模様です。
Cloud9と連携する
LambdaとCloud9を連携させることで、Cloud9の高度なIDE上でAlexaスキル用の関数をコーディングすることができます。Cloud9を使わずにLambda内で書くことも可能ですが、Cloud9の方が使いやすくておすすめ。
環境の作成
AWSからCloud9を選択し、「Create Environment」を押して、名前などを適当に入力して環境を作成します。
Lambdaと同じリージョンで作成していれば、少し前に作成したLambda関数が右ペインのAWS Resources
のRemote Functions
に表示されているので、それをインポートすることでコーディングできます。
コーディングが終わったらLocal Functions
を Deployすれば Lambdaに反映可能。必要なパッケージのインストール
必要なパッケージをCloud9上のbashコンソールでインストールしておきます。
cd package.json が入ったディレクトリ npm install ask-sdk --saveまずは、返事をするだけのスキルを作ってみる
Cloud9上に index.js を作成します。
どのディレクトリに置くかは、Cloud9でどの範囲をインポートしたかによるので一概に言えませんが、
もし package.json があればそれと同じ階層に作成します。const Alexa = require('ask-sdk'); /** * あいさつ */ const GreetingHandler = { canHandle(handlerInput) { const request = handlerInput.requestEnvelope.request; // 初回起動時 return request.type === 'LaunchRequest'; }, async handle(handlerInput) { return handlerInput.responseBuilder .speak('今日もおはようございます。') .withShouldEndSession(true) .getResponse(); }, }; /** * 全てマッチしなかった場合 */ const FallbackHandler = { canHandle() { return true; }, handle(handlerInput) { return handlerInput.responseBuilder .speak('申し訳ありません。もう一度お話しください。') .reprompt('もう一度お話しください。') .getResponse(); }, }; const ErrorHandler = { canHandle() { return true; }, handle(handlerInput, error) { return handlerInput.responseBuilder .speak('エラーが発生しました。もう一度お話しください。') .reprompt('もう一度お話しください。') .getResponse(); }, }; exports.handler = Alexa.SkillBuilders.standard() .addRequestHandlers( GreetingHandler, FallbackHandler ) .addErrorHandlers(ErrorHandler) .lambda();とりあえずこんな感じ。GreetingHandlerが今回作ったメインのハンドラー。
開発は主に、各イベントや発話ごとのハンドラーを作成し、それをexports.handlerに登録していくことになります。
Handlerで必須なのは、canHandle()
とhandle()
で、前者はどのようなイベントや発話を受け取ったときにそのハンドラーを起動するかを設定し、handle()
ではユーザーに画面や発話などを返したり裏で行う処理などをコーディングしていきます。ハンドラーが複数あった場合、
addRequestHandlers()
内で指定した順にcanHandle()
での判定が行われ、最初にマッチしたハンドラーが起動します。
上記例の様に、どのハンドラーの条件にもマッチしなかった場合に必ず受け取るFallbackHandlerを作っておくと良さげ。(switch
文のdefault
みたいなイメージ)デプロイ
コーディングが終わったら、Cloud9の右ペインの
AWS Resources
で作成したLocal Functions
を選択して↑
(Deploy)ボタンを押せば、Lambdaに反映されます。作ったスキルのテスト
エミュレーター上でテスト
alexa developer consoleの「テスト」タブに移動し、入力欄に呼び出し名 (少し前の項目で設定した名前) を入力するとスキルが起動して、「今日もおはようございます」と返ってくることを確認できました。
今はまだ作り込んでいないので、これですぐにスキルが終了してしまいますが、この後の作り込みでもうちょっとやり取りができるスキルを作っていきます。実端末でテスト
Alexa にアクセスし、アカウントを作って実端末と紐付けを行います。
ウィザードにしたがって進んでいくことで紐付けは完了します。有効なスキル - 開発スキル に行くと、今開発中のスキルが表示されるので、ここでAlexaに開発中のスキルをインストールできます。(ここは記憶が定かでないので間違っていたらすみません)
あとは、実端末で「アレクサ、○○」と話しかければスキルが起動します。ログを確認
スキルがうまく動作しなかった場合は、AWSのCloudWatchでログを確認できます。
CloudWatchのロググループをクリックすると作ったスキルが表示されるので、そこをクリックすることでログを確認できます。スキルの公開
さて、まだ現時点では公開できる内容のスキルではありませんが、全体の流れを追うため、一旦スキルの作り込みは後回しにし、スキルの作り込みが終わった前提でスキルの公開までの流れを書いていきます。
alexa developer consoleの「公開タブ」にアクセスし、公開設定をおこなっていきます。
質問項目が多岐に渡りますが、最低限、必須項目だけでも頑張って埋めていきます。
全て入力が終わり、何か問題があれば指摘を受けますので修正します。
問題なければ「実行」ボタンを押すことで、自動テストが実行されます。
問題なければ、Amazonスタッフに検証依頼を出すことになります。
その後数日以内にフィードバックがありますので、修正等のやり取りを行い、最終的に問題なければ無事公開されます。バージョン管理を行う
無事公開はできましたが、このままだとスキルを編集して反映すると即本番のスキルに影響が出てしまいます。
それだと問題があるので、Lambdaのエイリアス機能を使って、本番用と開発用を別々に管理していきます。エイリアスの作成
Lambdaにアクセスし、「アクション」→「新しいバージョンを発行」で、今の最新版に対してバージョン名をつけます。名前は「1.0.0」など好きな名前をつけます。
次に「エイリアスの作成」で本番用のエイリアスを作成します。名前は「prod」など適当につけ、先程作ったバージョン番号を選択します。スキルの向き先を変える
次に今作ったエイリアスのARNをコピーし、alexa developer consoleの「ビルド」タブの「エンドポイント」の「デフォルトの地域」の値を上書きします。
これで、今開発中のスキルは本番のエイリアスを向いていることになります。再度、本番公開
この状態で、本番公開を進めれば、スキルは本番用のエイリアスを向いた状態でリリースされます。
その後またエンドポイントを$LATEST
のARNに戻すことで、開発中のスキルは最新の状態を向きますが、本番スキルはエイリアスを向いたままとなり、プログラムを更新しても本番は影響を受けずにすみます。その後の運用
スキルを更新する場合は、再度新しいバージョンを作ってエイリアスをそのバージョンに向け直せば、本番のスキルを更新できます。インテントの修正が無ければ再度Amazonに依頼を出す必要はありません。(インテントの修正がある場合は毎回依頼が必要)
少し面倒な点として、再度Amazonに依頼を出す際、開発中のスキルを本番用エイリアスに向けた状態で依頼を出す訳にもいきませんが、かといって何らかのエイリアスに向けておかないと本番公開されたときにエイリアスを向かなくなってしまい困ってしまいます。
したがって、もう一つ本番用のエイリアス (prod2) を作成し、そこに向けた状態で依頼を出します。
そうすることで、本番公開後、prod2に向いた状態でスキルが公開されます。
今後は、prodとprod2を交互に切り替えて依頼していくことになります。(もっと良い方法があれば…)もうちょっと作り込みしてみる
問いかける
handle()の最後で以下のようなレスポンスを返すことで、ユーザーに問いかけを発信して、応答を待つことができます。
handle(handlerInput) { // ~略~ 色々な処理 // 問いかけ return handlerInput.responseBuilder .speak('あなたは男性ですか?女性ですか?') .reprompt('あなたは男性ですか?女性ですか?') .getResponse(); }なお、文章をAlexaが正しく読んでくれないときは、
'<phoneme alphabet="x-amazon-ja-jp" ph="オオダ\'シ">大田市</phoneme>'という感じの文をspeak()に入れることで正しく読んでくれます。「
\'
」は日本語アクセントが低音に移る点を指定。問いかけ後、返事を受け取る
canHandle()で受け取りたいインテントをキャッチすれば、そのハンドラーのhandle()が起動します。
以下の例は、肯定の応答を受けたときに起動する設定。canHandle(handlerInput) { const request = handlerInput.requestEnvelope.request; // 「はい」と応答されたとき if (request.type === 'IntentRequest' && request.intent.name === 'AMAZON.YesIntent') { return true; } return false; } handle(handlerInput) { // 「はい」と応答されたときの処理 }会話の進捗によって処理を変える
例えば、「はい」「いいえ」で答える質問を2回に分けて投げかける場合、1つ前の方法だとどっちの質問か区別せずに反応してしまいます。そこで、現在の会話の進捗をセッション変数に保持しておき、その進捗に応じて処理を行います。
// 最初の質問 const FirstQuestionHandler = { canHandle(handlerInput) { const request = handlerInput.requestEnvelope.request; // 初回起動時 return request.type === 'LaunchRequest'; } handle(handlerInput) { // セッション変数に現在の質問の位置を記憶 const attributesManager = handlerInput.attributesManager; const attributes = attributesManager.getSessionAttributes(); attributes.state = 'first'; return handlerInput.responseBuilder.speak('あなたは男性ですか?').reprompt('あなたは男性ですか?').getResponse(); } } // 2つ目の質問 const SecondQuestionHandler = { canHandle(handlerInput) { const request = handlerInput.requestEnvelope.request if (request.type === 'IntentRequest') { const attributesManager = handlerInput.attributesManager; const attributes = attributesManager.getSessionAttributes(); // 1つ目の質問の後のとき if (attributes.state && attributes.state === 'first') { // 「はい」か「いいえ」で答えたとき if (['AMAZON.YesIntent', 'AMAZON.NoIntent'].includes(request.intent.name)) { return true; } } } return false; } handle(handlerInput) { // セッション変数に現在の質問の位置を記憶 const attributesManager = handlerInput.attributesManager; const attributes = attributesManager.getSessionAttributes(); attributes.state = 'second'; return handlerInput.responseBuilder.speak('あなたは20歳以上ですか?').reprompt('あなたは20歳以上ですか?').getResponse(); } } // 結果 const ResultHandler = { canHandle(handlerInput) { const request = handlerInput.requestEnvelope.request if (request.type === 'IntentRequest') { const attributesManager = handlerInput.attributesManager; const attributes = attributesManager.getSessionAttributes(); // 2つ目の質問の後のとき if (attributes.state && attributes.state === 'second') { // 「はい」か「いいえ」で答えたとき if (['AMAZON.YesIntent', 'AMAZON.NoIntent'].includes(request.intent.name)) { return true; } } } return false; } handle(handlerInput) { // 処理 } }問いかけ時に画面に情報を表示する
handle()のレスポンスでテンプレート機能を使うことで画面に情報を表示させることができます。
テンプレートはいくつか種類があるようです。
また、画面タッチにも対応させることができます。
ただし画面表示に対応していない端末を考慮して処理を分岐させるといいでしょう。handle(handlerInput) { // 画面表示に対応している場合 if (handlerInput.requestEnvelope.context.System.device.supportedInterfaces.Display) { const viewport = handlerInput.requestEnvelope.context.Viewport; // 画面が丸い端末の場合 const isRound = viewport && viewport.shape === 'ROUND'; handlerInput.responseBuilder.addRenderTemplateDirective({ type: 'BodyTemplate2', // 今回は背景画像とその上に文字を表示するテンプレートを選択 token: 'token', backButton: 'HIDDEN', image: isRound ? new Alexa.ImageHelper().addImageInstance('https://画像URL').getImage() : null, backgroundImage: new Alexa.ImageHelper().addImageInstance('https://画像URL').getImage(), title: 'タイトル', textContent: new Alexa.RichTextContentHelper().withPrimaryText( '文章文章文章<br /><action token="detail">説明を聞く</action>' ).getTextContent(), }); } }上記で設置した「説明を聞く」ボタンのタッチは以下のようにcanHandle()内で検知することができます。
if (request.type === 'Display.ElementSelected' && request.token === 'detail') {スワイプして一覧の中から選べるようにする
スワイプして複数の商品から選択する、といったテンプレートもあります。
const template = { type: 'ListTemplate2', token: 'string', title: '選択してください。', backButton: 'HIDDEN', listItems: [] }; template.listItems.push({ token: 'detail_1', image: new Alexa.ImageHelper().addImageInstance('https://画像URL').getImage(), textContent: new Alexa.RichTextContentHelper().withPrimaryText('<font size="2">商品1</font>').getTextContent() }); } template.listItems.push({ token: 'detail_2', image: new Alexa.ImageHelper().addImageInstance('https://画像URL').getImage(), textContent: new Alexa.RichTextContentHelper().withPrimaryText('<font size="2">商品2</font>').getTextContent() }); } handlerInput.responseBuilder.addRenderTemplateDirective(template); return handlerInput.responseBuilder .speak('商品一覧を表示しています。画面を右へスワイプすると、すべての商品を見ることができます。商品の詳細を聞くには、画面にタッチするか、番号をおっしゃってください。') .reprompt('商品を選択してください。') .getResponse();発話時に一緒にカードでメッセージを通知する。
return handlerInput.responseBuilder .speak('ありがとうございました。詳しくはお送りしたカードを参照してください。') .withSimpleCard('購入した商品: XXX') .withShouldEndSession(true) // スキルを終了する .getResponse();スロットタイプの値を受け取る
前述のインテント作成の項目のような形でスロットタイプ型のインテントを作成した場合、スロットタイプのどの値が発話されたか、受け取ることができます。
例えば、{number}番目にする
と発話された場合、number
の値を受け取ることができますので、その値に応じて処理を分岐できます。// スロット値が渡された場合 if ((request.intent.slots.category.resolutions && request.intent.slots.number.resolutions.resolutionsPerAuthority[0].values) { const number = request.intent.slots.number.resolutions.resolutionsPerAuthority[0].values[0].value.id; }次回起動時に前回の状態に応じた処理を行う
次回起動時に前回の続きから処理を行ったり、前回どう応答したかに応じて処理を変えたい場合は、
DynamoDB
を利用した、状態を永続的に記憶する機能を使います。exports.handler = Alexa.SkillBuilders.standard().addRequestHandlers( // 略 FallbackHandler ) .addErrorHandlers(ErrorHandler) .withTableName('テーブル名') .withAutoCreateTable(true) .lambda();こんな感じで機能を有効にしつつ、
async handle(handlerInput) { const attributesManager = handlerInput.attributesManager; const persistentAttrs = await attributesManager.getPersistentAttributes(); persistentAttrs.hogehoge = '変数に入れる値'; attributesManager.setPersistentAttributes(persistentAttrs); await attributesManager.savePersistentAttributes(); }で状態を記録します。
await
しないといけないので、メソッドにasync
をつける必要があります。const persistentAttrs = await attributesManager.getPersistentAttributes(); if (typeof persistentAttrs.hogehoge !== 'undefined' && hogehoge === 'X') { return true; }あとは、こんな感じで判定できます。前回終了直前に、今の状態を記憶しておいて、次回起動時、状態が記憶されていれば再開用のハンドラーを呼び出せばいいでしょう。
通常、起動優先度を上げるために、addRequestHandlers
の上位のハンドラーで判定する必要があるでしょう。APIと通信してその結果に応じた処理をする
普通に Axios などを利用すれば良いです。以下、一例。handle()に
async
をつけるのを忘れないようにする必要があります。const axios = require('axios'); // 略 const response = await axios.post('https://APIのURL', parameters, { headers: { Authorization: 'Bearer ' + handlerInput.requestEnvelope.context.System.user.accessToken }, validateStatus: function (status) { return true; } }); if (response.status !== 200) {アカウントリンク機能を利用する
今回は省略し、必要に応じて別記事で書きたいと思います。
Amazon Payでの課金機能を利用する
今回は省略し、必要に応じて別記事で書きたいと思います。
- 投稿日:2020-02-16T02:57:34+09:00
npm audit fixで解決しないとき
随分古いバージョンのパッケージを使っていたシステムで、node.jsをアップデートをしたときに
found 98 vulnerabilities (39 low, 13 moderate, 46 high) in 10622 scanned packages 94 vulnerabilities require semver-major dependency updates. 4 vulnerabilities require manual review. See the full report for details.のような文言が出力されましたので、その対応を行います。
大体の場合は『
npm audit fix --force
』をすれば解決するようですが、今回は一部上手くいかないものがあったので、直接ファイルをイジイジして治すことにします。(この辺はあまり自信がないので、もしかしたら間違えてるかもです)
どのパッケージが問題なのか調べる
まずは、問題の原因であるパッケージの確認を行います。
$ npm audit => ┌───────────────┬──────────────────────────────────────────────────────────────┐ │ High │ Prototype Pollution │ ├───────────────┼──────────────────────────────────────────────────────────────┤ │ Package │ lodash │ ├───────────────┼──────────────────────────────────────────────────────────────┤ │ Patched in │ >=4.17.12 │ ├───────────────┼──────────────────────────────────────────────────────────────┤ │ Dependency of │ kouto-swiss │ ├───────────────┼──────────────────────────────────────────────────────────────┤ │ Path │ kouto-swiss > prefiks > lodash │ ├───────────────┼──────────────────────────────────────────────────────────────┤ │ More info │ https://npmjs.com/advisories/1065 │ └───────────────┴──────────────────────────────────────────────────────────────┘こんな表示になるかと思います。
今回の私の場合だと、lodashなるパッケージのバージョンに問題があったようです。
どうやら Patched in の欄で指定されているバージョンにlodashを変更すれば解決するようです。
では、今入っているlodashのバージョンを確認してみます。
$ npm ls lodash => willjekylltemplate@1.0.0 /home/misato/Documents/Blogs/for-misato-fans ├─┬ browser-sync@1.9.2 │ ├─┬ dev-ip@0.1.7 │ │ └── lodash@4.17.12 │ ├─┬ easy-extender@2.3.4 │ │ └── lodash@4.17.15 │ ├─┬ glob-watcher@0.0.7 │ │ └─┬ gaze@0.5.2 │ │ └─┬ globule@0.1.0 │ │ └── lodash@4.17.12 ├─┬ grunt@0.4.5 │ ├─┬ findup-sync@0.1.3 │ │ └── lodash@4.17.12 │ ├─┬ grunt-legacy-util@0.2.0 │ │ └── lodash@4.17.12 │ └── lodash@4.17.12 └─┬ kouto-swiss@0.11.14 └─┬ prefiks@0.3.3 └── lodash@2.4.2 <= こいつが原因
npm audit
では、lodashの推奨バージョンは >=4.17.12 になっていましたが、npm ls lodash
で見てみると、ひとつだけ 2.4.2 となっています。普通は
npm audit fix
で全てバージョンを調整してくれる?みたいなんですが、なぜかひとつだけバージョンが変更されなかったようです。とりあえず、こいつを直します。
package-lock.json でバージョンを修正
package-lock.jsonで
"lodash": "2.4.2"
を検索すると、下記のような感じでヒットしました。"requires": { "accord": "0.12.0", "gulp-util": "3.0.8", "lodash": "2.4.2", "replace-ext": "0.0.1", "stylus": "0.54.7", "through2": "0.6.5" }これを npm audit で調べた推奨バージョンに書き換える↓
"requires": { "accord": "0.12.0", "gulp-util": "3.0.8", "lodash": "4.17.12", "replace-ext": "0.0.1", "stylus": "0.54.7", "through2": "0.6.5" }後は、下記のコマンドを実行する。
$ sudo rm -r node_module => node_moduleの削除 $ npm install私の環境では、これで解決しました!!
(と言いつつ、結局各パッケージのバージョンアップは諦めて、nodeのバージョンを下げたのですが・・・ソレはまた別の話ですな)
誰かの参考になれば幸いです。
- 投稿日:2020-02-16T00:21:10+09:00
Node.jsでCSVファイルを読みこんでJSONデータに変換するサンプルコード
Node.jsでShift_JIS で書かれたCSVファイルを取り扱う必要があり、その際の備忘メモ。
JSON変換までできた方が便利なので、csvtojson を使ってみました。要約
- csvtojsonを使えば、標準的なCSVファイルを簡単にJSONデータに変更できました。
- Node.jsはファイル読み込みは基本UTF-8を想定しているようですが、UTF-8以外の場合は iconv-liteをかましてShift_JIS→UTF-8変換してあげればOKでした。
- csvtojson はコマンドラインからも呼び出せるので、コマンドラインでCSV→JSON変換できるのはなにげに便利です。
前提や環境
$ sw_vers ProductName: Mac OS X ProductVersion: 10.15.3 BuildVersion: 19D76 $ node --version v10.16.2 $ちなみにサンプルデータは 住所.jp という日本の住所情報を用いてみました。
やってみる
$ git clone --branch 0.1.0 https://github.com/masatomix/csv-sample-node.git $ cd csv-sample-node/ $ npm install落としてきたプロジェクトのディレクトリ構成はこんな感じです。処理対象のcsvファイル(
13tokyo.csv
)だけは、上記サイト から落としておきましょう。$ tree . ├── data │ └── 13tokyo.csv ←読み込むCSVファイル ├── src │ └── index.ts ├── dist │ ├── index.js │ └── index.js.map ├── package.json └── tsconfig.jsonソースコードはこんな感じ。
index.tsimport fs from 'fs' import iconv from 'iconv-lite' import csv from 'csvtojson' /** * 指定したパスのcsvファイルをロードして、JSONオブジェクトとしてparseする。 * 全行読み込んだら完了する Promise を返す。 * @param path */ const parse = (path: string): Promise<any[]> => { return new Promise((resolve, reject) => { let datas: any[] = [] fs.createReadStream(path) .pipe(iconv.decodeStream('Shift_JIS')) .pipe(iconv.encodeStream('utf-8')) .pipe(csv().on('data', data => datas.push(JSON.parse(data)))) // 各行読んだらココが呼ばれるので配列にpush .on('end', () => resolve(datas)) // 全部終わったらココにくるので、resolveする }) } if (!module.parent) { // 呼んでみる parse('./data/13tokyo.csv').then((results: any[]) => { // 郵便番号が「100-000x」のものに絞ってみた results = results.filter(address => address['郵便番号'].startsWith('100-000')) console.table(results) // for (const address of results) { // console.log(address) // } }) }実行してみます。
$ npm run dev > csv-sample-node@0.1.0-SNAPSHOT dev /Users/xxx/git/csv-sample-node > ts-node src/index.ts ┌─────────┬─────────────┬────────┬─────────┬─────────────┬────────────┬────────┬───────┬───────┬──────────┬────────┬────────┬────────┬────────────┬──────────┬───────┬───────┬──────────┬────┬──────┬────────┬───────┬───────┐ │ (index) │ 住所CD │ 都道府県CD │ 市区町村CD │ 町域CD │ 郵便番号 │ 事業所フラグ │ 廃止フラグ │ 都道府県 │ 都道府県カナ │ 市区町村 │ 市区町村カナ │ 町域 │ 町域カナ │ 町域補足 │ 京都通り名 │ 字丁目 │ 字丁目カナ │ 補足 │ 事業所名 │ 事業所名カナ │ 事業所住所 │ 新住所CD │ ├─────────┼─────────────┼────────┼─────────┼─────────────┼────────────┼────────┼───────┼───────┼──────────┼────────┼────────┼────────┼────────────┼──────────┼───────┼───────┼──────────┼────┼──────┼────────┼───────┼───────┤ │ 0 │ '100000000' │ '13' │ '13101' │ '131010000' │ '100-0000' │ '0' │ '0' │ '東京都' │ 'トウキョウト' │ '千代田区' │ 'チヨダク' │ '' │ ' ' │ '(該当なし)' │ '' │ '' │ '' │ '' │ '' │ '' │ '' │ '' │ │ 1 │ '100000400' │ '13' │ '13101' │ '131010006' │ '100-0004' │ '0' │ '0' │ '東京都' │ 'トウキョウト' │ '千代田区' │ 'チヨダク' │ '大手町' │ 'オオテマチ' │ '' │ '' │ '' │ '' │ '' │ '' │ '' │ '' │ '' │ │ 2 │ '100000200' │ '13' │ '13101' │ '131010039' │ '100-0002' │ '0' │ '0' │ '東京都' │ 'トウキョウト' │ '千代田区' │ 'チヨダク' │ '皇居外苑' │ 'コウキョガイエン' │ '' │ '' │ '' │ '' │ '' │ '' │ '' │ '' │ '' │ │ 3 │ '100000100' │ '13' │ '13101' │ '131010045' │ '100-0001' │ '0' │ '0' │ '東京都' │ 'トウキョウト' │ '千代田区' │ 'チヨダク' │ '千代田' │ 'チヨダ' │ '' │ '' │ '' │ '' │ '' │ '' │ '' │ '' │ '' │ │ 4 │ '100000300' │ '13' │ '13101' │ '131010051' │ '100-0003' │ '0' │ '0' │ '東京都' │ 'トウキョウト' │ '千代田区' │ 'チヨダク' │ '一ツ橋' │ 'ヒトツバシ' │ '' │ '' │ '1丁目' │ '01チョウメ' │ '' │ '' │ '' │ '' │ '' │ │ 5 │ '100000500' │ '13' │ '13101' │ '131010055' │ '100-0005' │ '0' │ '0' │ '東京都' │ 'トウキョウト' │ '千代田区' │ 'チヨダク' │ '丸の内' │ 'マルノウチ' │ '' │ '' │ '' │ '' │ '' │ '' │ '' │ '' │ '' │ │ 6 │ '100000600' │ '13' │ '13101' │ '131010057' │ '100-0006' │ '0' │ '0' │ '東京都' │ 'トウキョウト' │ '千代田区' │ 'チヨダク' │ '有楽町' │ 'ユウラクチョウ' │ '' │ '' │ '' │ '' │ '' │ '' │ '' │ '' │ '' │ └─────────┴─────────────┴────────┴─────────┴─────────────┴────────────┴────────┴───────┴───────┴──────────┴────────┴────────┴────────┴────────────┴──────────┴───────┴───────┴──────────┴────┴──────┴────────┴───────┴───────┘ $ちゃんとShift_JISのCSVを読み込めています。
また、CSVの1行目のヘッダ行の文字列をJSONデータのプロパティ名として扱えてますね。簡単です。おまけ: コマンドラインから使ってみる
npmに付属している
npx
コマンドを使うことで、csvtojsonをコマンドラインから呼び出せます。$ npx csvtojson ./data/13tokyo.csv {"�Z��CD":"101841500","�s���{��CD":"13","�s�撬��CD":"13101","����CD":"131010019","�X�֔ԍ�":"101-8415","���Ə��t���O":"1","� $あー Shift_JISはダメですね。ということで
nkf
をかましてみます。$ cat ./data/13tokyo.csv | nkf -S | npx csvtojson {"住所CD":"101006101","都道府県CD":"13","市区町村CD":"13101","町域CD" $
nkf
コマンドは適宜Homebrewなどで入れておきましょう。
おつかれさまでした。関連リンク・ソースコード