20190820のJavaScriptに関する記事は18件です。

JavaScriptによるユーザー情報の取得

ブラウザ、スクリーン、URLなど

JavaScriptではユーザーのIPアドレスを取得することができません。
必要な場合は、PHPなどJavaScript以外の方法でIPアドレスを取得する必要があります。

取得できる情報 属性
ホスト情報 location.host
ホスト名 location.hostname
ポート番号 location.port
URL(フル) location.href
プロトコル location.protocol
サーチ情報(?以降) location.search
ハッシュ(#以降) location.hash
ページURLのパス部分 location.pathname
ブラウザのコード名 navigator.appCodeName
ブラウザ名 navigator.appName
ブラウザバージョン navigator.appVersion
ブラウザの使用言語 navigator.language
ブラウザのプラットフォーム navigator.platform
ブラウザのユーザーエージェント navigator.userAgent
リファラー document.referrer
ドメイン名 document.domain
スクリーンの幅 screen.width
スクリーンの高さ screen.height
スクリーンの色深度(bit) screen.colorDepth
html
<div id="information"></div>
JavaScript
const information=document.getElementById("information");
const infoArray=[];

infoArray.ホスト=location.host;
infoArray.ホスト名=location.hostname;
infoArray.ポート番号=location.port;
infoArray.URL=location.href;
infoArray.プロトコル情報=location.protocol;
infoArray.サーチ情報=location.search;
infoArray.ハッシュ=location.hash;
infoArray.ページURLのパス=location.pathname;
infoArray.ブラウザのコードネーム=location.appCodeName;
infoArray.ブラウザ名=navigator.appName;
infoArray.ブラウザのバージョン=navigator.appVersion;
infoArray.ブラウザの使用言語=navigator.language;
infoArray.ブラウザのプラットフォーム=navigator.platform;
infoArray.ブラウザのユーザーエージェント=navigator.userAgent;
infoArray.リファラー=document.referrer;
infoArray.ドメイン=document.domain;
infoArray.スクリーンの幅=screen.width;
infoArray.スクリーンの高さ=screen.height;
infoArray.スクリーンの色深度=screen.colorDepth+"bit";

for(key in infoArray){
  information.innerHTML+=(key+":"+infoArray[key]+"<br>");
}

位置情報

Geolocation APIを使用します。
※プライバシー保護のため、ユーザーは位置情報送信時に確認を求められます。

メソッド 内容
getCurrentPosition() 現在地を取得するために使う
watchPosition() デバイスの位置が変化するたびに位置情報を更新するために使う
clearWatch() 位置情報を継続して監視することを解除するために使う
情報 属性
緯度 position.coords.latitude
経度 position.coords.longitude
高度 position.coords.altitude
緯度・経度の誤差 position.coords.accuracy
高度の誤差 position.coords.altitudeAccuracy
方角 position.coords.heading
速度 position.coords.speed
エラーコード 内容
1 ユーザーが位置情報の利用を拒否したときなど
2 デバイスの位置が判定できないときなど
3 タイムアウトしたとき

エラーコードはcode属性で取得できます。

html
<div id="information"></div>
JavaScript
const information=document.getElementById("information");
const infoArray=[];

// 位置情報取得成功時の処理
let successCallback=(position)=>{
  information.innerHTML="";

  infoArray.緯度=position.coords.latitude;
  infoArray.経度=position.coords.longitude;
  infoArray.高度=position.coords.altitude;
  infoArray.緯度と経度の誤差=position.coords.accuracy;
  infoArray.高度の誤差=position.coords.altitudeAccuracy;
  infoArray.方角=position.coords.heading;
  infoArray.速度=position.coords.speed;

  for(key in infoArray){
    information.innerHTML+=(key+":"+infoArray[key]+"<br>");
  }

}

// 位置情報取得失敗時の処理
const failureCallback=(error)=>{
  let errorMessage = "";
    switch(error.code){
      case 1:
        errorMessage = "位置情報の取得がユーザーに拒否されました";
        break;
      case 2:
        errorMessage = "位置情報が判定できません";
        break;
      case 3:
        errorMessage = "位置情報の取得処理がタイムアウトしました";
        break;
      }
    information.innerHTML=errorMessage;
}

//ユーザーの現在の位置情報を取得を実行
navigator.geolocation.watchPosition(successCallback, failureCallback);

加速度

DeviceOrientation Eventを使用します。
計測には専用のセンサーが必要なので、センサーが搭載されていないPCなどでは計測できません。
※iOSのsafariは「モーションと画面の向きのアクセス」がデフォルトでOFFになっているので、ONにしないと取得することができません。

情報 属性
加速度(X軸) acceleration.x
加速度(Y軸) acceleration.y
加速度(Z軸) acceleration.z
加速度+重力加速度(X軸) accelerationIncludingGravity.x
加速度+重力加速度(Y軸) accelerationIncludingGravity.y
加速度+重力加速度(Z軸) accelerationIncludingGravity.z
回転加速度(Z軸) rotationRate.alpha
回転加速度(X軸) rotationRate.beta
回転加速度(Y軸) rotationRate.gamma
html
<div id="information"></div>
JavaScript
const information=document.getElementById("information");
const infoArray=[];

//センサーから加速度を取得して書き出す処理
const deviceOrientationTest=()=>{window.addEventListener("devicemotion", (event)=> {          
  information.innerHTML="";

  infoArray.加速度X軸=parseFloat(event.acceleration.x);
  infoArray.加速度Y軸=parseFloat(event.acceleration.y);
  infoArray.加速度Z軸=parseFloat(event.acceleration.z);

  infoArray.加速度重力加速度X軸=parseFloat(event.accelerationIncludingGravity.x);
  infoArray.加速度重力加速度Y軸=parseFloat(event.accelerationIncludingGravity.y);
  infoArray.加速度重力加速度Z軸=parseFloat(event.accelerationIncludingGravity.z);

  infoArray.回転加速度X軸=parseFloat(event.rotationRate.beta);
  infoArray.回転加速度Y軸=parseFloat(event.rotationRate.gamma);
  infoArray.回転加速度Z軸=parseFloat(event.rotationRate.alpha);

  for(key in infoArray){
    information.innerHTML+=(key+":"+infoArray[key]+"<br>");
    };

  });

};

//センサーから加速度を取得して書き出す処理をループ実行
(deviceOrientationLoop=()=>{
  deviceOrientationTest();
  window.requestAnimationFrame(deviceOrientationLoop);
})();

OS別対応

AndroidとiOSは加速度が逆なので補正する必要があります。

iOSを補正する

Android(X:右、Y:上、Z:手前)を正にする場合はiOSを補正します。

JavaScript
if (navigator.userAgent.indexOf("iPhone") > 0 || navigator.userAgent.indexOf("iPad") > 0 ||navigator.userAgent.indexOf("iPod") > 0) {

  for(key in infoArray){
    information.innerHTML+=(key+":"+infoArray[key]*-1+"<br>");
  };

}

Androidを補正する

iOS(X:左、Y:下、Z:奥)を正にする場合はAndroidを補正します。

JavaScript
if ( navigator.userAgent.indexOf('Android') > 0 ) {

  for(key in infoArray){
    information.innerHTML+=(key+":"+infoArray[key]*-1+"<br>");
  };

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

JavaScriptでAmazon S3にファイルをアップロード

概要

Web ブラウザーから実行する JavaScript で、AJAX 通信で Amazon S3 の Bucket にできるだけセキュアに画像ファイルをアップロードできるようにしてみます。
取り扱い可能な画像ファイルは png、jpeg、gif を想定しています。
作業は、次の順で進めます。

  1. アップロード先となる S3 Bucket の作成
  2. S3 に PutObject だけが可能なポリシーを作成
  3. STS が使用するロールの作成
  4. アップロードのみ可能なIAMユーザーを作成
  5. Lambda 関数を作成
  6. Lambda が使用するロールにポリシーをアタッチ
  7. API Gateway を作成
  8. クライアントプログラムの作成

アップロード先となる S3 Bucket の作成

アップロード先となる Bucket を作成します。
ここでは、 images という名前で Bucket を作成します。

アップロード先のフォルダーを uploads としたいので、uploads フォルダーの作成もしましょう。

images Bucket は画像ファイルは Web ページからアクセスされるようにしたいので、S3 のプロパティから Static website hosting を有効にします。
インデックスドキュメントは、ファイルが存在しなくても index.html を指定しておけば Static website hosting が有効になります。

最終的に異なるドメインから S3 の画像ファイルを参照することになるので、CORS の設定が必要です。
CORS というのは、ドメインをまたいで JavaScript を実行可能にする仕組みです。読み方は“コルス”と発音することが多いようです。
簡単に説明すると、ドメインをまたいで JavaScript の実行を許すと他のページから自分の作った JavaScript を使っていたずらされたり、XSS などの脆弱性になりかねないので、JavaScript を実行可能なドメインを限定することで意図しない使われ方がされないようにするものと思っていただければと思います。

images Bucket の S3 のアクセス制限から CORS の設定を行いましょう。

ここでは、公開する Web アプリケーションのサーバーのホスト名を myserver.com とします。
ファイルの作成と更新を許可するため、HTTPメソッドはPUTとPOSTを許可します。

<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<CORSRule>
    <AllowedOrigin>https://myserver.com</AllowedOrigin>
    <AllowedMethod>PUT</AllowedMethod>
    <AllowedMethod>POST</AllowedMethod>
    <AllowedHeader>*</AllowedHeader>
</CORSRule>
</CORSConfiguration>

S3 に PutObject (リソースの作成・置換)だけが可能なポリシーを作成

最終的にファイルをアップロード可能な URL を公開するので、アップロード以外のことができてほしくはありません。
万が一アップロード可能な URL を実行可能な機能がなんらかの方法によって乗っ取られたとしても PutObject しかできなければファイルを盗み取られるようなことはありません。
そのため、S3:PutObject のみ可能なポリシーを作成します。
このポリシーは後述の Lambda 関数から使用します。

ここでは UploadablePolicy という名前にしました。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": "s3:PutObject",
            "Resource": "arn:aws:s3:::images/uploads/*"
        }
    ]
}

STS が使用するロールの作成

STS(Security Token Service) は一時的な認証情報を取得するために使用します。一時的な認証情報は S3 にアップロードするための URL を署名するために使用します。

ここでは、RoleForTemporary という名前にしました。
”ポリシーをアタッチします”ボタンを押して、UploadablePolicy をアタッチしてください。

アップロードのみ可能な IAM ユーザーを作成

特定の権限しかもたないユーザーのアクセスキーと、シークレットアクセスキーを使用したいので IAM ユーザーを作成します。
既存の IAM ユーザーのアクセスキーとシークレットアクセスキーを使用してもいいのですが、人に割り当てられていたものを使用する場合、その人が異動したり退職した場合など割り当てを変更する必要がでてきて煩わしいので、人に割り当てられていない IAM ユーザーにすることをオススメします。

ここでは、 upload_user というユーザー名の IAM ユーザーを作成しました。
この IAM ユーザーはプログラムによるアクセスしかしないので、アクセスの種類を”プログラムによるアクセス”にチェックを入れて作成しました。
アクセス許可の設定では、“既存のポリシーを直接アタッチ”から、UploadablePolicy をアタッチしてください。
タグの設定は不要です。

Lambda 関数を作成

Lambda 関数の作成で、“一から作成”で各設定を以下のようにします。

  • 名前・・・uploadFunc
  • ランタイム・・・Node.js 6.10
  • ロール・・・カスタムロールの作成

ロールのドロップダウンで、“カスタムロールの作成”を選択すると、ロールの概要を入力するページが開きます。
ロール名を入力してください。
ここでは、RoleForUploadFunc にしました。

続いて“関数の作成”ボタンを押して関数コードにプログラムを書いていきます。
uploadFunc フォルダー以下のプログラムを編集します。index.js は最初から用意されているファイルです。uploadFunc フォルダーに lib.js というファイルも作りましょう。
後述する API Gateway でリクエストパラメーターをリクエストパラメーターとして受け取らず、JSON で受け取るように設定するのですが、そうしている理由は、JSON で受け取ると、Lambda 関数単体でのテストがしやすいためです。

uploadFunc/index.js

'use strict';

var lib    = require('./lib');
var aws    = require("aws-sdk");
var sts    = new lib.sts("RoleForTemporary");
var bucket = "images";

module.exports.handler = function(event, context, cb) {
    if (!event.file) {
        return lib.respond(cb, 400, 'Parameter "file" is missing.');
    } else if (!event.type) {
        return lib.respond(cb, 400, 'Parameter "type" is missing.');
    }

    //STSでS3に書き込み権限のあるロールを要求
    sts.assumeRole(function (err, data) {
        if (err) return lib.respond(cb, err);
        else {
            var credencials = data.Credentials;
            var s3 = new aws.S3({
                "accessKeyId" : credencials.AccessKeyId,
                "secretAccessKey" : credencials.SecretAccessKey,
                "sessionToken" : credencials.SessionToken
            });
            var s3_params = {Bucket: bucket, Key: "uploads/" + event.file, ContentType: event.type}; // アップロードするURLに対し署名
            var signed_url = s3.getSignedUrl('putObject', s3_params);
            var response = {"signedurl":signed_url};

            return lib.respond(cb, null, response);
        }
    });
};

uploadFunc/lib.js

accessKeyID の値は、IAM ユーザー upload_user のアクセスキーをセットしてください。ここでは、ABCDEFGHIJKLMNOPQRST であったとして進めます。
secretAccessKey の値は、IAM ユーザー upload_user のシークレットアクセスキーをセットしてください。ここでは、abcdefghijklmnopqrstuvwxyz0123456789ABCD であったとして進めます。
RoleArn のアカウント ID は AWS のアカウント ID です。ここでは、123456789012 であったとして進めます。

'use strict';

var aws = require("aws-sdk");

function sts_(roleName) {
    this.stsobj = new aws.STS({
        "accessKeyId": "ABCDEFGHIJKLMNOPQRST",
        "secretAccessKey": "abcdefghijklmnopqrstuvwxyz0123456789ABCD"
    });
    this.param = {
        "RoleArn": "arn:aws:iam::123456789012:role/" + roleName,
        "RoleSessionName": "session_" + roleName
    };
}
sts_.prototype.assumeRole = function(cb) {
    return this.stsobj.assumeRole(this.param, cb);
};

module.exports = {
    "respond" : function(cb, err, body) {
        if (err) {
            if (typeof err == 'object') {
                if (!err.status) err.status = '400';
                else if (typeof err.status == 'number') err.status = err.status + '';
            } else if ((err + '').match(/^[0-9]{3}$/)) {
                err = {
                    "status" : err,
                    "message" : body
                };
            }
            err = JSON.stringify(err);
        }
        return cb(err, body);
    },
    "sts": sts_
};

Lambda が使用するロールにポリシーをアタッチ

IAM で、RoleForUploadFunc ポリシーを選択して、”ポリシーをアタッチします”ボタンを押して、UploadablePolicy をアタッチしてください。

API Gateway を作成

API Gateway を作成すると、EC2 で Apache や nginx を用意しなくても Web API を公開することができます。

Lambda のコンソールから、左側の”トリガーの追加”にある”API Gateway”を追加してください。

“トリガーの設定”の API のドロップダウンから“新規 API の作成”を選択してください。
そうすると、セキュリティというドロップダウンが現れるので、“オープン”を選択します。

”▼追加の設定”を押して、“バイナリメディアタイプ”を設定します。
追加するバイナリメディアタイプは以下の3つです。

  • image/png
  • image/gif
  • image/jpeg

設定しおえたら右下にある“追加”ボタンを押してください。

Lambda のコンソールで、API Gateway に uploadFunc-API というハイパーリンクが現れるので、それを押してください。
そうすると API Gateway のコンソールが表示されます。

リソース欄に /uploadFunc が表示されるのでそれを選択します。
そして、その上にある“アクション▼”と書かれたドロップダウンを押してください。そして“メソッドの作成”を押してください。

そうすると ANY の下にドロップダウンが表示されるので、そこで GET を選択してください。セットアップのページが開きます。

セットアップ

/uploadFunc - GET - セットアップと表示されたページが表示されるので以下のように設定します。

  • 統合タイプ・・・ Lambda 関数
  • Lambda プロキシ統合の使用・・・チェックしない
  • Lambda リージョン・・・ap-northeast-1 (もちろん、別のリージョンをご利用であれば変更してください)
  • Lamda 関数・・・uploadFunc
  • デフォルトタイムアウトの使用・・・チェックする

以上を入力したら”保存“ボタンを押してください。メソッドの実行設定のページが開きます。

メソッドの実行設定

メソッドの実行に関する設定を行います。メソッドリクエストと、統合リクエストの設定を変更します。メソッドレスポンスと統合レスポンスの設定は変更しません。

メソッドリクエスト

API Gateway が受け取るリクエストパラメーターを登録します。

URLクエリ文字列パラメータに、file と type を追加してください。それぞれ必須とキャッシュのチェックボックスはチェックしなくてかまいません。

統合リクエスト

公開した Web API が受け取ったリクエストパラメーターを JSON データに変換します。

マッピングテンプレートのリクエスト本文のパススルーから“テンプレートが定義されていない場合(推奨)”を選択し、Content-Type に application/json を追加します。
テンプレートの JSON を以下の通りにしてください。

{
    "file" : "$input.params('file')",
    "type" : "$input.params('type')"
}

CORS 設定

最終的に異なるドメインから STS のトークンを発行することになるので、CORS の設定が必要です。
STS のトークンは S3 に公開する URL を署名してセキュアにするために使用します。

リソース欄の /uploadFunc を選択します。
そして、その上にある“アクション▼”と書かれたドロップダウンを押してください。そして“CORSの有効化”を押してください。

CORS の有効化ページで、以下のように設定してください。

  • uploadFunc-API API のゲートウェイレスポンス・・・DEFAULT 4XX:チェックなし、DEFAULT 4XX:チェックなし
  • メソッド・・・GETとOPTIONSをチェック(OPTIONSはチェックを外せない)
  • Access-Control-Allow-Headers・・・'*'
  • Access-Control-Allow-Origin*・・・'*'

続いて、”CORS を有効にして既存の CORS ヘッダーを置換”を押してください。

メソッドのデプロイ

API Gateway で API を公開する作業を行います。この作業の後、Lambda のコンソールで関数を編集してもやりなおす必要はありません。

リソース欄の /uploadFunc を選択します。
そして、その上にある“アクション▼”と書かれたドロップダウンを押してください。そして“APIのデプロイ”を押してください。

API のデプロイと書かれた小さなウィンドウが表示されるので、デプロイされるステージから default を選択して、”デプロイ”ボタンを押してください。

クライアントプログラムの作成

以下のような HTML と JavaScirpt (jQuery) でファイルをアップロードします。
プログラム中の URL https://1234567890.execute-api.ap-northeast-1.amazonaws.com/default/uploadFunc は、Lambda のコンソールに表示されたものを使用してください。1234567890 の部分はランダムな英数字で構成されます。

アップロードの仕組みは以下の通りです。

  1. https://1234567890.execute-api.ap-northeast-1.amazonaws.com/default/uploadFunc?〜 でアップロード先の S3 の URL を取得する。
  2. 取得した URL に対して input type="file" のファイルを POST する。

1で取得するアップロード先の S3 の URL は 15 分有効な URL です。それ以降に使用すると、タイムアウトのエラーが発生します。本ページ中に 15 と入力するところがなかったのでお気づきかもしれませんが、デフォルトが 15 分だからです。
変更したければ、Lambda 関数の uploadFunc/index.js のコメント“// アップロードするURLに対し署名”のある行のパラメーターを変更してください。詳しくは AWS の資料をご覧ください。

<form action="." method="post" class="upload form">
  <div class="columns">
    <div class="column is-12">
      <div class="file has-name">
        <label class="file-label">
          <input class="file-input" type="file" name="resume">
          <div class="file-cta">
            <span class="file-icon"><i class="fa fa-cloud-upload" aria-hidden="true"></i></span>
            <span class="file-label">ファイルを選択...</span>
          </div>
          <span class="file-name"><i class="fa fa-question" aria-hidden="true"></i></span>
        </label>
      </div>
    </div>
  </div>
  <div class="columns">
    <div class="column is-12">
      <input type="button" class="upload button" name="upload-button" value="アップロード">
    </div>
  </div>

  <div class="columns">
    <div class="column is-12 uploaded"></div>
  </div>
</form>
<script>
  $(function () {
    $(".upload.button").on('click', function () {
      var error = null;
      var orig_file = $('.file-input').prop('files')[0];
      var up_file = "bar/foo.png";

      $.ajax({
        url: 'https://1234567890.execute-api.ap-northeast-1.amazonaws.com/default/uploadFunc?file=' + up_file + '&type=' + orig_file.type,
        type: 'GET'
      }).then(function (data) {
        return $.ajax({
          url: data.signedurl,
          type: 'PUT',
          data: orig_file,
          contentType: orig_file.type,
          processData: false
        });
      }).then(function (data) {
        console.log("Upload success");

        $(".uploaded").append($("<img />").attr("src", "http://images.s3-website-ap-northeast-1.amazonaws.com/uploads/" + up_file));
      }).fail(function (data) {
        error = data.message || data.statusText || data.errorText;
      })
        .always(function (jqXHR, textStatus) {
          if (error) {
            console.log("Error: " + error);
          }
        });

      // false を返してデフォルトの動作をキャンセル
      return false;
    });
  });
</script>

課題

本ページで紹介したファイルのアップロードだと、ファイルの格納場所を指定しているので、存在するファイルを上書きできてしまいます。
本ページで紹介したファイルのアップロードを実装するアプリによっては、他人が作成したファイルを上書きできてしまったりするので場合によっては脆弱性になりかねません。
実装するアプリによっては他人のアップロードしたファイルを上書きできないようにする仕組みを別途組み込んだり、する工夫が必要になると思います。
そういった点に注意してご利用ください。

参考にしたページ

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

TypeScriptでStateパターンな電卓アプリを作る

新たにTypeScriptの勉強を始めたので、知識定着の為にTypeScriptを用いて電卓を作成しました。
特にライブラリやフレームワークは利用していません。
TypeScriptの基本知識は一通り目を通した方向けです。

参考

環境

  • Windows 10
  • Node.js 10.15.1
  • TypeScript 3.5.3
  • webpack 4.37.0

※コンパイル後のコードはes2015です。

最新版TypeScript+webpack 4の環境構築まとめ

Stateパターンな電卓

完成図

電卓.gif

実物はこちらです。キーボード入力にも対応しています。
https://beebow6.github.io/typescript-calculator/dist/

全てのソースはこちらです。
https://github.com/BeeBow6/typescript-calculator

状態遷移表

TypeScriptの基本的なところをなるべく一通り体験すべく、Javaで電卓アプリを作成している下記サイトを参考に作成しました。
サンプルアプリでおぼえる実践的Android入門※要事前登録

今回の電卓の状態遷移表は下記のとおりです。

A:左辺入力前
BeforeLeftSideState
B:数値入力
NumberState
C:演算子入力
OperatorState
D:右辺入力後
AfterRightState
E:結果表示
ResultState
F:エラー
ErrorState
数値
0-9
.
数値表示
⇒Bへ
数値追加表示 数値表示
⇒Bへ
乗算演算子登録
数値表示
⇒Bへ
演算履歴削除
数値表示
⇒Bへ
-
円周率
π
円周率表示

⇒Bへ
円周率表示 円周率表示
⇒Bへ
乗算演算子登録
円周率表示
⇒Bへ
演算履歴削除
円周率表示
⇒Bへ
-
演算子
+-×÷
0表示
数値確定
演算子登録
⇒Cへ
数値確定
演算実行
演算子登録
⇒Cへ
演算子差し替え 演算子登録
⇒Cへ
演算履歴削除
数値確定
演算子登録
⇒Cへ
-
左括弧
(
左括弧登録 数値確定
乗算演算子登録
左括弧登録
⇒Aへ
左括弧登録
⇒Aへ
乗算演算子登録
左括弧登録
⇒Aへ
すべて削除
左括弧登録
⇒Aへ
-
右括弧
)
- 数値確定
右括弧登録
演算実行
⇒Dへ
- 右括弧登録 - -
符号反転
+/-
符号反転
⇒Bへ
符号反転 表示削除
符号反転
⇒Bへ
乗算演算子登録
符号反転
⇒Bへ
演算履歴削除
符号反転
⇒Bへ
-
結果表示
=
- 数値確定
演算実行
⇒Eへ
演算子破棄
演算実行
⇒Eへ
演算実行
⇒Eへ
- -
一つ削除
- 一つ削除 - - 演算履歴削除
一つ削除
⇒数値なし⇒Aへ
⇒数値あり⇒Bへ
-
削除
C
- 表示削除
⇒Aへ
表示削除 - すべて削除
⇒Aへ
-
全て削除
AC
すべて削除 すべて削除
⇒Aへ
すべて削除
⇒Aへ
すべて削除
⇒Aへ
すべて削除
⇒Aへ
すべて削除
⇒Aへ

State パターン | TECHSCORE

定数を列挙型(enum)で定義

入力される数値および小数点と各種演算子、ボタンタイプを列挙型(enum)で定義します。

/**
 * ボタンからの入力値およびボタンタイプを列挙型で定義
 */

// 演算子
enum OPERATOR {
  ADD = '+',
  SUBTRACT = '-',
  MULTIPLY = '*',
  DIVIDE = '/',
  LEFT_PAREN = '(',
  RIGHT_PAREN = ')'
}

// 数値
enum NUMBER {
  ZERO = '0',
  ONE = '1',
  TWO = '2',
  // ...
  EIGHT = '8',
  NINE = '9',
  POINT = '.'
}

// ボタンの種類
enum BUTTON_TYPE {
  NUMBER = '10',
  PI = '11',
  OPERATOR = '20',
  LEFT_PAREN = '21',
  RIGHT_PAREN = '22',
  INVERSION = '23',
  EQUAL = '80',
  CLEAR = '90',
  ALL_CLEAR = '91',
  BACKSPACE = '92'
}

/src/setting.ts | GitHub

数値で宣言した場合、定義外の数値もすり抜けるようですね。
typescript_enum.png

VIEWコンポーネント

抽象クラス

ReactのComponentみたいなVIEWコンポーネントの基底クラスを抽象クラスで定義し、ボタンやディスプレイ等の各コンポーネントはこれを継承するようにします。

継承.png

基底クラスの定義

DOM要素を生成・保持します。
生成した要素は読み取り専用で参照可能です。
抽象メソッドgetTemplate()では、各派生クラスでHTMLソースを定義させるようにしています。
このソースがプレースフォルダを含む場合、constructorで受け取ったマップ情報をもとに置換します。

/**
 * コンポーネントの基底クラス
 */

interface ReplaceMap {
  [key: string]: string | number;
}

export default abstract class Component<T> {
  // このクラス内でのみ参照可。派生クラスでも参照不可。
  private _element: T;

  constructor(map: ReplaceMap = {}) {
    const div: HTMLElement = document.createElement('div');
    div.innerHTML = this.replaceTemplate(this.getTemplate(), map);
    this._element = div.firstElementChild as unknown as T;
  }

  // 派生クラスで要実装
  protected abstract getTemplate(): string;
  // このクラス内でのみ参照可。派生クラスでも参照不可。
  private replaceTemplate(temp: string, map: ReplaceMap = {}) {
    return temp.replace(/\{\{(.+?)\}\}/g, (_, key) => map[key] as string || '');
  }
  // どこからも参照可能。読み取り専用。
  get element(): T {
    return this._element;
  }
}

/src/components/component.ts | GitHub

ジェネリック型(Generics)

DOM要素を保持するメンバ_elementジェネリック型で定義しています。
一律でHTMLElementとしてしまうと、<input>valueプロパティなど、一部の要素しか実装していないプロパティは存在しないプロパティとみなされてしまいます。
そのため、派生クラス側でHTMLInputElementHTMLButtonElementなどの具体的な型を指定させます。
div.firstElementChildで返される要素の型はElementnullなので、Type Assertions (キャスト)で指定の型に変換する必要があるのですが、直接div.firstElementChild as Tとすると、間違っていないかい?と駄目だしされました。
意図したものであることを伝えるには、div.firstElementChild as unknown as Tと、間にunknownを入れる必要があるそうです。

派生クラスで実装

ボタン

ボタンは下記リスト情報をもとに生成されます。

interface ButtonParams {
  text: string,
  type: BUTTON_TYPE,
  order: number,
  keyCodeList: string[],
  value?: NUMBER | OPERATOR,
  size?: number
}

const BUTTONS: ButtonParams[] = [
  {
    text: NUMBER.ZERO,
    value: NUMBER.ZERO,
    type: BUTTON_TYPE.NUMBER,
    order: 21,
    keyCodeList: ['48', '96']
  },
  {
    text: NUMBER.ONE,
    value: NUMBER.ONE,
    type: BUTTON_TYPE.NUMBER,
    order: 17,
    keyCodeList: ['49', '97']
  },
  // ...
};

Componentクラスを継承したボタンクラスでは、オーバーライドしたメソッドgetTemplate()でHTMLソースを定義しています。
ボタン情報リストの型定義をしたButtonParamsを拡張したButtonPropsを、コンストラクタの引数のインターフェースとしています。

/**
 * ボタンコンポーネント
 */

interface ButtonHandler {
  (type: BUTTON_TYPE, value?: NUMBER | OPERATOR): void;
}

interface ButtonProps extends ButtonParams {
  onClick: ButtonHandler;
}

class Button extends Component<HTMLButtonElement> {

  private type: BUTTON_TYPE;
  private value: NUMBER | OPERATOR;
  private keyCodeList: string[];
  private onClick: ButtonHandler;

  constructor({ text, order, size, ...props }: ButtonProps) {
    super({ text, order });

    this.element.style.gridColumnEnd = size ? 'span ' + size : '';

    if (props.type === BUTTON_TYPE.NUMBER) {
      this.element.classList.add('btn-num');
    }

    this.type = props.type;
    this.value = props.value;
    this.keyCodeList = props.keyCodeList;
    this.onClick = props.onClick;

    this.element.addEventListener('click', this.handleClick.bind(this));
    // ...
  }

  // Override
  getTemplate() {
    return `<button type="button" class="btn" style="order:{{order}}">
      {{text}}
    </button>`;
  }

  private handleClick() {
    this.onClick(this.type, this.value);
  }

  // ...
}

/src/components/button.ts | GitHub

パネル

ボタン等の設置先となるパネルコンポーネントは、パネル上にComponentを追加する為のadd()メソッドを持ちます。
このときもジェネリック型は明示する必要があるのですが、HTMLElementとすることで、複数種類の要素を受付けられるようにしています。

/**
 * 派生クラス:パネルコンポーネント
 */
import Component from './component';

class Panel extends Component<HTMLFormElement> {
  // Override
  getTemplate() {
    return '<form class="panel"></form>';
  }

  add(parts: Component<HTMLElement>) {
    this.element.appendChild(parts.element);
  }

  // ...
}

/src/components/panel.ts | GitHub

ディスプレイクラス

入力中の数値を表示するMainDisplayクラスと、演算のプロセスを表示するProcessDisplayクラスの2種があります。
どちらもButtonPanel同様、Componentクラスの派生クラスですが、入力値を制御・保持する為のロジックを持ちます。

MainDisplay

内部で入力値、小数点の有無、正負を管理しています。
小数点ボタンが重複してクリックされても、2回目以降の入力は受付けません。
また、12文字以上の数値は受付けません。(符号除く)
入力値は文字列で管理されていますが、パブリックメソッドgetNumber()で数値として取得できます。

const LENGTH_MAX: number = 12;

class MainDisplay extends Component<HTMLInputElement>{

  private data: string = '';
  private isDecimal: boolean = false;
  private isNegative: boolean = false;

  constructor() {
    super();
    this.clear();
  }

  get isEmpty(): boolean {
    return this.data === '';
  }

  getTemplate() {
  }
  // 末尾に数字を追加
  addNumber(value: NUMBER) {
    if (this.data.length >= LENGTH_MAX) return;

    // ...
    this.data += value;
    this.displayNumber();
  }
  // 既存データを削除して数字表示
  setNumber(value: NUMBER | number) {
    this.reset();

    // ...
    this.displayNumber();
    return;
  }
  // 符号反転
  invertSign() {
    this.isNegative = !this.isNegative;
    this.displayNumber();
  }

  getNumber() {
    return Number.parseFloat(this.element.value) || 0;
  }
  // 末尾の一文字削除
  removeLastNumber() {
    this.data = this.data.slice(0, -1);
    // ...
    this.displayNumber();
  }
  // 削除
  clear() {
  }
  // エラーメッセージ表示
  setError(message: string) {
  }
  // 表示
  private displayNumber() {
    const value = (this.isNegative ? MINUS : '') + this.data;
    this.element.value = value || NUMBER.ZERO;
  }
  // リセット
  private reset() {
    this.data = '';
    this.isDecimal = this.isNegative = false;
  }
  // エラーモード
  private toggleErrorMode(flg = false) {
    this.element.classList.toggle('is-error', flg);
  }
}

/src/components/mainDisplay.ts | GitHub

ProcessDisplay

入力された数値と演算子を順番に配列で保持しています。
左括弧の入力数を管理しており、余分な右括弧が入力されないようにしています。
配列を出力する際に右括弧が不足している場合は追加します。
負数は見やすいよう括弧で括ります。

class ProcessDisplay extends Component<HTMLInputElement> {

  private stack: (number | OPERATOR)[] = [];
  private currentOperator: OPERATOR | null = null;
  private countParen: number = 0;

  constructor() {
    super();
    this.clear();
  }

  getTemplate() {
  }
  // 括弧利用中か
  get isParenMode(): boolean {
    return this.countParen > 0;
  }
  // 数値登録
  setNumber(number: number) {
    this.setStack(number);
  }
  // 演算子登録
  setOperator(operator: OPERATOR = null) {
    this.currentOperator = operator;
    this.display();
  }
  // 左括弧登録
  setLeftParen() {
    this.countParen++;
    // ...
  }
  // 右括弧登録
  setRightParen() {
    if (!this.countParen) return;
    this.countParen--;
    // ...
  }
  // 履歴リスト取得
  getStack(): (number | OPERATOR)[] {
    const rightParens = Array(this.countParen).fill(OPERATOR.RIGHT_PAREN);
    return [...this.stack, ...rightParens];
  }
  // 結果表示
  setResult() {
    this.stack = this.getStack();
    this.display();
  }
  // 履歴削除
  clear() {
  }
  // 履歴追加
  private setStack(value: number | OPERATOR) {
    // ...
    this.display();
  }
  // 表示
  private display() {
    this.element.value = [...this.stack, this.currentOperator].join('');
  }
}

/src/components/processDisplay.ts | GitHub

四則演算

演算を実際に行う部分はクラスではなく関数で定義しています。
JavaScriptは仕様上、小数点の演算に誤差が発生してしまうのですが、一旦整数に直して演算することでそれを回避しています。
また、この電卓は括弧にも対応している為、逆ポーランド記法を用いて演算しています。
引数の型チェックの手間が省けている事意外は、ほぼ普通のJavaScriptの関数なので割愛いたします。
(別記事で書くかも。。。)

/src/arithmetic.ts | GitHub
/src/calculation.ts | GitHub

JavaScriptで、できるかぎり小数演算の誤差を少なくする方法
日曜プログラミングで電卓を作ってみる

電卓クラス

電卓の基本機能を備えたクラスAppを定義します。
各コンポーネントのインスタンスを生成してPanel上に設置します。
ボタンクリックイベント発生時に、後述するStateクラスに転送します。
入力値の管理は2つのディスプレイクラスに委譲しているため、必要に応じてこれらのメソッドを呼び出すだけです。

// 型チェック
const isNUMBER = (test: any): test is NUMBER => {
  return Object.values(NUMBER).includes(test);
};

class App {

  private panel: Panel;
  private process: ProcessDisplay;
  private display: MainDisplay;
  private state: State;

  constructor(rootElement: HTMLElement) {
    // コンポーネントの設置
    this.panel = new Panel();
    this.process = new ProcessDisplay();
    this.display = new MainDisplay();

    this.panel.add(this.process);
    this.panel.add(this.display);

    this.handleClick = this.handleClick.bind(this);

    BUTTONS.forEach(props => {
      this.panel.add(
        new Button({
          ...props,
          onClick: this.handleClick
        })
      );
    });

    rootElement.appendChild(this.panel.element);

    this.switchState(InitialState.instance);
  }

  /**
   * ボタンクリック時に、ボタンのタイプに応じてStateの各メソッド呼び出し
   * @param {BUTTON_TYPE} type
   * @param {String} value 
   */
  private handleClick(type: BUTTON_TYPE, value: NUMBER | OPERATOR) {
    switch (type) {
      case BUTTON_TYPE.NUMBER:
        if (isNUMBER(value)) {
          this.state.inputNumber(this, value);
          break;
        }
      case BUTTON_TYPE.PI:
        this.state.inputPi(this, Math.PI);
        break;
    // ...
  }

  // 状態切替
  switchState(nextState: State) {
    this.state = nextState;
  }
  // 数値表示
  displayNumber(value: number | NUMBER) {
    this.display.setNumber(value);
  }
  // 数値追加表示
  addDisplayNumber(value: NUMBER) {
    this.display.addNumber(value);
  }
  // 演算子登録
  setOperator(operator?: OPERATOR) {
    this.process.setOperator(operator);
  }
  // 数値確定
  determineNumber() {
    /**
     * MainDisplayクラスが保持する入力値を取得して、
     * ProcessDisplayクラスの入力履歴に登録する
     */ 
    const num: number = this.display.getNumber();
    this.process.setNumber(num);
  }
  // 括弧利用中確認
  checkParenMode() {
  }
  // 左括弧登録
  setLeftParen() {
  }
  // 右括弧登録
  setRightParen() {
  }
  // 符号反転
  invertSign() {
  }
  // 演算実行
  executeCalculation() {
    /**
     * ProcessDisplayクラスが保持する入力履歴を取得して、四則演算関数へ渡す
     * 演算結果を取得して、MainDisplayクラスで表示させる
     */ 
    const result: number = Calc(this.process.getStack());
    this.display.setNumber(result);
  }
  // 表示1文字クリア
  backSpaceDisplay(): boolean {
  }
  // 表示クリア
  clearDisplay() {
  }
  // 履歴クリア
  clearHistory() {
  }
  // 全てクリア
  clearAll() {
  }
  // エラー!!!
  setError(e: Error) {
  }
  // ...
}

/src/app.ts | GitHub

型ガード(Type Guard)

Appクラスにて、クリック時にボタンから呼ばれるコールバック関数handleClick()の第二引数valueは、列挙型のNUMBERもしくはOPERATORが渡されます。
一方、転送先のStateクラスの各メソッドは、どちらかしか受付けません。
NUMBERであるかを確認する関数isNUMBER()で、事前に型ガードを施しています。

TypeScriptでみかける"is"というキーワードについて

状態(State)クラス

TypeScriptにあってJavaScriptにない、Interfaceを利用して上記の状態遷移を実現します。

Stateパターン.png

StateのInterfaceの定義

それぞれのボタンごとに、クリック時の振る舞いを定義するメソッドを用意します。
AppクラスのhandleClick()メソッドから呼び出されます。

interface State {
  // 数値ボタンクリック
  inputNumber(app: App, value: NUMBER | number): void;
  // 円周率ボタンクリック
  inputPi(app: App, value: number): void;
  // 演算子ボタンクリック
  inputOperator(app: App, value: OPERATOR): void;
  // 左括弧ボタンクリック
  inputLeftParen(app: App): void;
  // 右括弧ボタンクリック
  inputRightParen(app: App): void;
  // 符号反転ボタンクリック
  inputInversion(app: App): void;
  // イコールボタンクリック
  inputEqual(app: App): void;
  // 一つ削除(←)ボタンクリック
  inputBack(app: App): void;
  // クリアボタンクリック
  inputClear(app: App): void;
  // オールクリアボタンクリック
  inputAllClear(app: App): void;
}

StateInterfaceの実装

上記インターフェースを実装する数値入力状態NumberStateは下記のとおりです。
渡されたAppクラスのインスタンスを必要に応じて操作するだけで、内部でデータは持ちません。
インスタンスを複数作成する必要が無い為、シングルトンで作成しています。

class NumberState implements State {
  // シングルトン
  private static _instance: NumberState;
  // コンストラクタがプライベートなので、外部からnewできない
  private constructor() { }
  // 静的プロパティからのみ取得可能
  static get instance(): NumberState {
    if (!this._instance) {
      this._instance = new NumberState();
    }
    return this._instance;
  }

  inputNumber(app: App, value: NUMBER) {
    app.addDisplayNumber(value);
  }
  inputPi(app: App, value: number) {
    app.displayNumber(value);
  }
  inputOperator(app: App, value: OPERATOR) {
    try {
      app.determineNumber();
      app.executeCalculation();
      app.setOperator(value);
      app.switchState(OperatorState.instance);
    } catch (e) {
      app.setError(e);
    }
  }
  inputLeftParen(app: App) {
    app.determineNumber();
    app.setOperator(OPERATOR.MULTIPLY);
    app.setLeftParen();
    app.switchState(BeforeLeftSideState.instance);
  }
  inputRightParen(app: App) {
    if (!app.checkParenMode()) return;

    try {
      app.determineNumber();
      app.setRightParen();
      app.executeCalculation();
      app.switchState(AfterRightSideState.instance);
    } catch (e) {
      app.setError(e);
    }
  }
  inputInversion(app: App) {
    app.invertSign();
  }
  inputBack(app: App) {
    if (app.backSpaceDisplay()) {
      app.switchState(BeforeLeftSideState.instance);
    }
  }
  inputEqual(app: App) {
    try {
      app.determineNumber();
      app.executeCalculation();
      app.toggleAnswerMode(true);
      app.switchState(ResultState.instance);
    } catch (e) {
      app.setError(e);
    }
  }
  inputClear(app: App) {
    app.clearDisplay();
    app.switchState(BeforeLeftSideState.instance);
  }
  inputAllClear(app: App) {
    app.clearAll();
    app.switchState(BeforeLeftSideState.instance);
  }
}

未入力もしくは左括弧入力直後の状態BeforeLeftSideStateは下記の様になります。
右括弧などの入力不可のイベントは、空の関数でオーバーライドしています。

class BeforeLeftSideState implements State {

  private static _instance: BeforeLeftSideState;
  private constructor() { }
  static get instance(): BeforeLeftSideState {
    if (!this._instance) {
      this._instance = new BeforeLeftSideState();
    }
    return this._instance;
  }

  inputNumber(app: App, value: NUMBER | number) {
    app.displayNumber(value);
    app.switchState(NumberState.instance);
  }
  inputPi(app: App, value: number) {
    this.inputNumber(app, value);
  }
  inputOperator(app: App, value: OPERATOR) {
    app.displayNumber(NUMBER.ZERO);
    app.determineNumber();
    app.setOperator(value);
    app.switchState(OperatorState.instance);
  }
  inputLeftParen(app: App) {
    app.setLeftParen();
  }
  inputInversion(app: App) {
    app.invertSign();
    app.switchState(NumberState.instance);
  }
  inputRightParen() { /* 対応しない */ }
  inputEqual() { /* 対応しない */ }
  inputBack() { /* 対応しない */ }
  inputClear() { /* 対応しない */ }
  inputAllClear(app: App) {
    app.clearAll();
  }
}

State側で必要に応じてapp.switchState()を呼び出して他のStateへの切り替えを行い、App側ではStateの変更を意識する必要はありません。
便利ですね。他所でも使えそう。

TypeScript 2ではシングルトン(Singleton)パターンが短く書ける

おわりに

以前はVBAをよく書いており、その際は必ず型を指定していました。
その為JavaScriptを始めたばかりの頃は、型指定が出来ないことを気持ち悪いな~と感じていました。
いつの間にか慣れてしまってはいたものの、型はそれとなく意識しながら書いていたので、Typescriptにさほど戸惑うことはありませんでした。
やはり、型を指定できるほうが便利だし楽しいな~と感じました。

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

子要素のイベントを親要素へ伝搬させないようにする

バブリングというもの

イベントは伝搬される。
そのため、子要素で発生したイベント(例えばクリックイベントなど)は、
最終的にルート要素まで伝搬し、Clickイベントがあると実行される。
このイベントの伝搬のことをバブリングという。

イベント発生から伝搬されるまで

以下記事でわかりやすく説明されています。
DOMイベントのキャプチャ/バブリングを整理する 〜 JSおくのほそ道 #017

イベントが発生すると下記の流れでイベント伝播が発生します。

  • (キャプチャフェーズ) DOMツリーをたどってルート要素から発生要素を探しに行く
  • (ターゲットフェーズ)発生要素を検出する
  • (バブリングフェーズ) 今度はルート要素まで遡る バブリング.png

W3C

子要素のイベントを親要素へ伝搬させないようにする

本題ですが、
子要素のイベント関数内で、
stopPropagation()メソッドを実行してあげることで止めることが可能。
構文としては、

event.stopPropagation();

Clickイベントを追加した場合は、
親要素へのイベントの伝搬がないかを確認するようにする。

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

RabbitMQを使ってみる by JavaScript

初めまして、Guuuuuchanと申します。
初めての投稿です。

はじめに

最近、業務でRabbitMQを使いました。
初めて実装した時はドキュメントを読んでもどういうものかよくわからず、まずは動かして理解しようかと思いました。

備忘録的なところが多いので、多少説明が足りなくて申し訳ないかもしれないです。
その時はコメントで補足などもらえたら助かります。

また、僕の場合はJavaScript(Node.js)での実装になるので、JavaでRabbitMQ使いたい方はこちらの方が書いた素晴らしい記事が参考になるかと思います。

AMQPとは?

この辺はWikiとか公式を読んでもらった方が正確です。

AMQPは「Advanced Message Queuing Protocol」の略です。

プラットフォームに依存しないメッセージングミドルウェアです。メッセージをキューに入れて、非同期で取り出すといった単純なものです。

公式サイトには「To become the standard protocol for interoperability between all messaging middleware(すべてのメッセージングミドルウェア間の相互運用性の標準プロトコルになるため)」と書かれています。

  • 特徴
    • オープンスタンダードなアプリケーション層のためのプロトコル
    • メッセージ指向
    • キューイング
    • ルーティング
    • セキュア
    • 堅牢性

RabbitMQってなに?

RabbitMQはAMQPを実装したオープンソースのミドルウェアです。
似たようなミドルウェアとしてActiveMAやIBM MQ Lightなどがあります。

Erlang上で動いており、クラスタリングとフェイルオーバーを実現するためにOpen Telecom Platformフレームワーク上で構築されています。耐久性やスケーリングを考慮して設計されているようです。

クライアントアプリは以下の言語で動きます。

  • Python
  • Java
  • Ruby
  • PHP
  • C#
  • JavaScript
  • Go
  • Elixir
  • Objective-C
  • Swift
  • Spring AMQP

なぜRabbitMQを使う必要があるのか?

結論から言うと
1. スケーリングできる
1. 耐障害性がある
1. メンテナンスがしやすい

例えば大量のデータをバッチ実行したい場合、途中で失敗した場合最初からやり直す必要があります。
RabbitMQを使えば、バッチの引数に与えるデータをRabbitMQのキューに入れておくことで、バッチが途中で終了した場合でも失敗した箇所から実行できます。
そして、データが大量にある場合、キューを取り出すジョブを増やすことでスケールすることが可能です。
また、データをキューに入れておくことで、データを入れる側と取り出す側で任意のタイミングで処理を実行することができるので、どちらがメンテナンス状態になってもさほど大きな問題にはなりません。

準備

僕の環境では以下のツールが入っているので、もし動かしたい場合は先に以下のツールを先に入れておいてください。

  • Docker
  • Node.js
  • yarn

まずはRabbitMQサーバを立てよう!

DockerでコンテナにRabbitMQサーバを立てます。

まず、適当なディレクトリを作ります。

mkdir sample-rabbitmq-server
cd sample-rabbitmq-server

docker-compose.ymlを作成し、コンテナの内容を記載します。

touch docker-compose.yml

docker-composeの内容は以下のように記載します。

version: '3'

services:
  my-queue:
    container_name: rabbitmq
    image: rabbitmq:3.7.17-management
    ports:
      - '5672:5672'
      - '15672:15672'
    volumes:
      - rabbitmq-data:/var/lib/rabbitmq

volumes:
  rabbitmq-data:
docker-compose up -d

ブラウザを開き、http://localhost:15672にアクセスしてみましょう。

Screen Shot 2019-08-01 at 14.17.31.png

guest/guestでログインできます。

Screen Shot 2019-08-01 at 14.17.39.png

ログイン後はこんな画面になるはず

Screen Shot 2019-08-01 at 14.16.52.png

とりあえず、サーバのインストールは以上です。

また、rabbitmqのバージョンを投稿時の最新で記載していますが、こちらのページで最新バージョンを知ることができます。
RabbitMQ Changelog — RabbitMQ

RabbitMQのクライアントを使ってメッセージを送ってみよう

今回の投稿ではJavaScript(Node.js)で実装してみます。

まず、プロジェクトを作ります。yarn initでNodeのプロジェクトを作ります。successが出てくればOKです。

mkdir rabbitmq-client
cd rabbitmq-client
yarn init // 連打でOK

こんなメッセージが出てくるはず
yarn init v1.16.0
question name (rabbitmq-client):
question version (1.0.0):
question description:
question entry point (index.js):
question repository url:
question author:
question license (MIT):
question private:
success Saved package.json
✨  Done in 4.98s.

次に、RabbitMQのクライアントを入れましょう!

yarn add amqplib

メッセージを送るプログラムを作ります。

vim send.js

const amqp = require('amqplib/callback_api');

amqp.connect('amqp://localhost', (error0, connection) => {
    if (error0) throw error0;

    connection.createChannel((error1, channel) => {
        if (error1) throw error1;

        const queue = 'hello';
        const msg = 'Hello world';

        channel.assertQueue(queue, {
            durable: false
        });
        channel.sendToQueue(queue, Buffer.from(msg));

        console.log(" Sent message :", msg);
    });
    setTimeout(() => {
        connection.close();
        process.exit(0)
    }, 500);
});

保存したら、実行してメッセージを送ります。

node send.js

実行後、コンソールにメッセージが出るはずです。

$ node send.js
 Sent message : Hello world

メッセージが本当に送れているかWebUIで確認してみましょう。

画像3 (キューが追加されている)

次はメッセージを受信してみよう

メッセージを受信するプログラムを作ります。

vim send.js
var amqp = require('amqplib/callback_api');

amqp.connect('amqp://localhost', function(error0, connection) {
    if (error0) {
        throw error0;
    }
    connection.createChannel(function(error1, channel) {
        if (error1) {
            throw error1;
        }

        var queue = 'hello';

        channel.assertQueue(queue, {
            durable: false
        });

        console.log(" [*] Waiting for messages in %s. To exit press CTRL+C", queue);

        channel.consume(queue, function(msg) {
            console.log(" [x] Received %s", msg.content.toString());
        }, {
            noAck: true
        });
    });
});

実行してみます。こんな

$ node receive.js

こんなメッセージが出ればOKです。

[*] Waiting for messages in hello. To exit press CTRL+C
 [x] Received Hello world

Ctrl + Cを押すまで、待機状態になります。
別のコンソールで、send.jsを実行すると非同期でreceive.jsに受信されます。

その他

receive.jsを動かさない状態で、send.jsを何度か実行するとどんどんQueueにメッセージが溜まっていきます。

その時、receive.jsを起動すると、一気にキューを受け取ります。

また、複数のコンソールでreceive.jsを起動すると、ランダム?でどれかのreceive.jsに受信されます。キューからメッセージを受け取る時は一度しか取れないですからです。これは設定で変更することができます。

最後に

最近のフロントエンドでは、非同期処理が当たり前のように使われるようになってきました。
もしかしたら今後、RabbitMQを使うシーンがまたあるかもしれないです。

また、別の投稿でRabbitMQの使い方を紹介しようと思っています。

参考文献

業務で使う場合はやはり公式ドキュメントが一番よいと思います。
Messaging that just works — RabbitMQ

RabbitMQのチュートリアルがあるのでこちらを参考にしてください
Getting started with RabbitMQ — RabbitMQ

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

Node JSのコールバックパターン

継続渡しスタイル(continuation-passing style : CPS)

JavaScriptにおいてコールバック関数とは、ある関数を呼び出す時に、引数として指定する関数で、FuncAの処理が完了した時にFuncAの結果を通知するために起動される関数のことを指します。

同期的継続渡しスタイル

test.js
function add(a, b, callback) {
  callback(a + b)
}

console.log('before')
add(1, 2, result => console.log('Result : ' + result))
console.log('after')

結果

example.sh
$ node test.js
before
Result : 3
after

非同期CPS

test.js
function addAsync(a, b, callback) {
  setTimeout(() => callback(a + b), 100)
}

console.log('before')
add(1, 2, result => console.log('Result : ' + result))
console.log('after')

結果

example.sh
$ node test.js
before
after
Result : 3

継続渡しではないコールバック

test.js
const result = [1, 5, 7].map(element => element - 1)
console.log(result) // [0, 4, 6]

ちなみに以下のことはダイレクトスタイル(Direct Style:DS)と呼びます。

example.js
function add(a, b) {
  return a + b
}

console.log(add(1,2)) // 3

同期処理か非同期処理か

どっちでもいいですが、まず避けなければならないのは「一貫性がないAPI」です。

同期と非同期の混在

もっとも危険なのは、ある条件には同期処理、ある条件には非同期処理を行う関数です。
以下のコードは一貫性がないコードの例です。

test.js
const fs = require('fs')
const cache = {}

function inconsistentRead(filename, callback) {
  if(cache[filename]) {
     callback(cache[filename]) // cacheにデータがある場合、同期的に実行される。
  } else {
     // cacheにデータがない場合、ひい同期関数fs.readFile()を呼び出す。
     fs.readFile(filename, 'utf8', (err, data) => {
       cache[filename] = data
       callback(data)
     }
  }
}

混在がもたらす問題

test.js
const fs = require('fs')
const cache = {}

function inconsistentRead(filename, callback) {
  if(cache[filename]) {
     callback(cache[filename]) // cacheにデータがある場合、同期的に実行される。
  } else {
     // cacheにデータがない場合、ひい同期関数fs.readFile()を呼び出す。
     fs.readFile(filename, 'utf8', (err, data) => {
       cache[filename] = data
       callback(data)
     })
  }
}

function createFileReader(filename) {
  const listeners = []
  inconsistentRead(filename, value => {
    listeners.forEach(listener => listener(value))
  })

  return {
    onDataReady: listener => listeners.push(listener)
  }
}

const reader1 = createFileReader('data.txt')
reader1.onDataReady(data => {
  console.log('First call data: ' + data)
  // しばらくしてから同じファイルを呼び出す
  const reader2 = createFileReader('data.txt')
  reader2.onDataReady(data => {
    console.log('Second call data: ' + data)
  })
})

結果

example.sh
$ node test.js
First call data: some data

解決策1同期APIの利用

同期関数を使えば解決できます。

test.js
const fs = require('fs')
const cache = {}
function consistentReadSync(filename) {
  if (cache[filename]) {
    return cache[filename]
  } else {
    cache[filename] = fs.readFileSync(filename, 'utf8') // 同期関数使用
    return cache[filename]
  }
}

しかし、以下の留意点があります。
・ある機能に関して常に同期バージョウンが用意されているとは限らない
・他のリクエストは処理待ちとなるため、全体的にパフォーマンスが落ちる。

解決策2遅延実行

同期的なコールバックが「将来」起動されるようにスケジュールする。

test.js
const fs = require('fs')
const cache = {}
function consistentReadAsync(filename) {
  if (cache[filename]) {
    process.nextTick(() => callback(cache[filename]))
  } else {
     fs.readFile(filename, 'utf8', (err, data) => {
       cache[filename] = data
       callback(data)
     }
  }
}

Node.jsのコールバック

エラーの伝播

test.js
const fs = require('fs')
function readJSON(filename, callback) {
  fs.readFile(filename, 'utf8', (err, data) => {
    let parsed
    if (err) {
      return callback(err)
    }

    try {
      parsed = JSON.parse(data)
    } catch (err) {
      return callback(err)
    }
    callback(null, parsed)
  })
}

キャッチされない例外

test.js
const fs = require('fs')
function readJSONThrows(filename, callback) {
  fs.readFile(filename, 'utf8', (err, data) => {
    let parsed
    if (err) {
      return callback(err)
    }
    callback(null, JSON.parse(data))
  })
}

try {
  readJSONThrows('nonjson.txt', err => {
    if (err) { console.log(err) } else { JSON.stringify(json) }
  })
} catch(err) {
   console.log('こうしてもキャッチはできません。')
}

上記の場合、キャッチされません。なぜなら、readJSONThrowsを呼び出すスタックとコールバックを呼び出すスタックがことなるからです。
これは下記のようにコードを作ったらキャッチされます。

test.js
readJSONThrows('nonjson.txt', err => {
  if (err) { console.log(err) } else { JSON.stringify(json) }
})

process.on('uncaughtException', (err) => {
  console.error('ここでキャッチ')
  process.exit(1) // エラーコード1で終了。これがないと実行を継続する。
})

参考文献
Node.jsデザインパターン 第2版 - Mario Casciaro (著), Luciano Mammino (著), 武舎 広幸 (翻訳), 阿部 和也 (翻訳)

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

緯度と経度を入力することによって現在位置から目的地の方角・距離を表示するPGM

緯度と経度を入力することによって現在位置から目的地の方角・距離を表示するPGMを作成しました
ローカルのPC上では動作しましたがスマホ等他ではどうも動かないようです(ダメダメですね...原因がわかり次第修正する予定ですがバグ内容・原因がわかる方いたら返信お願いします)
ちなみに現在位置の緯度経度は最初に距離再計算のボタン
を押すことで自動で入力されるようになっています...
作成の目的はどこかの話でグーグルマップの通信のデータ量が多すぎるとのことを聞いた
覚えがありましたので、最低限方向と距離だけわかるようなPGMがあれば良いな、と思ったからです...

参考githubURL: https://github.com/NanjoMiyako/ZatuMap

動作時の画像
screenshot1.JPG

参考URL:

JavaScriptによる位置情報の取得を簡単に解説(geolocation) | CodeCampus


Geolocation.getCurrentPosition() - Web API | MDN


スマホのセンサーから上下左右の傾きと方向をJavascriptで取得する DeviceOrientationEvent | ARAKAZE NOTE


2つの座標間の距離を求める - Qiita


HTML5 Canvas 入門 - Qiita


2地点間の距離と方位角 - 高精度計算サイト


Javascriptで2地点間の距離と方角を計算するライブラリ - 動かざることバグの如し


緯度経度から距離と方位を求めるプログラム - ロジカルアーツ研究所


緯度経度から距離と方位を求める方法 - ロジカルアーツ研究所


ラジアンから角度を求める関数をMathオブジェクトに定義する - Qiita


lineWidth = 線の幅-Canvasリファレンス


strokeStyle = "色・グラデーション・パターン"-Canvasリファレンス


canvas に図形を描く - 開発者ガイド | MDN

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

ESLint v6.2.0

v6.1.0 | 次 (2019/08/31 JST)

ESLint 6.2.0 がリリースされました。

お盆休み中に ESTree 仕様が更新されたので、BigInt と Dynamic Imports 構文をサポートしました。

既知の問題:

質問やバグ報告等ありましたら、お気軽にこちらまでお寄せください。

? 日本語 Issue 管理リポジトリ
? 日本語サポート チャット
? 本家リポジトリ
? 本家サポート チャット

? 本体への機能追加

BigInt と Dynamic Imports をサポートしました。

これらを利用するには、env.es2020 設定と、(デフォルトのパーサーでは) parserOptions.ecmaVersion 設定を指定します。

.eslintrc.json
{
    "parserOptions": {
        "ecmaVersion": 2020
    },
    "env": {
        "es2020": true
    }
}
  • parserOptions.ecmaVersion 設定により、新しい構文 (import("source")100n) が有効になります。
  • env.es2020 設定により、新しいグローバル変数 (BigInt, BigInt64Array, BigUint64Array) が有効になります。

設定ファイルに noInlineConfig プロパティが追加されました。

? RFC 22, #12091

.eslintrc.json
{
    "noInlineConfig": true
}

この設定は --no-inline-config CLI オプションとほぼ同等です。今まで CLI でしか指定できなかったものが設定ファイルでも指定できるようになりました。

? 新しいルール

function-call-argument-newline

関数呼び出しの各実引数の間に改行を入れるかどうかを矯正するスタイル ルールが追加されました。

/* eslint function-call-argument-newline: [error, consistent] */

//✘ BAD
foo(a, b,
    c);

//✔ GOOD
foo(a, b, c);
foo(
    a,
    b,
    c
);

Online Demo

? オプションが追加されたルール

特になし。

✒️ eslint --fix をサポートしたルール

特になし。

⚠️ 非推奨になったルール

特になし。

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

Nuxt で全てのページで index.html をルーティングの対象にする方法

  1. Nuxt は vue-router をつかってルーティングしている
  2. vue-router は alias つかって対象のパスをルーティングに追加できる
  3. nuxt.config.jsextendRoutes で vue-router のルーティングを変更できる

ということをふまえて、 extendRoutes をつかって全ての route に index.html を付与した alias を追加することで実現できました。

nuxt.config.js
import { resolve } from 'path'

const nuxtConfig = {
  router: {
    extendRoutes(routes, resolve) {
      for (const route of routes) {
        route.alias = resolve(route.path, 'index.html')
      }
    }
  }
}

参考

vue-router の alias について
extendroutes について

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

Nuxt.jsでウェブストレージを扱う

はじめに

ウェブストレージ便利ですよね、特にローカルストレージはCookieが使えないPWAや、DBを用意するほどでもないちょっとしたデータを保存したいときに使うことが多いのではないかと思います。

ただ、いざウェブストレージのデータを扱おうと実装を進めるとデータの同期や保存のタイミング等でつっかかることがままあります。
そこで今回はNuxt.jsでウェブストレージを簡単に扱う手法を考えましたので簡単にご紹介しようと思います。

実装方針

コンポーネントやページから直接ウェブストレージにアクセスしていると処理やデータが散乱してしまい辛い状況に陥りやすいです。
そこで、ウェブストレージの参照や処理は全てストアに一本化します。
各コンポーネントやページは共通のステートやアクションを参照することになりますのでデータが非常に扱いやすくなります。

実装

入力した値をウェブストレージに保存する簡単なデモを作成します。
今回はローカルストレージを使用しますがセッションストレージでも同様の方法で対応可能です。

ウェブストレージ操作用のクラスを作成

1つのウェブストレージを扱うだけならクラスを作るまでもないのですが、今回は複数のウェブストレージを管理することを前提に作成しておきます。
実装例ですので登録・削除する機能だけ用意してあります。

lib/storage.js
export default class {
  constructor(key) {
    this.key = key
    this.data = []
  }
  init() {
    this.data = JSON.parse(localStorage.getItem(this.key)) || []
  }
  save() {
    localStorage.setItem(this.key, JSON.stringify(this.data))
  }
  regist(payload) {
    this.data.push(payload)
    this.save()
  }
  remove(payload) {
    this.data = this.data.filter(el => el !== payload)
    this.save()
  }
}

クライアント起動時のプラグイン作成

クライアント起動時にウェブストレージのデータをストアへ注入する必要があるのですが、Nuxtのストアにはクライアント起動時の関数は用意されていませんのでその機能を作成します。
Nuxtのデフォルトにあるアクション「nuxtServerInit」にならって「nuxtClientInit」という名称で管理します。

plugins/nuxt-client-init.js
export default ({ store }) => {
  for (const key in store._actions) {
    if (key.match(/nuxtClientInit/)) {
      store.dispatch(key)
    }
  }
}
nuxt.config.js
plugins: [
  { src: '~/plugins/nuxt-client-init', mode: 'client' }
]

nuxt-client-init-moduleというモジュールが既にあるのですが、ルートモジュールでのみ機能するようです。
名前空間付きモジュールで「nuxtClientInit」を利用したい場合は上記のようなプラグインを作成してください。

ストア実装

ストアのアクションはあくまでインスタンスの関数を実行し、その結果をステートへ返すだけのシンプルな設計とします。
アクションに直接ウェブストレージ周りの機能を実装してもよいのですが、複数ウェブストレージを扱ったり機能が複雑になってくると煩雑になりやすいので、ウェブストレージの処理とストアの処理を明確に分担するように設計しています。

store/message.js
import LocalStorage from '~/lib/storage'

const storageMessage = new LocalStorage('message')

export const state = () => ({
  data: []
})

export const mutations = {
  dataUpdate(state, payload) {
    state.data = [...payload]
  }
}

export const actions = {
  nuxtClientInit({ commit }) {
    storageMessage.init()
    commit('dataUpdate', storageMessage.data)
  },
  regist({ commit }, payload) {
    storageMessage.regist(payload)
    commit('dataUpdate', storageMessage.data)
  },
  remove({ commit }, payload) {
    storageMessage.remove(payload)
    commit('dataUpdate', storageMessage.data)
  }
}

完成

あとは通常のストアと同様に利用できます。

pages/index.vue
<template>
  <div>
    <form @submit.prevent="regist()">
      <input type="text" v-model="message">
      <button type="button" @click="regist()">
        登録
      </button>
    </form>
    <ul>
      <li v-for="(item, index) in messageData" :key="index">
        {{ item }}
     <button type="button" @click="remove(item)">
            削除
         </button>
      </li>
    </ul>
  </div>
</template>

<script>
import { mapState } from 'vuex'

export default {
  data() {
    return {
      message: ''
    }
  },
  computed: {
    ...mapState({
      messageData: state => state.message.data
    })
  },
  methods: {
    regist() {
      if (this.message) {
        this.$store.dispatch('message/regist', this.message)
        this.message = ''
      }
    },
    remove(payload) {
      if (window.confirm('削除しますか?')) {
        this.$store.dispatch('message/remove', payload)
      }
    }
  }
}
</script>

おまけ(Vueインスタンス版)

小規模でストア用意するほどでもないよって人向けにプラグインでVueインスタンスへ注入する方法も記載しておきます。
以下のデモでは上記で使ったクラスを流用してください。

プラグインを作成する

Vueインスタンスへ注入するプラグインファイルを作成します。

plugins/storage.js
import Vue from 'vue'
import LocalStorage from '~/lib/storage'

Vue.prototype.$storageMessage = new LocalStorage('message')
Vue.prototype.$storageMessage.init()
nuxt.config.js
  plugins: [
    { src: '~/plugins/storage', mode: 'client' }
  ]

完成

設定はこれだけで後はページやコンポーネントからウェブストレージのデータを操作できます。

pages/index.vue
<template>
  <div>
    <form @submit.prevent="regist()">
      <input type="text" v-model="message">
      <button type="button" @click="regist()">
        登録
      </button>
    </form>
    <ul>
      <li v-for="(item, index) in messageData" :key="index">
        {{ item }}
     <button type="button" @click="remove(item)">
            削除
         </button>
      </li>
    </ul>
  </div>
</template>

<script>
import { mapState } from 'vuex'

export default {
  data() {
    return {
      messageData: [],
      message: ''
    }
  },
  mounted() {
    this.update()
  },
  methods: {
    update() {
      this.messageData = this.$storageMessage.data
    },
    regist() {
      if (this.message) {
        this.$storageMessage.regist(this.message)
        this.update()
        this.message = ''
      }
    },
    remove(payload) {
      if (window.confirm('削除しますか?')) {
        this.$storageMessage.remove(payload)
        this.update()
      }
    }
  }
}
</script>

さいごに

ウェブストレージのデータを更新するときに「ウェブストレージにあるデータ」と「ウェブストレージから引っ張ってきたデータ」と2つを管理する必要があるのでそこがもたつきやすいですよね。
今回のように「ウェブストレージ←→ストア←→ページ・コンポーネント」という形で一方向の処理で管理すると、土台さえ作ってしまえばウェブストレージ周りの処理を意識せずに扱えるのでオススメです。
大した手法ではないのですがウェブストレージの実装で手をこまねいている方は参考にしていただければ幸いです!

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

obniz OSをESP32(NefryBT)に入れて動かそう!

こんにちは!わみです!
今日は先日リリースされたobnizOSを使い始めてみたので、どんな感じなのかも合わせて書いていこうと思います。

そもそも「obniz」ってなに?って方は「arduinoとは全く違う、IoTツールobnizのしくみ」をご覧ください。

なにができるのか

HTML+JSでハードウエア(ESP32(NefryBT))を動かしてみた動画です!

JavaScriptやpythonでハードウエアを動かすことができる「obniz」ただ、6000円近くするので少々お値段がします…

その技術をお店で1500円ぐらいで売っているESP32のボードにインストールができるのが先日発表された「obnizOS」なのです!

私が作っている「Nefry BT」というデバイスにインストールしてみたのでその方法とどんな感じなのかをまとめてみようと思います。
ESP32を搭載したボードであれば、同じような手順で行うことができるのでぜひお試しください。

(ESP32-picoについては未サポート(具体例:M5stickC))

インストール

ライセンスを購入する

https://obniz.io/ja/console
上記ページから「デバイス」を選択し、「obniz OSのライセンスを新規購入」をクリックします。

ライセンスの選択画面で「Hobby」ライセンス、「新規にobnizIDを発行」、個数を選び、「金額を確認」をクリックします。
カードの情報がない場合、このタイミングで入力します。

確認画面が表示されて、問題なければ「ライセンスを購入」を押します。

購入が完了すると、「DeviceKey」をダウンロードできます。
これはインストール時に使用するので必ずダウンロードしておいてください。
(画像取り忘れたので公式ドキュメントの画像…)

前提条件

obniz cliを使用しますのでpython3.4以上のものをインストールしておいてください。

py -3 -Vでバージョン情報を確認することができます。
インストールされていれば、上記のコマンドをターミナルで実行すると次のようにバージョンが表示されます。

表示されない場合は、インストールされているか確認したり、pathが通っているか確認してください。

obniz_cliをインストール

obniz_cliとは、簡単にobnizOSをインストールするために作られたアプリになります。それを使って、セットアップを行っていきたいと思います。

ターミナルでpip3 install obniz_cliと入力します。

インストールが開始されるはずです!「Successfully installed obniz_cli」とでれば大丈夫です。

Nefry BTにobnizOSをインストール

Nefry BTをPCと接続し、obnizOSをインストールしていこうと思います。

ターミナルでobniz_cli flashosと入力します。

ESP32を接続しているポートを選択画面がでるので、ポートの横に書かれている数字(今回なら0)を入力します。ここで選んだポート名(今回ならCOM5)を覚えておいてください。

入力するとインストールが開始されます。

しばらくするとインストールが完了します。

次に無線LANの設定とライセンスの適用を行っていきます。

ターミナルでpython -m serial.tools.miniterm ポート名 115200と入力します。

ポート名は先ほど選んだものに変更しておいてください。

指示に従って、次の項目を入力します。

  • DeviceKey:購入時に取得したファイルに書かれた文字列
  • SSID:接続したい無線LANのもの
  • PASS:接続したい無線LANのもの
  • 固定IP or DHCP:基本的にはDHCPの「0」を選択する
    • 固定IPの場合 下記の項目を設定する
      • IP Address
      • Subnetmask
      • Default gateway

設定された項目が正しければ、最後に「Online」と表示されます。

正しくない場合、再度正しいデータを入力されるように求められるので再度行ってください。

JavaScriptでコードを書いてみる

JavaScriptでハードウエアが動かせるので少しコードを書いてみました。

なにがよいか私なりのメリットを挙げてみます。

  • HTMLと合わせて簡単に操作画面をつくれるところ
  • 開発環境を作成しなくてよいところ(公式サイトにエディタがある)
  • いくつかの部品のライブラリがあるのも始めやすいところ https://obniz.io/ja/sdk/parts/LED/README.md

みたいなところですかね!ここもメリットだよってのがあれば教えてもらえると!

動かしてみる

「Nefry BT」で動かせるようにボタンとフルカラーLEDをコントロールするプログラムをJavaScriptで書いてみたので試してみましょう!

http://obniz.io/ja/console/program

上記のURLをクリックすると、購入したobniz idが選択できるはずです。

選択して、「開く」を押してみましょう!

NefryBTではない別のESP32ボードの場合はそのままサンプルプログラムを動かしてみましょう!

サンプルプログラムがあるので一旦削除して、次のコードを貼りましょう!
コード内にある「your obniz id」と書かれたところを先ほど選んだobniz idに置き換えることを忘れないでください!

<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css">
    <script src="https://obniz.io/js/jquery-3.2.1.min.js"></script>
    <script src="https://unpkg.com/obniz@2.2.0/obniz.js" crossorigin="anonymous"></script>
  </head>
  <body>

    <div id="obniz-debug"></div>
    <h3>obnizOS on Nefry</h3>
    <button class="btn btn-primary m5" id="on">ON</button>
    <button class="btn btn-primary m5" id="off">OFF</button>

    <script>
      var obniz = new Obniz("your obniz id");

      obniz.onconnect = async function () {
        var color = obniz.wired("WS2812", {din: 16});
        color.rgb(0xFF, 255, 0);
        var led = obniz.wired("LED", {anode:26}); // io0 is anode. cathode is connected obniz GND other way.

        obniz.getIO(4).pull("3v");
        obniz.getIO(4).input(function(value){
          console.log("changed to " + value);
          color.rgb(Math.floor( Math.random() * (256) ), Math.floor( Math.random() * (256) ), Math.floor( Math.random() * (256) ));
        });
        $("#on").click(function(){
          led.on();
        })

        $("#off").click(function(){
          led.off();
        })
      }

    </script>
  </body>
</html>

貼り付けたら、右上の「実行」ボタンを押してください。

Nefry BTのSWを押すと、フルカラーLEDがランダムに光ります。
HTMLのボタンを押すと、26ピンに接続されたLEDが点灯したり、消灯したりします。

ピンの指定

ESP32のピン番号を直接指定します。

Nefryの場合、直接基板上に書かれていないので、次の画像を参考にしてください。

  • Nefry BT(無印)

nefrybt_pinmap.png

  • Nefry BT R2/R3

NefryBTr2_r3_pinmap.png

まとめ

obniz OSでこれからもっと便利にIoTを始められるようになるのではないでしょうか?

コードの使い勝手としては、obnizボードと大差ない気がします。ただ、ReadOnlyのIOやアナログ入力など制約があるところやモータドライバーがついていないのでモータを直接動かせないところもあるのでそのあたりはobnizボードのところが優れてるかなと思います。

Nefry BTではArduinoでコードを書くのが普通ですが、JavaScriptであったとしても私としてはハードウエアが好きになって、いろんなものが生まれてくるとすごくいいなと思っています。

ここまで読んでくれた方へ

ここまで読んでくださりありがとうございます。

いいねやコメント、SNSでの共有等をしてくださると、今後の励みになります。よろしくお願いします。

良かったらTwitterもフォローしてね

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

【備忘録④】React & TypeScript & Webpack4 & Babel7 & dev-server の最小構成ボイラープレートの作成 -Storybookの導入-

モジュール追加

# Storybookをインストール
$ npm install -D @storybook/cli

# プロジェクトへ移動
$ cd <プロジェクトパス>

# Storybookの設定ファイル等の雛形を作成
$ npx -p @storybook/cli sb init

「.storybook」ディレクトリに設定ファイルが作成される。

「stories」ディレクトリにストーリーのサンプルが作成される。

.storybook
├── config.js
└── webpack.config.js

stories
└── index.stories.js

package.jsonにStorybook起動、ビルド用コマンドが自動で追加される。

"storybook": "start-storybook -p 6006",
"build-storybook": "build-storybook"

また、基本となるアドオンとプロジェクトに使用しているフレームワーク(ここではReact)用のもジュルも自動で追加される。

"@storybook/addon-actions": "^5.1.9",
"@storybook/addon-links": "^5.1.9",
"@storybook/addons": "^5.1.9",
"@storybook/react": "^5.1.9",

起動確認

$ npm run storybook

「localhost:6006」にアクセス。

スクリーンショット 2019-07-24 17.03.39.png

アドオンの追加

便利なアドオンを追加する。

モジュール名 バージョン 詳細
@storybook/addon-knobs 5.1.9 プロパティを変更できる画面を追加
@storybook/addon-viewport 5.1.9 ビューポートを切り替えるボタンを追加
react-docgen-typescript-loader 3.1.0 ストーリーの例とか説明を追加
@storybook/addon-info 5.1.9 TypeScriptの型からコンポーネントのプロパティ説明を追加
@storybook/addon-console 1.2.1 Actionsタブにコンソールログを表示する
react-docgen-typescript-webpack-plugin 1.1.0 react-docgen-typescript-loader用の追加モジュール

モジュールをインストールする。

$ npm install -D @storybook/addon-knobs @storybook/addon-viewport react-docgen-typescript-loader @storybook/addon-info @storybook/addon-console react-docgen-typescript-webpack-plugin

TypeScriptで使用するので型定義も追加する。

$ npm install -D @types/storybook__react @types/storybook__addon-info @types/storybook__addon-actions @types/storybook__addon-knobs @types/storybook__addon-links

アドオン追加・設定

ここら辺が参考になる

https://storybook.js.org/docs/addons/using-addons/

「.storybook」ディレクトリの下にaddons.jsファイルを作成する。

storybook/addons.js
# .storybook/addons.js

import '@storybook/addon-actions/register';
import '@storybook/addon-links/register';
import '@storybook/addon-knobs/register';
import '@storybook/addon-viewport/register';
import '@storybook/addon-console';

設定ファイルを修正

storybook/config.js
# .storybook/config.js

import { configure, addDecorator } from '@storybook/react';
import { setConsoleOptions } from '@storybook/addon-console';
import { withInfo } from '@storybook/addon-info';
import { withKnobs } from '@storybook/addon-knobs';

setConsoleOptions({
  panelExclude: [],
});

const req = require.context('../src', true, /.stories.(tsx|js)$/);

function loadStories() {
  addDecorator(withInfo);
  addDecorator(withKnobs);

  req.keys().forEach(req);
}

configure(loadStories, module);
storybook/webpack.config.js
# .storybook/webpack.config.js

const path = require("path");
const SRC_PATH = path.join(__dirname, '../src');

module.exports = ({
  config
}) => {
  config.module.rules.push({
    test: /\.(ts|tsx)$/,
    include: [SRC_PATH],
    use: [{
        loader: require.resolve('babel-loader'),
        options: {
          presets: [
            ['react-app', {
              flow: false,
              typescript: true
            }]
          ],
        }
      },
      {
        loader: require.resolve('react-docgen-typescript-loader')
      }
    ]
  });
  config.resolve.extensions.push('.ts', '.tsx');
  return config;
};

確認用コンポーネントの作成

ボタンクリック時の動作、テキストの変更、表示切り替えが設定できる簡単なコンポーネントを用意する。

src/component/Button/index.tsx
# src/component/Button/index.tsx

import React from 'react';

export interface IButtonProps {
  text: string;
  flag?: boolean;
  action(): void;
}

const Button = (props: IButtonProps) => {
  const { text, flag, action } = props;
  return (
    <React.Fragment>
      { flag && <p>{text}</p> }
      <button onClick={ action }>Button</button>
    </React.Fragment>
  );
};

export default Button;

Storybookでの動作確認用コンポーネントを作成する。

src/component/Button/index.stories.tsx
# src/component/Button/index.stories.tsx

import React from 'react';

import {storiesOf} from '@storybook/react';
import {text, boolean} from '@storybook/addon-knobs';
import {action} from '@storybook/addon-actions';

import Button from '../Button';

const components = storiesOf('Components', module);
components.add('Button', () => (
  <Button
    text={text('テキスト', 'ボタンですよ')}
    flag={boolean('テキスト表示', true)}
    action={action('ボタンを押した!')}
  />
));

起動確認

$ npm run storybook

「localhost:6006」にアクセス。

ボタンクリック時の動作、テキストの変更、表示切り替えができることを確認。

ezgif.com-video-to-gif.gif

以上で確認完了。

作成したボイラープレートは こちら

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

既存プロジェクトのESLintをアップデートする

はじめに

既存プロジェクトのESLintのバージョンを見たら v1.10.3

さすがに古いので、最新バージョン v6.2.0 までジャンプアップします。

既存のESLintをアンインストール

$ yarn remove eslint

ESLintをインストール

$ yarn add -D eslint
## eslint v6.2.0

—initオプションで初期設定する

configファイルを作成するため、 --init オプションを使います。

$ ./node_modules/.bin/eslint --init

対話形式で色々聞かれるので、適宜選択していきます。

? How would you like to use ESLint?
  To check syntax only
❯ To check syntax and find problems
  To check syntax, find problems, and enforce code style

? What type of modules does your project use? (Use arrow keys)
❯ JavaScript modules (import/export)
  CommonJS (require/exports)
  None of these

? Which framework does your project use?
  React
  Vue.js
❯ None of these

? Where does your code run? (Press <space> to select, <a> to toggle all, <i> to invert selection)
❯◉ Browser
 ◯ Node

? How would you like to define a style for your project? (Use arrow keys)
❯ Use a popular style guide
  Answer questions about your style
  Inspect your JavaScript file(s)

? Which style guide do you want to follow? (Use arrow keys)
❯ Airbnb (https://github.com/airbnb/javascript)
  Standard (https://github.com/standard/standard)
  Google (https://github.com/google/eslint-config-google)

? What format do you want your config file to be in? (Use arrow keys)
❯ JavaScript
  YAML
  JSON

? Would you like to install them now with npm? 
❯ Yes
    No

これで .eslintrc.js が作成されているはずです。

eslintrc.js
module.exports = {
      env: {
        browser: true,
        es6: true
      },
      extends: [
        'airbnb-base'
      ],
      globals: {
        Atomics: 'readonly',
        SharedArrayBuffer: 'readonly'
      },
      parserOptions: {
        ecmaVersion: 2018,
        sourceType: 'module'
      },
      rules: {
      }
    };
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

既存プロジェクトのESLintをアップデート&自動修正させる

はじめに

既存プロジェクトのESLintのバージョンを見たら v1.10.3
さすがに古いので、最新バージョン v6.2.0 までジャンプアップします。

ここでは主にeslintのインストールとファイル一括自動修正、
あとはオマケでVS Codeでたまに起きるエラーについて書きました。

既存のESLintをアンインストール

$ yarn remove eslint

ESLintをインストール

$ yarn add -D eslint
## eslint v6.2.0

—initオプションで初期設定する

configファイルを作成するため、 --init オプションを使います。

$ ./node_modules/.bin/eslint --init

対話形式で色々聞かれるので、適宜選択していきます。

? How would you like to use ESLint?
  To check syntax only
❯ To check syntax and find problems
  To check syntax, find problems, and enforce code style

? What type of modules does your project use? (Use arrow keys)
❯ JavaScript modules (import/export)
  CommonJS (require/exports)
  None of these

? Which framework does your project use?
  React
  Vue.js
❯ None of these

? Where does your code run? (Press <space> to select, <a> to toggle all, <i> to invert selection)
❯◉ Browser
 ◯ Node

? How would you like to define a style for your project? (Use arrow keys)
❯ Use a popular style guide
  Answer questions about your style
  Inspect your JavaScript file(s)

? Which style guide do you want to follow? (Use arrow keys)
❯ Airbnb (https://github.com/airbnb/javascript)
  Standard (https://github.com/standard/standard)
  Google (https://github.com/google/eslint-config-google)

? What format do you want your config file to be in? (Use arrow keys)
❯ JavaScript
  YAML
  JSON

? Would you like to install them now with npm? 
❯ Yes
    No

これで .eslintrc.js が作成されているはずです。

eslintrc.js
module.exports = {
      env: {
        browser: true,
        es6: true
      },
      extends: [
        'airbnb-base'
      ],
      globals: {
        Atomics: 'readonly',
        SharedArrayBuffer: 'readonly'
      },
      parserOptions: {
        ecmaVersion: 2018,
        sourceType: 'module'
      },
      rules: {
      }
    };

—fixオプションで自動修正

eslintのルールも新しくなりコードの修正が必要です。

ファイルを一つひとつ開いて直すのは非効率なので、 —fix オプションを使って一括修正します。

## srcフォルダ内のjsファイルを修正する
$ ./node_modules/.bin/eslint --fix src/

自動修正できるものは修正され、できないものはエラーメッセージ等が吐き出されます。

変更前のルールと差異が大きい&ファイル量が多いとエラーメッセージだらけになると思いますが、どうかめげずに。

おまけ

複数プロジェクトでeslintを使ってて、かつVS Codeのprettier(プラグイン)の自動修正を使ってると、たまに変な修正をすることがあります。
そんなときはprettierプラグインをアンインストールして再度インストールすると直ります。

たぶん一つのeslintのルールに依存してしまってるぽいですね。
複数プロジェクトを行き来するときは気をつけましょう。

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

誰でもできるオリジナルwebサービスを着実にリリースする手順と考え方

スクリーンショット 2019-08-16 18.36.52.png

夏休みに小さいオリジナルのwebサービスを作ったので、構想からリリースまでに使ったツールなどオリジナルwebサービスをリリースするまでの手順と考え方ご紹介したいと思います:smiley:

技術的なことにはほぼ言及していません。これから初めてオリジナルのwebサービスを作ろうと思っている方にこんな感じでやってるのねって思ってもらえればうれしいです。

いままで作ったサービスはこちらにまとめています(ページからリンクしているそれぞれのサービスのまとめページはgif貼りすぎてめちゃくちゃ重いので開かないの推奨です;;)

作ったもの

名前
しるQ 「自分に気づこう」
https://siruq.site

コンセプト
しるQは毎日変わる問いに答えることで本当の自分を発見するサービスです。140文字にまとめてシェアして他の参加者の価値観と比べてみましょう!

作成期間
22時間くらい

なぜ作ったか

  • 最近自己認知とかコーチングに興味があるのでそのテーマで何か作りたかった
  • vueとfirebase使ってみたかった

お世話になったサービス
Sketch
Zoho show
hue360
adobe color
お名前.com
Firebase Realtime Databse
ためしがき

それでは以下の順に時系列でご紹介していきます!
1. サービス企画
2. サービスの具体化
3. 実装

企画

8/13日(火) 13:00

数ヶ月前からなんとなく以下のような構想が頭の中にありました。

  • cotreeというコーチングのサービス使わせてもらい自己認知って大切だよなーと思ってた
  • 数ヶ月前にQTODAYという家入一真さんの出題した質問にTwitterで答えるようなサービス(調べたらクローズしてた)があっていいなーって思ってた。(あまり情報のこってなかったので参考になりそうなnoteのリンク貼っておきます)
  • 同じ感じで自己認知を高めることをテーマにサービス作ってみようかなー
  • 最近会社でVueの勉強会やったからVueで何かつくってみたいなー

これだったら夏休み中にフロントだけで実装できるんじゃない?やろう!という流れで作るものが決定しました。

サービスのアイディアとか作りたいものは思いついたらtrelloにメモするようにしています。「アイディア」「概念」「面白そうなサービス」になんとなく分けて書いています。(最近はアイディアしか更新してない)

サービスの具体化

以下の手順でやることが多いです。

  1. 紙にUIとか機能書いてみる
  2. スライドにちょっと綺麗目に詳細書く

紙にUIとか機能書いてみる

今回はこんな感じで書いています。上がサービスの名前で下が機能と画面構成です。雑すぎて恥ずかしいw
「done is more than perfect」ってわざわざ紙に書いて自分に言い聞かせてます。これも間違っててただしくは「done is better than perfect」でした笑

スライドに綺麗目に書く

こちらに公開しました。
さすがに上記の雑な構成図で作り始めるのは辛いのと、この段階だと自分の中でもどこまで機能入れるかとか、サービスの目的とか決めきれてないのでスライド作りながら自分の頭の整理をしていきます。

作る目的書いてみたり

UIをちょっと綺麗に作ってみたり

UIデザイナーさんがよく使うらしいSketchというイラストレーターみたいなツールも数千円で買っているのですが全然使い方慣れないので、Zoho show(かgoogleのスライドのやつ)を使って作りました。素人なので背伸びせずにこっちの方が早いし楽です。

デザインはサービスのイメージからなんとなく連想して決めています。今回は

  • マインドフルネス的な感じ
  • 禅?
  • 「マインドフルネス」とか「Zen」で画像検索したら青系が多い

  • 青だとなんか欧米のマインドフルネス色が強い気がする紫にしたらなんか「禅」感でそう。

というふわっとした思考過程で紫になりました。色は以下のサービスを使って選びました。
hue360は既存の色から選ぶだけで楽なのでまず使えそうな色あるか探しています。ここで選んだ色をadobe colorのベースカラーにしていい感じの色のセットを使っています。作成途中にカラーコードコピペしたくなるのでスライドも雑に貼り付けます。

この段階でテーブルの設計とか、データの処理方法とかも考えますが今回は単純なサービスなのでやっていません。

サービス名も決めます

サービス名もこの段階でフィクスするようにしています。実装の前にgithubのレポジトリ作成したり、作業ディレクトリ作ったりする際にサービス名使う機会が多いのであとからサービス名変わるとずれて個人的に気持ちわるいです。
名前はコンセプトなどを書き出してそこから語呂合わせで決めました。決めるときはお名前.comでドメインが取れることを確認しています。一応ググって同じ名前のサービスがないことを確認しています。
「しるQ」は以下の候補から「自分をしるQ」に決めて、そのあとなんか長いなと思って「しるQ」に短縮しました。

自分に気づこう
ちょこっと自己分析
人生の問い
自分Q
自分をしるQ
Tweet自己分析

これで準備完了です!ここまでの作業に3~4時間つかっています。
この時点で早く終わらせないと俺の夏休みが開発だけで終わってしまう! と若干焦る:fearful:

実装

スライド見ながらガシガシ作っていきます。スマホから使うの前提なのでmax-width:600pxで作ります。ヘッダー、フッダー、ボディーのCSSとかはだいたいどのサービスも同じような実装なので、理想のサイトを参考にさせてもらいながら作ります。細かい調整は後でするのでそれっぽくなるように作ります。

最初こんな感じで

Twitterのウィジェット埋め込んだりしたら雰囲気でてきました。

今回はVueのコンポーネントやルーティングを勉強しながら作ったのでかなり時間かかってしまい、この時点で26時になり作業終了です。今日は13時~26時まで休憩除くと10時間くらい作業しました。

8/14日
出かける予定があったので毎日の質問を保存するためのFirebaseのRealtimeDatabseとのつなぎこみだけ実装しました。js側に質問全部保存するのが簡単そうですが、最近バックエンドエンジニア不要論で話題のFirebaseとかRealtime Databse使ってみたかったので連携させました。初NoSQLでした。

作業は3時間くらい

8/15日
しるQは投稿機能もユーザー登録機能も実装せずに、Twitterにおんぶに抱っこのサービスなので、どのようにTwitterに投稿して貰うか考えました。Twitterでシェアされた時に以下のようなOGPが出て毎日変わる質問を表示させたかったのですが、SPAだとプリレンダリングしないと実現できないことに気づきしかたなく実装しました。ここは結構詰まってしまって4時間くらい消費しました。

ところが...実装終わってテストしていると、PCの画面サイズだとOGPに質問文全部表示されていい感じなのですが、スマホでみると説明文カットされてました:upside_down:


スマホからの使用を想定しているのでこれでは意味がない...プリレンダリング無駄に実装してしまいました...たしかにQTODAYは質問ハッシュタグでつけてるんですよね...確認していなかった。大幅な時間ロスです。夏休み...
プリレンダリングでOGPに質問出すのはやめて、普通にハッシュタグに質問出すことにしました。

機能は大枠完成してきたので細かい所の調整をします。
ファビコン作ったり、ロゴを作ったり、サービス用のツイッターアカウント整えたりしました。
ロゴは一応Sketchで作りましたが特に難しいことはしておらず、"Hiragino Mincho Pro"という書体で文字書いて出力しただけです。Sketchの必要はもはやないです。

フォントはためしがきを利用させていただき雰囲気がでるフォントを探しました。今回はいろはまるを使用しています。最終的にはこんな感じに落ち着きました。

最初と比べると多少垢抜けましたかね...?
お気づきの方はいないかと思うのですが、この時点で最初にスライドで決めたカラーコードはガン無視していますw:joy:
クロームの開発者ツールでいい感じになるまで色をぐりぐり変えて調整しました。
image.png

作業は5時間くらい

8/16日
最終日はドメインを取得して設定したり、Firebaseに1ヶ月分の質問を登録したりしました。
siruq.netも取れたし「.net」の方がかっこいい気がするのですが千円くらいするんですよね。siruq.siteは60円だったので安さに負けました。作業は4時間くらい。以上で完成です!!

まとめ

1日でサクッとつくってやるぜ!とか思ってたんですが夏休みの 1/2くらいを吸い取られた開発となりました。随所で手を抜いたのですが(コードはひどいのでのせていない)それでも一つの形にするのは大変ですよね。個人開発は使える時間も限られてることが多いので、いかに最小限の機能で手を抜きつつそれなりの形にするためにライブラリとかツールを活用するの大切だなぁと思いました。

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

メニューが横幅に収まらない時に右上に収納されるやつ

あまりサンプルが見つからなかったのでやってみた。
こういう動きを何て言うのか分からないので、自分が見つけられなかっただけの可能性は否めないです・・・。

デモ

See the Pen navbar with menu storage by Yoshiaki Itakura (@negibouze) on CodePen.

ちょっと解説

せっかくなので、Intersection Observerを使ってみました。
ポイントはroot#menuにするところくらいです。

const setIntersectionObserver = () => {
  const observerOptions = {
    root: document.querySelector("#menu"), // menuをroot要素にする
    rootMargin: "0px",
    threshold: [0.8]
  };

  const items = document.querySelectorAll(".menu-item");
  observer = new IntersectionObserver(
      intersectionCallback,
      observerOptions
    );
  // menu-itemを監視対象にする
  for (const item of items) {
    observer.observe(item);
  }
}

entry.isIntersectingで処理を分岐します。
true: 見えるようになった
false: 見えなくなった

const intersectionCallback = (entries) => {
  entries.forEach(entry => {
    const target = entry.target;
    const storage = document.querySelector(".storage");
    if (entry.isIntersecting) {
      // navbarのメニューを表示する
      target.classList.remove("invisible");
      const len = overflowMenuItems.length;
      if (1 <= len) {
        if (len === 1) {
          // 最後の要素が表示された時は、more(縦の「・・・」)を非表示にする
          storage.classList.add("hide");      
        }
        // 収納していたメニューの先頭を削除する
        overflowMenuItems.shift();
      }
    } else {
      // あふれメニューの末尾に要素のクローンを追加する
      overflowMenuItems.unshift(target.cloneNode(true));
      // navbarのメニューを非表示にする
      target.classList.add("invisible");
      // more(縦の「・・・」)を表示する
      storage.classList.remove("hide");
    }
  });
  sortOverflowItems();
  createOverflowMenu();
}

並び順を保証するために、data属性のorderでsortします。
※これをやらないと、初回表示時に横幅が足りない時に並び順が保証されない

const sortOverflowItems = () => {
  overflowMenuItems.sort((a, b) => {
    return a.dataset.order < b.dataset.order ? -1 : 1;
  })
}

#overflowMenuの子要素にあふれた分のメニューを追加します。
差分更新にした方が良いと思いますが、ひとまず毎回、既存の子要素を全削除してから、再度追加しています。

const createOverflowMenu = () => {
  const menu = document.querySelector("#overflowMenu");
  // 一回全部消す
  while (menu.firstChild) {
    menu.removeChild(menu.firstChild);
  }
  for (const item of overflowMenuItems) {
    menu.appendChild(item);
  }
}

おまけ: ドラッグで横幅を変えるやつ(動作確認用)

/* 動作確認用 */
const wrapper = document.querySelector(".wrapper");

const addResizeEvent = () => {
  const rightEdge = document.querySelector(".right-edge");

  rightEdge.addEventListener("mousedown", initResize, false);
}

const initResize = (e) => {
  window.addEventListener("mousemove", Resize, false);
  window.addEventListener("mouseup", stopResize, false);
}

const Resize = (e) => {
  wrapper.style.width = `${e.clientX - wrapper.offsetLeft}px`;
}

const stopResize = (e) => {
  window.removeEventListener("mousemove", Resize, false);
  window.removeEventListener("mouseup", stopResize, false);
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

HTAで超簡単なデスクトップアプリを作ってみる

はじめに

Windows環境であれば、何もインストールしないでHTMLベースのデスクトップアプリが作れます。私、凄く気に入っていてかなり愛用し続けていますが、とってもマイナーな様子…

「HTMLは書いたことあるけど、アプリ作るのって…難しいんじゃね?」
という方に一回これを実行してみて欲しいです!

スクリプト

test.hta
<!doctype html>
<meta http-equiv="x-ua-compatible" content="ie=edge">
<meta charset="utf-8">
<title>秘密のアプリ</title>
<ul>
    <li><a id="notepad" href="#">メモ帳を起動する</a>
    <li><a href="https://qiita.com/sozaiya/items/475211daf85bbb208635">IEでsozaiyaさんの記事を表示する</a>
    <li><a id="shutdown" href="#">今すぐシャットダウンする</a>
</ul>
<script>
    var data = {
        shell: new ActiveXObject('wscript.shell')
    };
    document.getElementById("notepad").addEventListener("click", function () {
        data.shell.exec("notepad");
    }, false);
    document.getElementById("shutdown").addEventListener("click", function () {
        if (window.confirm("本当にシャットダウンしますか?")) {
            data.shell.exec("cmd /c shutdown -s -t 0");
        }
    }, false);
</script>

上記ファイルを作成したらダブルクリックで実行します。

解説

メモ帳を開いたり、シャットダウンしてみたりと、あまり実用的ではないですが、応用すれば色んなことできるかも?と感じてもらえたら嬉しいです(^^)

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

TypeScriptの型を実行時に保持するemitDecoratorMetadataと、Reflectを使わない情報取得の検証

TypeScriptの型を実行時に保持するemitDecoratorMetadataと、Reflectを使わない情報取得の検証

1.TypeScriptの型とemitDecoratorMetadata

 TypeScriptの型は、コンパイル時のエラーチャックや開発環境の入力補完などに利用されるだけで、実行時には何の影響もありません。実行時に何が設定されていたか確認しようとしても、トランスコンパイルされた時点で情報は綺麗さっぱり消え去ってしまいます。

 TypeScriptの型を実行時に取得するには、tsconfig.jsonにemitDecoratorMetadataを設定します。この機能は実験的なものであるため、今後仕様が変更される可能性があります。ということで現時点での仕様を確認していきたいと思います。

2.TypeScriptとDecorator

 TypeScriptにはJavaScriptには無い機能としてDecoratorがあります。クラスやメソッド、プロパティに対して、何らかの機能を割り込ませることが可能になります。そのDecoratorの機能の一つとして、コンパイル時にMetadataを埋め込む機能を利用することが出来ます。通常はReflectという機能を使ってMetadataを取得するのですが、今回は直接、変換されたものを取り出してみたいと思います。

3.検証用ソースコード

Test01.ts
function dummy(...params: unknown[]) {}

var __decorate = (
  param: [Function, Function],
  target: Function,
  key: string
) => {
  console.log(`${target.constructor.name}.${key} => ${param[1].name}`);
};
var __metadata = (key: string, type: Function) => type;

class Test {
  constructor() {}
  @dummy
  a?: number;
  @dummy
  b?: string;
}

 __decorate__metadataはクラスのプロパティに@dummyのDecoratorを挿入した時点で生成されるようになります。このプログラムはクラスの宣言しかしていませんが、以下の実行結果を得ることが出来ます。

実行結果

Test.a => Number
Test.b => String

 何が起こっているか確認するためには、トランスコンパイルされたコードを見るのが手っ取り早いです。

Test01.js
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
    if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
function dummy() {
    var params = [];
    for (var _i = 0; _i < arguments.length; _i++) {
        params[_i] = arguments[_i];
    }
}
var __decorate = function (param, target, key) {
    console.log(target.constructor.name + "." + key + " => " + param[1].name);
};
var __metadata = function (key, type) { return type; };
var Test = /** @class */ (function () {
    function Test() {
    }
    __decorate([
        dummy,
        __metadata("design:type", Number)
    ], Test.prototype, "a", void 0);
    __decorate([
        dummy,
        __metadata("design:type", String)
    ], Test.prototype, "b", void 0);
    return Test;
}());

 内容を確認するとmetadataに"design:type"として、型の情報が設定されています。通常だとデータはReflectに持って行かれてしまうのですが、decorateと__metadataを再定義して、データを持ってきています。型情報が欲しいだけなら素直にReflectを使いましょう。しかし今回は動作を確認するための検証コードなのでこういう形をとっています。

4.まとめ

 TypeScriptはJavaScriptに型を設定できるというところばかり注目されて、Decoratorに関してはあまり話題に上りません。QiitaにもDecoratorについて解説されている記事は何件かあるのですが、それほど流行っていない感じです。

 実行時は綺麗さっぱり消え去る一般的な型定義に対して、Decoratorは実行時に力を持ちます。付加的な機能をつけたり、定義型によって挙動を変えたりと、可能性は大きく広がります。

 ちなみに無理矢理情報を取得するより先に、Reflectの使い方を解説した方が先じゃないかと思った人もいるでしょう。やらない理由は簡単です。すでに該当する記事があるからです。同じような記事を書いても面白くないでしょう。

 TypeScriptは便利なのですが、実際にどんなコードに変換されているのか、実はよく分からずに使っていることもあります。そういうとき、中身を見てみるのも面白いと思います。

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