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

常に動くLINEBOTにお引っ越し(レンタルサーバ+PHP編)

$
0
0

今回のモチベーション

前回、こちらの記事を参考にWikipedia APIを使った、調べものLINE botを作った。

前回の記事
https://qiita.com/shima-07/items/2322598ca5a40cfee47b

だが、

  • ngrokを立ち上げている時しか使えないから普段使えない。
  • いざ、ngrokを立ち上げるとアドレスが変わってしまうため、Messaging API settingsのwebhook URLを毎度変えないと動かない。

うーん。。。

ngrok立ち上げるのめんどくさい! 常に使えるようにしないと意味ないじゃん!

と思ったわけです。だから『常に動くようにしよう!』が今回の動機です。

今回やったこと

  • 1. まずは nowを試してみた
  • 2. さくらのレンタルサーバでやることにした
  • 3. jsで書いていたものをPHPに書き直した

最終的にはPHP化してさくらサーバに載っけました。

1. まずは nowを試してみた

このあたりを参考に進めてみる。

さすが良記事!サクサク進むぜと思いながら最後までいきデプロイ完了!
簡単だったなあと思いながら、Webhook URLに入れて「Verify」をクリック・・・・

image.png

う。。まあよくある。

(1時間ほど立ち向かう)
色々と試すが私の手におえないと判断して諦める。

2. さくらのレンタルサーバでやることにした

この記事を発見!
http://blog.hetabun.com/line-bot-php-sakura

これ通りやることで、「こんにちは」に対して「こんにちは!」と元気よく返してくれるボットができました。

3. jsで書いていたものをPHPに書き直した

ここからが本番。前回あげた下記jsのコードと同じような振る舞いをPHPで書いていく。
PHPももちろん初心者である。

再掲

server.js
'use strict';constexpress=require('express');constline=require('@line/bot-sdk');constPORT=process.env.PORT||3000;// 追加constaxios=require('axios');constconfig={channelSecret:'作成したBOTのチャンネルシークレット',channelAccessToken:'作成したBOTのチャンネルアクセストークン'};constapp=express();app.get('/',(req,res)=>res.send('Hello LINE BOT!(GET)'));//ブラウザ確認用(無くても問題ない)app.post('/webhook',line.middleware(config),(req,res)=>{//ここのif文はdeveloper consoleの"接続確認"用なので後で削除して問題ないです。if(req.body.events[0].replyToken==='00000000000000000000000000000000'&&req.body.events[1].replyToken==='ffffffffffffffffffffffffffffffff'){res.send('Hello LINE BOT!(POST)');console.log('疎通確認用');return;}Promise.all(req.body.events.map(handleEvent)).then((result)=>res.json(result));});constclient=newline.Client(config);functionhandleEvent(event){if(event.type!=='message'||event.message.type!=='text'){returnPromise.resolve(null);}letmes=''// console.log(event.message.text);if(event.message.text.indexOf('')>-1){// ?を含んでいる場合にはwikiで検索したものを出して、含んでない場合はurlを返すvarstr=event.message.text;varresult=str.split('').join('');//?を取り除く処理mes=result+'の説明:';//wikiのbodyの前の一言getBody(event.source.userId,result);//wiki APIで取得できたらプッシュメッセージ}else{varresult=event.message.text;mes=result+'のURL:';//wikiのurlの前の一言getUrl(event.source.userId,result);//wiki APIで取得できたらプッシュメッセージ}returnclient.replyMessage(event.replyToken,{type:'text',text:mes});}constgetBody=async(userId,word)=>{constres=awaitaxios.get('http://wikipedia.simpleapi.net/api?keyword='+encodeURIComponent(word)+'&output=json');constitem=res.data;// console.log(item); awaitclient.pushMessage(userId,{type:'text',text:item[0].body,});}constgetUrl=async(userId,word)=>{constres=awaitaxios.get('http://wikipedia.simpleapi.net/api?keyword='+encodeURIComponent(word)+'&output=json');constitem=res.data;// console.log(item); awaitclient.pushMessage(userId,{type:'text',text:item[0].url,});}app.listen(PORT);console.log(`Server running at ${PORT}`);

 処理の整理

  • LINEからのメッセージを受け取る
  • そのメッセージをWikipedia APIに渡して結果を受け取る
  • メッセージによってLINE側に返却するものを変える
    • ?があるときはurlを返す
    • ?がないときはbodyを返す

LINEからのメッセージを受け取る

参考にした記事中にあった下記の$textで取れているからそれはOK。

LINEからのメッセージ.php
//ユーザーからのメッセージ取得$json_string=file_get_contents('php://input');$jsonObj=json_decode($json_string);$type=$jsonObj->{"events"}[0]->{"message"}->{"type"};//メッセージ取得$text=$jsonObj->{"events"}[0]->{"message"}->{"text"};//ReplyToken取得$replyToken=$jsonObj->{"events"}[0]->{"replyToken"};

そのメッセージをWikipedia APIに渡して結果を受け取る

ここが一番ハマった。
jsではres.data.item[0].bodyの構造で取れていたので、
同じノリで $value = $res->{"data"}->{"item"}[0]->{"body"};のような書き方をして、、当然何も取れず。

結論、下記のような取り方でできた。

wikipediaAPIからURLやbody取得部分.php
$keyword=mb_convert_encoding($text,"UTF-8","auto");$res=file_get_contents('http://wikipedia.simpleapi.net/api?keyword='.$keyword.'&output=json');$jsonwiki_decode=json_decode($res,true);// 0番目の物だけを抽出する。もっとたくさん抽出したいときはここを変更する$jsonwiki=$jsonwiki_decode[0];//欲しい項目だけの配列にする$wikidata=array('url'=>$jsonwiki["url"],'body'=>$jsonwiki["body"]);$URL=$wikidata["url"];$body=$wikidata["body"];

流れとしては、

  • LINEから受け取ったキーワードをAPIのkeywordとして渡せるようにエンコードする
  • それをAPIに渡し$resとして取得する
  • json_decodeして配列にする
  • そのキーワードに対しての一番先頭の回答を取得するため[0]を取得する
  • それに対して$wikidataとして必要な項目だけ取得する
  • LINEに返したいものは、$URL = $wikidata["url"]や $body = $wikidata["body"]として取得できる

補足

上記 $resにどんなものが入っているか見るために下記のようなものをページを作って見てました。( jsはconsole.log()で気軽に見えたけどphpではどうやってみたらいいかわからずわざわざこんなことしました。。。 )
image.png

check_data.php
<html><head><title> test </title></head><body><formmethod="POST"action="show.php">キーワード:
<inputtype="text"name="name"size="15"/><inputtype="submit"name="submit"value="送信" /></form><?phpif($_REQUEST['submit']!=null){$input=$_REQUEST[name];//$textのなかに'?'が含まれている場合$text=str_replace('?','',$input);$keyword=mb_convert_encoding($text,"UTF-8","auto");$res=file_get_contents('http://wikipedia.simpleapi.net/api?keyword='.$keyword.'&output=json');$jsonwiki_decode=json_decode($res,true);$jsonwiki=$jsonwiki_decode[0];$wikidata=array('url'=>$jsonwiki["url"],'body'=>$jsonwiki["body"]);$URL=$wikidata["url"];$body=$wikidata["body"];print('$input: '.$input.'-----');print('$text: '.$text.'-----');print('$keyword: '.$keyword.'-----');print('$res: '.$res.'-----');print('$jsonwiki_decode :'.$jsonwiki_decode.'-----');print('$jsonwiki :'.$jsonwiki.'-----');print('$wikidata :'.$wikidata.'-----');print('URL: '.$URL.'-----');print('body: '.$body.'-----');}?></body></html>

メッセージによってLINE側に返却するものを変える

  • ?がある場合はURLをLINEに返す。また、Wikipedia APIに渡すときには?を取り除く。
  • ?がない場合はBodyをLINEに返す。
分岐部分.php
if(strpos($text,'?')!==false){//$textのなかに'?'が含まれている場合$text=str_replace('?','',$text);$keyword=mb_convert_encoding($text,"UTF-8","auto");$res=file_get_contents('http://wikipedia.simpleapi.net/api?keyword='.$keyword.'&output=json');$jsonwiki_decode=json_decode($res,true);// 0番目の物だけを抽出する。もっとたくさん抽出したいときはここを変更する$jsonwiki=$jsonwiki_decode[0];//欲しい項目だけの配列にする$wikidata=array('url'=>$jsonwiki["url"],'body'=>$jsonwiki["body"]);$URL=$wikidata["url"];$body=$wikidata["body"];// メッセージ部分$response_format_text=["type"=>"text","text"=>$URL];}else{// ?が含まれないときの処理$keyword=mb_convert_encoding($text,"UTF-8","auto");$res=file_get_contents('http://wikipedia.simpleapi.net/api?keyword='.$keyword.'&output=json');$jsonwiki_decode=json_decode($res,true);// 0番目の物だけを抽出する。もっとたくさん抽出したいときはここを変更する$jsonwiki=$jsonwiki_decode[0];//欲しい項目だけの配列にする$wikidata=array('url'=>$jsonwiki["url"],'body'=>$jsonwiki["body"]);$URL=$wikidata["url"];$body=$wikidata["body"];// メッセージ部分$response_format_text=["type"=>"text","text"=>$body];}}

サンプル

全ソースコード

linebot.php
<?php$accessToken='アクセストークン';//ユーザーからのメッセージ取得$json_string=file_get_contents('php://input');$jsonObj=json_decode($json_string);$type=$jsonObj->{"events"}[0]->{"message"}->{"type"};//メッセージ取得$text=$jsonObj->{"events"}[0]->{"message"}->{"text"};//ReplyToken取得$replyToken=$jsonObj->{"events"}[0]->{"replyToken"};//メッセージ以外のときは何も返さず終了if($type!="text"){exit;}if($type=="text"){if(strpos($text,'?')!==false){//$textのなかに'?'が含まれている場合$text=str_replace('?','',$text);$keyword=mb_convert_encoding($text,"UTF-8","auto");$res=file_get_contents('http://wikipedia.simpleapi.net/api?keyword='.$keyword.'&output=json');$jsonwiki_decode=json_decode($res,true);// 0番目の物だけを抽出する。もっとたくさん抽出したいときはここを変更する$jsonwiki=$jsonwiki_decode[0];//欲しい項目だけの配列にする$wikidata=array('url'=>$jsonwiki["url"],'body'=>$jsonwiki["body"]);$URL=$wikidata["url"];$body=$wikidata["body"];// メッセージ部分$response_format_text=["type"=>"text","text"=>$URL];}else{// ?が含まれないときの処理$keyword=mb_convert_encoding($text,"UTF-8","auto");$res=file_get_contents('http://wikipedia.simpleapi.net/api?keyword='.$keyword.'&output=json');$jsonwiki_decode=json_decode($res,true);// 0番目の物だけを抽出する。もっとたくさん抽出したいときはここを変更する$jsonwiki=$jsonwiki_decode[0];//欲しい項目だけの配列にする$wikidata=array('url'=>$jsonwiki["url"],'body'=>$jsonwiki["body"]);$URL=$wikidata["url"];$body=$wikidata["body"];// メッセージ部分$response_format_text=["type"=>"text","text"=>$body];}}$post_data=["replyToken"=>$replyToken,"messages"=>[$response_format_text]];$ch=curl_init("https://api.line.me/v2/bot/message/reply");curl_setopt($ch,CURLOPT_POST,true);curl_setopt($ch,CURLOPT_CUSTOMREQUEST,'POST');curl_setopt($ch,CURLOPT_RETURNTRANSFER,true);curl_setopt($ch,CURLOPT_POSTFIELDS,json_encode($post_data));curl_setopt($ch,CURLOPT_HTTPHEADER,array('Content-Type: application/json; charser=UTF-8','Authorization: Bearer '.$accessToken));$result=curl_exec($ch);curl_close($ch);

おわりに

APIから返ってきた値をいい感じで取ってくるところでだいぶハマりました。
JSONデータの扱い方にもっと慣れないとなあー

次はHerokuを使ったお引っ越しもやってみようと思います。


Mac HomebrewでNode.jsをインストールする

$
0
0

目的

  • Mac端末にHomebrewを用いてNode.jsをインストールする方法をまとめる

実施環境

  • ハードウェア環境
項目情報備考
OSmacOS Catalina(10.15.3)
ハードウェアMacBook Air (11-inch ,2012)
プロセッサ1.7 GHz デュアルコアIntel Core i5
メモリ8 GB 1600 MHz DDR3
グラフィックスIntel HD Graphics 4000 1536 MB

実施条件

  • Homebrewが使用できる状態になっていること。

実施方法概要

  1. Nodebrewのインストール
  2. Node.jsのインストール
  3. 設定

実施方法詳細

  1. Nodebrewのインストール

    1. 下記コマンド実行してNodebrewをインストールする。

      $brew install nodebrew
      
    2. 下記コマンドを実行してNodebrewのインストールを確認する。

      nodebrew -v
      
  2. Node.jsのインストール

    1. 下記コマンドを実行して最新の安定バージョンをインストールする。(エラーが出た方はこちら→Mac Nodebrewを用いてNode.jsをインストールした時にエラーが出た話)

      $ nodebrew install-binary stable
      
    2. 下記コマンドを実行してNode.jsのインストールを確認する。

      $nodebrew --version
  3. 設定

    1. 先に実行したコマンド$ nodebrew --versionの出力の「# use a specific version number」の下部に記載されているコマンドを実行する。(「vX.X.X is not installed」というエラーが出た方はこちら→Mac Node.jsのバージョン有効化する手順でvX.X.X is not installedと出力された話)(X.X.Xはバージョン名)

      $nodebrew use vX.X.X
      >use vX.X.X
      
    2. 下記コマンドを実行してインストールしたNode.jsのバージョンが有効化されている事を確認する。

      $nodebrew ls・
      ・
      ・
      >current: vX.X.X
      
    3. 先にインストールしたバージョンが「current:」の後に記載されていれば導入完了である。

    4. 下記コマンドを実行してパスを通す。

      $echo'export PATH=$PATH:~/.nodebrew/current/bin/'>> ~/.bashrc
      
    5. 「.bashrc」ファイルがログイン時に読み込まれるように設定していない場合は下記の手順を実施する。

      1. 下記コマンドを実行して「.bash_profile」ファイルを開く

        $vi ~/.bash_profile
        
      2. 下記の内容を追記する。

        ~/.bash_profile
        source ~/.bashrc
        
    6. 下記コマンドを実行して「.bashrc」ファイルを読み込む

      $source ~/.bashrc
      
    7. 下記コマンド実行してパスが通っている事を確認する(command not foundにならなければOK)

      $npm
      

Netlify Functions で古のアクセスカウンター(アクセサリー)をつくる

$
0
0

はじめに

既にホスティングサービスが終了している懐かしの「ジオシティーズ」ですが、スケジュール上では 2020/3/31 に全ファイルの削除が行われるようです。

自分にはジオシティーズ上で 2005年くらいまで更新していたサイトがありましたので、FTP でファイルを救出し Netlifyの無料枠にて記念に再ホストすることにしました。

…できたものの何かが足りない。

2020-03-30_01-19.png

アクセスカウンターだ!

ということで Netlify に備わる Lambda なサービス Netlify Functions でアクセスカウンターをふとつくってみることにしました。

お気づきのように、Lambda のインスタンスが寝てしまうとカウンターが「飛ぶ」アクセスカウンター(アクセサリー)となっていますが、現代風に JSON や SVG などを使って処理するようになっています。

Netlify Functions について

Netlify Functionsは静的ファイルホスティングサービス Netlify が提供する、くだけて言えば Amazon AWS Lambda に対するプログラムの自動デプロイの仕組みで、Netlify のアカウントにて(クレジットカード登録が必要な AWS アカウントなしに)Lambda を利用することができます。

この記事で分かること

アクセスカウンターの動きはどうだろうという感じですが、いくつかの処理が参考になるかもしれないと Qiita に投稿することにしました。何か使える部分があったら幸いです。

  • Lambda でファイルシステムを操作する
  • netlify-lambda で追加の webpack.config を構成する
  • Lambda で jsdom を使って DOM を操作する

ソースコードと動作デモ

ここで使われているソースコードを github にコミットしてあります。ソース全容を確認しながら読んでいただくと分かりやすいかも知れません。

ソースコード一式

Netlify Functions Template. Includes joke access counter sample program.

https://github.com/h1romas4/netlify-functions-template

動作デモ(しばらくアクセスがないとカウントが 1に戻ります)

https://maple4estry.netlify.com/

Lambda でファイルシステムを操作する

Netlify Functions(Lambda) でも nodejsの fsオブジェクトを使ってファイルを操作することが出来ます。

fsで作成を行ったファイルは、この「アクセスカウンター」の動きどおり、インスタンスのリビルドなどのタイミングで消されますし、スケールした場合は値が分裂しますので、アルゴリズムとしてステートの性質をあてにしてはいけません。

API へのアクセス数をカウントする JSON ファイルを /tmpに書き込む処理抜粋:

importfsfrom'fs';import*ascommonfrom'../common'// async await 版の fs オブジェクトconstfsp=fs.promises;// カウンターファイル JSON 出力パスconstcounterJsonFile=common.tmp+"/counter.json";/**
 * Lambda エンドポイント
 */exports.handler=async(event)=>{constnow=newDate();letcounterJson={"createDate":now,"updateDate":now,"count":1};try{// カウンターファイル存在確認constdata=awaitfsp.readFile(counterJsonFile);// カウンターファイルが存在すれば JSON 解析してカウンターを更新counterJson=JSON.parse(data.toString('utf-8'));counterJson.updateDate=now;counterJson.count++;}catch(e){// カウンターファイルがなければ例外を潰して新規作成}// カウンターファイル書き込みawaitfsp.writeFile(counterJsonFile,JSON.stringify(counterJson));// ...}

Lambda のハンドラーを exports.handler = async (event)asyncとして fsの操作は fs.promisesからもらったオブジェクトで awaitしてあげると簡単でした。

ちなみにこの処理は readFileから writeFileまでに間がありますのでアトミックなカウントアップはできません。また nodejs の fs の実装を確認していませんが、ジオシティーズ世代の CGI カウンターよろしく壊れる可能性もあるでしょうか?(懐かしい)。

さて、/tmpは Lambda で使えるテンポラリー領域となっていますが、開発を行うローカル環境でそのまま /tmpにかかれると確認などが面倒なためちょっと小細工をしています。

AWS で設定される環境変数の有無で /tmpの位置を切り替え:

// カウンターファイルを作成するテンポラリーディレクトリexportlettmp='/tmp'// AWS Lambda で動いていない場合はテンポラリーを dist/tmp に設定if(!("AWS_REGION"inprocess.env)){tmp="./dist/tmp"try{fs.mkdirSync(tmp);}catch(e){}}

Netlify Functions から提供されている netlify-lambda SDK が ./dist/api以下にビルドを出力するため、合わせてテンポラリー領域を ./dist/tmpに設定しています。

netlify-lambda で追加の webpack.config を構成する

netlify-lambda SDK は内部的に webpack を使ってビルドしていますが、標準の webpack 設定をマージできる --configオプションが準備されています。

netlify-lambda --config ./webpack.functions.js build src/functions/endpoint

本アクセスカウンターでは、古では GIF 画像などで処理していたカウント画像に代わり、SVG/CSS/HTML を Lambda 上で処理し数字を描いてクライアントに返却したく、処理前の .htmlを文字列として importするため raw-loaderを webpack に追加設定しています。

// webpack.functions.jsmodule.exports={optimization:{minimize:false},module:{rules:[{test:/\.html$/i,exclude:/node_modules/,use:'raw-loader'}],}};

この設定によりプログラムから html文字列変数として importができます。

// counter.jsimporthtmlfrom'../resources/fujilcd.html'

このような追加の webpack 設定がある場合は --configオプションを活用すると良さそうです。

なお、Lambda 上にデプロイするソースをミニマイズしてもあまり意味がないため、どのプロジェクトでもこのコンフィグで optimization: { minimize: false },を入れておくとビルド時間短縮に役立つかもしれません。

Lambda で jsdom を使って DOM を操作する

nodejs 上で html などの DOM 操作を行う jsdomパッケージを Netlify Functions(Lambda?) 上で使う場合は少々コツが必要なようです。

通常通り packege.jsonに依存関係をいれると、

Error while initializing entrypoint: { Error: Cannot find module 'canvas'

というエラーで canvasパッケージを依存に入れても動作しませんでした。 issue を探ったところワークアラウンドがありましたので、この方法で回避しています。

https://github.com/jsdom/jsdom/issues/1708#issuecomment-462990288

I was able to work around this by adding "canvas": "file:./canvas" to the dependencies section of the app's package.json and creating canvas/index.js containing simply module.exports = {}.

プロジェクト上に空の canvasモジュールをつくって依存に追加:

// package.json{"dependencies":{"jsdom":"^16.2.1","canvas":"file:./src/functions/canvas","utf-8-validate":"^5.0.2","bufferutil":"^4.0.1"},}

jsdomが導入できれば、先程 import したような文字列 HTML を new JSDOM(html)で DOM 化しウェブブラウザーと同様に document.querySelectorなどで操作することができます。

importしたカウンター表示 HTMLを DOM 操作して CSS クラスを付けてカウント数を表示:

// counter.jsfunctionupdateLCD(html,number){constdom=newJSDOM(html);const{document}=dom.window;letcountString=number+""countString=("0".repeat(maxCount)+number);countString=countString.substring(countString.length-maxCount)for(leti=0;i<countString.length;i++){letnumber=countString.substring(i,i+1);constdigi=document.querySelector(`.digit-${i} svg`);digi.removeAttribute('class');digi.classList.add(`num-${number}`);}returndom.serialize();}

jsdomdom.serialize();にてできた DOM を文字列化できますので、これをアクセスしてきたウェブブラウザーに返却し、

// counter.jsexports.handler=async(event)=>{// ...// カウンター数 SVG を生成counterJson.html=updateLCD(html,counterJson.count);return{statusCode:200,headers:{"Content-Type":"application/json; charset=utf-8",},body:JSON.stringify(counterJson)}};

ウェブブラウザーは shadowDOM で画面にそのまま書き出しています。

/v1/counterとして Netlify Functions にデプロイした Lambda を呼び出すクライアントのソース:

<!DOCTYPE html><html><body><access-counter></access-counter><script>fetch('/v1/counter').then(function(response){returnresponse.json()}).then(function(json){customElements.define('access-counter',classextendsHTMLElement{constructor(){super();constshadowRoot=this.attachShadow({mode:'open'});shadowRoot.innerHTML=json.html;}})});</script></body></html>

終わりに

現代版ジオシティーズとも言える Netlify を今回初めて使ってみたのですが、Netlify Functions 含めいろいろできて便利でした。

試しに検索エンジン API もつくってみましたので、自分のブログになってしまっていますが、気になる方がもしいらっしゃいましたらご覧ください!

Netlify Functions で検索エンジン API をつくる

ふと思いつきまして Netlify で使える Lambda なサービス、Netlify Functions を用いて参照系検索 API を作成してみました。無料枠での挑戦です。

netlify-01.jpeg

Jest+CircleCIなプロジェクトにCodeCov(カバレッジレポート)を導入するまでの手順ハンズオン

$
0
0

概要

  • テストのコードカバレッジのレポートにCodeCovを使いバッジimage.pngをゲットするまでのハンズオンメモです

開発環境と構成

  • 開発環境
開発言語JavaScript(ES6)/Node.js
テストフレームワークJest
GitホスティングGitHub
CIツールCircleCI
カバレッジレポートCodeCov
  • 構成
    全体としてはざっくり以下のような構成となります

image.png

CodeCovのサインアップとプロジェクト設定

  1. codecovを開く

まず、CodeCovのサインアップから。

image.png

2.プロジェクトはGithubにホストしているのでGithubを選択

image.png

3.Githubへのアクセス認可の画面がでるので、Authorize CodeDevをクリック

image.png

4.CodeCovの初期画面がでる。

リポジトリがまだ登録されていないのでAdd Repositoryをクリック。

image.png

5.Choose a new repository belowという画面がでてレポジトリ一覧が表示される。

「ここで一覧から対象のリポジトリを選択すれば良し」と思ったら、、、すべてのリポジトリが表示されていない模様

image.png

6.直接リポジトリを指定する

image.png

↑のようなメッセージがでていたので、直接指定することにする。

https://github.com/riversun/event-listener-helperというリポジトリを設定したかったので、上のフォーマットにならい、https://codecov.io/gh/riversun/event-listener-helperにアクセスしたら、そのプロジェクトの初期画面が表示される。

image.png

7.CodeCovのトークンをメモする

初期画面に表示されているトークンをメモしておく。
あとでCircleCIの環境変数に登録する。

環境変数名はCODECOV_TOKENとなる。

CircleCI用のconfig.ymlにCodeCov関係の設定を追記する

CodeCov用のOrbがあるので、その利用を前提としてconfig.ymlを編集する

ちなみに、Orbsとは

Orbs とは
CircleCI Orbs は、ジョブ、コマンド、Executor のような設定要素をまとめた共有可能なパッケージです。 CircleCI 独自の認証済み Orbs のほか、パートナー企業によるサードパーティ製 Orbs を用意しています。

(出典:https://circleci.com/docs/ja/2.0/orb-intro/)

config.ymlを以下のようにした

version:2.1orbs:node:circleci/node@1.1.6codecov:codecov/codecov@1.0.5jobs:build-and-test:executor:name:node/defaultsteps:-checkout-node/with-cache:steps:-run:npm install-run:npm test-codecov/upload:file:./coverage/lcov.infoworkflows:build-and-test:jobs:-build-and-test

ポイント

  • orbscodecov: codecov/codecov@1.0.5を追加する

  • カバレッジレポートファイルのアップロードをするためのコマンドcodecov/uploadを以下のように追記する。

config.yml(抜粋)
-codecov/upload:file:./coverage/lcov.info
  • 実際なにやってるか
    • codecov/uploadの内部処理ではcurlコマンドでカバレッジレポートファイルlcov.infoCircleCI(docker)→CodeCovに送信している
    • lcov.info(カバレッジレポートが格納されたファイル)は[root]/coverage/lcov.infoに生成される前提。そのように生成されるようにJestの設定(jest.config.js)を次でする

Jestの設定

念のためにJestの設定をみておく。

やりたいことは以下の2点

  • Jestでテストするときにコードカバレッジも計測するようにすること
  • コードカバレッジのレポートが目的の場所に生成されるようにすること

ということでjest.config.jsの設定をまるっと掲載すると以下のようになる

jest.config.js
module.exports={verbose:true,testEnvironment:'jsdom',testMatch:["**/test/**/*.test.js"],testTimeout:5000,moduleDirectories:["node_modules"],transformIgnorePatterns:["node_modules/(?!(@riversun/event-emitter)/)"],"coverageDirectory":"./coverage/","collectCoverage":true};

本稿で重要なところは以下の部分

jest.config.js(抜粋)
"coverageDirectory":"./coverage/","collectCoverage":true

coverageDirectoryでカバレッジ計測レポートの関連ファイルの生成先を指定する
collectCoverage:trueでテストする毎にカバレッジ計測レポートが生成されるようになる
(jestコマンドに --coverageオプションをつけて、必要なときに生成するアプローチでもアリ)

CircleCIでプロジェクトをセットアップする

さてここからCircleCIでプロジェクトのセットアップをしていく。

1. https://circleci.com/を開いてGo to Appクリック(CircleCI自体のサインアップは割愛)

image.png

2.サードパーティ製のOrbsをつかえるようにする

さきほど、CodeCovOrbを使う記述をconfig.ymlに記述したが、サードパーティ製のOrbsを使うための設定を1度だけやっておく必要がある。

画面左の歯車アイコンをクリックすると、

image.png

Organization Settingsという画面がでるので、Allow uncertified orbsYesにすることで、サードパーティ製Orbsの使用を許可できる。

image.png

この設定はプロジェクト横断設定なので1度だけやればOK。

3.プロジェクトを追加する

さて、サードパーティ製のOrbsの許可までできたので、ここでCIしたいプロジェクトを登録する。

以下の画面で、Add projectをクリック

image.png

↑の画面じゃなくて、↓の画面の場合もある(↑のほうは新UIのプレビュー版で↓は現行UIという位置づけかな?いずれにせよAdd projectをクリックする。)

image.png

自分のプロジェクト一覧からCircleCIしたいプロジェクトのところにあるSet Up Projectをクリック

image.png

3.CircleCI用のConfigを追加する

ここでは、CircleCIが「自動でConfig作りましょうか?」とたずねてくれるが、自前のconfig.ymlを作ってPushしてあるので、Add Manuallyをクリック。

image.png

4.さっそくビルド

既に「circleci/config.ymlがあるならビルド開始できますよ」というメッセージが出るのでStart Buildingをクリックする。

image.png

5.予定通りビルド失敗

そして、CircleCI上にて、ビルドが失敗する。

image.png

CodeCovの環境変数を設定していないので最初のビルドは失敗した。

6.CodeCov用の環境変数を設定する

さきほどメモしたCodeCovの環境変数を、CircleCIにセットする。

現行UIでは以下のようにプロジェクト名の右にある歯車アイコンをクリックする

image.png

Environment Variablesを選択して、Add variableをクリック

image.png

ここで環境変数をセットできるので、環境変数名としてNameCODECOV_TOKENをセット、ValueにはさきほどCodeCovから取得した値をセットしてAdd Variable*をクリック

image.png

無事に環境変数をセットできた模様

image.png

ちなみに、
CircleCIの新UI(プレビュー版)のほうだと、同様の操作は以下のようになる。

対象のプロジェクトのPipelines画面で、Project Settingsをクリック

image.png

Environemnt Variablesを選択して、Add Variablesをクリックする

image.png

(あ、なるほど。UIレイアウト、デザインは違うけどラベルが一緒なので文字だけのドキュメンテーションにしたら同じ説明で済むようにできてますね)

CircleCIでビルドして、カバレッジレポートをCodeCovでみる

1.CircleCIでビルドする

ここまでで、CircleCIでビルドする準備ができたので、あとはコミット・プッシュするなり、ReRunするなりしてCircleCIを回すだけ。

以下のようにビルドが無事成功した模様。

image.png

2.CodeCovでカバレッジレポートを確認する

CodeCovを開き https://codecov.io/gh
さきほど登録したプロジェクトをクリック

image.png

カバレッジレポートが表示された

image.png

Readme.mdにカバレッジ計測バッジをはりつける

CodeCovのプロジェクト画面でSettingsタブでBadgeを選択すると、以下のようにバッジ用のコードがあるので、GithubのReadmeにバッジをつけたいときはMarkdownの内容をコピーして、それをReadme.mdにペーストすればできあがり。

image.png

ReadmeにCodeCovのバッジimage.pngが無事表示された
(本プロジェクトはこちら)

image.png

まとめ

  • 以下のようなJest,CircleCI,CodeCovの組み合わせの導入手順をご紹介しました

image.png

  • 便利でMotivationalなクラウドサービス群に感謝感謝!

[Node.js勉強会]ExpressフレームワークでTODOアプリを作ろう

$
0
0

この記事について

Node.jsの概念や、Expressの使い方について詳しく書いてある記事ではありません。
ある程度の土台が用意してあるので、それを元に実際に手を動かして、TODOアプリを完成させることがこの記事の目的です。

pugファイルと仮処理を記述 の部分までは、コピペで進めていただいても大丈夫です。
TODOの処理を記述 の部分から実際に、Node.jsの様々な書き方を試していただけたらと考えております

今回作成する、TODOアプリの動作は以下のような形です。
express-comp.mov.gif

Node.jsとは

Node.js はスケーラブルなネットワークアプリケーションを構築するために設計された非同期型のイベント駆動の JavaScript 環境です。
Node.js公式

非同期で処理が実行されるので、DBとの通信のような時間がかかる処理を実行する場合は async, await の記述が必要です。

Expressとは

Node.js のための高速で、革新的な、最小限のWebフレームワーク
Express公式

最小限の機能を持ったフレームワークです。基本的に利用するのは、ルーティングの機能かと思います。
LaravelやRailsのようなフレームワークの場合、プロジェクトを作成した時点でログイン機能、メール機能、など様々な機能が簡単に実装出来るように用意されていますが、Expressは全て自分でライブラリを選定して実装しないといけません。
そのため、フレームワーク自体の容量がとても小さいです。

事前準備

・ Git
・ Docker

手順1 dockerを利用して、Expressの実行環境を整える

スムーズにNode.jsの学習を進めるために、開発環境の構築をDockerで行います。

$ git clone https://github.com/KazukiSadasue/express-mysql-sample.git
$ cd express-mysql-sample
$ docker-compose up -d

上記のコマンドで、dockerコンテナが起動して、サーバーも立ち上がります。
http://localhost
に接続して、以下の画像のように表示されたら実行環境は整いました。
*もしかしたら、最初の1回目にサーバーの立ち上げ失敗するかもしれません。
その場合は、docker-compose restartで再度立ち上げると治ると思います。

express-first-step.png

手順2 作成するアプリケーションについて

簡単なTODOアプリケーションを作成します。
ルーティングは以下のようにしようと思います。

パスメソッド内容
/GETTODOトップ
/createPOSTTODO作成
/donePOSTTODO完了
/deletePOSTTODO削除

TODOテーブルは以下のようにします。

カラム説明
idINTEGERPRIMARYキー
contentSTRINGTODO内容(必須)
isDoneBooleanTODOの状態(必須、デフォルト0)
deletedAtDATETIME削除日時
createdAtDATETIME作成日時
updatedAtDATETIME更新日時

モデルの作成

テーブル設計から、TODOモデルを作成します。
modelsフォルダにtodo.jsを作成して、中身は以下のようにします。


todo.jsのコード
todo.js
'use strict';constloader=require('./sequelize-loader');constSequelize=loader.Sequelize;constTodo=loader.database.define('todos',{id:{type:Sequelize.INTEGER,primaryKey:true,autoIncrement:true,allowNull:false,},task:{type:Sequelize.STRING,allowNull:false,},isDone:{type:Sequelize.BOOLEAN,allowNull:false,defaultValue:0,},},{paranoid:true,freezeTableName:true,});module.exports=Todo;


次にapp.jsでモデルを読み込みます。

app.js
var logger = require('morgan');
var helmet = require('helmet');

// モデルの読み込み
+var Todo = require('./models/todo');
+Todo.sync();

モデルの作成が終わったら、Dockerコンテナを再度立ち上げます。

$ docker-compose restart

サーバーが再起動して、モデルに対応するテーブルが作成されます。
テーブルを確認するには、以下のコマンドを実行

$ docker exec -it mysql bash
# mysql -u root -p
Enter Password: パスワードは入力せずにEnter

mysql> use express-sample
mysql> show tables;
+--------------------------+
| Tables_in_express-sample |
+--------------------------+
| todos                    |
+--------------------------+

todosテーブルが作成されているのが確認出来ました!

pugファイルと仮処理を記述

TODOアプリのデザインはBootstrapを用いて作成しました。
以下のコードをコピペして配置してください。


views/index.pugのコード
views/index.pug
extends layout

block content
  h1 TODOアプリ
  div
  form(action="/create", method="post").form-inline.mb-3
    div.form-group
      input(type="text" name="task" placeholder="例: 晩御飯を作る").form-control
    button(type="submit").btn.btn-primary 追加

  div.mb-3
    h2 TODO
    form(name="todoform" method="post").mb-2
      ul.list-group
      each todo in todoTasks
        li.list-group-item.px-5
          input(name="todos", value=todo.id, type="checkbox", id=`todo-${todo.id}`).form-check-input
          label(for=`todo-${todo.id}`).form-check-label #{todo.task}
    if todoTasks.length
      button.btn.btn-success(onclick="done()").mr-2 達成
      button.btn.btn-danger(onclick="deleteTodo()") 削除
    else
      div TODOタスクはありません


  div
    h2 DONE
    form(name="doneform" method="post").mb-2
      ul.list-group
      each done in doneTasks
        li.list-group-item.px-5
          input(name="todos", value=done.id, type="checkbox", id=`todo-${done.id}`).form-check-input
          label(for=`todo-${done.id}`).form-check-label #{done.task}
    if doneTasks.length
      button.btn.btn-danger(onclick="deleteDone()") 削除
    else
      div DONEタスクはありません

  script(src="javascripts/todo.js")



puglic/javascripts/todo.jsのコード
public/javascripts/todo.js
functiondone(){document.todoform.action="/done";document.todoform.submit();}functiondeleteTodo(){document.todoform.action="/delete";document.todoform.submit();}functiondeleteDone(){document.doneform.action="/delete";document.doneform.submit();}



routes/index.jsのコード
routes/index.js
varexpress=require('express');varrouter=express.Router();// TODOトップページrouter.get('/',async(req,res,next)=>{consttodoTasks=[];constdoneTasks=[]res.render('index',{todoTasks,doneTasks,});});module.exports=router;


コードの記述が終わったら、 docker-compose restartコマンドでコンテナを立ち上げなおします。

すると、以下のような状態になるかと思います。
express-second-step.png

TODOの処理を記述

ここまで見た目の部分と、モデルの定義まで終わりました。
あとは、routes/index.js の部分に処理を書き加えるだけでTODOアプリが完成します。
ここから先は、色々と手を動かしてNode.jsの書き方に触れていただけたらと思います。

基本的な使い方

// ライブラリや、モデルの読み込みには、requireを利用するconst{Op}=require('sequelize');// getのルーティングrouter.get('/',()=>{});// postのルーティングrouter.post('/',()=>{});

Sequelizeの使い方

モデルの使い方は、 Sequelize公式ドキュメントを参考にします。
使い方の一部を抜粋してこちらに記述します。

// async を記述することで、非同期関数を定義する。router.get('/',async(req,res,next)=>{// isDoneがtrueのデータを全件取得// awaitを記述することで、DB処理の結果が返ってくるまで待つconsttodos=awaitTodo.findAll();})

Sequelizeの使う上で、Node.jsは非同期通信であることに注意しないといけません。
async, awaitを利用して、DB処理が終わってから次の処理を行うようにしないと、不具合の原因となってしまいます。

上記の内容を参考に、TODOアプリを完成させていただけたらと思います。

最後に

TODOアプリ完成しましたでしょうか?
最終的な私の実装例を記述致します。


routes/index.jsの実装例
routes/index.js
varexpress=require('express');varrouter=express.Router();constTodo=require('../models/todo');const{Op}=require('sequelize');// TODOトップページrouter.get('/',async(req,res,next)=>{consttodos=awaitTodo.findAll();consttodoTasks=todos.filter(todo=>todo.isDone===false);constdoneTasks=todos.filter(todo=>todo.isDone===true);res.render('index',{todoTasks,doneTasks,});});// TODO作成router.post('/create',async(req,res,next)=>{awaitTodo.create({task:req.body.task,});res.redirect('/');});// TODO完了router.post('/done',async(req,res,next)=>{consttodoModels=awaitgetModelsByIds(req.body.todos);for(consttodooftodoModels){awaittodo.update({isDone:true});}res.redirect('/');});// TODO削除router.post('/delete',async(req,res,next)=>{consttodoModels=awaitgetModelsByIds(req.body.todos);for(consttodooftodoModels){awaittodo.destroy();}res.redirect('/');});// リクエストのIDからTODOモデルを取得するasyncfunctiongetModelsByIds(ids){if(typeofids==='string'){ids=ids.split();}returnawaitTodo.findAll({where:{id:{[Op.in]:ids,}}});}module.exports=router;


もっと説明の記述が必要、xxが分からない等ありましたらコメントお願い致します!

今回のコードの配置場所
https://github.com/KazukiSadasue/express-mysql-sample

Node.js(express+ejs)のWebアプリサンプルをDocker上で動かす

$
0
0

はじめに

  • Node.jsをDockerで起動し、適当なWeb画面を表示させるところまでをやります
  • Node.jsでのサンプルはAkinari Tsugoさんの記事を参考に実装しました

やったこと

  • Node.js(express + ejs)の環境をdockerで構築する
  • express + ejsを使用したWebアプリサンプルを作成し、docker上で起動させる

使用するフレームワークについて

expressとは

  • Node.jsでWebアプリを開発する際に使用するフレームワーク
  • 画面遷移とかWebアプリ開発に必要な諸々をサポートする機能あり
  • expressの公式はこちら

ejsとは

  • JavaScriptでHTMLを作成できるテンプレート言語
  • JavaでいうところのJSPに相当する
  • ejsの公式はこちら

node.jsをdockerで構築する

Dockerfileの作成

  • 鉄板のalpineを選択
  • express、ejsのみをnpmで投入
FROM node:alpineRUN npm install express
RUN npm install ejs

# 作業ディレクトリへ移動WORKDIR /app# 3000番ポートを公開EXPOSE 3000

docker-compose.ymlの作成

  • volumesの設定など、何かとdocker-composeにしておいた方がいいのでdocker-compose.ymlを作成
  • tty: trueとしておくことで、コンテナをずっと起動状態にできる
  • build: .でDockerfileの位置を指定
  • volumes:の指定で、コンテナ内の/appをホスト側にもマウント
version: '3'
services:
  app:
    build: .
    tty: true
    container_name: node-sample01
    volumes:
      - ./app:/app
    ports:
      - "8080:3000"

Webアプリサンプル

ディレクトリ構成

  • ディレクトリ全体構成は以下の通り
app/
 ├ public/
 │ └ 静的なファイル群
 ├ routes
 │ └ index.js
 ├ views
 │ └ index.ejs
 └ app.js
Dockerfile
docker-compose.yml

app.js

varexpress=require('express');varapp=express();// テンプレートエンジンをEJSに設定app.set('views','./views');app.set('view engine','ejs');// public配下の静的ファイルは無条件に公開app.use('/public',express.static('public'));// URLと処理をマッピングapp.use('/',require('./routes/index.js'));// ポート3000で起動app.listen(3000);// アプリケーション開始時のログconsole.log('Server running at http://localhost:3000');

index.js

varexpress=require('express');varrouter=express.Router();// デフォルトルーティングrouter.get('/',function(request,response){// パラメータに値を設定、設定したパラメータはejs内で参照可能となるresponse.render('index',{title:'NodeSample01',message:'Hello Node.js'});});module.exports=router;

index.ejs

  • index.jsで設定したtitlemessageをejs内で参照
<!DOCTYPE html><htmllang="ja"><head><metacharset="UTF-8"><metahttp-equiv="X-UA-Compatible"content="IE=edge"><metaname="viewport"content="width=device-width, initial-scale=1"><title><%=title%></title><linkrel="stylesheet"href="/public/bootstrap/bootstrap.css"/><linkrel="stylesheet"href="/public/css/index.css"/><script type="text/javascript"src="/public/jquery/jquery.js"></script><script type="text/javascript"src="/public/bootstrap/bootstrap.js"></script><script type="text/javascript"src="/public/js/index.js"></script></head><body><navclass="navbar navbar-inverse navbar-fixed-top"><divclass="container"><divclass="navbar-header"><buttontype="button"class="navbar-toggle collapsed"data-toggle="collapse"data-target="#navbar"aria-expanded="false"aria-controls="navbar"><spanclass="sr-only">Toggle navigation</span><spanclass="icon-bar"></span></button><aclass="navbar-brand"href="#">Hello Project</a></div></div></nav><divclass="container"><divclass="starter-template"><p><%=message%></p><p><imgid="img"src="/public/images/drum.jpg"class="img-thumbnail"></p></div></div></body></html>

起動方法

  • docker-compose upにてコンテナを起動したら、以下でコンテナにログイン
docker exec-it node-sample01 /bin/ash
  • コンテナ内でNode.jsを起動
node app.js

サンプル画面の表示

  • http://localhost:8080で以下画面が表示される NodeSample01.png

AWS IoT のクライアントデバイス環境を簡単に作るスクリプト(Node.js v2 版)

$
0
0

ちょこっとテストするための、Node.js版のAWS IoT Device SDK v2のPubSubをすぐに試すスクリプトです。CLI上で数行でできます。
Python版はこちらだいたい同じです

Cloud9上での設定を想定しています。マネコンで作成するのが手間な場合に使います。
何をやっているか等は説明しません。。
あと、手動で何発かメッセージを送るだけであれば、IoT Coreのテスト機能を使うのがよいです。

準備

Cloud9 の環境にはUbuntuを選びます。Amazon Linuxだと、GLIBC_2.25が無いというエラーが出ております。
Cloud9の環境を作成し、以下をEnvironmentディレクトリ下に置きます。

setup-node-v2.sh
mkdir$THING_NAMEcd$THING_NAMEPOLICY_NAME=${THING_NAME}_Policy

aws iot create-thing --thing-name${THING_NAME}

git clone https://github.com/aws/aws-iot-device-sdk-js-v2.git
cd aws-iot-device-sdk-js-v2/samples/node/pub_sub/
npm install

wget -O rootca.pem \
    https://www.amazontrust.com/repository/AmazonRootCA1.pem

aws iot create-keys-and-certificate --set-as-active\--certificate-pem-outfile    certificate.pem \--public-key-outfile         public_key.pem  \--private-key-outfile        private_key.pem \--query certificateArn

echo-n CERTIFICATE_ARN: 
read str 
CERTIFICATE_ARN=$str

aws iot create-policy                     \--policy-name${POLICY_NAME}\--policy-document file://../../../../../policy.json

aws iot attach-thing-principal             \--thing-name$THING_NAME\--principal$CERTIFICATE_ARN

aws iot attach-principal-policy            \--policy-name$POLICY_NAME\--principal$CERTIFICATE_ARN

ポリシーは適宜修正すべきですが、一旦なんでも有りで。

policy.json
{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":["iot:*"],"Resource":["*"]}]}

実行

npm install aws-iot-device-sdk-v2

aws iot describe-endpoint --endpoint-type iot:Data-ATS 
export ENDPOINT=yourendpoint-ats.iot.ap-northeast-1.amazonaws.com
export THING_NAME=mything
./setup-node-v2.sh
cd$THING_NAME/aws-iot-device-sdk-js-v2/samples/node/pub_sub
node dist/index.js --endpoint$ENDPOINT--root-ca rootca.pem --cert certificate.pem --key private_key.pem

別のThingで試すときは、THING_NAMEを書き換えて実行します。

autodetect’: Could not find a JavaScript runtime. See https://github.com/rails/execjs for a list of available runtimes.

$
0
0

masterからサブブランチに切り替えて、

rails s

をしたら、

autodetect’: Could not find a JavaScript runtime. See https://github.com/rails/execjs for a list of available runtimes.

とエラー分が表示され、Javascriptになにか関係が??初めて見るエラー分で、何かしたかと思っていると、、、、

最近、何かとこのエラーに遭遇する人が多いだとか、、、、、

私の場合は、masterからサブブランチに切り替えたときに起こりましたが、これはタイミングが被っただけではないでしょうか。

解決策としては、gemfileに

gem 'therubyracer'
gem 'libv8'

として、bundle installでいけるのかと、思いきやここでエラー分が出たため、node.jsをインストールすることにしました。

一度、上記のgemをインストールできるか試してみてください。

node.jsをhomebrewを使ってインストールする場合は、

 brew install nodejs

でインストールできます。

見事rails sが可能になりました。


discord.js最新版での罠(私的メモ)

$
0
0

discord.jsが11.xから12.xにアップデートされたので自分が引っかかりまくった点をサクッと書いていきます

Nodejsとdjsのバージョン

discord.js 12.xからはどうやらnodejs 12.xからじゃないと出来ないっぽくて見事にハマってました
自分はちょっと都合でnodejs8.10くらいを使ってまして、discord.jsを入れてbotを起動させようとしたら謎の場所でエラー吐いてまして…色々調べてたらバージョン関係で使えなかったことがわかって仕方なくnodejsとnpmを最新版にして使いました

djsの仕様変更

色々と変わったので自分が知ってる限りの仕様変更部分をまとめます。

xxxs系

xxxs系は<client>.channels<message>.guild.rolesなどの複数の情報が入ってるやつです。
11.xでは<client>.guilds.get('channelId')などで出来たのですが、最新版になって<client>.guilds.**cache**.get('channelId')と、cacheをつけないとArrayとして認識されなくなれました(xxxsはmanager系になってました、それでcacheにCollection系が移動されたんだと思います。)

ボイス系

ボイス系は<message>.member.voiceです
前のバージョンは<message>.member.voiceChannel.join()とかだったと思うんですけど、最新版だと<message>.member.voice.channel.join()になってます。
あとはモジュールの変更ですかね
旧モジュール:
node-opus, ffmpeg-binaries
新モジュール:
@discordjs/opus, ffmpeg-static
opusscriptはまだ使えるかは不明。

追記(2020/3/31)

メッセージ関連

最新版djsになって、メッセージを送信する時のやつが完全に消えました

12.xで消えたもの
constch=<message>.channel;ch.sendCode();ch.sendEmbed();ch.sendFile();ch.sendFiles();ch.sendMessage();

これは11.xでもDEPRECATEDが付いていたので多分知ってた人は知ってたと思います。

ついでに、基本メッセージ送信は<message>.channel.send();です、リプライは<message>.reply();です。

message系に関して

embedのRichEmbedMessageEmbedに変更されています。

自分的にはこれ使うよりオブジェクト型で書いた方が直感的かなーと思ってたりしてます

embed-sample.js
<message>.channel.send({title:'Hello',description:'Discord.js!'});

こんな感じの書き方があるのでぜひこの書き方をおすすめしたい

おわり

自分が見てきたとこだとこれくらいですかね、他にもあったような気がするので思い出したら追記しようと思います。

参考

discord.js

これからはじめる、Gatsbyのインストールから静的サイトのビルドまで

$
0
0

Gatsbyは次のWordpressとも言われている、Reactベースのオープンソースフレームワーク。

超高速なWebサイトやブログ、アプリを簡単に作ることができ、今最も注目されているCMSツールでもあります。

ここではGatsbyをこれからはじめる人のために、インストール〜静的サイトのビルドまでをサクッと解説していきます。

Node環境のインストール

まずは環境のチェック。nodeは11.10以降にする必要がある。

brew使ってたので、brewでnodeをアップデートする。

node入ってない人はここからダウンロードできる。

brew upgrade node

// nodeをインストールしてない場合
brew install node

インストールできたらnodeのバージョンチェック。

node -v
v13.11.0

Gatsbyのインストール

npm install -g gatsby-cli
gatsby new gatsby-site
cd gatsby-site
gatsby develop

http://localhost:8000にアクセス。ページが表示されればOK。

Home___Gatsby_Default_Starter.png

静的サイトのビルド(生成)

コマンドで自動的にファイルを作成してくれる。

gatsby build

ビルドすると、puglicフォルダに静的サイトが作成される。あとはサーバーにアップすればWebサイトを表示できる。

Gatsbyのスターターライブラリを使ったインストール

Gatsbyはデフォルトだけでなく、ブログやWebサイト、ポートフォリオサイトなどのスターターライブラリがあり、効率よく開発をスタートできる。

スターターライブラリ
https://www.gatsbyjs.org/starters/?v=2

ブログを作りたい場合はこちら。

gatsby new gatsby-starter-blog https://github.com/gatsbyjs/gatsby-starter-blog

Gatsbyのリソース

ドキュメントがしっかりしてるので、そこ見ればだいたいのことはわかる。

プラグインやライブラリなどもリンクされてるので、公式のリソース集はチェックしておきましょう。

最新情報はTwitterやRedditを活用。

swagger-nodeとDockerで簡単にモックサーバーを構築する

$
0
0

はじめに

APIを呼び出す機能をテストする際に、とりあえずモックサーバーを立てたい。
そんなときに便利なのが、SwaggerのNode.js 製のモジュールであるswagger-nodeです。

SwaggerでAPIを定義しておくだけで、モックサーバーを起動できます。
便利なのは、自前でテストデータを用意しなくても、データ型(string, number, boolean, array, object etc)に応じた適当な値をレスポンスしてくれる点です。

Swaggerの概要やswagger-nodeの使い方はこちらで紹介されているため、詳細は割愛します。
Swaggerとswagger-node

この記事では、Node.jsのインストールが面倒なのと、環境を汚さずに使いたいということで、
swagger-nodeとDockerを組み合わせてモックサーバーを構築する方法を説明します。

前提

dockerとdocker-composeを利用できること

モックサーバーを構築する

Dockerfileの作成

まずはswagger-nodeを内包するDockerイメージを作成します。
最新のLTS版であるNode.js v12だとエラーになるようなので、v10を使用します。
https://github.com/swagger-api/swagger-node/issues/586
npm install --save swagger-routerすればOKとの情報もありましたが、未確認です。

適当なディレクトリでDockerfileを作成します。

FROM node:10# アプリケーションディレクトリを作成するWORKDIR /usr/src/app# swagger-nodeをグローバルインストールするRUN npm install swagger -gENTRYPOINT ["/bin/sh", "-c", "while :; do sleep 10; done"]

これをビルトしてDockerイメージを作成します。

$ docker build . -t swagger-node

swaggerプロジェクトの作成

swaggerプロジェクトを作成するために、swagger-nodeをインストールしたコンテナを起動して、swaggerコマンドを実行します。
作成したswaggerプロジェクトはホストにマウントするために、-v $PWD:/usr/src/appオプションを付けておきます。

$ docker run -d -v $PWD:/usr/src/app --name first-swagger-container swagger-node
$ docker exec -it first-swagger-container swagger project create sample-project
? Framework?
  connect
❯ express
  hapi
  restify
  sails

作成されたsample-projectはこのような構成になります。
swagger.yamlがAPI定義ファイルです。

sample-project/
 ├ api/
 │ ├ controllers/
 │ ├ helpers/
 │ ├ mocks/
 │ └ swagger
 │   └ swagger.yaml
 ├ config/
 ├ node_modules/
 ├ test/
 ├ .gitignore
 ├ app.js
 ├ package-lock.json
 ├ package.json
 └ README.md

なお、ここで作成したコンテナは不要なので削除します。

$ docker rm --force first-swagger-container

Dockerコンテナ起動

同じディレクトリにdocker-compose.yamlを作成して、swagger-mock(mockサーバーコンテナ)とswagger-editor(swagger.yamlを編集するためのコンテナ)を定義します。

docker-compose.yaml
version:"3.6"services:swagger-mock:build:.environment:-CHOKIDAR_USEPOLLING=trueports:-"10010:10010"volumes:-.:/usr/src/appnetworks:examples-net:ipv4_address:172.16.239.103# -m: モックモードとして起動するオプションentrypoint:bash -c "cd sample-project && swagger project start -m"swagger-editor:build:.ports:-"8000:8000"volumes:-.:/usr/src/appnetworks:examples-net:ipv4_address:172.16.239.104# -s:     起動時にブラウザが立ち上がらないようにする# -p:     指定しない場合ポートがランダムになるため8000で固定する# --host: ipv4_addressで指定したIPを設定するentrypoint:bash -c "cd sample-project && swagger project edit -s -p 8000 --host 172.16.239.104"networks:examples-net:name:examples-netdriver:bridgeipam:driver:defaultconfig:-subnet:172.16.239.0/24

swagger-mockのポイントとしては、CHOKIDAR_USEPOLLING=trueを設定している点です。
これは、マウントされているファイルが更新された際にコンテナを自動起動するための設定です。
これによって、swagger.yamlが更新された際に即時にモックサーバーに反映されるようになります。

  • 補足
    • swagger-nodeには、nodemonというモジュールが組み込まれており、ファイル更新を検知したら自動で再起動する仕組み(いわゆるホットリロード)が実装されています。 しかし、Dockerと組み合わせた場合にこれが効かなくなってしまい、Dockerコンテナとしてホットリロードする方法を採用しました。

それではDockerコンテナを起動してみます。

$ docker-compose up -d

コンテナが起動するとswagger-mockがモックサーバーとして利用できる状態になります。
試しにhttp://<コンテナのIPアドレス>:10010に対して、hello API(初回作成時のサンプルAPI)を呼ぶと、レスポンスを返してくれます。

$ curl  http://192.168.99.100:10010/hello?name=Scott
{"message":"Sample text"}

swagger.yamlを更新

目的とするAPIのモックサーバーを起動するには、swagger.yamlを編集する必要があります。
そこでswagger-editorコンテナを使用します。
ブラウザでhttp://<コンテナのIPアドレス>:8000にアクセスするとswagger.yamlの編集画面が開きます。

キャプチャ.PNG

試しに新しいAPIとして次のようにuser API(Get, POST)を定義してみます。
左側の編集パネルに次のように追記します。

swagger.yaml
swagger:"2.0"version:"0.0.1"title:Hello World App# during dev, should point to your local machinehost:localhost:10010# basePath prefixes all resource paths basePath:/# schemes:# tip: remove http to make production-grade-http-https# format of bodies a client can send (Content-Type)consumes:-application/json# format of the responses to the client (Accepts)produces:-application/jsonpaths:...(略).../user:x-swagger-router-controller:UserControllerget:operationId:getUserparameters:-name:idin:queryrequired:truetype:stringresponses:"200":description:Successschema:properties:name:type:stringage:type:numberpost:operationId:postUserparameters:-name:"user"in:"body"required:trueschema:required:-name-agetype:objectproperties:name:type:stringage:type:numberresponses:"200":description:Successschema:properties:message:type:string...(略)...

swagger.yamlの書き方について、基本的にはSwaggerの構文に従えばOKです。
ここで重要なのはx-swagger-router-controllerに任意の値を設定することです。
これはswagger-node独自の項目で、ルーティングするコントローラー(.js)を設定するものです。
swagger project startコマンドで普通に起動する場合は、ここで設定したコントローラー(.js)にルーティングされ、実装された処理に従ってレスポンスが返されます。
モックサーバーと起動する(-mオプションを付与する)場合は、コントローラーが存在しなくても、responsesで定義している型に応じてレスポンスを返してくれるのです。

動作確認

ブラウザの編集画面に書き込んだ時点で、swagger.yamlが更新され、上述したホットリロードの設定によってswagger-mockコンテナが再起動されます。
つまり、即時にモックサーバーに反映されています。
自身でコンテナを再起動する必要はありません。

実際にuser APIを呼んでみます。

$ curl http://192.168.99.100:10010/user?id=xxx
{"name":"Sample text","age":1}

$ curl -X POST -H 'Content-Type:application/json' -d '{"id": "aaa", "name": "hoge", "age": 10}' http://192.168.99.100:10010/user
{"message":"Sample text"}

適当な値が設定されたレスポンスを返してくれました。

所感

APIを呼び出す機能をテストしたい場合、Swagger定義をもらって編集画面を開いてコピペして、x-swagger-router-controllerという若干の設定を行えば、簡単にモックサーバーを起動できます。

あとは、自分でAPIを定義する場合にも、「swagger.yamlを編集→API呼び出しして確認」というライフサイクルを回しやすそうです。

おわりに

Dockerを勉強中なので、無駄な手順が含まれているように思います。(特にswaggerプロジェクトを作成するあたり)
おいおい勉強して更新していきたいと思います。

Pythonのprint出力をNode.jsが認識できない問題について

$
0
0

こんにちは。
こちらの記事は、Python-shellを用いたNode.jsとpythonの連携時に発生したバグの
解決方法について、自らの経験を記述しているものです。
英語が分かる方はこちらの公式ドキュメントを参照していただいた方が正確ですので
ご参照ください。
https://www.npmjs.com/package/python-shell

バージョン
python 3.8.1
pyenv 1.2.17
node.js 13.3.0

問題

Node.jsからpythonのスクリプトに引数を渡して実行させ、実行結果を
Node.jsで受け取ったところ、
python側の出力では正しくjsonデータが送られているのですが、
Node.js側では空文字からなるリスト['']しか受け取れないという現象が発生しました。
また、この現象が発生した際には、pyenvを用いて作成した環境下で行っていたのですが、
system環境で実行したところ、正常に値を受け取ることが判明し、
環境の違いによってコードが動いたり、動かなかったりする状態でした。

該当コードは以下の通りです。
Node.js側のコード

connect_test.js
let{PythonShell}=require('python-shell');letoptions={args:[input_string]};PythonShell.run('data_processing.py',options,function(err,data){if(err)throwerr;console.log(JSON.parse(data));});

Python側のコード

data_processing.py
importjsonimportsysdata_info=sys.argv[1]'''
コード中略
'''print(json.dumps(output_json))

解決策

Node.js側のコードのoption変数に以下のコードを付け足すことで
pyenvで作成した環境下でも動作するようになりました。

//修正前letoptions={args:[input_string]};//修正後letoptions={pythonOptions:['-u'],args:[input_string]};

ドキュメントによると
pythonOptions
 pythonへ渡す際のオプションスイッチの配列で、リアルタイムにprint()の結果を
受け取る際には、['-u']を指定する必要があるとのことです。

必ずつけ忘れないようにしましょう。
以上です。

tsoa で 3rd party 製の型利用時のエラーを無視するモンキーパッチ

$
0
0

tsoa で 3rd party 製の型利用時のエラーを無視するモンキーパッチ

  • この記事の対象者
    • tsoaを活用し始めていて、
    • generateSwaggerSpecgenerateRouteが自力でできて(できかけて)いる人
    • 外部の型つかうんじゃねぇよ、って tsoa に怒られてる人

前提

tsoaという便利なライブラリを、 express wrapper として活用しています。

かいつまんで説明すると、
controller を記述することで、 swagger 定義と express の routes 定義を自動出力でき、
controller と swagger のダブルメンテを行うことなく、快適に REST API 開発が可能になります。

ちなみに例に用いるリポジトリでは、
TypeScript 用 ORMapper である TypeORM と AuroraServerless MySQL を組み合わせてモデル定義運用しています)

サンプルコード

controller.ts
@Route("type-detector/v1")exportclassTypeDetectorControllerextendsController{@Get("resources")@SuccessResponse("200","okdayo")asyncgetResource():Promise<TestEntity[]>{returnawaitTestEntity.find();}}
entities.ts
constMOMENT_OPTS:ColumnOptions={type:"datetime",transformer:{from:(from:Date)=>from&&moment(from),to:(to:Moment)=>to?.toISOString(),},};@Entity({engine:"InnoDB ROW_FORMAT=DYNAMIC"})exportclassTestEntityextendsBaseEntity{constructor(init:Partial<TestEntity>){super();Object.assign(this,init);}@PrimaryGeneratedColumn("increment")seq:number;@Column()dateAt:Date;@Column(MOMENT_OPTS)dateAtM:Moment;}

@Entity@ColumnBaseEntityあたりは軒並み TypeORM の仕組みなので気にする必要はありません
Moment 型のフィールドを持った単なる class か interface と考えてください

問題点

これらの型定義と controller をもとに、
tsoa で generateSwaggerSpec等を実行しようとすると、
以下のように怒られてしまいます

$ ts-node-dev src/swagger/swagger-generator.ts

There was a problem resolving type of 'TestEntity'.(node:46567) UnhandledPromiseRejectionWarning:
Error: No matching model found for referenced type Moment.
If Moment comes from a dependency, please create an interface in your own code that has the same structure.
Tsoa can not utilize interfaces from external dependencies.
Read more at https://github.com/lukeautry/tsoa/blob/master/docs/ExternalInterfacesExplanation.MD

外部 module で定義されたモデルはよみこめねーよ、って言ってますね。
string, Date などの基本的な型(と、その複合体)しか使えない仕様のようです。

この場では具体的にいうと、以下2つの型が該当していました。

  • 時刻フィールドをもつために使っている Moment
  • TypeORM の仕組みを活用するために継承させている BaseEntity

解決したいこと

  • Moment 型のフィールド
    • 出力される SwaggerSpec / Express.Routes 上では、 Date 型として扱いたい
  • BaseEntity 型のフィールド
    • TypeORM の基本的な機能を提供するためのスーパークラスである
    • 継承することでフィールドが増えたりはしないので、単純に無視したい

解決策

Tsoa.TypeResolver が型の分析を司っているみたいです。
その処理に対し、上記の解決したい型であった場合は、
強制的に 別な型としてみなすようなモンキーパッチを作成しました。

swagger-generator.ts
import{generateRoutes,generateSwaggerSpec,RoutesConfig,SwaggerConfig}from"tsoa";import{TypeResolver}from"tsoa/dist/metadataGeneration/typeResolver";(async()=>{externalTypesPatch();constswaggerOpts:SwaggerConfig={schemes:["http","https"],host:"localhost:8080",basePath:"/",entryFile:"src/type-detector-express.ts",specVersion:3,outputDirectory:"src/swagger",controllerPathGlobs:["src/controllers/*.ts"],};constrouteOpts:RoutesConfig={basePath:"",entryFile:"src/type-detector-express.ts",routesDir:"src/swagger",};generateSwaggerSpec(swaggerOpts,routeOpts,undefined,[]).then(()=>console.log("Swagger Refreshed!"));generateRoutes(routeOpts,swaggerOpts,undefined,[]).then(()=>console.log("Routes Refreshed!"));})();/**
 * tsoaでは、3rd party の型が使えない(エラー吐かれる)仕様だが、
 * それでは困る場合に、任意の型を強制的に他の型としてみなすようにするモンキーパッチ
 */functionexternalTypesPatch(){TypeResolver.prototype["originResolve"]=TypeResolver.prototype.resolve;TypeResolver.prototype.resolve=function(...args){consttypeName=this?.typeNode?.typeName?.text;if(typeName){letoverride;switch(typeName){case"Moment":override={dataType:"datetime"};break;case"BaseEntity":override={dataType:"any"};break;case"Function":// このパッチを適用すると、なぜか Function 型?の処理で落ちるようになる事象への対策。// どんな影響があるか知らないが、観測範囲内では期待通り動いているので問題なかろうoverride={dataType:"any"};break;default:break;}if(override){console.log(`TypeResolver.eolsve Override for ${typeName} -> ${JSON.stringify(override)}`);returnoverride;}}returnthis.originResolve(...args);};}

結果

$ ts-node-dev src/swagger/swagger-generator.ts

TypeResolver.eolsve Override for Moment -> {"dataType":"datetime"}
TypeResolver.eolsve Override for Function -> {"dataType":"any"}
TypeResolver.eolsve Override for Moment -> {"dataType":"datetime"}
TypeResolver.eolsve Override for Function -> {"dataType":"any"}
Swagger Refreshed!
Routes Refreshed!


Schema に、 BaseEntity 由来の本来不要である target というフィールドができているのですが、
今回のリポジトリではその程度の汚染は許容範囲として無視します。
このあたりをキッチリきれいに整えるとしたらもう少し工夫が必要になるでしょう。

AWS Command Line Interface (CLI) の出力を `--query` で制御し、パイプラインで活用する

$
0
0

job_tobisyoku.png

これを

% aws organizations describe-organization
{
    "Organization": {
        "MasterAccountEmail": "master-account@your-organization-example.com",
        "MasterAccountArn": "arn:aws:organizations::111111111111:account/o-xxxxxxxxxx/111111111111",
        "MasterAccountId": "111111111111",
        "Id": "o-xxxxxxxxxx",
        "AvailablePolicyTypes": [
            {
                "Status": "ENABLED",
                "Type": "SERVICE_CONTROL_POLICY"
            }
        ],
        "FeatureSet": "ALL",
        "Arn": "arn:aws:organizations::111111111111:organization/o-xxxxxxxxxx"
    }
}

こうする話です

% aws organizations describe-organization --query 'Organization.MasterAccountEmail' --output text
master-account@your-organization-example.com

そしてたとえばこのように応用する

% aws ec2 describe-regions --query 'Regions[].{Name:RegionName}' --output text | xargs -I{} aws ec2 describe-availability-zones --region {} --query 'AvailabilityZones[].{c1:RegionName,c2:ZoneName,c3:State}' --output text
eu-north-1      eu-north-1a     available
eu-north-1      eu-north-1b     available
eu-north-1      eu-north-1c     available
ap-south-1      ap-south-1a     available
ap-south-1      ap-south-1b     available
...(省略)
  • ※全リージョン全AZを列挙

なぜ awscli の --queryオプションにこだわるのか。まず、 JSON 解析をわざわざ他のツールに依存したくないからです。

jqはイマイチだし、JSONなんだから JavaScript で書くのが一番自然な気はするとはいえ、Node.js で実際やってみるとこれはこれで面倒に感じてしまう。

最初の describe-organizationを JavaScript でやるとしたらこんなかんじでしょうか

% aws organizations describe-organization | node -e "
  const data = [];
  process.stdin
    .on('readable', () => {
      let chunk;
      while ((chunk = process.stdin.read()) !== null) {
        data.push( chunk );
      }
    } )
    .on('end', () => {
      process.stdout.write( JSON.parse( data.join( '' ) ).Organization.MasterAccountEmail );
    } );
"

https://nodejs.org/api/process.html#process_process_stdin

readlineだとこのような感じか

% aws organizations describe-organization | node -e "
  const data = [];
  require( 'readline' )
    .createInterface( { input: process.stdin } )
    .on( 'line', ( l ) => {
      data.push( l );
    } )
    .on( 'close', () => {
      process.stdout.write( JSON.parse( data.join( '' ) ).Organization.MasterAccountEmail );
    } );
"

やはり --queryパラメタでやった方が楽な気がします。メリットは次のような感じでしょうか

  • 他ツール非依存
  • パラメタを考案する必要はあるが、比較的低負担で見通しがよい
  • --output textとの組み合わせで以降の処理にもパイプで繋げやすい
  • ※場合によっては --filtersも有用

そもそもこういった出力制御というのは、本質的に頑張りたい処理ではないので、極力最小化させたいわけです

最小化すれば、小さな処理を組み合わせた複雑なこともより書きやすくなります

organizationsでも

aws organizations list-accounts --query 'Accounts[].{Id:Id,Name:Name}' --output text | while read a; do printf "$a\n"; aws organizations list-tags-for-resource --resource-id "`echo $a | awk '{print $1}'`" --query 'Tags[].{Key:Key,Value:Value}' --output table; done
  • ※全メンバーアカウントのタグを列挙
  • ※マスターアカウントで動作します

工夫次第で様々に活用できますが、リージョンやアカウントを横断した組織統制系のオペレーションなども、わざわざブラウザに行くまでもなく、このようにささっと CLI でやりますと、楽しいですよ。

Node.js+Expressのインストール、起動まで

$
0
0

node.jsのインストール
EC2のセキュリティグループの設定で3000番を開けておく

インストール
$ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.34.0/install.sh | bash
$ . ~/.nvm/nvm.sh
$ nvm install node

バージョン確認
$ node -e "console.log('Running Node.js ' + process.version)"

expressのインストール

npm init -y
npm i express -S

サーバーのファイルの記述し、実行する。

$ mkdir src
$ mkdir src/public
$ vi src/app.js
$ vi src/public/index.html
$ node src/app.js
src/app.js
// express モジュールのインスタンス作成constexpress=require('express');constapp=express();// パス指定用モジュールconstpath=require('path');// 3000番ポートで待ちうけるapp.listen(3000,()=>{console.log('Running at Port 3000...');});// 静的ファイルのルーティングapp.use(express.static(path.join(__dirname,'public')));// その他のリクエストに対する404エラーapp.use((req,res)=>{res.sendStatus(404);});
public/index.html
<!DOCTYPE html><head><metaname="viewport"content="width=device-width,initial-scale=1"><title>TEST</title><linkrel="stylesheet"href="/css/sample.css"></head><body><h1>Hello World!</h1><pid="hoge"></p><div><imgsrc="/img/sample.png"></div><script src="/js/sample.js"></script></body></html>

babelの設定ファイルをTypeScriptで書く

$
0
0

表題の通りです。
babelの設定ファイルといえば .babelrcbabel.config.jsでの記述が一般的だと思います。これを babel.config.tsに記述できるようにします。

1. 必要モジュールのインストール

とりあえず webpack前提で必要最低限なものだけインストールします。

npm i -D webpack webpack-cli typescript ts-node @babel/core @types/babel__core babel-loader @babel/preset-env

ほとんど説明するほどのものではないですが、重要なのは @types/babel__coreです。
ここに設定ファイルで使用する型情報が載っています。

2. TypeScriptの設定

tsconfig.jsonを書きます。

tsconfig.json
{"ts-node":{"compilerOptions":{"module":"commonjs","target":"es5",},},}

ここでは ts-nodeの設定だけ行います。

3. babelの設定

ここからが本題です。 babel.config.tsを書きますが、 設定の読み込み方によって書き方も変わってきます。
だいたい次の2通りに分けられるんじゃないでしょうか。

  1. プロジェクトルートにある babelの設定ファイル( .babelrcbabel.config.js)を babelが自動的に読み込む
  2. webpackから実行環境によって手動(設定ファイルのコード上)で呼び出す設定を変える

上記2パターンそれぞれについて説明をしていきます。

3.1. 設定ファイルを自動的に読み込む場合

この場合、 babel.config.tsは次のように書くことができます。

babel.config.ts
import{TransformOptions,ConfigAPI}from'@babel/core'exportdefaultfunction(api:ConfigAPI):TransformOptions{return{presets:['@babel/preset-env',],}}

TransformOptionsConfigAPIという見慣れないものが出てきましたが、 TransformOptionsはお馴染みの babelの設定ファイルの内容ConfigAPIbabelの設定APIのことです。
これを .tsではなく .jsで書くと次のようになります(ドキュメントにほぼ同じものが載っていますが)。

babel.config.js
module.exports=function(api){return{presets:['@babel/preset-env',],};}

ここまでくればあとは babel.config.tsbabelに読み込ませるだけですが、 babelwebpackのように .tsの設定ファイルをいい感じに解釈してくれません。

webpack.config.ts
// ...module:{rules:[{test:/\.[jt]sx?$/,loader:'babel-loader',exclude:/node_modules/,options:{configFile:'babel.config.ts'},},// ...

このように書いたとしても babelbable.config.tsをJSONとして読み込もうとするので構文エラーになります。
そこで tscコマンドで .jsファイルにトランスパイルする必要があります。

npx tsc babel.config.ts

これで babel.config.jsが生成されるので設定ファイルを読み込むことができますが、いちいちトランスパイルするのは面倒ですし、 babel.config.tsbabel.config.jsの2ファイルが並ぶのはあまり気持ちのいいものではないですね。

3.2. webpackから実行環境によって設定を変える場合

3.1. の方法だとあまりすっきりできないので、自分はこちらの方法を採用しています。
特に webpack4では webpack-mergeを使って呼び出す設定ファイルを切り替えるのがメジャーらしい(?)のでこちらの方が合っている気がします。

babel.config.ts
import{TransformOptions}from'@babel/core'// 開発環境用の設定exportconstdev:TransformOptions={presets:['@babel/preset-env',],sourceMaps:true,}// 本番環境用の設定exportconstprod:TransformOptions={presets:['@babel/preset-env',],}
webpack.config.dev.ts
import{Configuration}from'webpack'importmergefrom'webpack-merge'importconfigfrom'./webpack.config.common'import{devasdevBabelConfig}from'./babel.config'constdevConfig:Configuration=merge(config,{mode:'development',devtool:'source-map',devServer:{contentBase:path.resolve(__dirname,'dist'),},module:{rules:[{test:/\.[jt]sx?$/,loader:'babel-loader',exclude:/node_modules/,options:devBabelConfig,},// ...}exportdefaultdevConfig
webpack.config.prod.ts
import{Configuration}from'webpack'importmergefrom'webpack-merge'importconfigfrom'./webpack.config.common'import{prodasprodBabelConfig}from'./babel.config'constprodConfig:Configuration=merge(config,{mode:'production',module:{rules:[{test:/\.[jt]sx?$/,loader:'babel-loader',exclude:/node_modules/,options:prodBabelConfig,},// ...}exportdefaultprodConfig

おわりに

以上、 babelの設定ファイルを TypeScriptで書いてみました。
TypeScriptで記述することでタイポなども減らせていいんじゃないでしょうか。
もっといい方法などあれば教えて頂けるとうれしくなります。

SyntaxError: Identifier 'jest' has already been declared

$
0
0

create-react-appをejectしてyarn testを実行すると掲題のエラーが発生しました。
たまたま直せたので、直し方を書いておきます。

 FAIL  scripts/test.js
  ● Test suite failed to run

    SyntaxError: Identifier 'jest' has already been declared

      at Runtime._execModule (node_modules/jest-runtime/build/index.js:867:68)

jestがすでに定義されているよ、というエラーです。

このエラーだけだと何が起きているかよくわからなかったんですが、 https://github.com/facebook/create-react-app/issues/1319を読むと、scripts/test.jsもjestのテスト対象になっているせいで発生しているようでした。

jestの対象ディレクトリからscriptsを除外すれば直ります。

jest.config.js
module.exports={[...]modulePathIgnorePatterns:["<rootDir>/scripts/"],[...]

TypeORM x AuroraDataAPI - タイムゾーン問題への対処

$
0
0

TypeORM x AuroraDataAPI - TIMEZONE問題への対処

ローカルタイムゾーンがJSTだとしたとき、
DBに書き込まれる時刻が9時間未来になる(JSTのまま書かれてしまう)
DBから読み込まれる時刻が9時間過去になる(JST扱いで読まれてくる)問題への対処です。

前置き

本記事の解決法では、TypeORMの外側で自力で時刻を補正するため、
ライブラリ(TypeORMとAuroraプラグイン)の仕様が変わったり、バグが治った場合は毒になりえます。
そのへんの影響まで自分で管理してやんよシュババってひとむけです。

環境

  • Aurora Serverless MySQL 5.6
    • time_zone, system_time_zone: UTC
  • TypeORM 0.2.24
  • typeorm-aurora-data-api-driver 1.1.8
  • Node 10
    • timezone(TZ環境変数): Asia/Tokyo

上記環境 (TypeORM x AuroraDataAPI) での観測ですが、
DBが違う場合でも同じ症状には同じ対策が適用可能なはずです。

原因

おそらく、 typeorm-aurora-data-api-driverに関する、以下のバグです。

  • Data型を日付時刻Stringに変換して書き込む処理
    • TZ環境変数にもとづいて、出力を補正していない
    • 結果、書き込まれる時刻がズレる(JSTなら+9時間されてる)
    • 配布物でいうと、 typeorm-aurora-data-api-driver.umd.jsformatDate()
      • リポジトリでいうとどこかは、軽く見たけどわからなかった
  • 日付時刻StringをDate型に変換して読み出す処理
    • TZ環境変数にもとづいて、入力を補正していない
    • 結果、読み出される時刻がズレる(JSTなら-9時間されてる)
    • ソースで具体的にどこに該当するかは、ざっくりデバッグしたけどわからなかった。
      • query()中のresult補正かけてるあたりのどこかでやるべき、なのかも。

結果、JST環境からの読み書きだけ見ると、同じだけズレるので 書き込み前=読み込み前 となっていますが、
CreatedDateColumn() 等でDB側で生成された時刻を読みだした場合ズレたり、
そもそもDB直接覗くとUTCのくせにJST扱いで記録されてたりと、
いろいろ気持ち悪い現象が発生してしまっていました。

(補足)原因はわかってるけどコントリビュートしたくない

ざっくり調べた結果、上記原因が解消されれば治るは治るはずなんだけど、
以下の背景からコストが高くなりそうなのでしてません。
issueくらいは投稿してあげてもいいかも。手が空いたらやる。

  • それがTypeORM由来なのかプラグイン由来なのか正直わかりきらない
  • TypeORMのプラグインにコントリビュートするにはTypeORMの知識も必要である
  • プラグインの更新が活発でない(8ヶ月前とか)

対策

@Column()の option である transformer を使い、
書く直前と読んだ直後に自力で時刻を補正します。
Entity インスタンス自体の時刻には影響しないよう気を使っています。

orm-opts.ts
importmoment,{Moment}from"moment-timezone";import{ColumnOptions,EntityMetadata,EntitySchema}from"typeorm";/**
 * Date型のoffsetに基づいて読み書きされているので
 */constTYPEORM_LOCAL_OFFSET=newDate().getTimezoneOffset();/**
 * DBのタイムゾーンは基本UTCの前提とする。
 * 異なる場合は、setCorrectOffset()で任意の補正時間をセットする。
 */constDB_OFFSET=moment.tz("UTC").utcOffset();exportclassOrmOpts{/**
   * TypeORM ごしにDatetime型を書く直前、読んだ直後に、この分数だけ時刻をずらす。
   * DB時刻がUTCにも関わらず、JSTで(+09:00のまま)書き込まれたりする問題への対処のため。
   *
   * 初期値は 0(UTC) - TZ環境変数のoffset(JSTなら540)
   */staticCORRECT_OFFSET=DB_OFFSET-TYPEORM_LOCAL_OFFSET;staticMOMENT:ColumnOptions={type:"datetime",transformer:{from:(from:Date)=>{if(!from)returnfrom;constfromM=moment(from).add({minutes:OrmOpts.CORRECT_OFFSET});returnfromM;},to:(to:Moment)=>{if(!to)returnto;consttoD=to.clone().subtract({minutes:OrmOpts.CORRECT_OFFSET}).toDate();returntoD;}}};staticDATE:ColumnOptions={type:"datetime",transformer:{from:(from:Date)=>{if(!from)returnfrom;constfromM=(OrmOpts.MOMENT.transformerasany).from(from);returnfromM.toDate();},to:(to:Date)=>{if(!to)returnto;to=(OrmOpts.MOMENT.transformerasany).to(moment(to));returnto;}}};}
types.ts
exportclassTestEntityextendsBaseEntity{@PrimaryGeneratedColumn("increment")seq:number;@CreateDateColumn(OrmOpts.DATE)createdAt:Date;@CreateDateColumn(OrmOpts.MOMENT)createdAtM:Moment;@Column(OrmOpts.DATE)dateAt:Date;@Column(OrmOpts.MOMENT)dateAtM:Moment;}

結果

これで、ローカルのTIMEZONEに関わらず、
正しい時刻(このコードではUTC)でDBに書き込まれるようになります。
読み出すときはローカルTIMEZONEに補正されて読まれるようになります。

Node.jsをChrome DevToolsでデバッグする方法

$
0
0

Node.js は標準で Chrome DevTools と接続しデバッグする機能が付いています
普段から使っている Chrome DevTools と同じ感覚で使えます
ヒープメモリのスナップショットも取れるためメモリリーク調査にとても便利だったのでやり方をまとめておきます

対応バージョン

  • node v6.3.0以上

やり方

1. node を --inspectオプションをつけて起動する

node --inspect index.js

2. Chrome で chrome://inspectを開く

こんな画面が開きます
うまく動いていると Remote Target の下に認識しているアプリケーションが表示されます
スクリーンショット 2020-04-01 17.59.04.png

3. Open dedicated DevTools for Nodeをクリック

いつもの Chrome DevTools が開きます

4. デバッグする

ヒープメモリのスナップショットをとったり色々できます

参考

https://nodejs.org/en/docs/guides/debugging-getting-started/
https://medium.com/@paul_irish/debugging-node-js-nightlies-with-chrome-devtools-7c4a1b95ae27

途中で戻したり進めたりできるイテレータ

$
0
0

構文解析する際に、文字列イテレート中に戻したりスキップしたりできるイテレータがあれば便利かと思い作ってみました。
参考: https://developer.mozilla.org/ja/docs/Web/JavaScript/Guide/Iterators_and_Generators

functioniter(pattern){letindex=0return{[Symbol.iterator]:function(){returnthis},next:function(){if(index<pattern.length){return{value:pattern[index++],done:false}}else{return{done:true}}},fetch:function(){returnpattern[index]},back:function(){index>0&&index--},skip:function(){index++},}}

実際に利用したコードは長くなってしまったので、ちょっと無理矢理な使用例を示します。

使用例
letit=iter("Helo!!!!!, world!")for(letcofit){console.log(c)// 'Hel'if(c==='l')break}it.back()// back to 'l'console.log(it.next().value)// 'l'console.log(it.next().value)// 'o'while(it.fetch()==='!')it.skip()// skip '!'for(letcofit){console.log(c)// ', world!'}
実行結果
H
e
l
l
o
,

w
o
r
l
d
!
Viewing all 8833 articles
Browse latest View live