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

Discord.js 技術メモ #1

$
0
0

アクティビティを設定する

Botのアクティビティを設定する

client.on("ready",()=>{client.user.setActivity("Yuki | https://discord.gg/CN4dYAVYXW",{type:'PLAYING'});});

ファイル読み込み

ファイルを読み込み配列に入れる

constfs=require('fs');constreadline=require("readline");letreaddata=[""];varstream=fs.createReadStream("yuki/read.txt","utf8");varreader=readline.createInterface({input:stream});reader.on("line",(data)=>{readdata.push(data)})

ファイル書き込み

ファイルを書き込む配列に入れる

constfs=require('fs');fs.writeFileSync('yuki/write.txt','data');

特定のチャンネルにメッセージを送信する

特定のチャンネルにメッセージを送信する

client.channels.cache.get('000000000000000000').send({embed:{author:{name:"結貴 - Yuki による宣伝",icon_url:client.user.avatarURL()},description:"公式サーバーの招待URL \n https://discord.gg/CN4dYAVYXW",color:10181046,timestamp:newDate(),footer:{icon_url:client.user.avatarURL(),text:"結貴 - Yuki"}}})

メンバーにロールを付与する (Role Name)

メッセージの送信者にロールを付与する

constmember=message.guild.members.cache.find((member)=>member.id===message.author.id)member.roles.add(role)

メンバーからロールを剥奪する (Role ID)

メッセージ送信者からロールを剥奪する

constrole=message.guild.roles.cache.find((role)=>role.id==="000000000000000000")constmember=message.guild.members.cache.find((member)=>member.id===message.author.id)member.roles.remove(role)

わからないことがあったら

ここで質問を受け付けております


LINE × スプレッドシートのススメ(家計簿アプリ編)

$
0
0

はじめに

最近ですが、LINEでやり取りするトランザクションデータをデータベースではなく、ユーザのスプレッドシートで直接管理することを推してます

メリットとしてはこんな感じです。
1. サービス内でデータを保持しないということは、データ保持の責任リスクを軽減できる
2. ユーザに馴染みのあるExcel形式であり、スプレッドシートの機能で可視化できるのでデータ分析もしやすい

このメリットを活かすサービスとして、家計簿アプリを作りました。

家計簿アプリについて

コンセプト

  1. 後で家計簿に書こうとしても、ついつい忘れてしまう。なので、モバイルアプリで買ったその場で登録!!
  2. それでも人間は忘れてしまう。そんな時は、モバイルアプリでちまちま登録せず、PCでまとめて登録しよう!!
  3. 使い慣れたExcel形式だから、自分の好きなようにデータ分析もできる!!

2020_AdventCalendar用画像.png

2020_AdventCalendar用画像 (1).png

一応、以前作ったプレゼンですが、こちらでもコンセプトを語ってます。

(参考)LIFF×スプレッドシートのススメ

今回の改良点

今までは単純に買ったものをスプレッドシートに書き込むだけでした。
qiita①.png

巷でこんな声を聞きました。
「買い物してたら、ついつい買いすぎちゃって、月の生活費の予算を超えちゃったわ」

・・・なるほど。
じゃあ、月のトータル金額が見れるようにしましょう。

サーバー環境

サーバサイドはNode.jsで作っており、スプレッドシートの操作は以下のモジュールを使ってます。
google-spreadsheet

※Githubにも全ソースを上げてますが、以下のソースコードはQiitaに載せるにあたって分かりやすくするためにGithubのソースコードとは多少異なります。

家計botアプリ

まずは月の集計金額を出す必要があるので、query関数をスプレッドシートに書き込んでます。

letdoc=newGoogleSpreadsheet(sheetId);// スプレッドシートIDを指定// 認証関連のコードは省略します(※そのうちQiitaに記事書きます)// スプレッドシート(doc)から、対象のシート(sheet)を取得したところから書きます。// query関数を書き込むセルの指定awaitsheet.loadCells('F1');letcell=sheet.getCellByA1('F1');// 関数を書き込む際は「formula」を使います。cell.formula=`=query(A:C,"select B,sum(C) where B is not null group by B label B 'sum 買ったもの'",1)`;sheet.saveUpdatedCells();

これで、query関数を用いて集計できました。
qiita②.png

これだけではツマラナイので、グラフも入れて分かりやすくしてみます。
とはいえ、スプレッドシートの関数でグラフ化ができるのは今のところ少ないようです。

※本当は円グラフを使いたかったのですが、関数ではできないようなので棒グラフにしてみました。

// for文でぐるぐる回して、SPARKLINE関数を使って棒グラフを追加してますfor(leti=2;i<=index;i++){awaitsheet.loadCells(`H${i}`);letcell=sheet.getCellByA1(`H${i}`);cell.formula=`=SPARKLINE(G${i}, {"charttype","bar";"max",MAX(G:G)})`;sheet.saveUpdatedCells();}sheet.saveUpdatedCells();

すると、こんな感じでグラフが出ました
qiita③.png

モバイルでも確認できますので、買い物中も見れます

今後の改良点

実は、買ったもの(商品名)で集計しているので、例えば「生活費」みたいなカテゴリで集計する形にはまだなってません。
今度はその辺りを直したいと思ってます。

最後に

今回は改良した部分を中心に記事を書きましたが、そもそもユーザのスプレッドシートに書き込むための設定方法については書いてません。
次回はその辺りを書きたいと思います。

【TypeScript】型付けしてエラーを片付ける

$
0
0

この記事の目的

TypeScriptを使うとJavaScriptで発生する予期せぬバグが減るっていわれてるけど、具体的にどのようなケースでエラーが減るの?という疑問を具体的な例で解決する。

どんなエラーが片付くか?

ケース1: タイプミスが減りバグが抑制される

TypeScriptは、JavaScriptで起こりうるタイプミスや型付けができないことによって発生するバグを抑制してくれます。

以下のJavaScriptのコードを見てみましょう。
このコードは最後のコンソール出力でnoteBook.colorを出力しようとしていますが、noteBook.colorrとなっているためタイプミスが発生していると考えられます。

sample.js
constnoteBook={color:"",price:980,size:"A4"}console.log(noteBook.colorr);// colorをcolorrにしてある

こちらのJavaScriptのコードを実行してみると、undefinedが出力されます。
特にエラーの文言は出力していないため、この変数を使った処理で予期せぬバグが発生しない限り、このエラーには気付けません。

$ node sample.js
undefined

そこで、TypeScriptの登場です。
上記のsample.jssample.tsにコピペしてみます。

sample.ts
constnoteBook={color:"",price:980,size:"A4"}console.log(noteBook.colorr);// Property 'colorr' does not exist on type '{ color: string; price: number; size: string; }'. Did you mean 'color'?

JavaScriptの場合だとタイプミスがあった場合でもundefined が返却されるだけでしたが、TypeScriptの場合はコーディング中に指摘してくれます。
エラーとしては、type '{ color: string; price: number; size: string; }' にはプロパティ 'colorr' が存在しません。colorのことでしょうか?と出力してくれています。
VsCodeでコーディングした場合はこのように指摘してくれます。(ありがたい...!!)
image.png

ケース2: 静的型付けで変数の型を制約させる

JavaScriptの変数は動的型付けとなり、どんな値でも変数に代入することができます。
しかし、複数人でアプリケーションを構築する場合、予期せぬ値を使用されてしまうことがあります。
例えば、「数値型を引数に取りたい関数に文字列を渡してしまう」などです。
TypeScriptでは予め変数の型を定義できるため、他の値が設定されそうになると「この変数(引数)は数値型なので文字列型は代入できませんよ」と伝えてくれる機能があります。

まずは以下のJavaScriptのコードを見てみましょう。

sample2.js
functionsum(a,b){console.log(a+b);}sum(1,2);

こちらのコードを実行すると以下のように出力されます。

$ node sample2.js
3

当たり前ですね。笑
次に、sum関数の引数に文字列を入れてsample2.jsを実行してみようと思います。

sample2.js
functionsum(a,b){console.log(a+b);}sum(1,"2");//2つ目の引数に文字列"2"を代入

こちらを実行したら、一般的にはエラーを出力してもらいたいところですがJavaScriptの場合は以下のようにエラーを出力せずに実行できてしまうんです。。

$ node sample2.js
12

このようにsum関数に文字列"2"が代入されたことによって、 JavaScriptさんはa + bを文字列の連結と認識してしまい"1" + "2" = "12"という処理をしてしまったようです。
これはsum関数を実装したプログラマーにとっては予期せぬ振る舞いですね😱

こういったエラーですが、TypeScriptの型定義で解決できます!!
以下のコードは上記のsampe2.jsのsum関数の引数に対して、型定義を施してあります。
型定義の方法は簡単で、引数(変数)のとなりに:型を書くことで実現できます。
今回は、引数には数値型しか受け付けたくないので、引数の隣に:numberを記述しています。

sample2.js
functionsum(a:number,b:number){console.log(a+b)}sum(1,"2");//Argument of type 'string' is not assignable to parameter of type 'number'.

お、TypeScriptさんはコード実行前に最後の行に対して、エラーを指摘しているようです。
string 型の引数は number 型のパラメータには代入できません。と指摘されています。
そのとおりですね。とても親切。

ちなみにVsCode上だとこのようにエラーが表示されます。
image.png

まとめ

以上がTypeScriptを使うことによって、エラーが解決される具体的なケースでした。
TypeScriptを使えば、エラーに迅速に気づけますし、バグを含んだコードをcommitするリスクも低減されるので手戻り工数も削減されます。

それでは、よいTypeScriptライフを!

もし、よろしければLGTMいただけると幸いです!(ブログを書く励みになります!)

NRIハッカソン bit.Connect 2020 に参加してきたまとめ

$
0
0

はじめに

NRIハッカソン bit.Connect - Hack for NEWSTYLE というイベントに参加してきました。
https://bitconnect.nri.co.jp/

IBMクラウドのファンクションを活用していたのを評価され、IBM賞をいただきました!

企業さんの技術サポートも手厚く、第一線の方に直接Slackで手取り足取り教えてもらえたのでめちゃくちゃ贅沢な時間を過ごせました!

つくったもの

お地蔵さんデモ動画
https://www.youtube.com/watch?v=8mviNWsBKp8

構成図
コンセプト_page-0016.jpg

ハードウェア側の動作
ojizo_kasa_kick.gif

コンセプト_page-0015.jpg

余談ですが、最初は磁石にピップエレキバンを使ってみたんですが、意外と磁力が弱く、動作が安定しないので、100均で買った磁石に付け替えました。
IMG_0646.jpeg

ストーリーなど作品の詳細はこちら
https://protopedia.net/prototype/2151

ハードウェアの実装

今回は2人チームで参加し、私の担当がobnizを用いたハードウェアの部分だったので、実装について困ったことや役に立ったことなどをつらつらと書いていきます

IBMクラウドをはじめて触った友人(AWSは日常的に触ってる人)曰く、とても使いやすかったとのこと
・AWSみたいな迷子になりそうなUIではなく、やりたいことがどうしたらいいのかすぐわかるような素晴らしいUI
・ハッカソンで使うなら、初めてでもこっちの方が楽かもしれない
2021-02-25_22.28.51.png
2021-02-25_22.28.01.png

とのことなので、私も触ってみようかなと思いました。

構成について

今回の作品でobnizでやるべきことは以下の3点でした。
1. IBMクラウドのファンクションからAPIを叩いてハードウェアで動作をさせる
2. 1が起きてから元に戻るまでの時間を計測する
3. 2の結果をIBMクラウドのファンクションに知らせる

1~3の流れは、ユーザーがすぐ行動すれば一瞬ですが、なかなか行動しないユーザーもいると想定され、待機時間が読めません。
なので本来は、1の機能だけを請け負うobnizと2~3の機能だけを請け負うobnizの2台構成でやるべきですが、所持台数の制限と、ハッカソンという限られた時間の中で対応するために、1~3を一つのobnizで実装できるように試行錯誤を行いました。(obnizの木戸さんありがとうございました!)

obnizクラウドでWebhookURLを吐き出す

結論から言うとこれは、今回の要件には適していなかったので他のサービスを利用しました。

obnizクラウドはobnizのホスティングサービスで簡単にWebhookURLを吐き出せました(今回初利用)

obniz のコンソールにアクセスし、デバイスを選択します(デバイスをまだアカウントに紐づけてない人は紐付けをしてから)
スクリーンショット 2021-03-04 22.22.10.png
スクリーンショット 2021-03-04 22.22.20.png

いろんなテンプレートがありますが、今回は「空のプロジェクト」を選択
スクリーンショット 2021-03-04 22.22.34.png
スクリーンショット 2021-03-04 22.22.51.png

コードをちゃちゃっと書いて

<html><head><metacharset="utf-8"/><metaname="viewport"content="width=device-width, initial-scale=1"/><linkrel="stylesheet"href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"/><script src="https://code.jquery.com/jquery-3.2.1.min.js"></script><script src="https://unpkg.com/obniz@3.x/obniz.js"crossorigin="anonymous"></script></head><body><divid="obniz-debug"></div><divclass="container"><divclass="text-center"><pid="start_date-text">開始時間 : </p><pid="start_date"></p><pid="end_date-text">終了時間 : </p><pid="end_date"></p></div></div><script>//type in your obniz IDvarobniz=newObniz("OBNIZ_ID_HERE");obniz.onconnect=asyncfunction(){// obnizクラウド上での動作かどうかの判定if(Obniz.App.isCloudRunning()){obniz.display.print('===== start');// ソレノイドを0,1に繋ぐvarsolenoid=obniz.wired('Solenoid',{gnd:0,signal:1});solenoid.click();// マグネットスイッチを9,10,11に繋ぐvarct10=obniz.wired("CT10",{gnd:9,vcc:10,signal:11});myFunc=asyncfunction(){conststart_date=newDate();$("#start_date").text(start_date);obniz.display.print("start_date");obniz.display.print(start_date);// マグネットスイッチがONになる(笠が被される)のを待つ(ハッカソンみあふれるコード)awaitct10.stateWait(true);constend_date=newDate();$("#end_date").text(end_date);obniz.display.print("end_date");obniz.display.print(end_date);constscore=end_date-start_dateobniz.display.draw(score);}// APIをキックしてすぐだと、誤作動するので若干ディレイをかけて実行setTimeout(myFunc,1000);// 実際に動かしたコードではないので雰囲気が伝われば$.ajax({type:'POST',// このURLはすでに無効ですurl:'https://b3fcdcbe.us-south.apigw.appdomain.cloud/ojizo/record',data:{score:score},dataType:'json'});}}</script></body></html>

アプリの設定からブラウザ実行にチェックを入れて設定を更新します
スクリーンショット 2021-03-04 22.25.47.png

デバイス一覧から「Webhook URLの確認」が選択できるようになります
スクリーンショット 2021-03-04 22.26.52.png

Webhook URLが発行されます
スクリーンショット 2021-03-04 22.27.12.png

注意点

obnizクラウドではawaitをかけて待機していても、接続が最大30秒までなので今回の要件的には適さないようでした。

待機ができる

以上のことをobnizの木戸さんに相談したところ、 repl.itというサービスで実現できるかもとの情報をいただき、試してみました。
repl.itではNode.jsのホスティングが無料ででき、APIとしても公開できます。

コードをnodejs用にすこし修正し(こちら大変たすけていただきました🙇‍♂️ありがとうございました🙇‍♂️)

constexpress=require('express');constObniz=require('obniz');constfetch=require("node-fetch")constapp=express();constport=3000;letsolenoid=null;letct10=null;letobniz=newObniz("xxxxxxxx");obniz.onconnect=function(){console.log("connected")solenoid=obniz.wired('Solenoid',{gnd:0,signal:1});ct10=obniz.wired("CT10",{gnd:9,vcc:10,signal:11});}// トップページに来たときにapp.get('/',async(req,res)=>{//とりあえずレスポンスは先に返す(ブラウザ対策)res.json({"status":"OK"});if(obniz.connectionState==="connected"){obniz.display.print('===== start');solenoid.click();conststart_date=newDate();// $("#start_date").text(start_date);obniz.display.print("start_date");obniz.display.print(start_date);awaitct10.stateWait(true);constend_date=newDate();obniz.display.print("end_date");obniz.display.print(end_date);score=end_date-start_date// $.ajaxはnodejsで使えないのでfetchに変換constdata={user_id:obniz.id,start_date:start_date,end_date:end_date,};awaitfetch('https://b3fcdcbe.us-south.apigw.appdomain.cloud/ojizo/record',{method:"POST",mode:'cors',headers:{'Content-Type':'application/json'},body:JSON.stringify(data)})}})app.listen(port,()=>{console.log(`Example app listening at http://localhost:${port}`)});console.log("wakeup")

これで10分は待機できるようになりました!

まとめ

・ひさびさのハッカソンはすごく楽しい
・ピップエレキバンの磁力はそんなに強くない
・ハードウェアは極力シンプルな動きにしないとツラくなる(今回で言うと、やっぱどうにかobniz二台構成にしたほうがやりやすかったろうなぁ)
・IBMクラウドのファンクションはわかりやすくて便利だぞ

Google Apps Script練習 (Gmailの新着メールをLINEに転送)

$
0
0

GASを練習したいと思い、また自分自身Gmailのメールを見逃してしまう事が多いのでそれをなんとか出来ないかと思いスクリプトを作成しました。

gas.js
constLINE_NOTIFY_TOKEN=PropertiesService.getScriptProperties().getProperty('LINE_NOTIFY_TOKEN')constendPoint='https://notify-api.line.me/api/notify'// 1. 転送したいメールの送信元アドレスを指定constfromAddress=[''].join(' OR ')// 2. トリガーの設定間隔と合わせるconstminutesInterval=5functionmain(){constnotices=fetchNotices()if(notices.length===0){return}for(constnoticeofnotices){send(notice)}}functionfetchNotices(){constnow=Math.floor(newDate().getTime()/1000)constintervalMinutesAgo=now-(60*minutesInterval)// 3. 検索条件を設定constquery=`(is:unread from:(${fromAddress}) after:${intervalMinutesAgo})`// 4. メールを取得するconstthreads=GmailApp.search(query)if(threads.length===0){return[]}constmails=GmailApp.getMessagesForThreads(threads)constnotices=[]for(constmessagesofmails){constlatestMessage=messages.pop()constnotice=`
--------------------------------------
件名: ${latestMessage.getSubject()}受信日: ${latestMessage.getDate().toLocaleString()}
From: ${latestMessage.getFrom()}
--------------------------------------

${latestMessage.getPlainBody().slice(0,350)}
`notices.push(notice)// 5. メールを既読にするlatestMessage.markRead()}returnnotices}functionsend(notice){if(LINE_NOTIFY_TOKEN===null){Logger.log('LINE_NOTIFY_TOKEN is not set.')return}constoptions={'method':'POST','headers':{'Authorization':`Bearer ${LINE_NOTIFY_TOKEN}`},'payload':{'message':notice},}UrlFetchApp.fetch(endPoint,options)}

GmailにおけるThreadとMessageの違いについて理解に時間がかかりました。
 Thread: あるメールとそのメールに対する一連の返信(配列みたいになる)
 Message: 単体のメール1つ

GASもLINE APIも、本当便利…
GASって、他にもいろんな事できるんですね…Googleスプレッドシートを活用して議事録をいじったり…
まだまだ知らない事ばかりなので、一度ガッツリ時間を取って勉強したい。

ContentType書き換えでImageMagickのリサイズを成功させる

$
0
0

はじめに

先日、「オブジェクト名の変更」でアップロード済み画像のサムネイル生成を簡単にという記事を書きました。
その作業の中で、ContentTypeの問題によって一部の画像でImageMagickのリサイズ(サムネイル生成)に失敗していることが分かりました。
今回はなぜそうなったのか、どう対処したのかについてご紹介します。

リサイズに失敗する原因

ContentTypeを指定しないと、application/octet-streamになる

FirebaseのCloud Storageにファイルをアップロードする時にContentTypeが指定されていないと、判別できない場合はアップロード方法に応じてapplication/octet-streamまたはapplication/x-www-form-urlencodedに設定されます。
実際、一部の画像がapplication/octet-streamになってしまっていました。

application/octet-streamではImageMagickで画像のリサイズができない

画像のリサイズはImageMagickを利用しています。
ImageMagickでContentTypeがapplication/octet-streamだと、画像のリサイズができません。

サムネイル生成できるようにする対処法

ContentTypeを書き換える

ContentTypeがapplication/octet-streamの画像が存在することでImageMagickのリサイズが失敗していることが分かりました。
そこでContentTypeを以下のように書き換えましょう。

// ファイルからメタデータを取得するconstfile=awaitbucket.file(imagePath)awaitfile.getMetadata()if(file.metadata.contentType=='application/octet-stream'){// octet-streamだったら、image/jpegなどcontentTypeを書き換えるconstcontentType='image/jpeg'awaitfile.setMetadata({contentType:contentType,})}

サンプルでは簡単なソースコードをご紹介するために、application/octet-streamだったらimage/jpegに決め打ちで置き換えるようにしています。
実際のソースコードはjpg, jpeg, png, gifなど、必要なContentTypeを拡張子で判定してセットしています。

このようなcontentTypeの書き換えを行ったところ、無事にImageMagickで画像のリサイズができるようになりました。

最後に

このようなContentTypeの書き換えが発生しないように、本来はアップロード時にきちんと指定するのが良いと思います。
とはいえアップロード済みの画像のContentTypeを書き換える場面もあるかもしれません。
そんな時はこちらの方法を試してみてください。

「Alexa、出勤!」で快適なリモートワークを

$
0
0

完成したAlexa

 こんな感じで会話をしながら、最終的にslackに投稿してくれるAlexa Skillを実装しました。

リモートワークの悩み

 きっとQiitaを見てる大半の人はエンジニア、そうでなくとも何かしらIT関係のお仕事をしている方が多数かと思います。コロナの影響で一気にリモート化が進んだIT業界、自らも気づけばフルリモート生活が長らく続いています。
 『出勤する時間が無くなった』、『無駄な会議が無くなって業務時間の短縮に繋がった』など、個人的にリモートワークに対してはポジティブな印象を抱いているのですが、中には「家事と仕事が同時に襲ってくる」「子供の世話で手が離せなくてストレスを感じてしまう」など、必ずしも良いことばかりでは無いという方も多いように見受けられます。そんな中、「家事とかしながらslackに簡単なメッセージ投稿できたら楽じゃね?」と思い、今回Alexaからslackに投稿できるスキルを実装しました。決して「これでベッドにいながら話しかけるだけで出勤!!」とかのために実装したのでは無いので悪しからず!

仕様

 下記のシーケンス図は正常系の処理になります。

system.png

(補足)
1. Skillの呼び出し名は「スラック」とする。「Alexa、スラック」や「Alexa、スラック起動して」などどと話しかければ、スキルが起動する。
2. 起動後に「Slackを起動しました。どの様に投稿しますか?」と聞き返す。ユーザーが投稿内容をつぶやいた後、「Slackに◯◯」と投稿してもよろしいですか?」と聞き返す。
4. 2の後、「はい、イエス、オッケー」などをユーザーが発話するとslackへの投稿を行う。
5. 2の後、「だめ、いや、ノー」などをユーザーが発話するともう一度「どの様に投稿しますか?」と聞く。
6. 2の後、「はい」や「いいえ」に関連するワード以外(例:キツネ)を発話すると「キツネと投稿しますよろしいですか?」と聞く。
7. 無事、Slackへの投稿が終了した場合、「Slackに◯◯と投稿しました。」と発話する。
8. 投稿するワークスペースは1つだけ。またchannnelも1つだけとする。
9. 何らかの原因で投稿が完了しなかった場合、その旨をユーザーに伝える。

Slack API

 SlackAPIを使い、投稿をポストできるように準備します。この方の記事の通り進めていけば問題なくできました。このアプリをslackのチャンネルに追加することで、投稿することが確認できました。

curl -X POST 'https://slack.com/api/chat.postMessage' \
-d 'token=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' \
-d 'channel=#general' \
-d 'text=Alexa Talk Appです。投稿してみたよ。'

curl -X POST 'https://slack.com/api/chat.postMessage' \
-d 'token=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' \
-d 'channel=#general' \
-d 'text=これはさっきと違ってslackAPIを利用することで僕の代わりにアプリが僕のアカウントで話してる'

上記のシェルスクリプトを実行して以下のように投稿できれば成功です。
スクリーンショット 2021-02-27 0.58.59.png

 slackに投稿する方法をググった際、これより先にwebhookを使う方法が出てきました。そちらの方法だと追加したアプリ自身が投稿することは簡単に出来たのですが、私自身のアカウントを使用して投稿したい場合、この方が簡単そうなので、今回はこちらの方法で実装していくことにしました。

Alexa Skill

 Alexa SkillからLambdaにきちんと投稿内容を飛ばせるようにするために簡単な設定を行います。Alexaにはインテントシノニムスロットを使って、幾つもの会話パターンを事前登録する機能があります。例えば以下のようにスロットを設定するとAlexaは{}部分に入るワードをリクエストjsonの中に組み込んで出力します。

{post}って投稿して
{post}ってお願い
{post}でよろしく

ただ毎回、毎回「って投稿して」って言うのも煩わしいので、今回は下記の通り3つのインテントスロットとスロットタイプを設定しました。

screencapture-developer-amazon-alexa-console-ask-build-custom-amzn1-ask-skill-0aee8890-1b58-4b2f-9872-2134294651ec-development-ja-JP-intents-PostToSlackIntent-2021-03-05-01_18_16.png

post_slot.png

reply_no_slot.png

reply_yes_slot.png

これで、この中のどれかの発言をした場合、Alexa側で指定されたスロットとしてlambdaにリクエストを飛ばしてくれます。ただ実際問題、どのスロットに該当しないワードを発話したい場合の方が殆どだと思います。 ((例)今日は休みます。)
 その場合、Alexa側で自動的にどれかのスロットに分類してリクエストを飛ばしてくるのですが、labda側で強制的にpost_slotに値をswapすることで、より自然なコミュニケーションになる様に工夫しています。

Lambda

 まず関数を適切なロールをアタッチして上で作成します。次にLambda側のトリガーに先ほど作成したAlexaSkill側のSkillIDを登録し、次にAlexa側にエンドポイントとしてLambdaのARNを登録します。設定が終わりましたら、以下のレポジトリ [GitHub]をローカルにクローンして、必要なモジュールをインストールしzipで圧縮後、先ほど作成したlambdaにソースコードをアップロードします。

# クローンしたディレクトリに移動後、初期化&必要なモジュールをインストール
$ npm install
$ npm install --save ask-sdk-core
$ npm install --save ask-sdk-model
$ npm install --save request

# 圧縮後lambdaにアップロード
$ zip -r {ファイル名} index.js node_modules

 尚、コード内にトークンを直書きするのも色々危ないので、lambda側でSLACK_ENDPOINTTOKENCHANNELの三つを環境変数として予め登録しています。また、AlexaSkillの方もインテントやエンドポイントの設定が終わった後にきちんとデプロイしないと、正しく動作しません。

あとがき

 「こんなの作ってもどうせ使わない」とか思う人も多いと思います。実際僕もそうでした(笑)ただ意外に使い出してみると結構手放せなくなるのがスマートスピーカーの良いところ。これは使ってみないと分かりません。ぜひ皆さんもAlexaいじってみてください(スマホからもアプリとしてインストールできます。)

【Node.js】json-server導入【Mock API】

$
0
0

概要

APIのモック用にjson-serverを導入したので主な処理をざっくりとまとめた

json-serverについて

簡単にモックAPIを作成できるnodeのライブラリ
主にテスト作成時に使われるもの

導入

$ yarn add json-server

または

$ npm install –save-dev json-server

実際に使ってみる

次の内容のmock.jsonを作成する

{"test":{"id":"mock_id","text":"mock json desu"}}

ターミナルで次のコマンドを実行

$ json-server mock.json

リクエストメソッドGET、エンドポイントhttp://localhost:3000/testを叩くと次の内容のレスポンスを取得できる

{"id":"mock_id","text":"mock json desu"}

解説

mock.jsonの第一階層のキー名がそのままエンドポイントになる
例えば次のようにmock.jsonを作成すればエンドポイントが/test/sampleのAPIのモックを作れる

{"test":{"id":"mock_id_1","text":"mock json test"},"sample":{"id":"mock_id_2","text":"mock json sample"}}

リクエストメソッドは全てGETになる(仕様)
portの指定がなければ3000が割り振られる

オプションについて

-m {{ jsファイルのパス }}

APIモックが叩かれたらデータを取得する前に指定したjsファイルを実行してくれる
GET以外のリクエストメソッドのAPIのモックを作成したい時やトークンでの認証をする時などに使う

mock.json

{"test":{"id":"mock_id_1","text":"mock json test"},"test_patch":{}}

次の内容のmiddleware.jsを作成

module.exports=asyncfunction(req,_res,next){// リクエストメソッドが PATCH の場合、リクエストURLに _patch をつけ、リクエストメソッドを GET に直すif(req.method==='PATCH'){req.method='GET'req.url+='_patch'}next()}

ターミナルで次のコマンドを実行

$ json-server mock.json -m middleware.js

リクエストメソッドPATCHhttp://localhost:3000/testを叩くと次のレスポンスを取得する

{}

--routes {{ jsonファイルのパス }}

エンドポイントが/test/sampleのような形になる場合やエンドポイントにクエリパラメータが含まれる場合に使う

mock.json

{"mock_1":{"id":"mock_id_1","text":"mock json test"}}

次の内容のroutes.jsonを作成

{"/test/sample":"/mock_1"}

ターミナルで次のコマンドを実行

$ json-server mock.json --routes routes.json

http://localhost:3000/test/sampleを叩くと次のレスポンスを取得する

{
   "id": "mock_id_1",
   "text": "mock json test"
}

--host {{ host名 }}, -p {{ port番号 }}

ホスト名とポート番号を指定できる

おまけ

モックの数が増えるとmock.jsonの中身が煩雑になってくると思います
それの対策として他のファイルでjsonを作成して立ち上げ時にmock.jsonにまとめるという形にしました

次の内容のjsファイルを用意

constfs=require('fs')constpath=require('path')constroot=path.resolve('./','mocks')constapi=fs.readdirSync(root).reduce((api,file)=>{// ファイル名を取得constendpoint=path.basename(file,path.extname(file))// ファイル名をキー名にするapi[endpoint]=JSON.parse(fs.readFileSync(root+'/'+file,'utf-8'))returnapi},{})// 作成したjsonをmock.jsonに書き込むfs.writeFile('./mock.json',JSON.stringify(api),err=>{if(err)throwerr})

mocksフォルダを作成し、その中にファイル名がパスになるjsonファイルを作成する
エンドポイントごとにファイルを分けられるのでmock.jsonだけの状態よりはかなり見やすくなります
最後にサンプル用に作成したリポジトリを置いておきます


React初心者へ ReactとNode.js?フロントなのにサーバーサイド?

$
0
0

React初心者へ

Reactの復習をし始めたので、私も初心者ですが、React初心者向けに記事を書いてみました。
Reactの学習を始めてしばらくするとNode.jsに出会うと思いますが、フロントエンド開発なのになぜサーバーサイドの話が出てくるのかと疑問を頂いていませんか?
私もはじめはそう思っていました。Node.jsについてググって色々調べると「サーバーサイドのJavaSctipt」という間違った認識を得てしまいました。

Node.jsはJavaScriptの実行環境のこと

「サーバーサイドのJavaSctipt」という認識のとおり、サーバーサイドで動く、少し書き方の違うJavaScriptなのかと思っていませんか?私はそうでした。
JavaScriptは通常ブラウザでしか動かないのですが、PCでも動くようにするためのソフトウェアがNode.jsなのです。

詳しくはこちらの記事がおすすめです。

ReactはNode.jsなしでも動きます

htmlのscriptタグで
https://unpkg.com/react@17.0.1/umd/react.development.js
https://unpkg.com/react-dom@17.0.1/umd/react-dom.development.js
を読み込み、ReactDOM,renderメソッドを使用すれば動きます。

なぜReact開発にNode.jsなのか

Reactでの開発時にサーバーサイドのJavaScript環境として知られるNode.jsを使う理由は、

①アプリケーション開発時のパッケージ管理の手間を省くため
②webpackやbabelなどの便利ツールを使用するため。

などがあります。

①アプリケーション開発時のパッケージ管理の手間を省くため

Reactでアプリケーション開発を行っていると、様々なパッケージを使う必要があるが、それらのパッケージ同士は相互に依存関係にあります。
例えば、「パッケージAはパッケージBのver X.X.X、パーッケージCのver X.X.Xがないと正常に動作しない。」というような状況が各パッケージ毎にあります。アプリケーションの開発者はこれらのパッケージのバージョンを適切に管理する必要があるのですが、ある程度のアプリになるとそのパッケージを手動で管理するのは非常に困難です。

Node.js環境で開発すると、パッケージを管理するためのnpmやyarnというツールを使用することでき、CLIからコマンドによりパッケージのインストールや依存関係の調整などが簡単に行うことができるのです。

②webpackやbabelなどの便利ツールを使用するため。

JavaScriptファイル、CSSファイルなどをバンドルするwebpack、最新のJavaScriptで書かれたコードを古いブラウザでも認識できる形にコンパイルするbabelなど、フロントエンド開発に欠かせないツールもNode.js環境があるために使用できています。CreateReactAppを利用しReact開発を始めた初心者は、あまり意識しない部分かもしれませんが、これもNode.jsの恩恵なのです。

終わりに

React界隈では名著の呼び声高い「りあクト!」を参考に勉強しました。作中で業務未経験には少しハードルが高いというニュアンスで書かれており、たしかにそうでしたが、おすすめです!
https://oukayuka.booth.pm/

DockerコンテナでNode.jsを実行してみた

$
0
0

前の記事(コンテナ未経験なのでDockerを基礎から学んでみた)に引き続き、今回はコンテナでNode.jsのアプリケーションを動かし、ブラウザから開いてみることに挑戦してみました。

スクリーンショット 2021-03-06 13.44.31.png

実装は以下の手順で行いました。

  1. Node.jsアプリの作成
  2. Dockerfileの作成
  3. Dockerfileのビルド
  4. コンテナの起動
  5. ブラウザとアプリの接続

Node.jsアプリの作成

localhost:8080を開いたときにHi thereがブラウザに表示されることを目指します。

package.jsonindex.jsの中身は以下のようにします。

package.json
{"dependencies":{"express":"*"},"scripts":{"start":"node index.js"}}
index.js
constexpress=require('express');constapp=express();app.get('/',(req,res)=>{res.send('Hi there');});app.listen(8080,()=>{console.log('Listening on port 8080');});

Dockerfileの作成

Dockerイメージをビルドしたとき、ファイルシステムにアプリに必要なプログラム(dependencies)をインストールし、メタ情報にアプリの起動コマンド(npm start)を導入することを目指します。

前の記事でredisをコンテナで立ち上げたときのように、試しにalpineのベースイメージを使ってDockerfileを作成してみます。

FROM alpineRUN npm installCMD ["npm", "start"]

これをビルドすると、RUN npm installの部分でこけます。
なぜかというと、alpineイメージは軽量であるがゆえに、nodenpmもプリインストールされていないからです。

そのため、ベースイメージにはnodenpmがプリインストールされているnodeイメージなどを選択する必要があります。ただ、nodeイメージにはnpmやnode以外にもテキストエディタツールなどの余計なプログラムもプリインストールされているので、軽量なalpineにnpmnodeがプリインストールされたnode:alpinenode:slimなどの最適化されたベースイメージを使うのがベストプラクティスです。

ベースイメージをnode:alpineに変えて再度ビルドしてみます。

FROM node:alpineRUN npm installCMD ["npm", "start"]

このビルドもこけます。
なぜなら、node:alpineのファイルシステムにpackage.jsonが含まれておらず、インストールするプログラムが不明だからです。もしたまたまハードドライブにpackage.jsonが含まれていたとしても、コンテナに割り当てられたハードドライブのセグメントにファイルがあるわけではないので、npm installは実行できません。

ここでDockerfile内に、RUNの前にファイルコピーするための記述COPYを加えます。
COPY ./ ./とすることで、ローカルのカレントディレクトリのファイル(package.json, index.js)をコンテナ内のファイルシステムにコピーすることができます。

FROM node:alpineWORKDIR /usr/appCOPY ./ ./RUN npm installCMD ["npm", "start"]

*WORKDIR /usr/appという記述がありますが、これはファイルのコピー先(ワーキングディレクトリ)を指定しています。詳細は後述します。

これでようやくビルドが成功します。

(base) [9:42:15] → docker build -t suzuki0430/simpleweb:latest .                                                                               ~/Programs/docker/simpleweb
Sending build context to Docker daemon  4.096kB
Step 1/5 : FROM node:alpine
 ---> b3dce3e0529f
Step 2/5 : WORKDIR /usr/app
 ---> Using cache
 ---> d995d6f0fd23
Step 3/5 : COPY ./ ./
 ---> 8b999fe7dc6e
Step 4/5 : RUN npm install
 ---> Running in 52b539c857e3

added 50 packages, and audited 50 packages in 3s

found 0 vulnerabilities
npm notice
npm notice New minor version of npm available! 7.0.8 -> 7.6.1
npm notice Changelog: <https://github.com/npm/cli/releases/tag/v7.6.1>
npm notice Run `npm install -g npm@7.6.1` to update!
npm notice
Removing intermediate container 52b539c857e3
 ---> 5f7e4d45df44
Step 5/5 : CMD ["npm", "start"]
 ---> Running in e8085a051d01
Removing intermediate container e8085a051d01
 ---> e76e6a91dc2b
Successfully built e76e6a91dc2b
Successfully tagged suzuki0430/simpleweb:latest

コンテナの作成・起動を行います。

(base) [9:43:11] → docker run suzuki0430/simpleweb                                                                                             ~/Programs/docker/simpleweb

> start
> node index.js

Listening on port 8080

コンテナの起動も正常にできたみたいなのでこれで成功!といきたいところなのですが、ブラウザからアプリが開けません...えっ?
スクリーンショット 2021-03-06 10.26.21.png

ポートマッピング

アプリが開けなかったのは、コンテナのポート8080とローカルホストのポート8080が接続されていなかったためです。
ブラウザからlocalhost:8080にアクセスする際、localhost(ローカルPC)のポートに接続しようとします。ただ、今回はコンテナの中でNode.jsのアプリケーションを実行しているため、コンテナのポートと接続する必要があります。
スクリーンショット 2021-03-06 14.30.41.png

ローカルPCとコンテナのポートを紐付けることをポートマッピングと呼びます。
ポートマッピングを実行するためにはdocker run -p 8080:8080 [image id]コマンドを実行します。コンテナの作成・起動とポートマッピングを同時に実行してくれます。

これで開けるようになりました!
スクリーンショット 2021-03-06 10.43.55.png

キャッシュの利用

アプリは開けるようになったのですが、気になる部分が1つあります。それは今のDockerfileの内容だと、ファイルを更新してイメージをビルドする際にnpm installが必ず実行されてしまうことです。最終的にビルドは完了するので問題がないといったらないのですが、ビルドの待ち時間はできるだけ避けたいものです...

そこでキャッシュの利用を考えます。

DockerfileをRUN npm installの前にpackage.jsonのみをコピーするように書き換えます。

FROM node:alpineWORKDIR /usr/appCOPY ./package.json ./RUN npm installCOPY ./ ./CMD ["npm", "start"]

すると、package.jsonの中身が同じであれば、RUN npm installまでの手順をキャッシュによってスキップすることができるようになります。

Step 3/6 : COPY ./package.json ./
 ---> Using cache
 ---> 1e7f34ded131
Step 4/6 : RUN npm install
 ---> Using cache
 ---> 8f0ae8a2f7f6

ワーキングディレクトリの指定

Dockerfile内にWORKDIR /usr/appという記述がありますが、ここではRUN, CMD, ENTRYPOINT, COPY, ADD, docker run, execで実行するコンテナプロセスのワーキングディレクトリを指定しています。

WORKDIR /と指定することももちろんできるのですが、ルートディレクトリにはいろんなファイルやフォルダが含まれているので、ファイル名が同じだったりすると干渉が起こってしまいます。そのため、コンテナプロセス用のディレクトリを指定してあげる必要があります。

コンテナ起動時にシェルを開くと、確かにワーキングディレクトリが/usr/appとなっています。

(base) [11:06:07] → docker run -it suzuki0430/simpleweb sh                                                                                     ~/Programs/docker/simpleweb
/usr/app #

おわりに

Node.jsをコンテナで動かすことができるようになりました。次はDocker-composeについて学習します。

参考資料

Node.jsを使ってHTML, CSS, JavaScriptを軽量化・難読化する手順

$
0
0

はじめに

Windows環境で、Node.jsのモジュールを使って以下を行う手順をまとめます。

  • HTML, CSSの軽量化(minify)
  • JavaScriptの難読化(uglify)

使用モジュール

  • html-minifier→ HTMLファイルの軽量化モジュール
  • clean-css-cli→ CSSファイルの軽量化モジュール
  • uglify-es→ JavaScriptファイルの難読化モジュール

準備:モジュールのインストール

  1. Node.jsを取得しインストールする。→ https://nodejs.org/ja/

  2. Node.js command promptを起動する。

  3. 以下のコマンドを実行する。(2行目のプロキシ設定は必要な場合のみ実行)

$ npm -g config set registry http://registry.npmjs.org/
$ npm -g config set proxy http://xxx.xxx.xxx:8080
$ npm install -g html-minifier
$ npm install -g clean-css-cli
$ npm install -g uglify-es

使用方法

Node.js command promptから、以下のコマンドを実行する。

HTMLの軽量化

$ html-minifier --collapse-whitespace --remove-comments --remove-optional-tags --remove-redundant-attributes --remove-script-type-attributes --remove-tag-whitespace --use-short-doctype original.html -o original.min.html

CSSの軽量化

$ cleancss -o original.min.css original.css

JavaScriptの難読化

$ uglifyjs original.js -c --compress --mangle --output original.min.js

関連記事

node.jsとjQueryを使ってユーザー新規登録モーダルとログインモーダルを作ってみた

$
0
0

画面遷移をせずにユーザー新規登録機能を作ってみました。
そこまで大した出来ではないです。
(実は理想としていたものより少々乖離しています。)

node.jsやjQueryの導入はこちらでは省いています。
また、新規登録、ログインモーダル以外の要素もありません。ご了承ください。

node.jsのバージョンは v14.15.4です。
また、MySQLを使用しています。

1.必要なものをインストール

ターミナル
$ npm install express  //expressをインストールします
$ npm install mysql  //mysqlと接続できます
$ npm install express-session   //登録機能です
$ npm install bcrypt  //パスワードをハッシュ化してくれる機能です

これでインストールは以上です。

今回、ユーザー新規登録には、
username , email , password
の情報が必須となる設定にしています。

2.ルーティングのコードを書いていく

ディレクトリ構造は以下のようになっております
(node_modulesなどは省略しています。)

registation_app/
├ public/
│  ├ css/
│  │  └style.css/
│  ├ script.js/
│  └ validate.js/
├ views/
│  ├ log_in.ejs/
│  ├ sign_up.ejs/
│  ├ top.ejs/
│  └ uniq_error.ejs/
└ app.js/

app.js
// インストールしたものを適用させますconstexpress=require('express');constmysql=require('mysql');constsession=require('express-session');constapp=express();constbcrypt=require('bcrypt');app.use(express.static('public'));app.use(express.urlencoded({extended:false}));// DBと接続しますconstconnection=mysql.createConnection({host:'localhost',user:'root',password:'パスワードを入力',database:'DB名を入力'});// トップ画面のルーティングapp.get('/',(req,res)=>{res.render('top.ejs');});// 重複するメールアドレスがある場合の画面遷移app.get('/uniq_error',(req,res)=>{res.render('uniq_error.ejs');});// 新規登録のルーティングapp.post('/sign_up',(req,res,next)=>{constemail=req.body.email;connection.query('SELECT * FROM users WHERE email = ?',[email],(error,results)=>{if(results.length>0){res.render('uniq_error.ejs');}else{next();}});},(req,res)=>{bcrypt.hash(password,10,(error,hash)=>{connection.query('INSERT INTO users (username, email, password) VALUES (?, ?, ?)',[username,email,hash],(error,results)=>{req.session.userId=results.insertId;req.session.username=username;res.redirect('/');});});});// ログインのルーティングapp.post('/log_in',(req,res)=>{constemail=req.body.email;connection.query('SELECT * FROM users WHERE email = ?',[email],(error,results)=>{if(results.length>0){constplain=req.body.password;consthash=results[0].password;bcrypt.compare(plain,hash,(error,isEqual)=>{if(isEqual){req.session.userId=results[0].id;req.session.username=results[0].username;res.redirect('/');}else{res.redirect('/');}});}else{res.redirect('/');}});});// ログアウトのルーティングapp.get('/log_out',(req,res)=>{req.session.destroy(error=>{res.redirect('/');});});// ローカルホスト3000に接続app.listen(3000);});

3.トップ画面などのコーディング

top.ejs
<!DOCTYPE html><html><head><metacharset="utf-8"><title>registration_APP</title><linkrel="stylesheet"href="/css/style.css"><linkrel="stylesheet"href="https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css"><script src="https://code.jquery.com/jquery-3.5.1.min.js"></script><script type="text/javascript"src="script.js"></script><script type="text/javascript"src="validate.js"></script></head><body><header><divclass="header-wrapper"><ahref="/"class="header-logo-left">registration_APP</a><%if(locals.isLoggedIn){%><aclass="header-logo-right"href="/log_out">ログアウト</a><%}else{%><div><aclass="header-logo-right login-show">ログイン</a><aclass="header-logo-right signup-show">新規登録</a><%}%></div></div></header><%-include('sign_up');%><%-include('log_in');%></body></html>

<%- include('sign_up'); %>で新規登録モーダル
<%- include('log_in'); %>でログインモーダルを呼び出します。

ある特定のページでのみモーダルを出現させるのであればこの記述をする必要はありませんが、
複数のページでモーダルを出したい場合、このようにすると使いまわせるので便利です。

sign_up.ejs
//新規登録モーダル

<divclass="signup-modal-wrapper"id="signup-modal"><divclass="modal"><divid="signup-close-modal"><iclass="fa fa-2x fa-times"></i></div><divid="signup-form"><h2>新規登録</h2><formaction="/sign_up"method="post"id="sign_up-form"><pclass="error-message"id="sign_up-username-error-message"></p><inputclass="form-control"type="text"placeholder="ユーザー名"name="username"id="sign_up-username"><pclass="error-message"id="sign_up-email-error-message"></p><inputclass="form-control"type="text"placeholder="メールアドレス"name="email"id="sign_up-email"><pclass="error-message"id="sign_up-password-error-message"></p><inputclass="form-control"type="password"placeholder="パスワード"name="password"id="sign_up-password"><inputid="submit-btn"type="submit"value="登録する"></form></div></div></div>
log_in.ejs
//ログインモーダル

<divclass="login-modal-wrapper"id="login-modal"><divclass="modal"><divid="login-close-modal"><iclass="fa fa-2x fa-times"></i></div><divid="login-form"><h2>ログイン</h2><formaction="/log_in"method="post"><inputclass="form-control"type="text"placeholder="メールアドレス"name="email"><inputclass="form-control"type="password"placeholder="パスワード"name="password"><inputid="login-btn"type="submit"value="ログインする"></form></div></div></div>

これでトップ画面で二種類のモーダルをだせるようになりました。
(この段階では二種類のモーダルは現れた状態になっています。)
この後CSSを使ってモーダルを隠し、jQueryで表示させるアクションを作成します。

4.CSSをコーディング

CSSは全て書くととても長くなるので、必要最低限のところだけ
モーダルの大きさや文字の大きさ、背景色などはお好みで。

style.css
/* ヘッダーの『新規登録』『ログイン』部分 */.header-logo-right{display:inline;}.header-logo-right:hover{cursor:pointer;}/* モーダル部分 */.signup-modal-wrapper,.login-modal-wrapper{position:fixed;top:0;right:0;bottom:0;left:0;z-index:100;display:none;}.modal{position:absolute;top:20%;left:34%;background-color:#e6ecf0;padding:20px040px;border-radius:10px;width:450px;height:auto;text-align:center;}.fa-times{position:absolute;top:12px;right:12px;color:rgba(128,128,128,0.46);cursor:pointer;}#signup-form,#login-form{width:100%;}

これで新規登録、ログインボタンにカーソルを合わせるとポインタが変わります
さらにモーダル部分は隠れました。
それではjQueryを使ってモーダルが現れるように処理しましょう。

5.Jqueryをコーディング

script.js
$(function(){$('.signup-show').click(function(){$('#signup-modal').fadeIn();});$('#signup-close-modal').click(function(){$('#signup-modal').fadeOut();});});$(function(){$('.login-show').click(function(){$('#login-modal').fadeIn();});$('#login-close-modal').click(function(){$('#login-modal').fadeOut();});});

これで、『新規登録』を押すと『新規登録モーダル』が
『ログイン』を押すと『ログインモーダル』が出るようになりました。

これで、新規登録並びにログインモーダルが完成です。

・・・と言いたい所さんですが、このままだと空白でも登録ができてしまいます。
と言うわけでバリデーションをかけていきましょう

6.バリデーションをかける

今回はjQuery側でバリデーションチェックをします。
挙動としては、入力に問題がある場合、送信はできず問題箇所にエラー文が出るようにします。

validate.js
$(function(){$('#sign_up-form').submit(function(){varusernameValue=$('#sign_up-username').val();varemailValue=$('#sign_up-email').val();varpasswordValue=$('#sign_up-password').val();varerrorCount=0;if(usernameValue===""){$('#sign_up-username-error-message').text('ニックネームを入力してください');errorCount+=1;}else{$('#sign_up-username-error-message').text('');}if(emailValue===""){$('#sign_up-email-error-message').text('メールアドレスを入力してください');errorCount+=1;}elseif(!emailValue.match(/^[a-zA-Z0-9_.+-]+@([a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]*\.)+[a-zA-Z]{2,}$/)){$('#sign_up-email-error-message').text('メールアドレスが正しくありません');errorCount+=1;}else{$('#sign_up-email-error-message').text('');}if(passwordValue===""){$('#sign_up-password-error-message').text('パスワードを入力してください');errorCount+=1;}elseif(!passwordValue.match(/^(?=.*[a-zA-Z])(?=.*[0-9])[0-9a-zA-Z]{6,}$/)){$('#sign_up-password-error-message').text('パスワードは半角英数字6文字以上が必要です');errorCount+=1;}else{$('#sign_up-password-error-message').text('');}if(errorCount!==0){returnfalse;}});});

これでバリデーションチェックができます。
解説としてはerrorCountを0と最初に定義し、問題箇所1つにつきerrorCountが1上昇していきます。
そして最後、errorCountが0ではない場合送信できないように設定してあります。
そして全てのチェックが通ればerrorCountは0なので送信し、ユーザー登録できる。
と言う仕掛けになっております。

これにて完成です。
と思ったら間違えです。
このままだと同じメールアドレスが登録できてしまいます。

ここから先が私の作りたかったモーダルとの理想と実力の無さが出た場所でした。

7.実現したかった内容

ここで同じアドレスが登録されている場合にもerrorCountを1上昇させる処理をしたかったのですが、
ajax通信が何をやってもうまくいきませんでした。
jQuery単体では厳しいようで、PHPが必要なようです。
(jQueryだけでどうにかならないか色々勉強中です。)

仕方ないので、node.js側で同じメールアドレスがある場合は弾き、アラートを出す妥協案にしましたが、これまた失敗。
理由はnode.jsではalertメソッドが使えないようです。

8.苦し紛れに出した自分なりの答え

アラートが出せずにしばらく考え、出した結論が
『既に登録されているメールアドレスがあるため登録できませんでした』

と書かれたページへ遷移することでした。
コードで言うと、最初のapp.jsに既に記述してありますが、再び記述すると

javascript;app.js
app.post('/sign_up',(req,res,next)=>{constemail=req.body.email;connection.query('SELECT * FROM users WHERE email = ?',[email],(error,results)=>{if(results.length>0){res.render('uniq_error.ejs');}else{next();}});

こちらの部分になります。
これで重複するメールアドレスの場合はuniq_error.ejsに遷移します。

ちなみにuniq_error.ejsには
『既に登録されているメールアドレスがあるため登録できませんでした』
の文字しかありません。

9.終わりに

バリデーションチェックまでは苦戦しながらも進められたのですが、メールアドレスの重複チェックで躓き、数十時間もがき苦しみました。

まだまだ理解が足りない証拠ですね。とても悔しいです。
PHPの知識もそのうち入れたいと思います。

訪問者カウンター作成を通してDocker Composeを学習

$
0
0

Dockerについて記事を書くのは3回目です。
コンテナ未経験なのでDockerを基礎から学んでみた
DockerコンテナでNode.jsを実行してみた

今回はDocker Composeを使い、Node.jsとRedisからなる簡単な"訪問者カウンター"を作ってみました。
スクリーンショット 2021-03-07 10.47.15.png

Docker Composeって何?

複数のコンテナからなるシステムを簡単に構築するためのツールです。YAMLファイルを使ってアプリケーションサービスの設定を行い、コマンドを1つ実行するだけで、設定内容に基づいたアプリケーションサービスの生成・起動を行うことができます。

システム構築

Docker Composeを使わずに構築トライ

package.jsonindex.jsDockerfileを作成します。

package.json
{"dependencies":{"express":"*","redis":"2.8.0"},"scripts":{"start":"node index.js"}}

index.jsのapp.getの中で、redisへの訪問者数(visits)の保存とカウントを行います。

index.js
constexpress=require('express');constredis=require('redis');constapp=express();constclient=redis.createClient();client.set('visits',0);app.get('/',(req,res)=>{client.get('visits',(err,visits)=>{res.send('Number of visits is '+visits);client.set('visits',parseInt(visits)+1);});});app.listen(8081,()=>{console.log('Listening on port 8081');});

また、Dockerfileでは、ベースイメージをnode:alpineとし、ファイルのコピー先を'/app'と指定しています。

FROM node:alpineWORKDIR '/app'COPY package.json .RUN npm installCOPY . .CMD ["npm", "start"]

docker run redisでredisのコンテナを起動した後に、ターミナルの別タブを開いて、作成したDockerfileからイメージのビルド(docker build -t suzuki0430/visits:latest .)とコンテナ起動(docker run suzuki0430/visits)を行います。

すると、redis-serverとの接続ができないというエラーがでます。

Error: getaddrinfo ENOTFOUND redis-server

コンテナプロセスは独立して動いているので、各コンテナを接続したい場合はそれに関する設定を行う必要があります。
ここで使用するのがDocker Composeです。

Docker Composeを使って構築トライ

複数のDockerコンテナを同時起動するために、docker-compose.ymlファイルを以下のように記述します。

version: '3' // docker-composeのバージョン
services: // 起動したいコンテナの種類
  redis-server:
    image: 'redis' // redisイメージでコンテナを起動
  node-app:
    build: . // カレントディレクトリのDockerfileからイメージをビルドしてコンテナを起動
    ports: // ポートマッピング([localhostのポート番号]:[コンテナのポート番号])
      - '4001:8081'

また、redis-servernode-appの接続を行うために、index.jsに以下を追記します。6379はredisのデフォルトポート番号です。

constclient=redis.createClient({host:'redis-server',port:6379,});

Docker Composeはdocker-compose CLIのコマンドで操作します。

  • docker-compose up : コンテナ起動
  • docker-compose up --build : イメージビルド + コンテナ起動
  • docker-compose up -d : バックグラウンでコンテナ起動
  • docker-compose down : コンテナ停止
  • docker-compose ps : プロセス確認

docker-compose upを実行してみると、node-appredis-serverのコンテナが起動します。

(base) [16:12:32] → docker-compose up                                                                                                             ~/Programs/docker/visits
Recreating visits_node-app_1   ... done
Starting visits_redis-server_1 ... done
Attaching to visits_redis-server_1, visits_node-app_1
redis-server_1  | 1:C 06 Mar 2021 07:12:39.624 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
redis-server_1  | 1:C 06 Mar 2021 07:12:39.624 # Redis version=6.0.9, bits=64, commit=00000000, modified=0, pid=1, just started
redis-server_1  | 1:C 06 Mar 2021 07:12:39.624 # Warning: no config file specified, using the default config. In order to specify a config file use redis-server /path/to/redis.conf
redis-server_1  | 1:M 06 Mar 2021 07:12:39.626 * Running mode=standalone, port=6379.
redis-server_1  | 1:M 06 Mar 2021 07:12:39.626 # WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/sys/net/core/somaxconn is set to the lower value of 128.
redis-server_1  | 1:M 06 Mar 2021 07:12:39.626 # Server initialized
redis-server_1  | 1:M 06 Mar 2021 07:12:39.627 * Loading RDB produced by version 6.0.9
redis-server_1  | 1:M 06 Mar 2021 07:12:39.627 * RDB age 10216869 seconds
redis-server_1  | 1:M 06 Mar 2021 07:12:39.627 * RDB memory usage when created 0.79 Mb
redis-server_1  | 1:M 06 Mar 2021 07:12:39.627 * DB loaded from disk: 0.001 seconds
redis-server_1  | 1:M 06 Mar 2021 07:12:39.627 * Ready to accept connections
node-app_1      |
node-app_1      | > start
node-app_1      | > node index.js
node-app_1      |
node-app_1      | Listening on port 8081

以下の部分で、コンテナ同士の接続も確認することができます。

Attaching to visits_redis-server_1, visits_node-app_1

ブラウザでlocalhost:4001に接続すると、ちゃんと開けました。

スクリーンショット 2021-03-07 10.51.01.png

Docker Composeの自動再起動

システムの構築は完了したのですが、Docker Composeの理解を深めるために自動再起動の方法についても学びました。

index.jsのapp.getでprocess.exit(0)を記述し、node-appのプロセスを終了するようにします。

constprocess=require('process');app.get('/',(req,res)=>{process.exit(0);});

次にdocker-compose.ymlrestart: alwaysを加えます。

version: '3'
services:
  redis-server:
    image: 'redis'
  node-app:
    restart: always
    build: .
    ports:
      - '4001:8081'

docker-compose up --buildでイメージビルドとコンテナ起動を再実行してlocalhost:4001にアクセスすると、プロセス終了後にコンテナを再起動しようとします。

node-app_1      |
node-app_1      | > start
node-app_1      | > node index.js
node-app_1      |
node-app_1      | Listening on port 8081
visits_node-app_1 exited with code 0
node-app_1      |
node-app_1      | > start
node-app_1      | > node index.js
node-app_1      |
node-app_1      | Listening on port 8081
visits_node-app_1 exited with code 0

restart: always以外にも以下のような再起動ポリシーがあります。

  • "no": 再起動しない
  • always: コンテナが停止したらいかなる理由でも再起動する
  • on-failure: コンテナがエラーコードをだして停止したら再起動する
  • unless-stopped: ユーザがコンテナを強制終了しない限り常に再起動する

おわりに

次はDockerコンテナの本番環境へのデプロイについて学習します。

参考資料

npmのコマンド まとめ

$
0
0

目次

  1. インストール系
  2. パッケージやnpmの情報表示
  3. パッケージの検索・監査
  4. パッケージ開発
  5. その他

1. インストール系

npm init

初期化(package.jsonファイルを作成)する。

$ npm init

# パッケージ名などを入力するのを省略したい場合$ npm init -y$ npm init --yes

npm install

package.json(またはpackagelock.json、npmshrinkwrap.json)ファイルに記載したパッケージとそれらのパッケージに依存するパッケージをインストールする。
具体的には、node_modulesフォルダを作成し、必要なファイルをインストールする。

# package.jsonに記載されてあるパッケージをインストールする$ npm install# 以下でも実行が可能$ npm i
$ npm add

# パッケージを指定してインストールする$ npm install[package name]
$ npm install react  

# パッケージのバージョンを指定してインストールする$ npm install[package name]@version
$ npm install react@16            # reactのバージョン16をインストール$ npm install react@latest        # reactの最新バージョンをインストール# グローバルモードでパッケージをインストールする$ npm install-g[package name]
$ npm install--global[package name]

# devDependenciesとしてインストールする(デフォルトはDependenciesとしてインストールする)$ npm install-D[package name]
$ npm install--save-dev[package name]

# package.jsonに追記せずにインストールする$ npm install--no-save[package name]

npm ci

package-lock.json(またはnpm-shrinkwrap.json)の内容にそってパッケージをインストールする。
(npm installコマンドより高速に動作する)

npm installとの違い

  • package-lock.json or npm-shrinkwrap.jsonが存在する必要がある。
  • package-lock.jsonの依存関係がpackage.jsonの依存関係と一致しない場合、npm ciはpackage-lock.jsonを更新せずにエラーで終了する。
  • npm ciはパッケージ全体をインストールすることしかできない(個別は無理)。
  • node_modeulesが既に存在する場合、npm ciがインストールを開始する前に自動的に削除される。
  • package.jsonやpackage-lock.jsonへの書き込みは行われない。

npm unistall

パッケージをアンインストールする。

# 指定パッケージをアンインストールする$ npm uninstall [package name]
$ npm uninstall react     # reactをアンインストールする# 以下でも実行が可能$ npm remove [package name]
$ npm rm[package name]
$ npm r [package name]
$ npm un [package name]
$ npm unlink[package name]

# package.jsonのdependenciesから削除し、パッケージをアンインストールする$ npm uninstall -S[package name]
$ npm uninstall --save[package name]

# package.jsonのdevDependenciesから削除し、パッケージをアンインストールする$ npm uninstall -D[package name]
$ npm uninstall --save-dev[package name]

# package.jsonのoptionalDependenciesから削除し、パッケージをアンインストールする$ npm uninstall -O[package name]
$ npm uninstall --save-optional[package name]

# package.jsonから削除せず、パッケージをアンインストールする$ npm uninstall --no-save[package name]

npm prune

インストールされた不要なパッケージを削除する。
package-lockを有効にしている場合、自動的にnpm pruneされるため不要。

npm edit

インストールしたパッケージを編集する。
具体的には、デフォルトのエディタでインストールしたパッケージのディレクトリを開く。

$ npm edit [package name]

npm update

インストールされたパッケージを最新のバージョンに更新する。

# 全てのパッケージを最新バージョンに更新する$ npm update

# 以下でも実行が可能$ npm up
$ npm upgrade

# パッケージを指定した場合は、指定したパッケージのみ最新のバージョンに更新する$ npm update [package name]

npm outdated

パッケージのバージョンが古いかをチェックする。
インストールされたパッケージの現在のバージョン、最新のバージョンなどを表示する。

$ npm outdated [package name]

npm dedupe

依存関係の都合上で同じパッケージが異なるバージョンで複数インストールされた場合などに、
パッケージの重複を整理する。

# パッケージの重複を整理$ npm dedupe

# 以下でも実行が可能$ npm ddp
$ npm find-dups

2. パッケージやnpmの情報表示

npm

npmの使い方を簡易表示する。
(使用できるnpmのコマンド一覧、helpを表示する方法など)

# npmの使い方を簡易表示$ npm

# npmの使い方を詳細表示する$ npm -l# npmコマンドのクイックヘルプを表示する$ npm [command]-h$ npm install-h# インストールのヘルプを表示# npmのインストールバージョンを表示する$ npm --version$ npm -v

npm help

npmのヘルプを表示する。

# npmのヘルプを表示$ npm help

npm config

npmの設定ファイルを管理する。
(設定ファイルの内容を表示したり、追加・削除・変更を行う)

$ npm config

# 以下でも実行が可能$ npm c

# 設定ファイルの内容一覧を表示する$ npm config list

# 設定ファイルに内容を追加する$ npm config set[key] [value]

# 設定ファイルの内容を取得する$ npm config get [key] [value]

# 設定ファイルの内容を削除する$ npm config delete [key] [value]

# 設定ファイルの内容を削除する$ npm config edit

npm ls

インストールされたパッケージを一覧表示する。

# インストールされたパッケージを一覧表示$ npm list

# 以下でも実行が可能$ npm la
$ npm li

# 表示する階層はトップレベルのみ(依存するパッケージは表示しない)$ npm ls--depth=0

npm view

レジストリやパッケージの情報を表示する。

$ npm view [package name]

# 以下でも実行が可能$ npm info [package name]
$ npm show [package name]
$ npm v [package name]

npm root

npmのルートフォルダを表示する。
具体的には、npm_modulesの絶対フォルダパスを表示する。

$ npm root

npm bin

npmのbinディレクトリを表示する。

$ npm bin

# グローバルモードでnpmのbinディレクトリを表示する$ npm bin -g$ npm bin -global

npm explore

インストールされたパッケージのディレクトリを表示する。

# 指定パッケージのディレクトリを表示$ npm explore [package name]

npm docs

Webブラウザでパッケージのドキュメント/公式サイトを表示する。

# 指定パッケージの公式サイトを表示$ npm docs [package name]

# 以下でも実行が可能$ npm home [package name]

npm repo

Webブラウザでパッケージのリポジトリを表示する。

# 指定パッケージのリポジトリを表示$ npm repo [package name]

npm bugs

Webブラウザでパッケージのバグ/Issueを表示する。

# 指定パッケージのバグ/Issueを表示$ npm bugs [package name]

3. パッケージの検索・監査

npm search

レジストリからパッケージを検索する。

# 指定パッケージを検索$ npm search [package name]

# 以下でも実行が可能$ npm s [package name]
$ npm se [package name]
$ npm find [package name]

npm audit

セキュリティ監査を実行する。
(インストールしたパッケージの脆弱性をスキャンする)

# セキュリティ監査をする$ npm audit

# セキュリティ監査を実行し、json形式で出力する$ npm audit --json# セキュリティ監査を実行して脆弱性のあるバージョンはアップデートをする$ npm audit fix

# セキュリティ監査を実行して、脆弱性のあるパッケージは互換性のあるバージョンにアップデートする# 破壊的変更がある場合でも実行するため注意$ npm audit fix --force# セキュリティ監査を実行して、脆弱性のあるパッケージは互換性のあるバージョンにアップデートする# ただし、node_modulesは変更せずpackage-lock.jsonを更新する$ npm audit fix --package-lock-only# セキュリティ監査を実行して、脆弱性のあるパッケージは互換性のあるバージョンにアップデートする# ただし、devDependenciesは除く$ npm audit fix --only=prod

# セキュリティ監査を実行して、脆弱性のあるパッケージは互換性のあるバージョンにアップデートする# 監査の修正がどのように行われるかを把握したり、インストール情報をJSON形式で出力したりする$ npm audit fix --dry-run--json

npm doctor

npmが正常に動作する状態にあるか、使用している環境をチェックする。
チェック内容は以下の通り。

  • Node.jsとgitが実行できること
  • プライマリのnpmレジストリであるregistry.npmjs.comが利用可能できあること
  • node_moduleが存在し、現在のユーザーが書き込み可能であること
$ npm doctor

4. パッケージ開発

npm adduser

指定のレジストリにユーザーアカウントを追加する。

# http://myregistry.example.comレジストリにユーザーアカウントを追加する場合$ npm adduser --registry=http://myregistry.example.com

npm test

パッケージのテストをする。

$ npm test

npm build

パッケージのビルドをする。

$ pm build [package folder]

npm rebuild

パッケージのリビルドをする。

$ npm rebuild [package folder]

npm publish

パッケージを公開する。

$ npm publish [folder]

npm unpublish

パッケージを非公開にする。
レジストリからパッケージを削除する。

$ npm unpublish [package]

npm version

パッケージのバージョンを更新する、など。

# メッセージを設定してバージョン番号を更新する場合(%sはバージョン番号に置き換えられる)$ npm version patch -m"Upgrade to %s for reasons"

npm deprecate

パッケージの指定バージョンを非推奨にする。

$ npm deprecate[package@version]

npm dist-tag

ディストリビューションタグを変更する。

# ディストリビューションタグを追加する場合$ npm dist-tag add[package@version][tag]

npm shrinkwrap

npm-shrinkwrap.jsonファイルを作成する。

$ npm shrinkwrap

npm pack

パッケージからtarballを作成する。

npm pack [package]

npm ping

レジストリにpingする。

$ npm ping [registry]

npm profile

レジストリのプロフィールを変更する。

# プロフィールのプロパティに値を設定する場合$ npm profile set[property] [value]

npm star / npm unstar

お気に入りのパッケージをマークする / マークをはずす。

# お気に入りのパッケージをマークする$ npm star[package]

# お気に入りのパッケージのマークをはずす$ npm unstar [package]

npm stars

お気に入りのパッケージを表示する。

$ npm stars [user]

5. その他

npm completion

コマンドのタブ補完を可能にする。

# Bashに設定する場合$ npm completion >> ~/.bashrc

# Zshに設定する場合$ npm completion >> ~/.zshrc

npm xmas

ネタ、クリスマスツリーのアスキーアートを表示する。

$ npm xmas

Node.js WebアプリケーションのDocker環境構築まとめ

$
0
0

1.この記事の内容

ブラウザ経由で操作するWebアプリの実装が必要となり,Node.jsをDocker環境で動かすサンプルを実装しましたので紹介します.
Webページ上のフォームに入力されたデータをサーバ上にjsonファイルとして保存するプログラムです.

1-1.使用環境

  • Windows Subsystem for Linux上のUbuntu 20.04
  • Docker Imageはnode:12

1-2.ソースコード

本記事の内容は下記リポジトリの「01_docker_nodejs」~「03_file_access」までです.
本記事には概要のみを記載します.詳細はgithub内のソースコードを参照ください.

2.実装概要

2-1.Dockerfile

今回実装したサンプルはlocalhostアクセスを前提としていて,かつ,WSL2上に構築したDockerコンテナからアクセスするため,0.0.0.0でLISTENし,ポートは8080を使用します.
サーバの起動はnpmを使用しますが,サーバ上のファイルを保存するため,bashでコンテナを起動し「node server.js」または「npm start」を実行してサーバを起動します.

サーバ起動後はブラウザで「http://localhost:8080」へアクセスする.
なお,Dockerfile内の「CMD [ "/bin/bash" ]」を「CMD [ "npm", "start" ]」とすると「docker run」でサーバを起動することができます.

FROM node:12# アプリケーションディレクトリを作成するWORKDIR /usr/src/app# アプリケーションの依存関係をインストールする# ワイルドカードを使用して、package.json と package-lock.json の両方が確実にコピーされるようにします。# 可能であれば (npm@5+)COPY package*.json ./RUN npm install express axios --save# 本番用にコードを作成している場合# RUN npm install express axios --only=production# アプリケーションのソースをバンドルするCOPY . .EXPOSE 8080CMD [ "/bin/bash" ]

2-2.Webページのフォーム

index.htmlに自由記述のテキストボックスとSubmitボタンを配置し,Submitボタンがクリックされたらテキストボックスに入力されたデータがPOST送信される仕組みです.

<!doctype html><html><head><metacharset="utf-8"><title>POST sample</title><style>body{font-family:sans-serif;}input{padding:5px;font-size:1rem;}button{padding:5px;width:70px;font-size:1rem;}</style></head><body><formaction='/'method='POST'><table><tr><td>Name:</td><td><inputtype='text'name='name'value=''placeholder='Input your name'></td></tr><tr><td>Age:</td><td><inputtype='text'name='age'value=''placeholder='Input your age'></td></tr><tr><td>Phone:</td><td><inputtype='text'name='phone'value=''placeholder='Input your phone number'></td></tr><tr><td></td><td><buttontype='submit'>Submit</button></td></tr></table></form></body></html>

2-3.POST送信データの保存

Webフォームから入力されたnameフィールド名でjson保存先のディレクトリを生成し,Webフォームに入力されたname,age,phoneフィールドの内容をjson形式で保存します.

mkdir実行後,ファイルシステム上にディレクトリが生成される前にjsonファイル作成処理が実行される状況が生じたため,mkdir実行後にディレクトリ生成確認を行うようにしました.setIntervalを用いて1ms間隔でチェックし,100回チェックしてなお,ディレクトリ未生成状態が続けばタイムアウトする仕様で実装しました.

jsonファイル保存後はトップページに戻る仕様とし,GETリクエストを発行してページを取得し,ブラウザへ送信する.
以前はrequestモジュールが使用できたが,deprecatedだったので,代替手段のディスカッションの中で多そうだったaxiosモジュールを使用することとしました.

// App: Postapp.use(bodyParser.urlencoded({extended:true}));app.use(bodyParser.json());app.post('/',(req,res)=>{console.log(req.body);varuser_dir=path.join(user_dir_root,req.body.name);varuser_info_file=path.join(__dirname,user_dir,user_info);// create user directoryconsole.log(user_dir);console.log(user_info_file);console.log(__dirname);fs.mkdir(user_dir,{recursive:true},(err)=>{if(err)throwerr;});// create user information file after user directory is createdvarms=1;varcount=0;varcount_th=100;// timeout threshold: 100msvarinterval_id=setInterval(function(){if(fs.existsSync(user_dir)){clearInterval(interval_id);fs.writeFile(user_info_file,JSON.stringify(req.body,null,'\t'),(err)=>{if(err)throwerr;});console.log('[INFO] save user_info done');// back to top page after submitconstoptions={method:'GET',url:`http://${HOST}:${PORT}`,};axios.get(`http://${HOST}:${PORT}`).then(function(response){//                  console.log(response.data); // for debugres.send(response.data);});}else{count++;if(count>count_th){// timeoutconsole.log('[ERROR] Create directory failed: '+user_dir);throwError('[ERROR] Timeout');}}},ms);});

3.さいごに

環境立ち上げとしてjsonファイルの保存までを記事にしました.内容は基礎的な部分のみだと思いますが,どなたかの参考になりそうな情報が含められていれば幸いです.

4.関連リンク


【Vue 2.x】Vue CLI環境にViteを導入して開発ビルドを爆速にする

$
0
0

2021/02 に Vite 2.0 正式版がリリースされ、開発サーバーの起動や HMR (Hot Module Replacement) の動作が軽快なフロントエンド開発環境を手軽に構築できるようになってきました。

これとほぼ同時期に、既存の Vue 2.x + Vue CLI環境に Vite 環境を同居させ、開発ビルドにだけ Vite を活用する vue-cli-plugin-viteがリリースされました。このプラグインは「コードベースの変更なしに」を謳っていて、Vue CLI 向けの設定ファイル vue.config.jsを Vite 向けの設定に変換1することで Vite 開発サーバーを起動させる機能を持っています。
実際に Vue CLI 環境へ導入してみたところ、「コードベースの変更なしに」は概ね正しかったものの vue.config.jsなどに修正が必要でした。

設定に若干難しさはあったものの、開発サーバーの起動時間が 3 秒程度になって HMR の動作も大幅に改善されました
本記事では、設定ファイルの修正方法や Vite 開発サーバーにおける注意点などを共有します。

image.png

動作確認環境

  • macOS Catalina
  • Chrome 89
  • Node.js 14
  • npm 6
  • Vue CLI v4.5.11
    • vue 2.6.12
    • vue-cli-plugin-vite 0.3.2
    • typescript 3.9.9
    • sass (dart-sass) 1.32.8
    • sass-loader 8.0.2

プラグインの導入

Vue 2.x + Vue CLI 環境のディレクトリ上で vue add viteまたは npx @vue/cli add viteを実行してプラグインを導入します。

プラグインを導入すると、package.jsonnpm run viteのタスクが追加され、 bin/viteというシェバン付きの JavaScript ファイルが作成されます。

package.json
{"scripts":{"serve":"vue-cli-service serve","build":"vue-cli-service build","lint":"vue-cli-service lint","vite":"node ./bin/vite"}}

各種設定

開発サーバー立ち上げ時にブラウザを起動しないようにする設定

環境変数 BROWSER=noneをセットします:

package.json
{"scripts":{"serve":"vue-cli-service serve","build":"vue-cli-service build","lint":"vue-cli-service lint","vite":"BROWSER=none node ./bin/vite"}}

テンプレート上での空白の取り扱いの設定

Vue CLI 環境では whitespaceのデフォルト設定が condenseでしたが、Vite 環境では preserveになっていました。

condensepreserve
condenseでのレンダリング結果preserveでのレンダリング結果

この違いによって、ブラウザ上での余白の表示が変化することもあるため、Vite 開発環境においても whitespacecondenseにする設定を追加します:

vue.config.js
module.exports={pluginOptions:{vite:{vitePluginVue2Options:{vueTemplateOptions:{compilerOptions:{whitespace:'condense',},},},},},};

ネストが深くなっていてわかりにくいですが、各種オプション名は以下ような対応となっています:

  • vite: vue-cli-plugin-viteの設定
  • vitePluginVue2Options: vite-plugin-vue2の設定
  • vueTemplateOptions: @vue/component-compiler-utilsの設定
  • compilerOptions: vue-template-compilerの設定

<style lang="scss">ブロックへのコードの自動挿入設定

Vue CLI 環境には .vueファイルの <style lang="scss">ブロック2変数等を自動挿入する設定があります。
これを Vite 環境でも利用するには、vue.config.jsを以下のように設定します:

  • css.loaderOptions.sassで設定している場合は css.loaderOptions.scssに変更する
  • sass-loader v8 の場合、Vue CLI 環境では css.loaderOptions.scss.prependDataとして振る舞い、 Vite 環境では css.loaderOptions.scss.additionalDataとして振る舞うように設定する3
vue.config.js
// Vite 開発環境であるかを判定するための変数を用意する。// ここでは「開発サーバー立ち上げ時にブラウザを起動しないようにする」の設定した上で、その設定の有無にて判定します。constIS_VITE_ENV='BROWSER'inprocess.env;module.exports={css:{loaderOptions:{scss:{[IS_VITE_ENV?'additionalData':'prependData']:`
          $main-color: #123456;
        `,},},},};

バンドル結果を別のサーバーで配信している環境の場合

Vue CLI 環境の開発ビルド(npm run serve)でディスク書き込みを有効にして、バンドル結果の js ファイルや css ファイルを別のサーバー環境で配信している場合もあるかと思います。
この場合は Vite 開発サーバー起動と同時に、Vite 開発サーバーが配信するモジュールを読み込むためのコードをディスクに書き込む必要があります。

ここでは以下の状況を仮定します:

  • src/main.tsがエントリーポイント
    • 出力先は dist/app.js
  • npm run serve時のディスク書き込みが有効
  • Vue CLI / Vite 開発サーバーのポート番号は 33333
  • http://localhost:8080/foo/bar/dist/の内容を配信している
  • localhost:8080側で配信する HTML に <script src="/foo/bar/app.js"></script>が埋め込まれている
vue.config.js
module.exports={configureWebpack:{output:{filename:'[name].js',},},devServer:{port:33333,writeToDisk:true,},publicPath:`/foo/bar/`,};

vue-cli-plugin-vitedevServer.writeToDiskまでは考慮しないので、dist/app.jsに Vite 開発サーバー側のモジュールを参照させるコードを書き込む必要があります。

dist/app.jsの書き込み

http://localhost:33333/foo/bar/src/mainを読み込むように設定します。

この dist/app.jsモジュールではなく通常のスクリプトであるため、import文ではなく動的 import()を利用します4:

dist/app.js
import('http://localhost:33333/foo/bar/src/main');

npm run vite実行時に上記内容を書き込むため、bin/viteに以下の内容を追記します:

bin/vite
// 以下をファイル下部に追記するconstfs=require('fs').promises;// const path = require('path'); は既に定義されているconstgetPath=pathSegment=>path.resolve(__dirname,'../',pathSegment);(async()=>{// dist ディレクトリがない場合は作成するawaitfs.mkdir(getPath('dist/')).catch(()=>{});// dist/app.js を書き込むawaitfs.writeFile(getPath('dist/app.js'),`import('http://localhost:33333/foo/bar/src/main');`).catch(e=>console.error(e));})();

画像ファイルの配信するためにシンボリックリンクを作成

.vueファイルのテンプレートに <img src="@/assets/logo.png">のように書くと <img src="/foo/bar/src/assets/logo.png">のような img タグがレンダリングされるため、http://localhost:33333/foo/bar/src/assets/logo.png
ではなく http://localhost:8080/foo/bar/src/assets/logo.pngを参照しようとしてしまいます。

記事投稿時点では適切な設定オプションが無かった5ため、やむを得ず dist/src/へのシンボリックリンクを作成して localhost:8080側で画像ファイルを配信します:

$cd dist/
$ln-s ../src/ src

npm run vite実行時に上記の操作をするため、 以下のコードを bin/viteawait fs.mkdir(...);の下に追記します:

awaitfs.symlink('../src/',getPath('dist/src')).catch(()=>{});

/から始まるパスの画像ファイルを読み込めるようにする

.vueファイルのテンプレートに <img src="/img/photo.jpg">のように書いて http://localhost:8080/img/photo.jpgを参照しようとすると、Vue CLI 環境では問題なくコンパイルできるものの Vite 開発環境では以下のようなコンパイルエラーが出てしまいます:

[vite] Internal server error: Failed to resolve import "/img/photo.jpg". Does the file exist?

vite-plugin-vue2の実装を追っていったところ、以下のようにして transformAssetUrlsOptions.forceRequireの設定を追加する必要がありました:

vue.config.js
module.exports={pluginOptions:{vite:{vitePluginVue2Options:{vueTemplateOptions:{compilerOptions:{whitespace:'condense',},transformAssetUrlsOptions:{forceRequire:false,},},},},},};

注意事項

ES モジュールを用いていること

Vite 開発サーバーはモジュールの利用を前提としています。
特に IE11 はモジュールに非対応のため、IE11 で動作確認するには従来通り Vue CLI 環境を用いてバンドルする必要があります。

型エラーやフォーマットエラーはターミナル上のログとして表示されない

Vite は TypeScript による型チェックや Linter によるフォーマットチェックを行いません。エディタでの保存時に自動でフォーマットをする設定や、CI でエラーを検知できる仕組みなどを導入しておく必要があるでしょう。

esbuild を利用している

esbuildをトランスパイラとして用いているため、OS と CPU アーキテクチャに依存するバイナリが node_modulesにインストールされます。
特に Docker や仮想マシンを利用している場合は、異なる OS 間で node_modulesディレクトリを共用できないことに注意が必要です。

$file node_modules/esbuild/bin/esbuild
node_modules/esbuild/bin/esbuild: Mach-O 64-bit executable x86_64

まとめ

Vue CLI 環境ではプロジェクトの規模が大きくなればなるほど開発ビルドが重くなり、開発体験が悪くなっていく傾向がありましたが、vue-cli-plugin-viteのおかげでこの問題を解決できました。
設定に若干難しさがありますが、本記事を参考に Vite 開発環境を試していただければ幸いです。


  1. どのオプションが変換の対象となるかについては README.mdを参照 

  2. scss + Scoped CSS (<style lang="scss" scoped>) や scss + CSS Modules (<style lang="scss" module>) も対象 

  3. sass-loader v7 の場合は css.loaderOptions.scss.data, v9 の場合は css.loaderOptions.scss.additionalDataを用います。v9 以降であれば三項演算子と Computed property namesによる分岐は不要だと思われます。Vue CLI で環境構築した場合のデフォルトは v8 になっています(記事投稿時点) 

  4. 参照: https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/import 

  5. 参照: https://github.com/vitejs/vite/pull/2254 

ElectronのexecuteJavascriptでError: Script failed to executeが出る件

$
0
0

はじめに

初投稿です...
ElectronのBrowserViewでexecuteJavascriptを使った際、一見正しそうなスクリプトがエラーを吐く問題で少々悩んだので備忘録として。

問題のソースコード

要素の取得に失敗したらfalse、成功したら真を返したい

      const result = await browserView.webContents.executeJavaScript(`
        const anko = document.getElementById("anko"); 
        return anko == null ? false : true;
      `)

正しいソースコード

どうやらreturnで返しちゃだめらしいです。(そんなん知らんよ…)
最後に評価された値が返却されるらしい。

      const result = await browserView.webContents.executeJavaScript(`
     let val = true;
        const anko = document.getElementById("anko"); 
        anko == null ? false : true;
      `)

現在作っているアプリ

Youtube Live用のコメントビュワーです(Electron製)(ベータ版)
配信する方はぜひどうぞ
- https://tubug.netlify.app

LINEボットとATOM Echoでボイスメッセージを作る

$
0
0

ATOM Echoには、マイクとスピーカとボタンとLEDが付いています。
ATOM Echoのマイクにしゃべった言葉がLINEメッセージとして通知されるようにするとともに、LINEアプリから応答メッセージを入力したら、ATOM EchoのLEDが点灯し、さらにボタンを押したら応答メッセージが音声でATOM Echoのスピーカから流れるようにします。

image.png

いくつかのサービスを使っています

・録音したWAVEファイルを、Google Cloud Speech APIの音声認識サービスを使ってテキスト文字に起こします。
・LINEボットの機能を使って、メッセージをLINEアプリに通知したり、LINEアプリに入力したメッセージを受信したりします。
・受信したテキストメッセージを、Amazon Pollyの音声合成サービスを使って音声ファイルにします。
・ATOM Echoで、メッセージ通知を検知するために、MQTTでサブスクライブします。

ソースコードもろもろは、以下のGitHubに上げておきました。

poruruba/LinebotCarrier
 https://github.com/poruruba/LinebotCarrier

ATOM Echo側

以下のライブラリを利用しています。

m5stack/M5StickC
 https://github.com/m5stack/M5StickC
ボタンの検出に使っています。

knolleary/PubSubClient
 https://github.com/knolleary/pubsubclient
MQTTサブスクライブに使っています。

bblanchon/ArduinoJson
 https://github.com/bblanchon/ArduinoJson
MQTTサブスクライブで受信するJSONのパースに使っています。

adafruit/Adafruit_NeoPixel
 https://github.com/adafruit/Adafruit_NeoPixel
RGBのLED制御に使っています。

録音は、以下を参考にしました。
 M5StickCとSpeaker HatでAI Chatと会話
 m5stack/M5-ProductExampleCodes

こんな感じです。

Arduino\LinebotCarrier\src\main.cpp
// 録音用タスクvoidi2sRecordTask(void*arg){// 初期化recPos=0;memset(soundStorage,0,sizeof(soundStorage));vTaskDelay(100);// 録音処理while(isRecording){size_ttransBytes;// I2Sからデータ取得i2s_read(I2S_NUM_0,(char*)soundBuffer,BUFFER_LEN,&transBytes,(100/portTICK_RATE_MS));// int16_t(12bit精度)をuint8_tに変換for(inti=0;i<transBytes;i+=2){if(recPos<STORAGE_LEN){int16_t*val=(int16_t*)&soundBuffer[i];soundStorage[recPos]=(*val+32768)/256;recPos++;if(recPos>=sizeof(soundStorage)){isRecording=false;break;}}}//    Serial.printf("transBytes=%d, recPos=%d\n", transBytes, recPos);vTaskDelay(1/portTICK_RATE_MS);}i2s_driver_uninstall(I2S_NUM_0);pixels.setPixelColor(0,pixels.Color(0,0,0));pixels.show();if(recPos>0){unsignedlonglen=sizeof(temp_buffer);intret=doHttpPostFile((base_url+"/linebot-carrier-wav2text").c_str(),soundStorage,recPos,"application/octet-stream","upfile","test.bin",NULL,NULL,temp_buffer,&len);if(ret!=0){Serial.println("/linebot-carrier-wav2text: Error");}else{Serial.println((char*)temp_buffer);}}// タスク削除vTaskDelete(NULL);}voidi2sRecord(){isRecording=true;pixels.setPixelColor(0,pixels.Color(0,100,0));pixels.show();i2s_driver_uninstall(I2S_NUM_0);i2s_config_ti2s_config={.mode=(i2s_mode_t)(I2S_MODE_MASTER|I2S_MODE_RX|I2S_MODE_PDM),.sample_rate=SAMPLING_RATE,.bits_per_sample=I2S_BITS_PER_SAMPLE_16BIT,// is fixed at 12bit, stereo, MSB.channel_format=I2S_CHANNEL_FMT_ALL_RIGHT,.communication_format=I2S_COMM_FORMAT_I2S,.intr_alloc_flags=ESP_INTR_FLAG_LEVEL1,.dma_buf_count=6,.dma_buf_len=60,};esp_err_terr=ESP_OK;err+=i2s_driver_install(I2S_NUM_0,&i2s_config,0,NULL);i2s_pin_config_ttx_pin_config;tx_pin_config.bck_io_num=I2S_BCLK;tx_pin_config.ws_io_num=I2S_LRC;tx_pin_config.data_out_num=I2S_DOUT;tx_pin_config.data_in_num=I2S_DIN;//Serial.println("Init i2s_set_pin");err+=i2s_set_pin(I2S_NUM_0,&tx_pin_config);//Serial.println("Init i2s_set_clk");err+=i2s_set_clk(I2S_NUM_0,SAMPLING_RATE,I2S_BITS_PER_SAMPLE_16BIT,I2S_CHANNEL_MONO);// 録音開始xTaskCreatePinnedToCore(i2sRecordTask,"i2sRecordTask",4096,NULL,1,NULL,1);}

MP3の再生は、以下を利用させていただきました。

schreibfaul1/ESP32-audioI2S
 https://github.com/schreibfaul1/ESP32-audioI2S

PlatformIOを利用している場合は、zipファイルの中身をlibフォルダに突っ込めばコンパイルに含めてくれます。
ポート番号は、ATOM Echoの配線に合わせています。

ただし、うまく動かないところがあり、いくつか修正しています。(この直し方でよいのか自信がないですが。。。)
GitHubには、修正したファイルだけ上げてあります。

録音と再生を切り替えられるように、Audioのコンストラクターから再セットアップ用の関数Audio:setup()に分離しました。

変更前

Arduino\LinebotCarrier\lib\ESP32-audioI2S-master\src\Audio.cpp
Audio::Audio(){clientsecure.setInsecure();// if that can't be resolved update to ESP32 Arduino version 1.0.5-rc05 or higher//i2s configurationm_i2s_num=I2S_NUM_0;// i2s port numberm_i2s_config.mode=(i2s_mode_t)(I2S_MODE_MASTER|I2S_MODE_TX);m_i2s_config.sample_rate=16000;m_i2s_config.bits_per_sample=I2S_BITS_PER_SAMPLE_16BIT;m_i2s_config.channel_format=I2S_CHANNEL_FMT_RIGHT_LEFT;m_i2s_config.communication_format=(i2s_comm_format_t)(I2S_COMM_FORMAT_I2S|I2S_COMM_FORMAT_I2S_MSB);m_i2s_config.intr_alloc_flags=ESP_INTR_FLAG_LEVEL1;// high interrupt prioritym_i2s_config.dma_buf_count=8;// max buffersm_i2s_config.dma_buf_len=1024;// max valuem_i2s_config.use_apll=APLL_ENABLE;m_i2s_config.tx_desc_auto_clear=true;// new in V1.0.1m_i2s_config.fixed_mclk=I2S_PIN_NO_CHANGE;i2s_driver_install((i2s_port_t)m_i2s_num,&m_i2s_config,0,NULL);m_f_forceMono=false;m_filter[LEFTCHANNEL].a0=1;m_filter[LEFTCHANNEL].a1=0;m_filter[LEFTCHANNEL].a2=0;m_filter[LEFTCHANNEL].b1=0;m_filter[LEFTCHANNEL].b2=0;m_filter[RIGHTCHANNEL].a0=1;m_filter[RIGHTCHANNEL].a1=0;m_filter[RIGHTCHANNEL].a2=0;m_filter[RIGHTCHANNEL].b1=0;m_filter[RIGHTCHANNEL].b2=0;}

変更後

Arduino\LinebotCarrier\lib\ESP32-audioI2S-master\src\Audio.cpp
Audio::Audio(){clientsecure.setInsecure();// if that can't be resolved update to ESP32 Arduino version 1.0.5-rc05 or higher}voidAudio::setup(){i2s_driver_uninstall(I2S_NUM_0);//i2s configurationm_i2s_num=I2S_NUM_0;// i2s port numberm_i2s_config.mode=(i2s_mode_t)(I2S_MODE_MASTER|I2S_MODE_TX);m_i2s_config.sample_rate=16000;m_i2s_config.bits_per_sample=I2S_BITS_PER_SAMPLE_16BIT;m_i2s_config.channel_format=I2S_CHANNEL_FMT_RIGHT_LEFT;m_i2s_config.communication_format=(i2s_comm_format_t)(I2S_COMM_FORMAT_I2S|I2S_COMM_FORMAT_I2S_MSB);m_i2s_config.intr_alloc_flags=ESP_INTR_FLAG_LEVEL1;// high interrupt prioritym_i2s_config.dma_buf_count=8;// max buffersm_i2s_config.dma_buf_len=1024;// max valuem_i2s_config.use_apll=APLL_ENABLE;m_i2s_config.tx_desc_auto_clear=true;// new in V1.0.1m_i2s_config.fixed_mclk=I2S_PIN_NO_CHANGE;i2s_driver_install((i2s_port_t)m_i2s_num,&m_i2s_config,0,NULL);m_f_forceMono=false;m_filter[LEFTCHANNEL].a0=1;m_filter[LEFTCHANNEL].a1=0;m_filter[LEFTCHANNEL].a2=0;m_filter[LEFTCHANNEL].b1=0;m_filter[LEFTCHANNEL].b2=0;m_filter[RIGHTCHANNEL].a0=1;m_filter[RIGHTCHANNEL].a1=0;m_filter[RIGHTCHANNEL].a2=0;m_filter[RIGHTCHANNEL].b1=0;m_filter[RIGHTCHANNEL].b2=0;}

MP3ファイルが小さすぎると、再生されませんでしたので、ファイルサイズの下限を下げました。

変更前

Arduino\LinebotCarrier\lib\ESP32-audioI2S-master\src\Audio.cpp
if((InBuff.bufferFilled()>6000&&!m_f_psram)||(InBuff.bufferFilled()>80000&&m_f_psram)){

変更後

Arduino\LinebotCarrier\lib\ESP32-audioI2S-master\src\Audio.cpp
if((InBuff.bufferFilled()>1500&&!m_f_psram)||(InBuff.bufferFilled()>80000&&m_f_psram)){

なぜか、MP3の最後あたりの音が切れてしまいましたので、ウェイトを入れました。

変更前

Arduino\LinebotCarrier\lib\ESP32-audioI2S-master\src\Audio.cpp
if(m_f_webfile&&(byteCounter>=m_contentlength-10)&&(InBuff.bufferFilled()<maxFrameSize)){// it is stream from fileserver with known content-length? and// everything is received?  and// the buff is almost empty?, issue #66 then comes to an endplayI2Sremains();stopSong();// Correct close when play known length sound #74 and before callback #112sprintf(chbuf,"End of webstream: \"%s\"",m_lastHost);if(audio_info)audio_info(chbuf);if(audio_eof_stream)audio_eof_stream(m_lastHost);}

変更後

Arduino\LinebotCarrier\lib\ESP32-audioI2S-master\src\Audio.cpp
if(m_f_webfile&&(byteCounter>=m_contentlength-10)&&(InBuff.bufferFilled()<maxFrameSize)){// it is stream from fileserver with known content-length? and// everything is received?  and// the buff is almost empty?, issue #66 then comes to an endwhile(!playI2Sremains()){;}delay(500);stopSong();// Correct close when play known length sound #74 and before callback #112sprintf(chbuf,"End of webstream: \"%s\"",m_lastHost);if(audio_info)audio_info(chbuf);if(audio_eof_stream)audio_eof_stream(m_lastHost);}

WAVEファイルのアップロードは以下を参考にしました。
 ESP32でバイナリファイルのダウンロード・アップロード

MQTTサブスクライブは以下を参考にしました。
 ESP32で作るBeebotteダッシュボード

Node.js側

ざっと、以下のnpmモジュールを使っています。

・@line/bot-sdk
・google-cloud/speech
・mqtt
・fs
・aws-sdk

Node.js\api\controllers\linebot-carrier\index.js
constline=require('@line/bot-sdk');constspeech=require('@google-cloud/speech');constclient=newspeech.SpeechClient();constmqtt=require('mqtt');constfs=require('fs');constAWS=require('aws-sdk');constpolly=newAWS.Polly({apiVersion:'2016-06-10',region:'ap-northeast-1'});

以下の2つのエンドポイントを立ち上げています。

・/linebot-carrier
LINEボットのWebhookであり、LINEメッセージを受信すると呼び出されます。

・/linebot-carrier-wav2text
ATOM Echoからの、WAVファイルのアップロードを受け付けます。

説明が面倒なので、ソースをそのまま載せています。すみません。

Node.js\api\controllers\linebot-carrier\index.js
'use strict';constconfig={channelAccessToken:'【LINEチャネルアクセストークン(長期)】',channelSecret:'【LINEチャネルシークレット】',};constHELPER_BASE=process.env.HELPER_BASE||'../../helpers/';constResponse=require(HELPER_BASE+'response');varline_usr_id='【LINEユーザID】';constLineUtils=require(HELPER_BASE+'line-utils');constline=require('@line/bot-sdk');constapp=newLineUtils(line,config);constspeech=require('@google-cloud/speech');constclient=newspeech.SpeechClient();constmqtt=require('mqtt');constMQTT_HOST=process.env.MQTT_HOST||'【MQTTサーバのURL(例:mqtt://hostname:1883)】';constMQTT_CLIENT_ID='linebot-carrier';constMQTT_TOPIC_TO_ATOM='linebot_to_atom';constTHIS_BASE_PATH=process.env.THIS_BASE_PATH;constMESSAGE_MP3_FNAME=THIS_BASE_PATH+'/public/message.mp3';constfs=require('fs');constAWS=require('aws-sdk');constpolly=newAWS.Polly({apiVersion:'2016-06-10',region:'ap-northeast-1'});constmqtt_client=mqtt.connect(MQTT_HOST,{clientId:MQTT_CLIENT_ID});mqtt_client.on('connect',()=>{console.log("mqtt connected");});app.follow(async(event,client)=>{console.log("app.follow: "+event.source.userId);//  line_usr_id = event.source.userId;});app.message(async(event,client)=>{console.log("linebot: app.message");varbuffer=awaitspeech_to_wave(event.message.text);fs.writeFileSync(MESSAGE_MP3_FNAME,buffer);varjson={message:event.message.text};mqtt_client.publish(MQTT_TOPIC_TO_ATOM,JSON.stringify(json));varmessage={type:'text',text:'$',emojis:[{index:0,productId:"5ac1de17040ab15980c9b438",emojiId:120}]};returnclient.replyMessage(event.replyToken,message);});exports.fulfillment=app.lambda();exports.handler=async(event,context,callback)=>{if(event.path=='/linebot-carrier-wav2text'){//    console.log(new Uint8Array(event.files['upfile'][0].buffer));varnorm=normalize_wave8(newUint8Array(event.files['upfile'][0].buffer));// 音声認識varresult=awaitspeech_recognize(norm);if(result.length<1)throw'recognition failed';vartext=result[0];console.log(text);app.client.pushMessage(line_usr_id,app.createSimpleResponse(text));returnnewResponse({message:text});}};functionnormalize_wave8(wav,out_bitlen=16){varsum=0;varmax=0;varmin=256;for(vari=0;i<wav.length;i++){varval=wav[i];if(val>max)max=val;if(val<min)min=val;sum+=val;}varaverage=sum/wav.length;varamplitude=Math.max(max-average,average-min);/*
    console.log('sum=' + sum);
    console.log('avg=' + average);
    console.log('amp=' + amplitude);
    console.log('max=' + max);
    console.log('min=' + min);
*/if(out_bitlen==8){constnorm=Buffer.alloc(wav.length);for(vari=0;i<wav.length;i++){varvalue=(wav[i]-average)/amplitude*(127*0.8)+128;norm[i]=Math.floor(value);}returnnorm;}else{constnorm=Buffer.alloc(wav.length*2);for(vari=0;i<wav.length;i++){varvalue=(wav[i]-average)/amplitude*(32767*0.8);norm.writeInt16LE(Math.floor(value),i*2);}returnnorm;}}asyncfunctionspeech_recognize(wav){constconfig={encoding:'LINEAR16',sampleRateHertz:8192,languageCode:'ja-JP',};constaudio={content:wav.toString('base64')};constrequest={config:config,audio:audio,};returnclient.recognize(request).then(response=>{consttranscription=[];for(vari=0;i<response[0].results.length;i++)transcription.push(response[0].results[i].alternatives[0].transcript);returntranscription;});}asyncfunctionspeech_to_wave(message,voiceid='Mizuki',samplerate=16000){constpollyParams={OutputFormat:'mp3',Text:message,VoiceId:voiceid,TextType:'text',SampleRate:String(samplerate),};returnnewPromise((resolve,reject)=>{polly.synthesizeSpeech(pollyParams,(err,data)=>{if(err){console.log(err);returnreject(err);}varbuffer=Buffer.from(data.AudioStream);returnresolve(buffer);});});}

ファイルアップロードを受信するところは、以下を参考にしてください。
 バイナリファイルのアップロード・ダウンロードをする

WAVファイルから音声認識をするところ、テキスト文字列を音声ファイルに変換するところは、以下を参考にしてください。
 M5StickCとSpeaker HatでAI Chatと会話

LINEボットにより、メッセージ受信をトリガするには以下を参考にしてください。
 LINEボットを立ち上げるまで。LINEビーコンも。

以上

環境構築 6 Node.js

$
0
0

Node.jsの導入

Railsを動かすためにはNode.jsが必要となり、それをHomebrewを用いてインストールします。

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

ターミナルに入力

brew install node@14

Node.jsへのパスを設定しましょう

% echo 'export PATH="/usr/local/opt/node@14/bin:$PATH"' >> ~/.zshrc
% source ~/.zshrc

2. Node.jsが導入できたか確認

% node -v
v14.15.3 # 数値は異なる場合がある

環境構築 OSがMojave以前の場合

$
0
0

1. Command Line Toolsの導入

1-1. Command Line Toolsをインストール

ターミナルに入力

$ xcode-select --install

「インストール」をクリック。
「同意する」をクリック。

2. Homebrewの導入

2-1. Homebrewをインストール

コマンドを順番に1つずつ実行。

$ cd  #ホームディレクトリに移動
$ pwd #ホームディレクトリにいるかどうか確認
$ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)" # コマンドを実行

PCのパスワードを入力する。
「Press RETURN to continue or other key to abort」
が表示されたら、エンターキーをおす。

2-2. Homebrewがインストールされているか確認

上のコマンドだけ実行する。
バージョン情報が表示されれば無事にインストールされている。

$ brew -v
Homebrew 2.5.1 # 数値は異なる場合があります

2-3. Homebrewをアップデート

$ brew update

2-4. Homebrewの権限を変更

$ sudo chown -R `whoami`:admin /usr/local/bin

再度パスワードを求められた場合は、先ほどと同じように入力。

3. Rubyをインストール

Webアプリケーションの開発においては専用のRubyをインストールする必要があります。

3-1. rbenv と ruby-buildをインストール

$ brew install rbenv ruby-build

3-2. rbenvをどこからも使用できるようにする

$ echo 'eval "$(rbenv init -)"' >> ~/.bash_profile

3-3. bash_profileの変更を反映

$ source ~/.bash_profile

3-4. readlineをinstall

ターミナルのirb上で日本語入力を可能にする設定

$ brew install readline

3-5. readlineをどこからも使用できるようにする

$ brew link readline --force

3-6. rbenvを利用してRubyをインストール

$ RUBY_CONFIGURE_OPTS="--with-readline-dir=$(brew --prefix readline)"
$ rbenv install 2.6.

2.6.5と書いてあるのは今回インストールするRubyのバージョンです。

3-7. 利用するRubyのバージョンを指定

$ rbenv global 2.6.5

デフォルトでPCに入っていたRubyから、先ほどインストールしたRubyを使用するように切り替えることができました。

3-8. rbenvを読み込んで変更を反映

$ rbenv rehash

3-9. Rubyのバージョンを確認

以下のコマンドで最終確認。

$ ruby -v

4. MySQLを用意

4-1. MySQLのインストール

$ brew install mysql@5.6

4-2. MySQLの自動起動設定

MySQLは本来であればPC再起動のたびに起動し直す必要がありますが、それは面倒であるため、自動で起動するようにしておく。

$ mkdir ~/Library/LaunchAgents
$ ln -sfv /usr/local/opt/mysql\@5.6/*.plist ~/Library/LaunchAgents
$ launchctl load ~/Library/LaunchAgents/homebrew.mxcl.mysql\@5.6.plist

4-3.mysqlコマンドをどこからでも実行できるようにする

# mysqlのコマンドを実行できるようにする
$ echo 'export PATH="/usr/local/opt/mysql@5.6/bin:$PATH"' >> ~/.bash_profile
$ source ~/.bash_profile
# mysqlのコマンドが打てるか確認する
$ which mysql
# 以下のように表示されれば成功
/usr/local/opt/mysql@5.6/bin/mysql

4-4. mysqlを起動を確認

# mysqlの状態を確認するコマンドです
$ mysql.server status

# 以下のように表示されれば成功
 SUCCESS! MySQL running

5. Railsの導入

5-1. bundlerをインストール

Rubyの拡張機能(gem)を管理するためのbundler(バンドラー)をインストールする。

$ gem install bundler --version='2.1.4'

5-2. Railsをインストール

$ gem install rails --version='6.0.0'

5-3. rbenvを再読み込み

$ rbenv rehash

5-4. Railsが導入できたか確認

% rails -v
Rails 6.0.0   #「Rails」のあとに続く数字は変わる可能性があります

6. Node.jsの導入

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

$ brew install node@14

Node.jsへのパスを設定

$ echo 'export PATH="/usr/local/opt/node@14/bin:$PATH"' >> ~/.bash_profile
$ source ~/.bash_profile

6-2. Node.jsが導入できたか確認

$ node -v
v14.15.3 # 数値は異なる場合があります

7. yarnの導入

7-1. yarnをインストール

$ brew install yarn

7-2. yarnが導入できたか確認

$ yarn -v
Viewing all 8902 articles
Browse latest View live