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

HTML5/CSS3のみのコーダーがMEAN環境でゲームを作るまでの話(序)

$
0
0

自己紹介

こんにちは!ちゃばこと、くぅじやすまさです。
このエントリは技術エントリではないのでタグはつけてないんですけどQittaって開発日記みたいなのも投稿していいんですか。プログラミングはクソ稚魚なんですけど…

Qittaに登録した理由はエントリタイトルの通りです。
自分は2006年ごろからHTMLとCSSに触れて、それからメモ帳でコードをベタ書きしながらサイトを作ってきた化石です(WordPressに触れてPHPをかじったときでさえベタ書きでした)。恩師に化石って言われたので化石です。

数年前からbracketやCodaを使いはじめて、今は身なりだけは文明人です。でもgitを使いこなせていないのでやっぱり化石かもしれません…『稚魚の化石』に改名しろ。

ともあれ。そんな化石さんには目標があります。
それは…

MEAN環境でゲームを作ること!


……
おいおいHTMLとCSSと本当にちょっとのPHPしかできないクソ稚魚コーダーがプログラマーになろうだなんて無謀すぎないか?

無謀すぎるだろ。無謀だと思います。あなたは正しい。

でもものづくりがしたい!(ものづくりマニア)
創作仲間や創作が好きな人たちが遊べるうちのこゲームがつくりたーい!
なんでLAMP環境じゃないかというと、JavaScriptひとつで開発が済むからですね!

この軽はずみな言動を化石は激しく後悔することになるのだった…

どんなゲームを作りたいのか?

作れるかどうかはともかく、ぼんやりした構想をはっきりとさせておきましょう。
設計はけっこう単純で、1日に1回行動ポイントが配られ、ポイントの範囲内で冒険に出て結果を得る。それだけです。
診断メーカーでこういうものはけっこう作ってきたのですが、ログインしてユーザー情報を作成・保存し、毎日の行動をマイページに記録するといったシステムになります。
それにダイスを振ることのできる掲示板を併設して、他のプレイヤーとの交流を楽しんでもらう…こんなところでしょうか。
いわゆる、地図マップの上をキャラクターが移動したりシューティングなゲームではないですね。なのできっと、簡単に作れる…と信じたい!

このあまりに楽観的すぎる言動を化石は激しく後悔することになるのだった…

MEANスタックとは?

化石がMEANスタックという言葉を知ったのは、行きつけのゲームチャットの管理人さんのつぶやきでした。チャットシステムをMEAN環境で構築する準備を進めていたそうです。気になって調べてみると、JavaScriptで全ての開発を行う環境らしいではないですか。これは!と思って飛びついたのです。

MEANスタックとは、〈MongoDB+Express+Angular+Node.jsでWebアプリケーション開発を行うシステム構成〉のこと。

化・ω・石「MongoDBとAngularとNode.jsは聞いたことがあるぞ!(聞いたことがあるだけ)」

マジで聞いたことがあるだけなので救いようがない。助けてネット集合知!

MongoDBとは?

名前だけは聞いたことあるんですけど。
集合知によればこれは…「ドキュメント指向データベース」のひとつ!

化・ω・石「ドキュメント指向データベース?」

そこからかよ…

MySQLなどに代表されるリレーショナルデータベースは表形式でデータを保存しますが、MongoDBなどのドキュメント指向データベースは、表形式で保存できないデータなどを列指向で保存します。さまざまなデータ構造のドキュメントを混在して保存できるんですね。

それスパゲッティ構造にならないかという心配はありますが多分化石だから秘伝ソースになるんだと思う。世のプログラマーたちは化石より頭がSmartなのでそんなことにはならない。

MongoDBはJSONを元にした、BSONというバイナリ形式でデータを保存するNoSQLデータベース。データ結合には向いていないので、データ結合を頻繁に行う際はリレーショナルデータベースが良いでしょう…なるほどね。

Expressとは?

化・ω・石「これは…はじめて聞いたぞ!」
そうですか。集合知!

ExpressとはNode.js上で動くWebアプリ用フレームワークで、npmモジュールでインストールできます。

なるほどね!フレームワークなんですね!(それはわかる)
サーバサイドはNode.js+Expressというわけです。

Angularとは?

化・ω・石「聞いたことあるぞ!ブラウザゲー作ろうと思った時に出てきた」
そんな理解でいいのか。

AngularはフロントエンドのMVWフレームワーク。ver1.xのAngularJSとver2.xのAngular2ではだいぶ仕様が違うようなので、参考書は慎重に選びたいところです。

Node.jsとは?

化・ω・石「知ってる!(知っているとは言わない)」

ノンブロッキングI/OのJavaScriptサーバサイド実行環境ですね。npm使える。それくらいしか知らないんですけどそれでいいのだろうか。

ここで問題が発生

Node.jsをインストールするにはレンタルサーバーを卒業しなければいけません。
化石、VPSサーバの知識が0。

馬鹿じゃないの!?

馬鹿じゃないの…(絶望)

このままサーバーを立てたら乗っ取り踏み台され放題です!
ローカルでいろいろする方法もありますが、最終的にはVPSサーバ保守の勉強もしないといけません。

いやそれよりもまず…JavaScriptのお勉強です。
なにせJavaScriptといえばjQueryライブラリをインポートしてわちゃわちゃするくらいでコードをまともに読んだことがありません!要は何も書けないも同様。これではいけません。

そこで!

確かな力が身につくJavaScript「超」入門 第2版

デン!狩野先生のヤツ!
これでJavaScriptの基礎を勉強していきたいと思います。

開発環境

Macユーザーなのですが、絵描くにも小説を書くにもiPadに移行してしまって置き物ストレージになっている昨今。MacBookも持っていないのでiPadで開発できないだろうか。Codaで出来るかな。せっかく買ったしな。というわけで。

  • iPad(iOS 13.1.3)
  • Coda(もしくはtextastic)
  • working copy(git連携)

こんな感じで開発していきたいと思います。
まずはJavaScriptをマスターするところからだ!


急性中耳炎診断支援LINE Botを改良しHerokuにデプロイ

$
0
0

概要

プログラムの勉強を始めて4か月ほどの開業医です。

以前、忙しい臨床現場でも診療ガイドラインに沿った治療を簡単に行えるようにするため、医療者向けに急性中耳炎の重症度と推奨治療が簡単に分かるLINE Botを作成しました。

以前のものは初診の患者さんに対して重症度判定と推奨治療の提示を行うものだったのですが、今回は再診時の推奨治療を提示する機能を追加しました。

さらに、LINE Developersのリッチメニューを使用しUIを向上させ、Herokuにデプロイしました。今回も医療者向けのBotになっています。

実装

小児急性中耳炎診療ガイドライン(2018年 日本耳科学会)に沿った診療を支援するLINE Bot。
①初診時LINE上で質問に答えていくとスコアリングが行われ、急性中耳炎の重症度と推奨治療が提示される。
②再診時は前回行われた治療を選択すると今回の推奨治療が提示される。

概念図

node.jsでBot開発しLINE bot APIと連携。Herokuにデプロイ。
heruku nodeja line.jpg

動画

リッチメニューから選択ができ、クイックリプライボタンで表示されます。

作成方法

1. Botアカウントの作成

2. Node.jsでBot開発

3. ngrokでトンネリング

上記の1~3を以下の参考記事の通りに行います。
1時間でLINE BOTを作るハンズオン (資料+レポート) in Node学園祭2017

4. プログラム作成

コードを以下のように書き換えます。
LINE Developersのクリックリプライを使用しています。
LINE Developers

'use strict';// モジュールのインポートconstexpress=require('express');constline=require('@line/bot-sdk');constPORT=process.env.PORT||3000;constapp=express();// パラメータ設定constconfig={channelSecret:'自分のchannelSecret',channelAccessToken:'自分のchannelAccessToken'};// Webサーバー設定app.listen(PORT);// APIコールのためのクライアントインスタンスを作成constclient=newline.Client(config);// ルーター設定app.post('/bot/webhook',line.middleware(config),(req,res,next)=>{res.sendStatus(200);console.log(req.body.events);Promise.all(req.body.events.map(handleEvent)).then((result)=>res.json(result));});functionhandleEvent(event){if(event.type!=='message'||event.message.type!=='text'){returnPromise.resolve(null);}// LINE botのプログラム letmessage=event.message.text;letquestion="";letlabel1,label2,label3="";lettext1,text2,text3="";let[mes,currentScore]=message.split(":");if(message=="初診"){currentScore=0;question="2歳未満ですか?";label1="いいえ";//0text1="2歳以上 トータルスコア:"+String(currentScore);message=text1;label2="はい";//3text2="2歳未満 トータルスコア:"+String(currentScore+3);message=text2;label3="選択できません";text3="開始";message=text3;}elseif(mes=="2歳以上 トータルスコア"||mes=="2歳未満 トータルスコア"){question="耳は痛いですか?";label1="いいえ";//0text1="耳は痛くない トータルスコア:"+String(currentScore);message=text1;label2="痛い、痛かった";//1text2="耳が痛い、痛かった トータルスコア:"+String(Number(currentScore)+1);message=text2;label3="とても痛いのが続いている";//2text3="耳がとても痛いのが続いている トータルスコア:"+String(Number(currentScore)+2);message=text3;}elseif(mes=="耳は痛くない トータルスコア"||mes=="耳が痛い、痛かった トータルスコア"||mes=="耳がとても痛いのが続いている トータルスコア"){question="熱はありますか?";label1="37.5未満";//0text1="37.5未満 トータルスコア:"+String(currentScore);message=text1;label2="38.5未満";//1text2="38.5未満 トータルスコア:"+String(Number(currentScore)+1);message=text2;label3="38.5以上";//2text3="38.5以上 トータルスコア:"+String(Number(currentScore)+2);message=text3;}elseif(mes=="37.5未満 トータルスコア"||mes=="38.5未満 トータルスコア"||mes=="38.5以上 トータルスコア"){question="泣いたり、機嫌が悪いですか?";label1="いいえ"//0text1="機嫌は悪くない トータルスコア:"+String(currentScore);message=text1;label2="はい";//1text2="泣いたり、機嫌が悪い トータルスコア:"+String(Number(currentScore)+1);message=text2;label3="選択できません";text3="開始";message=text3;}elseif(mes=="機嫌は悪くない トータルスコア"||mes=="泣いたり、機嫌が悪い トータルスコア"){question="鼓膜が赤いですか?";label1="いいえ";//0text1="鼓膜は赤くない トータルスコア:"+String(currentScore);message=text1;label2="一部だけ赤い";//2text2="一部だけ赤い トータルスコア:"+String(Number(currentScore)+2);message=text2;label3="全体に赤い";//4text3="全体に赤い トータルスコア:"+String(Number(currentScore)+4);message=text3;}elseif(mes=="鼓膜は赤くない トータルスコア"||mes=="一部だけ赤い トータルスコア"||mes=="全体に赤い トータルスコア"){question="鼓膜は腫れていますか?";label1="いいえ";//0text1="鼓膜は腫れていない トータルスコア:"+String(currentScore);message=text1;label2="一部だけ腫れている";//4text2="一部だけ腫れている トータルスコア:"+String(Number(currentScore)+4);message=text2;label3="全体に腫れている";//8text3="全体に腫れている トータルスコア:"+String(Number(currentScore)+8);message=text3;}elseif(mes=="鼓膜は腫れていない トータルスコア"||mes=="一部だけ腫れている トータルスコア"||mes=="全体に腫れている トータルスコア"){question="耳垂れは出ていますか?";label1="いいえ";//0text1="耳垂れは出ていない トータルスコア:"+String(currentScore);message=text1;label2="少し出ている";//2text2="少し出ている トータルスコア:"+String(Number(currentScore)+2);message=text2;label3="たくさん出ている";//4text3="たくさん出ている トータルスコア:"+String(Number(currentScore)+4);message=text3;}console.log(message);letresult;if(mes=="耳垂れは出ていない トータルスコア"||mes=="少し出ている トータルスコア"||mes=="たくさん出ている トータルスコア"){if(currentScore<=5){result="急性中耳炎は軽症です。抗生物質は使わずに3日間様子を見ましょう。"}elseif(currentScore<11){result="急性中耳炎は中等症です。 AMPCを高用量で3~5日間投与しましょう。"}else{result="急性中耳炎は重症です。AMPCを高用量で使うか、CVA / AMPC1:14製剤を通常量で使うか、CDTR-PIを高用量で使いましょう。鼓膜切開も考慮しましょう。"}returnclient.replyMessage(event.replyToken,{type:'text',text:result});}//再診時プログラムif(message=="抗生剤非投与"){result="推奨治療:AMPC常用量3~5日投与";returnclient.replyMessage(event.replyToken,{type:'text',text:result});}elseif(message=="AMPC常用量"){result="推奨治療:感受性を考慮し以下のいずれかを3~5日間投与。 AMPC高用量 or CVA / AMPC1:14製剤 or CDTR-PI常用量";returnclient.replyMessage(event.replyToken,{type:'text',text:result});}elseif(message=="AMPC高用量 または CDTR-PI常用量"){result="推奨治療:感受性を考慮し以下のいずれかを3~5日間投与+鼓膜切開を考慮。AMPC高用量 or CVA / AMPC1:14製剤 or CDTR-PI高用量";returnclient.replyMessage(event.replyToken,{type:'text',text:result});}elseif(message=="CVA / AMPC1:14製剤 または CDTR-PI高用量"){result="推奨治療:感受性を考慮し以下のいずれかを3~5日間投与+鼓膜切開考慮。CVA / AMPC1:14製剤 or CDTR-PI高用量 or TBPM-PI常用量 or TFLX常用量";returnclient.replyMessage(event.replyToken,{type:'text',text:result});}elseif(message=="TBPM-PI常用量 または TFLX常用量"){result="推奨治療:感受性を考慮し以下のいずれかを3~5日間投与+鼓膜切開考慮。TBPM-PI高用量 or TFLX常用量。または下記のいずれかを3日間点滴静注。ABPC 150mg/kg/日,分3 or CTRX 60mg/kg/日,分2または分3(新生児は50mg/kg/日以下)";returnclient.replyMessage(event.replyToken,{type:'text',text:result});}else{returnclient.replyMessage(event.replyToken,{"type":"text","text":question,"quickReply":{"items":[{"type":"action","action":{"type":"message","label":label1,"text":text1}},{"type":"action","action":{"type":"message","label":label2,"text":text2}},{"type":"action","action":{"type":"message","label":label3,"text":text3}}]}});}}

5.Bot本体をHerokuにデプロイ

6.Webhookを設定

上記の5.6の作業をこちらの記事を参考に行いました。
LINEのBot開発 超入門(前編) ゼロから応答ができるまで

7.リッチメニューの設定
こちらを参考に行いました。
リッチメニューの画像作成はこうやる!

8.LINEアプリへのQRコード
ガイドラインQRコード.png

考察

LINE Developersのクリックリプライとリッチメニュー使いUIをさらに改善させたLINE Botを作ることができました。
今回も医療者向けのBotになっていますが、一般の方が使えるようなものを開発していきたいと思います。

[awscli] nodejs8.10を使用しているlambdaを洗い出すワンライナー

$
0
0

モチベーション

AWS Lambda: Node.js 8.10 is EOL, please migrate your functions to a newer runtime version.らしいので、nodejs8.10を使ってるlambda関数を洗い出したかった

https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/runtime-support-policy.html
https://qiita.com/kapioz/items/1e0fb80afc7d98bbde52

ワンライナー

aws lambda list-functions | jq -r ('.Functions'|.[]|'{ FunctionName: .FunctionName, Runtime: .Runtime}') | jq -r 'select(.Runtime == "nodejs8.10")'

こっちだと関数名のみ

aws lambda list-functions | jq -r ('.Functions'|.[]|'{ FunctionName: .FunctionName, Runtime: .Runtime}') | jq -r 'select(.Runtime == "nodejs8.10")' | jq '{ FunctionName: .FunctionName } | .FunctionName' -r

結果

$ aws lambda list-functions | jq -r ('.Functions'|.[]|'{ FunctionName: .FunctionName, Runtime: .Runtime}') |  jq -r 'select(.Runtime == "nodejs8.10")'
{
  "FunctionName": "hoge",
  "Runtime": "nodejs8.10"
}
{
  "FunctionName": "piyo",
  "Runtime": "nodejs8.10"
}
{
  "FunctionName": "wow",
  "Runtime": "nodejs8.10"
}
{
  "FunctionName": "yeah",
  "Runtime": "nodejs8.10"
}

Node(Express)でJWT

$
0
0

はじめに

ユーザー認証を実装したい。
スマホアプリ向けのAPI作っているときにユーザー認証が必要となり、JWTが良さそうだったので採用した。

以下、注意点

  • 本記事の目的はあくまでJWTの導入なので、脆弱性については触れていない。
  • 入門者にわかりやすくするため、有識者には誤解される表現を使っている可能性がある。

JWTとは

「Json Web Token」の略で、URLに埋め込めるJSON文字列のこと。
電子署名によってJSONが改ざんされていないかをチェックできるため、ユーザー認証などに使うことができる。
筆者は詳しく理解してないので、JWT使えばログイン機能作れる!、みたいに思っている。

JWTのしくみ

  1. クライアントは、認証情報(ユーザーIDとパスワード)をサーバに送る(いわゆるログイン処理)
  2. サーバは、データベースに接続して認証情報が正しいか確認
  3. サーバは、ユーザーIDと有効期限といった情報を持つJSONを秘密鍵で暗号化し、JWTの形でクライアントに返す
  4. 以降、クライアントは、渡されたJWTを使ってサーバと通信する(いわゆるログイン状態)

JWTの有効期限が切れたら、再び1に戻って新しくJWTを発行する必要がある。

コード

command
npm install jsonwebtoken
index.js
varexpress=require("express");varapp=express();varjwt=require("jsonwebtoken");// 秘密鍵設定、本当は環境変数使うべきapp.set("superSecret",hogehoge);varapiRoutes=express.Router();// ユーザー情報、本当はデータベースにあるのをとってくるべきvarusers=[{id:"user0",pass:"huga0"},{id:"user1",pass:"huga1"},{id:"user2",pass:"huga2"}];// クライアントから送られたIDとパスワード確認してtoken(jwt)発行// "/api/authenticate"apiRoutes.post("/authenticate",(req,res)=>{varpost_id=req.body.postId;varpost_pass=req.body.postPass;for(vari=0;i<users.length;i++){// IDとパスワードが正しかったらtoken発行if(post_id==users[i].id&&post_pass==users[i].pass){vartoken=jwt.sing(user_id,app.get("superSecret"),{algorithm:HS256,expiresIn:120});res.json({success:true,msg:"Authentication successfully finished",token:token});return;}}// IDとパスワードが正しくなかった場合res.json({success:false,msg:"Authentication failed"});});// 一度クライアントに返したtokenが改ざんされずにクライアントから送られてきたか確認apiRoutes.use((req,res,next)=>{vartoken=req.body.token;// tokenがない場合、アクセスを拒否if(!token){returnres.status(403).send({success:false,msg:"No token provided"});}// tokenが改ざんされていないかチェックjwt.verify(token,app.get(superSecret),(err,decoded)=>{// tokenが不正なものだった場合、アクセス拒否if(err){console.log(err);returnres.json({success:false,msg:"Invalid token"});}// 正しいtokenの場合、認証OKするreq.decoded;next();});});// 認証後、これ以降のURIにアクセス可能となる// "/api/private"apiRoutes.get("/private",(req,res)=>{res.json({msg:"Hello world!"});});app.use("/api",apiRoutes);

解説

jwt.sing(user_id,app.get("superSecret"),{algorithm:HS256,expiresIn:120});
  • アルゴリズム
    • algorithm: HS256 デフォルト値がこれになっており、記述しなくても問題ない
  • 有効期限
    • expiresIn: 120 120ミリ秒
    • expiresIn: 3h 3時間
    • expiresIn: 2days 2日
apiRoutes.post("/authenticate",(req,res)=>{...});apiRoutes.get("/private",(req,res)=>{...});

アクセスする際のURIは、"/authenticate""/private"ではなく、
/api/authenticate"/api/private"となる。
理由は、コードの一番下でapp.use("/api", apiRoutes);としているから。

さいごに

脆弱性については今後別の記事でまとめる予定。
間違いなどあれば是非コメントにお願いします。

参考サイト

cookieを使ってみる

$
0
0

cookieとは

cookieとはWEBサイトにおいて、IDやパスワードなどの会員情報や、IPアドレスを保存してくれるブラウザの機能です。
cookieがあるお陰で、いちいちログインし直す手間が省けたりします。

参考:WEB担当者Forum

cookieの仕組み

cookieはHTTPが用いられます。クライアントがWEBサーバーにHTTPリクエストを送った(ブラウザがWEBサイトにアクセスした)際に、WEBサーバーから、ページの情報と一緒にcookie情報が(HTTPレスポンスで)送られてきます。ブラウザはこのcookie情報を保存してくれます。
再び同じWEBサイトにアクセスした際に、ブラウザはこのcookie情報を確認します。
そしてそのcookie情報をWEBサイト(WEBサーバー)に送信(リクエスト)し、WEBサーバーからそのcookie情報に基づいたWEBサイトが表示されるというわけです。

参考:WEB担当者Forum

Node.jsでcookieを利用してみる。

まずはindex.jsファイルに以下のようなサーバー情報を書き込みます。

index.js
'use strict';consthttp=require('http');constserver=http.createServer((req,res)=>{res.end('hello node.js');});constport=8000;server.listen(port,()=>{console.info('Listening on '+port);});

これは、一般的なサーバー立ち上げファイルですね。
ここに以下のような記述をすることで、cookie情報を扱うサーバーに変身します。

index.js
'use strict';consthttp=require('http');constserver=http.createServer((req,res)=>{res.setHeader('Set-Cookie','last_access='+Date.now()+';');res.end('hello node.js');});constport=8000;server.listen(port,()=>{console.info('Listening on '+port);});

cookie情報を扱うために、レスポンスヘッダのSet-Cookieという項目に、 キー名=値;という形式で値を書き込みます。
キー名=値; expires=日付;というようにexpiresという有効期限を設定することもできます。

それでは、サーバーを立ち上げてcookieを確認してみます。
サーバーを立ち上げて、

$ node index.js

以下のURIにアクセスします。
http://localhost:8000

以下のような表示が出たら成功です。
スクリーンショット 2019-11-19 22.26.35.png

続いてデベロッパーツールを開いてみます。
スクリーンショット 2019-11-19 22.28.09.png

以上は、デベロッパーツールのApplicationという欄を押して、左側のStrageという欄のCookiesの中のhttp://localhost:8000というところをクリックすると表示されます。

スクリーンショット 2019-11-19 22.28.39.png
Nameという列にさきほどサーバーファイルで指定した、last_access
Valueという列にDate.now()が表示されます。Date.nowは今の時間をミリ秒で表した数字になります。

ちなみにこのcookieは、req.headers.cookieで取得することができます。
index.jsファイルを以下のように変更してみてください。

index.js
'use strict';consthttp=require('http');constserver=http.createServer((req,res)=>{constnow=Date.now();res.setHeader('Set-Cookie','last_access='+now+';');res.end(newDate(now).toString());});constport=8000;server.listen(port,()=>{console.info('Listening on '+port);});
const now = Date.now();
res.setHeader('Set-Cookie', 'last_access=' + now + ';');
res.end(new Date(now).toString());

が変更部分です。
1行目で、変数nowにDate.now()を代入します。
2行目で、last_accessの部分を変数nowとしています。
3行目はブラウザにnowを表示するようにしています。
new Date()はデータオブジェクトを作ってるのですが、その際に
()内に指定したnowの、現在時間ミリ秒157417...をTue Nov 19 2019 22:44:31 GMT+0900 (GMT+09:00)という読みやすい形式に直してくれます。ただこのままだと、文字列として扱えないので、.toStringで文字列に変換しています。

サーバーを立ち上げると以下のような表示になっているはずです。
スクリーンショット 2019-11-19 22.56.24.png

これでcookieの取得の完了です。
今回は日付をサーバーがcookieとして返してくれましたが、
IDやパスワードの情報を送ったりすることもできます。
ただ、このようなやり方は実践的でないため、実際に使う際はモジュールを使います。

参考:new Date()の使い方
参考:N予備校 プログラミングコース

AzureのAppServiceを使ってobnizを動かしてみました!)

$
0
0

この記事はobniz Advent Calendar 2018 19日目の記事です。
AzureのAppServiceを使ってobnizを動かしてみました!

・初めに

今年のアドベントカレンダーを書くに辺りQiita側に記事1本も上げてないのはどうなんだろうと思い、去年外部サイトに書いた記事をこちらに移植しました。
なぜAzureなのかというとobniz公式さんのサイトにはAWSでのやり方は書いてあったのですが、調べてもAzureでやっている人が見つからなかったからなかったのと、VisualStudioという強力なIDEを使って楽々開発したかったからです。
またこの記事は前に自分が書いた記事をまとめて加工したものになります。本来obnizを使って別のことをやりたかったのですが、Azureを使ってやりたいという需要がもしあった時に参考にできるような記事が今のところ見つからないためこのテーマを選びました。

・自分に関しても少しだけ

元々マイコンに関しては遊びでArduinoを触っていただけでWeb系に関しては全く分からず、javascriptはHello world程度しか触ったことがないです。Node.jsが何かもまったくわからない+またクラウドも全然わからない(笑)のため至らない点も多いで細かい点はご容赦願います。
ですがそんな自分でも出来たので、そこまで難しくはないんじゃないかなと思います。


↓以下本文
※OSはWindows10、Visualstudioは2017のcommunityを使用しています。

まずAzureはMicrosoftがやっているクラウドサービスです。登録の仕方などはどこにでも載っていると思うので登録出来ている前提で始めていきます。この先選択を間違えると課金が発生してしまうこともあるのですべて自己責任でお願いします。
https://azure.microsoft.com/ja-jp/


1.まずポータル(トップページみたいなもの)にログインし、リソースの作成を選ぶとこんな画面が出てきます。この中からWebAppを選択してください。
2018-11-04_LI




2.次に出るのはこの画面です。
・アプリ名はてきとうな名前を付けましょう。
・サブスクリプションは適当なものを選択しましょう。(サブスクリプションについては絶対に他の人に伝わらないようにしてください。サブスクリプションの公開でググるとミスして公開しちゃった人が恐ろしい額の請求を受けた体験談が見られます。)
・リソースグループもてきとうで大丈夫です。
・OSと公開はデフォルトで。
・プランはfreeのものを選ぶとお金がかからないはずです。※はずです。場所はとりあえず日本のどこかにしておきましょう。
終わったら作成をクリックします。
2018-11-04 (1)_LI


3.あとはデプロイ?されるのを待つだけです。
この後の作業ではVisualStudioが必要になります。無料のやつをダウンロードしてください。
https://visualstudio.microsoft.com/ja/vs/


4.Visualstudioを開きましょう。初めの画面にある「新しいプロジェクトの作成」か左上のメニューのファイル→新規作成→プロジェクトを選択しましょう。
そうなると下のような画面が出るので左メニューにある「JavaScript」の「Node.js」を選択。
その中から「空のAzure Node.js Webアプリケーション」を選択。名前等はデフォルトでも構いません。出来たらOKを押しましょう。
2018-11-04 (2)_LI



5.とりあえずこんな感じの画面が出てくるのでデフォルトのままクラウドにあげてみましょう。
(画像のコードはデフォルトではないですが無視してください。)
右のメニューにあるプロジェクトを右クリックして、その中にある発行を選択してください。
(その前にビルドが必要かもしれないです。上のメニューバーのビルドからソリューションのビルドを行ってください。)
2018-11-04 (13)_LI



6.この画面が出てきます。
Microsoft Azure App Serviceを選択してください。
2018-11-04 (14)



7.次はこの画面もしかしたらこの画面の前にサインイン画面になるかもしれないので、
その場合はサインインしてください。(Azure登録時にたぶんマイクロソフトを作成するのでそれを使用してください。)
サブスクリプション、表示はコンボボックスから選択できるやつから適当なものを選択しください・
その下の画面に作成したAppServiceの名前が出てくると思うので、選択しOKを押してください。
2018-11-04 (16)_LI



8.その次の画面。デフォルトでいいと思います。宛先URLが接続する際に必要になるURLです。
次へを選択しましょう。
2018-11-04 (17)_LI


9.次の画面です。このまま次へを選択(構成がビルド次第で変わるためReleaseでダメな場合はDebugを選択してくさい。)
2018-11-04 (18)_LI



10.次の画面。プレビューは名前の通りの機能です。試してもいいかもしれません。
ここで発行を押すとクラウドにUPされます。
2018-11-04 (19)_LI


宛先URLに書いてあったURLにアクセスするとHello Worldと書かれたページが出てくるはずです。
(多分発行したら勝手に出てきます。)

これでNode.jsをクラウド上で動かせたはずです。


長かったですが、ここまでがNode.jsをAzure上で動かすまでになります。ここからいよいよobnizの登場です!


まず作成したデフォルトのプロジェクトにnpmを追加します。
npmは自分もよくわかっていませんがライブラリみたいなものと認識しています。
obniz用のnpmがあるのでそれを取り込んでいきます。

↓実際のやり方

visual studioで前回のプロジェクトからソリューションエクスプローラーのプロジェクト下にnpmがあるので右クリックします。その中に「新しいnpmパッケージをインストールする」があるのでクリックします。

2018-11-04 (7)_LI



クリック後、検索窓が出てくるので「obniz」と入力します。
入力すると下記のような表示になります。「obniz」を選んで「パッケージのインストール」をクリック。あとはインストールが終わるのを待つだけです。
2018-11-03



ここからようやくコードを書いていく作業に入ります。
プロジェクトがデフォルトのままなら右のソリューションエクスプローラーに
server.jsという名前のファイルがあります。
ダブルクリックして開いてください。


そこの元のコードを消して以下のコードをコピペしてください。
server.js
'use strict';varhttp=require('http');varport=process.env.PORT||1337;varfs=require('fs');http.createServer(function(req,res){fs.readFile('./Page1.html','UTF-8',function(err,data){res.writeHead(200,{'Content-Type':'text/html'});res.end(data);});}).listen(port);


Node.jsを全く知らないためてきとうに調べて書いたコードですが、何となくいらない部分がある気がします。このコードはhtmlファイルを読み込む処理が書かれてるだけです。「Page1.html」というファイルが読み込むファイルです。次はこのhtmlファイルをプロジェクトに追加します。


下の画像にあるようにプロジェクトを右クリックして「追加」→「新しい項目」を選択します。
2018-11-04 (8)_LI



下の画像のような画面が表示されるのでHTMLファイルを「Page1.html」のまま追加します。
(下にスクロールしないとHTMLファイルが出ないかも)
2018-11-04 (9)



右のソリューションエクスプローラーに「Page1.html」が追加されているのでダブルクリックして、
さっきと同じように元のコードを消して下記のコードをコピペしてください。
Page1.html
<html><head><metaname="viewport"content="width=device-width, initial-scale=1"><script src="https://obniz.io/js/jquery-3.2.1.min.js"></script><script src="https://unpkg.com/obniz@latest/obniz.js"></script><style>#on{padding:35px;}#off{padding:35px;}</style></head><body><divid="obniz-debug"></div><h1>LED Switch</h1><buttonid="on">ON</button><buttonid="off">OFF</button><script>varobniz=newObniz("ここに番号");obniz.onconnect=asyncfunction(){varled=obniz.wired("LED",{anode:0,cathode:1});$("#on").on("click",function(){led.on();});$("#off").on("click",function(){led.off();});};</script></body></html>



obnizを触ったことがある人なら誰でも見たことあるLチカのコードです。
ほぼチュートリアルのままです。(ボタンサイズだけ少し大きくしています。)
※「ここに番号」のところに自分のobnizの番号を入れてください。

htmlを使わずにNode.jsでLチカも頑張ればできそうだったんですが、めんどくさかったのでNode.jsでHTMLを読み込む方式にしました。(たぶん邪道)


出来たら(左上のコンボボックスがDebugになっている場合はReleseに変更してください)メニューバーのビルドからソリューションのビルドを選択して前と同じように発行からAzure上にデプロイするだけです。
前と同じURLにアクセスすると下の画像のような画面になるはずです。もちろんスマホからでもアクセスできます。(下記の画面はlocalで接続しています。)

2018-11-04 (13)_LI
P_20181104_234249

ON、OFFを押すとLチカできるはずです。
これでobnizの公式ページ上からではなく、自分で建てたAzure上のApp Serviceを経由してobnizを操作することができるようになりました。

※試し終わったら下記の画像のようにAzureのポータルに入りましょう。
その中で左のメニューのApp Service→自分で作成したApp Serviceを選択!→この画面の上にある停止のところをクリックするとサービスを停止させることができます。この手順で何をしているかよくわからないと思った人はとりあえず手順に従ってサービスを停止させたほうがいいと思います。
2018-11-06 (1)_LI


obnizを動かすことはAWSや他のクラウドでも出来ると思いますが、自分はvisual studioから簡単にデプロイできるのが気に入りAzureを使いました。
とても長かったですが、VisualStudioが使えると開発が多少楽になると思います。
個人的にobnizは今までのマイコンボードとは違ったベクトルのマイコンボードでとても面白いと思うので盛り上がっていってほしいです!

今回やったことは基本的に下記のページとにらみ合いながらやりました。参考にしてみてください。https://docs.microsoft.com/ja-jp/azure/cloud-services/cloud-services-nodejs-develop-deploy-app



Swaggerファイルを自動生成する

$
0
0

はじめに

Swagger形式のOASを自動生成してくれるパッケージは無いか探していたところ、、、
TSOAという良さげなやつを見つけたので試してみました!

本記事で紹介する技術は以下になります。

  • TSOA
  • Node.js
  • TypeScript
  • express
  • swagger

作業環境

OS: Windows 10 Pro
Node: 12.1.0
npm: 6.9.0 

準備

Node+TypeScriptの環境を構築します。

PS C:\Users\user\Desktop\> mkdir project; cd project;
PS C:\Users\user\Desktop\project> npm init -y 
PS C:\Users\user\Desktop\project> npm install -D typescript @types/node ts-node
PS C:\Users\user\Desktop\project> ./node_modules/.bin/tsc --init

tsconfig.jsonの設定

今回、デコレータ、importでJSONの読み込みを行うので下記のように設定にしました。

tsconfig.json
{"compilerOptions":{//importでJSON読み込み用"moduleResolution":"node","resolveJsonModule":true,//デコレータ用"experimentalDecorators":true,//初期のまま"target":"es5","module":"commonjs","strict":true,"esModuleInterop":true,"forceConsistentCasingInFileNames":true}}

package.jsonの設定

ts-nodeをnpmスクリプトで実行できるようにpackage.jsonを書き換えます。

package.json
"scripts":{"ts-node":"ts-node"}

簡易的なAPIを作ってみる

プロジェクトにTSOAをNPMでインストールします。

PS C:\Users\user\Desktop\project> npm install tsoa --save

今回は下記のような単純なAPIを用意します。

  • GETでリクエストされたユーザIDからユーザ情報を取得するAPI
  • POSTでリクエストされた情報でユーザ情報を登録するAPI

まずは、APIのエンドポイントとなるControllerを作成します。

PS C:\Users\user\Desktop\project> vi /controllers/usersController.ts
/controllers/usersController.ts
import{Body,Controller,Get,Header,Path,Post,Query,Route,SuccessResponse}from'tsoa'import{User,RequestBody}from'../models/user'import{getUser,createUser}from'../services/userService';@Route('users')exportclassUsersControllerextendsController{// IDをPATHに含めたGETなAPIを定義@Get('/get/{id}')publicgetUser(id:number):User{returngetUser(id)}// POSTなAPI@Post('/create')publiccreateUser(@Body()requestBody:RequestBody):string{createUser(requestBody)return'success'}}

続いて、ユーザ情報の型定義となるModelを作成します。

PS C:\Users\user\Desktop\project> vi /models/user.ts
/models/user.ts
// 「id」と「name」をプロパティに持つUser情報の型定義exportinterfaceUser{id:numbername:string}// 「name」をプロパティに持つリクエスト情報の型定義exportinterfaceRequestBody{name:string}

最後に、API側で処理を行う部分となるServiceを作成します。

PS C:\Users\user\Desktop\project> vi /services/userService.ts
/services/userService.ts
import{User,RequestBody}from'../models/user'// ※Databaseとかに下記のようなデータ格納されているとするconstusers:Array<User>=[{id:1,name:'Tanka'},{id:2,name:'Suzuki'}]// IDでユーザ情報を取得する関数exportconstgetUser=(id:number):User=>{returnusers.find((user)=>user.id===id)||{}}// リクエストされた情報からユーザ情報を登録する関数exportconstcreateUser=(body:RequestBody):void=>{users.push({id:users.length+1,name:body.name})}

簡易的なAPIの作成が完了しました。

続いて、Swagger.jsonとルーティングファイルを自動生成するプログラムの作成に取り掛かります。

自動生成プログラムの作成

プロジェクト直下に下記のような自動生成用プログラムを用意しましょう。

PS C:\Users\user\Desktop\project> vi /generate.ts
/generate.ts
import{generateRoutes,generateSwaggerSpec,RoutesConfig,SwaggerConfig}from'tsoa'(async()=>{// 自動生成するSwagger.jsonの設定を定義constswaggerOptions:SwaggerConfig={basePath:'/api',entryFile:'./api/server.ts',specVersion:3,outputDirectory:'./api/dist',controllerPathGlobs:['./controllers/*Controller.ts'],}// 自動生成するルーティングファイルの設定を定義constrouteOptions:RoutesConfig={basePath:'/api',entryFile:'./api/server.ts',routesDir:'./api',}// 自動生成の実行awaitgenerateSwaggerSpec(swaggerOptions,routeOptions)awaitgenerateRoutes(routeOptions,swaggerOptions)})();

ここまでの作成が終わったらプロジェクトのディレクトリ構成は以下のような感じになっているのではないでしょうか。
1.png

では、実際に自動生成を行いたいと思います。

自動生成を行う

下記のコマンドを実行します。

PS C:\Users\user\Desktop\project> npm run ts-node generate.ts

すると、無事にSwagger.jsonとルーティングファイルが自動生成されました。
20191120103750.png

生成したファイルを使ってみる

実際に生成されたファイルを使ってみます。
expressを使ってAPIサーバを立ち上げて、自動生成されたルーティングファイルを使ってみましょう。
ついてに、swagger-ui-expressを使って、自動生成されたSwagger.jsonからSwaggerUI画面を立ち上げてみましょう。

PS C:\Users\user\Desktop\project> npm install --save express @types/express
PS C:\Users\user\Desktop\project> npm install --save swagger-ui-express @types/swagger-ui-express
PS C:\Users\user\Desktop\project> vi /api/server.ts
/api/server.ts
importexpressfrom'express'import{RegisterRoutes}from'./routes'import'../controllers/usersController'importswaggerUifrom'swagger-ui-express'importswaggerDocumentfrom'./dist/swagger.json'// ユーザリクエストの解析等constapp=express()app.use(express.json())app.use(express.urlencoded({extended:true}))// ルーティングファイルの登録RegisterRoutes(app)// Swagger UIの登録app.use('/api-docs',swaggerUi.serve,swaggerUi.setup(swaggerDocument))// サーバ起動app.listen(3000,()=>{ console.log('App running at http://localhost:3000')})

サーバの準備ができたら起動しましょう。

PS C:\Users\user\Desktop\project> npm run ts-node api/server.ts
App running at http://localhost:3000

CurlでAPIを叩いてみます。

# 「Satou」さんを登録してみる$ curl -X POST "http://localhost:3000/api/users/create"-H"accept: application/json"-H"Content-Type: application/json"-d"{\"name\"
:\"Satou\"}""success"# 「Satou」さんを取得してみる$ curl -X GET http://localhost:3000/api/users/get/3 -H"accept: application/json"{"id":3,"name":"Satou"}

無事に、ユーザの登録/取得が出来ました。

http://localhost:3000/api-docs/にアクセスしてSwaggerUIの画面も確認してみましょう。
3.png

無事に、SwaggerUIの立ち上げも出来ました。

以上、TSOAを使ってみたでした!

--max_old_space_sizeか--max-old-space-sizeか--max_old-space_size

$
0
0

ググると両方でてくる。ハイフン、アンダースコア。どっちが正しいのか

TL;DR;

全部OK、混ぜてもOK

node --max_old_space_size ./server.js # アンダースコアOK
node --max-old-space-size ./server.js # ハイフンOK
node --max-old-space-size ./server.js # 混ぜてもOK

v8.2以前だとアンダースコアのみOK

$HOME/.nvm/versions/node/v7.10.1/bin/node --max_old_space_size ./server.js # OK
$HOME/.nvm/versions/node/v7.10.1/bin/node --max-old-space-size ./server.js # NG

リンク


Next.jsでenvファイルを使用する

$
0
0

背景

Next.jsでfirebaseを使ったユーザー認証を作成したときにenvファイル使ったのでそのメモです。

実施

dotenv-webpackパッケージを使用します。

yarn add dotenv-webpack

.envにはfirebaseのapiKeyとmessagingSenderIdを記述しました。

.env
REACT_APP_FIREBASE_APIKEY=""
REACT_APP_FIREBASE_MESSAGING_SENDER_ID=""
firebase.js
importfirebasefrom'firebase/app'import'firebase/auth'// Your web app's Firebase configurationconstfirebaseConfig={apiKey:process.env.REACT_APP_FIREBASE_APIKEY,authDomain:"",databaseURL:"",projectId:"",storageBucket:"",messagingSenderId:process.env.REACT_APP_FIREBASE_MESSAGING_SENDER_ID,appId:"",measurementId:""};// Initialize Firebasefirebase.initializeApp(firebaseConfig);exportdefaultfirebase

next.config.jsの設定

next.config.js
constpath=require('path')constDotenv=require('dotenv-webpack')module.exports={webpack:config=>{config.plugins=config.plugins||[]config.plugins=[...config.plugins,// 設定を記述newDotenv({path:path.join(__dirname,'.env'),systemvars:true})]config.node={fs:'empty'}returnconfig}}

終わりに

Next.jsの記事がもっと増えていってほしいですね!

TypeORM�メモ

$
0
0

多対多など追記していきたいと思っています。

1対多(OneToMany)

document: https://typeorm.io/#/many-to-one-one-to-many-relations

entity(1の方)
import{Entity,Column,OneToMany}from"typeorm"import{Member}from"./Member"@Entity()exportclassTeam{@PrimaryGeneratedColumn()id?:number@Column()name?:string@OneToMany(type=>Member,member=>member.team)members:Member[]}
entity(多の方)
import{Entity,Column,ManyToOne}from"typeorm"import{Team}from"./Member"@Entity()exportclassMember{@PrimaryGeneratedColumn()id?:number@Column()name?:string@ManyToOne(type=>Team,team=>team.members)team:Team}
import{TeamRepository}from'./repository/TeamRepository'classTeamUsecase{teamRepository:TeamRepositoryconstructor(teamRepository:TeamRepository){this.teamRepository=teamRepository}execute=(name:string)=>{constresult=awaitthis.teamRepository.find({relations:["member"],where:{name}})}}

Node.js + ApolloServer + mongodb(mongoose)でリアルタイムなGraphQLサーバを構築する。

$
0
0

どうも。オンプレのインフラ企業からWeb系企業に転職し、4ヶ月のハヤシです。

最近は業務でgraphqlに触る機会があり、おもしろいと思ったので簡単なgraphqlサーバを構築します。
あまり詳しいことは書かず、とりあえず動く!を目標に書くので、よろしくお願いします。

GraphQLについて

GraphQL(グラフQL)は、APIのために作られた、データクエリとデータ操作のための言語と、保存されたデータに対してクエリを実行するランタイムである[2]。GraphQLは、2012年にFacebookの内部で開発され、2015年に公開された
ウェブAPIの開発に、RESTやその他のWebサービスと比較して、効率的、堅牢、フレキシブルなアプローチを提供する。GraphQLでは、クライアントが必要なデータの構造を定義することができ、サーバーからは定義したのと同じ構造のデータが返される。したがって、必要以上に大きなデータが返されるのを防ぐことができる。
※wikipediaより抜粋。

REST APIが今まで主流でしたがGraphQLはそれに代わるもので、近年人気が高まっているものらしいですね。

その中でGraphQLのsubscriptionは以下の様な特徴を持っています。websocketsを通してリアルタイム通信を実現しているようですね。

GraphQLサブスクリプションは、サーバーからのリアルタイムメッセージをリッスンすることを選択したクライアントにサーバーからデータをプッシュする方法です。サブスクリプションは、クライアントに配信するフィールドのセットを指定するという点でクエリに似ていますが、サーバーで特定のイベントが発生するたびに、単一の回答をすぐに返す代わりに結果が送信されます。

今回作るもの

Message投稿できるmutationと投稿されたMessageを確認できるQuery、リアルタイムで情報を取得できるsubscriptionのクエリを付属しているPlayGroundで確認できるgraphqlサーバの作成を行います。また通常のqueryやmutationで使われるhttpServerとは別にsubscriptionはWebSocketで動作しますので、同じポートでサーバが立ち上がるように設定します。

今回の構成では基本的には以下の4つのファイルが1セットとなって動作します。

  • server.js
    → graphqlサーバを立ち上げるための記述を行います。
  • resolvers.js
    →graphqlに来たクエリなどに対しての実際の処理を記述します。DBから情報をとってくる処理など。
  • schema.graphql
    →typeDefsです。graphqlの型などを定義します。ここに定義したものだけがクエリとして受け取れます。
  • models/Message.js
    →mongodbに接続するmongooseのスキーマを定義します。

nodeを使用するので予めnodeを使用するPCにインストールしておいて下さい。
v10.14.2で動作確認をしています。

0.Package導入

  • nodemon #自動でコードの変更を検知してサーバを再起動してくれる。
  • graphql
  • apollo-server-express #expressというnodeのフレームワークをApolloServerで使えるようにしてくれたもの
  • mongoose(mongodb用) mongodbとつなぐことができるormです。
  • babel nodejsでES5?の記述方法をすることができるツール

以下packege.jsonです。

{
  "name": "graphql",
  "version": "1.0.0",
  "main": "server.js",
  "scripts": {
    "server": "nodemon --exec babel-node server.js",
    "start": "babel-node server.js"
  },
  "keywords": [],
  "license": "ISC",
  "dependencies": {
    "apollo-server-express": "^2.9.7",
    "express": "^4.17.1",
    "graphql": "^0.13.2",
    "mongoose": "^5.2.6"
  },
  "devDependencies": {
    "@babel/core": "^7.7.2",
    "@babel/node": "^7.7.0",
    "@babel/preset-env": "^7.7.1",
    "concurrently": "^3.6.0",
    "nodemon": "^1.18.1"
  }
}

Packageをインストールする。

package.jsonを任意のディレクトリに貼り付けて以下のコマンドを実行し、インストールします。

npm install

node.jsでimport文などを使用するため、以下のファイルを作成してください。

.babelrc
{"presets":["@babel/preset-env"]}

これで開発に必要な準備は整いました。

1. 必要なファイル群を作成する。

以下のファイル達をserver.jsがあるディレクトリに作成してください。

typeDefs.graphql
typeMessage{content:String}typeSubscription{messagesCreated:Message!}typeQuery{Messages:[Message]!}typeMutation{addMessage(content:String):Message!}

実際の処理は書かず、このデータがほしい!といったリクエストに対し、このデータを返します。といった型みたいなものを定義します。実際のDB操作の処理はresoloversに書きます。


models/Message.js
constmongoose=require("mongoose");constMessageSchema=newmongoose.Schema({content:{type:String,required:true},});constMessage=mongoose.model("Message",MessageSchema);module.exports={Message};

Messageモデルには内容を示すcontentのみ定義します。
DBのMessageスキーマを定義します。ここで定義したデータしかmongodbに入りません。


resolvers.js
import{PubSub}from"apollo-server-express";constpubsub=newPubSub();constMESSAGE_CREATED="MESSAGE_CREATED";module.exports={Query:{Messages:async(_,args,{Message})=>{//Messageのcontextからすべてのメッセージを降順で取得します。constmessages=awaitMessage.find({}).sort({createdDate:"desc"})returnmessages;}},Mutation:{addMessage:async(_,{content},{Message})=>{   //Messageのcontextのインスタンスを生成し、contentの内容をmongooseを通してmongodbに保存し、pubsubを使ってsubscriptionしています。これで新規メッセージを投稿する度にリアルタイムで通知が行われます。constnewMessage=awaitnewMessage({content}).save();awaitpubsub.publish(MESSAGE_CREATED,{messagesCreated:newMessage});//ここでsubscriptionに通知をしています。returnnewMessage;}},//ここでpublishされたメッセージに対してasyncIteratorを使用して非同期でくるデータに対して反復処理をしています。Subscription:{messagesCreated:{subscribe:()=>pubsub.asyncIterator([MESSAGE_CREATED])}}};

このファイルはqueryやmutationをうけての実際にDBにデータを操作する処理を記述します。
PubSubをimportし、リアルタイムでデータを受け取ることができます。
Messages、addMessageはtypeDefsで定義したMessages、addMessageと対応しています。

各メソッドの引数については以下のようになっています。基本的にはargsと、contextとresultのみ実装を意識すれば大丈夫です。引数には順番があるので、注意が必要です。
fieldName(obj, args, context, info) { result }
くわしくは以下を参照ください。
https://www.apollographql.com/docs/graphql-tools/resolvers/


最後に上記のファイル達インポートした以下のファイルを作成してください。

server.js
constfs=require("fs");constpath=require("path");//apollo関連import{ApolloServer}from"apollo-server-express";constexpress=require("express");import{createServer}from"http";//schema,resolver関連importresolversfrom"./resolvers.js";constfilepath=path.join(__dirname,"typeDefs.graphql");consttypeDefs=fs.readFileSync(filepath,"utf-8");//modelconst{Message}=require("./models/Message");// MONGOOSE接続部分constmongoose=require("mongoose");//conncectに書いてあるURLは適宜変更してください。また、通常はenvファイルなどで管理するようにして下さい。mongoose.connect("mongodb+srv://test:***********@cluster0-9v8cy.mongodb.net/graphql-test?retryWrites=true&w=majority",{useNewUrlParser:true,useFindAndModify:false}).then(()=>console.log("DB connected")).catch(err=>console.error(err));//サーバ立ち上げconstserver=newApolloServer({//ここで上記で作ったファイルを含めています。typeDefs,resolvers,//ここでreturnされたcontextをresolversの第三引数で使用する事ができます。context:async({req,connection})=>{return{Message};},playground:true,introspection:true,});constapp=express();server.applyMiddleware({app,path:"/graphql"});//subscription(リアルタイム通信設定部分)consthttpServer=createServer(app);server.installSubscriptionHandlers(httpServer);constPORT=4000;httpServer.listen(PORT,()=>{console.log(`🚀 Server ready at http://localhost:${PORT}${server.graphqlPath}`);console.log(`🚀 Subscriptions ready at ws://localhost:${PORT}${server.subscriptionsPath}`);});

mongooseはmongodbAtlasを使用しているので、以下のサイトから登録して「mongoose.connect()」の部分に接続URLを記述してください。

https://www.mongodb.com/cloud/atlas


ここまでの手順でサーバが動作する準備が整いましたので実際に動作させてみます。

2.サーバ立ち上げ

server.jsが配置されているディレクトリで以下のコマンドを使用しサーバを立ち上げてください。

npm run start

以下のような表示が出ていればサーバの立ち上げには成功しています。

🚀 Server ready at http://localhost:4000/graphql
🚀 Subscriptions ready at ws://localhost:4000/graphql
(node:88080) DeprecationWarning: current Server Discovery and Monitoring engine is deprecated, and will be removed in a future version. To use the new Server Discover and Monitoring engine, pass option { useUnifiedTopology: true } to the MongoClient constructor.
DB connected

3.動作確認

実際に以下のURLにアクセスし、playGround上で動作を確認してみます。
http://localhost:4000/graphql

以下のような画面が表示されていると思います。ここで実際にクエリなどを叩いて動作を確認していきます。
スクリーンショット 2019-11-20 22.24.32.png

右側の部分にMessageCreatedにsubscriotionのクエリを作成し、画面中央の▶ボタンを押します。
スクリーンショット 2019-11-20 22.26.56.png

画面の表示が代わり右下にlisteningとなり、Messageの投稿を監視しつづけます。
スクリーンショット 2019-11-20 22.29.18.png

これでリアルタイムにデータの更新通知を受け取る事ができます。実際にMessageを投稿するmutationを叩いてみます。
右側の部分に以下の様に入力し画面中央の▶ボタンを押します。
スクリーンショット 2019-11-20 22.41.59.png

右側に以下のように表示されたらMessageの投稿に成功しています。
スクリーンショット 2019-11-20 22.42.42.png

MessageCreatedに戻ると以下のように右側にaddMessageで追加したテキストが表示されている事がわかります。
複数ブラウザを立ち上げてもリアルタイムに更新されると思います。あとはフロント側でこの更新された通知を受け取るなどすることができます。

スクリーンショット 2019-11-20 22.44.36.png

かなり駆け足でしたが、これでリアルタイムに更新されるgraphqlサーバを構築する事ができました。
次回は、フロントで作ったgraphqlサーバと接続して簡易的なチャットアプリを作成したいと思います。

ここまで読んでくださりありがとうございました。
何かおかしな点があれば修正いたしますので、コメントください。

本当の初心者のためのNode.js超入門 ~Webサーバー構築編~

$
0
0

はじめに

 タイトルにある通り、「本当にNode.js初心者です。」という方向けに書いているこのシリーズですが、前回の記事ではNode.jsの環境構築を行いました。
 今回は、Node.jsの醍醐味である(かなり個人的主観)Webサーバーの構築を行っていきたいと思います(ローカルサーバーです)。

前回インストールが完了したので、改めてここで私の実行環境について記載しておきます。
また、今回の記事をそのまま動かすのであれば、デスクトップ上など任意の位置にhttp_test(フォルダ名は何でもOK)というフォルダを作成し、ここで作業を行うことをおすすめします。

項目スペック
プロセッサIntel(R)Xeon
メモリ16GB
OSWindows 10Pro
node.jsv12.13.0
npm6.12.0
yarn1.19.1

npmとyarnについては後ほど説明します。

モジュールのインストール

 Node.jsでは(ほかの言語もそうですが)、やりたいことに応じて様々なモジュールが用意されています。ここではWebサーバー構築に必要なhttpというモジュールのインストールを行っていきます。

モジュールって?

 かなりざっくり説明させて頂きますと、「部品」です。
 似たような言葉にライブラリやプラグインといった言葉がありますが、使用側の目線が違う程度でどれもやれることは「機能の拡張」ということで同じです。
 余力のある方はこの辺りの違いについても調べてみて下さい。いろいろな意見があります。

npmとyarn

 それでは実際にモジュールをインストールしようと思ったとき、Node.jsではどのようにするかについて説明していきます。Node.jsにはパッケージマネージャー(モジュールのインストールを行ったりするもの)としてかなりメジャーなnpmと、Facebookが作成したYARNがあります。
 どちらもできることは特に変わらないのですが、この2つにはどのような違いがあるでしょうか?

yarnはnpmの問題点を解決することが目的

 広く世界中で利用されているnpmですが、以下の2点が問題として指摘されています。

  • インストール時にコードを実行することを許可しているため、セキュリティー上の問題がある
  • インストールパッケージの速度及び一貫性が不十分

 はい。もう難しいですね。
 YARNではセキュリティー面が改善されているんだろうなっていうアバウトな認識で今は大丈夫だと思います(超入門レベルですしね)。
 てことでYARN使いましょう(npmがいいんだという方はnpmで構いません)。

yarnをインストール

 npmはNode.jsをインストールした際に併せてインストールされますが、yarnは別途インストールを行う必要があります。こちらからインストールのページに飛び、OSとバージョンを確認の上ページ下部にある「インストーラをダウンロードする」でダウンロードして下さい。バージョンについては、「安定版」と書かれているものを選ぶことをおすすめします。
 あとは落ちてきたインストーラを実行してインストールを行うだけです。

image.png

インストール成否の確認

 例のごとくバージョン確認コマンドを実行して、正常にインストールが行われているかを確認しましょう。

YARNのバージョン確認
yarn --version
YARNのバージョン
1.19.1

 このようにインストールしたバージョンが表示されれば成功です。

yarnを使ってhttpモジュールをインストール

 前置きが大変長くなってしまい申し訳ありませんが、ここでようやくモジュールのインストールを行います。先ほどインストールしたyarnのコマンドを使ってインストールを行います。冒頭に記載したフォルダにcd ./Desktop/http_testなどのように移動してからインストールのコマンドを入力しましょう。

httpモジュールのインストール
yarn add http
インストールの実行結果
[1/4] Resolving packages...
[2/4] Fetching packages...
[3/4] Linking dependencies...
[4/4] Building fresh packages...

success Saved lockfile.
success Saved 1 new dependency.
info Direct dependencies
└─ http@0.0.0
info All dependencies
└─ http@0.0.0
Done in 0.68s.

 これでモジュールのインストールが完了しました。

package.json

 ここでちょっとしたポイントです。先ほどのコマンドを入力してモジュールのインストールが完了したタイミングで、作業環境内にpackage.jsonというファイルが作成されているかと思います。中身を見てみると

package.json
{"dependencies":{"http":"^0.0.0"}}

となっていますね。
 御覧の通り、このpackage.jsonには作成したNode.jsプロジェクトで利用しているライブラリをはじめ、様々な情報が記録されています。そしてここに記載のあるライブラリに関しては、別の実行環境にプロジェクトが移動した際に一発でインストールを行うことができるようになっています。プロジェクトで実際に使用するモジュールはここに記述されるようにしておきましょう。
 ちなみに、package.jsonへの追加を行わずにモジュールをインストールするコマンドは以下です。

パッケージへの記載なしインストール
yarn install http

JSファイルの作成

 さて、前回環境構築をしましたが、まだNode.jsのコードを一行も書いていませんよね。超入門なのにモジュール周りから説明するのかよとバッシングが聞こえてきそうですが、本当に必要なことを最短でお伝えしようと思っているのでご理解下さい。
 それではいよいよお待ちかねのNode.jsのコーディングの時間です。前回の記事でも記載した通り、Node.jsはサーバーサイドのJavaScriptですので、使用するファイルの形式は.jsです。まずはJavaScriptのファイルを作成しましょう(後のことを考えてファイル名はserver.jsとでもしておきましょう)。
 試しにJavaScriptからメッセージを表示するコマンドを記述してみます。

server.js
console.log("今さらながら、ようこそ");

 作成したファイルを実行します。Node.jsでjsファイルを実行する際には、node <ファイル名>としてあげます。

Node.jsでファイルを実行
node server.js
今さらながら、ようこそ

 入力したメッセージ通りに表示が行われたら成功です。
 これ以外のJavaScriptのコードについては、随時必要に応じて説明を行います。

httpサーバーの作成

 さて、今回の記事もいよいよ大詰めです。ここからは先ほどyarnでインストールしたhttpモジュールを使って、httpサーバーを立ち上げたいと思います。
 手順としては以下の通りです。
  1. httpモジュールの読み込み
  2. httpサーバーの立ち上げ
  3. Webブラウザ上にメッセージの表示

httpモジュールの読み込み

 インストール済みのモジュールの読み込みを行います。モジュールの読み込みにはrequire('<モジュール名>')を利用し、読み込んだモジュールを変数に格納します。
 また、ここから記載するJavaScriptのコードについてはJavaScriptを始めようと思い立った人のためのJavaScript超入門で解説を行っているので、併せてご覧ください。(ここでいうとconstとかが大事な部分なので、ご一読いただくことをおすすめします。)
 先ほどのconsole.log("今さらながら、ようこそ");を削除して、下のコードを記述しましょう(これだけではまだ何も起きません)。

httpモジュールの読み込み
consthttp=require('http');

httpサーバーの立ち上げ

 次に、先ほど読み込んだhttpモジュールのcreateServerメソッドを実行してhttpサーバーを立ち上げます(ここに出てくるアロー関数も先ほどの記事で解説しています)。
 createServer()内の関数が引数として持っているreqresはそれぞれリクエストとレスポンスを示しています。使い方については後で説明します。

 そして、server.listen(port)でサーバーを待機状態にします(portは8080番ポートを利用します)。これによって、Webブラウザを通じてサーバー側にリクエストが来るのを待ち続ける状態になります。

2.httpサーバーの立ち上げ
constserver=http.createServer((req,res)=>{// ここに処理を記述});constport=8080;server.listen(port);console.log('Server listen on port '+port);

Webブラウザ上にメッセージの表示

 先述したコードの中の// ここに処理を記述の部分に、Webブラウザからアクセスがきたタイミングでリアクションしてあげる処理を書きます。

3.Webブラウザ上にメッセージの表示
res.writeHead(200,{'Content-Type':'text/html; charset=utf-8'});res.write('<h1>Hello World</h1>');

res.writeHead: リクエストに対して返す値のヘッダー情報を定義します。
res.write: Webブラウザのページ上に表示する値を指定します。

コードの実行とローカルホストの表示

ここまで出来たら実行してみましょう。nodeコマンドを実行し、ウェブブラウザでlocalhost:8080と入力してみて下さい。

image.png

すると、res.writeHeadで指定したHTMLが表示されていると思います。

 ここまで出来たら今回はこれで終わりです。
 いかがでしたか?思っていた以上に簡単にWebサーバーを立ち上げることができたかと思います。

 実はこのhttpサーバー、別のモジュールを利用してもっと簡単に立ち上げることができるのですが、本記事はNode.jsの超入門ですのでWebアプリケーションを例にNode.jsの説明を行っていこうと思っています。
 とはいえ簡単な方も皆さんには知っておいて頂きたいので、別モジュールでの書き方についてはまた別記事を記載致しますので、そちらも併せてご覧頂ければと思います。

 今回も閲覧頂きありがとうございました。次回はNode.jsで立てたサーバーで、htmlファイルを表示する方法などについて説明しようと思います。

Submarine.jsを使ってみよう

$
0
0

この記事は「メールマガジンOSPN Press」へ寄稿した記事です
転載許可いただいているので、Qiitaでも投稿失礼します

Submarine.js誕生までの物語

こんなジレンマに陥ったことはありませんか?

このPlaybookを実行しても、このroleはスキップされるはず
Ansibleは冪等性を保証しているから、1度サーバに適用したroleはスキップされるはずなんだ……
でも、本当にスキップされるだろうか?
怖くて本番環境で試せない
ShellScriptなら一瞬で書ける、でもAnsibleで実現する方法が分からない
かといってShellScriptを冪等に書こうとするとif地獄に……
Ansibleはモジュール多すぎて欲しい機能が見つからない
しかもバージョンごとに挙動もモジュール名も違うだと!?
Shellで書くべきか、Ansibleに身を委ねるべきか……
このサーバは前に作ったこのroleを変数だけ変えて再利用して……
あれ? この変数も必要なんだっけ? この変数どこで使われてるんだ?
え? ディストリビューションごとに条件分岐している?!
なるほど分からん。ようし1から作り直しだ!

Ansibleを3年間利用した私は、この全部を経験しました

このような事態に陥る原因のほとんどは、Ansibleを利用するインフラエンジニアが、プログラミングの経験が乏しいために、無計画に無秩序にコードを書きつづけたことによるものです

AnsibleはYAML記法を採用し、豊富なモジュールを提供し、単純な条件分岐とループのみのでコードの実行順序を制御します。これは、プログラミング経験の少ないインフラエンジニアでもシンプルでとっつきやすいようなデザインです。しかし、それゆえにプログラミング言語としては非力な部分も多く、扱い方を間違えると悲惨な結果を招くことになります

昨今コンテナ技術やマイクロサービス、DevOpsやSREという新たな分野の流行に後押しされて、インフラエンジニアといえどもプログラミングのスキルが求められるようになってきています

そういった背景から、現代的な高級言語の特徴を持ち、より安全かつ柔軟な開発を実現する、新しい構成管理・自動化ツールであるSubmarine.jsは開発されました

この記事では

この記事ではSubmarine.jsのインストールと簡単な利用方法からはじめ、Submarine.jsの哲学、さらに応用編として、KVMサーバのリソース状況を管理して、あいているKVMに仮想マシンを自動で構築してくれるアロケーション機能を実装する方法を紹介します

Submarine.jsのベースとなる技術としてJavaScript(Node.js)がありますが、それを厳密に理解していなくても分かるように、詳細に説明していきます

ちなみにOpenStackなどのオーケストレーションツールで実現できることを、あえてSubmarine.jsで実装するのには理由があります。オーケストレーションツールは一般に、導入の際のシステム要件が厳しかったり、1度導入してしまうと、なかなか抜け出せなかったり、新しいバージョンを追いかけてアップグレードをするのが大変だったりと、こちらもこちらで、扱うエンジニアの力量が試されるツールだったりするのです

Submarine.jsを使うと、OpenStackよりかは導入が簡単で、自分たちで欲しい機能を追加したり、不具合を修正したり、別のツールと併用したりといったことができ、小回りの効く方法でオーケストレーションを実現できるようになります

目次

  • インストールと環境構築
  • まずはコードを書いて、動かしてみよう
  • Submarine.jsはメンテナの目線でデザインされている
  • モダンなJavaScript(Node.js)のパワーを活用しよう - 仮想マシンのアロケーション機能を実装する

インストールと環境構築

まずはSubmarine.jsのインストールと環境構築をします

※この記事の手順は、Ubuntu18.04にて検証しています

Node.jsをインストール

Submarine.jsはNode.jsで開発されているため、動作させるためにNode.jsのインストールが必要です

Node.jsはChromeなどのブラウザ上で動作するJavaScriptをサーバサイドでも実行できるようにした画期的なプログラミング言語です

Node.jsのサイトには、いつくかのインストール手順がありますが、Submarine.jsの公式リポジトリに記載されているとおり、NVMによるインストールをおすすめします

NVMとはNode Version Managerのことで、Node.jsの複数バージョンを簡単に切り替えられる便利なツールです

RubyのrenvやPythonのpyenvに相当するツールですね

nvmのgithubページを見るとcurlで一発でインストールできるようにしてくれてます

$ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.0/install.sh | bash
$ tail -n 4 .bashrc
$ . .bashrc
$ nvm --version

nvm-install-cropped.png

nvmがインストールできたところで、次はいよいよNode.jsをインストールします

Submarine.jsはNode.jsの長期サポート版(LTS)である10.x系(記事執筆時)での動作が保証されていますので、今回は10.x系の最新版をインストールします

$ nvm install --lts
$ node --version

node-install-cropped.png

Submarine.jsのインストール

次にSubmarine.jsをインストールします

まずはSubmarine.jsのコードを管理するプロジェクト用のディレクトリを作成します

$ mkdir project

そしてSubmarine.jsのソースコードをダウンロードします

Node.jsのパッケージ管理ツールとしてはnpmやyarnがありますが、Submarine.jsは現時点では単にtarballを展開してインストールします

Submarine.jsの執筆時点での最新バージョンはv1.1です

$ cd project
$ mkdir -p node_modules/v1.1
$ cd node_modules/v1.1
$ curl -LO https://gitlab.com/mjusui/submarine/-/archive/v1.1/submarine-v1.1.tar.gz
$ tar xzf submarine-v1.1.tar.gz
$ mv submarine-v1.1 Submarine
$ rm submarine-v1.1.tar.gz
$ node Submarine/src/HelloWorld

submarine-install-cropped.png

これでSubmarine.jsが、使えるようになりました

まずはコードを書いて、動かしてみよう

Submarine.jsがどういうものかはおいておいて、まずはコードを書いて、動かしてみます

JavaScriptやNode.jsの知識があると、ここで登場するコードを深く理解できますが、はじめはそれらの知識は必須ではありません

Submarine.jsを利用していく中で、少しずつ身に付けていくことをおすすめします

それよりもむしろShellScriptを上手く書く技術があると、Submarine.jsのパワフルさを感じることができるでしょう

サーバの状態を取得する

projectディレクトリに戻って、以下のファイルを作ってください

MyHost.js
constSubmarine=require('v1.1/Submarine');constMyHost=classextendsSubmarine{query(){return{hostname:'hostname -s',};}}constmyhost=newMyHost({conn:'sh',});myhost.current().then(console.log).catch(console.error);

そしてnodeコマンドで、コードを実行します

$ node MyHost.js

my-host-js-cropped.png

query関数の中で定義したhostname -sがlocalhost上のshで実行され、その結果がJSON形式で表示されます

ではMyHost.jsの中身を、順を追って説明していきます

MyHost.js
constSubmarine=require('v1.1/Submarine');

まず一行目。JavaScriptを知らない方からすると、いきなり分からない文字が出てきますが、一つずつ分解してみます

const : JavaScriptで定数を宣言する際に、定数の前に書きます。constで定義された定数には、値を再代入できないという特徴があります
require('v1.1/Submarine') : 先ほどnode_modules/v1.1/Submarineに展開したtarballを読み込んでいます

const Submarine=require('v1.1/Submarine');でSubmarineという定数にv1.1/Submarineから読み込んだものを代入するよ、という意味です
「これからSubmarine.jsのv1.1を使いますよ」と宣言している、という程度の理解で大丈夫です

そして次にMyHostという定数を宣言している部分

MyHost.js
constMyHost=classextendsSubmarine{~}

class extends Submarine { ~ }というのは、Submarineクラスを拡張して使う、という意味です

これは先ほどのrequire('v1.1/Submarine');の部分で、Submarineという定数の中にSubmarine.jsがあらかじめ用意したクラスが代入されているのですが、それに自分が使いたいようにアレンジを加えて使います、ということを宣言しているのです

クラスというのはオブジェクト指向プログラミングの世界の概念なのですが、ここでは単に「Submarine.jsがベースとなるコードをクラスという単位で、あらかじめ、まとめてくれている」というくらいの理解で大丈夫です

そして、いよいよクラスの中のShellコマンドが記述されたqueryという部分

MyHost.js
query(){return{hostname:'hostname -s',};}

これはSubmarine.jsのクラスがあらかじめ持っているqueryという関数を、自分用にカスタマイズした部分です

つまりこれは、Submarine.jsのクラスを拡張し、query関数を上書きしたことになります

ここではquery関数がreturn { ~ }で{ ~ }を結果として返すようになっています。この{ ~ }の中に{ <key>: <ShellScript>, ... }というフォーマットでShellScriptを記述しておきます。すると、のちに紹介するcurrent関数を実行した際に、Submarine.jsが、query関数のreturn結果をターゲットのサーバにログインして実行します。実行した結果、標準出力に表示された文字列が、最終的なcurrent関数の結果として返されます

query関数がreturnする値はkeyとShellScriptの組み合わせを複数持つこともできます

MyHost2.js
query(){return{hostname:'hostname -s',ip_addrs:String.raw`

        ip -o -f inet a \
          |awk '{print $4}'

      `,};}

my-host-2-js-cropped.png

上記のようにhostnameだけでなくip_addrsというkeyを追加し、ホストに設定されているIPアドレスの一覧を取得することもできます

とにかく、この部分は、プログラマが参照したい値を取得するShellScriptを自由に書いていいのです

ここで String.raw` ~ ` という書き方が新しく出てきました。これは` ~ `で囲われた文字列の中のエスケープ処理を無視して、生の文字列として扱うよ、という意味です

これがあることによって、ShellScriptを文字列として記述しても、わずらわしいエスケープ処理に悩まされずに済むのです

基本的に複数行にわたるコマンドを書くときには、必ずこれでくくることをおすすめします

また、複数のkeyとShellScriptを指定したことで、コンソール画面への出力結果のJSONにも、hostnameとip_addrsという2つのkeyと値が含まれるようになりました

ip_addrsはホストのIPアドレスが2つ以上あり、コマンドの実行結果が複数行にわたったため、結果が改行で区切られ、配列に格納されています

これがSubmarine.jsの第一の機能queryです。サーバの状態を確認するコマンドを複数実行してJSON形式にしてくれます

コードの残りの部分についても、説明しましょう

MyHost.js
constmyhost=newMyHost({conn:'sh',});

今度は小文字のmyhost定数に、先ほど定義したclassをnewして代入しています

定数の名前は何でもよいのですが、このnew Class( ~ )というのは、定義したclassを元にオブジェクトを生成するという意味です

このオブジェクトというのが、先ほども軽く登場したオブジェクト指向のオブジェクトなのですが、ここでは簡単に「定義したクラスを使いたいときにはnewする必要がある」ということだけ理解しておけばいいでしょう

new MyHost( ~ )の括弧の中で{ conn: 'sh' }と書かれていますが、これは「ローカルホストの/bin/shでコマンドを実行するよ」という意味です

この他にも{ conn: 'bash' }(=ローカルホストの/bin/bash)や{ conn: 'ssh', host: '<ip address>'}(=にssh)のように、実行先を指定できます

一度定義したクラスを、newする時に与える引数に応じて、実行先を変えることができるようになっています。これがオブジェクト指向の強みでもあります

そして最後

MyHost.js
myhost.current().then(console.log).catch(console.error);

このcurrentという関数は、Submarine.jsを拡張したクラスのオブジェクトが持っている関数です。queryで定義されたコマンドを実行し、結果をJSON形式で返す働きをします。まさに「サーバの現在の状態(=current state)」を取得する関数なのです

.thenと.catchというのは、queryで定義されたコマンドが全て成功すればthenが呼び出され、1つでもコマンドが失敗すればcatchが呼び出される、という動きをします

console.logは引数を標準出力に表示するJavaScriptの機能で、console.errorはエラー出力に出力します。つまりコマンドが成功したら標準出力、エラーになったらエラー出力に表示する、という処理を、ここでは行っています

以上がSubmarine.js第一の機能であるqueryです。queryというのは、まさに「サーバの現在の状態(=current state)」を問い合わせ(=query)する関数なのです

注意点としては、このqueryはあくまで状態の取得をするための機能なので、サーバの状態に変更を加えるコマンドは記述すべきではないということです。例えば、このquery関数の中で、新しくファイルを作成したり、パッケージをインストールしたりするようなコマンドは、書いてはいけません(書けばサーバに変更を加えることができますが、Submarine.jsでは、これはqueryに書かず、のちに紹介するcommandという関数で書く、というルールを設けています)

サーバの状態をテストする

query関数では、サーバの状態を取得することができました。今度は取得した状態をテストします

Submarine.jsでは、サーバの現在の状態が、あるべき状態なのか、そうでないのかを調べるためにtestという関数が用意されています

MyHost3.js
constSubmarine=require('v1.1/Submarine');constMyHost=classextendsSubmarine{query(){return{issue:'cat /etc/issue.net',};}test(stats){return{ubuntu_is_18_04_3_lts:stats.issue==='Ubuntu 18.04.3 LTS',};}}constmyhost=newMyHost({conn:'sh',});myhost.check().then(console.log).catch(console.error);

構造は、先ほどquery関数の例で紹介したコードとほぼ同じで、今回はquery関数に/etc/issue.netの内容をcatする単純なコマンドが定義されています

そして新しくtestという関数の定義が加えられ、さらに、current関数の変わりにcheck関数を実行しています

このtestという関数が、query関数と同じくSubmarine.jsを拡張しています

この関数の特性は、current関数の結果(すなわちquery関数で定義したコマンド群の結果)を第一引数として受け取り、それらを文字通りtestする関数となります

上のサンプルでは、queryでUbuntuのバージョン文字列を取得し、testで、その文字列がUbuntu 18.04.3 LTSと一致することを確認しています

my-host-3-js-cropped.png

このコードを実行すると、画像のようなJSON形式の値が表示されます

1つずつ、中身を確認していきましょう

stats : current関数の結果
tests : test関数でreturnされた結果
good : testsのうちテストをパスした数(trueの数)
bad : testsのうちテストをパスできなかった数(falseの数)
total : テストの数
ok : テストが全てパスした場合はtrue、1つでもパスできなかった場合はfalse

Submarine.jsはqueryにより、サーバの現在の状態を取得し、testでサーバが「あるべき状態」かどうか判定するという設計になっています

サーバに変更を加える

これまでのquery関数やtest関数は、サーバの状態を確認する、いわば読み取り(Read)の処理でしたが、今度は状態を変更する書き込み(Write)の処理を実行します

MyHost4.js
constSubmarine=require('v1.1/Submarine');constMyHost=classextendsSubmarine{query(){return{project2_dir:String.raw`

        test \
          -d /home/mjusui/project2 \
        && echo 'present' \
        || echo 'not present'

      `};}test(stats){return{project2_dir_exists:stats.project2_dir==='present'};}command(){returnString.raw`
      dir=/home/mjusui/project2/node_modules/v1.1

      mkdir -p $dir \
      && cd $dir \
      && curl -LO \
        https://gitlab.com/mjusui/submarine/-/archive/v1.1/submarine-v1.1.tar.gz \
      && tar xzf submarine-v1.1.tar.gz \
      && mv submarine-v1.1 Submarine
    `;}}constmyhost=newMyHost({conn:'sh',});myhost.correct().then(console.log).catch(console.error);

query, testときて、次はcommand関数を実装します

上記のサンプルコードでは、まずquery関数で/home/mjusui/project2がディレクトリとして存在している場合はpresent、存在していない場合はnot presentの文字列を返します。そしてtest関数でqueryの結果がpresentであることを確認しています(つまりホームディレクトリにproject2というディレクトリが存在しているか判定していることになります)

command関数の実行には、query関数の実行はcurrent関数、test関数はcheck関数、に対してcorrect関数を使用します

このcorrect関数は「testが失敗(1つでもfalseが含まれる)の場合のみcommandを実行する」という特徴があります。なので今回、新しく登場したcommand関数は「ホームディレクトリにproject2ディレクトリが存在しない場合に実行されるShellScript」を返すように書かれています

correct = 修正する、つまり失敗したテストが成功するように修正するという意味です

サンプルコードではcommand関数が呼び出された場合には/home/mjusui/project2を作成し、Submarine.jsを使えるようセットアップするShellScriptが定義されています(この記事でも紹介した手順です)

これをnodeコマンドで実行した結果が、下のスクリーンショットとなります

my-host-4-js-cropped.png

実行する前はproject2ディレクトリは存在しませんでしたが、1度の実行した後に生成されています。また、この記事で紹介したとおりHelloWorldが実行できていることから、Submarine.jsがセットアップできていることも確認できます

また2回目に実行した結果は、1回目と異なり、check関数の結果が表示されています。これは1回目のコマンド実行でproject2ディレクトリが作成されたため、2回目にはtest関数の結果が全てtrueとなったため、command関数の実行がスキップされたことを表しています

Submarine.jsはメンテナの目線でデザインされている

このようにSubmarine.jsでは、比較的にリスクの低い、状態の確認や判定(query,test = 読み込み)のコマンド、と、リスクの高い、状態の変更(command = 書き込み)のコマンドを分割することで、安全にコードを実行できるようになっています

Ansibleなどの構成管理ツールでは、コードを実行してみるまではサーバに変更が加えられるかどうかが分からないのに対して、Submarine.jsの場合はcurrent関数やcheck関数を実行すれば、サーバへの変更が走るかどうか確認できる、安心設計になっているのです

また、この設計は、現在の状態を確認し、あるべき状態を定義し、定義したとおりになるようシステムに変更を加える、というインフラエンジニアが当然のように行っているメンテナンスのプロセスと全く同じようになっています

モダンなJavaScript(Node.js)のパワーを活用しよう - 仮想マシンのアロケーション機能を実装する

ここまでで、Submarine.jsの基本的な使い方と設計思想について、説明しました。ここからは応用編です

Submarine.jsの機能を使って、OpenStackなどのオーケストレーションツールで提供されている、仮想マシンのアロケーション機能を実現します

動作環境

機能の実装に入る前に、この記事で利用した環境情報を明記しておきます

  • Submarine.js動作環境 OS : Ubuntu 18.04 Node.js : 10.16 Submarine.js : v1.1
  • KVMサーバ(2台) OS : Ubuntu 18.04 libvirtd : 4.0.0 virtinst : 1.5.1 cpu : 4 cores メモリ : 2GB ディスク : 30GB
  • 作成する仮想マシン OS : CentOS7 cpu: 2 cores メモリ : 1GB ディスク : 12GB

この記事では、KVM環境の構築は、すでに完了した状態から機能の実装をスタートしていきます

KVMサーバの環境構築ができているか確認するために、以下のコマンドで仮想マシンが作成できるか確認しておきます

$ mkdir -p tmp/isos
$ curl -LO http://ftp.riken.jp/Linux/centos/7/isos/x86_64/CentOS-7-x86_64-Minimal-1908.iso
$ virt-install \
  --name test-centos7-001 \
  --vcpu 2 \
  --memory 1024 \
  --disk size=12 \
  --noautoconsole \
  --nographics \
  --location \
    ./CentOS-7-x86_64-Minimal-1908.iso \
  --extra-args \
    'console=tty0 console=ttyS0,115200n8'
$ virsh list --all

virt-install-cropped.png

うまくいけばtest-centos7-001という名前の仮想マシンが作成されるでしょう

仮想マシン構築処理の流れ

今回はKVMサーバを2台用意しました。それらのサーバに対して、Submarine.jsを使って、以下のような流れで仮想マシンを構築していきます

  1. query関数でKVMサーバの以下リソース状況を取得する
    1-1. KVMサーバ上に、すでに存在する仮想マシン名
    1-2. 物理CPUの数
    1-3. 仮想CPUの数
    1-4. 物理メモリのサイズ
    1-5. 仮想マシンへ割り当てたメモリのサイズ
    1.6. 物理ディスクのサイズ
    1-7. 仮想マシンへ割り当てたボリュームのサイズ
  2. test関数でKVMのリソースに空きがあるか以下の式で判定する
    1-1. これから作成する仮想マシンの名前が、KVMサーバ上の仮想マシンと重複しない
    1-2. 仮想CPU数 + これから作成する仮想マシンのCPU数 < 物理CPUの数
    1-3. 仮想マシンのメモリ合計 + これから作成する仮想マシンのメモリ < 物理メモリのサイズ
    1-4. 仮想マシンのボリューム合計 + これから作成する仮想マシンのボリューム < 物理ディスクのサイズ
  3. 仮想マシンを作成する
    3-1. 2のtestが問題なかったKVMサーバの中から1台を選んで仮想マシンを作成する

複数台のサーバに対してコマンドを実行する

今回はKVMサーバが2台いるので、コードも複数台への接続に対応しなければなりません

「まずはコードを書いて、動かしてみよう」で見たサンプルコードは、全てlocalhostに対してShellScriptを実行していましたが、Submarine.jsは複数台のリモートサーバに対してShellScriptを実行することもできます。ただし、その際にはパスワードなしでsshできるようサーバに設定しておく必要があります

このあたりはAnsibleと同様です

複数台にSubmarine.jsでコマンドを実行する例を見てみましょう

これまで紹介したコードとの違いが分かりやすいよう、サンプルコードを3段階に分けて記載しています

まずは、この記事で以前にも紹介したとおり、Submarine.jsを拡張したクラスを定義します

Kvms.js
constSubmarine=require('v1.1/Submarine');constKvm=classextendsSubmarine{query(){return{hostname:'hostname -s',libvirtd:String.raw`

        virsh version \
          > /dev/null \
        && echo 'ready' \
        || echo 'not ready'

      `};}test(stats){return{libvirtd_is_ready:stats.libvirtd==='ready'};}}

ホスト名と、virshコマンドでKVM関連のツールのバージョンを取得できるかをquery関数で確認し、test関数でKVM環境が整っているかどうかを判定しています

ここまでは、これまで紹介したコードと同じようにクラスを定義した部分です

そしてここからが、定義したクラスを複数ホストに対して、適用できるようにする部分です

Kvms.js
constKvms=Submarine.hosts(host=>newKvm({conn:'ssh',host:host}),'ubu1804-kvm1','ubu1804-kvm2');

Submarine.hosts( ~ )という関数によって、定義したクラスを、複数サーバに対して適用できるようにします。ここで~の部分の2番目以降の引数には、リモートのホスト名(ここでは/etc/hostsで名前解決しています)かIPアドレスを記述します。そして、それらが1番目の引数として与えられた関数の第一引数(コード上ではhostという変数)に格納されて渡されるので、そのhost変数を使ってKvmクラスをnewします

2番目以降の引数は何個でも追加できますので、それらが増えた分だけ、1番目の引数に指定した関数が呼ばれることになります

ここでSubmarine.hosts( ~ )の第一引数に、見慣れない矢印(=>)が書かれていますが、JavaScriptでは、こういう書き方で関数を表現できます

これはfunction(host){ return new Kvm({ conn: 'ssh', host: host }); }と書くのと同じことです

こうして複数サーバに対してコマンドを実行できる、新たなクラスが生成されました

あとは新しく生成されたKvmsクラスをnewしてcheckコマンドを実行します

Kvms.js
constkvms=newKvms();kvms.check().then(console.log).catch(console.error);

ここはlocalhostでshellを実行する場合と同じで、currentやcorrect関数も使えます

実行結果は、check関数の結果がリモートサーバの数だけ配列に格納されて表示されます

kvm-js-cropped.png

これで複数台のサーバに対して、コマンドを実行できるようになりました

KVMサーバのリソース情報を取得する

いよいよ本格的にKVMサーバに対して、仮想マシンを作成する処理を実装していきます

いきなりSubmarine.jsにqueryを書いていってもよいのですが、今回は、KVMサーバ上で動作を確認しながらShellScriptを作成していくことにします

仮想マシン名の一覧を取得

新しく仮想マシンを作成する際に、作成する仮想マシン名がすでに存在するかどうか確認するため、KVMサーバから仮想マシン一覧を取得します

取得するコマンドは以下のようになります

vms.sh
sudo virsh list \--name\--all\
|grep -v"^\s*$"exit 0

CPUリソースの取得

仮想マシンを構築する際に、以下のCPUリソースに関する条件を満たす必要があります

(1)すでに存在する仮想マシンのCPUの数 + 新しく作成する仮想マシンのCPU < (2)物理CPUの数

(1)(2)のそれぞれを取得するShellScriptを書くと、こんな感じです

  • (1)すでに存在する仮想マシンのCPUの数
vcpus.sh
vcpus=0

  for vm in$(sudo virsh list \--name\--all\
    |grep -v"^\s*$");do
    vcpus=$(($vcpus+$(sudo virsh dumpxml \$vm\
      |grep "<vcpu .*</vcpu>"\
      |sed -e"s/^.*<vcpu .*>\([0-9]*\)<\/vcpu>$/\1/g")))done

  echo$vcpus
  • (2)物理CPUの数
cpus.sh
  virsh nodeinfo \
  |grep "^CPU(s):"\
  |awk '{print $2}'

メモリリソースの取得

メモリに関しても、CPUと同様の制約があります

(1)すでに存在する仮想マシンのメモリ + 新しく作成する仮想マシンのメモリ < (2)物理メモリ

これも(1)(2)を取得するShellScriptを書きます

  • (2)物理メモリ
memMB.sh
echo$(($(
    virsh \
      nodememstats \
    |grep "^total\s*:"\
    |awk '{print $3}')/1024))
  • (1)すでに存在する仮想マシンのメモリ
vmemMB.sh
vmemMB=0

  for vm in$(sudo virsh list \--name\--all\
    |grep -v"^\s*$");do
    vmemMB=$(($vmemMB+$(sudo virsh dumpxml \$vm\
      |grep "<memory .*unit='KiB'.*</memory>$"\
      |sed -e"s/^.*>\([0-9]*\)<.*$/\1/g")/1024))done

  echo$vmemMB

ボリュームリソースの取得

ボリュームの場合もCPU、メモリと同様です

  • 物利ボリューム
volGB.sh
echo$(($(cd /var/lib/libvirt/images \&&df-P.\
    |grep -v"^File"\
    |awk '{print $3 + $4}')/1024/1024))
  • すでに存在する仮想マシンに割り当てられたボリューム
vvolGB.sh
vvolGB=0

  for vm in$(sudo virsh list \--name\--all\
    |grep -v"^\s*$");do
    vvolGB=$(($vvolGB+$(sudo qemu-img info $(sudo virsh dumpxml \$vm\
        |grep "<source file='.*'/>"\
        |sed -e"s/<source file='\(.*\)'\/>/\1/g")\
      |grep "^virtual size: "\
      |sed -e"s/^virtual size: .*(\([0-9]*\) bytes)$/\1/g")/1024/1024/1024))done

  echo$vvolGB

KVMのリソースをqueryで取得する

すでに各種リソース状況を取得するShellScriptは書けたので、これらをquery関数の戻り値として定義すれば、queryの完成です

query関数が長くなりますが、そこまで読みにくくはないと思います

QueryKvm.js
constSubmarine=require('v1.1/Submarine');constQueryKvm=classextendsSubmarine{query(){return{hostname:'hostname -s',vms:String.raw`
        sudo virsh list \
          --name \
          --all \
        |grep -v "^\s*$"

        exit 0
      `,cpus:String.raw`
        virsh nodeinfo \
         |grep "CPU(s):" \
         |awk '{print $2}'
      `,vcpus:String.raw`
        vcpus=0

        for vm in $(
          sudo virsh list \
            --name \
            --all \
          |grep -v "^\s*$"
        );do
          vcpus=$(( $vcpus + $(
            sudo virsh dumpxml \
              $vm \
            |grep "<vcpu .*</vcpu>" \
            |sed -e "s/^.*<vcpu .*>\([0-9]*\)<\/vcpu>$/\1/g"
          ) ))
        done

        echo $vcpus
      `,memMB:String.raw`
        echo $(( $(virsh \
            nodememstats \
          |grep "^total\s*:" \
          |awk '{print $3}'
        ) / 1024 ))
      `,vmemMB:String.raw`
        vmemMB=0

        for vm in $(
          sudo virsh list \
            --name \
            --all \
          |grep -v "^\s*$"
        );do
          vmemMB=$(( $vmemMB + $(
            sudo virsh dumpxml \
              $vm \
            |grep "<memory .*unit='KiB'.*</memory>$" \
            |sed -e "s/^.*>\([0-9]*\)<.*$/\1/g"
          ) / 1024 ))
        done

        echo $vmemMB
      `,volGB:String.raw`
        echo $(( $(
          cd /var/lib/libvirt/images \
            && df -P . \
          |grep -v "^File" \
          |awk '{print $3 + $4}'
        ) /1024 /1024 ))
      `,vvolGB:String.raw`
        vvolGB=0

        for vm in $(
          sudo virsh list \
            --name \
            --all \
          |grep -v "^\s*$"
        );do
          vvolGB=$(( $vvolGB + $(
            sudo qemu-img info $(
              sudo virsh dumpxml \
                $vm \
              |grep "<source file='.*'/>" \
              |sed -e  "s/<source file='\(.*\)'\/>/\1/g"
            ) \
            |grep "^virtual size: " \
            |sed -e "s/^virtual size: .*(\([0-9]*\) bytes)$/\1/g"
          ) /1024 /1024 /1024 ))
        done

        echo $vvolGB
      `,};}}constQueryKvms=Submarine.hosts(host=>newQueryKvm({conn:'ssh',host:host,}),'ubu1804-kvm1','ubu1804-kvm2');constquerykvms=newQueryKvms();querykvms.current().then(console.log).catch(console.error);

実行結果はこんな感じ

query-kvms-js-cropped.png

このように、全体を実装する前にqueryだけ書いて手軽に動作を確認できるのもSubmarine.jsの利点ですね

仮想マシンを作成できるかどうかtestする

次はquery関数で取得した値をもとに、KVMサーバに仮想マシンが作成できるかどうかのtestを実装します

今回、作成する仮想マシンは以下のようなスペックです

  • 作成する仮想マシン
  OS : CentOS7
  cpu: 2 cores
  メモリ : 1GB
  ディスク : 12GB

新しく作成する仮想マシンに対して、KVMリソースに空きがあるかを確認するだけなので、test関数はquery関数ほど長くならずに済みます

先ほどのQueryKvms.jsにtest関数を追加します

TestKvms.js
constSubmarine=require('v1.1/Submarine');constTestKvm=classextendsSubmarine{query(){...}getVmSpecs(){return{name:'test-centos7-002',vcpus:2,vmemMB:1024,vvolGB:12,};}test(stats){constvm=this.getVmSpecs();return{vm_name_available:typeofstats.vms==='string'?!(stats.vms===vm.name):Array.isArray(stats.vms)?!stats.vms.includes(vm.name):false,cpus_available:stats.vcpus*1+vm.vcpus<stats.cpus,mem_available:stats.vmemMB*1+vm.vmemMB<stats.memMB,vol_available:stats.vvolGB*1+vm.vvolGB<stats.volGB,};}}constTestKvms=Submarine.hosts(host=>newTestKvm({conn:'ssh',host:host,}),'ubu1804-kvm1','ubu1804-kvm2');consttestkvms=newTestKvms();testkvms.check().then(console.log).catch(console.error);

ここでtest関数で定義された戻り値について、補足説明しておきます

  • vm_name_available
    仮想マシン名が重複していないかを確認する項目
    Submarine.jsの仕様上、KVM上の仮想マシンが1台のときにはstdoutは1行となるためquery関数の結果は文字列、2台以上のときには改行区切りの配列となるため、両パターンに対応するためにstats.vmsが文字列の場合と配列の場合で、それぞれテストする条件を変えています
  • cpu_available
    すでに存在する仮想マシンのvcpu数合計(stats.vcpus) + 新しく作成する仮想マシンのvcpu数(vm.vcpus) < KVMサーバの物理CPU数(stats.cpus)という式を表現しています。query関数の結果は全て文字列で返されるためstats.vcpus(文字列)に対して1をかける(*1)ことで数字に変換しています mem_available,vol_availableも同様

またgetVmSpecsという関数で、作成する仮想マシンのスペックを取得できるようにしています。仮想マシンのスペックを変えたいときには、この関数を修正するだけで済むので、独立して定義しています

あたまにthisをつけて実行しているのは、関数がこのクラスのものですよ、という程度の意味です。ここでは、あまり気にしなくてもよいでしょう。クラス内で同じクラスの関数を呼ぶときには、頭にthisを付けるんだ、程度の理解で大丈夫です

実行結果はこのようになります

test-kvms-js-cropped.png

ubu1804-kvm1には、先ほど作成したtest-centos7-001という仮想マシンが存在しているため、ubu1804-kvm2とはtest結果が異なることが分かります

このような場合にはubu1804-kvm2の方に仮想マシンを作成できるようにすれば、仮想マシンのアロケーション機能の実装が完了します

仮想マシンを作成する

ようやく、実際に仮想マシンを作成するところまで漕ぎつけました

test関数で、KVMサーバのリソースの空き状態を確認するところまではできていますので、あとは、空いているKVMの中から1台を選んで、仮想マシンを作成するコマンドをへ流すことができれば、アロケーション機能の完成です

まずは仮想マシンを作成するコマンドをSubmarine.jsに実装しましょう

CreateVm.js
constSubmarine=require('v1.1/Submarine');constAllocateVm=classextendsSubmarine{query(){...}getVmSpecs(){...}test(stats){...}batch(){constvm=this.getVmSpecs();returnString.raw`
      dir=/var/submarine/isos

      sudo mkdir -p \
        $dir

      sudo test -r \
        $dir/CentOS-7-x86_64-Minimal-1908.iso \
      || sudo curl -sL \
        -o $dir/CentOS-7-x86_64-Minimal-1908.iso \
      http://ftp.riken.jp/Linux/centos/7/isos/x86_64/CentOS-7-x86_64-Minimal-1908.iso \

      sudo virt-install \
        --name ${vm.name} \
        --vcpu ${vm.vcpus} \
        --memory ${vm.vmemMB} \
        --disk size=${vm.vvolGB} \
        --noautoconsole \
        --nographics \
        --location \
          $dir/CentOS-7-x86_64-Minimal-1908.iso \
        --extra-args \
          'console=tty0 console=ttyS0,115200n8'
    `;}}

AllocateVmというクラスを定義しました。queryとtestは前と変わらずです

ここで新しくbatchという関数が出てきました。「サーバに変更を加えるにはcommand関数を使う」とこの記事の前半で説明しましたが、ここではbatch関数というものを使っています

command関数が実行される条件を、もう一度確認しましすと「テストが1つでも失敗(false)の場合」でした。しかし、今回は「KVMリソースに空きがある=全てのテストが成功(true)」なので、command関数をここで使ってしまうと、本来実行してほしいサーバで実行されず、実行してほしくないサーバで実行されることになってしまうのです

そこでSubmarine.jsには「テストが全て成功(true)のときに実行される」batch関数というものが実装されています。command関数とは逆の条件で実行されます

これはまさに、「実行するために必要な前提条件を全て満たした場合のみ、バッチを実行する」という意図で実装されています

またcommand関数はcorrect(=修正する)関数で実行しましたが、batch関数はcall(呼ぶ)という関数で実行します

さらにもう一つ、仮想マシンのアロケーション機能を実装するのに、まだ紹介していない新しい機能を使います

CreateVm.js
constCreateVm=classextendsSubmarine.hosts(host=>newAllocateVm({conn:'ssh',host:host,}),'ubu1804-kvm1','ubu1804-kvm2'){complex(exams){letselected=false;returnexams.map((exam)=>{if(exam.ok&&selected==false){exam.tests.isTarget=true;exam.good++;exam.total++;selected=true;}else{exam.tests.isTarget=false;exam.bad++;exam.total++;exam.ok=false;}returnexam;});}};

これまでSubmarine.host関数は単体で使っていましたが、ここではSubmarine.host関数が返したクラスを、さらに拡張してcomplexという関数を実装しています

これまでSubmarine.jsではqueryやtestは、接続先のサーバごとに独立で実行されていましたが、このcomplexという関数は、あるサーバのテスト結果を別のサーバのテスト結果に反映したい場合に利用します

今回実装しようとしている機能は「2台のKVMサーバのうち、空いているホストを1台選択し、仮想マシンを作成する」というものですが、2台ともリソースに空きがありcomplex関数を定義していなかった場合には、2台ともに同じ仮想マシンが作成されることになってしまいます

これを避けるため、2台ともテストが成功した(リソースに空きがある)場合には、どちらか1台だけ選択し、残りの1台のテストは失敗になるよう、complex関数内で、テスト結果に情報を追加しているのです(isTargetというテスト項目を追加し、2つ以上はtrueにならないようにしています)

CreateVm.js
constcreatevm=newCreateVm();createvm.call().then(console.log).catch(console.error);

最後は応用的な機能が一挙に登場しましたが、あとはcall関数でbatchを実行するだけです

create-vm-js-cropped.png

仮想マシンが作成されたログが表示されたJSONのstdoutstdoutsというキーで確認できますね

もう1度実行すると、今度は両KVMともリソースに空きが無いため、仮想マシンの作成はされずにtest関数の結果が返ってきます

create-vm-js-twice-cropped.png

両KVMの仮想マシンを消して、再度実行すると、今度はどちらか1台にだけ仮想マシンが作成されることでしょう

おわりに

ここまでお読みいただきまして、ありがとうございます

インフラエンジニアの方は、普段JavaScriptに触れる機会がないため、Submarine.jsの独特な書き方が難しく感じられたかもしれません

一方、プログラミングに慣れている方には、よりITインフラへの間口が広がったかもしれません

JavaScriptは、従来からのWebブラウザ上で動作するコードから、今ではサーバサイドや機械学習、スマホアプリやゲーム開発、IoT分野など、ICTシステムのほぼ全ての領域で見られるようになりました

Submarine.jsが「JavaScript × インフラ」領域でのスタンダードとなれるよう、今後も果敢に開発して参ります

あなたの抱える課題に対して、最良の選択となりますことを心より願っております

以上ありがとうございました

Node.js: CLIでユーザの入力をインタラクティブに読み取る3行ぽっきりの実装

$
0
0

Node.jsでCLIアプリケーションを作る際に、インタラクティブなプロンプトを出して、ユーザのテキスト入力を受け取る実装を紹介する。

この実装は3行で済み、外部ライブラリを必要としない。

デモ

こんな感じで、ターミナルにタイプした文字をNode.jsで受け取るものを実装する。

2019-11-21 16-35-56.2019-11-21 16_36_49.gif

実装

上でのデモの完全な実装は次のコードになる:

functionreadText(){process.stdin.resume()returnnewPromise(r=>process.stdin.once('data',r)).finally(()=>process.stdin.pause())}(asyncfunctionmain(){process.stdout.write('> ')process.stdout.write('< '+awaitreadText())process.stdout.write('> ')process.stdout.write('< '+awaitreadText())})()

このうち、ユーザ入力を受け取るコアの部分は次の3行:

process.stdin.resume()newPromise(r=>process.stdin.once('data',r)).finally(()=>process.stdin.pause())
  • 1行目のresumeで標準入力の受付を再開する。
    • どこかの処理ですでにpauseしていた場合に再開しないと2行目が動かないため。
  • 2行目で標準入力を1回だけ受け取る。
  • 3行目のpauseで標準入力の受け付けを停止する。
    • ずっと受け付けっぱなしなると、CLIのプロセスが終了しないため。

Firebaseのユーザーにメールを配信する仕組み

$
0
0

実装するもの

今回は、Firebaseに登録してくれたUserにメール(登録認証メールではありません)を送る実装をしていきたいと思います。
仕組みは以下の画像に記載しております。
スクリーンショット 2019-11-21 17.19.40.png

①CloudFunctionsからFirestoreの情報を取得してレスポンスするAPIを作成
②GASでAPIを叩き、スプレッドシートに記入
③スプレッドシートからユーザー情報を取得し、GASからメールを送信する。

※CloudFunctionsから直接送る方法もあるのですが、今回はスプレッドシートにユーザー情報を保存することも踏まえたかったのでこちらの仕組みにました。

前提

  • Firebaseの基本的な操作がわかっている前提で說明していきます。
  • GASの使い方もわかっているものとしてます。

では、開発Start

① CloudFunctionsからFirestoreの情報を取得してレスポンスするAPIを作成

まずは、Firestoreは以下の画像のような仕組みにしてください。

スクリーンショット 2019-11-21 17.32.40.png

次に、CloudFunctionsの環境を整えていきます。
ターミナルやiTermを開いて、ディレクトリーに移動し、初期化コマンドを実行します。

mkdir ディレクトリ名
cd ディレクトリ名
firebase init

そうすると使用する機能を聞かれるのでFunctionsを選択し、他はEnterで進んで下さい。
完了すると以下のファイルが生成されているので確認してください。

functions
firebase.json

生成されていれば、functionsディレクトリに移動して下さい。

次に、cloudFunctionからFirestoreを操作する場合に必用なFirebaseAdminSDKをインストールしておきます。

npm install firebase-admin --save

完了したら、functions内にある、index.jsをエディターで開きます。
→index.jsでAPIを作成していきます。

コーディングする前に、Adminとして操作するために、プロジェクトの秘密鍵を生成します。

Firebaseでプロジェクトを開き→プロジェクトの設定→サービスアカウントを開きます。
スクリーンショット 2019-11-21 17.50.19.png

新しい秘密鍵を生成したらファイルができるのでそのファイルをfunctions内のindex.jsと同じ階層に配置します。
これで、Firestoreを操作できるようになります。

では、index.js内でFirestoreにからデータを取得してJSONを返すAPIを作成します。
全体像を貼っておきます。

index.js
constfunctions=require('firebase-functions');constadmin=require('firebase-admin');varserviceAccount=require('./秘密鍵のファイル');admin.initializeApp({credential:admin.credential.cert(serviceAccount),databaseURL:"https://プロジェクト名.firebaseio.com"});admin.credential.applicationDefault()vardb=admin.firestore();varuserDatas={"users":[]};exports.GetUserData=functions.https.onRequest((req,res)=>{db.collection("user").get().then(function(querySnapshot){userDatas={"users":[]};querySnapshot.forEach(function(doc){userDatas.users.push({"email":doc.data().email,"name":doc.data().name});});varjson=JSON.stringify(userDatas);res.send(json);});})

特にcodeの解説はないのですが、databaseURLのURlの部分は、秘密鍵を生成したサービスアカウントのページで確認できるので、そちらに沿って同じものを記入してください。

admin.initializeApp({
    credential: admin.credential.cert(serviceAccount),
    databaseURL: "https://プロジェクト名.firebaseio.com"
  });

これでFirestoreのデータを取得して、JSONとして返すものができたのでデプロイしていきます。
デプロイに関してもFirebase Deployという機能があるのでそちらを使用していきたいと思います。
ターミナル上で以下のコマンドを実行!

firebase deploy

デプロイが完了しましたら、URLが発行されるので(もしでてこなかったらFirebaseのページにいき、Functionsを確認して下さい。)
URLを控えて置きます。
これにて、Firebase側の処理は終了です👍

②GASでAPIを叩き、スプレッドシートに記入

では、GAS側の処理をしていきたいと思います。
まずは、emailとnameを取得して、スプレッドシートに記載するようにコーディングします。

function fetchUserInfo() {
 var response = UrlFetchApp.fetch("デプロイで発行されたURL"); 

  var json=JSON.parse(response.getContentText());
  var jsonCount = json["users"].length;

  var emails = [];
  var userNames = [];

  for (var i = 0; i < jsonCount; i++) {

    emails.push(json["users"][i]["email"]);
    userNames.push(json["users"][i]["name"]);
  };

  var sheet = SpreadsheetApp.getActiveSheet();

  /*emailをスプレッドシートに書き込む*/
  for(var i = 0; i <= jsonCount-1; i++) {
    if(!sheet.getRange(i+1, 1).getValue()){ 
      sheet.getRange(i+1, 1).setValue(emails[i]);
    }
  }

  /*userNameをスプレッドシートに書き込む*/
  for(var i = 0; i <= jsonCount-1; i++) {
    if(!sheet.getRange(i+1, 2).getValue()){ 
      sheet.getRange(i+1, 2).setValue(userNames[i]);
    }
  }

}

こちらを実行すると、スプレッドシートにemailとnameが記載されていると思うので確認して下さい。
スクリーンショット 2019-11-21 18.13.05.png

③スプレッドシートからユーザー情報を取得し、GASからメールを送信する。

最後に、GAS側でメールを送信していきます。

function sendEmail() {

  var sheet = SpreadsheetApp.getActiveSheet();

  var emails = [];
  var userNames = [];


  for(var i = 1; i <= 10; i++) {
    if(sheet.getRange(i, 1).getValue()){ 
      emails.push(sheet.getRange(i, 1).getValue())
    }
  }

  for(var i = 1; i <= 10; i++) {
    if(sheet.getRange(i, 2).getValue()){ 
      userNames.push(sheet.getRange(i, 2).getValue())
    }
  }


  for(var i = 0; i <= emails.length-1; i++) {
      var toEmail = emails[i]
      var toName = userNames[i]
      MailApp.sendEmail(toEmail, 'テスト', toName+"様");
  }

}

これにて完了です!!!!
こちらを実行すると、メールが配信されると思いますのでご確認下さい。

また、メール送信で色々カスタマイズしたいのであればこちらをご参考ください。
GASでのメール送信についてまとめてみる

最後に

今回はGASを経由したメール送信の仕組みを実装したのですが、こちらの方法ではデバックがしずらいなどの問題があったり、GAS側で処理でいる件数に制限があるので、ちょっと工夫しないいけない問題があります。
筆者もまだまだ駆け出しのものなので、この記事に誤りがあったり、他のよい方法があるなら教えていただきたい所存です。
宜しくお願いします。


homebrewとかyarnとかnodeとかnodenvとかgulpとかの開発環境の諸々のためのインストール。

$
0
0

【簡単】MacにHomebrewをインストールする方法と基本的な使い方

というわけでまずはこれ。

Homebrew macOS用パッケージマネージャー

/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

で、色々入ります。


次に、yarnのインストール。

Yarn

brew install yarn

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


で、からのnodeとかnpmのインストール。

Node.js 公式

ここで推奨版の最新のnodeを普通にサイトからインストール。


次にnodeのバージョン管理。nodenvを使います。
Node.jsのバージョンを自動で切り替えられるndenvが超便利 - Qiita

$ git clone [https://github.com/riywo/anyenv](https://github.com/riywo/anyenv) ~/.anyenv
$ echo 'export PATH="$HOME/.anyenv/bin:$PATH"' >> ~/.bash_profile
$ echo 'eval "$(anyenv init -)"' >> ~/.bash_profile

$ anyenv install --init 
↓これしようとしたらなんか↑これしろって怒られたからした(必要なのか謎)

$ exec $SHELL -l

一応.bash_profileを確認

export PATH="$HOME/.anyenv/bin:$PATH"
eval "$(anyenv init -)"

となっているはず。


ndenvじゃなく、nodenvを入れる(下記曰く、ndenvはいま非推奨)
anyenv + macOS環境構築 - Qiita

anyenv install —initしてなかったらまずは

anyenv install —init

で、

anyenv install nodenv

Install nodenv succeeded!って出るはず。

で、nodenv install -lで入れられるnodeの一覧みれる

これでローカルでプロジェクトごとにnodeのバージョンを分けられる。

$ nodenv install 10.16.00

そのディレクトリで

nodenv local 10.16.00

これでそのディレクトリのnodeのバージョンは10.16.00になる。


で、gulpのインストール。

[https://qiita.com/miiitaka/items/32836ec7a00e88600de2:embed:cite]

$ sudo npm install -g npm
$ sudo npm install -g gulp
$ sudo npm install gulp --save

【Nuxt.js】srcDirで作業ディレクトリを整理をしつつアプリを起動させてみた※初期設定推奨

$
0
0

Nuxt.jsの作業ディレクトリを整理する理由

Nuxt.jsで開発していると、ルート直下にフォルダとファイルが混在しまくってめっちゃカオス化します。

また、がっつり作業するようなコアファイル・フォルダ関係なく混在するので、脳内のメモリがどんどん食われていくんですよね…

開発が進めば進むほどディレクトリ構成がややこしくなるし、「後で変えよう!!」と言った場合にディレクトリ構成を変えるのも面倒です。

それに、共同開発してる場合だと、コンフリクトめっちゃ起きたりすると、なおさら面倒くさいみたいなこと往々にしてあるんですよね。(というか結構面倒くさかった。)

Nuxt.jsの作業ディレクトリを整理してみた

before

Nuxt.jsで普通に開発していると、下のディレクトリ構成のような感じで、ルート直下にフォルダとファイルが混在します。

app_name
├──assets
├──components
├──layouts
├──middleware
├──pages
├──plugins
├──static
├──store
├──nuxt.config.js
├──package.json
├──yarn.lock

//一部割愛

これらのディレクトリを以下のように整理します。

after

app_name
├──app
│  ├──assets
│  ├──components
│  ├──layouts
│  ├──middleware
│  ├──pages
│  ├──plugins
│  ├──static
│  ├──store
├──nuxt.config.js
├──package.json
├──yarn.lock

//一部割愛

ぶっちゃけツリーを文字で眺めても分かりづらいなと思ったので、2枚画像を貼り付けておきます。

before

スクリーンショット 2019-11-22 1.52.45.png

after

スクリーンショット 2019-11-22 1.52.12.png

これで、スッキリ度合いは伝わっていただけたかなと思ってます。

ディレクトリの構成は公式ドキュメントを参考にしました。

参考:https://ja.nuxtjs.org/api/configuration-srcdir

ただ、現在のプロジェクトではNuxt.jsとは、別でサーバーを立てるのでclientという切り分けは適してないなと思ったので、公式ドキュメントをまんま踏襲はしてません。

「サーバー側は別で作るし、Nuxt.jsで1つのアプリを構築しているようなもんだよな〜」と思ったので、もうappでいいかなと思って、そういう命名にしています。(同じプロジェクトの別案件で、Nuxtではありませんが、Flutterを使ってBLoCパターンで設計した際に、appという切り分けをしていたので踏襲したという感じです。)

ただ、ここの命名についてですが、下の引用の通りsrcとかも適切だよな〜と思いました。

ビルドを行うタイプの言語やプロジェクトではソースコードはsrc/配下に置くのが当たり前
引用:https://log.pocka.io/posts/nuxt-website-my-fav-config/

作業ディレクトリを整理して実行してもエラーが出る

作業ディレクトリを整理してyarn devnpm run devとかで起動しても以下のようなエラーが出ます。

#実行結果
ERROR  Could not compile template /../../../../../node_modules/@nuxt/vue-app/template/App.js: Cannot resolve "~/../../..
.css" from "/../../../../../../../...scss"

  at node_modules/@nuxt/builder/dist/builder.js:6013:17
  at async Promise.all (index 1)
  at async Builder.compileTemplates (node_modules/@nuxt/builder/dist/builder.js:5991:5)
  at async Builder.generateRoutesAndFiles (node_modules/@nuxt/builder/dist/builder.js:5677:5)
  at async Builder.build (node_modules/@nuxt/builder/dist/builder.js:5589:5)
  at async Object._buildDev (node_modules/@nuxt/cli/dist/cli-dev.js:98:5)
  at async Object.startDev (node_modules/@nuxt/cli/dist/cli-dev.js:56:7)
  at async Object.run (node_modules/@nuxt/cli/dist/cli-dev.js:43:5)
  at async NuxtCommand.run (node_modules/@nuxt/cli/dist/cli-command.js:2575:7)

// エラーログは適当にぼかしてます。

Nuxt.jsのsrcDirで作業ディレクトリを整理してもアプリ起動出来るようにする

結論、nuxt.config.jssrcDirで切り分けたディレクトリ名(app/)を指定してあげれば、アプリを起動することができます。

実際にソースコードを見てみましょう。

nuxt.config.js
exportdefault{// 省略srcDir:'app/'}

参考:https://ja.nuxtjs.org/api/configuration-srcdir/

これで、Nuxt.jsでディレクトリを整理しても起動するはずです。

yarn devnpm run devで起動してみましょう。

$ yarn dev

yarn run v1.15.2
$ nuxt
╭───────────────────────────────────────────────╮
   │                                               │
   │   Nuxt.js v2.10.2                             │
   │   Running in development mode (universal)     │
   │                                               │
   │   Listening on: http://192.168.100.61:3000/   │
   │                                               │
   ╰───────────────────────────────────────────────╯

ℹ Preparing project for development                                                                                                                                       19:41:34
ℹ Initial build may take a while                                                                                                                                          19:41:34
✔ Builder initialized                                                                                                                                                     19:41:34
✔ Nuxt files generated                                                                                                                                                    19:41:34

✔ Client
  Compiled successfully in 4.40s

✔ Server
  Compiled successfully in 3.76s

これで起動できました。

Nuxt.jsのsrcDirについて掘り下げ

srcDirの概要は以下です。

srcDir
このオプションで、Nuxt.js アプリケーションのソースディレクトリを指定できます。
引用:https://ja.nuxtjs.org/guide/configuration/

ソースディレクトリが何かを説明していない以上、「ソースディレクトリを指定できます。」と言われても…って感じ。

srcDirの説明をしているページだけでは、何をやってるかイマイチ分からなかったことはやや不満…

ただ、以下の説明で使い道についてはある程度理解できました。

もしアプリケーションのパスを node_modules なしで設定したいときは srcDir オプション を使ってください。
引用:https://ja.nuxtjs.org/api/configuration-rootdir

アプリケーションのパスを設定する時に、Nuxt.jsが用意してるプロパティで設定できるのは便利だな〜という感じです。

また、node_modulesは、npmyarninstallしたモジュール群が格納されています。

この辺をいじって、意味不明なバグ起きるとかになっても嫌なので触りません。

srcDirとrootDirのプロパティの関係性

以下に、srcDirrootDirのプロパティをまとめてみました。

API: srcDir プロパティ
型: String
デフォルト: rootDir の値
引用:https://ja.nuxtjs.org/api/configuration-srcdir

srcDirはデフォルトだとrootDirの値となるようです。

API: rootDir プロパティ
型: String
デフォルト: process.cwd()

Nuxt.js アプリケーションのワークスペースを指定します。
このプロパティは nuxt コマンド により上書きされ、そのコマンドの引数がセットされます(例: nuxt my-app/ を実行すると rootDir に my-app/ が絶対パス付きでセットされます)
引用:https://ja.nuxtjs.org/api/configuration-rootdir

rootDirで何もパスを指定しなかった場合は、process.cwd()が呼び出されます。

process.cwd()の役割は以下となります。

原文

The process.cwd() method returns the current working directory of the Node.js process.
引用:https://nodejs.org/api/process.html#process_process_cwd

直訳

process.cwd()メソッドは、Node.jsプロセスの現在の作業ディレクトリを返します。

また、rootDirのコマンド引数の渡し方は以下です。

$ yarn dev app/

yarn run v1.15.2
$ nuxt app/
╭─────────────────────────────────────────────╮
   │                                             │
   │   Nuxt.js v2.10.2                           │
   │   Running in development mode (universal)   │
   │                                             │
   │   Listening on: http://192.168.11.9:3000/   │
   │                                             │
   ╰─────────────────────────────────────────────╯

ℹ Preparing project for development                                                                                                                                       01:24:31
ℹ Initial build may take a while                                                                                                                                          01:24:31
✔ Builder initialized                                                                                                                                                     01:24:31
✔ Nuxt files generated                                                                                                                                                    01:24:31

✔ Client
  Compiled successfully in 4.51s

✔ Server
  Compiled successfully in 4.86s

ℹ Waiting for file changes                                                                                                                                                01:24:37
ℹ Memory usage: 223 MB (RSS: 299 MB) 

nuxt.config.jssrcDirにパスを指定しなくてもアプリは起動しました。

毎回コマンド叩くのはさすがに勘弁なので、srcDirを大人しく設定すればいいかな〜という感じです。

ちなみに、nuxt.config.jsrootDirにパスを指定しても普通に起動します。

rootDirsrcDirどちらでパスを指定すればいいのか」について、ProsConsを出すのは骨が折れるし、本質じゃないので諦めました。

そのため、脳死でsrcDirを使います。

Slack API 新機能を使ってアプリのホーム・ヴューを活用しよう🏡

$
0
0

先日おこなわれた TinySpec Osaka & Tokyoでお約束した通り、日本語の App Home チュートリアルを書きました😽
App Home Demo -Stickies

今回新しくリリースされた機能、App Homeは、ユーザと Slack を1対1で繋ぐことができるスペースで、さらにユーザに直感的にアプリを使ってもらうために加えられた機能なのです。App Home には3つのタブがあり、アプリについての情報をみるための About、チャット対話式ボット機能がある場合にそのボットとダイレクトメッセージで会話できる Messages、そして今回新しく Homeタブが加わりました。

この Home タブは、アプリと各ユーザの間を繋ぐプライベート・スペースで、動的で継続的なコンテンツを提供できるビジュアル・インターフェイスなのです。

App Home って前からあったよね?

もしかしてすでに、ユーザがアプリを開いた時に発生するイベント、 app_home_openedをご存知かもしれません。以前に紹介した Bolt JavaScript framework チュートリアルの Hello world, Bolt⚡️ Bolt フレームワークを使って Slack Bot を作ろうでも触れられています。今回はさらにそれがバージョンアップして、 Home タブとともに、メッセージだけではなく Block Kitを使って動的なコンテンツを表示するとこが可能になりました。

App Home タブ活用例

Google カレンダーの App Home タブ活用例を見てみましょう。
App Home in Google Calendar
ユーザは Home タブから毎日のカレンダーを見ることができるだけではなく、予定への返答を変更したり、Zoom などのカンファレンス・コールへのジョインなどもここから行うことができるのです。
他、ミーティングなどの予定のある1分前に通知をする機能もついているのです。グーグル、グッドジョブ👍

🔨 App Home を実装してみよう

さて、この App Home をどうやって使うことができるのか、一緒にアプリを制作しながら試してみましょう。ここでは、ユーザがちょっとしたメモをとることができる Stickies(付箋)アプリを作ってみます。

ユーザー・フロー

ここでのユーザー・フローは下の通りになります。

  1. ユーザが Slackクライアントの左メニューからアプリ名をクリック
  2. アプリの Home タブが開く
  3. ユーザが Home ビュー上にある Add a stickie (メモの追加)ボタンをクリック
  4. モーダルが表示されるので、ユーザがメモ内容を入力し Create(作成)をクリック
  5. Home が自動的に更新され、入力内容が ビューに反映される

App Home user flow GIF

アプリケーション・フロー

ここでのアプリ側のフローは次の通りになります。

  1. ユーザが App Home をオープンすることにより app_home_openedイベントが発生し、アプリのサーバに情報が送信される
  2. アプリは、そのイベント・ペイロードから user ID を抽出し views.publishメソッドによって、ボタン UI のある初期画面を表示
  3. ユーザがその “Add a Stickie” ボタンをクリックすることにより、そのインタラクションがトリガーされる
  4. アプリは views.openメソッドで入力フォーム UI のあるモーダルを表示
  5. ユーザがそのフォーム内容を送信することによって、view_submissionのついたもうひとつのインタラクションがトリガーされる 
  6. views.publishメソッドで App Home ビュー表示を更新

Diagram

さて、大まかなフローがわかったところで実際に Stickies アプリを作ってみましょう。 Glitch というサービス上にソースコードを置いてありますので "remix" してみましょう(リミックスとは GitHub の fork のような物、と思ってください)。このサービスではウェブ上でコードのエディットと実行ができますので、デプロイは不要なのでそのまま動かしてみましょう。

🎏🥫 Glitch でソースコードだけ覗いてみる
🎏🍴 Remix (fork) して自分で使う

⚙️ アプリのセットアップ

まずは Slack アプリの設定をしなければなりません。 Slack App マネージメントでアプリの作成をします。ポップアップが出ますのでそこでアプリの名称を設定し、アプリのインストール可の(=自分がアドミン権限を持つ)ワークスペースを選択してください。ない場合は新規でワークスペースを作成してから始めましょう。

アプリの初期設定ができましたら、次は Features > Bot User設定画面でボット・ユーザを有効にしてください。そのままデフォルト、もしくは任意の名前を決めてください。

今度は Features > OAuth & Permissionsで Bot トークンのスコープを追加します。chat.writeを選択してください。 (実際にはこのサンプルアプリにはチャットの機能がありませんので、このスコープを使う必要がないのですが、とりあえず追加してください! この最近追加された、以前より細やかになった権限設定については OAuth 2.0, Version 2 ドキュメントを参照してください。)

次に Features > App Home (下のスクリーンショットの、ステップ1参照)へ行ってください。このフィーチュアは 2019年11月現在ではまだベータですので、ベータ版を使うためにサインアップ(ステップ2)をしてもらう必要があります。
app_home_beta.png
サインアップをしましたら、Home タブが使えるようになります。このサンプルアプリではダイレクトメッセージは使用しませんが、Message タブはそのまま有効のままでも良いでしょう。

次に Features > Event Subscription画面へ行き、イベントを有効にしてください。(下のスクリーンショットのステップ1参照)。そして Request URL (ステップ 2) を入力します。Glitch 上のコードをリミックスして作業を進めている方は、 Request URL は https://プロジェクト名.glitch.me/slack/eventsのようになります。 (Glitch は新規でプロジェクトが作成される毎にプロジェクト名を自動生成します。 たいがいはハイフンで繋いだ2つの英単語からランダムに生成されるので、 fluffy-umbrellaのようなものになっていると思います。自分でカスタムのプロジェクト名もつけられますので、その際はそのカスタム名を使ってください。自分のサーバを使用している際は、そのURL + /slack/eventsにします。)

Request URL 入力が終わったら、Subscribe to bot eventsまでスクロールし、 app_home_openedイベントを加えてください (ステップ 3)。そして緑の Save Changesボタンで保存します。(ステップ 4).
events.png

次は同じように Features > Interactive Componentsで Slack サーバにインタラクティブ・ペイロードの送信先を伝える必要があります。 ここでも Request URL を指定してください。URL は https://プロジェクト名.glitch.me/slack/actionsとなります。そしてここでも Save Changesボタンで変更を保存してください。

ここで一度インストールしましょう。 Features > OAuth & Permissionsからインストールボタンで自分のワークスペースにこのアプリをインストールします。画面の指定にしたがってインストールしてください。一旦 OAuth を使ってのインストールプロセスが終了しましたら、次の場面でアクセス・トークンが発行されます。

さて、次はブラウザ上で使えるIDEの Glitch プロジェクト、またはお使いのコード・エディタに移りましょう。インストール終了時に発行された xoxb-トークンをコピーし、環境変数ファイル、.envSLACK_BOT_TOKENの値としてペーストしてください。
Glitch .env

もう一つの環境変数である、 Signing Secret キーは、Slack App マネージメント画面の Settings > Basic informationから取得することができます。

🖼 App Home を表示させよう

Express server で Node アプリをセットアップ

このチュートリアルでは、Node.js と Express サーバを使ってアプリを書いています。ここでは全ての API コールは、ごく一般的な HTTP リクエスト、レスポンスで行っていますので、Node 以外の言語をお使いの皆さんにも、ソースコードをみてもらえればどこでどのように API を呼び出しているかわかっていただけるかと思います。

⚡️ もっと簡単に Bolt フレームワークで書きたい!という方のためにも Bolt で書かれたソースコードも用意しています。しかしこのチュートリアルの説明そのものは Bolt や SDK、他のボット・フレームワークを使わない「バニラ」コードでの説明となります。

まず Node コードで、依存モジュールを追加して Express サーバを走らせます。そして raw リクエスト・ペイロードを使ってリクエスト情報の認証をします。詳しい説明は割愛しますが、index.js の 31 - 38 行目を参照してください。 143 -145 行目では Express サーバを実行しています。

リクエスト情報の認証についてもう少し詳しく知りたい方は、以前書いたチュートリアルの Slack メッセージ・アクション API を使ってディスカバラブルなアプリを作ろうの中のセクション、🔐 リクエスト情報の認証)を読んでみてください。

app_home_openedイベント・ペイロードを受け取るアプリケーション・エンドポイント

次に HTTP POST のルーティングメソッドを使って、イベントペイロードを受け取るエンドポイントを定義します(前のステップで設定した、Request URL)。何かのイベントが発生した際にはこのエンドポイントに Slack API からの JSON ペイロード情報が届くので、そこでイベントが app_home_openedであるかチェックし、そうであれば App Home ヴューを表示させる準備をします。

app_diagram_app_home_opend.png

ここでは、読みやすさを考え、簡略化されたコード・スニペットを使っていますが、全てみたい方は、 index.js 45 - 70 行目)をみてください:

app.post('/slack/events',async(req,res)=>{const{type,user,channel,tab,text,subtype}=req.body.event;if(type==='app_home_opened'){displayHome(user);}}

さて、ビューの表示には、リッチなコンテンツを Block Kitを使って構築しましょう:

constdisplayHome=async(user,data)=>{constargs={token:process.env.SLACK_BOT_TOKEN,user_id:user,view:awaitupdateView(user)};constresult=awaitaxios.post('/views.publish',qs.stringify(args));};

Home App にコンテンツを表示させるには view.publishメソッドを使います。 ここでは axiosモジュールをを使って HTTP POST 経由で API を呼び出しています。

Block Kit でリッチ・コンテンツを構築

ここでの例では、別の関数、updateViewを呼んでコンテンツを JSON で構築しています。この関数は Home ビュー UI を更新する毎に呼びだしています。

初期 UI の設定はこのようになります:

constupdateView=async(user)=>{letblocks=[{// Section with text and a buttontype:"section",text:{type:"mrkdwn",text:"*Welcome!* \nThis is a home for Stickers app. You can add small notes here!"},accessory:{type:"button",action_id:"add_note",text:{type:"plain_text",text:"Add a Stickie"}}},// Horizontal divider line {type:"divider"}];letview={type:'home',title:{type:'plain_text',text:'Keep notes!'},blocks:blocks}returnJSON.stringify(view);};

blocks配列部分は、Block Kit をつかったプロトタイプはこちらから見ることができます

実際のコードでは、この関数では、モーダルからユーザ入力された値を使い動的なコンテンツを扱います。この部分は後ほど。

ユーザのボタンクリックのハンドリング

ユーザがボタンをクリックするとモーダルが開きます。
app_diagram_button_click.png

Block Kit メッセージ・ビルディングブロックには action_idを定義してください。これはデータを受け取る識別子となります。(ここでは add_note)。

ユーザがボタンをクリックすると API サーバから Request URL に送信されるこのアクションについてのペイロードには trigger_idが含まれます。これはモーダルを開くための識別子となります。

app.post('/slack/actions',async(req,res)=>{const{token,trigger_id,user,actions,type}=JSON.parse(req.body.payload);if(actions&&actions[0].action_id.match(/add_/)){openModal(trigger_id);}});

モーダルを開く

次に入力フォーム・エレメントを、モーダルの中に表示しましょう。この例ではごくシンプルに、付箋にメモするテキストを入力するマルチライン(複数行)インプット・ボックスと、付箋の色を指定するドロップダウン・メニューを表示させます。

モーダルを表示させるには views.openメソッドを呼びます:

constopenModal=async(trigger_id)=>{constmodal={type:'modal',title:{type:'plain_text',text:'Create a stickie note'},submit:{type:'plain_text',text:'Create'},blocks:[// Text input{"type":"input","block_id":"note01","label":{"type":"plain_text","text":"Note"},"element":{"action_id":"content","type":"plain_text_input","placeholder":{"type":"plain_text","text":"Take a note... "},"multiline":true}},// Drop-down menu      {"type":"input","block_id":"note02","label":{"type":"plain_text","text":"Color",},"element":{"type":"static_select","action_id":"color","options":[{"text":{"type":"plain_text","text":"yellow"},"value":"yellow"},{"text":{"type":"plain_text","text":"blue"},"value":"blue"}]}}]};constargs={token:process.env.SLACK_BOT_TOKEN,trigger_id:trigger_id,view:JSON.stringify(modal)};constresult=awaitaxios.post('/views.open',qs.stringify(args));};

一見長いコードですが、ほとんどは JSON で UI を描いているだけです。 Block Kit プロトタイプはここから見ることができます。

フォーム・サブミッションのハンドリング

ユーザからのフォーム・サブミッションのハンドリングは、ボタンクリックのハンドリングと同じようにすることができます。
app_diagram_modal_submit.png

モーダル内のフォームがサブミットされた際には、このアクションのエンドポイントにペイロードが届きます。ボタンクリック時と区別するにはペイロードに含まれる typeの値をチェックしてください:

app.post('/slack/actions',async(req,res)=>{const{type,user,view}=JSON.parse(req.body.payload);elseif(type==='view_submission'){res.send('');// Make sure to respond to the server to avoid an errorconstdata={note:view.state.values.note01.content.value,color:view.state.values.note02.color.selected_option.value}displayHome(user.id,data);}});

この部分のコード全てを見るには index.js 107 - 133 行を参照してください。

App Home ビューの更新

ここでユーザから追加されたデータを元のデータに追加して、 Home タブの内容を
views.publishメソッドと使って書き換えます。

このサンプルコードでは、単純化するために永続データの格納に node-json-dbモジュールを使用しています。ユーザが新しいメモを追加する毎に、そのデータが下のデータ配列にプッシュされていきます。

その新しいデータを、元の JSON にアペンドしてできた新たな UI ブロックを、 views.publishメソットで再表示しています。

実際のソースコードは appHome.js の 17 - 152 行をみてみてください。このあたりは自分の好きなように処理してみてください。

アプリを実行してみよう

さて、これで一通りアプリが動くはずですので試してみましょう。Slack クライアントのサイドメニューの Appでみられるアプリ一覧からこの Stickies! アプリを探してクリックしてみてください。無事、App Home が表示されましたか?

Add a Stickie ボタンを押して、新しい付箋が Home ビューに反映されたか確認してみてください。🎉
tada_it_works.png

よりよいユーザ・エクスプリエンスを確立するには

さて、このチュートリアルが、新しいアプリを作る、もしくは既存のアプリをアップデートするためのインスピレーションになっていただけたらうれしいです。

ここでは viewsメソッドを用いた基本的な App Home 構築について書きましたが、次のチュートリアルでは、Shane DeWael (シェーン・デワエル)がベスト・プラクティスについて書いてゆきますので乞うご期待!

📄Related Slack API Documentation


ご質問がありましたら、私、 Tomomi @girlie_mac@SlackAPIへ日本語でツイートしてください!(もちろん英語も歓迎!)

原文 Tutorial: Building a home for your app 🏡 by Tomomi Imura (Slack)

英語版は西海岸時間の、11月22日にアップしますので、それまでは 404 Not Found 画面でゲームでも楽しんでください。


【Mac】Node.jsインストール by Homebrew

$
0
0

環境

MacOS High Sierra 10.13.3

手順

Homebrewインストール

・インストール

$ /usr/bin/ruby -e"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

※エラーになる場合は以下を実行してから行うと良い。

$sudo chown-R$(whoami)$(brew --prefix)/*

・インストール確認

$ brew help

nodebrewインストール

・インストール

$ brew install nodebrew

・パスを通す

$echo'export PATH=$HOME/.nodebrew/current/bin:$PATH'>> ~/.bash_profile
$source ~/.bash_profile

・インストール確認(バージョンが表示されればOK)

$ nodebrew -v

Node.jsインストール

・ディレクトリを作成

$mkdir-p ~/.nodebrew/src

・インストール可能なバージョンを確認

nodebrew ls-remote

・インストール
 - バージョン指定でインストール

$ nodebrew install-binary {version}

 - 最新バージョンをインストール

$ nodebrew install-binary latest

・インストールしたバージョンを確認

$ nodebrew ls

・有効化

$ nodebrew use {version}

・npmバージョンを確認

$ npm -v

WebstormのFileWatcher時に出る「env: node: No such file or directory」エラー

$
0
0

概要

  • JetBrains製品であるWebStormに内蔵されている「File Watcheres」という機能を使っているときに遭遇したエラーの解消方法
  • おそらくPhpStormなど他の製品でも同様の事象になる可能性がある

対象読者

  • 表題のエラーに遭遇した人
  • JetBrain製品使い

エラー事象

概要にも記載している通り、WebStormに内蔵されている「File Watcher」という機能を使おうとしたときに出たエラーです。
具体的にはファイル保存時に自動でPrettierをかけようと下記の通り設定をしようとします。

image.png

image.png

この状態で保存をして対象のファイル(今回の例ではTypescriptファイルである.tsファイル)を編集して保存し、動作を確かめます。
この際に下記のエラーに遭遇しました。

/path/to/project/root/node_modules/.bin/prettier --write index.ts
env: node: No such file or directory

Process finished with exit code 127

エラー調査

見たところ、単にNodeのバイナリが見つかっていないだけに見えます。通常のターミナル、WebStorm内蔵のターミナルからバイナリが引けるか確認してみます。。。普通にひけるやん。

image.png

シェル自体(筆者の環境ではzsh)にはnodeのパスは通っているように見えます。。。

解決方法

File Watchersの各コマンドには環境変数が設定できるところがあります。一番下のEnvironment variablesです。
image.png

ここを見ると、システム環境変数を含むという項目にチェックが入ってますが、SHELLがbashになっていたり、PATHが通常のターミナルを立ち上げて出力される結果よりも明らかに短い事がわかりました。

image.png

根本解決にはなっていない気がしていますが、ターミナルから下記コマンドを打ち込んで、PATHの設定内容を吐き出させます。それをコピーしてコマンドの環境変数のPATHとして登録をします。

$ echo$PATH
/Users/h・・・・・・・

image.png

環境変数を登録した状態で変更を保存することで無事nodeのバイナリが読み込まれてPrettierを実行できました。

Viewing all 9082 articles
Browse latest View live