20200917のJavaScriptに関する記事は29件です。

【Vue.js】イベント発生時にajax通信で取得した値が表示されずに嵌った話

はじめに

脱jQueryを目指すという流れで、ボタンをクリックしてajax通信を行う処理をVue.jsとaxiosで実装していたのですが、「変数の中身は変更されているのに表示に反映されない?なんで?」と嵌りました。無事解決しましたので、備忘録として書き残しておきます。。。
結論だけ言うと、「thenの後にはアロー関数を使おう」です。

また当方htmlやjavascriptの初心者なので、以下に記述しているサンプルコードにはひどい書き方のものも多いと思いますが、目をつぶってやってください。

追記

javascript初心者の嵌りどころである thisのスコープの違い が原因で Vueに値が渡っていなかった 可能性が高いようです。意外とシンプルな原因だけど、気付きづらかった…。

jQueryでの書き方

今までこういう処理は以下のような書き方で慣れていました。ajaxでGETし、responseのdataを表示に反映させるというもの。(本当はPOST処理を書くことが多いですが、簡略化のためGETにしています。)

index.html
<body>
<div id="app">
  <button id="display-btn">表示する</button>
  <div id="name-area"></div>
  <div id="age-area"></div>
</div>
<!-- my ajax script -->
<script type="text/javascript" src="myscript.js"></script>
</body>
myscript.js
$(function() {
  // ボタンが押されたらAPIを呼び出し、結果を表示する
  $('#display-btn').click(function() {
    $.ajax({
      url: '/get-json',
      type:"GET",
      dataType:"json",
      timespan:1000,
      success: function(data) {  // 通信成功
        console.log('Success!');
        const results = data.ResultSet;
        // 名前表示
        var name = document.createElement("p");
        var name_text = document.createTextNode("名前:" + results['name']);
        name.appendChild(name_text);
        document.getElementById("name-area").appendChild(name);
        // 年齢表示
        var age= document.createElement("p");
        var age_text = document.createTextNode("年齢:" + results['age']);
        age.appendChild(age_text);
        document.getElementById("age-area").appendChild(age);
      }
    })
    .fail(function(data){  // 通信失敗
      alert("ERROR!! occurred in Backend.");
    });
  });
});

返ってくるJSONデータは以下をイメージ。

{
    "name": "たなか",
    "age": 22
}

Vue.jsとaxiosで書いてみる

html側はこんな感じ。調べてみればすぐ分かる範囲ですし、問題ないかと。

index2.html
<body>
<div id="app">
  <button v-on:click="getJson">表示する</button>
  <div v-if="data">
    <p>名前:[[data.name]]</p>
    <p>年齢:[[data.age]]</p>
  </div>
</div>
<!-- my ajax script -->
<script type="text/javascript" src="myscript2.js"></script>
</body>

axiosのresponseを受け取ったときにfunctionを使うと嵌った

ボタンをクリックしたらGETして取得したデータを表示するスクリプトを書いていきます。
そのとき、私は以下のページなどを参考に書いていたら嵌りました。

結局は最適な記事が見つからず自己流で書いてたのが悪かったのですが…。
以下のように書いたら、コンソールからはきちんと変数が入っているのが確認できるのに、リアクティブになっていないのか知らんが画面表示が更新されないという現象になり、うまく行きませんでした。
コンソールでconsole.log(this.data)に対してObserverになっていないことも確認できたので、responseを代入する際にリアクティブではなくなっていることが原因なのかと思ったのですが、後からリアクティブにする方法を一生懸命探しても上手く行かずでした。

myscript2.js
var app = new Vue({
  el: "#app",
  data () {
    return {
      data: null
    }
  },
  methods: {
    getJson: function(){
      axios.get("/get-json")
      // thenで成功した場合の処理を書ける
      .then(function(responce){
        this.data = response.data;
        console.log(this.data);  // オブジェクトが代入されたか確認
      // catchでエラー時の挙動を定義する
      }).catch(function(error){
        console.log('ERROR!! occurred in Backend.');
      });
    }
  },
  delimiters: ['[[', ']]']
})

アロー関数を使わないとダメだった

はい、解決しました。上と比べてもらうと違いが分かるでしょうか。
.then(function(responce){と書くのが駄目だったらしく、.then(response => {というように、=>(アロー関数)を使用するように修正すると、画面表示が更新されるようになりました。

myscript2.js(修正版)
var app = new Vue({
  el: "#app",
  data () {
    return {
      data: null
    }
  },
  methods: {
    getJson: function(){
      axios.get("/get-json")
      // thenで成功した場合の処理を書ける
      .then(response => {   // !!ココが修正箇所!!
        this.data = response.data;
        console.log(this.data);  // オブジェクトがきちんと代入されたか確認
      // catchでエラー時の挙動を定義する
      }).catch(error => {   // !!ココが修正箇所!!
        console.log('ERROR!! occurred in Backend.');
      });
    }
  },
  delimiters: ['[[', ']]']
})

おわりに

後から調べると、methods内のaxiosでのajax通信ではアロー関数を使って書いていた記事が多かったわけですが。。。
なぜアロー関数じゃないとダメなのか、javascript初心者ですのでいまだによく分かっていません(よく調べきっていないというのもある)。知見者がいたら教えていただきたいです。 thisのスコープの違いが原因だろうと教えていただいたので、「はじめに」に追記しました。
ただこの罠に嵌っていたとき、値をその場で宣言して代入したりすると上手く表示されたりして「本当になんなんだ……!?」と怒りに震えたりしましたので、もし同じ罠に嵌った人の参考になれば幸いです。

また、以下のような記事も後から見つけました。フロントエンドもきちんと作るなら、このくらい構造的に書いた方がいいのかもしれないです。

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

CodeQLをローカルPCで実行する。ついでにクロスサイトスクリプティングの脆弱性を発見する。

やること

  1. CodeQL CLI をインストール
  2. starter pack をダウンロード
  3. 検査対象のプロジェクト(ソースコード)を用意
  4. CodeQL データベースを作成する
  5. CodeQL を実行してクロスサイトスクリプティングの脆弱性を発見する

Windows + Powershell の実行例を書いてるけど、他の環境でも同じようにできるはず。たぶん。

CodeQLI CLI をインストール

GutHub: CodeQL の Release ページから自分の環境にあったZIPファイルをダウンロードします。ZIPファイルを好きな場所に展開してください。

以下、C:\ql\codeql に展開した前提でコマンドを打っていきます。

PS C:\ql\codeql> ls
    ディレクトリ: C:\ql\codeql
Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
d-----        2020/09/09      4:41                cpp
d-----        2020/09/09      4:41                csharp
d-----        2020/09/09      4:41                go
d-----        2020/09/09      4:41                java
d-----        2020/09/09      4:27                javascript
d-----        2020/09/09      4:26                legacy-upgrades
d-----        2020/09/09      4:26                Open-Source-Notices
d-----        2020/09/09      4:27                python
d-----        2020/09/09      4:41                tools
d-----        2020/09/09      4:27                xml
------        2020/09/09      3:55             97 .codeqlmanifest.json
------        2020/09/09      3:55           2987 codeql
------        2020/09/09      3:55            141 codeql.cmd
------        2020/09/09      4:10         181248 codeql.exe
------        2020/09/09      3:57           7563 LICENSE.md

codeql.exe がCLI本体なので、PATHを通しておくとよいと思います。

setx PATH "$env:path;C:\ql\codeql"

PATH環境変数を設定したら powershell を再起動しておきましょう。(再起動なしでどうやってリロードするの?誰か教えて!)

codeql コマンドを叩いて、下記のような出力がでればCLIの準備はOK。

PS C:\ql> codeql
Usage: codeql <command> <argument>...
Create and query CodeQL databases, or work with the QL language.

starter をダウンロード

QLをイチから書くのはつらいので vscode-codeql-starter をダウンロードして利用すると良いです。

git clone --recursive https://github.com/github/vscode-codeql-starter.git

このリポジトリには実用的なサンプルQLがいっぱいあります。例えば javascript の XSS の可能性があるコードを検出する ql は、vscode-codeql-starter/ql/javascript/ql/src/Security/CWE-079 にある、など。

PS C:\ql\vscode-codeql-starter\ql\javascript\ql\src\Security\CWE-079> ls
    ディレクトリ: C:\ql\vscode-codeql-starter\ql\javascript\ql\src\Security\CWE-079
Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
d-----        2020/09/17     19:55                examples
-a----        2020/09/17     19:55           1492 ExceptionXss.qhelp
-a----        2020/09/17     19:55            731 ExceptionXss.ql
-a----        2020/09/17     19:55           1501 ReflectedXss.qhelp
-a----        2020/09/17     19:55            703 ReflectedXss.ql
-a----        2020/09/17     19:55           1576 StoredXss.qhelp
-a----        2020/09/17     19:55            691 StoredXss.ql
-a----        2020/09/17     19:55           2773 UnsafeJQueryPlugin.qhelp
-a----        2020/09/17     19:55            904 UnsafeJQueryPlugin.ql
-a----        2020/09/17     19:55           1576 Xss.qhelp
-a----        2020/09/17     19:55            713 Xss.ql
-a----        2020/09/17     19:55           2537 XssThroughDom.qhelp
-a----        2020/09/17     19:55            708 XssThroughDom.ql

※ちなみに CWE-079Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')

検査対象のプロジェクト(ソースコード)を用意

分析したいリポジトリを git clone すればいいです。

今回は特別にWebアプリケーションを作ったので、これをCodeQLで分析してみましょう。

./ql/my-awesome-webapp/index.html
<html>
<body>
こんにちは 
<script>document.write(decodeURI(location.hash.substring(1)));</script>
さん!
</body>
</html>

URLの#以降に名前を入れると挨拶してくれます。こんにちは!
2020-09-17-20-19-10.png

CodeQL データベースを作成する

CodeQLはソースコードを直接分析するわけではなく、ソースコードをもとにデータベースを作り、そのデータベースにクエリを実行して分析します。

さきほどのWebアプリケーションは C:\ql\my-awesome-webapp に置きました。次のコマンドでWebアプリのjavascriptを分析するためのデータベースを C:\ql\my-awesome-webapp-qldb に作成します。

PS C:\ql> cd .\my-awesome-webapp\
PS C:\ql\my-awesome-webapp> codeql database create ../my-awesome-webapp-qldb --language=javascript

...いっぱい...

[2020-09-17 20:28:39] [build] Done extracting C:\ql\codeql\javascript\tools\data\externs\web\window.js (15 ms)
Finalizing database at C:\ql\my-awesome-webapp-qldb.
Successfully created database at C:\ql\my-awesome-webapp-qldb.

今回はindex.htmlファイル1つしかありませんが、ファイルが複数ある場合でも全ファイルをデータベース化してくれます。言語が混じってても問題ありません。

コマンドの詳細は⇒Creating CodeQL databases: Creating CodeQL databases

QLクエリを実行して欠陥を発見する

作成したデータベースにクエリを実行します。

実行方法:

codeql database analyze {データベース名} `
{クエリ} `
--format {出力フォーマット} `
--output {出力ファイル名} `

下記は vscode-codeql-starter\ql\javascript\ql\src\Security\CWE-079\Xss.ql クエリを実行し、結果を test.csv 出力する実行例です。

PS C:\ql>codeql database analyze "my-awesome-webapp-qldb" `
vscode-codeql-starter\ql\javascript\ql\src\Security\CWE-079\Xss.ql `
--format csv `
--output test.csv `
--threads 0

初回実行時は驚くほど時間がかかります(1分くらいかかった)が、2回目以降は早いです。--threads 0 オプションを付けると CPU コアをいい感じに使ってくれます。詳しくは⇒Analyzing databases with the CodeQL CLI

クエリにはディレクトリを指定することもできます。この場合、ディレクトリ以下のクエリすべてが実行されます。セキュリティ関連はSecurityディレクトリにまとまってるので SAST として使うことができます!クエリが多いのでとっても時間がかかります。

PS C:\ql> codeql database analyze "my-awesome-webapp-qldb" `
>> vscode-codeql-starter\ql\javascript\ql\src\Security\ `
>> --format csv `
>> --output test2.csv `
>> --threads 0

100万年待ってもクエリが完了しないので、最初に実行したクエリの結果 test.csv を見てみます。

PS C:\ql> cat .\test.csv
"Client-side cross-site scripting","Writing user input directly to the DOM allows for a cross-site scripting vulnerability.","error","Cross-site scripting vulnerability due to [[""user-provided value""|""relative:///index.html:4:34:4:41""]].","/index.html","4","24","4","60"

Client-side cross-site scripting:ユーザが入力した値を直接DOMに書いたらクロスサイトスクリプティングできちゃうぞ!らしいです。

CSVの最後の5カラム "/index.html","4","24","4","60" はソースコードの該当箇所です。index.html の4行目24文字目~4行目60文字目 がクソコードと指摘しています。
2020-09-17-21-02-45.png
確認したところ、指摘通りクロスサイトスクリプティング攻撃が可能でした。リリースする前に気づけて良かった!
2020-09-17-20-20-16.png

おわり。

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

[js]return falseについてメモ

バブリング

HTML要素で子要素から親要素へイベントが伝播していくこと

下の例ではchild要素をクリックすると親要素のクリックイベントも発火する

<div id="parent" onClick="parentFunc()">
  <div id="child" onClick="childFunc()">
  </div>
</div>

簡単な対象法として jqueryのreturn false

htmlでonClickつけるのをやめて

$('#parent').click(function(){

 処理
 
});

$('#child').click(function(){
  処理
 return false;
});

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

【JavaScript】角が丸い四角形【Canvas】

イメージ

角丸の四角形を描画する関数です。
おまけで、吹き出しが下側に出るやつも作りました。
無題.png
使い方はこんな感じです。

const ctx = $('#myCanvas')[0].getContext("2d");

ctx.fillStyle = 'green';
ctx.strokeStyle = 'red';
ctx.lineWidth = 5;

// 角丸四角形  
fillRoundRect(ctx, 50, 100, 200, 100, 20);
strokeRoundRect(ctx, 50, 100, 200, 100, 20);

// 吹き出し付き角丸四角形    
fillBalloonRoundRect(ctx, 300, 100, 200, 100, 20, 30, 50, 50);
strokeBalloonRoundRect(ctx, 300, 100, 200, 100, 20, 30, 50, 50);

ソース

/**
 * 角が丸い四角形のパスを作成する
 * @param  {CanvasRenderingContext2D} ctx コンテキスト
 * @param  {Number} x   左上隅のX座標
 * @param  {Number} y   左上隅のY座標
 * @param  {Number} w   幅
 * @param  {Number} h   高さ
 * @param  {Number} r   半径
 */
function createRoundRectPath(ctx, x, y, w, h, r) {
    ctx.beginPath();
    ctx.moveTo(x + r, y);
    ctx.lineTo(x + w - r, y);
    ctx.arc(x + w - r, y + r, r, Math.PI * (3/2), 0, false);
    ctx.lineTo(x + w, y + h - r);
    ctx.arc(x + w - r, y + h - r, r, 0, Math.PI * (1/2), false);
    ctx.lineTo(x + r, y + h);       
    ctx.arc(x + r, y + h - r, r, Math.PI * (1/2), Math.PI, false);
    ctx.lineTo(x, y + r);
    ctx.arc(x + r, y + r, r, Math.PI, Math.PI * (3/2), false);
    ctx.closePath();
}

/**
 * 角が丸い四角形を塗りつぶす
 * @param  {CanvasRenderingContext2D} ctx コンテキスト
 * @param  {Number} x   左上隅のX座標
 * @param  {Number} y   左上隅のY座標
 * @param  {Number} w   幅
 * @param  {Number} h   高さ
 * @param  {Number} r   半径
 */
function fillRoundRect(ctx, x, y, w, h, r) {
    createRoundRectPath(ctx, x, y, w, h, r);
    ctx.fill();
}

/**
 * 角が丸い四角形を描画
 * @param  {CanvasRenderingContext2D} ctx コンテキスト
 * @param  {Number} x   左上隅のX座標
 * @param  {Number} y   左上隅のY座標
 * @param  {Number} w   幅
 * @param  {Number} h   高さ
 * @param  {Number} r   半径
 */
function strokeRoundRect(ctx, x, y, w, h, r) {
    createRoundRectPath(ctx, x, y, w, h, r);
    ctx.stroke();       
}

/**
 * 角が丸い四角形の吹き出し(吹き出しは下側)のパスを作成する
 * @param  {CanvasRenderingContext2D} ctx コンテキスト
 * @param  {Number} x   左上隅のX座標
 * @param  {Number} y   左上隅のY座標
 * @param  {Number} w   幅
 * @param  {Number} h   高さ
 * @param  {Number} r   半径
 * @param  {Number} bl  三角形の左上隅のX座標
 * @param  {Number} br  三角形の右上隅のX座標
 * @param  {Number} bh  三角形の高さ
 */
function createBalloonRoundRectPath(ctx, x, y, w, h, r, bl, br, bh) {
    ctx.beginPath();
    ctx.moveTo(x + r, y);
    ctx.lineTo(x + w - r, y);
    ctx.arc(x + w - r, y + r, r, Math.PI * (3/2), 0, false);
    ctx.lineTo(x + w, y + h - r);
    ctx.arc(x + w - r, y + h - r, r, 0, Math.PI * (1/2), false);        
    ctx.lineTo(x + br, y + h);
    ctx.lineTo(x + (br + bl) / 2, y + h + bh);
    ctx.lineTo(x + bl, y + h);
    ctx.lineTo(x + r, y + h);
    ctx.arc(x + r, y + h - r, r, Math.PI * (1/2), Math.PI, false);
    ctx.lineTo(x, y + r);
    ctx.arc(x + r, y + r, r, Math.PI, Math.PI * (3/2), false);
    ctx.closePath();
}

/**
 * 角が丸い四角形の吹き出し(吹き出しは下側)を塗りつぶす
 * @param  {CanvasRenderingContext2D} ctx コンテキスト
 * @param  {Number} x   左上隅のX座標
 * @param  {Number} y   左上隅のY座標
 * @param  {Number} w   幅
 * @param  {Number} h   高さ
 * @param  {Number} r   半径
 * @param  {Number} bl  三角形の左上隅のX座標
 * @param  {Number} br  三角形の右上隅のX座標
 * @param  {Number} bh  三角形の高さ
 */
function fillBalloonRoundRect(ctx, x, y, w, h, r, bl, br, bh) {
    createBalloonRoundRectPath(ctx, x, y, w, h, r, bl, br, bh);
    ctx.fill();     
}

/**
 * 角が丸い四角形の吹き出し(吹き出しは下側)を描画する
 * @param  {CanvasRenderingContext2D} ctx コンテキスト
 * @param  {Number} x   左上隅のX座標
 * @param  {Number} y   左上隅のY座標
 * @param  {Number} w   幅
 * @param  {Number} h   高さ
 * @param  {Number} r   半径
 * @param  {Number} bl  三角形の左上隅のX座標
 * @param  {Number} br  三角形の右上隅のX座標
 * @param  {Number} bh  三角形の高さ
 */
function strokeBalloonRoundRect(ctx, x, y, w, h, r, bl, br, bh) {
    createBalloonRoundRectPath(ctx, x, y, w, h, r, bl, br, bh);
    ctx.stroke();       
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

簡単なアプリ 複数計算機 +

簡単な計算機をつくて見ました。

使用言語はHTMLと、Javascriptです、

HTMLのコードです。

計算プログラム

  <h2>足し算</h2>

  <div>
     1つ目の値<input type="text" id="atai1">       <br>

     2つ目の値<input type="text" id="atai2">       <br>

     3つ目の値<input type="text" id="atai3">       <br>

     4つ目の値<input type="text" id="atai4">       <br>

     5つ目の値<input type="text" id="atai5">       <br>

            
     <button onclick="tashizan()">
        足し算
     </button>        <br>                           


     結果<br>

     <div id="kekka">


     </div> 

  </div>

次にJSのコードです。

 

   //足し算の式


        function tashizan(){



     text01 = document.getElementById('atai1');
     text02 = document.getElementById('atai2');
     text03 = document.getElementById('atai3');
     text04 = document.getElementById('atai4');
     text05 = document.getElementById('atai5');




     x = parseInt(0 + text01.value);
     y = parseInt(0 + text02.value);
     u = parseInt(0 + text03.value);
     k = parseInt(0 + text04.value);
     t = parseInt(0 + text05.value);





     z = x + y + u + k + t;

     kekka = document.getElementById('kekka');

     kekka.innerHTML = z; 

        }

完成形はこんな感じです。

足し算.jpg

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

[javascript] Auth0でManagement APIをより簡単に使う

通常ルート

Auth0 Management APIドキュメント

  1. Machine-to-Machine Applicationをまず作る(参考)
  2. デフォルトで存在するAuth0 Management APIを選択しpermissions(scopes)を選ぶ
  3. 出来たMachine-to-Machine Applicationのclient_idclient_secretでManagement APIのtokenを取る
  4. authorization: Bearer ${token}みたいな形でManagement APIを呼ぶ

REST APIなので通信ができれば言語は問わないが、Javascript(node)でAPIを呼ぶ前提なら、かなり簡素にかける。

node-auth0 npmパッケージを使う

readmeにあるサンプルコード
var ManagementClient = require('auth0').ManagementClient;
var auth0 = new ManagementClient({
  domain: '{YOUR_ACCOUNT}.auth0.com',
  clientId: '{YOUR_NON_INTERACTIVE_CLIENT_ID}',
  clientSecret: '{YOUR_NON_INTERACTIVE_CLIENT_SECRET}',
  scope: 'read:users update:users'
});

ManagementClientを使って、clientId、clientSecretを渡せば裏でManagement APIのtokenを取ってきてくれるので、コードがスッキリする。

githubのリポジトリを見るとわかるが、ManagementClientには26個のプロパティがあって、

// https://auth0.com/docs/api/management/v2#!/Users_By_Email/get_users_by_email
auth0.users.getByEmail("email@address.local", (err, users) => console.log(users))

みたいにAPIの呼び出しが全てラップされている。

現状Typescriptは用意されていないようなので、ソースを読まないといけないのが難点。
現状これがドキュメントっぽい。

躓き点

Application_Details.png

domainとかがdomain: '{YOUR_ACCOUNT}.auth0.com'になっているけど、最近作ったアカウントのApplicationのdomainは.usが間に入ってて、気づかずdev-tm15fv0o.auth0.comみたいな形でテストしてた。
これでずっとaccess_denied: {"error":"access_denied","error_description":"Unauthorized"}のエラーで悩まされてた。

ubuntu_❐_0_●_2_docker.png

(accountとも言うし、tenantとも言うし、他にも色々同じ箇所を別の言い方で説明しているのでとにかくドキュメントが読みづらい...)

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

Rails6でいいね機能【非同期通信】

はじめに

こんにちわ!プログラミング初心者のものです!
今回はめちゃめちゃ大変だったいいね機能の非同期通信を自身の備忘録と理解を深めるために投稿します!
初心者のため書き間違え等あると思いますがその時はコメント等で教えていただけると幸いです!

さて、今回は凄くシンプルないいね機能の実装をしました。
内容はハートを押すと色付きのものに変わるというもの。
いいねの数などは付けなかったので機能的には不十分かもしれないけど、見た目的にはいいよ!って感じです。

今回機能実装にあたり下記の記事を参考にさせていただきました!
railsとjsを使ったお手軽「いいね♡機能」

前提条件

・Railsのバージョンは6.0.0を使っていること。
バージョンの違いで記述が変わってくる所があるので、注意が必要です!
実際ここを気づかなかったせいでとても時間食いました。
他のバージョンを使用している場合は他の記事で同じ開発環境の方を探してみるのがいいかも知れません。
・deviseを使っていること
ユーザー機能の実装の為、deviseを導入している前提で書きます!
・user/postのコントローラー、モデル、テーブルは作成済み
今回は既に作成されてあるアプリに追加機能でいいね機能を実装する流れで進みます。
作成するアプリは様々だと思いますが、今回は写真投稿型のアプリを例としているため、ユーザー機能、投稿機能は作成されている前提で説明をしています。

準備


今回は非同期通信でいいねをするため、JavaScriptのライブラリであるjQueryを使用します。

1.Gemの導入

jQueryを使用する為に、Gemを導入しましょう。
Gemfileに下のコードを書きます。

gem 'jquery-rails'

ターミナルでbundle installを忘れずに!
僕は癖でサーバーの再起動もついでにやります!

その後アプリにjQueryを読み込む為のコードを追加します!
個人的にここが一番重要です!
ここが無いと非同期になりません!

application.html.erb
//<head>内の一番下に//
<script src="http://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js"></script>

2.likesテーブル、コントローラーの作成

いいね機能の実装にはuserとpostをつなぐ中間テーブルを作成する必要があります。
どのユーザーがいいねをしているか、どの投稿に対していいねをしているかの情報が必要だからです。
今回はlikesという名前で作成していきます。
ターミナルでモデルを作成したら、マイグレーションファイルを編集していきます。

class CreateLikes < ActiveRecord::Migration[6.0]
  def change
    create_table :likes do |t|
      t.integer :post_id
      t.integer :user_id

      t.timestamps
      t.index [:user_id, :post_id], unique: true
    end
  end
end

user_id、post_idを作成します。カラムの型はinteger型で作成します。
ターミナルでmigrateしたら、モデルファイルにアソシエーションを設定します。

models/like.rb
class Like < ApplicationRecord
  belongs_to :post
  belongs_to :user

  validates_uniqueness_of :post_id, scope: :user_id
end

user/postモデルとは
user/postモデル 1 対 多 likeモデル
の関係です。
user/postモデル側にはhas_manyでlikesを追加しましょう。
また、dependent: :destroyを付けると、投稿が削除された時にいいねも一緒に削除することができます。
そして、postモデルにはメソッドを定義します。

models/post.rb
class Post < ApplicationRecord
  belongs_to :user
  has_many :likes, dependent: :destroy

 def like_user(user_id)
    likes.find_by(user_id: user_id)
   end
end

このメソッドは後々使いますが、簡単に言うとユーザーが既にいいねをしているかどうかを判別する時に使うメソッドです。

続いてコントローラーです。
ターミナルでコントローラーを作成したら以下のように編集してください。

likes_controller.rb
class LikesController < ApplicationController
  before_action :set_post, only: [:create, :destroy]

  def create
    @like = Like.create(user_id: current_user.id, post_id: params[:post_id])
  end

  def destroy
    like = Like.find_by(user_id: current_user.id, post_id: params[:post_id])
    like.destroy
  end

  private

  def set_post
    @post = Post.find(params[:post_id])
  end
end

アクションはcreate,destroyのみです。
createアクションにて@likeにuser_idとpost_idを渡してあげます。
destroyアクションはcreateアクションをそのまま消すって感じですね。
人によってはfind_byではなくfindになる場合があります。

今回は可読性向上の為に before_actionにてプライベートメソッドを呼び出しています。
どの投稿か@postにpost_idを渡してあげます。

3.ルーティングをネストさせる

likesコントローラーをpostsコントローラーにネストさせ、親子関係を表現します。

routes.rb
resources :posts do
    resources :likes, only: [:create, :destroy]
  end

ネストさせたら一度ターミナルでrails routesで一度確認をしましょう。

本番

ここからが本番。
今回はいいねをしているときとしていないときの条件式を部分テンプレートを使って作ります。

1.部分テンプレートの作成

まず、views/likesディレクトリに部分テンプレートファイルを作成します。

今回の設定ではログイン状態でなければいいねボタンが表示されないようにしてます。
そしてログイン状態のユーザーの場合はそのユーザーが既にいいねをしているかどうかをlike_userメソッドを使って判断します。

_like.html.erb
<% if user_signed_in? %>

  <% unless @post.like_user(current_user.id).blank? %>
    <%= link_to post_like_path(post_id: @post.id, id: @post.likes[0].id), method: :delete, remote: true do %>
      <div class="vertical_like">
       <%= image_tag "icon_red_heart.png", size: '40x40' %>
      </div>
    <% end %>
  <% else %>
    <%= link_to post_likes_path(@post.id), method: :post, remote: true do %>
      <div class="vertical_like">
        <%= image_tag "icon_heart.png", size: '40x40' %>
      </div>
    <% end %>
  <% end %>
<% end %>

重要な点はlink_toのパスの引数の部分です。ここで使っているインスタンス変数をコントローラー側でしっかりと指定していないとエラーになります。
imgタグで使用しているアイコンは適宜、自分の表示させたい画像を指定させてください。

2.JavaScriptファイル作成

続いてJavaScriptを作成していきます。
部分テンプレートと同じディレクトリで作成しましょう!

create.js.erb
$('#likes_buttons<%= @post.id %>').html("<%= j(render partial: 'likes/like', locals: {post: @post,like: @like}) %>");
destroy.js.erb
$('#likes_buttons<%= @post.id %>').html("<%= j(render partial: 'likes/like', locals: {post: @post, like: @like}) %>");

それぞれ一行だけですね!
とても簡単です。

3.いいねを表示させたい所で呼び出す

そしていいねを呼び出したい場所で

 <div id="likes_buttons<%= @post.id %>">
   <%= render partial: 'likes/like', locals: { post: @post, like: @like} %>
 </div>

これで実装完了です!
今回はシンプルにアイコンだけ非同期で動くいいね機能を実装してみましたが、
終わってみればコードは簡単そうに見えますがエラーの地獄でした笑
インスタンス変数が違ったり、スペルミスがあったりと一つ抜けると動かないような感じでしたね。
解説が苦手なので、わかりにくい部分が多いかもしれませんが、いいね機能の記事はたくさんあるので複数参考にすると理解が深まりまると思います!

プログラミング初心者はこの機能を実装してみたい人が多い気がするので、少しでも参考になればと思います!
動くと感動するので実装させたい人は頑張ってください!!

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

連想配列のキャメルケースとスネークケースの変換忘備録

概要

クライアントとサーバーで形式がキャメルケースとスネークケースで違うのが面倒なので変換処理を作ったので忘備録

パラメータの連想配列をキャメルケースからスネークケースに変更

const fCamelToSnake = p => {
  return p.replace(/([A-Z])/g,
    function (s) {
      return '_' + s.charAt(0).toLowerCase()
    }
  )
}

const camelToSnakeByDict = arg => {
  const snakeParams = {}
  for (const key in arg) {
    const snakeKey = fCamelToSnake(key)
    snakeParams[snakeKey] = arg[key]
  }
  return snakeParams
}

結果

const camelDic = {appleBanana:1,  cherryApricotCoconut:'2', cranberryKiwiLemonLime: true}
const snakeDic = camelToSnakeByDict(camelDic)
console.log(snakeDic) 
/**
{
  apple_banana: 1,
  cherry_apricot_coconut: '2',
  cranberry_kiwi_lemon_lime: true
} 
**/

レスポンスの連想配列をスネークケースからキャメルケースに変更

const fSnakeToCamel = p => {
  return p.replace(/_./g,
    function (s) {
      return s.charAt(1).toUpperCase()
    }
  )
}

const isDictObject = o => {
  return o instanceof Object && !(o instanceof Array)
}

const isArrayObject = o => {
  return o instanceof Array
}

const snakeToCamelByDict = arg => {
   // こちらは階層が深くなるので再帰的に処理
   if (isArrayObject(arg)) {
    const newList = []
    for (const v of arg) {
      newList.push(snakeToCamelByDict(v))
    }
    return newList
  } else if (isDictObject(arg)) {
    const newDict = {}
    for (const key in arg) {
      const newVal = snakeToCamelByDict(arg[key])
      const newKey = fSnakeToCamel(key)
      newDict[newKey] = newVal
    }
    return newDict
  } else {
    return arg
  }
}

結果

const snakeDic = {apple_banana:1,  cherry_apricot_coconut:[{orange_papaya:'2'},{strawberry_plum:'3'}], cranberry_kiwi_lemon_lime: {mango_melon: '4'}}
const camelDic = camelToSnakeByDict(snakeDic)
console.log(camelDic) 
/**
{
  appleBanana: 1,
  cherryApricotCoconut: [ { orangePapaya: '2' }, { strawberryPlum: '3' } ],
  cranberryKiwiLemonLime: { mangoMelon: '4' }
}
**/

参照

https://qiita.com/thelarch/items/cc4707e1c7ef0d73ba73

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

連想配列のkeyをキャメルケースとスネークケースで変換する忘備録

概要

クライアントとサーバーで連想配列のkeyの形式がキャメルケースとスネークケースで違うのが面倒なので変換処理を作ったので忘備録

パラメータの連想配列のkeyをキャメルケースからスネークケースに変更

const fCamelToSnake = p => {
  return p.replace(/([A-Z])/g,
    function (s) {
      return '_' + s.charAt(0).toLowerCase()
    }
  )
}

const camelToSnakeByDict = arg => {
  const snakeParams = {}
  for (const key in arg) {
    const snakeKey = fCamelToSnake(key)
    snakeParams[snakeKey] = arg[key]
  }
  return snakeParams
}

結果

const camelDic = {appleBanana:1,  cherryApricotCoconut:'2', cranberryKiwiLemonLime: true}
const snakeDic = camelToSnakeByDict(camelDic)
console.log(snakeDic) 
/**
{
  apple_banana: 1,
  cherry_apricot_coconut: '2',
  cranberry_kiwi_lemon_lime: true
} 
**/

レスポンスの連想配列のkeyをスネークケースからキャメルケースに変更

const fSnakeToCamel = p => {
  return p.replace(/_./g,
    function (s) {
      return s.charAt(1).toUpperCase()
    }
  )
}

const isDictObject = o => {
  return o instanceof Object && !(o instanceof Array)
}

const isArrayObject = o => {
  return o instanceof Array
}

const snakeToCamelByDict = arg => {
   // こちらは階層が深くなるので再帰的に処理
   if (isArrayObject(arg)) {
    const newList = []
    for (const v of arg) {
      newList.push(snakeToCamelByDict(v))
    }
    return newList
  } else if (isDictObject(arg)) {
    const newDict = {}
    for (const key in arg) {
      const newVal = snakeToCamelByDict(arg[key])
      const newKey = fSnakeToCamel(key)
      newDict[newKey] = newVal
    }
    return newDict
  } else {
    return arg
  }
}

結果

const snakeDic = {apple_banana:1,  cherry_apricot_coconut:[{orange_papaya:'2'},{strawberry_plum:'3'}], cranberry_kiwi_lemon_lime: {mango_melon: '4'}}
const camelDic = camelToSnakeByDict(snakeDic)
console.log(camelDic) 
/**
{
  appleBanana: 1,
  cherryApricotCoconut: [ { orangePapaya: '2' }, { strawberryPlum: '3' } ],
  cranberryKiwiLemonLime: { mangoMelon: '4' }
}
**/

参照

https://qiita.com/thelarch/items/cc4707e1c7ef0d73ba73

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

今更きく…Vue3とは?

まえがき

最近、社内でVue2で作られているアプリケーションのUIを刷新する計画をしており、Vue3を導入するかの議論がされました。
2018年後半からVue3の開発が開始されてから約二年。
Vue3もRCとなり、リリースに向けて動きはじめました。
Vue3のComposition API等具体的な実装部分に触れている記事はよくみますが
大枠に触れている記事が少ないので書いてみます。

EvanYou氏がある記事の中で、「なぜ書き換えたか?」について語っているので、私なりにまとめました。

なぜ書き換えたか?

新しい言語機能の活用

ES2015の最新版への対応が各ブラウザで行われており、Vueも対応する必要がありました
その中でも、Proxyが一番注目しており、VueでもProxyを活用することで、パフォーマンス改善を行うことができます。

アーキテクチャの問題へ対応

Vueではコードが暗黙的な結合という形で、技術負債を積み上げてきました。
それにより、コントリビューターが変更を加えることが困難になっていました。
これらを解決し、コードを変更をしやすくする必要がありました。

改善点

Typescriptのサポート

Vue2はもともとプレーンESで作成されていましたが、TypeScriptをサポートしました。

内部パケージの分離

monorepo(一つのリポジトリでパッケージを管理すること)で、内部パッケージ化を行い、それぞれが独自のAPI、タイプ定義、およびテストを実装しています。
それにより、モジュール間の依存関係をより明確にし、開発者がすべてを読み、理解し、変更しやすくし、プロジェクトの貢献の障壁を下げ、長期的な保守性を改善しました。

RFCプロセスの設定

ユーザーが重大な変更についてフィードバックを提供できるよう、RFC(Request for Comments)プロセスを採用しました。
議論はGitHubリポジトリで行われ、提案はプルリクエストとして送信されるため、コメントで有機的に議論が展開されます。

Vueはもともと、軽量のフロントエンドフレームワークですが、更に軽量化を行っています。

仮想DOMのボトルネックの改善

Vue 3では、適切なAST変換パイプラインを使用してコンパイラーを書き直しました。
これにより、コンパイル時の最適化を行っています。

  • ブロック内のノード更新の際、ツリーのトラバースの最適化を行いました。
    • この最適化は、実行する必要のあるツリートラバーサルの量を1桁減らすことで、仮想DOMのオーバーヘッドの多くを回避します。
  • メモリ使用率軽減が大幅に向上し、ガベージコレクションの頻度が減少します。
  • 要素レベルでは、コンパイル段階で実行計画の作成を行い、ランタイムがそれをヒントに実行を行うことで高速化を実現しています。

これらの手法を組み合わせると、レンダリング更新のベンチマークが大幅に改善されています。
Vue3がVue2のCPU時間(Javascript計算の実行に費やされた時間)の10分の1未満になることもあります。

バンドルサイズの最小化

フロントエンドフレームワークは、サイズそのものがパフォーマンスに影響しています。
Vueはもともと軽量(圧縮しても約23KB)でしたが、2つの問題に気がつきました。

  • 利用していない機能まで、ダウンロードと解析のコストが発生している。
  • 機能を追加するにつれ、容量は無限に増え続ける。

この問題を解決するにはツリーシェイキングを行い、不要なコードを削除することでした。
Vue3では、ほとんどのグローバルなAPI、内部ヘルパーをESモジュールにすることで、これを実現しました。
多くの新機能の追加にも関わらず、Vue2の半分以下の容量を実現しています。

まとめ

ここでは触れていませんが多くの機能が追加されています。

  • Composition APIの導入
  • multi-v-model機能追加
  • Teleport機能追加
  • Fragment機能追加
  • Suspense機能追加
  • フィルターの廃止

等々…

上記に加え、パフォーマンスの向上を考えると、導入しない手はないかなと個人的に考えており
下位互換性もあるとのことなので、正式版がでたら対応を考えたいと思います。
Composition APIの導入で、少しReact側によったことからReactでもいいのでは?という賛否両論の意見が出ていますが
軽量や学習コストの低さという部分では、Vueが依然として変わらないと思うので、棲み分けとしては十分できているのかと思ってたりします。

今回は、ブログをもとに私が解釈した内容で書いているので、認識の齟齬等あれば教えて頂きたいです。

元ネタ:https://increment.com/frontend/making-vue-3/?ref=madewithvuejs.com#why-rewrite

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

RegExp の複数行記述、RegExp 内での変数参照がしたいので、正規表現を結合する関数を作った

問題

前回の記事 1 では,コードを 80 文字/行以内に収めたいけれど,正規表現リテラルは途中で改行できないので,長い正規表現を一旦文字列型変数に代入してから + で結合 → 正規表現化という手順を踏んでいました.

しかし,この方法には以下のような問題があります.

  • “\” を文字列に含めるには \\ としてやる必要があり,エスケープが二重になって鬱陶しい.
  • 文字列型なので当然正規表現としてのシンタックス・ハイライトはつかない.

解決法

そこで,*/foo/ + /bar|z/ といったかんじで正規表現同士を結合できないか調べてみたところ,RegExp.prototype.source を使う方法を見つけた2ので,それをもとに以下の関数を作ってみました.

TypeScript

concat-regexps.ts
/**
 * @author JuthaDDA
 * @see [RegExp の複数行記述、RegExp 内での変数参照がしたいので、
 *     正規表現を結合する関数を作った - Qiita
 *     ](https://qiita.com/juthaDDA/items/f1093b968faa3d810c1c)
 * @param regExps - Babel (< 7.11.0) を使う場合は, 
 *     名前付きキャプチー・グループを含むと正しく変換されない.
 *     `@babel/plugin-transform-named-capturing-groups-regex (< 7.10.4)` も未対応.
 */
const concatRegExps = ( regExps:RegExp[], flags?:string ):RegExp => {
    return RegExp(
        regExps.reduce( ( acc, cur ) => acc + cur.source, '' ),
        flags,
    );
};

JavaScript


concat-regexps.js
/**
 * @author JuthaDDA
 * @see [RegExp の複数行記述、RegExp 内での変数参照がしたいので、
 *     正規表現を結合する関数を作った - Qiita
 *     ](https://qiita.com/juthaDDA/items/f1093b968faa3d810c1c)
 * @param {RegExp[]} regExps - Babel (< 7.11.0) を使う場合は,
 *     名前付きキャプチー・グループを含むと正しく変換されない.
 *     `@babel/plugin-transform-named-capturing-groups-regex` (< 7.10.4) も未対応.
 * @param {string}   [flags]
 * @return {RegExp}
 */
export const concatRegExps = ( regExps, flags ) => {
    return RegExp(
        regExps.reduce( ( acc, cur ) => acc + cur.source, '' ),
        flags,
    );
};


説明

concatRegExps() 第 1 引数に RegExp の配列 regExps を,第 2 引数に 'g' などの flag を取ります.

RegExp.prototype.source には,“\” などをエスケープして string 化したソース・テキストが入っているので,これを regExps.reduce() で結合し,flag で指定したフラグを附けた RegExp を生成して return しています.なお,0 === regExps.length の場合の戻り値は,\(?:)\ です.

regExps の要素には,当然 RegExp 型の変数を含むことができるので,

const width = '50%';
const realNumRegExp = /[+-]?\d*\.?\d+/;
const unitRegExp = /(?:px|em|rem|vw|%)/;
const widthRegExp = concatRegExps( [ /^/, realNumRegExp, unitRegExp, /$/ ] );
const isValidWidth = widthRegExp.test( width );

のような使い方も可能です.

補足

Babel を使う場合は,@babel/plugin-transform-named-capturing-groups-regex3babel-plugin-transform-modern-regexp を入れても,名前付きキャプチャー・グループと RegExp.prototype.source を併用すると変換がおかしくなるので,第一引数の要素には名前付きキャプチャー・グループを含まないようにする必要があります.4


  1. 本記事の内容を反映して更新したので,編集前のコードは、こちら を見てください.  

  2. Cf. How can I concatenate regex literals in JavaScript? - Stack Overflow 

  3. Cf. `@babel/plugin-transform-named-capturing-groups-regex` fails to wrap all groups · Issue #10045 · babel/babel 

  4. 2020 年 9 月 17 日現在の最新版は,Babel 7.11.0, babel-plugin-transform-modern-regexp 7.10.4, babel-plugin-transform-modern-regexp 0.0.6 

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

RegExp の複数行記述、RegExp 内での変数参照がしたいので、RegExp を結合する関数を作った

問題

前回の記事 1 では,コードを 80 文字/行以内に収めたいけれど,正規表現リテラルは途中で改行できないので,長い正規表現を一旦文字列型変数に代入してから + で結合 → 正規表現化という手順を踏んでいました.

しかし,この方法には以下のような問題があります.

  • “\” を文字列に含めるには \\ としてやる必要があり,エスケープが二重になって鬱陶しい.
  • 文字列型なので当然正規表現としてのシンタックス・ハイライトはつかない.

解決法

そこで,*/foo/ + /bar|z/ といったかんじで正規表現同士を結合できないか調べてみたところ,RegExp.prototype.source を使う方法を見つけた2ので,それをもとに以下の関数を作ってみました.

TypeScript

concat-regexps.ts
/**
 * @author JuthaDDA
 * @see [RegExp の複数行記述、RegExp 内での変数参照がしたいので、
 *     正規表現を結合する関数を作った - Qiita
 *     ](https://qiita.com/juthaDDA/items/f1093b968faa3d810c1c)
 * @param regExps - Babel (< 7.11.0) を使う場合は, 
 *     名前付きキャプチー・グループを含むと正しく変換されない.
 *     `@babel/plugin-transform-named-capturing-groups-regex (< 7.10.4)` も未対応.
 */
const concatRegExps = ( regExps:RegExp[], flags?:string ):RegExp => {
    return RegExp(
        regExps.reduce( ( acc, cur ) => acc + cur.source, '' ),
        flags,
    );
};

JavaScript


concat-regexps.js
/**
 * @author JuthaDDA
 * @see [RegExp の複数行記述、RegExp 内での変数参照がしたいので、
 *     正規表現を結合する関数を作った - Qiita
 *     ](https://qiita.com/juthaDDA/items/f1093b968faa3d810c1c)
 * @param {RegExp[]} regExps - Babel (< 7.11.0) を使う場合は,
 *     名前付きキャプチー・グループを含むと正しく変換されない.
 *     `@babel/plugin-transform-named-capturing-groups-regex` (< 7.10.4) も未対応.
 * @param {string}   [flags]
 * @return {RegExp}
 */
const concatRegExps = ( regExps, flags ) => {
    return RegExp(
        regExps.reduce( ( acc, cur ) => acc + cur.source, '' ),
        flags,
    );
};


説明

concatRegExps() 第 1 引数に RegExp の配列 regExps を,第 2 引数に 'g' などの flag を取ります.

RegExp.prototype.source には,“\” などをエスケープして string 化したソース・テキストが入っているので,これを regExps.reduce() で結合し,flag で指定したフラグを附けた RegExp を生成して return しています.なお,0 === regExps.length の場合の戻り値は,\(?:)\ です.

regExps の要素には,当然 RegExp 型の変数を含むことができるので,

const width = '50%';
const realNumRegExp = /[+-]?\d*\.?\d+/;
const unitRegExp = /(?:px|em|rem|vw|%)/;
const widthRegExp = concatRegExps( [ /^/, realNumRegExp, unitRegExp, /$/ ] );
const isValidWidth = widthRegExp.test( width );

のような使い方も可能です.

補足

Babel を使う場合は,@babel/plugin-transform-named-capturing-groups-regex3babel-plugin-transform-modern-regexp を入れても,名前付きキャプチャー・グループと RegExp.prototype.source を併用すると変換がおかしくなるので,第一引数の要素には名前付きキャプチャー・グループを含まないようにする必要があります.4


  1. 本記事の内容を反映して更新したので,編集前のコードは、こちら を見てください.  

  2. Cf. How can I concatenate regex literals in JavaScript? - Stack Overflow 

  3. Cf. `@babel/plugin-transform-named-capturing-groups-regex` fails to wrap all groups · Issue #10045 · babel/babel 

  4. 2020 年 9 月 17 日現在の最新版は,Babel 7.11.0, babel-plugin-transform-modern-regexp 7.10.4, babel-plugin-transform-modern-regexp 0.0.6 

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

【JS】RegExp の複数行記述、RegExp 内での変数参照がしたいので、RegExp を結合する関数を作った

問題

前回の記事 1 では,コードを 80 文字/行以内に収めたいけれど,正規表現リテラルは途中で改行できないので,長い正規表現を一旦文字列型変数に代入してから + で結合 → 正規表現化という手順を踏んでいました.

しかし,この方法には以下のような問題があります.

  • “\” を文字列に含めるには \\ としてやる必要があり,エスケープが二重になって鬱陶しい.
  • 文字列型なので当然正規表現としてのシンタックス・ハイライトはつかない.

解決法

そこで,*/foo/ + /bar|z/ といったかんじで正規表現同士を結合できないか調べてみたところ,RegExp.prototype.source を使う方法を見つけた2ので,それをもとに以下の関数を作ってみました.

TypeScript

concat-regexps.ts
/**
 * @author JuthaDDA
 * @see [RegExp の複数行記述、RegExp 内での変数参照がしたいので、
 *     正規表現を結合する関数を作った - Qiita
 *     ](https://qiita.com/juthaDDA/items/f1093b968faa3d810c1c)
 * @param regExps - Babel (< 7.11.0) を使う場合は, 
 *     名前付きキャプチー・グループを含むと正しく変換されない.
 *     `@babel/plugin-transform-named-capturing-groups-regex (< 7.10.4)` も未対応.
 */
const concatRegExps = ( regExps:RegExp[], flags?:string ):RegExp => {
    return RegExp(
        regExps.reduce( ( acc, cur ) => acc + cur.source, '' ),
        flags,
    );
};

JavaScript


concat-regexps.js
/**
 * @author JuthaDDA
 * @see [RegExp の複数行記述、RegExp 内での変数参照がしたいので、
 *     正規表現を結合する関数を作った - Qiita
 *     ](https://qiita.com/juthaDDA/items/f1093b968faa3d810c1c)
 * @param {RegExp[]} regExps - Babel (< 7.11.0) を使う場合は,
 *     名前付きキャプチー・グループを含むと正しく変換されない.
 *     `@babel/plugin-transform-named-capturing-groups-regex` (< 7.10.4) も未対応.
 * @param {string}   [flags]
 * @return {RegExp}
 */
const concatRegExps = ( regExps, flags ) => {
    return RegExp(
        regExps.reduce( ( acc, cur ) => acc + cur.source, '' ),
        flags,
    );
};


説明

concatRegExps() 第 1 引数に RegExp の配列 regExps を,第 2 引数に 'g' などの flag を取ります.

RegExp.prototype.source には,“\” などをエスケープして string 化したソース・テキストが入っているので,これを regExps.reduce() で結合し,flag で指定したフラグを附けた RegExp を生成して return しています.なお,0 === regExps.length の場合の戻り値は,\(?:)\ です.

regExps の要素には,当然 RegExp 型の変数を含むことができるので,

const width = '50%';
const realNumRegExp = /[+-]?\d*\.?\d+/;
const unitRegExp = /(?:px|em|rem|vw|%)/;
const widthRegExp = concatRegExps( [ /^/, realNumRegExp, unitRegExp, /$/ ] );
const isValidWidth = widthRegExp.test( width );

のような使い方も可能です.

補足

Babel を使う場合は,@babel/plugin-transform-named-capturing-groups-regex3babel-plugin-transform-modern-regexp を入れても,名前付きキャプチャー・グループと RegExp.prototype.source を併用すると変換がおかしくなるので,第一引数の要素には名前付きキャプチャー・グループを含まないようにする必要があります.4


  1. 本記事の内容を反映して更新したので,編集前のコードは、こちら を見てください.  

  2. Cf. How can I concatenate regex literals in JavaScript? - Stack Overflow 

  3. Cf. `@babel/plugin-transform-named-capturing-groups-regex` fails to wrap all groups · Issue #10045 · babel/babel 

  4. 2020 年 9 月 17 日現在の最新版は,Babel 7.11.0, babel-plugin-transform-modern-regexp 7.10.4, babel-plugin-transform-modern-regexp 0.0.6 

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

JavaScriptを単体テストする流行りのフレームワークJestを試してみる

Jestとは

JestはJavaScriptの単体テストのフレームワークです。
https://jestjs.io/ja/

テストランナーだけでなく、モック機能やカバレッジの取得を使用することができます。npmのトレンドとしては2019年から伸びてmochaを超えるものとなっています。

image.png
https://www.npmtrends.com/jest-vs-jasmine-vs-mocha-vs-qunit

この記事は公式のサンプルコードを弄ってその挙動を確認するものとなっています。
また、実験環境は以下の通りです。

OS:MacOS Catalina 10.15.6
node.js: v14.10.1

簡単なはじめ方

jestを使用するためにnode.jsのプロジェクトを以下のように作成します。

 # package.jsonを作成する
 npm init -y
 # package.jsonにjestを追加してnode_modulesにインストール
 npm install --save-dev jest

jestではデフォルトでは__tests__フォルダ中のjavascritpをテストコードとして実行するので、__tests__フォルダにテストコードを追加します。

__tests__/sample.test.js
test('test 1.', ()=>{
  expect(1+2).toBe(3);
});

テストを動かすには以下のコマンドを実行します。

# テストを開始する
npx jest

image.png

設定ファイル

jestの設定は以下のいずれかの方法で指定することができます。

  • package.jsonに記載する
  • jest.config.jsに記載する
  • 実行時に--configオプションで指定したファイルに指定する

https://jestjs.io/docs/ja/configuration

jest.config.jsの作成

下記のコマンドで対話式でjest.config.jsを作成します。

npx jest --init

image.png

カバレッジの収集方法

下記のように--coverageオプションを指定して実行することでカバレッジの収集を行います。

npx jest --coverage

このコマンドを実行するとテストが実行されて、coverageフォルダに結果が格納されます。
coverage/lcov-report/index.htmlにはhtml形式でレポートが表示されています。

image.png

Jenkins用のテストレポートを作成

Jenkins用のファイルを出力するには下記のプラグインが必要になります。
https://www.npmjs.com/package/jest-jenkins-reporter

以下のコマンドでインストールしてください。

npm install --save-dev jest-jenkins-reporter

レポートの出力方法の指定はjest.config.jsとpackage.jsonを使用します。

jest.config.js
// 略
 testResultsProcessor: "jest-jenkins-reporter",
// 略
package.json
   "jestSonar": {
     "reportPath": "reports",
     "reportFile": "test-reporter.xml",
     "indent": 4
   }

この設定を行った後にjestを実行するとreportsフォルダにtest-reporter.xmlに保存されます。
Jenkinsはこれを利用することで単体テストの結果を取得します。

VSCodeを使用したテストコードのデバッグ方法

VSCodeでテストコードをデバッグするにはワークスペースに以下のファイルを作成します。

.vscode/launch.json
   {
     "version": "0.2.0",
     "configurations": [

       {
         "type": "node",
         "request": "attach",
         "name": "Attach",
         "port": 9229
       }
     ]
   }

次にターミナル上で以下のコマンドを実行します。

node --inspect-brk node_modules/.bin/jest --runInBand __tests__/matcher.test.js

この後に、VSCodeのAttachボタンを押下することでデバッグ実行が可能になります。
image.png

以下は実際の画面操作のスクリーンキャプチャとなります。
debug.gif

非同期の試験

https://jestjs.io/docs/ja/asynchronous

do引数の利用

テストメソッドの引数にdoneを与えて、テストメソッド中にそのdoneを実行することで任意のタイミングでテストの終了を通知することができます。

async.test.js
 function async_func() {
   return new Promise(resolve =>{
     console.log('start async_func');
     setTimeout(function() {
       console.log('end async_func');
       resolve('done!');
     }, 100);
   });
 }

 test('async callback test', done=> {
   async_func().then(msg => {
     expect(msg).toBe('done!');
     done();
   });
 });

doneを使用してテストメソッド中に呼ばれない場合はタイムアウトのエラーとなります。

エラーが発生するサンプル
 test('async callback not called', done=> {
   async_func();
 }); 

image.png

Promisesの試験

テスト対象のメソッドがPromisesを返す場合、テストメソッドでそのPromisesを返すことで、それが解決するまでテストメソッドの完了を待つことができます。
マッチャー関数のresolves/rejectsを使用してPromissesの結果を判定することも可能です。

async.test.js
 function async_func() {
   return new Promise(resolve =>{
     console.log('start async_func');
     setTimeout(function() {
       console.log('end async_func');
       resolve('done!');
     }, 100);
   });
 }

 function async_func_err() {
   return new Promise((resolve, reject) =>{
     console.log('start async_func_err');
     setTimeout(function() {
       console.log('end async_func_err');
       reject('error!');
     }, 100);
   });
 }
 test('async promiss resolve test 1', ()=> {
   return async_func().then(msg => {
     console.log('resolve test');
     expect(msg).toBe('done!');
   });
 });

 test('async promiss resolve test 2', ()=> {
   return expect(async_func()).resolves.toBe('done!');
 });

 test('async promiss reject test', ()=> {
   return async_func_err().catch(msg => {
     console.log('reject test');
     expect(msg).toBe('error!');
   });
 });

 test('async promiss reject test 2', ()=> {
   return expect(async_func_err()).rejects.toBe('error!');
 });

async/awaitの使用

async と awaitをテスト中に使用することも可能です。

async.test.js
 test('await ', async ()=> {
   const ret = await async_func();
   console.log(ret);
   expect(ret).toBe('done!');
 });

 test('await 2', async () => {
    await expect(async_func()).resolves.toBe('done!');
 });

setup,teardown使用したテスト毎の処理

各テストの開始前と開始後に特定の処理を実行することができます。

  • beforeAll
    • ファイル内のいずれかのファイルを実行する場合に実行される。
  • afterAll
    • ファイル内の全てのテストが完了した際に実行される。
  • beforeEach
    • ファイルまたはテストスイート内の各テストが実行される前に、関数を実行する。
  • afterEach
    • ファイルまたはテストスイート内の各テストが完了した際に、関数を実行する。

https://jestjs.io/docs/ja/setup-teardown

これらの関数はpromiseを返すことができ、jestはそのpromiseが解決するまでは処理が進みません。このデフォルトのタイムアウトは5000msとなっており、以下のように第二引数でタイムアウトの時間を調整することが可能です。

// 500msタイムアウトするのでエラーとなるサンプル
afterAll(()=> {
  console.log('afterall');
  return new Promise(r=> setTimeout(r, 1000));
}, 500);

beforeEach/afterEachは階層化されたdescribeごとに設定することができます。下記のコードはその実行順を検証したものとなります。

setup_teardown.test.js
 beforeAll(() => {
   console.log('beforeAll'); 
 });

 afterAll(() => {
   console.log('afterAll'); 
 });

 beforeEach(() => {
   console.log('before Each アウトスコープ');
 });

 afterEach(() => {
   console.log('after Each アウトスコープ');
 });

 test('test1', ()=> {
   console.log('tes1 を実行する');
 });

 test('test2', ()=> {
   console.log('tes2 を実行する');
 });

 describe('テストスィート', ()=> {
   beforeEach(() => {
     console.log('before Each スコープ内');
   });
   afterEach(() => {
     console.log('after Each スコープ内');
   });
   test('test3', ()=> {
     console.log('test3 を実行する');
   });
   test('test4', ()=> {
     console.log('test4 を実行する');
   });
 });

 test('test5', ()=>{
   console.log('test5を実行する');
 });

このコードの実行結果は以下のようになります。
describe中で指定したbeforeEach,afterEachは、その構造を考慮した順番で実行されていることが確認できます。

  ● Console

     console.log
       beforeAll

       at Object.<anonymous> (__tests__/setup_teardown.test.js:2:11)

     console.log
       before Each アウトスコープ

       at Object.<anonymous> (__tests__/setup_teardown.test.js:10:11)

     console.log
       tes1 を実行する

       at Object.<anonymous> (__tests__/setup_teardown.test.js:18:11)

     console.log
       after Each アウトスコープ

       at Object.<anonymous> (__tests__/setup_teardown.test.js:14:11)

     console.log
       before Each アウトスコープ

       at Object.<anonymous> (__tests__/setup_teardown.test.js:10:11)

     console.log
       tes2 を実行する

       at Object.<anonymous> (__tests__/setup_teardown.test.js:22:11)

     console.log
       after Each アウトスコープ

       at Object.<anonymous> (__tests__/setup_teardown.test.js:14:11)

     console.log
       before Each アウトスコープ

       at Object.<anonymous> (__tests__/setup_teardown.test.js:10:11)

     console.log
       before Each スコープ内

       at Object.<anonymous> (__tests__/setup_teardown.test.js:27:13)

     console.log
       test3 を実行する

       at Object.<anonymous> (__tests__/setup_teardown.test.js:33:13)

     console.log
       after Each スコープ内

       at Object.<anonymous> (__tests__/setup_teardown.test.js:30:13)

     console.log
       after Each アウトスコープ

       at Object.<anonymous> (__tests__/setup_teardown.test.js:14:11)

     console.log
       before Each アウトスコープ

       at Object.<anonymous> (__tests__/setup_teardown.test.js:10:11)

     console.log
       before Each スコープ内

       at Object.<anonymous> (__tests__/setup_teardown.test.js:27:13)

     console.log
       test4 を実行する

       at Object.<anonymous> (__tests__/setup_teardown.test.js:36:13)

     console.log
       after Each スコープ内

       at Object.<anonymous> (__tests__/setup_teardown.test.js:30:13)

     console.log
       after Each アウトスコープ

       at Object.<anonymous> (__tests__/setup_teardown.test.js:14:11)

     console.log
       before Each アウトスコープ

       at Object.<anonymous> (__tests__/setup_teardown.test.js:10:11)

     console.log
       test5を実行する

       at Object.<anonymous> (__tests__/setup_teardown.test.js:41:11)

     console.log
       after Each アウトスコープ

       at Object.<anonymous> (__tests__/setup_teardown.test.js:14:11)

     console.log
       afterAll

       at Object.<anonymous> (__tests__/setup_teardown.test.js:6:11)

特定のテストを実行するまたは実行しない。

test,it,describeにはskiponlyを使用してテストを実行するかどうかの制御を行うことができます。

skipは動かなくなったテストを暫定的にスキップする場合や、負荷が高くて通常は実行したくないテストを弾く場合に使用します。
まだ未実装のことを現したい場合はtodoを使用しましょう。todoではエラーを出力します。

onlyはデバッグ中に特定のテストのみを動作させる場合に使用します

なお、MsTestにあるpriorityは存在しないので、優先度を指定してのテストは行えないようです。

スキップの例.js
 describe('desc1', () => {
   test.skip('test1', () => {
   });

   test('test2', () => {
     // これだけ実行される
   });
 });

 describe.skip('desc2', ()=> {
   test('test3', () => {
   });

   test('test4', () => {
   });
 });
onlyの例.js
 describe.only('desc1', () => {
   test('test1', () => {
     // 実行される
   });

   test('test2', () => {
     // 実行される
   });
 });

 describe('desc2', ()=> {
   test('test3', () => {
   });

   test('test4', () => {
   });
 });

 describe.only('desc3', ()=> {
   test.only('test5', ()=> {
     // 実行される
   });

   test('test6', ()=> {
   });
 });

モック

jestではモックが使用できます。これを利用してテスト対象に依存する関数をテストに都合のいい振る舞いにすることができます。

特定の関数をモックにする例

spyOnを用いることで特定の関数をモックにすることができます。

テスト対象の関数

calc.js
 function sum(a, b) {
   return a + b;
 }
 function minus(a, b) {
   return a - b;
 }
 module.exports = {
   sum : sum,
   minus : minus
 }

テストコード

__tests__/sample.test.js
 const calc = require('../calc');

 test('test sum.', ()=> {
   expect(calc.sum(1, 2)).toBe(3);
 });
 test('特定の関数についてmockを使用して特定の値を返却する', ()=> {
   // モックを設定する
   console.log('spyOnの前', calc.sum, calc.minus);
   jest.spyOn(calc, 'sum').mockReturnValue(5);
   console.log('spyOnの後', calc.sum, calc.minus);

   // モックを呼び出す
   expect(calc.sum(1,2)).toBe(5);

   // モックを元の関数に戻す
   calc.sum.mockRestore();
   console.log('mockRestoreの後', calc.sum, calc.minus);
   expect(calc.sum(1,2)).not.toBe(5);
 });
 test('特定の関数についてmockを使用して特定の関数を実行する', ()=> {
   jest.spyOn(calc, 'sum').mockImplementation((a,b) => {
     console.log('mock function');
     return 100;
   });
   expect(calc.sum(1,2)).toBe(100);
   jest.restoreAllMocks();
   expect(calc.sum(1,2)).toBe(3);
 });

モジュール内の関数を全てモックにする

jest.mockを使用することで、jest.mockを実行したファイル内では、そのモジュールをモックとして使用します。
この例ではテストコード中でfsモジュールの関数をモックとして使用します。

テスト対象のモジュール

read_file.js
 const fs = require('fs');

 module.exports = function(path) {
   var text = fs.readFileSync(path, 'utf8');
   var lines = text.toString().split('\n');
   return lines;
 }

テストコード

テストコード.js
 const read_file = require('../read_file');
 const fs = require('fs')
 jest.mock('fs');
 test('test 1.', ()=>{
   const contents = 'test\ntest2\ntest3'
   fs.readFileSync.mockReturnValue(contents);
   const results = read_file('test.txt')
   console.log(results);
   expect(results).toEqual(['test','test2','test3']);
 });
 test('test 2.', ()=>{
   const contents = ''
   fs.readFileSync.mockReturnValue(contents);
   const results = read_file('test.txt')
   console.log(results);
   expect(results).toEqual([""]);
 });

マニュアルモックの使用例

マニュアルモックはモックデータを返すスタブを別ファイルに作成することができます。
__mocks__サブディレクトリにモックモジュールを作成します。

https://jestjs.io/docs/ja/manual-mocks

モックモジュール

__mocks__/fs.js
 'use strict';
 const fs = jest.createMockFromModule('fs');
 fs.readFileSync = function(path, enc) {
   return '1234\nabcde\n56789\n';
 }
 module.exports = fs;

テストコード
テストコードではfs.readFileSyncを使用するとモックモジュールで指定した関数が実行されます。

__tests__/manual_mock.test.js
 const read_file = require('../read_file');
 const fs = require('fs')
 jest.mock('fs');
 test('manual mock.', ()=>{
   const results = read_file('test.txt')
   expect(results).toEqual(['1234','abcde','56789', '']);
 });

タイマーと時刻の偽装

useFakeTimers を使用することでsetTimeout, setInterval, clearTimeout, clearInterval, nextTick, setImmediate、clearImmediateなどのタイマー関係の関数やシステム時刻の偽装を行えます。

システム時刻の偽装

setSystemTimeで偽の時間を指定することができます。ただし、これはuseFakeTimersでmodernを指定した時のみ有効です。
時刻を偽装しているときに本当の時刻を取得したい場合はgetRealSystemTimeを使用します。
時刻の偽装を止める場合はuseRealTimersを使用します。

時刻の偽装のテストコードの例.js
 test('時計のモックの確認', ()=> {

   const dumytime = new Date(2020, 0, 1, 23, 55,40);
   expect(new Date()).not.toEqual(dumytime);

   // Fakeの時間を使用する
   jest.useFakeTimers('modern');
   jest.setSystemTime(new Date(2020, 0, 1, 23,55,40));
   expect(new Date()).toStrictEqual(dumytime);

   console.log(new Date(jest.getRealSystemTime()));
   expect(new Date(jest.getRealSystemTime())).not.toStrictEqual(dumytime);

   // 通常の時間を使う
   jest.useRealTimers();
   expect(new Date()).not.toEqual(dumytime);
 });

タイマーの挙動の偽装

以下ではsetTimeoutの偽装を行いモックとして利用する例を示します。
https://jestjs.io/docs/ja/timer-mocks

テスト対象の関数

timerGame.js
 'use strict';

 function timerGame(callback) {
   console.log('Ready....go!');
   setTimeout(() => {
     console.log("Time's up -- stop!");
     callback && callback();
   }, 1000);
 }

 module.exports = timerGame;

テストコード

タイマーのモックの操作例.js
 test('waits 1 second before ending the game', () => {
   console.log(setTimeout);

   // setTimeout, setInterval, clearTimeout, clearInterval, nextTick, setImmediate
   // そして clearImmediateを偽装する
   jest.useFakeTimers();

   // setTimeoutがmockConstructorになる
   console.log(setTimeout);

   const timerGame = require('../timerGame');
   timerGame();

   // setTimeoutがどのように実行されたかを確認する
   expect(setTimeout).toHaveBeenCalledTimes(1);
   expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 1000);

   // タイマーシステムが保留しているあらゆるタイマーを削除します。
   // これを実行しないと他のテストでjest.runAllTimersを実行した際にsetTimeoutのコールバックが動作する
   jest.clearAllTimers()

   // 本物のTimer関数を使用するように戻す。
   jest.useRealTimers()
 });

useFakeTimers()を使用すると以降、setTimeoutがモックの関数になります。

偽装したタイマーを進める

偽装したタイマーはadvanceTimersByTimeで指定のmsを経過したことにすることができます。

タイマーを進めるテストコード.js
 test('タイマーを経過させる', () => {
   jest.useFakeTimers();


   const timerGame = require('../timerGame');
   const callback = jest.fn();

   timerGame(callback);

   // At this point in time, the callback should not have been called yet
   expect(callback).not.toBeCalled();

   // 半分だけ進める
   jest.advanceTimersByTime(500);

   expect(callback).not.toBeCalled();

   // 残り半分を進める
   jest.advanceTimersByTime(500);

   // Now our callback should have been called!
   expect(callback).toBeCalled();
   expect(callback).toHaveBeenCalledTimes(1);

   jest.clearAllTimers()
   jest.useRealTimers()
 });
偽装したタイマーのコールバックを実行する

偽装されたsetTImerはjest.runAllTimersまたはjest.runOnlyPendingTimersで設定したコールバックを実行することができます。
runAllTimersでは全てのタイマーを実行し、runOnlyPendingTimersは保留中のタイマーのみ実行します。
再帰的なタイマーの場合、runAllTimersを使用すると無限ループになるので、runOnlyPendingTimersを使用するようにします。

テスト対象のコード
再帰的なタイマーを使用しているとします。

infiniteTimerGame.js
 // infiniteTimerGame.js
 'use strict';

 function infiniteTimerGame(callback) {
   console.log('Ready....go!');

   setTimeout(() => {
     console.log("Time's up! 10 seconds before the next game starts...");
     callback && callback();

     // Schedule the next game in 10 seconds
     setTimeout(() => {
       infiniteTimerGame(callback);
     }, 10000);
   }, 1000);
 }

 module.exports = infiniteTimerGame;

テストコード

テストコードの例.js
 test('再帰的なタイマーを実行した場合', () => {
   jest.useFakeTimers();


   const infiniteTimerGame = require('../infiniteTimerGame');
   const callback = jest.fn();

   infiniteTimerGame(callback);


   // At this point in time, the callback should not have been called yet
   expect(callback).not.toBeCalled();


   // これだと無限ループになる
   // jest.runAllTimers();
   jest.runOnlyPendingTimers();

   // Now our callback should have been called!
   expect(callback).toBeCalled();
   expect(callback).toHaveBeenCalledTimes(1);

   expect(setTimeout).toHaveBeenCalledTimes(2);

   jest.clearAllTimers()
   jest.useRealTimers()
 });

期待値の確認

expectを使用して確認します。

テストコード.js
 test('test', ()=> {
   // test1()の結果が1であるか確認する
   expect(test1()).toBe(1);
   // test1()の結果が2でないことを確認する。
   expect(test1()).not.toBe(2);
 }

notを使用することで条件の反転が可能です。

  • toBe

    • プリミティブ値を比較したり、オブジェクトインスタンスの参照IDを確認したりします。
    • 浮動小数点のチェックには使用しないでください。
  • toBeCloseTo

    • 浮動小数点の近似的な値を比較します。
    • デフォルトでは小数点の第二位までチェックします。
浮動小数点のテスト例.js
 test('浮動小数点 - 桁数の確認', ()=> {
   // デフォルトでは有効桁二桁まで確認するーこれは合格する
   expect(0.1+0.201).toBeCloseTo(0.3);

   // 第二引数に有効桁を指定する。ーこれはNGとなる
   expect(0.1+0.201).toBeCloseTo(0.3,3);
 })
toEqualのテストコード.js
 function test5() {
   return {
     name : 'Abc',
     age : 15,
     group: {
       name : "Def"
     }
   }
 }

 test('equal', ()=> {
   expect(test5()).toEqual({
     name : 'Abc',
     age : 15,
     group: {
       name : "Def"
     }
   });
 }); 
  • toStrictEqual
    • toEqualに加えて型が一致するかも確認している
toStrictEqualのサンプルコード
 class LaCroix {
   constructor(flavor) {
     this.flavor = flavor;
   }
 }
 test('equal-strict',()=> {
   expect(new LaCroix('レモン')).toEqual({flavor: 'レモン'});
   expect(new LaCroix('レモン')).not.toStrictEqual({flavor: 'レモン'});
 });
  • toContain
    • 配列内に特定の値が含まれているか確認する
    • 文字列内に特定の文字列が含まれているか確認する
テストコード.js
 test('contein1', ()=> {
   const lst = ['abc', 'efg', 'hij'];
   expect(lst).toContain('efg');

   expect('abcdefghij').toContain('efg');
 });
  • toContainEqual
    • オブジェクトの配列中に同じ構造で同じ値のオブジェクトが含まれているか確認する
サンプルコード
 test('contein2', ()=> {
   const list = [
     {a:1234},
     {b:2345, c:'def'},
     {c:1111, d: { e:2222}}
   ];
   expect(list).toContainEqual({a:1234});
   expect(list).toContainEqual({b:2345, c:'def'});
   expect(list).toContainEqual({c:1111, d:{e:2222}});
 });
  • toThrow
    • 例外が発生することを確認する
    • 引数としてエラメッセージの文字、正規表現、エラーオブジェクト、クラスを与えることができる
サンプルコード.js
 test.only('throw', ()=> {
   expect(()=> {
     error_func();
   }).toThrow();

   expect(()=> {
     error_func();
   }).toThrow('Error');

   expect(()=> {
     error_func();
   }).toThrow(/rr/);

   expect(()=> {
     error_func();
   }).toThrow(new Error('Error'));

   expect(()=> {
     error_func();
   }).toThrow(Error);

 });

テストの同時実行

test.concurrentを使用することでテストを同時に実行することが可能です。
ただし26.4時点で実験的な機能なため以下のような問題が存在しています。
https://github.com/facebook/jest/labels/Area%3A%20Concurrent

以下のコードはconcurrentを使用しないケースと使用したケースの挙動を確認しています。

concurrentを使用しないケース
test('addition of 2 numbers', () => {
  console.log('test1');
  return new Promise(resolve =>{
    setTimeout(function() {
      console.log('end test1');
      expect(5 + 3).toBe(8);
      resolve('done!');
    }, 3000);
  });
});

test('subtraction 2 numbers', () => {
  console.log('test2');
  return new Promise(resolve =>{
    setTimeout(function() {
      console.log('end test2');
      expect(5 - 3).toBe(2);
      resolve('done!');
    }, 1000);
  });
});

この実行結果は以下のようになります。
image.png
1つ目のテストケースが完了した後に、2つ目のテストケースが完了することがわかります。

以下はconcurrentを使用したケースです。

concurrentを使用した場合
test.concurrent('addition of 2 numbers', () => {
  console.log('test1');
  return new Promise(resolve =>{
    setTimeout(function() {
      console.log('end test1');
      expect(5 + 3).toBe(8);
      resolve('done!');
    }, 3000);
  });
});

test.concurrent('subtraction 2 numbers', () => {
  console.log('test2');
  return new Promise(resolve =>{
    setTimeout(function() {
      console.log('end test2');
      expect(5 - 3).toBe(2);
      resolve('done!');
    }, 1000);
  });
});

この実行結果は以下のようになります。
image.png

この結果より同時にテストケースが動作していることが確認できます。

VueでJestを利用する

前提:
@vue/cli 4.5.6 がインストールされている。

単純な作成例

「vue create」を実行した際にマニュアルで設定を行うと単体テストをjestで行えるように指定できます。
ここでは後からjestを導入することを想定した手順とします。

以下のコマンドを実行することで、vueプロジェクトにjestを使うためのプラグインをインストールしています。

コマンド
 # デフォルトでvueのプロジェクトを作成する
 vue create vue_test
 cd vue_test
 # テストツールのインストール
 npm install --save-dev @vue/cli-plugin-unit-jest vue-template-compiler @vue/test-utils
 mkdir -p tests/unit

jest用の設定ファイルを作成します。

jest.config.js
 module.exports = {
   preset: '@vue/cli-plugin-unit-jest'
 }

tests/unitフォルダにテストコードを追加します。

tests/unit/example.spec.js
 import { shallowMount } from '@vue/test-utils'
 import HelloWorld from '@/components/HelloWorld.vue'

 describe('HelloWorld.vue', () => {
   it('renders props.msg when passed', () => {
     const msg = 'new message'
     const wrapper = shallowMount(HelloWorld, {
       propsData: { msg }
     })
     expect(wrapper.text()).toMatch(msg)
   })
 })

cli-plugin-unit-jestプラグインをインストールした後は、vue-cli-serviceコマンドを使用することで単体テストが行えるようになります。

テスト実行用コマンド
npx vue-cli-service test:unit

スナップショットテスト

jestはスナップショットテストをサポートしており、レンダリングしたHTMLが前回のテスト時と変更されたかをチェックすることができます。

https://jestjs.io/docs/ja/snapshot-testing#inline-snapshots
https://system.blog.uuum.jp/entry/2017/12/12/110000

スナップショットテストをjest+vueで行うにはvue-server-rendererをインストールして、node.jsでVueをレンダリングできるようにします。

インストール用のコマンド
npm install --save-dev vue-server-renderer

テストコード

test/unit/example.spec.js
 import { shallowMount } from '@vue/test-utils'
 import HelloWorld from '@/components/HelloWorld.vue'
 import { createRenderer } from 'vue-server-renderer'

 describe('HelloWorld.vue', () => {
   it('renders props.msg when passed', () => {
     const msg = 'new message'
     const wrapper = shallowMount(HelloWorld, {
       propsData: { msg }
     })
     expect(wrapper.text()).toMatch(msg)
   })
   it('snap shot test', () => {
     const msg = 'new message'
     const wrapper = shallowMount(HelloWorld, {
       propsData: { msg }
     })

     // コンポーネントをHTML文字列にレンダリング
     // https://system.blog.uuum.jp/entry/2017/12/12/110000
     const renderer = createRenderer()
     renderer.renderToString(wrapper.vm, (err, str) => {
       if (err) throw new Error(err)
       // 最新のスナップショットと一致するか比較
       expect(str).toMatchSnapshot()
     })    
   })
 })

このテストコードを実行すると、tests/unitに__snapshots__フォルダが作成されます。
何も変更せずにテストを際実行するとテストは合格します。
そこで、テストコードの以下を修正します。

// 略
const msg = 'new message!!'
// 略

修正後にテストを際実行すると以下のようなエラーが発生します。

image.png

もし、このエラーが予定通りの場合はスナップショットを更新してテストを合格にする必要があります。そのためには以下のコマンドを実行します。

スナップショットの更新のコマンド
npx vue-cli-service test:unit -u

このコマンドを実行後、スナップショットは更新されて、テストが合格するようになります。
この例のようにtoMatchSnapshotを使用すると外部ファイルにスナップショットを保存しますが、toMatchInlineSnapshotを使用することでテストコード中に期待するHTMLを記載することができます。

以下はインラインスナップショットのテストコードのサンプルです。初回実行前には期待値は存在しません。

   it("inline snap shot test"", () => {
     const msg = "new message!!";
     const wrapper = shallowMount(HelloWorld, {
       propsData: { msg }
     });

     // コンポーネントをHTML文字列にレンダリング
     // https://system.blog.uuum.jp/entry/2017/12/12/110000
     const renderer = createRenderer();
     renderer.renderToString(wrapper.vm, (err, str) => {
       if (err) throw new Error(err);
       expect(str).toMatchInlineSnapshot();
     });
   });

テストを実行するとテストコードは次のように変更されます。

   it("inline snap shot test", () => {
     const msg = "new message!!";
     const wrapper = shallowMount(HelloWorld, {
       propsData: { msg }
     });

     // コンポーネントをHTML文字列にレンダリング
     // https://system.blog.uuum.jp/entry/2017/12/12/110000
     const renderer = createRenderer();
     renderer.renderToString(wrapper.vm, (err, str) => {
       if (err) throw new Error(err);
       // 最新のスナップショットと一致するか比較
       expect(str).toMatchInlineSnapshot(`
         <div class="hello">
           <h1>new message!!</h1>
           <p>
             For a guide and recipes on how to configure / customize this project,<br>
             check out the
             <a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
           </p>
           <h3>Installed CLI Plugins</h3>
           <ul>
             <li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel" target="_blank" rel="noopener">babel</a></li>
             <li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint" target="_blank" rel="noopener">eslint</a></li>
           </ul>
           <h3>Essential Links</h3>
           <ul>
             <li><a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a></li>
             <li><a href="https://forum.vuejs.org" target="_blank" rel="noopener">Forum</a></li>
             <li><a href="https://chat.vuejs.org" target="_blank" rel="noopener">Community Chat</a></li>
             <li><a href="https://twitter.com/vuejs" target="_blank" rel="noopener">Twitter</a></li>
             <li><a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a></li>
           </ul>
           <h3>Ecosystem</h3>
           <ul>
             <li><a href="https://router.vuejs.org" target="_blank" rel="noopener">vue-router</a></li>
             <li><a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a></li>
             <li><a href="https://github.com/vuejs/vue-devtools#vue-devtools" target="_blank" rel="noopener">vue-devtools</a></li>
             <li><a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener">vue-loader</a></li>
             <li><a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">awesome-vue</a></li>
           </ul>
         </div>
       `);
     });
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【JS学習その⑤】プリミティブ型とオブジェクトそれぞれの挙動

JS学習シリーズの目的

このシリーズは、私ジャックが学んだJavaScriptのメカニズムについてアウトプットも兼ねて、
皆さんと知識や理解を共有するためのものです。
(理解に間違いがあればご指摘いただけると幸いです)

前置き

今回は、前回の記事で書いた内容を踏まえて解説していきます!
余裕がある方は前回の記事➡【JS学習その④】プリミティブ型とオブジェクト ~データ型~をご覧ください

参照とコピー

プリミティブ値とオブジェクトの参照とコピーについて解説します

プリミティブ値のコピー

main.js
let a = 'Hello';
let b = a;
b = 'Bye';

console.log(a); /*Hello*/
console.log(b); /*Bye*/

上記のコードでは、
1.変数aは'Hello'という文字列への参照先を保持している
2.変数bに変数aを代入した時に、'Hello'が別のメモリ空間にコピーされ、変数bはそのコピーされた'Hello'への参照先を保持する
3.変数bに新たに'Bye'という文字列を代入した時に、変数bの参照先が'Hello'から'Bye'に変更される
この時、変数aと変数bの参照先は、それぞれ独立しているため、変数bの参照先が変わっても変数aの参照先は変わらない

↑の流れになっています。
したがって、console.log()で確認した場合、コメントの内容になります。

このように、プリミティブ型の値を格納する変数を他の変数に渡した場合は、それぞれの値はそれぞれ独立して存在しているためどちらかの値を変更してももう一方の値が変更されることはありません。

オブジェクトのコピー

main.js
let a = {
    prop: 'Hello'
};
let b = a;
b.prop = 'Bye';

console.log(a);
// {
//     prop: 'Bye'
// };

console.log(b);
// {
//     prop: 'Bye'
// };

上記のコードでは、
1.変数aに{...}(オブジェクト)への参照先が保持される
2.1.は{prop}への参照先を保持している
3.2.のpropは'Hello'という文字列への参照先を保持している
4.変数bに1.の{...}(オブジェクト)への参照先がコピーされる
この時、変数aと変数bの{...}(オブジェクト)への参照先は同じ
5.bの参照先の{prop}の参照先を'Hello'から'Bye'に変更
この時、変数のaの{prop}の参照先も'Hello'から'Bye'に変更される

↑の流れになっています。

したがって、console.log()で確認した場合、コメントの内容になります。

このように、オブジェクトを保持している変数を他の変数に代入した場合には、プロパティを変更するとそのコピー元のオブジェクトにも影響します。

参照とconst

前述した参照とコピーの内容を踏まえると参照とconstの挙動も見えてきます

main.js
const a = 'Hello';
a = 'Bye'; /*エラー*/

const b = {
    prop: 'Hello'
};
b.prop = 'Bye'; /*正常に動作*/

console.log(b);
// {
//     prop: 'Bye'
// };

上記のコードでは、

  • constでプリミティブ値を代入した場合、代入した'Hello'への参照先がロックされるため、あとから他のプリミティブ値(今回は'Bye')を代入しても参照先が変更されないため、エラーとなります。
  • constでオブジェクトを代入した場合、{...}(オブジェクト)への参照先はロックされますが、{...}(オブジェクト)が参照しているオブジェクトの実体(今回は{prop})はロックされていないため、{prop}の参照先である'Hello'という文字列は変更できます

このように、参照とコピーによってconstでの挙動もプリミティブ型とオブジェクトで違いがあります。

参照と引数

一度「参照とコピー」のおさらいをします

main.js
let a = b;

これは変数の参照先の、もしくはオブジェクトへの参照のコピーを表す。
このことを踏まえて、

main.js
function fn(a) {
}

let a = 0;

fn(a);

これは

main.js
let a = b;

と同じ。
つまり、fn(a);の引数aにletで定義した変数aのプリミティブ値0を代入し、引数aの参照先を別のメモリ空間にコピーした数値0に変更した
ということです。

参照と分割代入

分割代入の定義

main.js
let {a,b} = object;

オブジェクトから特定のプロパティーを抽出して宣言を行う。

main.js
const a = {
    prop: 'hello'
}

let {prop} = a;
prop = 'bye';

console.log(a);
// {
//     prop: 'hello'
// }

console.log(prop); /*'bye'*/

上記のコードでは、
1.変数aに{...}(オブジェクト)への参照先を保持したあとに、letで変数として{prop}を宣言し、そこにaを代入しています。
2.この時、{prop}という変数にpropの値である'hello'(プリミティブ値)をコピーしたメモリ空間の参照先が保持されます。
3.そして、変数{prop}の値を'bye'に参照先を変更する。

↑の流れになっています。
したがって、console.log()で見た時に、コメントの内容になります。

このように、分割代入ではオブジェクトのプロパティーの値がコピーされて、新しい変数からの参照が貼られるということを理解してください。

参照の比較と値の比較

等価演算子によるオブジェクトの比較とプリミティブ値の比較について解説します。

main.js
const a = {
    prop: 0
}

const b = {
    prop: 0
}

console.log(a === b); /*false*/
console.log(a == b); /*false*/

console.log(a.prop === b.prop) /*true*/
console.log(a.prop == b.prop) /*true*/

const c = a;

console.log(a === c); /*true*/
console.log(a == c); /*true*/

上記のコードにおいて重要なことは、
等価演算子で比較した場合、

  • プリミティブ型ではの比較
  • オブジェクトでは参照の比較

↑のように比較されるということです

つまり、

  • 変数aと変数bを比較した場合、オブジェクトは参照の比較をするので、aとbは参照先が異なるのでfalse
  • a.propとb.propを比較した場合、プリミティブ型は値の比較をするので、参照先が違っても値が同じためtrue
  • 変数aと変数cを比較した場合、オブジェクトは参照の比較をするので、aとcは参照先が同じなのでtrue

↑のようになります。

まとめ

いかがでしたでしょうか。
今回は、前回に続いてプリミティブ型とオブジェクトそれぞれの挙動について解説しました。
プリミティブ型とオブジェクトの参照とコピーはデータの扱いにおいてとても重要なのでしっかりと理解しておきましょう!

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

指で3Dオブジェクトを動かすアプリをつくった

3Dの物体を指で動かしてみよう

A-frameを学び、animationの動きを見ていて、このanimationを指で動かせないかと思って、色々試してみました。動かせたら、ちょっとしたゲームができそうだなと。

できました

つくったものをまずご覧ください。

まだ機械学習の精度が低く、思ったようには動いてくれませんが、一応、手の動きに合わせて、アニメーションが変わるように設定できました。

また、使い方のチュートリアルが表示されるように、intro.jsも使ってみました。

WEBサイトも以下に公開しています。

こちら

※ただ、ローカルでファイルから開いた時は問題ないのですが、NetlifyでWEBサイトを公開すると、intro.jsのチュートリアルがうまく機能しません。原因がよくわかりません。

使った機能

・Javascript
・teachableMachine
・A-frame
・intro.js

つくりかた

ポイントだけ書いていきます。

まず対象物をA-frameで表示させる

ただA-frameのquickstartで書かれているやり方

<a-sphere position="0 1.25 -5" radius="1.25" color="#EF2D5E"></a-sphere>

これに後でanimationを追加する方法がわからなかったので、javascriptで対象物を作成してみました。

const scene = document.querySelector('a-scene');
let sphere = document.createElement('a-sphere');
sphere.setAttribute('color', '#FF9500');
sphere.setAttribute('radius', '0.5');
sphere.setAttribute('position', '0 0 -5');
scene.appendChild(sphere);

これで球体が表示されます。

以下の記事を参考にしました。

こちら

手の動きを機械学習で覚えさせる。

機械学習で使ったのが、teachableMachine。
手軽に学習できます。

image11.png

使い方も簡単。
上、下、左、右、それぞれの手の形を写真で撮って覚えさせるだけです。

そうすると、URLが発行され、以下のコードをいれると簡単に使えるようになります。

const imageModelURL = 'TeachableMachineで取得したURL';

手の動きによって、球体にアニメーションを追加する

手の動きを解析した結果で、アニメーションを追加させるようにしました。

classifier.classify(onDetect);
    function onDetect(err, results) {
        console.log(results);
        if (results[0]) {
            console.log(results[0].label);
            if (results[0].label === '') {
                // [FIX] sound 関数実行
                moveUp();
            } else if (results[0].label === '') {
                moveDown();
            } else if (results[0].label === '') {
                moveRight();
            } else if (results[0].label === '') {
                moveLeft();
            } 
        }
        classifier.classify(onDetect);
    }
    // sphere.setAttribute('position', '0 0 -5');
    function moveUp() {
        sphere.setAttribute('animation', 'property: position; to: 0 5 -3; dur: 2000; easing: linear; loop: true');
    }
    function moveDown() {
        sphere.setAttribute('animation', 'property: position; to: 0 -5 -3; dur: 2000; easing: linear; loop: true');
    }
    function moveRight() {
        sphere.setAttribute('animation', 'property: position; to: 5 0 -3; dur: 2000; easing: linear; loop: true');
    }
    function moveLeft() {
        sphere.setAttribute('animation', 'property: position; to: -5 5 -3; dur: 2000; easing: linear; loop: true');
    }

この機能を使えば、ゲームがつくれそう

今回はゲームをつくるまでにはたどり着けなかったんですが、手で物体が動かせるということは、簡単なゲームが作れそうです。

・ボールを目的地まで動かす
・ちょっとした野球ゲーム

など。

自分の手で野球ゲームが作れたら面白そうだな、なんて思いました。

よかったら、LGTMお願いします!!

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

機械学習で、指でA-frameの3Dオブジェクトを動かすアプリをつくった

3Dの物体を指で動かしてみよう

A-frameを学び、animationの動きを見ていて、このanimationを指で動かせないかと思って、色々試してみました。動かせたら、ちょっとしたゲームができそうだなと。

できました

つくったものをまずご覧ください。

まだ機械学習の精度が低く、思ったようには動いてくれませんが、一応、手の動きに合わせて、アニメーションが変わるように設定できました。

また、使い方のチュートリアルが表示されるように、intro.jsも使ってみました。

WEBサイトも以下に公開しています。

こちら

※ただ、ローカルでファイルから開いた時は問題ないのですが、NetlifyでWEBサイトを公開すると、intro.jsのチュートリアルがうまく機能しません。原因がよくわかりません。

→(解決)単純に、動画が許可されてしまうと、すぐに画像認識のプログラムが動いてしまい、intro.jsのチュートリアルがストップしてしまうだけでした。

使った機能

・Javascript
・teachableMachine
・A-frame
・intro.js

つくりかた

ポイントだけ書いていきます。

まず対象物をA-frameで表示させる

ただA-frameのquickstartで書かれているやり方

<a-sphere position="0 1.25 -5" radius="1.25" color="#EF2D5E"></a-sphere>

これに後でanimationを追加する方法がわからなかったので、javascriptで対象物を作成してみました。

const scene = document.querySelector('a-scene');
let sphere = document.createElement('a-sphere');
sphere.setAttribute('color', '#FF9500');
sphere.setAttribute('radius', '0.5');
sphere.setAttribute('position', '0 0 -5');
scene.appendChild(sphere);

これで球体が表示されます。

以下の記事を参考にしました。

こちら

手の動きを機械学習で覚えさせる。

機械学習で使ったのが、teachableMachine。
手軽に学習できます。

image11.png

使い方も簡単。
上、下、左、右、それぞれの手の形を写真で撮って覚えさせるだけです。

そうすると、URLが発行され、以下のコードをいれると簡単に使えるようになります。

const imageModelURL = 'TeachableMachineで取得したURL';

手の動きによって、球体にアニメーションを追加する

手の動きを解析した結果で、アニメーションを追加させるようにしました。

classifier.classify(onDetect);
    function onDetect(err, results) {
        console.log(results);
        if (results[0]) {
            console.log(results[0].label);
            if (results[0].label === '') {
                // [FIX] sound 関数実行
                moveUp();
            } else if (results[0].label === '') {
                moveDown();
            } else if (results[0].label === '') {
                moveRight();
            } else if (results[0].label === '') {
                moveLeft();
            } 
        }
        classifier.classify(onDetect);
    }
    // sphere.setAttribute('position', '0 0 -5');
    function moveUp() {
        sphere.setAttribute('animation', 'property: position; to: 0 5 -3; dur: 2000; easing: linear; loop: true');
    }
    function moveDown() {
        sphere.setAttribute('animation', 'property: position; to: 0 -5 -3; dur: 2000; easing: linear; loop: true');
    }
    function moveRight() {
        sphere.setAttribute('animation', 'property: position; to: 5 0 -3; dur: 2000; easing: linear; loop: true');
    }
    function moveLeft() {
        sphere.setAttribute('animation', 'property: position; to: -5 5 -3; dur: 2000; easing: linear; loop: true');
    }

チュートリアルを追加する

ちょっと前から気になっていたintro.jsを使ってみました。

サイトにも書いてある通り、簡単に使うことができました。

intro.jsのサイトから最新版をダウンロードして、

intro.jsのダウンロードページ

その内、intro.jsとintrojs.cssの2つのファイルを、index.htmlと同一ファイル内におさめ、index.htmlから読み込みます。

読み込み方

<script src="https://aframe.io/releases/1.0.4/aframe.min.js"></script>
<script src="intro.js"></script>
<link rel="stylesheet" type="text/css" href="introjs.css">

チュートリアルの内容を作成します。

<div data-intro="" data-step="1">※3ステップを読んでから、動画の再生ボタンを押してください。</div>
<div data-intro="" data-step="2">1.カメラの使用を許可してください</div>
<div data-intro="" data-step="3">2.動画に向かって上下左右を指で指してください</div>
<div data-intro="" data-step="4">3.すると、球を指の方向へ動かせます</div>
<div data-intro="" data-step="5">では、「Done」を押して、ステップ1からはじめましょう。</div>

そして、最後にscript内に

introJs().start();

これを書けば、完成です。

この機能を使えば、ゲームがつくれそう

今回はゲームをつくるまでにはたどり着けなかったんですが、手で物体が動かせるということは、簡単なゲームが作れそうです。

・ボールを目的地まで動かす
・ちょっとした野球ゲーム

など。

自分の手で野球ゲームが作れたら面白そうだな、なんて思いました。

よかったら、LGTMお願いします!!

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

フルスタックエンジニアになるためのモダンな8つのプロジェクト

こんにちは、たかとーです。
今日は8 Projects with modern designs to become a Full-stack Master 2020の日本語訳記事です!
*当記事は、Thuさんの許可を得て翻訳しています。


8 Projects with modern designs to become a Full-stack Master

自分のスキルを磨くためのプロジェクトを探していますか?新しいアイデアを思いつくのに行き詰っていませんか?

開発者として、私たちはコードを書くのが大好きですが、デザインやアイデアを見つけるのが難しいことがあります。

こんにちは、Thuです。ここ数ヶ月、私はデザインや要件を含む8つの実在するプロジェクトの作成に取り組んできました。

私は、あなたがスーパースターなフルスタックデベロッパーになるために必要な全てのスキルを鍛錬するのに役立つ8つのプロジェクトを用意しました。

これらのプロジェクトに取り組むことで、あなたは優れたポートフォリオを構築し、フルスタックマスターになれるでしょう。

1. Image Uploader

Image Uploader

練習項目

私がいつも恐れているものを開発していただきます。それは、画像アップローダーアプリです。フロントエンドから写真をアップロードする方法、データベースにファイルを保存する方法、簡単なAPIを構築する方法を学びます。

レベル

Front-end: ⭐⭐
Back-end: ⭐⭐⭐

2. My Unsplash

My Unsplash

練習項目

基本的なAPIの機能(RESTful APIでのGET、POST、PUT、DELETEなど)を構築する方法を学びます。

また、Unsplashのような画像グリッドの作り方も学びます。

レベル

Front-end: ⭐⭐⭐⭐
Back-end: ⭐⭐⭐⭐

3. CatWiki

CatWiki

練習項目

猫は大好きですか?外部APIを使用して猫のデータを取得し、ユーザーの行動を追跡するAPIを構築します。また、バックエンドでの検索やフィルタリングの仕組みを練習します。

このプロジェクトでは、猫だらけのレイアウトであなたのフロントエンドのスキルをテストします。

レベル

Front-end: ⭐⭐⭐⭐
Back-end: ⭐⭐⭐⭐

4. Authentication App

Authentication App

練習項目

バックエンドやフルスタックの開発者であれば、ユーザーデータや認証を扱うことは避けられません。このプロジェクトでは、ユーザー認証とユーザーデータを扱うシンプルながらも強力なアプリケーションを学び、構築していきます。

このプロジェクトは、今後のプロジェクトの基礎にもなります。

レベル

Front-end: ⭐⭐
Back-end: ⭐⭐⭐⭐

5. Shoppingify

Shoppingify

練習項目

ECアプリを構築するのは本当に大仕事です。しかし、もしあなたが練習をしたいのであれば、Shoppingifyは良いスタートになります。このプロジェクトは認証を必要としませんが、ECサイトが持っているほぼすべての機能を持っています。ショッピングカートの構築、新しいアイテムの追加、アイテムの編集、履歴やデータの管理などを学ぶことができます。

このプロジェクトでは、複雑で反応の良いレイアウトを作成するので、あなたのフロントエンドのスキルが高いレベルで試されます。

レベル

Front-end: ⭐⭐⭐⭐⭐
Back-end: ⭐⭐⭐⭐

6. Chat Group

Chat Group

練習項目

以前からチャットアプリを作りたいと思っていたのですが、やってみたら超楽しかったです。この課題では、複数のチャットグループアプリケーションを構築します。あなたはリアルタイムデータベースだけでなく、websocket、socketioのような技術にも精通するようになるかも知れません…。

認証アプリケーションを統合することで、ユーザーを管理する方法、グループに追加する方法、メッセージを処理する方法などを学びます。

また、シンプルだけどシンプルじゃないUIを構築していくことになるでしょう?。

レベル

Front-end: ⭐⭐⭐
Back-end: ⭐⭐⭐⭐

7. Tweeter - Twitter Clone

Tweeter - Twitter Clone

練習項目

このUI、シンプルでスッキリしていて大好きです。

ここで言っておきますが、Twitterを再現することができれば、あなたは何でもできます?。

今回のプロジェクトでは、Facebook、Twitterと同じようなプロフィールを作成します。フォロー・フォロワーの仕組み」の作り方を学びます。

また、新しいツイートを投稿したり、コメントを付けたりする方法も学びます。ツイートの種類に応じたフィルターのかけ方なども…。

これは間違いなく今までで最もチャレンジングなプロジェクトの一つです。しかし、あなたが多くのことを学ぶことを約束します ?

レベル

Front-end: ⭐⭐⭐⭐⭐
Back-end: ⭐⭐⭐⭐⭐

8. Thullo - Trello Clone

Thullo - Trello Clone

練習項目

そうですよね...もうこの記事では最後の挑戦になってしまいました。でも、言っておかないといけないことがあるんです。このプロジェクトは、私がデザインするにしても、めちゃくちゃ難易度高いんですよ?。

このプロジェクトでは、何百万ものことが行われているので、何千ものことを学ぶことができます。

チーム、プロジェクト、カード、リストなどの作成方法を学び、同時にユーザーはそれらを編集したり削除したりできるようになります。

フロントエンドでは、ドラッグ&ドロップ機能を作成します。

コメントやアップロードなどのトリッキーな機能も作成します。

他にもまだまだあります。見極めていきましょう?。

レベル

Front-end: ⭐⭐⭐⭐⭐
Back-end: ⭐⭐⭐⭐⭐

訳者感想

面白そうなプロジェクトが揃いましたね!
自分はSNSのような2C向けサービスが好きなので、Twitterクローン、チャットアプリがとても気になりました!正直この2つを再現できるようになればWebアプリに関しては何でもできるようになるのでは?と思いました。

自分もいくつか挑戦してみたいと思います!

素晴らしい記事をありがとうございました Thu?

訳者について

2019年5月よりバンクーバーを拠点に移し、現在スタートアップの開始に向けて試行錯誤しているソフトウェアでデベロッパーです。

近頃は、VCの方と話しながらアイディアのブラッシュアップなどを行いながらMVPの検証を進めています。

フリーランス案件も募集し始めました!React、NodeJs、TypeScript等フロント、バックエンド問わず行えます。是非宜しくお願い致します。

もしよろしければ、以下SNSもよろしく願いします!

Twitter: @taishikat0_Ja
Note: 日本人でも英語圏で戦えることを証明したい。28歳が会社を辞め、個人開発者としてカナダでひたすらもがき続けた一年間とこれから
Linkedin: Taishi Kato

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

フルスタックマスターになるためのモダンな8つのプロジェクト

こんにちは、たかとーです。
今日は8 Projects with modern designs to become a Full-stack Master 2020の日本語訳記事です!


8 Projects with modern designs to become a Full-stack Master

自分のスキルを磨くためのプロジェクトを探していますか?新しいアイデアを思いつくのに行き詰っていませんか?

開発者として、私たちはコードを書くのが大好きですが、デザインやアイデアを見つけるのが難しいことがあります。

こんにちは、Thuです。ここ数ヶ月、私はデザインや要件を含む8つの実在するプロジェクトの作成に取り組んできました。

私は、あなたがスーパースターなフルスタックデベロッパーになるために必要な全てのスキルを鍛錬するのに役立つ8つのプロジェクトを用意しました。

これらのプロジェクトに取り組むことで、あなたは優れたポートフォリオを構築し、フルスタックマスターになれるでしょう。

1. Image Uploader

Image Uploader

練習項目

私がいつも恐れているものを開発していただきます。それは、画像アップローダーアプリです。フロントエンドから写真をアップロードする方法、データベースにファイルを保存する方法、簡単なAPIを構築する方法を学びます。

レベル

Front-end: ⭐⭐
Back-end: ⭐⭐⭐

2. My Unsplash

My Unsplash

練習項目

基本的なAPIの機能(RESTful APIでのGET、POST、PUT、DELETEなど)を構築する方法を学びます。

また、Unsplashのような画像グリッドの作り方も学びます。

レベル

Front-end: ⭐⭐⭐⭐
Back-end: ⭐⭐⭐⭐

3. CatWiki

CatWiki

練習項目

猫は大好きですか?外部APIを使用して猫のデータを取得し、ユーザーの行動を追跡するAPIを構築します。また、バックエンドでの検索やフィルタリングの仕組みを練習します。

このプロジェクトでは、猫だらけのレイアウトであなたのフロントエンドのスキルをテストします。

レベル

Front-end: ⭐⭐⭐⭐
Back-end: ⭐⭐⭐⭐

4. Authentication App

Authentication App

練習項目

バックエンドやフルスタックの開発者であれば、ユーザーデータや認証を扱うことは避けられません。このプロジェクトでは、ユーザー認証とユーザーデータを扱うシンプルながらも強力なアプリケーションを学び、構築していきます。

このプロジェクトは、今後のプロジェクトの基礎にもなります。

レベル

Front-end: ⭐⭐
Back-end: ⭐⭐⭐⭐

5. Shoppingify

Shoppingify

練習項目

ECアプリを構築するのは本当に大仕事です。しかし、もしあなたが練習をしたいのであれば、Shoppingifyは良いスタートになります。このプロジェクトは認証を必要としませんが、ECサイトが持っているほぼすべての機能を持っています。ショッピングカートの構築、新しいアイテムの追加、アイテムの編集、履歴やデータの管理などを学ぶことができます。

このプロジェクトでは、複雑で反応の良いレイアウトを作成するので、あなたのフロントエンドのスキルが高いレベルで試されます。

レベル

Front-end: ⭐⭐⭐⭐⭐
Back-end: ⭐⭐⭐⭐

6. Chat Group

Chat Group

練習項目

以前からチャットアプリを作りたいと思っていたのですが、やってみたら超楽しかったです。この課題では、複数のチャットグループアプリケーションを構築します。あなたはリアルタイムデータベースだけでなく、websocket、socketioのような技術にも精通するようになるかも知れません…。

認証アプリケーションを統合することで、ユーザーを管理する方法、グループに追加する方法、メッセージを処理する方法などを学びます。

また、シンプルだけどシンプルじゃないUIを構築していくことになるでしょう?。

レベル

Front-end: ⭐⭐⭐
Back-end: ⭐⭐⭐⭐

7. Tweeter - Twitter Clone

Tweeter - Twitter Clone

練習項目

このUI、シンプルでスッキリしていて大好きです。

ここで言っておきますが、Twitterを再現することができれば、あなたは何でもできます?。

今回のプロジェクトでは、Facebook、Twitterと同じようなプロフィールを作成します。フォロー・フォロワーの仕組み」の作り方を学びます。

また、新しいツイートを投稿したり、コメントを付けたりする方法も学びます。ツイートの種類に応じたフィルターのかけ方なども…。

これは間違いなく今までで最もチャレンジングなプロジェクトの一つです。しかし、あなたが多くのことを学ぶことを約束します ?

レベル

Front-end: ⭐⭐⭐⭐⭐
Back-end: ⭐⭐⭐⭐⭐

8. Thullo - Trello Clone

Thullo - Trello Clone

練習項目

そうですよね...もうこの記事では最後の挑戦になってしまいました。でも、言っておかないといけないことがあるんです。このプロジェクトは、私がデザインするにしても、めちゃくちゃ難易度高いんですよ?。

このプロジェクトでは、何百万ものことが行われているので、何千ものことを学ぶことができます。

チーム、プロジェクト、カード、リストなどの作成方法を学び、同時にユーザーはそれらを編集したり削除したりできるようになります。

フロントエンドでは、ドラッグ&ドロップ機能を作成します。

コメントやアップロードなどのトリッキーな機能も作成します。

他にもまだまだあります。見極めていきましょう?。

レベル

Front-end: ⭐⭐⭐⭐⭐
Back-end: ⭐⭐⭐⭐⭐

訳者感想

面白そうなプロジェクトが揃いましたね!
自分はSNSのような2C向けサービスが好きなので、Twitterクローン、チャットアプリがとても気になりました!正直この2つを再現できるようになればWebアプリに関しては何でもできるようになるのでは?と思いました。

自分もいくつか挑戦してみたいと思います!

素晴らしい記事をありがとうございました Thu?

訳者について

2019年5月よりバンクーバーを拠点に移し、現在スタートアップの開始に向けて試行錯誤しているソフトウェアでデベロッパーです。

近頃は、VCの方と話しながらアイディアのブラッシュアップなどを行いながらMVPの検証を進めています。

フリーランス案件も募集し始めました!React、NodeJs、TypeScript等フロント、バックエンド問わず行えます。是非宜しくお願い致します。

もしよろしければ、以下SNSもよろしく願いします!

Twitter: @taishikat0_Ja
Note: 日本人でも英語圏で戦えることを証明したい。28歳が会社を辞め、個人開発者としてカナダでひたすらもがき続けた一年間とこれから
Linkedin: Taishi Kato

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

JavaScript: コレクションオブジェクトのtoArrayを安全にする

オブジェクト指向プログラミングには、「コレクションオブジェクト」「ファーストクラスコレクション」と呼ばれる、オブジェクトのリストをカプセル化したオブジェクトを作るテクニックがあります。

コレクションオブジェクトとは

コレクションオブジェクトは、値オブジェクトの一種で、次のような特徴を持ったオブジェクトです。

  • 特定のオブジェクトのリストである。
  • ビジネスロジックを持っている。

例えば、「商品コレクションオブジェクト」は、複数の「商品オブジェクト」を持ちます。ビジネスロジックとしては、商品オブジェクトの価格を合計して、合計金額を返すといった処理を持たせたりします。

JavaScriptでコレクションオブジェクトを実装してみる

コレクションオブジェクトはJavaScriptに固有の概念ではありませんが、JavaScriptでも実装することができます。

例えば、記事のリストである、記事コレクションオブジェクトを考えてみましょう。

まず、記事オブジェクトですが、話を単純にするために、IDと題名を持つオブジェクトとします:

const article1 = { id: 1, title: 'JSの変数入門' }

次に、記事コレクションオブジェクトですが、これは記事を複数保持できるような実装にするが出発点です:

class Articles {
  // private
  _articles = []

  add(article) {
    this._articles.push(article)
    console.log('OK: 記事を追加しました', article)
  }
}

// 記事コレクションオブジェクト
const articles = new Articles()
articles.add({ id: 1, title: 'JSの変数入門' })
articles.add({ id: 2, title: 'JSのクラス入門' })
articles.add({ id: 3, title: 'JSのオブジェクト指向入門' })

これだけだと、ただ配列をラップしただけのオブジェクトなので、ビジネスロジックを持たせます。例えば、「記事IDが重複した記事はaddできない」といったロジックです:

class Articles {
  /**
   * @private
   */
  _articles = []

  add(article) {
    // 記事IDの重複はゆるさない
    if (this._articleIdExists(article.id)) {
      throw new Error('Error: 記事IDが重複しています')
    }
    this._articles.push(article)
    console.log('OK: 記事を追加しました', article)
  }

  _articleIdExists(id) { /*...*/ }
}

これで、ただ配列をラップしたオブジェクトから抜け出して、ビジネス上の知識を持った一人前のコレクションオブジェクトになりました。

const articles = new Articles()

articles.add({ id: 1, title: 'JSの変数入門' })
//=> OK: 記事を追加しました { id: 1, title: 'JSの変数入門' }
articles.add({ id: 2, title: 'JSのクラス入門' })
//=> OK: 記事を追加しました { id: 2, title: 'JSのクラス入門' }
articles.add({ id: 2, title: 'Javaの変数入門' })
//=> Error: 記事IDが重複しています

最後の正しくないaddは、バリデーションが働いて記事ID:2の重複を阻止してくれます。いい感じです。

これで、記事コレクションオブジェクトの主要な機能が作れました。

危険なtoArrayの実装例

ただ、このままだとArticlesクラスは、記事を追加できるものの、中身を取り出すことができません。

そこで、toArrayメソッドを生やして、記事を配列として取り出せるようにしてみましょう。Articles_articlesプロパティは、配列なので、それをそのままreturnすれば良さそうです:

class Articles {
  _articles = []

  /* 中略 */

  toArray() {
    return this._articles
  }
}

これで、記事コレクションから記事一覧を取り出すことができます:

const allArticles = articles.toArray()
for (const article of allArticles) {
  /* なんかの処理 */
}

このtoArrayメソッドは、一見すると大丈夫そうですが、実は問題があります。

どのようなものかと言うと、toArrayを介して取得した配列に破壊的な操作をすると、記事コレクションオブジェクトが隠蔽している_articlesプロパティにもその影響が及んでしまうという問題です。

例えば、記事ID:2が入っている記事コレクションから、

const articles = new Articles()
articles.add({ id: 1, title: 'JSの変数入門' })
articles.add({ id: 2, title: 'JSのクラス入門' })

配列を取得し、

const articleArray = articles.toArray()

そこに、別の記事ID:2のオブジェクトをpushします:

articleArray.push({ id: 2, title: 'Javaの変数入門' })

すると、記事コレクションの中身も変わってしまいます:

console.log(articles)
// => Articles {
//   _articles: [
//     { id: 1, title: 'JSの変数入門' },
//     { id: 2, title: 'JSのクラス入門' },
//     { id: 2, title: 'Javaの変数入門' } ← 意図せず加わった
//   ]
// }

せっかくaddメソッドでID重複チェックを行っているのに、意図せずそれをすり抜けてしまう事故があり得るのです。

コレクションオブジェクトのtoArrayを安全にする方法

コレクションオブジェクトを実装するにあたって、「中身を返す場合は、それを改変できないようにして返すべし」という鉄則があります。

しかし、JavaScriptには手軽に配列を不変にする方法がありません。

なので、考え方を変えて、「返した配列が変更されてもコレクションオブジェクトに影響しないようにする」というアプローチで対応します。

具体的には、toArrayが呼び出されたときにArrayオブジェクトをコピーする方法です:

class Articles {
  /* 中略 */

  toArray() {
    return [...this._articles]
  }
}

こうしておけば、toArrayで取り出された配列に対して、破壊的な配列操作がされたとしても、記事コレクションの配列には影響しません:

const articles = new Articles()

articles.add({ id: 1, title: 'JSの変数入門' })
articles.add({ id: 2, title: 'JSのクラス入門' })

// toArrayで、配列を取得し、
const articleArray = articles.toArray()

// そこに記事2をpushしても、
articleArray.push({ id: 2, title: 'Javaの変数入門' })

// コレクションの中身も変わりません^o^
console.log(articles)
// => Articles {
//   _articles: [
//     { id: 1, title: 'JSの変数入門' },
//     { id: 2, title: 'JSのクラス入門' }
//   ]
// }

コレクションオブジェクトの設計を見直す

toArrayメソッドを安全にする話はここまで終わりです。

ここからはもう少し設計面でコレクションオブジェクトを安全にできないか考えてみたいと思います。どういうことかというと、toArrayメソッドが本当に必要なのか?ということです。

toArrayメソッドを生やすのは、コレクションに生えているメソッドだけでは、必要な操作ができないからではないでしょうか。例えば、記事コレクションなら、「投稿日でソートしたい」、「あるユーザの投稿だけに絞り込んだリストがほしい」といったニーズがあるのに、記事コレクションに生えているメソッドだとそれができない、だからtoArrayを生やしてそれに対応する、といった具合です。

しかし、よくよく考えてみると、「投稿日でソートしたい」などのニーズはどれもビジネスロジックです。これらのニーズは本来、コレクションオブジェクトで吸収してあげるべきです。そうしていけば、toArrayメソッドがコレクションオブジェクトに要らない場合も多々出てくるはずです。toArrayがコレクションからなくなれば、安全面での心配が少なくなります。

しかしながら、そのようにつぶさに対応していっても、最後に残ってしまいがちなニーズが、「コレクションをforで回したい」というものです。だからといって、「ループのためにはtoArrayは必要」と結論づけるのは早計です。forで回したいだけなら、イテレーターをコレクションオブジェクトに実装する選択肢があるからです:

class Articles {
  _articles = []

  /* 中略 */

  *[Symbol.iterator]() {
    yield* this._articles
  }
}

この[Symbol.iterator]というメソッドを生やしておけば、forに対応させることができます:

const articles = new Articles()
articles.add({ id: 1, title: 'JSの変数入門' })
articles.add({ id: 2, title: 'JSのクラス入門' })

for (const article of articles) {
  console.log(article)
}
//=> { id: 1, title: 'JSの変数入門' }
//=> { id: 2, title: 'JSのクラス入門' }

ちなみに、どうしても配列がほしいとなったときは、コレクションオブジェクトに対してスプレッド演算子を使うと、[Symbol.iterator]が呼び出され、配列を手に入れることもできます:

const articleArray = [...articles]

最後までお読みくださりありがとうございました。Twitterでは、Qiitaに書かない技術ネタなどもツイートしているので、よかったらフォローお願いします:relieved:Twitter@suin

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

sort()メソッドの比較関数について~~0を返す場合の処理は必要?~~

よしもと芸人もりたけんじ
が作成した、JavaScript練習問題集に取り組んでいました。
その中の、問22でsort()メソッドを使う場面があり、自分の解答が解答例とは違っていたので
自分のやり方をここに書いておきます。
どちらが正しいか、どちらが現場で多く使われるかはちょっとわからないですが参考までに...。

解答例

/*問22
配列['e','a','k','B','c']を
大文字小文字区別なく順番通りにしてください。期待する値['a','B','c', 'e','k']*/

//回答例
let array = ['e','a','k','B','c'];
array.sort(function(a,b){
 return a.toUpperCase() > b.toUpperCase() ? 1 : -1 ;
});

//結果:['a', 'B', 'c', 'e', 'k']

自分の解答

次に自分の解答です。

let array = ['e','a','k','B','c'];
array.sort(function(a,b) {
    let A = a.toUpperCase();
    let B = b.toUpperCase();

    if(A < B) {
        return -1;
    }
    if(A > B) {
        return 1;
    }
    return 0;
});

//結果:["a", "B", "c", "e", "k"]

どちらも最終的な結果は同じですね。

両者の主な違いは比較関数の違い?でしょうか。

解答例の場合であれば以下の部分。

//解答例の場合
function(a,b){
 return a.toUpperCase() > b.toUpperCase() ? 1 : -1 ; //「条件式? 式1 : 式2」

}

ここでは演算子の「?:」を使って条件式を表しています。
条件式がtrueの場合は式1を、falseの場合は式2を返します。

なので、下記のようにも書き換えられるでしょうか。

//解答例の比較関数を書き換えると...
function (a, b) {
    if (a.toUpperCase() > b.toUpperCase()) {
        return 1;
    }
    //a.toUpperCase() <= b.toUpperCase()
    {
        return -1;
    }
};

そして自分の解答は以下のように比較関数を作成しました。それ以外は解答例と同じです。

function(a,b) {
    let A = a.toUpperCase();
    let B = b.toUpperCase();

    if(A < B) {
        return -1;
    }
    if(A > B) {
        return 1;
    }
    return 0;
}

自分の解答の方では、function(a,b)が0を返した場合も条件式に含んでいます。
0を返したは場合は並べ替えはされずそのままになります。

compareFunction(a, b) が 0 を返した場合、a と b は互いに変更せず、他のすべての要素に対してソートします。注意: ECMAScript 標準はこの振る舞いを保証していないため、一部のブラウザー (例えば、遅くとも 2003 年以前のバージョンの Mozilla) はこれを遵守していません。

引用:Array.prototype.sort() - JavaScript | MDN

まとめ

今回の場合はどちらも結果は同じなので、あまり気になりませんが
現場とかではしっかり0を返した場合も条件式に入れておく方がいいのかな。

ひとまず個人的にはsort() メソッドの理解は前より深まったから今はこのくらいでいいや〜。

それと今回は比較関数についての説明を省きましたが、個人的に少しつまづいた部分でした。

「compare(a,b)のa,bって何を比較してんだ??

aとb比較して-1返すってどう言うこと??」
などなど。(なんで自分はこんなに理解力が乏しいんだ...)

そんな方は是非以下のサイトをみるのをオススメします! 自分も初めから見つけられていれば...!!
漫画形式でとてもわかりやすくsort()メソッドについて解説されています(勿論それ以外も!)

第14話:配列のソートと無名関数-マンガで分かる JavaScriptプログラミング講座

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

生産ラインのエラーを可視化してみる

何処でエラーが出て止まっているんだ?

私は工場に勤めていて設備が故障で停止した際の対処も仕事の一つです、長い生産ラインどこでエラーが出て止まているのか、今まではエラーが出ても現場にある表示機に映すだけで、現場に行かなければどんなエラーがどの場所で出ているかわからない(積層灯でなんとなく場所はわかっても細かい内容まではわからない)、、、ライン内の「どこで」「どのような」エラーがでて止まているのか可視化することで、現場についてから考えないですむなどのメリットがあったり、マネジャーであれば現場の状況を把握したいなどあると思います。

かっこよく映したい

生産数や稼働率は文字やグラフで出しているけど、文字やグラフ表示で「文字を読まなくてはならない」と思います。
A-Frameを使って3D表現で可視化して設備に紐づけてラインを横断して映し出すことができれば、「稼働率の低い機械はどこ?」なんてことも一発で解決できる!あと3D表示されているDashboardは「かっこいい」と思い挑戦してみました。

作ったもの

https://eloquent-shockley-90175d.netlify.app/

使用した技術

  • Vue.js
  • A-Frame
  • Firebase
  • axios
  • Raspberry Pi4(ロボットと仮定して使用)
  • Node-RED

構成図

Image from Gyazo

コード

コードはこちらにあります
https://gist.github.com/Toshiki0324/4a84065db800f717eaa2cb6228b6157d

躓いた部分

3Dモデルの読み込み

今回は無料でダウンロードできる3Dモデルを使用しました、実際に工場で使用するときにはCADで書いたものを.obj.gltfといった拡張子にエクスポートして使います、使用できるファイルの種類は公式サイトに記載されています。
作ったhtmlファイルをブラウザから開いても表示されない・・・という現象に陥りました、原因はいまいちわかりませんでしたが、以下の方法で解消しました。

A-Frameで3Dモデルを使用するときのメモ

要素の位置調整

要素の動かし方にも悩みました、テキストをどのあたりに出すか、線をどこからどこに引くかなど、どうしたらいいんだと悩んでいたら<ctrl> + <alt> + iで編集ができるという文言を発見、公式サイトにも載っていました。Visual Inspector と言うらしいです。
Image from Gyazo
左側で要素のを選択でき、右側で調整したりできます。
ちなみに終了すると元に戻るので、いじった項目と数字はメモかGyazoしましょう、めちゃくちゃ便利です。

謎の警告

いざ3D表現を完成させた後Consoleを見てみると警告だらけ。
以下のものが連続して出ていました。

[Vue warn]: Unknown custom element:
- did you register the component correctly?
For recursive components, make sure to provide the "name" option.

こちらの警告は以下で解決しました。

vue.jsでA-Framを使う

Vue.jsで使う場合はVue.configに設定が必要みたいです。

データの読み込み

今回はFirebaseのCloudFirestoreを使用しましたmemosというコレクションIDにデータを作成したのでmemosのデータを監視して
変更があったら読み込む形でリアルタイムに反映されるようにしました。
以下はCloudFirestoreの内容です。

Image from Gyazo

app.js
  db.collection('memos')
      .onSnapshot(function (querySnapshot) {
          for (let change of querySnapshot.docChanges()) {
              if (change.type === 'added') {
                  // データが追加された時
              }
              else if (change.type === 'modified') {
                  // データが変更された時
                  console.log("!!!!!!!!!");
                  location.reload();

            }
              else if (change.type === 'removed') {
                  // データが削除された時
              }
          }
      })

firestoreのデータに変更があった場合の処理に参考に察せていたできました

firestoreのonSnapshotを使う際に気を付けたいこと

エラーはRaspberryPiで

実際に工場の設備からデータを吸い上げることを考えて今回はRaspberryPi4を使用しました。ラインが実際のものではないので今回は設置は行いません、Node-REDを使用しUIを作成して疑似的なエラーを想定し実装しました。
以下のノードを使用しています

[{"id":"8d8035c6.7855c8","type":"tab","label":"フロー 1","disabled":false,"info":""},{"id":"82b42594.911fe8","type":"debug","z":"8d8035c6.7855c8","name":"debug","active":true,"tosidebar":true,"console":true,"tostatus":true,"complete":"true","targetType":"full","statusVal":"payload","statusType":"auto","x":540,"y":280,"wires":[]},{"id":"25fc6b77.52d974","type":"Firestore out","z":"8d8035c6.7855c8","name":"","collection":"memos","document":"japan","operation":"set","admin":"7db223e2.29c07c","eject":false,"x":390,"y":280,"wires":[["82b42594.911fe8"]]},{"id":"8d096c01.5f39d","type":"ui_button","z":"8d8035c6.7855c8","name":"","group":"2b01090b.1c7886","order":0,"width":0,"height":0,"passthru":false,"label":"エラー","tooltip":"","color":"","bgcolor":"","icon":"","payload":"{     \"age\": \"エラー発生中\",     \"name\": \"test0915\",     \"flag\": true }","payloadType":"json","topic":"","x":210,"y":260,"wires":[["25fc6b77.52d974"]]},{"id":"16ddc24c.589f5e","type":"ui_button","z":"8d8035c6.7855c8","name":"","group":"2b01090b.1c7886","order":1,"width":0,"height":0,"passthru":false,"label":"解除","tooltip":"","color":"","bgcolor":"","icon":"","payload":"{\"age\":\"\",\"name\":\"test0915\",\"flag\":false}","payloadType":"json","topic":"","x":210,"y":300,"wires":[["25fc6b77.52d974"]]},{"id":"7db223e2.29c07c","type":"firebase admin","z":"","name":""},{"id":"2b01090b.1c7886","type":"ui_group","name":"Group 1","tab":"792f850.1442c7c","order":1,"disp":true,"width":6},{"id":"792f850.1442c7c","type":"ui_tab","name":"Tab 1","icon":"dashboard","order":1}]

Image from Gyazo

同じWi-Fi環境であればこんなかんじ↓でラズパイのipアドレスhttp://192.168.1.〇〇:1880/ui/にアクセスできUIが表示されます。

Image from Gyazo

CodePenでグラフを探す

3Dのみの表示だと味気ない気がしたので、今回はCodePenから素敵な見た目のものを持ってきました
https://codepen.io/amcharts/pen/gbLpMR
Image from Gyazo

データの内容は全く関係ありませんがこんな感じ
Image from Gyazo

「カッコイイ!!」までは行けなかった

今回は1ヶ所しかエラーを実装できませんでしたしかも「エラー発生中」せっかくVue.jsを使用したので内容も表示できればよかった、ですが3Dで可視化できたことはうれしかった!作りこんでいくことでライン全体、若しくは工場全体を表示することも可能になりそう。

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

Async/awaitで非同期処理を実現する

下記を参照

Async/await - 現代の JavaScript チュートリアル

ちなみにasyncの使い方は下記のような感じ

async function f() {
    return 1;
}

f().then(function(value){
    console.log(value);
});
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ajaxの非同期通信処理について

マジでjavaScritpムズいくて泣きそうな筆者です(T ^ T)
今回はjavascriotのライブラリーである、
jQueryのajaxについて学んだので、書いていきます。

ajaxとは

ajaxとはざっくり説明すると、非同期処理ができる機能だと思ってください。
正式な名称は、Asynchronous JavaScript + XMLとなっています。
(Asynchronousは「非同期」)
この機能を使うと非同期で処理が行えるので、ページを再読み込みしなくても、
処理を反映させる事ができます。
イメージとしてこんな感じです。

同期処理

スクリーンショット 2020-09-17 3.06.20.png
図だけだと解りにくいですが、
イメージとしては、
1・ PCのブラウザで2チャンネルなどの掲示板を表示させている。
2・ 掲示板のスレッドに書き込みをする。
(この際、同期処理の場合は、書き込んだ文字などが、サーバに送信(リクエスト)されて、
何らかの処理を行って、書き込んだ文字などが反映されたHTMLファイルを返す(レスポンス)
このように、画面から文字などを送信し、リクエストする度にレスポンスが行われ、
再度画面リロードされる。
これが、同期処理になります。
これに対して、非同期処理は、下記のような形になります。

非同期処理

スクリーンショット 2020-09-17 3.18.41.png
1・ PCのブラウザで2チャンネルなどの掲示板を表示させている。
2・ 掲示板のスレッドに書き込みをする。
3・ サーバ側には、書き込んだ文字がajax通信で送られ、
サーバ側で処理が行われ、書き込んだ文字のオブジェクトだけが、
レスポンスされ、HTMLに書き込んだ文字だけが反映されます。
(この際、非同期処理の場合は、書き込んだ文字オブジェクトだけが、レスポンスされるので、HTMLファイルのリロードは起きません。
レスポンスされた文字だけをjavaScriptの処理などで、HTMファイルに
追加だけします。)

違い

同期処理は、サーバから新たなHTMLファイルがレスポンスされ、
画面が再描画されるの対して、
非同期処理は、サーバから文字などのオブジェクトだけを、返却して
現在クライアン側のPCで表示されているHTMLファイルに、
文字オブジェクトをjavaScriptなどの処理で、追加して表示させています。
ようは、新しいHTMLファイルを表示させているか、
すでに表示させているHTMLファイルに、非同期通信で、返却された
文字オブジェクトなどだけを追加して表示させているかの違いになります。
・補足
ajax通信でデータを送る場合は、データの形をJSONで送る事が多い。

sampleコード

sample.html.erb

<div class="contents">
  <% @posts.each do |post| %>
    <div class="content">
      <%= post.text %>
    </div>
  <% end %>
</div>

<%= form_for @post do |f| %>
  <%= f.text_area :text %>
  <%= f.submit class: "btn"%>
<% end %>
sample.js
$(function () {
  function buildPOst(post) {
    //テンプレートリテラルで、htmlを再構築
    let html = `<div class="content">
                  ${post.text}
                </div >`
    return html;
  }


  $('.new_post').on('submit', function (e) {
    //htmlのデフォルトの処理をキャンセルし、jQuery側の処理を行う。
    e.preventDefault();
    let formData = new FormData(this);
    let url = $(this).attr('action');

    $.ajax({
      url: url,
      type: 'POST',
      data: formData,
      dataType: 'JSON',
      processData: false,
      contentType: false
    })
      .done(function (post) {
        let html = buildPOst(post);
        $('.contents').append(html);
       })
      .fail(function () {
        alert('e');
      })
  })
});

処理コードをだいぶはしょって書いたので、相当分かりにくい感じですいません。

処理内容

まず、画面側で文字が入力送信ボタンを押すとサーバにリクエストをしますが、
この時にjavaScriptのイベントが発動します。
$('.new_post').on('submit', function (e)この部分。
非同期通信のajax処理が発動し、
form欄に入力されたデータなどをjson形式で、サーバに送ります。
サーバでは、入力された文字などのデータを処理し、
クライアント(PC画面)側にjson形式で返します。
そして、返却されたデータを再構築する処理を行いHTMLに、
入力した文字などが表示されます。

感想
今回の学習難易度は、自分にとってはかなり高かったです(T ^ T)
送ったデータを処理し、再度構築する?感じが全然イメージが
つかなかったのですが、何度も処理を書いているうちに、
処理順番とデータの再構築方法などが掴めてきました!

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

Material-UIのチェックボックスを使う際はdata属性に注意

はじめに

Reactの人気UIライブラリであるMaterial-UIにおいて、
チェックボックスのonChange時の処理を書いていたらハマった点がありました。

作ろうとしていたもの

テーブルの行ごとにチェックボックスが付いていてクリックすると削除等の処理を行えるものです。
要は各IDを持つリスト型のデータがあり、それに紐づくチェックボックスがあるUIになります。
ですが、このIDの取得ができませんでした。

テーブルのコードを書くのは煩雑なので、要点を絞って簡略化した例で説明します。
注:TypeScriptです。

失敗例

チェックボックスUIにdata属性で各アイテムのidを紐付け、
それをコンソールに表示させようとしています。

function Checkboxs1() {
  const items = [{ id: '1' }, { id: '2' }, { id: '3' }];

  const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    const id = event.currentTarget.dataset.id;
    console.log('id', id);
  };

  return (
    <ul>
      {items.map((item) => (
        <li key={item.id}>
          <Checkbox data-id={item.id} onChange={handleChange} />
        </li>
      ))}
    </ul>
  );
}

さて結果は...

スクリーンショット 2020-09-17 2.47.34.png

期待外れでした。

ちなみに、チェックボックスUIを<input type="checkbox"...のように素のJSXで表現すれば正しくIDは表示されます。

ここでマークアップ構造を確認すると...

スクリーンショット 2020-09-17 2.50.03.png

data属性はinput要素から沢山離れてしまっています。
落ち着いて考えればあれだけリッチなUIを作るためにはこれくらいマークアップが覆われているのはすぐ想像できるかもしれません。

ということで、data属性にidを紐付ける方法をやめました。

成功例

function Checkboxs2() {
  const items = [{ id: '1' }, { id: '2' }, { id: '3' }];

  const handleChange = (id: string) => {
    console.log('id', id);
  };

  return (
    <ul>
      {items.map((item) => {
        const id = item.id;
        return (
          <li key={id}>
            <Checkbox onChange={() => handleChange(id)} />
          </li>
        );
      })}
    </ul>
  );
}

これを同様にコンソールで確認してみると...

スクリーンショット 2020-09-17 2.54.58.png

正常に取得できています。
これで一件落着ですね。

一癖ありますが、これからもMaterial-UIと良好に付き合っていきたいと思います。

確認用のソースコード全体

App.tsx
import React from 'react';
import Checkbox from '@material-ui/core/Checkbox';
import './App.css';

function Checkboxs1() {
  const items = [{ id: '1' }, { id: '2' }, { id: '3' }];

  const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    const id = event.currentTarget.dataset.id;
    console.log('id', id);
  };

  return (
    <ul>
      {items.map((item) => (
        <li key={item.id}>
          <Checkbox data-id={item.id} onChange={handleChange} />
        </li>
      ))}
    </ul>
  );
}

function Checkboxs2() {
  const items = [{ id: '1' }, { id: '2' }, { id: '3' }];

  const handleChange = (id: string) => {
    console.log('id', id);
  };

  return (
    <ul>
      {items.map((item) => {
        const id = item.id;
        return (
          <li key={id}>
            <Checkbox onChange={() => handleChange(id)} />
          </li>
        );
      })}
    </ul>
  );
}

function App() {
  return (
    <div>
      <Checkboxs1 />
      <Checkboxs2 />
    </div>
  );
}

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

UIライブラリを使う際の落とし穴

はじめに

Reactの人気UIライブラリであるMaterial-UIにおいて、
チェックボックスのonChange時の処理を書いていたらハマった点がありました。

作ろうとしていたもの

テーブルの行ごとにチェックボックスが付いていてクリックすると削除等の処理を行えるものです。
要は各IDを持つリスト型のデータがあり、それに紐づくチェックボックスがあるUIになります。
ですが、このIDの取得ができませんでした。

テーブルのコードを書くのは煩雑なので、要点を絞って簡略化した例で説明します。
注:TypeScriptです。

失敗例

チェックボックスUIにdata属性で各アイテムのidを紐付け、
それをコンソールに表示させようとしています。

function Checkboxs1() {
  const items = [{ id: '1' }, { id: '2' }, { id: '3' }];

  const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    const id = event.currentTarget.dataset.id;
    console.log('id', id);
  };

  return (
    <ul>
      {items.map((item) => (
        <li key={item.id}>
          <Checkbox data-id={item.id} onChange={handleChange} />
        </li>
      ))}
    </ul>
  );
}

さて結果は...

スクリーンショット 2020-09-17 2.47.34.png

期待外れでした。

ちなみに、チェックボックスUIを<input type="checkbox"...のように素のJSXで表現すれば正しくIDは表示されます。

ここでマークアップ構造を確認すると...

スクリーンショット 2020-09-17 2.50.03.png

data属性はinput要素から沢山離れてしまっています。
落ち着いて考えればあれだけリッチなUIを作るためにはこれくらいマークアップが覆われているのはすぐ想像できるかもしれません。

ということで、data属性にidを紐付ける方法をやめました。

成功例

function Checkboxs2() {
  const items = [{ id: '1' }, { id: '2' }, { id: '3' }];

  const handleChange = (id: string) => {
    console.log('id', id);
  };

  return (
    <ul>
      {items.map((item) => (
        <li key={item.id}>
          <Checkbox onChange={() => handleChange(item.id)} />
        </li>
      ))}
    </ul>
  );
}

これを同様にコンソールで確認してみると...

スクリーンショット 2020-09-17 2.54.58.png

正常に取得できています。
これで一件落着ですね。

一癖ありますが、これからもMaterial-UIと良好に付き合っていきたいと思います。

教訓

HTMLの確認を怠らないようにしましょう。

更新履歴

2020/09/17 ソースコードを微調整しました

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

DDDをJavaScriptでやってみる PART 1

はじめに

Domein Driven Development (ドメイン駆動設計)はEvansの
Domain-Driven Design: Tackling Complexity in the Heart of SoftwareやVernonの実践ドメイン駆動設計で紹介されているように、大規模システムを作るときにとても有効な設計手法です。
これらの本ではJavaのコード例を紹介しています。

一方で、近年Java以外にも様々な言語でバックエンドのシステムは設計されるようになり、JavaScript(node)で書くという企業も増えてきています。

ところが、DDDのコード例は本を見てもネットを見てもJavaScriptのものはほとんどないです。
JavaScriptでAPI作らなきゃいけないけど、DDDは実現したい!
っていうときにどうすれば良いの? というときに役立つような実装例をこの記事では紹介していこうと思います。

読んで欲しい方

  • JavaScriptでDDDを実現したい人
  • DDDをなんとなくでも分かっている人

目標

  • DDDで設計されたシステムをJavaScript(TypeScript)で作ること、作れるようになっていること。

目次

PART1 ドメインモデルの実装方法(本記事)

  1. DDDとは何かをざっくり知り、ドメインモデルとドメインオブジェクトを知る。
  2. ValueObject
  3. Entity

PART2 ドメインサービス、リポジトリ、ファクトリーの実装方法(次回記事で書きます)

  1. Domain Service
  2. Repository
  3. Factory

PART3 集約の実装方法(次回記事で書きます)

  1. 集約とは
  2. 集約の実装方法

DDDで設計するメリット

DDDが何かということを説明する前に、DDDを使うメリットを紹介します。
DDDの最大のメリットは保守的であることです。

誰が見ても、ビジネスサイドのルールが何かということが容易に理解できるため、仕様追加や仕様変更を反映が簡単にできます。

ビジネスサイドのルールには例えば、ECサイトを考えた時にのサイトのユーザーができることがあげられます。
会員登録をしないと、購入ができないサイトもあれば、ゲストとして購入ができるECサイトもあります。このユーザーがどういう条件なら購入することができるのかというルールがビジネスサイドのルールです。

「誰が見ても」というのはそのシステムを作ったエンジニアだけでなく、例えば、そのシステムを知らない新しく入ってきたエンジニアはもちろん、もしかしたら、ビジネスサイドの人ですら理解できるレベルのわかりやすいコードになっているかもということです。

増税の時に困った話

例えば、2019年に増税がありました。
消費税率が8%から10%に変わるってだけなのに、めちゃくちゃ苦労したそこのあなた!(僕もなんですけど)、

const tax = Math.floor(price * 0.08);
const amount = price + tax;

みたいなコードが、値段を表示するために計算するAPIの中や、決済レコードをINSERTする処理が書いてあるところや、クレカ以外の決済手段による決済処理の部分、etc...に散在していて、それをいちいちgrepして探しに行って全部直ったのか自信を持って言えないからひたすらテストし続ける、なんてことがありませんでしたか?

税率が一括で変わる場合、定数管理しているtaxRateを0.08から0.1にすれば終わった話かもしれません。
しかし、軽減税率というものがありました。
これは、簡単に言えば商品によって税率が8%のものだったり10%のものだったりが混在しているという状況です。

つまり、全ソースコードの中からtaxRateを使っている箇所を見つけ出し、8%なのか10%なのかを判定(または判定する処理を追加)しなくてはいけないということになりました。

DDDの良いところ

DDDをちゃんとやればこんなことはなくなります。
変更箇所が一箇所というわけにはいかないかもしれませんが、どこに税を計算する処理が入っているかは明確だし、それをDDDがわかっている人なら、例え新人でも、ビジネスサイドの人間でもわかるという状況を作り出すことができます。

モデリングの仕方にもよりますが、例えばあるユーザーの支払いを表現するPaymentモデルを作れば、表示用にも、決済レコード作成時にも、クレカ以外の決済にもこのモデルを使えます。

そうすれば、税率が変更になっても、「あー、支払いのあのモデルのあのルールだけ変えれば良いな」っていうのがすぐわかります。

また、決済ルールがよくわからんっていう開発者がここのコードを読むだけで、「あー値段は税の他にも何か割引かれるような振る舞いもあるんだな」ということがわかります。

class Payment {
  id: number;
  userId: number;
  price: number;
  discount: number;
  taxRate: number;

  get tax() {
    return Math.floor(this.price * this.taxRate);
  }

  get amount() {
    return this.price + this.tax + this.discount;
  }
}

上のクラスはメンバー変数にtaxRateを持っている例です。この場合、インスタンス化する時にtaxRateを与える必要があります。
一方で、軽減税率に対応させたPaymentモデルの実装例もこの記事の後の方で紹介します。

ではどうやってDDDをJavaScriptで実現していけば良いかを説明していきます。

DDDとは何か

ドメイン駆動設計(DDD)とは名前のとおり、ドメインを元に設計していく手法です。ここでいうドメインとは業務領域のことです。DDDの考え方は、「必ず解決したいビジネス的な問題が存在し、その問題の解決のためにアプリケーションが存在している」というものであり、当然その解決方法は問題に寄り添ったものでなくてはいけない」というものです。
 
DDDの中では現実世界をモデル化した抽象概念である「ドメインモデル」を考え、それを実装するために具体化した「ドメインオブジェクト」というオブジェクトが主体になります。そのドメインモデルがあらゆる業務のなかに出てくる振る舞いをします。例えば、Userモデルがemailを変更する、みたいなことがあります。

DDDには戦略的設計と戦術的設計の大きく2つがあり、その両方が最終的に必要になります。
現実世界をモデル化し、ドメインモデルを作るところまでが戦略的設計と呼ばれ、「サブドメイン」や「コンテキスト」を考えるような作業があります。これらについては、JavaでもJavaScriptでも変わりません。
ですので、この部分はどの言語で実装するかは関係なしに本や記事を読めばOKです。

一方、ドメインモデルをドメインオブジェクトとしてコード上で表し実装するという部分が戦術的設計になります。
この戦術的設計をJavaScriptでやっていこう!というのがこの記事のメインになります。
JavaScriptでこのドメインオブジェクトをどう実装していくのかということを考えていきます。

ドメインモデルとドメインオブジェクト

DDDでは現実世界の業務で登場する人や物をモデル化する。これがドメインモデルだ。ドメインモデルそのものは「ドメインに含まれる概念を抽象化した存在」です。
ドメインモデル、ドメインオブジェクトについてはドメイン駆動設計入門 ボトムアップでわかる!ドメイン駆動設計の基本で非常にわかりやすく説明されています。

ドメインオブジェクトとは

ドメインオブジェクトはドメインモデルという抽象概念を実装するためにモデル化したオブジェクトです。
例えば、ユーザーというドメインモデルを考えたとすれば、これはUserクラスというモデルができ、これはJavaScriptの上ではObjectとして扱われるようになります。

ドメインオブジェクトを使うメリット

なぜ、ドメインオブジェクトを使うかというと、一つは現実世界の業務と対応させることができるからです。
実際にユーザー(User)や商品(Product)という物は現実世界に存在していて、ユーザーがECサイトで買い物をしたらUserがProdctを購入するという表現ができた方が良いですよね?

例えば、購入するときの処理が以下のように、リクエストパラメータからuserとproductをdbから探してきて、外部の決済サービスにリクエストして成功したら決済レコードを作り、商品のステータスを変更し、メールを送るという処理だったとします。

悪い例

そのままかくと以下のようになります。
この中の処理全体にtransactionが張られていることとします。

buy.ts
import _ from 'lodash';

interface BuyingParams {
  productIds: number[];
  userId: number;
}

export async function buyProduct(params: BuyingParams) {
  const { userId, productIds } = params;
 // DBから商品情報を取得
  const products = await db.products.findAll({
    where: {
      id: {
        $in: productIds,
      },
    },
  });

  // 在庫があり、購入できるかチェック
 if (products.length < productIds.length) {
    throw new Error('存在しない商品があります');
  }
  if (products.every(product => product.stockCount > 0)) {
    throw new Error('買えない商品が含まれています');
  }

 // DBからユーザー情報を取得
  const user = await db.users.findOne({
    where: {
      id: userId,
    },
  });

  // 購入できるユーザーかチェック
  if (!user || user.status === USER_STATUS.INVALID) {
    throw new Error('不正なユーザーです');
  }

  const amount = _.sum(products.map(product => product.price));

  // 外部決済サービス(GMO, pay.jp, stripe などなど)に決済レコードを作る
  // tradingIdという決済IDでやり取りするとする。
  const res = await axios.post('hogeUrl', {
    amount,
    tradingId: user.tradingId,
  });

 // 自社DBに決済レコードを作る
  const paymentPromiseList = [];
  for (const productId of productIds) {
    const req = db.payments.create({
      user_id: userId,
      product_id: productId,
      amount: product.price,
      paid_status: PAID_STATUS.COMPLETE,
      payment_service_transaction_id: res.payment_service_transaction_id,
    }]);
    paymentPromiseList.push(promise);
  }
  await Promise.all(paymentPromiseList);

  // 商品情報をupdate
  const productPromiseList = [];
  for (const product of products) {
    const promise = db.products.update({
      user_id: userId,
      purchased_date: res.paid_date,
      stock_count: product.stock - 1,
    });
    productPromiseList.push(promise);
  }
  await Promise.all(productPromiseList);

  // メールを送る
  mailer.send({
    to: userId,
    subject: '購入ありがとうございました',
    body: 'hogehoge',
  });
  return true;
}

のようになります。

何がダメか

これはやっていることはわかりますが、めちゃくちゃ手続き型です。
ただ単に、手続きを書いているだけです。

これを初めて読んだ新人エンジニアやビジネスサイドの人が仕様がわかるかと言われればNoです。

なぜか。

それは、現実世界に対応した振る舞いをしていないからです。
このbuyProductという関数が、購入商品のステータスを変更する処理やクレカ決済を外部APIに依頼する処理やユーザーにメールを送信する処理を全部行っています。

しかしこれでは「UserがProductを買う」という現実世界の振る舞いをそのまま反映しているわけではありません。
UserはProductを買うという振る舞いはするが、決済レコードを作るという振る舞いはしないし、productのステータスを変更するという振る舞いもしません。

これらは全て、システム的にDBにどういうレコードを作るかという話で、業務内容とは関係ありません。
一方で下のようにかくとどうでしょうか。

良い例
buy.ts
interface BuyingParams {
  productId: number;
  userId: number;
}

export async function buyProduct(params: BuyingParams) {
  const { userId, productIds } = params;

  const products = await productsRepository.findByIds(productIds);
  await productsService.checkExistence(products);

  if(!products.every(product => product.canBePurchased)) {
    throw new Error('買えない商品が含まれています');
  }

  const user = await userRepository.findById(userId);
  if(!user) {
    throw new Error('ユーザーが存在しません');
  }
  if(!user.canBuy) {
    throw new Error('不正なユーザーです');
  }
  await userBuyingService.buyProduct({ user, products });
  return true;
}

データベースをどう更新しているとか、どの外部決済サービスにアクセスしているかということなど、細かいことはわかりませんが、「とにかく、ユーザーが商品が買える状態かを確認して、OKならユーザーが購入する処理をしている」ということはわかります。
これが、中身は何をしているか詳しいことは知らないが、抽象的には何をしているかだいたいわかるということです。

実際はそれぞれuserRepositoryやuserBuyingServiceの中でより細かい処理をしています。
userBuyingServiceは以下のようにかけます。(userBuyingServiceはドメインサービスです。ドメインサービスについては後に述べます。)

userBuyingService.ts
import _ from 'lodash';

const externalPaymentService = new ExternalPaymentService();
export class UserBuyingService {
  buyProduct(params: Params) {
    const { user, products } = params;

    const payments = products.map(product => 
      new Payment({
        userId: user.id;
        price: product.price;
        taxRate: product.taxRate;
     product: Product;
      });
    }

   const amount = _.sum(payments.map(payment => payment.amount)); // payment.amountで税込計算

    // 外部決済サービス(GMO, pay.jp, stripe などなど)に決済レコードを作る
    await externalPaymentService({
      amount,
      tradingId: user.tradingId,
    });

  for (const payment of payments) {
     payment.complete();
   );

  // 自社DBに決済レコードを作る(のをrepositoryに依頼)
  await paymentRepository.create(payments);

   // 商品情報をupdate
   product.sell();
   await productRepository.save(product);

   // メールを送る
   mailerService.sendPurchaseComplete({
     to: userId,
   });
}

そして、productが購入されても良い商品なのか、userが商品を購入できる状態にあるのかをチェックするのはそのオブジェクト自身の振る舞いとして表されます。

例えばこの場合Productを表すドメインオブジェクトは下のように表すことができます。

product.ts
enum ProductType {
  normal = 'normal',
  reducedTaxTarget = 'reduced_tax_target',
}

class Product {
  id: number;
  price: number;
  stock_count: number;
  product_type: ProductType;

  get canBePurchased() {
    return this.stock_count > 0;
  }

  get isReducedTaxTarget() {
    return this.product_type === ProductType.reducedTaxTarget;
  }

  sell() {
    this.stock_count = this.stock_count - 1;
  }
}

このProduct classにはcanBePurchasedというgetterで購入可能かどうかを判定したり、sellというmethodで購入される振る舞いを表現しています。
このmethodはドメインサービスの中で使われています。
このモデルさえみればこの商品というドメインモデルがどのような振る舞いをしてどういう業務に使われるかを知ることができます。

このように現実に存在するproductというモデルに現実の業務上の振る舞いを表現することで、直感的にわかりやすい(仕様を理解しやすい)設計にすることができます。
引いては保守的なシステムを作ることができるというのがドメインオブジェクトを使う最大のメリットです。

また、ドメインのルールがドメインオブジェクトを定義したファイルに集約して書かれていることがもう一つの大きなメリットになります。

アイテム買えるかの条件はcanBePurchasedに集約されるため、ここの部分だけ変更すればいいですし、開発者はここの部分だけ見ればその仕様を把握することができます。
先に述べた手続き型のコードスタイルでは消費税のケースと同じようにgrepして全ての箇所を直さないといけないため漏れが発生するかもしれません。また、モジュール化して共通化させたとしてもどこに共通処理を書くかは開発ルールとして決め、それを守られるように運用しなくてはいけなくなります。
DDDでは現実世界の実体をモデル化したドメインオブジェクトに共通処理を書くような設計になっているため、コード規約として決めなくても、どこに何を書くのかということが明確になっています。(ドメインロジックはモデルに書くという決めごとがすでになされています。)

税率の問題を考え直してみる

例えば、冒頭のtaxRateの問題をもう一度考えてみます。
taxRateをいろいろなところで計算していて、軽減税率が入ってきた時にいちいち税率を確認しなくてはいけなくなっていた、というのが問題でした。
そこで、以下のようにPaymentモデルを定義して、そのモデルの中でtaxRateの計算ロジックを持たせます。
決済レコードを操作したり、値段を表示する時にインスタンス化したpaymentを使えば、必ずtaxRateはこの一か所で計算されることになります。
ビジネスロジックをまとめたことで、税率が変更になった場合もこのmethodだけを直せば良いことがわかります。

Payment.ts
class Payment {
  id: number;
  userId: number;
  price: number;
  discount: number;
  product: Product;

  get tax() {
    return Math.floor(this.price * this.taxRate);
  }

  get taxRate() {
    if(this.product && this.product.isReducedTaxTarget) {
      return TAX_RATE.EIGHT_PERCENT;
    }
    return TAX_RATE.TEN_PERCENT;
  }

  get amount() {
    return this.price + this.tax + this.discount;
  }
}

ドメインオブジェクトの種類

ドメインオブジェクトには2種類のオブジェクトがあります。
一つは値オブジェクト、もう一つはエンティティです。
値オブジェクトとは「システム(サービス)固有の値を表したオブジェクト」で、例えばuserNameや、statusのように値として扱うが、業務の中のルールによって制約を持ったオブジェクトとして扱われる。(例えばuserNameが5文字以内でなくてはいけない、など)。

userNameの実装例としては以下のようにオブジェクト化できます。

userName.ts
class UserName extends String {
  constructor(params: string) {
    super(params);
    // 5文字以内
    if (params.length > 5) {
      throw new Error('5文字以内にしてください');
    }
  }
  isEqual(params: userName) {
    return this.toString() === params.toString();
  }
}

値オブジェクトの性質は以下があります。

  • 不変である
  • 代入できる
  • 等価比較できる

あくまで、userNameも一つの値を示すので、このような値の性質があります。
isEqualメソッドによって等価比較することができ、あたかも値のように扱うことができます。

const userName1 = new UserName('hoge');
const userName2 = new UserName('hoge');
const userName3 = new UserName('fuga');

userName1.isEqual(userName2) // true
userName1.isEqual(userName3) // false

逆に、changeStatusみたいなメソッドを持たせて、自身のpropertyを変えることはできません。

もう一つのエンティティとは「一意なキーによって識別されるドメインモデルを実装したオブジェクト」です。
上であげたUserやProductがまさにそれです。
現実世界の業務上で登場する人や物がこのエンティティで表されることが多いです。

例えばuserNameが同じUserも違うUserで、これは一見同じpropertyをもつオブジェクトだが、実際は違う存在であるということを表現するために一意なキーを持たせて区別します。(だいたいidを持たせます。)
この点が値オブジェクトと異なる点です。

User エンティティの実装例を示します。

user.ts
interface UserParams {
  id: UserId;
  userName: UserName;
  email: Email;
}

class User {
  id: UserId;
  userName: UserName;
  email: Email;

  constructor(params: UserParams) {
    this.id = params.id;
    this.userName = params.userName;
    this.email = params.email;
  }

  changeUserName(name: UserName) {
    this.userName = newUserName;
  }
}

このようにエンティティにはidを持たせているので、例えば、userName, emailが同じuserでも違うid(一意なキー)を付与することで違うオブジェクトであることを表現します。
また、値オブジェクトと違い、自身のpropertyを変更したり、状態が変わることがあります。

changeUserNameのようなメソッドで振る舞いを持たせることができ、そのモデルが状態を変えることができます。
このようにエンティティの特徴として

  • idを除く全てのプロパティが同じエンティティは存在し得る。
  • 同じエンティティが属性を変更することができる。

ということが挙げられます。

ドメインオブジェクトの使い方

では、実際に値オブジェクトやエンティティを使っていけば良いでしょうか。

例えば、userのuserNameを変更する処理を考えます。
userはすでに登録してあるとして、DBにレコードがあるとします。

また、UserエンティティとuserName値オブジェクトは上で示したものを使うとします。

interface ChangeUserNameParams {
  name: string;
  userId: number;
}

const userRepository = new UserRepository();

async changeUserName(params: ChangeUserNameParams) {
  const { name, userId } = params;
  const user = await userRepostory.findById(userId);
  user.changeName(name);
  return userRepository.save(user);
}

class UserRepository {
  findById(id: number) {
    // DBから取得
    const _user = await db.users.findOne({
      attributes: ['user_name', 'id', 'email'],
      where { id },
    });

    // エンティティのインスタンスを作る
    const userId = new UserId(_user.id);
    const email = new Email(_user.email);
    const userName = new UserName(_user.user_name);

    const user = new User({
      email,
      id: userId,
      userName,
    });

    return user
  }

  save(user: User) {
    return db.users.update({
      user_name: user.userName,
      email: user.email,
    }, {
      where: { id: user.id },
    });
  }
}

changeUserNameという関数はusecaseです。
ここを見れば、userのuserNameを変えているんだな、とわかります。
UserId、 EmailはUserNameと同じような値オブジェクトです。

const user = await userRepostory.findById(userId);

という処理でUserエンティティのインスタンスを構成しています。
userRepostoryはDDDのRepositoryの役割(モデルの永続化と再構築)を果たします。(後々説明します。)

注目して欲しいのはuserRepositoryでUserモデルを再構築する部分です。
User classが保持するそれぞれの値オブジェクトをインスタンス化して、それらをもとにUserインスタンスを作成しています。
userNameを変更するためにまずUserName classをインスタンス化します。
その後、そのuserNameインスタンスをUser classに渡してuserインスタンスを作っています。

例えば、userNameを'Jonathan'に変更しようとすると、

const userName = new UserName(_user.user_name);

のところで5文字以内のルールを守れず、userNameインスタンスを作ることに失敗し、errorをthrowします。
このように、各モデルにおいてルールは集約化されており、インスタンス化できるということはそのモデルの全てが正しいデータ状態にあるということを約束してくれます。
、userインスタンスができたということはドメインルールに合ったモデルを作ることができているということになります。

これいちいちnewするの?

ここで気づいた方もいると思いますが、このUserインスタンスを作るのは結構めんどくさいです。
Userモデルの保持する値オブジェクトをいちいち全部インスタンス化しなくてはいけないからです。

上のuserRepositoryの中でも、userName, userId, Emailをインスタンス化しなくてはいけません。
このぐらいの小さいモデルでもめんどくさいのに、例えば、ユーザーが注文(Order)を複数持っているという現実のドメインを表現しようとすると、User classは以下のようになります。(これはUserとOrderを同じ集約として設計したことに対応する。)

class User {
  id: userId;
  email: Email;
  userName: UserName;
  orders: Order[];

  constructor(params: Params) {
    this.id = params.userId;
    this.email = params.email;
    this.userName = params.userName;
    this.orders = params.orders;
  }
}

class Order {
  id: OrderId;
  deliveryNumber: DeliveryNumber;

  constructor(params: Params) {
    this.id = params.orderId,
    this.deliveryNumber = params.deliveryNumber;
  }
}

こういう状況だと、Orderのインスタンスも作る必要があり、Orderのpropertyについてもnew Hogehoge()みたいにnewしまくる必要が出てきます。
上の例だとuserをインスタンス化する処理は以下のようになります。

class UserRepository {
  findByUserId(userId: UserId) {
    // databaseから取得
    const userWithOrder = await db.users.findOne({
      attributes: ['id', 'email', 'user_name'],
      include: [{
        model: db.orders,
        attributes: ['id', 'delivery_number'],
      }],
      where: {
        id: userId,
      },
    });

  // userWithOrderには
  //{ 
   //   id: 1,
   //   user_name: 'Tom',
   //   email: 'hoge@qmail.com',
   //   orders: [
   //     { id: 1, delivery_number: 111 },
   //     { id: 2, delivery_number: 222 },
   //   ],
   // },

    // のようなオブジェクトが入ってくる。


   // userWithOrderオブジェクトからUserモデルをインスタンス化
   // Userのプロパティの値オブジェクトのインスタンスを作る
   const userId = new UserId(userWithOrder.id);
   const email = new Email(userWithOrder.email);
   const userName = new UserName(userWithOrder.user_name);

   const seedOrders = [];
   for (const _order of user.orders) {
     // orderのインスタンスを作るためのorderのプロパティの値オブジェクトのインスタンスを作る
     const orderId = new OrderId(_order.id);
     const deliveryNumber = new DeliveryNumber(_order.delivery_number);

     // orderのインスタンスを作る
     const order = new Order({
       id: orderId,
       deliveryNumber,
     });
     seedOrders.push(order);
   }

   // そしてやっとUserをインスタンス化
   cosnt user = new User({
     id: userId,
     email,
     userName,
     orders: seedOrders,
   });
   return user;
  }
}

new が何回出てきたでしょうか。
これはやってみるとわかりますがめちゃくちゃ面倒です。  

ではどうするかというとJavaScriptにはこの問題を解決してくれるclass-transformerというライブラリがあります。
class-transformerによってこのようにネストされた集約モデルについても簡単に扱うことができます。

class-transformerを使った値オブジェクトとエンティティの実装

先に結論をいうと、値オブジェクトとエンティティの実装は以下の構成で行うのが良いでしょう。

・ class-transformerを使ってドメインオブジェクトのクラスを定義する
・ class-validatorを使ってモデルのメンバー変数のドメインルールを記述する。
・ class-transformer-validatorを使ってドメインオブジェクトをインスタンス化する。

class-transformer, class-validator, class-transformer-validatorの3個のライブラリを紹介します。
なぜこの構成にするのが良いのか、なぜこのライブラリを使うのかを説明していきます。

class-transformerを使ってドメインオブジェクトのクラスを定義する・

class-transformerはJavaScriptにおけるplain object とclass objectの変換機能を持つライブラリです。
例えば、

class User {
  firstName: string;
  lastName: string;
  email: string;
  nickName: string;
}

というクラスに対して、plainなobject

const plainTaro = {
  firstName: 'Taro',
  lastName: 'Tanaka',
  email: 'hoge@hoge.com',
  nickName,
};

に対して

import { plainToClass } from "class-transformer";

const taro = plainToClass(User, plainTaro);

と書けば、勝手にclassにしてくれます。
class-transformerはネストされたオブジェクトを扱う時にとても便利です。
先ほどのUserがOrderをいくつも持っている場合を考えます。
class-transformerを用いて以下のように表現できます。

import { plainToClass, Type } from 'class-transformer';

class User {
  id: number;
  name: string;

  @Type(() => Order)
  orders: Order[];
}

class Order {
  id: number;
  deliveryNumber: number;
}

const plainUser = {
  id: 1,
  name: 'Taro',
  orders: [
    { id: 1, deliveryNumber: 1234 },
  ],
}

const user = plainToClass(User, plainUser);

@Typeアノテーションを使うことで、メンバー変数のクラスを指定することができます。
これによって、plainToClassを1回使うだけで、1個の集約をインスタンス化することができます。

しかしこれにはいくつか注意しないといけない点があります。

  • 型定義が効かない
  • class-transformeを使うとconstructorにドメインルールを記述できない

という点です。

型定義できない問題とは、例えば、上のUserクラスに対して

import { plainToClass } from 'class-transformer';
const taro = plainToClass(User, { lastName: 123 });

としてもTSのエラーが出ません。
ここで定義したUserのlastNameはstringでなくてはいけない、というのがドメインのルールです。
ドメインのルールに沿っていないならば、エラーを出す必要があります。

ライブラリの中を見ると

plainToClass(cls, plain, options) {
  const executor = new TransformOperationExecutor_1.TransformOperationExecutor(enums_1.TransformationType.PLAIN_TO_CLASS, options || {});
  return executor.transform(undefined, plain, cls, undefined, undefined, undefined);
}

このように書いてあり、
このexecutorのtransformメソッドが受け取るplain object のvalueはというと

value: Record<string, any> | Record<string, any>[] | any,

となっています。
つまり、なんでも受け取れてしまいます。
plainToClassの中では内部的に第1引数で渡したclassのmember変数のkeyを元に新しいオブジェクトを生成して返すが、ここで型チェックは行われないようになっています。

もう一つの、class-transformeを使うとconstructorにドメインルールを記述できない問題についてです。
例えば、Userクラスに「nickNameは5文字以内でなくてはいけない」という制約を追加するとします。

interface userProps {
  firstName: string;
  lastName: string;
  email: string;
  nickName: string;
}
class User {
  firstName: string;
  lastName: string;
  email: string;
  nickName: string;

 constructor(props: userProps) {
    if(props.nickName.length > 5) {
      throw new Error('nickNameが長すぎます');
    }
  }
}

plainToClass(User, { nickName: 'tooLongName' }); // -> インスタンス化される

しかし、このように書いても、plainToClassはうまくUserの「nickNameは5文字以内でなくてはいけない」という制約を破ったインスタンスを作ってしまいます。

class-validatorを使ってモデルのメンバー変数のドメインルールを記述する

この2つの問題を解決するためにclass-validatorを使うと良いでしょう。
class-validatorを使うことで、様々なvalidationを定義でき、ドメインルールを記述することができます。

import { MaxLength } from 'class-validator';
class User {
  firstName: string;
  lastName: string;
  email: string;
  @MaxLength(5, {
    message: 'nickNameが長すぎます',
  })
  nickName: string;
}

このようにclass-validatorが提供しているデコレータを使えば、nickNameが5文字以内でなくてはいけないことを表現できます。
このとき

import { plainToClass } from 'class-transformer';
import { validate } from 'class-validator';

const jonathan = plainToClass(User, { nickName: 'Jonathan' });
validate(jonathan).catch(err => console.log(err));

のようにインスタンス化したUserがルールに沿っているかを判定することができます。
しかし、これはインスタンス化するときに弾いて欲しいですよね。
class-transformerを使わない場合、モデルのclassのconstructorでエラーを出せば良いのですが、それに相当することをclass-transformerはやってくれません。

class-transformer-validatorを使ってドメインオブジェクトをインスタンス化する

そこで、class-transformerとclass-validatorを組み合わせたclass-transformer-validatorというライブラリを追加で使います。
使いたいのはtransformAndValidateというmethodです。

plainToClassの代わりにtransformAndValidateを使って以下のようにかくことができます。
(class-transformer-validatorのtransformAndValidateはplainToClassの代替になります。その他の@Typeなどの機能はclass-transformerが必要なので、class-transformer-validatorがclass-transformerの代わりになるわけではありません。)

import { transformAndValidate } from "class-transformer-validator";

const jonathan = await transformAndValidate(User, { nickName: 'Jonathan' });
// error発生 インスタンス化できない。

これは、nickName値オブジェクトのconstructorのなかでnickNameのルールを確認して、ルールに反していればerrorを出す代わりにnickNameを使うUserエンティティにルールをかき、Userモデルのインスタンス化の時にルールをチェックするということをしていることになります。

つまり、今までに紹介したJavaScriptのStringやNumberを継承したprimitiveな値オブジェクトは定義せず、それを使う、エンティティでドメインルールを管理することになります。
Objectである値オブジェクトについてはそのままclass-transformerを使うことができます。

また、class-validatorが提供するvalidationにIsStringやIsIntのようなアノテーションがあり、型で制御できない部分をカバーすることができます。

import { plainToClass } from 'class-transformer';
import { IsString } from 'class-validator';
import { transformAndValidate } from "class-transformer-validator";

class User {
  id: number;
  @IsString()
  name: string;
}

const user = transformAndValidate(User, { nickName: 123 });
// -> error発生

また、集約をインスタンス化する時には、@ValidateNestedアノテーションを使います。

import { Type } from 'class-transformer';
import { IsString, ValidateNested } from 'class-validator';
import { transformAndValidate } from "class-transformer-validator";

class Task {
  id: number;
  @IsString()
  name: string;
}

class User {
  id: number;
  name: string;

  @Type(() => Task)
  @ValidateNested()
  tasks: Task[];
}

const plainUser = {
 id: 1,
 name: 'Taro',
 tasks: [
   { id: 1, name: 'hogetask' }
 ],
};

const invalidPlainUser = {
 id: 1,
 name: 'Taro',
 tasks: [
   { id: 1, name: 123 }
 ],
};

const userAggregation = await transformAndValidate(User, plainUser);
// -> ok
const userAggregation = await transformAndValidate(User, invalidPlainUser);
// -> error発生

これで、集約に対して、plain objectを一気にインスタンス化することができるというclass-transformerの強みを生かすことができます。

まとめ

  • DDDとは現実世界の業務を忠実にコード上に再現しようとする設計手法である。
  • そのために、ドメインオブジェクトで業務上に登場する人や物をモデルとして定義する。
  • このモデルを定義したclassにあらゆるドメインのルールが記載される。
  • JavaScriptにおいてはclass-transformerを用いることで簡単にモデルの定義とルールの記載ができ、インスタンス化も少ない記述量でできる。

次回予告

PART2ではドメインサービス、リポジトリ、ファクトリーについて説明していきます!

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

備忘録 Javascript コンストラクタ

javascript でのコンストラクタの使い方

var Human = function(name, weight, height){
    this.name = name;
    this.weight = weight;
    this.height = height;

    this.getBmi = function(){
        this.bmi = this.weight / (this.height * this.height);
        return this.bmi;
    }
}
var human = new Human("tanaka", 65, 1.7);
console.log(human.name + "さんのBMIは" + Math.round(human.getBmi()) + "です。");

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

Javascriptで該当月の何週目、何曜日の取得

ある月に関して全ての日付の、何週目か、何曜日かという情報をセットしておく必要がありその際に作成したロジック。特定の日付が何週目か、何曜日かという情報だけを撮るには若干オーバースペックだけれど、一応その方向でも使える。

YYYY-MM形式で渡すと、指定された月のすべての日付に関して、何週目か、何曜日(日曜始まりの0スタート)かという情報を返す。

See the Pen get_week_of_month_and_weekday by nicopin (@nicopin) on CodePen.

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