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

NodeJSの画像処理ライブラリ「sharp」を使って画像を連結する

$
0
0

はじめに

NodeJSでImageMagickのappendのようにサイズがバラバラの画像を単純に横並びで連結させたい機会があったのですが、単純すぎて中々記事が見つからなかったので備忘録的な意味合いも込めて書き残します。

やること

  • sharpで画像情報取得、連結

バージョン

NodeJS: v12.14.1
sharp: v0.23.4

成果物

出来上がる画像

output.png

ソースコード

terminal
yarn init -y
yarn add sharp

ソースはこちら
https://github.com/engabesi/appendImages

まず全文を貼ります。

index.js
constsharp=require("sharp");(async()=>{constimagePaths=["images/1.jpg","images/2.jpg","images/3.jpg"];constimageAttrs=[];// 連結する画像の情報取得constpromises=[];constimagePromise=path=>newPromise(asyncresolve=>{constimage=awaitsharp(path);letwidth=0,height=0;awaitimage.metadata().then(meta=>([width,height]=[meta.width,meta.height]));constbuf=awaitimage.toBuffer();resolve({width,height,buf});});imagePaths.forEach(path=>promises.push(imagePromise(path)));awaitPromise.all(promises).then(values=>{values.forEach(value=>imageAttrs.push(value));});// outputする画像の設定constoutputImgWidth=imageAttrs.reduce((acc,cur)=>acc+cur.width,0);constoutputImgHeight=Math.max(...imageAttrs.map(v=>v.height));lettotalLeft=0;constcompositeParams=imageAttrs.map(image=>{constleft=totalLeft;totalLeft+=image.width;return{input:image.buf,gravity:"northwest",left:left,top:0};});// 連結処理sharp({create:{width:outputImgWidth,height:outputImgHeight,channels:4,background:{r:255,g:255,b:255,alpha:0}}}).composite(compositeParams).toFile("output.png");})();

これでnode index.jsで実行すると上記のように左上詰めで画像が連結されます。

解説

まずsharpをimportします。
sharpについて詳しくは公式Documentを見てください。
https://github.com/lovell/sharp
https://sharp.pixelplumbing.com/en/stable/

constsharp=require("sharp");

画像情報取得

連結する画像パスの配列と画像情報を格納する配列を宣言します。

constimagePaths=["images/1.jpg","images/2.jpg","images/3.jpg"];constimageAttrs=[];

画像のwidth, height, bufferを取得するPromiseを作成し、Promise.allで全画像分並列実行させます。
Promise.allthenで先程宣言した画像情報を格納する配列に取得した情報をpushします

constpromises=[];constimagePromise=path=>newPromise(asyncresolve=>{constimage=awaitsharp(path);letwidth=0,height=0;awaitimage.metadata().then(meta=>([width,height]=[meta.width,meta.height]));constbuf=awaitimage.toBuffer();resolve({width,height,buf});});imagePaths.forEach(path=>promises.push(imagePromise(path)));awaitPromise.all(promises).then(values=>{values.forEach(value=>imageAttrs.push(value));});

なぜPromise実行時(imagePromise(path))のthenで処理しないのかですが、こちらのthenは実行が完了した順に走ることになります。
今回は連結される並びを固定にしたいのでPromise.allthenで処理を行っています。

出力画像設定

widthには全画像のwidth合計値、heightには全画像のheightの最大値を取得します

constoutputImgWidth=imageAttrs.reduce((acc,cur)=>acc+cur.width,0);constoutputImgHeight=Math.max(...imageAttrs.map(v=>v.height));

sharpcompositeというメソッドで画像の結合処理等を行います。
そのcompositeメソッドのparam設定を行います

lettotalLeft=0;constcompositeParams=imageAttrs.map(image=>{constleft=totalLeft;totalLeft+=image.width;return{input:image.buf,gravity:"northwest",left:left,top:0};});

inputに画像のbufferを入れます。
gravityは方角で指定します。今回左上詰めで連結させるのでnorthwestを入れます。
lefttopはoffsetをpx単位で設定します。
詳しくはsharpの公式Docを読んでください。
https://sharp.pixelplumbing.com/en/stable/api-composite/

画像出力

最後にsharpで画像を出力します。

sharp({create:{width:outputImgWidth,height:outputImgHeight,channels:4,background:{r:255,g:255,b:255,alpha:0}}}).composite(compositeParams).toFile("output.png");

これでoutput.pngがrootに作成されます。
連結の余白部分が透明でなくてもよかったり、サイズを削減したい場合は、
channelsを4から3に変更してbackgroundのalphaを消したり、
出力画像拡張子をjpgに変更等好きなように変更してください。

まとめ

これでrejectを使ってエラーハンドリングをしていない等、色々ゴリ押し気味ですが画像連結処理を実装できました。
一応車輪の再発明をしていないかとドキュメントを眺めましたがそれらしい処理が見当たりませんでした。
もしあれば教えていただけると助かります。


超初心者がwebpackerについて調べてみたぞ

$
0
0

Ruby on Rails を使って開発を始めた今日この頃。
サーバーサイドはそれとなく出来てきたからそろそろフロントも固めて行こうかしら。なんて思っていた僕はBootstrapをRailsで使おうと考えた。Bootstrapを使うのはいたって簡単。htmlのhead内に以下のリンクをぶち込めばいいだけ

application.html.erb
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css"
    integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">

なんて便利だこと。
しかし、Bootstrapを使うには他にも方法はある。それは以下のリンクにある通りだ。
https://qiita.com/rhistoba/items/f724dae231d7e28bf477
つまり、railsにBootstrapをインストールしてしまう方法である。
基本的には、リンクの記事通りに手順を踏めばインストールできるのであるが、超初心者の僕はいくつかのワカラナイ点があったので自分用のメモとして残すことにした。

前提

僕のスペック

  • プログラミング学習を始めて1ヶ月
  • HTML/CSS/Javascript/Ruby/Railsをprogateで一応学習済
  • Ruby on rails でポートフォリオを作成中

開発環境

  • 端末 : LENOVO ideapad 530S-14ARR
  • OS : Windows 10 Home ver.1809
  • シェル : PowerShell 5.1.17763.771
  • Ruby : 2.6.4
  • rails : 5.2.4.1

そもそもwebpackerってなんだ?

webpackerとはwebpackをRubyで使えるようにしたものらしい。
僕「webpackかぁ~…聞いたことあるよ。…でもよく知らない。」

webpackとは?

調べたところ、webpack というのは

Webpackとは、CSS、JavaScript、画像などを1つのファイルとしてまとめるためのモジュールバンドラーで、node.jsのモジュールの1つです。

とのこと。(引用元:https://www.sejuku.net/blog/68146)
僕「モジュールバンドラーってなんだよぉ(´;ω;`)」
僕「node.jsもよく分かんねーよぉ(´;ω;`)」
調べても分からないが続く。まさに分からないリレー状態。

モジュールバンドラーとは?

次にモジュールバンドラーについて知らべてみた。
モジュールバンドラーとはその名の通り、モジュール(部品)をまとめたものをいうらしい。ここでモジュールとはあるプログラムと考えるのが良さげ。

例えば、現在時刻を表示するプログラムを作るとしよう。ここで、現在時刻を取得するコードと、それを表示するコードと分けて作ったとする。これらのコードを合体させたいのだが、その方法として、表示するためのコードに現在時刻を取得するコードをインポートすることにしよう。この時、インポートされる側のコードをモジュールと呼ぶ

このような便利なコード集をモジュールバンドラーというみたい。
(参考:https://note.com/billion_dollars/n/n596fecfdeb2e)

node.jsとは?

node.jsとはサーバーサイドJavascriptである。
Javascriptは本来、ブラウザで動き見た目を作る(フロントで活躍する)言語なのだが、「この言語でサーバーサイドも作れたら嬉しくね?」という発想から作られたJavaScript 環境なんだとか。

僕「なるほどぉ。すごいじゃんnode.js」

(参考:https://eng-entrance.com/what-is-nodejs#Nodejs-2)

改めてwebpackって何ぞや?

ここまで調べたことをまとめて、もう一度考えるとwebpackとは、

CSS、JavaScript、画像などの部品を上手にまとめて1つにするnode.js(Javascript環境の1つ)のモジュールだ!

ということですね。要するにまとめ上手なお兄さんみたいな(雑)

とはいえ、なんでそんなにまとめたがるのか?

なんとなくwebpackについて分かったけど、まとめることにどんなメリットがあるのかしら?調べてみると通信との関係があるみたいだ。
僕たちがwebサイトを閲覧する時は、自分のPCからサーバーにリクエストを送り、そのレスポンスとしてあるwebページ(htmlファイルとか)を返してくれる。
しかし、そのhtmlが様々なコード(モジュール)や画像、css、javascriptなどを引用してきている時には、それらも同時に送ってやらないと不完全なwebページしか閲覧できない。だからそういう関係するファイルも一緒に送ってやるのだが、これらがバラバラだとサーバーがPCに送るのに時間が掛かってしまうみたい。
ここでそれらのファイルが1つにまとまっていることで素早く通信できるようだ。

まとめ

webpackerのことを調べて、本来の目的であるBootstrapのインストール忘れてた(/ω\)

AWS APIGateway/LambdaとJavascriptで簡易問い合わせサイトをつくる

$
0
0

概要

問い合わせフォーム(javascript) ⇒ APIGateway ⇒ Lambda(Node.js) ⇒ Lambda(Node.js) の流れで簡単な問い合わせサイトを作ります。
contact_form.png
一応レスポンシブにします。
contact_form2.png
バリデーションもあります。
contact_form3.png
contact_form4.png

index.htmlのコーディング

index.html
<!DOCTYPE html><htmllang="ja"><head><metacharset="UTF-8"/><metaname="viewport"content="width=device-width, initial-scale=1.0"/><metahttp-equiv="X-UA-Compatible"content="ie=edge"/><linkrel="stylesheet"href="style.css"/><title>問い合わせ</title></head><body><divclass="header"><ahref="#"class="logo">Hoge Hoge Company</a><aclass="active"href="#">Home</a></div><mainclass="main-container"><h2>問い合わせフォーム</h2><divclass="container"><formid="form"><divclass="row"><divclass="col-20"><labelfor="name">名前</label></div><divclass="col-80"><inputtype="text"id="name"name="name"placeholder="Your name.."/></div></div><divid="name-alert"class="alert-message"hidden>入力値が不正です。</div><divclass="row"><divclass="col-20"><labelfor="email">E-Mail</label></div><divclass="col-80"><inputtype="email"id="email"name="email"placeholder="Your E-Mail.."/></div></div><divid="email-alert"class="alert-message"hidden>入力値が不正です。</div><divclass="row"><divclass="col-20"><labelfor="dept">所属</label></div><divclass="col-80"><inputtype="text"id="dept"name="dept"placeholder="Ex: Example Co., Ltd."/></div></div><divid="dept-alert"class="alert-message"hidden>入力値が不正です。</div><divclass="row"><divclass="col-20"><labelfor="body">内容</label></div><divclass="col-80"><textareaid="body"name="body"placeholder="お気軽にお問い合わせください。"style="height:200px"></textarea></div></div><divid="body-alert"class="alert-message"hidden>入力値が不正です。</div><divclass="row"><buttonid="submitBtn">送信</button></div></form></div><!-- The Modal --><divid="myModal"class="modal"><!-- Modal content --><divid="modal-content"><spanclass="close">&times;</span><pid="result"></p><pid="detail"style="font-size: small;"></p></div></div></main><script src="main.js"defer></script></body></html>

CSSのコーディング

style.css
*{margin:0;padding:0;font-family:"Hiragino Kaku Gothic Pro","ヒラギノ角ゴ Pro W3","メイリオ",Meiryo,"MS Pゴシック",sans-serif;}/* Style the header with a grey background and some padding */.header{display:flex;flex-flow:rowwrap;align-items:center;justify-content:space-between;background-color:#000000;padding:20px10px;}/* Style the header links */.headera{color:#f2f2f2;text-align:center;padding:12px;text-decoration:none;font-size:18px;line-height:25px;border-radius:4px;}/* Style the logo link (notice that we set the same value of line-height and font-size to prevent the header to increase when the font gets bigger */.headera.logo{font-size:25px;font-weight:bold;}/* Change the background color on mouse-over */.headera:hover{background-color:#ddd;color:black;}/* Style the active/current link*/.headera.active{background-color:dodgerblue;color:white;}/* Add media queries for responsiveness - when the screen is 500px wide or less, stack the links on top of each other */@mediascreenand(max-width:500px){.header{justify-content:center;}.headera{padding:12px;}}.main-container{width:90%;margin:auto;}.main-containerh2{margin-top:12px;}/* Style inputs, select elements and textareas */input,textarea{width:100%;padding:12px;border:1pxsolid#ccc;border-radius:4px;box-sizing:border-box;resize:vertical;}.alert-red{border:#ff45002pxsolid;}.alert-green{border:#00ff7f2pxsolid;}.alert-message{color:#ff4500;font-size:small;text-align:end;margin-bottom:10px;}/* Style the label to display next to the inputs */label{padding:12px12px12px0;display:inline-block;}/* Style the submit button */#submitBtn{background-color:#4caf50;color:white;padding:10px18px;border:none;border-radius:4px;cursor:pointer;font-size:15px;float:right;}/* Style the container */.container{border-radius:5px;background-color:#f2f2f2;padding:20px;}/* Floating column for labels: 25% width */.col-20{float:left;width:20%;margin-top:6px;}/* Floating column for inputs: 75% width */.col-80{float:left;width:80%;margin-top:6px;}/* Clear floats after the columns */.row:after{content:"";display:table;clear:both;}/* Responsive layout - when the screen is less than 600px wide, make the two columns stack on top of each other instead of next to each other */@mediascreenand(max-width:600px){.col-20,.col-80,#submitBtn{width:100%;margin-top:0;}}/* The Modal (background) */.modal{display:none;/* Hidden by default */position:fixed;/* Stay in place */z-index:1;/* Sit on top */left:0;top:0;width:100%;/* Full width */height:100%;/* Full height */overflow:auto;/* Enable scroll if needed */background-color:rgb(0,0,0);/* Fallback color */background-color:rgba(0,0,0,0.4);/* Black w/ opacity */}/* Modal Content/Box */#modal-content{background-color:#fefefe;margin:15%auto;/* 15% from the top and centered */padding:20px;width:80%;/* Could be more or less, depending on screen size */}.request-loading{border:1pxsolid#888;}.request-success{border:1pxsolid#00ff7f;}.request-fail{border:1pxsolid#ff4500;}/* The Close Button */.close{color:#aaa;float:right;font-size:28px;font-weight:bold;}.close:hover,.close:focus{color:black;text-decoration:none;cursor:pointer;}

javascriptのコーディング

変数URLはAPIGatewayで発行してから実装します。

main.js
constURL='https://';// APIGatewayで作成したURLconstLOADING='処理中...';constRESULT_OK='リクエストを受け付けました。';constRESULT_OK_DETAIL="正常に処理が完了すると 'support@hogehoge.com' からメールが配信されます。";constRESULT_NG='リクエストの受付に失敗しました';constRESULT_NG_DETAIL='大変申し訳ありません。担当者へ直接お問い合わせください。';constDOMAIN='@sample.com';document.getElementById('email').value=DOMAIN;(()=>{constmodal=document.getElementById('myModal');constmodalContent=document.getElementById('modal-content');constsendBtn=document.getElementById('submitBtn');constspan=document.getElementsByClassName('close')[0];constresult=document.getElementById('result');constform=document.getElementById('form');constnameAlert=document.getElementById('name-alert');constemailAlert=document.getElementById('email-alert');constdeptAlert=document.getElementById('dept-alert');constbodyAlert=document.getElementById('body-alert');constresultDetail=document.getElementById('detail');constinputValueClear=()=>{form.name.value='';form.name.setAttribute('class','');form.email.value=DOMAIN;form.email.setAttribute('class','');form.dept.value='';form.dept.setAttribute('class','');form.body.value='';form.body.setAttribute('class','');result.innerHTML='';};// When the user clicks on <span> (x), close the modalspan.onclick=()=>{if(result.innerHTML!==LOADING){modal.style.display='none';if(result.innerHTML===RESULT_OK)inputValueClear();}};// When the user clicks anywhere outside of the modal, close itwindow.onclick=event=>{if(event.target===modal){if(result.innerHTML!==LOADING){modal.style.display='none';if(result.innerHTML===RESULT_OK)inputValueClear();}}};sendBtn.addEventListener('click',asyncevent=>{event.preventDefault();constname=form.name.value.trim();form.name.value=name;constemail=form.email.value.trim();form.email.value=email;constdept=form.dept.value.trim();form.dept.value=dept;constbody=form.body.value.trim();form.body.value=body;// validation checkif(validation(name,email,dept,body))return;result.innerHTML=LOADING;modalContent.setAttribute('class','request-loading');modal.style.display='block';constjsonData=JSON.stringify({name,email,dept,body});try{constres=awaitfetch(URL,{method:'POST',mode:'cors',cache:'no-cache',body:jsonData,headers:{'Content-Type':'application/json; charset=utf-8'}});console.log('Response!!',res.status);modalContent.setAttribute('class','request-success');result.innerHTML=RESULT_OK;resultDetail.innerHTML=RESULT_OK_DETAIL;}catch(error){console.log(error);modalContent.setAttribute('class','request-fail');result.innerHTML=RESULT_NG;resultDetail.innerHTML=RESULT_NG_DETAIL;}});constvalidation=(name,email,dept,body)=>{letvalidationResult=false;if(!name.length){form.name.setAttribute('class','alert-red');nameAlert.hidden=false;validationResult=true;}else{form.name.setAttribute('class','alert-green');nameAlert.hidden=true;}if(!email.length||!/^[^@]+@sample.com$/.test(email)){form.email.setAttribute('class','alert-red');emailAlert.hidden=false;validationResult=true;}else{form.email.setAttribute('class','alert-green');emailAlert.hidden=true;}if(!dept.length){form.dept.setAttribute('class','alert-red');deptAlert.hidden=false;validationResult=true;}else{form.dept.setAttribute('class','alert-green');deptAlert.hidden=true;}if(!body.length){form.body.setAttribute('class','alert-red');bodyAlert.hidden=false;validationResult=true;}else{form.body.setAttribute('class','alert-green');bodyAlert.hidden=true;}returnvalidationResult;};form.name.addEventListener('input',event=>{constname=event.target.value.trim();if(!name){event.target.setAttribute('class','alert-red');nameAlert.hidden=false;}else{event.target.setAttribute('class','alert-green');nameAlert.hidden=true;}});form.email.addEventListener('input',event=>{constemail=event.target.value.trim();event.target.value=email;if(!email||!/^[^@]+@sample.com$/.test(email)){event.target.setAttribute('class','alert-red');emailAlert.hidden=false;}else{event.target.setAttribute('class','alert-green');emailAlert.hidden=true;}});form.dept.addEventListener('input',event=>{constdept=event.target.value.trim();event.target.value=dept;if(!dept){event.target.setAttribute('class','alert-red');deptAlert.hidden=false;}else{event.target.setAttribute('class','alert-green');deptAlert.hidden=true;}});form.body.addEventListener('input',event=>{constbody=event.target.value.trim();if(!body){event.target.setAttribute('class','alert-red');bodyAlert.hidden=false;}else{event.target.setAttribute('class','alert-green');bodyAlert.hidden=true;}});})();

AWS SES(Simple Email Service)を設定

※ドメインはRoute53で取得済みの前提
※併せてACM(AWS Certificate Manager)で証明書を取得済み
※SESは送信のみの設定です。

  1. AWSへログイン
  2. SESのコンソールへ移動
  3. リージョンはバージニア北部を選択
  4. 左のナビゲーションからDomainsを選択
    image.png
  5. image.png をクリック
  6. ↓のように入力。自分のドメインとDKIM設定をチェックして、image.pngをクリックimage.png
  7. 次のモーダルではCNAMEやTXTが表示され、登録しろと指示が出る。Route53を利用している場合は、このモーダル上でDNSの登録がすべて完了する。
  8. こんな感じになればOK!(※現時点ではサンドボックス上での制限された利用が可能)
    image.png
  9. 左のナビゲーションでEmail Addressesを選択
    image.png
  10. Veryfy a New Email Addressでサンドボックス上で利用できるメールアドレスを登録する。
  11. 登録したメールアドレスをSend a Test Emailで登録したドメインからメールが送られるかテストして正常な動作を確認

最後に起動するLambdaを作成

  1. Lambdaのコンソールページへ移動
  2. image.png をクリック
  3. image.png を選択
  4. image.png 適当な関数名を入力
  5. ランタイムはNode.js 12.xを選択
  6. そのほかはデフォルトのままでOK
  7. image.png をクリック
  8. 環境変数へ配信元のアドレスを設定
    image.png
  9. 実行ロールにはSESの権限を許可
    image.png
  10. 関数コードへ↓のコードを実装
index.js
'use strict'constSES=require("aws-sdk/clients/ses");constses=newSES({region:"us-east-1"});// 米国東部(バージニア北部)constFROM=process.env.FROM;// 環境変数から取得exports.handler=async(event)=>{console.log(event);constTO=[event.email];constparams={Destination:{ToAddresses:TO},Message:{Body:{Text:{Data:[event.dept+''+event.name+'','','Hoge Hoge Companyです。','お問い合わせしていただきありがとうございます。','下記の内容で承りました。','','[お問い合わせ内容]'+"\n"+event.body,].join("\n"),Charset:"utf-8"}},Subject:{Data:'受付完了:Webからの問い合わせ',Charset:"utf-8"}},// FromSource:FROM};constresult={statusCode:200};try{constresponse=awaitses.sendEmail(params).promise();console.log('Response: ',response);result.body='OK!!!!!';}catch(error){console.log('Error: ',error);result.statusCode=error.code;result.body=error.message;}returnresult;};

process.envでは環境変数で設定したキーと値が利用可能(暗号化も可能)

最後に起動するLambdaのテスト

  1. image.pngテストイベントの選択をクリック
  2. ↓のようにテストを作成(※emailキーにはSESで登録したアドレスを記載すること)
    image.png
  3. image.png をクリックしてテスト実行
  4. 成功となればOK。失敗ならログを確認。
    image.png
  5. テストでemailキーに指定したアドレスにメールが配信されていればOK

最初に起動するLambdaを作成

  1. Lambdaのコンソールページへ移動
  2. image.png をクリック
  3. image.png を選択
  4. image.png 適当な関数名を入力
  5. ランタイムはNode.js 12.xを選択
  6. そのほかはデフォルトのままでOK
  7. image.png をクリック
  8. 環境変数へ配信元のアドレスと受付先のアドレスを入力
    image.png
  9. 実行ロールはSESとLambdaの権限を追加
    image.png
  10. 関数コードへ↓のコードを実装
index.js
'use strict'constSES=require("aws-sdk/clients/ses");constses=newSES({region:"us-east-1"});// 米国東部(バージニア北部)constTO=[process.env.TO];// 環境変数からの値を取得constFROM=process.env.FROM;// 次のLambdaを起動するための設定constLambda=require("aws-sdk/clients/lambda");constlambda=newLambda({region:"ap-northeast-1"});exports.handler=async(event)=>{constname=event.form.name;constemail=event.form.email;constdept=event.form.dept;constbody=event.form.body;constsesParams={Destination:{ToAddresses:TO},Message:{Body:{Text:{Data:['[名前] : '+name,'[メールアドレス] : '+email,'[所属部署] : '+dept,'[お問い合わせ] : '+"\n"+body,].join("\n"),Charset:"utf-8"}},Subject:{Data:'Webからの問い合わせ',Charset:"utf-8"}},// FromSource:FROM};// 次のLambdaに送るデータconstpayload={name,email,dept,body};console.log('payload: ',payload);constlambdaParams={FunctionName:"inpuirySendMailForm_2nd",// 最後に起動するLambdaの名称InvocationType:"Event",Payload:JSON.stringify(payload)};constresult={statusCode:200};try{constresponse=awaitses.sendEmail(sesParams).promise();console.log('Response: ',response);constcallLambda=awaitlambda.invoke(lambdaParams).promise();console.log("Lambda Response: ",callLambda);result.body='OK!!!!!';}catch(error){console.log('Error: ',error);result.statusCode=error.code;result.body=error.message;}returnresult;};

最初に起動するLambdaのテスト(次のLambdaも起動する)

  1. image.pngテストイベントの選択をクリック
  2. ↓のようにテストを作成
    image.png
  3. image.png をクリックしてテスト実行
  4. 成功となればOK。失敗ならログを確認。
    image.png
  5. テストでemailキーに指定したアドレスに受付完了メールが配信され、環境変数でTOに指定した受付先アドレスへ問い合わせ内容が配信されていればOK

APIGatewayの設定

  1. image.pngをクリック
  2. REST API構築を選択
    image.png
  3. API名説明を入力してAPIの作成をクリック(他はデフォルト)
    image.png
  4. アクション⇒リソースの作成を選択⇒リソース名を入力(リソースパスは自動入力)⇒CORSを有効化⇒リソースの作成をクリック
  5. アクション⇒メソッドの作成⇒POSTを選択⇒セットアップで最初に起動するLambda関数名を入力⇒保存をクリック
  6. 統合リクエストをクリック⇒マッピングテンプレートを追加⇒Content-Typeはapplication/json⇒テンプレートには↓を入力⇒保存をクリック
{"form":{"name":"$util.escapeJavaScript($input.path('$.name'))","email":"$util.escapeJavaScript($input.path('$.email'))","dept":"$util.escapeJavaScript($input.path('$.dept'))","body":"$util.escapeJavaScript($input.path('$.body'))"}}

APIGatewayのテスト

  1. テストを実行する
    image.png
  2. Lambdaが起動してメールが配信されればOK

APIGatewayのステージを作成

今回はv1というステージを作成

APIGatewayのデプロイ

リソース⇒アクション⇒APIのデプロイを選択⇒v1ステージを選択して⇒デプロイをクリック

発行されたURLをjavascriptのURL変数へ割り当て

全体の動作確認

問い合わせフォームへSESで登録したアドレスと必要事項を入力し、送信ボタンをクリックして動作を確認

実用化に向けて

  • LambdaのSDK呼び出しは動作確認ができているバージョンが呼び出されるようにする必要あり。
  • SESの上限緩和申請が必要。
  • APIGatewayでリソースポリシーを指定して不要なアクセスを防ぐ

最後に

かなり大雑把に書きました。
小規模利用なら十分かと思います。
役に立つかな?
計画性なく作ってしまったのでここからブラッシュアップ

DynamoDBのテーブルデータをNodejsでS3にBackupする

$
0
0

はじめに

DynamoDBに大量にあるテーブルのデータを一括でS3にバックアップしたい。。みたいな状況があったので、メモとして書いておきます。

ちなみにテーブルが少量であれば、Data Pipelineを使って簡単に出来ます!!
aws-sdkではData Pipelineの細かいオプションは設定出来なかったようなので、nodejsでData Pipelineを操作することはしていません。

環境

  • Nodejs v11.1.0
  • DynamoDB

事前準備

バックアップを行うためにS3に専用のバケットを作成しておいてください。
この記事ではbackupsというバケットを作成したという例で進めます。

プロジェクトを作成

適当ですが、dynamodb-backuperみたいなディレクトリを作成して進めていきます。

$ mkdir dynamodb-backuper

$ cd dynamodb-backuper

$ npm init

$ npm install aws-sdk dynamodb-backup-restore --save$ touch export.js restore.js

export.js

export.jsはその名通りDynamoDBから全テーブルのbackupを行うファイルです。

'use strict'constAWS=require('aws-sdk')constDynamoDB=newAWS.DynamoDB({region:'ap-northeast-1'})asyncfunctionmain(){lettables=awaitgetAllTableLists()tables.forEach((v)=>{exportToS3(v)})}// 実行main()// Get all table names from DynamoDBasyncfunctiongetAllTableLists(){letparams={}lettables=[]while(true){letresponse=awaitDynamoDB.listTables(params).promise()tables=tables.concat(response.TableNames)if(!response.LastEvaluatedTableName){break}else{params.ExclusiveStartTableName=response.LastEvaluatedTableName}}returntables}// Function to perform backupfunctionexportToS3(tableName){constBackup=require('dynamodb-backup-restore').Backupletconfig={S3Bucket:'backups',// 必須 - バケット名 S3Prefix:tableName,// 任意 - 保存するS3のサブフォルダ名S3Encryption:'AES256',// 任意 - 暗号化S3Region:'ap-northeast-1',// 必須 - リージョンDbTable:tableName// 必須 - エクスポートするテーブル名}letbackup=newBackup(config)backup.full()}

以下でエクスポートを実行出来ます。

$ node export.js

restore.js

restore.jsもそのままですが、export.jsでS3にエクスポートしたデータを今度はDynamoDBの指定のテーブルにインポートします。

リストアを行う前に事前にインポートするテーブルを作成しておく必要があります。
今回はnewTest1というテーブルを作ったとして進めます。
この時に注意が必要なのが、データ量によっては無料枠ではキャパシティーレベルが超えてしまうため、データサイズに応じてオンデマンドに変更してください。

以下のコードでは、execRestore関数の第一引数にS3にバックアップをとったtest1というサブフォルダを指定し、インポート用に作成したnewTest1というテーブル名を第二引数に設定します。

'use strict'functionexecRestore(bucketName,tableName){constRestore=require('dynamodb-backup-restore').Restoreletconfig={S3Bucket:'backups',// 必須S3Prefix:bucketName,// 任意S3Region:'ap-northeast-1',// 必須DbTable:tableName,// 必須DbRegion:'ap-northeast-1',// 必須}Restore(config)}// 実行execRestore('test1','newTest1')

以下でリストアを実行出来ます。

$ node restore.js

NPMでsudoを回避する技

$
0
0

UbuntuにNodeJSをインストールしたらまさかの不便さに遭遇

先日UbuntuにNodeJSやらNPMをインストールしたところ
グローバルにモジュールをインストールしようとしたら失敗しました。

sudoで実行すればインストールできたのですがGatsby.jsのチュートリアルをやっていたときに

  • 遭遇したエラーの一部
gyp ERR! stack Error: EACCES: permission denied, mkdir '/home/【ユーザー名】/scotch-blog/node_modules/sharp/build'

このエラーが発生したときに
NodeJSをまたインストールしないといけないのかなと暗い気持ちになっていましたが、以下の方法でnpmパッケージをインストールするディレクトリを指定することで事なきを得ました

以下はそのやり方を共有します

グローバルなモジュールをインストールするディレクトリは指定できる!!

元ネタのリンク

以下にエラー回避をマニュアルでやる方法をお伝えします

1. $HOMEのディレクトリにnpmのモジュール格納用ディレクトリを追加する

 mkdir ~/.npm-global

2. npm set configで今作ったディレクトリを指定する

 npm config set prefix '~/.npm-global'

3. ~/.profileで環境変数を追加する

 export PATH=~/.npm-global/bin:$PATH

4.$PATHを更新する

 source ~/.profile

以上です。お疲れさまでした。元ネタのリンクではそのほかにもnvmを使った方法などを紹介しています。NodeJSのバージョン管理ができる代物なので時間があればそちらにも挑戦したいところです。

更新履歴

  • 2020/1/14 新規作成

めっちゃ簡単にNodejsでCLIツールを作る

$
0
0

やりたいこと

そこまで凝ったことをしないCLIツールをNodejsで作りたい!
そこでcacを使ってそれを実現したいと思います。

こちらの記事で作成したプロジェクトを元に進めたいと思います。

パッケージとindex.jsの追加

$ npm i cac

$ touch index.js

cacの基本

constcli=require('cac')()cli.command('command','説明')// コマンド.option('--opt','説明')// 引数オプション.action((options)=>{// 実行したい処理console.log(options)// 引数の値をオブジェクトで受け取れる})cli.help()cli.parse()

ここまで書いたら、以下を実行することで確認出来る。

$ node index.js --help

さらにコマンドも実行できる。
以下のように変更します。

constcli=require('cac')()constexec=require('child_process').execcli.command('command','説明')// コマンド.option('--opt','説明')// 引数オプション.action((options)=>{// 実行したいコマンド(例でpwdコマンドを実行する)exec('pwd',(err,stdout,stderr)=>{if(err){console.log(err);}console.log(stdout);})})cli.help()cli.parse()

上記のようにしたら、node index.js commandとすることでpwdコマンドが実行されるかと思います。

index.js

それではいよいよ、前の記事で作成したexport.jsrestore.jsを操作する処理を記述していきます。
追加したindex.jsに追記していきます。

constcli=require('cac')()constexec=require('child_process').exec// エクスポート用のコマンドcli.command('export','run export from DynamoDB to S3').action(()=>{exec('node export.js',(err,stdout,stderr)=>{if(err){console.log(err);}console.log(stdout);})})// リストア用のコマンドcli.command('restore','run restore from S3 to DynamoDB').option('--bucket <name>','Bucket name used for import').option('--table <name>','Table name to import').action((options)=>{if(!options.bucket&&!options.table){console.log('「--bucket」オプションと「--table」を指定してください')}elseif(!options.bucket){console.log('「--bucket」オプションを指定してください')}elseif(!options.table){console.log('「--table」オプションを指定してください')}else{exec(`node restore.js ${options.bucket}${options.table}`,(err,stdout,stderr)=>{if(err){console.log(err);}console.log(stdout);})}})cli.help()cli.parse()

restore.js

restore.jsが現在はファイルに直接バケット名とテーブル名を記述していますが、これをコマンドライン引数で処理するように変更します。

'use strict'functionexecRestore(bucketName,tableName){constRestore=require('dynamodb-backup-restore').Restoreletconfig={S3Bucket:'backups',// 必須S3Prefix:bucketName,// 任意S3Region:'ap-northeast-1',// 必須DbTable:tableName,// 必須DbRegion:'ap-northeast-1',// 必須}Restore(config)}// CLIからコマンドライン引数を受け取って実行するexecRestore(process.argv[2],process.argv[3])

操作方法

# exportの実行$ node index.js export# restoreの実行$ node index.js restore --bucketバケット名 --tableテーブル名

で無理やりCLIを導入することが出来ました。

Dockerでnode.jsプロジェクトの開発環境構築

$
0
0

動機

  • 新しくMac Book Pro 2019を購入。nodeやpytonの開発環境構築にあたっては, 素の環境を綺麗なままにしたいことや、いろいろな環境での開発を行いためDockerを用いることにした。
  • Docker周りの設定や概念の理解で少し躓いたのでここに記す。エキスパートには当たり前すぎる内容だとしてもだ。

やりたいこと

node.js, angular, ionic, webpack を使ったweb appやPWAの開発を、
- ホストのローカル環境を汚すことなく(Dockerを用い)
- ホストで動くVS Codeを用いたコーディング&デバッグしつつ
- ホストのターミナルからgitを用いたバージョニングしつつ
- 仮想マシンでionic server等で起動したサーバとその上で動くweb appを、ホストのブラウザからアクセスし動作確認・デバッグ

手順

1. Docker desktop for Macをインストール

https://hub.docker.comからダウンロード。ユーザ登録も済ます。

2. プロジェクトのディレクトリ構造

最終的に下記になります。順番に作っていきましょう。

- Dockerfile
- docker-compose.yml
- app
   |- ここに諸々のソースを置く

3. Dockerfile

Dockerfie : Imageを作成する設定ファイル
Image : 仮想マシンの雛形。OS, インストールされたソフトウェアセット等からなる。

node.js入りのalpine linuxのimageをベースに、ionicとcordovaをインストールしたimageを作成する。

alpine linux : 超軽量linux. Dockerの仮想マシンOSとして頻繁に利用されている.

host
$ cat Dockerfile
FROM node:10.13-alpine
WORKDIR /app
RUN npm install-g ionic cordova
CMD ["sh"]

下記コマンドでDocker imageを作成します。最初に実行する場合ローカルにnode:10.13-alpineのイメージがないのでdocker hubからダウンロードされてきます。2回目以降はローカルのイメージが使われます。

host
$ docker build .-t watashino-image

-tはイメージの名前を与えるオプションです。適当につけてOK。
正常に完了すればdocker imagesでimageが作成されていることが確認できます。nodeのイメージもダウンロードされローカルに保存されています。

host
$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
watashino-image                latest              da6f921ba540        2 minutes ago       133MB
node                10.13-alpine        93f2dcbcddfe        13 months ago       70.3MB

このimageが仮想マシンの雛形となります。

4. docker-compose.yml

続いて、imageを実際に起動するためのファイルを記述します。

docker-compose.yml : docker-composeというコマンドへ与える設定ファイル

docker-compose :imageからcontainerを作成・起動するプログラム。

container : 厳密な話は置いておいて、ざっくりいうと、雛形であるimageから作成された仮想マシンを表す。型とそのインスタンス、という関係に近い。

実際のところ、"docker"コマンドだけでもcontainerの作成・起動はできるが、オプションの指定等が面倒だったり複数のcontainerの起動等も同時に行えるdocker-composeの方が便利そうなので、最初からこちらを採用。

host
$ cat docker-compose.yml
version: '3'
services:
  app:
    build: .
    image: watashino-image
    volumes:
      - ./app:/app
    ports:
      - "8100:8100"tty: true

下記のコマンドでcontainerの作成・起動。まだローカルにappフォルダを作っていない場合には作っておく。

host
$ mkdir app
$ docker-compose up -d

何事もなくコマンドが終了すれば成功。仮想マシンはバックグラウンドで動作している。下記コマンドで実行中のcontainerの一覧が見れる。

host
$ docker ps 

お気づきの方もいるでしょうが、実はstep 3のdocker buildのコマンドは実行しなくてもよかったのです。"build: ."の行はビルドに関する指示で、docker-composeファイルは指定されたimageが存在しない場合には自動的にDockerfileを参照してimageのbuildと、続いてcontaierの作成と起動を行います。imageとcontainerの関係を明らかにするために冗長な手順とさせてもらいました。

他の設定の意味:

volumesローカルの./appフォルダと仮想マシンにおける/app (root直下)を結びつけます。これによより仮想マシンとホストマシンのファイルが同期されるようになります。

portsポートフィワーディングの設定。仮想マシンのport 8100とローカルのport 8100を結びつけます。仮想マシンにおけるlocalhost:8100に、ホストマシンからlocalhost:8100でアクセスできるようになります。(ただし後述の注意点あり)

ttyこれがtrueではないと、containerは起動後すぐに終了してしまう。

5. 動作中の仮想マシンを操作

バックグランドで実行されているcontainerには下記のコマンドでアクセスできます。

host
$ docker exec app sh

何をしているかというとappサービス(docker-compose.xmlで指定した名称)でshの実行を指示しています。上記を実行するとシェルで仮想環境の中をいじれるようになります。

仮想環境から退出するには

container
# exit

します。

6. gitからソースを持ってくる&VS Codeで読み込む

app以下にソースコードを持ってきましょう。ローカルのappフォルダで通常通りgitでソースファイルを持ってきます。

host
$ cd app
$ git clone アドレス

仮想環境にgitをinstallすれば仮想環境で直接gitを使えるでしょうが(未確認)、わざわざそうする必要もないかと思います。

ファイルは同期されていますから、特に意識することなくローカル環境のVS Codeを開き、ローカルのappフォルダを開けばOK

7. 仮想環境でビルド&実行

host
$ docker-compose exec app sh

で仮想環境に入りappフォルダを覗けばソースコードが見えるはずです。あとは通常通りの開発ルーチンに従います。ここではionicプロジェクトの例では、下記のようなことを行います。ionic(ng)のビルドインサーバをポートフォワーディングで設定したportで起動しています。

container
# npm install# ionic serve -p 8100 --address 0.0.0.0

これによりホスト環境から localhost:8100 で仮想環境内のWEBサーバにアクセスできます。--address 0.0.0.0 を付けないとホストからは見れません。理由はこちらをご参考ください。

8. Containerの停止, 再開

開発が終わったら起動させておくのは無駄でしょうから、Containerを停止します。

host
$ docker-component stop

で停止,

host
$ docker-component up -d

で再開します。なお、containerを停止すると、Continerの起動中に作成したファイルは、volumesでホストのディレクトリに保存しているもの以外は全て消えてしまいます。

containerのライフサイクル、データの永続化については以下のような記事を参考ください。
- https://qiita.com/gounx2/items/23b0dc8b8b95cc629f32
- https://qiita.com/onokatio/items/fcc9f8f94f8533bb030a

なお今回はホストのファイルシステムとの同期、という方法のみを扱っていますが、そのほかにも永続化手段もある模様(未トライ)

9. container, imageの削除

ニッチもさっちも行かなくなった場合、全ての過去を消し去りたいと思ったら、docker-component downコマンドを使います。下記が参考になります。

気軽に環境丸ごと削除してやり直せるというのが、Dockerを用いた開発の最大のメリットでしょう。

以上です。

ブロガーの買ってよかったものを集計してランキング化したら本当にいいモノが見つかった

$
0
0

動機

ジャバ・ザ・ハットリ(@jabba )さんの技術書ランキングサイトをQiita記事の集計から作ったら、約4000冊の技術本がいい感じに並んだがとても面白かった。

特に面白く感じたのは「意見を集めてランキング形式に整える」と、良いモノはちゃんと上位に入ってくるところ。

この「意見を集めてランキング形式に整える」をブロガーの買ってよかったもの紹介記事に応用したら、紹介頻度の高い本当にいいものが見つかるのでは?

最近はAmazonのレビューもサクラコメントで溢れがちで、本当に良いものを見つけるのが難しくなっている世の中に多少のニーズはあるのでは?

と思ったのが今回のそもそもの動機。

作ってみた

実際にブロガーの買ってよかったもの紹介記事から紹介商品を集計してランキング形式で並べたのがこちら。

ブロガー買ってよかったものランキング|グッドグッズランク

この記事を書いている時点での上位5つのアイテムを試しに抜粋してみると以下の通りで、名実ともに良いものが並んだ。

1. Anker PowerCore Fusion 5000
2. Apple AirPods Pro
3. Fire TV Stick 4K
4. Fire TV Stick
5. ロジクール ワイヤレスマウス トラックボール 無線 M570t

1,4,5は私も実際に持っているので、「私ってモノを見る目あるのかも?」と勝手に錯覚するところだった。

このようにみんなから支持されているいいモノを確認できるだけでなく、後述するように隠れ良品や感性の近いブロガーに出会えるページになった。

気に入っているところ1:隠れ良品に出会える

上位にランクインしている評判になっているモノをチェックするのも面白いのだけど、個人的には30位以降に載っているモノを見るのがとてもおもしろい。

たとえばこのダンボールストッカー。

hidden_goods.png

6000件弱ある買ってよかったもの記事でたった4件でしか同じ商品を紹介していないので、目的なく検索しないと出会えない隠れ良品と言ってもよさそう。

こんな感じで今まで知らなかった隠れ良品に出会える。

気に入っているところ2:感性の近いブロガーに出会える

グッドグッズランクではランクインしている商品の紹介記事も併せて表示している。

blogger.png

商品をキーにして記事を集めているので、記事自体のSEO対策の良し悪しに依らず、平等にみなさんに記事が露出される。

自分の持っている商品を紹介している記事へのアクセスを通して、自分のモノ選びの感性に近いブロガーに出会える機会もあるのも面白いと思っている。

Googleに評価されずネットの海に埋もれている記事でも興味深い記事はたくさんあることがよく分かる。

改善点

Amazon APIを通して各商品のカテゴリデータも集めているので、カテゴリ別に表示しても面白いかな。

買ってよかった記事内でAmazonの商品ページへのリンクなしで紹介している商品はデータベースに登録されていないので、もっと他に隠れ良品がある可能性が残っている。

逆に1つのサイトの複数記事で紹介されている商品がランクインしている問題もある。それだけ推したいってことだからそのままにするか悩んでいる。

こうしたらもっと面白いかも等のご意見等がありましたらぜひコメントにてお知らせください。

ブロガー買ってよかったものランキング|グッドグッズランク

グッドグッズランクに使った技術

Python
React (Gatsby)
GraphQL
MongoDB
Netlify


無料枠でGAEとAESを使ってみる

$
0
0

Nuxt + Firebase + Elasticsearch 勉強用Qiita まとめ
ローカル環境でFirebaseとElasticsearchをつなぐnodeとElasticsearchを構築できました。

これを本番環境に落とし込むためnodeをGAEに、ElasticsearchをAmazon Elasticsearch Serviceに移行してみました。

GCPとAWSに触るのは初めてだったのでかなり大変でした。

GAE (Google App Engine)

Projectのセットアップはこちらのサイトを参考にしました
Google App Engine Node.jsを試してみる。 GAE/Node.js
App Engine スタンダード環境で Node.js を使用するためのクイックスタート

app.yamlはこちらのサイトを参考にしました。
Google App Engineを無料で運用する方法(2018年版)

AES (Amazon Elasticsearch Service)

AESの立ち上げはこちらのサイトを参考にしました。
AWS再入門 Amazon Elasticsearch Service編

20分でWebアプリを作ろう

$
0
0

この記事はIBM Cloudで初めてアプリケーションを作成する方を対象に記述しています。

<事前準備>

IBM CLOUD CLIのインストール

IBM Cloud ブラウザの操作

1.IBM CloudにLogin

2.Cloud Foundry の立ち上げ

カタログメニューからCLOUD Foundory を選択、パブリック・アプリケーションを”作成”します。
image.png

プランを指定し、言語を選択(例:SDK for Node.js)
image.png

アプリ名を記入します。(例:GetStartedNode)
ホスト名はアプリ名が自動入力されます。
*地域はダラス推奨、ドメイン名の初期値のままにしてください。

”作成”ボタンを押し、アプリケーションの開始を待ちます。

アプリURLにアクセスメニューより、Hello World!が表示されることを確認します。
image.png

<ここからはターミナルの操作>

3.Gitをクローンする(ローカルにディレクトリ毎コピーを作成)

ここをコピー:git clone https://github.com/IBM-Cloud/get-started-node

4.コピーしたget-started-nodeディレクトリに移動

ここをコピー: cd get-started-node

5.ローカル環境にアプリを入れて、スタート

ここをコピー: npm install
ここをコピー: npm start

6.ローカル環境での動作を確認

URL: http://localhost:3000
image.png

ここからはymlファイルの編集

7.ローカルにコピーされた”get-started-node”以下のmainfest.ymlをメモ帳などで編集、保存します。

image.png

applications:
- name: GetStartedNode
random-route: true
memory: 128M

name:2で指定したアプリ名に変更
memory: 2で指定した容量に変更
ymlファイルを上書き保存する

<ここからはターミナルの操作>

8.IBM Cloudにログインし、Cloud Foundryをターゲット指定しプッシュ

ここをコピー: ibmcloud login
ここをコピー: ibmcloud target --cf
ここをコピー: ibmcloud cf push

IBM Cloud ブラウザの操作

9.結果の確認

アプリURLにアクセスメニューより、「ようこそ、お名前を教えてください」が表示されることを確認します。

Apacheサーバーでnode.jsを動作させる(SSL対応)

$
0
0

私の話ですが「あるWebアプリケーションをApacheで稼働していてそのアプリケーションに対応したAPIを同じサーバー上にNode.jsで作りたい」ということがありました。
HTTPであればポートを指定したりリバースプロクシを設定したりすることで問題はないのですが、HTTPSの場合は通常ポート番号が443で固定です。HTTPSに限った話だとあまり記事がないと思い今回備忘録として残しておきます。

前提条件

・AWS EC2上でApacheが稼働中
・Apache / node.js / certbot(letsencrypt)がインストール済
・アプリのホスト名はexsample.comでAPIのホスト名はapi.example.comでDNS設定済
・HTTPSはec2のインスタンスで許可済

1.SSL対応
まずはexsample.comとapi.example.comのホストのバーチャルドメイン設定をします。

$ cd /etc/httpd/conf
$ vi httpd.conf
== (編集) ==
$ systemctl restart httpd

以下の記述を追加してください。その後にApacheを再起動します。

NameVirtualHost *:80
  <VirtualHost *:80>
    ServerAdmin admin@exsample.com
    DocumentRoot /var/www/html/
    ServerName www.exsample.com
    ServerAlias exsample.com
  </VirtualHost>
  <VirtualHost *:80>
    ServerAdmin admin@exsample.com
    DocumentRoot /var/www/html/
    ServerName api.exsample.com
    ServerAlias api.exsample.com
  </VirtualHost>

ブラウザでexsample.comとapi.exsample.comをアドレスバーに入れて確認します。(何もなければおそらくApacheのテストページが表示)アクセスできることが確認できたらSSL設定を行います。

$ certbot --apache -d exsample.com
$ certbot --apache -d api.exsample.com
$ systemctl restart httpd

いろいろな対話が発生しますがここでは省略します。ただし注意としてhttpsへの転送設定はceartbotのコマンドでは行わないでください。その方が確認するときに便利だからです。certbotについては詳しい記事がいろいろあります。時間があればリンクを貼っておきます。設定し終えたらhttps://付きで二つのホストのへアクセスをブラウザで確認しましょう。

2.Node.jsの起動
Node.jsを起動します。
https://qiita.com/someone-said-so/items/ed0aafee065efcbc3b57
にあるindex.jsを/home/ec2-user/api_sampleに作成します。

$ cd /home/ec2-user/api_sample
$ node index.js

これで3000番ポートでアクセスがあったときに{msg: 'hello express'}というjsonデータを返すAPIが稼働します。ひとまずhttp://api.exsample.com:3000でアクセスして確認しましょう。(1.でリダイレクトを指定しなかったのはこの確認をするため)

3.リバースプロクシの設定
準備が整ったので最終段階です。443番ポートでアクセスがあった場合のリバースプロクシを設定しましょう。

$ cd /etc/httpd/conf
$ vi httpd-le-ssl.conf
== (編集) ==
$ systemctl restart httpd

certbotでSSL設定をした場合、httpd-le-ssl.confというファイルが出来上がっているはずです。このファイルを編集します。###で囲まれている箇所を追記してください。Proxy関連の詳細設定が多いこと、パスをhttpで設定することがポイントです。(node.jsはhttpで待ち受けているため)

<IfModule mod_ssl.c>
  <VirtualHost *:443>
    ServerAdmin admin@exsample.com
    DocumentRoot /var/www/html/
    ServerName www.exsample.com
    ServerAlias exsample.com
SSLCertificateFile /etc/letsencrypt/live/exsample.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/exsample.com/privkey.pem
Include /etc/letsencrypt/options-ssl-apache.conf
</VirtualHost>
</IfModule>
<IfModule mod_ssl.c>
<VirtualHost *:443>
    ServerAdmin admin@exsample.com
    DocumentRoot /var/www/html/
    ServerName api.exsample.com
    ServerAlias api.exsample.com
SSLCertificateFile /etc/letsencrypt/live/api.exsample.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/api.exsample.com/privkey.pem
Include /etc/letsencrypt/options-ssl-apache.conf
    ###ここから追加###
    SSLEngine on
    SSLProxyVerify none
    SSLProxyCheckPeerCN off
    SSLProxyCheckPeerName off
    SSLProxyCheckPeerExpire off
    SSLProxyEngine on
    ProxyRequests off
    ProxyPass / http://api.exsample.com:3000/
    ProxyPassReverse / http://api.exsample.com:3000/
    ###ここまで###
</VirtualHost>
</IfModule>

これで設定は完了です。あとは
https://exsample.comでアクセスするとApacheのテストページ(そのまま)
https://api.exsample.comでアクセスするとjsonデータ
であることを確認します。

設定してみての感想ですが、リソースがあるなら別のサーバーを立てた方が手っ取り早いです。またEC2なら別のインスタンスでも追加すればよいでしょう。ですがリソースが限られている場合もありますし、DBがlocalhostからアクセスできた方がよいなどいろいろな制約があるかと思います。そのようなケースに役立ててもらえればと思います。

Typescriptチュートリアル

$
0
0

Typescriptを始めたいけどどこから初めて良いかわからないという方の、TypescriptのWebサイトに掲載されていた同名のページを少しだけアレンジして紹介します。

環境の準備

まず、node.jsがPCにインストールされている必要がある。インストールされていない場合、下記のページからLTSの最新版をダウンロードしてインストールします。
https://nodejs.org/en/

Typescriptのインストール

下記のコマンドを実行して、typescriptのライブラリをダウンロードしてきます。会社の環境などで、プロキシサーバが間にある方は、プロキシありの方のコマンドを実行してください。

プロキシなし
npm install -g typescript
プロキシあり
npm install -g typescript

最初のTypescriptファイルの作成

まずテキストエディタを開いて、下記のJavascriptコードをgreeter.tsというファイルに保存してください。

greeter.ts
functiongreeter(person){return"Hello, "+person;}letuser="Jane User";document.body.innerHTML=greeter(user);

Typescriptコードのコンパイル

次にコマンドプロンプトを開き、さっき作ったgreeter.tsが保存されているパスに移動してから下記のコマンドを実行します。

コマンド
tsc greeter.ts

実行すると、同じコードが書かれたgreeter.jsというファイルが作成されます。

次に、Typescriptが提供している機能を使ってコードを改良します。

greeterの引数にstringの型定義を追加しましょう。

functiongreeter(person:string){return"Hello, "+person;}letuser="Jane User";document.body.innerHTML=greeter(user);Typeannotations

上記のコードによって、javascriptにはない型定義の機能を追加することができました。試しに、変数userに数値を入れて、greeterメソッドを呼んでみます。

functiongreeter(person:string){return"Hello, "+person;}letuser=12345;document.body.innerHTML=greeter(user);

結果はこの通り。エラーが出力されました。

error TS2345: Argument of type 'number[]' is not assignable to parameter of type 'string'.

この場合でもgreeter.jsは作成され、使うこともできますが期待通りに動作しない可能性があることをTypescriptが警告を出します。

typeアノテーション

インタフェース

今度は、firstNameとlastNameを持っているオブジェクトを表現するインターフェースを作成します。Typescriptでは、

interface Person {
    firstName: string;
    lastName: string;
}

function greeter(person: Person) {
    return "Hello, " + person.firstName + " " + person.lastName;
}

let user = { firstName: "Jane", lastName: "User" };

document.body.innerHTML = greeter(user);
Classes

クラス

class Student {
    fullName: string;
    constructor(public firstName: string, public middleInitial: string, public lastName: string) {
        this.fullName = firstName + " " + middleInitial + " " + lastName;
    }
}

interface Person {
    firstName: string;
    lastName: string;
}

function greeter(person : Person) {
    return "Hello, " + person.firstName + " " + person.lastName;
}

let user = new Student("Jane", "M.", "User");

document.body.innerHTML = greeter(user);

TypescriptWebAppの起動

<!DOCTYPE html>
<html>
    <head><title>TypeScript Greeter</title></head>
    <body>
        <script src="greeter.js"></script>
    </body>
</html>

デバッグ

クロームで実行したJavascriptを実行し、デバッグをしてみましょう。
コードを止めると、javascriptのコードではなく、typescriptが表示されます。

まとめ

これにより、typescriptの開発環境ができました・
typescriptはjavascriptと異なりコンパイルというプロセスが入るため、実行しなくても記述みすや型の間違いを見つけてくれます。
記述量は多少増えますが、規模が大きくなればなるほど、開発を助けてくれるはずです。

参考

https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes.html

NestJsのDB接続周りでハマった話

$
0
0

何を作成する?

Nest.jsで環境ごとにデータベースの接続先を分けるために,接続情報を実行環境の環境変数から非同期で取得し作成する.

環境

  • Node.js v12.14.1
  • Nest.js v6.7.2
  • TypeORM v0.2.22
  • Postgresql v11.6

実装ログ

必要最小限の実装

参考)Nest.js Document > TECHNIQUES >Database

ライブラリインストール

TypeORM, Database Driver (Postgresql)をインストールする.

$ npm install @nestjs/typeorm typeorm pg
DB接続情報を定義

app.module.tsにデータベースの接続情報を定義する.

import{Module}from'@nestjs/common';import{TypeOrmModule}from'@nestjs/typeorm';import{ItemModule}from'./item/item.module';import{Connection}from'typeorm';import{join}from'path';@Module({imports:[ItemModule,// DBの接続情報を定義TypeOrmModule.forRoot({type:'postgres',host:'localhost',port:5432,username:'postgres',password:'postgres',database:'postgres',entities:[join(__dirname+'/**/*.entity{.ts,.js}')],synchronize:false,}),],})exportclassAppModule{}

一番シンプルな書き方です.自分一人しか触らず,環境もこれだけ!ということであればこの書き方で良いでしょう.

しかし,実際の開発では個人の開発環境,テスト環境,ステージング環境,本番環境と複数の環境が存在し,上記のような実装では環境ごとに接続情報をハードコードし直し → ビルド → デプロイという手順を踏む必要がありナンセンスです.

そのため,通常は環境変数に定義し,接続情報はその環境変数を参照し作成します.

と,いうことで環境変数を参照するようにapp.module.tsを修正します.

環境変数を参照するように実装を修正

※この方法では実行時に依存関係が解決できずエラーとなります.

参考)Nest.js Document > TECHNIQUES > Configuration

ライブラリインストール

環境変数を参照するために必要なライブラリをインストールします.

$npminstall@nestjs/config
ダミーの環境変数を用意

本来は,環境変数に定義するのですがサンプル実装なので環境変数ファイル(.env)をプロジェクトルートに作成する.

DATABASE_HOST=localhostDATABASE_PORT=5432DATABASE_USERNAME=postgresDATABASE_PASSWORD=postgresDATABASE_NAME=postgres
環境変数を参照するように接続定義を修正
import{Module}from'@nestjs/common';import{TypeOrmModule}from'@nestjs/typeorm';import{ConfigModule,ConfigService}from'@nestjs/config';import{ItemModule}from'./item/item.module';import{Connection}from'typeorm';import{join}from'path';@Module({imports:[ItemModule,ConfigModule.forRoot({envFilePath:'.env',isGlobal:true,// ignoreEnvFile: true, <- 環境変数から取得する場合はコメントアウトを外す.}),// 非同期で環境変数から値を取得し,接続情報を作成する.TypeOrmModule.forRootAsync({imports:[ConfigModule],useFactory:async(configServide:ConfigService)=>({type:'postgres'as'postgres',host:configServide.get('DATABASE_HOST'),port:Number(configServide.get('DATABASE_HOST')),username:configServide.get('DATABASE_USERNAME'),password:configServide.get('DATABASE_PASSWORD'),entities:[join(__dirname+'/**/*.entity{.ts,.js}')],synchronize:false,}),inject:[ConfigService],}),],})exportclassAppModule{constructor(privatereadonlyconnection:Connection){}}

起動後,以下のエラーが発生.

2:25:34 PM - Found 0 errors. Watching for file changes.
[Nest] 19111   - 01/13/2020, 2:25:35 PM   [NestFactory] Starting Nest application...
[Nest] 19111   - 01/13/2020, 2:25:35 PM   [InstanceLoader] TypeOrmModule dependencies initialized +24ms
[Nest] 19111   - 01/13/2020, 2:25:35 PM   [InstanceLoader] ConfigModule dependencies initialized +1ms
[Nest] 19111   - 01/13/2020, 2:25:35 PM   [ExceptionHandler] Nest can't resolve dependencies of the TypeOrmModuleOptions (?). Please make sure that the argument ConfigService at index [0] is available in the TypeOrmCoreModule context.

Potential solutions:
- If ConfigService is a provider, is it part of the current TypeOrmCoreModule?
- If ConfigService is exported from a separate @Module, is that module imported within TypeOrmCoreModule?
  @Module({
    imports: [ /* the Module containing ConfigService */ ]
  })
 +1ms
Error: Nest can't resolve dependencies of the TypeOrmModuleOptions (?). Please make sure that the argument ConfigService at index [0] is available in the TypeOrmCoreModule context.

Potential solutions:
- If ConfigService is a provider, is it part of the current TypeOrmCoreModule?
- If ConfigService is exported from a separate @Module, is that module imported within TypeOrmCoreModule?
  @Module({
    imports: [ /* the Module containing ConfigService */ ]
  })

    at Injector.lookupComponentInExports (/home/kawamura/docker/docker-services/sample-app/sample-back/node_modules/@nestjs/core/injector/injector.js:185:19)
    at processTicksAndRejections (internal/process/task_queues.js:94:5)
    at async Injector.resolveComponentInstance (/home/kawamura/docker/docker-services/sample-app/sample-back/node_modules/@nestjs/core/injector/injector.js:142:33)
    at async resolveParam (/home/kawamura/docker/docker-services/sample-app/sample-back/node_modules/@nestjs/core/injector/injector.js:96:38)
    at async Promise.all (index 0)
    at async Injector.resolveConstructorParams (/home/kawamura/docker/docker-services/sample-app/sample-back/node_modules/@nestjs/core/injector/injector.js:111:27)
    at async Injector.loadInstance (/home/kawamura/docker/docker-services/sample-app/sample-back/node_modules/@nestjs/core/injector/injector.js:78:9)
    at async Injector.loadProvider (/home/kawamura/docker/docker-services/sample-app/sample-back/node_modules/@nestjs/core/injector/injector.js:35:9)
    at async Promise.all (index 3)
    at async InstanceLoader.createInstancesOfProviders (/home/kawamura/docker/docker-services/sample-app/sample-back/node_modules/@nestjs/core/injector/instance-loader.js:41:9)

環境変数を参照するためのConfigServiceTypeOrmModuleOptions内で依存関係が解決できないことが原因らしい.

同じような事象がGithubのIssueにあがっていたので参考に載せておきます.
Can't init TypeOrmModule using factory and forRootAsync

環境変数を参照するように実装を修正

app.module.tsを以下のように修正します.

import{Module}from'@nestjs/common';import{TypeOrmModule}from'@nestjs/typeorm';import{ConfigModule}from'@nestjs/config';import{ItemModule}from'./item/item.module';import{TypeOrmConfigService}from'./common/database/type-orm-config.service';@Module({imports:[ConfigModule.forRoot({envFilePath:'.env',isGlobal:true,// ignoreEnvFile: true, <- 環境変数から取得する場合はコメントアウトを外す.}),TypeOrmModule.forRootAsync({imports:[ConfigModule],// 接続情報を作成するServiceクラスを定義useClass:TypeOrmConfigService,}),ItemModule,],})exportclassAppModule{}

type-orm-config.service.ts

import{TypeOrmOptionsFactory,TypeOrmModuleOptions}from'@nestjs/typeorm';import{Injectable}from'@nestjs/common';import{ConfigService}from'@nestjs/config';import{join}from'path';/**
 * DBの接続設定.
 */@Injectable()exportclassTypeOrmConfigServiceimplementsTypeOrmOptionsFactory{/**
   * DBの接続設定を環境変数をもとに作成します.</br>
   * 環境変数に設定されていない場合は,デフォルトの設定値を返却します.
   * @returns 接続情報
   */createTypeOrmOptions():TypeOrmModuleOptions{constconfigService=newConfigService();return{type:'postgres'as'postgres',host:configService.get('DATABASE_HOST','localhost'),port:Number(configService.get('DATABASE_PORT',5432)),username:configService.get('DATABASE_USERNAME','postgres'),password:configService.get('DATABASE_PASSWORD','postgres'),database:configService.get('DATABASE_NAME','postgres'),entities:[join(__dirname+'../**/*.entity{.ts,.js}')],synchronize:false,};}}

ConfigServiceをDIするのではなく,自分でnewするのがポイントです.

最後に

最近使い始めたのですが,素晴らしいフレームワークだとひしひしと感じております.
フレームワーク自体の良さはこちらの記事で紹介されています.

参考

ネットサークル「TeamKitten」のメンバー管理APIをNodeで書き直した

$
0
0

TeamKittenというインターネット上のサークルのリーダーをやっているTinyKittenです。

最近goa(v1だったかな)で書いていたメンバー管理用APIをNode.js(NestJS)で書き直したことと、エコシステムについて書いていきます。

晩酌中に書いてるので普段以上に文が意味不明になってるかと思いますがご容赦ください。

そもそもなぜ移行したか?

いままでgoaというGolangで書いたDSLをよしなにSwaggerと実装テンプレートに落とし込んでくれるすぐれものを使っていました。が、
突然のアップデートでgo getすらできなくなる
という事態が発生ししばらく安定して動くコードで騙し騙しで動かしていました。
しかし、APIにバグが見つかりどうにも現行コードベースでは修正できないと判断し移行した次第です。

goaをアップグレードすればよかったのでは?

それもそうなのですが、goa自体にそれほど固執がなかったのと、チーム内にGolangを書けるメンバーがいなかったのでNode.jsを使ったNestJSフレームワークに白羽の矢が立ったわけです。
それにデザインから書くのが割と面倒だったので。デザインから書くことで助かったことも割と多かったですが、うちのAPIはそこまでクリティカルなものではないので。

NestJSとは?

NestJSとは、AngularにインスパイアされたサーバーサイドNode.jsフレームワークです。
Angularにインスパイアされたということで、書き方もほぼAngularです。
仕事でAngularを長年(というほどでもない)使っているので、割とすんなり書ける感じでそこも選ぶ要因の一つでした。

スタックはどのように変わった?

teamkitten-gcp.png
goaを使っていたときはGCPを使っていました。なんとなくGoogleに揃えたかったのと、AWSのコンソールが複雑怪奇だと当時思ってたからです。
上記の図の通り、シンプルにすべてのTeamKitten内部サービスからGAEのAPIにアクセスし、バックではデータベースにCloudSQL(MySQL)、オブジェクトストレージにCloudStorageを用いていました。
今はどう変わったかというと、単純にAWSになりました。(ALBとかRoute53も増えてますが...)
teamkitten-aws.png
このような図はあまり書いたことがなく見づらくて恐縮です。
ポータル、公式サイト、すごいBOT等のエコシステムにAPIをつないでいます。(後述)
費用に関しては、個人的な機械学習に使ったEC2も混じっているのではっきりとは言えませんが、今の時点で\$100程度請求されています。(多分\$30くらいが機械学習)
GCPのときは\$50しませんでした。費用削減の余地はありそう。

良かった点は?

仕事ではTypeScript・Angularを扱っているのでスムーズに書ける点でした。
NestJSのドキュメントは割と充実しているので英語さえ多少読めればすぐに扱えます。
テストコードもだいたいAngularっぽいです。(JasmineがJestになってますが)

苦労した点は?

仕事柄、GoよりもJavaScript(ここではTypeScript)を扱うことの方が慣れているので、割とサクサク書くことができました。
が、強いて言うならTSLintを怒らせたくないがためにレスポンスのスネークケースをキャメルケースに置き換えたり、最初からデザインから書くわけではないのでSwaggerがまだ整備できていない点でしょうか。(多分クライアントを作るのは自分だけなのでそれほど必要はないですが)

エコシステムのポータルとは?

FireShot Capture 006 - TeamKitten Portal - portal.teamkitten.tk.png
メンバーの方々がログインして、自己紹介・アイコン・スクリーンネーム(ログイン用に使える名前)・カバーイメージなどを自主的に変更できるチーム内サービスです。
EXCメンバー(一般メンバーより偉いメンバー)は他の人のパスワード、担当を変更できたりします。
リーダー権限のメンバー(自分だけ)は、メンバー削除、権限降格・昇格などが可能にしてます。
また、監査という機能を新APIには実装して、誰が誰の情報をいじったかを可視化できるようにしました。これで権限を使って勝手なことをするメンバーがいたとしても、すぐに検出できます。(この機能は上位メンバーのみ見れます)
このサービスは今まではNuxt.jsで書かれていましたが、自分の個人的な趣向とまともなUIフレームワークを欲していたのでIonic 4にしました。
FireShot Capture 007 - TeamKitten Portal - portal.teamkitten.tk.png
新APIに対応させるために改装中です。

エコシステムのすごいBOTとは?

これに関してはチーム内の機密情報なのですが、DiscordのBOTです。まあおもちゃみたいな存在とだけ言います。あとはメンバーの照会ができます。

まとめ

適当に取り留めもなく書いてしまいましたが、Node.js化して正解でした。
メンバーもNode.jsならできるという方もいますし、個人的にもTypeScriptが一番しっくり来るので良かったです。

GitHub

サークルの公式GitHubチーム
API
Portal

npm を使用して、誰かが書いたコードをリユースする方法

$
0
0

npmで誰かが書いたコードを使用する

NPM(Node Package Manager)は、誰かが書いたコードを使用できる便利な管理システム。

自分で1から作るんじゃなくて、誰かが書いたコードを使用できるなら、それをリユースして無駄な時間を削減しようっていう考え方を元に作成された管理システム。

https://www.npmjs.com/

npm を使用するまでの手順

今回は intro-to-node のフォルダ内のindex.jsというファイルでnpmを使用できるようにします。

スクリーンショット 2020-01-14 15.14.42.png

npm を初期化する

コマンドラインでちゃんと intro-to-node フォルダに移動しているか確認してください。

スクリーンショット 2020-01-14 15.17.05.png

確認できたら

npm init

で初期化します。

色々入力を求められるので、こんな感じで入力していきます。

スクリーンショット 2020-01-14 15.20.32.png

まぁほとんどコマンドラインの提案通りにEnter押していくだけで、実際入力したのは、description(説明)とauthor(著者)のみ。

すると、package.jsonファイルが intro-to-nodeフォルダに作成されているのがわかります。

スクリーンショット 2020-01-14 15.22.19.png

ちなみに、package.jsonの中身はこんな感じ。

package.json
{"name":"intro-to-node","version":"1.0.0","description":"This is a introduction to node project.","main":"index.js","scripts":{"test":"echo \"Error: no test specified\"&& exit 1"},"author":"kibinag0","license":"ISC"}

npm で使用したいパッケージを選ぶ

npm の公式サイト
(https://www.npmjs.com/)
にいって、使用したいパッケージを選ぶ。

今回使うのは、下記のスーパーヒーローの名前を取得してくれるパッケージです。
https://www.npmjs.com/package/superheroes

『superheroes name』 と検索したら、出てきました。

使用するパッケージをインストールする

各パッケージには、インストールの部分があるので、コマンドラインを使用してインストールします。

スクリーンショット 2020-01-16 08.34.24.png

ちゃんと自分が使用するフォルダ(intro-to-node)にいるか確認してから、インストールを実行しましょう。

スクリーンショット 2020-01-16 08.37.29.png

『npm install パッケージ名』

これでインストールは完了。

パッケージを使用する

使用するときは、パッケージのUsageを見るとわかりやすいです。

スクリーンショット 2020-01-16 08.40.59.png

index.js
// superheroes をファイル内で使用できるようにするconstsuperheroes=require("superheroes");// Usage に記載されている random() を使用するvarhero=superheroes.random();// ランダムで Super Hero の名前を取得できる

これにてnpmの使用方法まとめ終了です。

このコンテンツはUdemyの The Complete 2020 Web Development Bootcampを参考にしています。


node.jsを10->12に更新したときに起きたエラーと対処内容

$
0
0

表題通りの記事です。
これからnode.jsを10->12に更新する方の助けになれば幸いです。

確認環境

  • Ubuntu: 18.04.3 LTS
  • node.js: 10.16.0、12.14.1
  • ビルドツール: gulp (後述しますが4系へのアップデートが必須でした)

やったこと(全体像)

  • nをつかってローカル環境のnode.jsを10->12に更新 (参考: nodeとnpmのバージョン更新方法)
  • npm ciして、出てきたエラーを潰す
    • firebaseの更新
    • node-sassの更新
  • npm run buildして、出てきたエラーを潰す
    • gulpを3系->4系へ移行

エラー1 (firebaseの更新により解消)

最初はイージーケースから。

事象

npm ciしたところ、大量(体感3分)のログが出力され、最後に以下のエラーが吐かれました。

# これ以前にも大量のエラーログが出る
cc1plus: error: unrecognized command line option ‘-Wno-cast-function-type’ [-Werror]
cc1plus: all warnings being treated as errors
grpc_node.target.mk:188: recipe for target 'Release/obj.target/grpc_node/ext/channel.o' failed
make: *** [Release/obj.target/grpc_node/ext/channel.o] Error 1

# 最後の方に出るmakeのログを見ることで、どのパッケージのビルドに失敗したかが分かる
# (repository-nameでgrepすると早く見つかる)
make: ディレクトリ '/home/user1/repository-name/node_modules/grpc/build' から出ます

gyp ERR! build error 
gyp ERR! stack Error: `make` failed with exit code: 2
gyp ERR! stack     at ChildProcess.onExit (/usr/local/lib/node_modules/npm/node_modules/node-gyp/lib/build.js:194:23)
gyp ERR! stack     at ChildProcess.emit (events.js:223:5)
gyp ERR! stack     at Process.ChildProcess._handle.onexit (internal/child_process.js:272:12)
gyp ERR! System Linux 4.15.0-74-generic
gyp ERR! command "/usr/local/bin/node" "/usr/local/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js" "build" "--fallback-to-build" "--library=static_library" "--module=/home/user1/repository-name/node_modules/grpc/src/node/extension_binary/node-v72-linux-x64-glibc/grpc_node.node" "--module_name=grpc_node" "--module_path=/home/user1/repository-name/node_modules/grpc/src/node/extension_binary/node-v72-linux-x64-glibc" "--napi_version=5" "--node_abi_napi=napi" "--napi_build_version=0" "--node_napi_label=node-v72"
gyp ERR! cwd /home/user1/repository-name/node_modules/grpc
gyp ERR! node -v v12.14.1
gyp ERR! node-gyp -v v5.0.5
gyp ERR! not ok 
node-pre-gyp ERR! build error 
node-pre-gyp ERR! stack Error: Failed to execute '/usr/local/bin/node /usr/local/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js build --fallback-to-build --library=static_library --module=/home/user1/repository-name/node_modules/grpc/src/node/extension_binary/node-v72-linux-x64-glibc/grpc_node.node --module_name=grpc_node --module_path=/home/user1/repository-name/node_modules/grpc/src/node/extension_binary/node-v72-linux-x64-glibc --napi_version=5 --node_abi_napi=napi --napi_build_version=0 --node_napi_label=node-v72' (1)
node-pre-gyp ERR! stack     at ChildProcess.<anonymous> (/home/user1/repository-name/node_modules/grpc/node_modules/node-pre-gyp/lib/util/compile.js:83:29)
node-pre-gyp ERR! stack     at ChildProcess.emit (events.js:223:5)
node-pre-gyp ERR! stack     at maybeClose (internal/child_process.js:1021:16)
node-pre-gyp ERR! stack     at Process.ChildProcess._handle.onexit (internal/child_process.js:283:5)
node-pre-gyp ERR! System Linux 4.15.0-74-generic
node-pre-gyp ERR! command "/usr/local/bin/node" "/home/user1/repository-name/node_modules/grpc/node_modules/.bin/node-pre-gyp" "install" "--fallback-to-build" "--library=static_library"
node-pre-gyp ERR! cwd /home/user1/repository-name/node_modules/grpc
node-pre-gyp ERR! node -v v12.14.1
node-pre-gyp ERR! node-pre-gyp -v v0.12.0
node-pre-gyp ERR! not ok 

調べたこと

ログにあるとおり、grpcのビルドに失敗しています。
(最初は大量のログにとまどいました。どのパッケージがインストールできていないか調べるのが第一歩です)

更新対象リポジトリのpackage.jsonにはgrpcの記述がなく、
npm lsで依存パッケージを調べたところ、firebaseがgrpcに依存していました。

$ npm ls
# 結果を抜粋
├─┬ firebase@5.5.7
│ ├─┬ @firebase/firestore@0.8.6
│ │ ├─┬ grpc@1.13.1 <- ここでエラー

解決策

執筆時点でfirebaseは7系が最新。単純に古いので、アップデートしたところ解決。
(メジャーバージョン更新による影響も今のところなし)

$ npm install -D firebase




エラー2 (node-sassの更新により解消)

エラー1をちょっとひねったパターン。

事象

npm ciしたところ、大量(体感3分)のログが出力され、最後に以下のエラーが吐かれました。

# これ以前にも大量のエラーログが出る
/home/user1/.node-gyp/12.14.1/include/node/v8.h:3039:5: note:   candidate expects 2 arguments, 1 provided
binding.target.mk:129: recipe for target 'Release/obj.target/binding/src/create_string.o' failed
make: *** [Release/obj.target/binding/src/create_string.o] Error 1

# 最後の方に出るmakeのログを見ることで、どのパッケージのビルドに失敗したかが分かる
make: ディレクトリ '/home/user1/repository-name/node_modules/node-sass/build' から出ます

gyp ERR! build error 
gyp ERR! stack Error: `make` failed with exit code: 2
gyp ERR! stack     at ChildProcess.onExit (/home/user1/repository-name/node_modules/node-gyp/lib/build.js:262:23)
gyp ERR! stack     at ChildProcess.emit (events.js:223:5)
gyp ERR! stack     at Process.ChildProcess._handle.onexit (internal/child_process.js:272:12)
gyp ERR! System Linux 4.15.0-74-generic
gyp ERR! command "/usr/local/bin/node" "/home/user1/repository-name/node_modules/node-gyp/bin/node-gyp.js" "rebuild" "--verbose" "--libsass_ext=" "--libsass_cflags=" "--libsass_ldflags=" "--libsass_library="
gyp ERR! cwd /home/user1/repository-name/node_modules/node-sass
gyp ERR! node -v v12.14.1
gyp ERR! node-gyp -v v3.8.0
gyp ERR! not ok 
Build failed with error code: 1

調べたこと

ログにあるとおり、node-sassのビルドに失敗しています。
とりあえずググったところ、node-sassは4.12.0でnode.jsに12対応したそうです。

更新対象リポジトリのpackage.jsonにはnode-sassの記述がなく、
npm lsで依存パッケージを調べたところ、gulp-sassがnode-sassの4.11に依存していました。

$ npm ls
# 結果を抜粋
├─┬ gulp-sass@4.0.2
│ ├─┬ node-sass@4.11.0  ←ここでエラー(4.12以降にする必要がある)

私は最初、この段階で「詰んだ」と勘違いしました。
gulp-sassの依存パッケージを変えるには、gulp-sass公式にissueなりPRなりを投げてpackage.jsonを書き換えなければならないと思ったからです。

解決策

明示的にnode-sassをインストールしたところ、nodeがよしなにやってくれました。

$ npm install -D node-sass@4.13.0  # 実行時点での最新バージョン
$ npm ls
# 結果を抜粋
├─┬ gulp-sass@4.0.2
│ ├── node-sass@4.13.0 deduped     <- 明示的に4.13を入れたところ、nodeが重複パッケージをまとめてくれた 

上記にdeduped(=重複排除)と表示されていますが、
これはnodeが「バージョン違いの同一パッケージを1つに統合した」ことを表しています。
(今回で言うとnode-sassの4.11, 4.13のうち、より新しい4.13に統合された)

この仕様は、今回のようなケースのほかに、
セキュリティ上の脆弱性を持つ孫パッケージ(こういう呼び方があるかは不明)のバージョンを更新したいときにも役立ちます。
(参考: https://cloudpack.media/41572)

$ npm ls
├─┬ lib-nobody-update@1.1.0 # 更新が止まっているライブラリ
│ ├── lib-something@2.2.0   # 2.3.0で脆弱性が修正されたライブラリ

$ npm install -D lib-something@2.3.0
$ npm ls
├─┬ lib-nobody-update@1.1.0
│ ├── lib-something@2.3.0 deduped
Llib-something@2.3.0

今回初めて知りましたが、なかなかに素敵。




エラー3 (gulpを3系->4系へ移行することで解消)

最後はgulpの話です。

事象

npm ci実行後、npm run buildしたところ以下のエラーが発生。

[18:21:08] Requiring external module @babel/register
fs.js:27
const { Math, Object } = primordials;
                         ^

ReferenceError: primordials is not defined
    at fs.js:27:26
    at req_ (/home/user1/repository-name/node_modules/natives/index.js:143:24)
    at Object.req [as require] (/home/user1/repository-name/node_modules/natives/index.js:55:10)
    at Object.<anonymous> (/home/user1/repository-name/node_modules/vinyl-fs/node_modules/graceful-fs/fs.js:1:37)
    at Module._compile (internal/modules/cjs/loader.js:955:30)
    at Module._compile (/home/user1/repository-name/node_modules/pirates/lib/index.js:99:24)
    at Module._extensions..js (internal/modules/cjs/loader.js:991:10)
    at Object.newLoader [as .js] (/home/user1/repository-name/node_modules/pirates/lib/index.js:104:7)
    at Module.load (internal/modules/cjs/loader.js:811:32)
    at Function.Module._load (internal/modules/cjs/loader.js:723:14)
npm ERR! code ELIFECYCLE
npm ERR! errno 1
npm ERR! my-package-name@0.0.0 build-gulp: `gulp build`
npm ERR! Exit status 1
npm ERR! 
npm ERR! Failed at the my-package-name@0.0.0 build-gulp script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.

npm ERR! A complete log of this run can be found in:
npm ERR!     /home/user1/.npm/_logs/2020-01-15T09_21_13_322Z-debug.log
ERROR: "build-gulp" exited with 1.
npm ERR! code ELIFECYCLE
npm ERR! errno 1
npm ERR! my-package-name@0.0.0 build: `run-p build-gulp build-client-ts`
npm ERR! Exit status 1
npm ERR! 
npm ERR! Failed at the my-package-name@0.0.0 build script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.

npm ERR! A complete log of this run can be found in:
npm ERR!     /home/user1/.npm/_logs/2020-01-15T09_21_13_421Z-debug.log

調べたこと

一見して何のエラーか分かりませんでしたが、とりあえずエラーメッセージでググったところ
gulp3系がnode12に未対応なのが原因だと判明。gulp4系は対応済み。

いっそビルド関連をwebpackに置き換えることも考えましたが、
今回は対応速度を重視してgulpを更新する方針にしました。

解決策

以下を参考にgulpを更新しました。
(破壊的変更が多く、そこそこ面倒でした)


要点をかいつまむと、以下のようになります。

  • gulp.task()は非推奨になった(この点に触れない記事が多い)
    • 後方互換性のため、一応まだ使える
    • 代わりに関数をexportするスタイルになった
  • タスクの直列実行、並列実行は新関数gulp.series(), gulp.parallel()で制御
// 3系: gulp.task(”タスク名”, タスク用の関数)gulp.task("buildFrontend",()=>{/* フロントエンドのビルド処理 */});gulp.task("buildBackend",()=>{/* バックエンドのビルド処理 */});gulp.task("build",["buildFrontend","buildBackend"]);// []は並列実行// 4系: export const タスク名 = タスク用の関数;exportconstbuildFrontend=()=>{/* フロントエンドのビルド処理 */};exportconstbuildBackend=()=>{/* バックエンドのビルド処理 */};exportconstbuild=gulp.parallel(buildFrontend,buildBackend);

3系に比べて可読性があがりましたね。
なお、exportの順番が大切で、自行より下で定義された関数を参照するとエラーになります。(これは微妙)

// ダメな例
export const build2 = gulp.parallel(buildFrontend2, buildBackend2);
export const buildFrontend2 = () => { /* フロントエンドのビルド処理 */ };
export const buildBackend2 = () => { /* バックエンドのビルド処理 */ }; 


// 実行すると以下のエラーが出る
$ npx gulp build
> my-package-name@0.0.0 build /home/duser1/repository-name
> gulp build

/home/duser1/repository-name/gulpfile.js:10
const build = gulp.parallel(buildFrontend2, buildBackend2);
                            ^
ReferenceError: Cannot access 'buildFrontend2' before initialization
    at Object.<anonymous> (/home/duser1/repository-name/gulpfile.js:10:29)
    at Module._compile (internal/modules/cjs/loader.js:955:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:991:10)
    at Module.load (internal/modules/cjs/loader.js:811:32)
    at Function.Module._load (internal/modules/cjs/loader.js:723:14)
    at Module.require (internal/modules/cjs/loader.js:848:19)
    at require (internal/modules/cjs/helpers.js:74:18)
    at execute (/home/duser1/repository-name/node_modules/gulp/node_modules/gulp-cli/lib/versioned/^4.0.0/index.js:36:18)
    at Liftoff.handleArguments (/home/duser1/repository-name/node_modules/gulp/node_modules/gulp-cli/index.js:201:24)
    at Liftoff.execute (/home/duser1/repository-name/node_modules/liftoff/index.js:201:12)
    at module.exports (/home/duser1/repository-name/node_modules/flagged-respawn/index.js:51:3)
    at Liftoff.<anonymous> (/home/duser1/repository-name/node_modules/liftoff/index.js:191:5)
    at /home/duser1/repository-name/node_modules/liftoff/index.js:149:9
    at /home/duser1/repository-name/node_modules/v8flags/index.js:138:14
    at /home/duser1/repository-name/node_modules/v8flags/index.js:41:14
    at /home/duser1/repository-name/node_modules/v8flags/index.js:53:7
npm ERR! code ELIFECYCLE
npm ERR! errno 1
npm ERR! my-package-name@0.0.0 build: `gulp build`
npm ERR! Exit status 1
npm ERR! 
npm ERR! Failed at the my-package-name@0.0.0 build script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.

npm ERR! A complete log of this run can be found in:
npm ERR!     /home/duser1/.npm/_logs/2020-01-15T10_04_26_061Z-debug.log




まとめ

  • バージョン更新は思った以上に大変
  • こまめな更新が、バージョン更新のコストを下げる
  • npm lsでパッケージの依存関係をチェックできる
  • nodeにはdedupeという素敵機能がある
  • node12でgulp3系は使えない。4系に移行するか、webpackなど他ツールへ移行する


webpackちゃんと使えるようになりたいです。

Could not find matching close tag for "

$
0
0

node.jsのエラーコード

Could not find matching close tag for "<%="

意味 “<%=“. に合うタグが見つかりませんでした。

LambdaからRedisのデータを取得したい

$
0
0

使用するもの

  • Lambda
  • Redis

私の環境

Node 12系

DockerでLamdaを用意しています。方法については、以下の記事を参考にしてください。
https://qiita.com/gdtypk/items/78b6a76dc9f212296c89

Redisも同様にDockerで起動しています。
redis:4.0

困ったこと

Node.jsは非同期で動作するので、そのあたりが苦労した。

コード

constredis=require("redis");constPromise=require('bluebird');// 接続情報constconfig={host:'redis',port:6379};letclient=null;letvalue="";constkey="key";exports.handler=async(event,context)=>{try{//Redisに接続client=awaitconnectRedis();// データの取得value=awaitfindValueOfKey(client,key);//Redisとの接続を切断disconnectRedis();//レスポンス返却console.error('レスポンス:'+value);context.succeed({statusCode:200,body:JSON.stringify(value)});}catch(error){console.error('エラー:',error);disconnectRedis();}};/** 
 * Redisに接続する処理。
 */functionconnectRedis(){returnnewPromise(function(resolve,reject){constclient=redis.createClient(config);client.on('connect',()=>{console.log('接続OK');resolve(client);});client.on('error',(error)=>{console.log(`接続NG`);reject(error);});});}/**
 * Redisとの接続を切断する。
 */functiondisconnectRedis(){if(client){console.log("切断します");client.end(true);}}/**
 * Keyを使用し、Valueを取得する。
 * @param client
 * @param key
 */functionfindValueOfKey(client,key){returnnewPromise(function(resolve){client.get(key,(err,reply)=>{resolve(reply);});});}

おわり

もっといい書き方があったり、コードがおかしいとかあれば、教えて下さい。

【文系新卒が】出来立てご飯を食べたかった話【GoogleHome LINE Node-RED】

$
0
0

事の始まり

僕はご飯が大好きな文系新卒君です。
僕はありがたいことに、家に帰るとご飯が毎日用意されています。
しかし学生時代のように出来立てのホカホカご飯を食べれる機会は減ってしまいました。
そう...社会人は学生とは違い帰る時間がバラバラ。
帰る時間を連絡しても、1日中ケータイを触ってるわけじゃないのでご飯の完成にラグが起きてしまう。

.....だったら帰宅時間を教えることのできるシステムを作ればいいじゃないか!!

全体図

どうすれば帰宅時間を知らせることができるか考えた結果、
「物理的」に知らせるのが一番じゃないかと考えました。
100.png

こんな感じで実装すれば、ラインメッセージと自宅へ音声送信両方が実現ができるはず!

実装

それでは実装していきますよ!!
事前準備は4つ

  • Node-REDのインストール
  • LINE Developersのアカウント
  • Google App Script
  • Node-REDと同じLANに接続されたGoogleHome

今回LINEとGoogleHomeを繋げる目的で利用するNode-REDですが
何かと何かを繋ぐ際にGUI上で実行できるWebアプリケーションと思って下さい。

参考資料
Node-RED日本ユーザ会
Windowsで実行する : Node-RED日本ユーザ会
LINEのBot開発 超入門(前編) ゼロから応答ができるまで
Google Homeの初期設定をする手順

1.Node-REDとGoogleHomeを繋げる

まず初めにNode-REDのページを開き、パレットの管理からnode-red-contrib-castを追加します。
これがGoogleHomeへ送ったメッセージを読み上げさせるノードになります。
インストールが完了したら、下図のフローを作成してください。
無題1.png

次にCastノードの設定を行います。

項目入力内容
IPGooglHomeのIPアドレス
languageja
Volume0~100(好みの音量サイズ)

設定後にinjectノードを実行し、設定した言葉がGoogleHomeから聞こえたらOKです!

2.Node-REDをネットワークに公開

このままではNode-REDはローカル外からのアクセスができません。
なので簡単にローカル環境を外部から接続させることのできるngrokを使いたいと思います。
ngrokをインストールしてNode-REDで設定されているポートをパスワード付きで公開してください。

EXAMPLES:
    ngrok http 80                    # secure public URL for port 80 web server
ngrok by @inconshreveable                                                                                                                                           (Ctrl+C to quit)

Session Status                online
Version                       2.2.4
Region                        United States (us)
Web Interface                 http://127.0.0.1:1880
Forwarding                    http://xxxxxxxx.ngrok.io -> localhost:1880
Forwarding                    https://xxxxxxxx.ngrok.io -> localhost:1880

Connections                   ttl     opn     rt1     rt5     p50     p90
                              6       0       0.05    0.02    23.33   25.98

Forwadingに書かれているURL(https)にアクセスができたらOKです!

ngrokの使い方(windows, mac)
少しでも安全にngrokを使用する(Basic認証)

3.LINEとNode-REDを繋げる

最後にLINEとNode-REDを繋げたいと思います。
まずはGoogle App Script(GAS)で以下のコードを張り付けて下さい。
※アクセス先の情報は自身で入力してください。

function doPost(e) {
  // アクセス先の情報
  var url = "ngrokで作成したHTTPSのURL" + "/line";
  var userid = "任意のID";
  var password = "任意のパスワード";

    // POSTメソッドの投稿データ
  var event = JSON.parse(e.postData.contents).events[0];
  var replyToken = event.replyToken
  var msg = event.message.text;
  var msgType = event.message.type;

  var payload = {"replyToken":  replyToken, "message":msg, "type":msgType};

    // POSTメソッドのオプション
    var options = {
      "method" : "POST",
      "headers" : {"Authorization" : " Basic " + Utilities.base64Encode(userid + ":" + password)},
      "payload" : payload,
      "muteHttpExceptions" : true
    }

    try{
      // POSTメソッドのリクエスト
      var response = UrlFetchApp.fetch(url, options);
      var content = response.getContentText("UTF-8");
    }catch(e){
      Logger.log(e.message);
    }
}

その後Webアプリケーションとして導入を行うことでWebhook用のURLが表示されるので、
それを忘れないようコピーして置いてください。(Current web app URLのところです)
無題2.png

LINE DevelopersへアクセスしてWebhook先へ先ほどのURLを貼り付けます。

無題3.png

ついでにチャンネルアクセストークンを使うのでメモしておきましょう。
100.png

あとはNode-RED上でLINE+GASからのメッセージとGoogleHomeを繋げてあげれば完了です!

4.LINEのメッセージとGoogleHomeを繋げる

いよいよ最終工程。完成状態はこんな感じになります!
無題4.png
最後に下に記載したJSONデータを読み込ませて、以下の項目を行えば終了です!!

  • googlehomeノードを最初に設定したgooglehomeノードに変更。
  • Line Reply API用設定Bearer "your_channel_access_token"""中身を先程メモしたチャンネルアクセストークンに変更。

※Line Reply API用設定でLINE Developersからチャンネルアクセストークンは確認できます。

作成したLINE botにメッセージを送信すれば、Googleホームが読み上げてくれます!!!

[{"id":"314b7d3f.2adc62","type":"tab","label":"フロー 1","disabled":false,"info":""},{"id":"72e325a4.cdbacc","type":"comment","z":"314b7d3f.2adc62","name":"line reply msg","info":"","x":310,"y":160,"wires":[]},{"id":"65a3144c.b1371c","type":"http in","z":"314b7d3f.2adc62","name":"","url":"/line","method":"post","upload":false,"swaggerDoc":"","x":170,"y":220,"wires":[["246b9d27.a33942","4ff3caa3.50ed94"]]},{"id":"accdaf77.6e5c9","type":"http response","z":"314b7d3f.2adc62","name":"","statusCode":"","headers":{},"x":850,"y":220,"wires":[]},{"id":"1bc5bba6.3b0244","type":"http request","z":"314b7d3f.2adc62","name":"Line Reply API","method":"POST","ret":"txt","paytoqs":false,"url":"https://api.line.me/v2/bot/message/reply","tls":"","persist":false,"proxy":"","authType":"","x":660,"y":220,"wires":[["accdaf77.6e5c9"]]},{"id":"246b9d27.a33942","type":"change","z":"314b7d3f.2adc62","name":"Line Reply API 用設定","rules":[{"t":"set","p":"headers.Content-Type","pt":"msg","to":"application/json","tot":"str"},{"t":"set","p":"headers.Authorization","pt":"msg","to":"Bearer \"your_channel_access_token\"","tot":"str"},{"t":"set","p":"payload.replyToken","pt":"msg","to":"payload.replyToken","tot":"msg"},{"t":"set","p":"payload.messages[0].type","pt":"msg","to":"text","tot":"str"},{"t":"set","p":"payload.messages[0].text","pt":"msg","to":"送信完了","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":420,"y":220,"wires":[["1bc5bba6.3b0244"]]},{"id":"894d56ce.436268","type":"cast-to-client","z":"314b7d3f.2adc62","name":"google home","url":"","contentType":"","message":"","language":"ja","ip":"","port":"","volume":"50","x":770,"y":380,"wires":[[]]},{"id":"97eee3ab.2f10f","type":"comment","z":"314b7d3f.2adc62","name":"googlehome","info":"","x":350,"y":340,"wires":[]},{"id":"4ff3caa3.50ed94","type":"change","z":"314b7d3f.2adc62","name":"受信したJSONからメッセージのみ取得","rules":[{"t":"set","p":"message","pt":"msg","to":"payload.message","tot":"msg"},{"t":"delete","p":"payload","pt":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":500,"y":380,"wires":[["894d56ce.436268"]]}]

あとがき

これにて我が家のホカホカご飯問題は幕を閉じた。
今日も出来立てご飯と幸せを噛みしめる文系新卒君であった。

文系新卒君の挑戦は続く...

Node.jsを用いた定期ツイートbot作成メモ

$
0
0

すぐ忘れるのでメモ。

もっと無駄の少ないやり方なんて知りません、、、

まずTwitterAPIの取得

申請の際の作文はbotにしか使わないという趣旨を書けばすぐに通るはずです。

API keyとToken keyが得られれば勝ち。

ツイート内容のコードを作る

用意するのはnpm i Twitternpm i node-schedule

ツイート
https://yukimonkey.com/js-application/twitter-bot-2/

画像ツイート
https://qiita.com/n0bisuke/items/6b269f61152e9f336c35

時間指定はNode-scheduleを使う

Heroku

アカウント作成

タイムゾーンについて、途中で気づいてめんどくさかったのでソースコードの時間を-9hしました。

Herokuへデプロイ

コマンドはこの記事を参考にしました。
https://qiita.com/daiki7nohe/items/035c39c1e538551b1f6c#git%E3%82%B3%E3%83%9F%E3%83%83%E3%83%88
この二つの記事を参考にapp.jsと、起動するためのコマンドをpackage.jsonに記述するか、procfileを作る
https://qiita.com/yfujii01/items/d675f654f1fcce3b9098
https://www.webprofessional.jp/building-facebook-chat-bot-node-heroku/

サーバーが寝ないようにする

Heroku Scheduler
https://miyabi-lab.space/blog/23

Viewing all 8875 articles
Browse latest View live