- 投稿日:2020-03-22T23:22:34+09:00
moment.jsで単位を日に指定してdiffを使う時、時間を考えず純粋に日が変わったかどうかで計算する
結論:
startOf('days')
で時間をまるめるconst dayOne = moment('2020-01-01 10:00:00').startOf('days'); const dayTwo = moment('2020-01-02 09:00:00').startOf('days'); console.log('dayOne', dayOne.format()); console.log('dayTwo', dayTwo.format()); console.log('diff', dayTwo.diff(dayOne, 'days'));はじめに
moment.jsには指定した二つの日時を比較し、出力するdiffという機能があります。
この機能は、2つの日付の差分を時間単位まで観測し、デフォルトでは
秒数
で出力されます。
diff
は第二引数で測定する時間単位を指定することが可能です。しかしその時、内部では秒数を任意の単位に再計算した値
を返しています。
また、その時小数点以下は切り捨てられてしまいますconst dayOne = moment('2020-01-01 10:00:00'); const dayTwo = moment('2020-01-02 11:00:00'); const dayThree = moment('2020-01-02 09:00:00'); const dayFour = moment('2020-01-02 10:00:00'); console.log("dayTwo", dayTwo.diff(dayOne, 'days')); // dayTwoはdayOneの時間を過ぎているので1 console.log("dayTwo asFloat", dayTwo.diff(dayOne, 'days', true)); console.log("dayThree", dayThree.diff(dayOne, 'days')); // dayThreeはdayOneの時間を過ぎてないので0 console.log("dayThree asFloat", dayThree.diff(dayOne, 'days', true)); console.log("dayFour", dayFour.diff(dayOne, 'days')); // dayFourはdayOneと同じ時間なので1 console.log("dayFour asFloat", dayFour.diff(dayOne, 'days', true));(tips: 第三引数に
true
を渡すことで値から小数点を切り捨てず返されます)以下はmomentjsで
diff
を行なっている箇所です。
GitHub - moment.js/moment/src/lib/moment/diff.js
厳密な日付を計算するのであればこれが最良ですが、時間を考えず単純に日にちが変わったかどうかを計算したい、といった場合も多いと思います。
startOf
を使えば、そのような機能を簡単に実装することができます。startOfとは
momentの機能の一つで、指定した時間単位の始まりの値を出力します。
startOfでday
を指定すると、日のはじまり0:00:00
に時刻を変換して出力されます。では
startOf
を用いでdiff
を使ってみます。例
const dayOne = moment('2020-01-01 10:00:00').startOf('days'); const dayTwo = moment('2020-01-02 09:00:00').startOf('days'); console.log('dayOne', dayOne.format()); console.log('dayTwo', dayTwo.format()); console.log('diff', dayTwo.diff(dayOne, 'days'));
diff
は単位以下の値が同値の場合は1
になるので、これで日にちが変わったかどうか計算することができます。アンチパターン、
Math.round
を使う小数点以下が切り捨てられるなら、
Math.round
で繰り上げるようにすればよい、という案もあります。
しかし、Math.round
は意図せぬ値まで繰り上げてしまいます。1. 第三引数にtrueを渡し、
Math.round
を使うconst dayOne = moment('2020-01-01 01:00:00'); const dayTw0 = moment('2020-01-02 00:00:00'); const dayThree = moment('2020-01-01 17:00:00'); console.log(Math.round(dayTwo.diff(dayOne, 'days', true)), dayTwo.diff(dayOne, 'days', true)); console.log(Math.round(dayThree.diff(dayOne, 'days', true)), dayThree.diff(dayOne, 'days', true));
この場合、ある程度時間がたってしまった値だと、日付が同じでも繰り上げてしまいますので使用することができません。
なので、startOf
が現状最良の手段だと思われます。
いかがでしたでしょうか?時間単位の
日
はあくまで例に使っただけなので、その他の単位でも使用することができると思います。momentはドキュメントが丁寧でわかりやすく、自動翻訳でもそれなりに見ることができるので、皆さんもよかったらご一読ください。
- 投稿日:2020-03-22T23:22:34+09:00
moment.jsで単位を日に指定してdiffを使う時、時間を考えず日が変わったかどうかだけで比較する
結論:
startOf('days')
で時間をまるめるconst dayOne = moment('2020-01-01 10:00:00').startOf('days'); const dayTwo = moment('2020-01-02 09:00:00').startOf('days'); console.log('dayOne', dayOne.format()); console.log('dayTwo', dayTwo.format()); console.log('diff', dayTwo.diff(dayOne, 'days'));はじめに
moment.jsには指定した二つの日時を比較し、出力するdiffという機能があります。
この機能は、2つの日付の差分を時間単位まで観測し、デフォルトでは
秒数
で出力されます。
diff
は第二引数で測定する時間単位を指定することが可能です。しかしその時、内部では秒数を任意の単位に再計算した値
を返しています。
また、その時小数点以下は切り捨てられてしまいますconst dayOne = moment('2020-01-01 10:00:00'); const dayTwo = moment('2020-01-02 11:00:00'); const dayThree = moment('2020-01-02 09:00:00'); const dayFour = moment('2020-01-02 10:00:00'); console.log("dayTwo", dayTwo.diff(dayOne, 'days')); // dayTwoはdayOneの時間を過ぎているので1 console.log("dayTwo asFloat", dayTwo.diff(dayOne, 'days', true)); console.log("dayThree", dayThree.diff(dayOne, 'days')); // dayThreeはdayOneの時間を過ぎてないので0 console.log("dayThree asFloat", dayThree.diff(dayOne, 'days', true)); console.log("dayFour", dayFour.diff(dayOne, 'days')); // dayFourはdayOneと同じ時間なので1 console.log("dayFour asFloat", dayFour.diff(dayOne, 'days', true));(tips: 第三引数に
true
を渡すことで値から小数点を切り捨てず返されます)以下はmomentjsで
diff
を行なっている箇所です。
GitHub - moment.js/moment/src/lib/moment/diff.js
厳密な日付を計算するのであればこれが最良ですが、時間を考えず単純に日にちが変わったかどうかを計算したい、といった場合も多いと思います。
startOf
を使えば、そのような機能を簡単に実装することができます。startOfとは
momentの機能の一つで、指定した時間単位の始まりの値を出力します。
startOfでday
を指定すると、日のはじまり0:00:00
に時刻を変換して出力されます。では
startOf
を用いでdiff
を使ってみます。例
const dayOne = moment('2020-01-01 10:00:00').startOf('days'); const dayTwo = moment('2020-01-02 09:00:00').startOf('days'); console.log('dayOne', dayOne.format()); console.log('dayTwo', dayTwo.format()); console.log('diff', dayTwo.diff(dayOne, 'days'));
diff
は単位以下の値が同値の場合は1
になるので、これで日にちが変わったかどうか計算することができます。アンチパターン、
Math.round
を使う小数点以下が切り捨てられるなら、
Math.round
で繰り上げるようにすればよい、という案もあります。
しかし、Math.round
は意図せぬ値まで繰り上げてしまいます。1. 第三引数にtrueを渡し、
Math.round
を使うconst dayOne = moment('2020-01-01 01:00:00'); const dayTw0 = moment('2020-01-02 00:00:00'); const dayThree = moment('2020-01-01 17:00:00'); console.log(Math.round(dayTwo.diff(dayOne, 'days', true)), dayTwo.diff(dayOne, 'days', true)); console.log(Math.round(dayThree.diff(dayOne, 'days', true)), dayThree.diff(dayOne, 'days', true));
この場合、ある程度時間がたってしまった値だと、日付が同じでも繰り上げてしまいますので使用することができません。
なので、startOf
が現状最良の手段だと思われます。
いかがでしたでしょうか?時間単位の
日
はあくまで例に使っただけなので、その他の単位でも使用することができると思います。momentはドキュメントが丁寧でわかりやすく、自動翻訳でもそれなりに見ることができるので、皆さんもよかったらご一読ください。
- 投稿日:2020-03-22T22:51:44+09:00
高階関数を書いたら、中級者になれた気がした。を批判したら上級者になれた気がした。をありがたく拝読した。
元記事
批判記事
高階関数を書いたら、中級者になれた気がした。を批判したら上級者になれた気がした。
休日ワイ
ワイ「おっ」
ワイ「ワイの記事に対して批判記事を書いてくださっている方がおるやんけ」
ワイ「ありがたいこっちゃ」
ワイ「いっちょ学ばせてもらおか!」音読完了
ワイ「なるほどな〜」
ワイ「大変勉強になったわ」
ワイ「必読の記事やな!」しかし気になることが
ワイ「ん?」
ワイ「なんやこのコード?」const mapArray=callback=> array=>{ for(let i=0;i<array.length;i++) array[i]=callback(array[i]); return array; }; const doubleArray=mapArray(x=>x*2); const tenMinusArray=mapArray(x=>x-10); console.log(doubleArray([1,2,3,4])) console.log(tenMinusArray([10,20,30,40]))ワイ「JavaScriptの配列には元々mapメソッドがあるから」
console.log([1,2,3,4].map(x => x * 2)) console.log([10,20,30,40].map(x => x - 10))ワイ「↑この2行だけで済むのに」
ワイ「なんで16行もコード書いてんねやろ?」
ワイ「mapArray
もdoubleArray
もtenMinusArray
も無駄ですやん」
ワイ「もしもワイがこんなコードを書いたなら・・・」ワイ「うわ!10行以上も無駄なコード書いてもうた!」
ワイ「しかも高階関数の記事なのに、mapという高階関数を再発明してもうた!」ワイ「って後悔してしまうなぁ・・・」
もしかして
ワイ「あっ!?」
ワイ「分かったで!」
ワイ「書いたことを後悔するような関数をわざと書いて」
ワイ「後悔関数と高階関数を掛けていらっしゃるんや!」
ワイ「いやー、ギャグセンスが高過ぎてかなわんわ!」
ワイ「ん、なになに?」
ワイ「このコードの説明が書いてあるで」カリー化と部分適用を使ってさらに綺麗に美しく書くことができる。
ワイ「確かに綺麗や」
ワイ「美しいくらい見事なボケやわ!」
ワイ「全体的に学びも多いし、素晴らしい記事やし」
ワイ「さすが上級者や!」〜おしまい〜
- 投稿日:2020-03-22T22:37:49+09:00
【ES6】 JavaScript初心者でもわかるPromise講座
はじめに
Promise
って...難しくないですか???
3ヶ月くらい前の私は、Promise
をほとんど理解できてないのにasync/await
とか使っちゃってたし、様々な記事を読み漁ってもなかなか理解できず、Promise
の正体を掴むのに時間がかかってしまいました。そんな3ヶ月くらい前の自分にも伝わるように、できる限り丁寧に
Promise
を説明していこうと思います。前提
本記事では、Promise以外のES6の書き方に関しては触れておりません。
アロー関数やテンプレート文字列などを例文で用いているため、わからない方がいましたら下記記事などを参考にしていただけると幸いです。
Promise
とはJavaScriptは処理を待てない!
まずはじめに、下記コードを実行してみると、どのような結果になるでしょうか。
console.log("1番目"); // 1秒後に実行する処理 setTimeout(() => { console.log("2番目(1秒後に実行)"); }, 1000); console.log("3番目");
1番目
、2番目
、3番目
...という順番で実行されることが理想的ではありますね。しかし、実際には下記のような順序で実行されてしまいます。
1番目 3番目 2番目(1秒後に実行)
JavaScriptは非同期言語であるため、一つ前の実行に時間がかかった場合、実行完了をまたずに次の処理が行われてしまいます。
Promise
は、処理の順序に「お約束
」を。
Promise
を日本語に翻訳すると「約束」です。
なので、私はPromise
のことを、処理の順序に「お約束」を取り付けることができるもの、処理を待機することや、その結果に応じて次の処理をすることお約束するものだと思っています。先ほどの例で詳しく見ていきましょう。
(コードの中身は、後ほど説明していきますので、わからなくて大丈夫です!)console.log("1番目"); // お約束を取り付けたい処理にPromise new Promise((resolve) => { //1秒後に実行する処理 setTimeout(() => { console.log("2番目(1秒後に実行)"); //無事処理が終わったことを伝える resolve(); }, 1000); }).then(() => { // 処理が無事終わったことを受けとって実行される処理 console.log("3番目"); });すると、実行結果は下記のようになります。
1番目 2番目(1秒後に実行) 3番目
Promise
を用いることで、理想の処理を実行することができました。それでは、より詳しく
Promise
をみていきましょう。
Promise
には3つの状態がある
Promise
には、PromiseStatus
というstatusがあり、3つのstatusがあります。
pending
: 未解決 (処理が終わるのを待っている状態)resolved
: 解決済み (処理が終わり、無事成功した状態)rejected
: 拒否 (処理が失敗に終わってしまった状態)
new Promise()
で作られたPromiseオブジェクトは、pendeing
というPromiseStatus
で作られます。
処理が成功した時に、PromiseStatus
はresolved
に変わり,then
に書かれた処理が実行されます。
処理が失敗した時は、PromiseStatus
がrejected
に変わり、catch
に書かれた処理が実行されます。
Promise
の大まかな処理の流れはわかりましたでしょうか。
それでは実際に書いてみましょう...!
Promise
の書き方Promiseインスタンスの作成
sample1.jsconst promise = new Promise((resolve, reject) => {});Promiseの引数には関数を渡し、その関数の第一引数に
resolve
を設定し、第二引数にreject
を任意で設定します。
(resolve
もreject
も関数です。)ちなみに、上記コードにて定義した
promise
をconsole.log
すると下記のようなPromiseオブジェクト
が返ってきます。作りたてのPromiseオブジェクトなので、
PromiseStatus
は、pendeing
ですね。
resolve
させよう
sample1
のコードをresolve
させてみましょう。// rejectは今回使わないため、引数から削除 const promise = new Promise((resolve) => { resolve(); }).then(() => { console.log("resolveしたよ"); });実行結果は下記のようになります。
resolveしたよ
resolve()
を呼び出すことによって、
PromiseStatus
はresolved
に変わり,then
の処理が走ってることがわかりますね。
resolve
関数に引数を渡してあげると...
resolve
関数は、引数を受け取ることができ、次に呼ばれるメソッド(then)の第一引数に渡してあげることができます。const promise = new Promise((resolve) => { // 引数に文字列を渡す resolve("resolveしたよ"); }).then((val) => { // 第一引数にて、resolve関数で渡した文字列を受け取ることができる console.log(val); });実行結果.resolveしたよresolve関数の引数にて渡された値が、
then
の第一引数に渡されてることがわかりますね。下記コード時点でのpromiseを確認してみましょう。
const promise = new Promise((resolve) => { // 引数に文字列を渡す resolve("resolveしたよ"); });
PromiseValue
に、resolveに渡した文字列が入ってることがわかりましたね。
PromiseValue
に格納された値を、次に呼ばれるメソッドの第一引数に渡すことができる、という仕組みになっております。
reject
させよう
sample1
のコードをreject
させてみましょう。const promise = new Promise((resolve, reject) => { reject(); }) .then(() => { console.log("resolveしたよ"); }) .catch(() => { console.log("rejectしたよ"); });実行結果は下記のようになります。
rejectしたよ
then
の処理は実行されず、catch
の処理が実行されることがわかりますね。
Promiseオブジェクト
を確認してみると...想定通り、
PromiseStatus
がrejected
に.....なってない!!!!!なぜ
rejected
にならない...?下記コード、つまり
catch
の処理を行なっていない状態のPromiseオブジェクト
をみてみましょう。const promise = new Promise((resolve, reject) => { reject(); });この時点では、
PromiseStatus
がrejected
ですね。
ちょっと安心ですね。笑(
Uncaught (in promise)
というエラーはrejectされた時に、一回もcatch
の処理が行われなかった時にでます。)実は、
catch
にて実行される関数がreturnした値をresolve
します。
めちゃくちゃ簡単にいうと、catch
はエラー返したら満足して、解決済みだ!ってするみたいです。
つまり、catchにて返されたpromiseオブジェクト
のPromiseStatus
はresolve
になります。んー、難しい。
Promiseのメソッドチェーン
処理が成功した時に
then
に書かれた処理が実行され、処理が失敗した時はcatch
に書かれた処理が実行されると説明しましたが、それらの処理の後にまた別のthen
を実行することができます。さらに、returnで返した値を第一引数として次の
then
に渡すことが可能です。resolveした場合.jsconst promise = new Promise((resolve, reject) => { resolve("test"); }) .then((val) => { console.log(`then1: ${val}`); return val; }) .catch((val) => { console.log(`catch: ${val}`); return val; }) .then((val) => { console.log(`then2: ${val}`); });実行結果.then1: test then2: test
resolve
されたため、1つめのthen
が実行されたあと、
return
でval
を受け取り、2つめのthen
が実行されました。rejectした場合.jsconst promise = new Promise((resolve, reject) => { reject("test"); }) .then((val) => { console.log(`then1: ${val}`); return val; }) .catch((val) => { console.log(`catch: ${val}`); return val; }) .then((val) => { console.log(`then2: ${val}`); });実行結果.catch: test then2: test
reject
されたため、1つめのthen
は処理されず、
1つめのcatch
が実行されたあとに2つめのthen
が実行されました。
Promise.all
とPromise.race
これらは、
Promise
を複数実行して、その結果に応じて次の処理に進む便利メソッドです。
Promise.all
Promise.all()
は配列でPromiseオブジェクトを渡し、
全てのPromiseオブジェクトがresolved
になったら次の処理に進みます。const promise1 = new Promise((resolve) => { setTimeout(() => { resolve(); }, 1000); }).then(() => { console.log("promise1おわったよ!"); }); const promise2 = new Promise((resolve) => { setTimeout(() => { resolve(); }, 3000); }).then(() => { console.log("promise2おわったよ!"); }); Promise.all([promise1, promise2]).then(() => { console.log("全部おわったよ!"); });実行結果.promise1おわったよ! promise2おわったよ! 全部おわったよ!
Promise.race
Promise.race()
はPromise.all()
と同じく配列でPromiseオブジェクトを渡し、
どれか1つのPromiseオブジェクトがresolved
になったら次に進みます。const promise1 = new Promise((resolve) => { setTimeout(() => { resolve(); }, 1000); }).then(() => { console.log("promise1おわったよ!"); }); const promise2 = new Promise((resolve) => { setTimeout(() => { resolve(); }, 3000); }).then(() => { console.log("promise2おわったよ!"); }); Promise.race([promise1, promise2]).then(() => { console.log("どれか一つおわったよ!"); });実行結果.promise1おわったよ! どれか一つおわったよ! promise2おわったよ!
async/await
を使ってもっと簡潔に!
Promise
の処理は、Promiseインスタンス
を生成したり,resolve
/reject
したりするため、場合によっては結構冗長的になってしまいます。
async/await
を使うとPromise
の処理をより簡潔に書くことができます。例に、下記の
Promise
の処理をasync/await
を使って、リファクタします。const alwaysLateBoy = (ms) => { new Promise((resolve) => { setTimeout(() => { resolve(); }, ms); }).then(() => { console.log(`すまん!${ms}ms待たせたな。`); }); };処理としては、引数で渡した
ms
秒分待機して、その後console.log
を走らせれば良さそうですね。上記の処理を
async/await
を用いると、下記のようにまとめることができます。// async function を定義 const alwaysLateBoy = async (ms) => { await new Promise((resolve) => { setTimeout(() => { resolve(); }, ms); }) // Promiseの結果が返ってくるまで実行されない console.log(`すまん!${ms}ms待たせたな。`) };
async
とは
async
は非同期関数を定義する関数宣言であり、関数の頭につけることで、
Promiseオブジェクト
を返す関数にすることができます。
そのような関数をasync function
といいます。const asyncFunc = async () => { return 1; };上記
async function
を実行すると、下記のようなPromiseオブジェクト
が返ってきます。
async function
が値をreturnした場合、Promiseは戻り値をresolveし、
その値はPromiseValue
として扱われます。
await
とは
await
は、Promiseオブジェクト
が値を返すのを待つ演算子です。
await
は必ず、async function
内で使います。下記のコードを例に考えてみましょう。
const asyncFunc = async () => { let x, y; x = new Promise(resolve => { setTimeout(() => { resolve(1); }, 1000 ) }) y = new Promise(resolve => { setTimeout(() => { resolve(1); }, 1000 ) }) console.log(x + y) }この処理では、Promiseが
resolve
するのを待つ処理を書いてないため、
console.log(x + y)
では、下記のような結果が返ってきます。実行結果...NaNこの処理を、Promiseが
resolve
するのを待つようにするには、
このPromise
オブジェクトが全て実行完了するのを待つPromise.all
を使えばできそうですが、await
を使うともっと簡単に実装できます。const asyncFunc = async () => { let x, y; // Promiseがresolveするまで待機 x = await new Promise((resolve) => { setTimeout(() => { resolve(1); }, 1000); }); // Promiseがresolveするまで待機 y = await new Promise((resolve) => { setTimeout(() => { resolve(1); }, 1000); }); console.log(x + y); };
console.log(x + y)
は、上二つのPromiseが値を返すのを待ってから実行されるため、実行結果は下記のようになります。実行結果...2おわりに
想像以上に長くなってしまい、後半からわかりづらいかもしれません。。。泣
少しでもPromise
に対して苦手意識がなくなったら良いなと思います。
(自分自身、この記事を書いていくうちに、さらにPromiseを理解できたような気がします。)参考記事
- 投稿日:2020-03-22T22:04:46+09:00
Electronでカレンダーを作る⑤
前回まで
前回はカレンダーの表示月を切り替えたら画面も切り替わるようにした。
ただ、切り替える度に画面更新で数秒真っ白になるのでダサかった。レスポンシブルな感じのUIにしたい。
HTMLを最小限にする
HTML内にカレンダーの構造をそのまま書いているのでこれをごっそり削って最小限の記述にする。
カレンダーの構造自体はJavaScriptで作っていく。before
index.html<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>ElectronCalendar</title> <script text="text/javascript" src="./js/index.js"></script> <link rel="stylesheet" href="./css/index.css"> </head> <body> <div class="calendar-wrapper"> <div class="preMonthButton"> <button type="button" id="preMonth"><先月</button> </div> <div class="nextMonthButton"> <button type="button" id="nextMonth">来月></button> </div> <table id="table" class="calendar"> <caption id="caption"></caption> <thead> <tr> <th>日</th> <th>月</th> <th>火</th> <th>水</th> <th>木</th> <th>金</th> <th>土</th> </tr> </thead> <tbody> <tr id="row1"> <td id="cell1"></td> <td id="cell2"></td> <td id="cell3"></td> <td id="cell4"></td> <td id="cell5"></td> <td id="cell6"></td> <td id="cell7"></td> </tr> <tr id="row2"> <td id="cell8"></td> <td id="cell9"></td> ...略 <td id="cell34"></td> <td id="cell35"></td> </tr> <tr id="row6"> <td id="cell36"></td> <td id="cell37"></td> <td id="cell38"></td> <td id="cell39"></td> <td id="cell40"></td> <td id="cell41"></td> <td id="cell42"></td> </tr> </tbody> </table> </div> </body> </html>after
index.html<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>ElectronCalendar</title> <script text="text/javascript" src="./js/index.js"></script> <link rel="stylesheet" href="./css/index.css"> </head> <body> <div class="calendar-wrapper"> <div class="preMonthButton"> <button type="button" id="preMonth"><先月</button> </div> <div class="nextMonthButton"> <button type="button" id="nextMonth">来月></button> </div> <div id="calendars"> </div> </div> </body> </html><div id="calendars">ってなんでこのIDにしたか謎。
index.jsを改修する
index.js'use strict'; //momentモジュール const moment = require("moment"); // カレンダーをキャッシュしておくMap let calendarMap = new Map(); // 画面に表示されている月 let currentDispMonth; window.onload = function() { //URL文字列から初期表示の月を取得 const month = parseURLParam(location.search).month; //直近3か月のカレンダー作成 createLatestCalendar(month); //カレンダーの親要素取得 const root = document.getElementById('calendars'); //子要素としてパネルを追加 const callendar = calendarMap.get(month); root.appendChild(callendar.getPanel()); //カレンダー表示 display(callendar.createContents); //先月ボタン押下時 document.getElementById('preMonth').onclick = function() { const localMoment = moment(currentDispMonth); const preMonth = localMoment.add(-1,'month').format("YYYY-MM"); const beforPanel = calendarMap.get(currentDispMonth).getPanel(); const afterCallendar = calendarMap.get(preMonth); const afterPanel = afterCallendar.getPanel(); changePanel(beforPanel, afterPanel, afterCallendar.createContents); createLatestCalendar(preMonth); }; //来月ボタン押下時 document.getElementById('nextMonth').onclick = function() { const localMoment = moment(currentDispMonth); const nextMonth = localMoment.add(1,'month').format("YYYY-MM"); const beforPanel = calendarMap.get(currentDispMonth).getPanel(); const afterCallendar = calendarMap.get(nextMonth); const afterPanel = afterCallendar.getPanel(); changePanel(beforPanel, afterPanel, afterCallendar.createContents); createLatestCalendar(nextMonth); }; }; /** * URLパラメータを分解してkey:valueの形式で返す。 * @param URL */ function parseURLParam(URL) { // URLパラメータを"&"で分離する const params = URL.substr(1).split('&'); var paramsArray = []; var keyAndValue = null; for(var i = 0 ; i < params.length ; i++) { // "&"で分離したパラメータを"="で再分離 keyAndValue = params[i].split("="); // パラメータを連想配列でセット paramsArray[keyAndValue[0]] = keyAndValue[1]; } // 連想配列パラメータを返す return paramsArray; } /** * パネルを切り替える * @param beforPanel * @param afterPanel * @param callback パネル切り替え後に実行される関数 */ function changePanel(beforPanel, afterPanel, callback) { //カレンダーの親要素取得 const root = document.getElementById('calendars'); //afterPanelでbeforPanelを置き換え root.replaceChild(afterPanel, beforPanel); display(callback); } /** * カレンダーを表示する。 * @param callback 実行される関数 */ function display(callback) { callback(); } /** * 指定した月の直近(当月、先月、来月)のCallendarオブジェクトを作成する。 */ function createLatestCalendar(month) { //当月分 if(!calendarMap.get(month)) { const callendar = new Callendar(month); callendar.createPanel(); calendarMap.set(month, callendar); } const localMoment = moment(month); //先月分 const preMonth = localMoment.add(-1,'month').format("YYYY-MM"); if(!calendarMap.get(preMonth)) { const preCallendar = new Callendar(preMonth); preCallendar.createPanel(); calendarMap.set(preMonth, preCallendar); } //今月分 const nextMonth = localMoment.add(2,'month').format("YYYY-MM"); if(!calendarMap.get(nextMonth)) { const nextCallendar = new Callendar(nextMonth); nextCallendar.createPanel(); calendarMap.set(nextMonth, nextCallendar); } } /** * カレンダークラス * @param month(YYYY-MM) */ const Callendar = function(month) { //コンストラクタ this._month = month; this._moment = moment(this._month); this._panel = {}; //コールバック関数で利用するのでthisを固定しておく const _this = this; /** * カレンダーのパネルを作成する。 */ this.createPanel = function() { // カレンダーのパネルを作成 const panel = document.createElement('div'); panel.id = 'panel'; // 子要素にテーブルを追加 const table = panel.appendChild(document.createElement('table')); table.id = 'table'; table.classList.add('calendar'); // テーブルにキャプション追加 const caption = table.appendChild(document.createElement('caption')); caption.id = 'caption'; // ヘッダー追加 const header = table.appendChild(document.createElement('thead')); // ヘッダーのカラムを作成する。 const headerRow = header.appendChild(document.createElement('tr')); headerRow.appendChild(document.createElement('th')).innerText = '日'; headerRow.appendChild(document.createElement('th')).innerText = '月'; headerRow.appendChild(document.createElement('th')).innerText = '火'; headerRow.appendChild(document.createElement('th')).innerText = '水'; headerRow.appendChild(document.createElement('th')).innerText = '木'; headerRow.appendChild(document.createElement('th')).innerText = '金'; headerRow.appendChild(document.createElement('th')).innerText = '土'; // body作成 let cellNum = 1; for (let i = 1; i < 7; i++) { const row = table.appendChild(document.createElement('tr')); row.id = 'row' + i; for(let j = 1; j < 8; j++) { const cell = row.appendChild(document.createElement('td')); cell.id = 'cell' + cellNum; cellNum++; } } this._panel = panel; }; /** * カレンダーの内容を作成する。 */ this.createContents = function() { const __moment = _this._moment; //captionを表示 document.getElementById('caption').innerText = __moment.format("YYYY年MM月"); //当月の日数を取得 const daysOfMonth = __moment.daysInMonth(); //月初の曜日を取得(index.htmlと合わせるために+1する) const firstDayOfManth = __moment.startOf('month').day() + 1; //カレンダーの各セルに日付を表示させる let cellIndex = 0; for(let i = 1; i < daysOfMonth + 1; i++) { if(i === 1) { cellIndex += firstDayOfManth; } else { cellIndex++; } document.getElementById("cell" + cellIndex).innerText = i; } //6行目の第1セルが空白なら6行目自体を非表示にする。 if(document.getElementById("cell36").innerText === "") { document.getElementById('row6').style.display = "none"; } currentDispMonth = _this._month; }; /** * パネルを取得する。 */ this.getPanel = function() { return this._panel; }; /** * 月を取得する。 */ this.getMonth = function() { return this._month; }; };修正・追加点
①カレンダーの構造を動的に作るようにする。
Calendarオブジェクトでカレンダーの構造(HTML要素)を作成して持っておくようにする。
Calendarオブジェクトは月ごとに一つ作られる。display関数にCalendarクラスのcreateContentsメソッドをコールバック関数として渡すことで画面にカレンダーの内容が表示される。
index.js/** * カレンダーを表示する。 * @param callback 実行される関数 */ function display(callback) { callback(); }※コールバック関数をそのまま実行しているだけなのでコールバックという名前は不適なので修正したい。
②ボタンを押したら画面の更新無しでカレンダーを切り替えるようにする。
カレンダーの表示の切り替えは、changePanel関数でHTML要素を置き換える。
index.js/** * パネルを切り替える * @param beforPanel * @param afterPanel * @param callback パネル切り替え後に実行される関数 */ function changePanel(beforPanel, afterPanel, callback) { //カレンダーの親要素取得 const root = document.getElementById('calendars'); //afterPanelでbeforPanelを置き換え root.replaceChild(afterPanel, beforPanel); ※ display(callback); }※replaceChildメソッドの引数はreplaceChild(置き換えたいHTML要素(Node), 置き換え対象のHTML要素)となるので注意。
③カレンダーの切り替え毎に前後一ヶ月のCalendarオブジェクトを作っておくようにする。
初期表示では今月・先月・来月分のCalendarオブジェクトを作成しておき、今月分のカレンダーの表示させる。
以後、ボタン押下毎にCalendarオブジェクトが作られていく。index.js/** * 指定した月の直近(当月、先月、来月)のCallendarオブジェクトを作成する。 * 対象月のCalendarオブジェクトがキャッシュされているならそれを使い回す。 */ function createLatestCalendar(month) { //当月分 if(!calendarMap.get(month)) { const callendar = new Callendar(month); callendar.createPanel(); calendarMap.set(month, callendar); } const localMoment = moment(month); //先月分 const preMonth = localMoment.add(-1,'month').format("YYYY-MM"); if(!calendarMap.get(preMonth)) { const preCallendar = new Callendar(preMonth); preCallendar.createPanel(); calendarMap.set(preMonth, preCallendar); } //今月分 const nextMonth = localMoment.add(2,'month').format("YYYY-MM"); if(!calendarMap.get(nextMonth)) { const nextCallendar = new Callendar(nextMonth); nextCallendar.createPanel(); calendarMap.set(nextMonth, nextCallendar); } }なんかごちゃっとしてる。
index.js//先月ボタン押下時 document.getElementById('preMonth').onclick = function() { const localMoment = moment(currentDispMonth); const preMonth = localMoment.add(-1,'month').format("YYYY-MM"); const beforPanel = calendarMap.get(currentDispMonth).getPanel(); const afterCallendar = calendarMap.get(preMonth); const afterPanel = afterCallendar.getPanel(); changePanel(beforPanel, afterPanel, afterCallendar.createContents); createLatestCalendar(preMonth); }; //来月ボタン押下時 document.getElementById('nextMonth').onclick = function() { const localMoment = moment(currentDispMonth); const nextMonth = localMoment.add(1,'month').format("YYYY-MM"); const beforPanel = calendarMap.get(currentDispMonth).getPanel(); const afterCallendar = calendarMap.get(nextMonth); const afterPanel = afterCallendar.getPanel(); changePanel(beforPanel, afterPanel, afterCallendar.createContents); createLatestCalendar(nextMonth); };こっちもごちゃっとしてる。
・キャッシュしたCalendarオブジェクトの取得方法
Calendarオブジェクトはグローバル変数のMapに保存されているので月をキーにして取り出せる。
index.js// カレンダーをキャッシュしておくMap let calendarMap = new Map();動かす
$ electron .初期表示
来月ボタン押下時
画面更新で真っ白になることが無くなった!!!(画像じゃ分からない)
TODO
・コードをもっとスッキリさせたい。
・Electronの機能をもっと使いたい。あとがき
・Javaの型で縛るプログラミングに慣れているので動的型付け言語は自由だけど不自由。
・ロジックを書くのはやっぱり楽しい。プログラミングの醍醐味だと思う。
・Electronの記事なのにほぼHTML・JSの記事になってる・・・。今回に関してはipc通信の箇所すら削っちゃってる!!!
- 投稿日:2020-03-22T21:25:29+09:00
魔法JS☆あれい 第10話「もうmapにもforEachにも頼らない」
登場人物
丹生(にゅう)あれい
魔法少女「魔法(マジカル)JS(女子小学生)☆あれい」として活動中。イテレー太
正体不明の魔法生物。第1部「ミューテーター・メソッド編」
* 第1話「popでpushした、ような……」
* 第2話「shiftはとってもunshiftって」
* 第3話「もうsortもreverseも怖くない」
* 第4話「fillも、spliceも、copyWithinもあるんだよ」第2部「アクセサ・メソッド編」
* 第5話「joinなんて、concatなわけない」
* 第6話「indexOfなの絶対lastIndexOf」
* 第7話「includesのsliceと向き合えますか?」第3部「イテレーション・メソッド編」
* 第8話「filterって、ほんとfindとfindIndex」
* 第9話「someなの、everyが許さない」map()
イテレー太「さあ、引き続き戦いの真っ最中だよ!」
あれい「いつまで続くんだよこの駄犬」
イ「召喚したデータは第8話を参照してね! そして、今回の敵の弱点は、『記事のタイトル(title
)と使用されているタグ(tags
)の名前(name
)を一覧にしたもの』だよ!」
あ「要は見出しか」
イ「そうだよ! さあ、記事の見出しをreturn
して敵を倒してよ!」
あ「面倒くせえなあ……」return items.map(item => `${item.title} (${item.tags.map(tag => tag.name).join('/')})`)あ「これでいいのか」
イ「あれい、今回も解説をお願い!」
あ「ああん? 面倒くせえなあ……だから、map()
メソッドですべての記事にアクセスしてtitle
とtags
を取得するだろ」
イ「ふむふむ」
あ「で、さらにtags
内のすべての要素にmap()
メソッドでアクセスして、name
のみの配列を作成してから、それをjoin()
メソッドで連結してるんだよ」
イ「いやー、さすがはあれい! 魔法少女コスが壊滅的に似合わないけど仕事は早いね!」
あ「しばくぞ」解説
map()
メソッドは、与えられた関数を配列のすべての要素に対して呼び出し、その結果からなる新しい配列を生成します。(MDNより)
配列のすべての要素に対してコールバック関数を実行し、返ってきた結果で新しい配列が作成されます。
コールバック関数では、filter()
メソッドと同じく、「現在の要素、現在の要素のインデックス、元の配列」という3つの引数を使用できます。forEach()
解説
forEach()
メソッドは与えられた関数を、配列の各要素に対して一度ずつ実行します。(MDNより)
さて、
forEach()
メソッドについては解説のみとなります。なぜなら、forEach()
メソッドには戻り値がないため、敵に攻撃できないからです。
配列のすべての要素に対して関数を実行する……という点ではmap()
メソッドなどと同じ動きをしますが、戻り値がないのでメソッドチェーンとして使用しづらく、map()
やreduce()
を使わずにあえてforEach()
を使うというケースが、正直言って思いつきません。
(……逆に言うと、他のオブジェクトであるMapやSetではforEach()
くらいでしか反復処理ができないけど、Arrayオブジェクトは便利なメソッドが豊富に揃っていて幸せだなー、という事なんでしょうね。多分……)あ「なんだこの試合放棄感丸出しの解説は」
イ「一応、記事のタイトルを順にコンソールに出力する例だけでも提示しておいてよ!」
あ「面倒くせえなあ……」items.forEach(item => console.log(item.title));次回予告
ずっと配列の要素達にコールバック関数を実行しながら、あなたは何も感じなかったの?
皆がどんな風にアキュムレータに代入されたか、わかってあげようとしなかったの?
- 投稿日:2020-03-22T20:46:45+09:00
vis.jsをさわってみて参考になった記事
経緯
vis.jsをさわった経緯
チームでWebアプリ開発をしようとして、ネットワーク図を書けるやつないかな~って探して見つかったのがこのvis.jsだった!紹介基準
僕が見て役に立ったもの!
僕はJavaScriptをそれまで書いたことがなく、ちゃんと文法がわかっている訳でもないです。そんな僕が紹介するものなので、結構信頼してもらっても大丈夫だと思います!(笑)参考にしたもの
vis.jsの公式ページ
これめっちゃすごくて、自分の実現したいことのコードは大体そろっています。「こういうネットワーク図書きたいな~」みたいな感じになったらとりあえずそういうものがあるかを見に行くといいと思います!公式githubからコードを取得できるのでコピペでとりあえず書けてしまいます。。。正直すごすぎてびっくりしました。
他にも3Dグラフとかいろいろあるのでやってみたいと思いました!(感想)vis.js公式ページはこちら!
https://visjs.org/vis.js ネットワーク図のサンプルページはこちら!
https://visjs.github.io/vis-network/examples/公式githubはこちら!
https://github.com/visjsQiitaの記事
ノードの名前を変更するコードが載っています!
https://qiita.com/KyoheiOkawa/items/1ca82afada3761c3be92vis.jsではなくJavaScript。ダブルクリックイベントを実装するときに助けてもらいました!
https://qiita.com/sashim1343/items/e3728bea913cadab677d基本的な書き方がしっかりのっています!!
https://qiita.com/junjis0203/items/ecc3f4eb9b3a1c4802e4終わりに
最後までありがとうございました!!!
僕が紹介したのはあくまでも僕が実装するときに役立ったもので、もっと他にも素晴らしい記事があると思うので他の記事も見てみてくださいね!
アプリとして完成すればそのリンクを追記したいと思います!
そのときにvis.jsのコードも載せようと思うのでもしよければ定期的に確認してみてください!
- 投稿日:2020-03-22T19:46:40+09:00
SvelteとElmを比べておきたい! テンプレートとview編
Stelte+Elmを学習中なもので、Vue.jsとElmを比べてみよう! テンプレートとview編に便乗させていただき、Svelte編を書かせていただきます。元記事のVue.jsとElmを対比してお読みくださいませ。
①オブジェクトの表示
Svelte
汎用ループを扱うことがディレクティブは
each
となります。コレクションのそれぞれ(each)を扱うイメージを持つと良いかと。<div id="app"> <ul> {#each items as { message }, i} <li> { message }</li> {/each} </ul> </div> <script> let items = [ { message: 'Foo' }, { message: 'Bar' } ] </script>Svelteの場合、htmlコードの中に、素のJSでitemsを用意します。
私はSvelte初心者なので、REPLで確認しながら実行しています。
https://svelte.dev/repl/
Svelteの利点
全体のコード量はVueより少し短いくらい、とっつき易いJavascriptの表現になりました。HTML側のテンプレートは少し慣れが必要ですが、数も少ないので何かを作り始める際には難しくないと思います。
Svelteの問題点
Vueと同じくやはりタイポをしてしまうと、リストの文字列が空っぽになってしまいます。この程度ならすぐ気付けるかもしれませんが、ベースがHtmlという元が文章を書くための表現方法のためシンタックスのミスなどは検知がしにくいですね。短くコンポーネント(ファイル)を保つなど人間がミスを起こしにくいような仕組みを工夫し続けなければなりません。正直、大規模な開発は厳しいかと。
② 数値の扱い
Svelte
演算が必要なところのみ素のJavaScriptを用いて、結果をHTMLが解釈し文字列を導き出します。
<div id="app"> <ul> {#each items as { num }, i} <li> x= { incl(num) }</li> {/each} </ul> </div> <script> let items = [ { num: 1 }, { num: 2 } ] const incl = (x)=>x+1 </script>Svelteの利点
表現がとても柔軟です。最初の例と変わらずに記述することができました。
Svelteの問題点
柔軟が故に以下のようなミスも起こし易いです。が、REPLで直ぐに結果を確認しておけばミスは減るでしょう。
③ 条件付き繰り返し
Svelte
Vueでは何やら留意点があるようですが、素のJSでロジックを書くSvelteでは淡々と記述するのみです。
<ul> {#each evenNumbers as x} <li> { x }</li> {/each} </ul> <script> const nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] const evenNumbers = nums.filter(x => x % 2 === 0) </script>Svelteの利点
依然として表記としては短く簡単ですね。
Vueの問題点
なんか、このあたりVue、つらそうで可哀想。
私のまとめ
Svelteは初期の学習コストが低く、Vueのように独自に覚えることが少ない点は良い点です。しかし、複雑性が増した時にJSで頑張って書くことになります。Vueと同じく、副作用も柔軟に書けることの利点が問題点に跳ね返ってくることがしばしばありそうです。その時にどう工夫をして解決していくかが課題となると思います。
Elmとの比較は、元記事を読みましょう!私はある規模を越えたらelmを使う!
すなわち、Svelte+elm =Sveltelm ;-)そして、Thanks, @ababup1192さん!
- 投稿日:2020-03-22T19:34:15+09:00
ReactNative + TypeScript + React HooksでFlatList使うときにrenderItemの型でハマった時の解決方法
導入
TypeScript無しでは、FlatListを書いたことあるので、スラスラ書けるかなと思って、
TypeScriptでFlatListを書いていました。しかし、TypeScriptのエラーに引っかかりまくり、なかなか解決できなかったので、
その解決方法を共有する目的で記事を書いています。前提
VersionとFlatListについての前提情報を記載します。
Version
- ReactNative: 0.59.8
- Expo: 35.0.0
FlatListとは?
スクロールできるリストのこといいます。
ReactNativeで作成するアプリで、リスト形式で表示したい時は多くの場合利用すると思います。ハマったこと
サンプルコードは以下の通りなのですが、ハマったのは、
FlatList
内のrenderItem
の型です。
TypeScriptを導入していなければ以下のサンプルコードのままでいいのですが、
TypeScriptを導入していると以下のコードでは怒られてしまいます。修正前のコード
import React from 'react'; import { SafeAreaView, View, FlatList, StyleSheet, Text } from 'react-native'; import Constants from 'expo-constants'; const DATA = [ { id: '1', title: 'アイテム1', }, { id: '2', title: 'アイテム2', }, { id: '3', title: 'アイテム3', }, { id: '4', title: 'アイテム4', }, { id: '5', title: 'アイテム5', }, { id: '6', title: 'アイテム6', }, { id: '7', title: 'アイテム7', }, ]; const Item = ({ title }) => { return ( <View style={styles.item}> <Text style={styles.title}>{title}</Text> </View> ); }; export default App = () => { return ( <SafeAreaView style={styles.container}> <FlatList data={DATA} renderItem={({ item }) => <Item title={item.title} />} keyExtractor={item => item.id} /> </SafeAreaView> ); } const styles = StyleSheet.create({ container: { flex: 1, marginTop: Constants.statusBarHeight, }, item: { backgroundColor: 'red', padding: 20, marginVertical: 8, marginHorizontal: 16, }, title: { fontSize: 32, }, });怒られたエラーメッセージ
Type
ListRenderItemInf<any>
is missing ...
ListRenderItemInfo<any>
が存在しないよというエラーが発生しました。解決方法
解決方法は、ListRenderItemInfo型をreact-naiveからimportしてきて、
FlatListのrenderItem
propsの型として定義してあげる方法です。修正後のコード
import React from 'react'; // ListRenderItemInfoをimportしてきます import { SafeAreaView, View, FlatList, StyleSheet, Text, ListRenderItemInfo } from 'react-native'; import Constants from 'expo-constants'; const DATA = [ { id: '1', title: 'アイテム1', }, { id: '2', title: 'アイテム2', }, { id: '3', title: 'アイテム3', }, { id: '4', title: 'アイテム4', }, { id: '5', title: 'アイテム5', }, { id: '6', title: 'アイテム6', }, { id: '7', title: 'アイテム7', }, ]; const Item = ({ title }) => { return ( <View style={styles.item}> <Text style={styles.title}>{title}</Text> </View> ); }; // Itemはどのような型なのかを設定します。 // interfaceなので慣習的にIをつけるので、IItemとします。 export interface IItem{ id: number; title: string; } // _renderItemというメソッドを作成します。 // ListRenderItemInfoを型に設定します。 const _renderItem = (listRenderItemInfo: ListRenderItemInfo<IItem>) => { return <Item title={listRenderItemInfo.item.title} />; }; export default App = () => { return ( <SafeAreaView style={styles.container}> <FlatList data={DATA} // 先程作成した_renderItemメソッドを当て込みます renderItem={_renderItem} keyExtractor={item => item.id} /> </SafeAreaView> ); } const styles = StyleSheet.create({ container: { flex: 1, marginTop: Constants.statusBarHeight, }, item: { backgroundColor: 'red', padding: 20, marginVertical: 8, marginHorizontal: 16, }, title: { fontSize: 32, }, });まとめ
TypeScriptでは思わぬところで、ハマってしまうことが多いですよね。
renderItemにも型が決められていると気づくことができれば、一瞬で解決できるのですが、焦ってしまい少し時間がかかってしまいました。また、ReactNativeの情報はまだ日本語で不足しているので、日本語では解決方法を見つけることができませんでした。
同じような構成でアプリを作成している皆さんの助けになれば幸いです。参考
https://reactnative.dev/docs/flatlist
https://github.com/ranuseh/TraderJoeProjectFrontend/blob/e24a9fc5372356a24028c9029a00627d3e9fae3c/app/screens/shoppingList.screen.tsx
- 投稿日:2020-03-22T19:18:55+09:00
historyオブジェクトについて
課題
- historyオブジェクトを利用して、ページをリダイレクトしたいがreduxを利用すると上手く動かない。
- console.logを仕込んでみると、どうやらhistoryオブジェクト自体がpropsとして渡されていないようだった。
環境
- node v12.7.0
- express 4.16.1
- react 16.12.0
- npm 6.10.2
- macOS Mojave 10.14.4
解決方法
reduxとreact-router-domのみを利用するとhistoryが利用できないので、reduxとreact-routerの統合が必要となる。
上記を実現するためにConnected React Routerというライブラリをインストールする。
(react-router-reduxは現在、非推奨)
引用元/@nacam403さんの記事実装の流れ
- まず、npmで上記のライブラリを全てインストールします。
npm install --save redux npm install --save react-router-dom npm install --save connected-react-router npm install --save redux-thunk
- index.jsを実装します。
index.jsimport React from 'react'; import ReactDOM from 'react-dom'; import { createStore, applyMiddleware } from 'redux' import { createBrowserHistory } from "history" import { routerMiddleware, ConnectedRouter } from 'connected-react-router' import thunk from "redux-thunk" import './index.css'; import App from './App'; import * as serviceWorker from './serviceWorker'; import rootReducers from './reducers' const history = createBrowserHistory() const store = createStore( rootReducers(history), applyMiddleware( thunk, routerMiddleware(history), ) ) ReactDOM.render(<App store={store} history={history} />, document.getElementById('root')); serviceWorker.unregister();
- App.jsを実装します。
AddFormでデータを登録して、CharacterListで一覧を表示する、というシンプルな構成です。
App.jsimport React, {Component} from 'react' import {Switch, Route} from 'react-router-dom' import { Provider } from "react-redux" import { ConnectedRouter } from 'connected-react-router' import AddForm from './crud_components/AddForm' import CharacterList from './crud_components/CharacterList' class App extends Component { render() { return ( <Provider store={this.props.store}> <ConnectedRouter history={this.props.history}> <Switch> <React.Fragment> <div className="App"> <div className="container"> <Route exact path="/addform" render={() => <AddForm store={this.props.store} history={this.props.history}/>}/> <Route exact path="/characterlist" render={() => <CharacterList store={this.props.store}/>}/> </div> </div> </React.Fragment> </Switch> </ConnectedRouter> </Provider> ); } } export default App;
- AddForm.jsを実装します。
データ登録時のメソッドのみ抜粋します。ここでリダイレクトしたいpathをhistoryにpushします。
AddForm.jsonSubmit(e){ e.preventDefault() // フォームsubmit時のデフォルトの動作を抑制 const newCharacter = { name: this.state.name, age: this.state.age } const store = this.props.store; character(newCharacter).then(res => { if (res) { this.props.history.push('/characterlist') store.dispatch(initializeForm()) // submit後はフォームを初期化 } }) }まとめ
これでhistoryが利用できるようになりました。
しかし、historyでリダイレクトする際にstoreのデータを引き継ぎできないという課題が残るため、引き続き調べていきます。
- 投稿日:2020-03-22T17:05:45+09:00
Nuxt.js 存在しないページを一括でリダイレクトしたい時
/pages/*.vue
を下記の内容で作成する。<template> <div> 404 redirect </div> </template> <script> export default { middleware({ redirect }) { return redirect('/') } } </script>That's it.
Relax, man.
- 投稿日:2020-03-22T17:01:35+09:00
React FCでFileを『Content-type: multipart/form-data』でPOSTする方法(axios)
案件でPOSTする際、
『Content-type: form-data』で送信する機会があったので、まとめます。
ボタン部分はマテリアルUIを使っています、初見の方は細かく気にしなくても大丈夫です環境
react 16.12.0
typescript 3.7.3
material-ui/core 4.8.0(Buttonに使用)したいこと
inputで選択したファイルをstateにセット、セットしたファイルを POSTする
全体
const IconUpload: FC = () => { const [userIconFormData, setUserIconFormData] = useState<File>() const handleSetImage = (e: ChangeEvent<HTMLInputElement>) => { if (!e.target.files) return const iconFile:File = e.target.files[0] setUserIconFormData(iconFile) } const handleSubmitProfileIcon = () => { const iconPram = new FormData() if (!userIconFormData) return iconPram.append('user[icon]', userIconFormData) axios .post( 'https://api/update', iconPram, { headers: { 'content-type': 'multipart/form-data', }, } ) } return ( <form> <p>アイコンアップロード</p> <input type="file" accept="image/*,.png,.jpg,.jpeg,.gif" onChange={(e: ChangeEvent<HTMLInputElement>) => handleSetImage(e)} /> <Button text="変更する" variant="contained" color="primary" type="button" onClick={handleSubmitProfileIcon} disabled={userIconPreview === undefined} /> </form> ) } export default IconUpload切り分けて解説
form周り、ボタン部分
<form> <p>アイコンアップロード</p> <input type="file" accept="image/*,.png,.jpg,.jpeg,.gif" onChange={(e: ChangeEvent<HTMLInputElement>) => handleSetImage(e)} /> <Button text="変更する" variant="contained" color="primary" type="button" onClick={handleSubmitProfileIcon} disabled={userIconPreview === undefined} /> </form>input
- type:fileにすることによってアップロードが可能になる
- accept:アップロード画面で、ここに指定された拡張子のファイルのみ選択可能になる
- onChange:通常のinputと異なり、valueにはFileを入れることはできない、なので別の場所に取得したFileを保持するfunctionを呼び出す
Button
- onClick:onChangeでセットしたstateをaxiosでPOSTするfunctionを呼び出す
ファイルを取得するfunction
const handleSetImage = (e: ChangeEvent<HTMLInputElement>) => { if (!e.target.files) return const iconFile:File = e.target.files[0] setUserIconFormData(iconFile) }
if (!e.target.files) return
TypeScriptでは、undefindになる可能性のある値に関してはエラーがでるので先に無い場合はreturnすることを明示的にしている
const iconFile:File = e.target.files[0]
これがファイル本体。
filesは複数選択可能の場合を備え、配列になっていて、[0]としてあげないと取得できない。
型はFile。
setUserIconFormData(iconFile)
stateにセットちなみに、
e.target.files[0]
をURLにしてimgに入れたいとなるとconst blobUrl = URL.createObjectURL(iconFile)
blob:http://パス
に変換され、URLをしてとして扱うことができます。ファイルをaxiosでPOSTするfunction
const createProfileIcon = () => { const iconPram = new FormData() if (!userIconFormData) return iconPram.append('user[icon]', userIconFormData) axios .post( 'https://api/update', iconPram, { headers: { 'content-type': 'multipart/form-data', }, } ) }ようやくAPI送信部分
content-type: multipart/form-data
で送信する際、気をつけること2つ1.
FormData
という形式で送ってあげなければいけない。
const iconPram = new FormData()
FormDataオブジェクトを作成
型はそのまんまで、FormData
iconPram.append('user[icon]', userIconFormData)
作成したFormDataオブジェクトにパラメーターを追加
append(キー, 送信したいファイル)2. headersに
'content-type': 'multipart/form-data'
を指定基本API通信する時はjsonが多いかと思いますが、jsonでは正しく送れず、空になるので注意
_axios.create
を使って、Classでまとめている場合にも、className.defaults.headers = { 'Content-Type': 'multipart/form-data' }で、上書きしてあげましょう
送信内容
こんな感じです。
確認ポイント
- RequestHeadersの
content-type
がmultipart/form-data
になっている- FormDataのvalueが
(binary)
になっているまとめ
これで正常にmultipart/form-dataでファイルをPOSTできるようになりました!
WEBサービスを作成する際、画像のアップロードはよく出でくるシチュエーションだとおもいますので、参考になれば幸いです初めてこの機能を実装する際には迷う部分がたくさんあって、ファイルのPOSTする形も何種かあると思っていましたが、2種類のみのようです。
- Fileのまま送信(multipart/form-data)
- Base64にエンコードして送信(application/json)
エンコードするとjsonのまま送れるメリットがあるのですが、ファイルサイズが20~30%あがるそうです。
やればやるほど奥が深く、正解がなくて迷いますが、楽しいですね
- 投稿日:2020-03-22T17:01:35+09:00
React FCで画像ファイルを『Content-type: multipart/form-data』でPOSTする方法(axios)
案件でPOSTする際、
『Content-type: form-data』で送信する機会があったので、まとめます。
ボタン部分はマテリアルUIを使っています、初見の方は細かく気にしなくても大丈夫です環境
react 16.12.0
typescript 3.7.3
material-ui/core 4.8.0(Buttonに使用)したいこと
inputで選択したファイルをstateにセット、セットしたファイルを POSTする
全体
const IconUpload: FC = () => { const [userIconFormData, setUserIconFormData] = useState<File>() const handleSetImage = (e: ChangeEvent<HTMLInputElement>) => { if (!e.target.files) return const iconFile:File = e.target.files[0] setUserIconFormData(iconFile) } const handleSubmitProfileIcon = () => { const iconPram = new FormData() if (!userIconFormData) return iconPram.append('user[icon]', userIconFormData) axios .post( 'https://api/update', iconPram, { headers: { 'content-type': 'multipart/form-data', }, } ) } return ( <form> <p>アイコンアップロード</p> <input type="file" accept="image/*,.png,.jpg,.jpeg,.gif" onChange={(e: ChangeEvent<HTMLInputElement>) => handleSetImage(e)} /> <Button text="変更する" variant="contained" color="primary" type="button" onClick={handleSubmitProfileIcon} disabled={userIconPreview === undefined} /> </form> ) } export default IconUpload切り分けて解説
form周り、ボタン部分
<form> <p>アイコンアップロード</p> <input type="file" accept="image/*,.png,.jpg,.jpeg,.gif" onChange={(e: ChangeEvent<HTMLInputElement>) => handleSetImage(e)} /> <Button text="変更する" variant="contained" color="primary" type="button" onClick={handleSubmitProfileIcon} disabled={userIconPreview === undefined} /> </form>input
- type:fileにすることによってアップロードが可能になる
- accept:アップロード画面で、ここに指定された拡張子のファイルのみ選択可能になる
- onChange:通常のinputと異なり、valueにはFileを入れることはできない、なので別の場所に取得したFileを保持するfunctionを呼び出す
Button
- onClick:onChangeでセットしたstateをaxiosでPOSTするfunctionを呼び出す
ファイルを取得するfunction
const handleSetImage = (e: ChangeEvent<HTMLInputElement>) => { if (!e.target.files) return const iconFile:File = e.target.files[0] setUserIconFormData(iconFile) }
if (!e.target.files) return
TypeScriptでは、undefindになる可能性のある値に関してはエラーがでるので先に無い場合はreturnすることを明示的にしている
const iconFile:File = e.target.files[0]
これがファイル本体。
filesは複数選択可能の場合を備え、配列になっていて、[0]としてあげないと取得できない。
型はFile。
setUserIconFormData(iconFile)
stateにセットちなみに、
e.target.files[0]
をURLにしてimgに入れたいとなるとconst blobUrl = URL.createObjectURL(iconFile)
blob:http://パス
に変換され、URLをしてとして扱うことができます。ファイルをaxiosでPOSTするfunction
const createProfileIcon = () => { const iconPram = new FormData() if (!userIconFormData) return iconPram.append('user[icon]', userIconFormData) axios .post( 'https://api/update', iconPram, { headers: { 'content-type': 'multipart/form-data', }, } ) }ようやくAPI送信部分
content-type: multipart/form-data
で送信する際、気をつけること2つ1.
FormData
という形式で送ってあげなければいけない。
const iconPram = new FormData()
FormDataオブジェクトを作成
型はそのまんまで、FormData
iconPram.append('user[icon]', userIconFormData)
作成したFormDataオブジェクトにパラメーターを追加
append(キー, 送信したいファイル)2. headersに
'content-type': 'multipart/form-data'
を指定基本API通信する時はjsonが多いかと思いますが、jsonでは正しく送れず、空になるので注意
_axios.create
を使って、Classでまとめている場合にも、className.defaults.headers = { 'Content-Type': 'multipart/form-data' }で、上書きしてあげましょう
送信内容
こんな感じです。
確認ポイント
- RequestHeadersの
content-type
がmultipart/form-data
になっている- FormDataのvalueが
(binary)
になっているまとめ
これで正常にmultipart/form-dataでファイルをPOSTできるようになりました!
WEBサービスを作成する際、画像のアップロードはよく出でくるシチュエーションだとおもいますので、参考になれば幸いです初めてこの機能を実装する際には迷う部分がたくさんあって、ファイルのPOSTする形も何種かあると思っていましたが、2種類のみのようです。
- Fileのまま送信(multipart/form-data)
- Base64にエンコードして送信(application/json)
エンコードするとjsonのまま送れるメリットがあるのですが、ファイルサイズが20~30%あがるそうです。
やればやるほど奥が深く、正解がなくて迷いますが、楽しいですね
- 投稿日:2020-03-22T16:48:11+09:00
YouTube Liveのスパムを自動で非表示にするChrome拡張機能を作った話
動機
YouTube Live Spam Killer - Chrome ウェブストア
突然ですが、自分はバーチャルYouTuber(VTuber)のライブストリーミングやそのアーカイブを複数窓垂れ流して作業するのが大好きです。
しかし、最近そのライブチャット欄に品のないユーザ名の出会い系スパムが多発しています。
これらのブロック作業で配信に集中できなかったりといった面倒が目立つようになりました。
せっかくの至福の時間を邪魔される、というのは人間誰しも嫌なものです。人間様がクソプログラムに苛つかせられる必要はありません。
技術でなんとかしましょう。YouTubeを利用している関係上、ブラウザ上でバックグラウンド動作してくれるタイプの技術を使うほうが見通しは良いです。
したがって今回、自分がメインで利用しているウェブブラウザであるGoogle Chrome上の拡張機能として実装することにしました。なお、手元の事前調査などは使い慣れているPythonを利用しているため、Python(のREPLであるiPython)とJavaScriptのコードが混在する点のみご承知おきください。
実験
事前調査
これらのスパムは数種のパターンが存在しますが、大方以下のような特徴を持っています。
- ユーザ名の形式:
(英語圏の女性名) (スパムユーザのアカウントに誘導する文言)
- 例:
Lilah ʜ0'ᴛ ᴄʜᴀᴛ ᴊᴏɪɴ ᴍᴇ [ɪ ᴍ ʟɪᴠᴇ]
- 最近ではコロナウィルス周りの文言になったスパムも登場している
- 直前のチャット欄に現れた(人間の)コメントをコピーし、絵文字を追加したコメントを行う
上記の例で示したように、『スパムユーザのアカウントに誘導する文言』は単純なフィルタで取り除けないように、プレーンな英語ではなく記号類を用いています。
In [1]: import unicodedata In [2]: target = "Lilah ʜ0'ᴛ ᴄʜᴀᴛ ᴊᴏɪɴ ᴍᴇ [ɪ ᴍ ʟɪᴠᴇ]" In [3]: for i, c in enumerate(target): ...: print(i, '"{}"'.format(c), '{:04x}'.format(ord(c)), ...: unicodedata.category(c), unicodedata.name(c)) ...: 0 "L" 004c Lu LATIN CAPITAL LETTER L 1 "i" 0069 Ll LATIN SMALL LETTER I 2 "l" 006c Ll LATIN SMALL LETTER L 3 "a" 0061 Ll LATIN SMALL LETTER A 4 "h" 0068 Ll LATIN SMALL LETTER H 5 " " 0020 Zs SPACE 6 "ʜ" 029c Ll LATIN LETTER SMALL CAPITAL H 7 "0" 0030 Nd DIGIT ZERO 8 "'" 0027 Po APOSTROPHE 9 "ᴛ" 1d1b Ll LATIN LETTER SMALL CAPITAL T 10 " " 0020 Zs SPACE 11 "ᴄ" 1d04 Ll LATIN LETTER SMALL CAPITAL C 12 "ʜ" 029c Ll LATIN LETTER SMALL CAPITAL H 13 "ᴀ" 1d00 Ll LATIN LETTER SMALL CAPITAL A 14 "ᴛ" 1d1b Ll LATIN LETTER SMALL CAPITAL T 15 " " 0020 Zs SPACE 16 "ᴊ" 1d0a Ll LATIN LETTER SMALL CAPITAL J 17 "ᴏ" 1d0f Ll LATIN LETTER SMALL CAPITAL O 18 "ɪ" 026a Ll LATIN LETTER SMALL CAPITAL I 19 "ɴ" 0274 Ll LATIN LETTER SMALL CAPITAL N 20 " " 0020 Zs SPACE 21 "ᴍ" 1d0d Ll LATIN LETTER SMALL CAPITAL M 22 "ᴇ" 1d07 Ll LATIN LETTER SMALL CAPITAL E 23 " " 0020 Zs SPACE 24 "[" 005b Ps LEFT SQUARE BRACKET 25 "ɪ" 026a Ll LATIN LETTER SMALL CAPITAL I 26 " " 0020 Zs SPACE 27 "ᴍ" 1d0d Ll LATIN LETTER SMALL CAPITAL M 28 " " 0020 Zs SPACE 29 "ʟ" 029f Ll LATIN LETTER SMALL CAPITAL L 30 "ɪ" 026a Ll LATIN LETTER SMALL CAPITAL I 31 "ᴠ" 1d20 Ll LATIN LETTER SMALL CAPITAL V 32 "ᴇ" 1d07 Ll LATIN LETTER SMALL CAPITAL E 33 "]" 005d Pe RIGHT SQUARE BRACKETこれはまだ素直な方ですが、ものによってはUnicodeの隅を突くような記号を突っ込んでくることもあります。
このような手口に対応する手法はいくつか考えられますが、この場合正規化を行い適当な英語表記に変換してやるのが一番楽でしょう。正規化概要
具体的な手法としては、次に示すような連想配列を用いてユーザ名を逐一英語小文字に変換していく手法を採っています。
const NORMALIZE_MAP = { a: ['ᴀ', 'ᗩ'], c: ['ᴄ', 'ᑕ'], e: ['3', 'ǝ', 'ᴇ', 'Ꭼ', 'є'], h: ['ʜ', 'н', 'ᕼ'], i: ['ɪ', 'ι', 'Ꭵ', 'í'], j: ['ᴊ'], k: ['ᴋ'], l: ['ʟ', 'Ꮮ', 'ᒪ'], m: ['ᴍ', 'ᗰ'], n: ['ɴ', 'ᑎ'], o: ['0', 'ᴏ'], p: ['ᴘ'], r: ['ʀ'], s: ['ꜱ'], t: ['т', 'ᴛ'], v: ['Ꮙ', 'ᴠ'], } // 執筆時のバージョン var normalizeWord = word => { // 単語単位で正規化を行う word = word.replace(/[!-/:-@\[-`{-~]/g, '') // 約物の除去 Object.keys(NORMALIZE_MAP).forEach(key => { NORMALIZE_MAP[key].forEach(item => { word = word.replace(new RegExp(item, 'g'), key) }) }) return word.toLowerCase() // 英語圏以外の文字に作用し、連想配列をすり抜けてしまう事象が確認されたため、 // 後付けでtoLowerCase()を作用させている }スパム検知手法
さらに、予め用意しておいたブラックリスト単語に合致するかを調べます。
const BLACKLIST_WORDS = ['chat', 'click', 'colona', 'here', 'hot', 'join', 'live', 'love', 'mask', 'me', 'photo', 'sex', 'tap'] // 執筆時のバージョン先頭の人名を除去した部分のユーザ名を正規化し、これらのブラックリスト単語が多くヒットすればスパムと見なします。
旧検知手法
別の検出方法として、複数の言語圏の文字を利用していることを検出する手法も考えていました。
しかし、これはあまり一般性がないことが判明したためボツとしました。ソース
https://github.com/Le96/youtube-live-spam-killer
参考
参考にした記事一覧です。
JavaScriptはほぼ触った経験がなかったので基本的なことも紛れています。言語仕様
Array.filterが使えないNodeListに対してフィルターをかける方法 - Qiita
JavaScript で forEach を使うのは最終手段 - Qiita
Javascript 連想配列(オブジェクト)をforEachでループさせたい。 - かもメモ
Javascript:任意の文字列を含むかどうかの確認方法 | WWWクリエイターズ
JavaScriptにおける連想配列のforループ操作 - Qiita
JSのArray.prototype.filter.callとArray.from().filterどっちが早いか - Qiita
Unicode Property Escapesについての補説
Unicodeの文字プロパティを指定した正規表現をみてみる(ECMAScript2018) - TES Blog
特定の文字列を全て置換する[Javascript] - Qiita(jQueryを利用しない)DOM操作
【Javascript】setInterval()で要素が現れるまで待つ - Qiita
iframeのonloadのタイミングでiframeのソースを確認する - みかづきブログ その3
JavaScript - js読み込みの際はnullになるが、devツールのコンソールではnullにならない|teratail
javascript - 読み込み完了の取得について - スタック・オーバーフロー
JavaScriptのMutationObserverでDOMの変化を監視する方法 - Qiita
ライブラリを使わない素のJavaScriptでDOM操作 - Qiita
要素の取得方法まとめ - QiitaChrome拡張機能
Chrome Extension を作って公開する - Qiita
Chrome 拡張機能のマニフェストファイルの書き方 - Qiita
Chromeブラウザの拡張機能を作ってみたい初心者向けに開発方法を紹介!【サンプルあり】 - Qiita
Chrome拡張 備忘録 1から10まで - Qiita
Chrome拡張の開発方法まとめ その1:概念編 - Qiita類似の試み a.k.a.先行研究
YouTube Live の絵文字スパムをブロックする - Qiita
YouTubeコピペスパムを抽出する(モデレータなら自動ブロック?) - Qiitaアイコン
コメントアイコン7 | アイコン素材ダウンロードサイト「icooon-mono」 | 商用利用可能なアイコン素材が無料(フリー)ダウンロ ードできるサイト
あとがき
本当はスパム報告・ブロックまで行いたかったのですが、手元のアイデアでは実装方法が浮かびませんでした。
ご教示いただければ非常に助かります。また、想定の甘さや新種スパムの発生などに伴い、False Positive/False Negativeなど発生している場合もぜひご教示ください。
- 投稿日:2020-03-22T16:34:30+09:00
DangerとGitHub ActionsでPull Request時に最低限のレビューを自動化する
概要
チーム開発において
Pull Request
時のコードレビューは必要不可欠です。
コードレビューにはもちろん、レビューワーの工数がかかります。
そこで、レビューワーの負担を少なくするために、
GitHub Actions
とDanger
で必要最低限のレビューを自動化し、本質的な部分のみ
レビューワーがチェックするようにしたいと思います。実装
ディレクトリ構造例
treedanger-github-actions ├── .eslintrc.js ├── .github | └── workflows | └── actions.yml ├── src | ├── greeting.js | └── greeting.test.js ├── dangerfile.js ├── package.json └── yarn.lockDanger
dangerfile.js
をルートディレクトリ配下に作成します。
Danger
はTypeScript
にも対応しているため、dangerfile.ts
としても問題ありません。danger.jslet isAllCheckPassed = true; if (danger.github.pr.title.includes('[WIP]')) { fail("Should NOT inclued 'WIP' in your PR title"); } if (!danger.github.pr.assignee) { warn("Should select PR reviewer"); isAllCheckPassed = false; } const hasIssuesNumber = /#[0-9]/.test(danger.github.pr.title); if (!hasIssuesNumber) { warn("Should include issues number in your PR title"); isAllCheckPassed = false; } const diffSize = Math.max(danger.github.pr.additions, danger.github.pr.deletions); if (diffSize > 500) { warn("Should reduce diffs less than 500"); isAllCheckPassed = false; } if (danger.github.pr.changed_files > 10 ) { warn("Should reduce change files less than 10"); isAllCheckPassed = false; } if (isAllCheckPassed) markdown('## All checkes have passed :tada:')チェック項目
- PR タイトルに'[WIP]'が含まれていないか
- レビューワー(assignree)が選択されているか
- PR タイトルにIssues番号(ex. #123)が含まれているか
- 追加行と削除行の合計が500行以下か
- 編集ファイル数が10ファイル以下か
fail()
を呼び出すとci
は失敗し、マージできないようになります。
warn()
の場合は警告を表示しますが、マージは可能です。今回は使用していませんが
message()
でメッセージ表示ができます。
markdown()
とすることでmarkdown
にも対応できます。package.json
GitHub Actions
で実行したいコマンドをpackage.json
に登録しておくと良いでしょう。package.json{ "scripts": { "lint": "eslint --ext .js ./src", "test": "jest", "danger": "danger ci" } }actions.yml
GitHub Actions
で実行したいworkflow
を記述します。.github/workflows/actions.ymlname: actions on: [pull_request] jobs: actions-ci: runs-on: ubuntu-latest steps: - name: Checkout Repository uses: actions/checkout@v2 - name: Prepare Node Env uses: actions/setup-node@v1 with: node-version: '12.x' - name: Install yarn run: npm i -g yarn - name: Install Dependencies run: yarn - name: Run ESLint run: yarn lint - name: Run Jest run: yarn test - name: Run Danger run: yarn danger:ci env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}実行結果
Pull Request
を作成するとGitHub Actions
が実行され、
下記のように結果がコメントとして投稿されます。最後に
ESLint
のように設定をカスタマイズすることができます。
Pull Request
作成時にもチームとしてルールを設けて見てはいかがでしょうか ☺️refs
- 投稿日:2020-03-22T16:25:48+09:00
【vue.js】3桁ごとにカンマを入れる正規表現【ES6】
3000000→3,000,000を表示されるようにしたい
index.html<body> <div id="app"> <p> 合計 {{ sum | numberWithDelimiter }}円 </p> </div> <script src="js/index.js"></script> </body>index.jsnew Vue({ el: '#app', data() { return { sum: 3000000 } }, filters: { numberWithDelimiter(value) { if(!value) return '0' return value.toString().replace(/(\d)(?=(\d{3})+$)/g, '$1, ') } } }) window.vm = vm
- 投稿日:2020-03-22T15:55:55+09:00
魔法JS☆あれい 第9話「someなの、everyが許さない」
登場人物
丹生(にゅう)あれい
魔法少女「魔法(マジカル)JS(女子小学生)☆あれい」として活動中。イテレー太
正体不明の魔法生物。第1部「ミューテーター・メソッド編」
* 第1話「popでpushした、ような……」
* 第2話「shiftはとってもunshiftって」
* 第3話「もうsortもreverseも怖くない」
* 第4話「fillも、spliceも、copyWithinもあるんだよ」第2部「アクセサ・メソッド編」
* 第5話「joinなんて、concatなわけない」
* 第6話「indexOfなの絶対lastIndexOf」
* 第7話「includesのsliceと向き合えますか?」第3部「イテレーション・メソッド編」
* 第8話「filterって、ほんとfindとfindIndex」some()
イテレー太「さあ、戦いはまだまだ続くよ!」
あれい「ちょっと休憩させろよ駄犬」
イ「召喚したデータは前回を参照してね! そして、今回の敵の弱点は、『'JavaScript'というタグを含む記事一覧』だよ! さあ、条件に合致するデータをreturn
して敵を倒してよ!」
あ「面倒くせえなあ……」return items.filter(item => item.tags.some(tag => tag.name === 'JavaScript'))あ「これでいいのか」
イ「あれい、また解説をお願い!」
あ「ああん? 面倒くせえなあ……だから、それぞれの記事アイテムのtags
プロパティをsome()
メソッドで検索して、name
が'JavaScript'のタグが1個でもあればtrue
を返してだな、それをfilter()
メソッドで抽出してるんだよ」
イ「さすがはあれい! 口は悪いけど仕事は早いね!」
あ「やかましいわ」解説
some()
メソッドは、配列の少なくとも 1 つの要素が、渡された関数によって実施されるテストに通るかどうかをテストします。(MDNより)
配列のすべての要素に対してテスト関数を実行し、条件に合致する要素が1つでもあれば
true
を、なければfalse
を返します。
テスト関数に渡される引数はfilter()
メソッドと同じです。every()
イ「問題。『本文(
body
)中に'JavaScript'という文字列を含む記事は、必ず'JavaScript'というタグが付けられている』。○か×か?」
あ「なんだよ急に。ウルトラクイズか」
イ「なんで女子小学生がウルトラクイズを知ってるのかは置いといて、その答えが次の敵の弱点だよ! さあ、答えを調査して、敵を倒してよ!」
あ「面倒くせえ事この上ねえな……」
イ「頑張れ! あれい!」return items.filter(item => /JavaScript/.test(item.body)) .every(item => item.tags.some(tag => tag.name === 'JavaScript'));あ「これでいいのかよ」
イ「あれい、また解説をお願い!」
あ「ああん? 面倒くせえなあ……だから、まずは本文中に'JavaScript'という文字列を含む記事だけを抽出してだな、次にその記事が'JavaScript'タグを含むかをsome()
メソッドで判定し、最後にすべてがtrue
を返したかをevery()
メソッドで判定してるんだよ」
イ「さすがはあれい! 女子小学生らしさは皆無だけど仕事は早いね!」
あ「黙れ」解説
every()
メソッドは、与えられた関数によって実行されるテストに、配列のすべての要素が通るかどうかをテストします。(MDNより)
every()
メソッドは、配列のすべての要素に対してテスト関数を実行し、すべての要素が条件に合致した場合のみtrue
を、そうでなければfalse
を返します。
テスト関数に渡される引数はfilter()
メソッドと同じです。次回予告
誰も、forEachに戻り値がない。
誰も、forEachの使いみちがいまいちわからない。第10話 もうmapにもforEachにも頼らない
- 投稿日:2020-03-22T15:42:15+09:00
JavaScript debounce
let timeoutId: NodeJS.Timeout | null = null; const debounce = (func: Function, delay = 1000): void => { if (timeoutId != null) { clearTimeout(timeoutId); } timeoutId = setTimeout(() => { func(); timeoutId = null; }, delay); };See the Pen debounce by Yoshiharu (@yoshiharu2580) on CodePen.
- 投稿日:2020-03-22T15:42:15+09:00
JavaScript debounce Lodash 使わない 自作
let timeoutId: NodeJS.Timeout | null = null; const debounce = (func: Function, delay = 1000): void => { if (timeoutId != null) { clearTimeout(timeoutId); } timeoutId = setTimeout(() => { func(); timeoutId = null; }, delay); };See the Pen debounce by Yoshiharu (@yoshiharu2580) on CodePen.
- 投稿日:2020-03-22T15:33:17+09:00
WebSocket の負荷テストは Artillery でシュッと簡単に実行しよう
Artillery は yaml ファイルに宣言的にシナリオを記述し、シンプルなインタフェースで負荷をかけることができる Nodejs 製の負荷テストツールです。
本記事では Artillery を使用して簡単に WebSocket サーバの負荷テストを実行する方法を紹介します。最小構成の WebSocket サーバ
まずはじめに WebSocket サーバを実装しましょう。今回は Node.js を使用します。
必要最小限の機能だけを提供します。ws ライブラリを使用して簡単に実装しましょう。server.jsconst WebSocket = require("ws"); const wss = new WebSocket.Server({ port: 3000 }, () => { console.log("server is now listening localhost:3000"); }); let connections = []; wss.on("connection", ws => { connections.push(ws); console.log(`new connection established. connections: ${connections.length}`); ws.on("close", () => { console.log(`connection closeed: ${connections.length}`); connections = connections.filter((conn, i) => conn !== ws); }); ws.on("message", message => { broadcast(JSON.stringify(message)); }); }); const broadcast = message => { connections.forEach((con, i) => { con.send(message); }); };起動して以下のメッセージが表示されれば準備完了です。
$ node server.js server is now listening localhost:3000
wscat で接続を確認する
サーバを起動したらまず wscat を使用して動作の確認をしましょう。wscat は npm でインストールできます。
$ npm install -g wscatWebSocket サーバを起動し、wscat で接続したら任意のメッセージを送信してみましょう。1つのクライアントからの送信を受けて、他のクライアントへ broadcast していることがわかります。
Artillery を使用して負荷テストを実行する
さて、ようやく本題です。Artillery を使用して負荷テストをかけてみましょう。
最小限のシナリオファイルのサンプルです。シナリオファイルはsenario.yml
のような名前をつけておきます。senario.yamlconfig: target: "ws://localhost:3000" phases: - duration: 20 arrivalRate: 10 scenarios: - engine: "ws" flow: - send: "hello"以下、コマンドで実行します。
$ artillery run senario.yml
アクティブなコネクション数を調整する
実際に WebSocket を用いたアプリケーションでは、常に多くのコネクションが張られていることが一般的です。上記のシナリオでは hello というメッセージを送ったらすぐにコネクションを切断してしまうので、常時アクティブなコネクションが少ない状態であまり現実的ではありません。まずは、
think
を指定してアクティブなコネクション数が増えるように調整しましょう。また、同じメッセージを繰り返し送信するloop
も指定できます。senario.yamlscenarios: - engine: "ws" flow: - send: "hello" - think: 1 # pause for 1 second - loop: - send: "world" count: 5オブジェクトの送信に対応する
さて先ほどまでは string 形式のデータだけに限定していましたが、
{"name":"john", "age":24}
のようにオブジェクト形式で送信する方が良いこともあるでしょう。以下のように記述すれば、送信時に Stringify してくれます。senario.yamlscenarios: - engine: "ws" flow: # the following will be stringified and sent as '{"name":"john","age":24}' - send: name: "john" age: 24カスタムコードを使用してタイムスタンプを付与する
また、送信したタイムスタンプを付与したいこともあります。このようなケースに対応するためにはカスタムコードを使用して柔軟に値を差し込むことができます。
senario.yamlconfig: target: "ws://localhost:3000" processor: "./custom.js" scenarios: - engine: "ws" flow: # custom code for timestanp - function: "createTimestampedObject" - send: "{{ data }}"非常にシンプルに記述し、シュッと実装することができました。Artillery は WebSocket だけではなく、HTTPのプロトコルもサポートしています。本記事は公式ドキュメントから参照していますので、詳細に理解されたい方は一度ドキュメントに目を通してみるのも良いでしょう。
また、CircleCIで継続的に負荷テストを実行するTipsをこちらの記事で紹介していますのでぜひご参照ください。
- 投稿日:2020-03-22T15:32:30+09:00
高階関数を書いたら、中級者になれた気がした。を批判したら上級者になれた気がした。
はじめに
「高階関数を書いたら、中級者になれた気がした」という記事を読んで色々ともやもやしたので批判をしてみる。といった趣旨の記事です。
きっかけはこちらのツイートから⇒https://twitter.com/ababupdownba/status/1241509344329363457?s=20
謝辞
この記事の不適切で良くないところを指摘し、記事の品質向上に貢献して頂いた皆さん
@kazatsuyu
@pink_bangbi
@le_panda_noir
に感謝の意を表します。本当にありがとうございます。早速見ていこう
あの記事の流れは、社長の無茶ぶりに頑張って答えながら進んでいくというものだ。
最初の社長の指令はこうだった。
社長「お仕事を持ってきたで」
社長「今日は↓こんな関数を作ってくれ」
- 引数として受け取ったHTML要素の高さを100ピクセルにする
社長「関数の名前はsetHeightで頼むわ」
社長「使うときのイメージとしては↓こんな感じや」そしてそれに対応したコードがこれだ。
// boxと言うIDを持った要素を取得。 const box = document.getElementById("box"); // 関数を呼び出して高さを設定。 setHeight(box);const setHeight = element => { element.style.height = '100px'; };ここまでは特に悪い所は無い。(強いて言えば
setHeight
という名前は少々わかりずらいので、setElementStyleHeight
とかの方がよさそうといった所)そして次の社長の指令はコチラ
社長「一つだけ要件を言い忘れてたんや・・・」
- 高さを100ピクセルにして、更に背景色を赤くしたい場合もある
そしてそれに対応して修正したコードがこれだ。
// 高さを100ピクセルにするだけの場合 setHeight(box); // 高さを100ピクセルにして、更に背景を赤くしたい場合 setHeight(box, true);const setHeight = (element, toRed) => { element.style.height = '100px'; if(toRed){ element.style.backgroundColor = 'red'; } };このコードにはひとつ問題点がある。
まあ、あえてこのような問題のあるコードにしてあるのは後で高階関数を使うための布石なのだろうが、そもそもこの例では高階関数を使うメリットは微塵もないので指摘していく。[問題] 関数の命名がおかしい
setHeight
はその名が意味するなら、高さを設定する関数だ。なので、背景色を赤にする処理を記述するのは間違っている。
正しくはこうするべきだろう。// 高さを100ピクセルにするだけの場合 setHeight(box); // 高さを100ピクセルにして、更に背景を赤くしたい場合 setHeightAndToRed(box);const setHeight = (element) => { element.style.height = '100px'; }; const setHeightAndToRed = (element) => { setHeight(element); element.style.backgroundColor = 'red'; };しかし、よく考えてほしい。社長は
setHeight
関数を作って欲しいと言っているのだ。
だが、あきらかにsetHeight
は命名がおかしいのでここは頑張って社長を説得するしかない。とまあ、こんな感じであの記事では
setHeight
関数にどんどん機能が上乗せされていく。高さを変えるという以外の機能が、だ。あの記事では最終的に
setHeight
関数の実装は以下の様になる。const setHeight = (element, toRed, width) => { element.style.height = '100px'; if(toRed){ element.style.backgroundColor = 'red'; } // 第三引数のwidthがあったら、要素の横幅を変更する if(width){ element.style.width = width + 'px'; } };
setHeight
関数があまりにも機能を持ちすぎているという問題をあの記事では、コールバック関数というものを使って解決している。
そのコードがこちら。const setHeight = (element, callback) => { element.style.height = '100px'; callback(element); }setHeight(box, element => { element.style.color = 'green'; element.innerHTML = 'こんにちわ!'; });高さを
100px
にすること以外の処理をコールバック関数に任せることで、機能を分割しているのである。
正直言って意図が分かりにくく、あまり良くないコードだと思う。[問題]そもそもコールバック関数を使うメリットが無い
コールバック関数なんて複雑で読みにくいものなんか使わずに、以下のようにすればいいだけなのだ。const setHeight = (element) => { element.style.height = '100px'; }setHeight(box); box.style.color="green"; box.innerHTML="こんにちわ!";コールバック関数を使う適切な動機ではない。
問題まとめ
- 例で出てくる関数の命名が適切ではない
- コールバック関数、および高階関数を使うメリットが全くない(むしろデメリットしかない)場合なのに、むりやり使っている。
しかし、高階関数の書き方を知ってもらうキッカケという視点で考えた時、あの記事はとても価値があるものだと思う。
じゃあ、高階関数を使うメリットってなんなの?
ここで全てのメリットを挙げるのは大変だが、一つ上げると処理の責務と処理呼び出しの責務と処理結果の扱いの責務を分離したいときに高階関数を使うと短く書けて嬉しいというメリットがある。
例1
まずは処理の責務と処理呼び出しの責務とを分離する例を見てみよう。
以下は5回カウントダウンした後に特定の処理をする
FiveCountDowner
クラスの実装と使用の例である。class FiveCountDowner{ constructor(){ this.currentCount=0; } update(){ this.currentCount++; if(this.currentCount==5) this.invoke(); //処理呼び出しの責務を果たしている } invoke(){ //処理の責務を果たしている console.log("起きて―!!"); } } const fiveCountDowner=new FiveCountDowner(); fiveCountDowner.update(); fiveCountDowner.update(); fiveCountDowner.update(); fiveCountDowner.update(); fiveCountDowner.update();https://wandbox.org/permlink/PCXFpXrMLlrEPi5I
注目すべきは責務だ。
FiveCountDowner
クラスは5回カウント後に処理されるInvoke
関数の実装を持っているし、Invoke
関数がいつ呼び出されるかも管理している。(5回カウントされた後にのみ呼び出すということですね)
これはつまり、FiveCountDowner
クラスは処理の責務も処理の呼び出しの責務も担っているということになる。
もし、5回カウント後に実行される処理内容を柔軟に変えれるようにしようとしたらどうすればいいだろうか?
それは、FiveCountDowner
クラスから処理の責務を分離する必要があることを意味する。
試しにクラスを分けてみようか。//処理の責務を果たしている class Okite{ invoke(){ console.log("起きて―!!"); } } //処理の責務を果たしている class Nero{ invoke(){ console.log("寝ろ"); } } class FiveCountDowner{ constructor(processObject){ this.currentCount=0; this.processObject=processObject; } update(){ this.currentCount++; if(this.currentCount==5) //処理呼び出しの責務を果たしている this.processObject.invoke(); } } const fiveCountDowner1=new FiveCountDowner(new Okite); fiveCountDowner1.update(); fiveCountDowner1.update(); fiveCountDowner1.update(); fiveCountDowner1.update(); fiveCountDowner1.update(); const fiveCountDowner2=new FiveCountDowner(new Nero); fiveCountDowner2.update(); fiveCountDowner2.update(); fiveCountDowner2.update(); fiveCountDowner2.update(); fiveCountDowner2.update();https://wandbox.org/permlink/jebwILGCsiwvnn9e
このようにして、責務を分割して処理を柔軟に変更することができた。
しかし、考えても見てほしい。いちいち新しい処理が増えることにOkite
とかNero
とか毎回クラスを作ったりするのは面倒ではないか?
そこで高階関数を使う。
以下が高階関数を使ったversionだ。class FiveCountDowner{ constructor(callback){ this.currentCount=0; this.callback=callback; } update(){ this.currentCount++; if(this.currentCount==5) this.callback(); } } const fiveCountDowner1=new FiveCountDowner( ()=> console.log("起きて―!!") ); fiveCountDowner1.update(); fiveCountDowner1.update(); fiveCountDowner1.update(); fiveCountDowner1.update(); fiveCountDowner1.update(); const fiveCountDowner2=new FiveCountDowner( ()=>console.log("寝ろ") ); fiveCountDowner2.update(); fiveCountDowner2.update(); fiveCountDowner2.update(); fiveCountDowner2.update(); fiveCountDowner2.update();https://wandbox.org/permlink/cQmlfjq0OQu9PJxB
このように、関数にクラスを渡すのではなく、関数を渡す。関数に関数を渡すことが高階関数というものである。
これ以外にも、関数を返す関数も高階関数と呼ぶ。
つまり、高階関数は関数を引数で受け取ったり、関数を返したりする関数のことを言う。例2
例1では処理の責務と処理呼び出しの責務とを分離する例だった。ここにさらに、処理をした結果の責務について考えていこうと思う。
そのために、以下の問題を例として使う
- 数値の配列の要素一つ一つを倍にした配列を作るdoubleArray関数を実装してほしい。元の配列を直接変更しても構わない。
- 数値の配列の要素一つ一つから-10にした配列を作るtenMinusArray関数を実装してほしい。元の配列を直接変更しても構わない。
まずは何も考えずに実装してみよう。
const doubleArray=(array)=>{ for(let i=0;i<array.length;i++) array[i]=array[i]*2; return array; }; const tenMinusArray=(array)=>{ for(let i=0;i<array.length;i++) array[i]=array[i]-10; return array; }; console.log(doubleArray([1,2,3,4])) console.log(tenMinusArray([10,20,30,40]))https://wandbox.org/permlink/77zhhqzApnmMyH2X
このコードには無駄がある。まずはそれを見つけるために処理を分解していこう。
- 配列の中身を1つずつ取り出すfor文
- array[i]=の要素更新処理結果を代入する
- 要素を更新する処理
array[i]*2
とarray[i]-10
- 結果を返すreturn文
要素を更新する処理以外は共通する部分だ。
処理の責務を要素を更新する処理。
処理呼び出しの責務をfor文内の処理。
処理をした結果の責務を要素更新処理結果を代入する処理と考える。
そうして、処理の責務だけを関数から分離したmapArray関数を高階関数を使って実装してみよう。
(JavaScriptには配列の標準のメソッドとしてmap関数があるが、今回の目的は高階関数を学ぶことなので車輪の再発明をやっていく)const mapArray=(array,callback)=>{ for(let i=0;i<array.length;i++) array[i]=callback(array[i]); return array; } const doubleArray=(array)=> mapArray(array,x=>x*2); const tenMinusArray=(array)=> mapArray(array,x=>x-10); console.log(doubleArray([1,2,3,4])) console.log(tenMinusArray([10,20,30,40]))https://wandbox.org/permlink/QXzZ5Vs6XjxSBNsE
例2(オマケ)
例2はカリー化と部分適用を使ってさらに綺麗に美しく書くことができる。
まあ、これを美しいと感じるかどうかは人によるが。const mapArray=callback=> array=>{ for(let i=0;i<array.length;i++) array[i]=callback(array[i]); return array; }; const doubleArray=mapArray(x=>x*2); const tenMinusArray=mapArray(x=>x-10); console.log(doubleArray([1,2,3,4])) console.log(tenMinusArray([10,20,30,40]))https://wandbox.org/permlink/3kqkUYJHuyHPLUR3
例3
高階関数には変数のスコープを短くするのにも役に立つ。
以下の例ではboxという変数がずっと下までスコープが続いてしまう。
もしboxは上の三行でしか使わないのならば、可読性が低くなる。const box = document.getElementById("box"); box.innerHTML = 'こんにちわ!'; box.style.height = '100px'; . . . //下にもプログラムは続く。これを高階関数でスコープを適切な範囲に狭めるとこうなる。
const setElementDesign=(elementId,callback)=>{ callback(document.getElementById(elementId)); }; setElementDesign("box",element=>{ element.innerHTML = 'こんにちわ!'; element.style.height = '100px'; });//element変数のスコープはここまで。element変数のスコープが
{}
によって限定されているのがわかるだろう。
元記事でも、document.getElementById
の処理をsetHeight
内に閉じ込めていればこの恩恵は得られたはずなので惜しかった。でも、これは少しメリットが弱い。なぜなら以下の様に書いても同じようにスコープを狭めることはできるからだ。
{ const box = document.getElementById("box"); box.innerHTML = 'こんにちわ!'; box.style.height = '100px'; }(()=>{ const box = document.getElementById("box"); box.innerHTML = 'こんにちわ!'; box.style.height = '100px'; })();(function(){ const box = document.getElementById("box"); box.innerHTML = 'こんにちわ!'; box.style.height = '100px'; })();高階関数まとめ
高階関数は関数を引数に取る関数や関数を返す関数をそう呼んでいるだけで、一般的な関数と同じ。
高階関数は処理の責務と処理の呼び出し責務・処理をした結果の責務を分離する時に短く書けるので便利。
無秩序に何でもかんでも何も考えずに高階関数にするのではなく、適切な場合のみに使用していきましょう。えっ?高階関数使いまくりたい?じゃあHaskellかScalaかLispへGo!。
- 投稿日:2020-03-22T14:05:25+09:00
ECMAScriptのプリミティブ値とプリミティブデータ型について調べたこと
はじめに
久しぶりにJavaScriptを触ったら引数の文字列判定で悩んだので、周辺情報として調べたプリミティブ値とプリミティブデータ型について記録します。
プリミティブ値とプリミティブデータ型
ECMAScript最新版1には7種類のプリミティブデータ型が存在しており、それぞれにプリミティブ値が定義されています2。プリミティブデータ型はオブジェクトではありませんが、nullとundefined以外は対応するラッパーオブジェクト3によりprototypeなどが提供されます。プリミティブデータ型、プリミティブ値の例の一覧は以下の通りです。
プリミティブデータ型 プリミティブ値の例 Boolean true, false Null null Undefined undefined Number 0 BigInt 0n String "" Symbol Symbol("") const primitiveDataTypeNameAndValues = { "Boolean":[true,false], "Null":[null], "Undefined":[undefined], "Number":[0], "BigInt":[0n], "String":[""], "Symbol":[Symbol("")]};プリミティブ値とObject.getPrototypeOf()
ECMAScript(ES2015~現時点)の仕様によりプリミティブ値は
typeof
、Object.getPrototypeOf()
、instanceof
の結果が一致しません。例えば文字列リテラル"abc"
についてこれらを実行した結果は次のようになります。typeof "abc" // "string" Object.getPrototypeOf("abc") // String {...} "abc" instanceof String // "false" "abc" instanceof Object // "false"
typeof
とObject.getPrototypeOf()
は異なる値を返し、Object.getPrototypeOf()
がStringを返すにも関わらずinstanceof
ではStringのインスタンスではないと判断されます。これはES2015からObject.getPrototypeOf()
がundefinedとnullを除くプリミティブ値に対してラッパーオブジェクトを返す仕様へ変更されたことによります4。次のコードを実行することですべてのプリミティブ値についてこれらの結果を確認することができます。
const primitiveDataTypeNameAndValues = { "Boolean":[true,false], "Null":[null], "Undefined":[undefined], "Number":[0], "BigInt":[0n], "String":[""], "Symbol":[Symbol("")]}; console.log([ "プリミティブデータ型", "プリミティブ値", "typeof プリミティブ値", "Object.getPrototypeOf(\"プリミティブ値\")"]); for (const [primitiveDataTypeName, primitiveDataValues] of Object.entries(primitiveDataTypeNameAndValues)) { for (const primitiveDataValue of primitiveDataValues) { const isUndefinedOrNull = primitiveDataValue === undefined || primitiveDataValue === null; console.log([ primitiveDataTypeName, primitiveDataValue, typeof primitiveDataValue, !isUndefinedOrNull ? Object.getPrototypeOf(primitiveDataValue) : null]); } }実行結果["プリミティブデータ型", "プリミティブ値", "typeof プリミティブ値", "Object.getPrototypeOf("プリミティブ値")"] ["Boolean", true, "boolean", Boolean] ["Boolean", false, "boolean", Boolean] ["Null", null, "object", null] ["Undefined", undefined, "undefined", null] ["Number", 0, "number", Number] ["BigInt", 0n, "bigint", BigInt] ["String", "", "string", String] ["Symbol", Symbol(), "symbol", Symbol]おまけ
文字列判定
JavaScriptの文字列にはプリミティブ値
"..."
とnew String(...)
により作成されたラッパーオブジェクトが存在しています5。したがって文字列判定ではどちらも考慮する必要があります。function isString(obj) { return typeof obj == "string" || obj instanceof String; } function isPrimitiveString(obj) { return typeof obj == "string"; } function isStringWrapper(obj) { return obj instanceof String; } const f = (obj) => [isString(obj), isPrimitiveString(obj), isStringWrapper(obj)] // [文字列,プリミティブ値,ラッパーオブジェクト] console.log(f("abc")); // [true, true, false] console.log(f(new String("abc"))); // [true, false, true]Object.getPrototypeOf(...).valueOf()の戻り値はプリミティブデータ型ごとに固定またはエラー
Object.getPrototypeOf()
の戻り値はラッパーオブジェクトなのでvalueOf()
を持ちますが、その戻り値はプリミティブ値によらずプリミティブデータ型ごとに一定となります。例えばNumber
では0
、Boolean
ではfalse
です。また、BigInt
とSymbol
ではTypeError
となります。const f = (o) => console.log([o, o.constructor.name, typeof o, o.valueOf()]); // [値、コンストラクタ名、typeof、valueOf()] f(Object.getPrototypeOf(true)); // [Boolean, "Boolean", "object", false] f(Object.getPrototypeOf(false)); // [Boolean, "Boolean", "object", false] f(Object.getPrototypeOf(123)); // [Number, "Number", "object", 0] f(Object.getPrototypeOf("abc")); // [String, "String", "object", ""] // f(Object.getPrototypeOf(123n)); // <TypeError> // f(Object.getPrototypeOf(Symbol("abc"))); // <TypeError>
- 投稿日:2020-03-22T14:05:25+09:00
ECMAScriptのプリミティブ値とデータ型について調べたこと
はじめに
久しぶりにJavaScriptを触ったら引数の文字列判定で悩んだので、周辺情報として調べたプリミティブ値とプリミティブデータ型について記録します。
プリミティブ値とプリミティブデータ型
ECMAScript最新版1には7種類のプリミティブデータ型が存在しており、それぞれにプリミティブ値が定義されています2。プリミティブデータ型はオブジェクトではありませんが、nullとundefined以外は対応するラッパーオブジェクト3によりprototypeなどが提供されます。プリミティブデータ型、プリミティブ値の例の一覧は以下の通りです。
プリミティブデータ型 プリミティブ値の例 Boolean true, false Null null Undefined undefined Number 0 BigInt 0n String "" Symbol Symbol("") const primitiveDataTypeNameAndValues = { "Boolean":[true,false], "Null":[null], "Undefined":[undefined], "Number":[0], "BigInt":[0n], "String":[""], "Symbol":[Symbol("")]};プリミティブ値とObject.getPrototypeOf()
ECMAScript(ES2015~現時点)の仕様によりプリミティブ値は
typeof
、Object.getPrototypeOf()
、instanceof
の結果が一致しません。例えば文字列リテラル"abc"
についてこれらを実行した結果は次のようになります。typeof "abc" // "string" Object.getPrototypeOf("abc") // String {...} "abc" instanceof String // "false" "abc" instanceof Object // "false"
typeof
とObject.getPrototypeOf()
は異なる値を返し、Object.getPrototypeOf()
がStringを返すにも関わらずinstanceof
ではStringのインスタンスではないと判断されます。これはES2015からObject.getPrototypeOf()
がundefinedとnullを除くプリミティブ値に対してラッパーオブジェクトを返す仕様へ変更されたことによります4。次のコードを実行することですべてのプリミティブ値についてこれらの結果を確認することができます。
const primitiveDataTypeNameAndValues = { "Boolean":[true,false], "Null":[null], "Undefined":[undefined], "Number":[0], "BigInt":[0n], "String":[""], "Symbol":[Symbol("")]}; console.log([ "プリミティブデータ型", "プリミティブ値", "typeof プリミティブ値", "Object.getPrototypeOf(\"プリミティブ値\")"]); for (const [primitiveDataTypeName, primitiveDataValues] of Object.entries(primitiveDataTypeNameAndValues)) { for (const primitiveDataValue of primitiveDataValues) { const isUndefinedOrNull = primitiveDataValue === undefined || primitiveDataValue === null; console.log([ primitiveDataTypeName, primitiveDataValue, typeof primitiveDataValue, !isUndefinedOrNull ? Object.getPrototypeOf(primitiveDataValue) : null]); } }実行結果["プリミティブデータ型", "プリミティブ値", "typeof プリミティブ値", "Object.getPrototypeOf("プリミティブ値")"] ["Boolean", true, "boolean", Boolean] ["Boolean", false, "boolean", Boolean] ["Null", null, "object", null] ["Undefined", undefined, "undefined", null] ["Number", 0, "number", Number] ["BigInt", 0n, "bigint", BigInt] ["String", "", "string", String] ["Symbol", Symbol(), "symbol", Symbol]おまけ
文字列判定
JavaScriptの文字列にはプリミティブ値
"..."
とnew String(...)
により作成されたラッパーオブジェクトが存在しています5。したがって文字列判定ではどちらも考慮する必要があります。function isString(obj) { return typeof obj == "string" || obj instanceof String; } function isPrimitiveString(obj) { return typeof obj == "string"; } function isStringWrapper(obj) { return obj instanceof String; } const f = (obj) => [isString(obj), isPrimitiveString(obj), isStringWrapper(obj)] // [文字列,プリミティブ値,ラッパーオブジェクト] console.log(f("abc")); // [true, true, false] console.log(f(new String("abc"))); // [true, false, true]Object.getPrototypeOf(...).valueOf()の戻り値はプリミティブデータ型ごとに固定またはエラー
Object.getPrototypeOf()
の戻り値はラッパーオブジェクトなのでvalueOf()
を持ちますが、その戻り値はプリミティブ値によらずプリミティブデータ型ごとに一定となります。例えばNumber
では0
、Boolean
ではfalse
です。また、BigInt
とSymbol
ではTypeError
となります。const f = (o) => console.log([o, o.constructor.name, typeof o, o.valueOf()]); // [値、コンストラクタ名、typeof、valueOf()] f(Object.getPrototypeOf(true)); // [Boolean, "Boolean", "object", false] f(Object.getPrototypeOf(false)); // [Boolean, "Boolean", "object", false] f(Object.getPrototypeOf(123)); // [Number, "Number", "object", 0] f(Object.getPrototypeOf("abc")); // [String, "String", "object", ""] // f(Object.getPrototypeOf(123n)); // <TypeError> // f(Object.getPrototypeOf(Symbol("abc"))); // <TypeError>
- 投稿日:2020-03-22T13:55:48+09:00
水戸黄門を使ってCORSを完全に理解した
はじめに
「完全に理解した」はもちろんネットスラングです。マサカリお待ちしてます。でもわかりやすいと思います。
ことの経緯
かつて書いた記事 をベースに、javascriptだけでgooglemapの情報が欲しいんだよね。っていう依頼があって、えっ?それやったことねぇなぁって思って。とはいえ多分DjangoのViewで googlemap api にコンタクトしてたやつを javascript に移植すればええんやろ?って思って、ajaxのurlにgoogleapiのurlを指定してみたら出てきたんだよね。うーわなにこれもしかして学習コスト高いやつ??とか思ったよね。
CORSとは
[オリジン間リソース共有 (CORS)](https://developer.mozilla.org/ja/docs/Web/HTTP/CORS)
オリジン間リソース共有Cross-Origin Resource Sharing (CORS) は、追加の HTTP ヘッダーを使用して、あるオリジンで動作しているウェブアプリケーションに、異なるオリジンにある選択されたリソースへのアクセス権を与えるようブラウザーに指示するための仕組みです。
もうね、日本語で書かれてるのに理解できないんだよね。オリジンってオリジン弁当かよって感じ。
助さん格さん
「この紋所が目に入らぬか!」ってやるじゃない?あれって、水戸黄門がいない世界線で、格さんがあの紋所を掲げても意味がないわけよ。なんやそのオブジェクトォー!みたいになるよね。つまりCORSの説明に都合がいいように表現すると、虎の威を借る狐が格さんなわけ。そういういわば「虎の威」がないと、爺さんのいない格さんは一般人なんだから、「委任状」がないなら大事なgoogleapiのデータはあげないもんねー!っていう仕組みがCORS。俺、あの有名人と知り合いなんだよね
委任状を書く(ヘッダーを加工する)必要がある
CORSは、ブラウザの「防衛機能」だ。つまり、サーバーサイド(Djangoでいう Views.py ね)の処理では何事もなく済んだapiデータ取得処理を、フロントエンドでやろうとすると出るエラーなんだ。わかってしまえば高速道路と下道。。。というのか「勘合貿易」みたいなもんよね。javascriptが単独でapiにアクセスしにいけないってこと。どっかのWebシステムからjavascriptファイルだけ取り出して別のWebシステムで再現できたら、googleapiさんは「誰にデータを渡した!?」となるわけだ。
えっなんでそんなjavascriptハブられてんの?
多分の話をすると、昔ブラウザ戦争が起こってた頃に、みんながIEの設定項目で「javascript切れ」って言われてたことあったんだよね。そのあたりに根がありそうな気がする。とにかく、javascript自身が、別の(自身以外の)リソースにアクセスする場合は「保護者の同意」が必要なわけだ。それはスネ夫で言えばジャイアンを連れてくるだし、格さんであれば水戸黄門を連れてくる、っていうことなわけだ。いや、実際にはジャイアンも水戸黄門もいなくていい。その「気配」というか「虎の威」があればいいのだ。ジャイアンや水戸黄門が「委任状」を書いてスネ夫や格さんに「権利があります」を説明できればいいわけ。
まとめ
ここまでべらべらといろんな単語や言い回しをわざと使ってきて、この状態でもういっかいこれを見てみましょう。
[オリジン間リソース共有 (CORS)](https://developer.mozilla.org/ja/docs/Web/HTTP/CORS)オリジン間リソース共有Cross-Origin Resource Sharing (CORS) は、追加の HTTP ヘッダーを使用して、あるオリジンで動作しているウェブアプリケーションに、異なるオリジンにある選択されたリソースへのアクセス権を与えるようブラウザーに指示するための仕組みです。
なんか理解度上がってない!?
javascriptが直接apiにコンタクトしたかったらヘッダーに委任状付ける必要があるよってことよね。で、結局「ことの経緯」はどうなったかって?CORSの問題でjavascriptだけではモックアップは無理ですっていって、Djangoでサーバーサイドごと作って渡したってわけ。あくまでサーバーの処理はサーバーに(javascriptは委任状なんかもらわずに自分の親分(自オリジン)に、自分のことは自分でやれってリクエストをするだけ。Ajaxは自オリジンにしかコンタクトできん)ってこっちゃ
- 投稿日:2020-03-22T13:48:23+09:00
JavaScript: 通常の関数とアロー関数は「書き方の違うだけ」ではない
アロー関数は、
function
キーワード使った関数のシュガーシンタックス、つまり、書き方が違うだけで機能は同じだという説明を耳にしたことはありませんでしょうか?この説明は、正確ではなく実際は様々な違いがあります。本稿では、これら2つの関数の性質の違いについて解説していきます。
結論
本稿の結論を先に示すと、2つの関数の違いは次のとおりです。
詳しく知りたい方は、続きをお読み頂ければと思います。
2種類の関数
JavaScriptには大きく分けて2つの関数があります。ひとつは、
function
キーワードを使って定義される関数です。function () {}もうひとつは、アロー関数(arrow function)です。名前のとおり矢印記号
=>
を使って定義される関数です。() => {}本稿では、アロー関数と
function
キーワードを使って定義される関数を区別するため、function
キーワードを使うほうの関数を「通常関数」と呼ぶことにします。英文で見かけるregular functionの翻訳になりますが、これは公式の用語ではなく、解説の便宜上のものとご理解頂ければと思います。単に「関数」というときは、通常関数とアロー関数どちらも指すこととします。関数の歴史
零式的に見ると、通常関数は古くからある言語機能であるのに対し、アロー関数は新しいものです。アロー関数はES2015(ES6)で導入されました。導入にあたっては、関数を短く書きたい、thisを束縛したくないという2つの理由があります。
通常関数とアロー関数の性質の違い
通常関数とアロー関数では、構文が違うというのは見て分かると思います。構文についての違いはここでは解説しません。
ここでは、文法以外の相違点をひとつひとつ見ていくことにします。
違い1: thisの指すもの
通常関数には
this
があり、this
が何を指すのかは、通常関数を実行したタイミングで決まります。function regular() { return this } // グローバルスコープで実行した場合、thisはグローバルオブジェクトを指す console.log(regular() === Window) //=> true // メソッドとして実行した場合、thisはオブジェクトを指す const obj = { method: regular } console.log(obj.method() === obj) //=> true通常関数の
this
の振る舞いの詳細についての解説は他の投稿に譲ります:アロー関数には、それ自体が保有する
this
はなく、関数の外のthis
を関数内で参照できるだけです。レキシカルスコープのthis
を参照します。つまり、アロー関数は定義したときに、this
が指すものがひとつに決まり、どうやって関数が実行されるかに左右されなくなります。const arrow = () => { return this } // 関数の外側のthis const lexicalThis = this // 関数定義したタイミングで、関数の外側のthisを参照する console.log(arrow() === lexicalThis) //=> true // メソッドとして実行しても、thisはメソッドが属するオブジェクトを指さない const obj = { method: arrow } console.log(obj.method() === obj) //=> false console.log(obj.method() === lexicalThis) //=> true // ちなみに上のarrowのthisの振る舞いを、通常関数で再現すると次のようになります: function regular() { return lexicalThis }以上の相違を踏まえると、同じ処理内容の関数でも、通常関数かアロー関数かによって、結果が異なってくる場合があるということが分かります:
this.name = 'bar' const foo = { name: 'foo', regular: function () { return this.name }, arrow: () => { return this.name } } console.log(foo.regular()) //=> foo console.log(foo.arrow()) //=> bar違い2: newできるかどうか
通常関数は
new
することができますが、アロー関数はnew
することができません。つまり、アロー関数はコンストラクタになることができません。function regular() { } const arrow = () => {} new regular() new arrow() //=> TypeError: arrow is not a constructorしたがって、通常関数は
class
でextends
できますが、アロー関数はできません:function regular() {} const arrow = () => {} class Foo extends regular {} class Bar extends arrow {} //=> TypeError: Class extends value () => {} is not a constructor or null違い3:
call
,apply
,bind
の振る舞い通常関数は、
call
,apply
,bind
メソッドの第一引数で、その関数のthis
を指すオブジェクトを指定することができます。アロー関数は、指定しても無視されます。function regular() { return this } const arrow = () => { return this } const obj = {} console.log(regular.bind(obj)() === obj) //=> ture console.log(arrow.bind(obj)() === obj) //=> false console.log(regular.apply(obj) === obj) //=> true console.log(arrow.apply(obj) === obj) //=> false console.log(regular.call(obj) === obj) //=> true console.log(arrow.call(obj) === obj) //=> false違い4:
prototype
プロパティの有無通常関数には
prototype
プロパティがありますが、アロー関数にはありません。function regular() {} const arrow = () => {} console.log(typeof regular.prototype) //=> object console.log(typeof arrow.prototype) //=> undefinedちなみに、どちらの関数も、内部プロパティ
[[Prototype]]
は一緒です:console.log( Object.getPrototypeOf(regular) === Object.getPrototypeOf(arrow) ) //=> trueアロー関数の
call
メソッドなどは、この内部プロパティの[[Prototype]]
のものになります:console.log( Object.getOwnPropertyNames( Object.getPrototypeOf(arrow) ) ) //=> [ // 'length', 'name', // 'arguments', 'caller', // 'constructor', 'apply', // 'bind', 'call', // 'toString' //]違い5:
arguments
の有無通常関数は、
arguments
で引数リストを参照できますが、アロー関数ではarguments
が定義されていないため、引数リストを参照できません。アロー関数のarguments
はレキシカルスコープ変数のarguments
を参照するだけです。const arguments = 'hoge' // レキシカルスコープ変数 function regular() { console.log(arguments) } const arrow = () => { console.log(arguments) } regular(1, 2, 3) //=> [Arguments] { '0': 1, '1': 2, '2': 3 } arrow(1, 2, 3) //=> hogeちなみに、アロー関数で引数リストを参照する場合は、可変長引数を定義する方法があります:
const arrow = (...arguments) => { console.log(arguments) } arrow(1, 2, 3) //=> [ 1, 2, 3 ]違い6: 引数名の重複
通常関数は、引数名の重複が許されますが、アロー関数は同じ引数名があるとエラーになります:
function regular(x, x) {} const arrow = (x, x) => {} // SyntaxError: Duplicate parameter name not allowed in this context違い7: 巻き上げ(hosting)
通常関数は、
var
相当の巻き上げ(hoisting)が起こります。なので、関数定義前に呼び出しのコードを書いても、関数が実行できます:regular() // エラーにならない function regular() {}アロー関数は巻き上げが起こりません:
arrow() // ReferenceError: Cannot access 'arrow' before initialization const arrow = () => {}通常関数の場合でも、
const
などを使った定義では、巻き上げが起こりません:regular() // ReferenceError: Cannot access 'regular' before initialization const regular = function () {}違い8: ジェネレータ関数
通常関数はジェネレータ関数を定義できますが、アロー関数はジェネレータ関数を定義する構文がそもそもありません。
function* regular() { yield 1 }違い9: 同じ関数名での再定義
通常関数は、同じ名前の関数を定義できます。最後の関数で上書きされます。
function regular() { console.log(1) } function regular() { console.log(2) } regular() //=> 2これも
var
相当の振る舞いと理解できます:var a = 1 var a = 2 console.log(a) //=> 2アロー関数は
let
やconst
の仕様上、同じ関数名で定義を上書きすることができません:let arrow = () => {} let arrow = () => {} //=> SyntaxError: Identifier 'arrow' has already been declaredもちろん、アロー関数でも
var
で宣言すると、上書き可能です:var arrow = () => {} var arrow = () => {}違い10:
super
,new.target
の有無アロー関数 - JavaScript | MDNによれば、アロー関数には束縛された
super
やnew.target
が無いようですが、これを示すサンプルコードが思いつかなかったので割愛します。まとめ
本稿では、JavaScriptの2種類の関数、通常関数とアロー関数の性質の違いについて説明しました。通常関数とアロー関数では、様々な性質の違いあり、単純に書き方の違いではないことが分かりました。
- 投稿日:2020-03-22T13:48:23+09:00
JavaScript: 通常の関数とアロー関数の違いは「書き方だけ」ではない。異なる性質が10個ほどある。
アロー関数は、
function
キーワード使った関数のシュガーシンタックス、つまり、書き方が違うだけで機能は同じだという説明を耳にしたことはありませんでしょうか?この説明は、正確ではなく実際は様々な違いがあります。本稿では、これら2つの関数の性質の違いについて解説していきます。
結論
本稿の結論を先に示すと、2つの関数の違いは次のとおりです。
詳しく知りたい方は、続きをお読み頂ければと思います。
2種類の関数
JavaScriptには大きく分けて2つの関数があります。ひとつは、
function
キーワードを使って定義される関数です。function () {}もうひとつは、アロー関数(arrow function)です。名前のとおり矢印記号
=>
を使って定義される関数です。() => {}本稿では、アロー関数と
function
キーワードを使って定義される関数を区別するため、function
キーワードを使うほうの関数を「通常関数」と呼ぶことにします。英文で見かけるregular functionの翻訳になりますが、これは公式の用語ではなく、解説の便宜上のものとご理解頂ければと思います。単に「関数」というときは、通常関数とアロー関数どちらも指すこととします。関数の歴史
歴史的に見ると、通常関数は古くからある言語機能であるのに対し、アロー関数は新しいものです。アロー関数はES2015(ES6)で導入されました。導入にあたっては、関数を短く書きたい、thisを束縛したくないという2つの理由があります。
通常関数とアロー関数の性質の違い
通常関数とアロー関数では、構文が違うというのは見て分かると思います。構文についての違いはここでは解説しません。
ここでは、文法以外の相違点をひとつひとつ見ていくことにします。
違い1: thisの指すもの
通常関数には
this
があり、this
が何を指すのかは、通常関数を実行したタイミングで決まります。function regular() { return this } // グローバルスコープで実行した場合、thisはグローバルオブジェクトを指す console.log(regular() === Window) //=> true // メソッドとして実行した場合、thisはオブジェクトを指す const obj = { method: regular } console.log(obj.method() === obj) //=> true通常関数の
this
の振る舞いの詳細についての解説は他の投稿に譲ります:アロー関数には、それ自体が保有する
this
はなく、関数の外のthis
を関数内で参照できるだけです。レキシカルスコープのthis
を参照します。つまり、アロー関数は定義したときに、this
が指すものがひとつに決まり、どうやって関数が実行されるかに左右されなくなります。const arrow = () => { return this } // 関数の外側のthis const lexicalThis = this // 関数定義したタイミングで、関数の外側のthisを参照する console.log(arrow() === lexicalThis) //=> true // メソッドとして実行しても、thisはメソッドが属するオブジェクトを指さない const obj = { method: arrow } console.log(obj.method() === obj) //=> false console.log(obj.method() === lexicalThis) //=> true // ちなみに上のarrowのthisの振る舞いを、通常関数で再現すると次のようになります: function regular() { return lexicalThis }以上の相違を踏まえると、同じ処理内容の関数でも、通常関数かアロー関数かによって、結果が異なってくる場合があるということが分かります:
this.name = 'bar' const foo = { name: 'foo', regular: function () { return this.name }, arrow: () => { return this.name } } console.log(foo.regular()) //=> foo console.log(foo.arrow()) //=> bar違い2: newできるかどうか
通常関数は
new
することができますが、アロー関数はnew
することができません。つまり、アロー関数はコンストラクタになることができません。function regular() { } const arrow = () => {} new regular() new arrow() //=> TypeError: arrow is not a constructorしたがって、通常関数は
class
でextends
できますが、アロー関数はできません:function regular() {} const arrow = () => {} class Foo extends regular {} class Bar extends arrow {} //=> TypeError: Class extends value () => {} is not a constructor or null違い3:
call
,apply
,bind
の振る舞い通常関数は、
call
,apply
,bind
メソッドの第一引数で、その関数のthis
を指すオブジェクトを指定することができます。アロー関数は、指定しても無視されます。function regular() { return this } const arrow = () => { return this } const obj = {} console.log(regular.bind(obj)() === obj) //=> ture console.log(arrow.bind(obj)() === obj) //=> false console.log(regular.apply(obj) === obj) //=> true console.log(arrow.apply(obj) === obj) //=> false console.log(regular.call(obj) === obj) //=> true console.log(arrow.call(obj) === obj) //=> false違い4:
prototype
プロパティの有無通常関数には
prototype
プロパティがありますが、アロー関数にはありません。function regular() {} const arrow = () => {} console.log(typeof regular.prototype) //=> object console.log(typeof arrow.prototype) //=> undefinedちなみに、どちらの関数も、内部プロパティ
[[Prototype]]
は一緒です:console.log( Object.getPrototypeOf(regular) === Object.getPrototypeOf(arrow) ) //=> trueアロー関数の
call
メソッドなどは、この内部プロパティの[[Prototype]]
のものになります:console.log( Object.getOwnPropertyNames( Object.getPrototypeOf(arrow) ) ) //=> [ // 'length', 'name', // 'arguments', 'caller', // 'constructor', 'apply', // 'bind', 'call', // 'toString' //]違い5:
arguments
の有無通常関数は、
arguments
で引数リストを参照できますが、アロー関数ではarguments
が定義されていないため、引数リストを参照できません。アロー関数のarguments
はレキシカルスコープ変数のarguments
を参照するだけです。const arguments = 'hoge' // レキシカルスコープ変数 function regular() { console.log(arguments) } const arrow = () => { console.log(arguments) } regular(1, 2, 3) //=> [Arguments] { '0': 1, '1': 2, '2': 3 } arrow(1, 2, 3) //=> hogeちなみに、アロー関数で引数リストを参照する場合は、可変長引数を定義する方法があります:
const arrow = (...arguments) => { console.log(arguments) } arrow(1, 2, 3) //=> [ 1, 2, 3 ]違い6: 引数名の重複
通常関数は、引数名の重複が許されますが、アロー関数は同じ引数名があるとエラーになります:
function regular(x, x) {} const arrow = (x, x) => {} // SyntaxError: Duplicate parameter name not allowed in this context通常関数でもStrict Modeの場合は、引数名の重複がエラーになります:
'use strict' function regular(x, x) {} // SyntaxError: Duplicate parameter name not allowed in this context違い7: 巻き上げ(hoisting)
通常関数は、
var
相当の巻き上げ(hoisting)が起こります。なので、関数定義前に呼び出しのコードを書いても、関数が実行できます:regular() // エラーにならない function regular() {}アロー関数は巻き上げが起こりません:
arrow() // ReferenceError: Cannot access 'arrow' before initialization const arrow = () => {}これは、アロー関数の性質というよりかは、
const
の性質によるもので、通常関数の場合でも、const
などを使った定義では、巻き上げが起こりません:regular() // ReferenceError: Cannot access 'regular' before initialization const regular = function () {}違い8: ジェネレータ関数
通常関数はジェネレータ関数を定義できますが、アロー関数はジェネレータ関数を定義する構文がそもそもありません。
function* regular() { yield 1 }違い9: 同じ関数名での再定義
通常関数は、同じ名前の関数を定義できます。最後の関数で上書きされます。
function regular() { console.log(1) } function regular() { console.log(2) } regular() //=> 2これも
var
相当の振る舞いと理解できます:var a = 1 var a = 2 console.log(a) //=> 2アロー関数は
let
やconst
の仕様上、同じ関数名で定義を上書きすることができません:let arrow = () => {} let arrow = () => {} //=> SyntaxError: Identifier 'arrow' has already been declaredもちろん、アロー関数でも
var
で宣言すると、上書き可能です:var arrow = () => {} var arrow = () => {}違い10:
super
,new.target
の有無アロー関数 - JavaScript | MDNによれば、アロー関数には束縛された
super
やnew.target
が無いようですが、これを示すサンプルコードが思いつかなかったので割愛します。まとめ
本稿では、JavaScriptの2種類の関数、通常関数とアロー関数の性質の違いについて説明しました。通常関数とアロー関数では、様々な性質の違いあり、単純に書き方の違いではないことが分かりました。
最後までお読みくださりありがとうございました。Twitterでは、Qiitaに書かない技術ネタなどもツイートしているので、よかったらフォローお願いします→Twitter@suin
- 投稿日:2020-03-22T13:08:13+09:00
Next.jsでのCORSエラー解決(Next.js + Golang)
Next.jsでのCORSエラー解決(Next.js + Golang)
React + GoでSPAを作りたく、ReactフレームワークのNext.jsを使ってみた。
SPAの最初で躓きがちなCORSエラーについて、案の定ハマったのでメモ。(Angularでの開発の際もだいぶハマった思い出)CORSエラーが出る
今回のようにフロントエンド、バックエンドでサーバを分けて開発する際に、そのままだとCORS(Cross-Origin Resource Sharing)についてのエラーが発生する。
これはブラウザの機能として、クロスドメイン(ドメインの違うサーバへの)アクセスが制限されているため起きます。
例えば、フロントhttp://localhost:3000
, バックhttp://localhost:5000
とした時、フロントからバックのapiを叩こうとするとcorsエラーとなります。(ドメイン違うので)正常にapi叩くにはこれを解決する必要があります。
Next.jsではカスタムサーバを利用し、プロキシする
Next.jsでカスタムサーバを使用するため、 プロジェクトルートに
server.js
を作成します。また、
http-proxy-middleware
というパッケージを使い、任意のリクエストをプロキシします。server.jsconst express = require('express'); const next = require('next'); const { createProxyMiddleware } = require('http-proxy-middleware'); const port = parseInt(process.env.PORT, 10) || 3000 const dev = process.env.NODE_ENV !== 'production' const API_URL = process.env.API_URL || 'http://localhost:5010' const app = next({ dev }) const handle = app.getRequestHandler() app.prepare().then(() => { const server = express(); server.use( '/api', createProxyMiddleware({ target: API_URL, pathRewrite: { "^/api": "" }, changeOrigin: true }) ); server.all('*', (req, res) => { return handle(req, res) }); server.listen(port, err => { if (err) throw err console.log(`> Ready on http://localhost:${port}`) }); });カスタムサーバを利用してサーバ起動するので
package.json
を修正します。package.json... "scripts": { "dev": "node server.js", "build": "next build", "start": "NODE_ENV=production node server.js" }, ...これで
yarn dev
でカスタムサーバで立ち上がります。リクエストがプロキシされる
上記により、
/api
へのリクエストはhttp://localhsot:5010
にプロキシされます。
例えば/api/auth
へリクエストした場合、http://localhost:5010/auth
にプロキシされます。
pathRewrite
の記述を消せばhttp://localhost:5010/api/auth
にアクセスします。まとめ
Next.jsのcorsエラーの解決は、カスタムサーバと
http-proxy-middleware
パッケージの利用で解決できます。また忘れてハマりそうなのでメモしておいた。
参考
- 投稿日:2020-03-22T13:08:13+09:00
Next.js(React)でのCORSエラー解決(Next.js + Golang)
Next.jsでのCORSエラー解決(Next.js + Golang)
React + GoでSPAを作りたく、ReactフレームワークのNext.jsを使ってみた。
SPAの最初で躓きがちなCORSエラーについて、案の定ハマったのでメモ。(Angularでの開発の際もだいぶハマった思い出)CORSエラーが出る
今回のようにフロントエンド、バックエンドでサーバを分けて開発する際に、そのままだとCORS(Cross-Origin Resource Sharing)についてのエラーが発生する。
これはブラウザの機能として、クロスドメイン(ドメインの違うサーバへの)アクセスが制限されているため起きます。
例えば、フロントhttp://localhost:3000
, バックhttp://localhost:5000
とした時、フロントからバックのapiを叩こうとするとcorsエラーとなります。(ドメイン違うので)正常にapi叩くにはこれを解決する必要があります。
Next.jsではカスタムサーバを利用し、プロキシする
Next.jsでカスタムサーバを使用するため、 プロジェクトルートに
server.js
を作成します。また、
http-proxy-middleware
というパッケージを使い、任意のリクエストをプロキシします。server.jsconst express = require('express'); const next = require('next'); const { createProxyMiddleware } = require('http-proxy-middleware'); const port = parseInt(process.env.PORT, 10) || 3000 const dev = process.env.NODE_ENV !== 'production' const API_URL = process.env.API_URL || 'http://localhost:5010' const app = next({ dev }) const handle = app.getRequestHandler() app.prepare().then(() => { const server = express(); server.use( '/api', createProxyMiddleware({ target: API_URL, pathRewrite: { "^/api": "" }, changeOrigin: true }) ); server.all('*', (req, res) => { return handle(req, res) }); server.listen(port, err => { if (err) throw err console.log(`> Ready on http://localhost:${port}`) }); });カスタムサーバを利用してサーバ起動するので
package.json
を修正します。package.json... "scripts": { "dev": "node server.js", "build": "next build", "start": "NODE_ENV=production node server.js" }, ...これで
yarn dev
でカスタムサーバで立ち上がります。リクエストがプロキシされる
上記により、
/api
へのリクエストはhttp://localhsot:5010
にプロキシされます。
例えば/api/auth
へリクエストした場合、http://localhost:5010/auth
にプロキシされます。
pathRewrite
の記述を消せばhttp://localhost:5010/api/auth
にアクセスします。まとめ
Next.jsのcorsエラーの解決は、カスタムサーバと
http-proxy-middleware
パッケージの利用で解決できます。また忘れてハマりそうなのでメモしておいた。
参考
- 投稿日:2020-03-22T13:05:28+09:00
JSのレスポンシブ対応をresizeからmatchMediaに移行した
JSでブレークポイント毎に処理を分ける場合の方法をresizeからmatchMediaに移行したので、その際のメモ書きです。
昔ながらの方法
恥ずかしながら、最近までJSでブレークポイント毎にJS処理を切り替える場合、
以下のように昔ながらのresizeイベントの監視を使っていました...昔ながらの方法/** * イベントリスナー */ const listener = () => { // リサイズ時に行う処理 if (window.innerWidth >= 768) { // 768px以上 console.log('PC用ブレークポイント用処理'); } else { // 768px未満 console.log('SP用ブレークポイント用処理'); } }; // リスナー登録 window.addEventListener('resize', listener); // 初期化処理 listener();この方法は、リサイズの度にゴリゴリlistener関数が実行されるので、できればやめたいと考えていました...
window.matchMediaをとりあえず使ってみた方法
以前、何かの記事で window.matchMedia を見かけたときは、以下のようにCSSのメディアクエリと同じ記法で書けるので、いいなーぐらいの認識でした。
window.matchMediaをとりあえず使ってみた方法/** * イベントリスナー */ const listener = () => { // リサイズ時に行う処理 if (window.matchMedia('(min-width: 768px)').matches) { // 768px以上 console.log('PC用ブレークポイント用処理'); } else { // 768px未満 console.log('SP用ブレークポイント用処理'); } }; // リスナー登録 window.addEventListener('resize', listener); // 初期化処理 listener();
これなら(orientation: portrait)
などデバイスの向きの判定もできますが、あんまりそういう複雑なメディアクエリ書く機会もなく、これなら別に置き換える意味も薄いなーとwindow.matchMediaをちゃんと使った方法
先日とあるOSSのissueを見ているときに以下のような
window.matchMedia
の使い方を見つけましたwindow.matchMediaをちゃんと使った方法const mediaQueryList = window.matchMedia("(max-width:767px)"); /** * イベントリスナー */ const listener = (event) => { // リサイズ時に行う処理 if (event.matches) { // 768px以上 console.log('PC用ブレークポイント用処理'); } else { // 768px未満 console.log('SP用ブレークポイント用処理'); } }; // リスナー登録 mediaQueryList.addListener(listener); // 初期化処理 listener(mediaQueryList);listener関数が実行されるのは、ブレークポイントが切り替わったタイミングのだけなので、
resizeの監視と比べて無駄にlistener関数が実行されなくなりますこの使い方がMDNのサンプルに載っていないのかなと思っていたら
matchMedia
の戻り値のMediaQueryListのほうに書いてありました。。。
そこまでは、なかなか見ない...
- 投稿日:2020-03-22T12:35:51+09:00
あなたのkintone開発にDeveloper Experienceはありますか?
はじめに
最近、kintoneを触っていて不満に思っていたのが、開発のパラダイムが前近代的になりがちであるということでした。
製品自体が小規模な部門内システム(これまでExcelファイルの共有やマクロで行っていたこと)をメインターゲットとしていると思われ、そもそも規模感のある開発を(敢えて?)想定しないためか、デプロイの管理機能を持っていません(サードパーティー製品で可能です)。
また、管理単位がアプリ(データベースの1テーブル)であり、複数アプリが連携するようなものを纏めて管理することができません(纏めて他の環境に新規作成するための「テンプレート」出力機能はありますが、アプリの更新には使えません)。
公式に配布されているCLIツール(customize-uploader、kintone-dts-gen)も単機能かつ単一アプリしか考慮しておらず、組織全体またはシステム全体の開発支援・デプロイツールとしては力不足と言わざるを得ません。
結果として、アプリのメタ情報は履歴管理しない(あるいはサードパーティー製品でブラックボックスとして管理)、コードのみはリポジトリ管理、デプロイは最悪手動で行うことになります。
システム内の小さなアプリではTypeScriptのボイラープレート準備の煩雑さから、そのままバンドラー無し・Babel無しのJavaScriptでコードを書いて、Webコンソールからアップロードすることもありました。(ボイラープレートくらい事前に準備しろよという声も聞こえてきそうですが・・・現実は思うようにならないのです)Salesforce DXのように、リポジトリ中心の開発、デプロイ操作が整合性のある形で実現できれば良いのですが、残念ながら現状の公式ツールでは実現できません。
そこで、kdxという新しいCLIツールを作成してみました。
CLI本体
https://github.com/shellyln/kdxプロジェクトテンプレート
https://github.com/shellyln/kdx-project-templateKDXとは
kintone CLI for development & deployment, with Developer Experience.
Enjoy type-safe and repository-centric development!KDXは、Developer Experienceを保ってkintoneを基盤とするシステムの開発・デプロイを行うためのCLIツールです。
主な機能として
- アプリのメタ情報(フィールド、レイアウト、一覧、コード、権限、等)の pull / push
- コードは、プロジェクト内の出力ファイルおよび静的ファイルの指定も可能
- コードのコンパイル・パッケージング(プロジェクトテンプレートの機能)
- アプリのメタ情報からTypeScript型情報・入力検証用スキーマを生成
- カスタム一覧のHTML編集支援(別のファイルに分離)
- dev/staging/prod等、複数環境へのデプロイ
を可能としています。
アプリ開発
コーディング
アプリのカスタマイズは、プロジェクトテンプレートを元にして以下の4ファイルの編集から始めることができます。
index.tsでは、スキーマに基づく独自の入力検証を行っています。スキーマファイルを編集することで、標準では行えない検証も宣言的に記述することができます。
また、検証後データには型が付いています。src/apps/MyApp1/index.tsimport { deserializeFromObject } from 'tynder/modules/serializer'; import { getType } from 'tynder/modules/validator'; import { SubmitEvent } from 'kdx/helpers/kintone-types'; import { validateThen } from 'kdx/helpers'; import './index.scss'; import { App } from '../../schema-types/MyApp1'; import AppSchema from '../../schema-compiled/MyApp1'; const schema = deserializeFromObject(BarSchema); const tyApp = getType(schema, 'App'); kintone.events.on([ 'app.record.create.submit', 'mobile.app.record.create.submit', 'app.record.edit.submit', 'mobile.app.record.edit.submit', 'app.record.index.edit.submit', ], (ev: SubmitEvent<unknown>) => { return validateThen<App>(ev, schema, tyApp, (rec) => { if (rec.tableA.length > 0) { rec.tableA[0].num1 = typeof rec.tableA[0].num1 === 'number' ? rec.tableA[0].num1 + 1 : void 0; } return rec; }, (ev) => { ev.error = 'Error! Error! Error!'; }); });src/apps/MyApp1/index.scss@charset "UTF-8"; @import "kdx/helpers/51-modern-default.css";テストランナーも設定されているので、テンプレートの以下のファイルを編集することで、ユニットテストを記述できます。
src/apps/MyApp1/text.tsimport 'mocha'; import { expect } from 'chai'; mocha.setup('bdd'); describe('test 1', () => { it('test 1-1', () => { expect(1).to.equal(0); }); }); document.addEventListener('DOMContentLoaded', () => { if (document.getElementById('mocha')) { mocha.run(); } });views/apps/MyApp1/TestRunner.html<div>Hello, World!</div> <link rel="stylesheet" href="https://unpkg.com/mocha/mocha.css" /> <div id="mocha"></div>検証の設定
メタ情報のpull時に自動生成されるスキーマファイルを編集することで、検証をカスタマイズできます。
編集後はcompile-schema
サブコマンドの実行が必要です。/** Sub-table */ export interface Table { @meta({fieldType: 'SINGLE_LINE_TEXT'}) itemName: string; @range(3, 5) @meta({fieldType: 'NUMBER'}) itemValue: number; } /** App */ export interface App { /** name */ @minLength(2) @maxLength(16) @match(/^[A-Z][0-9]{3}-.+/) @meta({fieldType: 'SINGLE_LINE_TEXT'}) name: string; /** amount (not required) */ @meta({fieldType: 'NUMBER'}) amount?: number; /** due date */ @stereotype('lcdate') @range('=today first-date-of-mo', '=today last-date-of-mo') @msg({ valueRangeUnmatched: 'Please input date of this month', }) @meta({fieldType: 'DATE'}) dueDate: string; /** last contact datetime */ @stereotype('lcdatetime') @minValue('=today first-date-of-mo') @lessThan('=today last-date-of-mo +1day') @msg({ valueRangeUnmatched: 'Please input date-time of this month', }) @meta({fieldType: 'DATETIME'}) contactDt: string; /** Sub-table */ @constraint('unique', ['itemName']) @meta({fieldType: 'SUBTABLE'}) table: Table[]; }デプロイ
https://github.com/shellyln/kdx/blob/master/README.md#configurations
に記載していますが、.env
ファイルに接続情報を記載した上で、pull
/push
サブコマンドを使用して行います。kdx pull MyApp1 npm run build kdx push MyApp1使用しているライブラリ・技術
APIアクセス
kintone REST APIへのアクセスは kintone-rest-api-client に依存しています。
スキーマ生成と入力検証
自作のライブラリ Tynder を使用しています。
kintone REST APIから取得したフィールド情報を基に、以下のようなスキーマ(Tynder DSL)をテキストとして生成します。export interface App { @meta({fieldType: 'NUMBER'}) amount?: number; }これを事前コンパイルして、型情報(.d.ts)とコンパイル済みスキーマ(.ts; 中身はJSONをデフォルトエクスポートしているのみ)を生成します。
アプリからこれらをインポートします。src/apps/MyApp1/index.tsimport { deserializeFromObject } from 'tynder/modules/serializer'; import { getType } from 'tynder/modules/validator'; import { SubmitEvent } from 'kdx/helpers/kintone-types'; import { validateThen } from 'kdx/helpers'; import './index.scss'; import { App } from '../../schema-types/MyApp1'; import AppSchema from '../../schema-compiled/MyApp1'; ...入力検証を通った際に渡されるオブジェクトは、イベントオブジェクトそのままのメタ情報が付属したものではなく、
{フィールド=値,...}
の「普通の」オブジェクトになっています。src/apps/MyApp1/index.ts... return validateThen<App>(ev, schema, tyApp, (rec) => { if (rec.tableA.length > 0) { ...これは、スキーマをランタイム型情報としても利用することで行っています。スキーマDSL内の
@meta({fieldType: 'NUMBER'})
の部分がkintoneの型を表しています。kintoneのイベントオブジェクトのデータには型情報が付いていると上述しましたが、サブテーブルの新規行の処理等を考えると、スキーマからのランタイム型情報が無ければ実現できません。(逆にイベントオブジェクトに型情報を付けてくる意味が薄いと言えます)さいごに
まだ、不具合があるかもしれませんが、何とかリリースできました。
足りないところもあるかと思うので、少しずつ機能強化できればと思います。