- 投稿日:2020-07-20T23:56:21+09:00
Node でお手軽スクレイピング 2020 年夏
皆さんは Web ページのスクレイピングって書いた事ありますか?私はあります。だってどんなに平和で平穏な生活を送っていても数年に一度はスクレイピングってしたくなりますよね。「うわーまじか!API ないのかよ…。」的な。
そうしたら HTTP クライアントと HTML パーサのライブラリを探してきてインストールした上でごりごり書くことになると思います。でも実際に書いてみると、そうやってライブラリのインストールをしたりサンプルコードで動作確認している時間よりも、HTML を解析して実際にパースしたところから対象の要素を取得して欲しい値を取り出す試行錯誤の時間の方が長かったっていう事はないですか?
今日ご紹介する Node でお手軽スクレイピングは、その辺の試行錯誤の手間を極力減らすことが出来る方法です。2020 年夏の最新版です。
まずは環境から。特に古いものを使う理由もないので 2020-07-20 時点の最新版
14.5.0
を使っています。$ node -v v14.5.0そしてプロジェクトの初期化を行って、2 つほどライブラリをインストールします。
$ npm init $ npm install node-fetch jsdom --save-devnode-fetch は Node 上でウェブブラウザと同じような
fetch
を使えるようにするライブラリです。普段 Web ベースの JS を書いてると、HTTP アクセスするにもfetch
が直感的で楽だなーと思うので選びました。GitHub 上のスターは 5.3k。素晴らしいですね。jsdom はウェブブラウザと同様の API セットを持った HTML DOM ツリーをメモリ上に構築することが出来るライブラリです。Pure JavaScript で実装されたウェブブラウザのサブセットと思うと理解しやすいかも知れません。GitHub 上のスターは 14.4k。今回の記事の要です。
必要なライブラリが揃ったところで早速スクリプトを書いていきましょう。サンプルに気象庁の東京都の週間天気予報のページを選びました。
index.mjs#!/usr/bin/env node import fetch from 'node-fetch'; import jsdom from 'jsdom'; const { JSDOM } = jsdom; (async () => { const res = await fetch('https://www.jma.go.jp/jp/week/319.html'); const html = await res.text(); const dom = new JSDOM(html); const document = dom.window.document; const nodes = document.querySelectorAll('#infotablefont tr:nth-child(4) td'); const tokyoWeathers = Array.from(nodes).map(td => td.textContent.trim()); console.log(tokyoWeathers); })();これだけ見て「あー、なるほど!」ってならない方のために詳細な解説は後ほど加えていきますが、まず一番のポイントは
const nodes
から始まる行以降です。お気づきでしょうか?この行以降はそのままウェブブラウザ上でも実行可能なことに。従来のスクレイピングでは、必要な DOM 要素を取得するためのクエリを探したり、得られたノードを加工して必要なリストに変換する試行錯誤に時間がかかっていました。その試行錯誤自体を無くすことは不可能ですが、ウェブブラウザ上のデベロッパーツールであれば、リアルタイムに結果を見ながら試行錯誤することでその手間を大幅に減らすことが出来ます。
そしてデベロッパーツール上で欲しい結果が得られるようになったら、そのコードをスクリプトファイルに貼り付ければそれだけでもうスクレイピングの完成です。このスクリプトを実行すると以下のような結果が得られます。
$ ./index.mjs [ '曇', '曇一時雨', '曇一時雨', '曇', '曇', '曇時々晴', '曇時々晴' ]従来に比べると革命的に楽に書ける事がお分かりいただけたのではないでしょうか。
さて、では約束通り詳細な解説を加えていきましょう。
#!/usr/bin/env node
今回、コマンドラインから直接スクリプトを実行しようかなと思ったので追加しています。
node
コマンドにファイルを渡して実行するのであれば不要です。import fetch from 'node-fetch'; import jsdom from 'jsdom'; const { JSDOM } = jsdom;
import
記法が使えるようになったのは嬉しいのですが、v14 のデフォルトではファイルの拡張子を.mjs
にしておく必要があるので注意して下さい。またjsdom
に関しては直接import { JSDOM } from 'jsdom'
と書きたくなるところですが、現状ではjsdom
が ES2015 Modules 構文をサポートしていないため、こういったまどろっこしい書き方になります。(async () => { // ... })();非同期処理があるので
await
を使いたいのですが、await
自体も非同期関数の中じゃないと使えないので、非同期の無名関数を作って即時実行しています。const res = await fetch('https://www.jma.go.jp/jp/week/319.html'); const html = await res.text();Web プログラミングで見慣れた書き方ですね。非同期に
fetch
した結果から、HTML を文字列として取得しています。XHR
を使っていた期間が長かったので私もうっかり間違えがちですが、XHR
のresponseText
と違って、fetch
で得られるレスポンスのtext
メソッドは非同期なのでそこにも注意が必要です。const dom = new JSDOM(html); const document = dom.window.document;さあ本記事の最大の見せ場です。
JSDOM
コンストラクタに HTML を文字列で渡すと、それをパースして DOM ツリーにしてくれます。そこには Web プログラミングでおなじみ、window
オブジェクトがあり、その中にdocument
オブジェクトがあります。const nodes = document.querySelectorAll('#infotablefont tr:nth-child(4) td'); const tokyoWeathers = Array.from(nodes).map(td => td.textContent.trim()); console.log(tokyoWeathers);この部分は、デベロッパーツール上で動作確認したものを貼り付けると言っていた部分です。ウェブブラウザ上でだと
:nth-child(4)
に相当する部分を楽に探せるのがいいですね。そこで得られたNodeList
オブジェクトをArray.from
でArray
に変換するというのは、今どきなテクニックかもしれません。以上でスクリプトの解説は終わりです。
最後に、忘れてはならないのはスクレイピングは最終手段であるという事です。API が提供されているサービスであれば必ずそちらを使うべきですし、やむを得ずスクレイピングする際はサーバに過度な負荷を与えることの無いよう気をつけましょう。
- 投稿日:2020-07-20T23:56:21+09:00
Node.js でお手軽スクレイピング 2020 年夏
皆さんは Web ページのスクレイピングって書いた事ありますか?私はあります。だってどんなに平和で平穏な生活を送っていても数年に一度はスクレイピングってしたくなりますよね。「うわーまじか!API ないのかよ…。」的な。
そうしたら HTTP クライアントと HTML パーサのライブラリを探してきてインストールした上でごりごり書くことになると思います。でも実際に書いてみると、そうやってライブラリのインストールをしたりサンプルコードで動作確認している時間よりも、HTML を解析して実際にパースしたところから対象の要素を取得して欲しい値を取り出す試行錯誤の時間の方が長かったっていう事はないですか?
今日ご紹介する Node.js でお手軽スクレイピングは、その辺の試行錯誤の手間を極力減らすことが出来る方法です。2020 年夏の最新版です。
まずは環境から。特に古いものを使う理由もないので 2020-07-20 時点の最新版
14.5.0
を使っています。$ node -v v14.5.0そしてプロジェクトの初期化を行って、2 つほどライブラリをインストールします。
$ npm init $ npm install node-fetch jsdom --save-devnode-fetch は Node.js 上でウェブブラウザと同じような
fetch
を使えるようにするライブラリです。普段 Web ベースの JS を書いてると、HTTP アクセスするにもfetch
が直感的で楽だなーと思うので選びました。GitHub 上のスターは 5.3k。素晴らしいですね。jsdom はウェブブラウザと同様の API セットを持った HTML DOM ツリーをメモリ上に構築することが出来るライブラリです。Pure JavaScript で実装されたウェブブラウザのサブセットと思うと理解しやすいかも知れません。GitHub 上のスターは 14.4k。今回の記事の要です。
必要なライブラリが揃ったところで早速スクリプトを書いていきましょう。サンプルに気象庁の東京都の週間天気予報のページを選びました。
index.mjs#!/usr/bin/env node import fetch from 'node-fetch'; import jsdom from 'jsdom'; const { JSDOM } = jsdom; (async () => { const res = await fetch('https://www.jma.go.jp/jp/week/319.html'); const html = await res.text(); const dom = new JSDOM(html); const document = dom.window.document; const nodes = document.querySelectorAll('#infotablefont tr:nth-child(4) td'); const tokyoWeathers = Array.from(nodes).map(td => td.textContent.trim()); console.log(tokyoWeathers); })();これだけ見て「あー、なるほど!」ってならない方のために詳細な解説は後ほど加えていきますが、まず一番のポイントは
const nodes
から始まる行以降です。お気づきでしょうか?この行以降はそのままウェブブラウザ上でも実行可能なことに。従来のスクレイピングでは、必要な DOM 要素を取得するためのクエリを探したり、得られたノードを加工して必要なリストに変換する試行錯誤に時間がかかっていました。その試行錯誤自体を無くすことは不可能ですが、ウェブブラウザ上のデベロッパーツールであれば、リアルタイムに結果を見ながら試行錯誤することでその手間を大幅に減らすことが出来ます。
そしてデベロッパーツール上で欲しい結果が得られるようになったら、そのコードをスクリプトファイルに貼り付ければそれだけでもうスクレイピングの完成です。このスクリプトを実行すると以下のような結果が得られます。
$ ./index.mjs [ '曇', '曇一時雨', '曇一時雨', '曇', '曇', '曇時々晴', '曇時々晴' ]従来に比べると革命的に楽に書ける事がお分かりいただけたのではないでしょうか。
さて、では約束通り詳細な解説を加えていきましょう。
#!/usr/bin/env node
今回、コマンドラインから直接スクリプトを実行しようかなと思ったので追加しています。
node
コマンドにファイルを渡して実行するのであれば不要です。import fetch from 'node-fetch'; import jsdom from 'jsdom'; const { JSDOM } = jsdom;
import
記法が使えるようになったのは嬉しいのですが、v14 のデフォルトではファイルの拡張子を.mjs
にしておく必要があるので注意して下さい。またjsdom
に関しては直接import { JSDOM } from 'jsdom'
と書きたくなるところですが、現状ではjsdom
が ES2015 Modules 構文をサポートしていないため、こういったまどろっこしい書き方になります。(async () => { // ... })();非同期処理があるので
await
を使いたいのですが、await
自体も非同期関数の中じゃないと使えないので、非同期の無名関数を作って即時実行しています。const res = await fetch('https://www.jma.go.jp/jp/week/319.html'); const html = await res.text();Web プログラミングで見慣れた書き方ですね。非同期に
fetch
した結果から、HTML を文字列として取得しています。XHR
を使っていた期間が長かったので私もうっかり間違えがちですが、XHR
のresponseText
と違って、fetch
で得られるレスポンスのtext
メソッドは非同期なのでそこにも注意が必要です。const dom = new JSDOM(html); const document = dom.window.document;さあ本記事の最大の見せ場です。
JSDOM
コンストラクタに HTML を文字列で渡すと、それをパースして DOM ツリーにしてくれます。そこには Web プログラミングでおなじみ、window
オブジェクトがあり、その中にdocument
オブジェクトがあります。const nodes = document.querySelectorAll('#infotablefont tr:nth-child(4) td'); const tokyoWeathers = Array.from(nodes).map(td => td.textContent.trim()); console.log(tokyoWeathers);この部分は、デベロッパーツール上で動作確認したものを貼り付けると言っていた部分です。ウェブブラウザ上でだと
:nth-child(4)
に相当する部分を楽に探せるのがいいですね。そこで得られたNodeList
オブジェクトをArray.from
でArray
に変換するというのは、今どきなテクニックかもしれません。以上でスクリプトの解説は終わりです。
最後に、忘れてはならないのはスクレイピングは最終手段であるという事です。API が提供されているサービスであれば必ずそちらを使うべきですし、やむを得ずスクレイピングする際はサーバに過度な負荷を与えることの無いよう気をつけましょう。
- 投稿日:2020-07-20T22:04:29+09:00
アプリ作成 コメント機能非同期化
非同期化記述解説
FormData
フォームのデータの送信に使用することができます。その他にも、キーのついたデータを伝送するためにフォームとは独立して使用することもできます。今回はコメントフォームがあるので、そのフォームの情報を取得するのに使います。
attrメソッド
要素が持つ指定属性の値を返します。
要素が指定属性を持っていない場合、関数はundefinedを返します。processDataオプション
デフォルトではtrueになっており、dataに指定したオブジェクトをクエリ文字列(例: msg.txt?b1=%E3%81%8B&b2=%E3%81%8D )に変換する役割があります。
クエリ文字列とは、WebブラウザなどがWebサーバに送信するデータをURLの末尾に特定の形式で表記したものの事です。contentTypeオプション
サーバにデータのファイル形式を伝えるヘッダです。こちらはデフォルトでは「text/xml」でコンテンツタイプをXMLとして返してきます。
ajaxのリクエストがFormDataのときはどちらの値も適切な状態で送ることが可能なため、falseにすることで設定が上書きされることを防ぎます。
この後、インクリメンタルサーチ機能をつけたいと思う。
- 投稿日:2020-07-20T21:10:31+09:00
簡単レシート印刷 receiptline でバーコードと QR コードを作ってみた
日本発のオープンソース receiptline でレシート印刷に少しずつトライしています。
ネットオークションやフリマアプリでレシートプリンターを探していましたが・・・
とうとう何台か落札することができました!まだ手元にないので、前回利用した開発ツールを引き続き使います。
今回はバーコードと QR コードです。バーコード
バーコードアイコンをクリックすると、ダイアログボックスが開きます。
適当にデータを入れて、サイズと可読文字の有無を選びましょう。
ここは無難にデフォルト設定の CODE128 にしておきます。
CODE128 は「〇〇 Pay」のバーコードに使われているそうです。キャンセルしたいときは、ダイアログボックスの外をクリックします。
編集エリアに code プロパティと option プロパティが追加されました。
code プロパティは、バーコードを出力します。
名前はcode
またはc
、値はバーコードデータです。option プロパティは、この行以降のバーコードを設定します。
名前はoption
またはo
、値はバーコードオプションです。
複数のオプション値は,
or 1 つ以上のスペースで区切ります。ReceiptLine{code:WIND402; option:code128,2,72,hri}QR コード
二次元コードアイコンをクリックすると、ダイアログボックスが開きます。
二次元コードは QR コードのみです。
適当にデータを入れて、サイズと誤り訂正レベルを選択します。キャンセルしたいときは、ダイアログボックスの外をクリックします。
編集エリアに code プロパティと option プロパティが挿入されました。
二次元コードのプロパティはバーコードと同じです。
option プロパティの値は QR コード用になっていますね。ReceiptLine{code:Do it! Make it! Shake it!; option:qrcode,5,M}バーコードの位置揃え
文字列と同様に、テーブルの区切り文字
|
で位置揃えができます。
バーコードと QR コードを、左揃えと右揃えにしてみます。code プロパティと option プロパティを分離して、短縮名を使います。
また、クワイエットゾーンが必要なので、間隔を1行ずつ空けることにします。ReceiptLine{o:code128,2,72,hri} |{c:WIND402} {c:WIND402}| = {o:qrcode,5,M} |{c:Do it! Make it! Shake it!} {c:Do it! Make it! Shake it!}|ちなみに、バーコードと QR コードは、2 列にしたり罫線を引いたりすることができません。
この制約はレシートプリンターのコマンド仕様に由来するようです。バーコードの種類
code プロパティと option プロパティの対応をまとめました。
種類 option code 使い道 CODE128 code128 ASCII 文字列 コード決済 CODE93 code93 ASCII 文字列 ??? NW-7
(Codabar)nw7
codabar数字と一部の記号
(先頭と末尾は ABCD)宅配伝票 ITF
(Interleaved 2 of 5)itf 偶数桁の数字 段ボール箱 CODE39 code39 英数字と一部の記号 現品票 JAN
(EAN)jan
ean13 桁の数字
8 桁の数字商品 UPC-A
UPC-Eupc 12 桁の数字
7 桁の数字北米の商品 QR Code qrcode ASCII 文字列
漢字続きは Web で 次回は、変換ライブラリの API を試してみようと思います。
- 投稿日:2020-07-20T20:31:09+09:00
AWS SDK のリクエストヘッダを見る方法
- AWS SDK for PHP の場合
例えばsqsクライアントの場合$client = new SqsClient([ 'region' => ***, 'version' => ***, 'credentials' => [ 'key' => ***, 'secret' => *** ], 'debug' => true, # ←コレを追加 ]);debugを
true
にすることで標準出力にログが出力される。
参考: https://docs.aws.amazon.com/ja_jp/sdk-for-php/v3/developer-guide/faq.html
- AWS SDK for JavaScript の場合
例えばsesクライアントの場合new AWS.SES().sendEmail(sesParams, function(err, data){ if(error){ console.log(this.httpResponse); // ← コレを追加 } else { console.log(this.httpResponse); } });
this
というのはAWS.Response
のことらしい。基本的にはレスポンスの情報を参照出来るらしいけど、リクエストした情報(AWS.Request
)も見れる。
参考: https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/using-a-callback-function.html
- 投稿日:2020-07-20T20:23:13+09:00
javascriptを使って簡単な計算機を作るpart4 入門者向け
計算機を作る
きっかけ
実際にあるような計算機を作りたくなった。
今回作る機能
・四則演算機能の追加
完成物
See the Pen oNbaVrZ by ライム (@raimumk2) on CodePen.
サンプルコード
HTML
caluculate.html<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link rel="stylesheet" href="css/caluculate.css"> <title>計算機</title> </head> <body> <div class="caluculate"> <div class="wrapper"> <input id="number-text" type="text"> </div> <div id="btns"> <div id="num-btns"> <button value="7" onclick="clickBtn(this)">7</button> <button value="8" onclick="clickBtn(this)">8</button> <button value="9" onclick="clickBtn(this)">9</button> <button value="4" onclick="clickBtn(this)">4</button> <button value="5" onclick="clickBtn(this)">5</button> <button value="6" onclick="clickBtn(this)">6</button> <button value="1" onclick="clickBtn(this)">1</button> <button value="2" onclick="clickBtn(this)">2</button> <button value="3" onclick="clickBtn(this)">3</button> <button value="0" onclick="clickBtn(this)">0</button> <button name="symbol" onclick="calc()">=</button> </div> <div id="symbol-btns"> <button value="+" name="symbol" onclick="clickBtn(this)">+</button> <button value="-" name="symbol" onclick="clickBtn(this)">-</button> <button value="*" name="symbol" onclick="clickBtn(this)">*</button> <button value="/" name="symbol" onclick="clickBtn(this)">/</button> </div> </div> </div> <script src="js/caluculate.js"></script> </body> </html>
CSS
caluculate.css* { margin: 0; padding: 0; } .caluculate { margin: 100px auto; } .wrapper { width: 300px; margin: 0 auto; } .wrapper > #number-text { width: 293px; height: 54px; margin-bottom: 5px; font-size: 48px; /* 右から左へ入力するためのスタイル */ text-align: right; } #btns { width: 300px; display: flex; margin: auto; } button { width: 65px; height: 57px; } #num-btns { margin: 5px; } #num-btns > button { margin-bottom: 5px; font-size: 24px; } #num-btns > button:last-child { width: 136px; } #symbol-btns { height: 228px; display: flex; flex-direction: column; display: inline-block; margin-top: 5px; } #symbol-btns > button { margin-bottom: 5px; height: 57px; font-size: 24px; text-align: center; }
Javascript
caluculate.jsvar number = document.getElementById('number-text'); function clickBtn(num) { number.value = number.value + num.value; }; function update(num) { number.value = num; } function calc() { var answer = new Function('return ' + number.value); update(answer().toString() ) }今回発生したエラーについて
イコールを押すと計算が行われ、計算結果をテキストボックスに返すというプログラムを、参考サイトを見ながら作っていても全然機能しないというのがしばらく続いた。
そのときに出た、エラーメッセージがこちら
最初は、returnの使い方を間違えているのかなと思い、returnについて調べ続けていても中々解決策が見つからず
最終的には、参考コードと自分のコードと何が違うかを見比べると
一つだけ違っていた箇所がありました。caluculate.js//変数の違いは無視してください //自分のコード new Function('return' + number.value); //参考元のコード new Function( 'return ' + v )returnのあとの空白があるかないかだったのです。
それで、自分のコードにもreturnの後に空白を足してみたところ
やっと、「1+1=2」ができるようになりました。原因を探ろうにも、ネット上には載ってなさそうだったので、根本的な解決にはまだ至ってはいないのですが、
今まで参考にしてきた計算機の作り方のサイトをもう一度見て回ると、いずれもreturnの後に空白はありました。私の勝手な推測にはなるのですが、
returnの使い方の説明をされているサイトでは、
return(空白)値や式などを記述
とあるので、エラーが出ていたときの処理では
return値や式などを記述
という風にやろうとしていた。とかかなぁと。ひとまず上記のサンプルコードで自分の思い通りには動いてくれるようになりました。
参考サイト
計算機の作り方など
JavaScript を使った「電卓Webアプリ」の作り方を中3の息子に教えてみた!(プログラミング初心者向け)JavaScriptで電卓プログラムを作成する方法を現役エンジニアが解説【初心者向け】
js関連
【JavaScript入門】returnの使い方と戻り値・falseのまとめ!【JavaScript入門】function(関数)の使い方、呼び出し・戻り値など総まとめ!
MDN:Number.prototype.toString()
エラー関連
MDN:ReferenceError: "x" is not definedJavaScriptで「’変数名/関数名’ is not defined」というエラーが出る原因と対処法を現役エンジニアが解説【初心者向け】
今後の構想
・クリアボタンと小数点ボタンの追加
(ボタン配置に苦戦すると思われる)・クリアボタンでテキストボックスの値をリセットする機能
・入力できる文字列や文字数などの制限
・記号入力は、ボタン切り替え機能を使って計算したい。要は、iphoneの電卓を想定
テキストボックスには記号は入力せず、内部的に入力する。
記号に文字数を使うと、長い桁になったときに足りなくなるし、見辛そうなので。
(テキストボックス表示例:「1」→(+ボタンを押す)→「1」→(=ボタンを押す)→「2」)・計算結果をリストに追加していく機能
(複数の計算があったとき用のメモ代わり)・計算結果リストの編集ができる機能
(作れそうなら作ってみる)ボタン切り替え機能と計算結果をリストに追加していく機能は、以前作ることができたので、今回のコードにも組み込んでいく。
最後に
今は、先駆者様のコードを真似しているに過ぎませんが、
プログラミングにおいては「1つだけ正解というわけではない」とよく聞くので、自分の力でコードを書けるようになりたいのと、いかにエラーと向き合えるかが今後の課題になりそうです。
- 投稿日:2020-07-20T20:05:35+09:00
【JavaScript】電卓のプログラムを作成してみた。
はじめに
まだまだ未熟なコードだとは思いますが、
jsの勉強で電卓のプログラムを作成したので記録用に記します。イメージキャプチャ
コード
html<body> <p id="errormessage"></p> <div class="calculator"> <table> <tr > <td colspan="4"> <div class="head"> <input id ="result" placeholder="0"/> </div> </td> </tr> <tr> <td colspan="2"> <div class="col2"> <button class="btn" onclick="update( '' ) ">clear</button> </div> </td> <td></td> <td> <div> <button onclick="btn('+')">+</button> </div> </td> </tr> <tr> <td> <div> <button onclick="btn('1')">1</button> </div> </td> <td> <div> <button onclick="btn('2')">2</button> </div> </td> <td> <div class="mr"> <button onclick="btn('3')">3</button> </div> </td> <td> <div> <button onclick="btn('-')">-</button> </div> </td> </tr> <tr> <td> <div> <button onclick="btn('4')">4</button> </div> </td> <td> <div> <button onclick="btn('5')">5</button> </div> </td> <td> <div> <button onclick="btn('6')">6</button> </div> </td> <td> <div> <button onclick="calc2('*')">×</button> </div> </td> </tr> <tr> <td> <div> <button onclick="btn('7')">7</button> </div> </td> <td> <div> <button onclick="btn('8')">8</button> </div> </td> <td> <div> <button onclick="btn('9')">9</button> </div> </td> <td> <div> <button onclick="calc2('/')">÷</button> </div> </td> </tr> <tr> <td colspan="2"> <div class="col2"> <button onclick="btn('0')">0</button> </div> </td> <td> <div> <button onclick="btn('00')">00</button> </div> </td> <td> <div> <button onclick="calc()">=</button> </div> </td> </tr> </table> </div> </body>js// 初期値0で四則演算子が押されたら'calcbtn'にする var state = 0; // inputに表示、エラー文消去 function show(elem) { document.querySelector('input').value += elem; errormessage.innerHTML = ''; } // var checkNum = new RegExp(/^[0-9]/); // checkNum.test(inputV) // 押されたボタンを表示 function btn(elem) { var inputV = document.querySelector('input').value; // 四則演算子ボタンが押された時 if (elem === '+' || elem === '-' || elem === '*' || elem === '/') { //四則演算子ボタンが入力された時に連続で押せないようにする if (state === 'calcbtn') { var inputV = document.querySelector('input').value; inputV = inputV.slice(0, -1); document.querySelector('input').value = inputV; show(elem) } else { state = 'calcbtn'; show(elem) } } else { state = 0; show(elem) } }; // 最初に'*','/'が入力された時に弾く function calc2 (elem) { var inputV = document.querySelector('input').value; if (inputV === '' || inputV === '+' || inputV === '-') { return; } else { btn(elem); } }; // input を更新する function update(elem) { document.querySelector('input').value = elem; errormessage.innerHTML = ''; } // =ボタンが押された時計算する var errormessage = document.getElementById('errormessage'); function calc() { var inputV = document.querySelector('input').value; try { var total = new Function('return ' + inputV); update( total().toString() ); } catch { errormessage.innerHTML = '入力値が不正です。' } }終わりに
少しでも後学者の方の参考になれば幸いです!
指摘などがありましたら、コメントなどによろしくお願いいたします!
- 投稿日:2020-07-20T20:02:46+09:00
[React] useStateのセッターで配列を更新しても再レンダリングされない時
前置き
Reactのstate hookで配列を定義していて、更新したいと思いセッターに値を渡すと中身は更新されているのにコンポーネントが再レンダリングされない事案にぶち当たりました。
const [hoge,setHoge] = React.useState<SampleType>(InitialArray)これに悩まされて数時間無駄にしたので誰かのお役に立てば...
問題のコード
問題となるコードのサンプルを作ってみました
/*--------------------------------略*/ const LunchList: React.FC = () => { const [lunchlist,setList] = React.useState<Lunch[]>(InitialArray) React.useEffect(()=>{ const setvalue = lunchlist setvalue.push('パスタ') //重要 setList(setvalue) }) /*--------------------------------略*/ }パット見動きそうなのですがこれだと前置きで話した通り再レンダリングされません。
なぜ?
調べてみたところReactのstate hookは
object.is()を使って変更があったかどうかを判別しているので、
今回の例のようにlunchlist
をコピーしたsetvalue
を
push()
などで直接操作してセッターに渡しても再レンダリングされないようです。公式の記事
If you return the same value from a Reducer Hook as the current state, React will bail out without rendering the children or firing effects. (React uses the Object.is comparison algorithm.)解決
オリジナルでもコピーでもダメなら新しい配列をつくってしまう
const setvalue = [...lunchlist, 'パスタ'] //解決 setList(setvalue)こうしてしまえば
object.is()
で変更があったか判別できるので再レンダリングしてもらえます以上
- 投稿日:2020-07-20T19:44:30+09:00
再帰関数が苦手なエンジニアのための再帰関数入門
エンジニアに転職して早2年半。いまだに再帰関数が苦手です。
再帰関数を含むコードレビューがあると「よく分からんけど、動作も良いしテストも書かれてるしヨシ!!approved!!」としてしまったことも...(絶対あかん?)。
さすがにそれはヤバイと、再帰関数を学び直したのでその結果をまとめてみました。再帰関数とは?
再帰関数とは、関数内で、自分自身を呼び出す関数です。
この時点で謎ですよね。最初にみたときは「無限ループでは?」って思いました。
以下再帰関数の例として度々あげられる階乗の計算です。const factorial = (n: number): number => { if (n < 2) { return 1; } return n * factorial(n - 1); };
factorial
関数内で、return n * factorial(n - 1)
と自分自身を呼び出しています。
この関数の結果は以下テストの通りです。test('factorial' => { expect(factorial(0)).toBe(1); expect(factorial(1)).toBe(1); expect(factorial(3)).toBe(6); expect(factorial(5)).toBe(120); expect(factorial(8)).toBe(40320); })確かに期待値通り、階乗を計算できていますね。
次項以降で再帰関数のコードを読む際のポイント、再帰関数での実装に向いている処理をまとめていきます。再帰関数を読むためのポイント
まず、コードリーディングで再帰関数を読み解くときに意識すると良いことをまとめます。
1. 基本ケースと再帰ケースで処理を分けて考えてみる
再帰関数内の処理は、自分自身を呼び出さない基本ケースと、自分自身を呼び出し再帰的に処理する再帰ケースの2つに分かれています。
それを意識しながらコードを読むと、どのタイミングで処理を抜けるのか分かり再帰関数が無限ループにならないことが理解できます。
先ほどの階乗の例でみると以下の通りです。const factorial = (n: number): number => { if (n < 2) { return 1; // ⭐基本ケース } return n * factorial(n - 1); // ⭐再帰ケース };
if (n < 2) {}
のif文で基本ケースと再帰ケースを分けているのが分かります。
再帰ケースにて引数のnは1ずつ減少していくので処理を繰り返せばいずれ、n < 2
の条件を満たします。
そうすると基本ケースに移動し、実際の値を返します。
基本ケースがない、もしくは基本ケースにたどり着かないケースがある再帰関数は処理が終わらずスタックオーバーフローに陥るので、再帰関数を書く際にも基本ケースと再帰ケースを分けて考えると良いと思います。2. コールスタックをイメージしてみる
次に実際にどのように処理されるのかをイメージするポイントです。
プログラムでは関数を呼び出すと、関数自身のローカル変数等を保持するフレームが生成されてコールスタックと呼ばれるスタックにpushされます。そして、関数の評価が完了するとコールスタックからpullされます。その流れを再帰関数で考えてみましょう。以下は、
factorial(3)
のケースのコールスタックを図にしています。
まず最初に、factorial(3)
がそのままスタックにpushされて、その後factorial(3)
を処理すると戻り値はfactorial(2)
でまだ計算結果を取得できないので、さらにfactorial(2)
スタックにpushされます。同じくfactorial(2)
の戻り値にもfactorial(1)
が含まれるので、factorial(1)
がスタックにpushされます。
factorial(1)
の処理は、基本ケースで1
を返すのでfactorial(1)
はpullされて次のfactorial(2)
に戻り値である1
を渡します。さらに、factorial(2)
ではその1
を使って計算ができるのでpullされて、戻り値の2
をfactorial(3)
に渡します。最後に、factorial(3
はその戻り値の2
を使って計算され6
という結果を返します。これでコールスタックがきれいに空になります。基本的にどの再帰関数もコールスタックに関数がどんどん積まれて、基本ケースになったら上から順に解決されていくという流れは同じです。
処理が追えなくなったら、一度コールスタックの図を書いてみると腹落ちするかもしれません。3. 評価結果を愚直に書き出してみる
コールスタックの図以外にも理解を助けるものとして評価結果を愚直に書き出してみるのもおすすめです。
実際にfactorial(4)
の時の評価結果を書き出すと以下のようになります。factorial(4) => 4 * factorial(3) // factorial(4)の時のreturn値 => 4 * (3 * factorial(2)) // factorial(3)の時のreturn値 => 4 * (3 * (2 * factorial(1))) // factorial(2)の時のreturn値 => 4 * (3 * (2 * (1))) // factorial(1)の時のreturn値 => 4 * 3 * 2 * 1 // カッコを外すと階乗の計算になっている! => 24 // 結果実際に順を追って評価結果を書き出してみると最終的には階乗の計算式になっていることがわかると思います。
引数に与える値を基本ケース付近にして評価結果を愚直に書き出すことで処理をイメージしやすくなるはずです。再帰関数が向いている処理は?
再帰関数の大体のイメージがついたところで、どのようなときに再帰が有効なのか?というのを考えてみます。
漸化式で表せる処理
ひとつは漸化式で表せるものです。ここでいう漸化式は以下Wikipediaの定義の通りです。
漸化式(ぜんかしき、英: recurrence relation; 再帰関係式)は、各項がそれ以前の項の関数として定まるという意味で数列を再帰的に定める等式である。
数列を再帰的に定める等式
と、説明からして再帰関数に関係してそうですね。
単純な漸化式で表せるものは、漸化式の数式を割とそのままコードに落とせば再帰関数が出来上がります。よく再帰関数の例であげられる
フィボナッチ数列
はまさにそれですね。
フィボナッチ数列は「前の2つの数を足したものが次の数になるという規則に基づいている数列」です。黄金比やひまわりの種の螺旋構造との関連とかの話が有名です。
フィボナッチ数列は以下漸化式で表せます。$F_0 = 0$,
$F_1 = 1$,
$F_n + 2 = F_n + F_n+1 (n ≥ 0)$これを変換してn番目のフィボナッチ数を出す公式は以下の通りです。
$F_0 = 0$,
$F_1 = 1$,
$F_n = F_n-1 + F_n-2$これをそのままコードに落とし込むと再帰関数の出来上がりです。
const fibonacci = (n: number): number => { if (n < 2) { return n; } return fibonacci(n - 1) + fibonacci(n - 2); };nが2未満の時は、nの値をそのまま返し(基本ケース)、それ以上の場合は$F_n-1 + F_n-2$を返しています(再帰ケース)。
このように漸化式で表せるものは比較的容易に再帰関数として定義できます。そのような要件が出てきたら一度再帰で処理できるか考えてもよいかもしれません。木構造を取り扱う処理
次に、木構造(枝分かれしながらデータが伸びていくデータ構造。ネストしているオブジェクト型など)のデータの各要素になんらかの変更を加える処理は再帰関数で有効です。
1つの階層に与える処理の中で、さらに階層が見つかったら自分自身を呼びだすという形ですね。割と業務で扱うことが多いオブジェクトのケース変換処理(snake to lowerCamel)を例にみてみます。
実装コードは以下の通りです。// スネークケースの文字列のキャメルケースへの変換を行う関数 // 再帰ではないので、今回は特に処理内容は見なくて大丈夫です const camelize = (str: string) => { return str.split("_").reduce<string>((acc, cur, i): string => { if (i === 0) { return cur.toLowerCase(); } return acc + cur.charAt(0).toUpperCase() + cur.slice(1).toLowerCase(); }, ""); }; // オブジェクトを受け取り、key名にcamelizeを適応する関数 const camelCaseDeep = (obj: Record<string, any>) => { const result = {} as Record<string, any>; Object.keys(obj).forEach((key) => { if (Object.prototype.toString.call(obj[key]) === "[object Object]") { obj[key] = camelCaseDeep(obj[key]); //⭐ ここで再帰的に実行している } result[camelize(key)] = obj[key]; }); return result; };
camelCaseDeep
がオブジェクトを受け取り再帰的に全てのキー名をsnakeケースからlowerCamelケースにケース変換しています。
ここでのポイントは、今までの例とは逆で基本ケースが、if文の外にあり再帰ケースがif文の中にあるという点です。探索中にオブジェクト型が見つかった場合のみ、さらに自分自身を再帰的に呼び出しています。
この関数では何層もネストしたオブジェクトでも全てのキーをケース変換可能です。これを再帰関数ではなく通常のループで書こうと思うとかなり大変です。
なので、ネストした木構造のデータを扱う場合は最初から再帰でトライしてみても良いかもです。再起関数を書く際に注意すべきこと
最後に再帰関数を実装するうえで注意すべきことをまとめます。
計算量の増加
再帰関数は簡潔に処理をかけるのですが、往々にして通常のループに比べて計算量が増加しがちです。
例えば例にあげたフィボナッチ数を求めるfibonacci
関数は1つの数値を出すのに内部で2つのfibonacci関数を実行しているので、計算量は$O(n^2)$となります。これでは入力が増えると指数関数的に計算量が増加してしまいます。
実際にChromeでfibonacci(50)
を実行するとしばらく結果が帰ってきません。これには対策としてはメモ化があります。
メモ化は再帰関数内で重複する呼び出し結果を保存しておいて、計算量の増加を防ぐ方法です。fibonacci数の計算の場合は、以下のように改善できます。const fibonacci = (n: number, memo: number[] = []): number => { if (n < 2) { return n; } if (memo[n]) { return memo[n]; } return memo[n] = fibonacci(n - 1, memo) + fibonacci(n - 2, memo); };fibonacci関数の第2引数に
memo
という配列を追加し、デフォルトで空配列を渡しておきます。そして、fibonacciの計算結果をmemoに毎回蓄積していきます。
一度呼び出されたfibonacci(n)
の結果はmemo[n]
に保存されているので、再度スタックに積まれることなく処理可能となり、計算量が$O(n)$まで減ります。
実際にChromeで実行した場合、通常のfibonacci関数だとfibonacci(50)
で結果がなかなか返ってきませんが、memo化を行った場合fibonacci(1000)
でも一瞬で計算できました。実装するときは再帰関数の計算量がどのようになるのか?メモ化は適応できないのか?一度考えた方が良いと思います。
スタックオーバーフロー
最後に、再帰関数に必ずついて回るのがスタックオーバーフローです。
スタックオーバーフローとはコールスタックに処理が積み上がりすぎて、メモリ領域が足りなくなり、プログラムが異常終了することです。
再帰はコールスタックをイメージしてみるで説明した通スタックを使います。再帰が深くなれば深くなるほどスタックが積み上がり最終的にスタックオーバーフローを引き起こすので注意が必要です。例えば例としてあげていた階乗を計算する
factorial
関数の場合は、Chromeでfactorial(13000)
を実行するとMaximum call stack size exceeded
とスタック・オーバーフローが発生します。その対策としては、
末尾再帰化
があります。
末尾再帰
とは再帰関数のうち、自分自身の呼び出しが末尾呼び出し(return時、最後に評価される処理)となっている再帰関数です。
末尾再帰の再帰関数を、末尾再帰最適化
を行ってくれる実行環境で実行すると、余計なスタックが積まれず、スタックオーバーフローが発生しなくなります。末尾再帰化した
factorial
関数はこちらです。
第2引数のaccumに計算結果が蓄積され、基本ケースでにaccumを返すように修正されています。const factorialTailCall = (n: number, accum: number = 1): number => { if (n === 0) { return accum; } return factorialTailCall(n - 1, n * accum); }現状のプラウザが
末尾再帰最適化
に対応していないため、実行結果の確認は出来ていないのですが、末尾再帰最適化
に対応した実行環境だとおそらくスタックオーバーが発生しないはずです。
末尾再帰化
について、私自身なかなか曖昧な理解なのでより詳しい説明はこちらの記事を参照してください。終わりに
以上、「再帰関数が苦手なエンジニアのための再帰関数入門」でした。
正直自分自身まだまだ全然自信はないのですが、記事まとめる段階で少しは理解が進んだので書いて良かったかなと思ってます。自分のように「再帰関数ぅぅ..?」となっている人の少しでも助けになれば幸いです。また、もし記事中に誤り等あれば、
マサカリ?優しさあるコメントで教えてもらえると嬉しいです。参考
- 投稿日:2020-07-20T19:31:42+09:00
【JavaScript】日付の取得、計算、バリデーション (Dateオブジェクト)
はじめに
日付の取得、日付の計算、便利なバリデーションなど学んだことを自分用のメモとして残します。
※後にlet,constなどの宣言について学ぶので今回は'var'を使用しています。Dateオブジェクトについて
DateオブジェクトはJavaScriptにもともと組み込まれているオブジェクトで、日付を取得したり、時間を計算する場合などによく使われます。
簡単に参考例を挙げてみます。
js// 本日の日時を取得する事ができます。 var today = new Date(); //取得したtoday(本日の日時)の西暦のみを取得します。 var Year = today.getFullYear(); //取得したtoday(本日の日時)の西暦のみを取得します。 var Day = today.getDate(); //取得したtoday(本日の日時)の西暦のみを取得します。 var Time = today.getHours(); . . . //など調べると多くのメソッドがあります。日付を入力してもらうプログラムを作る時など、日時を任意に設定することもできます。
js//各場所に数字を入力することで設定する事ができます。 //new Date(年, 月, 日,,,)←時間まで指定する事ができます var date = new Date(2020, 7, 20); //変数を当てはめることもできる var y = 2020; var m = 7; var d = 20; var date = new Date(y, m-1, d); // 月は「0」を起点(1月 = 0)とするので「-1」で調整します日付の計算
日付を取得して計算をすることもできます。
年、月、日などの計算もできます。jsconst date1 = new Date(2020, 7, 20); date.setDate(date.getDate() + 3); //3日後 //3日前を求めるときは - にします date.setYear(date.getyear() + 3); //年の計算 date.setMonth(date.getMonth() + 3); //月の計算日付のバリデーション
日付を入力してもらうプログラムなどの際の有効な数字を以下のコードを使用することで簡単にチェックする事ができます。
(※半角英数字、文字数などのバリデーションは必要に応じて設定しないといけません。)jsvar y = 2020; var m = 7; var d = 20; var date = new Date(y, m-1, d); var month = date.getMonth() + 1; // 月は「0」を起点とするので今度は「+1」で調整します if(m == month){ var result = "有効な日付"; } else { var result = "無効な日付"; }Dateオブジェクトで日付を取得する時、「7」を指定しているのに「Aug(8月)」になってしまいます。それは月は「0」を起点とするので7だと8月になります。(0 = 1月)
ですから、var date = new Date(y, m-1, d);で取得する時にm(月に−1しています)。
では、13月に相当する「12」を指定するとどうなるかというと、
jsvar today = new Date(2020, 12, 1); console.log(today) //Fri Jan 01 2021 00:00:00 GMT+0900 (日本標準時)月が1つ進んで2021年1月1日(水)が返ってきます。
これは日の部分も同じで、たとえば4月30日までしかないのに、4月31日を指定すると、以下のように5月1日が返されます。
js//2020年4月31日を指定 var today = new Date(2020, 3, 31); //Fri May 01 2020 00:00:00 GMT+0900 (日本標準時)この特性を使って、指定した月と返ってくる月が同じかどうか調べれば、日付が有効かどうか分かるということなんですね。
- 投稿日:2020-07-20T19:11:39+09:00
【vue/javascript】 ライブ動画再生がうまくいかなかったので、hls.jsで解決した話(IE11再生も)
はじめに
ライブ動画再生がうまくいかなかったので、hls.jsで解決した話(IE11再生も)。
ライブ映像だとなぜかvideojsでうまく再生されない。しかも安定性にも欠ける部分があるらしい。
・https://www.techlive.tokyo/archives/4295
・https://ch.nicovideo.jp/skas-web/blomaga/ar1798847ということでvideojsではなく、hls.jsを使用することにした結果、うまくいったのでメモ。
ちなみに、IE11再生もうまく行きます。
IE11再生させたい時には、開発者ツールを開いてネットワークタブにある「常にサーバから更新する」をオンにしてみてください。・https://www.gitmemory.com/issue/video-dev/hls.js/2421/547774657
結論
videojs同様に、ドシンプル。
<template> <video id="video_id" controls webkit-playsinline autoplay></video> </template> <script> import hlsjs from '' // hlsjsをnpmでインストール export default ({ data() { return { // hlsjsを機能させるための初期化処理を格納した変数 hls: new Hls() } }, methods: { // 動画再生時にこの関数を叩く playVideo: function(video_url){ this.$nextTick(function () { var video = $('#video_id').get(0); if(Hls.isSupported()) { var hls = this.hls; hls.loadSource(video_url); hls.attachMedia(video); hls.on(Hls.Events.MANIFEST_PARSED,function() { // 動画再生 video.play(); }); } else if (video.canPlayType('application/vnd.apple.mpegurl')) { video.src = video_url; video.addEventListener('loadedmetadata',function() { // 動画再生 video.play(); }); } }) }, // 動画停止時にこの関数を叩く stopVideo: function(){ // 動画停止 this.hls.destroy(); } } }) </script>ちなみに
videojsだと以下のように実装。
<template> <video id="video_id" controls webkit-playsinline autoplay></video> </template> <script> import videojs from '' // videojsをnpmでインストール export default ({ methods: { // 動画再生時にこの関数を叩く playVideo: function(video_url){ var player = videojs("video_id", { flash: { hls: { withCredentials: false } }, html5: { hls: { withCredentials: false } } } ); player.src({ src: video_url, type: 'application/x-mpegURL' }); player.play(); }, // 動画停止時にこの関数を叩く stopVideo: function(){ // 動画停止 videojs('video_id').dispose(); } } }) </script>
- 投稿日:2020-07-20T19:06:02+09:00
JavaScript Dateオブジェクトの罠について
Dateオブジェクトの罠について
年月日指定でオブジェクトを生成する時、区切り文字によって時刻が異なる
ハイフン区切りの場合
var date = new Date('2020-07-01'); console.log(date); // => Wed Jul 01 2020 09:00:00 GMT+0900 (日本標準時)スラッシュ区切りの場合
var date = new Date('2020/07/01'); console.log(date); // => Wed Jul 01 2020 00:00:00 GMT+0900 (日本標準時)
- ハイフン区切りの場合では、指定年月日の09:00:00を返すが、スラッシュ区切りの場合では、指定年月日の00:00:00を返す
- 投稿日:2020-07-20T18:45:51+09:00
学びの積み重ねを続けよう
日々の学びを3つずつ積み重ねていきます。
詳細については時間のある時に書いていこうと思います。
目的はアウトプットを続けること。JavaScript編
7月20日
!! 二重否定は文字列 String から 真偽値を取得したい時に使われる。
返り値として、trueまたはfalseが欲しい時に使いましょう。
vuex/index.jsexport const getters = { isAuthenticated (state) { return !!state.user } }RFCとはインターネットにおける技術仕様
自動車の部品などに使われるJISのインターネット版という印象。
JSONのファイル形式やURLの仕様について公開されている。HTTPレスポンスは最初の一行目「ステータス・コード」をみることによってリクエストの結果を知ることができる
この中でもチェックしておきたいコードは以下
200 OK リクエストが正常に完了 みんな大好き 302 Found 探し物は見つかったけれど別の場所にあるためリダイレクトを要する 401 Unauthrized ユーザ認証に失敗したことを表す。 あまり見覚えがない 403 Forbidden アクセス制限による拒否。権限が必要となる。 404 Not Found クライアント側に起因する。URLの打ち間違いなど 500 Internal Server Error サーバ側に起因するエラー
- 投稿日:2020-07-20T18:26:30+09:00
Tailwind on Rails
なぜTailwind on Rails?
- クラス名を決める必要がなくなる
- クラス名の衝突がなくなり、BEMやCSS設計から開放される
- デザインの修正により不要になったCSSが残ってしまうことがなくなる
- どの要素にどんなスタイルが当たっているかがすぐにわかる
- カラーコードやフォントサイズ、ブレイクポイント等の統一性を保ちやすい
- ネット上に転がっているサンプルコードを気軽に取り入れやすい(他の人が書いたコードでもカスタマイズが楽)
- スタイルの修正のたびに
app/assets/stylesheets/任意のフォルダ/任意のファイル
を開く必要がなくなる環境
Rails 6.0.3
導入
$ yarn add tailwindcss $ yarn tailwindcss init $ mkdir app/javascript/css $ touch app/javascript/css/tailwind.cssapp/javascript/css/tailwind.css@import "tailwindcss/base"; @import "tailwindcss/components"; @import "tailwindcss/utilities";app/javascript/packs/application.jsimport '../css/tailwind.css';postcss.config.jsmodule.exports = { plugins: [ //... require("tailwindcss"), //追加 require("autoprefixer"), //追加 require("postcss-preset-env")({ autoprefixer: { flexbox: "no-2009", }, stage: 3, }), ], };動作確認
$ rails g controller test indexconfig/routes.rbroot to: 'tests#index'app/views/tests/index.html.erb<div class="max-w-sm mx-auto bg-white shadow-lg rounded-lg overflow-hidden"> <div class="sm:flex sm:items-center px-6 py-4"> <img class="block mx-auto sm:mx-0 sm:flex-shrink-0 h-16 sm:h-24 rounded-full" src="https://randomuser.me/api/portraits/women/17.jpg" alt="Woman's Face"> <div class="mt-4 sm:mt-0 sm:ml-4 text-center sm:text-left"> <p class="text-xl leading-tight">Erin Lindford</p> <p class="text-sm leading-tight text-gray-600">Customer Support Specialist</p> <div class="mt-4"> <button class="text-purple-500 hover:text-white hover:bg-purple-500 border border-purple-500 text-xs font-semibold rounded-full px-4 py-1 leading-normal">Message</button> </div> </div> </div> </div>applyを使う
Tailwind CSSにはapplyという機能があり、複数のクラスをまとめて適用することができます。例えば同じボタンがあらゆる箇所に出現する場合、毎回
font-bold py-2 px-4 rounded bg-red-500 text-white hover:bg-red-700
などと書くのは大変なので、btn
というクラスを指定するだけで上記のクラスを適用するためにapplyを使います。TailwindをRailsで利用する場合applyを使用するのが難しいので、helper関数で対応することにします。
(こちらの記事ではRailsでapplyを使っているようですが、この通り設定するとapplyを適用した箇所以外のスタイルが効かなくなってしまいました)
$ rails g helper tailwind
app/helpers/tailwind_helper.rbmodule TailwindHelper def btn 'fosnt-bold py-2 px-4 rounded bg-red-500 text-white hover:bg-red-700' end endapp/controllers/application_controller.rbclass ApplicationController < ActionController::Base helper TailwindHelper endapp/views/tests/index.html.erb<a href='#' class='<%= btn %>'>ボタン</btn>懸念点
TailwindCSSでは、想定されうるあらゆるユーティリティークラスが用意されているので、他のCSSフレームワークよりファイルサイズが大きいです。
この問題を、PurgeCSSという機能を使いビルド時に実際に使われているクラスに関するスタイルだけを抽出する方法で解決しています。しかしRails上でTailwindを使う場合、PurgeCSSが使えません。(正確には設定方法がわかりません。分かる方がいたら教えてください。)
そのため通常よりファイルサイズが大きくなってしまいます。当初この点を懸念して、Tailwind on Railsは無理ではないかと考えていました。
Tailwindの公式サイトを確認したところ
Using the default configuration, the development build of Tailwind CSS is 1996kb uncompressed, 144.6kb minified and compressed with Gzip, and 37.kb when compressed with Brotli.
とあり、要はminify&gzip済で
144.6kb
とのこと。Bootstrapが22.1kb
ってことを考えるとまあ重いですが、許容範囲なんじゃないかと思っています。gzipの設定、ブラウザにキャッシュさせる期間の設定、CDNの活用とかをちゃんとやっていればクリティカルではないでしょう。
参考
- 投稿日:2020-07-20T17:51:43+09:00
JavaScriptで各ブラウザの使用言語を検出する方法
概要
JavaScriptではnavigatorオブジェクトのプロパティであるlanguageを利用することで、ブラウザで設定されている使用言語を簡単に検出することができます。
ここではlanguageプロパティと各ブラウザによる挙動の違いについてまとめます。Languageプロパティとは
navigatorとはユーザーが利用しているブラウザの詳細な情報を取得するオブジェクトであり、そのプロパティであるlanguageではブラウザの使用言語を検出することができます。
navigatorオブジェクトはwindowオブジェクトのプロパティの一つであるため、そのプロパティのプロパティであるlanguageには
window.navigator.language
といったコードでアクセスします。このlanguageプロパティは主要なブラウザではほぼ使用可能ですが、ブラウザのバージョンによっては未対応な場合もあります。
各ブラウザの動き
Chrome / Safari / Firefox / Edge
Chrome・Safari・Firefox・Edgeではlanguageプロパティが利用できます。
これらのブラウザは使用言語を複数設定できますが、languageプロパティはその中で一番優先順位が高い言語を取得します。
以下はChromeの動作例です。> window.navigator.language > "ja"一番上に設定されている日本語が返ってくることがわかります。
また、navigatorオブジェクトにはlanguagesプロパティという設定している全ての使用言語を取得できるプロパティも存在します。この一覧はブラウザで設定している優先順位通りの順で返ります。
> window.navigator.languages > (5) ["ja", "en", "az", "en-US", "en-GB"]なおlanguageプロパティは基本的には"ja"や"en"などの言語コードのみを返しますが、英語(アメリカ合衆国)、英語(イギリス)など国名を含めた言語を選択している場合は言語コードに国コードを組み合わせた"en-US""en-GB"といった値になるため注意が必要です。
Internet Explorer
最新バージョンのIE11はlanguageプロパティを利用できますが、それより前のバージョンでは非対応であり使おうとすると
undefined
が返ります。代替としてIEではブラウザの言語設定を検出するためのプロパティとして
browserLanguage
が存在し、これは古いバージョンでも使えます。また最新のIE11でもこのプロパティは残っています。なおChromeなどで利用できるlanguagesプロパティには非対応です。
前提条件
日本語を最優先に2つの言語を設定してあります。私が検証したIE11&Windows10の環境ではブラウザの言語設定をしようとするとWindows全体の言語設定画面に遷移しました。> window.navigator.browserLanguage > "ja-JP"補足
TypeScriptを使っている場合上記のwindow.navigator.browserLanguage
という書き方では以下のようなエラーが出ます。Property 'browserLanguage' does not exist on type 'Navigator'これはTypeScriptがbrowserLanguageをnavigatorオブジェクトのプロパティとして認識していないことが原因であり、ドットではなく角括弧を使うことで回避できます。
> window.navigator['browserLanguage'] > "ja-JP"IEでは単に「日本語」を選択しても"ja-JP"といった国名付きの言語コードが返るようです。
Opera
OperaもIE同様、古いバージョンではlanguageプロパティに対応しておらず、代わりに
browserLanguage
やuserLanguage
などを使う必要がありました。
しかし現行のバージョンではlanguageプロパティ及びlanguagesプロパティに対応しています。
その代わりなのかbrowserLanguage
やuserLanguage
は利用できなくなっています。こちらはIEと異なる点です。まとめ
- 主なブラウザの最新バージョンはどれもlanguageプロパティが使える
- まだまだ古いバージョンのIEを使っている人は多いのでその辺りのフォローは必要
- OperaなどIE以外でも古いバージョンだとlanguageプロパティが使えないブラウザはあるが、少しマイナー感があるブラウザをどこまでサポートするかは悩みどころ
- 投稿日:2020-07-20T16:19:28+09:00
EJS まとめ
EJSとは
JavaScriptで利用するシンプルなテンプレートエンジン。
拡張子は「.ejs」で、ビルドでHTMLを書き出します。
複数ページで共通のパーツがある時にPHPのようにまとめることができます。
EJS公式サイトEJSの導入
前提として、npmコマンドを使える状態にしておいてください。
プロジェクトファイル作成
dirという名前でプロジェクトフォルダを作成します。
dirという名前はお好きな名前に変えてOKです。ターミナルmkdir dir cd dir
npm初期化
ターミナルnpm init -y
EJS CLIのインストール
ターミナルnpm i -D ejs-cli
package.jsonの書き換え
package.json"scripts": { "build:ejs": "ejs-cli -b ejs/ -f '!(_)*.ejs' -o ./" }EJSファイルを作成
プロジェクトファイル直下にejsという名前でフォルダを作成
ejsフォルダにejsファイルを作成(ページ以外は先頭に_(アンダーライン)をつける)フォルダ構成イメージ
- dir - node_modules - package.json - ejs - index.ejs - _header.ejs - _footer.ejs - index.html ## 書き出されたHTMLファイルビルド
ビルドコマンドを使用して、HTMLファイルを書き出します。
ターミナルnpm run build:ejs
EJS 記法
基本的にEJSは、感覚的にPHPのような感じで使っていきます。
<% %>
このタグの中に、JavaScriptのように記述します。
変数の値は、他のファイルに引き継がれます。ejs<% var hoge = 'Hello World'; %> <% include _header %><%= %>
このタグの中に、出力したいコードを書いていきます。
HTMLエスケープされて出力されます。ejs<% var hoge = '<p>Hello World</p>'; %> <%= hoge %>出力されるHTMLファイル<p>Hello World</p><%- %>
このタグの中に、出力したいコードを書いていきます。
HTMLエスケープされないで出力されます。ejs<% var hoge = '<p>Hello World</p>'; %> <%- hoge %>出力されるHTMLファイル<Hello World><%_ _%>
このタグの中に、JavaScriptのように記述します。
<% %>との違いは、ホワイトスペースが取り除かれることです。
例えばif文の条件に当てはまらない時に余計な空白を消せるので出力されたコードがすっきりします。ejs<%_ var hoge = '<p>Hello World'; _%> <%= hoge %>出力されるHTMLファイル<p>Hello World</p><% -%>
このタグの中に、JavaScriptのように記述します。
<% %>との違いは、改行をなくすことができることです。
includeを使った時に改行されることがありますが、それをなくせるので、出力されたコードがすっきりします。ejs<% include _header -%><%# %>
このタグの中に、出力されたコードで表示したくないコメントを記述します。
ejs<%# コメント %>EJS 文法
for文
繰り返しHTMLを生成したい場合はfor文を使うことができます。
ejs<ul> <%_ for (var i = 1; i <= 3; i++) { _%> <li><%= i %></li> <%_ } _%> </ul>出力されるHTMLファイル<ul> <li>1</li> <li>2</li> <li>3</li> </ul>if文
条件によってHTMLを生成したい場合はif文を使うことができます。
ejs<%_ var hoge = 'yes'; _%> <%_ if(hoge === 'yes') { _%> <p>Yes!!!</p> <%_ } else { _%> <p>No!!!</p> <%_ } _%>出力されるHTMLファイル<p>Yes!!!</p>include (引数なし)
引数なしの場合は、includeしたいejsファイルをそのまま書いていきます。
ejs<% include _header %>include (引数あり)
引数ありの場合は、includeしたいejsファイルを第1引数で引数を第2引数で書いていきます。
ejs<% include('_header', { pageTitle: 'タイトル', pageDescription: 'ディスクリプション' %>まとめ
書き方 出力の仕方 <% %> 改行あり。ホワイトスペースあり。出力されない。 <%= %> エスケープあり。改行なし。ホワイトスペースなし。出力される。 <%- %> エスケープなし。改行なし。ホワイトスペースなし。出力される。 <%_ _%> 改行なし。ホワイトスペースなし。出力されない。 <% -%> 改行なし。ホワイトスペースなし。出力されない。 <%# %> 出力されない。コメント。 この記事が良いと思った方は、LGTMをしていただければ嬉しいです!
フォローも是非お願い致します(^^)
- 投稿日:2020-07-20T12:04:23+09:00
【React】お問い合わせフォームを実装しよう
今回はReactを使って、下図のようなお問い合わせフォームを実装していきます。
真偽値を使ったフォームの変換と入力情報の取得、エラーメッセージを実装していきます。
Reactを使えばフォームの入力やボタンのクリックに応じてリアルタイムに表示を変えることができます。完成図
お問い合わせ入力フォームがあり、
送信すると、
このように表示されているフォームを変換していきます。
また、入力がない場合にエラーメッセージが出力されるようにしましょう。雛形
完成コードになります。
要所で詳細に説明していきます。ContactFrom.jsimport React from 'react'; class ContactForm extends React.Component { constructor(props) { super(props); this.state = { isSubmitted: false, email: "sample@gmail.com", hasEmailError: false, content: "お問い合わせ内容", hasContactError: false, }; } handleSubmit() { this.setState({isSubmitted: true}); } handleEmailChange(event) { const inputValue = event.target.value; const isEmpty = inputValue === ""; this.state = { emial: inputValue, hasEmailError: isEmpty, } } handleContentChange(event) { const inputValue = event.target.value; const isEmpty = inputValue === ""; this.state = { content: inputValue, hasContentError: isEmpty, } } render() { let emailErrorText; if (this.state.hasEmailError) { emailErrorText = ( <span> emailを入力してください </span> ); } let contentErrorText; if (this.state.hascontentError) { contentErrorText = ( <span> お問い合わせ内容を入力してください </span> ); } let contactForm; if (this.state.isSubmitted) { contactForm = ( <span className = "message">送信完了しました<span> ); } else { contactForm = ( <form onSubmit={()=>{handleSubmit()}}> <p>メールアドレス(必須)</p> <input value = {this.state.email} onChange={(event)=>{handleEmailChange(event)}} /> {emailErrorText} <p>お問い合わせ(必須)</p> <textarea value = {this.state.content} onChange={(event)=>handleContenttChange(event)} /> {contentErrortext} <input type="submit" value="送信" /> </form> ); } return( <div className = "container"> {contactForm} </div> ); } export default ContactForm; }送信ボタンで表示を切り替える
stateを定義
stateでフォームが送信されたかどうかを管理していきます。
最初フォームは送信されていないため、isSubmittedの初期値はfalseです。ContactForm.jsconstructor(props) { super(props); this.state = { isSubmitted: false, }; }stateの表示と条件分岐
stateの表示と表示切り替えの条件分岐を作っていきます。
ContactForm.js// 空の変数を準備 let contactForm; // フォームが送信された場合の処理 if (this.state.isSubmitted) { contactForm = ( <span className = "message">送信完了しました<span> ); } else { // stateの初期値はfalseなので以下のJSXが初期で表示されます contactForm = ( <form onSubmit={()=>{handleSubmit()}}> <p>メールアドレス(必須)</p> <input value = {this.state.email} onChange={(event)=>{handleEmailChange(event)}} /> {emailErrorText} <p>お問い合わせ(必須)</p> <textarea value = {this.state.content} onChange={(event)=>handleContenttChange(event)} /> {contentErrortext} <input type="submit" value="送信" /> </form> ); } return( <div className = "container"> // 変数contactFormを定義しstateを表示 {contactForm} </div> ); }表示の切り替え
onSubmitイベントを使って、表示を切り替えていきましょう。
まずはsetStateを使ったhandleSubmitメソッドを作っていきます。ContactForm.jshandleSubmit() { this.setState({isSubmitted: true}); }次に<form>に対してonSubmitイベントを作っていきます。
ContactForm.js// 省略 } else { contactForm = ( // フォームを送信するとhandleSubmitメソッドを呼び出しstateが変更される <form onSubmit={()=>{handleSubmit()}}> <p>メールアドレス(必須)</p> // 省略以上でお問い合わせフォームの切り替えの実装は終了です。
次にエラーメッセージの実装です。入力情報の取得とエラーメッセージの表示
入力情報の取得かつ入力欄に何も内容がない場合にエラーメッセージを出力させましょう。
stateの設定と表示
stateを設定します。
ContactFrom.jsconstructor(props) { super(props); this.state = { isSubmitted: false, // emailの初期値を設定します email: "sample@gmail.com", // 入力値が空かどうかの状態を管理します hasEmailError: false, }; }stateを表示します。
inputでstateを表示させる時はvalue属性に値を指定しましょう。ContactFrom.js} else { contactForm = ( <form onSubmit={()=>{handleSubmit()}}> <p>メールアドレス(必須)</p> <input // state.emailの初期値を表示 value = {this.state.email} onChange={(event)=>{handleEmailChange(event)}} /> // 省略 ); }入力された値の取得
このままではstateの初期値は表示されたが、値の入力ができません。
フォームの入力や削除が行われたときに処理を実行するには、onChangeイベントを用います。
inputタグに対してonChangeイベントを指定しましょう。ContactForm.js} else { contactForm = ( <form onSubmit={()=>{handleSubmit()}}> <p>メールアドレス(必須)</p> <input value = {this.state.email} // onChangeイベントを使ってhandleEmailChangeメソッドを呼び出し値を更新します onChange={(event)=>{handleEmailChange(event)}} /> // 省略 }stateの更新
① event.target.velueとすることで入力値を取得することができます。定数inputValueに代入します。
② isEmptyに空のinputValueを代入します。
③ emailの更新の値をinputValueとすることで入力値を取得できます。
④ 入力値が空の時、hasEmailErrorの値をtrueにします。ContactFrom.js// 引数にeventを持たせる handleEmailChange(event) { const inputValue = event.target.value; ① const isEmpty = inputValue === ""; ② // 複数同時更新 this.state = { emial: inputValue, ③ hasEmailError: isEmpty, ④ } }エラーメッセージの表示
条件分岐を用いて、エラーメッセージの処理をしていきます。
ContactFrom.js// 空の変数を準備 let emailErrorText; // hasEmailErrorの値が空の場合の処理 if (this.state.hasEmailError) { emailErrorText = ( <span> emailを入力してください </span> ); }エラーメッセージを表示します。
ContactFrom.js} else { contactForm = ( <form onSubmit={()=>{handleSubmit()}}> <p>メールアドレス(必須)</p> <input value = {this.state.email} onChange={(event)=>{handleEmailChange(event)}} /> // 変数を定義しエラーメッセージを表示します {emailErrorText} // 省略 }お問い合わせ入力値の取得とエラーメッセージの表示は、
emailと同様の処理になるので割愛します。以上で一連の実装は終わりになります。
- 投稿日:2020-07-20T09:32:01+09:00
配列っぽいオブジェクトを任意のキーの配列に変換
配列っぽいオブジェクト
{ 8347923: { name: 'foo', age: '43', sort_id: 4, }, 349: { name: 'bar', age: '24', sort_id: 2, }, 982123: { name: 'baz', age: '31', sort_id: 1, }, 23: { name: 'qux', age: '14', sort_id: 3, }, ... }を
sort_id
順の配列に変換する。sortedArray(listObject) { const newArray = Object.entries(listObject).map(([key, value]) => ({ ...value, })) return newArray.sort((a, b) => (a.sort_id > b.sort_id ? 1 : -1)) },オブジェクトを適当に配列に変換して評価。
- 投稿日:2020-07-20T08:15:18+09:00
ユーザーのセッション情報をスケーラブルに保つ 2 つの方法
セッション管理について誤った認識を持っていたり、サーバのスケーラビリティを考慮しないままアプリケーションの開発を進めてはいけません。開発の終盤でサーバの負荷分散ができない、なんということになりかねません。
本記事では、Web 初心者向けにセッション管理の主要な2種類の方法について説明します。具体的な実装例として、Web サーバに express を使用しますが、基本的な考え方はどの言語、どの Web サーバでも同じです。
セッション管理とは
そもそも、セッション管理とは何でしょうか。
ブラウザから Web サーバへの通信は HTTP で行われます。HTTP プロトコルはステートレスです。つまり「状態」を持てません。
クライアントから Web サーバへリクエストがあるたびにクライアントとサーバ間でコネクションが張られ、リクエストを受け、レスポンスを返却した後、コネクションは破棄されます。
もう一度リクエストをしても、Web サーバは同じクライアントからリクエストがあったと判断することはできません。そこで一般的に Web アプリケーションにおいては、ユーザがログインした後、そのユーザ情報をセッションデータとしては何らかの方法で保持することが求められます。このログインしたユーザの情報を何とか保持しようとする仕組みのことを「セッション管理」と呼びます。
しっかりと学びたい人は MDN web docs を読みましょう。セッションデータの管理方法
ユーザーのセッションデータを保持するには主に2つの方法があります(実際には他にもいくつかありますが、一般的にはこの2つがあげられるでしょう)。
- セッション ID を使う
クライアントではセッション ID のみを Cookie 内に保持し、
サーバでセッション ID に紐づくセッションデータを保持する方法。- Cookie のみでユーザ情報を保持する
クライアントにてセッションデータ全てを Cookie 内に保持する方法。1. セッションIDを使う
1 の方法を採用すると、クライアントとサーバは以下のような関係になります。
ユーザ 0001 が自身のセッション ID を Cookie にid=0001
で保持しており、サーバサイドで 0001 に紐づくユーザ情報を取得しています。
ただしこの方法ではいくつか問題があります。通常 Web システムではバックエンドのサーバは冗長構成をとり、LB(ロードバランサ)でリクエストが負荷分散されます。また、スケールアウト/スケールインすることも考慮しなければいけません。
単一サーバ内でセッションデータを保持していた場合、1つ前にリクエストしたサーバ以外へルーティングされてしまうとセッションデータが取得できません。そこで Redis などの Key/Value で取得できるデータベースを用意しておき、どのサーバからも取得できるようにしておく構成とすることが一般的です。
2. Cookie のみでユーザ情報を保持する
2 の方式を採用することで、サーバサイドのスケーラビリティを簡単に担保できます。クライアントにユーザのセッションデータを全て格納しておき、毎回サーバサイドに送信することでユーザのセッションデータを維持できます。以下の図では、Cookie には
name=田中, email=tanaka@gmail.com
を格納しており、リクエストごとにサーバへ送信しています。実際にこの方式を使用する場合は、Cookie には生身のデータを保持することはありません。暗号化した文字列を保存し、サーバサイドで復号化して取り出すことが一般的でしょう。この方式を採ることでサーバー側にデータベースやリソースを用意する必要が無くなります。ただし、セッションデータの合計がブラウザの最大 Cookie サイズ(4096バイト)を超えることはできないことに注意しましょう。
セッションデータの管理方法の判断基準
まとめると、セッションデータの取り扱い方法で考慮するのは以下のようになります。
アプリケーションの仕様や必要に応じて選択できるようにしておきましょう。
管理方法 メリット デメリット 1. セッション ID を使う ・セッションデータのサイズの上限を気にする必要がない
・セッションデータはクライアントに対して不可視・サーバサイドで Redis などのスケーラブルなセッションストアを用意する必要がある。
2. Cookie のみでユーザ情報を保持する ・Redis などのスケーラブルなセッションストアが不要 ・ブラウザには Cookie のサイズ上限があり、大きなデータを保持できない(4096 バイト)
・Cookie データがクライアントに見えてしまう主要なライブラリ
さて、express でセッションデータを保持する有名なライブラリとしては以下の2つがあります。
モジュール 概要 express-session
1 の方法に対応。
クライアント上のセッション識別子のみを Cookie 内に格納し、セッションデータはサーバーに格納します。通常は Redis などのデータベースに保存します。cookie-session
2 の方法に対応。
クライアント上のセッションデータを Cookie 内に格納します。express-session
express-session は、セッションデータをサーバーに保管します。Cookie にはセッションデータそのものではなく、セッション ID のみを保存します。デフォルトで、メモリー内のストレージを使用するため、本番環境向けには設計されていません。本番環境では、Redis などのスケーラブルなセッションストアをセットアップする必要があります。互換性のあるセッションストアのリストを参照してください。
サーバ内メモリにセッションデータを保持する場合、最もシンプルな例は以下になるでしょう。使用できるオプションの詳細は公式 GitHub リポジトリを参照ください。
Cookie にセッション ID を保持し、サーバに保持されたセッションデータがあれば、それをカウントアップして返却しています。
特定のユーザのリクエスト数を計測しています。const express = require("express"); const session = require("express-session"); const app = express(); app.use( session({ secret: "input your secret string", // 署名に使用するシークレット文字列 cookie: { maxAge: 10000 }, // 10秒間リクエストがなければセッションデータは削除されます。 }) ); app.get("/", function (req, res, next) { res.setHeader("Content-Type", "text/html"); if (req.session.views) { req.session.views++; res.write("<p>views: " + req.session.views + "</p>"); res.write("<p>expires in: " + req.session.cookie.maxAge / 1000 + "s</p>"); res.end(); } else { req.session.views = 1; res.end("<p>welcome to the session demo. refresh!</p>"); } }); app.listen("3000", () => { console.log("Application started"); });もちろん、異なるクライアントごとに一意なセッション ID が発行されます。以下は Chrome と Firefox を同時にたちあげて振る舞いを観測しています。セッション ID に紐づいたセッションデータをサーバサイドから取り出せています。
さて、セッションデータをメモリに保存していますが、このままでは不十分です。サーバを再起動するとセッションデータが全て消えてしまったり、サーバを複数台用意してロードバランサなどで負荷分散する構成をとることができません。セッションストアを利用できるようにしましょう。
以下は、Redis を使用した例です。connect-redis モジュールを使用しています。
const redis = require('redis') const session = require('express-session') const RedisStore = require('connect-redis')(session) const redisClient = redis.createClient() app.use( session({ store: new RedisStore({ client: redisClient }), secret: 'keyboard cat', resave: false, }) )cookie-session
cookie-session はセッション・キーだけでなく、セッション全体を Cookie に保存します。ブラウザは Cookie 当たり最小 4096 バイトをサポートするので、比較的小さいデータを取り扱う場合にのみ使用を検討してください。
const cookieSession = require("cookie-session"); const express = require("express"); const app = express(); app.use( cookieSession({ name: "session", keys: ["key1", "key2"], maxAge: 10000, }) ); app.get("/", function (req, res, next) { res.setHeader("Content-Type", "text/html"); if (req.session.views) { req.session.views++; res.write("<p>views: " + req.session.views + "</p>"); res.end(); } else { req.session.views = 1; res.end("<p>welcome to the session demo. refresh! -- cookie-session --</p>"); } }); app.listen(3000);
express-session
とは異なり、session.sig
,session
というキーの Cookie が保管されています。セッションデータの実体は{ views: 3 }
のようなオブジェクトですが、暗号化されて保持されています。まとめ
express で使用される主要な2つのライブラリとその使用方法を簡単に説明しました。
セッションの管理は Web 開発において最も基本的なところですので、しっかり理解しておきましょう。
- 投稿日:2020-07-20T02:00:18+09:00
Vue.js + Contentfulでブログを作ったので使用技術・反省点をまとめる
※ブログ( https://coincidence.netlify.app/#/post/vue-js-contentful )からの転載です。
当ブログを作成するにあたって使用した技術、反省点を自分用にまとめておきます。
ソースコードはこちら→ https://github.com/AsazuTaiga/my-blog環境・構成
当ブログの提供環境をざっくり図で表すと、下のような感じです。
vue-cliで作成したVue製サイトを、Netlifyでホスティングしています。GitHubと連携してmasterへのmergeごとにビルド・デプロイが自動的に走ります。
コンテンツ管理(バックエンド)には、Contentfulを使用しています。ContentfulはヘッドレスCMSであり、自身でコンテンツモデルおよびコンテンツを作成することができます。JavaScript用のSDKが用意されており、コンテンツを指定・フィルタリング・ソートして取得することができます。各View・ComponetsはVue Routerに対してパスの変更を行い、また検知します。
他のView・Componentsによるパスの変更が変更された場合、dataプロパティに保持しているコンテンツを一旦削除したうえで、現在のパスで表示したいデータの取得をContentfulAdapterに依頼します。AdapterはContentful Delivery APIを叩いたレスポンスをPromiseの形でView・Componentsに渡します。
データの取得中、すなわちdataプロパティが空の場合はローディングを表示しておき、Promiseがresolveしたらレスポンス内容をdataに取り込んで表示します。Vuexどこで使っているの?というと、唯一グローバルに管理する必要がありそうだったメニューの展開/非展開の状態だけです。これも冷静に考えればメニュー用のコンポーネントで閉じているので、あまり意味はない(むしろ良くない)使い方ですね……。使いたかっただけです。
図には出していませんが、見た目を整えるのにVuetifyを使っています。
反省点
①非同期処理+ローディング表示のコンポーネントを作るべきだった
少なくとも、ローディング画面用のコンポーネント(ぐるぐる)を各Viewに配置して、同じCSSでセンタリングして、同じように非同期中はぐるぐるさせているのは、大変コスパが悪い気がしています。「コンポーネントは画面内での配置を意識しない」「Viewはコンポーネントの位置を決めて配置する」といった自分なりの方針でやっていたのですが、それにしたって同じコードを何度コピペしたかわかりません。こういった場合のベストプラクティスが何なのか、ご存じの方教えてください…。②ストアを適切に使う、いらないなら削るべきだった
各記事の内容をキャッシュさせる必要はないとしても、最低限、タグのIDの一覧くらいは最初に控えておいたほうが効率的でした。現状、タグに紐づく記事の一覧表示には、次のステップが発生しています。
(1) 選択されたタグの名前をもとにタグのIDを問い合わせる
(2) タグのIDをもとにそれに紐づく記事を取得するasync fetchBlogPostsByTagAtPage(tagName, pageNumber) { const tagResponse = await this.fetchTagsByName(tagName); if (tagResponse.total === 0) { throw new Error("タグが存在しません。"); } const tag = tagResponse.items[0]; return this.client.getEntries({ content_type: "blogPost", "fields.tags.sys.id": tag.sys.id, order: "-fields.publishDate", skip: (pageNumber - 1) * POSTS_PER_PAGE, limit: POSTS_PER_PAGE }); } async fetchTagsByName(name) { return this.client.getEntries({ content_type: "tag", "fields.name": name, limit: 1 }); }「タグのIDを問い合わせる」のひと手間を確実になくせて速度向上と通信量減が期待できるのにやらないのは、良くない怠慢です。そのうち直したいですね(こういう言い草をしてしまうときは、たいてい直さないパターンです)。
そもそも、各コンポーネントがContentful Adapterを直接使っているのもあまりよくない気がしています。Vuexで取得して保持、各コンポーネントは取得完了通知を受け取って表示を更新するだけなのが理想なのでしょうか。もちろんコンポーネントが状態を持つこと自体は悪ではないと思います。状態を層で閉じるか、コンポーネントで閉じるかの違いで、それぞれのメリット・デメリットがあります。そして、システムの規模が大きくなればVuexの恩恵が大きくなってくるのだと思います。
このブログくらいのミニマムな機能であれば、基本的に各コンポーネントで状態を閉じておいてよさそうです。もし上述のようにタグのリストを別で保持しておくストアが必要があったとしても、Vuexではない普通のストアで十分かもしれません。③CSSの共通定義を適切に管理するべきだった
開発中、何度もCSSで見た目をいじくりまわしながらあーでもないこーでもないとやっていたので、色定義やbox-shadowの定義がはちゃめちゃにハードコーディングされてしまいました。「自分用だから別にいいや……」という言い訳を開発中何度独り言ちたかわかりません。怖い。
序盤の手間を惜しむと、終盤・保守でかえって大変になります。Done is better than perfectといえど、限度はあります。④Vuetifyは本当に必要だったのか?グリッドだけ使えばよかったのでは?
v-cardやv-btnあたりのコンポーネントのCSSを!importantしまくって色々弄ったので、果たしてこれが良い方針なのか?と問われるとモニョります。v-container, v-row, v-col, v-iconあたりだけ選んで入れればよかったような気がしますし、もっと最小のライブラリがあればそれでよかったのではないか、いっそflexbox最近覚えたのでなにもいらなかったのでは、という気がしてなりません。開発効率アップには間違いなく寄与したVuetifyですが、その改造に腐心して醜いコードが大きく増えたのもまた事実です……。⑤Viewsに再利用性の低い部品を直接書くべきか、コンポーネント化すべきか
再利用が見込めないけどViewsが肥大化するのは防ぎたい、みたいな意味合いでコンポーネントを細かく分けるべきかどうか非常に悩みました。
色々試した結果、個人的には次のいずれかを満たす場合にコンポーネントとして切り出すべきだと思います。
- 状態を持つatoms, moleculesである
- 内部に複数のatoms, moleculesを抱えるorganismsである
- 複数の場所で使用されている
「1か所でしか使われず、状態を持たないatomsである」くらいならコンポーネント化をわざわざする必要はないのかな、という印象です。となると、ほとんどの場合にコンポーネント化を検討することになりそうです。
今回に関してはこの原則に従えてない部分も多いです。反省。良かった点
反省点だけ見ているとしんどいので、良かった点も。
①とにかく完成した、作っている最中もテンション高く臨めた
独りで何か作るときはとにかくこれが一番重要だと考えています。何かを作るときは、私の場合5割以上の場合で挫折します。今回はなんとかここまでこれたので、良かった…。②触りながらVuexを覚えられた
やや無理に取り入れた形になってしまいましたが、コンセプト・便利さ自体は理解できた気がします。ファースト・ステップとしては良かったとポジティブにとらえています。③コンポーネント設計をずっと考えながら生活できた
最終的にベストな形にもっていけたとは到底言えませんが、ブログ内のComponents(Atoms, Molecules, Organisims)とViews(Templates, Pages)をどうするかをずっとお風呂の中とかで考えていたので、このへんのコンポーネント志向の勘所みたいなものはだいぶつかめたのではないかと思います。お仕事中にデザイナーさんのカンプを見たときも、自然とコンポーネントの単位をどこにすべきかを、目で切り分けるようになってきました。④Neumorphismに触れられた
最新のデザイントレンドっぽいやつで、Neumorphismというものがあります。私の理解ですが、単色の画面にbox-shadowで光・陰・影を当てることで浮き出たように見せるワザのようです。
自分でCSSを調整するのはしんどかったので、こちらを使わせていただきました。
https://neumorphism.io/蛇足;ブログを自作した理由
そもそもなぜ機能も見た目も整っている既存のブログサービスを使用せずに、ヘッドレスCMSを使用してフロント周りを自作したか?という理由を一応メモしておきます:
- 自作のほうが愛着がわいてブログが継続できると思った
- カテゴリ・タグがブログの前提機能になっているが、どちらかだけで十分では?というのを検証したかった(このブログにはカテゴリはなく、タグだけです)
- Contentfulを利用して作成しておけば、将来的に移行する場合もスムーズだと思った
色々ありますが、結局一番大きいのは「作りたかったから作った」です。
作りたいベースではなく「ブログをやりたい」という理由であれば、既存のものを使ったほうが絶対良いと思います。
- 投稿日:2020-07-20T00:57:47+09:00
React hooksを基礎から理解する (useMemo編)
React hooksとは
React 16.8 で追加された新機能です。
クラスを書かなくても、state
などのReactの機能を、関数コンポーネントでシンプルに扱えるようになりました。
- React hooksを基礎から理解する (useState編)
- React hooksを基礎から理解する (useEffect編)
- React hooksを基礎から理解する (useContext編)
- React hooksを基礎から理解する (useReducer編)
- React hooksを基礎から理解する (useCallback編)
- React hooksを基礎から理解する (useMemo編)
今ここ
- React hooksを基礎から理解する (useRef編)
useMemoとは
useMemoは値を保存するためのhookで、何回やっても結果が同じ場合の値などを保存(メモ化)し、そこから値を再取得します。
不要な再計算をスキップすることから、パフォーマンスの向上が期待出来ます。
useCallbackは関数自体をメモ化しますが、useMemoは関数の結果を保持します。メモ化とは
メモ化とは同じ結果を返す処理について、初回のみ処理を実行記録しておき、値が必要となった2回目以降は、前回の処理結果を計算することなく呼び出し値を得られるようにすることです。
都度計算しなくて良くなることからパフォーマンスを向上が期待できます。基本形
依存配列が空の場合
const sampleMemoFunc = () => { const memoResult = useMemo(() => hogeMemoFunc(), []) return <div>{memoResult}</div> }依存配列=[deps] へ空配列を渡すと何にも依存しないので、1回のみ実行。
つまり、依存関係が変わらない場合はキャッシュから値をとってくる。依存配列に変数が入っている場合
props.nameが変わるたびに関数を再実行させたい場合は以下のように書きます。
const sampleMemoFunc = (props) => { const memoResult = useMemo(() => hogeMemoFunc(props.name), [prope.name]) return <div>{memoResult}</div> }依存配列=[deps] へ変数を並べると、変数のどれかの値が変わった時にfuncを再実行する。
つまり、依存関係が変わった場合に再実行する。サンプル
import React, {useMemo, useState} from 'react' const UseMemo = () => { const [count01, setCount01] = useState(0) const [count02, setCount02] = useState(0) const result01 = () => setCount01(count01 + 1) const result02 = () => setCount02(count02 + 1) // const square = () => { // let i = 0 // while (i < 2) i++ // return count02 * count02 // } const square = useMemo(() => { let i = 0 while (i < 200000000000) i++ return count02 * count02 }, [count02]) return ( <> <div>result01: {count01}</div> <div>result02: {count02}</div> {/* <div>square: {square()}</div> */} <div>square: {square}</div> <button onClick={result01}>increment</button> <button onClick={result02}>increment</button> </> ) } export default UseMemosquare関数をuseMemoに代入しない場合
const square = () => { let i = 0 while (i < 200000000000) i++ return count02 * count02 } return <div>square: {square()}</div>square関数をuseMemoに代入しない場合、square関数の処理に関係ないはずのresult01ボタンを押した場合でも明らかに処理が重い。
count01はsquare関数の処理は通していないので関係無いはずだが、コンポーネントが再生成されたタイミングでsquare関数が実行されてしまうことが原因で、処理が重くなっている。square関数をuseMemoに代入した場合
const square = useMemo(() => { let i = 0 while (i < 200000000000) i++ return count02 * count02 }, [count02]) return <div>square: {square}</div>square関数をuseMemoへ代入した場合、result01ボタンを押した時に処理の重さは感じられなくなった。
square関数をuseMemoに代入し値を保持することで、依存配列であるcount02が更新されない限り、square関数の処理が実行されなくなったため、result01ボタンを押した場合の処理が軽くなった。
最後に
次回は useRef について書きたいと思います。
参考にさせていただいたサイト
https://reactjs.org/