20200814のJavaScriptに関する記事は28件です。

外気の体感温度を教えてくれるLINEbotをAWS LambdaとSORACOMデバイスで作ってみた。

はじめに

  • 先日SORACOMのデバイスとArduinoを用いて省エネIoT機器を作ったを書いた。

    • ハッカソンでこういうモノ作ったよという記事。
  • 完成したのがハッカソン終了の2日前とかだったので、残り時間でできることをやろうとLINE botを作ってみた。
    IMG_20200814_215613.jpg

  • 話しかけると外の体感温度を教えてくれる。かわいい。アイコンをクール系美少女にしたい。

  • 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を活用できるらしい。

  • デバイスとの連携も夢ではないかも。少なくともボタンのクリックイベントはすぐに読めるようにできそう。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【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.js
const 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/

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

入力フォームの文字をtwitterに共有する方法

完成版

スクリーンショット 2020-08-14 20.34.03.png
入力してボタンを押すと、
スクリーンショット 2020-08-14 20.36.04.png
このようにご自身の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 Bootstrap4

index.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>

スクリーンショット 2020-08-14 21.02.02.png

先程よりtwitterのボタンらしくなりました。

参考

text_areaの内容をtwitterにシェアする https://teratail.com/questions/172448

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

昔発売されていたCASIOのゲーム電卓のデジタルインベーダーをJavaScriptで作成

1980年にカシオ計算機が発売した初代ゲーム電卓のデジタルインベーダーをJavaScriptで再現したものです。
2018年には復刻版も発売されて、ちょっと話題になっていました。

今回作成したものはこちらからプレイできます

スクリプト自体は数年前(2016年頃)に作成したものですが、リプレイ対応や難易度設定の追加など今回少し手直ししました。
作成した当時は表示する敵数字を決定するのに普通にMath.random()を使用していましたが、リプレイ機能を追加するにあたって同じ乱数列を出す必要があったので、今回Xorshiftを使ってシード値を指定することで同じ乱数列を出せるようにしています。

xorshift.js
class 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で元に戻ったようです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

WebAudioAPIとcanvasで音声波形動画を出力するサイトを作る

はじめに

YouTubeなどで見かけるよくあるミュージックビデオに音声波形が動いて表示されたりするのがあります。
そういう動画は調べてみると、WindowsのAviUtlという無料の動画編集ソフトの拡張機能で作られてるようです。
最近では、daniwell氏によりMusic Visualization GeneratorがWindows/Macのフリーソフトがリリースされ、簡単に音声波形動画が作れるようになりました。
そういった中で、Web上でも作れないかと調べてみたところ、どうやらHTML5のWebAPIとJavascriptでできるようなのでやってみました。

実装した機能

・画像読み込み、表示
・音声読み込み
・音声波形表示
・動画として出力

完成図

スクリーンショット 2020-08-14 19.41.42.png

今後の課題

  • webpで動画が出力されるのでmp4で書き出して、ボタンクリックでTwitterに載せれるようにしたい
  • canvasの性質上、モバイルへの対応が難しいので知見を深める
  • フロントで動画読み込みから出力をしているため、書き出し限界がありそう

参考記事

WebAudioAPIについて

使い方は以下を参考にしました。
Web Audio API | Codelab
Web Audio API - Web API | MDN
Web Audio API の基礎

WebAudioAPIで詰まったところは、以下のように仕様書を日本語訳してくれている方がいらっしゃるので辞書代わりに参考にしました。
Web Audio API ( 日本語訳 )

canvasについて

canvas に絵を描く - Web API | MDN

動画保存について

MediaRecorder APIをつかってCanvas/WebAudioなゲーム画面を録画する
JavaScriptでcanvasを録画して動画ファイルに保存する方法

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

WebAudioAPIとcanvasで音声波形動画を出力するサイトを作ってみた

はじめに

YouTubeなどで見かけるよくあるミュージックビデオに音声波形が動いて表示されたりするのがあります。
そういう動画は調べてみると、WindowsのAviUtlという無料の動画編集ソフトの拡張機能で作られてるようです。
最近では、daniwell氏によりMusic Visualization GeneratorがWindows/Macのフリーソフトがリリースされ、簡単に音声波形動画が作れるようになりました。
そういった中で、Web上でも作って共有できたら便利だなと思い、調べてみたところ、どうやらHTML5のWebAPIとJavascriptでできるようなのでやってみました。

作ったもの

Music Waves Visualizer

スクリーンショット 2020-08-14 19.41.42.png

コード

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 ( 日本語訳 )

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

監視カメラを用いたメールアラートシステムを開発してみる

日立製作所OSSソリューションセンタの横井です。

 前回の記事では、TensorFlow.jsを用いた画像認識のフローの開発手順を紹介しました。今回は応用として、監視カメラと画像認識を連携させたメールアラートシステムを開発してみましょう。以下の図の様に、監視カメラの画像に不審者が写った際、自動でメールでアラートを送るフローを作成します。

79f36cd3-d3d5-d63e-d77e-b1df41283549.png

開発するフロー

 以下の様なフローを開発します。このフローでは、定期的にウェブサーバから監視カメラの画像を取得し、左下の「元の画像」のノードの下に画像を表示します。その後、TensorFlow.jsノードを用いて画像認識を行い、認識結果と認識結果付き画像をそれぞれデバッグタブと「認識結果付き画像」のノードの下に表示します。

8b23a90d-cf4d-86aa-b2fd-6f27fc5399d9.png

 もし、画像認識にて人物(person)が写っていると判定された場合は、SendGridノードを用いて画像ファイルを添付したアラートメールを送信します。実際に監視カメラを準備するのは大変なため、ここではサンプルとして、神奈川県が川の水量を確認するために設置している監視カメラの画像を活用してみました。

 以降で本フローの作成手順を説明してゆきます。Node-RED環境は、ローカルPC環境Raspberry Pi環境、クラウド環境などを用意してください。

必要なノードのインストール

 Node-REDフローエディタの右上のハンバーガーメニューをクリックし、「パレットの管理」->「パレット」タブ->「ノードの追加」タブと遷移し、以下のノードをインストールします。

(1) 画像データを取得するフロー

 最初に、ウェブサーバから画像のバイナリデータを取得するフローを作成します。以下のフローの様に、injectノード(ワークスペースに配置すると名前が「タイムスタンプ」に変わります)、http requestノード、image previewノードを配置し、ワイヤーで接続します。

59cc6697-0b57-1483-e4ef-ca2b14cfd335.png

 その後、http requestノードをダブルクリックして、ノードのプロパティ設定を変更します。

http requestノードのプロパティ設定

 http requestノードのプロパティ設定画面にあるURLに、監視カメラの画像のURLを貼り付けます(Google Chromeでは、画像の上で右クリックすると現れるメニューから「画像アドレスをコピー」を選択すると、画像をURLがクリップボードへコピーされます)。また、出力形式として「バイナリバッファ」を選択します。

500bfd7e-2640-d885-fe93-e72442bf66f4.png

画像データを取得するフローを実行

 フローエディタ右上のデプロイボタンをクリックした後、injectノードの左側のボタンをクリックします。すると、injectノードからワイヤーを通してメッセージがhttp requestノードに送られ、監視カメラの画像を提供するウェブサーバから画像を取得します。画像データを取得後、バイナリ形式のデータを含むメッセージがimage previewノードに送られ、image previewノードの下に画像が表示されます。

9fc00c1e-99ea-2833-ff9a-96d6dac2a6b3.png

右下に監視カメラが撮影した川の画像が表示されましたね!

(2)取得した画像データに対して画像認識を行うフロー

 次に取得した画像に何が写っているかを分析するフローを作成します。cocossdノードと、debugノード(配置すると名前がmsg.payloadに変わります)、2つ目のimage previewノードを追加で配置します。その後、http requestノードの右側の出力端子とcocossdノードの左側の入力端子、cocossdノードの右側の出力端子とdebugノード、cocossdノードの右側の出力端子とimage previewノードの左側の入力端子をそれぞれワイヤーで接続します。これによって、監視カメラ画像のバイナリデータがcocossdノードに送られ、TensorFlow.jsを用いた画像認識が行われた後、物体名をdebugノードで表示し、画像認識結果付き画像をimage previewノードで表示します。

8877b07e-3391-394a-d205-8e43041be110.png

 cocossdノードは、変数msg.payloadに物体名、変数msg.annotatedInputに画像認識結果付き画像のバイナリデータを格納する仕様になっています。そのため、画像の表示に用いるimage previewノードはダブルクリックをして、ノードプロパティ設定を変更する必要があります。

image previewノードのプロパティ設定

 image previewノードはデフォルトで変数msg.payloadに格納された画像データを表示します。ここでは、このデフォルト変数をmsg.annotatedInputに変更します。

8275dd08-aee6-b9ab-338a-05799bce1b95.png

injectノードのプロパティ設定

 1分毎にフローを定期実行するため、injectノードのプロパティも変更します。繰り返しのプルダウンメニューで、「指定した時間間隔」を選択し、時間間隔として「1分」を設定します。また、デプロイボタンを押した後、すぐに定期実行処理を開始したいため、「Node-REDの起動後、0.1秒後、以下を行う」の左側のチェックボックスをオンにします。

74583f93-e2e9-f6ad-aec0-d0203f823bfb.png

画像認識を行うフローを実行

 デプロイボタンを押すと、すぐにフローの処理が実行されます。監視カメラに人物(著者本人)が写ると、右側のデバッグタブに"person"という画像認識結果が表示されます。またimage previewノードの下には、オレンジ色の四角でアノテーション付けされた画像が表示されます。

fb7c4f78-a6ef-dbbf-0235-468748f2f6e5.png

(3)監視カメラに人物が写った時に、メールを送信するフロー

 最後に、画像認識結果の物体名が"person"だった時に、アノテーション付き画像をメールで送付するフローを作成します。cocossdノードの後続のノードとして、条件判定を行うswitchノード、値の代入を行うchangeノード、メールを送信するsendgridノードを配置し、それぞれのノードをワイヤーで接続します。

71a0474f-d7b3-663a-899d-303bfb170b32.png

 その後、各ノードのプロパティ設定を変更します。

switchノードのノードプロパティ設定

msg.payloadに"person"という文字列を含む場合のみ、後続のフローが実行されるように設定します。それを実現するには、条件「==」の比較文字列(azの右側)に"person"を入力します。

a939b03e-9c6b-bd2b-596d-2604148ca1f0.png

changeノードのノードプロパティ設定

 認識結果付き画像をメールに添付するため、変数msg.annotatedInputに格納されている画像データを変数msg.payloadへ代入します。まず「対象の値」の右側の「AZ」のプルダウンメニューを開き「msg.」を選択します。その後、右側のテキストエリアに「annotatedInput」と入力します。

9346c9cd-41ef-3b77-2015-41d1845f75d2.png

 「AZ」をクリックすると表示されるプルダウンメニューで「msg.」に変更することを忘れることで、フローが上手く動かないことがよくありますので、「msg.」になっているか再度確認しましょう。

sendgridノードのノードプロパティ

 SendGridの管理画面から取得したAPIキー、送信元メールアドレス、送信先メールアドレスを設定します。

0686f088-f48d-c927-2af9-99136050600b.png

 最後に、各ノードでどんな処理を行っているか分かりやすくするために、各ノードのノードプロパティを開き、適切な名前を設定しました。

監視カメラに人物が写った時に、メールを送信するフローの動作確認

 監視カメラの画像に人物が写ると、前のフローの実行確認の同様に、デバッグタブに画像認識結果が表示され、「認識結果付き画像」のimage previewノードの下の画像にオレンジ色の枠が描画され、人物を正しく認識されていることが分かります。

f481fc2d-3107-cf55-cbf8-78dd0597828b.png

 その後、判定処理や代入処理、メール送信処理が上手く動作すると、スマホにはアノテーション付きの画像ファイルが添付されたメールが届きます。
71baa452-43ac-3e9e-8fc2-b2e5b434882f.png

最後に

 今回ご紹介した様なフローを用いて、Raspberry Piに接続したカメラを利用した、自宅の庭の簡易防犯システムを自分で構築することも可能です。また本格的には、ONVIF等のプロトコルに対応したネットワークカメラを用いて取得した画像データに対して画像認識を行うこともできそうです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

リアルタイムで更新するランキング画面を”手抜き”実装してみた

はじめに

ゲームセンターによく「最後にスコアのランキング表示」が出るゲームがあったと思うのですが、
それをちょっとWeb上で実装したいな〜って思ったので実装してみました

▼ イメージ

ただ、単純にランキング表示するだけだとつまらない(?)ので、
WebSocket通信も使って「外部からスコアデータを受信したらランキングをリアルタイムで更新」も作ってみました

完成イメージ

ranking-websocket.gif
新規データがどれかわかりやすいように無駄に色つけてみたりしてますが、まぁそこはお好みで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でいい感じにアニメーション(トランジション)を設定できるやつっぽいw

JS: 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.js
const 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のDynamo WebDBとして扱いやすい kintone とか使うといいですよ!!(宣伝)

kintoneの使い方はこちらでまとめてます!!

▼ kintoneの使い方 (データベース編)
https://qiita.com/RyBB/items/daabb9b60d804ee2242f

おわりに

さくっと記事書くつもりがなんだかんだ説明が長くなってしまった・・・

今回はWebsocketにローカルのNode.jsサーバー使ってますが、AWSのAPI GatewayもWebSocketに使えるので
「API Gateway + Lambda + DynamoDB」って構成も可能です!(従量課金ですが)

ランキング画面は外部ディスプレイとかに表示しっぱなしにしておいて、IoTデバイスとかでデータ追加したらこっちも変わる!とか面白そう

それでは!≧(+・` ཀ・´)≦

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ワイニート Google Apps Scriptの神ライブラリ「cheeriogs」でヤフートップをスクレイピングする。GASのスクレイピングはもうこのライブラリ一択やな。

ある日の我が家

ワイ「この記事で神ライブラリの存在に気づいた」
https://qiita.com/mogya/items/dedbbaec39447e74a124#comment-ff558bb5797f4056a374

ワイ 「早速この神ライブラリを使ってみるわ」

成果物

無題.png

ワイ 「ヤフートップタイトルリンクを出力しとる」

手順

2.png

ワイ 「リソース→ライブラリ」

3.png

ワイ 「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();
  });
}

ワイ 「実行する」

4.png

出力

無題.png

ワイ 「上手くいったで」

ワイの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

お仕事待ってます^^

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

自分用メモ 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はシングルスレッドになるのでアクセスが多い場所に強い!!
だから非同期になって処理が必要なのかな???

まとめ

調べる上でわからない言葉が数多く出てきたので主要なものだけメモさせていただきました。そのおかげで大変自己中心的な記事となっております!!

なんとなくこれでフロントやバックエンドなどの違いを構造的に少しイメージしやすくなったと感じております!!
何か他に大事なことがあればぜひぜひアドバイスお願いいたします!!

参照

初心者向け!3分で理解するNode.jsとは何か?

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

フォームの入力欄を動的に追加する

はじめに

独学でプログラミングを学習中の大学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.css
input, 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.js
var len = $('.text').length

このような入力欄の数を数える変数を1行目とかで宣言してしまうと、
(当たり前ですが)入力欄を増やしたとしても、lenは増えません。

参考

参考にさせていただきました。ありがとうございます。
https://qiita.com/SiskAra/items/5f4bc7ee4e598b863add

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

node.jsを使ってみる

node.jsをインストールしてみる。
参考:https://www.sejuku.net/blog/82322

1. ダウンロード

実行してみる。

% node --version
v12.18.3

2. Hello, world!

hello.js
console.log('Hello, world!')
% node hello.js
Hello, world!

と、無事実行できた。
参考:https://qiita.com/loremipsumjp/items/3d32a44fe80c9a2febbe

3. 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.js
const 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!
Hello World!

と表示されていたら、成功。

done!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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全盛の頃ストップウォッチ機能も持たせたアナログ時計を作ったことがあるのですが、時間に余裕ができたら同じような機能を持たせてみたいです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【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

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[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の」引き算、割り算、掛け算、足し算 です。
足し算だけ、様子がおかしいですね。

sample.png

そうなってしまう理由

本来、文字(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になり、イイ感じに足し算もしてくれていますね。
計算2.png

皆さん、良いエンジニアライフを。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ポートフォリオ 「FOOTBALL SHIRTS」  FWを使用せず素のPHPで制作しました。

初めに

フロントエンドエンジニアを目指してプログラミングを学習しています。
長田と申します。
プログラミング学習のアウトプットとして自作のWebサービス「FOOTBALL SHIRTS」のポートフォリオを制作しました。
この記事では「FOOTBALL SHIRTS」の概要や制作過程について説明します。
ソースコード↓
https://github.com/satoruosada/uniform

スクリーンショット 2020-08-13 13.10.07.png

目的

・フルスクラッチ開発を行うことでWebアプリの基本的な構成、動作を知る.
・自作のWebアプリで同じ初学者の方の役に立つサービスを提供したい.

スペック

使用言語 / HTML5/ CSS3 / Javascript / PHP

DBMS / MySQL

開発環境 / MacOS Catalina 10.15.6

バージョン管理 / SourceTree(3.0.15)

主な機能

ユーザー管理機能
 ・ユーザー登録機能
 ・ユーザーログイン機能
 ・ユーザー編集機能
 ・ユーザー削除機能

出品する商品登録管理機能
 ・商品登録
 ・商品編集
 ・商品詳細
 ・商品一覧(ページネーション)
 ・商品検索機能
 

商品詳細機能
 ・商品詳細表示
 ・商品へのリンク
 ・お気に入り機能
 ・掲示板機能(メッセージ投稿)

概要

「FOOTBALL SHIRTS」は、サッカーのユニフォームを出品するWebサービスです。

サッカーのユニフォームマニアが様々な世界中のサッカーのユニフォームを集めたり、転売できる専門のwebサービスを制作してみました。

開発手順

実装させたい主な機能から必要な項目を洗い出し、サンプルとしてExcelに必要なDB情報を書き出していきました。
洗い出した情報を元にテーブルを作成します。

スクリーンショット 2020-08-13 14.12.41.png

AdobeXDデザインカンプ作成

コーディング

デザインカンプを元に画面モックを作成
その後裏側の機能を実装していきます

セキュリティ対策

バリデーションチェック
サーバーサイド(PHP)側
 ☑︎未入力チェック
 ☑︎最大、最小文字数チェック
 ☑︎半角英数字チェック
 ☑︎正規表現を使用したemail型式チェック
 ☑︎正規表現を使用したURL型式チェック
 ☑︎同値チェック
 それぞれ関数を作成し各フォームで判定を行なっています。
以下一部抜粋です
スクリーンショット 2020-08-13 14.33.01.png

スクリーンショット 2020-08-13 14.33.36.png

フロントエンド(HTML)側

なりすまし対策について

セッションハイジャックによるなりすまし対策としてsession_regenerate_id関数を使用しています。
スクリーンショット 2020-08-13 14.41.26.png
session_regenerate_id関数は、現在のセッションのデータを保持したまま、セッションIDを新しく生成してくれます。

パスワードのハッシュ化について

パスワードをDBで登録する際は開発環境から見えてしまうのでセキュリティ上よくありません。
「FOOTBALL SHIRTS」ではpassword_hash関数でパスワードをハッシュ化してDB登録しています。
スクリーンショット 2020-08-13 14.45.45.png

ログイン時には、
password_verifiを使用し、ハッシュ化されたパスワードを確認しています。

スクリーンショット 2020-08-13 14.47.03.png
このとき$passはフォームからpostされたパスワード
DBから配列形式で取り出した情報を$resultに詰め
array_shiftを使って先頭から要素を一つ取り出し第二引数としています。

SQLインジェクション対策

DB接続時は、プレースホルダーを利用しSQL文を作成。
プリペアードステートメントを使うことでSQLインジェクション対策を行なっています。
スクリーンショット 2020-08-13 14.48.17.png

XSS(クロスサイトスクリプティング)対策

画面へ文字列や数値を出力する際は、htmlspecialchars関数を使いエスケープ処理を行なっています。

エスケープ処理とは特殊な文字を無害な文字に強制的に置き換える方法です
スクリーンショット 2020-08-13 14.50.00.png
第二引数のエスケープにはいくつか種類がありますが最もエスケープ文字数の多いENT_QUOTESを使用しています。

「FOOTBALL SHIRTS」で出来ること

①「FOOTBALL SHIRTS」へのユーザー登録、ログイン、ログアウト

スクリーンショット 2020-08-14 17.20.50.png

②ログイン後「FOOTBALL SHIRTS」の商品登録(出品)・編集・マイページ機能

ezgif.com-video-to-gif (5).gif

「FOOTBALL SHIRTS」の商品登録ページでは、「FOOTBALL SHIRTS」の商品名、カテゴリー、詳細コメント、商品の画像の登録が可能です。

画像登録にはjQueryを利用しドラック&ドロップでInputされるよう設定しています。

登録完了後はマイページに遷移し、きちんと登録できたことが確認できるようメッセージが表示されます。

マイページでは自身が登録した「FOOTBALL SHIRTS」の商品の閲覧、編集、自身のプロフィール編集、パスワード変更、退会、お気に入り登録した「FOOTBALL SHIRTS」の閲覧が可能です。

③「FOOTBALL SHIRTS」詳細画面に、Ajax処理によるお気に入り登録機能、掲示板機能を実装

お気に入り機能について
「FOOTBALL SHIRTS」の商品詳細画面では、商品画像右上のハートアイコンを押すことで
マイページのお気に入り一覧に商品を登録できます。
こちらはAjaxを用いて実装しています。

ezgif.com-video-to-gif (1).gif

掲示板機能について

「FOOTBALL SHIRTS」のやり取りについて気軽に質問できるように
掲示板機能を各詳細ページに実装しています。

まず掲示板自体が存在するかDB情報を確認。
無ければ新規作成します。
スクリーンショット 2020-08-14 14.55.03.png
もし既に掲示板情報があればメッセージのみ追加できるよう条件分岐させています。

スクリーンショット 2020-08-14 14.55.44.png

ezgif.com-video-to-gif (2).gif

④商品一覧・検索機能

 商品一覧ページにはページネーション、カテゴリ選択による表示順選択機能を実装しました。
ezgif.com-video-to-gif (3).gif

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

`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を使おうとした。

やりたいことは

  1. パスワードの入力を求めるメッセージはコンソールに表示したい
  2. ユーザーが入力するパスワードはコンソールに表示したくない
  3. 最終結果は(秘密のトークンを含むので)コンソールに表示したくない

である。
とりあえず

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されるのではないのか・・・?

ちょっとよくわからないので放置しとく・・・。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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 myService

AppSync とは

AppSync は AWS が提供するマネージド GraphQL サービスです。AWS の各種バックエンド(DynamoDB, Aurora, Elasticsearch, Lambda など)とシームレスに結合ができ、すばやく API バックエンドを構築できます。また、Subscription という機能により、複数クライアントが同時編集できる Web アプリケーションや、リアルタイムなチャットアプリを簡単に実現できます。

仕組み

なぜ GraphQL が求められるのか

spa.png

バックエンドに REST API、フロントエンドに React(または Vue.js や Angular)といった構成は一般的でしょう。バックエンドとフロントエンドは Rest の API 定義を互いに共有し、開発を進めます。有名なツールとして OpenAPI があります。OpenAPI による定義を介することで、私たちは以下のような恩恵を得ることができました。

OpenAPI による恩恵

  • API 定義を YAML 形式のファイルで管理する
  • HTML 形式の API ドキュメントを自動生成する
  • API 定義をコミュニケーションのハブとし、フロントエンドとバックエンドの開発体制を分離する
  • OpenAPI 定義からフロントエンドで使用する API コール用のコードを自動生成する
  • バックエンドのバリデーションは OpenAPI 定義から自動生成する
  • バックエンドのモックサーバを自動構築する

openapi-ページ1.png

コードを自動生成するツールは、以下の 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 アプリケーションにおいて、ユーザと友達の情報を取得する場合のリクエストを表しています。

graphqlvsrest.png

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 にアクセスし、データをクライアントに返却します。

openapi-ページ6.png

一部、公式ドキュメントより抜粋しています。

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 CloudFormationServerless Framework の使用をお勧めします。

本記事では、AppSync をローカルでテストする方法を紹介するために、Serverless Framework を使用した開発方法をガイドします。

Serverless Framework for Appsync

serverlessappsync.gif

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 が返却する構成にしています。

openapi-ページ3.png

以下のような しょぼTrello のようなアプリケーションのバックエンドになります。

a

Serverless プロジェクトを開始する

まずは Serverless Framework の AWS/TypeScript ボイラーテンプレートからプロジェクトのひな形を作ります。

$ serverless create --template aws-nodejs-typescript

lulzneko さんの「Serverless Framework と TypeScript でサーバレス開発事始め
」という記事でとても分かりやすく説明されています。

Serverless AppSync Plugin のインストール

プラグインを yarn インストールします。

$ yarn add serverless-appsync-plugin

npm を使用しても良いですが、yarn が推奨されています。

$ npm install serverless-appsync-plugin

プラグインの設定

serverless.yml にプラグインを追加します。

serverless.yml
plugins:
  - serverless-webpack
  - serverless-appsync-plugin # これを追加

custom 配下に appSync の設定を記載します。必要最小限しか説明していないので、詳しくは公式ドキュメントを参照ください。

serverless.yml
custom:
  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.graphql
type 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 deploy

AppSync をテストする

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.yml
plugins:
  - serverless-webpack
  - serverless-appsync-plugin
  - serverless-dynamodb-local
  - serverless-appsync-simulator # serverless-offline よりも上に記述する必要があります
  - serverless-offline

Lambda Resolver のために、Lambda Function を実装します。Webpack を使用して nodejs あるいは TypeScript のソースをコンパイルする場合、 ビルド済みのファイルが ./webpack/service に展開されます。以下の設定を忘れないようにしましょう。

serverless.yml
custom:
  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回のリクエストで複数のリゾルバーが起動し、レスポンスを返却していることがわかります。

grapiqllocalhost.gif

GraphQL リクエストをテストする

ローカルで AppSync のシミュレータを起動できたので、テストを記述していきます。テストのアプローチの方法は至極単純です。

  1. DynamoDB にデータを用意し
  2. GraphQL リクエストを実行すると
  3. 想定通りの結果が取得できること

を確認します。

GraphQL リクエストのテスト領域

テストの対象を図示すると以下のようになります。GrqphQL のリクエストとレスポンスをテストします。これはインテグレーションテストに相当します。

openapi-インテグレーションテスト.png

DynamoDB Local の seed データ

serverless-dynamodb-local には seed データを作成する機能があります。この機能を利用してもいいですね。

以下のように serverless.yml に追記します。

serverless.yml
custom:
  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.ts
import { 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.ts
import { 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.ts
import { 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 のリクエストとそれによって生成されるリゾルバーリクエストを検証します。

openapi-ユニットテスト.png

VTL のテストのためにヘルパー関数を作成する

VTL によって生成されるリゾルバーリクエストを検証するために、以下のようなヘルパー関数を用意しておきます。amplify-velocity-template 本体が提供している Compile などを使用します。

vtl.helper.ts
import * 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 に対して idstatus という一意なキー検索を行うリクエストを発行します(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.ts
test("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 に登録する前処理として createdAtupdatedAt などのフィールドを追加しています。このような複雑なロジックが組み込まれた VTL を単体でユニットテストできるのは非常に効果的です。

以下テストコードで、createdAtupdatedAt などのフィールドが正常に追加されているか確認します。※ 現在時刻が設定されるので expect.anything() を使用して曖昧な判定にしています。

createTask.resolver.test.ts
test("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 が使用できます。

CircleCI ドキュメント

ただし、今回使用する DynamoDB Local は Java 製なので、ちょっと工夫が必要です。この orbs で使用される Docker イメージには nodejs, Python しか入っていないので Java をインストールしましょう。OpenJDK が入れられれば十分です。

.circleci/config.yml は以下のようになります。

circleci/config.yml
version: 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 でリクエストを受け付けるサービスが起動したことを検知して、次の処理を実行できます。この仕組みを利用して、以下の一連の流れを実施します。

  1. AppSync Simulator を起動する。
  2. テストを実行する。
  3. AppSync Simulator を停止する。

デリバリの戦略

今回デリバリする対象の環境は dev, stg, prod の3つとします。
それぞれ以下のような用途を想定しています。

  • dev: テスト環境
  • stg: ステージング環境(本番環境と同等のスペック)
  • prod: 本番環境

以下のようにステージごとに参照できる変数を定義します。

serverless.yml
custom:
  stages:
    dev:
      userPoolId: ap-northeast-1_XXXXXXXXX
    stg:
      userPoolId: ap-northeast-1_YYYYYYYYY
    prod:
      userPoolId: ap-northeast-1_ZZZZZZZZZ

以下のように参照して使用できます。

serverless.yml
userPoolId: ${self:custom.stages.${opt:stage}.userPoolId}

このようにしておくことで、ステージを指定してことなるパラメータを読み込み、デプロイできるようになります。

$ sls deploy --stage dev

CloudForamtion マネジメントコンソール画面
image.png

AppSync マネジメントコンソール画面
image.png

複数環境にデプロイできています。実際のユースケースでは、本番環境だけ AWS 環境を分離しておくという方法を採用するかもしれません。その場合は @kudedasumnさんの「CircleCI で複数の AWS アカウントを扱う方法」という記事が大変参考になります。

まとめ

AppSync をローカルで開発し、テストする。また、CircleCI 上でテストを行い、複数の環境にデプロイする方法を説明しました。AppSync をはじめとし、GraphQL はモバイルや IoT など、すべてのデバイスがインターネットに接続し、相互作用する現代のアプリケーションにおいて必須ともいえる技術要素です。これからも GraphQL 関連のエコシステムは継続的にウォッチし、コミットしていきたいですね。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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") });

最近知った、大変便利。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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年。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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, filters

Vue.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

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ブラウザで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.rules
SUBSYSTEMS=="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();
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

カリー化チートシート【記述例一覧版】

拙作記事『カリー化チートシート』について,対応言語や補足追記が増えるにつれ,チートシートというよりは解説記事と化してきたので,あらためて記述例一覧版を作成.しばらくしたら,元記事タイトルを『カリー化記述まとめ』に変えて,こちらを『カリー化チートシート』にするかも.

記述例一覧

複数の記法が可能な言語は,最新バージョン&カリー化メソッド未使用の場合の,最も短い書き方のみを掲載.その他の記法やバージョン等による違いは元記事を参照.

言語 (λxy.(真, if x>y; and 偽, if x≦y)) 10 20
Haskell (\x y -> x > y) 10 20
Scheme (((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言語)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[SeerviceNow] Script Includeに定数を持たせる

きっかけ

Scopedアプリケーションのテーブル名が長くてタイプが面倒。
アプリケーションのプロパティを覚えられない。
ステータスの値が覚えられない。
Studioの中でまとまっているからそこまで探すのは大変ではないけれど、使用頻度が上がるとやっぱり一箇所にまとめたい。
つまり、衰えゆく記憶力や腕力を経験値でカバーしたい。

誰でも知ってることを今さら知っただけなのかもしれないけれど共有。

あえて冗長に書いていますが例えばこういうコード

before.js
var 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を定義する

スクリーンショット 2020-08-13 19.47.46.png

変更後

after.js
var gr = new GlideRecord(Constants.TABLE_PROPERTY);

繰り返し使う定数はこうするよう心がけようと思う。

参考URL
https://youtu.be/B94UUQPDyDg

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

(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-handle

excludeCredentialとuser.idについては実際に実装している記事を見た方が早いと思います。
これらのパラメータにより、「ユーザー」の情報と「クレデンシャル」の情報を1対多の関連として実装するために利用することができます。

最後に

WebサービスでWebAuthnを導入するとなると、やはり「(登録)解除」や「クレデンシャル情報の紛失」と言った事項の検討は避けられないと思います。
実際に設計・実装するとなった時に、上記の事項を忘れていると慌てそうなので、やはり事前に考慮しておきたいものです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

RailsでAjaxでいいね機能を実装する方法

「Railsでいいね機能を実装する方法」でいいね機能の実装方法をご紹介しましたが、今回はそのいいね機能をAjax(非同期通信)実装する方法をご紹介いたします。
完成系は以下のような感じです。
いいね_Ajax.gif

環境

  • 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.rb
  def 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
  end

jsファイルの作成

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を書き換えます。
これで完成です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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 -> loadDOMContentLoaded -> DOMContentLoaded も出力されていません)

当たり前のことなのですが、loadイベントが動くときにはDOMContentLoadedイベントはもう終わっているので、このタイミングでセットしても動いてくれないのです。

普通に実装しているとこんな書き方はしないのですが、DOMContentLoadedイベントを利用しているライブラリをloadイベント内で実行しようとする時など、気づかずにこの状態になっているかもしれないので注意です。

codepenでのサンプル

See the Pen DOMContentLoaded in load by mizukoshi akiya (@akiyah) on CodePen.

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

結局JavaScriptにおける変数ってどうしたらいいの?

そもそもJavaScriptの変数って何がどうなってるんだろう?

どうやらJavaScriptには変数の宣言を行う際に三つの方法があるらしいのだ!
その名もvar,let,constである!正直なことをぶっちゃけると私実は脳死でletを利用してました。そんな私自身のことはどうでもよくて〜実際どれがなんなのか、どれを使えばいいのよくわからないと言う人が多いと思います。そこで私の現段階での学習状況のアウトプットと私の主観を述べていこうと思います。

よくわかんないからとりあえず比較してみる...

私が調べたところ変数の宣言には3つの方法があり、それぞれ全く異なる特徴を持っています。それをまとめてみようと思います!

var let const
再代入 ×
再定義 × ×

なるほど〜こうみるとvarめっちゃ便利そう!constは変数というか定数じゃね!
という所感ですね〜
当時の僕はとりあえずvarの乱用をしてました。
まぁこんなテキトじゃよくないと思うのでちゃんと説明しようと思います

var

こちらはECMAScript2015以前のバージョンから利用されている変数の宣言方法ですね!
何度でも再代入ができて、再定義ができる何でも屋さん!
百聞は一見に如かず!
とりあえずなんか出力しようぜ〜
ということで誰でも思いつくようなこのコードを…デン!

hoge.js
function 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.js
function 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は使わない!ということを心がけましょう!というのが私の主観です...
参考になれば幸いです
それではまた〜ナムナム〜

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む