- 投稿日:2020-09-17T21:29:15+09:00
【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側はこんな感じ。調べてみればすぐ分かる範囲ですし、問題ないかと。
- デリミタを
[[ ]]
にしていますが、これはAPIサーバをFlaskで書くことが多く、Flaskのテンプレートエンジン(Jinja2)のデリミタ{{ }}
とコンフリクトしてしまうために変更しています。- 参考にした記事:Flask で Vue.js を使おうと思ったら出鼻をくじかれたのでメモ - Qiita
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.jsvar 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のスコープの違いが原因だろうと教えていただいたので、「はじめに」に追記しました。
ただこの罠に嵌っていたとき、値をその場で宣言して代入したりすると上手く表示されたりして「本当になんなんだ……!?」と怒りに震えたりしましたので、もし同じ罠に嵌った人の参考になれば幸いです。また、以下のような記事も後から見つけました。フロントエンドもきちんと作るなら、このくらい構造的に書いた方がいいのかもしれないです。
- 投稿日:2020-09-17T21:24:52+09:00
CodeQLをローカルPCで実行する。ついでにクロスサイトスクリプティングの脆弱性を発見する。
やること
- CodeQL CLI をインストール
- starter pack をダウンロード
- 検査対象のプロジェクト(ソースコード)を用意
- CodeQL データベースを作成する
- 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-079
はImproper 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の#以降に名前を入れると挨拶してくれます。こんにちは!
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 0100万年待ってもクエリが完了しないので、最初に実行したクエリの結果
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-17T21:23:33+09:00
[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; });
- 投稿日:2020-09-17T20:05:36+09:00
【JavaScript】角が丸い四角形【Canvas】
イメージ
角丸の四角形を描画する関数です。
おまけで、吹き出しが下側に出るやつも作りました。
使い方はこんな感じです。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(); }
- 投稿日:2020-09-17T17:50:18+09:00
簡単なアプリ 複数計算機 +
簡単な計算機をつくて見ました。
使用言語は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; }完成形はこんな感じです。
- 投稿日:2020-09-17T17:49:10+09:00
[javascript] Auth0でManagement APIをより簡単に使う
通常ルート
- Machine-to-Machine Applicationをまず作る(参考)
- デフォルトで存在するAuth0 Management APIを選択しpermissions(scopes)を選ぶ
- 出来たMachine-to-Machine Applicationの
client_id
、client_secret
でManagement APIのtokenを取る- 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は用意されていないようなので、ソースを読まないといけないのが難点。
現状これがドキュメントっぽい。躓き点
domainとかが
domain: '{YOUR_ACCOUNT}.auth0.com'
になっているけど、最近作ったアカウントのApplicationのdomainは.us
が間に入ってて、気づかずdev-tm15fv0o.auth0.com
みたいな形でテストしてた。
これでずっとaccess_denied: {"error":"access_denied","error_description":"Unauthorized"}
のエラーで悩まされてた。(accountとも言うし、tenantとも言うし、他にも色々同じ箇所を別の言い方で説明しているのでとにかくドキュメントが読みづらい...)
- 投稿日:2020-09-17T15:56:37+09:00
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 enduser_id、post_idを作成します。カラムの型はinteger型で作成します。
ターミナルでmigrateしたら、モデルファイルにアソシエーションを設定します。models/like.rbclass Like < ApplicationRecord belongs_to :post belongs_to :user validates_uniqueness_of :post_id, scope: :user_id enduser/postモデルとは
user/postモデル 1 対 多 likeモデル
の関係です。
user/postモデル側にはhas_manyでlikesを追加しましょう。
また、dependent: :destroyを付けると、投稿が削除された時にいいねも一緒に削除することができます。
そして、postモデルにはメソッドを定義します。models/post.rbclass 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.rbclass 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.rbresources :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>これで実装完了です!
今回はシンプルにアイコンだけ非同期で動くいいね機能を実装してみましたが、
終わってみればコードは簡単そうに見えますがエラーの地獄でした笑
インスタンス変数が違ったり、スペルミスがあったりと一つ抜けると動かないような感じでしたね。
解説が苦手なので、わかりにくい部分が多いかもしれませんが、いいね機能の記事はたくさんあるので複数参考にすると理解が深まりまると思います!プログラミング初心者はこの機能を実装してみたい人が多い気がするので、少しでも参考になればと思います!
動くと感動するので実装させたい人は頑張ってください!!
- 投稿日:2020-09-17T15:42:13+09:00
連想配列のキャメルケースとスネークケースの変換忘備録
概要
クライアントとサーバーで形式がキャメルケースとスネークケースで違うのが面倒なので変換処理を作ったので忘備録
パラメータの連想配列をキャメルケースからスネークケースに変更
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' } } **/参照
- 投稿日:2020-09-17T15:42:13+09:00
連想配列の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' } } **/参照
- 投稿日:2020-09-17T15:22:05+09:00
今更きく…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
- 投稿日:2020-09-17T15:17:00+09:00
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-regex3 や babel-plugin-transform-modern-regexp を入れても,名前付きキャプチャー・グループと
RegExp.prototype.source
を併用すると変換がおかしくなるので,第一引数の要素には名前付きキャプチャー・グループを含まないようにする必要があります.4
Cf. How can I concatenate regex literals in JavaScript? - Stack Overflow ↩
Cf. `@babel/plugin-transform-named-capturing-groups-regex` fails to wrap all groups · Issue #10045 · babel/babel ↩
2020 年 9 月 17 日現在の最新版は,Babel 7.11.0, babel-plugin-transform-modern-regexp 7.10.4, babel-plugin-transform-modern-regexp 0.0.6 ↩
- 投稿日:2020-09-17T15:17:00+09:00
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-regex3 や babel-plugin-transform-modern-regexp を入れても,名前付きキャプチャー・グループと
RegExp.prototype.source
を併用すると変換がおかしくなるので,第一引数の要素には名前付きキャプチャー・グループを含まないようにする必要があります.4
Cf. How can I concatenate regex literals in JavaScript? - Stack Overflow ↩
Cf. `@babel/plugin-transform-named-capturing-groups-regex` fails to wrap all groups · Issue #10045 · babel/babel ↩
2020 年 9 月 17 日現在の最新版は,Babel 7.11.0, babel-plugin-transform-modern-regexp 7.10.4, babel-plugin-transform-modern-regexp 0.0.6 ↩
- 投稿日:2020-09-17T15:17:00+09:00
【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-regex3 や babel-plugin-transform-modern-regexp を入れても,名前付きキャプチャー・グループと
RegExp.prototype.source
を併用すると変換がおかしくなるので,第一引数の要素には名前付きキャプチャー・グループを含まないようにする必要があります.4
Cf. How can I concatenate regex literals in JavaScript? - Stack Overflow ↩
Cf. `@babel/plugin-transform-named-capturing-groups-regex` fails to wrap all groups · Issue #10045 · babel/babel ↩
2020 年 9 月 17 日現在の最新版は,Babel 7.11.0, babel-plugin-transform-modern-regexp 7.10.4, babel-plugin-transform-modern-regexp 0.0.6 ↩
- 投稿日:2020-09-17T14:33:09+09:00
JavaScriptを単体テストする流行りのフレームワークJestを試してみる
Jestとは
JestはJavaScriptの単体テストのフレームワークです。
https://jestjs.io/ja/テストランナーだけでなく、モック機能やカバレッジの取得を使用することができます。npmのトレンドとしては2019年から伸びてmochaを超えるものとなっています。
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 jestjestではデフォルトでは__tests__フォルダ中のjavascritpをテストコードとして実行するので、__tests__フォルダにテストコードを追加します。
__tests__/sample.test.jstest('test 1.', ()=>{ expect(1+2).toBe(3); });テストを動かすには以下のコマンドを実行します。
# テストを開始する npx jest設定ファイル
jestの設定は以下のいずれかの方法で指定することができます。
- package.jsonに記載する
- jest.config.jsに記載する
- 実行時に--configオプションで指定したファイルに指定する
https://jestjs.io/docs/ja/configuration
jest.config.jsの作成
下記のコマンドで対話式でjest.config.jsを作成します。
npx jest --initカバレッジの収集方法
下記のように--coverageオプションを指定して実行することでカバレッジの収集を行います。
npx jest --coverageこのコマンドを実行するとテストが実行されて、coverageフォルダに結果が格納されます。
coverage/lcov-report/index.htmlにはhtml形式でレポートが表示されています。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ボタンを押下することでデバッグ実行が可能になります。
非同期の試験
https://jestjs.io/docs/ja/asynchronous
do引数の利用
テストメソッドの引数にdoneを与えて、テストメソッド中にそのdoneを実行することで任意のタイミングでテストの終了を通知することができます。
async.test.jsfunction 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(); });Promisesの試験
テスト対象のメソッドがPromisesを返す場合、テストメソッドでそのPromisesを返すことで、それが解決するまでテストメソッドの完了を待つことができます。
マッチャー関数のresolves/rejectsを使用してPromissesの結果を判定することも可能です。async.test.jsfunction 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.jstest('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.jsbeforeAll(() => { 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にはskip、onlyを使用してテストを実行するかどうかの制御を行うことができます。
skipは動かなくなったテストを暫定的にスキップする場合や、負荷が高くて通常は実行したくないテストを弾く場合に使用します。
まだ未実装のことを現したい場合はtodoを使用しましょう。todoではエラーを出力します。onlyはデバッグ中に特定のテストのみを動作させる場合に使用します
なお、MsTestにあるpriorityは存在しないので、優先度を指定してのテストは行えないようです。
スキップの例.jsdescribe('desc1', () => { test.skip('test1', () => { }); test('test2', () => { // これだけ実行される }); }); describe.skip('desc2', ()=> { test('test3', () => { }); test('test4', () => { }); });onlyの例.jsdescribe.only('desc1', () => { test('test1', () => { // 実行される }); test('test2', () => { // 実行される }); }); describe('desc2', ()=> { test('test3', () => { }); test('test4', () => { }); }); describe.only('desc3', ()=> { test.only('test5', ()=> { // 実行される }); test('test6', ()=> { }); });モック
jestではモックが使用できます。これを利用してテスト対象に依存する関数をテストに都合のいい振る舞いにすることができます。
特定の関数をモックにする例
spyOnを用いることで特定の関数をモックにすることができます。
テスト対象の関数
calc.jsfunction sum(a, b) { return a + b; } function minus(a, b) { return a - b; } module.exports = { sum : sum, minus : minus }テストコード
__tests__/sample.test.jsconst 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.jsconst fs = require('fs'); module.exports = function(path) { var text = fs.readFileSync(path, 'utf8'); var lines = text.toString().split('\n'); return lines; }テストコード
テストコード.jsconst 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.jsconst 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を使用します。時刻の偽装のテストコードの例.jstest('時計のモックの確認', ()=> { 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;テストコード
タイマーのモックの操作例.jstest('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を経過したことにすることができます。
タイマーを進めるテストコード.jstest('タイマーを経過させる', () => { 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;テストコード
テストコードの例.jstest('再帰的なタイマーを実行した場合', () => { 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を使用して確認します。
テストコード.jstest('test', ()=> { // test1()の結果が1であるか確認する expect(test1()).toBe(1); // test1()の結果が2でないことを確認する。 expect(test1()).not.toBe(2); }notを使用することで条件の反転が可能です。
- プリミティブ値を比較したり、オブジェクトインスタンスの参照IDを確認したりします。
- 浮動小数点のチェックには使用しないでください。
- 浮動小数点の近似的な値を比較します。
- デフォルトでは小数点の第二位までチェックします。
浮動小数点のテスト例.jstest('浮動小数点 - 桁数の確認', ()=> { // デフォルトでは有効桁二桁まで確認するーこれは合格する expect(0.1+0.201).toBeCloseTo(0.3); // 第二引数に有効桁を指定する。ーこれはNGとなる expect(0.1+0.201).toBeCloseTo(0.3,3); })
- toBeTruthy
- 真偽値のチェックを行う。
- undefinedか否かの確認
- undefinedではないことの確認
- オブジェクトが再起的に一致するか確認する。
toEqualのテストコード.jsfunction 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
- 配列内に特定の値が含まれているか確認する
- 文字列内に特定の文字列が含まれているか確認する
テストコード.jstest('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
- 例外が発生することを確認する
- 引数としてエラメッセージの文字、正規表現、エラーオブジェクト、クラスを与えることができる
サンプルコード.jstest.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); }); });この実行結果は以下のようになります。
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); }); });この結果より同時にテストケースが動作していることが確認できます。
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/unitjest用の設定ファイルを作成します。
jest.config.jsmodule.exports = { preset: '@vue/cli-plugin-unit-jest' }tests/unitフォルダにテストコードを追加します。
tests/unit/example.spec.jsimport { 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.jsimport { 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!!' // 略修正後にテストを際実行すると以下のようなエラーが発生します。
もし、このエラーが予定通りの場合はスナップショットを更新してテストを合格にする必要があります。そのためには以下のコマンドを実行します。
スナップショットの更新のコマンド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> `); });
- 投稿日:2020-09-17T12:16:47+09:00
【JS学習その⑤】プリミティブ型とオブジェクトそれぞれの挙動
JS学習シリーズの目的
このシリーズは、私ジャックが学んだJavaScriptのメカニズムについてアウトプットも兼ねて、
皆さんと知識や理解を共有するためのものです。
(理解に間違いがあればご指摘いただけると幸いです)前置き
今回は、前回の記事で書いた内容を踏まえて解説していきます!
余裕がある方は前回の記事➡【JS学習その④】プリミティブ型とオブジェクト ~データ型~をご覧ください参照とコピー
プリミティブ値とオブジェクトの参照とコピーについて解説します
プリミティブ値のコピー
main.jslet 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.jslet 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.jsconst 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.jslet a = b;これは変数の参照先の値、もしくはオブジェクトへの参照のコピーを表す。
このことを踏まえて、main.jsfunction fn(a) { } let a = 0; fn(a);これは
main.jslet a = b;と同じ。
つまり、fn(a);の引数aにletで定義した変数aのプリミティブ値0を代入し、引数aの参照先を別のメモリ空間にコピーした数値0に変更した
ということです。参照と分割代入
分割代入の定義
main.jslet {a,b} = object;オブジェクトから特定のプロパティーを抽出して宣言を行う。
main.jsconst 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.jsconst 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
↑のようになります。
まとめ
いかがでしたでしょうか。
今回は、前回に続いてプリミティブ型とオブジェクトそれぞれの挙動について解説しました。
プリミティブ型とオブジェクトの参照とコピーはデータの扱いにおいてとても重要なのでしっかりと理解しておきましょう!
- 投稿日:2020-09-17T12:14:19+09:00
指で3Dオブジェクトを動かすアプリをつくった
3Dの物体を指で動かしてみよう
A-frameを学び、animationの動きを見ていて、このanimationを指で動かせないかと思って、色々試してみました。動かせたら、ちょっとしたゲームができそうだなと。
できました
つくったものをまずご覧ください。
A-frameのアニメーションを、手で動かせるようにしてみました。ただ、右がうまく機能しなかった。。機械学習のやり直しかな。#protoout #Aframe #機械学習 #AI pic.twitter.com/EGLGB02SzH
— 柳瀬浩之@人材開発×UX専門家 (@btap_hiro) September 17, 2020まだ機械学習の精度が低く、思ったようには動いてくれませんが、一応、手の動きに合わせて、アニメーションが変わるように設定できました。
また、使い方のチュートリアルが表示されるように、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。
手軽に学習できます。使い方も簡単。
上、下、左、右、それぞれの手の形を写真で撮って覚えさせるだけです。そうすると、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お願いします!!
- 投稿日:2020-09-17T12:14:19+09:00
機械学習で、指でA-frameの3Dオブジェクトを動かすアプリをつくった
3Dの物体を指で動かしてみよう
A-frameを学び、animationの動きを見ていて、このanimationを指で動かせないかと思って、色々試してみました。動かせたら、ちょっとしたゲームができそうだなと。
できました
つくったものをまずご覧ください。
A-frameのアニメーションを、手で動かせるようにしてみました。ただ、右がうまく機能しなかった。。機械学習のやり直しかな。#protoout #Aframe #機械学習 #AI pic.twitter.com/EGLGB02SzH
— 柳瀬浩之@人材開発×UX専門家 (@btap_hiro) September 17, 2020まだ機械学習の精度が低く、思ったようには動いてくれませんが、一応、手の動きに合わせて、アニメーションが変わるように設定できました。
また、使い方のチュートリアルが表示されるように、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。
手軽に学習できます。使い方も簡単。
上、下、左、右、それぞれの手の形を写真で撮って覚えさせるだけです。そうすると、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と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お願いします!!
- 投稿日:2020-09-17T12:05:47+09:00
フルスタックエンジニアになるためのモダンな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
練習項目
私がいつも恐れているものを開発していただきます。それは、画像アップローダーアプリです。フロントエンドから写真をアップロードする方法、データベースにファイルを保存する方法、簡単なAPIを構築する方法を学びます。
レベル
Front-end: ⭐⭐
Back-end: ⭐⭐⭐2. My Unsplash
練習項目
基本的なAPIの機能(RESTful APIでのGET、POST、PUT、DELETEなど)を構築する方法を学びます。
また、Unsplashのような画像グリッドの作り方も学びます。
レベル
Front-end: ⭐⭐⭐⭐
Back-end: ⭐⭐⭐⭐3. CatWiki
練習項目
猫は大好きですか?外部APIを使用して猫のデータを取得し、ユーザーの行動を追跡するAPIを構築します。また、バックエンドでの検索やフィルタリングの仕組みを練習します。
このプロジェクトでは、猫だらけのレイアウトであなたのフロントエンドのスキルをテストします。
レベル
Front-end: ⭐⭐⭐⭐
Back-end: ⭐⭐⭐⭐4. Authentication App
練習項目
バックエンドやフルスタックの開発者であれば、ユーザーデータや認証を扱うことは避けられません。このプロジェクトでは、ユーザー認証とユーザーデータを扱うシンプルながらも強力なアプリケーションを学び、構築していきます。
このプロジェクトは、今後のプロジェクトの基礎にもなります。
レベル
Front-end: ⭐⭐
Back-end: ⭐⭐⭐⭐5. Shoppingify
練習項目
ECアプリを構築するのは本当に大仕事です。しかし、もしあなたが練習をしたいのであれば、Shoppingifyは良いスタートになります。このプロジェクトは認証を必要としませんが、ECサイトが持っているほぼすべての機能を持っています。ショッピングカートの構築、新しいアイテムの追加、アイテムの編集、履歴やデータの管理などを学ぶことができます。
このプロジェクトでは、複雑で反応の良いレイアウトを作成するので、あなたのフロントエンドのスキルが高いレベルで試されます。
レベル
Front-end: ⭐⭐⭐⭐⭐
Back-end: ⭐⭐⭐⭐6. Chat Group
練習項目
以前からチャットアプリを作りたいと思っていたのですが、やってみたら超楽しかったです。この課題では、複数のチャットグループアプリケーションを構築します。あなたはリアルタイムデータベースだけでなく、websocket、socketioのような技術にも精通するようになるかも知れません…。
認証アプリケーションを統合することで、ユーザーを管理する方法、グループに追加する方法、メッセージを処理する方法などを学びます。
また、シンプルだけどシンプルじゃないUIを構築していくことになるでしょう?。
レベル
Front-end: ⭐⭐⭐
Back-end: ⭐⭐⭐⭐7. Tweeter - Twitter Clone
練習項目
このUI、シンプルでスッキリしていて大好きです。
ここで言っておきますが、Twitterを再現することができれば、あなたは何でもできます?。
今回のプロジェクトでは、Facebook、Twitterと同じようなプロフィールを作成します。フォロー・フォロワーの仕組み」の作り方を学びます。
また、新しいツイートを投稿したり、コメントを付けたりする方法も学びます。ツイートの種類に応じたフィルターのかけ方なども…。
これは間違いなく今までで最もチャレンジングなプロジェクトの一つです。しかし、あなたが多くのことを学ぶことを約束します ?
レベル
Front-end: ⭐⭐⭐⭐⭐
Back-end: ⭐⭐⭐⭐⭐8. 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
- 投稿日:2020-09-17T12:05:47+09:00
フルスタックマスターになるためのモダンな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
練習項目
私がいつも恐れているものを開発していただきます。それは、画像アップローダーアプリです。フロントエンドから写真をアップロードする方法、データベースにファイルを保存する方法、簡単なAPIを構築する方法を学びます。
レベル
Front-end: ⭐⭐
Back-end: ⭐⭐⭐2. My Unsplash
練習項目
基本的なAPIの機能(RESTful APIでのGET、POST、PUT、DELETEなど)を構築する方法を学びます。
また、Unsplashのような画像グリッドの作り方も学びます。
レベル
Front-end: ⭐⭐⭐⭐
Back-end: ⭐⭐⭐⭐3. CatWiki
練習項目
猫は大好きですか?外部APIを使用して猫のデータを取得し、ユーザーの行動を追跡するAPIを構築します。また、バックエンドでの検索やフィルタリングの仕組みを練習します。
このプロジェクトでは、猫だらけのレイアウトであなたのフロントエンドのスキルをテストします。
レベル
Front-end: ⭐⭐⭐⭐
Back-end: ⭐⭐⭐⭐4. Authentication App
練習項目
バックエンドやフルスタックの開発者であれば、ユーザーデータや認証を扱うことは避けられません。このプロジェクトでは、ユーザー認証とユーザーデータを扱うシンプルながらも強力なアプリケーションを学び、構築していきます。
このプロジェクトは、今後のプロジェクトの基礎にもなります。
レベル
Front-end: ⭐⭐
Back-end: ⭐⭐⭐⭐5. Shoppingify
練習項目
ECアプリを構築するのは本当に大仕事です。しかし、もしあなたが練習をしたいのであれば、Shoppingifyは良いスタートになります。このプロジェクトは認証を必要としませんが、ECサイトが持っているほぼすべての機能を持っています。ショッピングカートの構築、新しいアイテムの追加、アイテムの編集、履歴やデータの管理などを学ぶことができます。
このプロジェクトでは、複雑で反応の良いレイアウトを作成するので、あなたのフロントエンドのスキルが高いレベルで試されます。
レベル
Front-end: ⭐⭐⭐⭐⭐
Back-end: ⭐⭐⭐⭐6. Chat Group
練習項目
以前からチャットアプリを作りたいと思っていたのですが、やってみたら超楽しかったです。この課題では、複数のチャットグループアプリケーションを構築します。あなたはリアルタイムデータベースだけでなく、websocket、socketioのような技術にも精通するようになるかも知れません…。
認証アプリケーションを統合することで、ユーザーを管理する方法、グループに追加する方法、メッセージを処理する方法などを学びます。
また、シンプルだけどシンプルじゃないUIを構築していくことになるでしょう?。
レベル
Front-end: ⭐⭐⭐
Back-end: ⭐⭐⭐⭐7. Tweeter - Twitter Clone
練習項目
このUI、シンプルでスッキリしていて大好きです。
ここで言っておきますが、Twitterを再現することができれば、あなたは何でもできます?。
今回のプロジェクトでは、Facebook、Twitterと同じようなプロフィールを作成します。フォロー・フォロワーの仕組み」の作り方を学びます。
また、新しいツイートを投稿したり、コメントを付けたりする方法も学びます。ツイートの種類に応じたフィルターのかけ方なども…。
これは間違いなく今までで最もチャレンジングなプロジェクトの一つです。しかし、あなたが多くのことを学ぶことを約束します ?
レベル
Front-end: ⭐⭐⭐⭐⭐
Back-end: ⭐⭐⭐⭐⭐8. 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
- 投稿日:2020-09-17T11:58:01+09:00
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に書かない技術ネタなどもツイートしているので、よかったらフォローお願いします→Twitter@suin
- 投稿日:2020-09-17T11:10:53+09:00
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()メソッドについて解説されています(勿論それ以外も!)
- 投稿日:2020-09-17T08:34:18+09:00
生産ラインのエラーを可視化してみる
何処でエラーが出て止まっているんだ?
私は工場に勤めていて設備が故障で停止した際の対処も仕事の一つです、長い生産ラインどこでエラーが出て止まているのか、今まではエラーが出ても現場にある表示機に映すだけで、現場に行かなければどんなエラーがどの場所で出ているかわからない(積層灯でなんとなく場所はわかっても細かい内容まではわからない)、、、ライン内の「どこで」「どのような」エラーがでて止まているのか可視化することで、現場についてから考えないですむなどのメリットがあったり、マネジャーであれば現場の状況を把握したいなどあると思います。
かっこよく映したい
生産数や稼働率は文字やグラフで出しているけど、文字やグラフ表示で「文字を読まなくてはならない」と思います。
A-Frameを使って3D表現で可視化して設備に紐づけてラインを横断して映し出すことができれば、「稼働率の低い機械はどこ?」なんてことも一発で解決できる!あと3D表示されているDashboardは「かっこいい」と思い挑戦してみました。作ったもの
https://eloquent-shockley-90175d.netlify.app/
生産ラインのエラーの可視化に挑戦してみました!3Dは苦手意識がありましたが何とか形はできた!#Protoout #JavaScript #NodeRed #RaspberryPi https://t.co/cHfZRQhQB5 pic.twitter.com/MtdfMMO4Ft
— Toshiki (@Hirasawa1987) September 16, 2020使用した技術
- Vue.js
- A-Frame
- Firebase
- axios
- Raspberry Pi4(ロボットと仮定して使用)
- Node-RED
構成図
コード
コードはこちらにあります
https://gist.github.com/Toshiki0324/4a84065db800f717eaa2cb6228b6157d躓いた部分
3Dモデルの読み込み
今回は無料でダウンロードできる3Dモデルを使用しました、実際に工場で使用するときにはCADで書いたものを
.obj
や.gltf
といった拡張子にエクスポートして使います、使用できるファイルの種類は公式サイトに記載されています。
作ったhtmlファイルをブラウザから開いても表示されない・・・という現象に陥りました、原因はいまいちわかりませんでしたが、以下の方法で解消しました。要素の位置調整
要素の動かし方にも悩みました、テキストをどのあたりに出すか、線をどこからどこに引くかなど、どうしたらいいんだと悩んでいたら
<ctrl> + <alt> + i
で編集ができるという文言を発見、公式サイトにも載っていました。Visual Inspector と言うらしいです。
左側で要素のを選択でき、右側で調整したりできます。
ちなみに終了すると元に戻るので、いじった項目と数字はメモか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で使う場合は
Vue.config
に設定が必要みたいです。データの読み込み
今回はFirebaseのCloudFirestoreを使用しましたmemosというコレクションIDにデータを作成したのでmemosのデータを監視して
変更があったら読み込む形でリアルタイムに反映されるようにしました。
以下はCloudFirestoreの内容です。app.jsdb.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のデータに変更があった場合の処理に参考に察せていたできました
エラーは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}]同じWi-Fi環境であればこんなかんじ↓でラズパイのipアドレス
http://192.168.1.〇〇:1880/ui/
にアクセスできUIが表示されます。CodePenでグラフを探す
3Dのみの表示だと味気ない気がしたので、今回はCodePenから素敵な見た目のものを持ってきました
https://codepen.io/amcharts/pen/gbLpMR
「カッコイイ!!」までは行けなかった
今回は1ヶ所しかエラーを実装できませんでしたしかも「エラー発生中」せっかくVue.jsを使用したので内容も表示できればよかった、ですが3Dで可視化できたことはうれしかった!作りこんでいくことでライン全体、若しくは工場全体を表示することも可能になりそう。
- 投稿日:2020-09-17T07:55:21+09:00
Async/awaitで非同期処理を実現する
下記を参照
Async/await - 現代の JavaScript チュートリアル
ちなみにasyncの使い方は下記のような感じ
async function f() { return 1; } f().then(function(value){ console.log(value); });
- 投稿日:2020-09-17T03:56:51+09:00
ajaxの非同期通信処理について
マジでjavaScritpムズいくて泣きそうな筆者です(T ^ T)
今回はjavascriotのライブラリーである、
jQueryのajaxについて学んだので、書いていきます。ajaxとは
ajaxとはざっくり説明すると、非同期処理ができる機能だと思ってください。
正式な名称は、Asynchronous JavaScript + XMLとなっています。
(Asynchronousは「非同期」)
この機能を使うと非同期で処理が行えるので、ページを再読み込みしなくても、
処理を反映させる事ができます。
イメージとしてこんな感じです。同期処理
図だけだと解りにくいですが、
イメージとしては、
1・ PCのブラウザで2チャンネルなどの掲示板を表示させている。
2・ 掲示板のスレッドに書き込みをする。
(この際、同期処理の場合は、書き込んだ文字などが、サーバに送信(リクエスト)されて、
何らかの処理を行って、書き込んだ文字などが反映されたHTMLファイルを返す(レスポンス)
このように、画面から文字などを送信し、リクエストする度にレスポンスが行われ、
再度画面リロードされる。
これが、同期処理になります。
これに対して、非同期処理は、下記のような形になります。非同期処理
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)
送ったデータを処理し、再度構築する?感じが全然イメージが
つかなかったのですが、何度も処理を書いているうちに、
処理順番とデータの再構築方法などが掴めてきました!
- 投稿日:2020-09-17T02:57:25+09:00
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> ); }さて結果は...
期待外れでした。
ちなみに、チェックボックスUIを
<input type="checkbox"...
のように素のJSXで表現すれば正しくIDは表示されます。ここでマークアップ構造を確認すると...
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> ); }これを同様にコンソールで確認してみると...
正常に取得できています。
これで一件落着ですね。一癖ありますが、これからもMaterial-UIと良好に付き合っていきたいと思います。
確認用のソースコード全体
App.tsximport 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;
- 投稿日:2020-09-17T02:57:25+09:00
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> ); }さて結果は...
期待外れでした。
ちなみに、チェックボックスUIを
<input type="checkbox"...
のように素のJSXで表現すれば正しくIDは表示されます。ここでマークアップ構造を確認すると...
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> ); }これを同様にコンソールで確認してみると...
正常に取得できています。
これで一件落着ですね。一癖ありますが、これからもMaterial-UIと良好に付き合っていきたいと思います。
教訓
HTMLの確認を怠らないようにしましょう。
更新履歴
2020/09/17 ソースコードを微調整しました
- 投稿日:2020-09-17T01:02:11+09:00
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 ドメインモデルの実装方法(本記事)
- DDDとは何かをざっくり知り、ドメインモデルとドメインオブジェクトを知る。
- ValueObject
- Entity
PART2 ドメインサービス、リポジトリ、ファクトリーの実装方法(次回記事で書きます)
- Domain Service
- Repository
- Factory
PART3 集約の実装方法(次回記事で書きます)
- 集約とは
- 集約の実装方法
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.tsimport _ 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.tsinterface 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.tsimport _ 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.tsenum 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.tsclass 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.tsclass 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.tsinterface 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ではドメインサービス、リポジトリ、ファクトリーについて説明していきます!
- 投稿日:2020-09-17T00:38:34+09:00
備忘録 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()) + "です。");
- 投稿日:2020-09-17T00:22:26+09:00
Javascriptで該当月の何週目、何曜日の取得
ある月に関して全ての日付の、何週目か、何曜日かという情報をセットしておく必要がありその際に作成したロジック。特定の日付が何週目か、何曜日かという情報だけを撮るには若干オーバースペックだけれど、一応その方向でも使える。
YYYY-MM形式で渡すと、指定された月のすべての日付に関して、何週目か、何曜日(日曜始まりの0スタート)かという情報を返す。
See the Pen get_week_of_month_and_weekday by nicopin (@nicopin) on CodePen.