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

JavaScript勉強の記録その25:fetchでAPI叩いてみた

やりたいこと

GithubのAPIを叩いて、GithubのIDを画面に出したい。

実装方法

1: HTMLの入力欄に自分のIDを入力
2: ボタンをクリック
3: APIをGETで叩いて、JSON形式で取得
4: h1要素として表示させる

用意したHTML

入力欄とボタンのみ用意

index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="utf-8">
  <title>JavaScript Basics</title>
</head>
<body>
  <input></input>
  <button>Get My GitHub ID</button>
  <h1></h1>
  <script src="main.js"></script>
</body>
</html>

用意したjavascriptファイル

APIを叩くと色々な値が返ってくるのですが、loginというキーがGitHubのIDになっています(console.logをしてあげるとよくわかる)。Fetchは何となくノリで書いたけど、動いたからOK。

こういったAjaxの実装をする時、普通はリクエストが成功したかどうか判定して、判定ごとに違う処理をしますがここでは割愛(というかpromiseよくわかってない)。

main.js
'use strict';
{
 const text = document.querySelector('input');

 document.querySelector('button').addEventListener('click', () => {
   let url = `https://api.github.com/users/${text.value}`;

   fetch(url).then(function(response) {
     response.json().then(function(json) {
       document.querySelector('h1').textContent = json.login;
     });
   });
 });


}

動画: https://i.gyazo.com/7dd2e09d16fc503b0b8a9b7aa06d0bf9.mp4

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

Java Script あつめる

Java Scriptとは

学習の目的

ProgateのJavascript(ES6)編で新たに学んだこと

知っとくとよき

・基本コード末尾に;つける。
・定数(const)は変数(let)と異なり、値を更新できない。
しかし、コードが長くなった際、 予期せぬ値の更新を防止できる。

if文

if (条件式1){true時ここのコードを実行}
else if(条件式2){条件式1がfalse時ここのコードを実行}
else{条件式2がfalse時ここのコードを実行}

で条件分岐。

switch文

switch(条件の値) {
case 値1:
条件の値と値1が等しい時の処理を記述
break;
case 値2:
条件の値と値2が等しい時の処理を記述
break;

・・・
default:
上記どの値とも等しくなかった場合の処理を記述
break;
}

比較演算子

a===bでaとbが等しいかを調べる。
a!==bでaとbが異なるかを調べる。

(ex)
const number=12;
console.log(number===12);

コンソール「true」

その他

console.log();で()内の文字記述。
//でその行はコメント出力。
let 変数名=値 ;で変数を定義。上書き時はletは不要。
const 定数名=値 ;で定数を定義。
${}で定数や変数を文字列化。この時、文字列全体は「ㅤ`ㅤ」で囲む。
X && Yで複数条件(XかつY)
X || Yで複数条件(XまたはY)

while文

while (条件式){
条件式が続く間、ここのコードを実行
}
;は不要。

(ex)
while(number<=100){
console.log(number);
number+=1;
}

コンソール
「1 2 3・・・」

for文

for(変数の定義;条件式;変数の更新){
条件式が続く間、ここのコードを実行
}
;は不要。

(ex)
for(let number=1 ;number<=100; number+=1{
console.log(number);
}

コンソール
「1 2 3・・・」

その他

+=1=++
-=1=--ここあdあd

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

【TypeScript】よく使う処理を複数のプロジェクトで使い回す

環境

  • npm: 6.4.1
  • typescript: 3.8.3

使いどころ

JavaScript(TypeScript)を書いているときに「こういうメソッドがあったら便利なのにな」と感じることがしばしばあると思います。例えば以下のような

.ts
// 配列の最初の要素を取得(自分で定義しないと使えない)
[1, 2, 4].first() // 1
[].first()        // undefined

// 1個次の要素を取得(自分で定義しないと使えない)
[1, 2, 4].nextOf(2) // 4
[1, 2, 4].nextOf(3) // undefined

もちろん以下のようにprototypeを操作したりライブラリを導入したりすることで使えるようにはなりますが、

Array.prototype.first = function<T>(): T | undefined {
  const arr = this as Array<T>
  return arr.length > 0 ? arr[0] : undefined
}

例えば複数のアプリを作っているとして、各アプリ内で毎回こういった定義をするのかということを考えると、できれば別プロジェクトに切り出したいところではあります。
ということで色々と試行錯誤したので備忘録を兼ねて投稿

概要

  • 共通処理専用のプロジェクトを作成する
  • それをGitHubにアップする
  • 共通処理利用側プロジェクトでnpm install 上記GitHubリポジトリのURL

手順

共通処理プロジェクトの作成

※ TypeScriptでちゃんと型が参照できるようにするためにその手順も含んでいます。

TypeScriptをインストール

npm install -D typescript

tsconfig.json を作成

./node_modules/.bin/tsc --init
以下のコンフィグで動作を確認済みです。

tsconfig.json
{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "strict": true,
    "declaration": true, // .d.ts ファイルが生成されるようにする
    "outDir": "dist" // ビルド後ファイル生成先ディレクトリ名
  }
}

共通処理を作成

my-array-extensions.ts
// 拡張メソッドの型の宣言をします。
// これを書くことで、ビルド時に型定義ファイル(aaa.d.ts)が作成され、
// 共通処理利用側プロジェクトのtypescriptでこの関数を参照できるようになります
declare global {
  interface Array<T> {
    nextOf: (item: T) => T | undefined
  }
}

export default function() {
  // 関数'nextOf'の実装を定義します。
  // たとえ関数が宣言されていても、この行を通らないと、
  // 実行時に '~~~.nextOf is not function' としてエラーになります。
  Array.prototype.nextOf = function<T>(item: T) {
    const arr = this as Array<T>
    const index = arr.indexOf(item)
    return index === -1 || index >= arr.length - 1 ? undefined : arr[index + 1]
  }
}
index.ts
// exportしないと .d.ts ファイルが作成されず、他のプロジェクトから nextOf の定義を参照できない
export * from './my-array-extensions'

// 上記のファイルで定義した関数を実行し、prototypeに関数を代入します。
// 他のプロジェクトから実行されるのはpackage.jsonのmainに記述したビルド後のファイルなので、
// そのファイルの中でprototypeへの代入処理が実行されないと処理が定義されません。
// myArrayExtensionsの名前は何でも可
import myArrayExtensions from './my-array-extensions'
myArrayExtensions()

package.json を作成

npm init -y

main, build, postinstallの行が重要です。

package.json
{
  "name": /* 適当なパッケージ名 */,
  "version": "1.0.0",
  "description": "",

  // 他のプロジェクトからの参照時にはこれが実行されます。
  "main": "dist/index.js",

  "dependencies": {
    "typescript": "^3.8.3"
  },
  "devDependencies": {},
  "scripts": {

    // tscの実行により .ts ファイルが .js ファイルに変換されます。
    "build": "tsc",

    // postinstallという名前は他のプロジェクトで `npm install 共通処理のプロジェクト` を
    // したときに自動的に実行されるコマンドです。
    // tsc(TypeScriptのトンラスパイル)を実行し、tsconfig.jsonに記載した./distディレクトリにトランスパイル後ファイルを出力します。
    "postinstall": "npm run build"

  },
  "author": "",
  "license": "MIT"
}

GitHubにアップロード

リポジトリを作ってアップロードします(詳細割愛)。gitignoreの中身はこちらです。

.gitignore
/node_modules
/dist

共通処理利用側プロジェクトでの準備

インストール

npm install https://github.com/リポジトリのURL.git

package.json
{
  /* 略 */

  "dependencies": {
    /* 共通処理のパッケージ名 */: "git+https://github.com/GitHubのリポジトリのURL.git"
  }

  /* 略 */
}

インポート

あとは利用側の方のプロジェクトでimportすれば利用可能です。

利用側プロジェクトのソース.ts
console.log([1, 2, 4].nextOf(2)) // TypeError: [1, 2, 4].nextOf is not a function

import '共通処理のパッケージ名' // import後に利用可能

console.log([1, 2, 4].nextOf(2)) // 4

共通処理プロジェクトで更新があった場合

GitHubのリポジトリにpushしたあと、利用側プロジェクトで npm install 共通処理プロジェクトのパッケージ名 で更新します。

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

読みやすいコードを書きたい!

この記事の趣旨

 自分の学習のメモとして残します。
 どなたかのお役に立てたら光栄です。

こんな方におすすめです

 ・コードが見づらいと言われる
 ・初心に返りたい
 このような方には何か発見があると思います。

読みやすいコードとは?

 ズバリ『良いコード』のこと。
 良いコードは、他人がそのコードを見た時に短時間で理解できるコードのことを言います。

 逆に、分かりづらい・理解しづらいコードは解読に時間がかかります。それだけ開発の工数もかかってしまい、効率が良くありません。

 では、良いコードの条件・要素を紐解いていきましょう!

●コードの命名に規則を

 変数やメソッドは好きなように命名ができます。
 ルールがありませんので、個人の好きなようにできます。
 特に共同開発の現場などでは、「他人が見てわかる」を意識する必要があります。

 ◎命名のポイント

  【目的がわかる単語を使う】
    例)new → new_account

  【汎用的な名前は避ける】
   ・一時的な変数などは避ける
   ・可読性を意識して

  【名前に情報を含める】
   大文字、小文字をルールに沿って活用
 
  【誤解されない名前を使う】
   ・何がしたいかが明確な名前
   ・説明的に長くなっても良いので、可読性重視
     例)read_books → already_read_books

●コードレイアウト

 プログラムの挙動に影響はないが、可読性を大幅にあげることができる

 ◎レイアウトのポイント

   ・整列    :縦列を揃える。イコールの位置など縦が揃うと見やすい
   ・一貫性   :似たような構造は同じフォーマットに統一できないか検討
   ・ブロック化 :同じ系統の変数などをまとめてグループ化すること

●コメント

 ・プログラムの動作を説明
 ・他の開発者がコードを読む際の理解を助ける
  ※多すぎても読むのに時間がかかるため、簡潔に

 ◎コメントのポイント

   ・理由をコメントする  :なぜそのコードを書いたか
   ・他の開発者へメモを残す:開発中のメモとして
   ・実際の例を記入する  :コメントでは伝わりづらい時は、コメントとしてコードを記載

まとめ

 結局大事なことは
  「人に対する思いやり」だなぁと。

 複数人で仕事をする以上、「自分だけ良ければそれで良い」という考えはNG。
 誰もが見やすく、仕事をしやすい状況を自分が作り出す意識が大切。

 そのための知識や技術であると思う。

 これからしっかり学んでいきましょう。

   

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

JavaScriptの難所「Prototype」について解説してみた(学習まとめ)

始めに(前書き)

どうもこんにちは、
今回はJavaScriptの難所であろう「Prototype」の概念について解説してみたいと思います。
この記事を読むにあたっては、ある程度オブジェクト指向について理解していないと難しいかと思いますので、読者様におかれましては、事前にオブジェクト指向について学習してから読まれることをオススメします。
また記事の内容に漏れや誤り等ありましたら、ご指摘いただけると助かります。
それでは、よろしくお願いいたします。

本題(本文)

JavaScriptは、(どうやら世間的には)一応はオブジェクト指向言語に分類されるそうですが、
この「Prototype」(以降「プロトタイプ」)の概念があるためにややこしいことになっています。
クラスとは、インスタンスの本になるもので、インスタンスが”実体”だとすると、その”設計図”にあたるものとなります。
たとえば、”自動車”がクラスなら、このクラスを本にして、”カローラ”だとか”フェラーリ”がつくられますが、どれもが自動車なので、同じクラスから継承させて実体をつくります。当然、”カローラ”や”フェラーリ”は、自動車としての(必要な)機能や性質を共通に持ちます。
ここで、プロトタイプとは、複数の任意のインスタンスの間で、(必ずしも)共通の機能や性質を見出せなくてもいいことになっています。
つまり、あるクラスを本にして、そのクラスから継承されているインスタンスであっても、
本になっている(=継承元となっている)クラスにはない機能や性質を持つことができるということです。
これは他の(一般に)オブジェクト指向言語に分類される言語のクラスに関する縛り・制約を緩めたような仕様となっています。
しかも、JavaScriptでは、実行時に動的にインスタンスの持つ機能や性質を変えられる(=メンバーを追加・削除・変更ができる)ようになっています。

終わりに(後書き)

この記事では、至極簡単にプロトタイプについて解説してみました。
実際に動くコードをつかって解説したほうがよかったかもしれませんが、
読者様にとって参考になる、あるいは納得していただけるものが書ける自信がなかったので、
とりあえず今回はコードはなしということで、この記事の執筆を終えたいと思います。
また暇があったら、この記事にコードを書き加えることになるかもしれません。
今回は以上です。
最後までお付き合いくださり、ありがとうございました。

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

kintoneでスクリプト処理が完了するまでレコード全体を非表示にする

はじめに

kintoneではレコード数やフィールド数が多くなると、JavaScriptによるスクリプト処理が終わる前にレコード画面が表示されることがあります。
この場合、setFieldShownや[フィールド].disabled=trueの処理が一瞬遅れることにより、非表示にしたいフィールドが一瞬見えてしまったり無効化したいフィールドが一瞬有効になったりしてしまいます。

実務上はほとんど問題ないのですが、何となくキモチ悪いのでこれを防ぐ方法を探したいと思います。

【注意】
この記事ではDOM操作を行っています。
将来のkintoneのアップデートにより動作しなくなる場合がありますので、あらかじめご了承ください。

基本方針

はじめに書いた通り、画面表示が一瞬遅れるのはスクリプトによる処理が終わる前にレコード画面を表示してしまうからです。
それなら、最初はレコード画面を非表示にしておいてスクリプト処理が完了した時点で表示するようにすれば解決できそうです。

想定される動作は以下の通りです。

  • レコード詳細・追加・編集・印刷画面において、レコード全体の初期状態を非表示にする。
  • スクリプトの処理によりレコード全体を表示する。

レコード全体の非表示化

kintoneのレコード全体を示すdiv要素はid="record-gaia"となっています。
まずはこのdiv要素を非表示にします。

と言っても、スクリプトを使って非表示にしたところで非表示にする処理自体が画面表示よりも遅れるため意味がありません。

ここは、CSSを使って目的のdiv要素を非表示にします。

desktop.css
#record-gaia
{
    visibility: hidden;
}

レコード全体を表示

スクリプトを使ってレコード全体を表示させます。

desktop.js
(function() {
    "use strict";

    kintone.events.on([
        "app.record.create.show",
        "app.record.edit.show",
        "app.record.detail.show",
        "app.record.print.show"
    ], function(event) {
        const recordGaia = document.getElementById("record-gaia");
        if (recordGaia) {
            recordGaia.style.visibility = "visible";
        }

        return event;
    });
})();


これで無事、スクリプト処理が終わった時点でレコード全体が表示されるようになりましたとさ。
めでたし、めでたし。

そうは問屋が卸さない

これだけで済めばよかったのですが、実はこのスクリプトでは問題があります。

kintoneでは、一部のボタンについては「クリックした際に画面遷移はする(=kintone上での画面表示イベントは発生する)がページは再読み込みされない」という動作仕様になっているようです。

これの何が問題かと言うと、前の画面を表示した際の画面表示イベントによりvisibilityvisibleになった状態のまま次の画面を表示した際の画面表示イベントが処理される、つまり画面表示イベント発生時点で初期状態が非表示になっていないケースがあるということです。

このままだと、それらのボタンをクリックした場合は画面表示が一瞬遅れる状態のままになってしまいます。

ボタンクリック時のイベントを追加

この問題を解消するため、対象のボタンに対して「クリック時にレコード全体を非表示にするイベント」を追加することにします。

今回の場合、クリックしてもページが再読み込みされないボタンは次の通りです(カッコ内はクラス名)。

  • 詳細画面:レコードを編集する(gaia-argoui-app-menu-edit)
  • 詳細画面:前のレコードに移動する(gaia-argoui-app-pager-prev)
  • 詳細画面:次のレコードに移動する(gaia-argoui-app-pager-next)
  • 編集画面:キャンセル(gaia-ui-actionmenu-cancel)
  • 編集画面:保存(※下記参照)


それぞれのボタンに対して、addEventListenerを使って「クリック時にレコード全体を非表示にするイベント」を追加します。
なお、編集画面での保存ボタンについてはapp.record.edit.submit.successイベントで代用できるため、そちらを使用します。

function hideRecordGaia() {
    const recordGaia = document.getElementById("record-gaia");
    if (recordGaia) {
        recordGaia.style.visibility = "hidden";
    }
}

kintone.events.on([
    "app.record.edit.submit.success"
], function(event) {
    hideRecordGaia();

    return event;
});

kintone.events.on([
    "app.record.detail.show",
    "app.record.edit.show"
], function(event) {
    const classNames = {
        "detail": ["gaia-argoui-app-menu-edit", "gaia-argoui-app-pager-prev", "gaia-argoui-app-pager-next"],
        "edit": ["gaia-ui-actionmenu-cancel"]
    };
    const eventType = event.type.replace(/^app\.record\.([^.]+)\.show$/, "$1");

    classNames[eventType].forEach(function(className) {
        const elms = document.getElementsByClassName(className);
        if (elms.length === 1) {
            elms[0].addEventListener("click", hideRecordGaia, false);
        }
    });

    return event;
});


これで、「レコードを編集する」などのボタンをクリックした瞬間に一度visibilityhiddenになり、その後スクリプト処理が完了した時点でvisibilityvisibleになります。

今度こそ、処理が完了するまでレコード全体を非表示にするスクリプトが完成しましたとさ。
どっとはらい。

それでも問屋は卸さない

これで完成かと思いきや、実際に動かしてみるとレコード詳細表示画面からレコード編集画面に遷移した際にレコード全体が表示されないことがありました。

これは何故でしょうか。
処理の動きを時系列で考えてみます。

  1. レコード詳細表示画面で編集ボタンをクリック。
  2. 追加したクリックイベントが発火し、visibilityhiddenにする。
  3. レコード編集画面での画面表示のイベントにより、visibilityvisibleにする。

本来はこのような順番で処理されるのが理想なのですが、レコード数やフィールド数が少ない時には2.と3.の順番が逆になる場合があることが分かりました。

つまり、

  1. レコード詳細表示画面で編集ボタンをクリック。
  2. レコード編集画面での画面表示のイベントにより、visibilityvisibleにする。
  3. 追加したクリックイベントが発火し、visibilityhiddenにする。

という順番で処理される場合があるのです。
こうなってしまうと、最終的にvisibilityhiddenになってしまうため、レコード全体が表示されなくなってしまいます。

非同期で処理を遅延

クリックイベントとkintone上での画面表示イベント、どちらが先に処理されるかはその時々で変わるため分かりません。
分からないものは仕方ないので、visibilityvisibleにする際に、その時点のvisibilityを取得して判定することにします。

取得したvisibilityvisibleの場合は、ボタンクリック時のイベントがまだ処理されていないと考えられるためvisibilityhiddenになるまで待機します。
visibilityhiddenになった、つまりボタンクリック時のイベントが処理されたのを確認した上でvisibilityvisibleに変更します。

ただし、何かの手違いでvisibilityhiddenにならない場合を考慮して、一定回数試行した後はvisibilityの状態に関わらずvisibleに変更するようにしておきます。

これらの処理を、setTimeoutを使って非同期ループ処理で実装します。
チェック間隔intervalは10msec、試行回数maxNumは20回にしてみます。

const interval = 10;
const maxNum = 20;

function loop(num) {
    setTimeout(function() {
        const style = window.getComputedStyle(recordGaia, null);
        if (num > maxNum || style.visibility === "hidden") {
            recordGaia.style.visibility = "visible";
        } else {
            loop(num + 1);
        }
    }, interval)
}

loop(0);

完成

ようやく完成しました。
最終的なコードは以下の通りです。

desktop.ja
(function() {
    "use strict";

    function hideRecordGaia() {
        const recordGaia = document.getElementById("record-gaia");
        if (recordGaia) {
            recordGaia.style.visibility = "hidden";
        }
    }

    kintone.events.on([
        "app.record.edit.submit.success"
    ], function(event) {
        hideRecordGaia();

        return event;
    });

    kintone.events.on([
        "app.record.detail.show",
        "app.record.edit.show"
    ], function(event) {
        const classNames = {
            "detail": ["gaia-argoui-app-menu-edit", "gaia-argoui-app-pager-prev", "gaia-argoui-app-pager-next"],
            "edit": ["gaia-ui-actionmenu-cancel"]
        };
        const eventType = event.type.replace(/^app\.record\.([^.]+)\.show$/, "$1");

        classNames[eventType].forEach(function(className) {
            const elms = document.getElementsByClassName(className);
            if (elms.length === 1) {
                elms[0].addEventListener("click", hideRecordGaia, false);
            }
        });

        return event;
    });

    kintone.events.on([
        "app.record.create.show",
        "app.record.edit.show",
        "app.record.detail.show",
        "app.record.print.show"
    ], function(event) {
        const recordGaia = document.getElementById("record-gaia");
        if (recordGaia) {
            const interval = 10;
            const maxNum = 20;

            function loop(num) {
                setTimeout(function() {
                    const style = window.getComputedStyle(recordGaia, null);
                    if (num > maxNum || style.visibility === "hidden") {
                        recordGaia.style.visibility = "visible";
                    } else {
                        loop(num + 1);
                    }
                }, interval)
            }

            loop(0);
        }

        return event;
    });
})();
desktop.css
#record-gaia
{
    visibility: hidden;
}

おわりに

実際の運用ではレコード一覧画面でも同様の処理を行った方が良いですのが、一覧画面は一覧画面で色々と検討すべき点があり長くなるので本記事では割愛します。


なお、最後に一つ注意点があります。

今回の方法ではCSSを使ってレコード全体を非表示にしているため、当然ですがスクリプトが実行されないとレコードの中身が表示されません。
そして、kintoneは仕様上、複数登録したスクリプトのうちどれか一つでエラーが発生すると他のスクリプトの実行も停止してしまいます。

他にもスクリプトやプラグインを登録している場合は、そちらでスクリプトエラーが発生した時にもレコードが表示されなくなってしまいますので、使用する際は注意してください。

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

nuxt.jsインストール手順備忘録

nuxt.jsインストール備忘録

  • バージョン 2.12.1

  • CDでインストールするディレクトリに移動
  • 以下のコマンドでインストール
  • project-name はプロジェクトの名前を入れる

npxを使う場合

npx create-nuxt-app <project-name>

yarnを使う場合

yarn create nuxt-app <project-name>

? Project name

? Project name xxx
  • プロジェクト名を入力

? Project description

  • 説明を入力
? Project description xxxxxx

Author name

  • 作者の名前を入力
? Author name xxx

Choose programming language

  • TypeScriptを使うか選択
? Choose programming language (Use arrow keys)
❯ JavaScript
  TypeScript

Choose the package manage

  • YarnとNpmのどちらかを使うか選択
? Choose the package manager (Use arrow keys)
❯ Yarn
  Npm

Choose UI framework

  • UIフレームワーク使うか選択
? Choose UI framework (Use arrow keys)
❯ None
  Ant Design Vue
  Bootstrap Vue
  Buefy
  Bulma
  Element
  Framevuerk
  iView
  Tachyons
  Tailwind CSS
  Vuesax
  Vuetify.js

Choose custom server framework

  • サーバーサイドのフレームワークを選択
? Choose custom server framework (Use arrow keys)
❯ None (Recommended)
  AdonisJs
  Express
  Fastify
  Feathers
  hapi
  Koa
  Micro

Choose the runtime for TypeScript

? Choose the runtime for TypeScript (Use arrow keys)
❯ Default
  @nuxt/typescript-runtime

Choose Nuxt.js module

  • Axios と PWA を使うか選択
? Choose Nuxt.js modules (Press <space> to select, <a> to toggle all, <i> to inv
ert selection)
❯◯ Axios
 ◯ Progressive Web App (PWA) Support
 ◯ DotEnv

Choose linting tools

  • 静的解析の選択
? Choose linting tools (Press <space> to select, <a> to toggle all, <i> to inver
t selection)
❯◯ ESLint
 ◯ Prettier
 ◯ Lint staged files
 ◯ StyleLint

Choose test framework

  • ユニットテストの選択
? Choose test framework (Use arrow keys)
❯ None
  Jest
  AVA

Choose rendering mod

  • SSRかSPAの選択
? Choose rendering mode (Use arrow keys)
❯ Universal (SSR)
  Single Page App

Choose development tool

  • jsconfig.jsonSemantic Pull Requestを入れるか選択
  • jsconfig.jsonVS Code向けのツール
  • Semantic Pull RequestはGitHub向けのツール
? Choose development tools (Press <space> to select, <a> to toggle all, <i> to i
nvert selection)
❯◯ jsconfig.json (Recommended for VS Code)
 ◯ Semantic Pull Requests

インストール完了

スクリーンショット 2020-03-29 15.09.55.png

起動する

yarnの場合

yarn run dev

npmの場合

npm run dev

ビルド

  • dist 以下にファイルが出力される
yarn run build

generate

  • dist 以下にHTMLの静的ファイルが出力させる
yarn run generate

その他

Sasaを使う

  • Sassをインストール
npm install --save-dev node-sass sass-loader
  • vueファイルでlang="scssまたはlang="sassにするとSassが使える
<style lang="scss" scoped>

Pugやcoffeeも可能

Pug
npm install --save-dev pug@2.0.3 pug-plain-loader
<template lang="pug">
  h1.red Hello {{ name }}!
</template>
CoffeeScript
npm install --save-dev coffeescript coffee-loader
<script lang="coffee">
export default data: ->
  { name: 'World' }
</script>

コンポーネント以外の独自で作るCSSを読み込む

  • nuxt.config.js のcssの部分にファイル名を入れる
css: [
  '@/assets/css/main.scss'
],

ルーティング

  • this.$route でURLが取れる
  • リンクを貼るときはnuxt-link :to=""を使う
  • パラメーターで動的に使うには _id.vue のようにファイル名にアンダースコアを付ける
<nuxt-link :to="'/xxx">リンク</nuxt-link>
  • generateするときに吐き出したいファイルを指定する時はnuxt.config.jsに設定する
  • /controller/actionの時
generate: {
  routes: [
    '/controller/action'
  ]
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScript 画像ファイルを読み込んでcanvasに表示する

前書き

 自作WEBアプリで躓いたので思い出せるようにメモ。とりあえず機能のみを実装

環境

 ブラウザ:Google Chrome
 言語:JavaScript(ライブラリ、フレームワークの使用なし)

HTML

設定すること
・画像を読み込むボタン
・画像を表示するcanvas
・CSSとJavaScriptは個人的に分けたいので分けています

index.html
<DOCTYPE HTML5>
<html>
 <head>
  <!--CSSファイルの読み込み-->
  <link rel="stylesheet" type="text/css" href="./index.css">
 </head>
 <body>
  <!--ファイルの読み込み 拡張子をPNG, JPEGに設定-->
  <input type="file" id="selectFile" accept=".png,.jpeg">

  <!--canvasの描画-->
  <canvas id="canvas"><canvas>

  <!--.jsファイルの読み込み-->
  <script type="text/javascript" src="index.js"></script>
 </body>
</html>

CSS

index.css

canvas{
  width: 300px;
  height: 400px;
  border: solid 1px #000;
}


この時点での表示内容はこのような形
スクリーンショット 2020-03-29 19.36.41.png

JavaScriptで実装

実装する機能
・input type="file"のファイルの取得
・ファイルのURLの取得
・canvasのsrcにファイルのURLを設定

index.js
var selFile = document.getElementById('selectFile'); // input type="file"の要素取得
var c = document.getElementById('canvas'); // canvasの要素取得
var ctx = c.getContext('2d');

selFile.addEventListener("change", function(evt){
 var file = evt.target.files; // fileの取得
 var reader = new FileReader();

 reader.readAsDataURL(file[0]); // fileの要素をdataURL形式で読み込む

 // ファイルを読み込んだ時に実行する
 reader.onload = function(){
  var dataUrl = reader.result; // 読み込んだファイルURL
  var img = new Image(); // 画像

  img.src = dataUrl;

  // 画像が読み込んだ時に実行する
 img.onload = function() {
   // canvasに画像ソースを設定する
   ctx.drawImage(img, 0, 0);

   // 画像のサイズを設定する場合
   // ctx.drawImage(img, 0, 0, 300, 400); heightとwidthも合わせて設定可能
   }
  }
}, false);

実装結果

ファイルを選択するとcanvasに画像が表示されるようになる

スクリーンショット 2020-03-29 20.00.23.png

参考

・画像ファイルの読み込み(https://javascript.programmer-reference.com/js-canvas-draw/)
・画像をcanvasに表示する(https://www.pazru.net/html5/File/040.html)

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

[JavaScript] Canvasでアナログ時計を作る

CanvasとJSを使って、こんな感じのアナログ時計を作ります。


See the Pen
WNvLqRJ
by nzzzz (@non_123)
on CodePen.


HTMLとCSS

htmlのCanvas要素で、描画領域の指定をします。
タグにwidthやheight属性を指定しない場合の描画領域は、canvasのデフォルト値300px * 150pxとなります。

index.html
<div class="clock-wrap">
  <canvas class="clock" width="300" height="300"></canvas>
  <time class="timearea"></time>
</div>

時計の背景色などをお好みで整える。

index.css
.clock-wrap {
  margin: auto;
  width: 350px;
  height: 350px;
  background-color: #f2f2f2;
}
.clock {
  display: block;
  margin: auto;
}
.timearea {
  display: inline-block;
  width: 100%;
  text-align: center;
}

JavaScript

script.js
(function (d) {

    //canvas要素を取得
    const el = d.querySelector('.clock');

    //コンテキストを取得
    const ctx = el.getContext('2d');

    //時計描画と現在時刻表示の関数を、1000ミリ秒ごとに実行する
    setInterval(() => {
      activateClock(ctx);
      showCurTime('.timearea');
    }, 1000);

    //現在時刻を取得
    function _getCurTime() {
      const cur = new Date();
      const time = {
        hour : cur.getHours() % 12, //12時間制の数字
        hourOriginal : cur.getHours(), //24時間制の数字
        min :  cur.getMinutes(),
        sec : cur.getSeconds()
      };
      return time;
    }

    //現在時刻を表示
    function showCurTime(elm) {
        const insertArea = d.querySelector(elm);
        const h = _getCurTime().hourOriginal;
        const m = _getCurTime().min;
        const s = _getCurTime().sec;
        const msg = `${h}${m}${s}`;

        insertArea.innerHTML = msg;
    }

    //時計を描画
    function activateClock(ctx, time) {

      //背景の円を描画
      ctx.beginPath();
      ctx.arc(150, 150, 115, 0, 2 * Math.PI); //円のパスを設定 ・・・補足1
      ctx.fill(); //円のパスを塗りつぶす

      //目盛を描画 ・・・補足2
      for (let i = 0; i < 60; i++) {
          let r = 6 * Math.PI / 180 * i; 
          const w = i % 5 === 0 ? 4 : 1; 
          _setCtxStyle(ctx, 'black', 'white', w); 
          _drawCtx(ctx, r, 100, 4); 
      }

      //現在時刻を定数に代入
      const h = _getCurTime().hour;
      const min = _getCurTime().min;
      const sec = _getCurTime().sec;

      //短針を描画 ・・・補足3
      const hourR = h * 30 * Math.PI / 180 + min * 0.5 * Math.PI / 180;
      _setCtxStyle(ctx, '', 'pink', 3);
      _drawCtx(ctx, hourR, 0, -60);

      //長針を描画 ・・・補足3
      const minR = min * 6 * Math.PI / 180;
      _setCtxStyle(ctx, '', 'yellow', 3);
      _drawCtx(ctx, minR, 0, -90);

      //秒針を描画 ・・・補足3
      const secR = sec * 6 * Math.PI / 180;
      _setCtxStyle(ctx, '', 'gray', 1);
      _drawCtx(ctx, secR, 0, -70);

    }

    //コンテキストの描画スタイルを設定する関数
    function _setCtxStyle(ctx, fillColor, strokeColor, lineWidth) {
      ctx.fillStyle = fillColor;
      ctx.strokeStyle = strokeColor;
      ctx.lineWidth = lineWidth;
      ctx.lineCap = 'round';
    }

    //線を描画する関数 ・・・補足4
    function _drawCtx(ctx, rotation, moveToY = 0, length) {
      ctx.save();
      ctx.translate(150, 150);
      ctx.rotate(rotation);
      ctx.beginPath();
      ctx.moveTo(0, moveToY);
      ctx.lineTo(0, moveToY + length);
      ctx.stroke();
      ctx.restore();
    }

})(document);

補足1:円のパスを設定

script.js
ctx.arc(150, 150, 115, 0, 2 * Math.PI);

円のパスは、.arc() メゾットで設定できます。

.arc(x, y, radius, startAngle, endAngle, anticlockwise);  
  • 座標x,yを中心にして、radiusの値を半径として、startAngle度からendAngle度まで描画する。
  • anticlockwiseにtrueを指定すると、反時計回りになる。

startAngle、endAngleの角度は、「ラジアン」という、「360°を2πとして表す単位」に変換する必要があります。

ラジアン = 度数 × π ÷ 180
 ↓ これをJSで表すと、
ラジアン = 度数 * Math.PI / 180;

今回は360°をラジアンに変換したいので、ラジアン = 360 * Math.PI / 180 → 2 * Math.PI となったわけです。
また、.arc()メゾットはパスを描くだけのメゾットなので、描画には.fill().stroke()を実行する必要があります。

補足2:目盛部分

script.js
//目盛を描画 
for (let i = 0; i < 60; i++) {
  let r = 6 * Math.PI / 180 * i; //①
  const w = i % 5 === 0 ? 4 : 1; //②
  _setCtxStyle(ctx, 'black', 'white', w); //③
  _drawCtx(ctx, r, 100, 4); //④
}

目盛描画のパーツとしては、2つあります。
1. 目盛をつける位置の設定と描画 ・・・①④
2. 目盛の見た目を設定 ・・・②③

目盛をつける位置の設定と描画

①で、④の関数 _drawCtx() 内の .rotate() メゾットで使う回転軸の角度を求めています。
ここでの角度も、「補足1:円のパスを設定」で説明した「ラジアン」で指定します。

関数 _drawCtx() 内の .translate(x, y) メゾットで決めた回転軸の位置を変えずに、rラジアンずつ回転させて目盛位置を決めて、 .moveTo(x, y) .lineTo(x, y) メゾットで目盛線を描画しています。

New Project (2).jpg

目盛の見た目を設定

②では、時刻を表す目盛を求めて目盛の太さを設定し、③の関数 _setCtxStyle() 内の .lineWidth プロパティに渡しています。

i % 5 === 0 ? 4 : 1; 
↓
i / 5 の余りが0なら、太さは4、それ以外なら1

補足3:針の描画

script.js
//短針を描画
const hourR = h * 30 * Math.PI / 180 + min * 0.5 * Math.PI / 180; //・・・①
_setCtxStyle(ctx, '', 'pink', 3); //・・・②
_drawCtx(ctx, hourR, 0, -60);  //・・・③

こちらも、仕組みとしては「補足2:目盛部分」と同じです。
1. 目盛をつける位置の設定と描画 ・・・①③
2. 目盛の見た目を設定 ・・・②

短針の場合は、1時間で進む角度に加えて、1分間で進む角度も足すことで、よくある時計のようにじわじわと次の時刻に短針が近づいていくようにしています。

補足4:線を描画する関数

script.js
//線を描画する関数
function _drawCtx(ctx, rotation, moveToY = 0, length) {
  ctx.save(); //・・・①

  //②
  ctx.translate(150, 150);
  ctx.rotate(rotation);
  ctx.beginPath();
  ctx.moveTo(0, moveToY);
  ctx.lineTo(0, moveToY + length);
  ctx.stroke();

  ctx.restore(); //・・・③
}

線を描画する関数の中身ですが、①③が肝になります。
①の .save() メゾットで初期設定を保存し、②で描画したのち、③の .restore() メゾットで①が実行された時の状態に戻しています。

針の描画の呼び出しは1回ずつですが、目盛の描画時はループ処理で複数回関数を呼び出しているので、.save() .restore() の処理が必要になります。

参考にしたもの

こちらの本でcanvas基礎から学ばせてもらいました。ありがとうございました。
KindleUnlimitedだと追加料金なしで読めるのでおすすめです。
ゲームで学ぶJavaScript入門 HTML5&CSSも身につく!

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

Concurrent Mode時代のReact設計論 (3) SuspenseやuseTransitionが何を解決するか

この記事は「Concurrent Mode時代のReact設計論」シリーズの3番目の記事です。

シリーズ一覧

SuspenseuseTransitionが何を解決するか

前回までは、PromiseをthrowしてSuspenseがキャッチするというConcurrent Modeの特徴、そして「非同期処理そのもの(Promise)をステートで管理する」という設計指針において欠かせない部品であるuseTransitionについて見てきました。

useTransitionは「2つのステートを同時に扱う」という斬新な概念を導入しました。そうまでしてConcurrent Modeが「Promiseをステートで管理する」という設計を貫く理由はおもに3つあると考えられます。まず非同期処理にまつわるロジックを分割するため、そして非同期処理をより宣言的に扱うためです。最後に、これは公式ドキュメントでも強調されていることですが、render-as-you-fetchパターンの実現です。ここからは、この3つを達成するためにどのような設計が必要かについて議論します。

前回出てきた「画面Aから画面Bに遷移するためにデータを読み込んでいる間は、画面Aに留まって読み込み中の表示にしたい」というシチュエーションについて再考してみます。従来(Concurrent Modeより前)の考え方では、画面Bへの遷移は2つの段階に分割できます。すなわち、「画面B用のデータをロード中の段階」と「ロードが終わって画面Bをレンダリングする段階」です。

この指針に基づいて作った従来型の実装をまず考えてみます。

非同期処理を含む画面遷移の従来型実装

画面Aと画面Bという2つの画面が存在しますから、今どちらの画面かといったステートを司る存在が必須です。とりあえずこれをRootと呼びましょう。画面Bは前回から例に出てきているUser[]型のデータを表示するとすると、Rootはこんな感じで定義できます。

type AppState =
  | {
      page: "A";
    }
  | {
      page: "B";
      users: User[];
    };

export const Root: FunctionComponent = () => {
  const [state, setState] = useState<AppState>({
    page: "A"
  });
  const goToPageB = () => {
    fetchUsers().then(users => {
      setState({
        page: "B",
        users
      });
    });
  };

  if (state.page === "A") {
    return <PageA goToPageB={goToPageB} />;
  } else {
    return <PageB users={state.users} />;
  }
};

Rootコンポーネントの最後に注目すると、今画面AにいるときはPageAをレンダリングし、画面BにいるときはPageBをレンダリングするようになっています。画面Aは画面Bに行くボタンを持っている想定なのでgoToPageBという関数をpropsで受け取ります。一方の画面BはUser[]を表示するのでUser[]をpropsで受け取ります。goToPageBが呼ばれた場合、fetchUsers()が完了するまでは現在の画面にとどまり、完了し次第setStateにより画面Bを表示という実装です。

PageAの実装はこんな感じになりますね。

const PageA: FunctionComponent<{
  goToPageB: () => void;
}> = ({ goToPageB }) => {
  const [isLoading, setIsLoading] = useState(false);
  return (
    <p>
      <button
        disabled={isLoading}
        onClick={() => {
          setIsLoading(true);
          goToPageB();
        }}
      >
        {isLoading ? "Loading..." : "Go to PageB"}
      </button>
    </p>
  );
};

画面Aは「画面B用のデータを読み込み中はローディング中の表示にする」というロジックのためにisLoadingステートを持っています。それ以外は特筆すべき点はありませんね。このステートをPageAの内部に持つか、それとも前述のAppStateの一部にするかは一考の余地がありますが、どちらも一長一短です。

この設計では、「画面Bのデータをロード中の段階」は、PageAisLoadingステートがtrueになり、RootfetchUsers()の結果を待っている段階として現れます。そして、「ロードが終わって画面Bをレンダリングする段階」はRootsetStateでステートを変更して画面Bをレンダリングする部分に対応しています。

従来型設計の欠点と限界

この設計(従来型設計)で注目すべきは、ページ遷移に係るロジックがRootに集約されているという点です。ページ遷移というのはそもそもページ横断的なロジックなので、Rootが一枚噛んでいることは不自然ではありません。

しかし、「画面B用のデータを待つ」という機能をPageBではなくRootが担っている点が残念です。今回のように単純なパターンならば大きな問題にはなりませんが、Reactが提唱する「render-as-you-fetch」パターンを実装したいときに問題となります。また、細かいことをいえば、「fetchUsers()の結果が帰ってきたらsetStateする」という処理は命令的な書き方であり、宣言的にUIを記述する流れに逆行しています。

ここで登場したrender-as-you-fetchパターンとは何かというと、複数のデータを表示してロードする際に、ロードできた部分から順次表示していくというパターンです。なるべく早く情報を表示するという目的のためにこの戦略が取られることもあるでしょう。そして明らかに、これを実現するには「データを待つ」という部分が画面Bの中で制御される必要があります。上述の「データがロードされるまで画面Bに制御を渡さない」という設計はこれと明らかに逆行しています。

さらに、これと上記の要件を組み合わせると、「画面Bのメインのデータがロードできるまでは画面Aに留まるが、それ以外のデータがまだでも画面Bに遷移して良い」みたいな仕様が誕生するかもしれません。これをそのまま実現しようとすると、データローディングのロジックがRoot内と画面B内に分割され、設計が壊滅的状況に陥ります。

すぐに思い当たる解決策は「メインのデータのみRootで読み込んで、それ以外のデータは画面Bがレンダリングされた後にuseEffectなり何なりから別途非同期処理を発火して読み込む」というものです。しかし、これには「メイン以外のデータの読み込みが画面Bがレンダリングされるまで始まらない」という致命的な問題があります。最近のWebアプリケーションにとってパフォーマンスは命なので、たかだか設計の都合程度の理由でデータ読み込み開始を送らせていいわけがありません。

ということで、ベストなUXを追求しようとすれば、手続き的なロジックにまみれた壊滅的な設計ができあがります。Concurrent Modeはこの状況に一石を投じました。

Concurrent Mode時代のデータローディング設計

前項で挙がった問題を纏めると、データを待つというロジックをRootが握っていることロジックが手続き的であること、そしてrender-as-you-fetchパターンが困難であることでした。

次は、これらの問題を解決するためのConcurrent Mode的設計パターンを見ていきます。まずRootはこのように書き換えられるでしょう。

type AppState =
  | {
      page: "A";
    }
  | {
      page: "B";
      usersFetcher: Fetcher<User[]>;
    };

export const Root: FunctionComponent = () => {
  const [state, setState] = useState<AppState>({
    page: "A"
  });
  const goToPageB = () => {
    setState({
      page: "B",
      usersFetcher: new Fetcher(() => fetchUsers())
    });
  };
  return (
    <Suspense fallback={null}>
      <Page state={state} goToPageB={goToPageB} />
    </Suspense>
  );
};

const Page: FunctionComponent<{
  state: AppState;
  goToPageB: () => void;
}> = ({ state, goToPageB }) => {
  if (state.page === "A") {
    return <PageA goToPageB={goToPageB} />;
  } else {
    return <PageB usersFetcher={state.usersFetcher} />;
  }
};

まずRoot内に目を向けると、fetchUsers()new Fetcher()の中に押し込まれました。これにより、goToPageBが持つロジックはステートを画面Bのものに更新するだけになりました。

新しくPageというコンポーネントができてstate.pageによる分岐がPageの中に入りましたが、これはページの外側にSuspenseを配置することが目的です。Suspenseコンポーネントをどこに配置すべきかは別途解説しますが、今回のようにページ遷移でサスペンドが発生するかもしれないときはページより外側に配置するのが適しています。いちいちgoToPageBを受け渡す必要があるのがダサいと思われるかもしれませんが、それはコンテキストなり何なりを使って解消できるのであまり本質的な問題ではありません。

続いて、PageAコンポーネントはこのようになります。

const PageA: FunctionComponent<{
  goToPageB: () => void;
}> = ({ goToPageB }) => {
  const [startTransition, isLoading] = useTransition({
    timeoutMs: 10000
  });
  return (
    <p>
      <button
        disabled={isLoading}
        onClick={() => {
          startTransition(() => {
            goToPageB();
          });
        }}
      >
        {isLoading ? "Loading..." : "Go to PageB"}
      </button>
    </p>
  );
};

isLoadinguseStateで宣言するのをやめてuseTransitionを使うようになりました。画面Bへの遷移(goToPageB())をstartTransitionで囲むことで、遷移時にサスペンドが発生したらボタンにLoadinng...が表示されるという制御がされています。

目ざとい方は、この設計は微妙だと思ったかもしれません。というのも、startTransitionは中でステートを更新することで意味を発揮する関数なのに、goToPageBという関数は「画面Bに遷移する」という抽象化された意味を持たされており、中でステートの更新が行われることが明らかではありません。今回はgoToPageBの実態がsetState({ ... })なので偶々うまくいっていますが、startTransitionsetStageという2つがセットで扱われないといけないことが設計に現れていないのがどうにも微妙です。

Reactの公式ドキュメントを読む限りはこれが大きな問題であるとは考えられていないようですが、個人的には改善の余地ありと感じるところです。

最後のPageBは特筆すべきところがありませんが、一応出しておきます。

const PageB: FunctionComponent<{
  usersFetcher: Fetcher<User[]>;
}> = ({ usersFetcher }) => {
  const users = usersFetcher.get();
  return (
    <ul>
      {users.map(({ id, name }) => (
        <li key={id}>{name}</li>
      ))}
    </ul>
  );
};

以上のコードでは、最初に述べた従来の設計の3つの問題が解消されています。まず、「データを待つというロジックをRootが握っていること」及び「ロジックが手続き的であること」については、Rootが持つロジックがsetStateだけになったことによって解消されました。画面Bがデータを待つという部分も、Suspenseの機能およびFetcherによって、手続き的な部分がReactの内部に隠蔽され、宣言的な書き方ができています。

最後の「render-as-you-fetchパターンが困難であること」については、この例が簡単なので現れていません。これについては次の記事で詳しく扱います。

まとめ

この記事では、ページ遷移という課題を例にとり、従来型の設計とConcurrent Mode時代の設計を比較し、Concurrent Modeによって従来存在した問題が解決できることを示しました。

尤も、何が問題で何か問題でないかということについて唯一解は存在しませんから、Concurrent Modeの視点からということにはなります。Reactはだんだんとopinionatedなライブラリの色を強くしてきていますから、この記事の内容に同意できなくてもそれは悪いことではありません。

この記事までが「Concurrent Mode時代のReact設計論」シリーズの前半です。前半ではConcurrent Modeの基礎を解説し、Concurrent Modeがどのような問題を解決したいのかについて示しました。

シリーズ後半では、Concurrent Modeを前提とした設計について議論します。先ほど少しだけ触れたように、この記事で出てきたConcurrent Modeのコードは従来の問題を解決しますが、これがベストな設計かどうかは疑う余地があります。次回以降の記事では、Concurrent Modeの恩恵をより受けるためにどのような設計がベストかについて考えていきます。

次の記事: 鋭意執筆中です。

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

Concurrent Mode時代のReact設計論 (2) useTransitionを活用する

この記事は「Concurrent Mode時代のReact設計論」シリーズの2番目の記事です。

シリーズ一覧

useTransitionを活用する

前回の記事ではConcurrent Modeの基礎的な機能と、それを扱うための考え方を説明しました。ボタンを押すとステートにFetcherが突っ込まれて、それにより再レンダリング・サスペンドが発生するという流れでした。

実は、その例ではサスペンドが発生した際に次のようなワーニングが発生します。

Warning: Container triggered a user-blocking update that suspended.

The fix is to split the update into multiple parts: a user-blocking update to provide immediate feedback, and another update that triggers the bulk of the changes.

Refer to the documentation for useTransition to learn how to implement this pattern.

これは、ボタンのonClickのようにユーザーの操作をきっかけとして、再レンダリング→サスペンドが発生したときに表示されるワーニングです。これが意味するところを噛み砕いて説明すると、「ユーザーの入力に対してはすぐにフィードバックを返すべきだから、サスペンドする(=新しいステートが表示されるまでに時間がかかる)のは良くない」ということです。

そして、このワーニングに対する対処法はずばりuseTransitionを使うことです。useTransitionを使うことで、ステートの更新でサスペンドが発生した場合に元々のステートを基にフィードバックを描画できるのです。

useTransitionの使用例

さっそく、先ほどの例にuseTransitionを追加してみましょう。useTransitionはユーザーへのフィードバックを念頭に置いた機能なので、ユーザーへのフィードバックとしてボタンを押したらローディング中はボタンがdisabledになるという実装を入れてみましょう。Containerをこのように変更します。

const Container: FunctionComponent = () => {
  // useTransitionの呼び出しを追加
  const [startTransition, isLoading] = useTransition({
    timeoutMs: 10000
  });
  const [usersFetcher, setUsersFetcher] = useState<
    Fetcher<User[]> | undefined
  >();

  return (
    <>
      <p>
        <button
          onClick={() => {
            // ステート更新をstartTransitionで囲む
            startTransition(() => {
              setUsersFetcher(new Fetcher(fetchUsers));
            });
          }}
          // isLoadingがtrueのときはdisabledに
          disabled={isLoading}
        >
          {isLoading ? "Loading..." : "Load Users"}
        </button>
      </p>
      <Suspense fallback={<p>Loading...</p>}>
        {usersFetcher ? <UserList usersFetcher={usersFetcher} /> : null}
      </Suspense>
    </>
  );
};

useTransitionはフックの一種なので、このように関数コンポーネントから呼び出します。結果はstartTransition関数とisLoading(真偽値)の組です。このstartTransitionはボタンのonClickハンドラの中で使われており、ステートの更新がstartTransitionで囲われています。startTransitionに渡されたコールバック関数は即座に呼び出されます。

この実装では、ボタンを押すと以下のスクリーンショットのような挙動となります。

screenshots-2.png

これを理解するために。useTransitionの挙動を簡単に説明します。startTransitionの内部で行われたステートの更新がサスペンドを発生させた場合、変更後ではなく変更前のステートがレンダリングされます。ただし、このとき変更前のステートではuseTransitionが返すisLoadingtrueになっています。投げられたPromiseが解決された場合は変更後のステートで再レンダリングされます。

useTransitionを使わない場合との違いはサスペンド中に現れます。useTransitionを使わない場合はSuspenseによるフォールバックが表示されますが、useTransitionを使う場合はフォールバックは表示されず、代わりにステート更新前の状態が(isLoadingtrueで)レンダリングされるのです。

useTransitionにオプションとして渡したtimeoutMsは、この「isLoadingtrueの状態」の最大持続時間を表します。この時間が過ぎてもPromiseが解決されなかった場合、諦めて変更後のステートがレンダリングされます。ただし、まだPromiseが解決されていないのでSuspenseによりフォールバックが表示されます。

ボタンがクリックされてからの流れは次のようになります。

  1. 初期状態では、usersFetcher=undefined, isLoading=falseである。(上のスクリーンショットの左の状態)
  2. startTransition内でsetUsersFetcherが呼ばれ、usersFetcherステートが更新される。(このときnew Fetcherで作られたオブジェクトをFとする)
  3. useTransitionの効果ににより、まずusersFetcher=undefined, isLoading=trueの状態でContainerがレンダリングされ、DOMに反映される。(上のスクリーンショットの真ん中の状態)
  4. 次に、新しいステート(usersFetcher=F, isLoading=false)でContainerがレンダリングされる。これはUserListのレンダリングに繋がり、UserListのレンダリングはサスペンドする。useTransitionの効果により、この状態はDOMに反映されない。
  5. Fが持つPromiseが解決されると、新しいステート(usersFetcher=F, isLoading=false)でContainerが再レンダリングされる。今回はサスペンドが発生せずにレンダリングが完了し、この状態がDOMに反映される。(スクリーンショットの右の状態)

ポイントは、useTransition内でステートの更新を行なった場合、新しいステートよりも「元のステート+isLoading=true」のレンダリングが優先されるということです。これは、isLoading=trueの状態でユーザーへのフィードバックを表すことを意図しているためです。ユーザーへのフィードバックは最優先で画面に反映されるべきであるため、これが最初に処理されます。

ちなみに、startTransitionの中と外の両方でステートの更新を行うことができます。この場合、startTransitionの外で行なった更新は3の段階で反映されています(もちろん5の段階にも反映されます)。

また、timeoutMsで設定した時間を超えない限り、Suspensefallbackで指定した内容は表示されなくなります。useTransitionをきちんと使っている限りは、Suspensefallbackはいわば最終防衛ラインのような扱いになり、高頻度でユーザーが目にするものではなくなります。

useTransitionの必要性

Concurrent Modeにおける設計ではPromiseをステートに持つことになると前回述べましたが、この立場ではuseTransitionの存在は必然的なものとなります。

そもそも、アプリの状態・画面表示といったものの変化は、Reactにおいてはステートの変化として表されます。ステートの変化によって起こることは再レンダリングです。そして、非同期処理によって発生するサスペンドは、再レンダリングの結果として起こります。

ということは、当然ながら、ステートを更新しないとサスペンドが発生しないということです。ステートを更新するということは、(Suspenseによるフォールバックになるかもしれませんが)新しい画面がレンダリングされるということであり、そうなると普通は古いステートは捨てられます。

しかし、これは時に問題となります。例えば、「画面Aから別の画面Bに遷移したい。ただし、画面Bを表示するには非同期処理によるデータの読み込みが必要」という場合を考えてみましょう。しかも、データの読み込み中は画面Aに留まって読み込み中の表示にしたいとします。このとき、非同期処理が完了し次第画面Bに遷移するようにするには、とにかく画面Bをレンダリングしてサスペンドさせる必要があります。しかし画面Bをレンダリングしてしまうと画面Aは消えてしまいます。

この問題に対して、useTransitionは「古い状態(画面A)と新しい状態(画面B)を同時に扱う」という方法で対処します。これはちょうど、gitでブランチを切って2つのバージョンのステートをメンテナンスするようなものです(Reactの公式ドキュメントでもこの例えが用いられています)。これによって、「まだ画面には反映されないけど新しいステートをレンダリングする」ということが可能になりました。

まとめ

この記事ではReactが発するワーニングをきっかけとしてuseTransitionを導入しました。Promiseをステートに入れるという設計方針をとったとき、useTransitionは欠かせない部品となります。

次回は、なぜそこまでしてPromiseをステートに入れたいのかについて議論します。

次の記事: Concurrent Mode時代のReact設計論 (3) SuspenseやuseTransitionが何を解決するか

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

Concurrent Mode時代のReact設計論 (2) `useTransition`を活用する

この記事は「Concurrent Mode時代のReact設計論」シリーズの2番目の記事です。

シリーズ一覧

useTransitionを活用する

前回の記事ではConcurrent Modeの基礎的な機能と、それを扱うための考え方を説明しました。ボタンを押すとステートにFetcherが突っ込まれて、それにより再レンダリング・サスペンドが発生するという流れでした。

実は、その例ではサスペンドが発生した際に次のようなワーニングが発生します。

Warning: Container triggered a user-blocking update that suspended.

The fix is to split the update into multiple parts: a user-blocking update to provide immediate feedback, and another update that triggers the bulk of the changes.

Refer to the documentation for useTransition to learn how to implement this pattern.

これは、ボタンのonClickのようにユーザーの操作をきっかけとして、再レンダリング→サスペンドが発生したときに表示されるワーニングです。これが意味するところを噛み砕いて説明すると、「ユーザーの入力に対してはすぐにフィードバックを返すべきだから、サスペンドする(=新しいステートが表示されるまでに時間がかかる)のは良くない」ということです。

そして、このワーニングに対する対処法はずばりuseTransitionを使うことです。useTransitionを使うことで、ステートの更新でサスペンドが発生した場合に元々のステートを基にフィードバックを描画できるのです。

useTransitionの使用例

さっそく、先ほどの例にuseTransitionを追加してみましょう。useTransitionはユーザーへのフィードバックを念頭に置いた機能なので、ユーザーへのフィードバックとしてボタンを押したらローディング中はボタンがdisabledになるという実装を入れてみましょう。Containerをこのように変更します。

const Container: FunctionComponent = () => {
  // useTransitionの呼び出しを追加
  const [startTransition, isLoading] = useTransition({
    timeoutMs: 10000
  });
  const [usersFetcher, setUsersFetcher] = useState<
    Fetcher<User[]> | undefined
  >();

  return (
    <>
      <p>
        <button
          onClick={() => {
            // ステート更新をstartTransitionで囲む
            startTransition(() => {
              setUsersFetcher(new Fetcher(fetchUsers));
            });
          }}
          // isLoadingがtrueのときはdisabledに
          disabled={isLoading}
        >
          {isLoading ? "Loading..." : "Load Users"}
        </button>
      </p>
      <Suspense fallback={<p>Loading...</p>}>
        {usersFetcher ? <UserList usersFetcher={usersFetcher} /> : null}
      </Suspense>
    </>
  );
};

useTransitionはフックの一種なので、このように関数コンポーネントから呼び出します。結果はstartTransition関数とisLoading(真偽値)の組です。このstartTransitionはボタンのonClickハンドラの中で使われており、ステートの更新がstartTransitionで囲われています。startTransitionに渡されたコールバック関数は即座に呼び出されます。

この実装では、ボタンを押すと以下のスクリーンショットのような挙動となります。

screenshots-2.png

これを理解するために。useTransitionの挙動を簡単に説明します。startTransitionの内部で行われたステートの更新がサスペンドを発生させた場合、変更後ではなく変更前のステートがレンダリングされます。ただし、このとき変更前のステートではuseTransitionが返すisLoadingtrueになっています。投げられたPromiseが解決された場合は変更後のステートで再レンダリングされます。

useTransitionを使わない場合との違いはサスペンド中に現れます。useTransitionを使わない場合はSuspenseによるフォールバックが表示されますが、useTransitionを使う場合はフォールバックは表示されず、代わりにステート更新前の状態が(isLoadingtrueで)レンダリングされるのです。

useTransitionにオプションとして渡したtimeoutMsは、この「isLoadingtrueの状態」の最大持続時間を表します。この時間が過ぎてもPromiseが解決されなかった場合、諦めて変更後のステートがレンダリングされます。ただし、まだPromiseが解決されていないのでSuspenseによりフォールバックが表示されます。

ボタンがクリックされてからの流れは次のようになります。

  1. 初期状態では、usersFetcher=undefined, isLoading=falseである。(上のスクリーンショットの左の状態)
  2. startTransition内でsetUsersFetcherが呼ばれ、usersFetcherステートが更新される。(このときnew Fetcherで作られたオブジェクトをFとする)
  3. useTransitionの効果ににより、まずusersFetcher=undefined, isLoading=trueの状態でContainerがレンダリングされ、DOMに反映される。(上のスクリーンショットの真ん中の状態)
  4. 次に、新しいステート(usersFetcher=F, isLoading=false)でContainerがレンダリングされる。これはUserListのレンダリングに繋がり、UserListのレンダリングはサスペンドする。useTransitionの効果により、この状態はDOMに反映されない。
  5. Fが持つPromiseが解決されると、新しいステート(usersFetcher=F, isLoading=false)でContainerが再レンダリングされる。今回はサスペンドが発生せずにレンダリングが完了し、この状態がDOMに反映される。(スクリーンショットの右の状態)

ポイントは、useTransition内でステートの更新を行なった場合、新しいステートよりも「元のステート+isLoading=true」のレンダリングが優先されるということです。これは、isLoading=trueの状態でユーザーへのフィードバックを表すことを意図しているためです。ユーザーへのフィードバックは最優先で画面に反映されるべきであるため、これが最初に処理されます。

ちなみに、startTransitionの中と外の両方でステートの更新を行うことができます。この場合、startTransitionの外で行なった更新は3の段階で反映されています(もちろん5の段階にも反映されます)。

また、timeoutMsで設定した時間を超えない限り、Suspensefallbackで指定した内容は表示されなくなります。useTransitionをきちんと使っている限りは、Suspensefallbackはいわば最終防衛ラインのような扱いになり、高頻度でユーザーが目にするものではなくなります。

useTransitionの必要性

Concurrent Modeにおける設計ではPromiseをステートに持つことになると前回述べましたが、この立場ではuseTransitionの存在は必然的なものとなります。

そもそも、アプリの状態・画面表示といったものの変化は、Reactにおいてはステートの変化として表されます。ステートの変化によって起こることは再レンダリングです。そして、非同期処理によって発生するサスペンドは、再レンダリングの結果として起こります。

ということは、当然ながら、ステートを更新しないとサスペンドが発生しないということです。ステートを更新するということは、(Suspenseによるフォールバックになるかもしれませんが)新しい画面がレンダリングされるということであり、そうなると普通は古いステートは捨てられます。

しかし、これは時に問題となります。例えば、「画面Aから別の画面Bに遷移したい。ただし、画面Bを表示するには非同期処理によるデータの読み込みが必要」という場合を考えてみましょう。しかも、データの読み込み中は画面Aに留まって読み込み中の表示にしたいとします。このとき、非同期処理が完了し次第画面Bに遷移するようにするには、とにかく画面Bをレンダリングしてサスペンドさせる必要があります。しかし画面Bをレンダリングしてしまうと画面Aは消えてしまいます。

この問題に対して、useTransitionは「古い状態(画面A)と新しい状態(画面B)を同時に扱う」という方法で対処します。これはちょうど、gitでブランチを切って2つのバージョンのステートをメンテナンスするようなものです(Reactの公式ドキュメントでもこの例えが用いられています)。これによって、「まだ画面には反映されないけど新しいステートをレンダリングする」ということが可能になりました。

まとめ

この記事ではReactが発するワーニングをきっかけとしてuseTransitionを導入しました。Promiseをステートに入れるという設計方針をとったとき、useTransitionは欠かせない部品となります。

次回は、なぜそこまでしてPromiseをステートに入れたいのかについて議論します。

次の記事: Concurrent Mode時代のReact設計論 (3) SuspenseやuseTransitionが何を解決するか

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

Concurrent Mode時代のReact設計論 (1) Concurrent Modeにおける非同期処理

Concurrent Modeは、現在(2020年3月)実験的機能として公開されているReactの新しいバージョンです。Reactの次のメジャーバージョン(17.x)で正式リリースされるのではないかと思っていますが、確証はありません。なお、React公式からもすでに結構詳細なドキュメントが出ています。

Concurrent Modeに適応したアプリケーションを作るためには、従来とは異なる新しい設計が必要となります。筆者はConcurrent Modeを使ったアプリケーションをひとつ試作してみました。この記事から始まる「Concurrent Mode時代のReact設計論」シリーズでは、ここから得た知見を共有しつつ、Concurrent Mode時代に適応したReactアプリケーションの設計を提案します。

なお、Concurrent Modeはまだ正式リリース前の機能です。今後正式リリースまでの間にAPIの変更などが発生してこの記事の内容が当てはまらなくなる可能性は否定できませんが、その際はご容赦ください。

ちなみに、作ったアプリケーションはこれです。(宣伝)

プルリクエストも大募集しています。問題の追加はConcurrent Modeを理解していなくても大丈夫です。(宣伝)

シリーズ一覧

現在は(3)まで公開済です。

イントロダクション

Concurrent ModeにおいてはReactの内部の実装が変更され、レンダリングの中断・再開をサポートするようになります。これにより、ユーザーの入力により素早く反応するなど、ReactアプリケーションのUX向上が期待できます。

Concurrent Modeは、useTransitionに代表される新しいAPIを搭載しており、Concurrent Modeを完全に活かすには新しいAPIを使いこなさなければいけません。useTransitionについては筆者の以前の記事が詳しいので、気になる方は合わせてお読みください。この記事の理解に必須ではありません。

冒頭で述べた通り、このシリーズでは筆者がConcurrent Modeを試してみた経験を基にして、Concurrent Mode時代に適応したReactアプリケーションの設計を提案します。もちろんこれが唯一解であると主張したいわけではありませんが、最も基本的な考え方として通用するものだと考えています。

なお、このシリーズではステート管理やデータフェッチング用の外部ライブラリを使わない、最も基本的なConcurrent Mode向け設計を議論します。これから先Concurrent Modeに適応したライブラリが増えることと思いますが、そのライブラリを使う場合はまた異なる設計となるかもしれない点はご了承ください。まあライブラリを使うかどうかで設計が変わるのは当たり前の話ですが。

なお、実際に手を動かしながら読みたいという方向けに、TypeScript + React Concurrent Modeの設定がしてあるCodeSandboxを用意してあります。適当にいじって試してみましょう。

非同期処理の扱い方が変わる

React Concurrent Modeの最大の特徴として「Promiseをthrowする」という衝撃的な仕様のみを知っていたという方も多いでしょう。Promiseというのは、非同期処理を表すのに非常に広く使われるオブジェクトです。

レンダリング時にPromiseをthrowするには、コンポーネントがPromiseを持っている必要があります。コンポーネントがPromiseを持つ場合の選択肢は主にステートに持つ(useStateとか)かrefで持つ(useRef)のどちらかです。もちろんpropsやuseContextで受け取ることもできますが、それは親のコンポーネントが何らかの手段でPromiseを調達しているので本質的にはやはり前記のどちらかです。

一般に、レンダリング結果に関わるものをuseRefで持つのは良くありません(後述しますが、Concurrent Modeではこれまで以上にこれを厳守する必要があります)。よって、Promiseをステートに持つことが必要になります。ただ、実際には生のPromiseでは機能不足なので、適当なラッパーを作ることになります(あとで具体例が出てきます)。

Promiseをステートに持つことで、コンポーネントは「非同期処理の途中」というステートをもはや表現する必要がなくなります。それは「レンダリングの中断(サスペンド)」で表せば良いのですから。つまり、例えば「データがあればロード済、データが無ければロード中」のようなロジックをコンポーネントが持つことは無くなります。

言い換えれば、コンポーネントはデータがロード中の場合の処理を気にする必要が無くなります。ただし、実際には「レンダリングの中断」の場合を別の場所(Suspenseのフォールバック、あるいはuseTransitionのトランジション中状態)でハンドリングする必要がありますから、非同期処理について全く考えなくていいわけではありません。その意味では、より正確に言えばConcurrent Modeは非同期処理の扱いをより疎結合に表現する手段を提供してくれるというところでしょう。従来我々が手ずから扱っていた非同期処理対応の一部分を、Reactが組み込みの機能として受け持ってくれるという見方もできます。

Concurrent Modeにおける非同期処理

では、改めてConcurrent Modeにおける非同期処理について説明します。

Concurrent Modeでは、コンポーネントがPromiseを投げることでサスペンド(レンダリングの中断)を表すことができます。その場合、当該のPromiseが解決されたら再度レンダリングが試みられます。まだ、サスペンドが発生したときに代替のビューを提供する機能が提供されます(SuspenseuseTransition)。

これらの機能を使うことで、Concurrent Modeではより宣言的に非同期処理を扱えるようになったと言えます。ただし、同時にこの機能はReactと非同期処理をより密結合なものにするという側面を持ち合わせています。その意味で、ReactやConcurrent Modeでよりopinionatedなライブラリになったと言えます。

まずは、Concurrent Modeにおける基本的な非同期処理の例を示します。例を通してConcurrent Modeの感覚を掴みましょう。

まず、先ほど少し言及したPromiseのラッパーを定義します。

PromiseをラップするFetcher<T>

Fetcher<T>という名前は我ながら微妙な気がするのですが、いい命名が思いつかないので募集中です。Fetcher<T>は内部にPromiseを持っており、さらに現在Promiseが現在どういう状態なのか(State<T>)を知っています。これにより、「Promiseがまだ解決されていなかったらそのPromiseを投げる」という、Promiseの現在の状態に基づく分岐を実装しています。

type State<T> =
  | {
      state: "pending";
      promise: Promise<T>;
    }
  | {
      state: "fulfilled";
      value: T;
    }
  | {
      state: "rejected";
      error: unknown;
    };

このState<T>型はPromiseの3つの状態(解決前、成功、失敗)を表現する型です。解決前の場合はそのPromiseを、成功済みの場合は結果の値(T型)を、そして失敗の場合はエラーの値を保持します。このState<T>を用いて書かれたFetcher<T>の実装は以下の通りです1

export class Fetcher<T> {
  private state: State<T>;
  constructor(fetch: () => Promise<T>) {
    const promise = fetch().then(
      value => {
        this.state = {
          state: "fulfilled",
          value,
        };
        return value;
      },
      error => {
        this.state = {
          state: "rejected",
          error,
        };
        throw error;
      },
    );
    this.state = {
      state: "pending",
      promise,
    };
  }

  public get(): T {
    if (this.state.state === "pending") {
      throw this.state.promise;
    } else if (this.state.state === "rejected") {
      throw this.state.error;
    } else {
      return this.state.value;
    }
  }
}

Fetcher<T>のコンストラクタはPromiseを返す関数を受け取ってすぐに呼び出します。ここで返されたPromiseの状態が監視され、this.stateに反映されます。

Fetcher<T>が唯一もつメソッドget()は、Promiseが解決済だった場合はその値を返します。まだ解決されていない場合はPromiseをthrowします。一応、Promiseが失敗していた場合はエラーを投げる処理も入れています。

ポイントは、getの返り値がT型になっている点です。Promiseをthrowして大域脱出するという荒技によって、getを呼んだ側は非同期処理の途中かどうかを意識しなくても良くなります。何せ、T型の値が返ってきているということはもうT型の値がある、つまり非同期処理の結果があるということなのですから。つまり、get()を呼んでT型の値を得たコンポーネントは、あたかも非同期処理がすでに完了しているかのように処理を進めればよいのです。まだ完了していなかった場合はPromiseが投げられてしまいますが、その場合はReactが頑張って処理してくれます。

React Hooksが登場した時に「Algebraic Effectだ」なんて騒がれもしましたが、それと根本的な思想は同じです。すなわち、Reactが裏で頑張ることでシンプルなAPIを外向きに提供しているのです。

また、これだけ単純なラッパーでも、Promiseを投げるという点ですでにReactと癒着しています。しかし、前述の利点を得るためにはこれは欠かせません。これが、冒頭で触れた「Reactと非同期処理がより密結合になる」ということの意味です。

Fetcherを使う例

Fetcherを使うコンポーネントは、例えばこんな見た目になります。

type User = { id: string, name: string };

const UserList: FunctionComponent<{
  usersFetcher: Fetcher<User[]>,
}> = ({ usersFetcher }) => {
  const users: User[] = usersFetcher.get();
  return (
    <ul>
      {users.map(({ id, name }) => (
        <li key={id}>{name}</li>
      ))}
    </ul>
  );
};

UserListコンポーネントは受け取ったFetcher<User[]>getメソッドをいきなり呼び出してUser[]を取得します。あとはそれを適当に表示するだけです。ここで、Fetcher<User[]>は「User[]型の結果を取得する非同期処理」そのものを表しています。get()メソッドは、「その結果を取得する。まだ取得できない場合は取得できるまでサスペンドする」という意味になります。

このUserListコンポーネントは例えば次のように使用できます(fetchUsersが実際にUser[]を取得する非同期処理を担当すると思ってください)。「Load Users」ボタンを押すとusersFetcherFetcher<User[]>のインスタンスが入ってUserListがレンダリングされます。なお、UserListはサスペンドする可能性があるので、このようにSuspenseで囲んでフォールバックコンテンツ(中でサスペンドが発生したときに代わりにレンダリングされる内容)を指定しておく必要があります。

なお、Suspenseの中身でサスペンドが発生した場合はSuspenseの中身全体がフォールバックコンテンツに置きかわります。そのため、Suspenseをどこに置くかは、レンダリングが中断した時にどこまでフォールバックコンテンツになってほしいかによって決めることになります。Suspenseがネストしていた場合は一番内側のSuspenseが反応します。

const Container: FunctionComponent = () => {
  const [usersFetcher, setUsersFetcher] = useState<
    Fetcher<User[]> | undefined
  >();

  return (
    <Suspense fallback={<p>Loading...</p>}>
      <p>
        <button
          onClick={() => {
            setUsersFetcher(new Fetcher(fetchUsers));
          }}
        >Load Users</button>
      </p>
      {usersFetcher ? <UserList usersFetcher={usersFetcher} /> : null}
     </Suspense>
  );
};

以上のようにして、実際に非同期処理を発生させて(fetchUsersを呼び出して)以降の流れが全部実装できました。これを実際に動作させると、非同期処理の途中は「Loading...」と表示されて読み込まれたらUserListの中身がレンダリングされます。

より具体的な流れとしては以下のことが発生しています。

  1. Container内でsetUsersFetcherが呼び出されることでusersFetcherステートにFetcherが入る。
  2. Containerが再レンダリングされてUserListがレンダリングされる。
  3. UserListがレンダリングされる(関数UserListが呼び出される)最中に、get()でPromiseがthrowされる(UserListがサスペンドする)。
  4. サスペンドが発生したので、Suspenseの中身として<p>Loading...</p>がレンダリングされる。
  5. しばらくしてusersFetcherが返したPromiseが解決される。
  6. ReactがPromiseの解決を検知し、以前サスペンドしたUserListが再レンダリングされる。
  7. 今回はget()がPromiseを投げない(解決済のため)のでUserListはサスペンドされずに描画される。

一応画面の動きを示しておくと、このようになります。

screenshots-1.png

従来の方式との比較

一応、従来の方式(Concurrent Modeより前の書き方)との比較を行なっておきます。一例ですが、素朴に書くならこんな感じでしょう。

const Container: FunctionComponent = () => {
  const [isLoading, setIsLoading] = useState(false);
  const [users, setUsers] = useState<User[] | undefined>();

  return (
    <>
      <p>
        <button
          onClick={() => {
            setIsLoading(true);
            fetchUsers().then(users => {
              setIsLoading(false);
              setUsers(users);
            });
          }}
        >
          Load Users
        </button>
      </p>
      {isLoading ? (
        <p>Loading...</p>
      ) : users ? (
        <UserList users={users} />
      ) : null}
    </>
  );
};

ロード中・ロード完了という状態を表すためにisLoadingというステートが新設されました(TypeScript wayでReactを書くで説明したようにこれはベストなステートの表現ではありませんが、今回の本質にはあまり関わりません)。ボタンがクリックされたときは、「ローディング状態をにする→非同期処理を発火→終わったら結果をステートに反映」というステップを踏みます。

Concurrent Modeに比べるとやはり複雑化しており、とくにContainerコンポーネントが非同期処理をハンドリングするためのロジックを内包するようになったのが気になります。これが非同期処理の辛い点であり、各種のライブラリが頑張って解決しようとしている点でもあります。

Concurrent Modeは、これに対して「非同期処理を表すオブジェクトそのものをステートに突っ込む」という斬新な解決策を提示しました。これは、非同期処理の扱いのつらい部分をサスペンドという機構に押し込むことで達成されています。

Concurrent Modeにおけるエラー処理

ここまでの例ではエラー処理を全く扱ってきませんでしたが、Concurrent Modeでは非同期処理に係るエラー処理も様変わりします。

というのも、非同期処理はPromiseで表されますが、Promiseというのは失敗(reject)する可能性があります。非同期処理におけるエラーはPromiseの失敗で表されます。では、throwしたPromiseが失敗したらどうなるのでしょうか。

答えは、Error Boundaryでキャッチされます。Error BoundaryはReact 16で導入された機能で、コンポーネントのレンダリング中にエラーが発生した場合にそれをキャッチしてエラー時のコンテンツをレンダリングできるものです。

従来は、非同期処理によるエラーはError Boundaryではキャッチされず、自前でハンドリングして必要なら自前でいい感じにUIに反映させるロジックを書く必要がありました。それは、非同期処理によって発生したエラーはレンダリング中に発生したエラーではないからです。

Concurrent ModeではPromiseをthrowするという機構によって非同期処理がレンダリングによって組み込まれますから、非同期処理によって発生したエラーもレンダリング中に発生したエラーとして扱われるのは自然なことです。

Error Boundaryは宣言的なエラー処理機構なので、Concurrent Modeでは非同期処理に対しても宣言的なエラー処理が可能になったということです。たいへん嬉しいですね。

まとめ

この記事では、Concurrent Modeの基礎である「Promiseをthrowする」という方針を実現するためにPromiseをステートに持って扱う方法について説明しました。これにより、より宣言的に非同期処理を扱えるようになると共に、エラー処理をError Boundaryの機構で統一的に扱えるようになりました。

次の記事: Concurrent Mode時代のReact設計論 (2) useTransitionを活用する


  1. 実際に上述のアプリで使われているバージョンではさらにgetOrUndefinedというメソッド(解決前だったらthrowするのではなくundefinedを返す)があるのですが、これが本質的に必要なのかは悩んでいます。設計力の不足により必要になってしまっただけかもしれません。 

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

常に動くLINEBOTにお引っ越し(レンタルサーバ+PHP編)

今回のモチベーション

前回、こちらの記事を参考にWikipedia APIを使った、調べものLINE botを作った。

前回の記事
https://qiita.com/shima-07/items/2322598ca5a40cfee47b

だが、

  • ngrokを立ち上げている時しか使えないから普段使えない。
  • いざ、ngrokを立ち上げるとアドレスが変わってしまうため、Messaging API settingsのwebhook URLを毎度変えないと動かない。

うーん。。。

ngrok立ち上げるのめんどくさい! 常に使えるようにしないと意味ないじゃん!

と思ったわけです。だから『常に動くようにしよう!』が今回の動機です。

今回やったこと

  • 1. まずは now を試してみた
  • 2. さくらのレンタルサーバでやることにした
  • 3. jsで書いていたものをPHPに書き直した

最終的にはPHP化してさくらサーバに載っけました。

1. まずは now を試してみた

このあたりを参考に進めてみる。

さすが良記事!サクサク進むぜと思いながら最後までいきデプロイ完了!
簡単だったなあと思いながら、Webhook URLに入れて「Verify」をクリック・・・・

image.png

う。。まあよくある。

(1時間ほど立ち向かう)
色々と試すが私の手におえないと判断して諦める。

2. さくらのレンタルサーバでやることにした

この記事を発見!
http://blog.hetabun.com/line-bot-php-sakura

これ通りやることで、「こんにちは」に対して「こんにちは!」と元気よく返してくれるボットができました。

3. jsで書いていたものをPHPに書き直した

ここからが本番。前回あげた下記jsのコードと同じような振る舞いをPHPで書いていく。
PHPももちろん初心者である。

再掲

server.js
'use strict';

const express = require('express');
const line = require('@line/bot-sdk');
const PORT = process.env.PORT || 3000;
// 追加
const axios = require('axios');

const config = {
    channelSecret: '作成したBOTのチャンネルシークレット',
    channelAccessToken: '作成したBOTのチャンネルアクセストークン'
};

const app = express();

app.get('/', (req, res) => res.send('Hello LINE BOT!(GET)')); //ブラウザ確認用(無くても問題ない)
app.post('/webhook', line.middleware(config), (req, res) => {

    //ここのif文はdeveloper consoleの"接続確認"用なので後で削除して問題ないです。
    if(req.body.events[0].replyToken === '00000000000000000000000000000000' && req.body.events[1].replyToken === 'ffffffffffffffffffffffffffffffff'){
        res.send('Hello LINE BOT!(POST)');
        console.log('疎通確認用');
        return; 
    }

    Promise
      .all(req.body.events.map(handleEvent))
      .then((result) => res.json(result));
});

const client = new line.Client(config);

function handleEvent(event) {
  if (event.type !== 'message' || event.message.type !== 'text') {
    return Promise.resolve(null);
  }

  let mes = ''
// console.log(event.message.text);
    if(event.message.text.indexOf('') > -1){
        // ?を含んでいる場合にはwikiで検索したものを出して、含んでない場合はurlを返す
    var str = event.message.text;
    var result = str.split( '' ).join( '' ); //?を取り除く処理
    mes = result + 'の説明:'; //wikiのbodyの前の一言
    getBody(event.source.userId,result); //wiki APIで取得できたらプッシュメッセージ

  }else{
    var result = event.message.text;
    mes = result + 'のURL:'; //wikiのurlの前の一言
    getUrl(event.source.userId,result); //wiki APIで取得できたらプッシュメッセージ
  }

  return client.replyMessage(event.replyToken, {
    type: 'text',
    text : mes 
});
}

const getBody = async (userId,word) => {
    const res = await axios.get('http://wikipedia.simpleapi.net/api?keyword='+ encodeURIComponent(word) + '&output=json');
    const item = res.data;
    // console.log(item); 

    await client.pushMessage(userId, {
        type: 'text',
        text: item[0].body,
    });
}
const getUrl = async (userId,word) => {
    const res = await axios.get('http://wikipedia.simpleapi.net/api?keyword='+ encodeURIComponent(word) + '&output=json');
    const item = res.data;
    // console.log(item); 
    await client.pushMessage(userId, {
        type: 'text',
        text: item[0].url,
    });
}

app.listen(PORT);
console.log(`Server running at ${PORT}`);

 処理の整理

  • LINEからのメッセージを受け取る
  • そのメッセージをWikipedia APIに渡して結果を受け取る
  • メッセージによってLINE側に返却するものを変える
    • ?があるときはurlを返す
    • ?がないときはbodyを返す

LINEからのメッセージを受け取る

参考にした記事中にあった下記の$text で取れているからそれはOK。

LINEからのメッセージ.php
//ユーザーからのメッセージ取得
$json_string = file_get_contents('php://input');
$jsonObj = json_decode($json_string);

$type = $jsonObj->{"events"}[0]->{"message"}->{"type"};
//メッセージ取得
$text = $jsonObj->{"events"}[0]->{"message"}->{"text"};
//ReplyToken取得
$replyToken = $jsonObj->{"events"}[0]->{"replyToken"};

そのメッセージをWikipedia APIに渡して結果を受け取る

ここが一番ハマった。
jsではres.data.item[0].body の構造で取れていたので、
同じノリで $value = $res->{"data"}->{"item"}[0]->{"body"}; のような書き方をして、、当然何も取れず。

結論、下記のような取り方でできた。

wikipediaAPIからURLやbody取得部分.php
        $keyword = mb_convert_encoding($text, "UTF-8", "auto");
        $res = file_get_contents('http://wikipedia.simpleapi.net/api?keyword=' . $keyword . '&output=json');

        $jsonwiki_decode = json_decode($res,true);
        // 0番目の物だけを抽出する。もっとたくさん抽出したいときはここを変更する
        $jsonwiki = $jsonwiki_decode[0];
        //欲しい項目だけの配列にする
        $wikidata = array(
            'url' => $jsonwiki["url"],
            'body' => $jsonwiki["body"]
        );
        $URL = $wikidata["url"];
        $body = $wikidata["body"]; 

流れとしては、

  • LINEから受け取ったキーワードをAPIのkeywordとして渡せるようにエンコードする
  • それをAPIに渡し$res として取得する
  • json_decodeして配列にする
  • そのキーワードに対しての一番先頭の回答を取得するため[0]を取得する
  • それに対して$wikidataとして必要な項目だけ取得する
  • LINEに返したいものは、$URL = $wikidata["url"] や $body = $wikidata["body"] として取得できる

補足

上記 $resにどんなものが入っているか見るために下記のようなものをページを作って見てました。( jsはconsole.log()で気軽に見えたけどphpではどうやってみたらいいかわからずわざわざこんなことしました。。。 )
image.png

check_data.php
<html>
<head>
<title> test </title>    
</head>

<body>
<form method="POST" action="show.php">
キーワード:
<input type="text" name="name" size="15" />
<input type="submit" name="submit" value="送信" />
</form> 

<?php
if($_REQUEST['submit'] != null){

    $input = $_REQUEST[name];
    //$textのなかに'?'が含まれている場合
    $text = str_replace('?', '', $input);
    $keyword = mb_convert_encoding($text, "UTF-8", "auto");
    $res = file_get_contents('http://wikipedia.simpleapi.net/api?keyword=' . $keyword . '&output=json');
    $jsonwiki_decode = json_decode($res,true);
    $jsonwiki = $jsonwiki_decode[0];
    $wikidata = array(
        'url' => $jsonwiki["url"],
        'body' => $jsonwiki["body"]
    );
    $URL = $wikidata["url"];
    $body = $wikidata["body"];

    print('$input: '.$input.'-----');
    print('$text: '.$text.'-----');
    print('$keyword: '.$keyword.'-----');
    print('$res: '.$res.'-----');
    print('$jsonwiki_decode :'.$jsonwiki_decode.'-----');
    print('$jsonwiki :'.$jsonwiki .'-----');
    print('$wikidata :'.$wikidata .'-----');
    print('URL: '.$URL.'-----');
    print('body: '.$body.'-----');  

 }
?>       
</body>
</html>

メッセージによってLINE側に返却するものを変える

  • ?がある場合はURLをLINEに返す。また、Wikipedia APIに渡すときには?を取り除く。
  • ?がない場合はBodyをLINEに返す。
分岐部分.php
    if(strpos($text,'?') !== false){
        //$textのなかに'?'が含まれている場合
        $text = str_replace('?', '', $text);
        $keyword = mb_convert_encoding($text, "UTF-8", "auto");
        $res = file_get_contents('http://wikipedia.simpleapi.net/api?keyword=' . $keyword . '&output=json');

        $jsonwiki_decode = json_decode($res,true);
        // 0番目の物だけを抽出する。もっとたくさん抽出したいときはここを変更する
        $jsonwiki = $jsonwiki_decode[0];
        //欲しい項目だけの配列にする
        $wikidata = array(
            'url' => $jsonwiki["url"],
            'body' => $jsonwiki["body"]
        );
        $URL = $wikidata["url"];
        $body = $wikidata["body"];

        // メッセージ部分
        $response_format_text = [
            "type" => "text",
            "text" => $URL
            ];

      }else{ // ?が含まれないときの処理
        $keyword = mb_convert_encoding($text, "UTF-8", "auto");
        $res = file_get_contents('http://wikipedia.simpleapi.net/api?keyword=' . $keyword . '&output=json');

        $jsonwiki_decode = json_decode($res,true);
        // 0番目の物だけを抽出する。もっとたくさん抽出したいときはここを変更する
        $jsonwiki = $jsonwiki_decode[0];
        //欲しい項目だけの配列にする
        $wikidata = array(
            'url' => $jsonwiki["url"],
            'body' => $jsonwiki["body"]
        );
        $URL = $wikidata["url"];
        $body = $wikidata["body"];      

        // メッセージ部分
        $response_format_text = [
            "type" => "text",
            "text" => $body
            ];
      }
    }

サンプル

全ソースコード

linebot.php
<?php

$accessToken = 'アクセストークン';

//ユーザーからのメッセージ取得
$json_string = file_get_contents('php://input');
$jsonObj = json_decode($json_string);

$type = $jsonObj->{"events"}[0]->{"message"}->{"type"};
//メッセージ取得
$text = $jsonObj->{"events"}[0]->{"message"}->{"text"};
//ReplyToken取得
$replyToken = $jsonObj->{"events"}[0]->{"replyToken"};

//メッセージ以外のときは何も返さず終了
if($type != "text"){
 exit;
}


if($type == "text"){
    if(strpos($text,'?') !== false){
        //$textのなかに'?'が含まれている場合
        $text = str_replace('?', '', $text);
        $keyword = mb_convert_encoding($text, "UTF-8", "auto");
        $res = file_get_contents('http://wikipedia.simpleapi.net/api?keyword=' . $keyword . '&output=json');

        $jsonwiki_decode = json_decode($res,true);
        // 0番目の物だけを抽出する。もっとたくさん抽出したいときはここを変更する
        $jsonwiki = $jsonwiki_decode[0];
        //欲しい項目だけの配列にする
        $wikidata = array(
            'url' => $jsonwiki["url"],
            'body' => $jsonwiki["body"]
        );
        $URL = $wikidata["url"];
        $body = $wikidata["body"];

        // メッセージ部分
        $response_format_text = [
            "type" => "text",
            "text" => $URL
            ];

      }else{ // ?が含まれないときの処理
        $keyword = mb_convert_encoding($text, "UTF-8", "auto");
        $res = file_get_contents('http://wikipedia.simpleapi.net/api?keyword=' . $keyword . '&output=json');

        $jsonwiki_decode = json_decode($res,true);
        // 0番目の物だけを抽出する。もっとたくさん抽出したいときはここを変更する
        $jsonwiki = $jsonwiki_decode[0];
        //欲しい項目だけの配列にする
        $wikidata = array(
            'url' => $jsonwiki["url"],
            'body' => $jsonwiki["body"]
        );
        $URL = $wikidata["url"];
        $body = $wikidata["body"];      

        // メッセージ部分
        $response_format_text = [
            "type" => "text",
            "text" => $body
            ];
      }
    }


$post_data = [
"replyToken" => $replyToken,
"messages" => [$response_format_text]
];

$ch = curl_init("https://api.line.me/v2/bot/message/reply");
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($post_data));
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
 'Content-Type: application/json; charser=UTF-8',
 'Authorization: Bearer ' . $accessToken
 ));
$result = curl_exec($ch);
curl_close($ch);

おわりに

APIから返ってきた値をいい感じで取ってくるところでだいぶハマりました。
JSONデータの扱い方にもっと慣れないとなあー

次はHerokuを使ったお引っ越しもやってみようと思います。

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

Catalinaのバージョンアップしたらrails sできなくなったけど、Node.jsのインストールで解決!

筆者の環境

macOS Catalina バージョン 10.15.4
使用言語:Ruby、JavaScript

エラー内容

不注意により、macOSがCatalina バージョン10.15.4に上がってしまった。

それから実装中のアプリでrails sすると、以下のエラーメッセージが表示されサーバーが起動しなくなった。

Could not find a JavaScript runtime. See https://github.com/rails/execjs for a list of available runtimes. (ExecJS::RuntimeUnavailable)

これで解決!

Node.jsをインストール

左側の12.16.1の方はインストールしても開けず、
右側(最新版)をインストールしたらrails s成功しました!

スクリーンショット 2020-03-29 19.12.24.png

番外編

他にも対処法はいくつかあり、therubyracerというGemのインストールでも解決できるようです。(参考リンクご参照ください)

現在、チーム開発中だったので、自分以外の複数端末への影響を考え今回はGem以外の方法を選択しました。

参考

にさせていただきました。ありがとうございます。
https://qiita.com/azusanakano/items/771dc9919f347de061d7

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

Dart における class とObject の使い方。

Dartにおける class と object の使い方

Classes

Creating a class (classを作成)

class Car {
  int numberOfWindows = 5; 

  void drive(){
    print('wheels start turning');
  }
}

Creating an Object from the Class (objectをclassから作成)

Car myCar = Car();

実際にclassとobjectを使ってCodeを書いてみましょう。

Practice Here

1.各プロパティに、デフォルト値を設定したclassを作成するパターン

void main(){
  // 2. class Carから, myNormalCar というObjectを作成。
  Car myNormalCar = Car(); 

  print(myNormalCar.numberOfSeat); // 4 
}

// 1. Car という classを作る。
class Car {
  // デフォルトとして、numberOfSeatを4, yearCreatedを2020とする。
  numberOfSeat = 4; 
  yearCreated = 2020;
}

もし、毎回classを作成する度に、違うプロパティ値を表示させたいなら... => パターン2へ

2.デフォルト値を設定せずに、classを作成するパターン(Constructorを使って)。

Constructors

void main(){
  Car myNormalCar = Car(5, 2019);

  Car hisCar = Car(2, 2016); 

  print(myNormalCar.numberOfSeat); // 5
  print(myNormalCar.yearCreated); // 2019
  print(hisCar.numberOfSeat); // 2
  hisCar.whatToDo('Turn Left.'); // Turn Left
}
// Car classを作成。
class Car {
  // 1. デフォルト値を設定しない。
  int numberOfSeat;
  int yearCreated;
  // 2. Constructor method を使う。
  Car(int numberOfSeat, int yearCreated){
    // numberOfSeat は、Carというclassを作成する時に入力するように指示(thisは、Carというclassを指している)。
    this.numberOfSeat = numberOfSeat;
    this.yearCreated = yearCreated;
  }

  // 4. Methodを付け加える。
  void function(String whatToDo){
    print(whatToDo);
  }
}

上のコードはさらに、簡潔に入力できる。

void main(){
  // 2. プロパティネームを入れずに、値のみ順番に入力すれば良い。
  Car myNormalCar = Car(5,2019);

  Car hisCar = Car(2,2016); 

  print(myNormalCar.numberOfSeat); // 5
  print(myNormalCar.yearCreated); // 2019
  print(hisCar.numberOfSeat); // 2

}
class Car {
  int numberOfSeat;
  int yearCreated;
  // {}を外す。
  Car(this.numberOfSeat, this.yearCreated);
}

3. 既製されたclassの役割を、新たなclassに組み込む。

1. まず、Car というclassを作成し、そのclassからnormalCarというObjectを作成。
// まず、作成されたclassから、Objectを作成。
void main(){
  Car normalCar = Car();

  print(normalCar.numberOfSeat); // 5
  normalCar.drive(); // wheels turn.

}

// まず、最初のclassを作成。
class Car {
  int numberOfSeat = 5; 

  void drive(){
    print('wheels turn.');
  }
}

2. Cαrというベースのclassを用いて、新たにElectricCar(Carに比べて、さらに機能を持つ)というclassを作成。

Sub classing with extends

void main(){
  Car normalCar = Car();

  print(normalCar.numberOfSeat); // 5
  normalCar.drive(); // wheels turn.

  ElectricCar myTesla = ElectricCar();
  myTesla.drive(); // wheels turn
  myTesla.recharge(); // 

}
class Car {
  int numberOfSeat = 5; 

  void drive(){
    print('wheels turn.');
  }
}

// Carより機能が多く搭載された、ElectricCarというclassを作成。
class ElectricCar extends Car {
  int batteryLevel = 100;

  void recharge(){ 
    batteryLevel = 100;
  }

}

3. 既製されたclassのメソッドの一部を書き換える。

void main(){
  // 新たなObjectを作成。
  LevitatingCar myMagLev = LevitatingCar();
  myMagLev.drive(); // glide forward
}

// 新たなclass(LevitatingCar: 既存のCarのメソッドを受け継ぐが、drive()が違う)を作成。
class LevitatingCar extends Car {

  // 既存の機能を書き換える
  @override 
  void drive(){ 
    print('glide forward');
  }
}

4. 既存のメソッド(ここでは drive())に、更なる機能を付け加える。

Super class calls with super

void main(){
  // 新たなObject myWaymoを作成。
  SelfDrivingCar myWaymo = SelfDrivingCar('Osaka');

  myWaymo.drive(); // wheels turn. sterring towards Osaka.
}

// 最初のベースとなるclassを作成。
class Car {
  int numberOfSeat = 5; 

  void drive(){
    print('wheels turn.');
  }
}

// Carの機能を持つ、さらにパワフルなclass SelfDrivingCarというclassを作成。
class SelfDrivingCar extends Car {
  String destination;

  SelfDrivingCar(String userSetDestination){
    destination = userSetDestination;
  }

  @override
  void drive(){
    // drive()の機能に、新たな機能を付け加える。
    super.drive();

    print('sterring towards $destination');
  }
}


参照: The Complete 2020 Flutter Development Bootcamp with Dart.
(https://www.udemy.com/course/flutter-bootcamp-with-dart/learn/lecture/14483538#questions)

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

JavaScript

//グローバル変数
var gbl = "grobal"

//変数
let x = 1

//定数
const T = "ABC"

//テンプレートリテラル
let t = `テンプレート${x}`

//オブジェクト
const obj= {key: "value"}

//配列
const arr = [1, 2, 3, 4]

//配列操作
arr[0]
arr.push(5)
arr.pop()
arr.unsift(0)
arr.shift()
arr.splice(0, 1, 10)
arr.fill(0)
arr.sort()
arr.reverse()

//日付
const d = mew Date(2020, 2, 29)

//正規表現
const r = /[a-z0-9]/

//ループ
for (let num in arr) {
 console.log(num)
}

//代入
let [x, y, ...z] = arr
let {key} = obj

//関数
function f(x) {
 return x
}

const f = () => {x}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

RSSで取得した記事を日付順にし、表示形式を変更する(Javascript・node.js)

解決する問題

RSSで取得した記事を、日付順にして日付の表示形式を変更する。

今回はRSSでコロナウイルスに関しての記事を取得しました。

対象読者

・node.jsでxmlを取得する事ができる方。
・express-generatorを触った事がある方。
・xmlの表示形式の知識がある方。

express-generatorを使ってXMLを取得しているところから進めていきます。

環境

OS: macOS
Node.js: v13.5.0
npm: 6.14.3
express: ~4.16.1
ejs: ~2.6.1,

RSSで記事を取得した時の状況

記事を取得し表示させると日付順にならず、見づらい

hello.js↓

router.get('/',(req, res, next) => {
    var opt = {
        host: 'news.google.com',
        port: 443,
        path: '/rss/search?q=corona&q=korona&hl=ja&gl=JP&ceid=JP:ja'
    };
    http.get(opt, (res2) => {
        var body = '';
        res2.on('data',(data) => {
            body += data;
        });
        res2.on('end', () => {
            parseString(body.trim(), (err, result) => {
                var data = {
                    title: 'コロナウイルスの最新情報を表示します',
                    content: result.rss.channel[0].item
                };
                res.render('hello', data);
            })
        });
    });
});  

hello.ejs↓

<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="content-type" content="text/html; charset=UTF-8">
        <title><%= title %></title>
        <link rel='stylesheet' href="/stylesheets/style.css" />
    </head>

    <body>
        <header>
            <h1><%= title %></h1>
        </header>
        <div role="main">
            <% if (content != null) { %>
                <ol>
                    <% for (var i in content) { %>
                        <% var obj = content[i]; %>
                        <li><%= obj.pubDate %><a href="<%=obj.link %>"><%= obj.title %></a></li>
                        </tr>
                    <% } %>
                </ol>
            <% } %>
        </div>
    </body>
</html>

スクリーンショット 2020-03-29 15.55.27.png

(今回取得したxml
https://news.google.com/rss/search?q=corona&q=korona&hl=ja&gl=JP&ceid=JP:ja)

解決策

①xmlの記事の中のitemを配列に入れていく。

for in を使ってxmlのitemを配列に入れる処理をループさせます。

配列を作る、ループを作る、配列へpushという順番です。

※ソートする為に配列へ入れます。

<!-- 配列を作成 -->
<% var hash = new Array %>

<!-- xmlitemをループ処理する -->
<% for (var i in content) { %>
    <% var obj = content[i]; %>  
    <!-- itemを配列へ追加 -->        
    <% hash.push(obj); %>
<% } %> 

※ejsの中にjsを書いています。
※content = result.rss.channel[0].item

②配列をソートして日付順に並べる

<!-- 配列をソートして日付順に変更 -->
<% hash.sort(function(a,b) {
    return (a.pubDate < b.pubDate ? 1 : -1);
}); %>

これで配列の中のitemが、xmlのpubDateの日付順に並びました。
※こちらもejsの中にjsを書いています。
※参考にした記事は下に載っています。

③日付の表示形式を変更

1、配列をfor inでループさせる
2、ループの中にDateオブジェクトを作成
3、日付の形式を変更
4、(西暦2020年を削除)任意
5、ループ処理の中で記事を表示する

<!-- 配列をループ処理 -->
<% for (var i in hash) { %>
    <% var obj2 = hash[i] %>

    <!-- 日付の表示形式を変更 -->
    <% 
        var date2 = new Date(obj2.pubDate);
        var b = date2.toLocaleString('ja-JP', {era:'long'});                            
        obj2.pubDate = b;
    %>
    <!-- 西暦を削除 -->
    <% var obj3_pubDate = obj2.pubDate.replace('西暦2020年', ''); %>

    <!-- 表示 -->
    <div class="center">
        <ul>
            <li>
                <a href="<%=obj2.link %>">
                    <div class="Date"><%=obj3_pubDate %></div>
                    <div class="title"><%="   " + obj2.title %></div>
                </a>
            </li>
         </ul>
    </div>
<% } %>

表示結果

css追加前↓
スクリーンショット 2020-03-29 17.25.04.png

css追加後↓
スクリーンショット 2020-03-29 17.26.40.png

変更後のコード

hello.ejs↓

<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="content-type" content="text/html; charset=UTF-8">
        <title><%= title %></title>
        <link rel='stylesheet' href="/stylesheets/style.css" />
    </head>

    <div class="style">
        <!-- 更新日時を日本語へ変更する -->
        <% var update2 = new Date(update);
           var update3 = update2.toLocaleString('ja-JP', {era:'long'});                            
           update = update3;
        %>
        <!-- 更新日時の西暦を消す -->
        <% var update4 = update.replace('西暦2020年', ''); %>

    </div>

    <body>
        <header>
            <h1><%= title %>更新(<%= update4 %>)</h1>
        </header>
        <div role="main">
            <% if (content != null) { %>
                    <!-- 配列を作成 -->
                    <% var hash = new Array %>

                    <!-- xmlのitemをループ処理する -->
                    <% for (var i in content) { %>
                        <% var obj = content[i]; %>  
                        <!-- itemを配列へ追加 -->        
                        <% hash.push(obj); %>
                    <% } %> 

                    <!-- 配列をソートして日付順に変更 -->
                    <% hash.sort(function(a,b) {
                        return (a.pubDate < b.pubDate ? 1 : -1);
                    }); %>

                    <!-- 配列をループ処理 -->
                    <% for (var i in hash) { %>
                        <% var obj2 = hash[i] %>

                        <!-- 日付の表示形式を変更 -->
                        <% 
                            var date2 = new Date(obj2.pubDate);
                            var b = date2.toLocaleString('ja-JP', {era:'long'});                            
                            obj2.pubDate = b;
                        %>
                        <!-- 西暦を削除 -->
                        <% var obj3_pubDate = obj2.pubDate.replace('西暦2020年', ''); %>

                        <!-- 表示 -->
                        <div class="center">
                            <ul>
                                <li>
                                    <a href="<%=obj2.link %>">
                                        <div class="Date"><%=obj3_pubDate %></div>
                                        <div class="title"><%="   " + obj2.title %></div>
                                    </a>
                                </li>
                            </ul>
                        </div>
                    <% } %>
            <% } %>
        </div>
    </body>
</html>

hello.js↓

var express = require('express');
var router = express.Router();

var http = require('https');
var parseString = require('xml2js').parseString;

router.get('/',(req, res, next) => {
    var opt = {
        host: 'news.google.com',
        port: 443,
        path: '/rss/search?q=corona&q=Coronavirus&hl=ja&gl=JP&ceid=JP:ja'
    };
    http.get(opt, (res2) => {
        var body = '';
        res2.on('data',(data) => {
            body += data;
        });
        res2.on('end', () => {
            parseString(body.trim(), (err, result) => {
                var data = {
                    title: 'コロナウイルスの最新情報を表示します',
                    content: result.rss.channel[0].item,
                    update: result.rss.channel[0].lastBuildDate
                };
                res.render('hello', data);
            })
        });
    });
});  

module.exports = router;

参考

配列を日付順にソートする方法
https://infoteck-life.com/a0107-js-array-sort-date/
https://www.p-nt.com/technicblog/archives/58

Dateオブジェクトの使い方入門
https://www.sejuku.net/blog/30171

Dateオブジェクトのプロパティ(日付の表示形式を参考にしました。)
https://so-zou.jp/web-app/tech/programming/javascript/grammar/object/date.htm

ループ処理(今回はfor inでループしています。)
https://qiita.com/endam/items/808a084859e3a101ab8f

文字列から指定した文字を削除する(日付の西暦2020年を削除する為に参考にしました。)
https://zukucode.com/2017/04/javascript-string-remove.html

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

【JS】RSSで取得した記事を日付順にし、表示形式を変更する(Javascript・node.js)

解決する問題

RSSで取得した記事を、日付順にして日付の表示形式を変更する。

今回はRSSでコロナウイルスに関しての記事を取得しました。

対象読者

・node.jsでxmlを取得する事ができる方。
・express-generatorを触った事がある方。
・xmlの表示形式の知識がある方。

express-generatorを使ってXMLを取得しているところから進めていきます。

環境

OS: macOS
Node.js: v13.5.0
npm: 6.14.3
express: ~4.16.1
ejs: ~2.6.1,

RSSで記事を取得した時の状況

記事を取得し表示させると日付順にならず、見づらい

hello.js↓

router.get('/',(req, res, next) => {
    var opt = {
        host: 'news.google.com',
        port: 443,
        path: '/rss/search?q=corona&q=korona&hl=ja&gl=JP&ceid=JP:ja'
    };
    http.get(opt, (res2) => {
        var body = '';
        res2.on('data',(data) => {
            body += data;
        });
        res2.on('end', () => {
            parseString(body.trim(), (err, result) => {
                var data = {
                    title: 'コロナウイルスの最新情報を表示します',
                    content: result.rss.channel[0].item
                };
                res.render('hello', data);
            })
        });
    });
});  

hello.ejs↓

<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="content-type" content="text/html; charset=UTF-8">
        <title><%= title %></title>
        <link rel='stylesheet' href="/stylesheets/style.css" />
    </head>

    <body>
        <header>
            <h1><%= title %></h1>
        </header>
        <div role="main">
            <% if (content != null) { %>
                <ol>
                    <% for (var i in content) { %>
                        <% var obj = content[i]; %>
                        <li><%= obj.pubDate %><a href="<%=obj.link %>"><%= obj.title %></a></li>
                        </tr>
                    <% } %>
                </ol>
            <% } %>
        </div>
    </body>
</html>

スクリーンショット 2020-03-29 15.55.27.png

(今回取得したxml
https://news.google.com/rss/search?q=corona&q=korona&hl=ja&gl=JP&ceid=JP:ja)

解決策

①xmlの記事の中のitemを配列に入れていく。

for in を使ってxmlのitemを配列に入れる処理をループさせます。

配列を作る、ループを作る、配列へpushという順番です。

※ソートする為に配列へ入れます。

<!-- 配列を作成 -->
<% var hash = new Array %>

<!-- xmlitemをループ処理する -->
<% for (var i in content) { %>
    <% var obj = content[i]; %>  
    <!-- itemを配列へ追加 -->        
    <% hash.push(obj); %>
<% } %> 

※ejsの中にjsを書いています。
※content = result.rss.channel[0].item

②配列をソートして日付順に並べる

<!-- 配列をソートして日付順に変更 -->
<% hash.sort(function(a,b) {
    return (a.pubDate < b.pubDate ? 1 : -1);
}); %>

これで配列の中のitemが、xmlのpubDateの日付順に並びました。
※こちらもejsの中にjsを書いています。
※参考にした記事は下に載っています。

③日付の表示形式を変更

1、配列をfor inでループさせる
2、ループの中にDateオブジェクトを作成
3、日付の形式を変更
4、(西暦2020年を削除)任意
5、ループ処理の中で記事を表示する

<!-- 配列をループ処理 -->
<% for (var i in hash) { %>
    <% var obj2 = hash[i] %>

    <!-- 日付の表示形式を変更 -->
    <% 
        var date2 = new Date(obj2.pubDate);
        var b = date2.toLocaleString('ja-JP', {era:'long'});                            
        obj2.pubDate = b;
    %>
    <!-- 西暦を削除 -->
    <% var obj3_pubDate = obj2.pubDate.replace('西暦2020年', ''); %>

    <!-- 表示 -->
    <div class="center">
        <ul>
            <li>
                <a href="<%=obj2.link %>">
                    <div class="Date"><%=obj3_pubDate %></div>
                    <div class="title"><%="   " + obj2.title %></div>
                </a>
            </li>
         </ul>
    </div>
<% } %>

表示結果

css追加前↓
スクリーンショット 2020-03-29 17.25.04.png

css追加後↓
スクリーンショット 2020-03-29 17.26.40.png

変更後のコード

hello.ejs↓

<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="content-type" content="text/html; charset=UTF-8">
        <title><%= title %></title>
        <link rel='stylesheet' href="/stylesheets/style.css" />
    </head>

    <div class="style">
        <!-- 更新日時を日本語へ変更する -->
        <% var update2 = new Date(update);
           var update3 = update2.toLocaleString('ja-JP', {era:'long'});                            
           update = update3;
        %>
        <!-- 更新日時の西暦を消す -->
        <% var update4 = update.replace('西暦2020年', ''); %>

    </div>

    <body>
        <header>
            <h1><%= title %>更新(<%= update4 %>)</h1>
        </header>
        <div role="main">
            <% if (content != null) { %>
                    <!-- 配列を作成 -->
                    <% var hash = new Array %>

                    <!-- xmlのitemをループ処理する -->
                    <% for (var i in content) { %>
                        <% var obj = content[i]; %>  
                        <!-- itemを配列へ追加 -->        
                        <% hash.push(obj); %>
                    <% } %> 

                    <!-- 配列をソートして日付順に変更 -->
                    <% hash.sort(function(a,b) {
                        return (a.pubDate < b.pubDate ? 1 : -1);
                    }); %>

                    <!-- 配列をループ処理 -->
                    <% for (var i in hash) { %>
                        <% var obj2 = hash[i] %>

                        <!-- 日付の表示形式を変更 -->
                        <% 
                            var date2 = new Date(obj2.pubDate);
                            var b = date2.toLocaleString('ja-JP', {era:'long'});                            
                            obj2.pubDate = b;
                        %>
                        <!-- 西暦を削除 -->
                        <% var obj3_pubDate = obj2.pubDate.replace('西暦2020年', ''); %>

                        <!-- 表示 -->
                        <div class="center">
                            <ul>
                                <li>
                                    <a href="<%=obj2.link %>">
                                        <div class="Date"><%=obj3_pubDate %></div>
                                        <div class="title"><%="   " + obj2.title %></div>
                                    </a>
                                </li>
                            </ul>
                        </div>
                    <% } %>
            <% } %>
        </div>
    </body>
</html>

hello.js↓

var express = require('express');
var router = express.Router();

var http = require('https');
var parseString = require('xml2js').parseString;

router.get('/',(req, res, next) => {
    var opt = {
        host: 'news.google.com',
        port: 443,
        path: '/rss/search?q=corona&q=Coronavirus&hl=ja&gl=JP&ceid=JP:ja'
    };
    http.get(opt, (res2) => {
        var body = '';
        res2.on('data',(data) => {
            body += data;
        });
        res2.on('end', () => {
            parseString(body.trim(), (err, result) => {
                var data = {
                    title: 'コロナウイルスの最新情報を表示します',
                    content: result.rss.channel[0].item,
                    update: result.rss.channel[0].lastBuildDate
                };
                res.render('hello', data);
            })
        });
    });
});  

module.exports = router;

参考

配列を日付順にソートする方法
https://infoteck-life.com/a0107-js-array-sort-date/
https://www.p-nt.com/technicblog/archives/58

Dateオブジェクトの使い方入門
https://www.sejuku.net/blog/30171

Dateオブジェクトのプロパティ(日付の表示形式を参考にしました。)
https://so-zou.jp/web-app/tech/programming/javascript/grammar/object/date.htm

ループ処理(今回はfor inでループしています。)
https://qiita.com/endam/items/808a084859e3a101ab8f

文字列から指定した文字を削除する(日付の西暦2020年を削除する為に参考にしました。)
https://zukucode.com/2017/04/javascript-string-remove.html

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

javascriptで数値をカンマ区切りにする

数値をカンマ区切りにする

もうあれこれ悩まない!数値にカンマをつけるたった一つの方法

<%= new Intl.NumberFormat('ja-JP', {maximumSignificantDigits: 3}).format(item.price) %>

オプションもたくさんありますのでcurrency対応がとても楽になりますね。
ほとんどのサイトではIntlのためにbabelを使う必要はありません。
(*対応したいブラウザが古い場合は必要になります)

参考

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/NumberFormat

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

Vue.jsでTo-Do Listを作ってみた。

Vue.jsの学習を本日から始めました!
めちゃくちゃ面白かったので、記事にします。

やったこと

「今日のTo-Do List」を作成しました。
普段はUdemyや本を参考に学習することが多いのですが、今回は久しぶりにドットインストールを使ってみました。
ドットインストールさん、分かりやすかったです。ありがとうございます。回し者ではありません。

作ったものはこんな感じです。
スクリーンショット 2020-03-29 16.54.22.png

ドットインストールを参考に作ったので、見た目はほぼドットインストールのままですが、ToDoをする予定時刻を自分で入れてみました。
これからもっと手のこんだアプリケーションをVue.jsで作っていきたいです。

学んだこと

導入が楽

scriptタグをhtmlファイルに記載するだけで使えました。
開発環境の構築は大変な時が多々ありますが、Vue.jsは非常に楽でした。

双方向データバインディング

テキストボックスに入力した途端に出力される文字が変わる、といったことがこの双方向データバインディングに例になると思います。
これを最初学んだときは驚きました。しかも、それが数行のコードですぐ簡単に実装できてしまうのです。

学びやすい

学習コストが少し低いように感じました。まだ最初だからだと思いますが、最初にしてはとっつきやすかったです。
これからVue Routerを使ってSPAを作ろうと思っておりますが、おそらくその過程で苦戦することでしょう。。。

これから

初めてJavaScriptのフレームワークを本格的に学習してみましたが、非常に興味深かったです。
上でも書いたようにこれからはVue.jsでシングルページアプリケーションを作ってみます。
その際にはまた記事にします!

参考

ドットインストール Vue.js入門

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

今更ながらReactのHooksを使ってみた

はじめに

2017年~2018年あたりでReact 16.x.xを使っていましたが、それ以降しばらく触っていませんでした。
去年の終わりくらいから改めてReactを触ろうとしたところ、Hooksなる機能がReact 16.8から追加されたということで、触ってみた際の学びを備忘録として残しておきます。Hooksいいですね!

対象Ver: 16.12.0

公式ドキュメント
https://ja.reactjs.org/docs/hooks-intro.html

※以降のコードは、私はこんな雰囲気で書いたんじゃよ、という備忘録ですので動作保証は致しません。
※私の検証ベースで記載している部分があるので、間違っていた場合はご指摘頂けると嬉しいです。

useState

state管理のHook。管理したいstate単位にuseStateを実行し、戻り値としてstate自身とそのsetter(setStateみたいなもの)を受け取る。useStateの引数は初期値。

loading.jsx
import React, { useState } from 'react'
import LoadingIcon from '../icons/loading'

const App = props => {
    const [content, setContent] = useState()

    // 何かアクションに応じてデータを取得
    const loadData = () => {
        apiCall().then(result => {
            setContent(result) // 取得したコンテンツを表示
        })
    }

    return (
        <div>
            <button onClick={loadData}>Load</button>
            <p>{content}</p>
        </div>
    )
}

らくちん。

useEffect

stateの変化を検知して処理を行う場合に使う。

loading.jsx
import React, { useState, useEffect } from 'react'

const App = props => {
    const [content, setContent] = useState()
    const [filteredContent, setFilteredContent] = useState()

    const loadData = () => {
        // 省略
    }

    useEffect(() => {
        // 何か処理をしてセット
        const filteredContent = filter(content)
        setFilteredContent(filteredContent)
    }, [content])

    return (
        <div>
            <button onClick={loadData}>Load</button>
            <p>{filteredContent}</p>
        </div>
    )
}

第二引数の配列には、ウォッチしたいstateを指定する。今回であればcontentが変化した際に処理を実行したいので、contentを指定。

useRef

何かしらの参照を持っておくためのハコみたいなイメージ。(段々説明が雑になってきました)
公式ドキュメントにもありますが、Reactコンポーネントにref={}で渡して、コンポーネントの参照を持って置くためのものと思っていましたが、汎用的な箱として利用可能です。

具体例を示した方が分かりやすいので、実際に私がはまった例とその解決策を。
データロード中かどうかをstateで管理して、多重ロードを避けるために書いたコードが以下。

loading.jsx
import React, { useState } from 'react'

const App = props => {
    const [content, setContent] = useState()
    const [loading, setLoading] = useState(false)

    const loadData = () => {
        // ロード中ならスキップ
        if (loading) return

        setLoading(true) // ロード中に設定

        apiCall().then(result => {
            setContent(result)
            setLoading(false) // ロード中ステータスを解除
        })
    }

    return (
        <div>
            <button onClick={loadData}>Load</button>
            <p>{content}</p>
        </div>
    )
}

これだとうまくいきませんでした。

なぜか。loadData関数を定義した時点でClosureにその時点のloading変数の内容を保持されるので、いつまで経ってもloadingはfalseのままでした。
なのでこうしました。

loading.jsx
import React, { useState, useRef } from 'react'

const App = props => {
    const [content, setContent] = useState()

    const loadingRef = useRef()
    loadingRef.current = false // 初期化

    const loadData = () => {
        // ロード中ならスキップ
        if (loadingRef.current) return

        loadingRef.current = true // ロード中に設定

        apiCall().then(result => {
            setContent(result)
            loadingRef.current = false // ロード中ステータスを解除
        })
    }

    return (
        <div>
            <button onClick={loadData}>Load</button>
            <p>{content}</p>
        </div>
    )
}

useRefの戻りはオブジェクトなので、それをClosureで持っておけば現在の値が参照可能なので、正しく動作するようになりました。
たぶん使い方は合っているハズ。。

useReducer

最初に書きましたが、Reduxのaction/reducerなどの記述量の多さが苦手で、できれば避けたいと思っていましたが、避けられない場面が出てきました。

サーバから取得した結果を順々に配列に追加していくような、以下のコードを書いてみました。

loading.jsx
import React, { useState, useRef } from 'react'

const App = props => {
    const { seq } = props
    const [results, setResults] = useState([])

    // コールバック参照用
    const resultsRef = useRef()
    resultsRef.current = results
    useEffect(() => { resultsRef.current = results }, [results])

    const addResult = result => {
        // 別オブジェクトにしないとReactが変更を検知しないので、別配列として処理
        const newResults = resultsRef.current.concat(result)
        setResults(newResults)
    }

    // 何かアクションに応じてデータを取得
    const loadData = (sequence) => {
        apiCall(sequence).then(result => {
            if (result.status === 404) return

            addResult(result) // 結果を配列に追加
            loadData(sequence + 1) // 最新を取得するまでループ
        })
    }

    return (
        <div>
            <button onClick={() => { loadData(seq) }}>Load</button>
            <p>{content}</p>
        </div>
    )
}

これでうまくいくかと思いきや、追加したデータが消えていたりする。。。
今試してみたサンプルコードは以下。

sample.jsx
    const [arr, setArr] = useState([])
    const arrRef = useRef()
    arrRef.current = arr
    useEffect(() => {
        arrRef.current = arr
        console.log('-----from-----')
        console.log(arrRef.current.length)
        console.log(arrRef.current)
        console.log('-----to-----')
    }, [arr])

    useEffect(() => {
        for(let i=0; i<100; i++) {
            const newArr = arrRef.current.slice()
            newArr.push(i)
            setArr(newArr)
        }
    }, [])

結果はこう。

-----from-----
0
[]
-----to-----
-----from-----
1
[99]
-----to-----

前のstateを踏まえて何か処理する場合、useRefで参照を持っていても不十分だったようです。
そこでuseReducerの出番。

sample.jsx
    const sampleReducer = (state, action) => {
        switch(action.type) {
            case 'add':
                const newArr = state.slice()
                newArr.push(action.payload)
                return newArr
            default:
                return state
        }
    }
    const [arr, dispatch] = useReducer(sampleReducer, [])

    useEffect(() => {
        console.log('-----from-----')
        console.log(arr.length)
        console.log(arr)
        console.log('-----to-----')
    }, [arr])

    useEffect(() => {
        for(let i=0; i<100; i++) {
            dispatch({ type: 'add', payload: i })
        }
    }, [])

結果はこう。

-----from-----
0
[]
-----to-----
-----from-----
100
(100) [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99]
-----to-----

100回出力されていないのは、おそらくdispatchを頻繁に実行したため、reducer側が良しなに更新タイミングを減らしてくださったのではと予想。

Reduxをご存知の方はお分かりと思いますが、前のstateを受けて処理が可能なので、addした分だけ情報が格納されています。

なので、前の状態に+αで変更する際はuseReducerを使うべき、というのが学びです。
そしてuseReducerを使ってみて思いましたが、結構簡素に書けますね。
以前はTypeScriptを使っていたこともあり、余計冗長に感じてしまったのかもしれません。

まとめ

  • useStateはstateとそのsetterを返す
    • stateがオブジェクトの場合、setterに指定するのは新しいオブジェクトにすること(Reactが検知できないっぽい)
  • useEffectはstateの変更を検知して処理を行うヤツ
  • useRefは使いやすいハコ
  • useReducerは前のstateを踏まえて処理したい場合に有効
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【曖昧さ回避】ブラウザレンダリングにおける「ファイルの読み込み」が意味するものとは

「ファイルの読み込み」とは

ブラウザレンダリングの仕組みを解説するサイトや書籍には、「ファイルを読み込んで〜」のような説明が多くあります。
自分がレンダリング工程を勉強しているときに、この「読み込み」という言葉がファイルのDownload(転送)を指すのか、ファイルのParse(解析)を指すのか、はたまたレンダリング全体のことを言っているのか、説明する場面によって意味が変わる曖昧な言葉だなーと感じていました。

ここではブラウザレンダリングの仕組みについて、1.HTMLのみ、2.HTMLとCSS、3.HTMLとJavaScript、4.HTMLとCSSとJavaScriptの4パターンに分けて、レンダリングフローに定義された言葉に当てはめながら説明していきたいと思います。

(検証環境:Google Chrome バージョン: 80.0.3987.87)

ブラウザレンダリングの仕組みの大枠

ブラウザレンダリングのフローは大きく4つの工程に分けられ、それぞれの工程は更にいくつかの細かい工程に分けられます。
(参考:Webフロントエンド ハイパフォーマンス チューニング -久保田 光則 (著)
ブラウザレンダリングの流れ.png

  • Loading(データのダウンロード・解析)
    • Download
    • Parse
  • Scripting(JSの実行)
  • Rendering(スタイルの計算、当て込み)
    • Calculate Style
    • Layout
  • Painting(描画)
    • Paint
    • Rasterize
    • Composite Layers

図を見ていると全ての工程がシリアル(直列)に進んでいくように誤解しやすいのですが、実際はそうではありません。
レンダリングエンジンがページ表示を最適化する中で、部分的にでも準備ができた段階で、都度次の工程に進むこともあります。

本記事では主にLoading(Download、Parse)Scriptingの工程に関して、ファイルごとにどのように影響を及ぼし合い、レンダリングの処理順が決まっているかについて説明します。

RenderingPaintingの工程を含むブラウザレンダリング全体の仕組みについては以下記事が詳しいです。
フロントエンジニアなら知っておきたいブラウザレンダリングの仕組みをわかりやすく解説! | LeapIn

1.HTMLのみ

はじめに外部ファイル「読み込み」記述が一切ない純粋なHTMLファイルについて、
ブラウザ検索バーにURLを入力し、HTTPプロトコルで通信してページを表示する場合を考えます。
(参考:ネットワークやTCP/IPやHTTPの基本(初学者向け) - Qiita

レンダリングの工程としては、まずHTMLDownloadが始まりますが、
ここでのポイントは、サーバからHTMLファイルなどのリソースが転送される手法は0か1の転送ではなく、
セグメントに分割しながら転送されるということです。
(どのくらいまとめて送るのかについてはサーバサイドで制御するようです)

前提として、ブラウザはUX向上のため画面に何も表示されていない時間を短くするように動きます。
よって全てのHTMLDownloadが完了していなくても、転送されたHTMLセグメントを元にParse(DOMツリー構築)や後続の処理が進み、準備ができたDOMから画面描画が始まります。

スクリーンショット 2020-03-29 2.12.07.png
上記はChrome DevToolsのNetworkパネルであり、一つのHTMLファイルをダウンロード完了するまでの解析図です。(テスト用にサーバサイド(PHP)でファイルの転送や解析速度を調整しています)

Waiting(TTFB:Time To First Byte)とはファイル転送リクエストを送ってからクライアント側で最初のデータを受け取るまでにかかる時間(主にサーバサイドの処理時間)であり、Content Downloadとは最初のデータを受け取ってから全てのデータを受け取りきるまでにかかる時間です。

解析グラフによるとContent Downloadに合計2sかかっていますが、その間も転送されてきているデータを元に別の処理(Parse、Rendering、Painting)が都度進んで描画が始まっており、それは同Performanceパネルで解析することができます。↓
スクリーンショット 2020-03-29 19.13.47のコピー.png
データを受け取る(Receive Data)たびに、HTMLParse(DOM構築)のフェーズを経て、Composite Layersまでの描画工程を完了していることが分かります。

このように準備ができたところから都度描画が行われることで、First Paint(画面に最初になにかしらが描画されタイミング)や、First Meaningful Paint(画面に最初にユーザーに意味のある表示がされたタイミング)などの表示タイミング差が存在します。

参考:Ace the Lighthouse Audit: Best Practices for Consistent Interactivity | Lumavate

2.HTMLとCSS

head要素の中のlink要素に外部CSS「読み込み」記述がある場合を考えます。

HTML
<!DOCTYPE html>
<html>
  <head>
    <link rel="stylesheet" href="style.css" />
  </head>
  <body>
    <!-- bodyの中身 -->
  </body>
</html>

CSSのDownload

この場合も、まずHTMLDownload、Parseが始まり、解析途中でlink要素を見つけた段階でCSSDownloadが始まります。↓
スクリーンショット 2020-03-29 14.07.42.png
CSSのDownloadはHTMLのParseをブロックしないので、CSSDownload中もHTMLParseが並行して進みます。
そしてその先に再び外部CSS「読み込み」記述ががあれば、同時に複数のCSSDownloadが始まります。

ただし、モダンブラウザでは(同じドメインの)TCP接続は同時に6本までという制限があるため、7本目以降の接続は前の接続の終了を待ってからとなります。
スクリーンショット 2020-03-29 14.45.11のコピー.png
見ての通りこれではダウンロードしたいファイルが多いほどページ表示速度が遅くなってしまいます。

そのため、対応策としてファイルを可能な限りまとめてリクエスト必要数を抑えたり、CDNなどを利用してあえて別ドメインから接続することでスループットを上げたり、一つのTCP接続で同時に複数のリクエスト/レスポンスを処理できるhttp/2プロトコルで通信するなどの手法が存在します。

参考:そろそろ知っておきたいHTTP/2の話 - Qiita

CSSのParse

CSSもHTMLと同様にDownloadの次の工程として、Parse(CSSOMの構築)の工程があります。
考慮すべき注意点は以下です。

  • CSSParseは見かけ上はHTMLParseと並行して行われる。
  • HTMLは描画工程に進もうとするDOMの、直前までに記載されているCSSのLoading(Download、Parse)が完了しない限り、Renderingフローに進まない。(描画処理が行われない)

HTMLParseCSSParseはどちらもレンダリングエンジンのmainスレッドで行われますが、mainスレッドでは同時に一つの処理しか行えないため、それぞれの処理が同時に走ることはありません。
ですが、HTMLParseのアイドル時間などにCSSParseが進むため、見かけ上は2つが並行して行われているように見えます。
(そもそもCSSParseにかかる時間はブラウザレンダリング全体の時間からすると極めて短く、議論に上がりにくい部分のようです。)

また、CSSのLoadingが進行中の場合は、たとえHTMLParseが先に完了していてもRenderingなどの次の工程に進まず、結果として画面描画が行われません。

これはブラウザがFOUCFlash of Unstyled Contentの略。スタイルがついていないコンテンツが一瞬表示されること)を防ぐために、CSSParseの完了を待ってスタイルが適応された画面描画を行おうとするためです。
スクリーンショット 2020-03-29 15.35.54のコピー.png
上記Performanceパネル解析図を見ても、Finish LoadingCSSParseの完了)まで、Calculate StyleなどのRendering工程に進んでいない(画面描画が行われていない)ことが分かります。

3.HTMLとJavaScript

以下のようにhead要素の中にscript要素を記述して、外部JavaScriptファイルを「読み込む」場合を考えます。

HTML
<!DOCTYPE html>
<html>
  <head>
    <script src="main.js"></script>
  </head>
  <body>
    <!-- bodyの中身 -->
  </body>
</html>

JSのDownloadとScripting

HTMLParseが始まってscript要素に到達するとJSのDownloadが始まります。
その時に重要なポイントが、JSのDownloadとScripting(実行)はHTMLParseをブロックするということです。

一度JSのDownloadが始まると、ダウンロードしたJSのScripting工程が完了しない限り、それ以降のHTMLParseが行われません。
これが、JSの記述はbodyの最後に記述するべきと言われる理由の一つです。
スクリーンショット 2020-03-29 20.29.02のコピー.png
上記図より、Send RequestJSDownloadが始まると、Evaluate Script工程が完了するまでHTMLParseが行われていないことが分かります。

async属性とdefer属性

script要素によるJSの「読み込み」記述はそれ以降のHTMLParseをブロックしますが、script要素asyncdeferの属性をつけることによってJSのDownloadを非同期に行い、HTMLParseと同時に処理することができます。

HTML
<script src="main.js" async ></script>

<!-- もしくは -->

<script src="main.js" defer ></script>

以下は先程と同じ記述で、defer属性を使用したときのPerformanceパネルの解析結果です。
スクリーンショット 2020-03-29 20.34.04のコピー.png
JSのDownloadが開始(send Request)しても、HTMLParseがブロックされずに先の工程に進み、最終的にComposite Layersまで完了して画面描画が行われているのが分かります。
その後JSのDownloadが完了した段階で、Scripting(Evaluate Script)処理が行われています。

参考:scriptタグに async / defer を付けた場合のタイミング - Qiita

4.HTMLとCSSとJavaScript

CSSとJavaScriptの両方の「読み込み」記述を書く場合です。
以下のようにlink要素の直下にscript要素を入れてみます。

HTML
<!DOCTYPE html>
<html>
  <head>
    <link rel="stylesheet" href="style.css" />
    <script src="main.js"></script>
  </head>
  <body>
    <!-- bodyの中身 -->
  </body>
</html>

CSSDownloadHTMLParseをブロックしないため、HTMLParsescript要素の記述に到達しJSのDownloadが始まります。
先程「HTMLは直前までのCSSLoading(Download、Parse)が完了していない限り、Renderingフローに進まない」と説明しましたが、実は同様にJSも直前までのCSSLoading(Download、Parse)が完了していない限り、Scriptingの工程に進まない性質があります。

つまりこの場合、CSSよりもJSのほうが速くDownloadが完了したとしても、CSSParseが完了するまでScriptingが待機状態になるということです。
スクリーンショット 2020-03-29 16.35.27のコピー.png
↑JSのほうがCSSよりも1s速くDownloadが完了していますが、
スクリーンショット 2020-03-29 16.34.37のコピー2.png
↑CSSのLoading(Download、Parse)完了を待ってから、Scripting(Evaluate Script)処理が実行されていることが分かります。

参考:DOMContentLoaded周りの処理を詳しく調べてみました - Qiita

ブラウザのプリロード機能

以下のようにJSの「読み込み」記述をCSSよりも前に書いた場合を考えます。

HTML
<!DOCTYPE html>
<html>
  <head>
    <script src="main.js"></script>
    <link rel="stylesheet" href="style.css" />
  </head>
  <body>
    <!-- bodyの中身 -->
  </body>
</html>

defer属性async属性がついていないscript要素による外部JSファイルの「読み込み」なので、JSのDownload、Scriptingが完了するまでそれ以下のHTMLParseが進まない、つまりCSSDownloadも進まないはずです。

しかし、モダンブラウザではその限りではありません。
NetWorkパネルを見てみると、JSとCSSのDownloadが同時に行われていることが分かります。
スクリーンショット 2020-03-29 21.22.31のコピー.png
実はChromeなどのモダンブラウザには、HTMLParseが進んでいない部分についてもDownloadが必要な記述がないか確認し、もしあれば事前にそのファイルのDownloadを開始する機能があります。(Preload Scanner

よってこの場合も、ブラウザはJSのDownload中にその先にあるCSSの「読み込み」記述を読み取り、CSSDownloadも同時に進めることでレンダリングを高速化しているのです。

※Preload Scanner機能で事前処理できるのはDownloadの工程だけです。ParseScriptingの工程は本来のレンダリングフローに沿って行われます。

参考:rel="preload"を極めるために必要な2種類のプリロード機能 | Raccoon Tech Blog

まとめ

  • HTMLはセグメントごとにDownloadが行われ、都度Parseなどの先の工程に進む
  • CSSのDownloadHTMLのParseをブロックしない
  • CSSのParseは見かけ上はHTMLParseと並行して行われる
  • HTMLは直前までのCSSLoading(Download、Parse)が完了していない限り、Renderingの工程に進まない
  • JSのDownloadScripting(実行)HTMLのParseをブロックする
  • JSも直前までのCSSLoading(Download、Parse)が完了しない限り、Scriptingの工程に進まない

誤った解釈等ございましたら、ご教授お願いいたします。。

参考

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

【CSS & JS】超簡単。ハンバーガーをクリックで×に変える&メニューをスライドアウトさせる方法

備忘録です。

ハンバーガーメニューをクリックすると、×印に変えるのってどうやるんだろう〜CSSでつくる方法もあるらしいけど、どうやるんだろう。密かにずっと気になっていたので調べてみました。

JavaScriptも使いますが、決してややこしいことはしなくていい方法を見つけました。

ここでは二つのことをします。
❶ハンバーガーにクリックすることで×印に変える
❷メニューを画面の右側からスライドアウトさせる。

まずはHTMLから。

HTML

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <link rel="stylesheet" href="css/styles.css" />
    <script
      src="https://code.jquery.com/jquery-3.4.1.min.js"
      integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo="
      crossorigin="anonymous"
    ></script>
    <title>Slide Out Menu</title>
  </head>
  <body>
    <header>
      <nav>
        <div class="burger-menu-icon">//①ハンバーガーのdivをつくる
          <input type="checkbox" id="burger" />//②Toggleするためにはcheckboxを書く!ここがカギ!
          <label for="burger">//③label forはinputのidとそろえる。
            <div class="burger-icon">
              <span></span>
              <span></span>
              <span></span>
            </div>//④labelのなかにハンバーガーアイコンを入れましょう。
          </label>
          <div class="slideoutMenu">//⑤スライドアウトさせるメニューは、burger-menu-iconのdiv内におさめます。
            <div class="opacity"></div>
            <div class="menu">
              <ul>
                <li><a href="#">Home</a></li>
                <li><a href="#">About</a></li>
                <li><a href="#">Contact</a></li>
                <li><a href="#">Design</a></li>
                <li><a href="#">Style</a></li>
                <li><a href="#">Reviews</a></li>
              </ul>
            </div>
          </div>
        </div>
      </nav>
    </header>
    <script src="script.js"></script>
  </body>
</html>

SCSS

.burger-icon {
  display: block;
  z-index: 11;
}

.burger-icon span { //ここでburger-iconをつくる
  display: block;
  background: #000; //background-colorではなく、backgroundで指定
  width: 50px;
  height: 5px;
  margin-bottom: 10px;
  margin-left: 90%;
  cursor: pointer;
  position: relative;
  top: 0;
}

input#burger { //labelここでcheckboxを消す
  display: none;
}

.burger-icon.open span:nth-child(2) { //クリックすると'open'というクラスがtoggleされるよう、JSに記述しています。つまり、これはハンバーガーをクリックするとハンバーガーの2本目の線が消えるという記述です。
  width: 0;
  opacity: 0;
}

.burger-icon.open span:nth-child(1) { //これはハンバーガーをクリックするとハンバーガーの1本目の線が45度回転し、上から15px下方向にずらすという記述です。
  transform: rotate(45deg);
  top: 15px;
}

.burger-icon.open span:nth-child(3) {//これはハンバーガーをクリックするとハンバーガーの3本目の線が-45度回転し、15px上方向にずらすという記述です。
  transform: rotate(-45deg);
  top: -15px;
}

.slideoutMenu {
  transform: translateX(100%); //メニューを右に100%ずらすことで、画面から消す。
  z-index: -1;
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  display: flex;
  transition: 0.3s;
  animation: slideOut 0.3s;
  .opacity,
  .menu {
    width: 50%;
    background-color: #5d348c;
    ul {
      list-style: none;
      li {
        padding-bottom: 3rem;
      }
      li a {
        text-decoration: none;
        font-size: 3rem;
        color: #ea5c5d;
        text-transform: uppercase;
      }
    }
  }
  .opacity {
    background-color: #ea5c5d;
    opacity: 0.7;
  }
}

@keyframes slideOut { //ここでスライドの動作をつくります。(slideIn、のほうがふさわしかったかも T_T )
  0% {
    transform: translateX(100%);
  }
  50% {
    transform: translatex(50%);
  }
  50% {
    transform: translatex(0%);
  }
}

input:checked ~ .slideoutMenu { //これがツワモノ!!!詳細は【覚えておきたいポイント3】で。
  transform: translateX(0%);
}

覚えておきたいポイント❶
<input>タグのあとに<label for="burger">を付け足しておけば、labelタグ内の要素をクリックすることでcheckboxをチェックできるようになります。

覚えたいおきたいポイント❷
.burger-icon.open span:nth-child(3), .burger-icon.open span:nth-child(1)ではハンバーガーの上の線と下の線を回転させ、top:〜で位置をずらしています。topの位置をずらすには、.burger-icon spanposition: relative; top: 0;と書かないと効かないので注意です。

覚えたいおきたいポイント❸
最後にあるinput:checked ~ .slideoutMenuを書くことで、メニューが右側から画面にスライドインしてくれます。
input:checkedは、「チェックボックスがcheckされていたら……」という意味です。~は、兄弟要素を取得したいよ〜という意味合いを持つ記号です。input.slideoutMenuはどちらともburger-menu-iconの子要素なので、兄弟要素に値します。~がないと効かないので注意です。

JS

$(".burger-icon").click(function() {
  $(this).toggleClass("open");
});

JSではシンプルに、「.burger-iconのクリックで、openというclassをtoggleしてね」と伝えているだけです。

地味に時間がかかりますが、、一番わかりやすいかなと感じました。こんなに簡単に実装できるとは!animationがわかると、CSSは一層おもしろみが増す気がします。

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

【CSS & JS】超簡単。ハンバーガーアイコンをクリックして×に変える&メニューをスライドアウトさせる方法

備忘録です。

ハンバーガーメニューをクリックすると、×印に変えるのってどうやるんだろう〜CSSでつくる方法もあるらしいけど、どうやるんだろう。密かにずっと気になっていたので調べてみました。

JavaScriptも使いますが、決してややこしいことはしなくていい方法を見つけました。

ここでは二つのことをします。
❶ハンバーガーにクリックすることで×印に変える
❷メニューを画面の右側からスライドアウトさせる

まずはHTMLから。

HTML

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <link rel="stylesheet" href="css/styles.css" />
    <script
      src="https://code.jquery.com/jquery-3.4.1.min.js"
      integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo="
      crossorigin="anonymous"
    ></script>
    <title>Slide Out Menu</title>
  </head>
  <body>
    <header>
      <nav>
        <div class="burger-menu-icon">//①ハンバーガーのdivをつくる
          <input type="checkbox" id="burger" />//②Toggleするためにはcheckboxを書く!ここがカギ!
          <label for="burger">//③label forはinputのidとそろえる。
            <div class="burger-icon">
              <span></span>
              <span></span>
              <span></span>
            </div>//④labelのなかにハンバーガーアイコンを入れましょう。
          </label>
          <div class="slideoutMenu">//⑤スライドアウトさせるメニューは、burger-menu-iconのdiv内におさめます。
            <div class="opacity"></div>
            <div class="menu">
              <ul>
                <li><a href="#">Home</a></li>
                <li><a href="#">About</a></li>
                <li><a href="#">Contact</a></li>
                <li><a href="#">Design</a></li>
                <li><a href="#">Style</a></li>
                <li><a href="#">Reviews</a></li>
              </ul>
            </div>
          </div>
        </div>
      </nav>
    </header>
    <script src="script.js"></script>
  </body>
</html>

ポイント❶

toggleするにはinput type="checkbox"を使う

ポイント❷

今回はハンバーガーアイコンをクリックすることで、メニューを引っ張り出したいです。なのでハンバーガーアイコンは、<label for="burger"></label>のなかに入れましょう。通常であればcheckboxをチェックしなければ、checkboxにチェックはつきませんが、label forというものを付け加えると、label forの中身をクリックするだけでcheckboxにcheckがつけられます。ただし一点。label for="burger"と、input id="burger"labelをinputと紐づけるには、この二つをそろえる必要があります。inputは必ずidで指定します。

ポイント❸

ハンバーガーアイコンと隠しているメニューは、同じdiv内に入れる。(この場合は.burger-menu-icon

SCSS

.burger-icon {
  display: block;
  z-index: 11;
}

.burger-icon span { //ここでburger-iconをつくる
  display: block;
  background: #000; //background-colorではなく、backgroundで指定
  width: 50px;
  height: 5px;
  margin-bottom: 10px;
  margin-left: 90%;
  cursor: pointer;
  position: relative;
  top: 0;
}

input#burger { //labelここでcheckboxを消す
  display: none;
}

.burger-icon.open span:nth-child(2) { //クリックすると'open'というクラスがtoggleされるよう、JSに記述しています。つまり、これはハンバーガーをクリックするとハンバーガーの2本目の線が消えるという記述です。
  width: 0;
  opacity: 0;
}

.burger-icon.open span:nth-child(1) { //これはハンバーガーをクリックするとハンバーガーの1本目の線が45度回転し、上から15px下方向にずらすという記述です。
  transform: rotate(45deg);
  top: 15px;
}

.burger-icon.open span:nth-child(3) {//これはハンバーガーをクリックするとハンバーガーの3本目の線が-45度回転し、15px上方向にずらすという記述です。
  transform: rotate(-45deg);
  top: -15px;
}

.slideoutMenu {
  transform: translateX(100%); //メニューを右に100%ずらすことで、画面から消す。
  z-index: -1;
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  display: flex;
  transition: 0.3s;
  animation: slideOut 0.3s;
  .opacity,
  .menu {
    width: 50%;
    background-color: #5d348c;
    ul {
      list-style: none;
      li {
        padding-bottom: 3rem;
      }
      li a {
        text-decoration: none;
        font-size: 3rem;
        color: #ea5c5d;
        text-transform: uppercase;
      }
    }
  }
  .opacity {
    background-color: #ea5c5d;
    opacity: 0.7;
  }
}

@keyframes slideOut { //ここでスライドの動作をつくります。(slideIn、のほうがふさわしかったかも T_T )
  0% {
    transform: translateX(100%);
  }
  50% {
    transform: translatex(50%);
  }
  50% {
    transform: translatex(0%);
  }
}

input:checked ~ .slideoutMenu { //これがツワモノ!!!詳細は【覚えておきたいポイント3】で。
  transform: translateX(0%);
}

覚えておきたいポイント❶

<input>タグのあとに<label for="burger">を付け足しておけば、labelタグ内の要素をクリックすることでcheckboxをチェックできるようになります。

覚えたいおきたいポイント❷

.burger-icon.open span:nth-child(3), .burger-icon.open span:nth-child(1)ではハンバーガーの上の線と下の線を回転させ、top:〜で位置をずらしています。topの位置をずらすには、.burger-icon spanposition: relative; top: 0;と書かないと効かないので注意です。

覚えたいおきたいポイント❸

最後にあるinput:checked ~ .slideoutMenuを書くことで、メニューが右側から画面にスライドインしてくれます。
input:checkedは、「チェックボックスがcheckされていたら……」という意味です。~は、兄弟要素を取得したいよ〜という意味合いを持つ記号です。input.slideoutMenuはどちらともburger-menu-iconの子要素なので、兄弟要素に値します。~がないと効かないので注意です。

JS

$(".burger-icon").click(function() {
  $(this).toggleClass("open");
});

JSではシンプルに、「.burger-iconのクリックで、openというclassをtoggleしてね」と伝えているだけです。

やりかたは何通りもあると思うのですが、個人的にはこれが一番わかりやすいかなと感じました。こんなに簡単に実装できるとは!animationがわかると、CSSは一層おもしろみが増す気がしました。

完成版のgif?
Screen Recording 2020-03-29 at 04.31 PM.gif

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

TypeScriptで学ぶデザインパターン〜Iterator編〜

対象読者

  • デザインパターンを学習あるいは復習したい方
  • TypeScriptが既に読めるあるいは気合いで読める方
    • いずれかのオブジェクト指向言語を知っている方は気合いで読めると思います
  • UMLが既に読めるあるいは気合いで読める方

環境

  • OS: macOS Mojave
  • Node.js: v12.7.0
  • npm: 6.14.3
  • TypeScript: Version 3.8.3

Iteratorパターンとは

集合(配列など)の各要素を走査する処理を抽象化したものです。
for文におけるループ変数の働きを一般化したものという理解で問題ありません。

サンプルコード

Iteratorパターンで作られたクラス群がどんなものになるのか確認していきましょう。デザインパターンといっても、文法的にトリッキーなことをしているわけではないので、少しずつ読みこなしてみてください。

今回は、題材として"ToDoリスト"を想定します。GitHubにも公開しています。

Main.ts

いきなりで恐縮ですが、このファイルはIteratorパターンと直接は関係ありません。Iteratorパターンで作られたクラス群を実際に使う処理が書かれています。

Main.ts
import Iterator from './modules/Iterator';
import TaskList from './modules/TaskList';

const taskList: TaskList = new TaskList(3);
taskList.addTask('ニュースチェック');
taskList.addTask('筋トレ');
taskList.addTask('生活用品買い物');

const iterator: Iterator = taskList.iterator();

while (iterator.hasNext()) {
  const task: string = iterator.next();
  console.log(task);
}

TaskListインスタンスが"ToDoリスト"そのものです。各タスクは単なる文字列("ニュースチェック"など)です。TaskListインスタンスが集合で、"ニュースチェック"などが集合の各要素という関係になっています。

while文周辺の処理で、集合の各要素にアクセスして"ニュースチェック"などの文字列をコンソールに出力しています。

次ファイルから、Iteratorパターンで作られたクラス群を実際に見ていきます。

modules/Iterator.ts

反復子を表現するインターフェースです。反復子とは、for文におけるループ変数のように集合の各要素を数え上げるために使用されるものです。

Iterator.ts
export default interface Iterator {
  hasNext(): boolean;
  next(): any;
}

hasNextメソッドは、集合の各要素を順番に処理する中で次の要素があるのかどうかを返却します。
nextメソッドは、集合の各要素を順番に処理する中で次の要素それ自体を返却します。
具体的な処理内容は本インターフェースの実装で確認しましょう。

modules/TaskListIterator.ts

Iteratorインターフェースの実装です。

TaskListIterator.ts
import Iterator from './Iterator';
import TaskList from './TaskList';

export default class TaskListIterator implements Iterator {
  private taskList: TaskList;
  private index: number;

  constructor(taskList: TaskList) {
    this.taskList = taskList;
    this.index = 0;
  }

  hasNext(): boolean {
    if (this.index < this.taskList.getLast()) {
      return true;
    } else {
      return false;
    }
  }

  next(): any {
    const task: string = this.taskList.getTask(this.index);
    this.index++;

    return task;
  }
}

重要な点は、集合をプロパティ(taskListプロパティ)として保持していることです。
次の要素があるのかどうかを返却(hasNestメソッド)したり次の要素それ自体を返却(nextメソッド)したりする際に、集合の情報が必要なのは納得いただけると思います。

modules/Aggregate.ts

集合を表現するインターフェースです。

iteratorメソッドは、集合に対応する反復子を返却します。

Aggregate.ts
import Iterator from './Iterator';

export default interface Aggregate {
  iterator(): Iterator;
}

modules/TaskList.ts

Aggregateインターフェースの実装です。

TaskList.ts
import Iterator from './Iterator';
import Aggregate from './Aggregate';
import TaskListIterator from './TaskListIterator';

export default class TaskList implements Aggregate {
  private tasks: string[];
  private last: number;

  constructor(length: number) {
    this.tasks = new Array(length);
    this.last = 0;
  }

  getTask(index: number): string {
    return this.tasks[index];
  }

  addTask(task: string): void {
    this.tasks[this.last] = task;
    this.last++;
  }

  getLast(): number {
    return this.last;
  }

  iterator(): Iterator {
    return new TaskListIterator(this);
  }
}

iteratorメソッド以外は、集合を構成する要素を作成(addTaskメソッド)したり参照(addTaskメソッド)したりする処理を表現しています。

全てのクラス群の紹介が終わったところで、Main.tsを起点に処理の流れ全体を再度確認しておきましょう。

クラス図

ここまでIteratorパターンで作られたクラス群を1つずつ確認してきました。次にクラス図を示します。Iteratorパターンの全体像を整理するのにお役立てください。

Iterator.png

  • ConcreteAggregate: サンプルコードではTaskListクラスが対応
  • ConcreteIterator: サンプルコードではTaskListIteratorクラスが対応

LucidChartを使用して作成

解説

最後に、このデザインパターンの存在意義を考えます。つまり、for文などでサクッと済みそうな処理をなぜわざわざクラス群に分けるのかを考えます。

お話をわかりやすくするために、サンプルコードに修正依頼が舞い込んできたケースを考えます。その内容が以下です。

??「配列の最初の要素から順番に処理している状態から、配列の最後の要素から順番に処理するようにしたい!」

どのように修正すればいいのかは割愛しますが、重要なことはMain.tsを修正する必要がないことです。これ実はかなりすごいことです。今回は"Iteratorパターンで作られたクラス群を実際に使う処理"がMain.tsにしかありませんが、それが他に100箇所あったらどうでしょう。Main.ts以外の100箇所も当然修正する必要はありません。

一方で、Iteratorパターンを使わずfor文などで直接集合を走査するロジックを書いた場合そうはいきません。たとえばfor (let i: number = 0; i < taskList.getLast(); i++) { /* 省略 */ }のようにMain.tsを実装していた場合、Main.tsの修正は必須です。同じような実装が他に100箇所あったら...悲しいことになりますね。

このように、Iteratorパターンを用いると集合の各要素を走査するロジックを集合の利用側(ここではMain.ts)から切り離すことができるのです。

補足

サンプルコードの実行方法を示します。まずはGitHubからリポジトリをクローンしましょう。次にIteratorPatternディレクトリに移動して以下コマンドを実行しましょう。

$ tsc Main.ts

Main.jsなどのJavaScriptファイルが生成されたら成功です。続いて同じディレクトリで以下コマンドを実行しましょう。

$ node Main.js

以下のようにタスクリストが表示されたら問題なく動作しています。

$ node Main.js
ニュースチェック
筋トレ
生活用品買い物

参考

あとがたり

初めてTypeScriptを書いてみたけど、かなり書きやすかった。
Javaを書いたことがあるからかな。

個人的に感じたことは以下だけど、もっとうまい書き方ややり方があるような気はしているので、引き続きTypeScriptの学習を進めていきたい所存。

  • importのあたりもっとうまいこと書けそう
  • Iteratorインターフェースのnextメソッドの返り値の型がanyになってしまったのがちょっと気持ち悪い
  • コンパイルの度にわざわざtscコマンド叩かずにSassで言うところのwatchみたいな機能でJavaScriptファイル自動生成も試したい

以上です。
最後までご覧いただきありがとうございました!

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

練習記録【JabaScript】初歩の初歩:Consoleで確認

必要なものは、VScodeとchrome。

index.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <title>JabaScript practice</title>
  </head>
  <body>

    <script src=js/main.js> //JabaScriptは別ファイルで保存
//ファイルを作成していなくても、js/main.jsにカーソル合わせてctlクリックで新しいファイルが作れるよ。
    </script>

   </body>

 </html>
main.js
'use strict'; //エラーチェックを厳格にできる

console.log("hello")
console.log('hello world from main.js'); //'',""どちらでも良い

//文章内に'が入るときは以下二つの方法
console.log("it's me!")
console.log('it\'s me') //バックスラッシュ\はその次の文字を無効化してくれる

console確認方法

chromeに移り、index.htmlをドラッグ。
ctl + shift + I で”デベロッパーツール”を表示。
consoleタブをクリック→画面更新すると

ーーーーーーーーーーーーーーーーーーーーーーー
hello
hello world from main.js
it's me!
it's me
ーーーーーーーーーーーーーーーーーーーーーーー
って表示されるよ。

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

練習記録【JabaScript】初歩の初歩:consoleで確認

必要なものは、VScodeとchrome。

index.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <title>JabaScript practice</title>
  </head>
  <body>

    <script src=js/main.js> //JabaScriptは別ファイルで保存
//ファイルを作成していなくても、js/main.jsにカーソル合わせてctlクリックで新しいファイルが作れるよ。
    </script>

   </body>

 </html>
main.js
'use strict'; //エラーチェックを厳格にできる

console.log("hello")
console.log('hello world from main.js'); //'',""どちらでも良い

//文章内に'が入るときは以下二つの方法
console.log("it's me!")
console.log('it\'s me') //バックスラッシュ\はその次の文字を無効化してくれる

console確認方法

chromeに移り、index.htmlをドラッグ。
ctl + shift + I で”デベロッパーツール”を表示。
consoleタブをクリック→画面更新すると

ーーーーーーーーーーーーーーーーーーーーーーー
hello
hello world from main.js
it's me!
it's me
ーーーーーーーーーーーーーーーーーーーーーーー
って表示されるよ。

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

練習記録【JavaScript】初歩の初歩:console.logで確認

必要なものは、VScodeとchrome。

index.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <title>JabaScript practice</title>
  </head>
  <body>

    <script src=js/main.js> //JabaScriptは別ファイルで保存
//ファイルを作成していなくても、js/main.jsにカーソル合わせてctlクリックで新しいファイルが作れるよ。
    </script>

   </body>

 </html>
main.js
'use strict'; //エラーチェックを厳格にできる

console.log("hello")
console.log('hello world from main.js'); //'',""どちらでも良い

//文章内に'が入るときは以下二つの方法
console.log("it's me!")
console.log('it\'s me') //バックスラッシュ\はその次の文字を無効化してくれる

console確認方法

chromeに移り、index.htmlをドラッグ。
ctl + shift + I で”デベロッパーツール”を表示。
consoleタブをクリック→画面更新すると

ーーーーーーーーーーーーーーーーーーーーーーー
hello
hello world from main.js
it's me!
it's me
ーーーーーーーーーーーーーーーーーーーーーーー
って表示されるよ。

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

Vuexを初めから丁寧に(1)~Vuexを理解するために必須の前提知識~

はじめに

この記事を読むと

  • Vuexを理解するために必要な知識を習得できます
  • Vuexを学ぶためのマイルストーンが明確となります

想定読者

  • Vue.js や Nuxt.js の初級〜中級者
  • Vuex を何となく雰囲気で使っている

前提知識

JavaScript 及び Vue についての基本知識があることは前提とします。
(Vue の基本知識がない方はこちらが入門書として最も最適です。)
『Vue.js 超入門』(掌田津耶乃/秀和システム)

またJavaScriptにおいては特に、オブジェクトの使い方にも慣れておくとスムーズでしょう。
(こちらの第9章が最も良い説明だと思います。)
『初めてのJavaScript 第3版 ―ES2015以降の最新ウェブ開発』(Ethan Brown, 武舎広幸,武舎るみ/オライリージャパン)

Vuex の理解が難しい原因

なぜ Vuex が難しいと感じるのでしょうか?
私の場合は専門用語の意味が省略されていることに起因していました。
さらに問題なのは、 「Vuexを理解するためのキーとなる用語」が、全く違う意味で使われているにも関わらず見た目は一般的な日本語と一緒なのでなんとなくわかった気になり、「何が分からないのか分からない」状況に陥ることです。
例えば Vuex における「状態」
「アプリケーションが保持するデータ」
のことを指します。
なので、「Vuex は状態管理ライブラリである」「Vuex は状態を管理するために単方向データフローを採用している」といった説明や図解※を見ても、肝心の「状態」が分からないので、文章の意味が消化できないまま頭を素通りしていくだけでした。

しかし逆に言うと、用語の意味さえ押さえておけば Vuex はスラスラ理解できます。

※Vuexデータフローの図解
Vuex図解(Vuex公式ドキュメントより)

Vuex を理解するためのツボ

さて、前置きが長くなりましたが本題です。
たった 4 つだけです。

  • 用語を正確に理解する

    • 「状態」
    • 「データフロー」
  • 「データフローの設計」と「状態管理」の意義を理解する

    • 信頼できる唯一の情報源(Single Source of Truth)
    • 単方向フロー(one-way data flow)
    • 情報と取得のカプセル化(Encapsulation of sorce and receiving)
  • Vuex の構成要素の役割と使い方を理解する

    • State
    • Getters
    • Mutations
    • Actions
  • ※「ストアのモジュール分割」は一旦省略します

Vuex に入る前に

いきなり Vuex に入るより、まず状態管理やデータフローの基本知識を押さえておくと、スムーズに理解が進みます。

「状態」とは

状態とは
「アプリケーションが保持するデータ」
のことです。
ユーザーの操作やイベントの発生などによってその値が更新されていきます。

例えば、EC サイトのショッピングカートです。カートは何も入っていない空の状態から始まり、ユーザーが商品をカートに入れる操作を行うことでカートは空の状態に戻り、購入処理が完了します。

規模が大きいアプリケーションは保持する状態の数、それぞれの組み合わせの数も多くなり、そのままでは扱いきれなくなります。

繰り返しになりますが、Vuex において「状態」は普段の日本語とは異なる特別な意味がある言葉なので注意してください。

データフローとは

「データフロー」とは
「状態を含む、アプリケーションが持つデータの流れ」
のことを指します。
具体的には、どこにデータを保持し、データを読み込む時や更新するときはどこからどのように行うのかという点を表すことが多いです。

データフローの設計において、以下の三つのプラクティスが重要です。

信頼できる唯一の情報源

「信頼できる唯一の情報源」(single source)とは、「管理する対象のデータを一箇所に集約することで管理を容易にすることを目的とする設計のパターン」です。

  • どのコンポーネントも同一のデータを参照するため、データや表示の不整合が発生しづらい
  • 複数のデータを組み合わせた処理を比較できる容易に実装できる
  • データの変更のログ出力、現在のデータの確認などの開発に便利なツールを作りやすい

「状態の取得・更新」のカプセル化

「状態の取得・更新」のカプセル化を行うことで、状態管理のコストを下げることができます。
例えばカウンターアプリの例では更新処理を store 内に記述することでカプセル化しており、コンポーネント側からは具体的にどのような実装がされているかは隠されています。

  • 状態の取得・更新のロジックを様々な場所から利用できる
  • 詳細な実装をビューから隠すことで、データ構造や取得、更新処理の変更の影響範囲を小さくする
  • デバッグ時に確認する場所が限られるため、デバッグが容易になる

単方向データフロー

単方向データフローにすることで、状態の取得、更新のコードが簡潔になります。
データが単方向でないと、データの取得と更新の両方を同時にできてしまい、より複雑な処理になり理解が難しくなってしまいます。

  • データを取得しつつ更新するといったようなことができなくなり、実装やデバッグが単純になる
  • データを取得、更新するために何をするかの選択肢が絞られて、理解が容易なコードをかきやすい

まとめ

ここまでデータフローの三つのプラクティスを見てきましたが、実はVuex は先ほど紹介したデータフローのプラクティスを全て満たします。

まず、Vuex はアプリケーションの状態やそれに付随するロジックが一つの場所(ストア)にまとまるように設計されているため、「信頼できる唯一の情報源」を満たします。

また、Vuex において状態の更新はミューテーションでのみ行うことができ、取得に関してもゲッターという機能で詳細な実装は隠蔽できるため「状態の取得と更新」のカプセル化も満たします。

さらに、状態の取得と更新の窓口が異なるため(冒頭の図解をもう一度参照ください)、強制的に実装が単方向データフローになります。

おわりに

いかがだったでしょうか。VueやNuxtで開発を行う方が、Vuexを理解するための助けになれば幸いです。
「状態管理」「データフロー」についてはバッチリですか?
次の記事ではいよいよ Vuex による状態管理について見ていきます。

参考文献

『Vue.js入門 基礎から実践アプリケーション開発まで』(川口和也, 喜多啓介, 野田陽平, 手島拓也, 片山真也/技術評論社)
Vue.jsについての書籍は増えてきていますが、問題なのはその殆どがVuexについての説明を省略していることです。Vue.jsやNuxt.jsを用いた実際の開発においてVuexによる状態管理は必須ですが、学習の障壁になるとして避けてしまっているのでしょう。私が読んだ中で唯一、Vuexについて丁寧に説明していたのが本書です。Vuex以外の内容も素晴らしいの一言。本書はVue.js・Nuxt.jsの開発に関わるエンジニアや組織にとって必携です。保存用・実用用・観賞用に3冊購入しましょう。あるいは、あなたが経営者の場合はぜひエンジニアに対して一人一冊ずつ買い与えてください。
ただし、全くVueについて未経験という方への第一歩としては内容が本格的すぎるかもしれません。その場合は『Vue.js 超入門』がおすすめです。

『Vue.js 超入門』(掌田津耶乃/秀和システム)
とにかく分かりやすく、まず概要を把握するために最適の一冊です。「なんとなくで良いので概要を把握する」⇨「より詳細で厳密な理解する」という流れで学ぶとスムーズです。

『初めてのJavaScript 第3版 ―ES2015以降の最新ウェブ開発』(Ethan Brown, 武舎広幸,武舎るみ/オライリージャパン)
JavaScriptの根本的な理解ができる、革命的な良書です。分厚いので手強そうに見えますが、実際はとても親切で分かりやすい作りです。本書も一人一冊は欲しいところです。

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