- 投稿日:2020-11-29T20:56:28+09:00
gulp実行エラーでnode.jsのバージョンを変更する
gulp実行エラー発生
$ gulpgulpを実行すると下記のエラーが発生した。
ReferenceError: primordials is not definedエラーの意味は、node.jsとgulpのバージョンが合ってないよということ。
ちなみにバージョンは下記
$ node -v v14.8.0 $ gulp -v CLI version: 2.3.0 Local version: 3.9.1 $ npm -v 6.9.0上記エラーの対処方法は2つ。
1.gulpのバージョンを3系から最新の4系に変更する。
2.node.jsのバージョンを下げる今回はnode.jsのバージョンを下げる方法で対応する。
ちなみにnode.jsはhomebrewでインストールしていました。
brew install node.jsここでいろいろ調べてみるとnodebrewという便利なツールがあることを発見。
nodebrewとはnode.jsのバージョンを管理できるツールとのこと。
homebrewでnodebrewをインストール
brew install nodebrew #インストールできているか確認 $ brew list nodebrew ##nodebrewセットアップ $ nodebrew setup環境変数の設定
~/.bash_profileファイルに以下を追記
bash_profile.export PATH=/usr/local/var/nodebrew/current/bin:$PATH export PATH=$HOME/.nodebrew/current/bin:$PATH#環境変数の反映 $ source ~/.bash_profile利用可能なnode.jsのバージョンを確認
$ nodebrew ls-remote v0.0.1 v0.0.2 v0.0.3 v0.0.4 v0.0.5 v0.0.6 v0.1.0 v0.1.1 v0.1.2 v0.1.3 v0.1.4 v0.1.5 v0.1.6 v0.1.7 v0.1.8 v0.1.9 v0.1.10 v0.1.11 v0.1.12 v0.1.13 v0.1.14 v0.1.15 v0.1.16 v0.1.17 v0.1.18 v0.1.19 v0.1.20 v0.1.21 v0.1.22 v0.1.23 v0.1.24 v0.1.25 v0.1.26 v0.1.27 v0.1.28 v0.1.29 v0.1.30 v0.1.31 v0.1.32 v0.1.33 v0.1.90 v0.1.91 v0.1.92 v0.1.93 v0.1.94 v0.1.95 v0.1.96 v0.1.97 v0.1.98 v0.1.99 v0.1.100 v0.1.101 v0.1.102 v0.1.103 v0.1.104 ~ #全てのバージョンが表示される利用したいバージョンのnode.jsをインストールする
今回はgulp3系に対応しているv10.16.0をインストール
// 特定のバージョンのインストール $ nodebrew install-binary v10.16.0 // 利用可能なバージョン(現在はこの2つのバージョンが利用できる) $ nodebrew ls v14.8.0 v10.16.0 current: v14.8.0インストールエラーが出る場合
Fetching: https://nodejs.org/dist/v10.16.0/node-v10.16.0-darwin-x64.tar.gz #=#=- # # Warning: Failed to create the file Warning: /Users/username/.nodebrew/src/v10.16.0/node-v10.16.0-darwin-x64.t Warning: ar.gz: No such file or directory 0.0% curl: (23) Failed writing body (0 != 985) download failed: https://nodejs.org/dist/v10.16.0/node-v10.16.0-darwin-x64.tar.gzインストールできるディレクトリが存在していないということなので、下記コマンドでディレクトリを作成する。
$ mkdir -p ~/.nodebrew/src //再度インストール $ nodebrew install-binary v10.16.0 //下記ディレクトリを見てみるとインストールされていることが確認できる。 /Users/username/.nodebrew/src $ ls -la drwxr-xr-x 4 username staff 128 11 29 19:43 . drwxr-xr-x 9 username staff 288 11 29 19:53 .. drwxr-xr-x 3 username staff 96 11 29 19:42 v10.16.0 drwxr-xr-x 3 username staff 96 11 29 19:43 v14.8.0node.jsのバージョンを変更する
// バージョンの切り替え $ nodebrew use v10.16.0 //変更確認 $ node -v v10.16.0node.jsのバージョンを変更することができたのでgulp実行
$ gulp正常にgulpが動作しました。
フロントエンドの環境構築はnode.jsのバージョンによって結構詰まることが多かったので、nodebrewでnode.jsのバージョン管理ができるのは非常に便利ですね。
- 投稿日:2020-11-29T19:28:41+09:00
WSL2のUbuntu20.04 でroot以外のユーザーでnpmコマンドが使えない
環境
- Windows 10 Pro
- WSL2
- Ubuntu20.04
状況
アプリケーション一覧からUbuntu20.04を起動し
sudo apt install npmでnpmをインストールした後,
npm --version
実行すると,
-bash: /mnt/c/Program Files/nodejs/npm: /bin/sh^M: bad interpreter: No such file or directory
sudo su
してからrootユーザーで実行すると普通にバージョン情報が出力される.
あと, PowerShell経由でwsl
コマンドで実行すると発生しない.改行コードをCRLFからLFへ変更
色々調べてると改行コードが悪さしてそうだったので,
CRLFからLFに変える無精してVSCodeで変更
CRLFを選択して,
LFを選択.一旦,
exit
して再起動.$ npm -v 6.14.4参考
- https://stackoverflow.com/questions/62947245/why-executing-npm-version-in-wsl2-as-specific-user-resulting-bad-interpreter
- https://qiita.com/ayasumi_primary/items/0225d5c89ff1f2e7e217![screen_npm_CRLF_to_LF.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/677894/b6764305-f44f-58e1-b128-09ee97263d55.png)
- 投稿日:2020-11-29T19:26:20+09:00
お猫様に振り向いてもらうピヨピヨマシンを作った
はじめに
こんばんわ!IoTLTアドベントカレンダー2日目です!!!
ねこIoTLT主催の3yakaと申します、フワッとしたフロントエンジニアです。
猫のために頑張って仕事をして猫のために。。。という猫中心の生活をしています。
猫との生活を充実させるためにねこIoTを楽しんでます〜クリスマスプレゼント的なお猫様に振り向いてもらうピヨピヨマシンを作った
おネコ様と暮らしていると、おそらくみなさんがやったことがあるであろう行為
「アレクサ! 鳥の鳴き声を教えて」みたいな、鳥の鳴き声を流すアレ。
若いネコさんは鳥のピヨピヨに反応して、興奮気味に鳥を探してくれます。(窓際にリアルに鳥さんが来るときと同じ反応をしてくれます)
しかし、「アレクサ! 〇〇・・・」的なことを言ってから鳥さんが鳴き始めると賢いお猫様はそのうちに相手にしてくれなくなります。いかに、人間がやっていないかのようにピヨピヨ言わせてお猫様を喜ばすかが重要になってきます。
やりたいこと
- ピヨピヨマシーンと離れたところでスイッチを入れたい → 遠隔操作
- 人間が近くでやるときは、人間の動作の直後ではなくてちょっとしてから鳴いて欲しい(自然を装いたい) → Queueを使えば良い?
- 外出時に猫カメラに映らない猫を呼び寄せたい → カメラの死角から出てきて欲しい
- アプリを増やしたくない → LINEBotだー
概要
LINEBotに「ピヨピヨ」と入れたら、
Azure Queue storageに登録して
数十秒後にラズパイから鳥の鳴き声を出すお猫様を喜ばせるピヨピヨマシーンを作ったんだけど、実験しすぎてあんまり反応してくれなくなった。。。
— 3yaka (@3yaka4) December 1, 2020
お猫様賢い。。。 pic.twitter.com/NK9W3iZBrrLINEBotを作る
LINEBotはこちらの記事を参考に
LINEBotからRaspberryPiで写真を撮ってLINEにおくる! - Qiita
こちらを追加node.jsif (event.message.text.match("ピヨピヨ")) { piyo = await bird(event.source.userId); await client.replyMessage(event.replyToken, { type: "text", text: "鳥さん待機!", }); } const bird = async (userId) => { const { QueueClient } = require("@azure/storage-queue"); let torisan = ownerName; async function main() { const queueName = torisan; console.log("\nCreating queue..."); console.log("\t", queueName); const queueClient = new QueueClient(connectionString, queueName); // Create the queue const createQueueResponse = await queueClient.create(); console.log("Queue created, requestId:", createQueueResponse.requestId); const sendMessageResponse = await queueClient.sendMessage(torisan); console.log("Messages added, requestId:", sendMessageResponse.requestId); // Get messages from the queue const receivedMessagesResponse = await queueClient.receiveMessages({ numberOfMessages: 5, }); console.log( "Messages received, requestId:", receivedMessagesResponse.requestId ); } main() .then(() => console.log("\nDone")) .catch((ex) => console.log(ex.message)); };RaspberryPiの準備
こちらから好きな鳥の鳴き声をもらってきます
自然・動物[1]|効果音ラボ
私はスズメの鳴き声にしましたラズパイの構成はこんな感じ
node.jsは最新で挑んだ!/home/torisan |- tori |ーnode.modules |ーpackage.json |ーraspi_tori.js |ーaudiofile |ー suzume.mp3使用機材は イヤホンマイク
スピーカーを設置したらにゃんこに怪しまれてしまったので、ラズパイからイヤホンの線を伸ばすとまぁまぁ距離を取れるので、引っ張ってイヤホン部分は隠しましょうraspi_tori.js"use strict"; const player = require("play-sound")(); // Azureとの連携用モジュール const azure = require("azure-storage"); // Azure上のBLOBストレージとの接続用サービスオブジェクト // 引数にBLOBストレージのConnectionStringを設定 var blobSvc = azure.createBlobService("***"); // Azure Storage の接続文字列を環境変数から取得 const connectionString = process.env.AZURE_STORAGE_CONNECTION_STRING; const { QueueClient } = require("@azure/storage-queue"); //Quereにかきこんでおとをだす const bird = "suzume"; async function main() { const AZURE_STORAGE_CONNECTION_STRING = process.env.AZURE_STORAGE_CONNECTION_STRING; const queueName = bird; console.log("\t", queueName); // Instantiate a QueueClient which will be used to create and manipulate a queue const queueClient = new QueueClient( AZURE_STORAGE_CONNECTION_STRING, queueName ); // Get messages from the queue const receivedMessagesResponse = await queueClient.receiveMessages({ numberOfMessages: 5, }); if (receivedMessagesResponse.receivedMessageItems.length > 0) { console.log("とりがなきます"); player.play("/home/pi/handson/audiofile/suzume.mp3", (err) => { if (err) throw err; }); let receivedMessage = receivedMessagesResponse.receivedMessageItems[0]; // 'Process' the message console.log("\tProcessing:", receivedMessage.messageText); // Delete the message const deleteMessageResponse = await queueClient.deleteMessage( receivedMessage.messageId, receivedMessage.popReceipt, receivedMessage.messageText ); console.log( "\tMessage deleted, requestId:", deleteMessageResponse.requestId ); } } main() .then(() => console.log("\nDone")) .catch((ex) => console.log(ex.message));おわりに
Twitterにあげた通り、実験をしすぎて相手にされなくなりました。
スズメの鳴き声を違う音声ファイルにしたらきっと探してくれるはず。。。Queueの記事は少なくて辛かったですが公式のドキュメントが完璧だったので読み解けてないけど動いてるからよし。
参考
クイック スタート:Azure Queue storage ライブラリ v12 - JavaScript | Microsoft Docs
- 投稿日:2020-11-29T19:26:20+09:00
お猫様に振り向いてもらうピヨピヨマシンを作った〜
はじめに
こんばんわ!IoTLTアドベントカレンダー2日目です!!!
ねこIoTLT主催の3yakaと申します、フワッとしたフロントエンジニアです。
猫のために頑張って仕事をして猫のために。。。という猫中心の生活をしています。
猫との生活を充実させるためにねこIoTを楽しんでます〜クリスマスプレゼント的なお猫様に振り向いてもらうピヨピヨマシンを作った
おネコ様と暮らしていると、おそらくみなさんがやったことがあるであろう行為
「アレクサ! 鳥の鳴き声を教えて」みたいな、鳥の鳴き声を流すアレ。
若いネコさんは鳥のピヨピヨに反応して、興奮気味に鳥を探してくれます。(窓際にリアルに鳥さんが来るときと同じ反応をしてくれます)
しかし、「アレクサ! 〇〇・・・」的なことを言ってから鳥さんが鳴き始めると賢いお猫様はそのうちに相手にしてくれなくなります。いかに、人間がやっていないかのようにピヨピヨ言わせてお猫様を喜ばすかが重要になってきます。
やりたいこと
- ピヨピヨマシーンと離れたところでスイッチを入れたい → 遠隔操作
- 人間が近くでやるときは、人間の動作の直後ではなくてちょっとしてから鳴いて欲しい(自然を装いたい) → Queueを使えば良い?
- 外出時に猫カメラに映らない猫を呼び寄せたい → カメラの死角から出てきて欲しい
- アプリを増やしたくない → LINEBotだー
概要
LINEBotに「ピヨピヨ」と入れたら、
Azure Queue storageに登録して
数十秒後にラズパイから鳥の鳴き声を出すお猫様を喜ばせるピヨピヨマシーンを作ったんだけど、実験しすぎてあんまり反応してくれなくなった。。。
— 3yaka (@3yaka4) December 1, 2020
お猫様賢い。。。 pic.twitter.com/NK9W3iZBrrLINEBotを作る
LINEBotはこちらの記事を参考に
LINEBotからRaspberryPiで写真を撮ってLINEにおくる! - Qiita
こちらを追加node.jsif (event.message.text.match("ピヨピヨ")) { piyo = await bird(event.source.userId); await client.replyMessage(event.replyToken, { type: "text", text: "鳥さん待機!", }); } const bird = async (userId) => { const { QueueClient } = require("@azure/storage-queue"); let torisan = ownerName; async function main() { const queueName = torisan; console.log("\nCreating queue..."); console.log("\t", queueName); const queueClient = new QueueClient(connectionString, queueName); // Create the queue const createQueueResponse = await queueClient.create(); console.log("Queue created, requestId:", createQueueResponse.requestId); const sendMessageResponse = await queueClient.sendMessage(torisan); console.log("Messages added, requestId:", sendMessageResponse.requestId); // Get messages from the queue const receivedMessagesResponse = await queueClient.receiveMessages({ numberOfMessages: 5, }); console.log( "Messages received, requestId:", receivedMessagesResponse.requestId ); } main() .then(() => console.log("\nDone")) .catch((ex) => console.log(ex.message)); };RaspberryPiの準備
こちらから好きな鳥の鳴き声をもらってきます
自然・動物[1]|効果音ラボ
私はスズメの鳴き声にしましたラズパイの構成はこんな感じ
node.jsは最新で挑んだ!/home/torisan |- tori |ーnode.modules |ーpackage.json |ーraspi_tori.js |ーaudiofile |ー suzume.mp3使用機材は イヤホンマイク
スピーカーを設置したらにゃんこに怪しまれてしまったので、ラズパイからイヤホンの線を伸ばすとまぁまぁ距離を取れるので、引っ張ってイヤホン部分は隠しましょうraspi_tori.js"use strict"; const player = require("play-sound")(); // Azureとの連携用モジュール const azure = require("azure-storage"); // Azure上のBLOBストレージとの接続用サービスオブジェクト // 引数にBLOBストレージのConnectionStringを設定 var blobSvc = azure.createBlobService("***"); // Azure Storage の接続文字列を環境変数から取得 const connectionString = process.env.AZURE_STORAGE_CONNECTION_STRING; const { QueueClient } = require("@azure/storage-queue"); //Quereにかきこんでおとをだす const bird = "suzume"; async function main() { const AZURE_STORAGE_CONNECTION_STRING = process.env.AZURE_STORAGE_CONNECTION_STRING; const queueName = bird; console.log("\t", queueName); // Instantiate a QueueClient which will be used to create and manipulate a queue const queueClient = new QueueClient( AZURE_STORAGE_CONNECTION_STRING, queueName ); // Get messages from the queue const receivedMessagesResponse = await queueClient.receiveMessages({ numberOfMessages: 5, }); if (receivedMessagesResponse.receivedMessageItems.length > 0) { console.log("とりがなきます"); player.play("/home/pi/handson/audiofile/suzume.mp3", (err) => { if (err) throw err; }); let receivedMessage = receivedMessagesResponse.receivedMessageItems[0]; // 'Process' the message console.log("\tProcessing:", receivedMessage.messageText); // Delete the message const deleteMessageResponse = await queueClient.deleteMessage( receivedMessage.messageId, receivedMessage.popReceipt, receivedMessage.messageText ); console.log( "\tMessage deleted, requestId:", deleteMessageResponse.requestId ); } } main() .then(() => console.log("\nDone")) .catch((ex) => console.log(ex.message));おわりに
Twitterにあげた通り、実験をしすぎて相手にされなくなりました。
スズメの鳴き声を違う音声ファイルにしたらきっと探してくれるはず。。。Queueの記事は少なくて辛かったですが公式のドキュメントが完璧だったので読み解けてないけど動いてるからよし。
参考
クイック スタート:Azure Queue storage ライブラリ v12 - JavaScript | Microsoft Docs
- 投稿日:2020-11-29T19:16:13+09:00
【Node.js express Docker】 Dockerを用いてNode.js Express MySQLの環境を構築する
前回の記事で作ったdockerファイルを整理した内容です。
【Node.js】 Dockerを用いてNode.js Express MySQLの環境を構築するまでの道のり
https://qiita.com/sho_U/items/0ef3dfc7b07b5e13fa18最初に用意するパッケージ
app.env
app.envMYSQL_SERVER=mysql MYSQL_USER=(ユーザー名) MYSQL_PASSWORD=(パスワード) MYSQL_DATABASE=(データーベース名)docker-compose.yml:
node.jsコンテナとmysqlコンテナを管理するyml
docker-compose.ymlversion: '3' services: mysql: image: mysql:5.7 container_name: (アプリ名)_db env_file: ./mysql/mysql.env environment: - TZ=Asia/Tokyo ports: - '3306:3306' volumes: - ./mysql/conf:/etc/mysql/conf.d/:ro - mysqldata:/var/lib/mysql networks: - backend app: build: . container_name: (アプリ名)_app env_file: ./app.env environment: - TZ=Asia/Tokyo - DEBUG=app:* tty: true ports: - '3000:3000' volumes: - ./src:/app working_dir: /app command: npm starttre networks: - backend depends_on: - mysql #使用するネットワークを作成。docker-composeの場合service以下の名前を使って名前解決されるため、appとmysqlが自動的に接続される。 networks: backend: volumes: mysqldata:Dockerfile:
アプリケーション用(node.js)コンテナを作るためのfile
dockerfile.FROM node:12 WORKDIR /appなぜかnpm installをRUNできないので,必要なパッケージはpackage.jsonに記載して、コンテナからnpm install を実施。
my.conf:
(コンテナ側の/etc/mysql/conf.d/に配置される。)
mysql/conf/my.conf[client] default-character-set=utf8mb4 [mysql] default-character-set=utf8mb4 [mysqldump] default-character-set=utf8mb4 [mysqld] character-set-server=utf8mb4 collation-server=utf8mb4_bin lower_case_table_names=1 # Enable access from the host machine. bind-address=0.0.0.0mysql.env
mysql/mysql.envMYSQL_ROOT_HOST=% MYSQL_ROOT_PASSWORD=(ルートパスワード) MYSQL_USER=(ユーザー名) MYSQL_PASSWORD=(パスワード) MYSQL_DATABASE=(データーベース名)src:
アプリケーション本体(空ディレクトリ)
package.json:
初期は空
package.json(空){}アプリケーション用のコンテナを作成する
コンテナをビルドする。
ホスト.docker-compose buildホスト.#コンテナを一時的に起動(--rmで停止後削除する。コンテナ起動後、bashに入る) docker-compose run --rm app /bin/bashコンテナ.# express-generatorでアプリケーションのひな形を生成 npx express-generator --view=ejspackage.jsonに必要なパッケージを記載(必要に応じて追加)
package.json{ "name": "アプリ名", "version": "0.0.0", "private": true, "scripts": { "start": "nodemon ./bin/www" //nodemon用起動scripts }, "dependencies": { "cookie-parser": "~1.4.4", "debug": "~2.6.9", "ejs": "^3.1.5", "express": "~4.16.1", "express-generator": "^4.16.1", "express-session": "^1.17.1", "express-validator": "^6.7.0", "http-errors": "~1.6.3", "morgan": "~1.9.1", "nodemon": "^2.0.6", "sequelize": "^6.3.5", "sequelize-cli": "^6.2.0" } }コンテナ.#インストール npm installコンテナ.#コンテナを抜ける(この仮コンテナは削除される) exit※この時、コンテナは削除されるが
docker-compose.ymlvolumes: - ./src:/appこの記述により、.(docker-compose.ymlがあるディレクトリ。つまりnodoDockerディレクトリの配下のsrcディレクトリにマウントされているため、ホスト側のsrcディレクトリに作成した雛形は残っている。
コンテナを起動させる
host.docker-compose updocker-compose.ymlcommand: npm startの記載により、コンテナ起動後、自動的にExpress.jsのアプリケーションがnodemonで起動する。
http://localhost:3000/
で確認。mysqlコンテナに入れるか確認
docker exec -it コンテナID bash mysql -uroot -p
- 投稿日:2020-11-29T19:16:13+09:00
【Node.js】 Dockerを用いてNode.js Express MySQLの環境を構築する
前回の記事で作ったdockerファイルを整理した内容です。
【Node.js】 Dockerを用いてNode.js Express MySQLの環境を構築するまでの道のり
https://qiita.com/sho_U/items/0ef3dfc7b07b5e13fa18最初に用意するパッケージ
app.env
app.envMYSQL_SERVER=mysql MYSQL_USER=(ユーザー名) MYSQL_PASSWORD=(パスワード) MYSQL_DATABASE=(データーベース名)docker-compose.yml:
node.jsコンテナとmysqlコンテナを管理するyml
docker-compose.ymlversion: '3' services: mysql: image: mysql:5.7 container_name: (アプリ名)_db env_file: ./mysql/mysql.env environment: - TZ=Asia/Tokyo ports: - '3306:3306' volumes: - ./mysql/conf:/etc/mysql/conf.d/:ro - mysqldata:/var/lib/mysql networks: - backend app: build: . container_name: (アプリ名)_app env_file: ./app.env environment: - TZ=Asia/Tokyo - DEBUG=app:* tty: true ports: - '3000:3000' volumes: - ./src:/app working_dir: /app command: npm starttre networks: - backend depends_on: - mysql #使用するネットワークを作成。docker-composeの場合service以下の名前を使って名前解決されるため、appとmysqlが自動的に接続される。 networks: backend: volumes: mysqldata:Dockerfile:
アプリケーション用(node.js)コンテナを作るためのfile
dockerfile.FROM node:12 WORKDIR /appなぜかnpm installをRUNできないので,必要なパッケージはpackage.jsonに記載して、コンテナからnpm install を実施。
my.conf:
(コンテナ側の/etc/mysql/conf.d/に配置される。)
mysql/conf/my.conf[client] default-character-set=utf8mb4 [mysql] default-character-set=utf8mb4 [mysqldump] default-character-set=utf8mb4 [mysqld] character-set-server=utf8mb4 collation-server=utf8mb4_bin lower_case_table_names=1 # Enable access from the host machine. bind-address=0.0.0.0mysql.env
mysql/mysql.envMYSQL_ROOT_HOST=% MYSQL_ROOT_PASSWORD=(ルートパスワード) MYSQL_USER=(ユーザー名) MYSQL_PASSWORD=(パスワード) MYSQL_DATABASE=(データーベース名)src:
アプリケーション本体(空ディレクトリ)
package.json:
初期は空
package.json(空){}アプリケーション用のコンテナを作成する
コンテナをビルドする。
ホスト.docker-compose buildホスト.#コンテナを一時的に起動(--rmで停止後削除する。コンテナ起動後、bashに入る) docker-compose run --rm app /bin/bashコンテナ.# express-generatorでアプリケーションのひな形を生成 npx express-generator --view=ejspackage.jsonに必要なパッケージを記載(必要に応じて追加)
package.json{ "name": "アプリ名", "version": "0.0.0", "private": true, "scripts": { "start": "nodemon ./bin/www" //nodemon用起動scripts }, "dependencies": { "cookie-parser": "~1.4.4", "debug": "~2.6.9", "ejs": "^3.1.5", "express": "~4.16.1", "express-generator": "^4.16.1", "express-session": "^1.17.1", "express-validator": "^6.7.0", "http-errors": "~1.6.3", "morgan": "~1.9.1", "nodemon": "^2.0.6", "sequelize": "^6.3.5", "sequelize-cli": "^6.2.0" } }コンテナ.#インストール npm installコンテナ.#コンテナを抜ける(この仮コンテナは削除される) exit※この時、コンテナは削除されるが
docker-compose.ymlvolumes: - ./src:/appこの記述により、.(docker-compose.ymlがあるディレクトリ。つまりnodoDockerディレクトリの配下のsrcディレクトリにマウントされているため、ホスト側のsrcディレクトリに作成した雛形は残っている。
コンテナを起動させる
host.docker-compose updocker-compose.ymlcommand: npm startの記載により、コンテナ起動後、自動的にExpress.jsのアプリケーションがnodemonで起動する。
http://localhost:3000/
で確認。mysqlコンテナに入れるか確認
docker exec -it コンテナID bash mysql -uroot -p
- 投稿日:2020-11-29T17:13:39+09:00
憧れのギニュー特戦隊の誰に似てるか判定するLINEbotを作ったから、ぜってぇ見てくれよなっ!
ギニュー特戦隊に入りたい
皆さんも(特に男性なら)、人生で一度はギニュー特戦隊に入隊したいと思いましたよね?
今回はその願いを少しでも叶えるべく、次のようなボットを作成しました。
* まさかそんな人はいないと思いますが、ギニュー特戦隊を知らない方はこちらのwikipediaをご確認ください。
ギニュー特戦隊識別bot完成!
— 北城雅照@足立慶友整形外科 (@kutuyanomusuko) November 27, 2020
僕はリクームでしたwww#protoout pic.twitter.com/52wTNdbfTu早速使ってみたい方は、一番下にQRコードを貼っておきますので、ご利用ください。
ちなみに、万が一、リクームとジース以外の方に似ていると判定されたら、その方は人間(ホモ・サピエンス)という概念を超えている方ですので、すぐにご連絡ください!開発環境の下準備
1) VScodeのインストール
VScodeのインストールついては、googleなどで他の記事を検索してください。
2) node.jsとnpmのインストール
Macでの環境作りは、こちらの別の記事にまとめてあります。
参考にしてください。
Macにnode.js,npmのインストール方法開発環境の準備
1) ginyuforceというフォルダを作成
2) VSCodeで上記フォルダを開き、フォルダ内にginyuforce.jsを作成
3) VSCode内でターミナルを開き、フォルダをnpm管理できるように初期化terminalコマンド$ npm init -yシステムの概要
Teachable Machineによる画像認識AIの作成
以前の記事(誰が使うかわからないけど、膝のレントゲン写真を送ったら、その膝がどの程度痛んでいるのか教えてくれるラインbotを作ってみた。)では画像判定AIをAIメーカーで作成したので、今回はTeachable Machineを使用し、画像判定AIを作成しました。
* 使い方については、こちらの記事>【備忘録】Teachable Machineの利用方法が参考になりました。ぜひ一度お読みください。(よかったらLGTMをお願いします。)
ginyuforce.jsのコード
ginyuforce.js'use strict'; // おまじない const { JSDOM } = require('jsdom'); var dom = new JSDOM(''); global.document = dom.window.document; global.HTMLVideoElement = dom.window.HTMLVideoElement; const canvas = require('canvas'); global.fetch = require('node-fetch'); const tmImage = require('@teachablemachine/image'); const express = require('express'); const line = require('@line/bot-sdk'); const fs = require('fs'); const PORT = process.env.PORT || 3000; const config = { channelSecret: '自身が作成したLINEbotのシークレットトークンを入力', channelAccessToken: '自身が作成したLINEbotのアクセストークンを入力', }; // Teachable Machine let model; // https://teachablemachine.withgoogle.com/ // ここでエクスポート、クラウドにモデルをアップロードした後に取得できる const URL = '作成したモデルのTeachable Machineのリンクを入力'; // ######################################## // Teachable Machineを使って画像分類をする部分 // ######################################## // 初期化が時間かかるので、node立ち上げ時に行うようにする async function initTeachableMachine() { const modelURL = URL + 'model.json'; const metadataURL = URL + 'metadata.json'; // モデルデータのロード model = await tmImage.load(modelURL, metadataURL); // クラスのリストを取得 // const classes = model.getClassLabels(); // console.log(classes); } initTeachableMachine(); async function predict(imgPath) { // canvasに画像をロードする const image = await canvas.loadImage(imgPath); // 判定する const predictions = await model.predict(image); // 一番近いもの順でソート predictions.sort((a, b) => { return b.probability - a.probability; }); return predictions; } // ######################################## // LINEサーバーからのWebhookデータを処理する部分 // ######################################## // LINE SDKを初期化します const client = new line.Client(config); // LINEサーバーからWebhookがあると「サーバー部分」から以下の "handleEvent" という関数が呼び出されます async function handleEvent(event) { // 受信したWebhookが「画像以外」であればnullを返すことで無視します if (event.message.type === 'image') { console.log("画像が送られてきた"); // 画像を保存 const downloadPath = './01.png'; //左記の名前で使用中のフォルダ内に送信された画像が保存される const getContent = await downloadContent(event.message.id, downloadPath); const result = await predict(getContent); // AIメーカーAPIの結果から、返信するメッセージを組み立てる let text = ''; let name = ''; name = result[0].className // 判定結果をテキストに代入 text = 'あなたは『' + name + "』に最も似ています!"; // これまでの結果を確認するためにコンソールに表示 console.log(result); console.log(name); console.log(text); // 判定結果に応じた画像を送信 if(name === 'ジース'){ return client.replyMessage(event.replyToken, [{ type: 'text', text: text },{ type: 'image', originalContentUrl: 'https://sleepy-mirzakhani-f338f8.netlify.app/gisu.jpg', previewImageUrl: 'https://sleepy-mirzakhani-f338f8.netlify.app/gisu.jpg' }]); }else if(name === 'リクーム'){ return client.replyMessage(event.replyToken,[{ type: 'text', text: text },{ type: 'image', originalContentUrl: 'https://sleepy-mirzakhani-f338f8.netlify.app/rikumu.jpg', previewImageUrl: 'https://sleepy-mirzakhani-f338f8.netlify.app/rikumu.jpg' }]); }else if(name === 'グルド'){ return client.replyMessage(event.replyToken, [{ type: 'text', text: text },{ type: 'image', originalContentUrl: 'https://sleepy-mirzakhani-f338f8.netlify.app/gurudo.jpg', previewImageUrl: 'https://sleepy-mirzakhani-f338f8.netlify.app/gurudo.jpg' }]); }else if(name === 'バータ'){ return client.replyMessage(event.replyToken, [{ type: 'text', text: text },{ type: 'image', originalContentUrl: 'https://sleepy-mirzakhani-f338f8.netlify.app/bata.jpg', previewImageUrl: 'https://sleepy-mirzakhani-f338f8.netlify.app/bata.jpg' }]); }else if(name === 'ギニュー隊長'){ return client.replyMessage(event.replyToken, [{ type: 'text', text: text },{ type: 'image', originalContentUrl: 'https://sleepy-mirzakhani-f338f8.netlify.app/ginyu.jpg', previewImageUrl: 'https://sleepy-mirzakhani-f338f8.netlify.app/ginyu.jpg' }]); } } // 「テキストメッセージ」であれば、受信したテキストをそのまま返事します if (event.message.type === 'text') { return client.replyMessage(event.replyToken, { type: 'text', text: event.message.text // ← ここに入れた言葉が実際に返信されます }); } } // ######################################## // LINEで送られた画像を保存する部分 // ######################################## function downloadContent(messageId, downloadPath) { const data = []; return client.getMessageContent(messageId) .then((stream) => new Promise((resolve, reject) => { const writable = fs.createWriteStream(downloadPath); stream.on('data', (chunk) => data.push(Buffer.from(chunk))); stream.pipe(writable); stream.on('end', () => resolve(Buffer.concat(data))); stream.on('error', reject); })); } // ######################################## // Expressによるサーバー部分 // ######################################## // expressを初期化します const app = express(); // HTTP GETによって '/' のパスにアクセスがあったときに 'Hello LINE BOT! (HTTP GET)' と返事します // これはMessaging APIとは関係のない確認用のものです app.get('/', (req, res) => res.send('<h1>Hello LINE BOT! (HTTP GET)</h1>')); // HTTP POSTによって '/webhook' のパスにアクセスがあったら、POSTされた内容に応じて様々な処理をします app.post('/webhook', line.middleware(config), (req, res) => { // Webhookの中身を確認用にターミナルに表示します console.log(req.body.events); // 空っぽの場合、検証ボタンをクリックしたときに飛んできた"接続確認"用 // 削除しても問題ありません if (req.body.events.length == 0) { res.send('Hello LINE BOT! (HTTP POST)'); // LINEサーバーに返答します console.log('検証イベントを受信しました!'); // ターミナルに表示します return; // これより下は実行されません } // あらかじめ宣言しておいた "handleEvent" 関数にWebhookの中身を渡して処理してもらい、 // 関数から戻ってきたデータをそのままLINEサーバーに「レスポンス」として返します Promise.all(req.body.events.map(handleEvent)).then((result) => res.json(result)); }); // 最初に決めたポート番号でサーバーをPC内だけに公開します // (環境によってはローカルネットワーク内にも公開されます) app.listen(PORT); console.log(`ポート${PORT}番でExpressサーバーを実行中です…`);実行に必要なパッケージのインストルール
下記のコードを入力し、実行に必要なパッケージを入力する。
2) Teachable Machineを動かすのに必要なパッケージ
terminalコマンド$ npm i @line/bot-sdk express1) LINEbotを動かすのに必要なパッケージ
terminalコマンド$ npm i @teachablemachine/image @tensorflow/tfjs canvas jsdomHerokuへのデプロイ
node.jsが動作する無料のクラウドサーバーであるHerokuにデプロイします。
下準備
1) Heroku CLIのインストール
https://devcenter.heroku.com/articles/heroku-cli2) Herokuアカウントの作成
https://signup.heroku.com/デプロイ
ターミナルで下記コマンドを実行します。
terminalコマンド$ heroku login実行するとブラウザが自動的に起動するので、ログインします。
すると、ターミナル上でもログインが完了します。その後、package.jsonの"scripts"の中に、"start"を下記のように追記し保存します。
package.json"scripts": { "test": "echo \"Error: no test specified\" && exit 1", "start": "node ginyuforce.js" },Heroku内にデプロイ先のディレクトリを作成します。
下記の順でターミナルにコマンドを入力します。terminalコマンド$ git initterminalコマンド$ heroku create 名付けたいデプロイ先の名前 //付けたい名前がなければ入力なしでもOKHeroku内にデプロイ先のディレクトリが作成されると、ターミナルに下記のように表示されます。
terminalCreating app... done, ⬢ デプロイ先の名前 https://デプロイ先の名前.com/ | https://git.heroku.com/デプロイ先の名前.gitこのデプロイ先ディレクトリに、作成したディレクトリを入れていきます。
下記の順でターミナルにコマンドを入力します。terminalコマンド$ git add .terminalコマンド$ git commit -m 'init'terminalコマンド$ git push heroku master最終的に下記のように表示されれば完了です。
terminalremote: https://デプロイ先の名前.herokuapp.com/ deployed to Heroku remote: remote: Verifying deploy... done. To https://git.heroku.com/デプロイ先の名前.git * [new branch] master -> masterこれでデプロイ完了です。
ファイルを更新する時
まず、更新するファイルが入ったディレクトリがherokuのデプロイ先のディレクトリと紐づいているかを確認します。
terminalコマンド$ git remote -v紐づいている場合
紐づいている場合は、ターミナルに下記のように表記されます。
terminalheroku https://git.heroku.com/heroku上のデプロイ先の名前 (fetch) heroku https://git.heroku.com/heroku上のデプロイ先の名前 (push)紐づいていない場合
紐づいていない場合は、ターミナルに下記のように表記されます。
terminalfatal: not a git repository (or any of the parent directories): .gitその場合は、ターミナルに下記の順に入力し、空のディレクトリを作成し、heroku上のデプロイしたいディレクトリと紐付けを行います。
terminalコマンド$ git init //空のディレクトリを作成terminalコマンド$ heroku git:remote -a heroku上のデプロイしたいディレクトリ名 //herokuのデプロイ先と先ほど作成した空のtディレクトリを紐付けその上で、下記の順にコマンドを入力し、ファイルを更新します。
terminalコマンド$ git add .terminalコマンド$ git commit -m 'upd'terminalコマンド$ git push heroku masterLINE Developersからbot作成
1) LINE Developersにアクセスし、LINEアカウントでログイン
2) プロバイダー作成
3) 新規チャンネルの作成
4) チャネルアクセストークンとチャネルシークレットの取得
1)〜4)までは下記を参考に進みました。
1時間でLINE BOTを作るハンズオン (資料+レポート) in Node学園祭2017 #nodefest
5) webhookの設定
Herokuから取得したデプロイ先のURLを入力。
/webhookをつけることを忘れずに。
QRコード
完成したギニュー特戦隊判定LINEbotのQRコードがこちらです。
どうでしたか皆さん?
ギニュー特戦隊に成る願いは叶えらましたか??
その他の記事
近すぎると小池都知事が『密です。』と連呼するデバイスを作ったら腹筋が崩壊したので、皆さんにも試して欲しい。
誰が使うかわからないけど、膝のレントゲン写真を送ったら、その膝がどの程度痛んでいるのか教えてくれるラインbotを作ってみた。
- 投稿日:2020-11-29T16:27:36+09:00
アレクサとTodoistでやることリスト・お買い物リスト
Todoistって、Amazon Echoと連携できるんですね!
しかもWebAPIが充実しているので、さらに連携の輪が広がりそうです。ちなみに、Todoistは、タスク管理ツールです。一般には、やることリストとか、買い物リストが挙げられますが、メモ的に使えて、かつ、期限を設定して予定を立てたり、失念するのを避けるのに役立ちます。
todoist
https://todoist.com/今回作成する全体の構成はこんな感じです。
すでに、todoistはAlexaとの連携をサポートしていますし、AndroidやiPhone用のアプリもありますので、その部分は特に難しいところはありません。
今回は、やることリスト・買い物リストの表示をWebページとして表示します。todoistはオフィシャルでWebAPIが充実していますし、npmモジュールもあるので、活用の幅が広がります。
また、さらに、PWA化することでネイティブアプリ化し、それをAndroidで固定アプリに設定することで、やることリスト表示の専用機に仕立て上げます。毎度のことですが、ソースコードもろもろをGitHubに上げておきました。
poruruba/todolist
https://github.com/poruruba/todolist準備
todoistのアカウント登録
まずは、以下のURLから、todoistのアカウント登録をします。
todoist
https://todoist.com/ja「はじめる」ボタンまたは右上のサインアップをクリックします。
ここでは、スマートフォンにインストールしているAmazon Alexaアプリに設定しているアカウントでサインアップし、認証を完了させます。
これでアカウントが登録されました。
todoistアプリをインストール
Google Playから、Todoistをインストールします。
todoistにアカウント登録したときのアカウントでログインします。
ログインが完了しました。
Alexa連携を設定
すでに、スマートフォンに、「Amazon Alexa」がインストールされている前提で進めます。
以降は、Androidでの操作です。まずは、下の方の「その他」→「設定」→「リスト」を選択します。
そうすると、Any.do、AnyList、Todoistが表示されていますので、Todoistの右側の⊕をタッチします。
以下のように表示されるので、「有効にして使用する」ボタンを押下します。
リストへのアクセス権の確認が表示されますので、「アクセス権を保存」ボタンを押下します。
そうすると、todoistのサイトに飛んで、Agreeするかが聞かれますので、「Agree」ボタンを押下します。
これで、連携設定が完了しました。
もう一度、todoistアプリの方を開いてみましょう。
そうすると、左上の三のマークをタッチし、プロジェクトのところをタッチすると、「Alexa ToDo リスト」というのと、「Alexaの買い物リスト」が増えているのがわかります。最後に、この「Alexa ToDo リスト」と「Alexaの買い物リスト」をお気に入りにしておきます。
「プロジェクト」→「プロジェクトを管理」をタッチし、「Alexa ToDo リスト」と「Alexaの買い物リスト」の右側にあるハートマークをタップし、赤色にします。これによって、この2つがお気に入りのプロジェクトとなりました。この意味はあとで、わかります。あと、お好みで、todoistアプリをAndroidのホーム画面にウィジェットとして登録してもよいかと思います。
とりあえずAlexa+todoist連携を試してみる
以下に、発話例がありますので、試してみましょう。
Amazon Alexa で Todoist を使う
https://get.todoist.help/hc/ja/articles/360010721059-Amazon-Alexa-%E3%81%A7-Todoist-%E3%82%92%E4%BD%BF%E3%81%86例えば以下をAmazon Echoに話しかけてみましょう。
・アレクサ、やることリストに洗濯を追加して。
・アレクサ、今日のやることリストは?
・アレクサ、やることリストの洗濯を完了にして
・アレクサ、買い物リストに納豆を追加して
・アレクサ、今日の買い物リストは?
・アレクサ、買い物リストの納豆を完了にしてめでたく、こんな感じで追加されました。
Node.jsサーバからtodoistのリストを取得する
たいていの方は上記まででよいのですが、勉強をかねて、拡張に挑戦します。
Node.jsからの操作には、以下のnpmモジュールを利用させていただきました。romgrk/node-todoist
https://github.com/romgrk/node-todoist以下も使っています。
node-fetch/node-fetch
https://github.com/node-fetch/node-fetchリスト取得する前に、ユーザごとにAPIトークンの取得が必要です。
todoist Developer: Authorization
https://developer.todoist.com/sync/v8/#authorizationOAuthに似たやり取りで、ブラウザ側とサーバ側での連携が必要です。
まずは、サーバのURLをtodoistに登録しておく必要があります。以下のURLを開いて、「Create a new app」ボタンを押下します。
App Management Console
https://developer.todoist.com/appconsole.htmlApp display nameには適当な名前を、App service URLには、これから立ち上げるNode.jsのWebページのURLを指定しておきます。
次に作成したappを選択肢、OAuth redirect URLを指定します。後で作成するのですが、立ち上げるNode.jsのWebページと同じにしておきます。(Single Page Applicationとして実装するため)。最後に、「Save settings」ボタンを押下します。
その時に表示される、「Client ID」「Client secret」を覚えておきます。後で使うので。
①サーバ側に、ユーザ識別子.jsonというファイルを作成しておきます。
これがばれてしまうと、ログインされてしまいますので、ユーザ識別子は、推測されにくいランダムな値にしましょう。②ブラウザからClient IDを指定して、ログインを開始する。
以下のように形成されるURLにジャンプします。
https://todoist.com/oauth/authorize?client_id=" + TODOIST_CLIENT_ID + "&scope=data:read&state=" + value
TODOIST_CLIENT_IDは、先ほど覚えておいたClient IDです。stateには正しくは乱数等推測されにくいものにするのですが、手を抜いていて、ユーザの識別子を指定します。①の通り、このユーザの識別子の名前+.jsonで、サーバ側にファイルが作成されている前提です。
③todoistのログインページが表示されるので、ログインする。
todoistのアカウント登録時に使った認証アカウントでログインします。
④認可コードを取得する
ログインが完了すると、OAuth redirect URLで指定したURLにジャンプしてきます。その時に、認可コードとstateが返ってきます。
stateは、②で指定した値のはずです。⑤認可コードからAPIトークンを取得する
認可コードを取得したので、これを使って以降のtodoistのWebAPI呼び出しに必要なAPIトークンを取得します。子の取得には、Client secretが必要です。秘匿の値として扱う必要があるので、Nodeサーバに渡して、サーバ側で実施します。
api/controllers/todoist/index.jsexports.handler = async (event, context, callback) => { var body = JSON.parse(event.body); var apikey = event.requestContext.apikeyAuth.apikey; if( !checkAlnum(apikey) ) throw 'apikey invalid'; var conf = await readConfigFile(apikey); if( event.path == '/todoist-callback' ){ var param = { client_id: TODOIST_CLIENT_ID, client_secret: TODOIST_CLIENT_SECRET, code: body.code }; var json = await do_post("https://todoist.com/oauth/access_token", param ); conf.token = json; await writeConfigFile(apikey, conf); return new Response({}); }elsebody.codeが、ブラウザから取得した認可コードです。
そうすると、todoistサーバから、APIトークンが返ってくるので、それをユーザ識別子ごとのファイルに保存します。ユーザ識別子はブラウザ側からAPI KeyとしてHTTPヘッダに指定してもらいます。あとは、ブラウザからのリクエストに対して、todoistのnpmモジュールTodoistを使ってリストを取得します。取得には、APIトークンが必要ですので、ユーザ識別子ごとのファイルから取り出して使っています。
api/contollers/todoist/index.jsif( event.path == '/todoist-list' ){ const todoist = Todoist(conf.token.access_token); await todoist.sync(); const projects = todoist.projects.get(); var favorite = projects.filter(item => item.is_favorite ); var favorite_ids = favorite.map(item => item.id); const items = todoist.items.get(); var favorite_items = items.filter(item => favorite_ids.includes(item.project_id)); const notes = todoist.notes.get(); var item_ids = favorite_items.map(item => item.id); var favorite_notes = notes.filter(item => item_ids.includes(item.item_id)); return new Response({items: favorite_items, projects: projects, notes: favorite_notes }); }あと、細かな処理をしていますが、やっているのは、
・プロジェクトIDからプロジェクト名に変換するために、プロジェクト一覧を取得
・プロジェクトのリストから、お気に入りにしたプロジェクトのIDを取得
・すべてのリストを取得し、お気に入りのプロジェクトIDのものを抽出
・すべてのノートを取得し、お気に入りのプロジェクトIDのものを抽出さきほどの、ブラウザ側とサーバ側の間の認可コードのやり取りの部分は以下です。
public/js/start.jsif( searchs.code ){ var param = { code: searchs.code }; history.replaceState(null, null, '.'); do_post_apikey(base_url + '/todoist-callback', param, searchs.state) .then(json =>{ console.log(json); Cookies.set("todo_apikey", searchs.state, { expires: EXPIRES }); this.apikey = searchs.state; this.todo_list_update() .then(() =>{ setInterval( () =>{ this.todo_list_update(true); }, UPDARTE_INTERVAL * 60 * 1000); }); });認証およびAPIキーがサーバ側で保持出来たら認証完了です。ブラウザ側では、ユーザ識別子をapikeyとしてCookieに保持しておきます。
リスト取得は以下の部分です。
public/js/start.jstodo_list_update: async function(silent){ if( !this.apikey ) return; try{ if( !silent ) this.progress_open(); var param = {}; var json = await do_post_apikey(base_url + '/todoist-list', param, this.apikey); this.todo_projects = json.projects; this.todo_notes = json.notes; var today = new Date(); today.setHours(0, 0, 0, 0) var todayTime = today.getTime(); var tomorrow = new Date(); tomorrow.setHours(0, 0, 0, 0) tomorrow.setDate(tomorrow.getDate() + 1); var tomorrowTime = tomorrow.getTime(); this.todo_list_expire = json.items.filter( item => item.due && Date.parse(item.due.date) < todayTime ); this.todo_list_today = json.items.filter( item => item.due && Date.parse(item.due.date) >= todayTime && Date.parse(item.due.date) < tomorrowTime ); this.todo_list_other = json.items.filter( item => item.due && Date.parse(item.due.date) >= tomorrowTime ); this.todo_list_someday = json.items.filter( item => !item.due ); }catch(error){ console.error(error); alert(error); }finally{ if( !silent ) this.progress_close(); } },これまたいろいろやっていますが、要は、期限切れのタスクの抽出、今日のタスクの抽出、明日以降のタスクの抽出、期限が設定されていないタスクの抽出、をしています。
this.todo_list_XXXXという変数に格納していますが、あとはVueが表示をよろしくやってくれます。ブラウザで表示してみましょう。
https://【立ち上げたサーバのURL】/index.html
最初に、API Keyの設定が必要です。「API Key」ボタンを押下して、ユーザ識別子を設定します。
さきほど追加した「洗濯」が表示されています。
期限を設定していなかったので、いつか のグループに入っています。todoistのAndroidアプリでもいいですし、todoistのWebページからでもどちらでよいですが、「洗濯」タスクの期限を今日にしてみましょう
10分ごとに、リロードしておいたので、ちょっと?待てば、今日のタスクに移動するかと思います。
PWA化する
詳細は、以下を参考にしてください。今回はPush通知は使っていません。
以下を追記しています。
index.htmlに以下を追加。
public/index.html<link rel="manifest" href="manifest.json">ページロード直後に以下を呼び出し。
public/js/start.jsif ('serviceWorker' in navigator) { navigator.serviceWorker.register('sw.js').then(async (registration) => { console.log('ServiceWorker registration successful with scope: ', registration.scope); }).catch((err) => { console.log('ServiceWorker registration failed: ', err); }); }sw.jsを作成し、public/sw.jsにデプロイ
これで、右上のアドレスバーのところの⊕を押せば、PWAアプリとしてインストールできるようになっているかと思います。
Androidでアプリ固定化
余っているAndroidタブレットを使って、常時リスト表示したいと思います。
Androidから同様にブラウザ(Chrome)から開き、メニューから「アプリをインストール」を選択して、アプリとしてインストールしておきます。
まずは、単独で起動でき、リストが表示されるところまで確認しておきます。次に、アプリを固定化します。
(以降は、Androidのバージョンによって見え方は違うかもしれません。)Androidの「設定」→「セキュリティ」→「アプリの固定」を選択します。
おそらくOFFになっているかと思いますので、ONにします。次に、現在実行中のアプリ一覧を表示し、PWAアプリとして起動したものの上にあるアイコンをタッチします。
そうすると、「固定」があるので、それをタッチします。こんな表示が出てきますので、よく読んで「OK」をタッチします。
これで、常時表示ができました。他のアプリが選択できなくなったと思います!できました!
アプリ固定を解除して戻りたい時には、ホームボタンと戻るボタンを一緒に長押しすれば戻れます。
以上
- 投稿日:2020-11-29T16:27:36+09:00
AlexaとTodoistでやることリスト・お買い物リスト
Todoistって、Amazon Echoと連携できるんですね!
しかもWebAPIが充実しているので、さらに連携の輪が広がりそうです。ちなみに、Todoistは、タスク管理ツールです。一般には、やることリストとか、買い物リストが挙げられますが、メモ的に使えて、かつ、期限を設定して予定を立てたり、失念するのを避けるのに役立ちます。
todoist
https://todoist.com/今回作成する全体の構成はこんな感じです。
すでに、todoistはAlexaとの連携をサポートしていますし、AndroidやiPhone用のアプリもありますので、その部分は特に難しいところはありません。
今回は、やることリスト・買い物リストの表示をWebページとして表示します。todoistはオフィシャルでWebAPIが充実していますし、npmモジュールもあるので、活用の幅が広がります。
また、さらに、PWA化することでネイティブアプリ化し、それをAndroidで固定アプリに設定することで、やることリスト表示の専用機に仕立て上げます。毎度のことですが、ソースコードもろもろをGitHubに上げておきました。
poruruba/todolist
https://github.com/poruruba/todolist準備
todoistのアカウント登録
まずは、以下のURLから、todoistのアカウント登録をします。
todoist
https://todoist.com/ja「はじめる」ボタンまたは右上のサインアップをクリックします。
ここでは、スマートフォンにインストールしているAmazon Alexaアプリに設定しているアカウントでサインアップし、認証を完了させます。
これでアカウントが登録されました。
todoistアプリをインストール
Google Playから、Todoistをインストールします。
todoistにアカウント登録したときのアカウントでログインします。
ログインが完了しました。
Alexa連携を設定
すでに、スマートフォンに、「Amazon Alexa」がインストールされている前提で進めます。
以降は、Androidでの操作です。まずは、下の方の「その他」→「設定」→「リスト」を選択します。
そうすると、Any.do、AnyList、Todoistが表示されていますので、Todoistの右側の⊕をタッチします。
以下のように表示されるので、「有効にして使用する」ボタンを押下します。
リストへのアクセス権の確認が表示されますので、「アクセス権を保存」ボタンを押下します。
そうすると、todoistのサイトに飛んで、Agreeするかが聞かれますので、「Agree」ボタンを押下します。
これで、連携設定が完了しました。
もう一度、todoistアプリの方を開いてみましょう。
そうすると、左上の三のマークをタッチし、プロジェクトのところをタッチすると、「Alexa ToDo リスト」というのと、「Alexaの買い物リスト」が増えているのがわかります。最後に、この「Alexa ToDo リスト」と「Alexaの買い物リスト」をお気に入りにしておきます。
「プロジェクト」→「プロジェクトを管理」をタッチし、「Alexa ToDo リスト」と「Alexaの買い物リスト」の右側にあるハートマークをタップし、赤色にします。これによって、この2つがお気に入りのプロジェクトとなりました。この意味はあとで、わかります。あと、お好みで、todoistアプリをAndroidのホーム画面にウィジェットとして登録してもよいかと思います。
とりあえずAlexa+todoist連携を試してみる
以下に、発話例がありますので、試してみましょう。
Amazon Alexa で Todoist を使う
https://get.todoist.help/hc/ja/articles/360010721059-Amazon-Alexa-%E3%81%A7-Todoist-%E3%82%92%E4%BD%BF%E3%81%86例えば以下をAmazon Echoに話しかけてみましょう。
・アレクサ、やることリストに洗濯を追加して。
・アレクサ、今日のやることリストは?
・アレクサ、やることリストの洗濯を完了にして
・アレクサ、買い物リストに納豆を追加して
・アレクサ、今日の買い物リストは?
・アレクサ、買い物リストの納豆を完了にしてめでたく、こんな感じで追加されました。
Node.jsサーバからtodoistのリストを取得する
たいていの方は上記まででよいのですが、勉強をかねて、拡張に挑戦します。
Node.jsからの操作には、以下のnpmモジュールを利用させていただきました。romgrk/node-todoist
https://github.com/romgrk/node-todoist以下も使っています。
node-fetch/node-fetch
https://github.com/node-fetch/node-fetchリスト取得する前に、ユーザごとにAPIトークンの取得が必要です。
todoist Developer: Authorization
https://developer.todoist.com/sync/v8/#authorizationOAuthに似たやり取りで、ブラウザ側とサーバ側での連携が必要です。
まずは、サーバのURLをtodoistに登録しておく必要があります。以下のURLを開いて、「Create a new app」ボタンを押下します。
App Management Console
https://developer.todoist.com/appconsole.htmlApp display nameには適当な名前を、App service URLには、これから立ち上げるNode.jsのWebページのURLを指定しておきます。
次に作成したappを選択肢、OAuth redirect URLを指定します。後で作成するのですが、立ち上げるNode.jsのWebページと同じにしておきます。(Single Page Applicationとして実装するため)。最後に、「Save settings」ボタンを押下します。
その時に表示される、「Client ID」「Client secret」を覚えておきます。後で使うので。
①サーバ側に、ユーザ識別子.jsonというファイルを作成しておきます。
これがばれてしまうと、ログインされてしまいますので、ユーザ識別子は、推測されにくいランダムな値にしましょう。②ブラウザからClient IDを指定して、ログインを開始する。
以下のように形成されるURLにジャンプします。
https://todoist.com/oauth/authorize?client_id=" + TODOIST_CLIENT_ID + "&scope=data:read&state=" + value
TODOIST_CLIENT_IDは、先ほど覚えておいたClient IDです。stateには正しくは乱数等推測されにくいものにするのですが、手を抜いていて、ユーザの識別子を指定します。①の通り、このユーザの識別子の名前+.jsonで、サーバ側にファイルが作成されている前提です。
③todoistのログインページが表示されるので、ログインする。
todoistのアカウント登録時に使った認証アカウントでログインします。
④認可コードを取得する
ログインが完了すると、OAuth redirect URLで指定したURLにジャンプしてきます。その時に、認可コードとstateが返ってきます。
stateは、②で指定した値のはずです。⑤認可コードからAPIトークンを取得する
認可コードを取得したので、これを使って以降のtodoistのWebAPI呼び出しに必要なAPIトークンを取得します。この取得には、Client secretが必要です。秘匿の値として扱う必要があるので、Nodeサーバに渡して、サーバ側で実施します。
api/controllers/todoist/index.jsexports.handler = async (event, context, callback) => { var body = JSON.parse(event.body); var apikey = event.requestContext.apikeyAuth.apikey; if( !checkAlnum(apikey) ) throw 'apikey invalid'; var conf = await readConfigFile(apikey); if( event.path == '/todoist-callback' ){ var param = { client_id: TODOIST_CLIENT_ID, client_secret: TODOIST_CLIENT_SECRET, code: body.code }; var json = await do_post("https://todoist.com/oauth/access_token", param ); conf.token = json; await writeConfigFile(apikey, conf); return new Response({}); }elsebody.codeが、ブラウザから取得した認可コードです。
そうすると、todoistサーバから、APIトークンが返ってくるので、それをユーザ識別子ごとのファイルに保存します。ユーザ識別子はブラウザ側からAPI KeyとしてHTTPヘッダに指定してもらいます。あとは、ブラウザからのリクエストに対して、todoistのnpmモジュールTodoistを使ってリストを取得します。取得には、APIトークンが必要ですので、ユーザ識別子ごとのファイルから取り出して使っています。
api/contollers/todoist/index.jsif( event.path == '/todoist-list' ){ const todoist = Todoist(conf.token.access_token); await todoist.sync(); const projects = todoist.projects.get(); var favorite = projects.filter(item => item.is_favorite ); var favorite_ids = favorite.map(item => item.id); const items = todoist.items.get(); var favorite_items = items.filter(item => favorite_ids.includes(item.project_id)); const notes = todoist.notes.get(); var item_ids = favorite_items.map(item => item.id); var favorite_notes = notes.filter(item => item_ids.includes(item.item_id)); return new Response({items: favorite_items, projects: projects, notes: favorite_notes }); }あと、細かな処理をしていますが、やっているのは、
・プロジェクトIDからプロジェクト名に変換するために、プロジェクト一覧を取得
・プロジェクトのリストから、お気に入りにしたプロジェクトのIDを取得
・すべてのリストを取得し、お気に入りのプロジェクトIDのものを抽出
・すべてのノートを取得し、お気に入りのプロジェクトIDのものを抽出(参考) todoist Sync API
https://developer.todoist.com/sync/v8/さきほどの、ブラウザ側とサーバ側の間の認可コードのやり取りにおいて、ブラウザ側の部分は以下です。
public/js/start.jsif( searchs.code ){ var param = { code: searchs.code }; history.replaceState(null, null, '.'); do_post_apikey(base_url + '/todoist-callback', param, searchs.state) .then(json =>{ console.log(json); Cookies.set("todo_apikey", searchs.state, { expires: EXPIRES }); this.apikey = searchs.state; this.todo_list_update() .then(() =>{ setInterval( () =>{ this.todo_list_update(true); }, UPDARTE_INTERVAL * 60 * 1000); }); });認証およびAPIトークンがサーバ側で保持出来たら認証完了です。ブラウザ側では、ユーザ識別子をapikeyとしてCookieに保持しておきます。
リスト取得は以下の部分です。
public/js/start.jstodo_list_update: async function(silent){ if( !this.apikey ) return; try{ if( !silent ) this.progress_open(); var param = {}; var json = await do_post_apikey(base_url + '/todoist-list', param, this.apikey); this.todo_projects = json.projects; this.todo_notes = json.notes; var today = new Date(); today.setHours(0, 0, 0, 0) var todayTime = today.getTime(); var tomorrow = new Date(); tomorrow.setHours(0, 0, 0, 0) tomorrow.setDate(tomorrow.getDate() + 1); var tomorrowTime = tomorrow.getTime(); this.todo_list_expire = json.items.filter( item => item.due && Date.parse(item.due.date) < todayTime ); this.todo_list_today = json.items.filter( item => item.due && Date.parse(item.due.date) >= todayTime && Date.parse(item.due.date) < tomorrowTime ); this.todo_list_other = json.items.filter( item => item.due && Date.parse(item.due.date) >= tomorrowTime ); this.todo_list_someday = json.items.filter( item => !item.due ); }catch(error){ console.error(error); alert(error); }finally{ if( !silent ) this.progress_close(); } },これまたいろいろやっていますが、要は、期限切れのタスクの抽出、今日のタスクの抽出、明日以降のタスクの抽出、期限が設定されていないタスクの抽出、をしています。
this.todo_list_XXXXという変数に格納していますが、あとはVueが表示をよろしくやってくれます。ブラウザで表示してみましょう。
https://【立ち上げたサーバのURL】/index.html
最初に、API Keyの設定が必要です。「API Key」ボタンを押下して、ユーザ識別子を設定します。
さきほど追加した「洗濯」が表示されています。
期限を設定していなかったので、いつか のグループに入っています。todoistのAndroidアプリでもいいですし、todoistのWebページからでもどちらでよいですが、「洗濯」タスクの期限を今日にしてみましょう
10分ごとに、リロードしておいたので、ちょっと?待てば、今日のタスクに移動するかと思います。
PWA化する
詳細は、以下を参考にしてください。今回はPush通知は使っていません。
以下を追記しています。
index.htmlに以下を追加。
public/index.html<link rel="manifest" href="manifest.json">ページロード直後に以下を呼び出し。
public/js/start.jsif ('serviceWorker' in navigator) { navigator.serviceWorker.register('sw.js').then(async (registration) => { console.log('ServiceWorker registration successful with scope: ', registration.scope); }).catch((err) => { console.log('ServiceWorker registration failed: ', err); }); }sw.jsを作成し、public/sw.jsにデプロイ
これで、右上のアドレスバーのところの⊕を押せば、PWAアプリとしてインストールできるようになっているかと思います。
Androidでアプリ固定化
余っているAndroidタブレットを使って、常時リスト表示したいと思います。
Androidから同様にブラウザ(Chrome)から開き、メニューから「アプリをインストール」を選択して、アプリとしてインストールしておきます。
まずは、単独で起動でき、リストが表示されるところまで確認しておきます。次に、アプリを固定化します。
(以降は、Androidのバージョンによって見え方は違うかもしれません。)Androidの「設定」→「セキュリティ」→「アプリの固定」を選択します。
おそらくOFFになっているかと思いますので、ONにします。次に、現在実行中のアプリ一覧を表示し、PWAアプリとして起動したものの上にあるアイコンをタッチします。
そうすると、「固定」があるので、それをタッチします。こんな表示が出てきますので、よく読んで「OK」をタッチします。
これで、常時表示ができました。他のアプリが選択できなくなったと思います!できました!
アプリ固定を解除して戻りたい時には、ホームボタンと戻るボタンを一緒に長押しすれば戻れます。
以上
- 投稿日:2020-11-29T15:34:28+09:00
Steinで複数条件(AND)でデータをとれない。。。
let userNameList = await store.read(tranSheet, { search: { group_no: groupNo.toString,date: today } }).then(data => { return data }).catch(e => console.log(e))結果のデータを見ると、2つ項目を指定するのはできるのだが、
最初の条件しかきいてないように見える。確かに、この書き方だとANDかORかわからないもんね。
- 投稿日:2020-11-29T15:34:28+09:00
Steinで複数条件(AND)でデータをとれない。。。⇒取れた メモ
let userNameList = await store.read(tranSheet, { search: { group_no: groupNo.toString,date: today } }).then(data => { return data }).catch(e => console.log(e))結果のデータを見ると、2つ項目を指定するのはできるのだが、
最初の条件しかきいてないように見える。確かに、この書き方だとANDかORかわからないもんね。
Steinのページには、Columnを複数指定できそうな記載があるのだが、、、
何を変えたわけではないが、なぜか取れるようになった。
let userNameList = await store.read(tranSheet, { search: { date: today, group_no: groupNo.toString() } }).then(data => { return data }).catch(e => console.log(e))何、この現象。。。
見えた!
STEP1 group_noの型が数字でとろうとしていたから取れなかった。
STEP2 group_noの文字列に変換がうまくいってなかった。
STEP3 うまく変換できて取れた!toString()になっていなかったからか!
- 投稿日:2020-11-29T15:05:45+09:00
オンライン配信授業で、手を挙げたら【●●さん】とさしてくれるwebアプリを作ってみた
自己紹介
大学時代C言語を学んでから十数年以来のプログラミングを始めています。
本業はweb制作で、お客様から悩みや感じている課題についてに相談を受ける事も多いです。
LINEbotを中心に、webの技術を利用してどのようにお客様の悩みや課題を解決出来るのか、広く学んでいます!今回は、Teachable Machineというgoogleが提供する機械学習が簡単に行えるウェブブラウザツールを知り、普段の生活に取り入れてみる事にしました!
【オンライン授業でのメンタルサポートAI】
実際に小学校中学年の子どもがオンラインで配信される塾の授業を受けているのですが、テレビ同様一方通行の授業なので、子供のやる気に波があるんです。
すぐに画面からいなくなったり、気が付いたら違う事をしていたり。。。
やっぱり対面での先生の問いかけがあったら、もっと積極的に授業に参加できるのになとは思いますが、このようなご時世なのでオンラインの方が安心ですよね。家での経験だけではなく、オンライン授業が浸透してきいるなという実感が最近ありました。先日とある塾の保護者会で、通学に不安を感じる低学年向けに配信授業コースを開設するという話がありました。
その時に、低学年だとさらに親が管理をする負担が大きいな~と思っていた所、オンライン授業をサポートするAIがあったら面白い!と思いついたので早速実装してみる事にしました。
シュミレーションです。
こんな機能があったら良いなと思ってます。子どもの表情や動きを機械学習して、状況を判断して問いかけます。
全体の流れ
今回は、機械学習初心者なので単純に
「手を挙げたらさしてくれる」
をまずはwebアプリとして実装します。
実際に作ったもの
webにアップしているのでクリックしていただくと体験できます
開発環境・利用ツール
・Teachable Machine
・Visual Code
・Line API
・node.js
・vue.jsTeachable Machineで画像サンプルを登録
Teachable Machineの使い方は【備忘録】Teachable Machineの利用方法で紹介しています。
1.手を挙げない
2.手を挙げるこの2パターンの画像を学習させます。
登録をしたらトレーニング
トレーニングしてTeachable Machine上では成功!
#Visual Conde
いよいよ、node.jsを利用して、Teachable Machineで作成したモデルを利用して、web上で動作させるようにしたいと思います。<body> <div class="wrap"> <h1>わかったらてをあげてくださいね!</h1> <div id="app"> <input v-model="name" placeholder="おなまえをおしえてください" size="30"> <p>おななえ: {{name}}</p> <p>よろしく おねがいします</p> <p>わかったら てのひらがみえるように てをあげてくださいね</p> <p class="res">{{result}}</p> <video id="video" width="640" height="480" autoplay></video> </div> </div> <script src="https://unpkg.com/ml5@latest/dist/ml5.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> <script> const imageModelURL = 'https://teachablemachine~始まるコードを入力します。'; let video; // let classifier; const app = new Vue({ el: '#app', data: { modelStat: 'モデルロード中...', detectedName: '見つかってない', name: '', result: '', }, //最初の1度だけ実装 async created() { // カメラからの映像取得 const stream = await navigator.mediaDevices.getUserMedia({ audio: false, video: true, }); // html内のidがvideoのdomを取得 video = document.getElementById('video'); // videoにカメラ映像を適用 video.srcObject = stream; // 自作モデルのロード classifier = await ml5.imageClassifier( imageModelURL + 'model.json', video, modelLoaded ); // モデルのロード完了時に実行される async function modelLoaded() { console.log('Model Loaded!'); app.modelStat = 'モデルデータのロード完了'; } async function interval() { // 挙手しているかjudge const myHandResults = await classifier.classify(); const myHand = myHandResults[0].label; if (myHand == "手を挙げる") { app.result = app.name + "さんっ!"; } else { app.result = ""; } console.log(myHand); } setInterval(interval, 3000); // 3秒ごとに処理 }, }); </script> </body>今回ポイントとなったのは2か所
Vue.jsのフォーム入力バインディングを使って、入力した内容を簡単に出力しています。
Vue.jsのドキュメント<input v-model="name" placeholder="おなまえをおしえてください" size="30"> <p>おななえ: {{name}}</p>リアルタイム処理をしたいので3秒間隔で判断しています。
setInterval(interval, 3000); // 3秒ごとに処理さいごに
実装していて、色々なアイディアが出てきました。
また、顔の表情を認識して、眠そうであれば楽しそうな音楽を流す等反応するAIも面白いと思いました。ただ、表情のサンプルを子供から撮るのが難しくて失敗。
その他にも、百人一首の正誤判定する音声認識も企画しましたが、2秒しか音声サンプル入力できないので、出だしの「ありあけの~」で終わってしまうので、これも失敗。
結果として、今回は手を挙げているか挙げていないかだったので、高確率で判断してくれる機械学習を体験する事が出来ました。ポーズを使うと骨格が取れるので、より高度に認識できるみたいです。
機械学習を使って、一方通行のオンライン配信授業でも授業に参加しているような感覚を体験する事で、オンライン授業がより充実したものになっていくのではないかな~と思いました。
- 投稿日:2020-11-29T07:40:19+09:00
[Node.js] response.writeHeadとresponse.setHeaderって何が違うの?
はじめに
Node.jsを勉強していて、setHeaderとwriteHeadって何が違うの...?となったのでソースコードなどから調べてみました。
setHeaderのソースコードを見てみる
setHeaderの実装部分を示します。setHeaderはheadersに名前と値を格納しているだけということがわかります。
headersに値を格納しているので、例えばgetHeader(name)によって値を取得することができます。OutgoingMessage.prototype.setHeader = function setHeader(name, value) { if (this._header) { throw new ERR_HTTP_HEADERS_SENT('set'); } validateHeaderName(name); validateHeaderValue(name, value); let headers = this[kOutHeaders]; if (headers === null) this[kOutHeaders] = headers = ObjectCreate(null); headers[name.toLowerCase()] = [name, value]; };(https://github.com/nodejs/node/blob/master/lib/_http_outgoing.js)
writeHeadのソースコードを見てみる
writeHeadの一部を抜き出したものを示します。
this[kOutHeaders]がtruthyな値かどうかで場合分けがされています。this[kOutHeaders]はヘッダーの名前と値をキャッシュしているデータになるので、setHeader()などでキャッシュされているかどうかで分かれることになります。
setHeader()が呼び出されていた場合、writeHeadの内部でもsetHeader()が実行され、writeHeadの引数で指定した名前と値もキャッシュされます。
一方、setHeader()が呼び出されていない場合、引数で指定した名前と値をheadersに格納するだけとなります。let headers; if (this[kOutHeaders]) { // Slow-case: when progressive API and header fields are passed. let k; if (ArrayIsArray(obj)) { if (obj.length % 2 !== 0) { throw new ERR_INVALID_ARG_VALUE('headers', obj); } for (let n = 0; n < obj.length; n += 2) { k = obj[n + 0]; if (k) this.setHeader(k, obj[n + 1]); } } else if (obj) { const keys = ObjectKeys(obj); for (let i = 0; i < keys.length; i++) { k = keys[i]; if (k) this.setHeader(k, obj[k]); } } if (k === undefined && this._header) { throw new ERR_HTTP_HEADERS_SENT('render'); } // Only progressive api is used headers = this[kOutHeaders]; } else { // Only writeHead() called headers = obj; }(https://github.com/nodejs/node/blob/299984561eff45bddc5bb802e5b22d47277e5ca5/lib/_http_server.js)
具体例
setHeaderを使っている場合は、console.log(res.getHeader("content-Type"));が実行されると
content-Type':'text/html; charset=UTF-8と表示されます。"use strict" const http = require("http"); const server = http.createServer((req, res) => { res.setHeader('Set-Cookie', "accessed_date=" + Date.now() + ";"); res.writeHead(200, { 'content-Type':'text/html; charset=UTF-8' }); res.end(); console.log(res.getHeader("content-Type")); // => 'content-Type':'text/html; charset=UTF-8' }); const port = 8000; server.listen(port, () => { console.info("listening on" + port); })setHeaderを使っていない場合undefinedと表示されます。
"use strict" const http = require("http"); const server = http.createServer((req, res) => { res.writeHead(200, { 'content-Type':'text/html; charset=UTF-8' }); res.end(); console.log(res.getHeader("content-Type")); // => undefined }); const port = 8000; server.listen(port, () => { console.info("listening on" + port); })まとめ
- setHeaderを使うと値をキャッシュすることができる
- 例えばgetHeaderなどで値を取り出すことができる
- setHeaderの実行後はwriteHeadでも値のキャッシュが行われるようになる
- writeHeadだけを使うと値のキャッシュなどが行われない
- その分、処理は早くなる
- 投稿日:2020-11-29T04:43:43+09:00
prettierを速く動かしたくてparallel-prettierに.prettierignoreを読む修正を加えて動かして速くなったか
背景
prettier とは
prettier はソースコードを整形するフォーマッターです。
ごく少ない設定項目しか持たないため、事前準備をほとんどすることなく使い始めることができます。prettierは複数のプログラミング言語を整形することができます。
今回はprettierでJavaScriptのソースコードをフォーマットします。prettierの並列化
500ファイルのJavaScriptのソースコードをバンドルする際に
prettier --check
コマンドを実行して、フォーマット漏れが無いか確認しています。
実行すると3.33秒掛かかります。~ time npx prettier --check 'src/lib/**/*.js' All matched files use Prettier code style! ________________________________________________________ Executed in 3.33 secs fish external usr time 4.92 secs 125.00 micros 4.92 secs sys time 0.20 secs 875.00 micros 0.20 secsこれをもう少し速くしたいです。
prettier
はファイル単位で、フォーマットをチェックします。
ファイル間の依存関係がないため、ファイル単位での並列化は容易なはずです。現にparallel-prettierという、prettierを並列実行するラッパーコマンドが存在します。
今どきの開発用PCのCPUはマルチコアです。
依存関係のないタスクを並列化すれば、コア数が許す限り理想値に近い性能向上が見込めそうです。
4コアのCPUで4並列化すれば、4倍の早さ、0.8
秒でチェックが完了するのではないでしょうか?解決したい課題
parallel-prettierの性能は?
さっそく
parallel-prettier
を使ってみましょう。~ time npx pprettier --check 'src/lib/**/*.js' bundle.js modules/jquery.jsPlumb-1.5.5-min.js modules/jquery.jsPlumb-1.5.5.js ✖ 3 files were not formatted ________________________________________________________ Executed in 9.18 secs fish external usr time 488.60 millis 126.00 micros 488.48 millis sys time 107.34 millis 903.00 micros 106.44 millis実行時間が9秒に伸びました!?
parallel-prettierは
.prettierignore
を無視原因はparallel-prettierが
.prettierignore
を無視しているためです。対象ディレクトリにはいくつかのバンドル済みライブラリが配置されています。
それらのファイルは.prettierignore
ファイルに記載して、prettierの対象から外しています。
.prettierignore
を削除して、prettierを実行すると~ time npx prettier --check 'src/lib/**/*.js' Checking formatting... [warn] src/lib/bundle.js [warn] src/lib/modules/jquery.jsPlumb-1.5.5.js [warn] Code style issues found in the above file(s). Forgot to run Prettier? ________________________________________________________ Executed in 9.02 secs fish external usr time 12.89 secs 111.00 micros 12.89 secs sys time 0.50 secs 834.00 micros 0.50 secs実行時間は9秒に伸び、チェックに失敗します。
parallel-prettierとほぼ同じ結果になります。これらのライブラリをparallel-prettierのチェック対象から除外しなくては、高速化できません。
また、チェックに失敗するので、本来の目的であるフォーマット済みの検証ができなくなります。parallel-prettierが
.prettierignore
を無視するのを解決するprettierのAPIにはprettier.getFileInfoがあります。
これを使うと.prettierignore
ファイルに記載されているかどうか判定できます。
次のように使います。const { ignored } = await prettier.getFileInfo(file.path, { ignorePath: './.prettierignore', });これをparallel-prettierに組み込んだものが
です。
結果
上記ブランチをチェックアウトして
npm run build
を実行するとdist/src/index.js
ができます。~ time ~/parallel-prettier/dist/src/index.js --check 'src/lib/**/*.js' ✔ Checked 554 files ________________________________________________________ Executed in 2.67 secs fish external usr time 422.47 millis 133.00 micros 422.34 millis sys time 85.01 millis 869.00 micros 84.14 millis2.67秒に縮まりました。やったね1.25倍速くなりました!
「えーっ、2倍にもならないのかよ・・・」がっかりです。参考に、
prettierignore
に記載しているライブラリを削除して、本家parallel-prettierを使ってみます。~ time npx pprettier --check 'src/lib/**/*.js' ✔ Checked 551 files ________________________________________________________ Executed in 2.81 secs fish external usr time 412.89 millis 127.00 micros 412.76 millis sys time 84.67 millis 962.00 micros 83.70 millis2.81秒です。
prettierignore
を参照したことで遅くなったわけではありません。
今回のファイル群は、並列化してもあまり速くならないようです。考察
今回の事例ではparallel-prettier並列化してもあまり速くなりませんでした。
並列化して速くならないことは、よくあることです。しかし、parallel-prettierのREADMEには、16〜22倍高速化されたと書いてあります。
これは納得いきません。
原因の候補を考えてみます。prettierのフォーマット処理はCPUバウンドでない?
フォーマット対象ファイルの読み込みがボトルネック?
parallel-prettierはファイル読み込みもワーカーに分散して、並列化しています。PCのIOの限界に到達するまで高速化するはず。
PCのIOが限界に到達するか調べる方法がわかりません。
メモリ不足で仮想ディスクを使っている?
メモリ余ってます。
ファイル単位の並列化ではボトルネックが解消されない?
特定のファイルのフォーマットがボトルネック?
否定する決め手はありません。
ファイル単位のフォーマット実行速度を計測すると良さそうに思います。ファイルのフォーマット処理より、ファイル収集処理がボトルネック?
printfデバッグした感じ、フォーマット処理が始まるまでに秒単位の時間はかかっていません。
prettierが十分高速?
prettierはすでに、並列化されていて、さらに並列化しても速くならない?
現象としては魅的な理由です。
例えば、prettier実行時、nodeプロセスのCPU使用率が100%を超えています。
もっともらしく思えます。
単に、Node.js自体は並列化され(マルチスレッドで動い)ているからです。Feature Request: Parallel/Clustered Prettier · Issue #4980 · prettier/prettierがopen中なのと矛盾しています。
また、prettierのソースコードを検索しても、
cluster
、child_process
、spawn
、worker-thread
、cpu
などのそれらしい単語は含まれていません。
並列化しているとは思えません。parallel-prettierの使っているprettierのバージョンが古い?
最新バージョン
2.2.1
を使ってもparallel-prettierの実行時間に変化はありませんでした。マルチプロセスによる並列化ではV8のJITコンパイラの効きが悪い?
- parallel-prettierのREADMEに載っているスコアと矛盾しています。
- Node.jsのCluster APIの存在とも矛盾します。
過去の経験上からもなさそうな気はします。
sosukesuzuki/prettier-parallel: Runs Prettier with Worker Threads はWorker Threadを使ってprettierを並列化しています。
parallel-prettierとprettier-parallelの性能を比較すると何かわかるかもしれません。
prettier-parallelはフォーマット対象のファイルが決め打ちです。
試すには何かしら修正が必要です。オチ
実のところwebpackの実行に8秒かかっているため、prettierの3秒は、バンドル手順全体からみるとボトルネックではありません。