20200720のJavaScriptに関する記事は21件です。

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-dev

node-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 を使っていた期間が長かったので私もうっかり間違えがちですが、XHRresponseText と違って、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.fromArray に変換するというのは、今どきなテクニックかもしれません。

以上でスクリプトの解説は終わりです。

最後に、忘れてはならないのはスクレイピングは最終手段であるという事です。API が提供されているサービスであれば必ずそちらを使うべきですし、やむを得ずスクレイピングする際はサーバに過度な負荷を与えることの無いよう気をつけましょう。

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

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-dev

node-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 を使っていた期間が長かったので私もうっかり間違えがちですが、XHRresponseText と違って、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.fromArray に変換するというのは、今どきなテクニックかもしれません。

以上でスクリプトの解説は終わりです。

最後に、忘れてはならないのはスクレイピングは最終手段であるという事です。API が提供されているサービスであれば必ずそちらを使うべきですし、やむを得ずスクレイピングする際はサーバに過度な負荷を与えることの無いよう気をつけましょう。

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

アプリ作成 コメント機能非同期化

非同期化記述解説

スクリーンショット 2020-07-20 21.51.14.png

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-20 21.59.49.png

この後、インクリメンタルサーチ機能をつけたいと思う。

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

簡単レシート印刷 receiptline でバーコードと QR コードを作ってみた

日本発のオープンソース receiptline でレシート印刷に少しずつトライしています。

ネットオークションやフリマアプリでレシートプリンターを探していましたが・・・
とうとう何台か落札することができました!

まだ手元にないので、前回利用した開発ツールを引き続き使います。
今回はバーコードと QR コードです。

01.png

バーコード

バーコードアイコンをクリックすると、ダイアログボックスが開きます。

02.png

適当にデータを入れて、サイズと可読文字の有無を選びましょう。

ここは無難にデフォルト設定の CODE128 にしておきます。
CODE128 は「〇〇 Pay」のバーコードに使われているそうです。

キャンセルしたいときは、ダイアログボックスの外をクリックします。

03.png

編集エリアに code プロパティと option プロパティが追加されました。

code プロパティは、バーコードを出力します。
名前は code または c、値はバーコードデータです。

option プロパティは、この行以降のバーコードを設定します。
名前は option または o、値はバーコードオプションです。
複数のオプション値は , or 1 つ以上のスペースで区切ります。

ReceiptLine
{code:WIND402; option:code128,2,72,hri}

04.png

QR コード

二次元コードアイコンをクリックすると、ダイアログボックスが開きます。

05.png

二次元コードは QR コードのみです。
適当にデータを入れて、サイズと誤り訂正レベルを選択します。

キャンセルしたいときは、ダイアログボックスの外をクリックします。

06.png

編集エリアに code プロパティと option プロパティが挿入されました。

二次元コードのプロパティはバーコードと同じです。
option プロパティの値は QR コード用になっていますね。

ReceiptLine
{code:Do it! Make it! Shake it!; option:qrcode,5,M}

07.png

バーコードの位置揃え

文字列と同様に、テーブルの区切り文字 | で位置揃えができます。
バーコードと 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!}|

08.png

ちなみに、バーコードと 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
ean
13 桁の数字
8 桁の数字
商品
UPC-A
UPC-E
upc 12 桁の数字
7 桁の数字
北米の商品
QR Code qrcode ASCII 文字列
漢字
続きは Web で

次回は、変換ライブラリの API を試してみようと思います。

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

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

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

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.js
var 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() )
}

今回発生したエラーについて

イコールを押すと計算が行われ、計算結果をテキストボックスに返すというプログラムを、参考サイトを見ながら作っていても全然機能しないというのがしばらく続いた。

そのときに出た、エラーメッセージがこちら

スクリーンショット 2020-07-17 18.37.23.png

最初は、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:Function

MDN:Number.prototype.toString()

エラー関連
MDN:ReferenceError: "x" is not defined

JavaScriptで「’変数名/関数名’ is not defined」というエラーが出る原因と対処法を現役エンジニアが解説【初心者向け】

今後の構想

・クリアボタンと小数点ボタンの追加
(ボタン配置に苦戦すると思われる)

・クリアボタンでテキストボックスの値をリセットする機能

・入力できる文字列や文字数などの制限

・記号入力は、ボタン切り替え機能を使って計算したい。要は、iphoneの電卓を想定

テキストボックスには記号は入力せず、内部的に入力する。
記号に文字数を使うと、長い桁になったときに足りなくなるし、見辛そうなので。
(テキストボックス表示例:「1」→(+ボタンを押す)→「1」→(=ボタンを押す)→「2」)

・計算結果をリストに追加していく機能
(複数の計算があったとき用のメモ代わり)

・計算結果リストの編集ができる機能
(作れそうなら作ってみる)

ボタン切り替え機能と計算結果をリストに追加していく機能は、以前作ることができたので、今回のコードにも組み込んでいく。

最後に

今は、先駆者様のコードを真似しているに過ぎませんが、
プログラミングにおいては「1つだけ正解というわけではない」とよく聞くので、自分の力でコードを書けるようになりたいのと、いかにエラーと向き合えるかが今後の課題になりそうです。

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

【JavaScript】電卓のプログラムを作成してみた。

はじめに

まだまだ未熟なコードだとは思いますが、
jsの勉強で電卓のプログラムを作成したので記録用に記します。

イメージキャプチャ

53b1de600e7a31e3ad019f3162f2973e.gif

コード

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 = '入力値が不正です。'
  }
}

終わりに

少しでも後学者の方の参考になれば幸いです!
指摘などがありましたら、コメントなどによろしくお願いいたします!

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

[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()で変更があったか判別できるので再レンダリングしてもらえます

以上

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

再帰関数が苦手なエンジニアのための再帰関数入門

エンジニアに転職して早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されます。

名称未設定2.001.jpeg

factorial(1)の処理は、基本ケースで1を返すのでfactorial(1)はpullされて次のfactorial(2)に戻り値である1を渡します。さらに、factorial(2)ではその1を使って計算ができるのでpullされて、戻り値の2factorial(3)に渡します。最後に、factorial(3はその戻り値の2を使って計算され6という結果を返します。これでコールスタックがきれいに空になります。

名称未設定2.002.jpeg

基本的にどの再帰関数もコールスタックに関数がどんどん積まれて、基本ケースになったら上から順に解決されていくという流れは同じです。
処理が追えなくなったら、一度コールスタックの図を書いてみると腹落ちするかもしれません。

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とスタック・オーバーフローが発生します。

スクリーンショット 2020-07-19 6.12.07.png

その対策としては、末尾再帰化があります。
末尾再帰とは再帰関数のうち、自分自身の呼び出しが末尾呼び出し(return時、最後に評価される処理)となっている再帰関数です。
末尾再帰の再帰関数を、末尾再帰最適化を行ってくれる実行環境で実行すると、余計なスタックが積まれず、スタックオーバーフローが発生しなくなります。

末尾再帰化したfactorial関数はこちらです。
第2引数のaccumに計算結果が蓄積され、基本ケースでにaccumを返すように修正されています。

const factorialTailCall = (n: number, accum: number = 1): number => {
    if (n === 0) {
        return accum;
    }
    return factorialTailCall(n - 1, n * accum);
}

現状のプラウザが末尾再帰最適化に対応していないため、実行結果の確認は出来ていないのですが、末尾再帰最適化に対応した実行環境だとおそらくスタックオーバーが発生しないはずです。

末尾再帰化について、私自身なかなか曖昧な理解なのでより詳しい説明はこちらの記事を参照してください。

末尾再帰による最適化 - Qiita

終わりに

以上、「再帰関数が苦手なエンジニアのための再帰関数入門」でした。
正直自分自身まだまだ全然自信はないのですが、記事まとめる段階で少しは理解が進んだので書いて良かったかなと思ってます。自分のように「再帰関数ぅぅ..?」となっている人の少しでも助けになれば幸いです。

また、もし記事中に誤り等あれば、マサカリ? 優しさあるコメントで教えてもらえると嬉しいです。

参考

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

【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」で調整します

日付の計算

日付を取得して計算をすることもできます。
年、月、日などの計算もできます。

js
const date1 = new Date(2020, 7, 20);


date.setDate(date.getDate() + 3);
//3日後
//3日前を求めるときは - にします


date.setYear(date.getyear() + 3);
//年の計算
date.setMonth(date.getMonth() + 3);
//月の計算



日付のバリデーション

日付を入力してもらうプログラムなどの際の有効な数字を以下のコードを使用することで簡単にチェックする事ができます。
(※半角英数字、文字数などのバリデーションは必要に応じて設定しないといけません。)

js
var 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」を指定するとどうなるかというと、

js
var 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 (日本標準時)

この特性を使って、指定した月と返ってくる月が同じかどうか調べれば、日付が有効かどうか分かるということなんですね。

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

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

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

学びの積み重ねを続けよう

日々の学びを3つずつ積み重ねていきます。

詳細については時間のある時に書いていこうと思います。
目的はアウトプットを続けること。

JavaScript編

7月20日

!! 二重否定は文字列 String から 真偽値を取得したい時に使われる。

返り値として、trueまたはfalseが欲しい時に使いましょう。

vuex/index.js
export 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 サーバ側に起因するエラー 
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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.css
app/javascript/css/tailwind.css
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";
app/javascript/packs/application.js
import '../css/tailwind.css';
postcss.config.js
module.exports = {
  plugins: [
    //...
    require("tailwindcss"), //追加
    require("autoprefixer"), //追加
    require("postcss-preset-env")({
      autoprefixer: {
        flexbox: "no-2009",
      },
      stage: 3,
    }),
  ],
};

動作確認

$ rails g controller test index
config/routes.rb
root 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.rb
module TailwindHelper
  def btn
    'fosnt-bold py-2 px-4 rounded bg-red-500 text-white hover:bg-red-700'
  end
end
app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  helper TailwindHelper
end
app/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の活用とかをちゃんとやっていればクリティカルではないでしょう。

参考

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

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の動作例です。

前提条件
日本語を最優先に5つの言語を設定してあります。
スクリーンショット 2020-07-17 16.54.35.png

> 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全体の言語設定画面に遷移しました。

コメント 2020-07-17 140605.png

> 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プロパティに対応しておらず、代わりにbrowserLanguageuserLanguageなどを使う必要がありました。
しかし現行のバージョンではlanguageプロパティ及びlanguagesプロパティに対応しています。
その代わりなのかbrowserLanguageuserLanguageは利用できなくなっています。こちらはIEと異なる点です。

まとめ

  • 主なブラウザの最新バージョンはどれもlanguageプロパティが使える
  • まだまだ古いバージョンのIEを使っている人は多いのでその辺りのフォローは必要
  • OperaなどIE以外でも古いバージョンだとlanguageプロパティが使えないブラウザはあるが、少しマイナー感があるブラウザをどこまでサポートするかは悩みどころ
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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ファイル

&lt;Hello World&gt;

<%_ _%>

このタグの中に、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をしていただければ嬉しいです!
フォローも是非お願い致します(^^)

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

【React】お問い合わせフォームを実装しよう

今回はReactを使って、下図のようなお問い合わせフォームを実装していきます。
真偽値を使ったフォームの変換と入力情報の取得、エラーメッセージを実装していきます。
Reactを使えばフォームの入力やボタンのクリックに応じてリアルタイムに表示を変えることができます。

完成図

お問い合わせ入力フォームがあり、

Image from Gyazo

送信すると、

Image from Gyazo

このように表示されているフォームを変換していきます。
また、入力がない場合にエラーメッセージが出力されるようにしましょう。

Image from Gyazo

雛形

完成コードになります。
要所で詳細に説明していきます。

ContactFrom.js
import 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;

}

送信ボタンで表示を切り替える

Image from Gyazo
           ↓
Image from Gyazo

stateを定義

stateでフォームが送信されたかどうかを管理していきます。
最初フォームは送信されていないため、isSubmittedの初期値はfalseです。

ContactForm.js
  constructor(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.js
  handleSubmit() {
    this.setState({isSubmitted: true});
  }

次に<form>に対してonSubmitイベントを作っていきます。

ContactForm.js
    // 省略

    } else {
      contactForm = (
        // フォームを送信するとhandleSubmitメソッドを呼び出しstateが変更される
        <form onSubmit={()=>{handleSubmit()}}>
          <p>メールアドレス(必須)</p>

    // 省略

以上でお問い合わせフォームの切り替えの実装は終了です。
次にエラーメッセージの実装です。

入力情報の取得とエラーメッセージの表示

入力情報の取得かつ入力欄に何も内容がない場合にエラーメッセージを出力させましょう。

Image from Gyazo

stateの設定と表示

stateを設定します。

ContactFrom.js
  constructor(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と同様の処理になるので割愛します。

以上で一連の実装は終わりになります。

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

配列っぽいオブジェクトを任意のキーの配列に変換

配列っぽいオブジェクト

{
  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))
},

オブジェクトを適当に配列に変換して評価。

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

ユーザーのセッション情報をスケーラブルに保つ 2 つの方法

2way.png

セッション管理について誤った認識を持っていたり、サーバのスケーラビリティを考慮しないままアプリケーションの開発を進めてはいけません。開発の終盤でサーバの負荷分散ができない、なんということになりかねません。

本記事では、Web 初心者向けにセッション管理の主要な2種類の方法について説明します。具体的な実装例として、Web サーバに express を使用しますが、基本的な考え方はどの言語、どの Web サーバでも同じです。

セッション管理とは

そもそも、セッション管理とは何でしょうか。

ブラウザから Web サーバへの通信は HTTP で行われます。HTTP プロトコルはステートレスです。つまり「状態」を持てません。
クライアントから Web サーバへリクエストがあるたびにクライアントとサーバ間でコネクションが張られ、リクエストを受け、レスポンスを返却した後、コネクションは破棄されます。
もう一度リクエストをしても、Web サーバは同じクライアントからリクエストがあったと判断することはできません。

そこで一般的に Web アプリケーションにおいては、ユーザがログインした後、そのユーザ情報をセッションデータとしては何らかの方法で保持することが求められます。このログインしたユーザの情報を何とか保持しようとする仕組みのことを「セッション管理」と呼びます。
しっかりと学びたい人は MDN web docs を読みましょう。

セッションデータの管理方法

ユーザーのセッションデータを保持するには主に2つの方法があります(実際には他にもいくつかありますが、一般的にはこの2つがあげられるでしょう)。

  1. セッション ID を使う
    クライアントではセッション ID のみを Cookie 内に保持し、
    サーバでセッション ID に紐づくセッションデータを保持する方法。
  2. 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");
});

session.gif

もちろん、異なるクライアントごとに一意なセッション ID が発行されます。以下は Chrome と Firefox を同時にたちあげて振る舞いを観測しています。セッション ID に紐づいたセッションデータをサーバサイドから取り出せています。

multi.gif

さて、セッションデータをメモリに保存していますが、このままでは不十分です。サーバを再起動するとセッションデータが全て消えてしまったり、サーバを複数台用意してロードバランサなどで負荷分散する構成をとることができません。セッションストアを利用できるようにしましょう。

以下は、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 } のようなオブジェクトですが、暗号化されて保持されています。

cookie-session.gif

まとめ

express で使用される主要な2つのライブラリとその使用方法を簡単に説明しました。
セッションの管理は Web 開発において最も基本的なところですので、しっかり理解しておきましょう。

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

Vue.js + Contentfulでブログを作ったので使用技術・反省点をまとめる

※ブログ( https://coincidence.netlify.app/#/post/vue-js-contentful )からの転載です。


当ブログを作成するにあたって使用した技術、反省点を自分用にまとめておきます。
ソースコードはこちら→ https://github.com/AsazuTaiga/my-blog

環境・構成

当ブログの提供環境をざっくり図で表すと、下のような感じです。

environment

vue-cliで作成したVue製サイトを、Netlifyでホスティングしています。GitHubと連携してmasterへのmergeごとにビルド・デプロイが自動的に走ります。
コンテンツ管理(バックエンド)には、Contentfulを使用しています。ContentfulはヘッドレスCMSであり、自身でコンテンツモデルおよびコンテンツを作成することができます。JavaScript用のSDKが用意されており、コンテンツを指定・フィルタリング・ソートして取得することができます。

design

各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が肥大化するのは防ぎたい、みたいな意味合いでコンポーネントを細かく分けるべきかどうか非常に悩みました。
色々試した結果、個人的には次のいずれかを満たす場合にコンポーネントとして切り出すべきだと思います。

  1. 状態を持つatoms, moleculesである
  2. 内部に複数のatoms, moleculesを抱えるorganismsである
  3. 複数の場所で使用されている

「1か所でしか使われず、状態を持たないatomsである」くらいならコンポーネント化をわざわざする必要はないのかな、という印象です。となると、ほとんどの場合にコンポーネント化を検討することになりそうです。
今回に関してはこの原則に従えてない部分も多いです。反省。

良かった点

反省点だけ見ているとしんどいので、良かった点も。

①とにかく完成した、作っている最中もテンション高く臨めた
独りで何か作るときはとにかくこれが一番重要だと考えています。何かを作るときは、私の場合5割以上の場合で挫折します。今回はなんとかここまでこれたので、良かった…。

②触りながらVuexを覚えられた
やや無理に取り入れた形になってしまいましたが、コンセプト・便利さ自体は理解できた気がします。ファースト・ステップとしては良かったとポジティブにとらえています。

③コンポーネント設計をずっと考えながら生活できた
最終的にベストな形にもっていけたとは到底言えませんが、ブログ内のComponents(Atoms, Molecules, Organisims)とViews(Templates, Pages)をどうするかをずっとお風呂の中とかで考えていたので、このへんのコンポーネント志向の勘所みたいなものはだいぶつかめたのではないかと思います。お仕事中にデザイナーさんのカンプを見たときも、自然とコンポーネントの単位をどこにすべきかを、目で切り分けるようになってきました。

④Neumorphismに触れられた
最新のデザイントレンドっぽいやつで、Neumorphismというものがあります。私の理解ですが、単色の画面にbox-shadowで光・陰・影を当てることで浮き出たように見せるワザのようです。
自分でCSSを調整するのはしんどかったので、こちらを使わせていただきました。
https://neumorphism.io/

蛇足;ブログを自作した理由

そもそもなぜ機能も見た目も整っている既存のブログサービスを使用せずに、ヘッドレスCMSを使用してフロント周りを自作したか?という理由を一応メモしておきます:

  • 自作のほうが愛着がわいてブログが継続できると思った
  • カテゴリ・タグがブログの前提機能になっているが、どちらかだけで十分では?というのを検証したかった(このブログにはカテゴリはなく、タグだけです)
  • Contentfulを利用して作成しておけば、将来的に移行する場合もスムーズだと思った

色々ありますが、結局一番大きいのは「作りたかったから作った」です。
作りたいベースではなく「ブログをやりたい」という理由であれば、既存のものを使ったほうが絶対良いと思います。

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

React hooksを基礎から理解する (useMemo編)

React hooksとは

React 16.8 で追加された新機能です。
クラスを書かなくても、 stateなどのReactの機能を、関数コンポーネントでシンプルに扱えるようになりました。

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 UseMemo

square関数を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/

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