20190622のNode.jsに関する記事は8件です。

nodenvで最新のNode.jsを入れてみる

lightsail起動して、nodenvでNode.js入れるまで。普通にlithgsailのNode.js入ってるイメージ使えばいいんだけど、何か色々の用途で使いたかったからプレーンなOSスタートだった。

nodenvインストールする

https://github.com/nodenv/nodenv

この辺見ながら上からする。

% git clone https://github.com/nodenv/nodenv.git ~/.nodenv
% cd ~/.nodenv && src/configure && make -C src

使ってるshellはzshなので指示に従ってzshrcでPATHを通す。

~/.zshrc
export PATH="$HOME/.nodenv/bin:$PATH"

initしろって書いてあるからする。

% ~/.nodenv/bin/nodenv init
# Load nodenv automatically by appending
# the following to ~/.zshrc:

eval "$(nodenv init -)"

なぜzshrcにもう一度書き込まなきゃならないのか分からないけど、書いてあるからする。

~/.zshrc
export PATH="$HOME/.nodenv/bin:$PATH"
eval "$(nodenv init -)"

nodenv-doctor とやらが状態を見てくれるというのでする。これで全部OKって出るらしい。

% curl -fsSL https://github.com/nodenv/nodenv-installer/raw/master/bin/nodenv-doctor | bash
Checking for `nodenv' in PATH: /home/ec2-user/.nodenv/bin/nodenv
Checking for nodenv shims in PATH: OK
Checking `nodenv install' support: not found
Unless you plan to add Node versions manually, you should install node-build.
Please refer to https://github.com/nodenv/node-build#installation

Counting installed Node versions: none
  There aren't any Node versions installed under `/home/ec2-user/.nodenv/versions'.
  You can install Node versions like so: nodenv install 2.2.4
Auditing installed plugins: OK

nodenv installnot found って言ってて急に裏切られた気になる。

node-build入れる

Optionalって書いてあるけど、 node-build を入れなきゃだめみたいだ。

https://github.com/nodenv/node-build#readme

上記を見ると、インストール先が /usr/local になってる。今までec2-userでnodenv入れようとしてたけど、大丈夫なのかな? PATHが通ってればいいのかもしれない。rootになって下記をした。

$ git clone https://github.com/nodenv/node-build.git /usr/local/src
$ PREFIX=/usr/local /usr/local/src/node-build/install.sh

実行しても何も表示されないけど、これでいいっぽい。無慈悲。ec2-userに戻って、doctorに聞いてみる。

% curl -fsSL https://github.com/nodenv/nodenv-installer/raw/master/bin/nodenv-doctor | bash
Checking for `nodenv' in PATH: /home/ec2-user/.nodenv/bin/nodenv
Checking for nodenv shims in PATH: OK
Checking `nodenv install' support: /usr/local/bin/nodenv-install (node-build 4.6.1)
Counting installed Node versions: none
  There aren't any Node versions installed under `/home/ec2-user/.nodenv/versions'.
  You can install Node versions like so: nodenv install 2.2.4
Auditing installed plugins: OK

あ、installが使えるようになったっぽい。

最新のnode入れてみる

インストールできるバージョン確認

% nodenv install -l
・・・
12.4.0
・・・・

インストール

% nodenv install 12.4.0
Downloading node-v12.4.0-linux-x64.tar.gz...
-> https://nodejs.org/dist/v12.4.0/node-v12.4.0-linux-x64.tar.gz
Installing node-v12.4.0-linux-x64...
Installed node-v12.4.0-linux-x64 to /home/ec2-user/.nodenv/versions/12.4.0

結構早く終わる。

% node --version
nodenv: node: command not found

The `node' command exists in these Node versions:
  12.4.0

見つからないのか、見つかったのか、どっちなのか……。で、これはきっとアレだと思った。 use とか global でバージョン指定してないからだと思った。global らしい。

% nodenv global 12.4.0
% node --version
v12.4.0
% npm --version
6.9.0

あ、できた。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Node for MaxでTypeScriptを使う

つくったもの

Node for Max向けに簡単なTypeScriptの開発環境を作りました。

エディター
medium.gif

Max APIの型定義が効いているので、快適に開発ができます。

ソースコード: github

機能

ts-nodeモード

Max 8.0.4のアップデートnode.script にCLIオプションが渡せるようになりました。

これによって、 nodeの-rオプションが使えるようになったので、ts-node/registerを使うことができます。

つまり、.jsを吐かずにTypeScriptを実行できて、REPLとかもできちゃうし、 ファイルを吐き出す時間がないので、小さなプロジェクトなら実行にかかる時間もjsとほとんど変わらずサクサクです。

tscモード

tscを使ってトランスパイルしたファイルを通常通りに吐き出せるようにもしてあるので、ts-nodeが嫌いな人はそちらで。

ボイラープレートの構成(省略版)

├── @types
│ └── max-api.d.ts <-- MaxAPIの型定義ファイル
├── index.ts <-- ts-nodeモードのエントリーポイント
├── lib/ <-- tscしたらここに入る
├── main.maxpat <-- Maxはこれを開く
├── node_modules
├── package.json
├── run-transpiled.js <-- tscモードの時のエントリーポイント
└── tsconfig.json

ボイラープレートについて

メインのMaxファイルがこんな感じで、
Screen Shot 2019-06-22 at 21.17.23.png

これに対応するコードが

index.ts
import * as MaxAPI from 'max-api'
import * as fs from 'fs'
import { say } from 'cowsay'
import { Log } from './utils'

enum actionTypes {
  LIST_FILES = 'list-files',
  SAY = 'say'
}

MaxAPI.addHandler(actionTypes.LIST_FILES, () => {
  fs.readdir(process.cwd(), (err, files) => {
    if (err) {
      MaxAPI.post(err.message, "error")
    }
    const text = say({
      text: 'Node for Max is Awesome!'
    });

    Log(...files)
    MaxAPI.outlet(...files, text)
  })
})

MaxAPI.addHandler(actionTypes.SAY, (...whatever: string[]) => {
  const text = say({
    text: whatever.join(' ')
  })
  Log(text)
  MaxAPI.outlet(text)
})

こんな感じ。(サンプルがcowsayかよというツッコミはさておき、、)

で、umenuから ts-node を使うか tsc を使うか選択すると、

Screen Shot 2019-06-22 at 21.20.27.png

こんな感じでスイッチングされる。
node.script@options に渡しているのがそのままnodeに渡されるのでこれはつまり

node -r ts-node/register index.ts

ということ。 (-r オプションは指定したライブラリを実行前にrequireしてくれる)

tscモード の場合は右の node.script run-transpiled.js (※.jsに注目)が実行される。

これは単純に、

run-transpiled.js
/**
 * @description This file should only be called after source code is transpiled.
 * @author Yuichi Yogo
 */
require('./lib/index')

lib/ にtscでトランスパイルしたjsファイルが入ることが期待されているので(そこの同期処理はMaxパッチ側で書いてある)、それをシンプルにrequireするだけ。

まとめ

今回のボイラープレート作成にあたり、Max APIの型定義ファイルをCylclingがpublishしていないので、自分で用意しました。
とても不便なのでcycling '74にお願いしておきました。(対応してくれるかどうかは知らない)

そのうちnpm installで最新の型が手に入るようになるといいですね!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

toioを動かす(Node.js)

公式
https://github.com/toio/toio.js#computer-prerequisites

動作環境

image.png

node -v
v8.16.0

動作確認

git clone https://github.com/toio/toio.js.git   # clone repository
cd toio.js                                      # move to repository root
yarn install                                    # install dependencies
yarn build                                      # build @toio/* packages
yarn example:keyboard-control                  # start sample application (see below)

yarnがないときは適宜

durationに0を設定すると楽しい!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【2019年6月版】jsのクロージャとconst

結論

クロージャの中で使いたい変数の宣言に注意しよう。

  • 参照のみの変数は const 宣言マスト。でもあとは気にしなくてOK。
  • 書き込みしたい変数は let 宣言して腹をくくる

時々、忘れてしまうのでメモです。

クロージャ内から使用される変数に刮目せよ

js ではクロージャをバンバン使いますが、クロージャ内で使用する変数の宣言を適当にしてしまうとバグを埋め込む原因となります。
例えば以下のようなコードで、

// DB や APIなどから非同期で処理〜
function getPromise(c) {
    return new Promise((accept)=>accept(c));
}

// 何かのマスターを元にループで処理したい
const array = [1,2,3,4,5];
for(a of array) {
    msg = `[${a}] <- イマココ`;
    getPromise(a).then(v => {
        console.log(`値: ${v}`);
        console.log(`処理中: ${msg}`);
    })
     msg = `[${a}] <- 終了!お疲れ様でした。`
    console.log(msg);
}

getPromise(a).then(v => {...} で変数 a の値が変数 v に渡されてきますが、この v => {...} の中で参照されている変数 msg の宣言はどうなっているでしょうか?

msg = '[${a}] <- イマココ' で宣言無しにいきなり変数定義してしまっています。熟練の方は一瞬でをいをいおと気づいていただけるでしょうが、例えば僕は数千行に渡るデータコンバート用の単発のバッチを涙ながらにjsでガリガリ書いているわけです。

DB A の テーブル B からデータを読み込み、 DB C の テーブル D のマスターを参照しつつデータを変換して DB E の テーブル F へデータを書き込んでいくわけです。いちいち変数宣言してられるかーーー!!

・・・とりあえず区切りのいいところで試しに動かして見るか、とテスト環境を対象に上記のバッチを流してみると・・・

$ npm start
[1] <- 終了!お疲れ様でした。
[2] <- 終了!お疲れ様でした。
[3] <- 終了!お疲れ様でした。
[4] <- 終了!お疲れ様でした。
[5] <- 終了!お疲れ様でした。
値: 1
処理中: [5] <- 終了!お疲れ様でした。
値: 2
処理中: [5] <- 終了!お疲れ様でした。
値: 3
処理中: [5] <- 終了!お疲れ様でした。
値: 4
処理中: [5] <- 終了!お疲れ様でした。
値: 5
処理中: [5] <- 終了!お疲れ様でした。

処理中: のメッセージで イマココ を確認したかったのに、終了 メッセージが表示されています。ウホー!
最初の5行の出力はちょっと目障りなので出力をやめてみましょう。

function getPromise(c) {
    return new Promise((accept)=>accept(c));
}

const array = [1,2,3,4,5];
for(a of array) {
    msg = `[${a}] <- イマココ`;
    getPromise(a).then(v => {
        console.log(`値: ${v}`);
        console.log(`処理中: ${msg}`);
    });
    msg = `[${a}] <- 終了!お疲れ様でした。`;
}

これを実行してみます。

$ npm start
> node index.js

値: 1
処理中: [5] <- 終了!お疲れ様でした。
値: 2
処理中: [5] <- 終了!お疲れ様でした。
値: 3
処理中: [5] <- 終了!お疲れ様でした。
値: 4
処理中: [5] <- 終了!お疲れ様でした。
値: 5
処理中: [5] <- 終了!お疲れ様でした。

終了メッセージに書き換わっている・・・。

いやいや、getPromise(a) の後で msg = `[${a}] <- 終了!お疲れ様でした。`; とmsgの代入処理をしているのでこの問題点はすぐに気づけるはずです。俺としたことが・・・ちょっと疲れたのかな。。。

それではこれではどうでしょう?

function getPromise(c) {
    return new Promise((accept)=>accept(c));
}

const array = [1,2,3,4,5];
for(a of array) {
    msg = `[${a}] <- イマココ`;
    getPromise(a).then(v => {
        console.log(`値: ${v}`);
        console.log(`処理中: ${msg}`);
    });
}

getPromise(a) の後で msg には何も入れていません。というか msg は宣言してからクロージャの中でしか使用していません。よし、万全。指差し確認、OK!

では、バッチを動かしてみましょう。

$ npm start
> node index.js

値: 1
処理中: [5] <- イマココ
値: 2
処理中: [5] <- イマココ
値: 3
処理中: [5] <- イマココ
値: 4
処理中: [5] <- イマココ
値: 5
処理中: [5] <- イマココ

イマココ メッセージが全部 5 になっているーーー!!!
これはメッセージに出力しているだけなので、msg が変わってしまっていても別にフーンで済むと思うかもしれません。
では、この msg 変数がDBの書き込み先だったり、APIのエンドポイントだったと想定したらどうでしょう?
すべてのデータが5番の接続先へ流れていきます。APIやRDBMSなら引数エラーや最悪でもスキーマの不一致などでフェイタルエラーで止まってくれるかもしれませんが、MongoDBやKVSなどのスキーマレスのNoSQLやただのフォルダ、アップロードするだけなどの場合は普通に処理が完了してしまうでしょう。
もう、白目、ですね・・・。想像しただけで胃が縮む思いです。

どうしてこうなったのか・・・? 理解するのには最初の冗長なメッセージを出していたバージョンのプログラムをもう一度、見るのがわかりやすいのです。

function getPromise(c) {
    return new Promise((accept)=>accept(c));
}

for(a of array) {
    msg = `[${a}] <- イマココ`;
    getPromise(a).then(v => {
        console.log(`値: ${v}`);
        console.log(`処理中: ${msg}`);
    });
    msg = `[${a}] <- 終了!お疲れ様でした。`;
    console.log(msg);
}

そして実行結果は、

$ npm start
[1] <- 終了!お疲れ様でした。
[2] <- 終了!お疲れ様でした。
[3] <- 終了!お疲れ様でした。
[4] <- 終了!お疲れ様でした。
[5] <- 終了!お疲れ様でした。
値: 1
処理中: [5] <- 終了!お疲れ様でした。
値: 2
処理中: [5] <- 終了!お疲れ様でした。
値: 3
処理中: [5] <- 終了!お疲れ様でした。
値: 4
処理中: [5] <- 終了!お疲れ様でした。
値: 5
処理中: [5] <- 終了!お疲れ様でした。

となります。

forループの最後で宣言した console.log(msg); の結果である、

[1] <- 終了!お疲れ様でした。
[2] <- 終了!お疲れ様でした。
[3] <- 終了!お疲れ様でした。
[4] <- 終了!お疲れ様でした。
[5] <- 終了!お疲れ様でした。

がすべて先に表示されています。

つまり、先にfor ループ が完了している ということです。 Promise で処理を後回しにしたので、すべての for ループが完了してから、Promise(accept)=>accept(c) が呼び出され、v => {...} が呼び出されます。

・・・for ループが完了しているのに、for ループ内で宣言した変数msg はどうなっているのか・・・?

これがクロージャがクロージャたる所以なのですが、v => {...} が宣言された時点でのすべての変数がレキシカル環境として記憶されています。要はどこかで変数 msg の存在を覚えててくれるわけですが、この変数が書き込み有りか無しかで持ち方が変わってしまう、というのがキモなワケです。

今回は手抜きをして変数宣言をすっ飛ばした結果、デフォルトの宣言である書き込み可能な var として定義されてしまいました。
よってクロージャの中では書き込み用の msg というみんなで読み書きできる1つの場所として変数がmsgが使用されています。その結果、forの最後のループで書き込まれた5がすべてのクロージャで参照された、ということになります。

これはクロージャの中から値を取り出したい場合のような明確な書き込み用途の変数の場合は当然の結果です。
というわけで、書き込み用途ではない場合には明確に読み取り専用変数であるconstの宣言がマストなのです。

ここではmsgは変わって欲しくない変数のため、以下のように const 宣言をちゃんとします。

function getPromise(c) {
    return new Promise((accept)=>accept(c));
}

const array = [1,2,3,4,5];
for(a of array) {
    const msg = `[${a}] <- イマココ`;
    getPromise(a).then(v => {
        console.log(`値: ${v}`);
        console.log(`処理中: ${msg}`);
    });
}

そして、実行してみると・・・

$ npm start

> node index.js

値: 1
処理中: [1] <- イマココ
値: 2
処理中: [2] <- イマココ
値: 3
処理中: [3] <- イマココ
値: 4
処理中: [4] <- イマココ
値: 5
処理中: [5] <- イマココ

const 宣言をして、宣言した時点の値をちゃんとキャプチャさせた結果、ちゃんと 1〜5 のイマココメッセージが表示されました。やれやれだぜ。。。

さらに今回は変数の宣言をせずにデフォルトの var が使われてしまいましたが、varは関数スコープでfunctionオブジェクトのプロパティのように振る舞うので、副作用が大きいです。

function majisuka() {
    var x = 'マジで?'
    let y = '嘘でしょ?'
    {
        var x = 'マジすか?'
        let y = '本当に?'
    }
    console.log(`x is [${x}]`);
    console.log(`y is [${y}]`);
}
majisuka();

こんな適当なプログラムでも、ちゃんと結果を予測できますか?結果は以下のようになります。

x is [マジすか?]
y is [嘘でしょ?]

varで宣言したxはブロック内で宣言したxで見事に上書きされています。マジすか?!
そんなわけでクロージャ内で書き込みたい変数にはちゃんと let 宣言をしましょう!!

ちなみに java では

String msg = "こんにちわ";   // final 宣言していない

new Thread(() -> {
    msg = "こんばんわ";     // -> これは"コンパイルエラー"
    System.out.println(msg);
});

などの、クロージャ外で宣言された変数へのアクセスはコンパイルエラーとなります。final宣言が Must で読み取り専用の変数しか渡せません。

例えば上記のように本当にマルチスレッドで動かすためのクロージャでは、クロージャの外部へ値を書き出すことはそのままスレッド間通信となるので、変数への書き込みのようなお手軽な読み書きじゃダメなのですね。
このように場合に応じてロックのストラテジを各自でちゃんと考えて用意してね、というわけなのです。

js はシングルスレッドで動くかなり特殊な言語で、クロージャが同時に実行されることはないので、一つの変数の読み書き、というレベルで済んでしまいます。お手軽なのはいいですが、副作用は大きいので気をつけてね!というお話でした〜。

参考ページ

クロージャ - JavaScript | MDN

var - JavaScript | MDN

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【2019年6月版】jsでクロージャに渡す変数はとりあえずconstしておけッ!!

結論

クロージャの中で使いたい変数の宣言に注意しよう。

  • 参照のみの変数は const 宣言マスト。でもあとは気にしなくてOK。
  • 書き込みしたい変数は let 宣言して腹をくくる
  • var使わない

時々、忘れてしまうのでメモです。

クロージャ内から使用される変数に刮目せよ

js ではクロージャをバンバン使いますが、クロージャ内で使用する変数には適切な宣言をしておかないとバグを埋め込む原因となります。
例えば以下のようなコードで、

// DB や APIなどから非同期で処理〜
function getPromise(c) {
    return new Promise((accept)=>accept(c));
}

// 何かのマスターを元にループで処理したい
const array = [1,2,3,4,5];
for(a of array) {
    msg = `[${a}] <- イマココ`;
    getPromise(a).then(v => {
        console.log(`値: ${v}`);
        console.log(`処理中: ${msg}`);
    })
     msg = `[${a}] <- 終了!お疲れ様でした。`
    console.log(msg);
}

getPromise(a).then(v => {...} で変数 a の値が変数 v に渡されてきますが、この v => {...} の中で参照されている変数 msg の宣言はどうなっているでしょうか?

msg = '[${a}] <- イマココ' で宣言無しにいきなり変数定義してしまっています。熟練の方は一瞬でをいをいおと気づいていただけるでしょうが、例えば僕は数千行に渡るデータコンバート用の単発のバッチを涙ながらにjsでガリガリ書いているわけです。

DB A の テーブル B からデータを読み込み、 DB C の テーブル D のマスターを参照しつつデータを変換して DB E の テーブル F へデータを書き込んでいくわけです。いちいち変数宣言してられるかーーー!!

・・・とりあえず区切りのいいところで試しに動かして見るか、とテスト環境を対象に上記のバッチを流してみると・・・

$ npm start
[1] <- 終了!お疲れ様でした。
[2] <- 終了!お疲れ様でした。
[3] <- 終了!お疲れ様でした。
[4] <- 終了!お疲れ様でした。
[5] <- 終了!お疲れ様でした。
値: 1
処理中: [5] <- 終了!お疲れ様でした。
値: 2
処理中: [5] <- 終了!お疲れ様でした。
値: 3
処理中: [5] <- 終了!お疲れ様でした。
値: 4
処理中: [5] <- 終了!お疲れ様でした。
値: 5
処理中: [5] <- 終了!お疲れ様でした。

処理中: のメッセージで イマココ を確認したかったのに、終了 メッセージが表示されています。ウホー!
最初の5行の出力はちょっと目障りなので出力をやめてみましょう。

function getPromise(c) {
    return new Promise((accept)=>accept(c));
}

const array = [1,2,3,4,5];
for(a of array) {
    msg = `[${a}] <- イマココ`;
    getPromise(a).then(v => {
        console.log(`値: ${v}`);
        console.log(`処理中: ${msg}`);
    });
    msg = `[${a}] <- 終了!お疲れ様でした。`;
}

これを実行してみます。

$ npm start
> node index.js

値: 1
処理中: [5] <- 終了!お疲れ様でした。
値: 2
処理中: [5] <- 終了!お疲れ様でした。
値: 3
処理中: [5] <- 終了!お疲れ様でした。
値: 4
処理中: [5] <- 終了!お疲れ様でした。
値: 5
処理中: [5] <- 終了!お疲れ様でした。

終了メッセージに書き換わっている・・・。

いやいや、getPromise(a) の後で msg = `[${a}] <- 終了!お疲れ様でした。`; とmsgの代入処理をしているのでこの問題点はすぐに気づけるはずです。俺としたことが・・・ちょっと疲れたのかな。。。

それではこれではどうでしょう?

function getPromise(c) {
    return new Promise((accept)=>accept(c));
}

const array = [1,2,3,4,5];
for(a of array) {
    msg = `[${a}] <- イマココ`;
    getPromise(a).then(v => {
        console.log(`値: ${v}`);
        console.log(`処理中: ${msg}`);
    });
}

getPromise(a) の後で msg には何も入れていません。というか msg は宣言してからクロージャの中でしか使用していません。よし、万全。指差し確認、OK!

では、バッチを動かしてみましょう。

$ npm start
> node index.js

値: 1
処理中: [5] <- イマココ
値: 2
処理中: [5] <- イマココ
値: 3
処理中: [5] <- イマココ
値: 4
処理中: [5] <- イマココ
値: 5
処理中: [5] <- イマココ

イマココ メッセージが全部 5 になっているーーー!!!
これはメッセージに出力しているだけなので、msg が変わってしまっていても別にフーンで済むと思うかもしれません。
では、この msg 変数がDBの書き込み先だったり、APIのエンドポイントだったと想定したらどうでしょう?
すべてのデータが5番の接続先へ流れていきます。APIやRDBMSなら引数エラーや最悪でもスキーマの不一致などでフェイタルエラーで止まってくれるかもしれませんが、MongoDBやKVSなどのスキーマレスのNoSQLやただのフォルダ、アップロードするだけなどの場合は普通に処理が完了してしまうでしょう。
もう、白目、ですね・・・。想像しただけで胃が縮む思いです。

どうしてこうなったのか・・・? 理解するのには最初の冗長なメッセージを出していたバージョンのプログラムをもう一度、見るのがわかりやすいのです。

function getPromise(c) {
    return new Promise((accept)=>accept(c));
}

for(a of array) {
    msg = `[${a}] <- イマココ`;
    getPromise(a).then(v => {
        console.log(`値: ${v}`);
        console.log(`処理中: ${msg}`);
    });
    msg = `[${a}] <- 終了!お疲れ様でした。`;
    console.log(msg);
}

そして実行結果は、

$ npm start
[1] <- 終了!お疲れ様でした。
[2] <- 終了!お疲れ様でした。
[3] <- 終了!お疲れ様でした。
[4] <- 終了!お疲れ様でした。
[5] <- 終了!お疲れ様でした。
値: 1
処理中: [5] <- 終了!お疲れ様でした。
値: 2
処理中: [5] <- 終了!お疲れ様でした。
値: 3
処理中: [5] <- 終了!お疲れ様でした。
値: 4
処理中: [5] <- 終了!お疲れ様でした。
値: 5
処理中: [5] <- 終了!お疲れ様でした。

となります。

forループの最後で宣言した console.log(msg); の結果である、

[1] <- 終了!お疲れ様でした。
[2] <- 終了!お疲れ様でした。
[3] <- 終了!お疲れ様でした。
[4] <- 終了!お疲れ様でした。
[5] <- 終了!お疲れ様でした。

がすべて先に表示されています。

つまり、先にfor ループ が完了している ということです。 Promise で処理を後回しにしたので、すべての for ループが完了してから、Promise(accept)=>accept(c) が呼び出され、v => {...} が呼び出されます。

・・・for ループが完了しているのに、for ループ内で宣言した変数msg はどうなっているのか・・・?

これがクロージャがクロージャたる所以なのですが、v => {...} が宣言された時点でのすべての変数がレキシカル環境として記憶されています。要はどこかで変数 msg の存在を覚えててくれるわけですが、この変数が書き込み有りか無しかで持ち方が変わってしまう、というのがキモなワケです。

今回は手抜きをして変数宣言をすっ飛ばした結果、デフォルトの宣言である書き込み可能な var として定義されてしまいました。
よってクロージャの中では書き込み用の msg というみんなで読み書きできる1つの場所として変数がmsgが使用されています。その結果、forの最後のループで書き込まれた5がすべてのクロージャで参照された、ということになります。

これはクロージャの中から値を取り出したい場合のような明確な書き込み用途の変数の場合は当然の結果です。
というわけで、書き込み用途ではない場合には明確に読み取り専用変数であるconstの宣言がマストなのです。

ここではmsgは変わって欲しくない変数のため、以下のように const 宣言をちゃんとします。

function getPromise(c) {
    return new Promise((accept)=>accept(c));
}

const array = [1,2,3,4,5];
for(a of array) {
    const msg = `[${a}] <- イマココ`;
    getPromise(a).then(v => {
        console.log(`値: ${v}`);
        console.log(`処理中: ${msg}`);
    });
}

そして、実行してみると・・・

$ npm start

> node index.js

値: 1
処理中: [1] <- イマココ
値: 2
処理中: [2] <- イマココ
値: 3
処理中: [3] <- イマココ
値: 4
処理中: [4] <- イマココ
値: 5
処理中: [5] <- イマココ

const 宣言をして、宣言した時点の値をちゃんとキャプチャさせた結果、ちゃんと 1〜5 のイマココメッセージが表示されました。やれやれだぜ。。。

var はやばい。

さらに今回は変数の宣言をせずにデフォルトの var が使われてしまいましたが、varは関数スコープでfunctionオブジェクトのプロパティのように振る舞うので、副作用が大きいです。

function majisuka() {
    var x = 'マジで?'
    let y = '嘘でしょ?'
    {
        var x = 'マジすか?'
        let y = '本当に?'
    }
    console.log(`x is [${x}]`);
    console.log(`y is [${y}]`);
}
majisuka();

こんな適当なプログラムでも、ちゃんと結果を予測できますか?結果は以下のようになります。

x is [マジすか?]
y is [嘘でしょ?]

varで宣言したxはブロック内で宣言したxで見事に上書きされています。マジすか?!
そんなわけでクロージャ内で書き込みたい変数にはちゃんと let 宣言をしましょう!!

ちなみに java では

String msg = "こんにちわ";   // final 宣言していない

new Thread(() -> {
    msg = "こんばんわ";     // -> これは"コンパイルエラー"
    System.out.println(msg);
});

などの、クロージャ外で宣言された変数へのアクセスはコンパイルエラーとなります。final宣言が Must で読み取り専用の変数しか渡せません。

例えば上記のように本当にマルチスレッドで動かすためのクロージャでは、クロージャの外部へ値を書き出すことはそのままスレッド間通信となるので、変数への書き込みのようなお手軽な読み書きじゃダメなのですね。
このように場合に応じてロックのストラテジを各自でちゃんと考えて用意してね、というわけなのです。

js はシングルスレッドで動くかなり特殊な言語で、クロージャが同時に実行されることはないので、一つの変数の読み書き、というレベルで済んでしまいます。お手軽なのはいいですが、副作用は大きいので気をつけてね!というお話でした〜。

参考ページ

クロージャ - JavaScript | MDN

var - JavaScript | MDN

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

CentOSへNode.jsをインストール

書いてあること

  • CentOSへのNode.jsのインストール手順

環境

  • CentOS Linux release 7.6.1810 (Core)
  • Node.js v10.16.0
  • Npm 6.9.0

リポジトリを作成

バージョン番号を確認

https://nodejs.org

リポジトリを作成

bash
$ curl -sL https://rpm.nodesource.com/setup_10.x | bash -

リポジトリが作成されたことを確認

bash
$ ls -l /etc/yum.repos.d

インストール

Node.jsをインストール

bash
$ yum install -y nodejs

バージョンを確認

bash
$ node --version
$ npm --version
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[LINE BOT]コスパの良いプロテインをおススメするLINE BOTを作りたかった

目次:

  • 初めに
  • 何でこんなモノ作ったの?
  • 「プロテインチョイサー」の仕様
  • 実際に作ってみた
    • 開発環境
    • 前準備
    • 実際のコード
    • QRコード
  • 感想
  • おまけ
    • 作り方
    • QRコード

初めに:

この記事はプロトタイピング専門スクール「ProtoOut Studio」の課題として作成しています。

最近、仕事がうまくいかない…
人間関係が不安…
現代社会を生きる上で、そういった悩みで煩悶することがままあるでしょう。どうすれば、それらの悩みを克服できるだろう…
それは、筋トレです。筋トレをすれば、あらゆる悩みは克服できます。
その筋トレをサポートするLINE BOTを作りました。
名前は「プロテインチョイサー」です。
なお、当記事は筋肉初心者向けです。安心して筋トレに活かしてください!

何でこんなモノ作ったの?:

私は半年前から筋トレに励んでいまして、目標は「体脂肪率15%を切る」ことです。
ご存じの通り、筋トレには良質なたんぱく質が不可欠です。
なので、プロテインを常飲することが筋トレ界では支配的です。
しかし、プロテインには複数の種類が存在し、値段、成分、量もバラバラで選びにくい。プロテインは種類ごとに飲み方があり、間違った飲み方をすると逆に太ってしまうなどの悪影響が出てしまいます。これらの要素が厳しい壁となり、立ちはだかっています。特に、値段は誰しもが気になって仕方がないポイントでしょう。
もちろん、筋肉があれば乗り越えられます。
しかし、筋肉初心者には、やはり厳しいと思います。
これでは、思い通りの筋トレができない!
そういう方のために、当BOTを作りました。

「プロテインチョイサー」の仕様:

・友達登録の際、あいさつとしてプロテイン解説サイトのURLを表示してきます。
筋肉初心者の方は、そこでプロテインについて勉強してください。

・次に、知りたいプロテインの種類の名前を送ります。
すると、おススメのプロテインを表示してくれます。
タップするとプロテイン販売サイト(大体AMAZONか楽天)に移動するので、そこでプロテインを購入し、筋トレに励んでください。

実際に作ってみた:

●開発環境
Node.js v10.16.0
npm v6.9.0
Windows10 pro

●前準備
@n0bisukeさんの記事を参考にしてプロットを作っておく。

●実際のコード 
protein_choice.js
当コードの内容としては、以下の通りです。
「はい」のメッセージを受け取るとプロテインの種類名を聞いてきます。
次に「ホエイプロテイン」などの名称を入力すると、Googleショッピングタブにスクレイピングしに行き、条件に合ったプロテインURLを5個表示します。
※なお、価格帯は2500-5000、レビュースコア降順をデフォルトの設定としています。

node.js
'use strict';

// 使用パッケージ群
const express = require('express');
const line = require('@line/bot-sdk');
const clientFetch = require('cheerio-httpcli');
// LINE用定数群
const PORT = process.env.PORT || 3000;
const config = {
    channelSecret: 'd309c32c2d348179e7e82506bf8272a2',
    channelAccessToken: 'KA5eK58WOGwt8RvsmHLHFkhStSRwy8JBtuE9w9FBZXYPbHJVviNTKYWekQOPsi9yqLO/vGHvC4HDqPxeDOg/zmVRrDrLeRyUYtaGa/W/RL4sxcrJ0eHCekRF9KcjLvvkhR6ZAi/Cwv8D+A8bRxsyogdB04t89/1O/w1cDnyilFU='
};
// スクレイピング用定数群 日本語だと文字コードの影響で文字化けするので、翻訳する
const wordWhei = 'Wheyprotein';
const wordCasein = 'Caseinprotein';
const wordSoy = 'Soyprotein';
const url = 'https://www.google.com/search?'
const googleUrl = 'https://www.google.com';
// マッスル用定数群
const proteinGoodbye = 'では、よい筋肉ライフを!';
const proteinRecommended = 'あなたにおススメのプロテインはコレだ!';
const proteinImportant = '言うまでもないが、最重要なのは規則正しい生活とバランスの良い食事です。';

const app = express();

app.post('/webhook', line.middleware(config), (req, res) => {
    console.log(req.body.events);
    Promise
        .all(req.body.events.map(handleEvent))
        .then((result) => res.json(result));
});

const client = new line.Client(config);

function handleEvent(event) { 
    if (event.type !== 'message' || event.message.type !== 'text') {
        return Promise.resolve(null);
    }
    let message = proteinRecommended;
    switch (event.message.text) {// メッセージの内容で処理分岐
        case 'ホエイプロテイン':
            getProtein(event.source.userId, wordWhei)
            return client.replyMessage(event.replyToken, {
                type: 'text',
                text: message
              });
        case 'カゼインプロテイン':
            getProtein(event.source.userId, wordCasein)
            return client.replyMessage(event.replyToken, {
                type: 'text',
                text: message
              });
        case 'ソイプロテイン':
            getProtein(event.source.userId, wordSoy)
            return client.replyMessage(event.replyToken, {
                type: 'text',
                text: message
              });
        default: // プロテイン以外のメッセージ対応
            return client.replyMessage(event.replyToken, {
                type: 'text',
                text: proteinGoodbye + proteinImportant
            });                
    }
}

const getProtein = async (userId, text) => { // スクレイピング処理
    let urlArray = [];
    let messageReply = '';
    clientFetch.fetch(url, { // 検索条件指定(googleショッピングタブで価格指定、レビュースコア降順で検索)
        tbm: 'shop',
        q: text,
        vw: 1,
        mr: 1,
        price: 1,
        ppr_min: '2500',
        ppr_max: '5000',
        p_ord: 'rv'
    },
    function (error, $, res) {
        $('a', '.eIuuYe').each(function (idx) { // プロテイン販売サイトのURLを5個まで取得
            for (let i = 0; i < 5; i++) {
                urlArray[i] = googleUrl + $(this).attr('href')
            }
            messageReply = urlArray
        });
    });
    await client.pushMessage(userId, {
        type: 'text',
        text: messageReply,
    });
}

(process.env.NOW_REGION) ? module.exports = app: app.listen(PORT);
console.log(`Server running at ${PORT}`);

●QRコード
未完成のBOTですが、『顰像』(しかみぞう)として公開します。
ProteinQRCode.png

感想:

今まで余りnode.jsに触ってこなかったため、LINE message APIの仕様が今一把握できず、大分苦戦しました。
結局、課題を提出できなかったので、まだまだ筋肉が足りないと痛感します。
Webhookに繋げられなかったり、スクレイピングがうまくいかなかったり、そもそもAPIかましてないじゃん…と紆余曲折を経ました。
それにしても、Proto out studioの皆さんは発想も技術も凄い…
同じ生徒としてそこはかとない敗北感を感じますが、その感情を糧にして筋トレに励みます!
今回はGoogleのショッピングタブを使いましたが、行く行くはプロテインの成分、量、袋のタイプ等の検索条件を増やし、その人に最適なプロテインを表示できるようにしたいです。
価格.comで出来るじゃんとか言うな
そして、オリジナリティ溢れるUI、UXが高いBOTないしサービスを作り、ローンチしたい。綺麗なコーディングもしたい。そのためには、日々勉強、筋トレあるのみ。
筋肉は一日にして為らず。

おまけ:

簡易版プロテインチョイサーなるモノも作ってみました。
これは、LINEオフィシャルアカウントマネージャーでリッチメニューを作成したものになります。
そして、私が作りたかったモノのプロトタイプです。

●作り方
LINEオフィシャルアカウントマネージャーに移動する。
LineLogo.png

あいさつタブ(緑色の箇所)をクリック。メッセージを書いた後、”変更を保存”をクリック
Hellow.png

リッチメニュータブをクリック。色々と編集。
Richmenu.png
完成!

LINEデベロッパーに移動し、QRコードをスキャンして友達になってみましょう。

●QRコード(簡易版プロテインチョイサー)
TemporaryQRCode.png

プログラミングしなくてもここまでのモノが出来るとは、いい時代になったものだ…

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Node.jsでAWSの請求金額をLINEで通知する

LINEDevelopersの設定をする

  • LINEDevelopersに登録とチャンネル(通知が送られてくるアカウント)を作成する。
    LINE Developers公式

LINE.png

チャンネルを作ると、上のような設定状況を見ることができる。名前は「AWS料金通知」とした。
ここでAPIを使うためのアクセストークンとメッセージを送信する先のユーザコードを確認しておく。

LINEAPIのプッシュメッセージでメッセージを送る

まず自分のLINEアカウントにメッセージを送信する。

const https = require('https');

const LINEMessage = (() => {
  let _postData = '';
  let _message = '';
  let _requestBody = new Object();

  const setMessage = (data) => {
    _message = String(`今月のAWS概算請求金額は「${data}$」です。`);
    _requestBody.to = process.env.LINE_USER_ID;
    _requestBody.messages = [{'type': 'text','text': _message}];
    _postData = JSON.stringify(_requestBody);
  }

  const pushMessage = () => {
    return new Promise(resolve => {
      //setMessageの使用を強制させる
      if(_postData === ''){
        console.log('setMessageの使用が必要です');
        return;
      }
      const headers= {
        'Content-Type': 'application/json; charset=UTF-8',
        'Authorization': `Bearer ${process.env.LINE_ACCESS_TOKEN}`,
        'Content-Length':Buffer.byteLength(_postData)
      };

      const _options = {
        hostname: 'api.line.me',
        port: 443,
        path: '/v2/bot/message/push',
        method: 'POST',
        headers: headers
      };

      const req = https.request(_options, (res) => {
        console.log('statusCode:', res.statusCode);
        res.on('end', () => {
          resolve(true);
        });
      });

      req.on('error', (e) => {
        console.error(e);
      });

      req.write(_postData);
      req.end();
    });
  }
  return {
    pushMessage: pushMessage,
    setMessage: setMessage
  }
})();

LINEから提供されているbot-sdkは使用せず、Node.js標準のhttpsモジュールを使用してエンドポイントにPOSTする方式でメッセージを送信する。外部ライブラリを使用した場合AWS Lambda側でパッケージを使用する用の設定が別途必要。

  • 私がハマった部分

メッセージボディが下記の場合は、400エラーを返す。

const requestBody = {
  'to':'XXXXXXXXXXX',
  'messages':{
    'type': 'text',
    'text': 'Hello, world'
  }
};

公式ドキュメントを見ながら、何が間違っているのかわからず2,3時間これに消費した。
メッセージボディが下記の場合は、正しい。(配列形式になっているか)

const requestBody = {
  'to':'XXXXXXXXXXX',
  'messages': [{
    'type': 'text',
    'text': 'Hello, world'
    }]
};

参考:
LINEAPIへPOSTしてプッシュメッセージを送信する
テキストメッセージのJSONオブジェクト

AWSのAPI権限周りの設定をする

  • IAMユーザのポリシーの設定でCloudWatchReadOnlyAccessを付与する。
  • 認証情報をローカルに保存する

参考:
共有認証情報ファイルから Node.js に認証情報をロードする
~/.aws/credentialsファイルでプロファイルを切り替える

AWSのCloudWatchから概算請求金額を取得する

CloudWatchのgetMetricStatisticsメソッドを使って請求金額を取得する。

const AWS = require('aws-sdk');

const FechAWSBilling = (() => {
  const cw = new AWS.CloudWatch({region: 'us-east-1'});
  const date = new Date
  const endTime = date.toISOString();
  date.setDate(date.getDate() -1);
  const startTime = date.toISOString();

  const params = {
    StartTime: startTime,
    EndTime: endTime,
    MetricName:'EstimatedCharges',
    Namespace:'AWS/Billing',
    Period:86400,
    Dimensions:[
      {
        Name:'Currency',
        Value:'USD'
      }
    ],
    Statistics:['Maximum']
  };

  const getMetricStatistics = async () => {
    return new Promise(resolve => {
      cw.getMetricStatistics(params, (err, data) => {
        if (err){
          console.log(err, err.stack);
        }
        resolve(data);
      });
    })
  }
  return{
    getMetricStatistics: getMetricStatistics
  }
})();

ハマりやすい部分はパラメータの部分だと思われる。
初見では各プロパティが何を意味しているのか全く分からなかった。(今もよく分かっていない)

const params = {
  StartTime:'2019-06-10T12:21:45.351Z',
  EndTime:'2019-06-12T12:21:45.351Z',
  MetricName:'EstimatedCharges',
  Namespace:'AWS/Billing',
  Period:86400,
  Dimensions:[
    {
      Name:'Currency',
      Value:'USD'
    }
  ],
  Statistics:['Maximum']
};

MetricName:アクセスするリソース名
CloudWatchメトリクス名

Namespace:アクセスするサービス名
CloudWatch名前空間

Period:86400:データの取得幅
1=1秒なので
60(秒) * 60(分) * 24(時間) = 86400(秒) = 1日

参考:
CloudWatchの概念
CloudWatchAPIドキュメント

Amazon Lambdaの登録を行う

毎日定時にCloudWatchのイベントを発生させ、それをトリガーにしてLambdaを起動させる。lambda.png

CloudWatchのルールの登録を行う
ru-ru.png

毎日18時にイベントを発火する。(単位はGMT,日本時間との時差は9時間)
しかしこの公式通りやってもうまく設定できなかった。なぜ?
Rate または Cron を使用したスケジュール式

  • Lambdaにソースコードを登録をする
const https = require('https');
const AWS = require('aws-sdk');

/** 
 * LINEでPushメッセージを送信する
*/
const LINEMessage = (() => {
  let _postData = '';
  let _message = '';
  let _requestBody = new Object();

  const setMessage = (data) => {
    _message = String(`今月のAWS概算請求金額は「${data}$」です。`);
    _requestBody.to = process.env.LINE_USER_ID;
    _requestBody.messages = [{'type': 'text','text': _message}];
    _postData = JSON.stringify(_requestBody);
  }

  const pushMessage = () => {
    return new Promise(resolve => {
      //setMessageの使用を強制させる
      if(_postData === ''){
        console.log('setMessageの使用が必要です');
        return;
      }
      const headers= {
        'Content-Type': 'application/json; charset=UTF-8',
        'Authorization': `Bearer ${process.env.LINE_ACCESS_TOKEN}`,
        'Content-Length':Buffer.byteLength(_postData)
      };

      const _options = {
        hostname: 'api.line.me',
        port: 443,
        path: '/v2/bot/message/push',
        method: 'POST',
        headers: headers
      };

      const req = https.request(_options, (res) => {
        console.log('statusCode:', res.statusCode);
        res.on('end', () => {
          resolve(true);
        });
      });

      req.on('error', (e) => {
        console.error(e);
      });

      req.write(_postData);
      req.end();
    });
  }
  return {
    pushMessage: pushMessage,
    setMessage: setMessage
  }
})();

/** 
 * AWSから請求金額を取得する
*/
const FechAWSBilling = (() => {
  const cw = new AWS.CloudWatch({region: 'us-east-1'});
  const date = new Date
  const endTime = date.toISOString();
  date.setDate(date.getDate() -1);
  const startTime = date.toISOString();

  const params = {
    StartTime: startTime,
    EndTime: endTime,
    MetricName:'EstimatedCharges',
    Namespace:'AWS/Billing',
    Period:86400,
    Dimensions:[
      {
        Name:'Currency',
        Value:'USD'
      }
    ],
    Statistics:['Maximum']
  };

  const getMetricStatistics = async () => {
    return new Promise(resolve => {
      cw.getMetricStatistics(params, (err, data) => {
        if (err){
          console.log(err, err.stack);
        }
        resolve(data);
      });
    })
  }
  return{
    getMetricStatistics: getMetricStatistics
  }
})();


exports.handler = async () => {
  const bill = await FechAWSBilling.getMetricStatistics();
  LINEMessage.setMessage(bill.Datapoints[0].Maximum);
  const pushComplete = await LINEMessage.pushMessage();
  if(pushComplete){
    console.log('関数は正常に実行されました。');
    return;
  }
};
  • 私がハマった部分
    Nodeの非同期処理に注意する。 下記のコードは、Pushメッセージ送信が完了する前にLambda側での処理が終了してECONNRESETエラーが発生する場合がある。Lambda外でのデバック時はエラーが発生しない分、原因調査に数時間消費した。lambda側でのテストでメッセージの受信ができること確認する。
  const bill = await FechAWSBilling.getMetricStatistics();
  LINEMessage.setMessage(bill.Datapoints[0].Maximum);
  LINEMessage.pushMessage();
}

下記のコードのようにPushメッセージが完了するようにする。

  const bill = await FechAWSBilling.getMetricStatistics();
  LINEMessage.setMessage(bill.Datapoints[0].Maximum);
  const pushComplete = await LINEMessage.pushMessage();
  if(pushComplete){
    console.log('関数は正常に実行されました。');
    return;
  }
}

ソースコードの登録が完了したら、LINEで請求金額が通知されることを確認する。

LINE通知.png

以上です、ありがとうございました。

※AWSの公式ドキュメントの膨大さに気絶しそうになる。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む