20200322のJavaScriptに関する記事は30件です。

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'));

スクリーンショット 2020-03-22 22.51.26.png

はじめに

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));

スクリーンショット 2020-03-22 22.49.55.png

(tips: 第三引数にtrueを渡すことで値から小数点を切り捨てず返されます)

以下はmomentjsでdiffを行なっている箇所です。
GitHub - moment.js/moment/src/lib/moment/diff.js


厳密な日付を計算するのであればこれが最良ですが、時間を考えず単純に日にちが変わったかどうかを計算したい、といった場合も多いと思います。
startOfを使えば、そのような機能を簡単に実装することができます。

startOfとは

momentの機能の一つで、指定した時間単位の始まりの値を出力します。
スクリーンショット 2020-03-22 23.02.30.png
startOfでdayを指定すると、日のはじまり0:00:00に時刻を変換して出力されます。

参考: moment.js - Start of Time

では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'));

スクリーンショット 2020-03-22 22.51.26.png

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));

スクリーンショット 2020-03-22 23.10.12.png
この場合、ある程度時間がたってしまった値だと、日付が同じでも繰り上げてしまいますので使用することができません。
なので、startOfが現状最良の手段だと思われます。


いかがでしたでしょうか?時間単位のはあくまで例に使っただけなので、その他の単位でも使用することができると思います。momentはドキュメントが丁寧でわかりやすく、自動翻訳でもそれなりに見ることができるので、皆さんもよかったらご一読ください。

moment.js - Docs

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

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'));

スクリーンショット 2020-03-22 22.51.26.png

はじめに

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));

スクリーンショット 2020-03-22 22.49.55.png

(tips: 第三引数にtrueを渡すことで値から小数点を切り捨てず返されます)

以下はmomentjsでdiffを行なっている箇所です。
GitHub - moment.js/moment/src/lib/moment/diff.js


厳密な日付を計算するのであればこれが最良ですが、時間を考えず単純に日にちが変わったかどうかを計算したい、といった場合も多いと思います。
startOfを使えば、そのような機能を簡単に実装することができます。

startOfとは

momentの機能の一つで、指定した時間単位の始まりの値を出力します。
スクリーンショット 2020-03-22 23.02.30.png
startOfでdayを指定すると、日のはじまり0:00:00に時刻を変換して出力されます。

参考: moment.js - Start of Time

では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'));

スクリーンショット 2020-03-22 22.51.26.png

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));

スクリーンショット 2020-03-22 23.10.12.png
この場合、ある程度時間がたってしまった値だと、日付が同じでも繰り上げてしまいますので使用することができません。
なので、startOfが現状最良の手段だと思われます。


いかがでしたでしょうか?時間単位のはあくまで例に使っただけなので、その他の単位でも使用することができると思います。momentはドキュメントが丁寧でわかりやすく、自動翻訳でもそれなりに見ることができるので、皆さんもよかったらご一読ください。

moment.js - Docs

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

高階関数を書いたら、中級者になれた気がした。を批判したら上級者になれた気がした。をありがたく拝読した。

元記事

高階関数を書いたら、中級者になれた気がした。

批判記事

高階関数を書いたら、中級者になれた気がした。を批判したら上級者になれた気がした。

休日ワイ

ワイ「おっ」
ワイ「ワイの記事に対して批判記事を書いてくださっている方がおるやんけ」
ワイ「ありがたいこっちゃ」
ワイ「いっちょ学ばせてもらおか!」

音読完了

ワイ「なるほどな〜」
ワイ「大変勉強になったわ」
ワイ「必読の記事やな!」

しかし気になることが

ワイ「ん?」
ワイ「なんやこのコード?」

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行もコード書いてんねやろ?」
ワイ「mapArraydoubleArraytenMinusArrayも無駄ですやん」
ワイ「もしもワイがこんなコードを書いたなら・・・」

ワイ「うわ!10行以上も無駄なコード書いてもうた!」
ワイ「しかも高階関数の記事なのに、mapという高階関数を再発明してもうた!」

ワイ「って後悔してしまうなぁ・・・」

もしかして

ワイ「あっ!?」
ワイ「分かったで!」
ワイ「書いたことを後悔するような関数をわざと書いて」
ワイ「後悔関数高階関数を掛けていらっしゃるんや!」
ワイ「いやー、ギャグセンスが高過ぎてかなわんわ!」
ワイ「ん、なになに?」
ワイ「このコードの説明が書いてあるで」

カリー化と部分適用を使ってさらに綺麗に美しく書くことができる。

ワイ「確かに綺麗や」
ワイ「美しいくらい見事なボケやわ!」
ワイ「全体的に学びも多いし、素晴らしい記事やし」
ワイ「さすが上級者や!」

〜おしまい〜

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

【ES6】 JavaScript初心者でもわかるPromise講座

はじめに

Promiseって...難しくないですか???
3ヶ月くらい前の私は、Promiseをほとんど理解できてないのにasync/awaitとか使っちゃってたし、様々な記事を読み漁ってもなかなか理解できず、Promiseの正体を掴むのに時間がかかってしまいました。

そんな3ヶ月くらい前の自分にも伝わるように、できる限り丁寧にPromiseを説明していこうと思います。

前提

本記事では、Promise以外のES6の書き方に関しては触れておりません。
アロー関数やテンプレート文字列などを例文で用いているため、わからない方がいましたら下記記事などを参考にしていただけると幸いです。

ES2015(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で作られます。
処理が成功した時に、PromiseStatusresolvedに変わり,thenに書かれた処理が実行されます。
処理が失敗した時は、PromiseStatusrejectedに変わり、catchに書かれた処理が実行されます。

Promiseの大まかな処理の流れはわかりましたでしょうか。
それでは実際に書いてみましょう...!

Promiseの書き方

Promiseインスタンスの作成

sample1.js
const promise = new Promise((resolve, reject) => {});

Promiseの引数には関数を渡し、その関数の第一引数にresolveを設定し、第二引数にrejectを任意で設定します。
(resolverejectも関数です。)

ちなみに、上記コードにて定義したpromiseconsole.logすると下記のようなPromiseオブジェクトが返ってきます。

Promiseオブジェクト pending

作りたてのPromiseオブジェクトなので、PromiseStatusは、pendeingですね。

resolveさせよう

sample1のコードをresolveさせてみましょう。

// rejectは今回使わないため、引数から削除
const promise = new Promise((resolve) => {
  resolve();
}).then(() => {
  console.log("resolveしたよ");
});

実行結果は下記のようになります。

resolveしたよ

Promiseオブジェクトを確認してみると...
Promiseオブジェクト resolved

resolve()を呼び出すことによって、
PromiseStatusresolvedに変わり,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したよ");
});

スクリーンショット 2020-03-22 18.24.11.png

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オブジェクトを確認してみると...

スクリーンショット 2020-03-22 18.42.37.png

想定通り、PromiseStatusrejectedに.....なってない!!!!!

なぜrejectedにならない...?

下記コード、つまりcatchの処理を行なっていない状態のPromiseオブジェクトをみてみましょう。

const promise = new Promise((resolve, reject) => {
  reject();
});

Promiseオブジェクト reject

この時点では、PromiseStatusrejectedですね。
ちょっと安心ですね。笑

(Uncaught (in promise)というエラーはrejectされた時に、一回もcatchの処理が行われなかった時にでます。)

実は、catchにて実行される関数がreturnした値をresolveします。
めちゃくちゃ簡単にいうと、catchはエラー返したら満足して、解決済みだ!ってするみたいです。
つまり、catchにて返されたpromiseオブジェクトPromiseStatusresolveになります。

んー、難しい。

Promiseのメソッドチェーン

処理が成功した時にthenに書かれた処理が実行され、処理が失敗した時はcatchに書かれた処理が実行されると説明しましたが、それらの処理の後にまた別のthenを実行することができます。

さらに、returnで返した値を第一引数として次のthenに渡すことが可能です。

resolveした場合.js
const 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が実行されたあと、
returnvalを受け取り、2つめのthenが実行されました。

rejectした場合.js
const 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.allPromise.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オブジェクトが返ってきます。

スクリーンショット 2020-03-22 22.00.44.png

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を理解できたような気がします。)

参考記事

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

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 .
初期表示

image.png

来月ボタン押下時

image.png

画面更新で真っ白になることが無くなった!!!(画像じゃ分からない)

TODO

・コードをもっとスッキリさせたい。
・Electronの機能をもっと使いたい。

あとがき

・Javaの型で縛るプログラミングに慣れているので動的型付け言語は自由だけど不自由。
・ロジックを書くのはやっぱり楽しい。プログラミングの醍醐味だと思う。
・Electronの記事なのにほぼHTML・JSの記事になってる・・・。今回に関してはipc通信の箇所すら削っちゃってる!!!

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

魔法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() メソッドですべての記事にアクセスしてtitletagsを取得するだろ」
イ「ふむふむ」
あ「で、さらにtags内のすべての要素にmap() メソッドでアクセスして、nameのみの配列を作成してから、それをjoin()メソッドで連結してるんだよ」
イ「いやー、さすがはあれい! 魔法少女コスが壊滅的に似合わないけど仕事は早いね!」
あ「しばくぞ」

解説

map() メソッドは、与えられた関数を配列のすべての要素に対して呼び出し、その結果からなる新しい配列を生成します

MDNより)

配列のすべての要素に対してコールバック関数を実行し、返ってきた結果で新しい配列が作成されます。
コールバック関数では、filter() メソッドと同じく、「現在の要素、現在の要素のインデックス、元の配列」という3つの引数を使用できます。

forEach()

解説

forEach() メソッドは与えられた関数を、配列の各要素に対して一度ずつ実行します。

MDNより)

さて、forEach() メソッドについては解説のみとなります。なぜなら、forEach() メソッドには戻り値がないため、敵に攻撃できないからです。
配列のすべての要素に対して関数を実行する……という点ではmap() メソッドなどと同じ動きをしますが、戻り値がないのでメソッドチェーンとして使用しづらく、map()reduce()を使わずにあえてforEach() を使うというケースが、正直言って思いつきません。
(……逆に言うと、他のオブジェクトであるMapSetではforEach()くらいでしか反復処理ができないけど、Arrayオブジェクトは便利なメソッドが豊富に揃っていて幸せだなー、という事なんでしょうね。多分……)

あ「なんだこの試合放棄感丸出しの解説は」
イ「一応、記事のタイトルを順にコンソールに出力する例だけでも提示しておいてよ!」
あ「面倒くせえなあ……」

items.forEach(item => console.log(item.title));

次回予告

ずっと配列の要素達にコールバック関数を実行しながら、あなたは何も感じなかったの?
皆がどんな風にアキュムレータに代入されたか、わかってあげようとしなかったの?

第11話 最後に残ったreduceとreduceRight

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

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/visjs

Qiitaの記事

ノードの名前を変更するコードが載っています!
https://qiita.com/KyoheiOkawa/items/1ca82afada3761c3be92

vis.jsではなくJavaScript。ダブルクリックイベントを実装するときに助けてもらいました!
https://qiita.com/sashim1343/items/e3728bea913cadab677d

基本的な書き方がしっかりのっています!!
https://qiita.com/junjis0203/items/ecc3f4eb9b3a1c4802e4

終わりに

最後までありがとうございました!!!
僕が紹介したのはあくまでも僕が実装するときに役立ったもので、もっと他にも素晴らしい記事があると思うので他の記事も見てみてくださいね!
アプリとして完成すればそのリンクを追記したいと思います!
そのときにvis.jsのコードも載せようと思うのでもしよければ定期的に確認してみてください!

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

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/
repl.PNG

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>

実行結果:
23.PNG

Svelteの利点

表現がとても柔軟です。最初の例と変わらずに記述することができました。

Svelteの問題点

柔軟が故に以下のようなミスも起こし易いです。が、REPLで直ぐに結果を確認しておけばミスは減るでしょう。

12.PNG

③ 条件付き繰り返し

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>

sv.PNG

Svelteの利点

依然として表記としては短く簡単ですね。

Vueの問題点

なんか、このあたりVue、つらそうで可哀想。

私のまとめ

Svelteは初期の学習コストが低く、Vueのように独自に覚えることが少ない点は良い点です。しかし、複雑性が増した時にJSで頑張って書くことになります。Vueと同じく、副作用も柔軟に書けることの利点が問題点に跳ね返ってくることがしばしばありそうです。その時にどう工夫をして解決していくかが課題となると思います。
Elmとの比較は、元記事を読みましょう!私はある規模を越えたらelmを使う!
すなわち、Svelte+elm =Sveltelm ;-)

そして、Thanks, @ababup1192さん!

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

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_sample.gif

ハマったこと

サンプルコードは以下の通りなのですが、ハマったのは、
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のrenderItempropsの型として定義してあげる方法です。

修正後のコード

   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

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

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.js
import 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.js
import 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.js
   onSubmit(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のデータを引き継ぎできないという課題が残るため、引き続き調べていきます。

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

Nuxt.js 存在しないページを一括でリダイレクトしたい時

/pages/*.vue
を下記の内容で作成する。

<template>
  <div>
    404 redirect
  </div>
</template>
<script>
export default {
  middleware({ redirect }) {
    return redirect('/')
  }
}
</script>

That's it.
Relax, man.

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

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' }

で、上書きしてあげましょう

送信内容

スクリーンショット 2020-03-22 16.48.47.png

こんな感じです。

確認ポイント

  1. RequestHeaderscontent-typemultipart/form-dataになっている
  2. FormDataのvalueが(binary)になっている

まとめ

これで正常にmultipart/form-dataでファイルをPOSTできるようになりました!
WEBサービスを作成する際、画像のアップロードはよく出でくるシチュエーションだとおもいますので、参考になれば幸いです

初めてこの機能を実装する際には迷う部分がたくさんあって、ファイルのPOSTする形も何種かあると思っていましたが、2種類のみのようです。

  • Fileのまま送信(multipart/form-data)
  • Base64にエンコードして送信(application/json)

エンコードするとjsonのまま送れるメリットがあるのですが、ファイルサイズが20~30%あがるそうです。
やればやるほど奥が深く、正解がなくて迷いますが、楽しいですね

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

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' }

で、上書きしてあげましょう

送信内容

スクリーンショット 2020-03-22 16.48.47.png

こんな感じです。

確認ポイント

  1. RequestHeaderscontent-typemultipart/form-dataになっている
  2. FormDataのvalueが(binary)になっている

まとめ

これで正常にmultipart/form-dataでファイルをPOSTできるようになりました!
WEBサービスを作成する際、画像のアップロードはよく出でくるシチュエーションだとおもいますので、参考になれば幸いです

初めてこの機能を実装する際には迷う部分がたくさんあって、ファイルのPOSTする形も何種かあると思っていましたが、2種類のみのようです。

  • Fileのまま送信(multipart/form-data)
  • Base64にエンコードして送信(application/json)

エンコードするとjsonのまま送れるメリットがあるのですが、ファイルサイズが20~30%あがるそうです。
やればやるほど奥が深く、正解がなくて迷いますが、楽しいですね

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

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
要素の取得方法まとめ - Qiita

Chrome拡張機能

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など発生している場合もぜひご教示ください。

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

DangerとGitHub ActionsでPull Request時に最低限のレビューを自動化する

概要

チーム開発において Pull Request 時のコードレビューは必要不可欠です。
コードレビューにはもちろん、レビューワーの工数がかかります。
そこで、レビューワーの負担を少なくするために、
GitHub ActionsDanger で必要最低限のレビューを自動化し、本質的な部分のみ
レビューワーがチェックするようにしたいと思います。

実装

ディレクトリ構造例

tree
danger-github-actions
├── .eslintrc.js
├── .github
|  └── workflows
|    └── actions.yml
├── src
|  ├── greeting.js
|  └── greeting.test.js
├── dangerfile.js
├── package.json
└── yarn.lock

Danger

dangerfile.js をルートディレクトリ配下に作成します。
DangerTypeScript にも対応しているため、 dangerfile.ts としても問題ありません。

danger.js
let 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.yml
name: 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 が実行され、
下記のように結果がコメントとして投稿されます。

スクリーンショット 2020-03-22 16.18.31.png

最後に

ESLint のように設定をカスタマイズすることができます。
Pull Request 作成時にもチームとしてルールを設けて見てはいかがでしょうか ☺️

refs

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

【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.js
new 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
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

魔法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にも頼らない

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

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.

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

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.

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

WebSocket の負荷テストは Artillery でシュッと簡単に実行しよう

Artillery は yaml ファイルに宣言的にシナリオを記述し、シンプルなインタフェースで負荷をかけることができる Nodejs 製の負荷テストツールです。
本記事では Artillery を使用して簡単に WebSocket サーバの負荷テストを実行する方法を紹介します。

最小構成の WebSocket サーバ

まずはじめに WebSocket サーバを実装しましょう。今回は Node.js を使用します。
必要最小限の機能だけを提供します。ws ライブラリを使用して簡単に実装しましょう。

server.js
const 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 wscat

WebSocket サーバを起動し、wscat で接続したら任意のメッセージを送信してみましょう。1つのクライアントからの送信を受けて、他のクライアントへ broadcast していることがわかります。

wscat

Artillery を使用して負荷テストを実行する

さて、ようやく本題です。Artillery を使用して負荷テストをかけてみましょう。
最小限のシナリオファイルのサンプルです。シナリオファイルは senario.yml のような名前をつけておきます。

senario.yaml
config:
  target: "ws://localhost:3000"
  phases:
    - duration: 20
      arrivalRate: 10
scenarios:
  - engine: "ws"
    flow:
      - send: "hello"

以下、コマンドで実行します。

$ artillery run senario.yml

run

アクティブなコネクション数を調整する

実際に WebSocket を用いたアプリケーションでは、常に多くのコネクションが張られていることが一般的です。上記のシナリオでは hello というメッセージを送ったらすぐにコネクションを切断してしまうので、常時アクティブなコネクションが少ない状態であまり現実的ではありません。まずは、think を指定してアクティブなコネクション数が増えるように調整しましょう。また、同じメッセージを繰り返し送信する loop も指定できます。

senario.yaml
scenarios:
  - engine: "ws"
    flow:
      - send: "hello"
      - think: 1 # pause for 1 second
      - loop:
          - send: "world"
        count: 5

オブジェクトの送信に対応する

さて先ほどまでは string 形式のデータだけに限定していましたが、{"name":"john", "age":24} のようにオブジェクト形式で送信する方が良いこともあるでしょう。以下のように記述すれば、送信時に Stringify してくれます。

senario.yaml
scenarios:
  - engine: "ws"
    flow:
      # the following will be stringified and sent as '{"name":"john","age":24}'
      - send:
          name: "john"
          age: 24

カスタムコードを使用してタイムスタンプを付与する

また、送信したタイムスタンプを付与したいこともあります。このようなケースに対応するためにはカスタムコードを使用して柔軟に値を差し込むことができます。

senario.yaml
config:
  target: "ws://localhost:3000"
  processor: "./custom.js"
scenarios:
  - engine: "ws"
    flow:
      # custom code for timestanp
      - function: "createTimestampedObject"
      - send: "{{ data }}"

非常にシンプルに記述し、シュッと実装することができました。Artillery は WebSocket だけではなく、HTTPのプロトコルもサポートしています。本記事は公式ドキュメントから参照していますので、詳細に理解されたい方は一度ドキュメントに目を通してみるのも良いでしょう。

また、CircleCIで継続的に負荷テストを実行するTipsをこちらの記事で紹介していますのでぜひご参照ください。

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

高階関数を書いたら、中級者になれた気がした。を批判したら上級者になれた気がした。

はじめに

「高階関数を書いたら、中級者になれた気がした」という記事を読んで色々ともやもやしたので批判をしてみる。といった趣旨の記事です。

きっかけはこちらのツイートから⇒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]*2array[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!。

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

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~現時点)の仕様によりプリミティブ値はtypeofObject.getPrototypeOf()instanceofの結果が一致しません。例えば文字列リテラル"abc"についてこれらを実行した結果は次のようになります。

typeof "abc" // "string"
Object.getPrototypeOf("abc") // String {...}
"abc" instanceof String // "false"
"abc" instanceof Object // "false"

typeofObject.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では0Booleanではfalseです。また、BigIntSymbolでは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>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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~現時点)の仕様によりプリミティブ値はtypeofObject.getPrototypeOf()instanceofの結果が一致しません。例えば文字列リテラル"abc"についてこれらを実行した結果は次のようになります。

typeof "abc" // "string"
Object.getPrototypeOf("abc") // String {...}
"abc" instanceof String // "false"
"abc" instanceof Object // "false"

typeofObject.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では0Booleanではfalseです。また、BigIntSymbolでは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>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

水戸黄門を使って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 ヘッダーを使用して、あるオリジンで動作しているウェブアプリケーションに、異なるオリジンにある選択されたリソースへのアクセス権を与えるようブラウザーに指示するための仕組みです。

もうね、日本語で書かれてるのに理解できないんだよね。オリジンってオリジン弁当かよって感じ。

助さん格さん

image.png
「この紋所が目に入らぬか!」ってやるじゃない?あれって、水戸黄門がいない世界線で、格さんがあの紋所を掲げても意味がないわけよ。なんやそのオブジェクトォー!みたいになるよね。つまりCORSの説明に都合がいいように表現すると、虎の威を借る狐が格さんなわけ。そういういわば「虎の威」がないと、爺さんのいない格さんは一般人なんだから、「委任状」がないなら大事なgoogleapiのデータはあげないもんねー!っていう仕組みがCORS。

俺、あの有名人と知り合いなんだよね

それを防ごうとしてるんじゃないか!?
image.png

委任状を書く(ヘッダーを加工する)必要がある

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は自オリジンにしかコンタクトできん)ってこっちゃ

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

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

したがって、通常関数はclassextendsできますが、アロー関数はできません:

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

アロー関数はletconstの仕様上、同じ関数名で定義を上書きすることができません:

let arrow = () => {}
let arrow = () => {}
//=> SyntaxError: Identifier 'arrow' has already been declared

もちろん、アロー関数でもvarで宣言すると、上書き可能です:

var arrow = () => {}
var arrow = () => {}

違い10: super, new.targetの有無

アロー関数 - JavaScript | MDNによれば、アロー関数には束縛されたsupernew.targetが無いようですが、これを示すサンプルコードが思いつかなかったので割愛します。

まとめ

本稿では、JavaScriptの2種類の関数、通常関数とアロー関数の性質の違いについて説明しました。通常関数とアロー関数では、様々な性質の違いあり、単純に書き方の違いではないことが分かりました。

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

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

したがって、通常関数はclassextendsできますが、アロー関数はできません:

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

アロー関数はletconstの仕様上、同じ関数名で定義を上書きすることができません:

let arrow = () => {}
let arrow = () => {}
//=> SyntaxError: Identifier 'arrow' has already been declared

もちろん、アロー関数でもvarで宣言すると、上書き可能です:

var arrow = () => {}
var arrow = () => {}

違い10: super, new.targetの有無

アロー関数 - JavaScript | MDNによれば、アロー関数には束縛されたsupernew.targetが無いようですが、これを示すサンプルコードが思いつかなかったので割愛します。

まとめ

本稿では、JavaScriptの2種類の関数、通常関数とアロー関数の性質の違いについて説明しました。通常関数とアロー関数では、様々な性質の違いあり、単純に書き方の違いではないことが分かりました。


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

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

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.js
const 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 パッケージの利用で解決できます。

また忘れてハマりそうなのでメモしておいた。

参考

Next.js 公式ドキュメント日本語翻訳プロジェクト - カスタムサーバーとルーティング

Nextで特定のAPI リクエストをproxyする方法 - Qiita

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

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.js
const 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 パッケージの利用で解決できます。

また忘れてハマりそうなのでメモしておいた。

参考

Next.js 公式ドキュメント日本語翻訳プロジェクト - カスタムサーバーとルーティング

Nextで特定のAPI リクエストをproxyする方法 - Qiita

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

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関数が実行されるので、できればやめたいと考えていました...:bomb:

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) などデバイスの向きの判定もできますが、あんまりそういう複雑なメディアクエリ書く機会もなく、これなら別に置き換える意味も薄いなーと:innocent:

window.matchMediaをちゃんと使った方法

先日とあるOSSのissueを見ているときに以下のようなwindow.matchMediaの使い方を見つけました:bulb:

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関数が実行されなくなります:hugging::hugging::hugging:

この使い方がMDNのサンプルに載っていないのかなと思っていたらmatchMediaの戻り値のMediaQueryListのほうに書いてありました。。。
そこまでは、なかなか見ない...

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

あなたのkintone開発にDeveloper Experienceはありますか?

はじめに

最近、kintoneを触っていて不満に思っていたのが、開発のパラダイムが前近代的になりがちであるということでした。

製品自体が小規模な部門内システム(これまでExcelファイルの共有やマクロで行っていたこと)をメインターゲットとしていると思われ、そもそも規模感のある開発を(敢えて?)想定しないためか、デプロイの管理機能を持っていません(サードパーティー製品で可能です)。

また、管理単位がアプリ(データベースの1テーブル)であり、複数アプリが連携するようなものを纏めて管理することができません(纏めて他の環境に新規作成するための「テンプレート」出力機能はありますが、アプリの更新には使えません)。

公式に配布されているCLIツール(customize-uploaderkintone-dts-gen)も単機能かつ単一アプリしか考慮しておらず、組織全体またはシステム全体の開発支援・デプロイツールとしては力不足と言わざるを得ません。

結果として、アプリのメタ情報は履歴管理しない(あるいはサードパーティー製品でブラックボックスとして管理)、コードのみはリポジトリ管理、デプロイは最悪手動で行うことになります。
システム内の小さなアプリではTypeScriptのボイラープレート準備の煩雑さから、そのままバンドラー無し・Babel無しのJavaScriptでコードを書いて、Webコンソールからアップロードすることもありました。(ボイラープレートくらい事前に準備しろよという声も聞こえてきそうですが・・・現実は思うようにならないのです)

Salesforce DXのように、リポジトリ中心の開発、デプロイ操作が整合性のある形で実現できれば良いのですが、残念ながら現状の公式ツールでは実現できません。

そこで、kdxという新しいCLIツールを作成してみました。

CLI本体
https://github.com/shellyln/kdx

プロジェクトテンプレート
https://github.com/shellyln/kdx-project-template

KDXとは

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.ts
import { 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.ts
import '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.ts
import { 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のイベントオブジェクトのデータには型情報が付いていると上述しましたが、サブテーブルの新規行の処理等を考えると、スキーマからのランタイム型情報が無ければ実現できません。(逆にイベントオブジェクトに型情報を付けてくる意味が薄いと言えます)

さいごに

まだ、不具合があるかもしれませんが、何とかリリースできました。
足りないところもあるかと思うので、少しずつ機能強化できればと思います。

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