- 投稿日:2020-09-22T22:56:50+09:00
【JavaScript】関数とPromiseと、ときどきasync await
関数の種類、早見表
名前 ソースコード 補足 名前付き関数 function() 関数名 {} 匿名関数(関数式) const 関数名 = function() {} 高階関数 function(関数) {関数} 引数に関数をとる関数のこと。また、引数の関数をコールバック関数という。 コールバック関数の課題
コールバック関数を使うと、ネストになりがち
↓
new Promise(()=>{}).thenで、ネストを解消
でも、行数が増えがち
↓
async (){ await }で、簡略化通信成功時と失敗時のコールバック関数
const 非同期関数 = function (成功時関数, 失敗時関数) { if (...) { 成功時関数(成功res) } else { 失敗時関数(失敗res) } }Promise
new Promise (function (resolve, reject) { asynchronous( function(成功res) { resolve(成功res) }, function(失敗res) { reject(失敗res) }, ) })axios
function getStatus () { return new Promise(function (resolve, reject) { axios.get("URL") .then(function(res) { resolve(res.status) }).catch(function(error) { reject(error.res.status) }) }) }async await
async function () { try { const 成功res = await 非同期関数 成功時処理 } catch(失敗res) { 失敗時処理 } }axios
async function getStatus () { try { const res = await axios.get("URL") return res } catch(error) { return error } }上記のように、分かりやすく
try
も書いたが、いらなくても良い。
- 投稿日:2020-09-22T22:48:28+09:00
json2csvで出力したCSVファイルがExcelで文字化けしてしまう
はじめに
以前まとめた、【Node.js】JSON文字列をCSVファイルとして吐き出すという記事で、
json2csv
というモジュールを使用し、CSVファイルを正常に出力できたと思っていたのですが、ExcelでCSVファイルを開くと文字化けしてしまっていたので、修正を行いました。結論
・
withBOM
オプションをtrue
にする解説
json2csv
のドキュメントにwithBOM
オプションの説明が記載されていました。Unicode Support
Excel can display Unicode correctly (just setting the withBOM option to true). However, Excel can't save unicode so, if you perform any changes to the CSV and save it from Excel, the Unicode characters will not be displayed correctly.
withBOM
オプションをtrue
にするだけで、ExcelではUnicodeを正しく表示できる、的なことが書かれていますね。
なので、インスタンス化時にwithBOM
オプションを付与してあげます。json2csvのインスタンス化const json2csvParser = new Parser({fields, transforms, withBOM: true});これで、Excelでも文字化けすることなく、正常にCSVファイルが表示できるかと思います。
BOMって何だ?
「Byte Order Mark」の正式名称で、BOMが付与されたファイルは
Unicodeで書かれている
ことを証明しています。
しかし、Unicodeで書かれたファイル全てにBOMが付与されている訳ではありません。BOMがあってもなくてもどちらでもいいそうです。BOMが存在する理由は、絶対にこのファイルがUnicodeで書かれていることを示す必要がある場合があるからで、今回のExcelでCSVファイルを開く時などが該当します。
ExcelではBOMが付与されたCSVファイルでないと、文字コードが認識されずに文字化けを引き起こしてしまうのです。参考
バイトオーダマーク(英:Byte Order Mark)とは
簡単に文字化け回避!UNICODE (UTF-8) の CSV データを表示する方法
- 投稿日:2020-09-22T21:12:08+09:00
初心者によるVue.jsハンズオン
はじめに
初めて記事を書くので暖かく見守って下さい。
仕事でVue.jsを使用するということで、JS初心者が手っ取り早くVue.jsに触れてみる。早速触ってみる
用意するものは使い慣れたブラウザとエディターがあれば十分です。
私は今回以下のものを用いて開発を行います。
-ブラウザ:Google Chrome
- エディター:Visual Studio Codeまずはベースページの作成
index.html<html> <body> Hello, World <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> </body> </html>こちらはvue.js本体を読み込むだけの空ページの作成を行っています。
今回のハンズオンでは、vue.jsのCDNを使っていきます。こちらのコードをVSCodeで書いたら保存しましょう。
そしてGoogle Chromeにドラッグ&ドロップすることで表示できます、便利。(今まで右クリックしてブラウザに表示していたので、、、、)表示されたら、まずブラウザに「Hello, World」と表示されるか確認しましょう。
そして、動作としてvue.jsがリクエストされているか確認するためにChromeの開発者ツールを使いましょう。
確認方法は開発者ツール(windowsの場合は「F12」キー)->Networkです。
以下の画像のようになっていれば、大丈夫です!
それではVue.jsを実行/展開
index.html<html> <body> <div id="app"></div> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> <script> new Vue({ el: '#app', template: '<button></button>', }); </script> </body> </html>こちらのコードでは、Vue.jsの展開ポイントとなるdivを作成し、newで展開しています。
先ほどのブラウザをリロードしてボタンが表示されたら成功です!解説
1. 展開ポイントは<p>でもでもなんでもいいですが慣例的に<div>を使います。
2. idもなんでもいいですが同じく慣例的にappと名付けます。ボタンに振る舞いを追加し。紐づける
index.html<html> <body> <div id="app"> </div> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> <script> new Vue({ el: '#app', template: '<button @click="showMessage"></button>', methods: { showMessage() { alert('ok'); }, }, }); </script> </body> </html>先ほどのコードに、ユーザーがボタンをクリックしてアラートを表示させるコードを追加します。
実際にクリックしたらメッセージを表示されたら成功です。解説
1. Vue.jsの挙動は基本的にmethodsの中に定義していきます
2. Vue.jsでは生のhtmlのonclick="..."と同じようにhtmlタグにリスナ/ハンドラを記述します状態を保持する
index.html<html> <body> <div id="app"> </div> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> <script> new Vue({ el: '#app', template: '<button @click="toggle"></button>', data: function () { return { active: false, }; }, methods: { toggle() { this.active = !this.active; }, showMessage() { alert('ok'); }, }, }); </script> </body> </html>こちらのコードでは、中でステートを保持して、クリック時にそのステートを変更するようにします。
今回は内部の状態変更のみなので確認できないです。解説
1. 状態、ステートは基本的にdata配下に定義していきます
2. elやtemplate、methodsと違い、dataの値には無名関数を利用して、状態一覧のオブジェクトを返すようにします
3. テンプレート中でdataの値を参照する場合はthisは付けない、methodsの中で参照する場合はthisを付ける、ので注意テンプレートに値を埋め込む
index.html<html> <body> <div id="app"> </div> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> <script> new Vue({ el: '#app', template: '<button @click="toggle">{{ active }}</button>', data: function () { return { active: false, }; }, methods: { toggle() { this.active = !this.active; }, showMessage() { alert('ok'); }, }, }); </script> </body> </html>こちらのコードでは、テンプレートに{{ }}で値を埋め込んでいて、クリック時の状態反転に連動してボタンのラベルが変わったら成功です。
解説
1. {{ ステートの変数名 }} でテンプレート中に変数を埋め込むことができます
2. Vueはリアクティブなため、変数の値が変わったら(テンプレートを書き直さなくても)自動的に反映されます{{ }}の中に簡単なjs処理を記述する
index.html<html> <body> <div id="app"> </div> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> <script> new Vue({ el: '#app', template: '<button @click="toggle">{{ active ? "Like済み" : "Likeする" }}</button>', data: function () { return { active: false, }; }, methods: { toggle() { this.active = !this.active; }, showMessage() { alert('ok'); }, }, }); </script> </body> </html>ここはactive変数そのものだとBooleanがStringにキャストされてtrue/falseと表示されるのでボタンのラベルっぽい表示にしてます。
解説
1.{{ }} の中ではjsコードそのものを記述できます
(ただし過度な記述をすると可読性が下がるので注意)コンポーネント化する
index.html<html> <body> <div id="app"> </div> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> <script> Vue.component('like-button', { template: '<button @click="toggle">{{ active ? "Like済み" : "Likeする" }}</button>', data: function () { return { active: false, }; }, methods: { toggle() { this.active = !this.active; }, showMessage() { alert('ok'); }, }, }); new Vue({ el: '#app', template: '<div><like-button />aaa<br />bbb<br />ccc<br /><like-button /></div>', }); </script> </body> </html>最後に、いままで書いてきた一連のテンプレートと振る舞い、状態をセットとしてコンポーネント化します。
解説
1.コンポーネントとして切り出すことで再利用できるようにします最後に
今回はここで終了です!
次回も続きを書ければいいなと思います。
最後までご覧いただきありがとうございました!
- 投稿日:2020-09-22T20:35:22+09:00
document.createElementより便利
忘れない内にメモ
今まで生のJSを使わないと行けない環境でDomをいじくり回したい時はvar id = "hoge"; var text = "Lorem ipsum"; var header = document.createElement("h1"); header.setAttribute("id",id); header.textContent = text; // <h1 id="hoge">Lorem ipsum</h1>とクッソ面倒な事をやってたけど
var id = "hoge"; var text = "Lorem ipsum"; var parser = new DOMParser(); var temp = "<h1 id=" + id + ">" + text + "</h1>"; var doc = parser.parseFromString(temp, "text/html");と簡潔に書けるらしい。
テンプレートリテラル使えるなら
var id = "hoge"; var text = "Lorem ipsum"; var parser = new DOMParser(); var temp = `<h1 id="${id}">${text}</h1>`; var doc = parser.parseFromString(temp, "text/html");めっちゃ楽になる。
- 投稿日:2020-09-22T20:20:28+09:00
適当な文字列を 0 から 1 までの実数に振り分けるロジック (PHP/TypeScript)
最近 A/B テストのように確率的な条件分岐を実現する文脈で、適当な入力文字列から区間 $ [0, 1] $ に含まれる数値を出力するようなハッシュ関数を実装する機会がありました。単に定められた範囲の数値を出力するだけでなく、出力される数値に偏りが出ないように(関数の出力値の分布が連続一様分布とみなせる 1 ように)するという要件もありました。
イメージとしては以下のコードのようになります。
// 同じ文字列を入力に取ると同じ数値が出力される strToNumberBetweenZeroAndOne('abc'); // output: 0.7283949105904 strToNumberBetweenZeroAndOne('abc'); // output: 0.7283949105904 // 別の文字列を入力に取ると別の数値が出力される strToNumberBetweenZeroAndOne('Hello'); // output: 0.095208030923864 strToNumberBetweenZeroAndOne('World!'); // output: 0.31755707966684 strToNumberBetweenZeroAndOne('0123456789'); // output: 0.51892998626938 // 色々な文字列に対して出力値をプロットしていくと、数値の確率分布が区間 [0, 1] の一様分布に近づく。このような関数を PHP と TypeScript で実装したので紹介します。
実装
PHP
PHP の場合は以下のように実装できます。
function strToNumberBetweenZeroAndOne(string $message): float { // 1 $hashHex = hash('sha256', $message); // 2 $maxHex = str_repeat("f", 64); return hexdec($hashHex) / hexdec($maxHex); }1 の部分では
hash('sha256', $string)
で入力文字列を SHA256 2 で64桁の固定長16進数に変換し、hexdec()
でその10進数表現を得ています。文字列から16進数への変換は bin2hex で行うこともできますが、文字列のサイズが大きすぎると桁あふれを起こしてしまうので使用を避けています。2 の部分では 1 で得られた10進数を64桁の16進数の最大値
str_repeat("f", 64)
の10進数表現で割ることで数値を算出しています。TypeScript
import CryptoJS from 'crypto-js' const strToNumberBetweenZeroAndOne = (message: string): number => { const hashHex = CryptoJS.SHA256(message).toString() const maxHex = 'f'.repeat(64) return parseInt(hashHex, 16) / parseInt(maxHex, 16) }内容は PHP と特に変わりませんが、 SHA256 のハッシュ化を行うために crypto-js というライブラリを利用しています。
検証
実際にランダムな文字列を入力にメソッドの出力値 3 を集計してヒストグラムを作成してみると、おおよそ一様分布になっていることが分かります。
- 投稿日:2020-09-22T19:03:03+09:00
なぜ我々は頑なにforを避けるのか
動機
前回の記事を投稿したことを某SNSで通知したところ、そのSNSでこんなコメントをいただいた。転記する許可を取ったわけでは無いので私なりに要約させていただくと、
なぜそんなトリッキーな書き方をしてまでforを使うのを避けるのか
そんな書き方をして可読性を下げるくらいなら素直にforを使う方が良いということだと理解している。
なるほど、一理ありそうだ。しかし一方で、前回貼ったStackOverflowのQ&Aはなかなかの人気記事(質問に1243ポイント、回答に最大で1559ポイント)なので「多少トリッキーなことをしてでもforを書きたくない!!」という意見をもつプログラマも一定以上いるのだろう。当然私もその1人だ。
ということで、この記事で「なぜそこまで意固地になってまでforを書きたくないのか」を説明することにする。
尚、今回は前回の記事つながりで言語はJavaScriptを使うが、基本的にはどの言語でも同じだと思って欲しい。
理由1 そもそもforの構文って結構複雑
こんな配列があるとする。
const suites = ["H","C","D","S"]; //H:ハート C:クローバー D:ダイア S:スペード const nums = ["A","2","3","4","5","6","7","8","9","T","J","Q","K"]; //トランプの数字この配列を元にジョーカーを除いたトランプ52枚すべてを標準出力(
console.log
)したい。
例えば、ハートの2なら"H:2"、スペードのキングなら"S:K"と出力する。
for
を使って素直に書くならこんな感じだろうか。for(let i = 0; i < suites.length; i++){ for(let j = 0; j < nums.length; j++){ console.log(suites[i] + ":" + nums[j]); } }初学者だった時やしばらくプログラミングから離れて久々に書いた時のことを思い出してほしい。
あれ、
i < suites.length
でいいんだっけ?i < suites.length - 1
だっけ?
それとも、i <= suites.length
だっけ??こんなこと悩んだ経験はないだろうか?
初学者だけではなく、ベテランであっても仕事が立て込んでいて疲れていれば
i <= suites.length
と誤って書いてしまうかも知れない。
貴方はソースレビューでこのバグを発見できる自信があるだろうか?テストフェーズで発見できれば良いが、うまく境界値がテストパターンからすり抜けてしまったら...?一方で、
for
を避けてArray#forEach
で実装した例を見てみよう。suites.forEach(suite => nums.forEach(num => console.log(suite + ":" + num)));どうだろう?少なくともforEachの文法を覚えていれば、
<
か<=
かといった問題やlength - 1
かどうかといった問題に悩まされることも疲れていて間違えてしまうことも無い。ソースレビューで誰がどう読んだってsuites
の要素数とnums
の要素数分ループするのは明らかである。前者の例と後者の例、どちらが安全だろうか?
for
とは本質的にバグりやすい複雑な構文なのである。理由2 配列に添字でアクセスしたくない
同じサンプルを再掲する。
const suites = ["H","C","D","S"]; //H:ハート C:クローバー D:ダイア S:スペード const nums = ["A","2","3","4","5","6","7","8","9","T","J","Q","K"]; //トランプの数字 for(let i = 0; i < suites.length; i++){ for(let j = 0; j < nums.length; j++){ console.log(suites[i] + ":" + nums[j]); } }今度は、
console.log(suites[i] + ":" + nums[j]);に注目して欲しい。
今回はたまたま
suites
の添字をi
に、nums
の添字をj
にしてループしたのだが、両者を取り違えてsuites[j]
やnums[i]
にアクセスしてしまう可能性はどれくらいあるだろか?「いやいや、馬鹿にするな!それくらい覚えてるよ!!」
と言えるのは、今回のサンプルがたまたま短くて単純だからだ。
実際の現場で書くソースコードはもっと長くてもっと複雑だ。もちろん、常に短くてシンプルなコードを書くという心意気は大事だが、現実にはそうはならないことだって多々あるのも事実だ。
その前提でもう一度尋ねる。貴方は添字を取り違えたりしないだろうか?
添字を取違いが起きていることをソースレビューで指摘できるだろうか?
Array#forEach
で書く例も再掲する。suites.forEach(suite => nums.forEach(num => console.log(suite + ":" + num)));こちらではそもそも各要素に添字でアクセスする代わりに
suite
やnum
のような関数への引数を通じてアクセスしている。添字の取り違えなんて起きようがない。理由3 副作用がないことを保証したい
極端な例で恐縮だが、
const suites = ["H","C","D","S"]; //H:ハート C:クローバー D:ダイア S:スペード const nums = ["A","2","3","4","5","6","7","8","9","T","J","Q","K"]; //トランプの数字 for(let i = 0; i < suites.length; i++){ for(let j = 0; j < nums.length; j++){ console.log(suites[i] + ":" + nums[j]); i = 0; } }こんなコードを書いたら
i = 0;
のおかげでこのループは無限ループになってしまう。「当たり前だし、こんなことをわざとやる奴はいない」って?
ではこんなパターンはどうだろう。
const suites = ["H","C","D","S"]; //H:ハート C:クローバー D:ダイア S:スペード const nums = ["A","2","3","4","5","6","7","8","9","T","J","Q","K"]; //トランプの数字 for(let i = 0; i < suites.length; i++){ for(let j = 0; j < nums.length; j++){ console.log(suites[i] + ":" + nums[j]); i++; j++; } }これなら、徹夜明けか何かで疲れていて、「自分は
while
でループを書いている」と錯覚していれば起こり得そうだ。
わざとやる奴はいない?そりゃそうだ。わざとじゃないからバグなんだ。やる奴がいるかどうかが問題なのではなくて、起こり得てしまうことが問題な訳で。
繰り返しになるが、このサンプルのような短いシンプルなコードで
for
を使っているのが問題なのではない。実戦で書くもっと長くて複雑なコードで問題が表面化する。本当は書きたくも無いし読みたくも無いのだが、実戦ではこんなコードはざらに存在する。
for(let i = 0; i < arr1.length; i++){ /** * なんか100行くらいの処理 */ //途中で多重ループに入る。。。 for(let j = 0; j < arr2.length; j++){ //このループの中もとても長い。。 } /** * さらに100行くらい。。 */ }このループが安全であることを確かめるために200行以上のソースを読まなければいけないのが大変な訳だ。
せめてforEach
を使ってくれていれば少なくともループは安全に回っていそうというのが一目で分かる。この心理的効果が意外と大きい。
forEach
を使うとどうなるか。suites.forEach(suite => nums.forEach(num => console.log(suite + ":" + num)));そもそも
i
やj
がいないのでi++;
などをやりようが無いというのもあるし、仮に何らかの事情で添字を使う場合でも、suites.forEach((suite, i) => nums.forEach((num, j) => { console.log(suite + ":" + num); i++;//関数の引数であるiがインクリメントされるだけなのでループは普通に回る }));このように仮に間違えがあってもループそのものは安全である。添字でのアクセスを避けているのでOutOfIndexにならないのも大きい。
理由4 結果の形や処理・変換の意図をより明確化したい
少し要求を変えて、
trump
という変数(もしくは定数)に['H:A','H:2',...,'S:K']
という配列を格納したい場合。
for
で書くとこう。let trump = []; for(let i = 0; i < suites.length; i++){ for(let j = 0; j < nums.length; j++){ trump.push(suites[i] + ":" + nums[j]); } }
for
を避けて、今回はforEach
ではなくmap
で書くとこうなる。const trump = suites.map(suite => nums.map(num => suite + ":" + num)).flat();例によって、この程度の長さのシンプルなロジックであれば大した差は無い。問題は、処理が複雑化した時だ。
let result = []; for(let i = 0; i < arr1.length; i++){ for(let j = 0; j < arr2.length; j++){ /** * なんか色々処理。分岐もあったりとか。 */ result.push[somevalue]; } }この処理を見て、最終的なresultの形を予想できるだろうか?
arr1.length * arr2.length
の配列になる?
いやいや、途中の処理でbreak;
してたら?分岐の中にresult#push
しないパスがあったら?
もっと言えば、プログラマがその複雑な処理に気を取られて、うっかりresult.push[somevalue];
を書くのを忘れていたら?
もっと極端なこと言えば、途中でresult = undefined;
なんて代入がある可能性すらある。ここにあげた例のようなことが「無い」って証明するためには、forの中身をいちいち全部読まなければいけない。
これはしんどい。というかそもそも、この形を見た時に、
このプログラマはresultにarr1とarr2から生成した新たな配列を格納しようとしている
って読み取れるだろうか?読み取れるのはおそらくソースを全部読んだあとだろう。
一方で、
const result = arr1.map(valOfArr1 => arr2.map(valOfArr2 => { /** * なんか色々処理。分岐もあったりとか。 */ return somevalue; })).flat();この場合は、resultの中身が長さ
arr1.length * arr2.length
の配列であることが保証される。確かに、jsにおいては
return
を忘れて、全て値がundefined
になっている可能性はある。
しかし、TypeScriptであれば型チェックに引っかかるのでreturn忘れを事前に気づくことができる。
上のfor
の例でresult.push(someValue);
を忘れた場合は型チェックでは引っかからない。それよりも何よりも、こちらの方がプログラマが「arr1とarr2を組み合わせて新しい配列を作ろうとしている」という意図が明確になる。
これがかなり重要で、意図が明確かつ結果が保証されているかどうかで、
/** * なんか色々処理。分岐もあったりとか。 */
を読むストレスが全然違う。これは大きい。
(ちょっと閑話休題)可読性って何だろうね
こういう議論をしているとたまに「可読性」の定義が合わない人がいる。
例えば、
初心者でも(誰でも)読みやすく書くのが可読性の向上だ
という人がいるが、それは違うと思う。
その理屈が通るのであれば、日本語から漢字を撤廃して全てひらがなに統一すべきだ。その方が初心者である幼児や勉強初めたての外国人の方にとってわかりやすい。そうならないのは何故だろうか。
「読める人の多さ」と「表記としての効率性」はトレードオフだ。
幼稚園児に読ませる文章を書くときは全てひらがなで書くべきだが、それを大体の日本人の大人相手にやっていたら効率が悪い。もっと言えば、例えばある程度オブジェクト指向を勉強した相手に、
ここもうちょっとカプセル化しない?
って提案するのはシンプルでわかりやすいが、プログラミング覚えたての初心者には伝わらない。
このsetterにnull突っ込んだらどうなると思う?
インスタンス化したあと、setHoge()を呼ばずにexecuteFoo()を読んだらエラーになるよね?ちょっと工夫してみようよ。というように噛み砕いて伝えなければいけない。「効率」を考えた時どちらの方が良いだろうか。
可読性の向上というのは、
想定読者のレベルをある一定だと想定して、問題なく読めてかつ効率的な表記を目指す
ことだと思う。
例えば、初心者に教えている際に
[...Array(100).keys()].map(n => n + 1).map(n => n % 15 === 0 ? 'FizzBuzz' : n % 3 === 0 ? 'Fizz' : n % 5 === 0 ? 'Buzz' : n.toString()).forEach(s => console.log(s));こんなワンライナーを書けるようになりましょうね
って言ったら流石にバカだと思う。ちゃんと
for
で回すループを教えるべきだ。
(というか上級者相手でもここまでのワンライナーはほぼ"遊び"だろうが。。)でも、ある程度ES6を勉強している向けに、
const suites = ["H","C","D","S"]; //H:ハート C:クローバー D:ダイア S:スペード const nums = ["A","2","3","4","5","6","7","8","9","T","J","Q","K"]; //トランプの数字 const trump = suites.map(suite => nums.map(num => suite + ":" + num)).flat();このコードを書くのはむしろ読みやすいし、何より(今まで説明した通り)安全な記述だ。
前回の記事でいう、
const N = 100; [...Array(N).keys()].forEach(i => { //...なんか処理(iには0...N-1(=99)が格納される) });というハックはそのあたりのバランスが確かに微妙だとは思う。
人によっては、「可読性を損なうデメリットの方が大きい」という人もいるだろう。
もうこれ以上は宗教戦争の域に入りそうだが、それでも私は僅差でfor
を避けるメリットの方を推したい。それでもforを勉強すべき理由
さて、散々
for
の悪口を言っていたのだが、だからと言ってfor
(やwhile
)を勉強しなくても良い理由にはならない。きちんと勉強すべきだ。理由は以下の通りだ。
1.様々な言語に対応できる
近頃は大体の言語で配列のmap/filter/reduceなり拡張forなりをサポートしている。あのJavaですら(失礼?)近年ではStreamAPIを導入して関数型っぽい記述をできるようになっている。
ただ、例えば私が知る限りでは例えばC言語には拡張forやmap/filter/reduceなどの組み込み関数は無いように思う。
基本的な
for
やwhile
はそれぞれの言語で方言はあれどほぼ全ての言語で使える文法のはずなので、やはり勉強しておくべきである。2.どうしても使わなければならない/使った方がわかりやすいロジックやアルゴリズムに直面することがある
例えば、ソートやシャッフルなど。
下記はこちらから引用させていただいたフィッシャー・イエーツシャッフルの実装だが、
const shuffle = ([...array]) => { for (let i = array.length - 1; i >= 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [array[i], array[j]] = [array[j], array[i]]; } return array; }このように添字を使ってアクセスした方がはるかにわかりやすいパターンがある。
こういったアルゴリズムをわざわざ
for
を避けて書くのはあまり賢い選択では無いだろう。3.漏れのある抽象化の法則
本記事の冒頭で書いたSNSで議論させていただいた相手に教えて頂いた(不勉強ながら初めて知りました。。ありがとうございます!)
かの有名なJoel on Softwareの一節で、この記事の文脈に沿って要約すると、Array#forEachやArray#mapを使っていて何か問題が生じた時、forやwhileの書き方を知らなかったら問題解決できないよね?
ということらしい。
それは本当にその通りだと思う。わかりやすい例で言えば、
Array#forEach
ではbreak;
によるループからの離脱ができない。
どうしてもbreak;
が必要な場合はfor
やwhile
に書き換える必要があるが、裏を返せばそれができないレベルのプログラマにはArray#forEach
を使いこなすのが難しいということだ。まとめ:forを"ダサい"と表現したのは軽率だったと反省している
前回の記事で、典型的な
for
文を書いてこんなダサいループを回したく無い
と書いた。
正直、記事冒頭で読み手を惹きつけるためのネタのつもりで書いたのだが、軽率であったと反省している。
そんなことを書いたせいかどうかはわからないが、前回紹介したようなハックに対して
わざとトリッキーに書いて頭いいぶってカッコつけている
という印象を持たれてしまったようだ。それははっきり言って心外だ。
前述した通り、確かに
const N = 100; [...Array(N).keys()].forEach(i => { //...なんか処理(iには0...N-1(=99)が格納される) });は可読性的には微妙なラインだ。
でも、それを天秤にかけてもfor
を避けたい理由がキチンとある。この記事に書いた通り、実利的な理由だ。前回も引用したStackOverflowの質問者さんの質問文に、
To me it feels like there should be a way of doing this without the loop.
意訳: ループをしないでやる何かうまい方法があるような気がするんだと書いてある。
私もそれを思っていた。でも私は、こういった局面で何も工夫をせずに妥協してfor
を使っていた。それを妥協しないでちゃんと質問した質問者さんも、それに見事なハックで答えた回答者さんも本当にかっこいいと思う。
"カッコつけている"わけではなくて。
- 投稿日:2020-09-22T17:44:33+09:00
サクラエディタで指定した文字数を出力するマクロを作ってみた。
はじめに
入力値のパターンの試験で、色々のパターンの文字列を準備する際に、動的に出力できたら便利だなと思いさくらエディタのマクロを作成してみました。
全角半角混在のパターンなど、まだまだ改良の余地はあるのでブラッシュアップしていきたいと思います。実行結果
全角文字(5文字)出力の場合
全角文字(15文字)出力の場合
半角文字(25文字)出力の場合
全角記号(128文字)出力の場合
半角記号(256文字)出力の場合
ソース
OutpurStr.js// ==================== // =main処理 // ==================== // 出力用全角文字 var arrayForZenStrOutput = ['か','き','く','け','こ']; // 出力用全角文字(5文字区切り) var arrayForZenStrOutputLen5 = ['あああああ','いいいいい','ううううう','えええええ','おおおおお']; // 出力用半角文字 var arrayForHanStrOutput = ['f','g','h','i','j']; // 出力用半角文字(5文字区切り) var arrayForHanStrOutputLen5 = ['aaaaa','bbbbb','ccccc','ddddd','eeeee']; // 出力用半角記号 var arrayForZenSignOutput = ['」','*','@','<','>']; // 出力用半角記号(5文字区切り) var arrayForZenSignOutputLen5 = ['!!!!!','+++++','?????','「「「「「','ーーーーー']; // 出力用全角記号 var arrayForHanSignOutput = [':','*','@','<','>']; // 出力用全角記号(5文字区切り) var arrayForHanSignOutputLen5 = ['?????','+++++','[[[[[','-----',']]]]]']; // Jscriptはvarのみの定義となるが定数っぽく // 全角文字出力パターン var ZEN_STR_OUTPUT_PTN = '0'; // 半角文字出力パターン var HAN_STR_OUTPUT_PTN = '1'; // 全角記号出力パターン var ZEN_SING_OUTPUT_PTN = '2'; // 半角記号出力パターン var HAN_SING_OUTPUT_PTN = '3'; // 指定行に記載した数値を取得 var num = parseInt(Editor.GetLineStr(0)); // 改行 引数は文字コードだが、13は現在の入力改行コード指定により変換されるらしい Char(13); // 出力パターン選択 var outputPtn = InputBox('全角文字:0 | 半角文字:1 | 全角記号:2 | 半角記号:3 ','',1); // 出力パターン判定 switch(outputPtn) { // 全角文字出力パターン case ZEN_STR_OUTPUT_PTN: outputStr(num, arrayForZenStrOutput, arrayForZenStrOutputLen5); break; // 半角文字出力パターン case HAN_STR_OUTPUT_PTN: outputStr(num, arrayForHanStrOutput, arrayForHanStrOutputLen5); break; // 全角記号出力パターン case ZEN_SING_OUTPUT_PTN: outputStr(num, arrayForZenSignOutput, arrayForZenSignOutputLen5); break; // 半角記号出力パターン case HAN_SING_OUTPUT_PTN: outputStr(num, arrayForHanSignOutput, arrayForHanSignOutputLen5); break; } // ==================== // =出力関数 // ==================== function outputStr(num, arrayForStrOutput, arrayForStrOutputLen5) { if(num <= arrayForStrOutput.length){ // 1文字ー4文字以内の場合 for(var i = 0; i < num; i++){ Editor.InsText(arrayForStrOutput[i]); } } else { // 5文字以上の場合 // 同一文字列(5文字区切り) + 端数分の文字列を出力 // 商 var quotient = Math.floor(num/arrayForStrOutput.length); // 余り var remainder = num % arrayForStrOutput.length; if (quotient < arrayForStrOutput.length) { // 5文字ー25文字以内の場合 for(var i = 0; i < quotient; i++) { Editor.InsText(arrayForStrOutputLen5[i]); } } else { // 26文字以上の場合 // 商 var quotient2 = Math.floor(quotient/arrayForStrOutput.length); // 余り var remainder2 = quotient % arrayForStrOutput.length; for (var j = 0; j < quotient2; j++) { for(var i = 0; i < arrayForStrOutput.length; i++) { Editor.InsText(arrayForStrOutputLen5[i]); } } for(var i = 0; i < remainder2; i++){ Editor.InsText(arrayForStrOutputLen5[i]); } } // 端数分の文字列を出力 for(var i = 0; i < remainder; i++){ Editor.InsText(arrayForStrOutput[i]); } } }
- 投稿日:2020-09-22T17:16:33+09:00
javascript addEventListenerで引数渡したいしremoveもしたい!
環境
初書:2020/09/22
PC:macOS 10.15.6前置き
javascriptで、addEventListenerを使いたい時に、引数を渡したいと思ったが、同時にremoveEventListenerで削除もしたい時(タイトルの通り)にどうするのか迷ったのでメモ。
前提
IEは知らない
addEventListenerで引数を渡す
これは調べたら比較的すぐに出てくるので、簡単に実装できる(と思う)
参考サイト:【JavaScript】addEventListenerで関数に引数をわたす|北の南|noteindex.html<div id="youso">div要素</div> <script> const id = document.querySelector("#youso"); id.addEventListener('click', {arg: "aa", handleEvent: fuc}); handleEvent: fuc}); function fuc(){ console.log(this.arg); //"aa" } </script>・・・引数という表現でいいのか知らないがとりあえず渡すことはできた。
addEventListenerの第二引数に、呼び出す関数を入れたhandleEvent
と、その他引数名と値を入れたオブジェクトを渡すことによって、呼び出した関数に値を渡すことが可能。
注意点は、関数内のthis
が、addEventListenerの第二引数に全て置き換わる。removeEventListenerでイベントを消す
普通にやる場合は特に難しいことはなく、
removeEventListener
を呼ぶだけでできる。
ただ、上記の方法で追加した場合は、index.html<div id="youso">div要素</div> <script> const id = document.querySelector("#youso"); id.addEventListener('click', {arg: "aa", handleEvent: fuc}); function fuc(){ console.log(this.arg); // "aa" id.removeEventListener('click', {arg: "aa", handleEvent: fuc}); } </script>この方法では削除ができない。
理由は単純で、第二引数のオブジェクトが、{arg: "aa", handleEvent: fuc}==={arg: "aa", handleEvent: fuc}
=falseになるからである。
ということは、第二引数のオブジェクトがtrueになるようにすればいい。
無名関数ではないのだが、似たような方法がこちらに記載されていたので、参考にしながら付け足してみる。(そして関数化する)index.html<div id="youso">div要素</div> <script> let counter = 0; const eventlist = []; const num = addlistener("#youso", "click", fuc, {arg :"aa"}); function fuc() { console.log(this.arg); // "aa" removelistener(num); } function addlistener(selectors, type, handleEvent, arg = {}, option = null) { counter++; const eventlistner = {handleEvent, ...arg}; const array = {counter, selectors, type, eventlistner, option}; eventlist.push(array); const docs = document.querySelectorAll(selectors); for (const doc of docs) { doc.addEventListener(type, eventlistner, option); } return counter; } function removelistener(num) { const find_in = (e) => e.counter === num; const value = eventlist.findIndex(find_in); const {selectors, type, eventlistner, option} = eventlist[value]; const docs = document.querySelectorAll(selectors); for (const doc of docs) { doc.removeEventListener(type, eventlistner, option); } } </script>このように、addEventListenerの第二引数を保存しておき、removeEventListenerに同じオブジェクトを渡すことで、削除できる。
関数の使い方について
addlistener関数でイベントを追加し、addlistenerの戻り値で得た数値をremovelistenerに渡すことで削除できる。
addlistener関数の引数は、
selectors = querySelectorAllの引数
,
type = addEventListenerの第一引数
,
handleEvent = addEventListenerの第二引数
,
arg = 渡したい引数
,
option = addEventListenerの第三引数
引数は引数として渡したい
一応上記コードで表題はクリアしているのだが、一つ気になるとすれば、引数が引数として受け取れないこと。
現状引数を受け取るのは、this.xxxという形を取らないといけないので、これをクリアしたい。ということで、addEventListenerと呼び出す関数の間にワンクッション挟むことで、これを実現していこうと思う。
index.html<div id="youso">div要素</div> <script> let counter = 0; const eventlist = []; const num = addlistener("#youso", "click", fuc, ["aa"]); function fuc(e,arg) { console.log(this); //windowオブジェクト console.log(e); // mouseeventオブジェクト console.log(arg); // "aa" removelistener(num); } function receivelistener(e) { let arg = this.arg_eve.arg; let func = this.arg_eve.handleEvent; if (!Array.isArray(arg)) { arg = []; } return func.call(window,e, ...arg); }; function addlistener(selectors, type, handleEvent, arg = [], option = null) { counter++; let eventlistner = { arg_eve: { arg, handleEvent }, handleEvent: receivelistener, }; const array = {counter, selectors, type, eventlistner, option}; eventlist.push(array); var docs = document.querySelectorAll(selectors); for (const doc of docs) { doc.addEventListener(type, eventlistner, option); } return counter; } function removelistener(num) { const find_in = (e) => e.counter === num; const value = eventlist.findIndex(find_in); const {selectors, type, eventlistner, option} = eventlist[value]; var docs = document.querySelectorAll(selectors); for (const doc of docs) { doc.removeEventListener(type, eventlistner, option); } } </script>ワンクッションとしてreceivelistener関数を用意。
thisとして受け取らないといけなかった引数を、この中で変換し引数として渡す。
また、addEventListener関数の第二引数に関数を直接渡す方法だと、受け取った関数のthis
はwindow
オブジェクトを返すので、.call
を使うことでthisにwindow
オブジェクトを渡している。(あと第一引数は勿論mouseevent)使い方としては、
const num = addlistener("#youso", "click", fuc, ["aa"]);
のように、第一引数にイベントを追加する要素のCSSセレクター、第二引数にイベントの種類、第三引数に呼び出す関数、第四引数に渡したい引数の配列、第五引数にaddEventListenerのオプションを渡す。
イベントを消したい時は、addlistenerの戻り値をremovelistenerに渡すだけ。
また、呼び出された側の関数は、第一引数にmouseevent、第二引数以降に渡された引数がそのまま入る。終わりに
唯一の問題は、removeをする際に一つずつは消せないこと
あと型チェックは入れていないので、その辺は注意かもしれない参考サイト
【JavaScript】addEventListenerで関数に引数をわたす|北の南|note
【JavaScript】addEventListenerの無名関数をremoveEventListenerで消す方法 | Web活
- 投稿日:2020-09-22T16:58:07+09:00
React触ったことない人間が、Next.jsでアプリ作成し高速でVercelにデプロイするまで
■はじめに
・目的はNext.jsの大枠に触れてみるというところです。
※学習コストの低いVueを使ってポートフォリオを作成したもののNext.jsが気になりすぎて
※そのため、アプリ作成と書いてはいますが、アプリの具体的な中身はほぼ空っぽです。
※今後、簡単なプロフィールサイトを作成していく予定です■デプロイ先
・Vercel(https://profsite.vercel.app/)
■手順
1.環境構築
・next.jsのプロジェクト作成
npx create-next-app profsite・ターミナルでプロジェクトディレクトリに移動
cd profsite2.node.jsのインストール(ver10.13 以上必要)
・バージョン確認
node -v※未インストールの場合(こちらの記事がとてもわかりやすいです。)
https://qiita.com/kyosuke5_20/items/c5f68fc9d89b84c0df093.ローカルサーバー起動
・コンパイル
npm run dev・http://localhost:3000へアクセスするとトップ画面が表示される
4.Editorでトップ画面を少し編集してみる
・/pages/index.jsを編集すると、トップ画面が変更される(pages/index.js = ルート)
5.新しくディレクトリを切ってみる
・pages/skills/my-skills.jsを作成(自身のスキルセットを表示するページを想定)
export default function showSkills() { return ( <h1>My Skills</h1> )}・http://localhost:3000/skills/my-skillsへアクセスすると表示される
つまり、作成したディレクトリ構造がそのままURLになる6.クライアントサイドナビゲーション
・ページ遷移をクライアント側のjavascript処理で実行し、サーバには通信を行わないのでサクサク
・リンクコンポーネントを使用する必要があるので、index.jsに読み込む
【index.js】import Link from 'next/link' export default function Home() { return ( <Link href="/skills/my-skills"><a>My Skills</a></Link> )}※余談
Adjacent JSX elements must be wrapped in an enclosing tag
・エラーがでてしまった
コンポーネントを以下のように、div等の要素で囲んで上げる必要がある
(Reactで作成するコンポーネントは、 トップレベル(一番親となる階層)の要素を
一つにする必要があるため)【index.js】
import Link from 'next/link' export default function Home() { return ( <div> <Link href="/skills/my-profile"><a>My Profile</a></Link> <Link href="/skills/my-works"><a>My Works</a></Link> <Link href="/skills/my-skills"><a>My Skills</a></Link> <Link href="/skills/contact"><a>Contact</a></Link> </div> )}リンクをクリックするとクライアントサイドナビゲーション(ページ遷移をクライアント側のjavascript処理で実行し、サーバには通信を行わない)が動作するので、サクサクです。
7.画像ファイルの読み込み
・publicフォルダでurl(src)を管理している
・publicをルートとしたルートパスで記載する
【my-profile.js】export default function showProfiles() { return ( <div> <h1>My Profile</h1> <div> <img src="/myface.JPG" alt="私の画像" /> </div> </div> )}8.メタデータ編集
・headコンポーネントを使用することで、headタグをページごとに設定できる
・下記は一例
【index.js】import Link from 'next/link' import Head from 'next/head' export default function Home() { return ( <div> <Head> <title>home</title> </Head> <Link href="/profile/my-profile"><a>My Profile</a></Link> <Link href="/works/my-works"><a>/My Works</a></Link> <Link href="/skills/my-skills"><a>/My Skills</a></Link> <Link href="/contact/contact"><a>/Contact</a></Link> </div> )}【my-profile.js】
import Head from 'next/head' export default function showProfiles() { return ( <div> <Head> <title>my profile</title> </Head> <h1>My Profile</h1> <div> <img src="/myface.JPG" alt="私の画像" /> </div> </div> )}9.cssでレイアウトを変更してみる
・小さなcss modulesを作成後、layoutコンポーネントを対象のjsコンポーネントにラップすることで、
cssを適用することが可能
・cssモジュールは基本、機能ごとに分ける。(文字のサイズ、フォントを整えるためのモジュール、画像のサイズを整えるためのモジュール等)
・ルートにcomponentsディレクトリを作成しlayout.jsを配置
/components/layout.js【layout.js】
※childrenはreactの特別なpropsの書き方(Layoutコンポーネントで囲まれた要素を返す)export default function Layout({ children }) { return ( <div>{children }</div> )}・コンポーネント(どれでも良い。例としてmy-profile.js)をLayoutコンポーネントでラップする
【my-profile.js】
import Head from 'next/head' import Layout from '../../components/layout' export default function showProfiles() { return ( <div> <Layout> <Head> <title>my profile</title> </Head> <h1>My Profile</h1> <div> <img src="/myface.JPG" alt="私の画像" /> </div> </Layout> </div> )}・上記だけだとdivで囲んだだけなので、css moduleを使ってレイアウトを変更する
・/componentsディレクトリにlayout.module.cssを作成(※拡張子は必ず.module.css)
【layout.module.css】.container { max-width: 36rem; padding: 0 1rem; margin: 3rem auto 6rem; }・layout.module.cssを読み込んで、layout.jsのclassNameに適用する
【layout.js】import styles from './layout.module.css' export default function Layout({ children }) { return ( <div className={ styles.container }> { children } </div> )}【ブラウザ】
・Layoutコンポーネントにラップされている要素にcssが適用される
※class名がユニークな値になる仕様
10.グローバルcssを適用する
・app.js(pages/app.js)にimportしたcssは全てのコンポーネントに適用される
・大元のglobals.cssはapp.jsでのみimportすることができる
・つまり、globals.cssの内容をapp.jsにimportすれば全コンポーネントにcssが適用されるということ【_app.js】
import '../styles/globals.css' function MyApp({ Component, pageProps }) { return <Component {...pageProps} /> } export default MyApp【styles/globals.css】
・わかりやすいように、テキストカラーを赤に。html, body { color:red; padding: 0; margin: 0; font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; } a { color: inherit; text-decoration: none; } * { box-sizing: border-box; }11.gitにpush
いつものやつなので、割愛。。
・githubでリポジトリ作成
・gitにpushcd profsite git init git remote add origin https://github.com/masayan1126/profsite.git git remote -v git add . git commit -m "" git push -u origin master12.Vercel(バーサル)へデプロイ
・初期設定でデプロイすると、static generation(コンパイル時にHTML生成)が適用される
・カスタムドメイン(取得した有料ドメインを使用することも可能)
・自動でSSL化されている(すご)
・アカウント作成
ここからは画面の指示通りで簡単な手順なので詳細は割愛します。
githubのアカウントサインインし、あとはプロジェクト名とか、取り込み対象の
リポジトリを選択してデプロイして完了です。
【リポジトリのインポート】
【デプロイ完了】
■最後に
・Next.jsを作られた方、天才すぎます!!
・所要時間は2時間くらいで、この記事を書く時間よりも短かったです。(笑)
- 投稿日:2020-09-22T16:08:09+09:00
Nuxtjsで静的サイト運用時、レイアウト変更したページで2回マウントされてた問題
- WebサービスのフロントでNuxtjs(v2.14)を使った。
- 複数のlayoutを使い分ける必要があったため、ファイルを作成し、pageファイル内で指定した。
- page内で、マウント時に外部サービスへデータをpost(記録)する処理を書いた。
- 公開はnuxt generateで静的なサイトを作成し、それをアップした。
- 実際に公開ページにアクセスして処理を走らせてみる
- 外部サービスのログを見ると、データが2回処理されている???←問題発覚
- PC、android端末では同様の現象は起きていなかった。
より細かく動作を見ていくと、
iPhone端末のsafariで
Nuxtjsのライフサイクルが二周している。Nuxtjsのgithubを見ると過去に似たような問題が挙がっていた。
https://github.com/nuxt/nuxt.js/issues/5703記事中であるように、
「this.$nuxt.layoutName」が一旦defaultで表示されたのち、
指定したものに切り替わっていた。試しにレイアウトの指定を解除、
または指定をdefaultにすると、問題は起きなくなった。また、記事中にある「window.〇〇」に処理の経過を記録するやり方の流用で
非同期でのデータ取得をstoreに移し、
データ取得処理の進行状態をstateで記録するようにすると、
二回目の取得処理を回避できるため、
当面の問題(二重記録)はクリアできた。
(uglyな手法って書かれてるけど)割とぶつかりそうなネタだけど、Qiitaで見当たらなかったので初投稿テストです。
- 投稿日:2020-09-22T15:15:23+09:00
JavaScriptで配列をsortする方法
この記事を書いた理由
JavaScriptを勉強したばかりの時期に、あれ?配列をソートするにはどうしたら良いんだ?
と考える機会が増えてくると思います。以下に、昇順と降順のパターンを羅列してます。今回はidを対象に並び替えを実施してますが、名前を対象にすることも可能なので、少し面倒なやり方を選びました。
まずは、配列を作るところからスタート!
main.js/*idとnameが含まれている配列*/ let arry = [ { id: 5, name: "ayaka" }, { id: 7, name: "uadagawa" }, { id: 8, name: "katou" }, { id: 3, name: "kentaro" } ];❶:配列のidを降順で表示させる場合
main.jsarry.sort(function (a, b) { if (b.id > a.id) return 1; else return -1; }); document.write(JSON.stringify(arry)); /*結果 [{"id":8,"name":"katou"}, {"id":7,"name":"uadagawa"}, {"id":5,"name":"ayaka"}, {"id":3,"name":"kentaro"}] */❷:配列のidを昇順で表示させる場合
main.jsarry.sort(function (a, b) { if (b.id < a.id) return 1; else return -1; }); document.write(JSON.stringify(arry)); /*結果 [{"id":3,"name":"kentaro"}, {"id":5,"name":"ayaka"}, {"id":7,"name":"uadagawa"}, {"id":8,"name":"katou"}] */理屈としては、returnで1や−1と返している部分は、idの並び順です。
ex) 1,3なら、1が1番目に来て、3が2番目に来るって感じ。昇順ならね。
returnの1なんだけど、個人的には+1だと思ってるw最後に、document.writeの部分は、console.logにしても良いんじゃないのか
という意見もあると思う。main.jsdocument.write(JSON.stringify(arry)); /*ここは、console.logに変更してもらっても構わない。*/codesandboxで、console.logもしくはdocument.writeを記述しても[object][object].......となってしまうのを防ぐために、上記の書き方をしています。jsonパッケージが入ってる場合、少し厄介ですね。
初学者からの視点で記述したので、誰かの勉強の手助けになれば嬉しいです!では!
- 投稿日:2020-09-22T15:07:30+09:00
React: コンポーネントのロジックをカスタムフックに切り出す ー カウンターの作例で
Reactにフックが採り入れられて、関数コンポーネントに状態をもたせられるようになりました。
「フックとは、関数コンポーネントにstateやライフサイクルといった Reactの機能を"接続する(hook into)"ための関数です」(「要するにフックとは?」)。さらに、フックを独自につくって、コンポーネントからロジックを切り出すこともできます。そうすれば、コンポーネントのコードがすっきり見やすくなるとともに、そのカスタムフックを使い回すこともできるのです。
自分独自のフックを作成することで、コンポーネントからロジックを抽出して再利用可能な関数を作ることが可能です。
(「独自フックの作成」より)本稿は簡単なカウンターの作例をとおして、フックの役割や考え方について解説します。
Create React Appでアプリケーションのひな形をつくる
まず、Reactアプリケーションのひな形は、Create React Appでつくりましょう。コマンドラインツールで
npx create-react-app
につづけて、アプリケーション名(今回はreact-custom-hook
)を打ち込んでください。npx create-react-app react-custom-hook
アプリケーション名でつくられたディレクトリに切り替えて(
cd react-custom-hook
)、コマンドyarn start
でひな形アプリケーションのページがローカルホスト(http://localhost:3000/
)で開くはずです。
useState
とプロパティ(props
)でカウンターをつくるまずは、カスタムフックは用いず、
useState
とプロパティ(props
)によりカウンターをつくりました(コード001)。アプリケーションのモジュール
src/App.js
にuseState
で状態変数(count
)を定め、関数としてはカウンターの減算(decrement()
)と加算(increment()
)が備わっています。それらをプロパティ(counter
)として受け取るのが、このあと定めるカウンター表示のコンポーネントCounterDisplay
です。コード001■
useState
とプロパティを用いたアプリケーションモジュールsrc/App.jsimport React, { useState } from 'react'; import CounterDisplay from './CounterDisplay'; import './App.css'; const initialCount = 0; function App() { const [count, setCount] = useState(initialCount); const decrement = () => setCount(count - 1); const increment = () => setCount(count + 1); return ( <div className="App"> <CounterDisplay counter={{ count, decrement, increment }} /> </div> ); } export default App;カウンター表示のモジュール
src/CounterDisplay.js
は、アプリケーションから受け取ったプロパティ(counter
)により、カウンタの値(counter.count
)表示と減算(counter.decrement
)・加算(counter.increment
)の処理を行います(コード002)。コード002■カウンター表示のモジュール
src/CounterDisplay.jsimport React from "react"; const CounterDisplay = ({ counter }) => { return ( <div> <button onClick={counter.decrement}>-</button> <span>{counter.count}</span> <button onClick={counter.increment}>+</button> </div> ); } export default CounterDisplay;これで簡単なカウンターができ上がりました(図001)。ただカウンターのカウントアップ・ダウンをするだけで、アプリケーションモジュールには、状態を使って何か行うという処理がありません。あとで加わるという想定にして、今回の作例からは省きました。また、CSS(
src/index.css
とsrc/App.css
)は、基本的なフォントや余白の設定のみです。確かめたい方は、最後に掲げるCodeSandboxの作例(サンプル001)をご覧ください。図001■でき上がったカウンター
アプリケーションのロジックをカスタムフックに切り出す
アプリケーションモジュール
src/App.js
のロジック、つまり状態変数とその処理関数を、このあと定めるカスタムフックに切り出しましょう。src/App.js// import React, { useState } from 'react'; import React from 'react'; // const initialCount = 0; function App() { /* カスタムフックに切り出す const [count, setCount] = useState(initialCount); const decrement = () => setCount(count - 1); const increment = () => setCount(count + 1); */ }つぎのコード003が、カウンターのカスタムフックのモジュール(
src/useCounter.js
)です。フックの名前はuse
ではじめるお約束になっています。状態が備えられ、フックも使えるのは、関数コンポーネントと同じです。違いとしては、JSXの要素を返さなくて構いません。戻り値は、カウンターの値(count
)と減算(decrement()
)・加算(incfrement()
)の関数を収めたオブジェクトとしました。コード003■カウンターのカスタムフック
src/useCounter.jsimport { useState } from 'react'; export const useCounter = (initialCount = 0) => { const [count, setCount] = useState(initialCount); const decrement = () => setCount(count - 1); const increment = () => setCount(count + 1); return { count, decrement, increment }; };カスタムフックを使う
アプリケーションモジュール(
src/App.js
)はカスタムフック(useCounter
)の呼び出しにより、カウンターの状態を操作するための参照(count
とdecrement
およびincrement
)が得られます。前掲コード001とプロパティ名を揃えましたので、カウンター表示のコンポーネント(CounterDisplay
)に渡すプロパティやモジュール(src/CounterDisplay.js
)のコードは書き替える必要がありません。src/App.jsimport { useCounter } from './useCounter'; function App() { const { count, decrement, increment } = useCounter(); return ( <div className="App"> <CounterDisplay counter={{ count, decrement, increment }} /> </div> ); }アプリケーションモジュール(
src/App.js
)の記述全体は、つぎのコード004のとおりです。ロジックを切り離したので、表示に専念することになりました。作例をCodeSandboxに公開します(サンプル001)。コード004■ロジックを切り離したアプリケーション
src/App.jsimport React from 'react'; import { useCounter } from './useCounter'; import CounterDisplay from './CounterDisplay'; import './App.css'; function App() { const { count, decrement, increment } = useCounter(); return ( <div className="App"> <CounterDisplay counter={{ count, decrement, increment }} /> </div> ); } export default App;サンプル001■カスタムフックを使ったカウンター
- 投稿日:2020-09-22T12:20:51+09:00
Electron(v10.1.2現在)の IPC 通信入門 - よりセキュアな方法への変遷
Electron における IPC 通信
Electron で Desktop アプリケーションを作るにあたって理解しなければならないのは、根幹を成す「IPC通信」かと思います。IPC は Inter-Process Communication、プロセス間通信の略です。
なぜ IPC 通信を使うのか? Electron のベースは、Chromium のため
Electron は、Chromium を使用しているため、Chromium のマルチプロセスアーキテクチャも使用されます。そのため Electron の各 Webページはそれぞれのプロセスで実行され、
process
というオブジェクトを持って処理を行っています。Electron アプリケーションアーキテクチャ - Electron 公式ドキュメント
https://www.electronjs.org/docs/tutorial/application-architecture#main-and-renderer-processesElectron のフレームワークの一部である、Node.js も、スレッドで処理するには、どうしても重い処理などはデッドロックを回避できません。そこで Node.js は、ネットワークアプリケーションにおいてすべての処理を「プロセス」として非同期で処理するというノンブロッキング I/O とイベントループという仕様が採用されました。
そのため、Node.js ベースの、Electron は、非同期の「プロセス間通信」の仕様がそのまま採用されています。
メインプロセスと、レンダラープロセス
メインプロセス
アプリケーションが開始されてから終了されるまでを制御します。1アプリケーションにつき、1プロセスしかなく、メインプロセスが複数のレンダラープロセスと通信を行うことでアプリケーションを制御します。また、各OSのネイティブ要素(Node.jsモジュール)の管理も担当します。
逆を言えば、レンダラープロセスから、アプリケーションの終了はできないし、OSのネイティブ要素に直接アクセスすることはできません。
メインプロセス (main process) - Electron 公式ドキュメント
https://www.electronjs.org/docs/glossary#%E3%83%A1%E3%82%A4%E3%83%B3%E3%83%97%E3%83%AD%E3%82%BB%E3%82%B9-main-processレンダラープロセス
レンダラープロセスは、メインプロセスとちがって複数作ることができます。
主に「表示」や「GUIなどのインターフェース」を担当します。レンダラープロセス間同士での通信は行えず、常にメインプロセスを通じて通信することになります。
今までやってきたレンダラープロセスからメインプロセスへの IPC 通信
まず、前提として、レンダラープロセスからメインプロセスへの通信を行うには、レンダラープロセスで Electron や、他の Node.js モジュールを動作させるための「設定」を行わないと行けません。
メインプロセスで、レンダラープロセス(表示側のウェブページ)を読み込むところで
nodeIntegration=true
としてあげる必要があります。main.jsmainWindow = new BrowserWindow({ width: 1024, height: 640, webPreferences: { nodeIntegration: true /** ココ **/ }, }); // HTMLファイルをロードする mainWindow.loadURL(`file://${__dirname}/index.html`)
nodeIntegration
は、Electron v.5 からデフォルトがfalse
となってしまったので、プログラマー側で有効true
にしてやる必要があります。なぜ、デフォルトが、false
となってしまったのかは、セキュリティの問題からですが、これについては別の IPC 通信方法を提示しつつ後述します。ipcRendererから、ipcMainへのIPC通信
非同期で通信する方法で、メインプロセスのコード例は以下の通りです。
main.jsconst { ipcMain } = require('electron') ipcMain.on('asynchronous-message', (event, arg) => { // channel名は「asynchronous-message」 console.log(arg) // "ping"を表示 event.reply('asynchronous-reply', 'pong') })index.js// レンダラープロセス(ウェブページ) const { ipcRenderer } = require('electron') ipcRenderer.on('asynchronous-reply', (event, arg) => { console.log(arg) // "pong"を表示 }) ipcRenderer.send('asynchronous-message', 'ping') // channel名は「asynchronous-message」上の例は、非同期通信の例ですが、もし同期通信を行いたい場合は、
send()
の部分をsendSync()
に書き換えれば実現できます。ただし、同期通信にした場合、メインプロセスが呼ばれている間は レンダラープロセス上の処理は完全にブロックされます。メインプロセスからの応答があるまでは、レンダラープロセス側の操作画面はいわゆるフリーズしたような状態になります。描画処理も止まるので、ローディング画面のような CSS アニメーションも容赦なく固まります。
ということから、よっぽどの理由がない限りは、非同期通信を使うことをオススメします。
ipcMain から、ipcRendererへのIPC送信
当然ですが、ipcMain から、ipcRenderer へデータ送信することもできます。
webContents.send(channel, ...args)
https://www.electronjs.org/docs/api/web-contents#contentssendchannel-argsイメージとしては、以下の図の通りです。
メインプロセス側のコードは、
main.js// メインプロセス const { app, BrowserWindow } = require('electron') let win = null app.whenReady().then(() => { win = new BrowserWindow({ width: 800, height: 600 }) win.loadURL(`file://${__dirname}/index.html`) win.webContents.on('did-finish-load', () => { win.webContents.send('ping', 'whoooooooh!') // レンダラープロセスへsendしています }) })受けるレンダラープロセス側はこう書きます。
index.html<html> <body> <script> require('electron').ipcRenderer.on('ping', (event, message) => { console.log(message) // 'whoooooooh!' と出力される }) </script> </body> </html>ipcRenderer.invoke() を使う
Electron v.7 から追加になったメソッドです。もともとは、
remote
モジュールを使うことでレンダラープロセス内であたかもメインプロセス内のオブジェクトを直接触れるように見せかけることができた仕組みを安全に置換したものです。ちなみに、remote
モジュールは、Electron v.10 では非推奨で、将来的に使えなくなる可能性は高いです。参考:[Electron] IPC には新しい ipcRenderer.invoke() メソッドを使ったほうが便利 (v7+)
https://qiita.com/jrsyo/items/abe19dff2d950132d9cdそこで、
ipcRenderer.invoke()
メソッドです。index.js// レンダラープロセス ipcRenderer.invoke('some-name', someArgument).then((result) => { // ... })main.js// メインプロセス ipcMain.handle('some-name', async (event, someArgument) => { const result = await doSomeWork(someArgument) return result })こうすることで、
ipcRenderer.on()
といった受け側を作る必要がなく、メインプロセスからPromise
のデータで受け取れるようになるので便利です。受けるメインプロセスが、ipcMain.on()
ではなく、ipcMain.handle()
になっていることに注意してください。なお、参考先の例にあるように、
index.js// renderer から Main プロセスを呼び出す const data = await ipcRenderer.invoke('invoke-test', 'ping') console.log(data)と、ワンライナーで書くこともできます。
セキュアなIPC通信 contextBridge を使う
と、ここまで、レンダラープロセスでも Electron API や Node.js モジュールを使う前提のコード例を書いてきましたが、実際には、Electron v.5 からデフォルトが、
nodeIntegration=false
となったので、将来的にはレンダラープロセス側でipcRenderer.on()
といった処理が行えなくなるかもしれません。そこで、よりセキュアな IPC 通信を行うために追加されたのが、
contextBridge
です。contextBridge - Electron公式ドキュメント
https://www.electronjs.org/docs/api/context-bridge#contextbridgeexposeinmainworldapikey-api公式ドキュメントでは、
分離されたコンテキスト間に、安全、双方向で同期されたブリッジを作成します
と、分かりにくい説明がされていますが、分かりやすく図にすると以下の通りです。
ようは、メインプロセスにある Electron API や、Node.js モジュールをレンダラープロセスで扱わせずに、メインプロセスの外部にAPIとして別途定義して、それを関数のように呼ぶという方法です。ただ、
contextBridge.exposeInMainWorld(apiKey, api)
については、まだ Experimental ですが、積極的に使っていった方が良いでしょう。これにはドキュメントにあるように、以下の考えから来ています。
メインプロセスとレンダラープロセスの違い - Electron公式ドキュメント
https://www.electronjs.org/docs/tutorial/application-architecture#%E3%83%A1%E3%82%A4%E3%83%B3%E3%83%97%E3%83%AD%E3%82%BB%E3%82%B9%E3%81%A8%E3%83%AC%E3%83%B3%E3%83%80%E3%83%A9%E3%83%BC%E3%83%97%E3%83%AD%E3%82%BB%E3%82%B9%E3%81%AE%E9%81%95%E3%81%84ウェブページでは、ネイティブ GUI 関連の API を呼び出すことは許可されていません。これは、ウェブページがネイティブ GUI リソースを管理することは非常に危険であり、リソースをリークさせるのは容易いからです。 ウェブページで GUI 操作を実行する場合、ウェブページのレンダラープロセスはメインプロセスと通信して、メインプロセスがそれらの操作を実行するよう要求する必要があります。
しかし、contextBridge を使ったからといって安全は担保できない
とはいえ、今やこれも安全な方法とは言えないようです。つまり、
contextBridge
を使えば、すべて安心というわけでもないのです。ElectronでcontextBridgeによる安全なIPC通信(受信編)
https://qiita.com/pochman/items/62de713a014dcacbad68にあるように、汎用的な書き方をするとダメなようです。
const { contextBridge, ipcRenderer} = require("electron"); contextBridge.exposeInMainWorld( "api", { send: (channel, data) => { // レンダラーからの送信用 ipcRenderer.send(channel, data); }, on: (channel, func) => { // メインプロセスからの受信用 ipcRenderer.on(channel, (event, ...args) => func(...args)); } } );Electronのセキュリティについて大きく誤認していたこと
https://qiita.com/sprout2000/items/2b65f7d02e825549804bここに書いてあるとおり、上記のコードの書き方は、Electron 公式ドキュメントによれば、
https://www.electronjs.org/docs/api/context-bridge#contextbridgeexposeinmainworldapikey-api// ❌ Bad code contextBridge.exposeInMainWorld('myAPI', { send: ipcRenderer.send })という書き方は、unsafe だそうです。
曰く、
いかなる種類の引数フィルタリングなしで強力なAPIを直接公開しています。これにより、どんなウェブサイトでも、可能にしてほしくない任意のIPCメッセージを送信することができるようになります。IPCベースのAPIを公開する正しい方法は、代わりにIPCメッセージごとに1つのメソッドを提供することでしょう
とのこと。// ✅ Good code contextBridge.exposeInMainWorld('myAPI', { loadPreferences: () => ipcRenderer.invoke('load-prefs') })つまり、1つの IPC 通信につき、1処理とした方が良いでしょう。
前の図にも描きましたが、レンダラープロセスからは
preload.js
に登録されている関数を呼び、preload.js
の中で、IPC 通信を行うというのが安全ということになります。worldSafeExecuteJavaScript: ture にしておくべきか
ちなみに Electron の v10.1.2 現在では、
BrowserWindow
のoptions
にある、webPreferences: { worldSafeExecuteJavaScript: false }
がデフォルト値になっていますが、ドキュメントを見ると、将来的に Electron が v12 になったとき、true
となり、Deprecated
となるようですので、可能ならtrue
にしておく方が良いでしょう。ドキュメントによると、
webFrame.executeJavaScript
で受け渡されるJavaScriptコードの値が安全に引き渡されるようにサニタイズされると書いてありますが、具体的にどう行われるかは書いてありません。そもそも、webFrame.executeJavaScript
自体がイレギュラーっぽい使い方なので、避けておいた方が良いかもしれません。
BrowserWindow
を作るときは、以下のようなwebPreferences
のオプション設定にすると良いでしょう。main.jslet mainWindow = new BrowserWindow({ title: config.name, width: 1024, height: 640, minWidth: 1024, minHeight: 640, webPreferences: { worldSafeExecuteJavaScript: true, // In Electron 12, the default will be changed to true. nodeIntegration: false, // XSS対策としてnodeモジュールをレンダラープロセスで使えなくする contextIsolation: true, // レンダラープロセスに公開するAPIのファイル preload: __dirname + '/preload.js' } });サンプルプログラム
これら、セキュアな IPC 通信のサンプルプログラムを作成しました。「3分間タイマー」という、いかにも単純なアプリですが、
nodeIntegration=false
かつ、contextBridge
を使って実装されています。preload.jsconst { contextBridge, ipcRenderer} = require("electron"); contextBridge.exposeInMainWorld( "api", { TimerStart: () => ipcRenderer.invoke("ipc-timer-start") .then(result => result) .catch(err => console.log(err)), TimerStop: () => ipcRenderer.send("ipc-timer-stop"), TimerReset: () => ipcRenderer.send("ipc-timer-reset"), DisplayTimer: (channel, listener) => { ipcRenderer.on("ipc-display-timer", (event, arg) => listener(arg)); } } );この上記コードでは、"
api
"とkey
を設定しています。ですので、呼び出す側(レンダラープロセス)のindex.html
では以下のように呼び出します。index.html<!DOCTYPE html> <html lang="ja"> <head><meta charset="UTF-8"> <title></title> <link rel="stylesheet" href="style.css"> <script type="text/javascript"> window.onload = () => { // 「開始」ボタンをクリック document.getElementById('button-start').addEventListener('click', async () => { const result = await window.api.TimerStart("ipc-timer-start"); if (result === true) { document.getElementById('button-reset').textContent = "停止"; } else { document.getElementById('button-reset').textContent = "リセット"; } }); // 「リセット」or「停止」ボタンをクリック document.getElementById('button-reset').addEventListener('click', async () => { if ( document.getElementById('button-reset').textContent === "停止" ) { document.getElementById('button-reset').textContent = "リセット"; window.api.TimerStop("ipc-timer-stop"); } else { window.api.TimerReset("ipc-timer-reset"); } }); } // タイマー(ミリ秒)の受け取り window.api.DisplayTimer("ipc-display-timer", (milliseconds) => { // console.log("ipc-display-timer: " + arg); if (milliseconds < 900){ document.getElementById('button-reset').textContent = "リセット"; return; } let min = parseInt((milliseconds / 1000) / 60); let sec = parseInt(milliseconds / 1000) % 60; document.getElementById('timer-number').textContent = ('00' + min).slice(-2) + ':' + ('00' + sec).slice(-2); }); </script> </head> <body> <div> <div id="timer-number">3:00</div> </div> <div class="button-wrapper"> <button id="button-start" class="button button-color-blue">開始</button> <button id="button-reset" class="button button-color-red">リセット</button> </div> </body> </html>
window.api.TimerStart(channel)
や、window.api.TimerStop(channel)
のように、設定した "api
" キーを通して関数を呼んでいます。これらのさらなる詳細、つまりサンプルプログラムは、以下の GitHub にアップロードされています。SimpleTimer - GitHub
https://github.com/hibara/SimpleTimerサンプル実行ファイルは以下の通りです。
macOS:
https://github.com/hibara/SimpleTimer/releases/download/v1.0.0/SimpleTimer-darwin-x64.zipWindows:
https://github.com/hibara/SimpleTimer/releases/download/v1.0.0/SimpleTimer-win32-ia32.zip参考
ipcRenderer.invoke()
https://qiita.com/jrsyo/items/abe19dff2d950132d9cdElectronでcontextBridgeによる安全なIPC通信(受信編)
https://qiita.com/pochman/items/62de713a014dcacbad68
- 投稿日:2020-09-22T12:06:28+09:00
あなたがまだ使っていないかもしれないHTML5の便利機能10選
こんにちは、たかとーです??
こちらは、10 useful HTML5 features, you may not be usingの翻訳記事になります。
当記事は、Tapasさんの許可を得て翻訳しています。Tweet
10 useful HTML5 features, you may not be using
HTML5
は新しいものではありません。最初のリリース(2008年1月)以来いくつかの機能を使用してきました。100DaysOfCode
の取り組みの一環として、HTML5の機能リストをもう一度よく見てみました。何か見つけたかな?私は今のところあまり使っていません。この記事では、過去にあまり使ったことがなかったが、今では便利になった
HTML5
の機能を10個挙げています。また、Netlify
でホストされている、実際に動作する例を作成しました。参考になることを願っています。Detailsタグ
<details>
タグは、ユーザーに必要なときだけ詳細を提供します。ユーザーにオンデマンドでコンテンツを表示する必要がある場合は、このタグを使用します。デフォルトでは、ウィジェットは閉じています。開くと、ウィジェットは展開され、コンテンツが表示されます。
<summary>
タグは、<details>
と一緒に使われ、見出しを指定します。コード
<details> <summary>Click Here to get the user details</summary> <table> <tr> <th>#</th> <th>Name</th> <th>Location</th> <th>Job</th> </tr> <tr> <td>1</td> <td>Adam</td> <td>Huston</td> <td>UI/UX</td> </tr> </table> </details>動作例
ここで実際に触ることができます: https://html5-tips.netlify.app/details/index.html
Tips
GitHubのReadmeで、必要に応じて詳細情報をで表示するために使ってみましょう。これは、Reactコンポーネントの膨大な量のプロパティリストを隠して、クリックされたら表示する例です。
Content Editable属性
contenteditable
は、コンテンツを編集可能にするために要素に設定できる属性です。<div>
、<p>
、<ul>
などで動作します。<element contenteditable="true|false">
のように指定する必要があります。注意: contenteditable属性がその要素に指定されていないとき、親要素から値を受け継ぎます。
コード
<h2>Shoppping List(Content Editable)</h2> <ul class="content-editable" contenteditable="true"> <li>1. Milk</li> <li>2. Bread</li> <li>3. Honey</li> </ul>動作例
ここで実際に触ることができます: https://html5-tips.netlify.app/content-editable/index.html
Tips
この属性を使うことで、
<span>
や<div>
を編集可能にし、CSSを使ってリッチなコンテンツを追加できます。これはinputフィールドを使用するよりもずっと良いでしょう。ぜひお試しください!Mapタグ
<map>
タグはイメージマップの定義に役立ちます。イメージマップとは、1つ以上のクリック可能な領域を持つ画像のことです。<area>
タグでクリック可能な領域を決定します。クリック可能な領域は、矩形、円、多角形の領域のいずれかになります。形状を指定しない場合は、領域を画像全体として考慮します。コード
<div> <img src="circus.jpg" width="500" height="500" alt="Circus" usemap="#circusmap" /> <map name="circusmap"> <area shape="rect" coords="67,114,207,254" href="elephant.htm" /> <area shape="rect" coords="222,141,318, 256" href="lion.htm" /> <area shape="rect" coords="343,111,455, 267" href="horse.htm" /> <area shape="rect" coords="35,328,143,500" href="clown.htm" /> <area shape="circle" coords="426,409,100" href="clown.htm" /> </map> </div>動作例
ここで実際に触ることができます: https://html5-tips.netlify.app/map/index.html
Markタグ
<mark>
タグでテキストをハイライトしてみましょう。コード
<p> Did you know, you can <mark>"Highlight something interesting"</mark> just with an HTML tag? </p>動作例
ここで実際に触ることができます: https://html5-tips.netlify.app/mark/index.html
data-* 属性
data-*
属性は、ページやアプリケーションにプライベートなカスタムデータを保存するために使用されます。保存されたデータはJavaScriptで使用して、さらなるユーザー体験を生み出すことができます。
data-*
属性は2つの部分から構成されますThe data-* attributes consist of two parts:
* 属性名に大文字を含めるべきではなく、またdata-
プレフィックスの後は1文字以上が必要です
* 属性値は任意の文字列になりますコード
<h2>Know data attribute</h2> <div class="data-attribute" id="data-attr" data-custom-attr="You are just Awesome!" > I have a hidden secret! </div> <button onclick="reveal()">Reveal</button>JavaScriptで以下のように操作します。
function reveal() { let dataDiv = document.getElementById('data-attr'); let value = dataDiv.dataset['customAttr']; document.getElementById('msg').innerHTML = `<mark>${value}</mark>`; }注意: JavaScriptでこれらの属性値を読み取るには、完全なHTML名(data-custom-attr)で
getAttribute()
を使用できますが、標準ではもっと簡単な方法を定義しています:dataset
プロパティを使用です。動作例
ここで実際に触ることができます: https://html5-tips.netlify.app/data-attribute/index.html
Outputタグ
<output>
タグは計算結果を表します。通常、この要素は計算結果のテキスト出力を表示するための領域を定義します。コード
<form oninput="x.value=parseInt(a.value) * parseInt(b.value)"> <input type="number" id="a" value="0" /> * <input type="number" id="b" value="0" /> = <output name="x" for="a b"></output> </form>動作例
ここで実際に触ることができます: https://html5-tips.netlify.app/output/index.html
Datalistタグ
<datalist>
タグは、あらかじめ定義されたオプションのリストを指定し、ユーザーはそのリストにさらにオプションを追加することができます。これはautocomplete
機能を提供しており、タイプアヘッドで目的のオプションを取得することができます。コード
<form action="" method="get"> <label for="fruit">Choose your fruit from the list:</label> <input list="fruits" name="fruit" id="fruit" /> <datalist id="fruits"> <option value="Apple"></option> <option value="Orange"></option> <option value="Banana"></option> <option value="Mango"></option> <option value="Avacado"></option> </datalist> <input type="submit" /> </form>動作例
ここで実際に触ることができます: https://html5-tips.netlify.app/datalist/index.html
Range(Slider)値
range
はinputタグのtype属性値で、スライダーのようなセレクタを実現します。コード
<form method="post"> <input type="range" name="range" min="0" max="100" step="1" value="" onchange="changeValue(event)" /> </form> <div class="range"> <output id="output" name="result"> </output> </div>動作例
ここで実際に触ることができます: https://html5-tips.netlify.app/range/index.html
Meterタグ
<meter>
タグを使って、与えられた範囲でデータを測ってみましょう。コード
<label for="home">/home/atapas</label> <meter id="home" value="4" min="0" max="10">2 out of 10</meter><br /> <label for="root">/root</label> <meter id="root" value="0.6">60%</meter><br />動作例
ここで実際に触ることができます: https://html5-tips.netlify.app/meter/index.html
Tips
プログレスバーのようなものを表示する際には
<progress>
タグを使用してください。<label for="file">Downloading progress:</label> <progress id="file" value="32" max="100">32%</progress>Inputsタグ
inputタイプにはいくつか特別な使い方があります。
コード
required
inputフィールドを必須アイテムにします。
<input type="text" id="username1" name="username" required />autofocus
input要素にカーソルを置くと自動でフォーカスします。
<input type="text" id="username2" name="username" required autofocus />validation with regex
正規表現をつかって入力値をバリデーションできます。
<input type="password" name="password" id="password" placeholder="6-20 chars, at least 1 digit, 1 uppercase and one lowercase letter" pattern="^(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{6,20}$" autofocus required />Color picker
シンプルなカラーピッカーです。
<input type="color" onchange="showColor(event)" /> <p id="colorMe">Color Me!</p>訳者感想
Reactなど、JavaScript関連の新しい情報はキャッチアップするようにしているのですが、HTMLに関しては全くできておらず、目からウロコな記事でした。
いままでJavaScriptを使って実現していたようなものをHTMLのみで表現できるのは、開発者にとっても楽で最高ですよね!
素晴らしい記事をありがとうございました Tapas✨
訳者について
2019年5月よりバンクーバーを拠点に移し、現在スタートアップの開始に向けて試行錯誤しているソフトウェアでデベロッパーです。
近頃は、VCの方と話しながらアイディアのブラッシュアップなどを行いながらMVPの検証を進めています。
フリーランス案件も募集し始めました!React、NodeJs、TypeScript等フロント、バックエンド問わず行えます。是非宜しくお願い致します。
もしよろしければ、以下SNSもよろしく願いします!
Twitter: @taishikat0_Ja
Note: 日本人でも英語圏で戦えることを証明したい。28歳が会社を辞め、個人開発者としてカナダでひたすらもがき続けた一年間とこれから
Linkedin: Taishi Kato
- 投稿日:2020-09-22T12:00:56+09:00
Next.jsにjestとenzymeを導入(next/babel使用)
Next.jsにjestとenzymeを導入(next/babel使用)
以前、Next.jsにjestとenzymeを導入するという記事を書きました。
上記の手順でjestの実行はできたのですが、yarn devでアプリ起動するとなにやらbabelに関するエラーが。。
どうやらNext.js起動すると追加したbabelの設定ファイルの方が読み込まれて、babelのエラーが出てしまっているよう。
そこで、jestで使うbabelをnext.jsのbabelに変更したところ、よりすっきりした設定になったのでメモ。
next.jsのbabelが使える
next.jsにはデフォルトでbabelが入っており、これがjsxのトランスパイルなどjestにも適用できることが分かりました。
こちらの方がスッキリとした手順・設定で構築できます。jestインストール
$ npm install --save-dev jestjest設定ファイルを生成
$ jest --initcommand not found: jest の場合
以下を実行します。
./node_modules/.bin/jest --initEnzymeインストール
yarn add --dev enzyme enzyme-adapter-react-16Enzymeの利用時は一度だけ
Enzyme.configure()
を呼ぶ必要があるため、下記のスクリプトを追加。jest.setup.jsimport Enzyme from "enzyme"; import Adapter from "enzyme-adapter-react-16"; Enzyme.configure({ adapter: new Adapter() });Jestのテスト前に実行されるようにする。
jest.config.jsmodule.exports = { // ... setupFiles: ['./jest.setup.js'], // ... }babel.config.jsを設定
module.exports = { "presets": ["next/babel"], };
テストファイルのignore
Cypressを導入しており、jest実行でcypressのspecも読まれてしまうので、ignore設定をしました。
jest.config.js... testPathIgnorePatterns: [ "/node_modules/", "/cypress/" ], ...
- 投稿日:2020-09-22T11:27:36+09:00
let宣言では同じ変数名は使えない、そんなことは分かっているけれど
let宣言では同じ変数名は使えない、そんなことは分かっているけれど
エラー内容
「この変数名は既に宣言されています」
何度ファイル内を見直して、同じ変数名がlet宣言されていないことを確認するも表示されるこのエラー。
見直せど、見直せど原因がわからない。原因
コードレビューしてもらい発覚した原因。
それはファイルをリファクタリングしたときにミスをしており、ファイルを二重読み込みしていたこと。同じ操作を複数のファイルに記述していたheader箇所を外部ファイル化したときに、その外部ファイル(Bとする)内でも該当のJavaScriptファイル(Cとする)を読み込んでいたのだ。
外部ファイルBを読み込んだファイル(Aとする)の両方でJavaScriptファイルCを呼び出していたため、二重呼び出し(二重読み込み)の状態になりエラーが発生していた。
AーBーC
|
C<html> <head> <script defer src="C.js"></script> </head> <body> <header> ~~~~~~~ </header> </body> </html>header箇所を外部ファイル化するにも関わらず、htmlタグやheadタグ、bodyタグを記述してさらにJavaScriptファイルCまで読み込んでいたことがエラーの原因だった。
<header> ~~~~ </header>本来、外部ファイルBではJavaScriptファイルCを呼び出す必要はなかったため削除し、header箇所だけ記述しておおもとのPHPファイルAでrequire_once('headerファイル');と呼びだすことでエラー解決できた。
- 投稿日:2020-09-22T11:12:35+09:00
Javascriptチュートリアル
JavaScriptコードエディター
概要 : このチュートリアルでは、JavaScriptのコードエディター及び、Javascriptコードを記載するためのVisual Studio Codeのインストール方法について学習します。
ポピュラーなJavaScriptコードエディター
Javascriptコードを記載する基本的なエディターとして、Windows上のNotepadのようなテキストエディターがあります。
しかし、Javascriptコードの記載を簡単かつ早く行うためには、Javascriptコードエディターが必要です。基本的なエディターに対しJavascriptコードエディターは、構文の強調表示、インデント、オートコンプリート、ブレースマッチング機能など様々な機能を提供しています。
また、いくつかのエディターはJavascriptやその他のコードをデバッグするのに便利な機能を提供しています。Javascriptコードエディターとしてポピュラーなものに以下のようなものがあります。
* Visual Studio Code
* Atom
* Notepad++
* Vim
* Gnu Emacsこれらは全て無料で使用できます。ここではVisual Studio Codeを取り上げます。
Visual Studio Code
Visual Studio CodeはMicrosoftによって開発された無料のオープンソースコードエディターです。
Windows,Linux,MacOSを含むプラットフォームで動作します。
Visual Studio Codeは高度にカスタマイズされており、テーマ、キーボードショートカットなどのような環境設定や、様々な拡張機能をインストールして使用できます。
また、インテリセンス、デバッグ、フォーマット、コードナビゲーション、リファクタリングの他多くの優れた言語機能を組み込みでサポートしています。Visual Studio Codeでサポートされている全ての機能を理解するには以下を参照してください。
JavaScript in Visual Studio CodeVisual Studio Codeのダウンロード
Visual Studio Codeは以下のリンクからダウンロードできます。
Download Visual Studio CodeVisual Studio Codeのインストール
Visual Studio Codeは数分で簡単にインストールできます。
Live Server拡張機能のインストール
Live Server拡張機能は、Javascriptのコードを変更するだけでWebブラウザの更新をすることなくページを更新できる機能を持っています。
便利な機能なので是非使用してください。
このチュートリアルでは、Visual Studio Codeのインストール方法について記載しました。
- 投稿日:2020-09-22T11:12:35+09:00
Javascriptコードエディター
JavaScriptコードエディター
概要 : このチュートリアルでは、JavaScriptのコードエディター及び、Javascriptコードを記載するためのVisual Studio Codeのインストール方法について学習します。
ポピュラーなJavaScriptコードエディター
Javascriptコードを記載する基本的なエディターとして、Windows上のNotepadのようなテキストエディターがあります。
しかし、Javascriptコードの記載を簡単かつ早く行うためには、Javascriptコードエディターが必要です。基本的なエディターに対しJavascriptコードエディターは、構文の強調表示、インデント、オートコンプリート、ブレースマッチング機能など様々な機能を提供しています。
また、いくつかのエディターはJavascriptやその他のコードをデバッグするのに便利な機能を提供しています。Javascriptコードエディターとしてポピュラーなものに以下のようなものがあります。
* Visual Studio Code
* Atom
* Notepad++
* Vim
* Gnu Emacsこれらは全て無料で使用できます。ここではVisual Studio Codeを取り上げます。
Visual Studio Code
Visual Studio CodeはMicrosoftによって開発された無料のオープンソースコードエディターです。
Windows,Linux,MacOSを含むプラットフォームで動作します。
Visual Studio Codeは高度にカスタマイズされており、テーマ、キーボードショートカットなどのような環境設定や、様々な拡張機能をインストールして使用できます。
また、インテリセンス、デバッグ、フォーマット、コードナビゲーション、リファクタリングの他多くの優れた言語機能を組み込みでサポートしています。Visual Studio Codeでサポートされている全ての機能を理解するには以下を参照してください。
JavaScript in Visual Studio CodeVisual Studio Codeのダウンロード
Visual Studio Codeは以下のリンクからダウンロードできます。
Download Visual Studio CodeVisual Studio Codeのインストール
Visual Studio Codeは数分で簡単にインストールできます。
Live Server拡張機能のインストール
Live Server拡張機能は、Javascriptのコードを変更するだけでWebブラウザの更新をすることなくページを更新できる機能を持っています。
便利な機能なので是非使用してください。
このチュートリアルでは、Visual Studio Codeのインストール方法について記載しました。
- 投稿日:2020-09-22T07:50:23+09:00
GASでTwitterのアカウントがBANされているかを調べる
目標
GoogleスプレッドシートにTwitterIDを羅列して
それらを自動でAPIに投げて結果を書き込んでくれるようにします。Twitterのアカウントの状態を調べるには
shadowban
(https://shadowban.eu/)
こちらにIDを打ち込んでTwitterアカウントの状態を調べることができます。
今回はサイト内で使われているAPI(https://shadowban.eu/.api/)
をGAS内で使います。TwitterアカウントのBANの種類
4つのTwitterシャドウバンの種類と解説 気づかぬうちになってるかも)
(https://kanasys.com/tech/824)STEP1 スプレッドシートを読み込もう!!
二つの方法がありますが
script.gasfunction main() { const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet(); const sheet = SpreadsheetApp.openById(spreadsheetId).getSheetByName(sheetName); /こっち }
- 一番目は、GASと紐づけられた(スプレッドシート内のツールからGASを記述するとき)スプレッドシートに有効です。
- 二番目はスプレッドシートのIDとシート名を指定して読み込めます。
今回は二番目のスプレッドシートのIDとシート名を記述して読み込む方法を使います。
script.gasconst spreadsheetId = 'google.com/spreadsheets/d/<-----ID------>/edit'; const sheetName = 'シート1'; // const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet(); //GASがスプレッドシートと紐づいている時の取得方法 const sheet = SpreadsheetApp.openById(spreadsheetId).getSheetByName(sheetName);STEP2 取得するテーブルの幅を設定し、取得しよう!
セル単品を取得する場合は
script.gasconst accountsArr = sheet.getRange(columnLength, rowLength).getValue();
getvalue()
で値を配列になって取得できます。今回は幅を選択して取得するので
script.gasconst accountsArr = sheet.getRange(startColumn, startRow, endColumn, endRow).getValue();
getValues()
をつかって幅丸ごと取得します。その前に!!
各、値を設定しましょうかscript.gasconst headerLength = 1; // アカウント名 状態 などのヘッダー情報の高さ const accountColumn = 1; //アカウント名が羅列してあるカラム const statusColumn = 2; //状態を記録するカラム const accountLength = sheet.getLastRow() - headerLength; // アカウントの数(ヘッダーの高さを考慮)各記録を設定したら、実際に取得します。
script.gasfunction main() { const spreadsheetId = 'XXXXXXXXXXXXXXXXXXXXXXXX'; const sheetName = 'シート1'; const sheet = SpreadsheetApp.openById(spreadsheetId).getSheetByName(sheetName); const headerLength = 1; const accountColumn = 1; const statusColumn = 2; const accountLength = sheet.getLastRow() - headerLength; const accountsArr = sheet.getRange(accountColumn + headerLength ,accountColumn, accountLength, accountColumn).getValues(); }script.gas[['配列1'],['配列2'],['配列3']]こんな感じで取得できましたね。
STEP3 GAS内でAPIをコールしてみよう!
簡単。。。
script.gasresponse = UrlFetchApp.fetch(url).getContentText(); let json = JSON.parse(response);なので、、、
shadowban APIのURLを入れます。
パスパラメータがないと404が帰ってくるので、安倍晋三さんのIDを入れます。(一番有名?なので)script.gasconst shadowbanApi = 'https://shadowban.eu/.api/AbeShinzo'; response = UrlFetchApp.fetch(shadowbanApi).getContentText(); let json = JSON.parse(response);帰ってくるjsonの中身はこんな感じでした。
response.json{ "profile": { "sensitives": { "possibly_sensitive": 0, "counted": 1111, "possibly_sensitive_editable": 758 }, "protected": false, // 鍵アカウントの場合はtrueになります。 "exists": true, // 存在するか。 "screen_name": "AbeShinzo", "has_tweets": true // tweetが1回以上あるか }, "timestamp": 1600727024.7363904, "tests": { "more_replies": { "ban": false, // true ならばReply deboosting banになります "tweet": "1150369849270398978", "in_reply_to": "1150369468402417664" }, "search": "1307120105478934528", "typeahead": true, "ghost": { "ban": false // true ならばghost banになります。 } } }このjsonで一つ注意なのですが、
アカウントが凍結されている場合は
tests
のプロパティがつきません(凍結されるとshadowbanのテストができないため)STEP4 スプレッドシートから取得したIDでAPI回そう書き込もう!
めんどくさくなりました。説明すると長くなるので全部記述します。
script.gasfunction main() { const spreadsheetId = 'XXXXXXXXXXXXXXXXXXXXXXXX'; const sheetName = 'シート1'; const sheet = SpreadsheetApp.openById(spreadsheetId).getSheetByName(sheetName); const headerLength = 1; const accountColumn = 1; const statusColumn = 2; const accountLength = sheet.getLastRow() - headerLength; const accountsArr = sheet.getRange(accountColumn + headerLength ,accountColumn, accountLength, accountColumn).getValues(); for(let i = 1; i <= accountLength; i++) { const url = shadowbanApi + accountsArr[i-1][0]; const response = callApi(url); const status = searchStatus(response) sheet.getRange(i + headerLength, statusColumn).setValue(status.message.join(',')); sheet.getRange(i + headerLength, statusColumn).setBackground(status.color); } } function callApi(url) { try { response = UrlFetchApp.fetch(url).getContentText(); let json = JSON.parse(response); return json; } catch (e) { console.log('APIエラー:', e); return {}; } } function searchStatus(json) { let status = { message: [], color: "" } if(json === undefined || json === null || Object.keys(json).length === 0) return status; if (!json.profile.exists) { status.message.push('存在しません'); status.color = 'red'; } else if (json.profile.protected) { status.message.push('鍵アカウント'); } else if (json.profile.suspended) { status.message.push('凍結されています'); status.color = 'red'; } else if (!json.profile.has_tweets) { status.message.push('tweetされていません'); } if(json.tests) { if (json.tests.typeahead === false) { status.message.push('Search suggestion ban.'); status.color = 'yellow'; } if (json.tests.search === false) { status.message.push('Search ban.'); status.color = 'red'; } if (json.tests.ghost.ban === true) { status.message.push('Ghost ban'); status.color = 'red'; } if (json.tests.more_replies) { if (json.tests.more_replies.error === 'ENOREPLIES') { status.message.push('has not made any reply tweets.'); } else if (json.tests.more_replies.ban === true) { status.message.push('Reply deboosting ban.'); status.color = 'yellow'; } } } if(status.message.length === 0) status.message.push('正常'); return status; }投げやりになりました。
肝はscript.gasfor(let i = 1; i <= accountLength; i++) { const url = shadowbanApi + accountsArr[i-1][0]; const response = callApi(url); const status = searchStatus(response) sheet.getRange(i + headerLength, statusColumn).setValue(status.message.join(',')); sheet.getRange(i + headerLength, statusColumn).setBackground(status.color); }ここですね。
setValue('値')
で指定したCellに書き込むことができます。
setBackground('red')
でセルの背景に色をつけることができます。今回は
4つのTwitterシャドウバンの種類と解説 気づかぬうちになってるかも?
(https://kanasys.com/tech/824)程度軽になっているものは'yellow'
程度重になっているものは'red'
で背景色をつけました。※一部shadowbanのフロントエンドのソースコードを参考にしました。
STEP5 お試しあれ
一応もう一度コード記述します。
script.gasfunction main() { const spreadsheetId = 'XXXXXXXXXXXXXXXXXXXXXXXX'; const sheetName = 'シート1'; const sheet = SpreadsheetApp.openById(spreadsheetId).getSheetByName(sheetName); const headerLength = 1; const accountColumn = 1; const statusColumn = 2; const accountLength = sheet.getLastRow() - headerLength; const accountsArr = sheet.getRange(accountColumn + headerLength ,accountColumn, accountLength, accountColumn).getValues(); for(let i = 1; i <= accountLength; i++) { const url = shadowbanApi + accountsArr[i-1][0]; const response = callApi(url); const status = searchStatus(response) sheet.getRange(i + headerLength, statusColumn).setValue(status.message.join(',')); sheet.getRange(i + headerLength, statusColumn).setBackground(status.color); } } function callApi(url) { try { response = UrlFetchApp.fetch(url).getContentText(); let json = JSON.parse(response); return json; } catch (e) { console.log('APIエラー:', e); return {}; } } function searchStatus(json) { let status = { message: [], color: "" } if(json === undefined || json === null || Object.keys(json).length === 0) return status; if (!json.profile.exists) { status.message.push('存在しません'); status.color = 'red'; } else if (json.profile.protected) { status.message.push('鍵アカウント'); } else if (json.profile.suspended) { status.message.push('凍結されています'); status.color = 'red'; } else if (!json.profile.has_tweets) { status.message.push('tweetされていません'); } if(json.tests) { if (json.tests.typeahead === false) { status.message.push('Search suggestion ban.'); status.color = 'yellow'; } if (json.tests.search === false) { status.message.push('Search ban.'); status.color = 'red'; } if (json.tests.ghost.ban === true) { status.message.push('Ghost ban'); status.color = 'red'; } if (json.tests.more_replies) { if (json.tests.more_replies.error === 'ENOREPLIES') { status.message.push('has not made any reply tweets.'); } else if (json.tests.more_replies.ban === true) { status.message.push('Reply deboosting ban.'); status.color = 'yellow'; } } } if(status.message.length === 0) status.message.push('正常'); return status; }GAS内ではこうですね。
ちゃんとスプレッドシートのIDとシート名入れてくださいね!終わりに
GAS簡単ですね。
気になったのはログ表示が遅いです。普通にjavascript書いてて
console.log()
で待つことってあまりないですから、辛くなりました。恵まれてますね。
- 投稿日:2020-09-22T06:56:39+09:00
プログラミング TypeScript スケールするJavaScriptアプリケーション開発のレビュー(読みながら)
どうも初めましてNakZです。はじめてQiitaの記事を書きます。
よろしくお願いします。
表題の通り読みながらレビューしていこうと思います。
レビューというかメモ書きというかあやふやな記事になってます。読み終わったら綺麗にまとめなおします。
全力で著作権に配慮しますので、違反してる箇所があればご指摘くださいm(_ _)m
念のため記事中のコードは全て自分で書き、できる限りシンプル(書籍のオリジナリティを侵害しないよう)になるように心掛けてます。目的
- 記事を更新しながら読むことで積読しないようにする
- Qiitaで記事書いてみたい
最近アルバイトでTypeScriptを使うことになったのでモチベーション維持のために書きます。
独学で雑にプログラミングの勉強してきたので型とかクラスとか真面目に勉強するのも悪くないなと思って読むことにしました。全体の感想(読んでる途中だけど)
訳書なのに全体的にこなれた文章なので読みやすい(特にジョークが面白い)。ただ、込み入った説明の部分は直訳的になってしまっているようで多少意味が通りづらく実際にコードを動かしてみないと何を伝えたいのか汲み取りづらかったりする。(コードを動かすのは良いことなので別に良いけども)
JavaScriptの経験がないとコードサンプルが読みづらいかもしれない。僕もJavaScriptは簡単なDOM操作のためにしか使った経験がなかったのでコードサンプルの中のコードだとか、細かい文法を調べながら読むことが割と多い。
言語の仕様を詳しく知ることでできることの幅が広がりそうだなというのを感じながら読めるので買って良かったと思ってます。今まで雑に使ってきたGoとかPythonの仕様もちゃんと勉強したくなった。以下、章ごとのレビュー
1章 イントロダクション
静的型付け言語であることのメリットとTypeScriptの柔軟性がたくさん語られていました。
2章 TypeScript:全体像
- TypescriptのコンパイラはTypeScriptのコードをJavaScriptのコードにコンパイラすることの説明
- tsconfig.jsonの書き方。(TypeScriptはルートディレクトリにtsconfig.jsonがないといけない)
- "Hello,World!"プログラム
- 練習問題(練習問題というより型チェックの体験)
型チェックのおかげで可愛いバリスタとデートできるようになるらしい。
VSCodeでのTypeScriptのデバッグの方法
本に書いてあるコードを気軽に試したかったのでVSCodeでデバッグする方法調べました。
- プロジェクトのディレクトリに移動して
Shift + command + D
でデバッグ画面を表示させます。- To customize Run and Debug create a launch.json file.ってのをクリックする
- Node.jsを選択
.vscode/launch.json
ってファイルができるのでそこの"program": ~
の~を実行したいファイルのパスに変える。僕の場合、本の手順にしたがってtsconfig.jsonを書いたので"program":${workspaceFolder}/dist/index.js
になっている。これはtsconfig.jsで"outDir": "dist"
としたからである。これでデバッグができる。ちなみにTypeScriptのファイルを書き換えたあとデバッグすると自動でtsファイルをコンパイルして実行してくれる(便利)
3章 型について
今まで型についてなあなあにやってきていたので勉強する良い機会になりました。
- いろんな型があるんだなあ。。。ってなるし一つ一つ結構詳しく書いてくれてる。
練習問題に解答がついているのが凄くありがたい。
今まであまり型について真面目に考えたことなかったので良い勉強になりました。この章から少し内容が堅くなってきてる気がする。(この本は最初に理論の話をしてから実装のパートに移るらしい)4章 関数
- レストパラメーター便利そう。Pythonの
*args
みたいな感じで使えば良いんかな。- JavaScriptはクラスだけじゃなくて関数にも
this
がついてるのはじめて知りました。- 呼び出しシグネチャちゃんと定義したら、関数が変な挙動しそうだったら事前にわかるし便利そう。
- 関数式のオーバーロードよくわからん。関数宣言のオーバーロードの方が使いやすくないの?
- ジェネリック型便利そう
type Foo = { bar? :string baz?: string } let hoge :Foo hoge ={} console.log(hoge.bar)これ
hoge.bar
に値入れてないからエラーになると思ったけど、一応、undefined
って出力されるのか。
ジェネリック関数便利、以下、書き方忘れたときに思い出すときのためのコードtype Fuga ={ <T>(bar:T):T }// type Hoge = <T>(foo:T)=> T と同じ let fuga:Fuga = function (bar){ return bar } console.log("fuga:",fuga("fuga"))この書き方だと、関数
fuga
に引数を渡して呼び出すタイミングでジェネリック型Tの型を決定する。しかし、type Hoge<T> ={ (foo:T):T }// type Hoge<T> = (foo:T)=> T と同じ let hoge:Hoge<string> = function (foo :string){ return foo } console.log("hoge:",hoge("hoge"))の書き方だと、関数の宣言時にジェネリック型Tの型を決定する。
複数使うときは<T,U>
って感じにする。type Filter = { <T,U>(farray: T[],garray:U[] ,f:(fitem:T)=>boolean, g:(gitem :U) => boolean):(T|U)[] | undefined } let filter :Filter = (farray,garray,f,g) =>{ let result = [] for(let element of farray){ if( f(element) ===true ){ result.push(element) } } for(let element of garray){ if( g(element) ===true ){ result.push(element) } } return result } let f = (n:number)=> {return n >1} let g = (str:string)=>{return str.length === 3} let out = filter([1,2,3],["bar","foo"],f,g) if (out !== undefined){ console.log(...out) }else{ console.log("out is undefined.") }JavaScriptの配列ってどうなってんの?
JavaScriptって下のコード書けるのか。Pythonだと
IndexError: list assignment index out of range
って怒られたけど。let hoge = [] hoge[2] = "hoge" console.log(hoge)気になったので調べてみると
let hoge = [] hoge[2] = "hoge" console.log(hoge[0])だと
undefined
が出力された。0,1がなくても2にメモリ割り当てられるの?気が向いたら調べたい。5章 クラスとインターフェース
6章 高度な型
7章 エラー処理
8章 非同期プログラミングと並行、並列処理
9章 フロントエンドとバックエンドのフレームワーク
10章 名前空間とモジュール
11章 JavaScriptとの相互運用
12章 TypeScriptのビルドと実行
13章 終わりに
付録A 型演算子
付録B 型ユーティリティー
付録C 宣言の振る舞い
付録D サードパーティーJavaScriptモジュールのための宣言ファイルの書き方
付録E トリプルスラッシュ・ディレクティブ
付録F 安全性に関するTSCコンパイラーフラグ
付録G TSX
付録H ESLintとAST
- 投稿日:2020-09-22T02:15:02+09:00
ブログの表示高速化のためにやったこと・調べたこと
元記事
https://coincidence.netlify.app/post/blog-display-speed
(ブログからの転載になります。)表示が遅い!!
このブログ、現状でもかなり表示が遅いのですが、本記事での取り組みをやる前はさらに遅かったです。
参考までにLighthouseのスコアをお示ししますと、Performanceスコアが32〜36点ほどでした。
この状態から以下の対応を行なったことで、50点以上を安定して取れるようになりました。正直このブログにとっては意味のない対応もありましたが、勉強の意味で残しておこうと思います。
各施策の最初には星印で効果の度合いを示しました。黒星(ぬりつぶし)が多い方が、効果があったものになります。ただしこれは、私のSPAブログ環境ではこうだった、という話ですので、設計が異なれば有効な対策も異なるということはご注意くださいませ。(大丈夫だと思いますけど)施策1:APIへの問合せ回数減
効果 ★★★☆☆
これは、トップページ(/page/1)の表示時間短縮には直接影響しない部分ですが、画面遷移時のオーバーヘッドを減らす意味で非常に役立ちました。
SPAの基本的な振る舞いとして、「情報の更新が必要であればその都度サーバーサイドに問合せ、結果を取得して表示更新する」という像があると思うのですが、WebページというよりWebアプリエンジニアの私はこの考えに縛られすぎていたようです。ページの表示、タグ一覧の表示、タグに紐づく記事一覧の表示…といった振る舞いをSPAブログにさせるために、毎回Contentfulに問い合わせをしていたのです。情報の更新はされていないにもかかわらず。
これはあまりに無駄が大きいので、ビルド実行前にContentful APIから情報を取得してjsonとして保存するスクリプトを書きました。記事数が増えてきたらまた対応を考えなければならないかもしれませんが、とりあえずは表示速度改善の意味で満足できる結果になりました。施策2:コードチャンクの整理(highlight.js)
効果 ★★★★☆
スコア向上の意味ではこれの効果が大きかったです。
Vue CLIで作成したプロジェクトは、yarn run build --report
すると/dist/report.html
を生成してくれます。webpackでバンドルしたコードチャンクのうちなにが重いか?を可視化してくれる素晴らしいツールです。(単体ではwebpack-bandle-analyzerとして配布されています。有名だから今更かも。)
見ると、highlight.jsが大量のチャンクを吐き出していたので、何事かと思い調べたら、どうやら単にrequire
するだけでは不要な言語パックまで引き連れてきてしまうそうです。面倒でもちゃんとregisterLanguage
した方がいいみたいね。// このやり方だと重くなります import hljs from "highlight.js"; // こうしたらマシです(ちなみにhtmlはデフォルトでハイライトしてくれるっぽい) import hljs from "highlight.js/libs/highlight"; import javascirpt from "highlight.js/lib/languages/javascript"; import css from "highlight.js/lib/languages/css"; hljs.registerLanguage("javascript", javascript); hljs.registerLanguage("css", css);ちなみに、最新バージョンだと
registerLanguage
がまともに動いてくれないバグがあるようなので、大人しくバージョンを下げて使いましょう。package.jsonを以下のとおり更新して、yarn
もしくはnpm i
です。{ "dependencies": { "highlight.js": "~9.18.1", ... } }施策3:トップページのプレレンダリング(prerender-spa-plugin)
効果 ★☆☆☆☆
SPA高速化の手段の一つとして代表的なのがプレレンダリングですが、私の場合はそもそもHTMLの構造が複雑なために速度低下を招いていたわけではなさそうなので、これはあまり効果がありませんでした。また、ブログという性質上ルーティングが動的に変わるので、とりあえずよく参照されるトップページだけこの対応を埋め込みましたが、全てのブログページをプレレンダリングするのはやや設定が面倒そうな印象でした。
(ちょっと頑張ればやれなくもなさそうですが、記事数が増えるに従ってビルド時間がとんでもないことになりそうな気がしています。)
ページ数が限定されているSPAなら、やってみるのは大いにアリなのではないでしょうか。施策4:ルーティングコンポーネントの非同期読み込み
効果 ★★★☆☆
これもなかなか良い感じに速度を改善してくれました。
メインの領域はVue Routerで切り替えているのですが、どのルートにアクセスしても、直近で必要のないJSを読み込んでしまっていたようなので、とりあえずはそのルートに必要なJSだけ呼び出してもらうように設定しました。Vue Router公式ドキュメントが参考になりましたが、要するにrouterの処理を書いているファイル内のコンポーネントのimport文を、なにも考えずに書き換えれば良いです。// before..最初に全部読み込み import HogeComponent from "@/components/HogeComponent.vue"; // after ..遷移時に必要に応じて読み込み const HogeComponent = () => import("@/components/HogeComponent.vue");Nuxt.jsとかGridsome、VuePressに移行したい
施策3でプレレンダリングを試してみたとはいえ、結局のところは静的サイトジェネレータに対応した上記のライブラリを使った方がよいのでは?感が拭えない結果となりました。Lighthouseのスコアも50止まりですしね。大人しく、JAMStackな設計のベストプラクティスに従ったほうがよさそうだ、というなんともしょっぱい結論となり恐縮です。
Nuxtとか使う時はまた記事書きます。ではでは。
- 投稿日:2020-09-22T01:25:37+09:00
PubNubで5分でリアルタイムWebこと初め + MQTTでデバイス連携も #protoout #iotlt #ヒーローズリーグ
PubNubを使うと簡単にリアルタイムWeb的なものを作れます。老舗サービスですが今更ながらまとめてみます。IoTLTで紹介したやつですね。
#ヒーローズリーグ
のハッカソンでPubNubを触ったので改めてメモ。M5Stackハッカソンでやった内容メモです。
とりあえず簡単なリアルタイムWebとMQTTの利用を試してみます。
5分で、って書いたのは一旦ブラウザ上だけで試すところくらいまでです。
SDKの数が多いので色々なアプリケーションの接着剤的に使えるかなと思います。
PubNubとは
PUbNubはリアルタイムなメッセージングやMQTTなどの裏側の仕組みを肩代わりしてくれるBaaSサービスです。
リアルタイムチャットなどでWebsocketやSocket.ioのサーバーを自前で用意しなくても、デバイスとのやりとりでMQTTブローカーを自前で用意しなくても良いという便利サービスです。
しかもけっこう無料で使えます。(そんなに調べてない)
アプリケーション作成し、Pub/Subキーの取得
まずはアカウント作成しましょう。
- ログイン後の画面
- APP作成、
CREATE NEW APP
から今回M5Stackで使いたかったのでfor_m5stackというアプリ名にしました。
Chat App
とが選択できますが、
Other Messaging Use CasesOther Messaging Use Cases
を選択します。まず試すだけだとこちらの方が入りやすいです。
作成したアプリを選択すると
Publish KEY
とSubscribe Key
がわかります。
Publish Key
とSubscribe KEY
を記録しておきましょう。ブラウザで試す
まずはブラウザで簡単なメッセージングを試します。
PubNubはWebsocketの接続を提供してくれます。
片方のブラウザを操作すると、もう片方のブラウザでも反応がある といった仕組みをコピペで作ってみます。
HTMLファイルの作成
index.htmlを作成、VScodeなどで作成しましょう。
キーは適宜変更しましょう。
<!DOCTYPE html> <html> <head> <title>Publish Subscribe Tutorial</title> </head> <body> <input id="publish-button" type="submit" value="Click here to Publish"/> </body> <script src="https://cdn.pubnub.com/sdk/javascript/pubnub.4.21.7.min.js"></script> <script> const uuid = PubNub.generateUUID(); const pubnub = new PubNub({ publishKey: "pub-c-xxxxxxxxxxxxxxx", subscribeKey: "sub-c-xxxxxxxxxxxxxxx", uuid: uuid }); const button = document.getElementById('publish-button'); button.addEventListener('click', () => { pubnub.publish({ channel : "pubnub_onboarding_channel", message : {"sender": uuid, "content": "Hello From JavaScript SDK"} }, function(status, response) { //Handle error here }); }); pubnub.subscribe({ channels: ['pubnub_onboarding_channel'], withPresence: true }); pubnub.addListener({ message: function(event) { let pElement = document.createElement('p'); pElement.appendChild(document.createTextNode(event.message.content)); document.body.appendChild(pElement); }, presence: function(event) { let pElement = document.createElement('p'); pElement.appendChild(document.createTextNode(event.uuid + " has joined. That's you!")); document.body.appendChild(pElement); } }); pubnub.history( { channel: 'pubnub_onboarding_channel', count: 10, stringifiedTimeToken: true, }, function (status, response) { let pElement = document.createElement('h3'); pElement.appendChild(document.createTextNode('historical messages')); document.body.appendChild(pElement); pElement = document.createElement('ul'); let msgs = response.messages; for (let i in msgs) { msg = msgs[i]; let pElement = document.createElement('li'); pElement.appendChild(document.createTextNode('sender: ' + msg.entry.sender + ', content: ' + msg.entry.content)); document.body.appendChild(pElement); } } ); </script> </html>試す
Live Serverなどでローカルサーバーを起動させてアクセスします。
こんな表示になります。そして 複数のブラウザでアクセスし、片方のボタンを押すと、もう片方にも反応があれば成功です。
ブラウザ(クライアント)間でのメッセージングが出来ている状態です。
同じパソコン内でやってるとイメージがつきにくいかもしれないですが、別のパソコンやスマホ同士でも問題なく動作します。
おまけ: MQTTも試す
ブラウザではWebSocketですが、デバイスに接続する際はMQTTも利用できます。
mosquitto_pubとmosquitto_sub でMQTT確認
ここまでで一旦のメッセージングは
インストールはbrewで(macのみ)
$ brew install mosquittoSubscribe(mosquitto_sub)
n0bisuke
がクライアントID。任意の文字列で大丈夫。
m5stack
がトピック名。これも任意の文字列で大丈夫。$ mosquitto_sub -h mqtt.pndsn.com \ -t 'pub-c-xxxxxxxxxxxxxxx/sub-c-xxxxxxxxxxxxxxxx/m5stack' \ -i 'pub-c-xxxxxxxxxxxxxxx/sub-c-xxxxxxxxxxxxxxxx/n0bisuke' \Publish(mosquitto_pub)
n0bisuke
がクライアントID。任意の文字列で大丈夫。
m5stack
がトピック名。これも任意の文字列で大丈夫。$ mosquitto_pub -h mqtt.pndsn.com \ -t 'pub-c-xxxxxxxxxxxxxxx/sub-c-xxxxxxxxxxxxxxxx/m5stack' \ -i 'pub-c-xxxxxxxxxxxxxxx/sub-c-xxxxxxxxxxxxxxxx/n0bisuke' \ -m 'ほげほげ'ためす
mosquitto_subを起動した状態で、mosquitto_pubを実行すると、-mで送ったメッセージ(今回はほげほげ)がmosquitto_sub側に表示されます。
Arduinoで試す
ESP32用ですが、Arduinoで試す方法もこちらに簡単にまとめてます。
ブラウザ+デバイス(MQTT)
ブラウザとブラウザ(WebSocket)、デバイスとデバイス(MQTT)を試しましたが、ブラウザ+デバイスを試します。
先ほどのブラウザのコードを変えずに試してみます。
- ブラウザ -> デバイス
元々のコードでチャンネル(MQTT的にはトピック)が
pubnub_onboarding_channel
となっていました。なのでmosquitto_subでは以下のように設定して試してみましょう。$ mosquitto_sub -h mqtt.pndsn.com \ -t 'pub-c-xxxxxxxxxxxxxxx/sub-c-xxxxxxxxxxxxxxxx/pubnub_onboarding_channel' \ -i 'pub-c-xxxxxxxxxxxxxxx/sub-c-xxxxxxxxxxxxxxxx/n0bisuke' \ブラウザのボタンを押すと、mosquitto_subに反応があります。
- デバイス -> ブラウザ
デバイスからブラウザに送るときはmosquitto_pubで
{"content":"Hello!!! from MQTT!"}
といったメッセージを送るとブラウザ側にメッセージが表示されます。$ mosquitto_pub -h mqtt.pndsn.com \ -t 'pub-c-xxxxxxxxxxxxxxx/sub-c-xxxxxxxxxxxxxxxx/pubnub_onboarding_channel' \ -i 'pub-c-xxxxxxxxxxxxxxx/sub-c-xxxxxxxxxxxxxxxx/n0bisuke' \ -m '{"content":"Hello!!! from MQTT!"}'まとめ
- ブラウザ+ブラウザ
- デバイス+デバイス
- ブラウザ+デバイス (双方向)
を簡単に紹介しました。デバイスと言いつつMac上のMQTTクライアントなので実際にはデバイス側コードは各自試してみてください。
HTTPだけしか知らないと、デバイス+デバイスや、ブラウザ->デバイスはなかなか難しいのでIoT系の何かを作るときにも使えますし、デバイス無しでチャットアプリみたいなものを作るときも使えると思います。
使いこなしていきたい。
- 投稿日:2020-09-22T01:08:54+09:00
カウントダウンタイマーの実装
はじめに
ポートフォリオで実装予定のカウントダウンタイマーを実装する。
タイマーを実装するにはjQueryが必要と書いてある記事もあるが、今回はjQueryを利用せずに実装を進めていくことにする。カラムの作成
まずは、いつものように
rails generate migration Addカラム名Toテーブル名 カラム名:データ型でマイグレーションファイルを作成する。
今回の場合、制限時間を表示させる場所を表示させたいのでdeadlineをカラム名として、rails generate migration AddDeadlineToMission deadline:datetimeとする。
class CreateMissions < ActiveRecord::Migration[6.0] def change create_table :missions do |t| t.integer :user_id t.text :content t.string :penalty t.datetime :deadline t.timestamps end end end上記のマイグレーションファイルの7行目に
t.datetime :deadline
を追加する。
そして、親の顔より見た$ rails db:migrateを実行して、データベースに保存する。
タイマーの実装
HTMLの記述
上記のような赤字のタイマーを設定するには、
app/views/missions/new.html.erb<p> <%= f.hidden_field :deadline, :id => "deadline.id" %> <input type="text" id="userYear" >年 <input type="text" id="userMonth">月 <input type="text" id="userDate" >日 <input type="text" id="userHour" >時 <input type="text" id="userMin" >分 <input type="text" id="userSec" >秒 </p> <p id="RealtimeCountdownArea" ></p> #ここにタイマーが表示されるとする。
ここで、:id => deadline.id
は後のjavascriptの記述において効果を発揮するため、記述している。javascriptの記述
今回は、
app/views/missions/new.html.erb
のscriptタグにjavascriptを記述することとする。
以下の通りである。app/views/missions/new.html.erb<script> function set2fig(num) { // 数値が1桁だったら2桁の文字列にして返す var ret; if( num < 10 ) { ret = "0" + num; } else { ret = num; } return ret; } function isNumOrZero(num) { // 数値でなかったら0にして返す if( isNaN(num) ) { return 0; } return num; } function showCountdown() { // 現在日時を数値(1970-01-01 00:00:00からのミリ秒)に変換 var nowDate = new Date(); var dnumNow = nowDate.getTime(); // 指定日時を数値(1970-01-01 00:00:00からのミリ秒)に変換 var inputYear = document.getElementById("userYear").value; var inputMonth = document.getElementById("userMonth").value - 1; var inputDate = document.getElementById("userDate").value; var inputHour = document.getElementById("userHour").value; var inputMin = document.getElementById("userMin").value; var inputSec = document.getElementById("userSec").value; var targetDate = new Date( isNumOrZero(inputYear), isNumOrZero(inputMonth), isNumOrZero(inputDate), isNumOrZero(inputHour), isNumOrZero(inputMin), isNumOrZero(inputSec) ); var dnumTarget = targetDate.getTime(); // 表示を準備 var dlYear = targetDate.getFullYear(); var dlMonth = targetDate.getMonth() + 1; var dlDate = targetDate.getDate(); var dlHour = targetDate.getHours(); var dlMin = targetDate.getMinutes(); var dlSec = targetDate.getSeconds(); var msg1 = "期限の" + dlYear + "/" + dlMonth + "/" + dlDate + " " + set2fig(dlHour) + ":" + set2fig(dlMin) + ":" + set2fig(dlSec); // 引き算して日数(ミリ秒)の差を計算 var diff2Dates = dnumTarget - dnumNow; if( dnumTarget < dnumNow ) { // 期限が過ぎた場合は -1 を掛けて正の値に変換 diff2Dates *= -1; } // 差のミリ秒を、日数・時間・分・秒に分割 var dDays = diff2Dates / ( 1000 * 60 * 60 * 24 ); // 日数 diff2Dates = diff2Dates % ( 1000 * 60 * 60 * 24 ); var dHour = diff2Dates / ( 1000 * 60 * 60 ); // 時間 diff2Dates = diff2Dates % ( 1000 * 60 * 60 ); var dMin = diff2Dates / ( 1000 * 60 ); // 分 diff2Dates = diff2Dates % ( 1000 * 60 ); var dSec = diff2Dates / 1000; // 秒 var msg2 = Math.floor(dDays) + "日" + Math.floor(dHour) + "時間" + Math.floor(dMin) + "分" + Math.floor(dSec) + "秒"; // 表示文字列の作成 var msg; if( dnumTarget > dnumNow ) { // まだ期限が来ていない場合 msg = msg1 + "までは、あと" + msg2 + "です。"; } else { // 期限が過ぎた場合 msg = msg1 + "は、既に" + msg2 + "前に過ぎました。"; } // 作成した文字列を表示 document.getElementById("RealtimeCountdownArea").innerHTML = msg; document.getElementById("deadline.id").value = targetDate; #最重要記述 } // 1秒ごとに実行 setInterval('showCountdown()',1000); </script>ここで、先程の
:id => deadline.id
が活きてくる。このような記述を追加することで初めて、
javascriptで処理された結果をvalue(値)として受け取り、それを画面上で表示させることができる。これでようやく、タイマーを実装することができる。タイマーだけを表示させたい場合
なお、日時の記入欄を表示させたくない場合も考えられる。下の写真のように期限とタイマーの残り時間だけを表示させたいという場合には、
app/views/missions/show.thml.erb<p>期限 <%= @mission.deadline %> <br> <input type="hidden" id="userYear" value = "<%= @mission.deadline.year %>" > <input type="hidden" id="userMonth"value = "<%= @mission.deadline.month %>" > <input type="hidden" id="userDate" value = "<%= @mission.deadline.day %>" > <input type="hidden" id="userHour" value = "<%= @mission.deadline.hour %>" > <input type="hidden" id="userMin" value = "<%= @mission.deadline.min %>" > <input type="hidden" id="userSec" value = "<%= @mission.deadline.sec %>" > </p> <p id="RealtimeCountdownArea" ></p> <script> function set2fig(num) { // 数値が1桁だったら2桁の文字列にして返す var ret; if( num < 10 ) { ret = "0" + num; } else { ret = num; } return ret; } function isNumOrZero(num) { // 数値でなかったら0にして返す if( isNaN(num) ) { return 0; } return num; } function showCountdown() { // 現在日時を数値(1970-01-01 00:00:00からのミリ秒)に変換 var nowDate = new Date(); var dnumNow = nowDate.getTime(); // 指定日時を数値(1970-01-01 00:00:00からのミリ秒)に変換 var inputYear = document.getElementById("userYear").value; var inputMonth = document.getElementById("userMonth").value - 1; var inputDate = document.getElementById("userDate").value; var inputHour = document.getElementById("userHour").value; var inputMin = document.getElementById("userMin").value; var inputSec = document.getElementById("userSec").value; var targetDate = new Date( isNumOrZero(inputYear), isNumOrZero(inputMonth), isNumOrZero(inputDate), isNumOrZero(inputHour), isNumOrZero(inputMin), isNumOrZero(inputSec) ); var dnumTarget = targetDate.getTime(); // 表示を準備 var dlYear = targetDate.getFullYear(); var dlMonth = targetDate.getMonth() + 1; var dlDate = targetDate.getDate(); var dlHour = targetDate.getHours(); var dlMin = targetDate.getMinutes(); var dlSec = targetDate.getSeconds(); var msg1 = "期限の" + dlYear + "/" + dlMonth + "/" + dlDate + " " + set2fig(dlHour) + ":" + set2fig(dlMin) + ":" + set2fig(dlSec); // 引き算して日数(ミリ秒)の差を計算 var diff2Dates = dnumTarget - dnumNow; if( dnumTarget < dnumNow ) { // 期限が過ぎた場合は -1 を掛けて正の値に変換 diff2Dates *= -1; } // 差のミリ秒を、日数・時間・分・秒に分割 var dDays = diff2Dates / ( 1000 * 60 * 60 * 24 ); // 日数 diff2Dates = diff2Dates % ( 1000 * 60 * 60 * 24 ); var dHour = diff2Dates / ( 1000 * 60 * 60 ); // 時間 diff2Dates = diff2Dates % ( 1000 * 60 * 60 ); var dMin = diff2Dates / ( 1000 * 60 ); // 分 diff2Dates = diff2Dates % ( 1000 * 60 ); var dSec = diff2Dates / 1000; // 秒 var msg2 = Math.floor(dDays) + "日" + Math.floor(dHour) + "時間" + Math.floor(dMin) + "分" + Math.floor(dSec) + "秒"; // 表示文字列の作成 var msg; if( dnumTarget > dnumNow ) { // まだ期限が来ていない場合 msg = "Mission終了まで、あと" + msg2 ; } else { // 期限が過ぎた場合 msg = msg1 + "は、既に" + msg2 + "前に過ぎました。"; } // 作成した文字列を表示 document.getElementById("RealtimeCountdownArea").innerHTML = msg; document.getElementById("deadline.id").value = targetDate; } // 1秒ごとに実行 setInterval('showCountdown()',1000); </script>上記のように
input type = "hidden"
とすれば、記入欄が画面上に表示されないものの、
<input type = ・・・>
の6つが削除されている訳ではないためこれで正常に起動する。まとめ
なかなか難易度が高かったが、達成感はすごかった。少しでも参考にしていただけたら幸いである。
参考記事
https://www.nishishi.com/javascript-tips/realtime-countdown-deadline.html
- 投稿日:2020-09-22T01:08:54+09:00
Rails6 カウントダウンタイマーの実装
はじめに
ポートフォリオで実装予定のカウントダウンタイマーを実装する。
タイマーを実装するにはjQueryが必要と書いてある記事もあるが、今回はjQueryを利用せずに実装を進めていくことにする。作成順序
具体的な手順としては
①制限時間のカラムであるdeadlineを作成する
②タイマーを画面上に表示させるため、HTMLの記述をする
③制限時間を表示させたいので、javascriptを使って記述を行うカラムの作成
まずは、いつものように
$rails generate migration Addカラム名Toテーブル名 カラム名:データ型でマイグレーションファイルを作成する。
今回の場合、投稿部分に制限時間を表示させたいのでdeadlineをカラム名として、
$rails generate migration AddDeadlineToMission deadline:datetimeとする。
db/migrate/20200903084112_create_missions.rbclass CreateMissions < ActiveRecord::Migration[6.0] def change create_table :missions do |t| t.integer :user_id t.text :content t.string :penalty t.datetime :deadline t.timestamps end end end上記のマイグレーションファイルの7行目に
t.datetime :deadline
を追加する。
そして、親の顔より見た$ rails db:migrateを実行して、データベースに保存する。
タイマーの実装
HTMLの記述
上記のような赤字のタイマーを設定するには、
app/views/missions/new.html.erb<p> <%= f.hidden_field :deadline, :id => "deadline.id" %> <input type="text" id="userYear" >年 <input type="text" id="userMonth">月 <input type="text" id="userDate" >日 <input type="text" id="userHour" >時 <input type="text" id="userMin" >分 <input type="text" id="userSec" >秒 </p> <p id="RealtimeCountdownArea" ></p> #ここにタイマーが表示されるとする。
ここで、:id => deadline.id
は後のjavascriptの記述において効果を発揮するため、記述している。javascriptの記述
今回は、
app/views/missions/new.html.erb
のscriptタグにjavascriptを記述することにする。以下の通りである。
app/views/missions/new.html.erb<script> function set2fig(num) { // 数値が1桁だったら2桁の文字列にして返す var ret; if( num < 10 ) { ret = "0" + num; } else { ret = num; } return ret; } function isNumOrZero(num) { // 数値でなかったら0にして返す if( isNaN(num) ) { return 0; } return num; } function showCountdown() { // 現在日時を数値(1970-01-01 00:00:00からのミリ秒)に変換 var nowDate = new Date(); var dnumNow = nowDate.getTime(); // 指定日時を数値(1970-01-01 00:00:00からのミリ秒)に変換 var inputYear = document.getElementById("userYear").value; var inputMonth = document.getElementById("userMonth").value - 1; var inputDate = document.getElementById("userDate").value; var inputHour = document.getElementById("userHour").value; var inputMin = document.getElementById("userMin").value; var inputSec = document.getElementById("userSec").value; var targetDate = new Date( isNumOrZero(inputYear), isNumOrZero(inputMonth), isNumOrZero(inputDate), isNumOrZero(inputHour), isNumOrZero(inputMin), isNumOrZero(inputSec) ); var dnumTarget = targetDate.getTime(); // 表示を準備 var dlYear = targetDate.getFullYear(); var dlMonth = targetDate.getMonth() + 1; var dlDate = targetDate.getDate(); var dlHour = targetDate.getHours(); var dlMin = targetDate.getMinutes(); var dlSec = targetDate.getSeconds(); var msg1 = "期限の" + dlYear + "/" + dlMonth + "/" + dlDate + " " + set2fig(dlHour) + ":" + set2fig(dlMin) + ":" + set2fig(dlSec); // 引き算して日数(ミリ秒)の差を計算 var diff2Dates = dnumTarget - dnumNow; if( dnumTarget < dnumNow ) { // 期限が過ぎた場合は -1 を掛けて正の値に変換 diff2Dates *= -1; } // 差のミリ秒を、日数・時間・分・秒に分割 var dDays = diff2Dates / ( 1000 * 60 * 60 * 24 ); // 日数 diff2Dates = diff2Dates % ( 1000 * 60 * 60 * 24 ); var dHour = diff2Dates / ( 1000 * 60 * 60 ); // 時間 diff2Dates = diff2Dates % ( 1000 * 60 * 60 ); var dMin = diff2Dates / ( 1000 * 60 ); // 分 diff2Dates = diff2Dates % ( 1000 * 60 ); var dSec = diff2Dates / 1000; // 秒 var msg2 = Math.floor(dDays) + "日" + Math.floor(dHour) + "時間" + Math.floor(dMin) + "分" + Math.floor(dSec) + "秒"; // 表示文字列の作成 var msg; if( dnumTarget > dnumNow ) { // まだ期限が来ていない場合 msg = msg1 + "までは、あと" + msg2 + "です。"; } else { // 期限が過ぎた場合 msg = msg1 + "は、既に" + msg2 + "前に過ぎました。"; } // 作成した文字列を表示 document.getElementById("RealtimeCountdownArea").innerHTML = msg; document.getElementById("deadline.id").value = targetDate; #最重要記述 } // 1秒ごとに実行 setInterval('showCountdown()',1000); </script>ここで、先程の
:id => deadline.id
が活きてくる。このような記述を追加することで初めて、
javascriptで処理された結果をvalue(値)として受け取り、それを画面上で表示させることができる。これでようやく、タイマーを実装することができる。タイマーだけを表示させたい場合
なお、日時の記入欄を表示させたくない場合も考えられる。下の写真のように期限とタイマーの残り時間だけを表示させたいという場合には、
app/views/missions/show.thml.erb<p>期限 <%= @mission.deadline %> <br> <input type="hidden" id="userYear" value = "<%= @mission.deadline.year %>" > <input type="hidden" id="userMonth"value = "<%= @mission.deadline.month %>" > <input type="hidden" id="userDate" value = "<%= @mission.deadline.day %>" > <input type="hidden" id="userHour" value = "<%= @mission.deadline.hour %>" > <input type="hidden" id="userMin" value = "<%= @mission.deadline.min %>" > <input type="hidden" id="userSec" value = "<%= @mission.deadline.sec %>" > </p> <p id="RealtimeCountdownArea" ></p> <script> function set2fig(num) { // 数値が1桁だったら2桁の文字列にして返す var ret; if( num < 10 ) { ret = "0" + num; } else { ret = num; } return ret; } function isNumOrZero(num) { // 数値でなかったら0にして返す if( isNaN(num) ) { return 0; } return num; } function showCountdown() { // 現在日時を数値(1970-01-01 00:00:00からのミリ秒)に変換 var nowDate = new Date(); var dnumNow = nowDate.getTime(); // 指定日時を数値(1970-01-01 00:00:00からのミリ秒)に変換 var inputYear = document.getElementById("userYear").value; var inputMonth = document.getElementById("userMonth").value - 1; var inputDate = document.getElementById("userDate").value; var inputHour = document.getElementById("userHour").value; var inputMin = document.getElementById("userMin").value; var inputSec = document.getElementById("userSec").value; var targetDate = new Date( isNumOrZero(inputYear), isNumOrZero(inputMonth), isNumOrZero(inputDate), isNumOrZero(inputHour), isNumOrZero(inputMin), isNumOrZero(inputSec) ); var dnumTarget = targetDate.getTime(); // 表示を準備 var dlYear = targetDate.getFullYear(); var dlMonth = targetDate.getMonth() + 1; var dlDate = targetDate.getDate(); var dlHour = targetDate.getHours(); var dlMin = targetDate.getMinutes(); var dlSec = targetDate.getSeconds(); var msg1 = "期限の" + dlYear + "/" + dlMonth + "/" + dlDate + " " + set2fig(dlHour) + ":" + set2fig(dlMin) + ":" + set2fig(dlSec); // 引き算して日数(ミリ秒)の差を計算 var diff2Dates = dnumTarget - dnumNow; if( dnumTarget < dnumNow ) { // 期限が過ぎた場合は -1 を掛けて正の値に変換 diff2Dates *= -1; } // 差のミリ秒を、日数・時間・分・秒に分割 var dDays = diff2Dates / ( 1000 * 60 * 60 * 24 ); // 日数 diff2Dates = diff2Dates % ( 1000 * 60 * 60 * 24 ); var dHour = diff2Dates / ( 1000 * 60 * 60 ); // 時間 diff2Dates = diff2Dates % ( 1000 * 60 * 60 ); var dMin = diff2Dates / ( 1000 * 60 ); // 分 diff2Dates = diff2Dates % ( 1000 * 60 ); var dSec = diff2Dates / 1000; // 秒 var msg2 = Math.floor(dDays) + "日" + Math.floor(dHour) + "時間" + Math.floor(dMin) + "分" + Math.floor(dSec) + "秒"; // 表示文字列の作成 var msg; if( dnumTarget > dnumNow ) { // まだ期限が来ていない場合 msg = "Mission終了まで、あと" + msg2 ; } else { // 期限が過ぎた場合 msg = msg1 + "は、既に" + msg2 + "前に過ぎました。"; } // 作成した文字列を表示 document.getElementById("RealtimeCountdownArea").innerHTML = msg; document.getElementById("deadline.id").value = targetDate; } // 1秒ごとに実行 setInterval('showCountdown()',1000); </script>上記のように
input type = "hidden"
とすれば、記入欄が画面上に表示されないものの、<input type = ・・・>
の6つが削除されている訳ではないためこれで正常に起動する。また、
<input type="hidden" id="userYear" value = "<%= @mission.deadline.year %>" >
の
value="<%= @mission.deadline.year %>"
の部分がなぜそのような記述となるかについて説明する。これは、deadlineはdatetimeというデータ型をとるカラムであるのだが、datetimeには年・月・日・時・分・秒といった日時の情報が保存されているからである。したがって、上記のような記述でnew.html.erbの部分で記述した期限の日時が取り出せることになる。まとめ
なかなか難易度が高かったが、達成感はすごかった。少しでも参考にしていただけたら幸いである。
参考記事
https://www.nishishi.com/javascript-tips/realtime-countdown-deadline.html
- 投稿日:2020-09-22T00:46:08+09:00
webpackでReact+Typescriptの環境構築をする
Qiita初投稿です。これからどんどん書いていこうとおもうのでご意見・ご要望ありましたら気軽にコメントしていただけると、自身の成長にもつながるのでよろしくお願いします!
webpackとは?
一言で表すとJavascriptを1つのファイルにまとめる事ができるツールです。
他にも様々な機能がありますが、ここでは割愛します。1つのファイルにまとめるメリット
1つにまとめることでブラウザとサーバーの通信回数を減り、通信速度が速くなります。
実際にやってみる
実際にwebpackを使って、React+Typescriptの環境構築をしてみましょう。
ディレクトリの作成
まず、ターミナルで以下のコマンドでディレクトリを作り、VSCodeで開きます(ファイル名の部分は好きな名前で大丈夫です)。codeコマンドがない場合は普通にファイル作って、VSCodeで開けば大丈夫です。
ターミナルmkdir [ファイル名] code [ファイル名]何もファイルがない状態だと思います。
必要なパッケージのインストール
次にVSCodeのターミナルで以下のコマンドを打つと、
package.json
が作成されます。VSCodeのターミナル//npmを使う場合 npm init -y //yarnを使う場合 yarn init -yその後必要なパッケージをダウンロードしていきます。
VSCodeのターミナル//npm npm install --save react react-dom npm install --save-dev @types/react-dom @types/webpack @types/webpack-dev-server ts-loader ts-node typescript webpack webpack-cli webpack-dev-server //yarn yarn add react react-dom yarn add -D @types/react-dom @types/webpack @types/webpack-dev-server ts-loader ts-node typescript webpack webpack-cli webpack-dev-serverインストールしたパッケージの簡単な説明を表にまとめました。
react,react-dom Reactを書くのに必要 @types/~ @typesに続くパッケージの型宣言ファイルが含まれている ts-loader TypescriptをJavascriptにコンパイルするのに使う ts-node Typescriptのファイルを直接実行できる typescript Typescriptを書くのに必要 webpack Javascriptのファイルを1つにまとめる webpack-cli webpackコマンドを使うのに必要 webpack-dev-server ファイルを変えた時に差分ビルドをしてくれる webpackの設定
次に以下のコマンドを実行して、webpackの設定ファイルである、
webpack.config.ts
を作成します。VSCodeのターミナルtouch webpack.config.ts作成された
webpack.config.ts
に以下のように記述します。webpack.config.js
でも大丈夫です。その場合はConfigurationの部分を消します。設定ファイルは勉強も兼ねて、デフォルトと同じになっているところも書いているため、少し冗長な部分があります。
webpack.config.tsimport path from 'path'; import { Configuration } from 'webpack'; const config: Configuration = { context: path.join(__dirname, 'src'), entry: './index.tsx', output: { path: path.join(__dirname, 'dist'), filename: 'bundle.js', publicPath: '/assets', }, module: { rules: [ { test: /\.tsx?$/, use: 'ts-loader', }, ], }, mode: "development", resolve: { extensions: ['.ts', '.tsx', '.js', '.jsx'], }, devtool: "inline-source-map", devServer: { contentBase: path.join(__dirname, 'static'), open: true, port: 3000, }, }; export default config;webpack.config.ts(js)の設定ファイルの簡単な説明を以下の表にまとめました。
path ファイルやディレクトリのpathを操作できる。デフォルトでnode_modulesに入っている Configuration configのtype __dirname カレントディレクトリを示す output ファイルの出力設定(path:出力ファイルのディレクトリ名、filename:出力ファイル名、publicPath:バンドルファイルをアップロードする場所) module rules(test:コンパイルするファイル、use:コンパイルに使うツール) mode 開発(development)か本番(production)か resolve extensions:importで省略したい拡張子 devtool デバッグ用のツール(mode:develop) devServer 開発用のサーバー(contentBase:サーバーの起点とするディレクトリ、open:ブラウザを自動で起動するか、port:ポート番号) Typescriptの設定
次に以下のコマンドを実行して、Typescriptの設定ファイルである、
tsconfig.json
を作成します。VSCodeのターミナルtouch tsconfig.json作成された
tsconfig.json
に以下のように記述します。tsconfig.json"compilerOptions": { "sourceMap": true, "baseUrl": "./", "target": "es5", "strict": true, "module": "commonJs", "jsx": "react", "lib": ["ES5", "ES6", "DOM"], "allowSyntheticDefaultImports": true, "esModuleInterop": true, "isolatedModules": true, } }tsconfig.jsonの設定の簡単な説明を以下の表にまとめました。
sourceMap ソースマップを見れるようにするか baseUrl tsconfig.jsonの場所 target どのバージョンでJavascriptを出力するか strict 型付けのルールを厳しくする module Typescriptのモジュールをどのバージョンで出力するか jsx jsxの書式を有効化 lib コンパイルに使用する組み込みライブラリ allowSyntheticDefaultImports default importを使うか esModuleInterop import * 以外も使えるようにするか isolatedModules exportを必須にするか 次に以下のコマンドでsrcディレクトリを作り、その中に
index.tsx
を作成します。VSCodeのターミナルmkdir src && touch src/index.tsx作成した
src/index.tsx
に以下のように記述します。import React from 'react'; import ReactDOM from 'react-dom'; ReactDOM.render(<h1>Hello World!</h1>, document.getElementById('app'));次に以下のコマンドでstaticディレクトリを作成し、その中に
index.html
を作成します。VSCodeのターミナルmkdir static && static/index.html作成した
static/index.html
に以下のように記述します。<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> </head> <body> <div id="app"></div> <script src="/assets/bundle.js"></script> </body> </html>実行
これで最低限の環境は整いました。
VSCodeのターミナルで以下のコマンドを実行します。VSCodeのターミナル//npm npx webpack-dev-server //yarn yarn webpack-dev-serverすると
localhost:3000
がブラウザで開かれて、Hello World!が表示されると思います。ここまで読んでいただきありがとうございます。少しでもwebpackについてイメージできたら嬉しいです。
今後も記事を書いていこうと思うので感想などいただけるとモチベーションにつながります。参考記事