Quantcast
Channel: Node.jsタグが付けられた新着記事 - Qiita
Viewing all 8837 articles
Browse latest View live

正規表現を使った処理で、いい感じの場所に区切り文字を入れる

$
0
0

モチベ

繋がってしまった文字列でいい感じのところに区切り文字を入れたいことってありますよね(?)
自分の場合は繋がってしまった時間帯の表記を、1区間ごとにカンマで区切りたいなーっていうタイミングがありました。
処理前 13:00~15:0017:00~22:309:00~23:00
処理後 13:00~15:00,17:00~22:30,9:00~23:00

区切りたいところに特定の文字があれば、JSだったらreplace()とかsplit()を使って簡単に区切り文字を入れ替えたり、配列にしたりできます。

まず思いついた方法

今回の場合、上記なような考え方ができないため他の方法をとる必要がありました。
3つ以上数字が並んでいる場所の2文字目のあとに、カンマを入れるという感じでいけそうです。
最初に書いたコードがこちらです。

// timeDurations → "13:00~15:0017:00~22:309:00~23:00"functionformatTimeDurations(timeDurations){letarray=timeDurations.split(/[0-9]{3,4}/g)if(array.length<2)returntimeDurationsletbridgeArray=timeDurations.match(/[0-9]{3,4}/g)letformatted=""for(leti=0;i<bridgeArray.length;++i){formatted+=array[i]formatted+=bridgeArray[i].substring(0,2)+','+bridgeArray[i].substring(2)}formatted+=array[array.length-1]returnformatted}// formatted → "13:00~15:00,17:00~22:30,9:00~23:00"

細かい説明は省きますが、先述したように3つ以上数字が並んでいる場所の2文字目のあとに、カンマを入れるという処理をしています。

正規表現をもっと上手に使った方法

上記の処理でも十分良かったのですが、正規表現についてさらに調べているとさらにいい方法があることに気がつきました。
正規表現を()で囲むとその正規表現に該当する文字列をグループ化して、変数のように再利用することができます。

functionformatDurations(durations){returndurations.replace(/([0-9]{2})([0-9]+)/g,'$1,$2')}

ここでは、数字が3つ以上連続しているところから、最初の2つを$1、その後に続くものを$2として、"$1,$2"を表示するという処理をしています。

参考にさせていただいたサイト

正規表現チートシート的な
該当文字列を用いた置換


pm2でdeployしようとするとbash: npx: command not foundで失敗する

$
0
0

原因

  • bash_profileにnvmの設定を記述していた
  • bash_profileは対話モードじゃないと読み込まれない 参考
  • しょうがないので/usr/local以下のnvmとは関係ない古いnpmが呼ばれる

→npxコマンドが存在しない

現状

ecosystem.json

"deploy":{"production":{"user":"dev","host":"**********","ref":"origin/master","repo":"**********","path":"/home/dev/www/","post-deploy":"npm install && npx sequelize db:migrate"}

対策

その1:nvmの設定をbash_profileではなくbashrcに記述する
その2:post-deployにsource ~/.bash_profileを追加する

"post-deploy":"source ~/.bash_profile && npm install && npx sequelize db:migrate"

検索しても同じ状況の人が全然見つからなかったのでたどり着くのに時間がかかりました・・・。

Macにnode.jsとnpmをinstallする

$
0
0

目的

MACにnode.jsとnpmをinstallする

環境

MAC OS Catalina 10.15.2で実施

作業ログ

bash
# xcodeのinstall (gccのinstallに必要)$ xcode-select --install# gccのinstall (nodebrewのinstallに必要)$ brew install gcc

# nodebrewのinstall$ brew install nodebrew

# nodebrewでinstallされるファイルが格納されるdirectoryを作成$ mkdir-p ~/.nodebrew/src
$ nodebrew install-binary stable

# currentの表示されるのが使うversion$ nodebrew list
v12.14.0

current: none

# stableを使うと指定$ nodebrew use stable
use v12.14.0

# node.jsのversion確認$ node -v
v12.14.0

# npmのversion確認$ npm -v
6.13.4

全世界の夜更かしさんに送る、Google Home(mini) + Nature Remo + 鯖(Synology NAS) + Node.jsでつくる夜更かし防止装置のすヽめ(google-home-notifier未使用)

$
0
0

はじめに

ついつい夜更かしをしてしまうの方に向けにGoogle HomeとNature Remoを組み合わせて「指定した時間以降、部屋が明るければGoogle Homeより早く寝るように警告を発する装置」をNode.jsで実装する作例をご紹介します!!
市販品を組み合わせるだけなのでお手頃に作れます!!(たぶん)

ちなみに似たような作例はよくありますが、多くの記事では「google-home-notifier」と呼ばれるGoole Homeに簡単にプッシュ発話をさせるライブラリが使われており、google-ttsの仕様に依存していたり、バグが多かったりして動かないことが多いのでなるべく根本ロジックから実装する方法でやっていきます。

構成

プレゼンテーション1.png

必要なもの

Nature Remo

所謂スマートリモコン。TVやエアコンなどの赤外線リモコン信号を記憶し、WiFiに繋がったこの製品から記録した赤外線信号を発することで、スマホからまとめて家電操作を可能にする製品。 帰宅前に会社からエアコンを操作して部屋を冷やしとくなどの使い方ができます。
色々なメーカーから類似品が大量に販売されていますが、このNature Remoには温度、湿度、照度、人感センサーなどが搭載されており、しかも外部APIから値を取得することができるのです!! このAPIを応用して部屋の照度を取得します。

Google Home (miniでも可)

言わずと知れたGoogle製スマートスピーカー。 このGoogle Homeより「早く寝ろ!!」といった感じに音声で警告するようにします。しかし、現状の公式SDKでは自発的に発音させることができません。。。
そこで、Google製スマートデバイスで搭載されている「キャスト機能」を活用します。キャスト機能とは、「音楽や動画をストリーミングデータとして他のデバイスに配信する」機能のことで、スマホのYoutubeアプリからTV(クロムキャスト)に飛ばして再生したり、Spotifyアプリから音楽をGoogle Homeに飛ばして再生することができます。 このキャスト機能を応用することで、任意の音声ファイルをGoogle Homeに再生させることができます。

ローカルサーバーか何か(RaspberryPiやNASなど)

基本的にGoogle Homeにキャストできるのはローカルデバイスに限られるので、ローカルネットワーク内で常時動かせるNode.js環境が必要になります。 ルータに穴を開ける、ngrokを使って中継させる方法もあるようです?? 今回は、自宅に置いてあるSynology製NASのNode.js上でタスクスケジューラーを起点に動かします。

(一から用意する人は、お手軽なWiFi搭載マイコンボードESP8266がコスパ最強かも? 「ESP8266 から Google Home に喋らせるライブラリ」 https://qiita.com/horihiro/items/4ab0edf415916a2cd542)

設計

照度の取得

「Nature Remo Cloud API」より取得します。 アクセストークンの発行が必要です。
https://developer.nature.global/

音声合成

警告音声の生成には無料で手軽に使える音声合成API「VoiceText Web API」を使います。 感情の操作もでき、本家にも劣らないほど流暢です。 使うにはAPIキーが必要なので、下記より利用申請しましょう。
「VoiceText Web API」(レファレンス)
https://cloud.voicetext.jp/webapi

ゴリゴリにリクエストパラメータを書いて実装も良いですが、面倒なので有志の方が作られたNode.js向けライブラリもあるのでそちらを使います。
「node-voicetext」
https://github.com/pchw/node-voicetext

どんな感じの音声か知りたい人はこちらから⬇️
http://voicetext.jp/samplevoice/

キャスト機能

キャスト機能の機器間の詳細なプロトコル情報はGoogle公式からは公開されていないよう??ですが、有志の方が解析して再現したライブラリ?があるのでそちらを使いキャスト機能のクライアント部分を実装します。
「castv2」
https://github.com/thibauts/node-castv2

当初、VoiceText Web APIで生成したmp3音声ファイルをGoogle Homeにストリーミングで投げればいけると思っていましたが、どうやらGoogle Homeが受け取れるのはURL情報だけっぽい?ので、mp3ファイルを配信するWEBサーバを建てる実装も必要になります。

Google HomeのIPアドレスの特定

キャストするには相手先のIPアドレス特定する必要があります。幸い、Google Homeは「mDNS」というプロトコルに対応しており、マルチキャストアドレス224.0.0.251、UDP5353ポートに向けてmDNS queryと呼ばれるメッセージを投げると対応している機器が一斉にIPアドレスを返します。また、この応答情報内のTXTレコードにGoogle Homeのデバイス名情報があるので、それで識別することができます。こちらも一から実装するのは面倒なのでライブラリに頼ります。
「mdns」
https://github.com/agnat/node_mdns

 interfaceIndex: 6,
  type: 
   ServiceType {
     name: 'googlecast',
     protocol: 'tcp',
     subtypes: [],
     fullyQualified: true },
  replyDomain: 'local.',
  flags: 2,
  name: 'Google-Home-Mini-ccebf2ba7hogehogehoge5ff2e9',
  networkInterface: 'en0',
  fullname: 'Google-Home-Mini-ccebhogehogehoge._googlecast._tcp.local.',
  host: 'ccebf2ba-7c7d-ed85-6986-c0af325ff2e9.local.',
  port: 8009,
  rawTxtRecord: <Buffer 23 69 64 3d 63 63 65 62 66 32 62 61 37 63 99 64 65 64 38 35 36 39 38 36 63 30 61 66 33 32 99 66 66 32 65 39 23 63 64 3d 36 42 44 33 99 33 33 36 36 30 ... >,
  txtRecord: 
   { id: 'ccebf2ba7c7ded9999af325ff2e9',
     cd: '6BD343366053C3C999990F6AF76C',
     rm: 'E705D56999969244F',
     ve: '05',
     md: 'Google Home Mini',
     ic: '/setup/icon.png',
     fn: 'ダイニング ルーム',
     ca: '198660',
     st: '0',
     bs: 'FA8FCA855A41',
     nf: '1',
     rs: '' },
  addresses: [ '192.168.1.21' ] 

実装

照度の取得

レファレンスに書いてある通りヘッダーにアクセストークンを入れてリクエストを投げます。

asyncfunctiongetIl(){returnaxios.get('https://api.nature.global/1/devices',{headers:{'Accept':'application/json','Authorization':'Bearer AjmyJhogehogKSRFg3owta-DfnahogehodksQ2g.ezTpIVtf-V8znl7HSfg-PYhogehoge0r0WxXTFA'},data:{}}).then(response=>{returnresponse.data[0].newest_events.il.val;}).catch(err=>{console.log(err);reject(err);});}

音声ファイルの生成

話者、感情値などの各種パラメータを設定しVoiceText Web APIより音声ファイルを取得してローカルに保存します。

functionconvertToText(text){returnnewPromise(function(resolve,reject){voice.speaker(voice.SPEAKER.TAKERU).emotion(voice.EMOTION.HAPPINESS).emotion_level(voice.EMOTION_LEVEL.HIGH).volume(150).format('mp3').speak(text,function(e,buf){if(e){console.error(e);reject(e);}else{fs.writeFileSync(OUT_PATH,buf,'binary');resolve();}});});}

キャスト用音声ファイル配信WEBサーバを建てる

ExpressでWEBサーバーを建て、キャスト用のmp3音声ファイルを配信します。

functionstartSever(){constserver=app.listen(PORT,console.log('port: '+PORT));app.get(`/audio/${FILE_NAME}`,(req,res)=>{fs.readFile(`./${FILE_NAME}`,(err,data)=>{if(err)res.status(400).send(err.toString());else{res.setHeader('Content-Length',data.length);res.write(data);res.end();}});});returnserver;}

Google HomeのIPアドレスを取得

mdnsクエリーを投げてレスポンスを数秒待ちます。 検索結果の中より対象デバイス名のIPアドレスを抜き出します。

functionsearchDeviceIp(device_name){letdevice_ip='';returnnewPromise((resolve,reject)=>{browser.start();// serviceUpイベントより情報を抜き出すbrowser.on('serviceUp',(service)=>{if(device_name.replace(/\s+/g,'')===service.txtRecord.fn.replace(/\s+/g,'')){device_ip=service.addresses[0];}});// 検索時間setTimeout(()=>{browser.stop();resolve(device_ip);},SEARCH_TIMEOUT);});}

キャスト機能

キャストする際にGoogle Homeの音量をコントロールすることができます。エラーで死んだ時に必ずコネクションをクローズ!!

functionsay(host,content){constserver=startSever();constclient=newClient();client.connect(host,()=>{client.setVolume({level:0.5},(err,newvol)=>{if(err)console.log("there was an error setting the volume");});client.launch(DefaultMediaReceiver,(err,player)=>{if(err){console.log(err);server.close();return;}player.on('status',status=>{console.log(`status broadcast playerState=${status.playerState}`);});constmedia={contentId:content,contentType:'audio/mp3',streamType:'BUFFERED'};player.load(media,{autoplay:true},(err,status)=>{client.close();server.close();});});});client.on('error',err=>{console.log(`Error: ${err.message}`);client.close();server.close();});};

 最終実装

/*****************************************************************//*                 コンフィグ&パラメーター                       *//*****************************************************************/constNAS_IP='192.168.22.9'/* NASのIPアドレス */constPORT=8083;/* NASのポート */constFILE_NAME='voice.mp3';/* 音声合成出力ファイル名 */constGOOGLE_HOME_NAME='ダイニング ルーム';constVOICE_MESSAGE='寝る時間です! 直ちに歯磨きをして就寝しましょう!!'constNATURE_REMO_API_KEY='AjmyJV5syfoghogehogeREJdksQ2g.ezTpIVtf-V8znl7HJjrF0r0WxXTFA';/* Nature Remo Cloud API アクセストークン */constVOICE_TEXT_API_KEY='w7ahogehogebiam';/* VoiceText Web API アクセストークン *//* パラメーター */constSEARCH_TIMEOUT=2000;/* Goole Homeの探索時間(ms) */constIL_THRESHOLD=100;/* 照度の閾値 */constATTENTION_INTERVAL=90*1000;/* 警告のインターバル(ms) */constMONITORING_ENDTIME=dayjs().hour(3).minute(15).add(1,'day');/* 監視終了時間 次の日の3時15分(日本時間)*/constGOOGLE_HOME_VOLUME=0.5;/* Google Homeのボリューム*//*****************************************************************//*                         ライブラリ                            *//*****************************************************************//* 各種ライブラリ */constfs=require('fs');constaxios=require('axios');constapp=require('express')();constClient=require('castv2-client').Client;constDefaultMediaReceiver=require('castv2-client').DefaultMediaReceiver;constVoiceText=require('voicetext');constvoice=newVoiceText(VOICE_TEXT_API_KEY);constmdns=require('mdns');constbrowser=mdns.createBrowser(mdns.tcp('googlecast'));/* 日付ライブラリ&日本ロケール設定 */require('dayjs/locale/ja');constdayjs=require('dayjs');dayjs.locale('ja');/*****************************************************************//*                 メインルーチン                                *//*****************************************************************/constOUTPUT_PATH=`./${FILE_NAME}`;/* 音声ファイル出力先 */constURL=`http://${NAS_IP}:${PORT}/audio/${FILE_NAME}`;/* NAS上の音声ファイルURL */constgoogle_home_ip=searchDeviceIp(GOOGLE_HOME_NAME);(async()=>{// 音声データ作成awaitconvertToText(VOICE_MESSAGE,OUTPUT_PATH);// 初回チェックif(!awaitisSleep()){say(google_home_ip,URL,GOOGLE_HOME_VOLUME);}// 監視開始constid=setInterval(async()=>{if(!awaitisSleep()){say(GOOGLE_HOME_IP,URL,GOOGLE_HOME_VOLUME);}elseif(dayjs().isAfter(MONITORING_ENDTIME)){// 監視終了clearInterval(id);}},ATTENTION_INTERVAL);})();/*****************************************************************//*               各種関数                                        *//*****************************************************************/functionsearchDeviceIp(device_name){letdevice_ip='';returnnewPromise((resolve,reject)=>{browser.start();// serviceUpイベントより情報を抜き出すbrowser.on('serviceUp',(service)=>{if(device_name.replace(/\s+/g,'')===service.txtRecord.fn.replace(/\s+/g,'')){device_ip=service.addresses[0];}});// 検索時間setTimeout(()=>{browser.stop();resolve(device_ip);},SEARCH_TIMEOUT);});}functionstartSever(port,file_name){constserver=app.listen(port,console.log('port: '+port));// ルーティング設定app.get(`/audio/${file_name}`,(req,res)=>{fs.readFile(`./${file_name}`,(err,data)=>{if(err)res.status(400).send(err.toString());else{res.setHeader('Content-Length',data.length);res.write(data);res.end();}});});returnserver;}functionsay(host,content,volume){constserver=startSever(PORT,FILE_NAME);constclient=newClient();// デバイスへの接続client.connect(host,()=>{// 音量設定client.setVolume({level:volume},(err,newvol)=>{if(err)console.log("there was an error setting the volume");});// キャストの開始client.launch(DefaultMediaReceiver,(err,player)=>{if(err){console.log(err);server.close();return;}player.on('status',status=>{console.log(`status broadcast playerState=${status.playerState}`);});constmedia={contentId:content,contentType:'audio/mp3',streamType:'BUFFERED'};// 音声再生player.load(media,{autoplay:true},(err,status)=>{client.close();server.close();});});});// 接続できない場合のエラー処理client.on('error',err=>{console.log(`Error: ${err.message}`);client.close();server.close();});};functionconvertToText(text,output_path){returnnewPromise((resolve,reject)=>{voice.speaker(voice.SPEAKER.TAKERU).emotion(voice.EMOTION.HAPPINESS).emotion_level(voice.EMOTION_LEVEL.HIGH).volume(150).format('mp3').speak(text,(e,buf)=>{if(e){console.error(e);reject(e);}else{fs.writeFileSync(output_path,buf,'binary');resolve();}});});}asyncfunctiongetIl(api_key){returnaxios.get('https://api.nature.global/1/devices',{headers:{'Accept':'application/json','Authorization':`Bearer ${api_key}`},data:{}}).then(res=>{returnres.data[0].newest_events.il.val;}).catch(err=>{console.log(err);reject(err);});}asyncfunctionisSleep(){constil=awaitgetIl(NATURE_REMO_API_KEY);return(il<IL_THRESHOLD);}

運用

Google Homeのデバイス名の確認

自分で設定したデバイス名をお忘れの方は、スマホのGoogle Homeアプリより確認することができます。
Screenshot_20191227-100900.png

NASへの設置、タスクスケジューラーの設定

NASの適当なディレクトリに実装したソースファイルを設置し、消灯時間に自動実行するようにタスクスケジューラーに登録します。
SynologyNASであれば、コントロールパネル→タスクスケジューラーより登録できます!
image.png

これで夜更かし生活ともおさらば!!

MNISTのデータベースをJSONで得る

$
0
0

MNISTのWebサイトで配布されている手書き数字のデータは、バイナリ形式です。これを、JSONデータに変換する方法をメモしました。

http://yann.lecun.com/exdb/mnist/

DBをダウンロードして、解凍します。

mnistdb2json.js
// MNIST手書きデータをJSONに変換する for Node.jsconstCONV_LIMIT=5000// 小さなJSONを作成する場合// サンプルファイルの保存先を指定varDIR_IMAGE=__dirname+"/image";// モジュールの取り込みvarfs=require('fs');// 変換処理convertToJSON("train");convertToJSON("t10k");functionconvertToJSON(basename){console.log("convert: "+basename);// 各種ファイル名を決定varfile_images=basename+"-images-idx3-ubyte";varfile_labels=basename+"-labels-idx1-ubyte";varfile_csv=basename+".json";// ファイルを開くvarf_img=fs.openSync(file_images,"r");varf_lbl=fs.openSync(file_labels,"r");// var f_out = fs.openSync(file_csv, "w+");if(!fs.existsSync(DIR_IMAGE)){fs.mkdirSync(DIR_IMAGE);}// ヘッダを読むvarbuf_i=newBuffer(16);fs.readSync(f_img,buf_i,0,buf_i.length);varbuf_l=newBuffer(8);fs.readSync(f_lbl,buf_l,0,buf_l.length);// ヘッダをチェックvarmagic=buf_i.readUInt32BE(0);varnum_images=buf_i.readUInt32BE(4);varnum_rows=buf_i.readUInt32BE(8);varnum_cols=buf_i.readUInt32BE(12);varnum_size=num_rows*num_cols;if(magic!=0x803){console.error("[ERROR] Broken file=",magic.toString(16));process.exit();}console.log("num_of_images="+num_images);console.log("num_of_rows="+num_rows);console.log("num_of_cols="+num_cols);console.log("num_of_pixel_size="+num_size);// 画像を取り出すvarbuf_img=newBuffer(num_size);varbuf_lbl=newBuffer(1);varmini_csv="";varlabels=[];vardatas=[];for(vari=0;i<num_images;i++){// 経過を表示if(i%1000==0)console.log(i+"/"+num_images);if(i>CONV_LIMIT)break;// 画像を読むvarpos_i=i*num_size+16;fs.readSync(f_img,buf_img,0,num_size,pos_i);// ラベルを読むvarpos_l=i*1+8;fs.readSync(f_lbl,buf_lbl,0,1,pos_l);varno=buf_lbl[0];// PGM形式として保存 (テスト用)if(i<30){vars="P2 28 28 255\n";for(varj=0;j<28*28;j++){s+=buf_img[j]+"";s+=(j%28==27)?"\n":"";}varimg_file=DIR_IMAGE+"/"+basename+"-"+i+"-"+no+".pgm";fs.writeFileSync(img_file,s,"utf-8");}// CSVとして保存varcells=[];for(varj=0;j<28*28;j++){constv=Math.floor(buf_img[j]/255*100)/100cells.push(v);}labels.push(no)datas.push(cells)}varobj={"label":labels,"data":datas}fs.writeFileSync(file_csv,JSON.stringify(obj),"utf-8")console.log("ok:"+basename);}

firebase データ取得にfor文を使いつつ、その中で非同期処理する方法

$
0
0

この記事は、どっかからAPIをとってきたデータを、firebaseのデータ群と比較参照する場合、
非同期処理とfor文をどうやって組み合わせれば良いかというところにいつも引っかかっていてしまっていたので、その備忘録です。

環境

  • Node.js
  • Cloud firestore

どんな問題なのか

firestore にあるcollection からデータを全てとってくる場合、下記の例のように、forEachを使ってとってくるのが普通です。

varquery=db.collection('contents').get();query.then(function(snapshot){snapshot.forEach(function(childSnapshot){...// childsnapshot の処理});});

しかし、このforEach文の中、つまり、childsnapshotの処理の中に、時間がかかる関数を呼び出していて非同期処理をする必要があるとなった場合、
forEachは非同期処理に対応していないため、一筋縄でいかないのが問題でした。

具体的に、下記のような重い処理をchildSnapshotの処理内で行うと、

varquery=db.collection('contents').get();query.then(function(snapshot){snapshot.forEach(asyncfunction(childSnapshot){console.log('before_func');awaitheavyfunc(childSnapshot);//重い処理console.log('after_func');});});constheavyfunc=function(data){...// API をとってくるなどの重い処理console.log('inside_func');}
// snapshot が2つのdataを持っている場合before_funcafter_funcbefore_funcafter_funcinside_funcinside_func

このような結果が得られ、heavyfuncの処理がforEachの処理に追いついていない状態になります。

どうやったか

varquery=db.collection('contents').get();query.then(asyncfunction(snapshot){forawait(letchildSnapshotofsnpashot.docs){console.log('before_func');awaitheavyfunc(childSnapshot);//重い処理console.log('after_func');}});constheavyfunc=function(data){...// API をとってくるなどの重い処理console.log('inside_func');}

forEachではなく、for ~ ofを使ってループ内での非同期処理を実現しました。

具体的に

  • for ~ ofを使う際、そのループにもawaitを付けたす必要がありました。
  • snapshotのデータを一つずつ取ってくる時に、snapshot.docsのようにdocsを呼び出さないと、データを取得することが出来ませんでした。

単一リポジトリ内の複数のpackageで共通の型を使い、さらに型をpackage化

$
0
0

概要

小規模のサーバーサイド・フロントエンドのように、
マルチパッケージを含む構成のリポジトリにおいて・・・

root
├── functions
│   └── package.json    // サーバーサイドの package
└── hosting
    └── package.json    // フロントエンドの package

以下を満たすためのメモです。

  • functions, hosting から単一の型定義を使う
  • root-types package として、型を export する
    • ここでexportした型を、外部のプロジェクトからも使えるようにする

最終形態

上記の条件を満たすリポジトリの最終形です

root
├── package.json  // root-types package
├── types
|   ├── dist      // root-types としての配布物
|   ├── src       // root-types の実装
|   |   ├── index.ts    // root-types で export する名前空間の定義
|   |   ├── modules.ts  // 型をまとめて簡潔に export するためのクッション
|   |   └── impl        // 型の実装
|   |        ├── hoge-types.ts
|   |        └── piyo-types.ts
|   └── tsconfig.json
|
├── functions
│   └── package.json    // サーバーサイドの package
└── hosting
    └── package.json    // フロントエンドの package

root-types package の作成

package.json

types/distを配布物とします。

package.json
{"name":"root-types","main":"types/dist/index.js","files":["types/dist"],"scripts":{"build":"npx tsc --project types/tsconfig.json",},"devDependencies":{"typescript":"^3.7.4"}}

types/tsconfig.json

types/dist/に型定義を出力するように定義します。

types/tsconfig.json
{"compilerOptions":{"module":"commonjs","moduleResolution":"node","noImplicitReturns":true,"noUnusedLocals":true,"outDir":"dist","sourceMap":true,"strict":true,"target":"es2017","esModuleInterop":true,"allowSyntheticDefaultImports":true,"skipLibCheck":true,"declaration":true},"compileOnSave":true,"include":["src"]}

types/src/index.ts

modules.tsで export しているすべてを、
Rootという名前でくるんでexportしています

types/src/index.ts
import*asRootfrom"./modules";export{Root};

types/src/modules.ts

types/src/impl/以下の実装をまとめて export するためのクッションです
export 対象のファイルが増えるたび、追記します

types/src/modules.ts
export*from"./impl/hoge-types";export*from"./impl/piyo-types";

types/src/impl

export したい型の実装です

types/src/impl/hoge-types.ts
exportinterfaceHoge{str:string;}
types/src/impl/piyo-types.ts
exportenumPiyo{ONE=1,TWO=2,THREE=3,}

ビルド

npm run buildを実行すると、 types/dist/以下に型定義 d.tsを伴って出力されます

内部の package から型を参照

hosting/のソースから、通常の package と同様に root-typesを利用できるようにします
(functions/以下も同様です)

以下のように利用できるようになります。

hosting/src/index.ts
import{Root}from'root-types';consthoge:Root.Hoge={str:"hello"};console.log(hoge,Root.Piyo.ONE);// {str: "hello"} 1

[方法1] npm link

hosting/以下で npm link ../を実行すると、
hosting/node_modules/以下に root-typesのシンボリックリンクが生成されます

postinstall に link を記述

このままでは毎回 npm link ../を実行しなくてはなりません。

そこで、 hosting/package.jsonに以下の script を追記し、
npm install 後に自動的に実行されるようにします

hosting/package.json
"scripts":{"postinstall":"npm link ../",}

[方法2] npm install

こっちのほうが楽かもしれません。
./hostingnpm install ../を実行するだけです

hosting/package.json
"dependencies":{"root-types":"file:..",}

外部の package から利用する

今回作成した root-types package はほかの private プロジェクトで参照するのみで、
npm に publish したりはしません。
外部のプロジェクトで、 rootリポジトリを直接参照して root-typesをinstallできました。

npm install git+ssh://git@gitlab.com:YOU/root.git --save

package.json
"dependencies":{"root-types":"git+ssh://git@gitlab.com:YOU/root.git",}

参考
nodejsで自作別リポジトリ(gitlab/github)をモジュール化して利用するには?

まとめ

マルチパッケージのリポジトリで型を共有したり、
型をexportして他のリポジトリで使ったりする機会が増えてきたので書きました。

同一リポジトリ・マルチパッケージでサクッと楽して開発できるだけで、
キレイスッキリな構成ではない気がするので、
大きく育てるプロジェクトではもっと最適なソリューションがないか検討したいですね。

【IBM Watson】AIが性格を推定してくれるPersonality Insigthtsデモサイトの構築方法 (Node.js編)

$
0
0

皆さん、こんにちは。戸倉彩です。

今回は今すぐ使えるIBM Watson Personality Insights APIのサービスを使用した、テキストやTwitterのツイートを分析するWebアプリケーションを開発する方法を紹介します。パブリッククラウドのサービスを使用しますが、無料の範囲で作成することができるので、自己学習やコミュニティのハンズオン等の題材に迷った時には役立てていただければと思います。

IBM Watson Personality Insights (性格分析)とは

Personality Insightsは、IBMが提供しているIBM Watsonサービスの一つで、テキストから筆者のパーソナリティ(ビッグ・ファイブ、価値、ニーズ)の3つの特徴をAIから推測します。詳細についてはIBM Watson Personality Insights 公式サイトをご覧ください。

Personality Insightsデモサイトとは

IBM Watson Personality Insightsが使われているデモサイトがこちらに公開されています。リアルタイムに「サンプルのTwitterアカウント」「テキスト入力」「ご自身のTwitterからツイートされたテキスト」による分析を体験できます。

デモのために設置されているサイトのため、アクセスが集中した場合やメンテナンス時には正常に動作しない可能性もあるようです。
GitHubにソースコードが公開されていますので、macOSやWindowsのパソコンをお持ちの方であればいつでも誰でも入手して、自分でデモサイトを手元の環境に構築して動かすことができます。また、Apache 2.0ライセンスで配布しているため、商用利用も可能です。
万が一、ソースコードに問題があることを発見した場合には、直接GitHubのリポジトリにIssueをあげて報告をしてください。

始める前に

1. Node.jsのインストール

Node.js公式サイトから入手できます。2019年12月27日現在、12.14.0 LTSが推奨版となっています。

2. IBM Cloudのアカウントの取得

IBM Cloud上で提供されているIBM Watson Personality Insightsのサービスを使用するため、ライトアカウント/従量課金(PayG)アカウント/サブスクリプションアカウントのいづれかのアカウントが必要です。これからIBM Cloudを始める場合は、過去に投稿した「無料ではじめられるライト・アカウント登録方法」をご覧いただき、アカウントの登録を行なってください。

3. IBM Cloud CLIのインストール (IBM CloudにWebサイトを公開する場合)

下記のコマンドを実行して、IBM Cloud CLIのインストールを行います。

  • MacおよびLinuxの場合、次のコマンドを実行します。
curl -sL https://ibm.biz/idt-installer | bash
  • Windowsの場合、 管理者としてPowerShellで次のコマンドを実行します。
[Net.ServicePointManager]::SecurityProtocol = "Tls12"; iex(New-Object Net.WebClient).DownloadString('https://ibm.biz/idt-win-installer')

4. Twitterアカウントのログイン情報の取得

特定のTwitterアカウントでツイートされた内容を分析させたい場合は、対象となるアカウントのログイン情報を入手する。
もし、これからTwitterアカウントを取得したい場合には、下記サイトよりTwitterアカウントを作成し、ツイートをはじめる。※100程度の単語からの分析が可能ですが、より正確な分析のためには、より多くのテキストが存在していることが望ましいです。
https://twitter.com/i/flow/signup

操作の手順

大まかな操作の流れは下記の4つになります。
1. Personality Insightsのサービス作成
2. GitHub上のリソースのクローン
3. 環境ファイルの設定 (.env)
4. Twitterアプリケーションの設定
5. ローカル実行

1. Personality Insightsのサービス作成

  1. WebブラウザからIBM Cloudにアクセスしてログインする。
  2. IBM Cloudダッシュボードから右上の[カタログ]メニューをクリックする。
  3. 左側メニューの[AI]をクリックする。
  4. 右側に表示されたAIカテゴリとして提供しているサービス一覧から[Personality Insights]を見つけてクリックする。
  5. 下記の項目を確認した後、右側の[サマリー]で設定した内容を確認して正しければ[作成]ボタンをクリックしてサービスを作成する。
  • 地域の選択
    • デフォルト[ダラス]のままでもOKです
    • ドロップダウンメニューから[東京]も選択できます
  • 料金プランの選択
    • 今回は、[ライト(無料)]にチェックが入って選択されていることを確認してください
    • 検証や実案件などで1ヶ月あたりに1,000件を超えるAPI呼び出しを行う場合には[ティア]または[プレミアム]を選択してください
  • サービス名
    • デフォルトのままでもOKですが、任意のサービス名をアルファベット、記号、数字の組み合わせで指定することが可能です。
    • 今回の例では[PersonalityInsights-Japan]を指定してます。

6.しばらくして[リリース・リスト]ページが表示されたら、左側の[サービス資格情報]メニューをクリックする。

7. [サービス資格情報]に自動生成された[Auto-generated service credentials][資格情報の表示]リンクをクリックする。
8. 表示された資格情報の[apiey]および[url]は後ほど使うため、メモ帳などにコピーしておく。

2. GitHub上のリソースのクローン

ターミナルから下記コマンドを実行してGitHubからソースコードを入手する。

git clone https://github.com/watson-developer-cloud/personality-insights-nodejs.git

3. 環境ファイルの設定 (.env)

1.[personality-insights-nodejs]ディレクトリを移動する。

cd personality-insights-nodejs/

2.エディタでフォルダの中身を開いて .env.exampleファイルを .envというファイル名でコピーを作成する。
3. .envファイルを開き、前の手順で取得したサービス資格情報を追加し、ファイルを保存する。

PERSONALITY_INSIGHTS_IAM_APIKEY= ここにapikeyをコピペする
PERSONALITY_INSIGHTS_URL= ここにurlをコピペする

4. Twitterアプリケーションの設定

  1. Twitter Developerサイトからアプリケーションを作成する。
  2. コールバックURLを追加する。
  • ローカル環境の場合: http://localhost:3000/auth/twitter/callback
  • クラウド環境の場合: 各クラウドサービスの仕様を確認してURLを指定してください。
  • IBM Cloud環境の場合: <application-name>.mybluemix.net/auth/twitter/callback

3..envファイルにTwitterアプリケーションの資格情報を追加し、ファイルを保存する。

TWITTER_CONSUMER_KEY=<consumer-key>
TWITTER_CONSUMER_SECRET=<consumer-secret>

5. ローカル実行

  1. 下記のコマンドを実行して依存関係をインストールする。
npm install

2.アプリケーションを実行する

npm start

3.Webブラウザで localhost:3000にアクセスする。
4. Personality Insightsデモサイトが表示されれば成功です!!!

指定したTwitterアカウントのツイートによるテキストから性格を分析したい場合には「あなたのTiwtterによる分析」ボタンをクリックして操作を行なってみてください。

このアプリケーションについて

このアプリケーションは、Node.jsで動くWebアプリケーションです。後は、お好みのクラウドサービス上にデプロイしたり、自由にカスタマイズしてご活用ください。

ディレクトリ構成

.
├── app.js                       // express entry point
├── config                       // express configuration
│   ├── error-handler.js
│   ├── express.js
│   ├── i18n.js
│   ├── passport.js
│   └── security.js
├── helpers                      // utility modules
│   ├── personality-insights.js
│   └── twitter-helper.js
├── i18n                         // internationalization
│   ├── en.json
│   ├── es.json
│   └── ja.json
├── manifest.yml
├── package.json
├── public
│   ├── css
│   ├── data                     // sample text and tweets
│   ├── fonts
│   ├── images
│   └── js
├── router.js                   // express routes
├── server.js                   // application entry point
├── test
└── views                       // ejs views

IBM Cloudを活用してWebサイトを公開する方法

  1. IBM Cloud CLIを用いてターミナルまたはコマンドラインからIBM Cloudにログインする。
    ※ログインの際に、APIエンドポイントや地域がus-southに指定されていても問題ありません。
ibmcloud login

2.下記のコマンドを実行してCloud Foundry (IBM CloudでWebサイトを手軽にホスティングすることができるサービス) をターゲットにします。

ibmcloud target --cf

3.manifest.ymlファイルの3行目のアプリケーション名の部分を編集し、ファイルを保存する。
例えば - name: my-app-nameなど、ユニークな文字列を指定してください。Webサイトを公開する際にURLの一部になるため、ご注意ください。

4.アプリケーションをIBM Cloudにデプロイします。
ネットワーク環境により必要な時間は異なりますが、安定している環境の場合には数分〜で完了します。

ibmcloud app push

5.アプリケーションをデプロイしたURL (https://アプリ名.mybluemix.net) にアクセスして、公開されたことを確認します。

例えば https://personality-insights-qiitademo.mybluemix.net

今回はここまでです。お疲れ様でした!
Have a nice Geek Life♪

※Twitterで最新情報配信中 @ayatokura


React &バックエンド処理の画面をとりあえず公開するまで

$
0
0

やりたいこと

Reactを使ったフロント側と、バックエンドとの連携処理を最低限動かし、Herokuで公開するまでをメモします。
千里の道も一歩から。

全体イメージ

ローカルでの全体イメージは以下です。
Reactはフロントエンドのフレームワークなので、サーバサイドのバックエンド処理は別物として分離してしまったほうが分かりやすそうです。
SnapCrab_NoName_2019-12-26_23-11-19_No-00.png

手順

前提

以下ができていることを前提にします。

  • Node.jsがローカルにインストールされていること
  • GithubHerokuのアカウントを持っていること
  • gitがローカルにインストールされていること
  • Heroku CLIがローカルにインストールされていること

Githubリポジトリ準備

Githubにフロント用、バック用の空リポジトリを作っておきます。
SnapCrab_NoName_2019-12-26_23-50-16_No-00.png

以降、リポジトリ名やフォルダ名は自分用に読み替えてください。

バックエンド作成

バックエンドのNode.jsアプリを作っていきます。

# フォルダ準備cd ~/git
mkdir nodejs-template-back
cd nodejs-template-back/

# 初期化 色々聞かれるが全部EnterでOK
npm init

# expressをインストール
npm install-s express

package.jsonが生成されますが、少し記載が足りないので修正しておきます。
enginesでのnode.jsバージョン指定、scripts:startで起動時のスクリプト指定を追加しています。

package.json
{"name":"nodejs-template-back","version":"1.0.0","engines":{"node":"12.x"},"description":"","main":"index.js","scripts":{"start":"node index.js","test":"echo \"Error: no test specified\"&& exit 1"},"author":"","license":"ISC","dependencies":{"express":"^4.17.1"}}

直下にソースを用意します。GETしかない単純なREST APIです。
データは、最終的にはDBから取ったりすることになると思いますが、今時点では定数を返すだけです。
また、フロントとバックのURLが異なるので、フロントのjavascriptからAPIを呼べるようにCORS設定を入れています。

index.js
constexpress=require('express');constapp=express();// CORS設定。異なるURLからでも呼び出せるようにするapp.use(function(req,res,next){res.header("Access-Control-Allow-Origin","*");res.header("Access-Control-Allow-Headers","Origin, X-Requested-With, Content-Type, Accept");next();});// jsonを使えるようにするapp.use(express.json());// サーバ起動constport=process.env.PORT||5001;app.listen(port,()=>console.log(`Listening on port ${port}...`));// DBの代わりconstuser={id:1,name:'Taro'};// GET /api/userapp.get('/api/user',(req,res)=>{res.send(user);});

他、いくつか必要なファイルを作っておきます。

・モジュールをgitに登録しないための.gitignoreファイル

.gitignore
/node_modules

・Herokuの設定ファイル。これを作っておかないと後でheroku経由で実行したときにWarningが出ます

Procfile
web: npm start

ここまでできたらGithubに登録しておきます。

cd ~/git/nodejs-template-back
git init
git add .
git commit -m"first commit"
git remote add origin https://github.com/【アカウント名】/nodejs-template-back.git
git push -u origin master

フロントエンド作成

続いてフロントエンドを作っていきます。

# Reactアプリを作成。少し時間がかかるcd ~/git
npx create-react-app nodejs-template-front
cd nodejs-template-front

package.jsonを確認し、もしreact-scriptsのバージョンが3.3.0だった場合、3.2.0に落としておきます。
3.3.1以降であればこの手順は不要(なはず)です
参考:create-react-appで作ったアプリがhttpsだと動かない

cat package.json
npm install react-scripts@3.2.0

生成されたソースを改造し、バックエンドのAPIを呼び出すようにしてみます。

src/App.js
importReactfrom'react';importlogofrom'./logo.svg';import'./App.css';// 修正1: コンストラクタを作りたいので、関数コンポーネントからクラスに修正classAppextendsReact.Component{//function App() {// 修正2: コンストラクタを追加constructor(props){super(props);this.state={};// バックエンドのAPIを呼び出し、this.state.nameに結果を保管.// 呼び出し先はローカルとサーバ上で可変にしたいので環境変数からとるfetch(process.env.REACT_APP_BACKEND_URL+"/api/user").then(response=>response.json()).then(json=>this.setState({name:json.name}));}// 修正3: renderメソッドにするrender(){return(<divclassName="App"><headerclassName="App-header"><imgsrc={logo}className="App-logo"alt="logo"/>{/* 修正4: API呼び出し結果を反映 */}<p>Hi,{this.state.name}.Edit<code>src/App.js</code> and save to reload.
</p>
<aclassName="App-link"href="https://reactjs.org"target="_blank"rel="noopener noreferrer">LearnReact</a>
</header>
</div>
);}}exportdefaultApp;

バックエンドAPIの呼び出し先は可変にしたいので、ローカル用の値を環境変数にセットしておきます。
.envというファイルを作成し、その中に書いておくことで、上記ソースのようにprocess.env.環境変数名 で呼び出せます。
「REACT_APP_」で始まる環境変数名にしておく必要があるので注意です。

.env
REACT_APP_BACKEND_URL=http://localhost:5001

他、バック同様にHeroku用の必要ファイルを作っておきます。

Procfile
web: npm start

Githubに登録します。

cd ~/git/nodejs-template-front
git init
git add .
git commit -m"first commit"
git remote add origin https://github.com/【アカウント名】/nodejs-template-front.git
git push -u origin master

Herokuアプリ準備

Herokuにフロント用、バック用の空アプリを作っておきます。画面からNew -> Create new app で作るだけです。
SnapCrab_NoName_2019-12-26_23-39-14_No-00.png

それぞれGithubと連携させます。また、一度Manual deployをしておきます。
参考:heroku 初級編 - GitHub から deploy してみよう -

また、ローカルのリポジトリはこの時点でGithubとしか紐づいていないので、Herokuとも関連付けておきます。

# バックcd ~/git/nodejs-template-back/
heroku git:remote -a nodejs-template-back
# 確認
git remote -v
   heroku  https://git.heroku.com/nodejs-template-back.git (fetch)    // 追加された
   heroku  https://git.heroku.com/nodejs-template-back.git (push)     // 追加された
   origin  https://github.com/uemuram/nodejs-template-back.git (fetch)
   origin  https://github.com/uemuram/nodejs-template-back.git (push)# フロントcd ~/git/nodejs-template-front/
heroku git:remote -a nodejs-template-front
# 確認
git remote -v
   heroku  https://git.heroku.com/nodejs-template-front.git (fetch)   // 追加された
   heroku  https://git.heroku.com/nodejs-template-front.git (push)    // 追加された
   origin  https://github.com/uemuram/nodejs-template-front.git (fetch)
   origin  https://github.com/uemuram/nodejs-template-front.git (push)

フロント用に、バックエンドのURLをHeroku側の環境変数に設定します。Heroku上で動かすときは、.envではなくこちらで設定した環境変数が使われます。

# 設定
heroku config:set REACT_APP_BACKEND_URL=https://nodejs-template-back.herokuapp.com
# 確認
heroku config
   REACT_APP_BACKEND_URL: https://nodejs-template-back.herokuapp.com  // 設定された

動作確認(ローカル)

フロント、バックをそれぞれ起動してみます。
バックはフロントとポートを変えるために、明示的にポートを指定しています。

# バックcd ~/git/nodejs-template-back/
heroku local-p 5001

# フロントcd ~/git/nodejs-template-front/
heroku local

それぞれ以下のURLで確認できます。

SnapCrab_NoName_2019-12-27_17-19-53_No-00.png

フロント側で、バックのAPIの呼び出し結果(name)が表示できていることがわかります。

動作確認(Heroku)

続いてHeroku側の確認です。

# バックcd ~/git/nodejs-template-back/
heroku open

# フロントcd ~/git/nodejs-template-front/
heroku open

SnapCrab_NoName_2019-12-27_17-24-2_No-00.png

Heroku上のバックAPIを呼び出せていることがわかります。

最後に

最低限動くところまでを作ることができました。ソースは以下に置いています。
https://github.com/uemuram/nodejs-template-back
https://github.com/uemuram/nodejs-template-front

DB処理やAPIの認証、各種ライブラリはここから追加していきます。

初心者がGitHubでJavaScriptのコードを書き換えるまでにやったこと

$
0
0

笹澤さんのところでプログラミング開発の練習をさせていただいている、茨城大学の浅野です。

GitHubなどは全くの初心者であった自分が、GitHubでJavaScriptのコードを書き換えるまでに至った流れを書いておきます。

可能な限りGitHub初心者の人にも伝わる言葉で書くことを目標に頑張ります。

1. そもそもGitHubって何って話から

GitHub(ギットハブ)って何だろうというところから始めます。

GitHubとは...

「GitのHub」です。

怒らないでください。

でもこの認識結構大事みたいで、GitとGitHubってどうやら違うみたいなんです。

1.1. Gitとは「みんなで編集しやすいシステム」

仕切り直して、Gitって何だろうというところから始めます。

Gitとは「みんなで一つのファイルを編集しやすいようにしてくれるシステム」のことです。

ファイルでは説明しづらいので、みんなで小説を書くことを考えてみます。

1.2. 小説をみんなで書くことで考えるGit

Aさん、Bさん、Cさんの3人で一つの小説を書きあげることを考えます。

Aさんが導入の部分を書き上げたとします。

Aさんは「導入」と書かれた封筒に導入の原稿を入れておきました。

次の日にBさんが導入を少し書き直して、自分が書いた本文の原稿と一緒に封筒の中に入れておきました。Aさんが書いた導入は、Bさんによって書き直されてしまったので、もう読むことはできません。

Cさんがその封筒を見たとき、どう見えるでしょうか。

「Aさんが作った導入と書かれた封筒」の中に「Aさんが書き、Bさんが書き直した導入」と「Bさんが書いた本文」が入っていることは、なかなか伝わりにくいでしょう。

また、Aさんが書いた導入を復元することはBさんにしかできません。どこを書き換えたのかを知っているのはBさんだけだからです。

このような「集団で一つの文書を編集する」ことに対して、「誰が何をいつしたのか」を記録しつつ、取り組みやすくしてくれるのがGitという仕組みなのです。

1.3. GitHubとは

改めてGitHubとは何かというと...

GitのHubなわけです。

Hubは「ハブ空港」とか「ハブ配線」とかいうときのハブです。

ハブは中心地とか結節点とかの意味ですね。

いろんな人たちのGitが集まっているWebサービスが「GitHub」なわけです。

2. 開発環境を導入する

それでは早速GitHubを利用するときに使うものをインストールなどしていきます。

2.1. 使うもの

1.Node.js

Javascript プログラムを開発するのに必須なプログラムです。
https://nodejs.org/ja/

2.Git

ソースコードを管理するのに必要なプログラムです。先述したGitという仕組みを利用するのに必要なものです。
https://git-scm.com/

3.Visual Studio Code

ソースコードを書くエディタです。プログラミングを書く専用のメモ帳のようなものです。
https://code.visualstudio.com/

4.Sourcetree

必ず必要ではありませんが、2のGitはそのままでは使いにくいので、簡単に作業を行うためのソフトです。(個人的には必須。)
https://www.sourcetreeapp.com/

3. 実際にGitで作業する

さて、上記のソフトを用いてGitHubにあるJavaScriptのコードを書き換えてみたいと思います。

先に断っておくと、詳細な工程ではありません。あくまで「初心者がGitHubでJavaScriptのコードを書き換えるまでにやったこと」であることに注意です。

作業の手順

1.コードをGitHubから自分のパソコン内に複製する

スクリーンショット 2019-12-27 17.43.52.png

①GitHubの「clone or download」のタブでURLをコピー
②コマンドプロンプトで「git clone -b develop ①のURL」と実行

これで自分のパソコン内に複製されます。

2.自分のパソコン内でコードを書き換えてみる

①Visual Studio Codeで先ほど複製したフォルダを開く
②フォルダ内のコードを書き換えてみる

これで自分のパソコン内のコードを書き換えました。

3.一時保存する

スクリーンショット 2019-12-27 17.54.27.png

①Sourcetreeで自分のパソコン内のフォルダをAddする
→Addすると、変更が加えられたときに「やらないといけないリスト」みたいな感じでまとめてくれます。

4.変更した内容はどんな内容なのか記録する

スクリーンショット 2019-12-27 17.56.07.png

①作業ツリーの中からインデックスに追加(+ボタン)
②下の空欄に変更内容を書いて、コミットする

5.自分のパソコン内のコードでGitHubのコードを置き換える

①プッシュタブから、GitHubのコードを置き換える。

これで一連の流れは終わりです。

4. さいごに

書いてみると全然分かりやすく書けなかったです。(笑)

でもこうして詳しいことがわからなくても、JavaScriptのコードをGitHubというWebサービスを通じて書き換えることができると、「分からないことが分かるようになる」と思います。

それであれば、あとは調べたり勉強するだけです。

環境の導入など、最初の第一歩の手助けになれば幸いです。

docker-lambda - AWS Lambda の開発効率を爆アゲする

$
0
0

要約

  • AWS Lambda の Function を実装するときは docker-lambdaを使用すると開発スピードが上がる。
  • Docker コンテナが提供されているので、導入に手間がない。
  • AWS Lambda の Function 実行だけでなく、 Deploy package の作成にも便利。

はじめに

AWS Lambda を使用することが増えてきたのですが、イチイチ AWS アカウントの設定や、 AWS Lambda へのデプロイが手間だと感じていました。そこで、ローカル開発環境上で実行できる方法を探したところ、 docker-lambdaなるものの存在を知ったのでお試ししてみました。

概要

本記事の概要は次の通りです。

書くこと

  • docker-lambda の導入方法
  • docker-lambda の実行方法(Node.js版)
  • AWS Lambda へのデプロイ用パッケージを作成する方法

書かないこと

  • docker 環境の構築方法
  • Node.js や npm のインストール方法

想定環境

この記事で実現するシステムの構成イメージは下図の通りです。
今回は Node.js を使用して実行することを想定します。

image.png

Web Application を実装する際は、コンテナ内に VS Code Server を配置したいなと考えているのですが、今回は コンテナの特徴上、コンテナが稼働するOS上での実装の方が都合が良さそうなのでこの構成にしました。

docker-lambda の使用によって実現できること

docker-lambda とは

ItemContent
CopyrightMichael Hart and LambCI contributors
GitHublambci/docker-lambda
LICENSEMIT License
What you Can- Run Code on Docker Container
- Build and Deploy Code to AWS Lambda
Mode- a single execution (Default)
- an API server that listens for invoke events

実現できること

docker-lambda を使用することによって、特に次のようなことが便利になります。

  • AWS アカウントがなくても AWS Lambda Function をお試し実行できる。
  • AWS環境へと デプロイする手間なくお試し実行できる。

ローカル環境でデバッグと実行できるのが助かりますね。

docker-lambda を試す

0. 基盤構成

この記事で実現している構成の基本的な要素は以下の記事で解説しています。

1. 実行コードの準備

サンプルコードは、以下の通りです。

index.js
exports.handler=async(event,context)=>{// process 関連の情報を色々出力console.log("process.execPath: ",process.execPath)console.log("process.execArgv: ",process.execArgv)console.log("process.argv: ",process.argv)console.log("process.cwd(): ",process.cwd())console.log("process.mainModule.filename: ",process.mainModule.filename)console.log("__filename: ",__filename)console.log("process.env: ",process.env)console.log("process.getuid(): ",process.getuid())console.log("process.getgid(): ",process.getgid())console.log("process.geteuid(): ",process.geteuid())console.log("process.getegid(): ",process.getegid())console.log("process.getgroups(): ",process.getgroups())console.log("process.umask(): ",process.umask())// 引数の値を出力console.log("event: ",event)console.log("context: ",context)context.callbackWaitsForEmptyEventLoop=false// 実行時間console.log("context.getRemainingTimeInMillis(): ",context.getRemainingTimeInMillis())returncontext.logStreamName}

2. docker-lambda を使用して実行

実行方法の詳細については 「lambci/docker-lambda」 を参照頂くとして、 Lambda 関数を実行するための基本構文は以下の通りです。

command(bash@GuestOS)
# # Run a command in a new container# # --rm          Automatically remove the container when it exits# --volume , -v     Bind mount a volume# # (unzipped) lambda code at /var/task# (unzipped) layer code at /opt,# # [<handler>] : if Node.js -> handler = file_name.function_name#   ex. Filename: index.js, Function nama: handler#         -> index.handler## 実行完了   : https://github.com/lambci/docker-lambda#docker-tags# 環境変数   : https://github.com/lambci/docker-lambda#environment-variables# 
docker run --rm\-v<code_dir>:/var/task:ro,delegated \[-v<layer_dir>:/opt:ro,delegated] \
  lambci/lambda:<runtime> \[<handler>] [<event>]

1.) で作成したコードを試す場合のコマンドは次の通りです。

command(bash@GuestOS)
## 成功すると、色々と出力されます。#
docker run --rm\-v"$PWD":/var/task \
  lambci/lambda:nodejs12.x \
  index.handler '{"runtime": "nodejs12.x"}'

以上のように、lambci/lambdaに、コードの値やオプションを適切に引き渡すことで、 Docker コンテナ上で AWS Lambda の機能を実行し、結果を取得することが出来ます。

手間も少なく、非常に便利ですね。

デプロイ パッケージの作成

docker-lambdaを使用することで、 AWS Lambda へデプロイするパッケージファイルを作成することが出来ます。

以降はデプロイ・パッケージの作成方法を記載します。

1. package.json の作成

AWS Lambda でアップロードする package をゼロから作成してゆきます。

command(bash@GuestOS)
# Create Project Directory$ mkdir docker-lambda-deploy-package &&cd$_# (Global) Install npm-add-dependencies# これはやっても、やらなくても OK です。$ sudo npm install-g npm-add-dependencies

# Create New Project (package.json)$ npm init
This utility will walk you through creating a package.json file.
It only covers the most common items, and tries to guess sensible defaults.

See `npm help json`for definitive documentation on these fields
and exactly what they do.

Use `npm install<pkg>` afterwards to install a package and
save it as a dependency in the package.json file.

Press ^C at any time to quit.
package name: (docker-lambda-deploy-package)[Enter]
version: (1.0.0)[Enter]
description:   [Enter]
entry point: (index.js)[Enter]
test command:   [Enter]
git repository:   [Enter]
keywords:   [Enter]
author:   [Enter]
license: (ISC)[Enter]
About to write to /vagrant/workspace/private/docker-lambda-deploy-package/package.json:

{"name": "docker-lambda-deploy-package",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {"test": "echo \"Error: no test specified\"&& exit 1"},
  "author": "",
  "license": "ISC"}


Is this OK? (yes)[Enter]

# install axios$ npm-add-dependencies axios
This script adds dependencies (latest or specified versions) to the package.json file without installing them
Adding packages to 'dependencies'...
Processed: axios, latest version: 0.19.0
Done.

2. index.js の作成

メインの関数を作成します。

index.js
constaxios=require("axios");exports.handler=async(event,context)=>{constres=awaitaxios.get("http://dummy.restapiexample.com/api/v1/employee/1");console.log(res.data);returnres.data;}

3. デプロイ用 Docker Image の作成と Build 、 package 作成

ビルド及びデプロイ用の Docker Image を作成します。
正常にビルド出来たらコンテナを起動し、パッケージを作成します。

command(bash@GuestOS)
# Create New Dockerfile for Build$ cat<<EOF> Dockerfile 
FROM lambci/lambda:build-nodejs12.x
ENV LANG C.UTF-8
ENV AWS_DEFAULT_REGION ap-northeast-1

COPY . .

CMD npm install
CMD zip -r9 deploy_package.zip .
EOF

## Build Container# --tag , -t        Name and optionally a tag in the ‘name:tag’ format#$ docker build \-t aws-lambda-nodejs12.x-deploy-package \.# Create Deploy Package.$ docker run --rm\-v"$PWD":/var/task \
  aws-lambda-nodejs12.x-deploy-package:latest

# 作成ファイルの確認$ ls
deploy_package.zip  Dockerfile  index.js  package.json

4. Lambda Function の作成|更新

ここまでくれば、あとは CLI なり AWS Web Console なりで Lambda Function を作成すれば完成です。

おわりに

AWS Lambda の実行とパッケージングをスムーズにローカル環境で実行することが出来るので、開発効率が大きく改善しそうです。

参考

Node.js Gmail送信

$
0
0

はじめに

Gmailアカウントでメール送信をしたい場合があります。
gmail-sendを利用すると便利です。

gmail-sendをインストール

npm install --save gmail-send

送信アカウントの設定

constsend=require('gmail-send')({user:'xxx@gmail.com',pass:'xxxアカウントのパスワード',to:'taro@yahoo.co.jp',subject:'Gmailからのメール送信確認の件',});

テキストメールを送信する

send({text:'●●様 \nこちらは2行目です。\n',},(error,result,fullResult)=>{if(error)console.error(error);console.log(result);})

HTML形式メールを送信する

send({html:'●●様 <br/> こちらは2行目です。<br/>',},(error,result,fullResult)=>{if(error)console.error(error);console.log(result);})

ファイル添付などの書き方はGithubのサイトで確認できます。
https://github.com/alykoshin/gmail-send

Gmailのセキュリティ変更

Gmailのアカウントはソースからの送信は許可しないと、エラーになります。
image.png

安全性の低いアプリのアクセスにオフの場合はオンにする必要があります。
image.png

これでエラーが回避できますので、パスワードの管理は十分注意が必要です。
またGmail送信アカウントのパスワードを変更した場合、ソース側のパスワードも忘れずに設定する必要があります。

AWSの場合セキュリティグループの設定

Gmailアカウントで送信したい場合、456ポートを許可する必要があります。

参考URL:https://github.com/alykoshin/gmail-send

以上

AWS LambdaとServerless Frameworkで爆速で作るTwitterbot

$
0
0

0. はじめに

ここ1年はStackstormばかり扱っているのですが、年末だし他の技術も触るかー!と思いたち、色々自分の作業ディレクトリを漁っていたところ、Twitterbotなるものを発掘しました。

Stackstorm???という方はこちらをご参照ください。(自演)
Dockerで始めるStackstorm再入門1/3(環境構築からOrquestaで書いたWorkflowの結果をslackに通知するところまでのチュートリアル)

話を戻します。
そのTwitterbotですが、私はvpsを使って運用していました。
ただ、そんなに頻繁に動かさないので、また勉強も兼ねて、このたびAWS Lambda(以下、lambda)移行にチャレンジした次第です。

lambdaってなに?という方は、AWSがオフィシャルなハンズオンを公開しているので、そちらをご参照ください。

本記事では、サンプルコードも用意しているので、ぜひお試しください。

1. 目次

  • 2. 環境/バージョン情報
  • 3. Serverless Frameworkとは
  • 4. ローカル開発環境の準備
  • 5. serverless.ymlを編集
  • 6. lambdaの実行ファイルを編集
  • 7. lambdaをローカルから実行
  • 8. lambdaをデプロイ
  • 9. 参考

2.環境/バージョン情報

ローカル開発環境

  • Ubuntu: 19.04 (Disco Dingo)
  • npm: 6.13.4
  • Python: 3.7.3

    • tweepy: 3.8.0
    • oauthlib: 3.1.0
    • requests: 2.22.0
    • requests-oauthlib: 1.3.0他
  • serverless

    • Framework Core: 1.60.4
    • Plugin: 3.2.6
    • SDK: 2.2.1
    • Components Core: 1.1.2
    • Components CLI: 1.4.0

AWS Lambda

  • python3.7

3. Serverless Frameworkとは

そもそもServerless Frameworkとはなんなのか。
公式ドキュメントでは、このように紹介されています。

The Serverless Framework consists of an open source CLI that makes it easy to develop, deploy and test serverless apps across different cloud providers, as well as a hosted Dashboard that includes features designed to further simplify serverless development, deployment, and testing, and enable you to easily secure and monitor your serverless apps.

参考: Serverless Framework Documentation

個人的に感じた特徴は以下のとおりです。

  • node.js製フレームワーク
  • ローカルで開発したファンクションをserverlessコマンドを使って任意のプロバイダープラットフォーム(AWS, GCPなど)にデプロイ
  • デプロイの設定はserverless.ymlで定義

4. ローカル開発環境の準備

  • サンプルリポジトリをクローン
  • AWSクレデンシャルの設定
  • Serverless Frameworkのインストールとプロジェクトの作成
  • プラグインのインストール

サンプルリポジトリをクローン

gkz@localhost ~$ git clone https://github.com/gkzz/lambda_twbot.git \
&& cd lambda_twbot

AWSクレデンシャルの設定

gkz@localhost ~$ aws configure
~/.aws/config
[default]
region = ap-northeast-1
output = json
~/.aws/credentials
[default]
aws_access_key_id = xxxxxxxxxxx
aws_secret_access_key = yyyyyyyyyyyyy

Serverless Frameworkのインストールとプロジェクトの作成

gkz@localhost ~$ sudo npm install -g serverless
gkz@localhost ~$ serverless create \
> --template aws-python3 \
> --name src \
> --path src
Serverless: Generating boilerplate...

(略)

 _______                             __
|   _   .-----.----.--.--.-----.----|  .-----.-----.-----.
|   |___|  -__|   _|  |  |  -__|   _|  |  -__|__ --|__ --|
|____   |_____|__|  \___/|_____|__| |__|_____|_____|_____|
|   |   |             The Serverless Application Framework
|       |                           serverless.com, v1.60.4
 -------'

Serverless: Successfully generated boilerplate for template: "aws-python3"

プラグインのインストール

今回は以下の3つのプラグインをインストールしました。

プラグインの名称目的
serverless-python-requirementsライブラリをインストールするため
serverless-dotenv-plugin環境構築を読み込むため
serverless-offlineローカル開発環境でlambdaを実行するため

参考までにserverless-python-requirementsをインストールした際のコマンドを貼りますが、他の2つも同様にインストールしています。

gkz@localhost ~$ sudo npm install --save serverless-python-requirements

プロジェクト構成

serverless,ymlを編集する前にディレクトリ構成を確認しておきましょう。
特に確認したい点は以下の2点です。

  • serverless.ymlからみた.envの配置場所です。
    • Twitterのtokenは./config/.envから読み取ります。
  • requirements.txtはserverless.ymlと同じ階層に配置します。
gkz@localhost ~$ cat src/handler.py.tmpl > src/handler.py
gkz@localhost ~$ cat config/.env.tmpl > config/.env
gkz@localhost ~$ tree -L 2
.
└── src                    # プロジェクトフォルダ
    ├── 37                 # python -m venv $nameで作ったpython3.7の仮想環境の名称
    ├── config             # .envなど変数が記載されたファイルの親ディレクトリ
    ├── handler.py         # lambdaの実行関数
    ├── node_modules
    ├── package.json
    ├── package-lock.json
    ├── __pycache__
    ├── requirements.txt   # tweepyなど必要なライブラリが記載
    ├── serverless.yml     # lambdaをローカル開発環境からデプロイするのに使う
    └── serverless.yml.org # プロジェクトフォルダを作る際に生成されたserverless.ymlのサンプルファイル

5 directories, 6 files

5. serverless.ymlを編集

/lambda_twbot/serverless.yml
service:srccustom:dotenv:basePath:./config/# ./lambda_twbot/src/.config/stage:${env:STAGE}# basepath/.envから環境変数STAGEを読み込むregion:${env:REGION} # 同様に環境変数REGIONを読み込むpythonRequirements:dockerizePip:non-linuxprovider:name:awsruntime:python3.7stage:${self:custom.stage}region:${self:custom.region}plugins:-serverless-python-requirements-serverless-dotenv-plugin-serverless-offlinefunctions:rtweet:handler:handler.rtweet# $filename.$function(handler.pyのrtweet関数を実行)memorySize:256timeout:90sevents:-schedule:cron(0/20 * * * ? *)# 毎日20分おきfav:handler:handler.favmemorySize:256timeout:90sevents:-schedule:cron(0/600 * * * ? *) # 毎日600分おき

6. lambdaの実行ファイルを編集

/lambda_twbot/src/handler.py
# coding: UTF-8
try:importunzip_requirementsexceptImportError:passimporttweepyimporttimeimportosimportrandomimporttracebackdef_set_token():""" set twitter token """CK=os.environ['CONSUMER_KEY']CS=os.environ['CONSUMER_SECRET']AT=os.environ['ACCESS_TOKEN']AS=os.environ['ACCESS_TOKEN_SECRET']auth=tweepy.OAuthHandler(CK,CS)auth.set_access_token(AT,AS)returntweepy.API(auth)def_find_tweet(token):found=[]kws=['#python 本','#lambda 楽しい',]forkwinkws:fortweetintweepy.Cursor(token.search,kw).items(5):found.append(tweet)returnfounddefrtweet(event,context):""" entry point of rtweet """counter=0token=_set_token()tweets=_find_tweet(token)fortweetintweets:try:#print('\nRetweet Bot found tweet by @' + tweet.user.screen_name + '. ' + 'Attempting to retweet.')
tweet.retweet()logger.info(tweet.retweet())counter+=1#print('Retweet published successfully.')
# Where sleep(10), sleep is measured in seconds.
# Change 10 to amount of seconds you want to have in-between retweets.
# Read Twitter's rules on automation. Don't spam!
time.sleep(random.randint(2,5))# Some basic error handling. Will print out why retweet failed, into your terminal.
excepttweepy.TweepErroraserror:print('''Retweet not successful. 
            Reason: {r}
            '''.format(r=error.reason))exceptStopIteration:print(traceback.format_exc())breakreturn"RtweetConts: {num}".format(num=counter)deffav(event,context):""" entry point of fav """counter=0token=_set_token()tweets=_find_tweet(token)fortweetintweets:try:tweet.favorite()# Where sleep(10), sleep is measured in seconds.
# Change 10 to amount of seconds you want to have in-between retweets.
# Read Twitter's rules on automation. Don't spam!
time.sleep(random.randint(2,5))# Some basic error handling. Will print out why retweet failed, into your terminal.
excepttweepy.TweepErroraserror:print('''Fav not successful. 
            Reason: {r}
            '''.format(r=error.reason))exceptStopIteration:print(traceback.format_exc())breakreturn"FavConts: {num}".format(num=counter)#if __name__ == "__main__":
#    rtweet('', '')
/lambda_twbot/src/requirements.txt
certifi==2019.11.28
chardet==3.0.4
idna==2.8
oauthlib==3.1.0
PySocks==1.7.1
requests==2.22.0
requests-oauthlib==1.3.0
six==1.13.0
tweepy==3.8.0
urllib3==1.25.7

7. lambdaをローカルから実行

実行する前にrequirements.txtからライブラリをインストールすることと、-fあるいは-functionで実行関数を指定することを忘れないでください。

gkz@localhost ~$ python3.7 -m venv 37 && \
> source 37/bin/activate \
> pip install -r requirements.txt 
gkz@localhost ~$ serverless invoke local --function rtweet
gkz@localhost ~$ serverless invoke local --f fav

8. lambdaをデプロイ

デプロイは以下のコマンドで実行できます。

gkz@localhost ~$ serverless deploy -v
gkz@localhost ~$ sls deploy -v

ファイルを編集した際には一度削除してから改めてデプロイします。
やりかたはdeployをremoveに変えるだけです。

gkz@localhost ~$ serverless remove
gkz@localhost ~$ sls remove

9. 参考

P.S. Twitterもやってるのでフォローしていただけると泣いて喜びます:)

@gkzvoice

Python-shellの使い方

$
0
0

1.目的

Node.js上で動作するJavascripコードからPythonコードを呼び出す。またPythonコードの出力をJavascriptコードが受け取り、受け取ったデータをJavascript上で処理したい。

1.JPG

2.手段

Python-shellを使います。Python-shellのソースはこちら

3.環境設定、インストール

Python-shellを使うための事前準備やインストール方法を説明します。
node.jsとpythonは既にインストール済とします。

環境
・ubuntu 18.04 on Raspberry pi4 (Docker)
・node.js v12.14.0
・npm 6.13.4

3-1 インストール方法

npmを使用してpython-shellをインストールする。

root@24f85fb6fd69:/home# npm install python-shell
npm WARN home@1.0.0 No description
npm WARN home@1.0.0 No repository field.

+ python-shell@1.0.8
updated 1 package and audited 103 packages in 2.37s
found 0 vulnerabilities

3-2 使用例

ディレクトリTESTを作り、下記のコードが記載されたtest.jsとsample.pyを配置する。

2.JPG

/home/pi/test/test.js
var{PythonShell}=require('python-shell');//PythonShellのインスタンスpyshellを作成する。jsから呼ぶ出すpythonファイル名は'sample.py'varpyshell=newPythonShell('sample.py');//jsからpythonコードに'aaa'を入力データとして提供する pyshell.send(5);//pythonコード実施後にpythonからjsにデータが引き渡される。//pythonに引き渡されるデータは「data」に格納される。pyshell.on('message',function(data){console.log(data);});
/home/pi/test/sample.py
importsysdata=sys.stdin.readline()#標準入力からデータを取得する
num=int(data)defsum(a):returna+3print(sum(num))

3-3 実行

/home/pi/test/
root@24f85fb6fd69:/home/test# node test.js  //test.jsを実行する
8//pythonで処理をされた結果がjavascriptに引き渡され実行される。

3.JPG

Firebase開発環境構築

$
0
0

久々に個人アプリを作ろうと思い、BEにFirebaseを使ってみようと一から構築したのでその備忘録です。
関数の実行するのにデプロイにて確認するやり方が多く、ローカルで確認する方法がなかなか見つからなかったので、そこもまとめてみました。

Node.jsインストール

// Node.jsのバージョン管理にnodebrewを導入する
$ brew install nodebrew
$ echo "export PATH=$HOME/.nodebrew/current/bin:$PATH" >> ~/.bash_profile
$ source ~/.bash_profile
$ which nodebrew
/usr/local/bin/nodebrew
$ nodebrew setup
Fetching nodebrew...
Installed nodebrew in $HOME/.nodebrew

========================================
Export a path to nodebrew:

export PATH=$HOME/.nodebrew/current/bin:$PATH
========================================

// インストール可能なバージョンを確認
$ nodebrew ls-all
remote:
v0.0.1    v0.0.2    v0.0.3    v0.0.4    v0.0.5    v0.0.6 
...

$ nodebrew install-binary v8.17.0
Fetching: https://nodejs.org/dist/v8.17.0/node-v8.17.0-darwin-x64.tar.gz
######################################################################## 100.0%
Installed successfully

// インストール済みのnodeのリストを確認
$ nodebrew ls
v8.17.0
v10.18.0
v12.14.0

current: v10.18.0

$ nodebrew use v8.17.0
use v8.17.0

$ node -v
v8.17.0

FirebaseのNode.jsはv8をベースにしているとのことでしたので、今回はv8を入れて始めることにしました。

Node.js 10 ランタイムは現在ベータ版です。Node 8 に関数をデプロイするには firebase-tools 4.0.0 以降が必要です。安定していて async/await 構文がサポートされている Node 8 を使用することを強くおすすめします。

https://firebase.google.com/docs/functions/manage-functions?hl=ja#set_nodejs_version

IDEインストール

今回開発に当たってIDEはVSCodeを使うことにします。非常に軽量ですが、機能も十分なので、簡単なシステム構築時に使っていました。
以下から安定板をインストール。
https://code.visualstudio.com/

Firebaseインストール

まず予めFirebaseのWebコンソールにてプロジェクトを作成しておきます。
参考: https://www.sejuku.net/blog/86468

$ npm install firebase-functions@latest firebase-admin@latest --save
$ npm install -g firebase-tools
// プロジェクトを配置するディレクトリ作成
$ mkdir test
$ cd test

Firebaseのプロジェクト設定

$ firebase login
// WebブラウザのURLが表示されるので、それを踏んでWeb上からログインを完了させる。
$ firebase init

     ######## #### ########  ######## ########     ###     ######  ########
     ##        ##  ##     ## ##       ##     ##  ##   ##  ##       ##
     ######    ##  ########  ######   ########  #########  ######  ######
     ##        ##  ##    ##  ##       ##     ## ##     ##       ## ##
     ##       #### ##     ## ######## ########  ##     ##  ######  ########

? Which Firebase CLI features do you want to set up for this folder? Press Space to select features, the
n Enter to confirm your choices. 
 ◯ Database: Deploy Firebase Realtime Database Rules
 ◯ Firestore: Deploy rules and create indexes for Firestore
 ◉ Functions: Configure and deploy Cloud Functions
❯◉ Hosting: Configure and deploy Firebase Hosting sites
 ◯ Storage: Deploy Cloud Storage security rules
 ◯ Emulators: Set up local emulators for Firebase features
// 今回はホスティングとサーバ上でNode.jsを動かすためのが目的なので上記2つをチェック

? Please select an option:
❯ Use an existing project 
  Create a new project 
  Add Firebase to an existing Google Cloud Platform project 
  Don't set up a default project 
// すでにWebコンソール上にあるプロジェクトで始めるため「Use an existing project」を選択。

? Select a default Firebase project for this directory: (Use arrow keys)
❯ backend-test-218d8 (backend-test) 
...

? What language would you like to use to write Cloud Functions? 
  JavaScript 
❯ TypeScript 
// サーバーサイドで使う言語選択。

? Do you want to use TSLint to catch probable bugs and enforce style? (Y/n) y
// TSLintを使うかどうか。使った方がいいかと。

? Do you want to install dependencies with npm now? (Y/n) y
// 関連するNodeモジュールをインストールするか聞かれるのでYES。

? What do you want to use as your public directory? (public)
// 公開用のディレクトリを聞かれるが、特に変更がなければそのままEnter

? Configure as a single-page app (rewrite all urls to /index.html)?
// SPAの設定が聞かれますが、特に必要がなければそのままEnter(デフォルトNo)

✔  Firebase initialization complete!
// 完了。お疲れ様でした。

functionsにてHello Worldを表示する

VS Codeにて作ったプロジェクトを開き、functionsフォルダにあるindex.tsを開きます。

import*asfunctionsfrom'firebase-functions';// // Start writing Firebase Functions// // https://firebase.google.com/docs/functions/typescript//exportconsthelloWorld=functions.https.onRequest((request,response)=>{response.send("Hello from Firebase!");});

コメントアウトされている部分を解除します。(VS CodeのショートカットはCommand + /)
※ Command + Sで保存を忘れずに。
TypeScriptはトランスパイルといってtsファイルからjsファイルへの変換が必要です。
このトランスパイルとサーバの起動を同時に行うスクリプトがpackage.jsonにすでに用意されているので、それを実行します。

"scripts": {
    "lint": "tslint --project tsconfig.json",
    "build": "tsc",
    "serve": "npm run build && firebase serve --only functions",
$ pwd
/Users/hoge/Fuga/firebase/test/functions 
$ npm run serve

> functions@ serve /Users/hoge/Fuga/firebase/test/functions
> npm run build && firebase serve --only functions


> functions@ build /Users/hoge/Fuga/firebase/test2/functions
> tsc

✔  functions: Using node@8 from host.
✔  functions: Emulator started at http://localhost:5000
i  functions: Watching "/Users/hoge/Fuga/firebase/test2/functions" for Cloud Functions...
✔  functions[helloWorld]: http function initialized (http://localhost:5000/backend-test-218d8/us-central1/helloWorld).

最後に出力されるURLにアクセスすれば、関数が実行されWeb上に「Hello from Firebase!」と出力されます。


yarn installで"The engine "node" is incompatible with this module"というエラーが発生した件

$
0
0

yarnをinstallしようとしたら、以下のエラーが発生しました

error get-caller-file@2.0.5: The engine "node" is incompatible with this module.
Expected version "6.* || 8.* || >= 10.*". Got "9.5.0"
error Found incompatible module.
info Visit https://yarnpkg.com/en/docs/cli/install for documentation about this command.

環境

Ruby: 2.6.3
Rails: 5.2.3

メッセージを和訳してみた

「バージョンは、6.* または8.* または10.*以上がいいんだけど、あなたは9.5.0だったよ」
てなとこかな?

解決策

nodeのバージョンを確認

$ node -v
v9.5.0

やっぱし。

インストール済みのnodeのバージョンを確認

$ nvm ls
      v8.0.0
      v8.9.0
      v8.11.1
      v8.16.0
->    v9.5.0
      v9.10.0
       system
default  v9.5.0

9.5.0以外にもインストールされてた。

インストールできるバージョン一覧を表示させる

$ nvm ls-remote
        v0.1.14
        v0.1.15
        v0.1.16
// 省略
        v13.3.0
        v13.4.0
        v13.5.0

めちゃめちゃたくさん出てきた。

特定のバージョンをインストールする

今回は10.0.0にします。

$ nvm install 10.0.0
Downloading and installing node v10.0.0...
Downloading https://nodejs.org/dist/v10.0.0/node-v10.0.0-darwin-x64.tar.xz...
######################################################################## 100.0%
Computing checksum with shasum -a 256
Checksums matched!
Now using node v10.0.0 (npm v5.6.0)

バージョンを確認

$ node -v
v9.5.0

まだ変わってなかった。

バージョンを固定する

$ nvm use 10.0.0
Now using node v10.0.0 (npm v5.6.0)

再度、バージョンを確認する

$ node -v
v10.0.0

バージョンを変更できた。

yarnをインストールする

$ yarn install

無事にインストール成功!

ReferenceError: fetch is not definedで困ったときに読む記事

$
0
0

少年は困っていた。

node-fetch がfetchできない。

ReferenceError: fetch is not defined
    at Client.request (/var/task/node_modules/amazon-cognito-identity-js/lib/Client.js:55:3)
    at /var/task/node_modules/amazon-cognito-identity-js/lib/CognitoUser.js:333:18
    at AuthenticationHelper.getLargeAValue (/var/task/node_modules/amazon-cognito-identity-js/lib/AuthenticationHelper.js:96:4)
    at CognitoUser.authenticateUserDefaultAuth (/var/task/node_modules/amazon-cognito-identity-js/lib/CognitoUser.js:309:24)
    at CognitoUser.authenticateUser (/var/task/node_modules/amazon-cognito-identity-js/lib/CognitoUser.js:270:16)
    at /var/task/dist/index.js:69:21
    at new Promise (<anonymous>)
    at fetchCredentials (/var/task/dist/index.js:68:12)
    at cognitoProvider (/var/task/dist/index.js:65:18)
    at async Runtime.exports.handler (/var/task/dist/index.js:34:31)

環境

  • TypeScript 3.7
  • Node.js 12.13.1
  • AWS Lambda

やろうとしていたこと

エンドユーザーがHTTP通信を介してLambdaに認証情報を送り、それをバックエンドAPIで独自認証し、CognitoIdPoolから得たCredentials情報を返すLambdaを作成していた。

初期構築時も同じ問題に詰まったが、Module内に書き込むことでなんとか問題を解決することが出来た。
しかし、 node-fetchは2度刺す。

二度 刺すっ・・・・・!

Lambdaのリポジトリをcodepipelineを用いたCI/CD環境へと移行する際に再度問題が発生。

再度同じエラーが発生するようになった。
しかしリポジトリにnode_modulesをプッシュしている人はいるのだろうか、いやいない。私もプッシュをしていないその一人だった。

あれ、これどうやって実行するんだ……?
あらゆる記事を探しまくった結果解決を得ることが出来たのでここに記しておきたい。
結局公式に丁寧に書いてくれてたんだけどね。

npmのReadmeによると

This library uses the Fetch API. For older browsers or in Node.js, you may need to include a polyfill. For example.
このライブラリではfetchAPIを利用するため、古いブラウザやNode.jsでは以下のようにpolyfillを行う必要があります。
※polifill = 隙間家具のイメージ。APIを利用するために準備が必要ですよということ。

global.fetch = require('node-fetch');
var AmazonCognitoIdentity = require('amazon-cognito-identity-js');

やってることはnode-fetchをグローバルオブジェクトとして読み込み、呼び出した場所で扱えるようにしてるってわけですね。
なるほど。
まぁJavascriptだったんで、Typescriptとして動くように書き換えます。

import Global = NodeJS.Global;
export interface GlobalWithCognitoFix extends Global {
    fetch: any
}
declare const global: GlobalWithCognitoFix;
global.fetch = require('node-fetch').default;

こんな感じでfetchAPIを使用するコードでimportしてやるとfetchAPIが機能するようになります。
ここで注意して欲しいのが、fetchAPIを含むライブラリよりも先にimportが必要なので、脳死でコードの先頭に置いておくと良いと思います。

npm - amazon-cognito-identity-js

なっがいSQLをNode.jsで生成してみた

$
0
0

背景

  • 似たような処理の繰り返しなので共通化できそう
  • でも、SQLの知識がそこまでない
  • Gitでコード管理しているが、なっがいSQLをプッシュすると、全コードにおけるSQLの割合が増えてなんか気分が悪い

なっがいSQLサンプル

SELECT箇所・LEFT JOIN箇所で多数の重複があり、見やすくするため省略しています

-- CREATE TABLE sample_tableSELECTpb.id,p.name,p.team,-- bat1IFNULL(fst.slug_ave,0)ASrate1,IFNULL(fst.pa,0)ASpa1,IFNULL(fst.ab,0)ASab1,IFNULL(fst.tb,0)AScnt1,-- bat2-- ︙-- bat3-- ︙-- bat4-- ︙-- bat5-- ︙-- bat6-- ︙-- bat7IFNULL(sev.slug_ave,0)ASrate7,IFNULL(sev.pa,0)ASpa7,IFNULL(sev.ab,0)ASab7,IFNULL(sev.tb,0)AScnt7,-- 各項目合計CASEWHEN(IFNULL(fst.ab,0)+IFNULL(scd.ab,0)+IFNULL(thr.ab,0)+IFNULL(fur.ab,0)+IFNULL(fif.ab,0)+IFNULL(six.ab,0)+IFNULL(sev.ab,0))>0THENROUND((IFNULL(fst.tb,0)+IFNULL(scd.tb,0)+IFNULL(thr.tb,0)+IFNULL(fur.tb,0)+IFNULL(fif.tb,0)+IFNULL(six.tb,0)+IFNULL(sev.tb,0))/(IFNULL(fst.ab,0)+IFNULL(scd.ab,0)+IFNULL(thr.ab,0)+IFNULL(fur.ab,0)+IFNULL(fif.ab,0)+IFNULL(six.ab,0)+IFNULL(sev.ab,0)),5)ELSENULLENDASrate,IFNULL(fst.ab,0)+IFNULL(scd.ab,0)+IFNULL(thr.ab,0)+IFNULL(fur.ab,0)+IFNULL(fif.ab,0)+IFNULL(six.ab,0)+IFNULL(sev.ab,0)ASab,IFNULL(fst.pa,0)+IFNULL(scd.pa,0)+IFNULL(thr.pa,0)+IFNULL(fur.pa,0)+IFNULL(fif.pa,0)+IFNULL(six.pa,0)+IFNULL(sev.pa,0)ASpa,IFNULL(fst.tb,0)+IFNULL(scd.tb,0)+IFNULL(thr.tb,0)+IFNULL(fur.tb,0)+IFNULL(fif.tb,0)+IFNULL(six.tb,0)+IFNULL(sev.tb,0)AScnt,'e'ASeolFROMbaseball._player_batterpbLEFTJOINplayerpONpb.id=p.id-- bat1LEFTJOIN(SELECTh.batter,COUNT(h.batterORNULL)ASpa,COUNT(eb.nameISNULLORNULL)ASab,COUNT(hi.rst_idIN(2,3,4)ORNULL)+COUNT(hi.rst_id=6ORNULL)*2+COUNT(hi.rst_id=8ORNULL)*3+COUNT(hi.rst_id=9ORNULL)*4AStb,CASEWHENCOUNT(eb.nameISNULLORNULL)>0THENROUND((COUNT(hi.rst_idIN(2,3,4)ORNULL)+COUNT(hi.rst_id=6ORNULL)*2+COUNT(hi.rst_id=8ORNULL)*3+COUNT(hi.rst_id=9ORNULL)*4)/COUNT(eb.nameISNULLORNULL),5)ELSEnullENDASslug_aveFROMbaseball._bat_all_infohLEFTJOINexclude_batting_infoebONh.`1_result`=eb.nameLEFTJOINhit_id_infohiONh.`1_rst_id`=hi.rst_idWHEREh.`1_result`ISNOTNULLGROUPBYbatter)ASfstONfst.batter=pb.id-- bat2-- ︙-- bat3-- ︙-- bat4-- ︙-- bat5-- ︙-- bat6-- ︙-- bat7LEFTJOIN(SELECTh.batter,COUNT(h.batterORNULL)ASpa,COUNT(eb.nameISNULLORNULL)ASab,COUNT(hi.rst_idIN(2,3,4)ORNULL)+COUNT(hi.rst_id=6ORNULL)*2+COUNT(hi.rst_id=8ORNULL)*3+COUNT(hi.rst_id=9ORNULL)*4AStb,CASEWHENCOUNT(eb.nameISNULLORNULL)>0THENROUND((COUNT(hi.rst_idIN(2,3,4)ORNULL)+COUNT(hi.rst_id=6ORNULL)*2+COUNT(hi.rst_id=8ORNULL)*3+COUNT(hi.rst_id=9ORNULL)*4)/COUNT(eb.nameISNULLORNULL),5)ELSEnullENDASslug_aveFROMbaseball._bat_all_infohLEFTJOINexclude_batting_infoebONh.`7_result`=eb.nameLEFTJOINhit_id_infohiONh.`7_rst_id`=hi.rst_idWHEREh.`7_result`ISNOTNULLGROUPBYbatter)ASsevONsev.batter=pb.id;

共通化できそうなポイント

  • SELECT箇所
    • rate, pa, ab, cntの4カラムについて、末尾に1~7をそれぞれ付与しただけ
    • 各項目の合計算出カラム
      • カラム名を連結するだけなのでコードで簡単に実現できそう
  • baseball._player_batterLEFT JOINを7回繰り返している箇所
    • その中のSQLについても、ほとんど同じ
    • 違うのは以下の2点
      • _bat_all_infoLEFT JOINする際の結合条件やWHERE句で指定するカラムが1_result7_resultであること
      • LEFT JOINした後のAlias (fst~sev)

共通化コード

requireしている他のコードについては省略させていただきます。

  • execute: fsモジュールでsql形式のファイルを出力する
  • getFileName: ファイルのフルパスからファイル名のみを抽出する
  • cols: カラム名を+で連結した結果の末尾3文字を削除する
// average_slugging.js"use strict";const{execute,getFilename,cols}=require("./util/func");const{BATS_COL}=require("../constants");letsql=`-- CREATE TABLE ${getFilename(__filename)}
`;// -------------------- [select part] --------------------// player_infosql+=`SELECT
  pb.id, p.name, p.team,
  `;letabCols="";letpaCols="";lettbCols="";// any info(rate, pa, ab, cnt) per inningObject.keys(BATS_COL).map(bat=>{constbatName=BATS_COL[bat];sql+=`-- bat${bat}`;sql+=`
  IFNULL(${batName}.slug_ave, 0) AS rate${bat},
  IFNULL(${batName}.pa, 0) AS pa${bat},
  IFNULL(${batName}.ab, 0) AS ab${bat},
  IFNULL(${batName}.tb, 0) AS cnt${bat},
  `;abCols+=`IFNULL(${batName}.ab, 0) + `;paCols+=`IFNULL(${batName}.pa, 0) + `;tbCols+=`IFNULL(${batName}.tb, 0) + `;});// about `total`sql+=`-- 各項目合計`;sql+=`
  CASE WHEN(${cols(abCols)}) > 0 THEN ROUND((${cols(tbCols)})/(${cols(abCols)}), 5) ELSE NULL END AS rate,
  ${cols(abCols)} AS ab,
  ${cols(paCols)} AS pa,
  ${cols(tbCols)} AS cnt,
  `;// -------------------- /[select part] --------------------sql+=`'e' AS eol
FROM baseball._player_batter pb
  LEFT JOIN player p ON pb.id = p.id`;// -------------------- [left join part] --------------------// left join part per inningObject.keys(BATS_COL).map(bat=>{constbatName=BATS_COL[bat];sql+=`-- bat${bat}`;sql+=`
  LEFT JOIN (
    SELECT 
          h.batter,
      COUNT(h.batter OR NULL) AS pa,
      COUNT(eb.name IS NULL OR NULL) AS ab,
      COUNT(hi.rst_id IN (2, 3, 4) OR NULL) + COUNT(hi.rst_id = 6 OR NULL) * 2 + COUNT(hi.rst_id = 8 OR NULL) * 3 + COUNT(hi.rst_id = 9 OR NULL) * 4 AS tb,
      CASE WHEN COUNT(eb.name IS NULL OR NULL) > 0 THEN ROUND((COUNT(hi.rst_id IN (2, 3, 4) OR NULL) + COUNT(hi.rst_id = 6 OR NULL) * 2 + COUNT(hi.rst_id = 8 OR NULL) * 3 + COUNT(hi.rst_id = 9 OR NULL) * 4) / COUNT(eb.name IS NULL OR NULL), 5) ELSE null END AS slug_ave
    FROM
        baseball._bat_all_info h
    LEFT JOIN exclude_batting_info eb ON h.\`${bat}_result\` = eb.name
    LEFT JOIN hit_id_info hi ON  h.\`${bat}_rst_id\` = hi.rst_id
    WHERE h.\`${bat}_result\` IS NOT NULL
    GROUP BY batter
  ) AS ${batName} ON ${batName}.batter = pb.id
  `;});// -------------------- /[left join part] --------------------// generateexecute(`${getFilename(__filename)}`,sql);

実行方法

$ node average_slugging.js

SQLを生成する利点

  • とにかくSQLの修正が容易になる
    • 例えば現在の5カラム(batter, pa, ab, tb, slug_ave)以外に出力したいカラムがある場合、1箇所を修正するだけでLEFT JOIN7箇所全てを反映できる
    • 7箇所全てを少しずつ修正するのは馬鹿らしい
  • 新たなSQLを生成するのにも役に立つ
    • 今回はプロ野球選手の各打席(第1打席〜第7打席)の長打率を求めるSQLでしたが、出塁率や打率などのSQLを作成する際はすぐに作成することができる
  • 生成したSQLは.gitignoreに追加することでGitでの管理が不要
    • 生成するこのコードのみ管理することで、コードにおけるSQLの内訳が減る

まとめ

よくいろんなエンジニアもおっしゃっている楽をするために苦労するということを実践してみました
今後も何かめんどくさいな、と思ったことに対して、楽に何かできることがあれば投稿します

今回Node.jsで書いたこの内容をSQLでできる方法をご存知でしたら、ぜひご教授いただきたいです
最後まで読んでくださりありがとうございました

AWS+NodeJSでサーバレスな環境構築①

$
0
0

はじめに

サーバレスは完全に未経験ですが、勉強がてら備忘録として残しておこうと思います。
表現等が正しく無い場合はご指摘いただければ、幸いです。

サーバレスってなぁに?

簡単に行ってしまうと、ユーザーがサーバー領域を意識せず、直接利用出来るサービスを活用した構成のことです。
「Lambda」は設定されているプログラムを起動させる実行環境となります。起動条件が整った際に、プログラムをLambda環境に呼び出し、実行されます。この為、Lambdaでは、実行した時間とその回数のみの課金となります。

向いているサービス
・待機時間の長いシステム
・CPUの負荷が時間帯によって差のあるシステム

不向きなサービス
・常にシステムの動いている必要のあるサービス
・高負荷な状態が長時間続くシステム

もう少し詳しいことを知りたい場合はこちらのサイトをご覧ください。
サーバーレス アーキテクチャ

Lambda関数の作成

・AWS Lambdaページ>関数の作成>一から作成
・関数名を入力、ランタイムの選択(必要であれば)
・その他はデフォルト値のままで作成ボタン

API Gatewayのトリガーを追加と設定

・「トリガーを追加」ボタンを押し、API Gatewayを選択
・APIを「新規のAPI」を選択
・その他はデフォルト値のままで追加ボタン

ソース

index.js
'use strict'letfs=require('fs')letpath=require('path')exports.handler=(event,context,callback)=>{letfilePath=path.join(__dirname,'page.html')lethtml=fs.readFileSync(filePath).toString()sendHtmlResponse(context,200,html)}functionsendHtmlResponse(context,statusCode,html){letresponse={'statusCode':statusCode,'headers':{'Content-Type':'text/html'},'body':html}context.succeed(response)}
page.html
<!DOCTYPE html><htmllang="ja"><head><metacharset="utf-8"><title>サーバレス</title></head><body><h1>やっちゃおう!サーバレスで</h1></body></html>

ページにアクセス

API Gatewayの設置したトリガーをクリックし、表示されてるURLをクリックすれば、アクセスできるはずです。
スクリーンショット 2019-12-30 17.28.03.png
遷移先のページでこのように表示されれば、成功です。
スクリーンショット 2019-12-30 18.21.51.png

終わりに

次はLambdaとdynamodbやS3と組み合わせて投稿しようかと思います。

Node.js の Async Hooks API の動作を検証しました

$
0
0

必要に迫られて、Node.js の Async Hooks API について調べたので、その仕組を実例を用いて説明します。

Async Hooks とは?

Node.js の Stability: 1 - Experimental (2019/12/30 現在) な機能です。
主に 非同期呼出を追跡するのに使われています。例えば以下の様な NPM Module が Async Hooksを使っています。

  • longjohn→ 非同期呼出で途切れる Stack trace を繋げて表示する
  • trace→ 非同期呼出で途切れる Stack trace を繋げて表示する
  • express-http-context→ リクエスト毎に異なるコンテキストに値を保存/取得する express middleware

対象読者

事前に Node.jsでのイベントループの仕組みとタイマーについてについて知っておくことをオススメします。

動作環境

本記事は以下の環境で試しています。

  • Mac OS 10.13.6
  • Node.js v12.13.0

私は nodebrewを使っています。

nodebrew binary-install v12.13.0
nodebrew use v12.13.0

コード

記事で書くコードそのものが↓です。

Async Hooksの概要

公式の Node.js >> Async Hooks >> Overviewの通りですが、実際に動かして見ないとドキュメントだけ読んでも分かりづらいです。まずはざっくり概要です。

Async Hooks自体は、以下の様に async_hooks.createHook()するだけで使えます。

constasync_hooks=require('async_hooks');// 現在 (↓が実行されてる瞬間) の Async ID を取得できます。consteid=async_hooks.executionAsyncId();// Async Hook を生成します。// ここで渡した4つの callback functions が、非同期呼び出しの際に呼ばれる様になります。constasyncHook=async_hooks.createHook({init,before,after,destroy,});// 有効にしないと callback functions が呼ばれません。asyncHook.enable();

前述の async_hooks.createHook()に渡した4つの callback functions は自由に実装できます。
今回は、確認の為に、呼出の際の引数値を Arrayに保存しておく様にしました。

// [init] setTimeout() 等の非同期処理 (callback function) の登録をした時に呼ばれます。functioninit(asyncId,type,triggerAsyncId,resource){// Promise による callback task 登録の場合、Promise オブジェクト自身が来るので、// ちゃんと GC されるように参照保持しないようにします。arguments[3]=resource.constructor.name==='PromiseWrap'?resource.toString():resource;historyInit.push(arguments);}// [before] 登録した callback function が実行される「直前」に呼ばれます。functionbefore(asyncId){historyBefore.push(asyncId);}// [after] 登録した callback function が実行された「後」に呼ばれます。functionafter(asyncId){historyAfter.push(asyncId);}// [destroy] 非同期リソースが破棄 (≒登録した非同期処理の完了) された時に呼ばれます。functiondestroy(asyncId){historyDestroy.push(asyncId);}// Async Hooks の動作確認の為に、init, before, after, destroy 呼出の際の引数値をここ↓に保存しておきます。consthistoryInit=[];consthistoryBefore=[];consthistoryAfter=[];consthistoryDestroy=[];

この状態で setTimeout()等を実行すると、 init, before, after, destroy callback functions が順に呼ばれるようになります。

init, before, after, destroy の定義

時系列

API setIntervalでの例です。

setInterval.png

init

非同期 API (setTimeout, Promise等) を実行した瞬間に呼ばれます。

別の言い方をすると、非同期処理 (callback functions) が event queue に登録された時です。

  • 1度だけ 必ず呼ばれます
引数名説明
asyncIdNumber分岐元 (親) の Async ID
typeStringTimeout, PROMISE等の識別子が来ます。resourceの名称です。
triggerAsyncIdNumber分岐元 (親) の Async ID
resourceObject実行した非同期処理の情報。Promiseの場合、promise object 自身が来るので、参照保持で GC を阻害しないように注意。

before

登録した callback function が実行される 直前に呼ばれます。

  • 一度も呼ばれない事があります例えば net.createServerで Socket listen していても、接続がなければ callback 実行されません
  • setInterval等、一度の callback 登録で before複数回呼ばれる事があります
引数名説明
asyncIdNumber分岐元 (親) の Async ID

after

登録した callback function が実行された に呼ばれます。

  • beforeと同様に、 一度も呼ばれないもしくは 複数回呼ばれる事があります
  • callback で例外が発生し catch されなかった場合、 uncaughtException event もしくは handler 実行の後に、afterが呼ばれます
引数名説明
asyncIdNumber分岐元 (親) の Async ID

destroy

非同期リソース resource が 破棄 (≒登録した非同期処理の完了) された時に 一度だけ呼ばれます。

具体的にいつ呼ばれるかはまちまちで、例えば Promiseの場合は promise object が GC により破棄された際に呼ばれます

引数名説明
asyncIdNumber分岐元 (親) の Async ID

Examples

ドキュメントを読んだだけでは、init, before, after, destroy callback functions がそれぞれ、どのタイミングで、何回呼ばれるのか、よく分かりません。

実際に実行して試してみます。

下準備

前述のコードをファイル名 register-hook.jsとし、以下の Debug Print 関数を module.exportsします。

わざと setTimeoutで表示処理を非同期実行してます。

register-hook.js
constasync_hooks=require('async_hooks');constasyncHook=async_hooks.createHook({init,before,after,destroy});asyncHook.enable();// ...// 省略...// .../**
 * async_hooks で取得した非同期呼び出しの履歴を表示します.
 */module.exports=function(){// 表示処理が呼出元とは異なる event task として実行される様、setTimeout する. (event queue に入れておく)setTimeout(functionprintHistory(){const[init,before,after,destroy]=[[...historyInit],[...historyBefore],[...historyAfter],[...historyDestroy]];console.log('FirstExecutionAsyncId: ',eid,'\n');console.log('async_hook calls: init: ',init,'\n');console.log('async_hook calls: before: ',before,'\n');console.log('async_hook calls: after: ',after,'\n');console.log('async_hook calls: destroy: ',destroy,'\n');},10);}

(1) setTimeout で Async Hooks

example-setTimeout.png

まずは Async Hooksを有効にした状態で setTimeoutを呼ぶとどうなるか見てみます。

index_setTimeout.js
constprintHistory=require('./register-hook');function_01_SetTimeoutCallbackFunction(){printHistory();// ← setTimeout 経由で console.log する.}setTimeout(_01_SetTimeoutCallbackFunction);

以下の様に printHistory()で出力されます。

FirstExecutionAsyncId:  1 

async_hook calls: init:  [
  [Arguments] {
    '0': 2,
    '1': 'Timeout',
    '2': 1,
    '3': Timeout {
      _idleTimeout: 1,
      _idlePrev: null,
      _idleNext: null,
      _idleStart: 44,
      _onTimeout: [Function: _01_SetTimeoutCallbackFunction],
      _timerArgs: undefined,
      _repeat: null,
      _destroyed: true,
      [Symbol(refed)]: null,
      [Symbol(asyncId)]: 2,
      [Symbol(triggerId)]: 1
    }
  },
  [Arguments] {
    '0': 3,
    '1': 'Timeout',
    '2': 2,
    '3': Timeout {
      _idleTimeout: 10,
      _idlePrev: null,
      _idleNext: null,
      _idleStart: 46,
      _onTimeout: [Function: printHistory],
      _timerArgs: undefined,
      _repeat: null,
      _destroyed: false,
      [Symbol(refed)]: true,
      [Symbol(asyncId)]: 3,
      [Symbol(triggerId)]: 2
    }
  }
] 

async_hook calls: before:  [ 2, 3 ] 

async_hook calls: after:  [ 2 ] 

async_hook calls: destroy:  [ 2 ] 

エントリファイルの index_setTimeout.jsが実行された直後の Async ID は 1です

initは2回呼ばれています。

1回目は setTimeout(_01_SetTimeoutCallbackFunction) (id=2) で、 index_setTimeout.jsファイル自身 (id=1) からトリガーされた非同期タスクである事が分かります。

2回目は setTimeout(printHistory) (id=3) で、先程の setTimeout(_01_SetTimeoutCallbackFunction) (id=2) からトリガーされた非同期タスクである事が分かります。

beforeは2回呼ばれています。

function _01_SetTimeoutCallbackFunction (id=2) と printHistory (id=3) が呼出されたからです

afterは1回呼ばれています。

function _01_SetTimeoutCallbackFunction (id=2) の実行は完了したが、 printHistory (id=3) はまだ途中だからです

destroyは1回呼ばれています。

function _01_SetTimeoutCallbackFunction (id=2) の破棄は完了したが、 printHistory (id=3) はまだ実行途中で破棄されてないからです

(2) setInterval で Async Hooks

example-setInterval.png

次に Async Hooksを有効にした状態で setIntervalを呼ぶとどうなるか見てみます。

index_setInterval.js
constprintHistory=require('./register-hook');letcount=0;function_01_SetIntervalCallbackFunction(){if(++count>10){clearInterval(intervalID);printHistory();// ← setTimeout 経由で console.log する.}}constintervalID=setInterval(_01_SetIntervalCallbackFunction,10);

以下の様に printHistory()で出力されます。

FirstExecutionAsyncId:  1 

async_hook calls: init:  [
  [Arguments] {
    '0': 2,
    '1': 'Timeout',
    '2': 1,
    '3': Timeout {
      _idleTimeout: -1,
      _idlePrev: null,
      _idleNext: null,
      _idleStart: 157,
      _onTimeout: null,
      _timerArgs: undefined,
      _repeat: 10,
      _destroyed: true,
      [Symbol(refed)]: null,
      [Symbol(asyncId)]: 2,
      [Symbol(triggerId)]: 1
    }
  },
  [Arguments] {
    '0': 3,
    '1': 'Timeout',
    '2': 2,
    '3': Timeout {
      _idleTimeout: 10,
      _idlePrev: null,
      _idleNext: null,
      _idleStart: 171,
      _onTimeout: [Function: printHistory],
      _timerArgs: undefined,
      _repeat: null,
      _destroyed: false,
      [Symbol(refed)]: true,
      [Symbol(asyncId)]: 3,
      [Symbol(triggerId)]: 2
    }
  }
] 

async_hook calls: before:  [
  2, 2, 2, 2, 2,
  2, 2, 2, 2, 2,
  2, 3
] 

async_hook calls: after:  [
  2, 2, 2, 2, 2,
  2, 2, 2, 2, 2,
  2
] 

async_hook calls: destroy:  [ 2 ]

initは2回呼ばれています。

1回目は setInterval(_01_SetIntervalCallbackFunction, 10) (id=2) で、 index_setInterval.jsファイル自身 (id=1) からトリガーされた非同期タスクである事が分かります。

2回目は setTimeout(printHistory) (id=3) で、先程の setTimeout(_01_SetIntervalCallbackFunction) (id=2) からトリガーされた非同期タスクである事が分かります。

beforeは11回呼ばれています。

このコードでは、1度の setInterval(_01_SetIntervalCallbackFunction, 10)で callback function _01_SetIntervalCallbackFunction (id=2) は 10 ms おきに実行され、clearInterval()されるまで 計10回呼ばれています。

また、 printHistory (id=3) も1度呼出され、カウントされます。

afterは10回呼ばれています。

beforeで述べた通り、_01_SetIntervalCallbackFunction (id=2) は 計10回実行されました。

printHistory (id=3) の処理はまだ途中の為、カウントされません。

destroyは1回呼ばれています。

clearIntervalの実行により callback function _01_SetTimeoutCallbackFunction (id=2) は破棄されます。

printHistory (id=3) はまだ実行途中で破棄されてないので、カウントされません。

(3) Promise で Async Hooks

example-promise.png

最後に Async Hooksを有効にした状態で new Promise(callback)を呼ぶとどうなるか見てみます。

index_promise.js
constprintHistory=require('./register-hook');function_01_PromiseCallbackFunction(resolve,_){resolve();}function_02_PromiseThenCallbackFunction(_){// GC で Promise オブジェクトを破棄しないと "async_hooks.destroy" callback は呼ばれない.setTimeout(global.gc);printHistory();// ← setTimeout 経由で console.log する.}newPromise(_01_PromiseCallbackFunction).then(_02_PromiseThenCallbackFunction);

以下の様に printHistory()で出力されます。

FirstExecutionAsyncId:  1 

async_hook calls: init:  [
  [Arguments] {
    '0': 2,
    '1': 'PROMISE',
    '2': 1,
    '3': '[object PromiseWrap]'
  },
  [Arguments] {
    '0': 3,
    '1': 'PROMISE',
    '2': 2,
    '3': '[object PromiseWrap]'
  },
  [Arguments] {
    '0': 4,
    '1': 'Timeout',
    '2': 3,
    '3': Timeout {
      _idleTimeout: 1,
      _idlePrev: null,
      _idleNext: null,
      _idleStart: 49,
      _onTimeout: [Function: gc],
      _timerArgs: undefined,
      _repeat: null,
      _destroyed: true,
      [Symbol(refed)]: null,
      [Symbol(asyncId)]: 4,
      [Symbol(triggerId)]: 3
    }
  },
  [Arguments] {
    '0': 5,
    '1': 'Timeout',
    '2': 3,
    '3': Timeout {
      _idleTimeout: 10,
      _idlePrev: null,
      _idleNext: null,
      _idleStart: 49,
      _onTimeout: [Function: printHistory],
      _timerArgs: undefined,
      _repeat: null,
      _destroyed: false,
      [Symbol(refed)]: true,
      [Symbol(asyncId)]: 5,
      [Symbol(triggerId)]: 3
    }
  }
] 

async_hook calls: before:  [ 3, 4, 5 ] 

async_hook calls: after:  [ 3, 4 ] 

async_hook calls: destroy:  [ 2, 3, 4 ]

ちょっとごちゃごちゃしているのは、new Promise(callback)に加え、
Promise.then()の呼出と、setTimeout(global.gc)の呼出があるからです。

initは4回呼ばれています。

1回目は new Promise(_01_PromiseCallbackFunction) (id=2) で、 index_promise.jsファイル自身 (id=1) からトリガーされた非同期タスクである事が分かります。

2回目は promise.then(_02_PromiseThenCallbackFunction) (id=3) で、 new Promise(_01_PromiseCallbackFunction) (id=2) からトリガーされた非同期タスクである事が分かります。

3回目は意図的にコード setTimeout(global.gc)を記述した為に、記録されています。

destroyで後述しますが、 Promiseの場合は GC で promise object 破棄されるまで destroyが呼出されません

4回目は setTimeout(printHistory) (id=5) で、先程の promise.then(_02_PromiseThenCallbackFunction) (id=3) からトリガーされた非同期タスクである事が分かります。

beforeは3回呼ばれています。

正直意図しない動きをしています。

  • new Promise(_01_PromiseCallbackFunction) (id=2) では before呼出されない
  • promise.then(_02_PromiseThenCallbackFunction) (id=3) では before呼出される

以下の通り、公式のドキュメントにもそれとなく書いてありますが、どうしてこういう動作になるのか理解できていません。

afterは2回呼ばれています。

beforeと同様です。

printHistory (id=5) の処理はまだ途中の為、カウントされません。

destroyは3回呼ばれています。

Promiseの場合、promise object が 破棄 (=GC)されるまで destroyは呼出されません。

本コードでは、意図的に global.gc()を実行しました。

printHistory (id=5) はまだ実行途中で破棄されてないので、カウントされません。

Viewing all 8837 articles
Browse latest View live