- 投稿日:2020-04-04T23:35:36+09:00
【knex】this.dialectに関してのエラー解決
エラー文
Using 'this.dialect' to identify the client is deprecated and support for it will be removed in the future. Please use configuration option 'client' instead.
解決法
var knex = require('knex')({ dialect: 'mysql', connection: { host: 'localhost', user: 'root', password: '(パスワード)', database: '(データベース名)', charset: 'utf-8' } });上の文の、dialectをclientに変更する。
- 投稿日:2020-04-04T23:35:10+09:00
Vue.js で作るForm(フォーム)
Vue.jsで作るフォーム
form の input 要素 や textarea 要素、 select 要素に双方向 (two-way) データバインディングを作成するには、v-model ディレクティブを使用することができます。それは、自動的に入力要素のタイプに基づいて要素を更新するための正しい方法を選択します。ちょっと魔法のようですが、v-model はユーザーの入力イベントにおいてデータを更新するための基本的な糖衣構文 (syntax sugar) で、それに加えて、いくつかのエッジケースに対しては特別な配慮をしてくれます。
<参考文献>
入力フォーム input (text)
HTML
<!-- v-model.lazyでinputからカーソルが離れた際に発火するようにする --> <input id="title" type="text" v-model.lazy="eventData.title"> <pre>{{ eventData.title }}</pre> <!-- 確認 -->JavaScript
export default { data(){ return{ eventData:{ //eventDataプロパティにtitleの初期値を設定 title:'', } } } }入力フォーム input (number)
HTML
<!-- v-model.numberでvalueを数値に固定 --> <input id="maxNumber" type="number" v-model.number="eventData.maxNumber"> <p>{{ typeof eventData.maxNumber }}</p> <!-- 確認 -->JavaScript
export default { data(){ return { eventData:{ maxNumber: 0, } } } }input (先頭と後尾の改行を取り除く)
HTML
<!-- v-model.trimで改行を取り除く --> <input id="host" type="text" v-model.trim="eventData.host"> <pre>{{ eventData.host }}</pre>JavaScript
export default { data(){ return{ eventData:{ host: '' } } } }チェックボックス(単体)
HTML
<input type="checkbox" id="isPrivate" v-model="eventData.isPrivate"> <label for="checkbox">非公開</label> <p>{{ eventData.isPrivate }}</p>JavaScript
export default { data(){ return{ eventData:{ isPrivate: false, //boolean型で返ってきます } } } }チェックボックス(複数)
HTML
<input type="checkbox" id=10 value="10代" v-model="eventData.target"> <label for="10">10代</label> <input type="checkbox" id=20 value="20代" v-model="eventData.target"> <label for="20">20代</label> <input type="checkbox" id=30 value="30代" v-model="eventData.target"> <label for="30">30代</label> <input type="checkbox" id=40 value="40代" v-model="eventData.target"> <label for="40">40代</label> <p>{{ eventData.target }}</p>JavaScript
export default { data(){ return{ eventData:{ target: [], //配列で指定 } } } }ラジオボタン
HTML
<input type="radio" id="free" value="free" v-model="eventData.price"> <label for="free">無料</label> <input type="radio" id="paid" value="paid" v-model="eventData.price"> <label for="paid">有料</label> <p>{{ eventData.price }}</p>JavaScript
export default { data(){ return{ eventData:{ price: "free" } } } }セレクトボックス
HTML
<select v-model="eventData.location" multiple> <option v-for="location in locations" v-bind:key="location"> {{ location }}</option> </select> <p>{{ eventData.location }}</p>JavaScript
export default { data(){ return { locations: ["東京", "埼玉", "千葉", "神奈川"], eventData:{ location: [] } } } }
- 投稿日:2020-04-04T23:25:56+09:00
Instagramでスクロール中にモーダルが表示され閲覧できなくなる問題を解決する。
はじめに
※ 悪用厳禁です。
我々のようなエンジニア
(隠キャ)はInstagramという(陽キャ御用達の)サービスを使う機会がないですし、アカウントも作成していないと思います。しかし、自分の大好きな芸能人の写真をInstagramで
ニヤニヤしながら見ていると突如、モーダルが表示され、モーダルを閉じることもスクロールすることもできなくなり閲覧できない状態になってしまいます。その名も、「Instagram問題」
大概の人は、そこで諦めるかもしれません。
でも、我々は諦めません。この問題を解決する術があるからです。該当する問題
ログインをしていないときに、しばらくスクロールすると次のような画面になります。
この状態になると、半透明になっているところをクリックしてもモーダルは閉じませんし、スクロールもできません。写真は有名ユーチューバーHikakinのInstagramを使用しています。
あなたなら、このInstagram問題をどう解決しますか?
答えは簡単です。次の3つのステップを順に行えば、アカウントを持っていなくても、自由に好きな芸能人の写真をみることができます。
Step1 デベロッパーツールを表示する
これは簡単ですね。
option + command + i
を押せば表示されます。
設定によって表示される位置は異なりますが、以下のように表示されればオッケーです。Step2 モーダルを消す
まず、モーダルを表示しているタグを探します。
デベロッパーツールをElementsタブの状態のまま、モーダル上で右クリック
->検証
をクリックすると、該当するタグが示されます。
青くハイライトされているところが、モーダルを表示しているタグです。
右クリック
->Delete element
をクリックするとモーダルの表示が消えます。しかし、まだスクロールができません。
Step3 スクロールを可能にする。
スクロールができない原因を考えます。
CSSでは、overflow: hidden;
を使うとスクロールができなくなるので、このCSSが指定されている要素を探します。
Instagramではbodyタグのstyle属性に指定されていました。bodyタグ上で
右クリック
->Edit attribute
をクリックすると、属性が編集できるようになるので、style属性を削除しましょう。おわり
無事に、アカウントを登録することなく自由にInstagramを閲覧できるようになりました。これで、あなたは心配することなく思う存分楽しむことができます。
おまけ
紹介した3ステップは全てボタンをクリックして行いました。よりエンジニアらしくするために、コードで再現しましょう。
まずは、デベロッパーツールを開いて、表示されるモーダルのclass名かId名を確認します。先ほどの写真からclass名は
RnEpo
であることがわかります。準備ができたので、まずはモーダルを削除しましょう。
const modal = document.getElementsByClassName('RnEpo')[0]; modal.parentNode.removeChild(modal);次に、スクロールを可能にしましょう。
bodyタグのstyle属性を削除します。document.body.removeAttribute('style');
- 投稿日:2020-04-04T22:43:10+09:00
JavaScriptでタブ機能を実装する(シンプル、IE対応)
はじめに
シンプルなタブの実装を速やかに終わらせるための備忘録。
CSSは最低限のデザインだけです。クラス名も好きに変えちゃってください。ソースコード
index.html<!-- タブメニュー --> <ul> <li class="tab active">Portfolio</li> <li class="tab">Jobs</li> <li class="tab">Design</li> <li class="tab">Event</li> </ul> <!-- タブコンテンツ --> <div class="panel show">Portofolioです</div> <div class="panel">Jobsです</div> <div class="panel">Designです</div> <div class="panel">Eventです</div>style.css/* 子要素の.tabをfloatで左に浮かせたので親要素の高さを確保する */ ul:after{ content: ''; display: block; clear: both; } /* タブメニューのデザイン */ .tab{ padding: 5px 7px; text-align:center; display:block; float:left; } /* 選択中のタブの色を変える */ .tab.active{ background: #000; border-radius: 3px; color:#FFF; transition: all 0.5s ease-out; } /* .showがついていない.panelは全て非表示 */ .panel { display: none; } /* .showがついた.panelを表示する */ .panel.show { display: block; }main.js// worksページタブ機能 const tabs = document.getElementsByClassName('tab'); for(let i = 0; i < tabs.length; i++) { tabs[i].addEventListener('click', tabSwitch); } // タブをクリックすると実行する関数 function tabSwitch(){ // .tabを名付けた要素のクラスを付け替える処理 document.getElementsByClassName('active')[0].classList.remove('active'); this.classList.add('active'); // コンテンツのclassの値を変更 document.getElementsByClassName('show')[0].classList.remove('show'); const arrayTabs = Array.prototype.slice.call(tabs); const index = arrayTabs.indexOf(this); document.getElementsByClassName('panel')[index].classList.add('show'); };解説
getElementsByClassName('active')[0]
getElementsByClassName()
・対象となるクラス名が設定されているHTML要素をすべて取得できるなぜ[0]をつけるのか?
与えられたクラス名で得られる子要素すべての配列ライクのオブジェクトを返します。documentオブジェクトで呼び出されたときは、ルートノードを含む、完全な文書が検索されます。
https://developer.mozilla.org/ja/docs/Web/API/Document/getElementsByClassNameよって配列を個別で取得する必要がある。この場合は、.activeがついている要素の1番目を取得している。(上のソースコードであれば、.activeは1つしかないので[0]となる)
console.log(getElementsByClassName('active')); //HTMLCollection [] // length: 1 // 0: li.tab.active <= ここから.activeの要素を取得する // __proto__: HTMLCollectionclassList
・対象要素に設定しているクラスを配列のように扱えるオブジェクト
・読み取り専用のプロパティ
・Element.classList.メソッドでよく使われる。
メソッド名 機能 add() クラスを追加 remove() クラスを削除 toggle() クラスがあれば削除・無ければ追加 contain() クラス名の有無を true / false で返す replace( oldClass, newClass ) oldClassをnewClassで置き換え Array.prototype.slice.call(tabs)
・tabsはHMTLCollectionという配列風オブジェクトなので、配列に変換する。
arrayTabs.indexOf(this)
・String オブジェクトの中からfromIndexで1番最初に現れた値のインデックスを返す。
・値が見つからない場合は-1を返す。
・document.getElementsByClassName('tab')で取得したHTMLCollectionをArray.prototype.slice.call()で配列に変換したので、indexOf()でインデックスを取得し、.showをつける.panelを判別する。おわりに
間違いありましたらご指摘お願いします!
- 投稿日:2020-04-04T22:37:32+09:00
【JavaScript】thisが指すオブジェクトがわけわからなくなるので書いておく
はじめに
JavaScriptを始めた時に、一番最初につまづくポイントってthisば気がします。
自分も訳わかりませんでした。
そんな時のためにチラッと確認できるようここにまとめておきます。開眼!JavaScriptを参考にしています。
結論
関数が実行された時に、thisは設定されています。
その呼び出された関数をプロパティかメソッドとして保持しているオブジェクトがthisに設定されています。コードにしてみる
const people = { name: 'java男', age: 23, greet: function () { console.log(`I am ${this.name}`) } } people.greet() // 出力 : I am java男ここでいうプロパティかメソッドとして保持しているオブジェクトとは、peopleオブジェクトとなります。
つまり、this.nameが指すのは、peopleオブジェクトのnameプロパティとなります。const testObj = { test: 'test' } const thisTest = function () { console.log(`${this}`) } testObj.thisTest = thisTest // (1) [object Object] testObj.thisTest() // (2) [object global] thisTest()(1)のthisはtestObjオブジェクトを指しています。なぜなら、thisTestをtestObjのメソッドとして定義したからです。
対して(2)のthisはグローバルオブジェクトを指しています。なぜなら、ここで実行されているthisTestを保持しているのはグローバルオブジェクトであるためです。終わり
アロー関数ではthisをグローバルに束縛するとか他にもありますが、とりあえずごちゃつくのでここで終わります。
ややこしくなりがちがthis、きっちり学んでいきましょう。
- 投稿日:2020-04-04T21:07:39+09:00
JavaScriptの繰り返し(ループ)処理について
- 投稿日:2020-04-04T20:45:30+09:00
jQueryのappendで取得元が消えてしまうときの回避方法
はじめに
エレメントから要素を取得して、その要素を別の場所にコピーする処理をjQueryのappendでしようとしていたのですが、取得元が消えてしまいハマってしまったので、その回避策のメモです。
事象
以下のような画面があったとして、「テストタイトル1」をクリックすると、「テストタイトル2」の下に「テスト内容」がコピーされるプログラムを作成したかったとします。
テストタイトル1 // ① クリック テスト内容 テストタイトル2 // ② この下に「テスト内容」がコピーされるそこで、以下のようなHTMLとJavascriptを実装します。
html<html> <head> <meta charset="utf-8"> <title>テスト</title> <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script> </head> <body> <div id="test" class="test"> <div class="test-title-1"> テストタイトル1 <div class="test-contents">テスト内容</div> </div> <div class="test-title-2"> テストタイトル2 </div> </div> </body> <script> $('.test-title-1').on('click', function(){ const target = event.currentTarget; // クリックした要素(test-title-1)を取得 const testContents = $(target).find('.test-contents'); // test-contentsクラスの要素取得 $('.test-title-2').append(testContents); // test-title-2クラスへappend }); </script> </html>すると、なぜか結果は以下のようになり、「テストタイトル1」直下のテスト内容が消えてしまいます。
テストタイトル1 テストタイトル2 テスト内容原因
jQueryオブジェクトのappend()は追加or移動という仕様でした。
公式サイトを見ると、以下のように記載がありました。
If an element selected this way is inserted into a single location elsewhere in the DOM, it will be moved into the target (not cloned)
(日本語訳)
「この方法で選択された要素がDOMの他の場所の単一の場所に挿入された場合、それはターゲットに移動されます」つまり、取得したDOMはappendすると、別の要素へ移動されてしまうのです。。。
回避策
これを回避するには、appendする時に、変数を複製すればOKです。
先ほどのサンプルプログラムを利用すると、具体的には、clone()メソッドを以下のように使います。
html<html> <head> <meta charset="utf-8"> <title>テスト</title> <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script> </head> <body> <div id="test" class="test"> <div class="test-title-1"> テストタイトル1 <div class="test-contents">テスト内容</div> </div> <div class="test-title-2"> テストタイトル2 </div> </div> </body> <script> $('.test-title-1').on('click', function(){ const target = event.currentTarget; const testContents = $(target).find('.test-contents').clone(); // ここにclone()を追加 $('.test-title-2').append(testContents); }); </script> </html>すると、取得した要素は一度クローン(複製)されるため、appendが移動するのは複製した要素となるので、以下のように正常にコピーできます。
テストタイトル1 // ① クリック テスト内容 テストタイトル2 テスト内容 // ② テスト内容が正常にコピーされるさいごに
2020年から個人ブログはじめました!
フリーランスエンジニアになって得た知識と経験をもとに、フリーランスエンジニアに関する情報をはじめ、IT技術情報や業界情報、エンジニアライフハック等のコンテンツを配信していく予定です。
まだまだ記事数は少ないのですが、週単位で更新してますので、もしご興味ございましたら、みていただけると嬉しいです。
さいごまでお読みいただき、ありがとうございました。
- 投稿日:2020-04-04T20:34:20+09:00
問いかけると柴犬の画像を返してくれるLINE botを作ってみた
概要
LINE botでできることを調べていたら、画像を返すことが可能とのことで試してみました。
ただ画像を返すだけでは面白くないので、柴犬APIで画像を拾ってきて表示することにしました。最後にソースコードの全体を載せています。柴犬APIについてはこちらをご参照ください。実際の動きは次のようになります。
【デモ】
LINE botって、画像を返してくれる機能もあるのですね!ということで、問いかけると柴犬の画像を返してくれるLINE botを作ってみました?#protoout pic.twitter.com/iHNFAQvcfR
— まえぷー@出窓菜園 BWG (@kmaepu) April 4, 2020構成
主な構成は次の通りです。LINE botからテキストを送ると、ローカルにあるNode.jsサーバにWebhookが投げられ、LINE Messaging APIのフォーマットに従って画像URlが返されます。
作ってみる
開発環境
OS:Windows 10
Node.js:v10.15.3【ライブラリバージョン】
@line/bot-sdk:6.8.4
express:4.17.1
axios:0.19.2プログラム解説
プログラムの全体は最後に載せるとして、ここでは重要なところのみ解説します。
LINE botのサーバで、画像を返すフォーマットは次の通りです。return client.replyMessage(event.replyToken, { type: 'image', originalContentUrl: 'オリジナルサイズの画像URL', previewImageUrl: 'LINEアプリのトーク画面にプレビューされるサイズの画像URL' });画像URLを指定するため、GoogleドライブやDropboxなどのストレージを使う必要があります。
また、使用できる画像サイズに限度があるようです。詳しくはこちらのLINE Developerサイトを参照してください。今回の例では、次のように柴犬APiを利用して得られた画像URLを、固定で入力しています。
return client.replyMessage(event.replyToken, { type: 'image', originalContentUrl: 'https://cdn.shibe.online/shibes/907fed97467e36f3075211872d98f407398126c4.jpg', previewImageUrl: 'https://cdn.shibe.online/shibes/907fed97467e36f3075211872d98f407398126c4.jpg' });余談
ここまで至るにも苦労しました(;'∀')
なぜか画像が送れないなーと色々悩んでいて、最終的にはDiscordのProtoOutStudioの技術質問チャンネルに投げかけました。嬉しいことに2期生の方が動作確認して誤り個所を教えていただきました。本当に感謝しています。
結局は返すフォーマットが間違っていただけなんて....。次からはもっとちゃんと仕様を読んで理解せねば!と、いうことで画像を送ることができました。次は自宅で稼働している菜園管理システムとつなげて、画像を一定時間ごとに送れるようにしていこうと思います。
参考
LINE Messaging API でできることまとめ【送信編】
ソースコード
'use strict'; const axios = require('axios'); // 追加 const express = require('express'); const line = require('@line/bot-sdk'); const PORT = process.env.PORT || 3000; const config = { channelSecret: 'LINE botのチャンネルシークレット', channelAccessToken: 'LINE botのアクセストークン' }; const app = express(); app.get('/', (req, res) => res.send('Hello LINE BOT!(GET)')); //ブラウザ確認用(無くても問題ない) app.post('/webhook', line.middleware(config), (req, res) => { console.log(req.body.events); //ここのif文はdeveloper consoleの"接続確認"用なので後で削除して問題ないです。 if(req.body.events[0].replyToken === '00000000000000000000000000000000' && req.body.events[1].replyToken === 'ffffffffffffffffffffffffffffffff'){ res.send('Hello LINE BOT!(POST)'); console.log('疎通確認用'); return; } Promise .all(req.body.events.map(handleEvent)) .then((result) => res.json(result)); }); const client = new line.Client(config); function handleEvent(event) { if (event.type !== 'message' || event.message.type !== 'text') { return Promise.resolve(null); } return client.replyMessage(event.replyToken, { type: 'image', originalContentUrl: 'https://cdn.shibe.online/shibes/907fed97467e36f3075211872d98f407398126c4.jpg', previewImageUrl: 'https://cdn.shibe.online/shibes/907fed97467e36f3075211872d98f407398126c4.jpg' }); } app.listen(PORT); console.log(`Server running at ${PORT}`);
- 投稿日:2020-04-04T20:34:20+09:00
LINE botから画像送信~問いかけると柴犬の画像を返してくれるLINE botを作ってみた
概要
LINE botでできることを調べていたら、画像を返すことが可能とのことで試してみました。
ただ画像を返すだけでは面白くないので、柴犬APIで画像を拾ってきて表示することにしました。最後にソースコードの全体を載せています。柴犬APIについてはこちらをご参照ください。実際の動きは次のようになります。
【デモ】
LINE botって、画像を返してくれる機能もあるのですね!ということで、問いかけると柴犬の画像を返してくれるLINE botを作ってみました?#protoout pic.twitter.com/iHNFAQvcfR
— まえぷー@出窓菜園 BWG (@kmaepu) April 4, 2020構成
主な構成は次の通りです。LINE botからテキストを送ると、ローカルにあるNode.jsサーバにWebhookが投げられ、LINE Messaging APIのフォーマットに従って画像URlが返されます。
作ってみる
開発環境
OS:Windows 10
Node.js:v10.15.3【ライブラリバージョン】
@line/bot-sdk:6.8.4
express:4.17.1
axios:0.19.2プログラム解説
プログラムの全体は最後に載せるとして、ここでは重要なところのみ解説します。
LINE botのサーバで、画像を返すフォーマットは次の通りです。return client.replyMessage(event.replyToken, { type: 'image', originalContentUrl: 'オリジナルサイズの画像URL', previewImageUrl: 'LINEアプリのトーク画面にプレビューされるサイズの画像URL' });画像URLを指定するため、GoogleドライブやDropboxなどのストレージを使う必要があります。
また、使用できる画像サイズに限度があるようです。詳しくはこちらのLINE Developerサイトを参照してください。今回の例では、次のように柴犬APiを利用して得られた画像URLを、固定で入力しています。
return client.replyMessage(event.replyToken, { type: 'image', originalContentUrl: 'https://cdn.shibe.online/shibes/907fed97467e36f3075211872d98f407398126c4.jpg', previewImageUrl: 'https://cdn.shibe.online/shibes/907fed97467e36f3075211872d98f407398126c4.jpg' });余談
ここまで至るにも苦労しました(;'∀')
なぜか画像が送れないなーと色々悩んでいて、最終的にはDiscordのProtoOutStudioの技術質問チャンネルに投げかけました。嬉しいことに2期生の方が動作確認して誤り個所を教えていただきました。本当に感謝しています。
結局は返すフォーマットが間違っていただけなんて....。次からはもっとちゃんと仕様を読んで理解せねば!と、いうことで画像を送ることができました。次は自宅で稼働している菜園管理システムとつなげて、画像を一定時間ごとに送れるようにしていこうと思います。
参考
LINE Messaging API でできることまとめ【送信編】
ソースコード
'use strict'; const axios = require('axios'); // 追加 const express = require('express'); const line = require('@line/bot-sdk'); const PORT = process.env.PORT || 3000; const config = { channelSecret: 'LINE botのチャンネルシークレット', channelAccessToken: 'LINE botのアクセストークン' }; const app = express(); app.get('/', (req, res) => res.send('Hello LINE BOT!(GET)')); //ブラウザ確認用(無くても問題ない) app.post('/webhook', line.middleware(config), (req, res) => { console.log(req.body.events); //ここのif文はdeveloper consoleの"接続確認"用なので後で削除して問題ないです。 if(req.body.events[0].replyToken === '00000000000000000000000000000000' && req.body.events[1].replyToken === 'ffffffffffffffffffffffffffffffff'){ res.send('Hello LINE BOT!(POST)'); console.log('疎通確認用'); return; } Promise .all(req.body.events.map(handleEvent)) .then((result) => res.json(result)); }); const client = new line.Client(config); function handleEvent(event) { if (event.type !== 'message' || event.message.type !== 'text') { return Promise.resolve(null); } return client.replyMessage(event.replyToken, { type: 'image', originalContentUrl: 'https://cdn.shibe.online/shibes/907fed97467e36f3075211872d98f407398126c4.jpg', previewImageUrl: 'https://cdn.shibe.online/shibes/907fed97467e36f3075211872d98f407398126c4.jpg' }); } app.listen(PORT); console.log(`Server running at ${PORT}`);
- 投稿日:2020-04-04T19:58:56+09:00
TypeScriptで学ぶデザインパターン〜Template Method編〜
対象読者
- デザインパターンを学習あるいは復習したい方
- TypeScriptが既に読めるあるいは気合いで読める方
- いずれかのオブジェクト指向言語を知っている方は気合いで読めると思います
- UMLが既に読めるあるいは気合いで読める方
環境
- OS: macOS Mojave
- Node.js: v12.7.0
- npm: 6.14.3
- TypeScript: Version 3.8.3
本シリーズ記事一覧(随時更新)
- TypeScriptで学ぶデザインパターン〜Iterator編〜
- TypeScriptで学ぶデザインパターン〜Adapter編〜
- TypeScriptで学ぶデザインパターン〜Template Method編〜
Template Methodパターンとは
処理の大枠を持たせるためのパターンです。
"処理の大枠"というのがテンプレートです。処理の流れをざっくり規定して、その流れに沿って具体的な処理を埋め込んでいくことができます。
サンプルコード
Template Methodパターンで作られたクラス群がどんなものになるのか確認していきましょう。
今回は、題材として"エディターのヘルプ"を想定します。GitHubにも公開しています。
「Markdownで太字にする場合は
**
か__
で文字列で囲う」といったヘルプを表示させるような機能を作っていきます。modules/EditorExample.ts
エディターのヘルプを表示させるためのテンプレートを示す抽象クラスです。
EditorExample.tsexport default abstract class EditorExample { type: string; text: string; abstract strong(): string; abstract italic(): string; showExample(): void { console.log('種別が' + this.type + 'の場合の記述例は以下です。'); console.log('太字: ' + this.strong()); console.log('斜体: ' + this.italic()); } }
strong
メソッドとitalic
メソッドは本クラスのサブクラスで実装します。
showExample
メソッドがテンプレートで、大まかな処理の流れを規定しています。modules/HtmlEditorExample.ts
テンプレートの実装クラスです。
HtmlEditorExample.tsimport EditorExample from './EditorExample'; export default class HtmlEditorExample extends EditorExample { type: string = 'HTML'; text: string; constructor(text: string) { super(); this.text = text; } strong(): string { const strongExample: string = '<strong>' + this.text + '</strong>'; return strongExample; } italic(): string { const italicExample: string = '<i>' + this.text + '</i>'; return italicExample; } }
strong
メソッドとitalic
メソッドを実装していることを確認してください。modules/MarkdownEditorExample.ts
テンプレートの実装クラスです。
HtmlEditorExample
クラスと立ち位置は同じです。MarkdownEditorExample.tsimport EditorExample from "./EditorExample"; export default class MarkdownEditorExample extends EditorExample { type: string = 'Markdown'; text: string; constructor(text: string) { super(); this.text = text; } strong(): string { const strongExample: string = this.getStrongExample(); return strongExample; } italic(): string { const italicExample: string = this.getItalicExample(); return italicExample; } private getStrongExample(): string { const exampleNotations: string[] = [ '**', '__' ]; let strongExample: string = ''; for (let exampleNotation of exampleNotations) { strongExample += exampleNotation + this.text + exampleNotation; if (exampleNotation !== exampleNotations[exampleNotations.length - 1]) { strongExample += ', '; } } return strongExample; } private getItalicExample(): string { const exampleNotations: string[] = [ '*', '_' ]; let italicExample: string = ''; for (let exampleNotation of exampleNotations) { italicExample += exampleNotation + this.text + exampleNotation; if (exampleNotation !== exampleNotations[exampleNotations.length - 1]) { italicExample += ', '; } } return italicExample; } }
strong
メソッドとitalic
メソッドを実装していることを確認してください。Main.ts
Template Methodパターンで作られたクラス群を実際に使う処理が書かれています。
Main.tsimport EditorExample from './modules/EditorExample'; import HtmlEditorExample from './modules/HtmlEditorExample'; import MarkdownEditorExample from './modules/MarkdownEditorExample'; const htmlEditorExample: EditorExample = new HtmlEditorExample('こんにちは!'); htmlEditorExample.showExample(); const markdownEditorExample: EditorExample = new MarkdownEditorExample('こんにちは!'); markdownEditorExample.showExample();
showExample
メソッドでテンプレートを実行していることを確認してください。クラス図
ここまでTemplate Methodパターンで作られたクラス群を1つずつ確認してきました。次にクラス図を示します。Template Methodパターンの全体像を整理するのにお役立てください。
解説
最後に、このデザインパターンの存在意義を考えます。
EditorExample
抽象クラスが存在しない場合を考えてみます。つまり、showExample
メソッドのようなメソッドをHtmlEditorExample
クラスとMarkdownEditorExample
クラスそれぞれに定義している場合を考えてみます。もし、showExample
メソッドのようなメソッドを修正する必要があったらHtmlEditorExample
クラスとMarkdownEditorExample
クラス両方を修正する必要が生じてしまいます。何が言いたいかというと、EditorExample
抽象クラスにshowExample
メソッドを定義することでロジックを共通化することができるのです。補足
サンプルコードの実行方法はこちらと同様です。
参考
あとがたり
EditorExample
抽象クラスのshowExample
メソッドにJavaでいうところのfinal
を付与させたかったけど今のところやり方がわからず...。できないわけないと思うんだけどな。
- 投稿日:2020-04-04T18:10:53+09:00
LINE BOTで天気を返すサンプルがngrokで動いてnowで動かない件
こちらのサンプルがngrokで動いて、Nowだとうまく動かない件の対応。
axiosを使って別のサーバーにリクエストを出してるので非同期処理のあたりが怪しいですね。
もとのコード
これだとngrokでうまく動くけど、now上でうまく動かないというレポート
server.js・ ・ 省略 ・ ・ function handleEvent(event) { if (event.type !== 'message' || event.message.type !== 'text') { return Promise.resolve(null); } let mes = '' if(event.message.text === '天気教えて!'){ mes = 'ちょっとまってね'; //待ってねってメッセージだけ先に処理 getNodeVer(event.source.userId); //スクレイピング処理が終わったらプッシュメッセージ }else{ mes = event.message.text; } return client.replyMessage(event.replyToken, { type: 'text', text: mes }); } const getNodeVer = async (userId) => { const res = await axios.get('http://weather.livedoor.com/forecast/webservice/json/v1?city=400040'); const item = res.data; await client.pushMessage(userId, { type: 'text', text: item.description.text, }); } ・ ・ 省略 ・ ・書き換え
replyとpushのタイミングを変えてみる。
server.js・ ・ 省略 ・ ・ async function handleEvent(event) { if (event.type !== 'message' || event.message.type !== 'text') { return Promise.resolve(null); } //"天気教えて"以外の場合は反応しない if(event.message.text !== '天気教えて') { return client.replyMessage(event.replyToken, { type: 'text', text: '"天気教えて"と言ってね' }); } let mes = ''; mes = 'ちょっと待ってね'; //"ちょっと待ってね"ってメッセージだけ先に処理 await client.replyMessage(event.replyToken, { type: 'text', text: mes }); //axiosを使って天気APIにアクセス const CITY_ID = `400040`; //ライドアのAPIから取得したいシティのIDを const URL = `http://weather.livedoor.com/forecast/webservice/json/v1?city=${CITY_ID}`; const res = await axios.get(URL); const item = res.data; return client.pushMessage(event.source.userId, { type: 'text', text: item.description.text, }); } ・ ・ 省略 ・ ・おまけ: 関数分けサンプル
関数に分けるとこんな感じ。
server.js・ ・ 省略 ・ ・ async function handleEvent(event) { if (event.type !== 'message' || event.message.type !== 'text') { return Promise.resolve(null); } //"天気教えて"以外の場合は反応しない if(event.message.text !== '天気教えて') { return client.replyMessage(event.replyToken, { type: 'text', text: '"天気教えて"と言ってね' }); } let mes = ''; mes = 'ちょっと待ってね'; //"ちょっと待ってね"ってメッセージだけ先に処理 await client.replyMessage(event.replyToken, { type: 'text', text: mes }); const CITY_ID = `400040`; //ライドアのAPIから取得したいシティのIDを return getWeather(event.source.userId, CITY_ID); } const getWeather = async (userId, CITY_ID) => { //axiosを使って天気APIにアクセス const URL = `http://weather.livedoor.com/forecast/webservice/json/v1?city=${CITY_ID}`; const res = await axios.get(URL); const item = res.data; return client.pushMessage(userId, { type: 'text', text: item.description.text, }); } ・ ・ 省略 ・ ・
- 投稿日:2020-04-04T15:21:36+09:00
CKeditor4と画像アップロード(CKfinder使用しない)
行ったこと
- Cakephp にCKeditor4を適用
- 画像アップロードの処理をコントローラに書く
CKeditor適用
View(ctp)
ファイルcdn 経由で呼び出し
※Fullバージョンがおすすめです。<?= $this->Html->script('https://cdn.ckeditor.com/4.14.0/full/ckeditor.js') ?>※コントローラ指定
var url = '<?= $this->Url->build(['controller' => 'ImgUpload', 'action' => 'index']) ?>';var editor = CKEDITOR.replace('editor', { language: 'ja', toolbarCanCollapse: true, filebrowserUploadMethod: 'form', filebrowserUploadUrl: url, /* 上で指定したurl(変数) */ image_previewText: '画像アップロード', };ツールバーのオプションなどは今回省略します。
画像アップロード処理
上で指定した
ImgUploadController
の内容namespace App\Controller\Admin; use App\Controller\AppController; class ImgUploadController extends AppController { public function index() { if (isset($_FILES['upload']['name'])) { $uploaddir = WWW_ROOT; $file = $_FILES['upload']['tmp_name']; $file_name = $_FILES['upload']['name']; $file_name_array = explode(".", $file_name); $extension = end($file_name_array); $new_image_name = rand() . '.' . $extension; chmod($uploaddir.'/uploads', 0777); $allowed_extension = array("jpg", "gif", "png"); if (in_array($extension, $allowed_extension)) { move_uploaded_file($file, $uploaddir.'/uploads/'.$new_image_name); $function_number = $_GET['CKEditorFuncNum']; $url = '/uploads/'.$new_image_name; $message = ''; echo "<script type='text/javascript'>window.parent.CKEDITOR.tools.callFunction($function_number, '$url', '$message');</script>"; return; } } } }webroot ∟ uploads ∟ ここに画像ファイルが保存される参照サイト: https://www.webslesson.info/2019/01/uploading-image-in-ckeditor-with-php.html
- 投稿日:2020-04-04T15:04:27+09:00
[JavaScript] Arrayメソッド破壊・非破壊チートシート
JSのArrayメソッドで破壊非破壊がまとまっている表が欲しいな〜〜
と思ったので作りました。チートシート
メソッド名 破壊 非破壊 用途 splice() ○ 配列から配列の一部を取り出し、新しい配列を返す slice() ○ 配列から配列の一部を取り出し、新しい配列を返す push() ○ 配列の末尾に要素を追加し、新たな配列の長さを返す unshift() ○ 配列の先頭に要素を追加し、新たな配列の長さを返す pop() ○ 配列の最後の要素を取り除き、その値を返す shift() ○ 配列の先頭の要素を取り除き、その値を返す filter() ○ 引数として与えられた関数を各配列要素に対して実行し、それに合格したすべての配列要素からなる新しい配列を返す reduce() ○ 引数として与えられた関数を(左から右へ)各配列要素に対して実行し、単一の値にして返す reduceRight() ○ 引数として与えられた関数を(右から左へ)各配列要素に対して実行し、単一の値にして返す map() ○ 配列内の各要素を引数として与えられた関数で順番に加工し、新しい配列を返す concat() ○ 配列に他の配列や値をつないでできた新しい配列を返す find() ○ 引数として与えられた関数を満たす配列内の最初の要素の値を返す findIndex() ○ 引数として与えられた関数を満たす場合、配列内の インデックス を返す indexOf() ○ 引数に与えられた値と同じ値を持つ配列要素の内、最初のものの添字を返す includes() ○ 引数の値が配列に含まれているかどうかを true または false で返す sort() ○ 配列の要素をソートする reverse() ○ 配列の要素を反転させ、配列を書き換える join() ○ 配列の全要素を順に引数の文字列で連結した文字列を新たに作成して返す。区切り文字や指定されない場合、デフォルトは,になる fill() ○ 配列中の開始インデックスから終了インデックスまで固定値に変更する copyWithin() ○ サイズを変更せずに、配列の一部を同じ配列内の別の場所にシャローコピーして返す 最後に
関数自体を詳しく説明はしていないので、必要があればMDN web docsをご参考ください。
今後も追加していきますが一旦。
- 投稿日:2020-04-04T13:56:34+09:00
nowのデプロイで古い書き方からのマイグレートメモ
now.shを使うときにv2系の書き方でも警告が出るようになってますね。
もう1系は使えないのかも。nowで詰まった人がいたのでメモ。
今まで書いてたやり方と修正点
これまでは、これでよかったのですが......
now.json{ "version": 2, "name": "mylinebot", "builds": [{ "src": "server.js", "use": "@now/node" }], "routes": [ { "src": "/", "dest": "server.js" }, { "src": "/webhook", "dest": "server.js" } ] }now --target production
nameプロパティの注意
まずはここ。
❗️ The `name` property in now.json is deprecated (https://zeit.ink/5F)ここを読むと、書いてますが
NOTE: The name property has been deprecated in favor of Project Linking, which allows you to link a ZEIT Now Project to your local codebase when you run now.
nameプロパティが非推奨と言われます。
なのでnameプロパティを外します。修正版はこちら。
now.json{ "version": 2, "builds": [{ "src": "server.js", "use": "@now/node" }], "routes": [ { "src": "/", "dest": "server.js" }, { "src": "/webhook", "dest": "server.js" } ] }デプロイコマンド
次にここです。
WARN! We recommend using the much shorter `--prod` option instead of `--target production` (deprecated)もともとの書き方の
now --target production
がnow --prod
で良いよと言われます。短い方が良いですね。now --prod
これでOKです。
おまけ: 実行時
実際のデプロイで表示されるコンソールの紹介です。
対話的に質問されます。"deployの設定をしますか?"的な質問です。エンターかYをタイプして進みましょう。
Now CLI 17.1.1 ? Set up and deploy “~/Documents/ds/playground/mylinebot”? [Y/n] ←ここでエンターもしくはY次にデプロイ先のアカウントを選択。 たぶんチームアカウントとかあると選択肢に載ってくるんだと思いますが、たぶん最初は自分のアカウントだけなので自分のアカウントが表示されるのを確認したらエンター。
? Which scope do you want to deploy to? ● n0bisuke ←ここでエンター次になんて名前でデプロイするか聞かれます。package.jsonのnameプロパティに書いてある名前が表示されるので、エンターかYをタイプして進みます。
? Found project “n0bisuke/mylinebot”. Link to it? [Y/n] ←ここでエンターもしくはYこれでデプロイできるはず......!
- 投稿日:2020-04-04T12:53:10+09:00
【Rails】Ajaxを用いた非同期いいね機能の実装
目標
開発環境
・Ruby: 2.5.7
・Rails: 5.2.4
・Vagrant: 2.2.7
・VirtualBox: 6.1
・OS: macOS Catalina前提
ログイン機能、投稿機能を実装済み。
いいね機能の実装
テーブル
usersテーブル
カラム データ型 name string introduction text profile_image_id string booksテーブル
カラム データ型 title string body text book_id integer user_id integer favoritesテーブル
カラム データ型 user_id integer book_id integer モデル
user.rbhas_many :favorites, dependent: :destroy def favorited_by?(book) favorites.where(book_id: book.id).exists? endbook.rbhas_many :favorites, dependent: :destroyfavorite.rbbelongs_to :user belongs_to :bookルーティング
routes.rbresources :books do resource :favorites, only: [:create, :destroy] endコントローラー
favorites.controll.rbclass FavoritesController < ApplicationController def create @book = Book.find(params[:book_id]) # いいねボタンを連打しても1回しかいいね出来ない様に条件をつける unless current_user.favorited_by?(@book) favorite = current_user.favorites.new(book_id: @book.id) favorite.save redirect_to @book end end def destroy @book = Book.find(params[:book_id]) favorite = current_user.favorites.find_by(book_id: @book.id) favorite.destroy redirect_to @book end endビュー
1.
投稿一覧
をパーシャル化books/index.html.erb<% @books.each do |book| %> <tr> <%= render 'books', book: book %> </tr> <% end %>3. showページ等でもいいねボタンを使い回したいので、さらに
いいねボタン
をパーシャル化books/_books.html.erb<td> <%= render 'favoritebutton', book: book %> </td>4.
いいねボタン
を作成books/_favoritebutton.html.erb# current_userがその投稿をいいねしているかによって表示を変えている <% if book.favorited_by?(current_user) %> <%= link_to book_favorites_path(book), method: :delete, remote: true do %> <i class="fa fa-heart" aria-hidden="true" style="color: red;"></i> <%= book.favorites.count %> <% end %> <% else %> <%= link_to book_favorites_path(book), method: :post, remote: true do %> <i class="fa fa-heart-o" aria-hidden="true"></i> <%= book.favorites.count %> <% end %> <% end %>非同期機能の実装
1. jQueryの導入
Gemfilegem 'jquery-rails'ターミナル$ bundleapplication.js//= require rails-ujs //= require activestorage //= require turbolinks //= require jquery //= require_tree .2. いいね後のジャンプ先を削除
favorites.controll.rbclass FavoritesController < ApplicationController def create @book = Book.find(params[:book_id]) unless current_user.favorited_by?(@book) favorite = current_user.favorites.new(book_id: @book.id) favorite.save end end def destroy @book = Book.find(params[:book_id]) favorite = current_user.favorites.find_by(book_id: @book.id) favorite.destroy end end3. パーシャルの親要素にクラスをつける
books/_books.html.erb#eachで呼び出されている各投稿のいいねボタンに対して、それぞれ一意のクラスを付けている <td class="favoritebutton_<%= book.id %>"> <%= render 'favoritebutton', book: book %> </td>4. JavaScriptファイルの作成
favorites/create.js.erb$(".favoritebutton_<%= @book.id %>").html("<%= j(render 'books/favoritebutton', book: @book ) %>");favorites/destroy.js.erb$(".favoritebutton_<%= @book.id %>").html("<%= j(render 'books/favoritebutton', book: @book ) %>");
$(".favoritebutton_<%= @book.id %>")
➡︎「3」で付けたクラスを指定
.html("<%= j(render 'books/favoritebutton', book: @book ) %>");
➡︎いいねボタンのパーシャルをrenderしている参考サイト
- 投稿日:2020-04-04T12:29:27+09:00
JavascriptでCSS (Sass) プロパティを利用する
この記事は https://css-tricks.com/getting-javascript-to-talk-to-css-and-sass/ の翻訳記事となります。
(2020/4/3 寄稿記事)JavaScriptとCSSは20年以上にわたって依存し合っているわけですが、いまだ相互のデータのやりとりには大変な苦労があります。
もちろん、これまでも数多くのアプローチが紹介されていますが、ここではシンプルかつ直感的なアプローチを紹介したいと思います。
ここで言うシンプルかつ直感的と言うのは、構造を変えての試みということではなく、CSSカスタムプロパティやSass変数の利用について述べることにします。CSSカスタムプロパティとJavaScript
カスタムプロパティというものはそうたいそうなものではありません。ブラウザーのサポートが開始されてから、JavaScriptからその値を操作することが可能になりました。
カスタムプロパティをJavaScriptで利用する方法にはいくつかありますが、
setProperty
を利用する方法が挙げられます。document.documentElement.style.setProperty("--padding", 124 + "px"); // 124pxCSS変数を抽出するには、
getComputedStyle
を使います。このロジックの背景はいたってシンプルです。カスタムプロパティはスタイルの一部ですので、算出スタイル (computed style) の一部でもあると言えるのです。getComputedStyle(document.documentElement).getPropertyValue('--padding') // 124px同様に
getPropertyValue
がありますが、これによりHTML上のインラインスタイルの値を抽出することができます。document.documentElement.style.getPropertyValue("--padding'"); // 124pxカスタムプロパティはスコープ定義となるので、特定の要素から算出スタイル (computed style) を取得する必要があります。
ここでは、前述のsetProperty
を使って :root に変数を定義しているので値を得ることができています。Sass変数をJavaScriptで利用する
Sassは事前処理(pre-processing)言語ですので、ウェブサイトの一部として機能するには事前にCSSへの変換が行われます。
そのため、CSSカスタムプロパティと同じようにJavaScriptからアクセスすることはできません。CSSカスタムプロパティは算出スタイル (computed style) としてDOM上でアクセスが可能だからです。これを変えるためにbuildプロセスを変更する必要があります。とはいえ、多くの場合loaderがbuildプロセスをになってくれるのでそう大きな変更は必要ないと思います。しかし、そうではないプロジェクトの場合は、Sassモジュールのインポート (import) と翻訳 (tranlating) を行う3つのモジュールを利用して以下のように webpack の設定を行います。
module.exports = { // ... module: { rules: [ { test: /\.scss$/, use: ["style-loader", "css-loader", "sass-loader"] }, // ... ] } };To make Sass (or, specifically, SCSS in this case) variables available to JavaScript, we need to “export” them.
Sass変数(今回は SCSS)をJavaScriptで利用するには:export
する必要があります。// variables.scss $primary-color: #fe4e5e; $background-color: #fefefe; $padding: 124px; :export { primaryColor: $primary-color; backgroundColor: $background-color; padding: $padding; }この
:export
はwebpackが変数をimportするための魔法です。何が良いかというと、変数名を変えることができる (camelCase) ことと、利用する変数を選別できることです。次にこのSassファイル (variables.scss) をJavaScriptでimportします。これで、変数へのアクセスが可能となります。
import variables from './variables.scss'; /* { primaryColor: "#fe4e5e" backgroundColor: "#fefefe" padding: "124px" } */ document.getElementById("app").style.padding = variables.padding;ただ、この
:export
にはいくつかの制約があるので述べておいた方が良いと思います。It must be at the top level but can be anywhere in the file.
- Topレベル階層に位置する必要があるが、ファイル内のどこで定義しても良い
- 同一ファイル内、複数箇所で定義されている場合、キー (key) と値 (value) は結合され一緒にexportされる
- キーの重複がある場合、後続のキーの値が優先される
- 値にはCSSで有効な任意の文字が利用できる(スペースも可)
- 値には引用符('
"
)は不要(リテラル文字列として扱われるため)There are lots of ways having access to Sass variables in JavaScript can come in handy. I tend to reach for this approach for sharing breakpoints. Here is my breakpoints.scs file, which I later import in JavaScript so I can use the matchMedia() method to have consistent breakpoints.
JavaScriptからSass変数にアクセスすることが有用であるケースは多くありますが、筆者の場合、breakpoint を共有するために利用することがよくあります。
以下のbreakpoints.scss
をJavaScriptでimportし、matchMedia()
で一貫した breakpoint の利用を可能としています。// Sass variables that define breakpoint values $breakpoints: ( mobile: 375px, tablet: 768px, // etc. ); // Sass variables for writing out media queries $media: ( mobile: '(max-width: #{map-get($breakpoints, mobile)})', tablet: '(max-width: #{map-get($breakpoints, tablet)})', // etc. ); // The export module that makes Sass variables accessible in JavaScript :export { breakpointMobile: unquote(map-get($media, mobile)); breakpointTablet: unquote(map-get($media, tablet)); // etc. }もう一つのアプローチとしてアニメーションがあります。 アニメーションで利用する
duration
は通常CSSSで保持されますが、複雑なアニメーションになるとJavaScriptが必要となります。// animation.scss $global-animation-duration: 300ms; $global-animation-easing: ease-in-out; :export { animationDuration: strip-unit($global-animation-duration); animationEasing: $global-animation-easing; }変数 export に
strip-unit()
を利用していますが、これはJavaScript側でのparse処理を容易にするためです。
CSS、SassとJavaScriptでのデータのやりとりが簡単にできることは嬉しいことで、このような変数の共有はコードをシンプルかつ無駄のないもの(原文にはDRYとあります)にしてくれます。
もちろん、他にもこれを実現するアプローチはあります。Les James氏は2017に興味深いアプローチを紹介しています。彼が紹介しているのは、JSONを利用したデータの共有です。ただ、偏った見方になるかもしれませんが、この記事で紹介したアプローチが最もシンプルで直感的であると思います。すでに運用しているCSSやJavaScriptへの面倒な変更は不要です。
他にもアプローチがあれば、ぜひコメントください。どう解決しているのかを知りたいです!
- 投稿日:2020-04-04T12:14:01+09:00
東京都におけるコロナウイルス感染者数推移をグラフ化、都道府県毎の累計感染者数を表示してみた。
閲覧ありがとうございます。
やったこと
コロナウイルスの感染者情報が載っているJsonファイルにアクセスし、東京都の感染者の推移をグラフ化、都道府県毎の累計感染者数を表示してみました。
- JavaScriptのfetchメソッドを使ってネット上のJsonファイルにアクセス
- 取得したJsonファイルを修正
- グラフの表示
- 日本地図の表示
都道府県をクリックすると、感染者数が出るようになっております。使ったJsonファイル
使った言語
JavaScript
JQuery詰まったこと
fetchメソッドで取得した値がchart.jsに上手く反映されない
fetchメソッドで欲しい値は取得したのですが、最初の画面ロード時にchart.jsに反映されませんでした。
なぜか、オプション+コマンド+iを押して検証モードにすると、グラフが上手く表示されるというエラーに出会いました、、、
どうやらfetchメソッドの中にchart.jsでグラフ表示をする処理を書かなければならなかったようです。
How To Make A Chart Using Fetch & REST API's不正解
// fetchでJsonファイル取得 fetch(urlNation) .then(function(response) { return response.json(); }) .then(function(myJson) { for(var i = 0; i < myJson.patients_summary.data.length; i++){ total += myJson.patients_summary.data[i].小計; let numPerDay = myJson.patients_summary.data[i].日付.substr(0,10); inDate.push(numPerDay); inNumber.push(total); console.log(myJson.patients_summary.data[i].日付); } return false; }); // chart.jsでグラフ表示 var myBarChart = new Chart(ctx, { type: 'bar', data: { labels: inDate, datasets: [ { label: 'Infexted Patients', data: inNumber, backgroundColor: "rgba(21,255,0,0.8)" } ] }, options: { title: { display: true, text: 'Coronavirus Cases' }, scales: { yAxes: [{ ticks: { suggestedMax: 1500, suggestedMin: 0, stepSize: 100, callback: function(value, index, values){ return value } } }] }, } });正解
fetch(urlNation) .then(function(response) { return response.json(); }) .then(function(myJson) { for(var i = 0; i < myJson.patients_summary.data.length; i++){ total += myJson.patients_summary.data[i].小計; let numPerDay = myJson.patients_summary.data[i].日付.substr(0,10); inDate.push(numPerDay); inNumber.push(total); console.log(myJson.patients_summary.data[i].日付); } var myBarChart = new Chart(ctx, { type: 'bar', data: { labels: inDate, datasets: [ { label: 'Infexted Patients', data: inNumber, backgroundColor: "rgba(21,255,0,0.8)" } ] }, options: { title: { display: true, text: 'Coronavirus Cases' }, scales: { yAxes: [{ ticks: { suggestedMax: 1500, suggestedMin: 0, stepSize: 100, callback: function(value, index, values){ return value } } }] }, } }); return false; });まとめ
実際に自分で作ってみて、感染者が指数関数的に増えていて、驚いています。
手洗いうがい、十分な睡眠と栄養摂取をして、対策をしましょう!
- 投稿日:2020-04-04T11:32:34+09:00
JavaScript の非同期処理とシングルスレッド
本記事の目的
JavaScript や Node.js はよくシングルスレッドだ〜、と言われますが、では非同期処理はどうやって実行されているのか (Non-Blocking I/O) をざっくりと (私の身内に) 説明する為のサンプルコードです。
検証環境
- iMac (Retina 5K, 27-inch Late 2014), 4 GHz Intel Core i7
- Node.js v12.13.0
$ nodebrew install-binary v12.13.0 $ nodebrew use v12.13.0ブラウザ JavaScript の Event loop はまたちょっと違います。
早速サンプルコードから
以下の様な JavaScript
index.js
を、Node.js で実行します。
- 【処理 1】ミリ秒で終わる処理を
setTimeout()
で 5 秒後に発火.- 【処理 2】ミリ秒で終わる処理を
setTimeout()
で 0 秒後に発火.- 【処理 3】10 秒かかる同期処理を実行.
- 時間の計測には Node.js 標準 API の perf_hooks モジュールを使用しています。 Node.js プロセス実行開始からのミリ秒を得られます
- コード中では、ミリ秒 → 秒、に変換して表示しています
index.jsconst { performance } = require('perf_hooks'); /** * @return 本スクリプトを実行してからの経過秒数. */ const seconds = () => performance.now() / 1000; const secondsPadded = () => seconds().toFixed(6).padStart(10, ' '); // 長さ揃える. //////////////// 処理3つ //////////////// /** * 処理 1 (非同期, 5 秒後に発火). */ const func1 = () => { console.log(`${secondsPadded()} seconds --> 処理 1 (非同期, 5 秒後に発火)`); }; /** * 処理 2 (非同期, 0 秒後に発火). */ const func2 = () => { console.log(`${secondsPadded()} seconds --> 処理 2 (非同期, 0 秒後に発火)`); }; /** * 処理 3 (同期. 10 秒かかる). */ const func3 = () => { while (seconds() < 10) { /* consuming a single cpu for 10 seconds... */ } console.log(`${secondsPadded()} seconds --> 処理 3 (同期, 10 秒かかる)`); }; //////////////// 計測開始 //////////////// console.log(`${secondsPadded()} seconds --> index.js START`); // [非同期] 5 秒後に実行. setTimeout(func1, 5000); // [非同期] 即時実行. setTimeout(func2); // 同期実行. func3(); console.log(`${secondsPadded()} seconds --> index.js END`); //////////////// 計測終了 ////////////////期待値?
なんとなく 「こう動作するだろう...」 という気分になるのは ↓ でしょう。
$ node index.js 0.000000 seconds --> index.js START 0.000000 seconds --> 処理 2 (非同期, 0 秒後に発火) 5.000000 seconds --> 処理 1 (非同期, 5 秒後に発火) 10.000000 seconds --> 処理 3 (同期, 10 秒かかる) 10.000000 seconds --> index.js END実際は...
現実はこうです。何故でしょうか。
$ node index.js 0.175104 seconds --> index.js START 10.000085 seconds --> 処理 3 (同期, 10 秒かかる) 10.000210 seconds --> index.js END 10.000955 seconds --> 処理 2 (非同期, 0 秒後に発火) 10.001161 seconds --> 処理 1 (非同期, 5 秒後に発火)シングルスレッドだから、順番に処理している
おおよそ、Node.js の内部では ↓ のように処理がシングルスレッドで行われています。
- JavaScript コンテキストの生成時にイベントループが生成されます
- 最初のエントリ JavaScript
index.js
がタスクとして、未実行キューに乗ります- イベントループ
- 未実行キューから
index.js
タスクが取り出され、実行が開始されます
setTimeout(処理1, 5秒)
が実行され、【処理 1】がタイマーキューに追加されますsetTimeout(処理2, 0秒)
が実行され、【処理 2】がタイマーキューに追加されます- 【処理 3】が同期的に実行され、10 秒間、CPU (シングルコア) を専有します
index.js
タスクの実行が終了します- イベントループ
- タイマーキューから 有効期限が切れたタスク【処理 2】 を取り出し、実行が開始されます
- 【処理 2】タスクの実行が終了します
- イベントループ
- タイマーキューから 有効期限が切れたタスク【処理 1】 を取り出し、実行が開始されます
- 【処理 1】タスクの実行が終了します
実際はタイマー Phase はキューではない (FIFO でもない) ですが、説明の都合上そう表記しました。
要はイベントループにて、実行可能なタスクがあれば即時実行し、なければ I/O 待ち (epoll) をすることになります。
結論
つまり、
setTimeout()
等の非同期タイマー処理は...
- 指定した時間が来たら即座に Callback を実行する. (OS 割り込みみたいに)
ではなく...
- 指定した時間を 過ぎてたら Callback を できるだけ早く 実行する
ですね。
それは Promise や、Network Socket I/O 待ちである fetch でも同じで...
- Callback が実行可能になってから (現在実行中の他の処理を待って) 順番が来たら (やっと) 実行開始する
です。
参考文献
- 投稿日:2020-04-04T11:32:34+09:00
JavaScript (Node.js) の非同期処理とシングルスレッド
本記事の目的
JavaScript や Node.js はよくシングルスレッドだ〜、と言われますが、では非同期処理はどうやって実行されているのか (Non-Blocking I/O) をざっくりと (私の身内に) 説明する為のサンプルコードです。
検証環境
- iMac (Retina 5K, 27-inch Late 2014), 4 GHz Intel Core i7
- Node.js v12.13.0
$ nodebrew install-binary v12.13.0 $ nodebrew use v12.13.0ブラウザ JavaScript の Event loop はまたちょっと違います。
早速サンプルコードから
以下の様な JavaScript
index.js
を、Node.js で実行します。
- 【処理 1】ミリ秒で終わる処理を
setTimeout()
で 5 秒後に発火.- 【処理 2】ミリ秒で終わる処理を
setTimeout()
で 0 秒後に発火.- 【処理 3】10 秒かかる同期処理を実行.
- 時間の計測には Node.js 標準 API の perf_hooks モジュールを使用しています。 Node.js プロセス実行開始からのミリ秒を得られます
- コード中では、ミリ秒 → 秒、に変換して表示しています
index.jsconst { performance } = require('perf_hooks'); /** * @return 本スクリプトを実行してからの経過秒数. */ const seconds = () => performance.now() / 1000; const secondsPadded = () => seconds().toFixed(6).padStart(10, ' '); // 長さ揃える. //////////////// 処理3つ //////////////// /** * 処理 1 (非同期, 5 秒後に発火). */ const func1 = () => { console.log(`${secondsPadded()} seconds --> 処理 1 (非同期, 5 秒後に発火)`); }; /** * 処理 2 (非同期, 0 秒後に発火). */ const func2 = () => { console.log(`${secondsPadded()} seconds --> 処理 2 (非同期, 0 秒後に発火)`); }; /** * 処理 3 (同期. 10 秒かかる). */ const func3 = () => { while (seconds() < 10) { /* consuming a single cpu for 10 seconds... */ } console.log(`${secondsPadded()} seconds --> 処理 3 (同期, 10 秒かかる)`); }; //////////////// 計測開始 //////////////// console.log(`${secondsPadded()} seconds --> index.js START`); // [非同期] 5 秒後に実行. setTimeout(func1, 5000); // [非同期] 即時実行. setTimeout(func2); // 同期実行. func3(); console.log(`${secondsPadded()} seconds --> index.js END`); //////////////// 計測終了 ////////////////期待値?
なんとなく 「こう動作するだろう...」 という気分になるのは ↓ でしょう。
$ node index.js 0.000000 seconds --> index.js START 0.000000 seconds --> 処理 2 (非同期, 0 秒後に発火) 5.000000 seconds --> 処理 1 (非同期, 5 秒後に発火) 10.000000 seconds --> 処理 3 (同期, 10 秒かかる) 10.000000 seconds --> index.js END実際は...
現実はこうです。何故でしょうか。
$ node index.js 0.175104 seconds --> index.js START 10.000085 seconds --> 処理 3 (同期, 10 秒かかる) 10.000210 seconds --> index.js END 10.000955 seconds --> 処理 2 (非同期, 0 秒後に発火) 10.001161 seconds --> 処理 1 (非同期, 5 秒後に発火)シングルスレッドだから、順番に処理している
おおよそ、Node.js の内部では ↓ のように処理がシングルスレッドで行われています。
- JavaScript コンテキストの生成時にイベントループが生成されます
- 最初のエントリ JavaScript
index.js
がタスクとして、未実行キューに乗ります- イベントループ
- 未実行キューから
index.js
タスクが取り出され、実行が開始されます
setTimeout(処理1, 5秒)
が実行され、【処理 1】がタイマーキューに追加されますsetTimeout(処理2, 0秒)
が実行され、【処理 2】がタイマーキューに追加されます- 【処理 3】が同期的に実行され、10 秒間、CPU (シングルコア) を専有します
index.js
タスクの実行が終了します- イベントループ
- タイマーキューから 有効期限が切れたタスク【処理 2】 を取り出し、実行が開始されます
- 【処理 2】タスクの実行が終了します
- イベントループ
- タイマーキューから 有効期限が切れたタスク【処理 1】 を取り出し、実行が開始されます
- 【処理 1】タスクの実行が終了します
実際はタイマー Phase はキューではない (FIFO でもない) ですが、説明の都合上そう表記しました。
要はイベントループにて、実行可能なタスクがあれば即時実行し、なければ I/O 待ち (epoll) をすることになります。
結論
つまり、
setTimeout()
等の非同期タイマー処理は...
- 指定した時間が来たら即座に Callback を実行する. (OS 割り込みみたいに)
ではなく...
- 指定した時間を 過ぎてたら Callback を できるだけ早く 実行する
ですね。
それは Promise や、Network Socket I/O 待ちである fetch でも同じで...
- Callback が実行可能になってから (現在実行中の他の処理を待って) 順番が来たら (やっと) 実行開始する
です。
参考文献
- 投稿日:2020-04-04T10:56:05+09:00
Chart.jsで遊んでみた
折れ線グラフも棒グラフも自由自在!
chart.html<!DOCTYPE html> <html> <head> <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.3/Chart.min.js"></script> </head> <body> <canvas id="myChart"></canvas> <script> var ctx = document.getElementById('myChart').getContext('2d'); var chart = new Chart(ctx, { // The type of chart we want to create type: 'horizontalBar', //bar 棒グラフ、line 折れ線グラフ、horizontalBar 横の棒グラフ // The data for our dataset data: { labels: ['January', 'February', 'March', 'April', 'May', 'June'], datasets: [{ label: '@NJ', data: [80, 50, 60, 40, 30, 150], backgroundColor: 'skyblue', borderColor: 'blue', borderWidth: 0, fill: false, pointStyle: 'rect' },{ label: '@haruka', data: [100, 100, 40, 50, 30, 100], // borderColor: 'blue', // borderWidth: 5, backgroundColor: [ 'hsla(90, 60%, 60%, 0.3)', 'hsla(180, 60%, 60%, 0.3)', 'hsla(270, 60%, 60%, 0.3)', 'hsla(360, 60%, 60%, 0.3)', 'hsla(0, 60%, 60%, 0.3)', 'hsla(80, 60%, 60%, 0.3)', ], lineTension: 0, pointStyle: 'triangle' }] }, // Configuration options go here options: { // scales: { // yAxes: [{ // ticks: { // // min: 0, // // max:100 // suggestedMin: 0, // suggestedMax: 100, // stepSize: 10, // callback: function(value, index, values){ // return 'JPY'+ value; // } // } // }] // }, //積み上げ棒グラフ scales:{ xAxes: [{ stacked: true }], yAxes: [{ stacked: true }] }, title: { display: true, text: 'Annual Sales', fontSize: 18, position:'left' }, animation: { duration: 0 }, legend:{ // position: 'right' // display: false } } }); var myLineChart = new Chart(ctx, { type: type, data: data, options: options }); </script> </body> </html>結果は以下の図の通り。最初は折れ線グラフで作ったり枠の色変えたり
縦の棒グラフ作ってたりしたから不要なコードも沢山あります。
(//でコメントにしてるよ)
折れ線グラフ
index.html<!DOCTYPE html> <html lang="ja"> <head> <meta charset="utf-8"> <title>my Chart</title> </head> <body> <canvas id ="my_chart"> Canvas not supported... </canvas> <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.3/Chart.min.js"></script> <script> 'use strict'; var type = 'line'; var data = { labels: [2010, 2011, 2012, 2013], datasets: [{ label: '@haruka', data: [120, 130, 140, 150] }, { label: '@koji', data: [180, 200, 150, 300] }] }; var options; var ctx = document.getElementById('my_chart').getContext('2d'); var myChart = new Chart(ctx, { type: type, data: data, options: options }); </script> </body> </html>
- 投稿日:2020-04-04T10:45:09+09:00
リモート勤務状況を Slack に常に反映する
はじめに
リモートワーク、最近は、テレワーク、自宅勤務、在宅勤務、様々な呼び方をすることが多いですね。世界中を席巻する例の細菌感染とともに、かつてない流行りをみせています。あなたの会社ではどうですか?
そもそもは、専門職なんかは特に顕著ですが、職場に行かなくたって上手く回る仕事は世の中にありふれているし、そうした可能性を否定するいくつもの
つまらない主張は、きっかけさえあればこんな風にサクッと是正されるものかもしれません。誰が毎朝毎夕満員電車で汗だくで他人とおしくらまんじゅうしたいです?これまで、リモートワークというものは、多くの人たちの固定観念ともいえるその職場主義によって、ほとんどの組織で導入を妨げられてきた歴史があります。そんな練度の低さでは、おいそれと使いこなせるわけはないのです。上手くいかないのが正常です。相応のナレッジが必要です。私も実戦経験が少なく苦戦する日々です。それをまず理解したいところです。
ということで前置きが長くなってしまいましたが、まぁだいたいのエンジニアが所属する組織では、コミュニケーションの中心に Slack のようなテキストチャットメインのコラボレーションツールを使っていると思います。リモートワークを導入するにあたり、メンバーの勤務状況をツールを通して把握することはチームにとって重要です。今回は、無料でこの仕組みの自動化を目指します。
構成
Slack API トークン取得
非推奨のレガシートークンを使う手もありますが、ちゃんと OAuth トークンの取得方法を載せておきます。
↓アプリを作成します。
↓ワークスペースに対するアクセス権限を定義します。
users.profiles:write というユーザープロフィールを変更できる権限のみをユーザートークンに設定します。
これでトークンが悪意のある第三者に知られても、あなたのプロフィールを変更することしかできません。↓インストールします。
↓権限を許可すると、トークンが生成されます。
Google Apps Script 作成
↓Spreadsheet に紐付くスクリプトを作成します。
Spreadsheet は、IFTTT Webhook で受信したデータのストレージとして機能します。↓スクリプトエディタを開きます。
↓スクリプトをコピペし、Slack トークンをはじめとした必要な情報を埋めます。
↓IFTTT の Webhook となる Web App をデプロイします。
↓Execute the app as: Me (自分が実行), Who has access to the app: Anyone, even anonymous (誰でもアクセス可) となっていることに注意してください。
↓Web App の次は、トリガーを追加します。
↓実行する関数は update です。また Spreadsheet の内容をポーリングすることになるので、実行頻度は短めにします。
↓ちなみにスクリプトの実行ログはここで見れます。
Google Calendar 予定を追加
在宅勤務を想定する以上、特定の場所にいるだけでステータスを決定するわけにはいきません。寝ている間もずっと働いてることになってしまいます。カレンダーの予定も考慮することにしましょう。
↓このような形で、その日はどこで働くのかわかるように、場所をタイトルに含めた予定をまとめてドカンと追加しましょう。画像では、休暇を入れながら、原則として月曜だけ出社することにしています。
IFTTT レシピ作成
在宅勤務、職場勤務、この 2 つの場所のレシピを作成します。今回は 2 つだけですが、場所の数だけレシピは必要です。
↓まずは、在宅勤務レシピの This を作成していきます。
↓This は、Location サービスを設定します。
↓入出の情報が欲しいので、トリガーは左下の "You enter or exit an area" にします。
↓あなたの家の場所を範囲指定します。範囲が広すぎても狭すぎてもよくないので、ちょうどいい感じに調整します。
↓今度は That です。
↓That は Webhooks にします。入出のタイミングで Web App を実行してもらうためです。
Location で設定した位置範囲に来た時、Google Apps Script の Web App に、
entered
またはexited
という文字列が POST されます。
それぞれ、範囲への入と出を表します。
URL は Web App をデプロイした際に取得できる URL の末尾に?place={場所名}
を追加しておきます。場所名は、レシピによって異なるので以下を参考に設定してください。
- 在宅勤務レシピ URL に追加する文字列:
?place=%E5%9C%A8%E5%AE%85
- 職場勤務レシピ URL に追加する文字列:
?place=%E8%81%B7%E5%A0%B4
JavaScript の実行環境があれば、以下の要領で場所名を作成できます。
encodeURIComponent('在宅') == '%E5%9C%A8%E5%AE%85' encodeURIComponent('職場') == '%E8%81%B7%E5%A0%B4'↓
↓レシピの名前を設定して完了です。
↓Connected になっていることを確認します。
同じ要領で「職場勤務」レシピも作成したら完成です!
さいごに
Unlicense だから自由にフォークしてね。特化するも汎化するも応用するも良しです。
- 投稿日:2020-04-04T02:37:04+09:00
【JavaScript】月初・月末取得や日数0付けや日付比較
概要
JavaScriptのDateオブジェクトを使用している中で、よく忘れてしまう事を備忘録として残します。
月初の取得
function getFirstDate (date) { return new Date(date.getFullYear(), date.getMonth(), 1); } var date = getFirstDate(new Date()); console.log(date); // Wed Apr 01 2020 00:00:00 GMT+0900 (日本標準時)月末の取得
function getLastDate (date) { return new Date(date.getFullYear(), date.getMonth() + 1, 0); } var date = getLastDate(new Date()); console.log(date); // Thu Apr 30 2020 00:00:00 GMT+0900 (日本標準時)日付や月の前に0を付ける
var today = new Date(); var date = ("0"+today.getDate()).slice(-2); var month = ("0"+ (today.getMonth()+1)).slice(-2); console.log(`${today.getFullYear()}-${month}-${date}`) // 2020-04-04日付の比較
var date1 = new Date(); var date2 = new Date("2020-03-01"); console.log(date1.getTime() > date2.getTime()) // true
- 投稿日:2020-04-04T02:15:01+09:00
Concurrent Mode時代のReact設計論 (4) コンポーネント設計にサスペンドを組み込む
この記事は「Concurrent Mode時代のReact設計論」シリーズの4番目の記事です。
シリーズ一覧
- Concurrent Mode時代のReact設計論 (1) Concurrent Modeにおける非同期処理
- Concurrent Mode時代のReact設計論 (2) useTransitionを活用する
- Concurrent Mode時代のReact設計論 (3) SuspenseやuseTransitionが何を解決するか
- Concurrent Mode時代のReact設計論 (4) コンポーネント設計にサスペンドを組み込む
- Concurrent Mode時代のReact設計論 (5) トランジションを軸に設計する(仮)
- Concurrent Mode時代のReact設計論 (6) ステート管理ライブラリの展望(仮)
- Concurrent Mode時代のReact設計論 (7) まとめ(仮)
コンポーネント設計にサスペンドを組み込む
前回の最後にrender-as-you-fetchという概念が出てきました。これは、ReactのConcurrent Modeのドキュメントにおいて提唱されているUXパターンであり、読み込んで表示すべきデータが複数ある場合に、全てが読み込み完了するまで待つのではなく読み込めたデータから順に表示するというものです。
このパターンの良し悪しはともかく、これはConcurrent Mode時代のコンポーネント設計を議論するための格好の題材です。
基本パターン: データごとにPromiseを分ける
Concurrent Modeにおいてrender-as-you-fetchを実現するには、それぞれのデータに対して異なるPromise(
Fetcher
)を用意する必要があります。そして、各データを担当するコンポーネントを用意して、それぞれのコンポーネントがサスペンドします。そうすることで、それぞれのデータが用意できた段階でコンポーネントのサスペンドが解除(再レンダリング)され、その部分のデータが表示されます。
具体例として、ユーザーのリストを3種類読み込んでrender-as-you-fetch戦略で表示するコンポーネントを書いてみるとこんな感じです。
const PageB: FunctionComponent<{ dailyRankingFetcher: Fetcher<User[]>; weeklyRankingFetcher: Fetcher<User[]>; monthlyRankingFetcher: Fetcher<User[]>; }> = ({ dailyRankingFetcher, weeklyRankingFetcher, monthlyRankingFetcher }) => { return ( <> <Suspense fallback={<p>Loading users...</p>}> <Users usersFetcher={dailyRankingFetcher} /> </Suspense> <Suspense fallback={<p>Loading users...</p>}> <Users usersFetcher={weeklyRankingFetcher} /> </Suspense> <Suspense fallback={<p>Loading users...</p>}> <Users usersFetcher={monthlyRankingFetcher} /> </Suspense> </> ); }; const Users: FunctionComponent<{ usersFetcher: Fetcher<User[]>; }> = ({ usersFetcher }) => { const users = usersFetcher.get(); return ( <ul> {users.map(({ id, name }) => ( <li key={id}>{name}</li> ))} </ul> ); };
PageB
は3種類のFetcher<User[]>
を受け取ります。実際にFetcher<User[]>
から得てデータを表示するのは別に用意したUsers
コンポーネントが担当しており、PageB
の役割は各Users
要素をSuspense
で囲むことです。ポイントは、このように
Users
をそれぞれSuspense
で囲まないといけないということです。復習すると、Suspense
の役割はその内部で発生したサスペンドをキャッチして、その場合にfallback
で指定されたフォールバックコンテンツを代わりにレンダリングすることです。Suspense
の中のどこでサスペンドが発生しようと、そのSuspense
の中身全体がフォールバックします。このことから、
Suspense
を用いてrender-as-you-fetchパターンを実装するには、あるコンポーネントがサスペンドしても他の部分に影響を与えないようにする必要があります。ここではSuspense
を複数並べることでこれを達成しています。実際この
PageB
を適当なデータでレンダリングすると、下のスクリーンショットのように一つずつLoading users...
がUsers
によってレンダリングされたデータに置き換わっていく挙動をとります。ちなみに、
Suspense
の組み立て方によって色々な表示パターンを実現することができます。例えば、次のようにすると、dailyRankingFetcher
が用意できるまでは何も表示せず、用意できたら残りを待つという挙動になります。<Suspense fallback={<p>Loading users...</p>}> <Users usersFetcher={dailyRankingFetcher} /> <Suspense fallback={<p>Loading users...</p>}> <Users usersFetcher={weeklyRankingFetcher} /> <Suspense fallback={<p>Loading users...</p>}> <Users usersFetcher={monthlyRankingFetcher} /> </Suspense> </Suspense> </Suspense>このように、コンポーネントが非同期処理の結果をどのように待ってどう表示するのかというロジックは
Suspense
を用いて書くことができます。これはつまり、Concurrent Modeではrender-as-you-fetchパターンに必要なロジックを実際にそのデータを表示するコンポーネントが(内部でのコンポーネント分割は起こりますが)記述できるということを表しています。前回の記事で示した問題の一つがConcurrent Modeでは解決されているわけです。おまけに、
Suspense
という道具を用いて、データローディングに係るロジックをJSXというきわめて宣言的な表現により記述することができています。一応誤解がないように述べておくとJSXという構文は重要ではなく、本質的にこの点に寄与しているのはコンポーネントが成す木構造という表現方法なのですが。なお、この立場に立つと、レンダリングのサスペンドというのはコンポーネントが発生させる現象ですから、サスペンドする役割を持つコンポーネント(今回は
Users
)を明確にすることが重要になります。コンポーネントにdoc commentなどを書く際に、「このコンポーネントはいつサスペンドするのか」を明示しておくのもよいでしょう。
useTransition
とSuspense
の関係まず
useTransition
について復習します。このフックからはstartTransition
関数を得ることができ、startTransition
の内部で発生したステート更新により再レンダリングが発生してそのレンダリングでサスペンドが発生した場合、サスペンドが解消されるまで画面に更新前のステートを表示し続けられるというものでした。
useTransition
が絡むと、Suspense
に係るコンポーネント設計はかなり複雑な様相をとります。これに関連して、ひとつ重要な事実を覚えていただく必要があります。それは、再レンダリング時に新たにマウントされた
Suspense
の中で起きたサスペンドはuseTransition
からは無視されるという点です。言い方を変えれば、useTransition
の効果を発動するには、あらかじめ用意してあったSuspence
にサスペンドをキャッチしてもらう必要があるということです。この挙動はバグなのではと筆者は一瞬思いましたが、このissueで説明されている通りこれは仕様です。
コンポーネントを設計する際にはこのことを念頭に考える必要があります。すなわち、あるコンポーネントの中でサスペンドを発生させるにあたり、それが
useTransition
に対応するサスペンド(外部のSuspense
によりキャッチされることを意図したサスペンド)なのか、それともuseTransition
に対応しないサスペンド(自身の中で新たに生成したSuspense
によりキャッチされるサスペンド)なのかを意識的に区別しなければならないということです。では、先ほど出てきた
PageB
の場合はどうでしょうか。const PageB: FunctionComponent<{ dailyRankingFetcher: Fetcher<User[]>; weeklyRankingFetcher: Fetcher<User[]>; monthlyRankingFetcher: Fetcher<User[]>; }> = ({ dailyRankingFetcher, weeklyRankingFetcher, monthlyRankingFetcher }) => { return ( <> <Suspense fallback={<p>Loading users...</p>}> <Users usersFetcher={dailyRankingFetcher} /> </Suspense> <Suspense fallback={<p>Loading users...</p>}> <Users usersFetcher={weeklyRankingFetcher} /> </Suspense> <Suspense fallback={<p>Loading users...</p>}> <Users usersFetcher={monthlyRankingFetcher} /> </Suspense> </> ); };
startTransition
中のステート更新で新たにPageB
コンポーネントがマウントされた場合、3つのSuspense
コンポーネントがマウントされ、その中で発生したサスペンドは即座にキャッチされます。このとき、ステート更新によって発生したサスペンドは全て新たにマウントされたSuspense
によってキャッチされることになります。よって、このステート更新では
useTransition
の効果は発揮されません。ステート更新が行われた瞬間にPageB
がレンダリングされDOMに反映されます。PageB
は最初3つのLoading users...
を表示することになるでしょう。実際、このPageB
に前回の記事で出てきたRoot
とPageA
を繋げてみるとそのような挙動になります。興味がある方は実際にやってみましょう。
useTransition
のための応用的なコンポーネントデザインこのことを踏まえて、
PageB
をuseTransition
に対応するように改良するにはどうすればよいか考えてみましょう。もし「全部ロードされるまで前の画面を表示し続けたい」という場合は話は簡単で、それぞれのUsers
コンポーネントをSuspense
で囲むのをやめればよいです。また、例えば「dailyRankingFetcher
がロード完了するまでは前の画面を表示し続けたい」のような場合も、対応するUsers
だけSuspense
で囲まなければ対応できます。厄介なのは、「どれか1つのデータが読み込めるまでは前の画面を表示し続けたい」というような場合です。この場合はただ
Suspense
を消すだけでは達成できません。Promiseの機能を思い出すと、「どれか1つのPromiseが解決するまで待つ」という挙動は
Promise.race
により達成できます。ということで、今回はFetcher.race
を用意すれば解決できますね。
Fetcher.race
の実装を用意するとこんな感じです(コンストラクタをあのインターフェースにしたので実装がひどいことになっていますがサンプルだと思って大目に見てください)。static race<T extends Fetcher<any>[]>( fetchers: T ): Fetcher<FetcherValue<T[number]>> { for (const f of fetchers) { if (f.state.state === "fulfilled") { const result = new Fetcher<any>(() => Promise.resolve()); result.state = { state: "fulfilled", value: f.state.value }; return result; } else if (f.state.state === "rejected") { const result = new Fetcher<any>(() => Promise.resolve()); result.state = { state: "rejected", error: f.state.error }; } } return new Fetcher(() => Promise.race(fetchers.map(f => (f as any).promise)) ); }ちなみに、型に出てきた
FetcherValue
はこのように定義しています。型安全な実装が厳しい場合でも、型パズルでも何でも駆使して関数のインターフェースだけは正確さを守るというのが堅牢なTypeScriptプログラムを書くコツです。type FetcherValue<F> = F extends Fetcher<infer T> ? T : unknown;話を元に戻すと、この
Fetcher.race
を使ってPageB
をこのように定義すれば、「どれか1つのデータが来るまでサスペンドする」という挙動が実現できます。Fetcher.race([...])
はget
メソッドを使用するためだけに作られており、その値はPageB
直下では使われていません。このように、値を得ることではなくサスペンドすることが主目的のFetcher
というのも存在し得ます。少し話が違いますが、筆者も第1回の記事で紹介したアプリケーションではFetcher<void>
を多用しています。const PageB: FunctionComponent<{ dailyRankingFetcher: Fetcher<User[]>; weeklyRankingFetcher: Fetcher<User[]>; monthlyRankingFetcher: Fetcher<User[]>; }> = ({ dailyRankingFetcher, weeklyRankingFetcher, monthlyRankingFetcher }) => { Fetcher.race([ dailyRankingFetcher, weeklyRankingFetcher, monthlyRankingFetcher ]).get(); return ( <> <Suspense fallback={<p>Loading users...</p>}> <Users usersFetcher={dailyRankingFetcher} /> </Suspense> <Suspense fallback={<p>Loading users...</p>}> <Users usersFetcher={weeklyRankingFetcher} /> </Suspense> <Suspense fallback={<p>Loading users...</p>}> <Users usersFetcher={monthlyRankingFetcher} /> </Suspense> </> ); };再レンダリング時のサスペンド設計
ここからは、一旦最初の
PageB
に頭を戻して考えます。const PageB: FunctionComponent<{ dailyRankingFetcher: Fetcher<User[]>; weeklyRankingFetcher: Fetcher<User[]>; monthlyRankingFetcher: Fetcher<User[]>; }> = ({ dailyRankingFetcher, weeklyRankingFetcher, monthlyRankingFetcher }) => { return ( <> <Suspense fallback={<p>Loading users...</p>}> <Users usersFetcher={dailyRankingFetcher} /> </Suspense> <Suspense fallback={<p>Loading users...</p>}> <Users usersFetcher={weeklyRankingFetcher} /> </Suspense> <Suspense fallback={<p>Loading users...</p>}> <Users usersFetcher={monthlyRankingFetcher} /> </Suspense> </> ); };これまでの議論では
PageB
が新規にマウントされた場合を考えていました。このときは中身のSuspense
が新規にマウントされるので、その中のUsers
がサスペンドしてもuseTransition
が反応しないのでした。では、
PageB
がすでにマウントされている状態で、startTransition
の中のステート更新に起因してPageB
のpropsが変わった場合はどうでしょうか。新しくpropsから渡されたFetcher
によってサスペンドした場合、それをキャッチするのはPageB
がレンダリングしたSuspense
であることに変わりませんが、今回はこれらのSuspense
はあらかじめマウントしてあったSuspense
です。なぜなら、前回のPageB
のレンダリングによってこのSuspense
はすでにマウントされていたからです。よって、この場合はuseTransition
が働きます。つまり、
PageB
は「新しくマウントされたときはuseTransition
に非対応だが、マウント済の状態でpropsが更新された時はuseTransition
に対応」という特徴を持つコンポーネントなのです。Concurrent Modeによほど精通していなければ、コンポーネントの定義を一目見てこのことを見抜くのは難しいでしょう。この状態はなんだか一貫性がありませんね。これでも良いならばこの実装で問題ありませんが、どちらかに統一したいということもあるでしょう。まず、常に
useTransition
に対応にしたい場合の方法は先ほどまで述べた通りで、Suspense
を消すなり、Suspense
の中ではなくPageB
自体がサスペンドするなりといった方法があります。一方、常に
useTransition
非対応にしたい場合はどうすれば良いでしょうか。答えは、「propsが変わるたびにSuspense
をマウントし直す」です。そうすることでSuspense
は常に新しくマウントされた扱いとなり、その中でのサスペンドはuseTransition
に影響しなくなります。Suspense
をマウントし直すにはkey
を用います。Reactでは、同じコンポーネントでも異なるkey
が与えられた場合は別のコンポーネントと見なされますから、propsが変わるたびにSuspense
に与えるkey
を変えることで、Suspense
をアンマウント→マウントさせることができます。具体的な方法の一例としては、まず次のような
useObjectId
カスタムフックを用意します。export const useObjectId = () => { const nextId = useRef(0); const mapRef = useRef<WeakMap<object, number>>(); return (obj: object) => { const map = mapRef.current || (mapRef.current = new WeakMap()); const objId = map.get(obj); if (objId === undefined) { map.set(obj, nextId.current); return nextId.current++; } return objId; }; };このフックは
PageB
の中で次のように使います。今回のコードではそれぞれのSuspense
にkey
が与えられており、key
を計算するためにuseObjectId
を使用しています。const PageB: FunctionComponent<{ dailyRankingFetcher: Fetcher<User[]>; weeklyRankingFetcher: Fetcher<User[]>; monthlyRankingFetcher: Fetcher<User[]>; }> = ({ dailyRankingFetcher, weeklyRankingFetcher, monthlyRankingFetcher }) => { const getObjectId = useObjectId(); return ( <> <Suspense key={`${getObjectId(dailyRankingFetcher)}-1`} fallback={<p>Loading users...</p>} > <Users usersFetcher={dailyRankingFetcher} /> </Suspense> <Suspense key={`${getObjectId(weeklyRankingFetcher)}-2`} fallback={<p>Loading users...</p>} > <Users usersFetcher={weeklyRankingFetcher} /> </Suspense> <Suspense key={`${getObjectId(monthlyRankingFetcher)}-3`} fallback={<p>Loading users...</p>} > <Users usersFetcher={monthlyRankingFetcher} /> </Suspense> </> ); };
useObjectId
フックは関数getObjectId
を返します。この関数は各オブジェクトに対して異なるIDを返します。同じオブジェクトに対しては何回呼んでも同じIDが返されます。これをkey
に組み込むことによって、daylyRankingFetcher
などに別のFetcher
が渡されたタイミングでSuspense
に渡されるkey
も更新され、新たなSuspense
がマウントされた扱いになります。この実装により、
PageB
が別のpropsで再レンダリングされた場合でも、内部で発生するサスペンドの影響を内部に封じ込めてuseTransition
に影響させないことが可能になりました。
Suspense
とuseTransition
の関係を整理するここまでは、
Suspense
とuseTransition
の関係を解説し、ユースケースに合わせた実装法を紹介してきました。なんだか場当たり的な印象を受けた読者の方も多いと思いますので、もう少し整理して見直してみましょう。あるコンポーネントがPromise(をラップする
Fetcher
)を受け取るとします。そのコンポーネントの責務がそのデータを表示することであれば、必然的にそのコンポーネントはサスペンドを発生させることになります。コンポーネント内で発生しうるサスペンドは3種類に分類できます。3種類のサスペンドは、「コンポーネントの外にサスペンドが出て行くかどうか」と「
useTransition
に対応するかどうか」に注目すると次の表のようにそれぞれ異なる特徴を持ちます。
サスペンドの種類 外に出て行くか useTransition
対応1 内部の Suspense
でキャッチされないサスペンドYes Yes1 2 内部で新規にマウントされた Suspense
にキャッチされるサスペンドNo No 3 内部の既存の Suspense
にキャッチされるサスペンドNo Yes パターン1が一番スタンダートなサスペンドでしょう。あるコンポーネントが
Fetcher
から得たデータを表示することが責務ならば、データがまだない場合にそのコンポーネントがサスペンドするのは自然なことです。パターン2は逆にサスペンドを完全に内部で抑え込むパターンです。サスペンドが発生しても、そのことはコンポーネントの外部には検知されません。データがまだ無いときの挙動を完全にコンポーネント内で制御したい場合に適しています。
パターン3は、コンポーネントが新規にマウントされた場合は発生せず、再レンダリングのときのみ可能な選択肢です。これは扱うのがやや難しいですが、コンポーネントの内部で
useTransition
を使いたい場合などはこれが一番自然な選択肢となることが多いでしょう。コンポーネントのロジックを実装する際には、これらを組み合わせることもあるでしょう。例えば、先ほど出てきた
Fetcher.race
の例は1と2の合わせ技です。コンポーネントの使い勝手という観点からは、パターン1が最も有利です。パターン1はコンポーネントの外側に
Suspense
を配置すれば2や3に変換できますが、逆に2や3を1に変換することはできないからです。パターン1と2や3の使い分けはコンポーネントの責務に応じて決めるのが良いでしょう。具体的には、データがない場合にフォールバックを表示するという責務をコンポーネントが持っているのであれば、2か3を選択することになります。逆に、その責務を持たずデータがない場合はサスペンドすべきならば、1を選択しなければなりません。
誰が
Fetcher
を用意するのかConcurrent Modeにおいては、誰かいつ非同期処理を開始する(
Fetcher
を用意する)のかがとても重要です。従来の基本的なパターンは、データを表示する責務を持ったコンポーネントがuseEffect
の中で非同期処理を開始するというものです。Fetcher
と組み合わせればこのような実装になるでしょう。const PageB: FunctionComponent = () => { const [dailyRankingFetcher, setDailyRankingFetcher] = useState< Fetcher<User[]> | undefined >(undefined); useEffect(() => { setDailyRankingFetcher(new Fetcher(() => fetchUsers())); }, []); return dailyRankingFetcher !== undefined ? ( <Users usersFetcher={dailyRankingFetcher} /> ) : null; };しかし、2つの理由からこの実装は忌避すべきです。一つ目の理由は、一度レンダリングされたあと
useEffect
内ですぐに再度レンダリングを発生させていることです。これはReactにおける典型的なアンチパターンの一つです。もう一つの理由は、こうすると
PageB
が自動的にuseTransition
に非対応になるからです。PageB
が最初にレンダリングされたときはまだサスペンドが発生しませんから、PageB
に遷移するきっかけとなったステート更新ではサスペンドが発生しなかったことになります。もしPageB
に遷移するときにuseTransition
を使いたければ、このような実装は必然的に選択肢から除外されます。では、どうすればよいのでしょうか。大きく分けて2つの選択肢があります。基本的には、これまでやってきたように外から
Fetcher
を渡すことになります。これについては次回の記事で詳しく扱います。もう一つ、
useEffect
の中ではなく最初のレンダリング中に直にFetcher
を用意するという戦略を思いついた方もいるかもしれません。しかし、ほとんどの場合これは無理筋です。
useState
でFetcher
を用意することはできない例えば、次のような実装を試してみましょう。
useState
は関数を渡すと最初のレンダリング時にその関数が呼び出されてステートの初期化に用いられます。次のようにすることでdailyRankingFetcher
をいきなりFetcher
で初期化し、初手でサスペンドを発生させることができます。const PageB: FunctionComponent = () => { const [dailyRankingFetcher] = useState(() => new Fetcher(() => fetchUsers())); return <Users usersFetcher={dailyRankingFetcher} />; };しかし、これは期待通りに動きません。
PageB
はずっとサスペンドしたままになります。その理由は、PageB
がレンダリングされるたびに新しいFetcher
インスタンスが生成されるからです。
PageB
が最初にレンダリングされた場合はuseState
に渡された関数が呼ばれて新しいFetcher
インスタンスがdailyRankingFetcher
に入ります。ここまでは想定通りですが、その後サスペンド明けにPageB
が再度レンダリングされたとき、PageB
は初回レンダリングという扱いになります。よって、dailyRankingFetcher
に入るのはまた新しく作られたFetcher
インスタンスとなり、PageB
は再度サスペンドします。これを繰り返すことになり、PageB
は永遠に内容をレンダリングすることができません。すなわち、レンダリングの結果サスペンドが発生したときはレンダリングが完了したと見なされず、
useState
フックなどの内容はセーブされません。あたかも、そのレンダリングが無かったかのように扱われます。useMemo
なども同じです。この性質により、「最初にサスペンドしたレンダリング」から「サスペンド明けのレンダリング」に情報を渡すことは自力では困難です。そのため、最初のレンダリングの中で作った
Fetcher
インスタンスをサスペンド明けのレンダリングで手に入れることができず、サスペンドが空けても何をレンダリングすればいいか分からなくなってしまいます。Fetcher
をpropsで外から受け取ることでこの問題は回避できるのです。サスペンドとコンポーネントの純粋性
useState
がだめならuseRef
なら、と思った方もいるかもしれませんが、実はuseRef
でも無理です。useRef
はレンダリングをまたいで同じオブジェクトを返すのが特徴でしたが、useRef
によって返されるオブジェクトは最初のレンダリングで作られます。よって、「最初のレンダリング」が何回も繰り返されれば毎回新しいrefオブジェクトが作られることになり、やはりサスペンド前後の情報の受け渡しは困難です。ただし、最初のレンダリング以外の場合は注意が必要です。そもそも最初のレンダリング以外であっても、サスペンドしたレンダリングの結果は残りません。例えば、
useMemo
はサスペンドしたレンダリングにおいて計算された値はキャッシュしません。そのレンダリング中に値を計算したという事実が無かったことにされるからです。しかし、
useRef
は「毎回同じオブジェクトを返す」のが役割ですから、初回以外であればサスペンドしたレンダリングとサスペンド明けのレンダリングではuseRef
から同じオブジェクトが返されます。これを用いることで、サスペンドしたレンダリングから何らかの情報を残すことができます。明らかに、このようなことは避けるべきです。それは、このような
useRef
の使用はレンダリングの純粋性を破壊しているからです。レンダリングの純粋性とは、「コンポーネントをレンダリングしても副作用が発生しない」という意味で、「意味もなくコンポーネントをレンダリングしても(=関数コンポーネントを関数として呼び出しても)安全である」という意味でもあります。Concurrent Modeにおいては「コンポーネントがレンダリングされた(関数コンポーネントとして呼び出された)」ことは「そのコンポーネントのレンダリング結果がDOMに反映される」ことを意味しません。サスペンドが発生する可能性があるからです。この状況下でReactが好き勝手にレンダリングを試みるための前提として、コンポーネントは純粋であるべきとされているのです。
実際、Reactでは副作用は
useEffect
内で行うように推奨しています。useEffect
はコンポーネントが実際にDOMにマウントされた場合にコールバックが呼び出されます。サスペンドによりDOMに反映されなかった場合はコールバックは発生しません。また、レンダリングが純粋であることを強調するためか、Conncurrent Modeではデフォルトで1回のレンダリングで関数コンポーネントが2回呼び出されるようになっています(おそらくproductionでは1回)。これは、純粋でないコンポーネントを作ってしまった際に発生するバグを検出しやすくするためでしょう。実は先ほどの
useState
のサンプルでも、1回PageB
がレンダリングされるたびにFetcher
インスタンスが2個作られていました。非同期処理を発生させるのも副作用ですから、そもそもuseState
のステート初期化時にFetcher
インスタンスを作るのは無理筋だったということになります。
useRef
に話を戻しますが、Concurrent Modeではrefオブジェクトへのアクセス(特に書き込み)は副作用であると考えるべきです。先ほど説明したように、レンダリング中にrefオブジェクトに書き込むと、サスペンドしたレンダリングの影響がそれ以降に残ってしまうため、コンポーネントのレンダリングが純粋でなくなるからです。refオブジェクトは、useEffect
のコールバック内やイベントハンドラなど、副作用が許された世界でのみアクセスすべきです。refオブジェクトはもはや完全に副作用の世界の住人なのです。目ざとい方は、先程出てきた
useObjectId
はuseRef
に書き込んでいたじゃないかと思われるかもしれません。それはそのとおりなのですが、実はuseObjectId
はレンダリングの純粋性を損なわないように注意深く実装されています。純粋性を壊さない注意深い実装ならば、useRef
を使える可能性もあるのです。無理なときは無理なので無理だと思ったら潔く諦めるべきですが。まとめ
この記事では、サスペンドを念頭に置いたコンポーネント設計をどのようにすべきかについて議論しました。
重要なのは、サスペンドはその発生の仕方によって3種類に分類できるということです。さらに、これらを組み合わせることでより複雑なパターンを実装することもできます。もちろん、コンポーネントの記述は宣言的な書き方が保たれています。
Concurrent Modeでは、あるコンポーネントがどのような状況下でどの種類のサスペンドを発生させるのかということをコンポーネント仕様の一部として考えなければなりません。これは特に
useTransition
と組み合わせるときに重要です。Concurrent Mode時代のコンポーネント設計では、コンポーネントの責務は何なのかということを冷静に見極めて、そのコンポーネントはどのようにサスペンドすべきかということを考えなければならないのです。記事の後半では、Concurrent Modeでは特にレンダリングの純粋性が重要であることを開設しました。これを踏まえると、初手でサスペンドするコンポーネントは必然的に
Fetcher
を外部から受け取ることになります。次回の記事では、誰が
Fetcher
を作ってどう受け渡すのかについて考えていきます。次の記事: 鋭意執筆中です。
このコンポーネントの外部に設置された既存の
Suspense
にキャッチ場合はuseTransition
に反応しないサスペンドとなりますが、それはこのコンポーネントの預かり知るところではありません。このコンポーネントがuseTransition
に対応する可能j性を消しているわけではないことからYesとしています。 ↩
- 投稿日:2020-04-04T01:46:15+09:00
FullCalendarとIndexedDB
javascriptのカレンダーライブラリFUllcalendarを用いてアプリを作成しています。
(公式URL : https://fullcalendar.io)IndexedDBに保存しているデータをカレンダーにイベントとしてしようとして以下を実行致しました。
以下の例ではdateが'2020/4/5'から'2020/4/10'のデータを抽出しています。//オブジェクトの定義 var calendar; //IndexedDBの定義 var db = new Dexie("testDB"); db.version(1).stores({ testTable: '++id,worker,date,notes' });db.testTable .where('date') .between('2020/4/5','2020/4/10',true,true) .uniqueKeys() .then( (r) => { for (var i =0; i<r.length; i++) { calendar.addEvent({ title: 'test', start: r[i], allDay: true }); } calendar.render(); });しかし、データは取得できていますが、カレンダーにはイベントが表示されません。
どのようにすればIndexedDBから取得したデータをカレンダーに表示出来るでしょうか?
お分かりになられる方がいらっしゃいましたら何卒ご助言宜しくお願い致します。
- 投稿日:2020-04-04T00:35:41+09:00
javascriptの代入の基礎(深いコピー、浅いコピー)
1.
string
,number
,boolean
の代入配列やオブジェクト以外を代入する場合、値が代入される。
let ori_num = 10; let new_num = ori_num; // 変数の場合数字を代入 ori_num = 20; console.log(ori_num, new_num); //output => 20, 102. 配列
array
の代入配列を代入する場合、代入される配列は元の配列を参照する。
そのため、元の配列(ori_arr
)の値が変更されると、代入される配列(new_arr
)の値も変更されてしまう。const ori_arr = ['aaa', 'bbb']; const new_arr = ori_arr; // 元の配列を参照 ori_arr[1] = 'ccc'; // 元の配列の値が変わると… // 新しく作成した配列も変わる! console.log(ori_arr,new_arr); // output => ["aaa","ccc"], ["aaa","ccc"]この対策として以下の4つの方法がある。
対策1:
slice
でコピーconst ori_arr =['aaa', 'bbb']; const new_arr = ori_arr.slice(); ori_arr[1] = 'ccc'; //元の配列の値を変えても... // 新しく作成した配列には反映されない! console.log(ori_arr, new_arr); // output => ["aaa","ccc"] ["aaa","bbb"]対策2: 新しい配列作成+
concat
const ori_arr =['aaa', 'bbb']; const new_arr = [].concat(ori_arr); ori_arr[1] = 'ccc'; //元の配列の値を変えても... // 新しく作成した配列には反映されない! console.log(ori_arr, new_arr); //["aaa","ccc"] ["aaa","bbb"]対策3: ES6 spread
const ori_arr =['aaa', 'bbb']; const new_arr = [...a]; ori_arr[1] = 'ccc'; //元の配列の値を変えても... // 新しく作成した配列には反映されない! console.log(ori_arr, new_arr); //["aaa","ccc"] ["aaa","bbb"]対策4:
Array#from
const ori_arr =['aaa', 'bbb']; const new_arr = Array.from(ori_arr); ori_arr[1] = 'ccc'; //元の配列の値を変えても... // 新しく作成した配列には反映されない! console.log(ori_arr, new_arr); // output => ["aaa","ccc"] ["aaa","bbb"]3.
Object
の代入オブジェクトを代入する場合、代入されるオブジェクトは元のオブジェクトを参照する。
そのため、元のオブジェクト(ori_obj
)の値が変更されると、代入されるオブジェクト(new_obj
)の値も変更されてしまう。const ori_obj = { a:"aaa" } const new_obj = ori_obj; //元のオブジェクトに代入 ori_obj.a = "bbb"; //元のオブジェクトの値を変えると... // 新しく作成したオブジェクトも変更される! console.log(ori_obj, new_obj); // output => {a:"bbb"}, {a:"bbb"}そのための対策方法は下記の通り。
対策?:assign
を使用(浅いコピー)
Object#assign
は浅いコピー(shallow copy)のため、
オブジェクトの中のオブジェクトの値(下記の例だとshallow.b.c
)は元のオブジェクトobj
に依存してしまう。const obj = { a : 'aaa', b: { c: 'ccc' } } //浅いコピー (shallow copy) const shallow = Object.assign({}, obj); obj.a = 'change'; // オブジェクトを変更 obj.b.c ='change'; // オブジェクトの中のオブジェクトを変更 console.log(obj, shallow); // output // obj = {a:"change",b:{c:"change"}} // shallow = {a:"aaa", b:{c:"change"}} オブジェクトの中のオブジェクトは変更されるそこで、以下のような実装でこの問題を解決する。
対策:
JSON.parse(JSON.stringify(obj))
を使用(深いコピー)
JSON#parse
はstring
をJSON
に変換し、
JSON#stringify
はJSON
をstring
に変換する。const obj = { a : 'aaa', b: { c: 'ccc' } } //深いコピー (deep copy) const deep = JSON.parse(JSON.stringify(obj)); obj.a ='change'; obj.b.c ='change'; console.log(obj, deep); // output // obj = {a:"change",b:{c:"change"}} // shallow = {a:"aaa", b:{c:"ccc" }}
JSON.parse(JSON.stringify(obj))
が深いコピーになる理由// JSON.stringify(obj)が JSON.parse("{aaa:bbb,{ccc:ddd}}"); // のようにいったん文字列に置き換えているから
- 投稿日:2020-04-04T00:21:01+09:00
時間を持て余す小3がママのプログラムを手伝った話
きっかけ
Webデザインをしている嫁が、ビデオ通話でマイクボリュームのデザインをしていて、サンプルを動作こみで社内レビューに出そうとしてました。
こんなデザインです。
■■■■□□□
1秒毎にこうなったり、
■□□□□□□
こうなったり。
■■■■■□□プログラマーであればJavascriptですぐに実装できますが、Webデザイナーである嫁は何から手をつけていいかわからず。当然プログラマー出身SEの僕にどうしたらいいか聞いてきます。
えーっと正直めんどくさいw
そもそもいきなり構文を書くよりロジックというものを分かってもらわないと今後の為になりません!
ということで、コロナの影響で休校中の暇な小3の息子にサンプルを作らせましたw利用させたツール
DeNAさんが提供している「プログラミングゼミ」です。
実際にコードを書かずに、ブロックの組み立て(フローチャート)でアプリケーションを作るというものです。
視覚的にロジックが分かるので息子の作り上げたロジックをまずは嫁に理解してもらおうという作戦です。僕の要求
- 1が押されたらランプが1個光る
- 2が押されたらランプが2個光る
- 3が押されたらランプが3個光る
- 4が押されたらランプが4個光る
- 5が押されたらランプが5個光る
これを指示しました。
結果、基本デザインは以下のようになりました。
ランプ=電球を僕はイメージしてましたが、息子の中では魔法のランプと受け取ったようです。
実装部分
まずは呼び出し部分です。ボタンにこのロジックが付与されています。
ボタンをタッチされたら何かしらのデータを送っています。次に受け取り部分です。
それぞれのランプは値を受け取ったあとにお化け?を呼び出すようになっています。(=光る)まあ、実際に引数の数字にあまり意味はありませんが、そこは見なかった事に。
この呼び出しと受け取りが5セット存在しているプログラムになりました。
完成品
GIFアニメーションにしてみました。実際にクリックしている様子は見えませんが、3、4、2をクリックしています。
作成依頼をして30分くらいでつくりあげました。
とうぜん他にもいろいろなロジックがあるんですが、これはこれですごくよくできていると思います。結局のところ。。。
嫁が実装したプログラムはfor文使ってました。
(for文の中に余分なコードももあってやっぱりよく理解できてなさそうでした)
途中まではできてましたが、結局うまく動かない部分があり、仕上げは僕が。。。