20200416のNode.jsに関する記事は10件です。

AWS lambda(Node.js)でオレオレ証明書(self-signed)を一時的に信頼してSSL通信を行う方法

はじめに

AWS lambdaのNode.js(https標準モジュール)で実装した、WEBサイトへhttpsのリクエストを投げる処理で、以下の2つのエラーが発生した際の対応についての記事です。
※急いでいる方、ソースコードだけ見たい方はここから見ればOKです

スクリーンショット 2020-04-16 22.24.09.png
スクリーンショット 2020-04-16 18.31.17.png

これ何

調べたところ、リクエスト先のWEBサーバから送出されているサーバ証明書に対してのnodejs内部での検証失敗のため生じたエラーでした。
2つのエラーの違いは、
* 自分自身が署名(オレオレ証明書(self-signed))
* サーバ証明書の発行元が信頼されていないか
?2つのエラーは共に、Node.jsとして信頼1していないCA証明書にチェーンしていたため生じていました。

環境

  • AWS lambda
  • Node.js ランタイム12.x
  • 接続イメージ [AWS lambda]-->(WAN HTTP over SSL)-->[WEBサーバ]

対処案

3つ考えましたが、妥協点で私の環境では3つ目で実装しましたので以下にまとめます。

1.WEBサーバ側にちゃんとしたサーバ証明書2に変更してもらう

この対応ができたらこんな記事はいらない気もする。
自局署名の問題については、ネット上でも従前から議論されおり3、本記事をご覧になっている方でも既知のことと思います。WEBサーバ管理者に対しては、セキュリティの観点で懸念点を伝えてさしあげる程度にしました。

2.TLSハンドシェイクエラーを無視・無効にする

NODE_TLS_REJECT_UNAUTHORIZEDを環境変数に定義し、値を0とすれば検証自体がdisableになる模様。これは、curlでいうところの--insecureオプションと類似していますが、検証を全て無視するため有効期限やトラストアンカーとのチェーン等々を丸っとすっ飛ばす模様。(詳細は未検証のため割愛)
https://nodejs.org/api/cli.html#cli_node_tls_reject_unauthorized_value

3.オレオレ証明書(self-signed)を一時的に信頼する

公式ドキュメントに書いてありました。
https://nodejs.org/api/tls.html#tls_tls_connect_options_callback
具体的には、通信先のWEBサーバのオレオレ証明書(サーバ証明書自体)ないしは、サーバ証明書のissuerとなっている現状信頼されていない自局CA証明書(pem形式)を取得し、https.requestのoptionにcaを追加するというものです。以下にサンプルを用いて実装例を示しています。

※今回の実装ではpemファイルの読み込みを、諸般の事情によりlambdaのみで完結したかったため、環境変数に事前に設定して、そこから読み込ませるという方法を使っています。(S3に入れて読み込むという方法もありかと思います。)

3-1.pemファイルを1行化する

環境変数にpemを入れるため、改行を¥nに置換します。

[work@localhost]$ awk 'NF {sub(/\r/, ""); printf "%s\\n",$0;}' cacert.pem
-----BEGIN CERTIFICATE-----\nAIIDQjCCAiHCEQCQzvg6BX4eF(中略)v2wk3xtME7i8Jb2aUQH9vFVYuXUN2\nUO+j8l1OA4p1ew0kCsit2HOn\n-----END CERTIFICATE-----\n [work@localhost]$

余談ですが、当初は、pem形式って改行まで含んで形式であるということを理解しておらず、改行を単純に削除して環境変数に入れていたためエラーが発生して、ここで地味に悩みました。ちなみに、pemの改行は、Windows形式(CR+LF)、Unix形式(LF)どちらで良い模様なのでCRを削除後に置換している。4

3-2.AWS lamda環境変数の設定

上コマンドで表示された内容を環境変数にCA_PEMとして保存します。
スクリーンショット 2020-04-16 14.43.23.png

3-3. 実際のスクリプト(sample)

const https = require('https');

//ここで環境変数からcacertにPEMを読み込んでいる
var cacert = process.env['CA_PEM'].replace(/\\n/g, '\n');

exports.handler = (event, context) => {
    //caを追加してcacertを設定
    const options = {
      protocol: 'https:',
      host: 'example.com',
      path: '/index.html',
      port: 443,
      method: 'GET',
      timeout: 8000,
      ca: cacert,
      headers: {
        'User-Agent': 'AWS-lambda',
      }
    };

    let req =  https.request(options, (res) => {
      res.setEncoding('utf8');
      let body = '';
      res.on('data', (chunk) => {
        body += chunk;
      });
      res.on('end', () => {
        console.log(body);
      });
    });
    req.end();
}

3-4. エラー解消。取れました

Response:
null

Request ID:
"XXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"

Function Logs:
START RequestId: XXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX Version: $LATEST
2020-04-16T14:26:29.930Z    XXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX INFO
<!DOCTYPE html>
<html lang="jp">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Hello</title>
</head>
<body>
    This is test page ;>
</body>
</html>

END RequestId: XXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
REPORT RequestId: XXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
Duration: 210.24 ms Billed Duration: 300 ms Memory Size: 128 MB Max Memory Used: 68 MB  

  1. Node.jsのSSL通信時のトラストアンカーってなんだろ?と思い調べた。https://github.com/nodejs/node/blob/bf7409e9740ce602b09e088aac70b7c817f5d27c/doc/guides/maintaining-root-certs.md 読んでみると、このソースに書いてあるよとのこと。Mozilla NSSのトラストアンカーと合わせているみたいですね、定期的にメンテはされている模様。ここに自己署名をここに入れるのは影響が大きそうです。 

  2. Node.jsのトラストアンカー1に含まれるCAに直接または間接的に署名されているサーバ証明書のこと。 

  3. Qiitaですと、こちらの記事がとても参考になりました。オレオレ証明書を使いたがる人を例を用いて説得する 

  4. 出典:https://stackoverflow.com/questions/57870914/how-to-create-a-single-line-x509-certificate-that-can-be-parsed-by-openssl-comma 

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

Dockerコマンド ~学習メモ書き

前提

・導入メモになります。
・いろいろ試してみたが、やっぱりHyperVがいい

準備するもの

アプリケーション側

アプリケーション側はnodejs を使用します。
今回はnode の公式リポジトリを使用するための、Dockerfile

app-server/Dockerfile
FROM node:5
RUN npm -g install redis
ENV NODE_PATH C:\nodejs\node_modules

ENTRYPOINT ["node", "app.js"]
app-server/src/app.js
var redis = require('redis');
var redis_client = redis.createClient(6379, "noderedis");
var listen_port = 10080;

require('http').createServer(function (request, response) {
    redis_client.incr('counter', function(error, reply) {
        response.writeHead(200, {'Content-Type': 'text/plain'});
        response.end("You accessed here " + reply + " times.\n");
    });
}).listen(listen_port, '0.0.0.0');
console.log("Server is running on port " + listen_port + ".");

Docker compose ファイルの作成

docker-compose.yml
nodeapp:
  build: "./app-server"
  container_name: "nodeapp"
  working_dir: "/usr/src/app"
  ports:
   - "10080:10080"
  volumes:
   - "$PWD/app-server/src:/usr/src/app"
  links:
   - "noderedis"
noderedis:
  image: "redis:3"
  container_name: "noderedis"

コンテナ一覧を確認

$ docker ps

実行中のコンテナ一覧

$ docker ps -a

コンテナ起動

$ docker-compose up 

コンテナ停止

$ docker stop *contenaID*

おまけ

コンテナ削除

$ docker rm *contenaID*
$ docker-compose kill

マウントできない時に疑うポイント

参考になったURL

https://qiita.com/jusotech10/items/cb8077efb9b7a74dfdcc

まとめ

もともとインストールしてるoracleの仮想マシーンとDocker toolbox の相性が合わない⇒ 一旦両方ともアンインストールして、入れ直すときに、仮想マシーンにチェックして入れ直すとうまくいく

Windowsのドッカーのツールボックスのマウントは、
C:¥Users直下とマウントされるみたいです。

お金に余裕あるなら、Windows10proを最初に買ってHyperVの仮想環境を使うのが手っ取り早いし起動も早い。。仕事上はproなので環境周りが楽でした。ダッシュボードでコンテナ管理が使えるので便利かと思います。

前に作ったVagrantfile環境の整理したら、オラクルもドッカーもアンインストールしてモジュール置くためのディレクトリのマウントに再挑戦してみようと思います。。

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

GASの処理速度が遅いし制約が厳しいのでNode.jsに移行した(Webアプリ)

はじめに

弓道で用いるwebアプリを開発しているんですが、データの読み書きがクソ遅いので、
GoogleAppsScript+スプレッドシートからNode.js+Firebaseに移行しました。
(GoogleAppsScript Web APIからCloud Functions APIに変えたいうこと)

Google Apps ScriptもNode.jsも基本は一緒ですが...

これでかなりwebアプリの待機時間が短くなったので、記事にしようと思います。

私の使用用途は、スプレッドシートにユーザーやデータを保管し、それを読み書きする、つまりwebアプリのバックエンドとしての利用でGASの処理速度が遅いと感じたものであるので読む際は注意してください。

結果から

どれだけ早くなったか先に紹介しておきます。

的中を記録し、その月のデータをとってくる処理を例に紹介します。記録するデータは日付、中たった数、引いた本数、的中率の四つのデータです。月のデータは付けた日の月のデータ全てとってきます。

↓Google Apps Script + スプレッドシート
gas.gif
ちなみにタイムは2.5秒ほど
スクリーンショット 2020-04-16 18.25.04.png

↓Node.js + Firebase(Cloud Functions API使用)

ezgif.com-video-to-gif (1).gif

スクリーンショット 2020-04-16 18.36.14.png

タイムは驚異の0.4秒!!!!!!!!!!!

約2秒ほど早くなりました。ユーザビリティも右肩上がりでとどまることを知らないでしょう。

*上のフェッチしてきたgifの後半に表示されるデータを見ればわかりますが、ばらつきがある状態で行ったので正確ではないですがほぼ同じデータ量です。
*cloud Funcions APIを使用した方は、ある程度時間を置いてAPIアクセスした際には4秒くらいかかっていました。これは、一定時間おくとcloud Funcionsが寝ちゃうからです。詳細は別記事にしようかと思います。

GoogleAppsScript + スプレッドシートでの読み書きが遅いのはなぜか?

Webアプリのバックエンドとして使っていて、GoogleAppsScript+スプレッドシート(データベース)の読み書きが遅いのはなぜだろう。

1、APIの呼び出し回数が多い

Spreadsheetサービスでは、以下のようなメソッドなどを使ってスプレッドシートにアクセスをするたびにAPIが呼び出される。

・getActiveSpreadsheet()
・getSheetByName()
・getDataRange()
・getLastRow()
・getRange()
・getValue()
・setValue()

二重のfor文の中で
sheet.getRange(i,j).getValue();
なんかした日には終わりで、行数×列数分APIアクセス実行されるので、かなり時間がかかる。

なので、下のようにできるだけ呼び出すAPIを減らすために全てのセルのデータを配列として一回のアクセスでとるよう設計している。
sheet.getDataRange().getValues();

私のwebアプリで、部員全員のデータをとってくる処理があるんですが、この処理に二回のAPIの呼び出し✖️部員数30だったので計60回ほどAPIアクセスをしていました。60回とはいえ、体感3秒ほど待たされたのでユーザビリティがすこぶる悪いかったです。

2、読み書きするデータ量が多いと一回のAPIリクエストが遅い
一回のスプレッドシートの読み書き自体がおそらく長い。*ここで長いと言うのはNode.js+Firebaseと比較して

読み書きするデータ量にもよるだろうけど、パスワード、Eメール、その他ユーザーを管理するデータをスプレッドシートにデータベースとして管理するのには少し難があった。それもユーザー数が多いとなおさら。

膨大なユーザーを管理したりせず、個人で使うデータ量なら全然問題ない。

GoogleAppsScript(WebAPI)の制約

次に各制約について紹介する。
GoogleAppsScript(WebAPI)の制約は、

https://www.bugbugnow.net/2018/12/GoogelAppsScript-restriction.html

これをみればわかるが一日のURLフェッチ(APIアクセス)が20,000/日、さらには無料のGoogleアカウントでは、スクリプトの実行時間が6分と地味に少ない。

いや用途によっては十分なんだろうが、私の使用目的では、一日10回URLフェッチを行うとして、ユーザー数2000を超えると上限に達してしまう。もう少し大規模なSPAにしようと思ったら物足りない。

有料プランもあるがそちらも上限が決まっているので、ユーザーが増えて上限がきたら嫌なのでやはりwebアプリのバックエンドとして使うのは厳しいのかもしれない。そもそもそんな使い方する人はほとんどいないだろうが、個人開発をしている人なら同じような境遇にあった人もいそう。

Cloud Functions APIの制約

それに比べてCloud Functions APIの制約というと

https://firebase.google.com/docs/firestore/quotas?hl=ja

呼び出し
関数の呼び出し料金は定額制です。HTTP リクエストから呼び出される関数(HTTP 関数)、バックグラウンド関数、call API から行われる呼び出しなど、呼び出し元によって料金が変わることはありません。

月間呼び出し回数 料金(100 万単位)
最初の 200 万回 無料
200 万回を超えた分 $0.40

米ドル以外の通貨でお支払いの場合は、Cloud Platform SKU に記載されている該当通貨の料金が適用されます。
呼び出し料金は 1 回あたり $0.0000004 の単価制で、関数の結果や実行時間に関係なく請求されます。ただし、毎月最初の 200 万回までは無料です。

だそう。ほー。無料枠がかなり多いw
一ユーザーが一日に10回APIアクセスするとして

10✖️30 = 300

毎月最初の 200 万回まで無料なので2000,000÷300 = 6,666人まで無料枠でいけるぞ!

一日10回APIアクセスを行うとして、ユーザー数2,000を超えると上限に達してしまうGASのAPIに大してCloud Functions APIは 6,666人までいける
断然、Cloud Functions APIの方がいい。さらに超えた分に対しては超えた分の請求なので都合がいい。上限を超えた際には一回のAPIアクセスをするのに約0.0004円ほどだし格安すぎる。

ユーザーが6万6666人にて毎月2,000万回APIアクセスするとしたら約8,000円ほど。
ガソリン代くらいでしょう。車持ってないけど。

gas+スプレッドシート からNode.js + Firebaseに移行

以上よりGoogle Apps Script + スプレッドシート ⇨ Node.js + Firebase に移行しました。

gasのコード全部で500行くらいだったのでNode.jsに書き換えるのも一週間くらいで終わりました。

↓冒頭で見せたアプリ内で使われる的中率を書き込みに行き、月のデータをフェッチする処理
gasのコードとNode.jsのコードの比較(一部)を紹介します。

gas
//的中率書き込み
function submitFetchData(e){
  var sheetID =e.parameter["id"];
  var sheetName =e.parameter["name"];
  var date = e.parameter.p1;
  var hitArrow = e.parameter.p2;
  var allArrow = e.parameter.p3;
  var hitRate = hitArrow / allArrow * 100;
  var year =e.parameter["year"];
  var month = e.parameter["month"];
  //JSONオブジェクト格納用の入れ物
  var rowData = {}; 

  //書込先スプレッドシートのIDを入力
  var sheet = SpreadsheetApp.openById(sheetID).getSheetByName(sheetName);
  var arrayData = [[ date, hitArrow, allArrow, hitRate]];

  var rows = arrayData.length;
  var cols = arrayData[0].length;

  sheet.insertRows(2,1);

  //シートに配列を書き込み
  sheet.getRange(2,1,1,cols).setValues(arrayData);

  var thisDate = year + month;
  var sheet = SpreadsheetApp.openById(sheetID).getSheetByName(sheetName); 

  var data = sheet.getDataRange().getValues();
  var array = data.map(function(value,index){
    return (value.reduce(function(r,c,i){
      var result = r;
      switch(i){
        case 0: result.month = c;break;
      }
      return result;
    },{}))
  });
  array.shift();

  var date = array.map(function(value,index){
    var thisDate = {};
    var year,month;
    year = value.month.split('/');
    thisDate.year = year[0];
    month = year[1].split('/');
    thisDate.month = month[0];
    return thisDate;
  })
  var dateArray = date.map(function(value, index){
    return value.year + value.month;
  });

   var firstIndex = dateArray.indexOf(thisDate);
   var lastIndex = dateArray.lastIndexOf(thisDate);

  var newData = sheet.getRange(Number(firstIndex+2),1, Number(lastIndex+2) - Number(firstIndex+2) +1, data[0].length).getValues();  
  //オブジェクトに変換
  var newArray = newData.map(function(value,index){
    return (value.reduce(function(r,c,i){
      var result = r;
      switch(i){
        case 0: result.date = c;break;
        case 1: result.hit_arrow = c;break;
        case 2: result.all_arrow = c;break;
        case 3: result.hit_rate = c;break;
      }
      return result;
    },{}))
  }); 
   var output = ContentService.createTextOutput(JSON.stringify(newArray, null, 2));
   output.setMimeType(ContentService.MimeType.TEXT); 
   return output;
}

Node.js
//的中率書き込み&月のデータフェッチ
app.get('/hitWrite', async (req, res) => {
    console.log('-----hitWrite-----')
    const rec_name = req.query["rec_name"];
    const id = req.query["id"];
    const team = req.query["team"];
    const hit_arrow = req.query["hit_arrow"];
    const all_arrow = req.query["all_arrow"];
    const hit_rate = hit_arrow / all_arrow * 100;
    const date = req.query["date"];
    const year = req.query["year"];
    const month = req.query["month"];
    const arrayData = {
        all_arrow: Number(all_arrow),
        date,
        hit_arrow: Number(hit_arrow),
        hit_rate: Number(hit_rate),
        id: Math.random().toString(32).substring(2)//ランダムID生成
    };
    let records;
    await db.collection("college").doc(team).collection(id).doc("user_data").update({
        [rec_name]: admin.firestore.FieldValue.arrayUnion(arrayData)
    })
        .then(() => {
            return (
                db.collection("college").doc(team).collection(id).doc("user_data").get()
                    .then((snapshot) => {
                        return (
                            records = snapshot.data()[[rec_name]].filter((value) => {
                                if (value.date.split('/')[0] === year && value.date.split('/')[1] === month) {
                                    return value;
                                }
                            }),
                            res.json(JSON.stringify(records))
                        )
                    })
                    .catch((err) => {
                        console.log(err)
                        res.end();
                    })
            )
        })
        .catch((err) => {
            console.log(err)
            res.end();
        })

})

かなりすっきりしました。

読みやすい、書きやすい、共同開発しやすくなりましたね。

おわりに

アプリの待機時間が短くなったので、Google Apps Script + スプレッドシート から Node.js + Firebase に移行してかなりよかったです。

ただ、金額的にも月にできるAPIアクセス数にも厳しさがあったGoogle Apps Scriptですが、スプレッドシートに書き込めれるので、部内でシートの共有なんかして直接記録を見たりできるのでこっちも便利だったりします。

ただ、ユーザー数が増えると厳しいのでやっぱり捨てざるをおえないってことでおさらばしないとけません。

このwebアプリのユーザー数は自分の大学のユーザーのみの30人なので、当分気にすることはないのですがね。ふふ

最後まで見ていただき、ありがとうございました。

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

GASの処理速度が遅いし制約が厳しいのでNode.jsに移行したら高速に

はじめに

弓道で用いるwebアプリを開発しているんですが、データの読み書きがクソ遅いので、
GoogleAppsScript+スプレッドシートからNode.js+Firebaseに移行しました。
(GoogleAppsScript Web APIからCloud Functions APIに変えたいうこと)

Google Apps ScriptもNode.jsも基本は一緒ですが...

これでかなりwebアプリの待機時間が短くなったので、記事にしようと思います。

私の使用用途は、スプレッドシートにユーザーやデータを保管し、それを読み書きする、つまりwebアプリのバックエンドとしての利用でGASの処理速度が遅いと感じたものであるので読む際は注意してください。

結果から

どれだけ早くなったか先に紹介しておきます。

的中を記録し、その月のデータをとってくる処理を例に紹介します。記録するデータは日付、中たった数、引いた本数、的中率の四つのデータです。月のデータは付けた日の月のデータ全てとってきます。

↓Google Apps Script + スプレッドシート
gas.gif
ちなみにタイムは2.5秒ほど
スクリーンショット 2020-04-16 18.25.04.png

↓Node.js + Firebase(Cloud Functions API使用)

ezgif.com-video-to-gif (1).gif

スクリーンショット 2020-04-16 18.36.14.png

タイムは驚異の0.4秒!!!!!!!!!!!

約2秒ほど早くなりました。ユーザビリティも右肩上がりでとどまることを知らないでしょう。

*上のフェッチしてきたgifの後半に表示されるデータを見ればわかりますが、ばらつきがある状態で行ったので正確ではないですがほぼ同じデータ量です。
*cloud Funcions APIを使用した方は、ある程度時間を置いてAPIアクセスした際には4秒くらいかかっていました。これは、一定時間おくとcloud Funcionsが寝ちゃうからです。詳細は別記事にしようかと思います。

GoogleAppsScript + スプレッドシートでの読み書きが遅いのはなぜか?

Webアプリのバックエンドとして使っていて、GoogleAppsScript+スプレッドシート(データベース)の読み書きが遅いのはなぜだろう。

1、APIの呼び出し回数が多い

Spreadsheetサービスでは、以下のようなメソッドなどを使ってスプレッドシートにアクセスをするたびにAPIが呼び出される。

・getActiveSpreadsheet()
・getSheetByName()
・getDataRange()
・getLastRow()
・getRange()
・getValue()
・setValue()

二重のfor文の中で
sheet.getRange(i,j).getValue();
なんかした日には終わりで、行数×列数分APIアクセス実行されるので、かなり時間がかかる。

なので、下のようにできるだけ呼び出すAPIを減らすために全てのセルのデータを配列として一回のアクセスでとるよう設計している。
sheet.getDataRange().getValues();

私のwebアプリで、部員全員のデータをとってくる処理があるんですが、この処理に二回のAPIの呼び出し✖️部員数30だったので計60回ほどAPIアクセスをしていました。60回とはいえ、体感3秒ほど待たされたのでユーザビリティがすこぶる悪いかったです。

2、読み書きするデータ量が多いと一回のAPIリクエストが遅い
一回のスプレッドシートの読み書き自体がおそらく長い。*ここで長いと言うのはNode.js+Firebaseと比較して

読み書きするデータ量にもよるだろうけど、パスワード、Eメール、その他ユーザーを管理するデータをスプレッドシートにデータベースとして管理するのには少し難があった。それもユーザー数が多いとなおさら。

膨大なユーザーを管理したりせず、個人で使うデータ量なら全然問題ない。

GoogleAppsScript(WebAPI)の制約

次に各制約について紹介する。
GoogleAppsScript(WebAPI)の制約は、

https://www.bugbugnow.net/2018/12/GoogelAppsScript-restriction.html

これをみればわかるが一日のURLフェッチ(APIアクセス)が20,000/日、さらには無料のGoogleアカウントでは、スクリプトの実行時間が6分と地味に少ない。

いや用途によっては十分なんだろうが、私の使用目的では、一日10回URLフェッチを行うとして、ユーザー数2000を超えると上限に達してしまう。もう少し大規模なSPAにしようと思ったら物足りない。

有料プランもあるがそちらも上限が決まっているので、ユーザーが増えて上限がきたら嫌なのでやはりwebアプリのバックエンドとして使うのは厳しいのかもしれない。そもそもそんな使い方する人はほとんどいないだろうが、個人開発をしている人なら同じような境遇にあった人もいそう。

Cloud Functions APIの制約

それに比べてCloud Functions APIの制約というと

https://cloud.google.com/functions/pricing?hl=ja

呼び出し
関数の呼び出し料金は定額制です。HTTP リクエストから呼び出される関数(HTTP 関数)、バックグラウンド関数、call API から行われる呼び出しなど、呼び出し元によって料金が変わることはありません。

月間呼び出し回数 料金(100 万単位)
最初の 200 万回 無料
200 万回を超えた分 $0.40

米ドル以外の通貨でお支払いの場合は、Cloud Platform SKU に記載されている該当通貨の料金が適用されます。
呼び出し料金は 1 回あたり $0.0000004 の単価制で、関数の結果や実行時間に関係なく請求されます。ただし、毎月最初の 200 万回までは無料です。

だそう。ほー。無料枠がかなり多いw
一ユーザーが一日に10回APIアクセスするとして

10✖️30 = 300

毎月最初の 200 万回まで無料なので2000,000÷300 = 6,666人まで無料枠でいけるぞ!

一日10回APIアクセスを行うとして、ユーザー数2,000を超えると上限に達してしまうGASのAPIに大してCloud Functions APIは 6,666人までいける
断然、Cloud Functions APIの方がいい。さらに超えた分に対しては超えた分の請求なので都合がいい。上限を超えた際には一回のAPIアクセスをするのに約0.0004円ほどだし格安すぎる。

ユーザーが6万6666人にて毎月2,000万回APIアクセスするとしたら約8,000円ほど。
ガソリン代くらいでしょう。車持ってないけど。

gas+スプレッドシート からNode.js + Firebaseに移行

以上よりGoogle Apps Script + スプレッドシート ⇨ Node.js + Firebase に移行しました。

gasのコード全部で500行くらいだったのでNode.jsに書き換えるのも一週間くらいで終わりました。

↓冒頭で見せたアプリ内で使われる的中率を書き込みに行き、月のデータをフェッチする処理
gasのコードとNode.jsのコードの比較(一部)を紹介します。

gas
//的中率書き込み
function submitFetchData(e){
  var sheetID =e.parameter["id"];
  var sheetName =e.parameter["name"];
  var date = e.parameter.p1;
  var hitArrow = e.parameter.p2;
  var allArrow = e.parameter.p3;
  var hitRate = hitArrow / allArrow * 100;
  var year =e.parameter["year"];
  var month = e.parameter["month"];
  //JSONオブジェクト格納用の入れ物
  var rowData = {}; 

  //書込先スプレッドシートのIDを入力
  var sheet = SpreadsheetApp.openById(sheetID).getSheetByName(sheetName);
  var arrayData = [[ date, hitArrow, allArrow, hitRate]];

  var rows = arrayData.length;
  var cols = arrayData[0].length;

  sheet.insertRows(2,1);

  //シートに配列を書き込み
  sheet.getRange(2,1,1,cols).setValues(arrayData);

  var thisDate = year + month;
  var sheet = SpreadsheetApp.openById(sheetID).getSheetByName(sheetName); 

  var data = sheet.getDataRange().getValues();
  var array = data.map(function(value,index){
    return (value.reduce(function(r,c,i){
      var result = r;
      switch(i){
        case 0: result.month = c;break;
      }
      return result;
    },{}))
  });
  array.shift();

  var date = array.map(function(value,index){
    var thisDate = {};
    var year,month;
    year = value.month.split('/');
    thisDate.year = year[0];
    month = year[1].split('/');
    thisDate.month = month[0];
    return thisDate;
  })
  var dateArray = date.map(function(value, index){
    return value.year + value.month;
  });

   var firstIndex = dateArray.indexOf(thisDate);
   var lastIndex = dateArray.lastIndexOf(thisDate);

  var newData = sheet.getRange(Number(firstIndex+2),1, Number(lastIndex+2) - Number(firstIndex+2) +1, data[0].length).getValues();  
  //オブジェクトに変換
  var newArray = newData.map(function(value,index){
    return (value.reduce(function(r,c,i){
      var result = r;
      switch(i){
        case 0: result.date = c;break;
        case 1: result.hit_arrow = c;break;
        case 2: result.all_arrow = c;break;
        case 3: result.hit_rate = c;break;
      }
      return result;
    },{}))
  }); 
   var output = ContentService.createTextOutput(JSON.stringify(newArray, null, 2));
   output.setMimeType(ContentService.MimeType.TEXT); 
   return output;
}

Node.js
//的中率書き込み&月のデータフェッチ
app.get('/hitWrite', async (req, res) => {
    console.log('-----hitWrite-----')
    const rec_name = req.query["rec_name"];
    const id = req.query["id"];
    const team = req.query["team"];
    const hit_arrow = req.query["hit_arrow"];
    const all_arrow = req.query["all_arrow"];
    const hit_rate = hit_arrow / all_arrow * 100;
    const date = req.query["date"];
    const year = req.query["year"];
    const month = req.query["month"];
    const arrayData = {
        all_arrow: Number(all_arrow),
        date,
        hit_arrow: Number(hit_arrow),
        hit_rate: Number(hit_rate),
        id: Math.random().toString(32).substring(2)//ランダムID生成
    };
    let records;
    await db.collection("college").doc(team).collection(id).doc("user_data").update({
        [rec_name]: admin.firestore.FieldValue.arrayUnion(arrayData)
    })
        .then(() => {
            return (
                db.collection("college").doc(team).collection(id).doc("user_data").get()
                    .then((snapshot) => {
                        return (
                            records = snapshot.data()[[rec_name]].filter((value) => {
                                if (value.date.split('/')[0] === year && value.date.split('/')[1] === month) {
                                    return value;
                                }
                            }),
                            res.json(JSON.stringify(records))
                        )
                    })
                    .catch((err) => {
                        console.log(err)
                        res.end();
                    })
            )
        })
        .catch((err) => {
            console.log(err)
            res.end();
        })

})

かなりすっきりしました。

読みやすい、書きやすい、共同開発しやすくなりましたね。

おわりに

アプリの待機時間が短くなったので、Google Apps Script + スプレッドシート から Node.js + Firebase に移行してかなりよかったです。

ただ、金額的にも月にできるAPIアクセス数にも厳しさがあったGoogle Apps Scriptですが、スプレッドシートに書き込めれるので、部内でシートの共有なんかして直接記録を見たりできるのでこっちも便利だったりします。

ただ、ユーザー数が増えると厳しいのでやっぱり捨てざるをおえないってことでおさらばしないとけません。

このwebアプリのユーザー数は自分の大学のユーザーのみの30人なので、当分気にすることはないのですがね。ふふ

最後まで見ていただき、ありがとうございました。

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

GASの処理速度が遅いのでNode.jsに移行したらwebアプリが高速に

はじめに

弓道で用いるwebアプリを開発しているんですが、データの読み書きがクソ遅いので、
GoogleAppsScript+スプレッドシートからNode.js+Firebaseに移行しました。
(GoogleAppsScript Web APIからCloud Functions APIに変えたいうこと)

Google Apps ScriptもNode.jsも基本は一緒ですが...

これでかなりwebアプリの待機時間が短くなったので、記事にしようと思います。

私の使用用途は、スプレッドシートにユーザーやデータを保管し、それを読み書きする、つまりwebアプリのバックエンドとしての利用でGASの処理速度が遅いと感じたものであるので読む際は注意してください。

結果から

どれだけ早くなったか先に紹介しておきます。

的中を記録し、その月のデータをとってくる処理を例に紹介します。記録するデータは日付、中たった数、引いた本数、的中率の四つのデータです。月のデータは付けた日の月のデータ全てとってきます。

↓Google Apps Script + スプレッドシート
gas.gif
ちなみにタイムは2.5秒ほど
スクリーンショット 2020-04-16 18.25.04.png

↓Node.js + Firebase(Cloud Functions API使用)

ezgif.com-video-to-gif (1).gif

スクリーンショット 2020-04-16 18.36.14.png

タイムは驚異の0.4秒!!!!!!!!!!!

約2秒ほど早くなりました。ユーザビリティも右肩上がりでとどまることを知らないでしょう。

*上のフェッチしてきたgifの後半に表示されるデータを見ればわかりますが、ばらつきがある状態で行ったので正確ではないですがほぼ同じデータ量です。
*cloud Funcions APIを使用した方は、ある程度時間を置いてAPIアクセスした際には4秒くらいかかっていました。これは、一定時間おくとcloud Funcionsが寝ちゃうからです。詳細は別記事にしようかと思います。

GoogleAppsScript + スプレッドシートでの読み書きが遅いのはなぜか?

Webアプリのバックエンドとして使っていて、GoogleAppsScript+スプレッドシート(データベース)の読み書きが遅いのはなぜだろう。

1、APIの呼び出し回数が多い

Spreadsheetサービスでは、以下のようなメソッドなどを使ってスプレッドシートにアクセスをするたびにAPIが呼び出される。

・getActiveSpreadsheet()
・getSheetByName()
・getDataRange()
・getLastRow()
・getRange()
・getValue()
・setValue()

二重のfor文の中で
sheet.getRange(i,j).getValue();
なんかした日には終わりで、行数×列数分APIアクセス実行されるので、かなり時間がかかる。

なので、下のようにできるだけ呼び出すAPIを減らすために全てのセルのデータを配列として一回のアクセスでとるよう設計しています。
sheet.getDataRange().getValues();

しかし、私のwebアプリでは、部員全員のデータをとってくる処理があるんですが、この処理に一回のAPIの呼び出し✖️部員数30で、計30回ほどAPIアクセスをする必要があります。

この処理が約体感3秒ほど待たされるので、アプリとしては使い勝手が悪いです。

2、読み書きするデータ量が多いと一回のAPIリクエストが遅い
一回のスプレッドシートの読み書き自体がおそらく長い。*ここで長いと言うのはNode.js+Firebaseと比較して

読み書きするデータ量にもよるだろうけど、パスワード、Eメール、その他ユーザーを管理するデータをスプレッドシートにデータベースとして管理するのには少し難があった。それもユーザー数が多いとなおさら。

膨大なユーザーを管理したりせず、個人で使うデータ量なら全然問題ない。

GoogleAppsScript(WebAPI)の制約

次に各制約について紹介する。
GoogleAppsScript(WebAPI)の制約は、

https://www.bugbugnow.net/2018/12/GoogelAppsScript-restriction.html

これをみればわかるが一日のURLフェッチ(APIアクセス)が20,000/日、さらには無料のGoogleアカウントでは、スクリプトの実行時間が6分と地味に短い。

いや用途によっては十分なんだろうが、私の使用目的では、一日10回URLフェッチを行うとして、ユーザー数2000を超えると上限に達してしまう。もう少し大規模なSPAにしようと思ったら物足りない。

有料プランもあるがそちらも上限が決まっているので、ユーザーが増えて上限がきたら嫌なのでやはりwebアプリのバックエンドとして使うのは厳しいのかもしれない。そもそもそんな使い方する人はほとんどいないだろうが、個人開発をしている人なら同じような境遇にあった人もいそう。

Cloud Functions APIの制約

それに比べてCloud Functions APIの制約というと

https://cloud.google.com/functions/pricing?hl=ja

呼び出し
関数の呼び出し料金は定額制です。HTTP リクエストから呼び出される関数(HTTP 関数)、バックグラウンド関数、call API から行われる呼び出しなど、呼び出し元によって料金が変わることはありません。

月間呼び出し回数 料金(100 万単位)
最初の 200 万回 無料
200 万回を超えた分 $0.40

米ドル以外の通貨でお支払いの場合は、Cloud Platform SKU に記載されている該当通貨の料金が適用されます。
呼び出し料金は 1 回あたり $0.0000004 の単価制で、関数の結果や実行時間に関係なく請求されます。ただし、毎月最初の 200 万回までは無料です。

だそう。ほー。無料枠がかなり多いw
一ユーザーが一日に10回APIアクセスするとして、1ヶ月で

10✖️30 = 300

毎月最初の 200 万回まで無料なので2000,000÷300 = 6,666人まで無料枠でいけるぞ!

一日10回APIアクセスを行うとして、ユーザー数2,000を超えると上限に達してしまうGASのAPIに大してCloud Functions APIは 6,666人までいける
断然、Cloud Functions APIの方がいい。さらに超えた分に対しては超えた分の請求なので都合がいい。上限を超えた際には一回のAPIアクセスをするのに約0.0004円ほどだし格安すぎる。

ユーザーが6万6666人にいるとして毎月2,000万回APIアクセスするとしたら約8,000円ほど。
一月のガソリン代くらいなら払える。

gas+スプレッドシート からNode.js + Firebaseに移行

以上よりGoogle Apps Script + スプレッドシート ⇨ Node.js + Firebase に移行しました。

gasのコード全部で500行くらいだったのでNode.jsに書き換えるのも一週間くらいで終わりました。

↓冒頭で見せたアプリ内で使われる的中率を書き込みに行き、月のデータをフェッチする処理
gasのコードとNode.jsのコードの比較(一部)を紹介します。

gas
//的中率書き込み
function submitFetchData(e){
  var sheetID =e.parameter["id"];
  var sheetName =e.parameter["name"];
  var date = e.parameter.p1;
  var hitArrow = e.parameter.p2;
  var allArrow = e.parameter.p3;
  var hitRate = hitArrow / allArrow * 100;
  var year =e.parameter["year"];
  var month = e.parameter["month"];
  //JSONオブジェクト格納用の入れ物
  var rowData = {}; 

  //書込先スプレッドシートのIDを入力
  var sheet = SpreadsheetApp.openById(sheetID).getSheetByName(sheetName);
  var arrayData = [[ date, hitArrow, allArrow, hitRate]];

  var rows = arrayData.length;
  var cols = arrayData[0].length;

  sheet.insertRows(2,1);

  //シートに配列を書き込み
  sheet.getRange(2,1,1,cols).setValues(arrayData);

  var thisDate = year + month;
  var sheet = SpreadsheetApp.openById(sheetID).getSheetByName(sheetName); 

  var data = sheet.getDataRange().getValues();
  var array = data.map(function(value,index){
    return (value.reduce(function(r,c,i){
      var result = r;
      switch(i){
        case 0: result.month = c;break;
      }
      return result;
    },{}))
  });
  array.shift();

  var date = array.map(function(value,index){
    var thisDate = {};
    var year,month;
    year = value.month.split('/');
    thisDate.year = year[0];
    month = year[1].split('/');
    thisDate.month = month[0];
    return thisDate;
  })
  var dateArray = date.map(function(value, index){
    return value.year + value.month;
  });

   var firstIndex = dateArray.indexOf(thisDate);
   var lastIndex = dateArray.lastIndexOf(thisDate);

  var newData = sheet.getRange(Number(firstIndex+2),1, Number(lastIndex+2) - Number(firstIndex+2) +1, data[0].length).getValues();  
  //オブジェクトに変換
  var newArray = newData.map(function(value,index){
    return (value.reduce(function(r,c,i){
      var result = r;
      switch(i){
        case 0: result.date = c;break;
        case 1: result.hit_arrow = c;break;
        case 2: result.all_arrow = c;break;
        case 3: result.hit_rate = c;break;
      }
      return result;
    },{}))
  }); 
   var output = ContentService.createTextOutput(JSON.stringify(newArray, null, 2));
   output.setMimeType(ContentService.MimeType.TEXT); 
   return output;
}

Node.js
//的中率書き込み&月のデータフェッチ
app.get('/hitWrite', async (req, res) => {
    console.log('-----hitWrite-----')
    const rec_name = req.query["rec_name"];
    const id = req.query["id"];
    const team = req.query["team"];
    const hit_arrow = req.query["hit_arrow"];
    const all_arrow = req.query["all_arrow"];
    const hit_rate = hit_arrow / all_arrow * 100;
    const date = req.query["date"];
    const year = req.query["year"];
    const month = req.query["month"];
    const arrayData = {
        all_arrow: Number(all_arrow),
        date,
        hit_arrow: Number(hit_arrow),
        hit_rate: Number(hit_rate),
        id: Math.random().toString(32).substring(2)//ランダムID生成
    };
    let records;
    await db.collection("college").doc(team).collection(id).doc("user_data").update({
        [rec_name]: admin.firestore.FieldValue.arrayUnion(arrayData)
    })
        .then(() => {
            return (
                db.collection("college").doc(team).collection(id).doc("user_data").get()
                    .then((snapshot) => {
                        return (
                            records = snapshot.data()[[rec_name]].filter((value) => {
                                if (value.date.split('/')[0] === year && value.date.split('/')[1] === month) {
                                    return value;
                                }
                            }),
                            res.json(JSON.stringify(records))
                        )
                    })
                    .catch((err) => {
                        console.log(err)
                        res.end();
                    })
            )
        })
        .catch((err) => {
            console.log(err)
            res.end();
        })

})

かなりすっきりしました。

読みやすい、書きやすい、共同開発しやすくなりましたね。

おわりに

アプリの待機時間が短くなったので、Google Apps Script + スプレッドシート から Node.js + Firebase に移行してかなりよかったです。

ただ、金額的にも月にできるAPIアクセス数にも厳しさがあったGoogle Apps Scriptですが、スプレッドシートに書き込めれるので、部内でシートの共有なんかして直接記録を見たりできるのでこっちも便利だったりします。

ただ、ユーザー数が増えると厳しいのでやっぱり捨てざるをおえないってことでおさらばしないとけません。

このwebアプリのユーザー数は自分の大学のユーザーのみの30人なので、当分気にすることはないのですがね。ふふ

最後まで見ていただき、ありがとうございました。

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

Macでnodebrewを使用したnodeのインストールと設定

(前手順)pkgで入れたnodeの削除 (入れてた場合)

こちらの記事を参考に削除を実施。

$ lsbom -f -l -s -pf /var/db/receipts/org.nodejs.node.pkg.bom \
| while read i; do
  sudo rm /usr/local/${i}
done

lsbomコマンドとは

BOMファイルの内容を表示するコマンド。BOMファイルとは、
「Mac OS X Bill of Materials File」のことで、Apple社固有のファイル。
インストーラ(Installer.app)を使用するMacOS X標準のパッケージファイルに含まれている。

(参考:lsbom- BOMファイルの内容を表示する)

$ sudo rm -rf /usr/local/lib/node \
     /usr/local/lib/node_modules \
     /var/db/receipts/org.nodejs.*

1.nodebrewインストールと環境変数の設定

  • ターミナルから、brewでnodebrewをインストール
$ brew install nodebrew
  • 正常にインストールされたか確認
$ nodebrew -vnodebrew -v
  • bash_profileにnode関連のディレクトリパスを追加
    ~./配下を汚さないように、以下のように設定。
NODEBREW_HOME=/usr/local/var/nodebrew/current
export NODEBREW_HOME
export NODEBREW_ROOT=/usr/local/var/nodebrew

/usr/local/var/nodebrewディレクトリがない場合は作成。
/usr/local/var/nodebrew/currentは使用するバージョンのnodeディレクトリのシンボリックリンクとなる。

(/usr/local/var/nodebrew/currentをlsした例)

$ ls -l /usr/local/var/nodebrew/current
lrwxr-xr-x  1 hoge-pc  admin  35  6 12 15:24 /usr/local/var/nodebrew/current -> /usr/local/var/nodebrew/node/v11.9.0
  • bash_profileに追加後反映
$ source ~/.bash_profile

2.nodeのインストール

  • 以下のnodebrewコマンドを実行
# 最新版を入れる場合
$ nodebrew install-binary latest

# バージョン指定で入れる場合
$ nodebrew install-binary <バージョン>
# 例
$ nodebrew install-binary v11.9.0
  • 正常にインストールされたか確認
$ nodebrew ls
  • 現在使用するnodeバージョンを設定する。
nodebrew use <バージョン名>
# 例
$ nodebrew use v11.9.0

複数のバージョンをインストールして、バージョンを切り替える場合も
上記のuseコマンドを使用する。

  • 指定したnodeバージョンが使用可能な状態になっているか確認
$ node -v

(Tips) nodebrewコマンド

  • インストール可能なnodeのバージョンを知りたい場合
$ nodebrew ls-remote
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

npm install でエラー。gyp: No Xcode or CLT version detected! 

npmを使ってみる

今回初めて、Node.jsを使うためにセットアップ!
と思っていたのですが、いきなりエラー続きで意味も分からず苦戦していたので共有します。
皆様が同じ目に合いませんように。

$ cd work/01-webpack 

まずは、フォルダを作成

$ npm init -y

そして、package.jsonファイルを作成を作成しました。
ここまでは、良かったのですが。。。
アプリケーションで使用するモジュールをインストールするために、webpackをインストールする時に

$ npm install webpack webpack-cli --save-dev

を行ったあと、エラーが。

No receipt for 'com.apple.pkg.CLTools_Executables' found at '/'.

No receipt for 'com.apple.pkg.DeveloperToolsCLILeo' found at '/'.

No receipt for 'com.apple.pkg.DeveloperToolsCLI' found at '/'.

gyp: No Xcode or CLT version detected! ←ココ
gyp ERR! configure error 
gyp ERR! stack Error: `gyp` failed with exit code: 1
gyp ERR! stack     at ChildProcess.onCpExit (/Users/mac/.nodebrew/node/v12.16.2/lib/node_modules/npm/node_modules/node-gyp/lib/configure.js:351:16)
gyp ERR! stack     at ChildProcess.emit (events.js:310:20)
gyp ERR! stack     at Process.ChildProcess._handle.onexit (internal/child_process.js:275:12)
gyp ERR! System Darwin 19.3.0
gyp ERR! command "/Users/mac/.nodebrew/node/v12.16.2/bin/node" "/Users/mac/.nodebrew/node/v12.16.2/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js" "rebuild"
gyp ERR! cwd /Users/mac/Desktop/work/01-webpack/node_modules/fsevents
gyp ERR! node -v v12.16.2
gyp ERR! node-gyp -v v5.1.0
gyp ERR! not ok 

そして気になった場所はココですね。

gyp: No Xcode or CLT version detected!

XcodeとCLTのバージョンがおかしい?

Xcodeは以前にインストールしていたので確認してみました。
すると、久しくみていなかったので、アップデートしていませんでした。
それが、原因かもと思いアップデート!!
そしてもう一度$ npm install webpack webpack-cli --save-dev

しかし、エラーは変わらず。。

※インストールしていない方はこれで

$ xcode-select --install

解決策

https://github.com/nodejs/node-gyp/blob/master/macOS_Catalina.md#i-did-all-that-and-the-acid-test-still-does-not-pass--
上記のGitHubを参考に進めました(I did all that and the acid test still does not pass :-以下)

$ sudo rm -rf $(xcode-select -print-path)
$ sudo rm -rf /Library/Developer/CommandLineTools
$ xcode-select --install

この後もう一度

$ npm install webpack webpack-cli --save-dev

を行う。

無事エラーは出ず、進めることができました!

参考記事

https://qiita.com/dmrt/items/90ad12850c0ed29758a2

https://github.com/nodejs/node-gyp/blob/master/macOS_Catalina.md#i-did-all-that-and-the-acid-test-still-does-not-pass--

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

破壊するかどうか

ほぼ備忘録。よく使うメソッドがArrayを破壊するかどうか
ググるときは日本語なら配列 [メソッド名] 破壊的、英語ならarray [メソッド名] mutatingあたりがよい

破壊する

  • fill()
  • pop()
  • push()
  • reverse()
  • shift()
  • sort()
  • splice()
  • unshift()

破壊しない

  • concat()
  • entries()
  • every()
  • filter()
  • find()
  • forEach()
  • includes()
  • indexOf()
  • join()
  • keys()
  • map()
  • reduce()
  • some()

参考情報

  • 手元の環境はNode v12.16.1
  • Array - JavaScript | MDN:built-inメソッドの一覧、ブラウザ・Nodeのバージョンごとの対応状況も載ってる
  • Does it mutate?:サンプルコードなどをまとめたサイト
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

「うちでヨガしよう」ヨガポーズがあっているか判定してくれるLINE bot~Node.jsに読み込む~

概要

 コロナウイルの影響で外出自粛、在宅ワークをされている方が多いと思います。普段よりも運動量が減ってしまうので、何かできないかと考えてヨガを支援するLINE botを開発しています。

やりたいこと

 機能としては、お題のポーズ写真がbotから送られてくるので、そのポーズをとった自分の写真をbotに送ると正しいポーズか判定してくれます。

 まずはLINE botアカウントだけ作りました。
image.png

実装

 学習モデルの作成は次の2つの記事で解説しています。
Googleが提供しているサービス「Teachable Machine」でヨガポーズの学習モデルを作って遊んでみた
Teachable Machineで作成したモデルを使用する方法(webブラウザ編)

 この記事ではNode.jsにTeachable Machineのライブラリをインストールする方法を記載してます。

必要なライブラリは次のコマンドでインストールできます。

npm i @tensorflow/tfjs
npm i @teachablemachine/pose

で、次のようにrequireします。

const tmPose = require('@teachablemachine/pose');

この記事のメイン部分はおわりにします。

が、このライブラりを使うにあたり、node-fetchが必要なようで、インストールしてrequireしてもNot defineエラーが出てしまいます....。

学習モデルをネットからロードする処理になっていそうなので、ローカルに落としてくると改善されそうです。

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

在宅ワーク中に会議中サインをobnizとLINEBotで作ってみた deploy編

はじめに

この記事は前回在宅ワーク中に会議中だよサインをobnizとLINEBotで作ってみた - Qiitaのなかで外部Deployができなかったので前回のコードを元にHerokuにDeployしました。
deployとは別のところで一部未完成です。

概要

「今オンラインミーティング中!!!!ノックしないでー、開けないでー」みたいなことで気まずい思いをしたことが増えてきたので、いわゆる トイレの空き情報「空」 みたいなサイネージが欲しくて作りました。

LINEBotに「開けないで」といれると obnizのディスプレイに 『 × 』 を描画し
「終わったよ」 といれるとけしてくれます。
 

できたもの

(ngrokからHerokuにかわりましたが、よく考えると見た目は前回同じだった。。。)
IMG_20200415_214353-COLLAGE (2).jpg

おともだちになってね

使い方

  • (会議が始まったら)LINEBotに「開けないで」
    っていれると 私の部屋にあるobnizに 「 × 」マークが出ます

  • (会議が終わったら)LINEBotに「終わったよ」
    っていれると 私の部屋にあるobnizの 「 × 」マークが消えます

環境

Node.js v13.7.0
MacBook Pro macOS Mojave
Visual Studio Code 1.44.0
使用部材
obniz

node-canvasのために ➀

obnizのディスプレイに描画するために 「node-canvas」をつかいます。
この子の取り扱いが厄介で、はまりまくりました。

node-canvas のインストールは、

「npm install canvas」

とするだけですが、その前に cairo がインストールされていなければなりません。
cairoのインストールの準備をします。

heroku buildpacks:set https://github.com/ddollar/heroku-buildpack-multi.git

.buildpacks ファイルを作ります。

cat << EOF > .buildpacks
https://github.com/mojodna/heroku-buildpack-cairo.git
https://github.com/heroku/heroku-buildpack-nodejs.git
EOF

このふたつをやったあとに

npm install canvas

しましょう!!!

コード

node.js
'use strict';

// obniz呼び出し
var Obniz = require('obniz');
var obniz = new Obniz("***");  // Obniz_ID に自分のIDを入れます


const express = require('express');
const line = require('@line/bot-sdk');
const PORT = process.env.PORT || 8080;

let matrix;

const { createCanvas } = require('canvas')
const canvas = createCanvas(128, 64);
const ctx = canvas.getContext('2d');


const config = {
  channelAccessToken: '***',
  channelSecret: '***'
};

const app = express();

app.get('/', (req, res) => res.send('Hello LINE BOT!(GET)')); 

app.post('/webhook', line.middleware(config), (req, res) => {
  console.log(req.body.events);

  Promise
    .all(req.body.events.map(handleEvent))
    .then((result) => res.json(result));
});

const client = new line.Client(config);

// obniz接続
obniz.onconnect = async function () {
  obniz.display.clear();
  obniz.display.print("HELLO");
}

async function handleEvent(event) {
  let linemes = event.message.text;

  if (event.type !== 'message' || event.message.type !== 'text') {
    return Promise.resolve(null);
  }

  let mes = event.message.text;
  if (event.message.text === '開けないで') {

    mes = '表示を出すよ'; //待ってねってメッセージだけ先に処理 
    obniz.display.clear();

    getAskObnizTemp(event.source.userId);
  }
  else if (event.message.text === '終わったよ') {
    obniz.display.clear();
    mes = "はーい"
    clearmes(event.source.userId);
  }
  else {
    obniz.display.clear();
    getlinemes(event.source.userId, linemes);
  }


  return client.replyMessage(event.replyToken, {
    type: 'text',
    text: mes 
  });
}
const getlinemes = async (userId, linemes) => {
  // ディスプレイをクリアにできないので黒い四角を書く
  // obniz.display.setColor('#000000');
  // obniz.display.rect(0, 0, 127, 127, true);

  ctx.fillStyle = "white";
  ctx.font = "20px sans-serif";
  ctx.fillText(linemes, 0, 60);
  obniz.display.clear();
  obniz.display.draw(ctx);
  obniz.display.print(linemes);

  await client.pushMessage(userId, {
    type: 'text',
    text: `${linemes}にしたよ`,
  });
}
const getAskObnizTemp = async (userId) => {
  ctx.strokeStyle = 'rgba(255,255,255,1)'
  ctx.beginPath()
  ctx.lineTo(0, 0)
  ctx.lineTo(127, 63)
  ctx.stroke()
  ctx.beginPath()
  ctx.lineTo(127, 0)
  ctx.lineTo(0, 63)
  ctx.stroke()
  obniz.display.draw(ctx);

  await client.pushMessage(userId, {
    type: 'text',
    text: "バツにしたよ",
  });
}
const clearmes = async (userId) => {
  obniz.display.clear();

  await client.pushMessage(userId, {
    type: 'text',
    text: `けしたよ`,
  });
}
// app.listen(8080);
app.listen(process.env.PORT || 8080);

console.log(`Server running at ${PORT}`);

node-canvasのために ②

上記コードをHerokuにデプロイします。(ちょっと普通じゃない)

git add --a
git commit -m "firstcommit" 
git push heroku master

ここでエラーのような怖い見た目とともに rejected とでてきます。

aaa.png

error とかでていますが、アプリケーションを実行させます。

heroku ps:scale web=1

アプリケーションにアクセスします

heroku open

スクリーンショット 2020-04-15 23.26.32.png
ブラウザが立ち上がってデプロイできました!!!!!!!!!!!!!!


動いていないところ

  1. Herokuにもっていくと、ngrokでは動いていた、 LINEBotに 入力した文字を ディスプレイに出す処理がごかなくなってしまいました。 (LINEの方にはおうむ返しが来るのですが)
node.js
const getlinemes = async (userId, linemes) => {
  // ディスプレイをクリアにできないので黒い四角を書く
  obniz.display.setColor('#000000');
  obniz.display.rect(0, 0, 127, 127, true);

  ctx.fillStyle = "white";
  ctx.font = "20px sans-serif";
  ctx.fillText(linemes, 0, 60);
  obniz.display.clear();

//drawもprintもきかない
  obniz.display.draw(ctx);
  obniz.display.print(linemes);

  await client.pushMessage(userId, {
    type: 'text',
    text: `${linemes}にしたよ`,
  });
}

2.ディスプレイがクリアにできない
obniz.display.clear();がうごいてくれず、
obniz.display.print();も動かず。何が足りないのでしょう。

3.クリアにできないため黒い四角で塗りつぶす
白色で試してみたけど、なんか一瞬出たりしたりしたような。。。

参考サイト

heroku open
node-canvas をインストールしてみました - ふにょい日記
Automattic/node-canvas: Node canvas is a Cairo backed Canvas implementation for NodeJS.

感想

obniz難しい。

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