- 投稿日:2020-08-14T22:48:39+09:00
外気の体感温度を教えてくれるLINEbotをAWS LambdaとSORACOMデバイスで作ってみた。
はじめに
先日SORACOMのデバイスとArduinoを用いて省エネIoT機器を作ったを書いた。
- ハッカソンでこういうモノ作ったよという記事。
話しかけると外の体感温度を教えてくれる。かわいい。アイコンをクール系美少女にしたい。
GPS情報とかとも組み合わせられるのでは?とこれを書きながらも拡張性を感じる作品である。
前提として、GPS マルチユニット SORACOM Editionを屋外に置いておく必要がある。
なぜ作ったか
- 実は今回のデバイスの使用言語はかなり迷走しており、C → JavaScript → Pythonと行きついた感じだった。
- JavascriptのプログラムはAPI呼び出しまで行ったがArduinoとの連携がうまくいかなかったので撤退したもので、これが上手くいっていればAWS Lambdaで幅広くできたのに……と恨み言を吐いていた。
- もったいないので、AWS LambdaをAPI呼び出しのプログラムで動かしてLINE botにしようということになった。
- 供養です。お盆なので。
構成
- 以下のページを参考にLINE botの作り方を学んだ。いじる部分はほとんどLINE Messenger APIの周りだけだったので実質コピペ。
- 今になって思えばこの記事だけで構築可能だが、結構時間を溶かした。
- API Gatewayの設定を行うときはAWS Lambdaの「トリガを追加」で追加したAPI Gatewayのリンクから飛ぶよ、などがわからず……
- でも他と比べたらめちゃめちゃわかりやすい。
- 以下のプログラムで用いられているsoracom_apiとatobは
npm installしなければならない。- その後、上の記事の3. index.jsとnode_modulesを圧縮しLambdaにアップロードするのときに一緒に圧縮した。
index.js"use strict"; const line = require("@line/bot-sdk"); const atob = require("atob"); const client = new line.Client({ channelAccessToken: process.env.ACCESSTOKEN }); // ①SDKをインポート const crypto = require("crypto"); exports.handler = function (event, context) { let body = JSON.parse(event.body); let signature = crypto .createHmac("sha256", process.env.CHANNELSECRET) .update(event.body) .digest("base64"); let checkHeader = (event.headers || {})["X-Line-Signature"]; if (signature === checkHeader) { // ②cryptoを使ってユーザーからのメッセージの署名を検証する //ここから追加部分 global.atob = require("atob"); var Soracom = require('soracom_api'); var soracom = new Soracom({email: 'メールアドレスをここに',password:'パスワードをここに'}); soracom.get('/data/Subscriber/マルチユニットのSIMの番号?sort=desc&limit=1',function(err,res){ console.log({err:err,res:res}); console.log(res[0].content); var d = res[0]["content"]; var e = JSON.parse(d); var decoded = atob(e["payload"]); var ans =JSON.parse(decoded); var temp = ans["temp"]; var humi = ans["humi"]; var A = ((temp - 1/2.3 * (temp-10) * (0.8-humi/100))); global.M = A.toFixed(1); }); //ここまで if (body.events[0].replyToken === "00000000000000000000000000000000") { let lambdaResponse = { statusCode: 200, headers: { "X-Line-Status": "OK" }, body: '{"result":"connect check"}', }; context.succeed(lambdaResponse); // ③接続確認エラーを確認する。 } else { //let text = body.events[0].message.text; //これも追加部分 if(global.M>26.5){ var t = "エアコンをつけた方が良いです。"; }else{ var t = "窓を開けても良いかもしれません。"; } let text = "現在の外気の体感気温は"+global.M+"℃です。"+t; const message = { type: "text", text: text, }; //ここまで client .replyMessage(body.events[0].replyToken, message) .then((response) => { let lambdaResponse = { statusCode: 200, headers: { "X-Line-Status": "OK" }, body: '{"result":"completed"}', }; context.succeed(lambdaResponse); }) .catch((err) => console.log(err)); // ④リクエストとして受け取ったテキストをそのまま返す } } else { console.log("署名認証エラー"); } };
- 追加したのはAPIアクセス部分と出力生成部分の2か所。
APIアクセス部分
- soracom_apiというライブラリのおかげでPythonの時よりもアクセスしやすくなっている。
ただ、データの取得が全て関数内で行われるが故に関数スコープで関数外に持ち出せないため、持ち出したい変数には
global.をつけないと使えない。
- これが載ってる記事見つからなかった……常識なんだろうな、すみません。
var Aで用いられているのはミスナールの体感温度関数。通常だと小数15桁まで得られてしまうので、
.toFixed(1)で丸め込んだ。出力生成部分
- 閾値以上ならエアコンを勧め、以下なら窓開けを勧める。
- Messaging APIリファレンス曰く、文字を送るにはJSON形式の
message = { type: "text", text: text, };をclient.replyMessage(replyToken, message)のメソッドとして用いるだけでいいらしい。感想
製作時間が短いので地味なものになってしまったが、SORACOMデバイスのデータを簡単にLINE botに活かせることが分かった。
- それとコードの供養もできた。
SORACOM Funcというものを用いるとより簡単にAWS Lambdaを活用できるらしい。
デバイスとの連携も夢ではないかも。少なくともボタンのクリックイベントはすぐに読めるようにできそう。
- 投稿日:2020-08-14T21:29:41+09:00
【Node.js mysql】mysql接続 データを取り出す
【ゴール】
mysql接続 データを取り出す
【開発環境】
■ Mac OS catalina
■ Homebrew 2.4.9
■ mysql Ver 8.0.21【実装】
appを作成
*homebrewのインストールが事前に必要です
*mysqlのインストールが事前に必要です
*mysqlのコマンドも合わせて覚えれますmac.terminal// ディレクトリ作成 $ mkdir js $ cd js $ npm init -y $ npm install express $ npm install ejs $ npm isntall mysql $ touch server.js $ touch app.ejs // mysql でDATABASEを作成 $ mysql -u root -p $ mysql> CREATE DATABASE JS; $ use JS; $ CREATE TABLE User (id int auto_incremet, name char(10)); $ INSERT INTO User value (1, tarou);コーディング
①
■ npmのmysqlを読み込み。
■「createCOnnection」で({})内の任意のDBに接続②
■ cosnt sql = データベースへの命令を定数へ
■「query」メソッドで、DBへ命令
■「if(err) throw err;」はエラーがあっても挙動させる
■「err, result」にDBからのデータが格納されていますserver.jsconst express = require('express'); const app = express(); // ① 〜 const mysql = require('mysql'); const connection = mysql.createConnection({ host: 'localhost', user: 'root', password: 'あなたのsqlのパスワード', database: 'JS' }); // ここまで function views (){ app.set('views', 'views'), app.set('view enigine', 'ejs'); } app.get('/', (req, res) => { // ② 〜 const sql = 'select * from User id = 1'; connection.query(sql, (err, result) => { if(err) throw err; console.log(result); views(); res.render('app', {user: result} ); }) // ここまで② }) app.listen(3000, (req, res) => { console.log('success'); })app.ejs<%= user.id %> <%= user.name %>確認
*localhost 3000接続
mac.terminal$ node app.js success以上
【合わせて読みたい】
■ 【HOMEBREW】 Mac OSのパッケージマネージャーについて node.jsやってたら学んだ事
https://qiita.com/tanaka-yu3/items/65dac47443cc08914a86■【node.js】 node.jsインストール 芋っていたけど、簡単だった件...
JavaScript
https://qiita.com/tanaka-yu3/items/739db5ffed24a8d9ae4b■DBonline
https://www.dbonline.jp/
- 投稿日:2020-08-14T21:09:28+09:00
入力フォームの文字をtwitterに共有する方法
完成版
入力してボタンを押すと、
このようにご自身のTwitterアカウントに文字が入力されている状態です。作成
使用環境: HTML5
index.html<!--入力フォーム--> <input type="text" id="content"> <!--ご自身のTwitterアカウントへ行きます--> <button id="twitter" class="btn" type="button">Tweet</button> <script> // twitter共有機能 document.getElementById("twitter").addEventListener('click', function(event) { event.preventDefault(); var left = Math.round(window.screen.width / 2 - 275); var top = (window.screen.height > 420) ? Math.round(window.screen.height / 2 - 210) : 0; window.open( "https://twitter.com/intent/tweet?text=" + encodeURIComponent(document.getElementById("content").value), null, "scrollbars=yes,resizable=yes,toolbar=no,location=yes,width=550,height=420,left=" + left + ",top=" + top); }); </script>これで共有機能が完成しました。
bootstrapやCSSを導入するとtwitterのボタンらしくなります
使用環境: HTML5 Bootstrap4index.html<!--buttonスタイルを整える--> <button id="twitter" class="btn" style="background-color:#00aced; color:white;" type="button"><i class="fab fa-twitter"></i> Tweet</button> <input type="text" id="content"> <script> // twitter共有機能 document.getElementById("twitter").addEventListener('click', function(event) { event.preventDefault(); var left = Math.round(window.screen.width / 2 - 275); var top = (window.screen.height > 420) ? Math.round(window.screen.height / 2 - 210) : 0; window.open( "https://twitter.com/intent/tweet?text=" + encodeURIComponent(document.getElementById("content").value), null, "scrollbars=yes,resizable=yes,toolbar=no,location=yes,width=550,height=420,left=" + left + ",top=" + top); }); </script> <!--bootstrap導入--> <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.6/umd/popper.min.js"></script> <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"></script> <script defer src="https://use.fontawesome.com/releases/v5.7.2/js/all.js"></script>先程よりtwitterのボタンらしくなりました。
参考
text_areaの内容をtwitterにシェアする https://teratail.com/questions/172448
- 投稿日:2020-08-14T19:42:52+09:00
昔発売されていたCASIOのゲーム電卓のデジタルインベーダーをJavaScriptで作成
1980年にカシオ計算機が発売した初代ゲーム電卓のデジタルインベーダーをJavaScriptで再現したものです。
2018年には復刻版も発売されて、ちょっと話題になっていました。今回作成したものはこちらからプレイできます
スクリプト自体は数年前(2016年頃)に作成したものですが、リプレイ対応や難易度設定の追加など今回少し手直ししました。
作成した当時は表示する敵数字を決定するのに普通にMath.random()を使用していましたが、リプレイ機能を追加するにあたって同じ乱数列を出す必要があったので、今回Xorshiftを使ってシード値を指定することで同じ乱数列を出せるようにしています。xorshift.jsclass XorShift { constructor(seed = Math.random() * 0x7fffffff) { this.x = 123456789; this.y = 362436069; this.z = 521288629; this.w = seed; } next() { const t = this.x ^ (this.x << 11); this.x = this.y; this.y = this.z; this.z = this.w; this.w = (this.w ^ (this.w >>> 19)) ^ (t ^ (t >>> 8)); return this.w; } random() { return (this.next() + 0x80000000) / 0x100000000; } rand(min, max) { if(min > max) [min, max] = [max, min]; return min + Math.floor(this.random() * (1 + max - min)); } } // example console.log('SEED無指定'); const x0 = new XorShift(); // SEED無指定(Math.random()でSEED決定) for(let i = 0; i < 10; ++i) console.log(x0.next()); console.log('SEED指定'); const x1 = new XorShift(12345678); // SEED指定 for(let i = 0; i < 10; ++i) console.log(x1.next()); // 速度と出現する値のばらつき具合確認 const arr = {}; console.time(); const x = new XorShift(); for(let i = 0; i < 1e7; ++i) { const r = x.rand(1, 10); if(arr[r] === undefined) arr[r] = 1; else ++arr[r]; } console.log(arr); console.timeEnd();Xorshiftのおかげでリプレイについては概ね正確に再現できていますが、まだ稀になにかのタイミングでずれることがあるようで(ずれる原因は
Xorshiftとは無関係の部分です)、最後まで再現できていたらラッキー、程度に捉えてもらえると助かります。最近のiOS、iPadOSでは、
touchstart/touchendイベントの連打が最低でも0.5秒程度開けないと効かない(それより早い連打は無視される)不具合があったようでプレイに支障をきたしていたのですが、ともにバージョン13.6で元に戻ったようです。
- 投稿日:2020-08-14T19:42:38+09:00
WebAudioAPIとcanvasで音声波形動画を出力するサイトを作る
はじめに
YouTubeなどで見かけるよくあるミュージックビデオに音声波形が動いて表示されたりするのがあります。
そういう動画は調べてみると、WindowsのAviUtlという無料の動画編集ソフトの拡張機能で作られてるようです。
最近では、daniwell氏によりMusic Visualization GeneratorがWindows/Macのフリーソフトがリリースされ、簡単に音声波形動画が作れるようになりました。
そういった中で、Web上でも作れないかと調べてみたところ、どうやらHTML5のWebAPIとJavascriptでできるようなのでやってみました。実装した機能
・画像読み込み、表示
・音声読み込み
・音声波形表示
・動画として出力完成図
今後の課題
- webpで動画が出力されるのでmp4で書き出して、ボタンクリックでTwitterに載せれるようにしたい
- canvasの性質上、モバイルへの対応が難しいので知見を深める
- フロントで動画読み込みから出力をしているため、書き出し限界がありそう
参考記事
WebAudioAPIについて
使い方は以下を参考にしました。
Web Audio API | Codelab
Web Audio API - Web API | MDN
Web Audio API の基礎WebAudioAPIで詰まったところは、以下のように仕様書を日本語訳してくれている方がいらっしゃるので辞書代わりに参考にしました。
Web Audio API ( 日本語訳 )canvasについて
動画保存について
MediaRecorder APIをつかってCanvas/WebAudioなゲーム画面を録画する
JavaScriptでcanvasを録画して動画ファイルに保存する方法
- 投稿日:2020-08-14T19:42:38+09:00
WebAudioAPIとcanvasで音声波形動画を出力するサイトを作ってみた
はじめに
YouTubeなどで見かけるよくあるミュージックビデオに音声波形が動いて表示されたりするのがあります。
そういう動画は調べてみると、WindowsのAviUtlという無料の動画編集ソフトの拡張機能で作られてるようです。
最近では、daniwell氏によりMusic Visualization GeneratorがWindows/Macのフリーソフトがリリースされ、簡単に音声波形動画が作れるようになりました。
そういった中で、Web上でも作って共有できたら便利だなと思い、調べてみたところ、どうやらHTML5のWebAPIとJavascriptでできるようなのでやってみました。作ったもの
コード
komura-c/music-waves-visualizer
実装した機能
WebAPIとCanvas、Javascriptを駆使して以下を実装しました。
・画像と音声の読み込み、表示
・音声波形表示
・動画として出力また、CSSフレームワークのMaterializeを使ってMaterialDesignを導入し、ボタンやSnackBarのようなToastsを実装しています。
画像と音声の読み込み
FileReader.onload - Web API | MDN
を参考にして、画像を読み込むと同時にcanvasに描画しています。
音声も同様ですが、読み込み時にbase64としてデコードすることでWebAudioAPIに渡した上で、FileReaderに読み込ませています。// audioLoad const LoadSample = (audioCtx, audioDataUrl) => { fetch(audioDataUrl) .then((response) => { return response.arrayBuffer(); }) .then((arrayBuffer) => { audioCtx.decodeAudioData(arrayBuffer).then((decodedData) => { buffer = decodedData; }); }) };音声波形表示
canvas に絵を描く - Web API | MDNを参考にしました。
モード別に描画方法を変えることで周波数バー・折れ線・円形を実現しています。動画として出力
以下の記事を参考に実装しました。
MediaRecorder APIをつかってCanvas/WebAudioなゲーム画面を録画する
JavaScriptでcanvasを録画して動画ファイルに保存する方法今後の課題
- webpで動画が出力されるのでmp4で書き出して、ボタンクリックでTwitterに載せれるようにしたい
- canvasの性質上、モバイルへの対応が難しいので知見を深める
- フロントで動画読み込みから出力をしているため、書き出し限界がありそう
参考記事
WebAudioAPIについて
使い方は以下を参考にしました。
Web Audio API | Codelab
Web Audio API - Web API | MDN
Web Audio API の基礎WebAudioAPIで詰まったところは、以下のように仕様書を日本語訳してくれている方がいらっしゃるので辞書代わりに参考にしました。
Web Audio API ( 日本語訳 )
- 投稿日:2020-08-14T19:29:33+09:00
監視カメラを用いたメールアラートシステムを開発してみる
日立製作所OSSソリューションセンタの横井です。
前回の記事では、TensorFlow.jsを用いた画像認識のフローの開発手順を紹介しました。今回は応用として、監視カメラと画像認識を連携させたメールアラートシステムを開発してみましょう。以下の図の様に、監視カメラの画像に不審者が写った際、自動でメールでアラートを送るフローを作成します。
開発するフロー
以下の様なフローを開発します。このフローでは、定期的にウェブサーバから監視カメラの画像を取得し、左下の「元の画像」のノードの下に画像を表示します。その後、TensorFlow.jsノードを用いて画像認識を行い、認識結果と認識結果付き画像をそれぞれデバッグタブと「認識結果付き画像」のノードの下に表示します。
もし、画像認識にて人物(person)が写っていると判定された場合は、SendGridノードを用いて画像ファイルを添付したアラートメールを送信します。実際に監視カメラを準備するのは大変なため、ここではサンプルとして、神奈川県が川の水量を確認するために設置している監視カメラの画像を活用してみました。
以降で本フローの作成手順を説明してゆきます。Node-RED環境は、ローカルPC環境、Raspberry Pi環境、クラウド環境などを用意してください。
必要なノードのインストール
Node-REDフローエディタの右上のハンバーガーメニューをクリックし、「パレットの管理」->「パレット」タブ->「ノードの追加」タブと遷移し、以下のノードをインストールします。
- node-red-contrib-tensorflow : TensorFlow.jsを用いた画像認識を行うノード
- node-red-contrib-image-output : フローエディタ上に画像を表示するノード
- node-red-contrib-sendgrid : SendGridを用いてメールを送信するノード
(1) 画像データを取得するフロー
最初に、ウェブサーバから画像のバイナリデータを取得するフローを作成します。以下のフローの様に、injectノード(ワークスペースに配置すると名前が「タイムスタンプ」に変わります)、http requestノード、image previewノードを配置し、ワイヤーで接続します。
その後、http requestノードをダブルクリックして、ノードのプロパティ設定を変更します。
http requestノードのプロパティ設定
http requestノードのプロパティ設定画面にあるURLに、監視カメラの画像のURLを貼り付けます(Google Chromeでは、画像の上で右クリックすると現れるメニューから「画像アドレスをコピー」を選択すると、画像をURLがクリップボードへコピーされます)。また、出力形式として「バイナリバッファ」を選択します。
画像データを取得するフローを実行
フローエディタ右上のデプロイボタンをクリックした後、injectノードの左側のボタンをクリックします。すると、injectノードからワイヤーを通してメッセージがhttp requestノードに送られ、監視カメラの画像を提供するウェブサーバから画像を取得します。画像データを取得後、バイナリ形式のデータを含むメッセージがimage previewノードに送られ、image previewノードの下に画像が表示されます。
右下に監視カメラが撮影した川の画像が表示されましたね!
(2)取得した画像データに対して画像認識を行うフロー
次に取得した画像に何が写っているかを分析するフローを作成します。cocossdノードと、debugノード(配置すると名前がmsg.payloadに変わります)、2つ目のimage previewノードを追加で配置します。その後、http requestノードの右側の出力端子とcocossdノードの左側の入力端子、cocossdノードの右側の出力端子とdebugノード、cocossdノードの右側の出力端子とimage previewノードの左側の入力端子をそれぞれワイヤーで接続します。これによって、監視カメラ画像のバイナリデータがcocossdノードに送られ、TensorFlow.jsを用いた画像認識が行われた後、物体名をdebugノードで表示し、画像認識結果付き画像をimage previewノードで表示します。
cocossdノードは、変数msg.payloadに物体名、変数msg.annotatedInputに画像認識結果付き画像のバイナリデータを格納する仕様になっています。そのため、画像の表示に用いるimage previewノードはダブルクリックをして、ノードプロパティ設定を変更する必要があります。
image previewノードのプロパティ設定
image previewノードはデフォルトで変数msg.payloadに格納された画像データを表示します。ここでは、このデフォルト変数をmsg.annotatedInputに変更します。
injectノードのプロパティ設定
1分毎にフローを定期実行するため、injectノードのプロパティも変更します。繰り返しのプルダウンメニューで、「指定した時間間隔」を選択し、時間間隔として「1分」を設定します。また、デプロイボタンを押した後、すぐに定期実行処理を開始したいため、「Node-REDの起動後、0.1秒後、以下を行う」の左側のチェックボックスをオンにします。
画像認識を行うフローを実行
デプロイボタンを押すと、すぐにフローの処理が実行されます。監視カメラに人物(著者本人)が写ると、右側のデバッグタブに"person"という画像認識結果が表示されます。またimage previewノードの下には、オレンジ色の四角でアノテーション付けされた画像が表示されます。
(3)監視カメラに人物が写った時に、メールを送信するフロー
最後に、画像認識結果の物体名が"person"だった時に、アノテーション付き画像をメールで送付するフローを作成します。cocossdノードの後続のノードとして、条件判定を行うswitchノード、値の代入を行うchangeノード、メールを送信するsendgridノードを配置し、それぞれのノードをワイヤーで接続します。
その後、各ノードのプロパティ設定を変更します。
switchノードのノードプロパティ設定
msg.payloadに"person"という文字列を含む場合のみ、後続のフローが実行されるように設定します。それを実現するには、条件「==」の比較文字列(azの右側)に"person"を入力します。
changeノードのノードプロパティ設定
認識結果付き画像をメールに添付するため、変数msg.annotatedInputに格納されている画像データを変数msg.payloadへ代入します。まず「対象の値」の右側の「AZ」のプルダウンメニューを開き「msg.」を選択します。その後、右側のテキストエリアに「annotatedInput」と入力します。
「AZ」をクリックすると表示されるプルダウンメニューで「msg.」に変更することを忘れることで、フローが上手く動かないことがよくありますので、「msg.」になっているか再度確認しましょう。
sendgridノードのノードプロパティ
SendGridの管理画面から取得したAPIキー、送信元メールアドレス、送信先メールアドレスを設定します。
最後に、各ノードでどんな処理を行っているか分かりやすくするために、各ノードのノードプロパティを開き、適切な名前を設定しました。
監視カメラに人物が写った時に、メールを送信するフローの動作確認
監視カメラの画像に人物が写ると、前のフローの実行確認の同様に、デバッグタブに画像認識結果が表示され、「認識結果付き画像」のimage previewノードの下の画像にオレンジ色の枠が描画され、人物を正しく認識されていることが分かります。
その後、判定処理や代入処理、メール送信処理が上手く動作すると、スマホにはアノテーション付きの画像ファイルが添付されたメールが届きます。
最後に
今回ご紹介した様なフローを用いて、Raspberry Piに接続したカメラを利用した、自宅の庭の簡易防犯システムを自分で構築することも可能です。また本格的には、ONVIF等のプロトコルに対応したネットワークカメラを用いて取得した画像データに対して画像認識を行うこともできそうです。
- 投稿日:2020-08-14T19:08:33+09:00
リアルタイムで更新するランキング画面を”手抜き”実装してみた
はじめに
ゲームセンターによく「最後にスコアのランキング表示」が出るゲームがあったと思うのですが、
それをちょっとWeb上で実装したいな〜って思ったので実装してみましたただ、単純にランキング表示するだけだとつまらない(?)ので、
WebSocket通信も使って「外部からスコアデータを受信したらランキングをリアルタイムで更新」も作ってみました完成イメージ
新規データがどれかわかりやすいように無駄に色つけてみたりしてますが、まぁそこはお好みでw
コードは GitHub に上げてますにしても、画面リロードなしでしかも別のブラウザから画面いじれるってすごい仕組みだなぁ・・
構成図
わりとシンプルです
ランキング表示の画面はVueでサクっと作ってます。ほぼテンプレそのまま
データ送信は開発者ツールのコンソール上でサクッと。サーバー側はNode.jsでサクサクっとお惣菜や唐揚げが手抜き料理ってなるなら、今回の実装こそまさに手抜き実装
ただこれが言いたかっただけだったり・・・
フロント側の準備
とりあえずvue-cliでファイル群を生成します
vue-cliが入ってない方はまずvue-cliをインストールしてください
インストール方法はググればすぐ出てきます(手抜き)# vueのテンプレ生成 $ vue create sample ## defaultを選択してできあがるまでちょっと待つ # フォルダ移動 $ cd sampleこんな感じのディレクトリ構造になってると思います
. ├── README.md ├── babel.config.js ├── node_modules/ ├── package.json ├── public/ ├── src │ ├── App.vue ⭐修正 │ ├── assets │ ├── components/ │ │ └── HelloWorld.vue ⭐修正 │ └── main.js └── yarn.lock修正するのは
.vueファイルの2つだけApp.vue<template> <div id="app"> <HelloWorld/> ←ここらへんをシンプルに </div> </template> ~ 他は変更箇所ないので省略 ~デフォルトだといらない要素があるので削除します
HelloWorld.vue の修正
今回のメインファイルなのでそこそこ手を加えます。全体は GitHub で確認してください!
HTML、JS、CSSごとに説明します!HTML: ランキングのテーブル表示
ランキング自体は
<table></table>で作ってます
要素はデータ追加に合わせてどんどん増えていくので、<tr></tr>を v-for でスコアデータ分ループさせるようにしてますあとで、このスコアデータの配列にデータを追加する仕組みを作るので、
結果的に「データを追加する → スコアデータの配列が増える → v-for でループする数が増える → 行が増える」となります
ここらへんの動的処理がVueだとめっちゃ楽な気がしますHelloWorld.vue<template> <div class="hello"> <table class="ranking-table"> <thead> <tr> <th>RANK</th> <th>NAME</th> <th>SCORE</th> </tr> </thead> <transition-group tag="tbody"> <tr v-for="(post, index) in allScoreData" :key="post.id"> <td :class="{newRecord: post.isNew}">{{index + 1}} 位</td> <td :class="{newRecord: post.isNew}">{{post.name}}</td> <td :class="{newRecord: post.isNew}">{{post.score}}</td> </tr> </transition-group> </table> </div> </template> 〜〜データを追加した際に、ランキングの行の追加でアニメーションを設定してますが、
こちらにはVueの transition を使っていますtransizion
自分もよくわかっていないですが、Vueでいい感じにアニメーション(トランジション)を設定できるやつっぽいwJS: Websocket通信
JSでは主にWebSocket通信と受信したデータを配列に格納する処理をしてます
HelloWorld.vue〜〜 <script> export default { name: 'HelloWorld', data() { return { allScoreData: [], // {id: Number, name: String, score: Number, isNew: Boolean} } }, mounted() { const socket = new WebSocket('ws://localhost:5001'); socket.onmessage = event => { this.addData(JSON.parse(event.data)); } }, methods: { addData(data) { // 既存のスコアのisNewをfalseにする (= 新規レコードのフラグを外す) const list = this.allScoreData.map(val => { return { id: val.id, name: val.name, score: val.score, isNew: false }; }); list.push({ id: list.length + 1, name: data.name, score: Number(data.score), isNew: true }); // 並び順をスコアの降順に変更 this.allScoreData = list.sort((a,b) => (a.score > b.score ? -1 : 1)) }, } } </script> 〜〜今回、WebSocketではNAMEとSCOREの2種類のデータを扱うので、JSONにしてやりとりしてます
サーバー上で動かすNode.jsは適当なポート(5001)でやってますCSS: アニメーション
transitionの設定項目についてはドキュメントに書いてあります
適当にパラメータの数値をいじるとアニメーションが変化しますHelloWorld.vue〜〜 <style> .ranking-table { margin: auto; width: 50vw; } .newRecord { background-color: #ff7f50; } /* animation */ .v-leave-active, .v-enter-active { transition: opacity .5s, transform .5s; } .v-leave-to, .v-enter { opacity: 0; transform: translateX(200px); } .v-leave, .v-enter-to { opacity: 1; } .v-move { transition: transform .5s; } </style>サーバー側の準備
Node.jsで動かすプログラムもシンプル!
Websocket用ライブラリの ws をつかって、ひたすら待機しながらデータ来たら横に流すだけserver.jsconst ws = require('ws').Server; const wsServer = new ws({ port: 5001 }); wsServer.on('connection', server => { server.on('message', message => { wsServer.clients.forEach(client => { client.send(message); }); }); }); console.log('websocket起動中...');接続するclient(ブラウザ)が複数あるので、forEach を使って接続してる全部のclientにデータを送り返してます
あとはこれはNode server.jsで実行するのみ補足
今回は送られてきたデータをただリアルタイムにランキング表示するだけなのですが、
実際に使おうとすると データを保存したい ってなると思います
(現状ではランキング側のブラウザを再読み込みするとリセットされるので)その場合は
AWSのDynamoWebDBとして扱いやすい kintone とか使うといいですよ!!(宣伝)kintoneの使い方はこちらでまとめてます!!
▼ kintoneの使い方 (データベース編)
https://qiita.com/RyBB/items/daabb9b60d804ee2242fおわりに
さくっと記事書くつもりがなんだかんだ説明が長くなってしまった・・・
今回はWebsocketにローカルのNode.jsサーバー使ってますが、AWSのAPI GatewayもWebSocketに使えるので
「API Gateway + Lambda + DynamoDB」って構成も可能です!(従量課金ですが)ランキング画面は外部ディスプレイとかに表示しっぱなしにしておいて、IoTデバイスとかでデータ追加したらこっちも変わる!とか面白そう
それでは!≧(+・` ཀ・´)≦
- 投稿日:2020-08-14T18:45:36+09:00
ワイニート Google Apps Scriptの神ライブラリ「cheeriogs」でヤフートップをスクレイピングする。GASのスクレイピングはもうこのライブラリ一択やな。
ある日の我が家
ワイ「この記事で神ライブラリの存在に気づいた」
https://qiita.com/mogya/items/dedbbaec39447e74a124#comment-ff558bb5797f4056a374ワイ 「早速この神ライブラリを使ってみるわ」
成果物
ワイ 「ヤフートップのタイトルとリンクを出力しとる」
手順
ワイ 「リソース→ライブラリ」
ワイ 「Add a libraryの所に」
1ReeQ6WO8kKNxoaA_O0XEQ589cIrRvEBA9qcWpNqdOP17i47u6N9M5Xh0ワイ 「バージョンは11にしたわ」
ワイ 「次にコード晒す」function myFunction() { const html = UrlFetchApp.fetch('https://www.yahoo.co.jp').getContentText('UTF-8'); // jqueryチックに使えるように変換 const $ = Cheerio.load(html); const _li = $('main article section ul') .eq(0) .find('li'); // ヤフートップニュースを表示 _li.map(function(i) { console.log(_li.eq(i).text()); console.log( _li .eq(i) .find('a') .attr()['href'] ); console.log(); }); }ワイ 「実行する」
出力
ワイ 「上手くいったで」
ワイのGitHubとか
GitHub: https://github.com/yuzuru2
YouTube: https://www.youtube.com/channel/UCuRrjmWcjASMgl5TqHS02AQ
Qiita: https://qiita.com/yuzuru2
LINE: https://line.me/ti/p/-GXpQkyXAm
Twitter: https://twitter.com/yuzuru_program
成果物まとめ: https://qiita.com/yuzuru2/items/b5a34ad07d38ab1e7378お仕事待ってます^^
- 投稿日:2020-08-14T18:28:27+09:00
自分用メモ Node.js
はじめに
プログラミングを学び始めて日が浅く、javascriptやreactを少しいじったことがある程度ですが、よくNode.jsと言う言葉が出てくるので、少し調べたのでアウトプットしておきます!!
勉強する上でまた作っていく上で直接知らなくても今まで良かったのでスルーしていましたが、どうしてムズムズしていました注意)調べたのですが、わからない単語が出てきたので、それらも一緒にメモしております!大変自己中心的な記事になってます!!
javascriptって
元々javascriptはブラウザで動きをつけるように開発された言語!!
クリック後の挙動やアラート、画像がスライドしたり、タイマーなど、、、しかしjavascriptはブラウザだけでなくサーバーサイドでも動かせる言語になった!!
Node.js
サーバーサイドjavascriptと呼ばれるのだが、その代表的なものがNode.jsになる!!
そのためNode.jsはサーバーサイドjavascript!!
しかしNode.jsは、javascriptをサーバーサイドで動かせてくれるもの(プログラム)ではなく、サーバーサイドでjavascriptを使わせてくれるようにプラットフォームになる!(場所を提供)なぜ
ブラウザ側とサーバーサイドで言語が分かれているとそれら二つの言語を使って開発していかなければならない!!
しかしNode.jsを使うとどちらの言語も同じ言語でかけるのでめちゃくちゃ楽になる!!(効率化)
しかし同じ言語で書けるだけであってサーバーサイドの知識が要らないと言うわけではない!!それほど夢のツールではない!!
強み
リアルタイムWeb
この分野はNode.jsはとても強い。
非同期通信のため(シングルスレッド)リアルタイムアプリケーション
facebook,twitter,instagram,chat,FXトレードなど常にデータベースと接続して更新し、レンダリングするアクセスの多い場所に最適
ここからはこちらを参照!!
JavaやPHPのようなプログラムでは、接続ごとに新しいスレッドが作られる。8GBのRAMで計算すると、最大ユーザー数は5000名以下しかアクセスができなくなる。これを増やそうと思ったら、コンピュータを増やすしかない。
Node.jsはこの問題を解決する数少ないプログラミング言語だ。シングルスレッドで非同期処理を行い、この問題に対応できる。RAM
作業領域を表す言葉
なんらかの処理、画面に何かを表示したりなどの際の作業用のメインメモリ!!これが多いと一度に多くのアプリケーションを利用できる!!ROM
保存領域を表す言葉
スレッドとは
コンピューター連続して命令を出す列!!命令の流れのようなもの1つの
シングルスレッド
1つのスレッドで命令を順に実行していく!!
マルチスレッド
並行処理。プログラムの異なる箇所で個別に行い、スレッドを複数に分けること
だからマルチスレッドになるとスレッドが複数になってRAMを多く使ってしまうと言うことなのかな??
Node.jsはシングルスレッドになるのでアクセスが多い場所に強い!!
だから非同期になって処理が必要なのかな???まとめ
調べる上でわからない言葉が数多く出てきたので主要なものだけメモさせていただきました。そのおかげで大変自己中心的な記事となっております!!
なんとなくこれでフロントやバックエンドなどの違いを構造的に少しイメージしやすくなったと感じております!!
何か他に大事なことがあればぜひぜひアドバイスお願いいたします!!参照
- 投稿日:2020-08-14T18:23:12+09:00
フォームの入力欄を動的に追加する
はじめに
独学でプログラミングを学習中の大学3年生です。
今回が初投稿になります。フォームの値を配列で受け取るときに入力欄を動的に追加したいことがあったので、
調べてまとめてみました。
初学者にも分かりやすいように、できるだけ簡潔なコードになるよう心がけています。CodePen
以下のページで動作を確認できます。
コードも見やすいかもしれません。
https://codepen.io/yuji-207/pen/OJNMYgvコード
form.html<form action="" method=""> <input type="text" name="text[]" class="text"></input> <button type="button" class="btn-clone">clone</button> <button type="button" class="btn-remove">remove</button> </form>バックエンドで配列textとしてデータを受け取れます。
stylesheet.cssinput, button { display: block; } .btn-remove { display: none; }入力欄の削除ボタンはデフォルトで非表示にしておきます。
script.js$(function() { // button var btn_clone = $('.btn-clone'); // 追加ボタン var btn_remove = $('.btn-remove'); // 削除ボタン // clone btn_clone.click(function() { var text = $('.text').last(); // 最後尾にあるinput text .clone() // クローン .val('') // valueもクローンされるので削除する .insertAfter(text); // inputを最後尾に追加 if ($('.text').length >= 2) { $(btn_remove).show(); // inputが2つ以上あるときに削除ボタンを表示 } }); // remove btn_remove.click(function() { $('.text') .last() .remove(); if ($('.text').length < 2) { btn_remove.hide(); // inputが2つ未満のときに削除ボタンを非表示 } }); });入力欄が2つ以上あるときだけ、削除ボタンを表示させます。
ハマったこと
初歩の初歩で少しだけハマりました。
ここでハマる人はあまりいないと思いますが、一応…script.jsvar len = $('.text').lengthこのような入力欄の数を数える変数を1行目とかで宣言してしまうと、
(当たり前ですが)入力欄を増やしたとしても、lenは増えません。参考
参考にさせていただきました。ありがとうございます。
https://qiita.com/SiskAra/items/5f4bc7ee4e598b863add
- 投稿日:2020-08-14T17:57:00+09:00
node.jsを使ってみる
node.jsをインストールしてみる。
参考:https://www.sejuku.net/blog/823221. ダウンロード
- https://nodejs.org/ja/
- https://nodejs.org/dist/v12.18.3/node-v12.18.3.pkg
- node-v12.18.3.pkgを実行してインストール。
実行してみる。
% node --version v12.18.32. Hello, world!
hello.jsconsole.log('Hello, world!')% node hello.js Hello, world!と、無事実行できた。
参考:https://qiita.com/loremipsumjp/items/3d32a44fe80c9a2febbe3. npmを使い、expressをインストールしてみる
参考:https://qiita.com/tarotaro1129/items/e02fbb911dc4af412ad0
npmとは、パッケージ管理システム。gemみたいなもの。
expressは、node.js版の簡易なWebサーバ。WEBrickみたいなもの。% mkdir myapp % cd myapp % npm init
- ここで、10回ほどリターンを押す。package.jsonが作られる。
package name: (myapp) version: (1.0.0) description: entry point: (index.js) test command: git repository: keywords: author: license: (ISC) About to write to /Users/eto/dev/Day17_node/myapp/package.json:package.json{ "name": "myapp", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC" }% npm install express --save - package-lock.jsonが生成される。
- app.jsというファイルを作る。
app.jsconst express = require('express') const app = express() app.get('/', (req, res) => { res.send('Hello World!') }) app.listen(8000, () => console.log('Example app listening on port 8000!'))起動してみる。
% node app.js Example app listening on port 8000!
- http://localhost:8000/ にアクセスしてみる。Webブラウザに、
Hello World!と表示されていたら、成功。
done!
- 投稿日:2020-08-14T17:38:10+09:00
JavaScript アナログ時計
JavaScriptでアナログ時計を作成してみました。
動作サンプル文字盤、各針それぞれにcanvasを振り分けて描画したものを重ね合わせて表示しています。
HTML
index.html<!doctype html> <html lang='ja'> <head> <meta charset='utf-8'> <meta name='viewport' content='user-scalable=no,width=device-width,initial-scale=1' /> <title>Analog Clock</title> </head> <body> <canvas id='face' width='350' height='350'></canvas> <div id='dh'><canvas id='hour' width='200' height='20'></canvas></div> <div id='dm'><canvas id='minute' width='200' height='20'></canvas></div> <div id='ds'><canvas id='second' width='200' height='20'></canvas></div> <script src="./script.js"></script> </body> </html>JavaScript
script.js"use strict"; const canvasHour = document.getElementById('hour'); const ctxHour = canvasHour.getContext('2d'); const canvasMinute = document.getElementById('minute'); const ctxMinute = canvasMinute.getContext('2d'); const canvasSecond = document.getElementById('second'); const ctxSecond = canvasSecond.getContext('2d'); const canvasFace = document.getElementById('face'); const ctxFace = canvasFace.getContext('2d'); canvasHour.style.position = 'absolute'; canvasMinute.style.position = 'absolute'; canvasSecond.style.position = 'absolute'; // 針回転中心の相対位置 canvasHour.style.left = '-8px'; canvasHour.style.top = '-4px'; canvasMinute.style.left = '-12px'; canvasMinute.style.top = '-4px'; canvasSecond.style.left = '-50px'; canvasSecond.style.top = '-4px'; const dh = document.getElementById('dh'); const dm = document.getElementById('dm'); const ds = document.getElementById('ds'); // 針位置 dh.style.position = dm.style.position = ds.style.position = 'fixed'; dh.style.left = dm.style.left = ds.style.left = '160px'; dh.style.top = dm.style.top = ds.style.top = '160px'; // 各針移動 let secondInertia = 0; setInterval(function(){ const n = (Date.now() / 1000 - new Date().getTimezoneOffset() * 60) % 86400; const hour = n / 120; const minute = n / 10; const second = Math.floor(n) * 6; if(Math.abs(secondInertia - second) > 30) secondInertia = second; secondInertia += Math.abs(secondInertia - second) / 3; dh.style.transform = `rotate(${(270 + hour) % 360}deg)`; dm.style.transform = `rotate(${(270 + minute) % 360}deg)`; ds.style.transform = `rotate(${(270 + secondInertia) % 360}deg)`; }, 20); // 時針描画 drawHands({ ctx: ctxHour, draw: [{type: 'cline', color: '#444444', positions:[ 0,4, 6,0, 15,4-3, 85,4-2, 89,4, 85,4+2, 15,4+3, 6,8 ]}], }); // 分針描画 drawHands({ ctx: ctxMinute, draw: [{type: 'cline', color: '#444444', positions:[ 0,4, 10,0, 30,4-2, 138,4-1.5, 145,4, 138,4+1.5, 30,4+2, 10,8 ]}], }); // 秒針描画 drawHands({ ctx: ctxSecond, draw: [{type: 'cline', color: '#bbbb88', positions:[ 0,4, 4,1, 32,1, 36,3, 189,4, 36,5, 32,7, 4,7 ]}], }); // 針描写 function drawHands(params) { const c = params.ctx; const draw = params.draw; for(const d of draw) { if(d.type == 'cline') { c.beginPath(); c.strokeStyle = d.color; c.moveTo(d.positions[0], d.positions[1]); const l = d.positions.length; for(let i = 0; i < l / 2; i++) { c.lineTo(d.positions[(2 + i * 2) % l], d.positions[(3 + i * 2) % l]); } c.stroke(); } } } // 文字盤描画 canvasFace.style.position = 'fixed'; canvasFace.style.top = '0px'; canvasFace.style.left = '0px'; ctxFace.width = 350; ctxFace.height = 350; for(let i = 0; i < 60; i++){ ctxFace.beginPath(); ctxFace.strokeStyle = (i % 5 === 0) ? '#808080' : '#c0c0c0'; const r = (Math.PI * 2) / 60 * i; const c = Math.cos(r); const s = Math.sin(r); const sr = (i % 15 === 0) ? 120 : (i % 5 === 0) ? 130 : 140; ctxFace.moveTo(160 + c * sr, 160 + s * sr); ctxFace.lineTo(160 + c * 150, 160 + s * 150); ctxFace.stroke(); }アナログ時計というとFlash全盛の頃ストップウォッチ機能も持たせたアナログ時計を作ったことがあるのですが、時間に余裕ができたら同じような機能を持たせてみたいです。
- 投稿日:2020-08-14T17:01:09+09:00
【Nuxt.js】Nuxt文法編:created
? この記事はWP専用です
https://wp.me/pc9NHC-z1前置き
ライフサイクルフックの1つcreated()
似ていると言われるmounted()との違いや
使い方を解説していきます??♀️created()
created()とは
API通信を行うものです?
基本はcreated()を使用し、
どうしてもDOMを操作しないといけない時に
mounted()を使います?Vuexでいうならactionsにあたる部分です❣️
Vuexを使用する際は
actionsをcreated()内で
dispatchを使って呼び出すこともできます。実行タイミング
インスタンス作成後、
マウンティング前(DOMが作られる前)
https://jp.vuejs.org/v2/api/#createdつまり
$elプロパティや
getElementByIdなどのDOM操作は使えず、
thisコンテキストは使えます?比較
まずはAPIを使わず
簡単なコードで比較しましょう?? 続きはWPでご覧ください?
https://wp.me/pc9NHC-z1
- 投稿日:2020-08-14T16:54:24+09:00
[JavaScript]動的形付け stringの罠
はじめに
「JavaScriptは、動的型付け言語である」
これは今では広く知られている言葉のひとつです。今回は、動的型付けが意図せず働くところと
その対処法を記載してみます。
※今時そんな使い方しないけど!!!というツッコミはご勘弁を...使用するコード
何の捻りもない、シンプルなものです。
1番目のvalue属性には「10」
2番目のvalue属性には「5」を入れています。
見た目には数値に見えますが、型を調べてみると string になっていますね。Stringで計算しようものなら、他言語であれば瞬時に怒られたりしますが
安心してください。JavaScriptさんは怒りません。test.html<html> <body> <div> 1番目<input type="text" id="calc1" value=10> </div> <div> 2番目<input type="text" id="calc2" value=5> </div> </body> <script> // 1番目の要素を取得 let calc1 = document.getElementById('calc1').value; console.log('1番目の値は:' + calc1); console.log('1番目の型は:' + typeof(calc1)); // 2番目の要素を取得 let calc2 = document.getElementById('calc2').value; console.log('2番目の値は:' + calc2); console.log('2番目の型は:' + typeof(calc2)); // 引き算 console.log(calc1 - calc2); // 割り算 console.log(calc1 / calc2); // 掛け算 console.log(calc1 * calc2); // 足し算 console.log(calc1 + calc2); </script> </html>表示結果
Consoleに表示されている5行目のものは、上から
「10と5の」引き算、割り算、掛け算、足し算 です。
足し算だけ、様子がおかしいですね。そうなってしまう理由
本来、文字(string)の引き算や掛け算は、出来るようになっていません。
ですが、冒頭に記述したようにJavaScriptは動的型付け言語です。
エンジニアの目に見えないところでは以下のような事が起こっています。-掛け算の場合-
JavaScriptさん「いや...えっ...文字の掛け算とか出来ないんですが!?...ハッ!!一瞬数値に変換して計算すればイイのでは??」-足し算の場合-
JavaScriptさん「文字の連結!!」対処法
結論からになりますが「数値に変換する」これだけです。
Number()やparseInt()を利用する事で、数値に変換する事が可能です。test.html<html> <body> <div> 1番目<input type="text" id="calc1" value=10> </div> <div> 2番目<input type="text" id="calc2" value=5> </div> </body> <script> // 1番目の要素を取得(parseInt使用) var calc1 = parseInt(document.getElementById('calc1').value); console.log('1番目の値は:' + calc1); console.log('1番目の型は:' + typeof(calc1)); // 2番目の要素を取得(parseInt使用) var calc2 = parseInt(document.getElementById('calc2').value); console.log('2番目の値は:' + calc2); console.log('2番目の型は:' + typeof(calc2)); // 引き算 console.log(calc1 - calc2); // 割り算 console.log(calc1 / calc2); // 掛け算 console.log(calc1 * calc2); // 足し算 console.log(calc1 + calc2); </script> </html>型がNumberになり、イイ感じに足し算もしてくれていますね。
皆さん、良いエンジニアライフを。
- 投稿日:2020-08-14T15:33:20+09:00
ポートフォリオ 「FOOTBALL SHIRTS」 FWを使用せず素のPHPで制作しました。
初めに
フロントエンドエンジニアを目指してプログラミングを学習しています。
長田と申します。
プログラミング学習のアウトプットとして自作のWebサービス「FOOTBALL SHIRTS」のポートフォリオを制作しました。
この記事では「FOOTBALL SHIRTS」の概要や制作過程について説明します。
ソースコード↓
https://github.com/satoruosada/uniform目的
・フルスクラッチ開発を行うことでWebアプリの基本的な構成、動作を知る.
・自作のWebアプリで同じ初学者の方の役に立つサービスを提供したい.スペック
使用言語 / HTML5/ CSS3 / Javascript / PHP
DBMS / MySQL
開発環境 / MacOS Catalina 10.15.6
バージョン管理 / SourceTree(3.0.15)
主な機能
ユーザー管理機能
・ユーザー登録機能
・ユーザーログイン機能
・ユーザー編集機能
・ユーザー削除機能出品する商品登録管理機能
・商品登録
・商品編集
・商品詳細
・商品一覧(ページネーション)
・商品検索機能
商品詳細機能
・商品詳細表示
・商品へのリンク
・お気に入り機能
・掲示板機能(メッセージ投稿)概要
「FOOTBALL SHIRTS」は、サッカーのユニフォームを出品するWebサービスです。
サッカーのユニフォームマニアが様々な世界中のサッカーのユニフォームを集めたり、転売できる専門のwebサービスを制作してみました。
開発手順
実装させたい主な機能から必要な項目を洗い出し、サンプルとしてExcelに必要なDB情報を書き出していきました。
洗い出した情報を元にテーブルを作成します。AdobeXDデザインカンプ作成
コーディング
デザインカンプを元に画面モックを作成
その後裏側の機能を実装していきますセキュリティ対策
バリデーションチェック
サーバーサイド(PHP)側
☑︎未入力チェック
☑︎最大、最小文字数チェック
☑︎半角英数字チェック
☑︎正規表現を使用したemail型式チェック
☑︎正規表現を使用したURL型式チェック
☑︎同値チェック
それぞれ関数を作成し各フォームで判定を行なっています。
以下一部抜粋です
フロントエンド(HTML)側
なりすまし対策について
セッションハイジャックによるなりすまし対策としてsession_regenerate_id関数を使用しています。
session_regenerate_id関数は、現在のセッションのデータを保持したまま、セッションIDを新しく生成してくれます。パスワードのハッシュ化について
パスワードをDBで登録する際は開発環境から見えてしまうのでセキュリティ上よくありません。
「FOOTBALL SHIRTS」ではpassword_hash関数でパスワードをハッシュ化してDB登録しています。
ログイン時には、
password_verifiを使用し、ハッシュ化されたパスワードを確認しています。
このとき$passはフォームからpostされたパスワード
DBから配列形式で取り出した情報を$resultに詰め
array_shiftを使って先頭から要素を一つ取り出し第二引数としています。SQLインジェクション対策
DB接続時は、プレースホルダーを利用しSQL文を作成。
プリペアードステートメントを使うことでSQLインジェクション対策を行なっています。
XSS(クロスサイトスクリプティング)対策
画面へ文字列や数値を出力する際は、htmlspecialchars関数を使いエスケープ処理を行なっています。
エスケープ処理とは特殊な文字を無害な文字に強制的に置き換える方法です
第二引数のエスケープにはいくつか種類がありますが最もエスケープ文字数の多いENT_QUOTESを使用しています。「FOOTBALL SHIRTS」で出来ること
①「FOOTBALL SHIRTS」へのユーザー登録、ログイン、ログアウト
②ログイン後「FOOTBALL SHIRTS」の商品登録(出品)・編集・マイページ機能
「FOOTBALL SHIRTS」の商品登録ページでは、「FOOTBALL SHIRTS」の商品名、カテゴリー、詳細コメント、商品の画像の登録が可能です。
画像登録にはjQueryを利用しドラック&ドロップでInputされるよう設定しています。
登録完了後はマイページに遷移し、きちんと登録できたことが確認できるようメッセージが表示されます。
マイページでは自身が登録した「FOOTBALL SHIRTS」の商品の閲覧、編集、自身のプロフィール編集、パスワード変更、退会、お気に入り登録した「FOOTBALL SHIRTS」の閲覧が可能です。
③「FOOTBALL SHIRTS」詳細画面に、Ajax処理によるお気に入り登録機能、掲示板機能を実装
お気に入り機能について
「FOOTBALL SHIRTS」の商品詳細画面では、商品画像右上のハートアイコンを押すことで
マイページのお気に入り一覧に商品を登録できます。
こちらはAjaxを用いて実装しています。掲示板機能について
「FOOTBALL SHIRTS」のやり取りについて気軽に質問できるように
掲示板機能を各詳細ページに実装しています。まず掲示板自体が存在するかDB情報を確認。
無ければ新規作成します。
もし既に掲示板情報があればメッセージのみ追加できるよう条件分岐させています。④商品一覧・検索機能
- 投稿日:2020-08-14T15:24:42+09:00
`npm token create --json`をchild_processで実行しつつ、結果のトークンはコンソールに表示しない方法
npm token createをNodeJSのプログラム中から呼びたくて、execだとプロセスが終わるまで帰ってこないので、対話的なプログラムには向かない
帰ってこないconst exec = child.exec("npm token create --json", (err, stdout, stderr) => { console.info({ err, stdout, stderr }) })非同期処理を書きたくないのでspawnSyncを使おうとした。
やりたいことは
- パスワードの入力を求めるメッセージはコンソールに表示したい
- ユーザーが入力するパスワードはコンソールに表示したくない
- 最終結果は(秘密のトークンを含むので)コンソールに表示したくない
である。
とりあえずconst child = require('child_process') const options = { encoding: "utf-8", stdio: "inherit" } const result = child.spawnSync("npm", ["token", "create", "--json"], options); console.info("result", result)こうすると、1と2はクリアできるけど3がだめ。結果が普通に表示される。(そりゃそうだ)
じゃあこういうことでしょ?
const child = require('child_process') const { Writable } = require('stream') class Hook extends Writable { _write(chunk, encoding, callback) { console.log("chunk", chunk.toString()) callback() } } const hook = new Hook(); hook.fd = 1 // これがないとspawnに渡せない const options = { encoding: "utf-8", stdio: ["inherit", hook, "inherit"] } const result = child.spawnSync("npm", ["token", "create", "--json"], options) console.info("result", result)と思ってやってみたが、挙動は変わらない。
hook._writeが呼ばれていない。
ちなみにhook.fdの値をいくつか変えて試してみたが、コンソールに表示されなくなったりnpm ERR! code EPIPEが出たりする。調べていると、spawnはカスタムストリームを受け取れない(バグ)という情報があった。
https://stackoverflow.com/questions/34967278/nodejs-child-process-spawn-custom-stdio
https://github.com/nodejs/node-v0.x-archive/issues/4030これにならってこうすると、
const child = require('child_process') const result = {}; const { Writable } = require('stream') class Hook extends Writable { _write(chunk, encoding, callback) { const str = chunk.toString() try { result = JSON.parse(str) } catch { console.log(str) } callback() } } const hook = new Hook(); const options = { stdio: ["inherit", "pipe", "inherit"] } const process = child.spawn("npm", ["token", "create", "--json"], options) process.stdout.pipe(hook) process.on("close", () => { console.info("result", result) })1と3はクリアできたが2がダメ。
1と3についても、渡ってきたデータがJSON.parseを通るかどうかで分岐させているのでどうもすっきりしない。ここにあるような、
process.stdout.writeを一時的に上書きするアプローチもだめだった。
上書きしたwriteが呼ばれてなさそうな挙動。https://stackoverflow.com/questions/26675055/nodejs-parse-process-stdout-to-a-variable
https://gist.github.com/pguillory/729616結局、stdinもstdoutもpipeしてしまって、状況をひとつひとつハンドリングするこのようなコードになってしまった。
1も2も3もクリアできはしたけど、ぜんぜん納得いってない。const child = require('child_process'); // stdinとstdoutはユーザーコードで処理する。stderrは親プロセスに流す const options = { stdio: ['pipe', 'pipe', 'inherit'] } const proc = child.spawn("npm", ["token", "create", "--json"], options); // デフォルトは`Buffer`(バイト列)なのでutf-8を指定 proc.stdout.setEncoding('utf-8') // パスワード入力を受け付けるためにprompt-syncを使う const prompt = require('prompt-sync')({ sigint: true }) let result = {}; proc.stdout.on('data', (data) => { try { // JSONでパースしてみる result = JSON.parse(data) // できたら結果が出たということなので子プロセスを終了してよい proc.kill() } // パースできなかったらこちらへ catch (e) { // パスワード入力を求められてたら if (data === "npm password: ") { // prompt.hideで入力受け付け const answer = prompt.hide(data) // 入力された文字列をproc.stdinに渡す // 改行コードを送らないと向こうで処理を始めてくれない proc.stdin.write(answer + "\n") } } }); proc.stdout.on('end', () => { console.info("result", result) });node main.js npm password: result { token: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx', cidr_whitelist: [], readonly: false, created: '2020-08-14T06:03:21.551Z' }
そういえば、stdoutをpipeしているのに、
npm password:がコンソールに表示されるのはなんでだ・・・?https://github.com/npm/cli/blob/latest/lib/token.js#L210
https://github.com/npm/cli/blob/latest/lib/utils/read-user-info.js#L41
https://github.com/npm/read/blob/master/lib/read.js#L19こう読み進めていくと、結局
process.stdoutを使っていそうなのだが、child_process内でのprocess.stdoutへの出力がpipeされるのではないのか・・・?ちょっとよくわからないので放置しとく・・・。
- 投稿日:2020-08-14T14:01:38+09:00
Effective AppSync 〜 Serverless Framework を使用した AppSync の実践的な開発方法とテスト戦略 〜
AppSync は AWS が提供するマネージド GraphQL サービスです。Amplify と統合することにより、スキーマさえ宣言すれば GraphQL の Query, Mutation, Subscription コードを自動生成します。バックエンド GraphQL エンドポイントやデータソースを構築し、即座に動く環境が手に入ります。
こちら は過去の記事ですが、リアルタイム掲示板アプリの主要機能を 15 分で作った例を紹介しています。
PoC のように使用する分には Amplify CLI を使用してサクッと開発してしまう方法が効果的ですが、実際のプロダクト開発ではそれだけでは不十分な場合が多いでしょう。複数環境へのデプロイの戦略、テストをどうするか、マイクロサービスバックエンドと接続するにはどのようなパッケージ構成にするべきかなど、課題が山積します。
本記事では AppSync をどのようにテストするべきか、ローカルでどのように開発を行い、CI/CD のフローに乗せていくべきかを考察し、1つの案を提示します。
AppSyncの概要について理解している方は、このあたりから読むとよいかと思います。
なお、サンプルソースは以下のリポジトリにホストしています。Serverless Framework の template として公開していますので、以下コマンドで作成ください。
$ serverless create \ --template-url https://github.com/daisuke-awaji/serverless-appsync-offline-typescript-template \ --path myServiceAppSync とは
AppSync は AWS が提供するマネージド GraphQL サービスです。AWS の各種バックエンド(DynamoDB, Aurora, Elasticsearch, Lambda など)とシームレスに結合ができ、すばやく API バックエンドを構築できます。また、Subscription という機能により、複数クライアントが同時編集できる Web アプリケーションや、リアルタイムなチャットアプリを簡単に実現できます。
なぜ GraphQL が求められるのか
バックエンドに REST API、フロントエンドに React(または Vue.js や Angular)といった構成は一般的でしょう。バックエンドとフロントエンドは Rest の API 定義を互いに共有し、開発を進めます。有名なツールとして OpenAPI があります。OpenAPI による定義を介することで、私たちは以下のような恩恵を得ることができました。
OpenAPI による恩恵
- API 定義を YAML 形式のファイルで管理する
- HTML 形式の API ドキュメントを自動生成する
- API 定義をコミュニケーションのハブとし、フロントエンドとバックエンドの開発体制を分離する
- OpenAPI 定義からフロントエンドで使用する API コール用のコードを自動生成する
- バックエンドのバリデーションは OpenAPI 定義から自動生成する
- バックエンドのモックサーバを自動構築する
コードを自動生成するツールは、以下の openapi-generator が有名です。
REST API 開発の課題
OpenAPI とそのエコシステムの登場によって、REST API 開発は随分と楽になりましたが、依然として課題は残り続けるものです。REST API 開発における課題には以下のようなものが挙げられます。
- API の叩き方が決まっていない(API によって自由に決められてしまう)
- 1ページ表示するために、いくつも API を実行しなければいけない。
- 不要なフィールドまで API で取得してしまう。
REST という思想は素晴らしいですが、リソースベースにアプリケーションを作成していくとどうしてもこのような課題が発生します。フロントエンドは必要なデータを必要なだけ1クエリで取得したい。バックエンドは必要なデータを素早く漏れなく提供したいのです。
GraphQL が解決すること
REST API 開発の課題を解決するために GraphQL は誕生しました。
ちょうど Web 業界全体がモバイルにシフトしていた頃の話です。2012 年 2 月に、Facebook のエンジニアが GraphQL の最初のプロトタイプをチームに共有した時には最高にクールな瞬間だったでしょう。GraphQL: The Documentary というドキュメンタリーが製作されています。
GraphQL は以下のような特徴を持っており、REST API 開発の課題を解決します。
- スキーマとよばれる API の型定義により、フロントエンドとバックエンドがコミュニケーションをとる。
- クライアントは /graphql という単一のエンドポイントにアクセスすることでクリーンなインタフェースを保つ。
- クライアントが指定したフィールドだけを取得する。
- サブスクリプションを使用してリアルタイム処理を行う。
GraphQL を使用することでフロントエンドは必要なデータを必要なだけ1クエリで取得できるようになります。例えば以下の例では、SNS アプリケーションにおいて、ユーザと友達の情報を取得する場合のリクエストを表しています。
REST の場合、ユーザ1件取得の API(/uesrs/:userId)と友達一覧取得の API(/users/:userId/friends)の2つを呼び出しています。レスポンスには画面に表示する必要のない不必要なフィールドも含まれるのでしょう。一方で GraphQL の場合は必要なフィールドをネスト構造で指定し、1リクエストで画面表示に必要な情報を取得しています。
AppSync の役割
GraphQL は優れたソリューションですが、自前で構築するにはいくらかの苦労を伴います。
- 認証、認可処理を記述する。
- 各種バックエンドデータソースにアクセスするリゾルバーを作成する。
- WebSocket を使用するため、GraphQL サーバがスケールしても問題ないように、Redis などのデータストアを用意する。
- グローバルにエラーハンドリングして Node アプリケーションが落ちないように配慮する。
- ログを出力する。
- etc...(その他の数えきれない苦労)
AppSync は上記の苦労を限りなく少なくできます。例えば、認証・認可処理は Cognito UserPool と連携できますし、サーバのスケーラビリティは意識する必要がありません。ログ出力も標準搭載されており、INFO や ERROR などのレベルに応じて出力する設定ができます(もちろん CloudWatchLogs に出力されます)
AppSync の基本
AppSync の構成要素は大きく分けて、以下の3つです。
- Schema
- DataSource
- Resolver
GraphQL の型は Schema として宣言します。Resolver は GraphQL リクエストを Resolver リクエストに変換します。この Resolver に実質的なロジックが集約することになります。Resolver リクエストは、DataSource にアクセスし、データをクライアントに返却します。
一部、公式ドキュメントより抜粋しています。
Schema / スキーマ
各 GraphQL API は単一の GraphQL スキーマで定義されます。以下はスキーマの例です。
type Task { id: ID! name: String! status: String! } type Query { getTask(id: ID!, status: String!): Task }DataSource / データソース
データソースは、GraphQL API で操作できる AWS アカウント内のリソースです。AWS AppSync は、以下をデータソースとしてサポートしています。
- AWS Lambda
- Amazon DynamoDB
- リレーショナルデータベース (Amazon Aurora Serverless)
- Amazon Elasticsearch Service
- HTTP エンドポイント
AWS AppSync API は、複数のデータソースを操作するよう設定できます。これにより、単一の場所にデータを集約できます。
Resolver / リゾルバー
GraphQL リゾルバーは、タイプのスキーマのフィールドをデータソースに接続します。リゾルバーはリクエストを実行するメカニズムです。
AWS AppSync のリゾルバーは、Apache Velocity Template Language (VTL) で記述されたマッピングテンプレートを使用して、GraphQL 表現をデータソースで使用できる形式に変換します。
AppSync を構築するいくつかの方法
さて、そろそろ本題に移ります。AppSync を構築するためにはいくつかの方法があるでしょう。
方法 メリット デメリット AWS コンソール画面から作成 直感的なインタフェースで作成でき、簡単に検証できる 複数の環境に全く同じ構成で構築しづらい
デリバリーを自動化できないAWS CLI デリバリーを自動化できる 冪等性を持たないので、エラー発生時にリカバリが困難 AWS CloudFormation デリバリーを自動化できる
冪等性があり、エラー発生時に元の状態にもどるスキーマ定義ファイルを S3 に配置するなどの手間が多い
YAML のコード量が多すぎるAmplify CLI デリバリーを自動化できる
Cloudformation のコードを自動生成、即座に環境構築ができる
冪等性があり、エラー発生時に元の状態にもどるamplify コマンドが抽象化しすぎていて、何かあった時に解決が困難
Amplify CLI の学習コスト(かなり独特な使い心地)
ローカルでテストできるツールセットが少ないServerless Framework デリバリーを自動化できる
適度に抽象化しており、細部まで定義しようと思えば全てを記述できる
冪等性があり、エラー発生時に元の状態にもどる
ローカルでテストできるツールセットが豊富Serverless Framework の学習コスト(そんなにない)
それぞれ、開発するプロダクトの特性に合わせて選定すべきです。たとえば PoC 的にプロトタイプをすぐに作りたいのなら AWS コンソール画面から作成 するか Amplify CLI を使用する方法が良いでしょう。 プロダクションでの使用を見据えてテスト環境やステージング複数など、複数の環境にデプロイする必要があるのなら AWS CloudFormation か Serverless Framework の使用をお勧めします。
本記事では、AppSync をローカルでテストする方法を紹介するために、Serverless Framework を使用した開発方法をガイドします。
Serverless Framework for Appsync
Serverless Framework を使用して AppSync を構築するためには以下の2通りの方法があります。
Serverless AppSync Plugin
Serverless Framework のプラグインです。ローカルの schema.graphql や mapping-template を参照し、AppSync をデプロイします。Serverless Components / aws-app-sync
Serverless Components の AppSync コンポーネントです。最小限のコード量で AppSync に加えて、カスタムドメインまでついた意味のある単位のリソース群をデプロイします。本記事では Serverless AppSync Plugin の方法を説明します。基本的な Serverless Framework の使用方法は割愛します。公式のドキュメントか、堀家 さんの記事「Serverless Framework の使い方まとめ」がとてもわかりやすく説明されていますので。そちらをご参照ください。
Serverless AppSync Plugin
Serverless AppSync Plugin を使用して、DynamoDB と Lambda をデータソースとした AppSync を作成していきます。Serverless AppSync Plugin はその他に、Elasticsearch や HTTP データソースもサポートしています。
タスク管理アプリを想定した AppSync を実装します。タスク情報は DynamoDB テーブルにストアされ、各種 CRUD 操作ができます。アプリケーションのバージョンや名前の情報は Lambda が返却する構成にしています。
以下のような しょぼTrello のようなアプリケーションのバックエンドになります。
Serverless プロジェクトを開始する
まずは Serverless Framework の AWS/TypeScript ボイラーテンプレートからプロジェクトのひな形を作ります。
$ serverless create --template aws-nodejs-typescriptlulzneko さんの「Serverless Framework と TypeScript でサーバレス開発事始め
」という記事でとても分かりやすく説明されています。Serverless AppSync Plugin のインストール
プラグインを yarn インストールします。
$ yarn add serverless-appsync-pluginnpm を使用しても良いですが、yarn が推奨されています。
$ npm install serverless-appsync-pluginプラグインの設定
serverless.yml にプラグインを追加します。
serverless.ymlplugins: - serverless-webpack - serverless-appsync-plugin # これを追加custom 配下に appSync の設定を記載します。必要最小限しか説明していないので、詳しくは公式ドキュメントを参照ください。
serverless.ymlcustom: dynamodb: stages: - dev - stg - prod start: port: 8000 inMemory: true appSync: # AppSync API の名前 name: ${opt:stage, self:provider.stage}_taskboard_backend # 認証方式 今回は Cognito を使用する authenticationType: AMAZON_COGNITO_USER_POOLS userPoolConfig: awsRegion: ap-northeast-1 userPoolId: ap-northeast-1_XXXXXXXXX defaultAction: ALLOW # スキーマファイル 複数指定することも可能 schema: schema.graphql # データソース 今回は DynamoDB と Lambda を使用する dataSources: - type: AMAZON_DYNAMODB name: ${opt:stage, self:provider.stage}_task description: タスク管理テーブル config: tableName: { Ref: Table } serviceRoleArn: { Fn::GetAtt: [AppSyncDynamoDBServiceRole, Arn] } region: ap-northeast-1 - type: AWS_LAMBDA name: ${opt:stage, self:provider.stage}_appInfo description: "Lambda DataSource for appInfo" config: functionName: appInfo iamRoleStatements: - Effect: "Allow" Action: - "lambda:invokeFunction" Resource: - "*" # マッピングテンプレートファイルを格納しているディレクトリ mappingTemplatesLocation: mapping-templates mappingTemplates: # アプリケーションの情報を取得する - dataSource: ${opt:stage, self:provider.stage}_appInfo # dataSources で定義したデータソース名を指定 type: Query field: appInfo request: Query.appInfo.request.vtl response: Query.appInfo.response.vtl # タスク情報を1件取得する - type: Query field: getTask kind: PIPELINE # AppSync の関数を使ってパイプラインリゾルバを使う場合 request: "start.vtl" response: "end.vtl" functions: - getTask # functionConfigurations で定義した関数名を指定 # タスク情報を複数件取得する - dataSource: ${opt:stage, self:provider.stage}_task type: Query field: listTasks request: "Query.listTasks.request.vtl" response: "Query.listTasks.response.vtl" # タスク情報を作成する - dataSource: ${opt:stage, self:provider.stage}_task type: Mutation field: createTask request: "Mutation.createTask.request.vtl" response: "end.vtl" # タスク情報を更新する - dataSource: ${opt:stage, self:provider.stage}_task type: Mutation field: updateTask request: "Mutation.updateTask.request.vtl" response: "end.vtl" # タスク情報を削除する - dataSource: ${opt:stage, self:provider.stage}_task type: Mutation field: deleteTask request: "Mutation.deleteTask.request.vtl" response: "end.vtl" # AppSync の関数 functionConfigurations: - dataSource: ${opt:stage, self:provider.stage}_task name: "getTask" request: "getTask.request.vtl" response: "getTask.response.vtl" # Lambda Function は通常の Serverless Framework の使い方と一緒 functions: appInfo: handler: src/functions/handler.appInfo name: ${opt:stage, self:provider.stage}_appInfo resources: Resources: Table: Type: AWS::DynamoDB::Table Properties: TableName: ${opt:stage, self:provider.stage}_task AttributeDefinitions: - AttributeName: id AttributeType: S - AttributeName: status AttributeType: S KeySchema: - AttributeName: id KeyType: HASH - AttributeName: status KeyType: RANGE ProvisionedThroughput: ReadCapacityUnits: 5 WriteCapacityUnits: 5 # AppSync が DynamoDB を操作できるロール AppSyncDynamoDBServiceRole: Type: "AWS::IAM::Role" Properties: RoleName: ${opt:stage, self:provider.stage}-appsync-dynamodb-role AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Principal: Service: - "appsync.amazonaws.com" Action: - "sts:AssumeRole" Policies: - PolicyName: "dynamo-policy" PolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Action: - "dynamodb:Query" - "dynamodb:BatchWriteItem" - "dynamodb:GetItem" - "dynamodb:DeleteItem" - "dynamodb:PutItem" - "dynamodb:Scan" - "dynamodb:UpdateItem" Resource: - "*"schema.graphql および、mapping-templates は以下のとおりです(コード量が多いので折りたたんでいます)。サンプルソース全体は こちらの GitHub リポジトリを参照ください。
schema.graphql と mapping-templates 配下の VTL ファイル
schema.graphqltype AppInfo { name: String! version: String! } type Task { id: ID! name: String! status: String! } type Query { appInfo: AppInfo getTask(id: ID!, status: String!): Task listTasks( filter: ModelTaskFilterInput limit: Int nextToken: String ): ListTasks } input ModelTaskFilterInput { id: ModelIDInput name: ModelStringInput and: [ModelTaskFilterInput] or: [ModelTaskFilterInput] not: ModelTaskFilterInput } type ListTasks { tasks: [Task] nextToken: String } type Mutation { createTask(input: CreateTaskInput!, condition: ModelTaskConditionInput): Task updateTask(input: UpdateTaskInput!, condition: ModelTaskConditionInput): Task deleteTask(input: DeleteTaskInput!, condition: ModelTaskConditionInput): Task } input CreateTaskInput { id: ID name: String! status: String! } input UpdateTaskInput { id: ID! name: String status: String } input DeleteTaskInput { id: ID! status: String! } input ModelTaskConditionInput { name: ModelStringInput and: [ModelTaskConditionInput] or: [ModelTaskConditionInput] not: ModelTaskConditionInput } # 以下、AppSyncとDynamoDBで使用可能な GraphQL Schema の共通定義 input ModelIDInput { ne: ID eq: ID le: ID lt: ID ge: ID gt: ID contains: ID notContains: ID between: [ID] beginsWith: ID attributeExists: Boolean attributeType: ModelAttributeTypes size: ModelSizeInput } enum ModelAttributeTypes { binary binarySet bool list map number numberSet string stringSet _null } input ModelBooleanInput { ne: Boolean eq: Boolean attributeExists: Boolean attributeType: ModelAttributeTypes } input ModelFloatInput { ne: Float eq: Float le: Float lt: Float ge: Float gt: Float between: [Float] attributeExists: Boolean attributeType: ModelAttributeTypes } input ModelIntInput { ne: Int eq: Int le: Int lt: Int ge: Int gt: Int between: [Int] attributeExists: Boolean attributeType: ModelAttributeTypes } input ModelSizeInput { ne: Int eq: Int le: Int lt: Int ge: Int gt: Int between: [Int] } enum ModelSortDirection { ASC DESC } input ModelStringInput { ne: String eq: String le: String lt: String ge: String gt: String contains: String notContains: String between: [String] beginsWith: String attributeExists: Boolean attributeType: ModelAttributeTypes size: ModelSizeInput }getTask.request.vtl{ "version": "2018-05-29", "operation": "GetItem", "key": { "id": $util.dynamodb.toDynamoDBJson($ctx.args.id), "status": $util.dynamodb.toDynamoDBJson($context.args.status) } }getTask.response.vtl$util.toJson($ctx.result)
デプロイ
あとはいつも通り、
serverless deployコマンドでリソースを構築します。$ serverless deployAppSync をテストする
Serverless Framework を使用して AppSync をデプロイできました。次はテストです。もちろん、AWS 上にデプロイした AppSync に対して Query を実行したり、Mutation を実行してデータを登録しても良いでしょう。ただしその場合、チーム開発は非常に難しくなります。データを共有しなければいけなくなりますし、そもそもユニットテストのようなものは書けません。Web 開発において私たちはこの課題をどう解決するかを知っています。ローカルで AppSync, DynamoDB, Lambda を起動し、データを登録し、API を実行してデータを取得 できる環境を用意すれば良いのです。
Serverless AppSync Simulator
Amplify CLI はローカルで AppSync を起動するシミュレータを提供しています。
新機能 – Amplify CLI を使用したローカルモックとテスト
$ amplify mock apiというコマンドを実行すると、ローカル環境に AppSync の API エンドポイント(シミュレータ)と、GrapiQl が起動します。
この機能は内部的には amplify-appsync-simulator
というパッケージを使用しています。これをラップする形で serverless-appsync-simulator という Serverless Framework のプラグインが公開されているので、こちらを使用します。使用方法
DynamoDB リゾルバーを使用する場合、DynamoDB をローカルで立ち上げるので、
serverless-dynamodb-localプラグインも必要になります。
serverless.yml には以下のように記述しましょう。serverless.ymlplugins: - serverless-webpack - serverless-appsync-plugin - serverless-dynamodb-local - serverless-appsync-simulator # serverless-offline よりも上に記述する必要があります - serverless-offlineLambda Resolver のために、Lambda Function を実装します。Webpack を使用して nodejs あるいは TypeScript のソースをコンパイルする場合、 ビルド済みのファイルが ./webpack/service に展開されます。以下の設定を忘れないようにしましょう。
serverless.ymlcustom: appsync-simulator: location: ".webpack/service"起動するには以下のコマンドを使用します。package.json に npm-scripts を登録しておくと便利です。
$ sls offline start起動すると、以下のようなログが出力されます。
... Serverless: AppSync endpoint: http://localhost:20002/graphql Serverless: GraphiQl: http://localhost:20002 ...早速ローカルで起動した GrapiQl の画面を開いてみましょう。1回のリクエストで複数のリゾルバーが起動し、レスポンスを返却していることがわかります。
GraphQL リクエストをテストする
ローカルで AppSync のシミュレータを起動できたので、テストを記述していきます。テストのアプローチの方法は至極単純です。
- DynamoDB にデータを用意し
- GraphQL リクエストを実行すると
- 想定通りの結果が取得できること
を確認します。
GraphQL リクエストのテスト領域
テストの対象を図示すると以下のようになります。GrqphQL のリクエストとレスポンスをテストします。これはインテグレーションテストに相当します。
DynamoDB Local の seed データ
serverless-dynamodb-local には seed データを作成する機能があります。この機能を利用してもいいですね。
以下のように serverless.yml に追記します。
serverless.ymlcustom: dynamodb: stages: - dev start: port: 8000 inMemory: true migrate: true # DynamoDB Local 起動時にテーブルを作成する seed: true # DynamoDB Local 起動時にシードデータを挿入する seed: dev: sources: - table: ${self:provider.stage}_task # dev_task というテーブル名を想定している sources: [./migrations/tasks.json]
migrations/tasks.jsonには以下のように記述しておくことで、DynamoDB Local が起動した際にデータが自動的に挿入されます。migrations/tasks.json[ { "id": "1", "name": "掃除をする", "status": "NoStatus" }, { "id": "2", "name": "選択をする", "status": "InProgress" }, { "id": "3", "name": "宿題をする", "status": "Done" } ]Jest を使用してテストを行う
Jest を使用してテストを記述します。Cognito UserPool を使用している場合、リクエスト時に Authorization ヘッダーが必要です。ローカルで動かす際には形式さえ合っていればなんでも良いようです。API キーを必要としている場合は
x-api-keyというヘッダーを指定する必要があります。graphql-operation.test.tsimport { GraphQLClient, gql } from "graphql-request"; import { getTask } from "./query"; import { createTask, deleteTask } from "./mutation"; const client = new GraphQLClient("http://localhost:20002/graphql", { headers: { // format さえ合っていればなんでもいいようです Authorization: "euJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI3ZDhjYTUyOC00OTMxLTQyNTQtOTI3My1lYTVlZTg1M2YyNzEiLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiaXNzIjoiaHR0cHM6Ly9jb2duaXRvLWlkcC51cy1lYXN0LTEuYW1hem9uYXdzLmNvbS91cy1lYXN0LTFfZmFrZSIsInBob25lX251bWJlcl92ZXJpZmllZCI6dHJ1ZSwiY29nbml0bzp1c2VybmFtZSI6InVzZXIxIiwiYXVkIjoiMmhpZmEwOTZiM2EyNG12bTNwaHNrdWFxaTMiLCJldmVudF9pZCI6ImIxMmEzZTJmLTdhMzYtNDkzYy04NWIzLTIwZDgxOGJkNzhhMSIsInRva2VuX3VzZSI6ImlkIiwiYXV0aF90aW1lIjoxOTc0MjY0NDEyLCJwaG9uZV9udW1iZXIiOiIrMTIwNjIwNjIwMTYiLCJleHAiOjE1OTY5NDE2MjkwLCJpYXQiOjE1NjQyNjQ0MTMsImVtYWlsIjoidXNlckBkb21haW4uY29tIn0.mKvvVDRN07IvChh1uHloKz5NdUe2bRu6fyPOpzVbE_M", }, }); describe("dynamodb resolver", () => { test("createTask / getTask by id and status / deleteTask", async () => { const valiables = { id: "123456789", name: "新しいタスク", status: "NoStatus", }; // 新規にデータを作成する const created = await client.request(createTask, valiables); expect(created).toStrictEqual({ createTask: valiables }); // 作成したデータが取得できる const got = await client.request(getTask, { id: valiables.id, status: valiables.status, }); expect(got.getTask).toEqual(valiables); // データが削除できる const deleted = await client.request(deleteTask, { id: valiables.id, status: valiables.status, }); expect(deleted).toStrictEqual({ deleteTask: valiables }); }); });ちなみに、データ作成・削除のミューテーションおよび、タスクデータを取得するクエリは以下のように作成しています。フロントエンドで使用できるように、クエリ、ミューテーション、サブスクリプションのソースはライブラリとして開発しておくとベターでしょう。
mutation.tsimport { gql } from "graphql-request"; export const createTask = gql` mutation create($id: ID!, $name: String!, $status: String!) { createTask(input: { id: $id, name: $name, status: $status }) { id name status } } `; export const deleteTask = gql` mutation delete($id: ID!, $status: String!) { deleteTask(input: { id: $id, status: $status }) { id name status } } `;query.tsimport { gql } from "graphql-request"; export const getTask = gql` query getTask($id: ID!, $status: String!) { getTask(id: $id, status: $status) { id name status } } `;VTL をテストする
AppSync は各リゾルバーにリクエスト処理を流す際に VTL(Velocity Template Language) を使用してクライアントからの GraphQL リクエストを、データソースへのリクエストに変換します。
認可処理(Cognito の UserGroup が Admin だったら実行できるようにするなど)、入力パラメータのバリデーション、パラメータの変換処理(大文字にするなど)など様々なビジネスロジックを VTL に記述することになります。この VTL ファイルが AppSync のサーバサイドの機能(関数)という位置付けになります。VTL テストの領域
GraphQL リクエストをテストする方法はインテグレーションテストのレイヤーでした。クライアントからのリクエストとレスポンスを検証するため、複雑な VTL を記述した場合、テストの網羅性を担保することが難しくなります。
VTL 単体でのユニットテストを検討しましょう。
テストの対象を図示すると以下のようになります。GrqphQL のリクエストとそれによって生成されるリゾルバーリクエストを検証します。VTL のテストのためにヘルパー関数を作成する
VTL によって生成されるリゾルバーリクエストを検証するために、以下のようなヘルパー関数を用意しておきます。
amplify-velocity-template本体が提供しているCompileなどを使用します。vtl.helper.tsimport * as fs from "fs"; import * as path from "path"; import { Compile, parse } from "amplify-velocity-template"; import { map } from "amplify-appsync-simulator/lib/velocity/value-mapper/mapper"; import * as utils from "amplify-appsync-simulator/lib/velocity/util"; /** * VTLファイル内で展開される context を作成する */ const createVtlContext = <T>(args: T) => { const util = utils.create([], new Date(Date.now()), Object()); const context = { args, arguments: args, }; return { util, utils: util, ctx: context, context, }; }; /** * 指定パスのファイルを参照し、入力パラメータをもとに、vtlファイルによりマッピングされたリゾルバリクエストJSONをロードする */ const vtlLoader = (filePath: string, args: any) => { const vtlPath = path.resolve(__dirname, filePath); const vtl = parse(fs.readFileSync(vtlPath, { encoding: "utf8" })); const compiler = new Compile(vtl, { valueMapper: map, escape: false }); const context = createVtlContext(args); const result = JSON.parse(compiler.render(context)); return result; };VTL テストを実行する
まずはシンプルな
getTask.request.vtlというファイルをテストする方法を考えます。このファイルは入力値を元に DynamoDB に対してidとstatusという一意なキー検索を行うリクエストを発行します(id はプライマリーキー、status はソートキー)getTask.request.vtl{ "version": "2018-05-29", "operation": "GetItem", "key": { "id": $util.dynamodb.toDynamoDBJson($ctx.args.id), "status": $util.dynamodb.toDynamoDBJson($context.args.status) } }
getTask.request.vtlのテストです。GraphQL リクエストの引数を args として与え、生成されるリゾルバーリクエストの JSON を検証しています。getTask.resolver.test.tstest("getTask.request.vtl", () => { const args = { id: "000", status: "InProgress", }; const result = vtlLoader("../mapping-templates/getTask.request.vtl", args); expect(result).toStrictEqual({ version: "2018-05-29", operation: "GetItem", key: { id: { S: "000" }, status: { S: "InProgress" }, }, }); });ID 指定で1件取得するリゾルバーはシンプルすぎますね。次に
Mutation.createTask.req.vtlをみていきましょう。これは指定したパラメータをもとに、1件データを登録する処理です。Mutation.createTask.req.vtl## [Start] Prepare DynamoDB PutItem Request. ** $util.qr($context.args.input.put("createdAt", $util.defaultIfNull($ctx.args.input.createdAt, $util.time.nowISO8601()))) $util.qr($context.args.input.put("updatedAt", $util.defaultIfNull($ctx.args.input.updatedAt, $util.time.nowISO8601()))) $util.qr($context.args.input.put("__typename", "Task")) #set( $condition = { "expression": "attribute_not_exists(#id)", "expressionNames": { "#id": "id" } } ) #if( $context.args.condition ) #set( $condition.expressionValues = {} ) #set( $conditionFilterExpressions = $util.parseJson($util.transform.toDynamoDBConditionExpression($context.args.condition)) ) $util.qr($condition.put("expression", "($condition.expression) AND $conditionFilterExpressions.expression")) $util.qr($condition.expressionNames.putAll($conditionFilterExpressions.expressionNames)) $util.qr($condition.expressionValues.putAll($conditionFilterExpressions.expressionValues)) #end #if( $condition.expressionValues && $condition.expressionValues.size() == 0 ) #set( $condition = { "expression": $condition.expression, "expressionNames": $condition.expressionNames } ) #end { "version": "2017-02-28", "operation": "PutItem", "key": { "id": $util.dynamodb.toDynamoDBJson($util.defaultIfNullOrBlank($ctx.args.input.id, $util.autoId())), "status": $util.dynamodb.toDynamoDBJson($context.args.input.status) }, "attributeValues": $util.dynamodb.toMapValuesJson($context.args.input), "condition": $util.toJson($condition) } ## [End] Prepare DynamoDB PutItem Request. **複雑になってきました。入力として
condition(条件)を与えていたり、DynamoDB に登録する前処理としてcreatedAtやupdatedAtなどのフィールドを追加しています。このような複雑なロジックが組み込まれた VTL を単体でユニットテストできるのは非常に効果的です。以下テストコードで、
createdAtやupdatedAtなどのフィールドが正常に追加されているか確認します。※ 現在時刻が設定されるのでexpect.anything()を使用して曖昧な判定にしています。createTask.resolver.test.tstest("Mutation.createTask.req.vtl / expect attributeValues: createdAt, updateAt etc...", () => { const args = { input: { id: "001", name: "study", status: "InProgress", }, }; const result = vtlLoader("../mapping-templates/Mutation.createTask.req.vtl", args); expect(result).toEqual({ version: "2017-02-28", operation: "PutItem", key: { id: { S: "001" }, status: { S: "InProgress" }, }, attributeValues: { __typename: { S: "Task", }, createdAt: { S: expect.anything(), }, id: { S: "001", }, name: { S: "study", }, status: { S: "InProgress", }, updatedAt: { S: expect.anything(), }, }, condition: { expression: "attribute_not_exists(#id)", expressionNames: { "#id": "id", }, }, });CircleCI を使用してテストする
これまで実装してきたことを CircleCI 上で実行します。
CircleCI の最大の魅力である orbs を使いましょう。serverless-framework-orb が使用できます。ただし、今回使用する DynamoDB Local は Java 製なので、ちょっと工夫が必要です。この orbs で使用される Docker イメージには nodejs, Python しか入っていないので Java をインストールしましょう。OpenJDK が入れられれば十分です。
.circleci/config.ymlは以下のようになります。circleci/config.ymlversion: 2.1 orbs: aws-cli: circleci/aws-cli@1.0 serverless: circleci/serverless-framework@1.0 jobs: build: docker: - image: circleci/node:12 steps: - checkout - run: name: install dependencies command: yarn install test: executor: serverless/default steps: - checkout - run: name: apt update command: sudo apt update - run: name: apt install java command: sudo apt install openjdk-8-jdk - run: name: install dependencies command: yarn install - run: name: setup for dynamodb local command: yarn sls:setup - run: name: unit test command: yarn ci workflows: version: 2 build_and_test: jobs: - build - test
package.jsonの npm-scripts はこのように記述しています。package.json"scripts": { "sls:setup": "sls dynamodb install", "start": "sls offline start", "test": "jest", "start-server": "yarn start", "ci": "start-server-and-test start-server http://localhost:20002 test" },AppSync Simulator の起動を待つ必要があるため、
start-server-and-testを使用しています。このライブラリを使用することで、http でリクエストを受け付けるサービスが起動したことを検知して、次の処理を実行できます。この仕組みを利用して、以下の一連の流れを実施します。
- AppSync Simulator を起動する。
- テストを実行する。
- AppSync Simulator を停止する。
デリバリの戦略
今回デリバリする対象の環境は
dev,stg,prodの3つとします。
それぞれ以下のような用途を想定しています。
- dev: テスト環境
- stg: ステージング環境(本番環境と同等のスペック)
- prod: 本番環境
以下のようにステージごとに参照できる変数を定義します。
serverless.ymlcustom: stages: dev: userPoolId: ap-northeast-1_XXXXXXXXX stg: userPoolId: ap-northeast-1_YYYYYYYYY prod: userPoolId: ap-northeast-1_ZZZZZZZZZ以下のように参照して使用できます。
serverless.ymluserPoolId: ${self:custom.stages.${opt:stage}.userPoolId}このようにしておくことで、ステージを指定してことなるパラメータを読み込み、デプロイできるようになります。
$ sls deploy --stage dev複数環境にデプロイできています。実際のユースケースでは、本番環境だけ AWS 環境を分離しておくという方法を採用するかもしれません。その場合は @kudedasumnさんの「CircleCI で複数の AWS アカウントを扱う方法」という記事が大変参考になります。
まとめ
AppSync をローカルで開発し、テストする。また、CircleCI 上でテストを行い、複数の環境にデプロイする方法を説明しました。AppSync をはじめとし、GraphQL はモバイルや IoT など、すべてのデバイスがインターネットに接続し、相互作用する現代のアプリケーションにおいて必須ともいえる技術要素です。これからも GraphQL 関連のエコシステムは継続的にウォッチし、コミットしていきたいですね。
- 投稿日:2020-08-14T13:37:56+09:00
Prettier の ifBreak を現在の group 以外に使う
Prettier の doc ユーティリティに
ifBreakというのがあります。
現在の group が break されてるかどうかに応じて print する doc を変えることができる便利ユーティリティです。
実は、現在の group 以外の group を指定することが可能です。group を作るときに id を付与しておくと
ifBreakでその group を見ることができます。const printed = group(concat(parts), { id: Symbol.for("foo") });const printed = ifBreak("foo", "bar", { groupId: Symbol.for("foo") });最近知った、大変便利。
- 投稿日:2020-08-14T13:15:04+09:00
JavaScriptの制御構文(4)〜for...in命令とfor...of命令〜
はじめに
こんにちは!
今日も備忘録として投稿させて頂きます!
昨日は、繰り返し処理で使うfor命令について投稿しましたが、今日は、連想配列(オブジェクト)や配列などの繰り返し処理を実行する際に使う、for...in命令、for...of命令について投稿します!for…in命令
連想配列(オブジェクト)の要素を取り出して、先頭から繰り返し処理をしていく命令には、for…in命令を使います。
for(仮変数 in 連想配列) { 繰り返す処理 }for…of命令
配列などを順番に列挙し繰り返し処理をしていく命令には、for…of命令を使います。for…of命令は、ES2015で新しく追加された命令です。
配列以外にも、NodeListやイテレータ・ジェネレータなども処理できます。for(仮変数 of 列挙可能なオブジェクト) { 繰り返す処理 }for...in命令とfor...of命令の両方に、共通して見られる、「仮変数」とは、連想配列(オブジェクト)のキーや、配列の要素を一時的に格納する変数のことです。名前はなんでもオッケー!
(ただし、あとで見てもわかりやすいものにすること。)具体例
例)連想配列(for...in命令)
let datas = { apple: 300, banana: 120, watermelon: 500 }; for (data in datas) { console.log(data + '=' + datas[data]); } // 結果 // apple=300 // banana=120 // watermelon=500上記の例では、変数datasに連想配列(オブジェクト)が格納されており、変数dataに、apple・banana・watermelonといったキーが一旦格納されます。
そして、繰り返し処理をする際に、
連想配列全体の変数datas[ キーを格納している変数data ]
によって、要素を順に取り出して処理をしています。
また、配列の時にも使えます!
が、あまり推奨はされていないみたいですね...例)配列など(for...of命令)
let datas = ['apple', 'banana', 'watermelon']; for (data of datas) { console.log(data) } // 結果 // apple // banana // watermelon上記の例では、変数datasに配列が格納されており、変数dataに、apple・banana・watermelonといった、配列の要素が一旦格納されます。
そして、繰り返し処理をする際に、
要素を指定すると、処理が実行されて、consoleには要素の値が順に表示されます。
ただし、連想配列には使えません。このように、連想配列(オブジェクト)や配列に関しては、通常のfor命令ではなく、for…in命令や、for…of命令を利用します。
これまで、いくつか個人開発をしていて思うのは、この文法結構よく使っているなと感じます!
特に、最初に紹介した、for...in命令は、ユーザーによる入力値をリスト表示したりする時など、出てきたりしましたね!今日はここまでです!
理解不足等による、不備等ございましたら、コメントお願い致します!参考文献
山田祥寛著『[改訂新版]JavaScript本格入門 ~モダンスタイルによる基礎から現場での応用まで』(2018)
※第1刷は、2016年。
- 投稿日:2020-08-14T12:26:40+09:00
JavaScript(JS)で変数宣言 書き方を整理してみた
今回はJavaScript(JS)の変数宣言の書き方に関してお伝えしていきたいと思います。
JavaScriptでの宣言のパターンについて、列挙してみました。
好みの問題もあるかと思いますが、チーム内で話し合う際の材料になれば幸いです。
変数宣言の書き方 値代入の場合
パターン1 改行&変数ごとに
letをつけるlet userId = 1; let user = 'kumkum'; let age = 29;パターン2 1行にまとめて書く
let userId = 1, userName = 'kumkum', age = 29;パターン3 改行&末尾カンマでつなげる
let userId = 1, userName = 'kumkum', age = 29;パターン4 改行&先頭にカンマでつなげる
let userId = 1 , userName = 'kumkum' , age = 29;有名フレームワークはどのように書いているのか?
※コードを全てチェックできていないので、他の書き方が存在しているかもしれません。ご了承願います。
Vue.js
// src/compiler/parser/filter-parser.js let inSingle = false let inDouble = false let inTemplateString = false let inRegex = false // test/unit/modules/vdom/modules/dom-props.spec.js const vnode = new VNode('a', { domProps: { src: 'http://localhost/' }}) const elm = patch(null, vnode)Vue.jsはパターン1の書き方をしていますね。
React.js
// packages/scheduler/src/TracingSubscriptions.js let didCatchError = false; let caughtError = null; // scripts/release/utils.js const {exec} = require('child-process-promise'); const {createPatch} = require('diff');React.jsもパターン1の書き方ですね。
変数宣言の書き方 宣言のみの場合
パターン1 改行&変数ごとに
letをつけるlet userId; let userName; let age;パターン2 1行にまとめて書く
let userId, userName, age;パターン3 改行&末尾カンマでつなげる
let userId, userName, age;パターン4 改行&先頭にカンマでつなげる
let userId , userName , age;有名フレームワークはどのように書いているのか?
Vue.js
// src/compiler/parser/index.js let delimiters let transforms let preTransforms let postTransforms // src/compiler/parser/filter-parser.js let c, prev, i, expression, filtersVue.jsはパターン1とパターン2の書き方が混在していました。
ざっと見た感じだと、全文字数が短い場合はパターン2(1行にまとめる)の書き方をしているようです。React.js
// packages/react-devtools-shared/src/__tests__/profilingCharts-test.js let React; let ReactDOM; let Scheduler; let SchedulerTracing; // scripts/babel/__tests__/transform-test-gate-pragma-test.js let shouldPass; let isFocused;React.jsはパターン1の書き方をしていました。
値代入あり、なしに関わらず、全て1行ごとに変数宣言しているようです。参考文献
・現代の JavaScript チュートリアル
https://ja.javascript.info/variables
・Vue.js(Github)
https://github.com/vuejs/vue/search?p=3&q=let&unscoped_q=let
・React.js(Github)
https://github.com/facebook/react/search?p=4&q=let&unscoped_q=let
- 投稿日:2020-08-14T08:55:22+09:00
ブラウザでFelicaLiteSの非暗号化領域を読む
カードリーダー
https://www.sony.co.jp/Products/felica/consumer/products/RC-S380.html
RC-S380/S
アマゾンで買えますデモ
https://yusukem99.github.io/FelicaReader/example/
FelicaReaderのインストール
ライブラリを作りました。
https://github.com/yusukem99/FelicaReader
RC-S380/Sのどちらでも動作確認済みです。各OSの必要な設定
Linux
udevのUSBカードリーダー許可設定
udevの設定変更が必要です。設定後udevを再起動する必要があります。
usernameをユーザ名に変更してください。/etc/udev/rules.d/90-nfc.rulesSUBSYSTEMS=="usb", ATTRS{idVendor}=="054c", ATTRS{idProduct}=="06c1", ACTION=="add", GROUP="username", MODE="660" SUBSYSTEMS=="usb", ATTRS{idVendor}=="054c", ATTRS{idProduct}=="06c3", ACTION=="add", GROUP="username", MODE="660"Windows
ドライバを汎用ドライバに変更する必要があります。
読み取り
const reader = new FelicaReader(); await reader.getDevice(); await reader.deviceInit() let data = await reader.readWithOutEncryption();
- 投稿日:2020-08-14T08:49:49+09:00
カリー化チートシート【記述例一覧版】
拙作記事『カリー化チートシート』について,対応言語や補足追記が増えるにつれ,チートシートというよりは解説記事と化してきたので,あらためて記述例一覧版を作成.しばらくしたら,元記事タイトルを『カリー化記述まとめ』に変えて,こちらを『カリー化チートシート』にするかも.
記述例一覧
複数の記法が可能な言語は,最新バージョン&カリー化メソッド未使用の場合の,最も短い書き方のみを掲載.その他の記法やバージョン等による違いは元記事を参照.
言語 (λxy.(真, if x>y; and 偽, if x≦y)) 10 20 Haskell (\x y -> x > y) 10 20Scheme (((lambda (x) (lambda (y) (> x y))) 10) 20)Python (lambda x: lambda y: x > y)(10)(20)Ruby -> x { -> y { x > y } }[10][20]JavaScript (x => y => x > y)(10)(20)Scala ((x: Int) => (y: Int) => x > y)(10)(20)Perl sub { my $x = shift; return sub { my $y = shift; return $x > $y }; }->(10)->(20)Go言語 func(x int) func(int) bool { return func(y int) bool { return (x > y) } }(10)(20)PHP (fn($x) => fn($y) => $x > $y)(10)(20)Julia (x -> y -> x > y)(10)(20)Emacs Lisp, Common Lisp (funcall (funcall (lambda (x) (lambda (y) (> x y))) 10) 20)R言語 (function(x) { function(y) { x > y } })(10)(20)更新履歴
- 2020-08-14:大きい方の値ではなく真偽値を返す記述例に変更
- 2020-08-14:初版公開(Haskell,Scheme,Python,Ruby,JavaScript,Scala,Perl,Go言語,PHP,Julia,Emacs Lisp/Common Lisp,R言語)
- 投稿日:2020-08-14T08:09:55+09:00
[SeerviceNow] Script Includeに定数を持たせる
きっかけ
Scopedアプリケーションのテーブル名が長くてタイプが面倒。
アプリケーションのプロパティを覚えられない。
ステータスの値が覚えられない。
Studioの中でまとまっているからそこまで探すのは大変ではないけれど、使用頻度が上がるとやっぱり一箇所にまとめたい。
つまり、衰えゆく記憶力や腕力を経験値でカバーしたい。誰でも知ってることを今さら知っただけなのかもしれないけれど共有。
例
あえて冗長に書いていますが例えばこういうコード
before.jsvar defaultValue = 'x_322048_ngmapp_test_table'; var targetTable = gs.getProperty('x_322048_ngmapp.target_table_name', defaultValue); var gr = new GlideRecord(targetTable);やり方
Script IncludeにConstantsを定義する
変更後
after.jsvar gr = new GlideRecord(Constants.TABLE_PROPERTY);繰り返し使う定数はこうするよう心がけようと思う。
- 投稿日:2020-08-14T04:06:37+09:00
(FIDO) WebAuthnによる認証を導入する前に考慮しておきたいこと
近年、WebAuthnによる認証の記事をよく見かけるようになりました。パスワードレス認証を実現する仕組みとして、注目されていますね。
WebAuthnの「認証」と「登録」に関する記事はよく見かけますが、それ以外の「(登録)解除」や「クレデンシャル情報の紛失」と言ったことに関して説明している記事が少なかったので、W3CのWebAuthnに関するドキュメントをちらっと見てみました。WebAuthnについて、投稿時点で参考にした資料は以下です。
https://www.w3.org/TR/2020/WD-webauthn-2-20200730/Decommissioning(解除)について
W3Cのドキュメントに、「Decomissioning(解除)」について記載されています。(Non-normative)
https://www.w3.org/TR/webauthn-2/#sctn-sample-decommissioning上記リンクの記載によると、解除が望まれるケースとして以下が想定されています。
(もちろん、これ以外のケースも、個々のサービスに応じて検討は必要)①ユーザーがクレデンシャルを紛失したことを報告
<処理フロー>
(1)Webサイトにて認証完了後、「端末の紛失・盗難の報告」と言ったページに進む。
(2)サーバは、ユーザーに紐づくクレデンシャル情報を一覧にしたページを表示
(3)ユーザーは一覧から削除したいクレデンシャル情報を選択
(4)選択されたクレデンシャル情報をサーバ側のデータベースから削除
(5)以降、削除したクレデンシャル情報での認証は不可上記手順の(1)では、紛失したクレデンシャル以外で認証を行う手段があることを前提としていると思われます。
例として、以下のようなパターンが挙げられます。
・ユーザーに紐づく公開鍵クレデンシャル情報を、複数個サーバに登録している。
・WebAuthn以外の認証方法でもユーザー認証が行える。②サーバによる不活性なクレデンシャル情報の削除
<処理フロー>
(1)サーバのメンテナンス中に、クレデンシャルを削除。
(2)以降、削除したクレデンシャル情報での認証は不可サーバメンテナンスや定期的に行われるバッチ処理などで、削除条件に該当するクレデンシャル情報を削除するケースですね。
サービスによっては、(後述のように、推奨はされていませんが)ユーザーアカウントとクレデンシャル情報との関連を1対1に制限している場合もあるかもしれません。安易にクレデンシャル情報を削除してしまうと、代替の認証方法がない場合に詰みますので、削除要否も含め検討した方が良さそうです。
個々のサービスの特性や、認証処理の統計情報等を鑑みて、削除対象とする条件を決める必要がありそうです。③ユーザーによるAuthenticatorからのクレデンシャル情報削除
<処理フロー>
(1)Authenticator固有の方法(例:端末の設定画面など)で、ユーザー自らクレデンシャル情報を削除
(2)以降、削除したクレデンシャル情報での認証は不可
※ユーザーが削除したクレデンシャル情報は使用されなくなるので、上記②の運用を行なっている場合、(削除条件により)サーバ側で管理しているクレデンシャル情報も削除される。Credential Loss(クレデンシャル情報の紛失)
W3Cのドキュメントにて、クレデンシャル情報の紛失について触れられています。
WebAuthnでは、クレデンシャル情報のバックアップや、Authenticator間のクレデンシャル情報の共有については定義されていません。
したがって、ユーザーがRPサーバに対して、1つのクレデンシャル情報しか登録していない場合、Authenticator(認証器)の紛失・機種変更などにより、認証手段も失うこととなります。これに対し、W3Cのドキュメントでは、「複数のクレデンシャル情報の登録」と「excludeCredentialとuser.idオプションの活用」について述べています。
複数のクレデンシャル情報の登録
Relying Parties SHOULD allow and encourage users to register multiple credentials to the same account.
(https://www.w3.org/TR/webauthn-2/#sctn-credential-loss-key-mobilityより引用)
「一つのアカウントに対し、複数のクレデンシャル情報を登録することをユーザーに促すべきである」
ということですね。このようにすれば、一つのクレデンシャル情報(Authenticator)を紛失しても、もう一つのクレデンシャル情報を使用して認証することが可能です。
シンプルなテーブル定義としては、「User」テーブルと「Credential」テーブルが1対多の関連となっている形が考えられます。ただ、全てのユーザーが複数のクレデンシャル情報を登録するのは理想的なケースで、一つのクレデンシャル情報しか登録しないユーザーも多いでしょう。
WebAuthn以外の認証方法が存在する既存のWebサービスの場合、従来からの認証方法(パスワード認証など)によるログインも可能とする、と言った措置も考えられるかもしれません。ただし、WebAuthnがパスワードレス認証を実現する仕組みなのに、ここでパスワードのみによる認証を許可してしまうのは本末転倒です。
なので、クレデンシャル情報登録済みのユーザーが、WebAuthn以外で認証を実施した時は、多要素認証や多段階認証を行うことはユーザーの意思に関わらず必須とした方が良いと思われます。また、WebAuthn以外の認証方法を事前に許可するかの設定画面などを設けておき、デフォルトではWebAuthn以外は許可しない、といった運用も考えられるかもしれません。
この場合、1つのアカウントに紐づくクレデンシャル情報が1つだけの場合に詰んでしまうので、カスタマーサポートへの窓口案内等の導線を予め用意しておくなど、事前に検討が必要そうです。excludeCredentialとuser.idオプションの活用
Relying Parties SHOULD make use of the excludeCredentials and user.id options to ensure that these different credentials are bound to different authenticators.
(https://www.w3.org/TR/webauthn-2/#sctn-credential-loss-key-mobilityより引用)
「RPは異なるクレデンシャル情報が、それぞれ異なるAuthenticatorに紐づくことを確かなものとするために、"excludeCredentials"と"user.id"オプションを活用するべきである」
excludeCredentials
excludeCredentialsは、クレデンシャル登録時にRPサーバから返却される情報で、1つのAuthenticatorで同一のアカウントに対するクレデンシャルが複数登録されることを回避するために使用されることを意図したものです。
excludeCredentialsに設定されているクレデンシャルと一致するクレデンシャルがAuthenticatorに既に存在していた時、クライアント(WebブラウザやOSのSDKなど)はエラーを返却することが要求されるものとしています。https://www.w3.org/TR/webauthn-2/#dom-publickeycredentialcreationoptions-excludecredentials
user.id
user.idも、クレデンシャル登録時にRPサーバから返却される情報で、RPサーバに登録されるユーザーを識別するための情報として扱われます。
https://www.w3.org/TR/webauthn-2/#dom-publickeycredentialuserentity-id
https://www.w3.org/TR/webauthn-2/#user-handleexcludeCredentialとuser.idについては実際に実装している記事を見た方が早いと思います。
これらのパラメータにより、「ユーザー」の情報と「クレデンシャル」の情報を1対多の関連として実装するために利用することができます。最後に
WebサービスでWebAuthnを導入するとなると、やはり「(登録)解除」や「クレデンシャル情報の紛失」と言った事項の検討は避けられないと思います。
実際に設計・実装するとなった時に、上記の事項を忘れていると慌てそうなので、やはり事前に考慮しておきたいものです。
- 投稿日:2020-08-14T03:36:57+09:00
RailsでAjaxでいいね機能を実装する方法
「Railsでいいね機能を実装する方法」でいいね機能の実装方法をご紹介しましたが、今回はそのいいね機能をAjax(非同期通信)実装する方法をご紹介いたします。
完成系は以下のような感じです。
環境
- Ruby 2.5.7
- Rails 5.2.4
前提
- この記事によって、いいね機能が実装済みであること
index.html.erbを編集
2つのlink_to(method: :delete と method: post)に remote: trueを追記します。
remote: trueを記載することで、Ajaxでの処理を実行することができます。index.html.erb<div class="container"> <h1>記事一覧</h1> <table class="table"> <% @posts.each do |post| %> <tr> <td><%= post.title %></td> <td> <% if post.liked_by?(current_user) %> <% like = Like.find_by(user_id: current_user.id, post_id: post.id) %> <%= link_to like_path(like), method: :delete, remote: true do %> <span class="glyphicon glyphicon-heart" aria-hidden="true" style="color: red;"> <span><%= post.likes.count %></span> <% end %> <% else %> <%= link_to post_likes_path(post), method: :post, remote: true do %> <span class="glyphicon glyphicon-heart" aria-hidden="true" style="color: gray;"> <span><%= post.likes.count %></span> <% end %> <% end %> </td> </tr> <% end %> </table> </div>いいね機能の部分をテンプレート化する
index.html.erbと同じディレクトリに以下のファイルを作成し、いいね機能の部分をコピーし、貼り付けます。
_like.html.erb<% if post.liked_by?(current_user) %> <% like = Like.find_by(user_id: current_user.id, post_id: post.id) %> <%= link_to like_path(like), method: :delete, remote: true do %> <span class="glyphicon glyphicon-heart" aria-hidden="true" style="color: red;"> <span><%= post.likes.count %></span> <% end %> <% else %> <%= link_to post_likes_path(post), method: :post, remote: true do %> <span class="glyphicon glyphicon-heart" aria-hidden="true" style="color: gray;"> <span><%= post.likes.count %></span> <% end %> <% end %>部分テンプレート(_like.html.erb)を呼び出すため、いいね機能の部分があったところにrenderを記述します。
また、Ajaxの処理がされる部分を識別できるように id を記述します。index.html.erb<div class="container"> <h1>記事一覧</h1> <table class="table"> <% @posts.each do |post| %> <tr> <td><%= post.title %></td> <td id="like-<%= post.id %>"> <!--idで識別できるようにする--> <%= render "like", post: post %> <!--renderで部分テンプレートを呼び出す--> </td> </tr> <% end %> </table> </div>controllerの編集
各アクションの最後にredirect_backをしていましたが、redirect_backをすると再読み込みをしていまい、Ajaxが機能しません。
そのため、redirect_backを削除します。likes_controller.rbdef create like = Like.new(user_id: current_user.id, post_id: params[:post_id]) @post = like.post like.save end def destroy like = Like.find(params[:id]) @post = like.post like.destroy endjsファイルの作成
remote: trueによってjs形式のリクエストを送信しているため、実行するアクション名(createやdestroy)のjsファイルを最終的に探しに行きます。
そのため、app/views/配下にlikesフォルダを作成し、そのフォルダの中に create.js.erb と destory.js.erb を作成します。create.js.erb$("#like-<%= @post.id %>").html("<%= j(render 'posts/like', post: @post) %>");destory.js.erb$("#like-<%= @post.id %>").html("<%= j(render 'posts/like', post: @post) %>");idで識別し部分的にhtmlを書き換えます。
これで完成です。
- 投稿日:2020-08-14T02:07:11+09:00
loadイベントの中でDOMContentLoadedイベントをセットしても動いてくれません
DOMContentLoadedイベントはloadイベントよりも早く動いてくれるので、DOMContentLoadedイベントを使うことで処理のタイミングを早めることができますが、注意があります。
loadイベントの中でDOMContentLoadedイベントをセットしても、動いてくれません。サンプルコード
let log = (message) => (document.getElementById("log").innerHTML += "<br/>" + message); window.addEventListener("load", () => { log("load"); window.addEventListener("load", () => log("load -> load")); window.addEventListener("DOMContentLoaded", () => log("load -> DOMContentLoaded") ); }); window.addEventListener("DOMContentLoaded", () => { log("DOMContentLoaded"); window.addEventListener("load", () => log("DOMContentLoaded -> load")); window.addEventListener("DOMContentLoaded", () => log("DOMContentLoaded -> DOMContentLoaded") ); });実行結果
DOMContentLoaded load DOMContentLoaded -> load
load -> DOMContentLoadedが出力されていないことがわかりますね。(他にもload -> loadとDOMContentLoaded -> DOMContentLoadedも出力されていません)当たり前のことなのですが、loadイベントが動くときにはDOMContentLoadedイベントはもう終わっているので、このタイミングでセットしても動いてくれないのです。
普通に実装しているとこんな書き方はしないのですが、DOMContentLoadedイベントを利用しているライブラリをloadイベント内で実行しようとする時など、気づかずにこの状態になっているかもしれないので注意です。
codepenでのサンプル
See the Pen DOMContentLoaded in load by mizukoshi akiya (@akiyah) on CodePen.
- 投稿日:2020-08-14T02:00:14+09:00
結局JavaScriptにおける変数ってどうしたらいいの?
そもそもJavaScriptの変数って何がどうなってるんだろう?
どうやらJavaScriptには変数の宣言を行う際に三つの方法があるらしいのだ!
その名もvar,let,constである!正直なことをぶっちゃけると私実は脳死でletを利用してました。そんな私自身のことはどうでもよくて〜実際どれがなんなのか、どれを使えばいいのよくわからないと言う人が多いと思います。そこで私の現段階での学習状況のアウトプットと私の主観を述べていこうと思います。よくわかんないからとりあえず比較してみる...
私が調べたところ変数の宣言には3つの方法があり、それぞれ全く異なる特徴を持っています。それをまとめてみようと思います!
var let const 再代入 ○ ○ × 再定義 ○ × × なるほど〜こうみると
varめっちゃ便利そう!constは変数というか定数じゃね!
という所感ですね〜
当時の僕はとりあえずvarの乱用をしてました。
まぁこんなテキトじゃよくないと思うのでちゃんと説明しようと思いますvar
こちらはECMAScript2015以前のバージョンから利用されている変数の宣言方法ですね!
何度でも再代入ができて、再定義ができる何でも屋さん!
百聞は一見に如かず!
とりあえずなんか出力しようぜ〜
ということで誰でも思いつくようなこのコードを…デン!hoge.jsfunction hoge(true) { var a = "GoodMorining!"; console.log(a); // ここではGoodMorning!と表示してほしい if (){ var a = "GoodAfternoon!"; console.log(a); // ここではGoodAfternoon!と表示していい } console.log(a); // ここではGoodMorining!と表示してほしい } hoge();しかし!これでは最後のconsole.logの結果だけうまくいきません!
なんで〜
て、まぁ原因はわかってるんでづけどね〜
先程の比較表絡みて分かる通り、varで宣言した変数は再び再定義することができましたよね!if文の中で変数aが再定義され、またvarによる変数宣言は関数スコープと言われ、関数すべてに適用されてしまいます。つまり、if分の中だけという一つのブロックのみ変数aの値を変えたいという時つまりブロックスコープができません!これは困った〜さらに言えばこれ実は変数名の競合が起きてしまいかねないのです。気付いたら同じ変数名のものがたくさん!なんてことになったらエラー処理が大変なことになってしまいます...
というように一見便利そうに見えるvarですが実は少し使いにくい!これを解消してくれたのがECMAScript2015(ES6)の記法により追加されたlet,constです!let
これは基本的には
varと同じような変数です!しかし、二つ大きな違いがあります。それが再定義の不可、ブロックスコープの二つです。
先ほど作ったプログラムをletで行ってみましょう!hoge2.jsfunction hoge2() { let a = "GoodMorining!"; console.log(a); // ここではGoodMorning!と表示してほしい if (true){ let a = "GoodAfternoon!"; console.log(a); // ここではGoodAfternoon!と表示していい } console.log(a); // ここではGoodMorining!と表示してほしい } hoge2();これで狙い通りの表示ができました〜
やったね!
これがブロックスコープです!
関数ないでaという変数をletによって宣言していますが、これによってまず再定義が不可能になりました。しかし、ここである人は思うかもしれません!あれ、if文のところで再定義してない?
いいところに目をつけますね〜120点をあげましょう!
しかし、思い出してくださいletによる宣言はブロックスコープ、つまりif文という単一のブロックの中ではまだaという関数は存在していません!つまりここで宣言しても何も怒られないのです。そしてif文の中で宣言したaはif文の外では無かったことになります。よって、思惑通りの出力結果になるのです。ちゃんちゃんconst
あれー?
じゃあこのconstっていうのは何?
先程の比較表をみてみましょう、再定義ふかで再代入も不可でしたよね!つまり一度宣言して代入したら2度と変更することができないいわば定数みたいなものです。
序章らへんで僕は脳死でletを使うといいましたが、本来この行為はあまり宜しくはないです。プログラムの中で変数の値を再代入することはほとんどないようです。(実体験と、私の先輩談より)であれば、どこかの表しに間違えて変数に再代入してしまった!な〜んてことが起きないように基本的にはconstを使うことが推奨されるわけです!しかし、それでもどうしても再代入しなければならない時があるかもしれません、そういう時だけletを用いることがお勧めされるのです。では変数についてもう一度まとめましょう
var
なんでもできるが変数名の競合が起きてしまいエラーの原因になることがしばしば、また意図せぬ場所でスコープが起きてしまう。よって非推奨
let
再代入ができるが再定義が不可能であるため変数名の競合をある程度防げるvarよりは安全性が高く使用することがまぁ推奨できるが、しかし意図せず再代入することに注意が必要!
const
再代入、再定義共に不可能で認識としては定数(数学的には)と言った方が理解しやすいかも…
競合も防げて、意図しない再代入も防げるため推奨
var let const 再代入 ○ ○ × 再定義 ○ × × スコープ 関数スコープ ブロックスコープ ブロックスコープ 推奨 非推奨 やむを得ない場合 推奨 さ〜てまとめに入りましょう!
基本的には
constを利用し、プログラム内で止むを得ずどうしても再代入しなければならない時のみletを利用し、varは使わない!ということを心がけましょう!というのが私の主観です...
参考になれば幸いです
それではまた〜ナムナム〜




































































