- 投稿日:2019-08-20T23:55:24+09:00
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>JavaScriptconst 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>JavaScriptconst 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>JavaScriptconst 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を補正します。
JavaScriptif (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を補正します。
JavaScriptif ( navigator.userAgent.indexOf('Android') > 0 ) { for(key in infoArray){ information.innerHTML+=(key+":"+infoArray[key]*-1+"<br>"); }; }
- 投稿日:2019-08-20T23:49:59+09:00
JavaScriptでAmazon S3にファイルをアップロード
概要
Web ブラウザーから実行する JavaScript で、AJAX 通信で Amazon S3 の Bucket にできるだけセキュアに画像ファイルをアップロードできるようにしてみます。
取り扱い可能な画像ファイルは png、jpeg、gif を想定しています。
作業は、次の順で進めます。
- アップロード先となる S3 Bucket の作成
- S3 に PutObject だけが可能なポリシーを作成
- STS が使用するロールの作成
- アップロードのみ可能なIAMユーザーを作成
- Lambda 関数を作成
- Lambda が使用するロールにポリシーをアタッチ
- API Gateway を作成
- クライアントプログラムの作成
アップロード先となる 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 の部分はランダムな英数字で構成されます。アップロードの仕組みは以下の通りです。
- https://1234567890.execute-api.ap-northeast-1.amazonaws.com/default/uploadFunc?〜 でアップロード先の S3 の URL を取得する。
- 取得した 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> 〜課題
本ページで紹介したファイルのアップロードだと、ファイルの格納場所を指定しているので、存在するファイルを上書きできてしまいます。
本ページで紹介したファイルのアップロードを実装するアプリによっては、他人が作成したファイルを上書きできてしまったりするので場合によっては脆弱性になりかねません。
実装するアプリによっては他人のアップロードしたファイルを上書きできないようにする仕組みを別途組み込んだり、する工夫が必要になると思います。
そういった点に注意してご利用ください。参考にしたページ
- 投稿日:2019-08-20T21:40:24+09:00
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パターンな電卓
完成図
実物はこちらです。キーボード入力にも対応しています。
https://beebow6.github.io/typescript-calculator/dist/全てのソースはこちらです。
https://github.com/BeeBow6/typescript-calculator状態遷移表
TypeScriptの基本的なところをなるべく一通り体験すべく、Javaで電卓アプリを作成している下記サイトを参考に作成しました。
サンプルアプリでおぼえる実践的Android入門※要事前登録今回の電卓の状態遷移表は下記のとおりです。
A:左辺入力前
BeforeLeftSideStateB:数値入力
NumberStateC:演算子入力
OperatorStateD:右辺入力後
AfterRightStateE:結果表示
ResultStateF:エラー
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へ定数を列挙型(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' }VIEWコンポーネント
抽象クラス
ReactのComponentみたいなVIEWコンポーネントの基底クラスを抽象クラスで定義し、ボタンやディスプレイ等の各コンポーネントはこれを継承するようにします。
基底クラスの定義
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
プロパティなど、一部の要素しか実装していないプロパティは存在しないプロパティとみなされてしまいます。
そのため、派生クラス側でHTMLInputElementやHTMLButtonElementなどの具体的な型を指定させます。
div.firstElementChild
で返される要素の型はElement
かnull
なので、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種があります。
どちらもButton
やPanel
同様、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 | GitHubJavaScriptで、できるかぎり小数演算の誤差を少なくする方法
日曜プログラミングで電卓を作ってみる電卓クラス
電卓の基本機能を備えたクラス
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) { } // ... }型ガード(Type Guard)
App
クラスにて、クリック時にボタンから呼ばれるコールバック関数handleClick()
の第二引数value
は、列挙型のNUMBER
もしくはOPERATOR
が渡されます。
一方、転送先のState
クラスの各メソッドは、どちらかしか受付けません。
NUMBER
であるかを確認する関数isNUMBER()
で、事前に型ガードを施しています。TypeScriptでみかける"is"というキーワードについて
状態(State)クラス
TypeScriptにあってJavaScriptにない、Interfaceを利用して上記の状態遷移を実現します。
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にさほど戸惑うことはありませんでした。
やはり、型を指定できるほうが便利だし楽しいな~と感じました。
- 投稿日:2019-08-20T19:24:40+09:00
子要素のイベントを親要素へ伝搬させないようにする
バブリングというもの
イベントは伝搬される。
そのため、子要素で発生したイベント(例えばクリックイベントなど)は、
最終的にルート要素まで伝搬し、Clickイベントがあると実行される。
このイベントの伝搬のことをバブリングという。イベント発生から伝搬されるまで
以下記事でわかりやすく説明されています。
DOMイベントのキャプチャ/バブリングを整理する 〜 JSおくのほそ道 #017イベントが発生すると下記の流れでイベント伝播が発生します。
子要素のイベントを親要素へ伝搬させないようにする
本題ですが、
子要素のイベント関数内で、
stopPropagation()メソッドを実行してあげることで止めることが可能。
構文としては、
event.stopPropagation();
Clickイベントを追加した場合は、
親要素へのイベントの伝搬がないかを確認するようにする。
- 投稿日:2019-08-20T18:18:10+09:00
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-serverdocker-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にアクセスしてみましょう。
guest/guestでログインできます。
ログイン後はこんな画面になるはず
とりあえず、サーバのインストールは以上です。
また、rabbitmqのバージョンを投稿時の最新で記載していますが、こちらのページで最新バージョンを知ることができます。
RabbitMQ Changelog — RabbitMQRabbitMQのクライアントを使ってメッセージを送ってみよう
今回の投稿では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.jsvar 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 worldCtrl + Cを押すまで、待機状態になります。
別のコンソールで、send.jsを実行すると非同期でreceive.jsに受信されます。その他
receive.jsを動かさない状態で、send.jsを何度か実行するとどんどんQueueにメッセージが溜まっていきます。
その時、receive.jsを起動すると、一気にキューを受け取ります。
また、複数のコンソールでreceive.jsを起動すると、ランダム?でどれかのreceive.jsに受信されます。キューからメッセージを受け取る時は一度しか取れないですからです。これは設定で変更することができます。
最後に
最近のフロントエンドでは、非同期処理が当たり前のように使われるようになってきました。
もしかしたら今後、RabbitMQを使うシーンがまたあるかもしれないです。また、別の投稿でRabbitMQの使い方を紹介しようと思っています。
参考文献
業務で使う場合はやはり公式ドキュメントが一番よいと思います。
Messaging that just works — RabbitMQRabbitMQのチュートリアルがあるのでこちらを参考にしてください
Getting started with RabbitMQ — RabbitMQ
- 投稿日:2019-08-20T18:16:49+09:00
Node JSのコールバックパターン
継続渡しスタイル(continuation-passing style : CPS)
JavaScriptにおいてコールバック関数とは、ある関数を呼び出す時に、引数として指定する関数で、FuncAの処理が完了した時にFuncAの結果を通知するために起動される関数のことを指します。
同期的継続渡しスタイル
test.jsfunction 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.jsfunction 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.jsconst result = [1, 5, 7].map(element => element - 1) console.log(result) // [0, 4, 6]ちなみに以下のことはダイレクトスタイル(Direct Style:DS)と呼びます。
example.jsfunction add(a, b) { return a + b } console.log(add(1,2)) // 3同期処理か非同期処理か
どっちでもいいですが、まず避けなければならないのは「一貫性がないAPI」です。
同期と非同期の混在
もっとも危険なのは、ある条件には同期処理、ある条件には非同期処理を行う関数です。
以下のコードは一貫性がないコードの例です。test.jsconst 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.jsconst 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.jsconst 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.jsconst 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.jsconst 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.jsconst 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.jsreadJSONThrows('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 (著), 武舎 広幸 (翻訳), 阿部 和也 (翻訳)
- 投稿日:2019-08-20T17:31:32+09:00
緯度と経度を入力することによって現在位置から目的地の方角・距離を表示するPGM
緯度と経度を入力することによって現在位置から目的地の方角・距離を表示するPGMを作成しました
ローカルのPC上では動作しましたがスマホ等他ではどうも動かないようです(ダメダメですね...原因がわかり次第修正する予定ですがバグ内容・原因がわかる方いたら返信お願いします)
ちなみに現在位置の緯度経度は最初に距離再計算のボタン
を押すことで自動で入力されるようになっています...
作成の目的はどこかの話でグーグルマップの通信のデータ量が多すぎるとのことを聞いた
覚えがありましたので、最低限方向と距離だけわかるようなPGMがあれば良いな、と思ったからです...参考githubURL: https://github.com/NanjoMiyako/ZatuMap
参考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
- 投稿日:2019-08-20T17:13:42+09:00
ESLint v6.2.0
前 v6.1.0 | 次 (2019/08/31 JST)
ESLint 6.2.0 has been released: https://t.co/L7srPm1jkE
— ESLint (@geteslint) August 18, 2019
This release adds support for parsing ES2020 features (bigint, dynamic import) using the default parser! Please give it a try and let us know if you see any issues.ESLint
6.2.0
がリリースされました。お盆休み中に ESTree 仕様が更新されたので、BigInt と Dynamic Imports 構文をサポートしました。
既知の問題:
babel-eslint
を利用している場合、変数の利用判定が変になる。これはbabel-eslint
の対応待ちです (過去 ESLint 5.0.0 で実施された Breaking Changes に関わる問題であるため)。- npmで更新したとき、JSXの解析がうまくいかなくなる場合がある。npm の不具合によるものです。一度
node_modules
とpackage-lock.json
を削除して再インストールしてみてください。それでも解決しない場合、package.json
のdevDependencies
に"acorn": "^7.0.0"
を追加し、node_modules
とpackage-lock.json
を削除して再インストールしてみてください。質問やバグ報告等ありましたら、お気軽にこちらまでお寄せください。
? 本体への機能追加
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
プロパティが追加されました。.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 );? オプションが追加されたルール
特になし。
✒️
eslint --fix
をサポートしたルール特になし。
⚠️ 非推奨になったルール
特になし。
- 投稿日:2019-08-20T14:31:09+09:00
Nuxt で全てのページで index.html をルーティングの対象にする方法
- Nuxt は vue-router をつかってルーティングしている
- vue-router は
alias
つかって対象のパスをルーティングに追加できるnuxt.config.js
のextendRoutes
で vue-router のルーティングを変更できるということをふまえて、
extendRoutes
をつかって全ての route にindex.html
を付与したalias
を追加することで実現できました。nuxt.config.jsimport { resolve } from 'path' const nuxtConfig = { router: { extendRoutes(routes, resolve) { for (const route of routes) { route.alias = resolve(route.path, 'index.html') } } } }参考
- 投稿日:2019-08-20T13:57:42+09:00
Nuxt.jsでウェブストレージを扱う
はじめに
ウェブストレージ便利ですよね、特にローカルストレージはCookieが使えないPWAや、DBを用意するほどでもないちょっとしたデータを保存したいときに使うことが多いのではないかと思います。
ただ、いざウェブストレージのデータを扱おうと実装を進めるとデータの同期や保存のタイミング等でつっかかることがままあります。
そこで今回はNuxt.jsでウェブストレージを簡単に扱う手法を考えましたので簡単にご紹介しようと思います。実装方針
コンポーネントやページから直接ウェブストレージにアクセスしていると処理やデータが散乱してしまい辛い状況に陥りやすいです。
そこで、ウェブストレージの参照や処理は全てストアに一本化します。
各コンポーネントやページは共通のステートやアクションを参照することになりますのでデータが非常に扱いやすくなります。実装
入力した値をウェブストレージに保存する簡単なデモを作成します。
今回はローカルストレージを使用しますがセッションストレージでも同様の方法で対応可能です。ウェブストレージ操作用のクラスを作成
1つのウェブストレージを扱うだけならクラスを作るまでもないのですが、今回は複数のウェブストレージを管理することを前提に作成しておきます。
実装例ですので登録・削除する機能だけ用意してあります。lib/storage.jsexport 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.jsexport default ({ store }) => { for (const key in store._actions) { if (key.match(/nuxtClientInit/)) { store.dispatch(key) } } }nuxt.config.jsplugins: [ { src: '~/plugins/nuxt-client-init', mode: 'client' } ]
nuxt-client-init-module
というモジュールが既にあるのですが、ルートモジュールでのみ機能するようです。
名前空間付きモジュールで「nuxtClientInit」を利用したい場合は上記のようなプラグインを作成してください。ストア実装
ストアのアクションはあくまでインスタンスの関数を実行し、その結果をステートへ返すだけのシンプルな設計とします。
アクションに直接ウェブストレージ周りの機能を実装してもよいのですが、複数ウェブストレージを扱ったり機能が複雑になってくると煩雑になりやすいので、ウェブストレージの処理とストアの処理を明確に分担するように設計しています。store/message.jsimport 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.jsimport Vue from 'vue' import LocalStorage from '~/lib/storage' Vue.prototype.$storageMessage = new LocalStorage('message') Vue.prototype.$storageMessage.init()nuxt.config.jsplugins: [ { 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つを管理する必要があるのでそこがもたつきやすいですよね。
今回のように「ウェブストレージ←→ストア←→ページ・コンポーネント」という形で一方向の処理で管理すると、土台さえ作ってしまえばウェブストレージ周りの処理を意識せずに扱えるのでオススメです。
大した手法ではないのですがウェブストレージの実装で手をこまねいている方は参考にしていただければ幸いです!
- 投稿日:2019-08-20T13:56:16+09:00
obniz OSをESP32(NefryBT)に入れて動かそう!
こんにちは!わみです!
今日は先日リリースされたobnizOSを使い始めてみたので、どんな感じなのかも合わせて書いていこうと思います。そもそも「obniz」ってなに?って方は「arduinoとは全く違う、IoTツールobnizのしくみ」をご覧ください。
なにができるのか
NefryでobnizOS動いた!! pic.twitter.com/YAbw8ggR5B
— わみ@NefryとかFlutter本とか (@wamisnet) August 17, 2019HTML+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(無印)
- Nefry BT R2/R3
まとめ
obniz OSでこれからもっと便利にIoTを始められるようになるのではないでしょうか?
コードの使い勝手としては、obnizボードと大差ない気がします。ただ、ReadOnlyのIOやアナログ入力など制約があるところやモータドライバーがついていないのでモータを直接動かせないところもあるのでそのあたりはobnizボードのところが優れてるかなと思います。
Nefry BTではArduinoでコードを書くのが普通ですが、JavaScriptであったとしても私としてはハードウエアが好きになって、いろんなものが生まれてくるとすごくいいなと思っています。
ここまで読んでくれた方へ
ここまで読んでくださりありがとうございます。
いいねやコメント、SNSでの共有等をしてくださると、今後の励みになります。よろしくお願いします。
良かったらTwitterもフォローしてね
- 投稿日:2019-08-20T13:01:17+09:00
【備忘録④】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.jspackage.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」にアクセス。
アドオンの追加
便利なアドオンを追加する。
モジュール名 バージョン 詳細 @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-pluginTypeScriptで使用するので型定義も追加する。
$ 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」にアクセス。
ボタンクリック時の動作、テキストの変更、表示切り替えができることを確認。
以上で確認完了。
作成したボイラープレートは こちら
- 投稿日:2019-08-20T12:34:53+09:00
既存プロジェクトのESLintをアップデートする
はじめに
既存プロジェクトのESLintのバージョンを見たら
v1.10.3
。さすがに古いので、最新バージョン
v6.2.0
までジャンプアップします。既存のESLintをアンインストール
$ yarn remove eslintESLintをインストール
$ 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.jsmodule.exports = { env: { browser: true, es6: true }, extends: [ 'airbnb-base' ], globals: { Atomics: 'readonly', SharedArrayBuffer: 'readonly' }, parserOptions: { ecmaVersion: 2018, sourceType: 'module' }, rules: { } };
- 投稿日:2019-08-20T12:34:53+09:00
既存プロジェクトのESLintをアップデート&自動修正させる
はじめに
既存プロジェクトのESLintのバージョンを見たら
v1.10.3
。
さすがに古いので、最新バージョンv6.2.0
までジャンプアップします。ここでは主にeslintのインストールとファイル一括自動修正、
あとはオマケでVS Codeでたまに起きるエラーについて書きました。既存のESLintをアンインストール
$ yarn remove eslintESLintをインストール
$ 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.jsmodule.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のルールに依存してしまってるぽいですね。
複数プロジェクトを行き来するときは気をつけましょう。
- 投稿日:2019-08-20T12:08:48+09:00
誰でもできるオリジナルwebサービスを着実にリリースする手順と考え方
夏休みに小さいオリジナルのwebサービスを作ったので、構想からリリースまでに使ったツールなどオリジナルwebサービスをリリースするまでの手順と考え方ご紹介したいと思います
![]()
技術的なことにはほぼ言及していません。これから初めてオリジナルの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にメモするようにしています。「アイディア」「概念」「面白そうなサービス」になんとなく分けて書いています。(最近はアイディアしか更新してない)
サービスの具体化
以下の手順でやることが多いです。
- 紙にUIとか機能書いてみる
- スライドにちょっと綺麗目に詳細書く
紙に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時間つかっています。
この時点で早く終わらせないと俺の夏休みが開発だけで終わってしまう! と若干焦る実装
スライド見ながらガシガシ作っていきます。スマホから使うの前提なので
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に質問文全部表示されていい感じなのですが、スマホでみると説明文カットされてました
![]()
スマホからの使用を想定しているのでこれでは意味がない...プリレンダリング無駄に実装してしまいました...たしかにQTODAYは質問ハッシュタグでつけてるんですよね...確認していなかった。大幅な時間ロスです。夏休み...
プリレンダリングでOGPに質問出すのはやめて、普通にハッシュタグに質問出すことにしました。機能は大枠完成してきたので細かい所の調整をします。
ファビコン作ったり、ロゴを作ったり、サービス用のツイッターアカウント整えたりしました。
ロゴは一応Sketchで作りましたが特に難しいことはしておらず、"Hiragino Mincho Pro"という書体で文字書いて出力しただけです。Sketchの必要はもはやないです。
フォントはためしがきを利用させていただき雰囲気がでるフォントを探しました。今回はいろはまるを使用しています。最終的にはこんな感じに落ち着きました。
最初と比べると多少垢抜けましたかね...?
お気づきの方はいないかと思うのですが、この時点で最初にスライドで決めたカラーコードはガン無視していますw![]()
クロームの開発者ツールでいい感じになるまで色をぐりぐり変えて調整しました。
作業は5時間くらい
8/16日
最終日はドメインを取得して設定したり、Firebaseに1ヶ月分の質問を登録したりしました。
siruq.netも取れたし「.net」の方がかっこいい気がするのですが千円くらいするんですよね。siruq.siteは60円だったので安さに負けました。作業は4時間くらい。以上で完成です!!まとめ
1日でサクッとつくってやるぜ!とか思ってたんですが夏休みの 1/2くらいを吸い取られた開発となりました。随所で手を抜いたのですが(コードはひどいのでのせていない)それでも一つの形にするのは大変ですよね。個人開発は使える時間も限られてることが多いので、いかに最小限の機能で手を抜きつつそれなりの形にするためにライブラリとかツールを活用するの大切だなぁと思いました。
- 投稿日:2019-08-20T11:29:12+09:00
メニューが横幅に収まらない時に右上に収納されるやつ
あまりサンプルが見つからなかったのでやってみた。
こういう動きを何て言うのか分からないので、自分が見つけられなかっただけの可能性は否めないです・・・。デモ
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); }
- 投稿日:2019-08-20T11:14:17+09:00
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>上記ファイルを作成したらダブルクリックで実行します。
解説
メモ帳を開いたり、シャットダウンしてみたりと、あまり実用的ではないですが、応用すれば色んなことできるかも?と感じてもらえたら嬉しいです(^^)
- 投稿日:2019-08-20T09:04:57+09:00
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.tsfunction 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は便利なのですが、実際にどんなコードに変換されているのか、実はよく分からずに使っていることもあります。そういうとき、中身を見てみるのも面白いと思います。