- 投稿日:2020-03-29T23:19:37+09:00
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
- 投稿日:2020-03-29T22:59:14+09:00
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Ⅲ
Ⅳ
Ⅴ
Ⅵ
Ⅶ
- 投稿日:2020-03-29T22:07:08+09:00
【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すれば利用可能です。
利用側プロジェクトのソース.tsconsole.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 共通処理プロジェクトのパッケージ名
で更新します。
- 投稿日:2020-03-29T21:16:31+09:00
読みやすいコードを書きたい!
この記事の趣旨
自分の学習のメモとして残します。
どなたかのお役に立てたら光栄です。こんな方におすすめです
・コードが見づらいと言われる
・初心に返りたい
このような方には何か発見があると思います。読みやすいコードとは?
ズバリ『良いコード』のこと。
良いコードは、他人がそのコードを見た時に短時間で理解できるコードのことを言います。逆に、分かりづらい・理解しづらいコードは解読に時間がかかります。それだけ開発の工数もかかってしまい、効率が良くありません。
では、良いコードの条件・要素を紐解いていきましょう!
●コードの命名に規則を
変数やメソッドは好きなように命名ができます。
ルールがありませんので、個人の好きなようにできます。
特に共同開発の現場などでは、「他人が見てわかる」を意識する必要があります。◎命名のポイント
【目的がわかる単語を使う】
例)new → new_account【汎用的な名前は避ける】
・一時的な変数などは避ける
・可読性を意識して【名前に情報を含める】
大文字、小文字をルールに沿って活用
【誤解されない名前を使う】
・何がしたいかが明確な名前
・説明的に長くなっても良いので、可読性重視
例)read_books → already_read_books●コードレイアウト
プログラムの挙動に影響はないが、可読性を大幅にあげることができる
◎レイアウトのポイント
・整列 :縦列を揃える。イコールの位置など縦が揃うと見やすい
・一貫性 :似たような構造は同じフォーマットに統一できないか検討
・ブロック化 :同じ系統の変数などをまとめてグループ化すること●コメント
・プログラムの動作を説明
・他の開発者がコードを読む際の理解を助ける
※多すぎても読むのに時間がかかるため、簡潔に◎コメントのポイント
・理由をコメントする :なぜそのコードを書いたか
・他の開発者へメモを残す:開発中のメモとして
・実際の例を記入する :コメントでは伝わりづらい時は、コメントとしてコードを記載まとめ
結局大事なことは
「人に対する思いやり」だなぁと。複数人で仕事をする以上、「自分だけ良ければそれで良い」という考えはNG。
誰もが見やすく、仕事をしやすい状況を自分が作り出す意識が大切。そのための知識や技術であると思う。
これからしっかり学んでいきましょう。
- 投稿日:2020-03-29T20:48:35+09:00
JavaScriptの難所「Prototype」について解説してみた(学習まとめ)
始めに(前書き)
どうもこんにちは、
今回はJavaScriptの難所であろう「Prototype」の概念について解説してみたいと思います。
この記事を読むにあたっては、ある程度オブジェクト指向について理解していないと難しいかと思いますので、読者様におかれましては、事前にオブジェクト指向について学習してから読まれることをオススメします。
また記事の内容に漏れや誤り等ありましたら、ご指摘いただけると助かります。
それでは、よろしくお願いいたします。本題(本文)
JavaScriptは、(どうやら世間的には)一応はオブジェクト指向言語に分類されるそうですが、
この「Prototype」(以降「プロトタイプ」)の概念があるためにややこしいことになっています。
クラスとは、インスタンスの本になるもので、インスタンスが”実体”だとすると、その”設計図”にあたるものとなります。
たとえば、”自動車”がクラスなら、このクラスを本にして、”カローラ”だとか”フェラーリ”がつくられますが、どれもが自動車なので、同じクラスから継承させて実体をつくります。当然、”カローラ”や”フェラーリ”は、自動車としての(必要な)機能や性質を共通に持ちます。
ここで、プロトタイプとは、複数の任意のインスタンスの間で、(必ずしも)共通の機能や性質を見出せなくてもいいことになっています。
つまり、あるクラスを本にして、そのクラスから継承されているインスタンスであっても、
本になっている(=継承元となっている)クラスにはない機能や性質を持つことができるということです。
これは他の(一般に)オブジェクト指向言語に分類される言語のクラスに関する縛り・制約を緩めたような仕様となっています。
しかも、JavaScriptでは、実行時に動的にインスタンスの持つ機能や性質を変えられる(=メンバーを追加・削除・変更ができる)ようになっています。終わりに(後書き)
この記事では、至極簡単にプロトタイプについて解説してみました。
実際に動くコードをつかって解説したほうがよかったかもしれませんが、
読者様にとって参考になる、あるいは納得していただけるものが書ける自信がなかったので、
とりあえず今回はコードはなしということで、この記事の執筆を終えたいと思います。
また暇があったら、この記事にコードを書き加えることになるかもしれません。
今回は以上です。
最後までお付き合いくださり、ありがとうございました。
- 投稿日:2020-03-29T20:20:01+09:00
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上での画面表示イベントは発生する)がページは再読み込みされない」という動作仕様になっているようです。これの何が問題かと言うと、前の画面を表示した際の画面表示イベントにより
visibility
がvisible
になった状態のまま次の画面を表示した際の画面表示イベントが処理される、つまり画面表示イベント発生時点で初期状態が非表示になっていないケースがあるということです。
このままだと、それらのボタンをクリックした場合は画面表示が一瞬遅れる状態のままになってしまいます。ボタンクリック時のイベントを追加
この問題を解消するため、対象のボタンに対して「クリック時にレコード全体を非表示にするイベント」を追加することにします。
今回の場合、クリックしてもページが再読み込みされないボタンは次の通りです(カッコ内はクラス名)。
- 詳細画面:レコードを編集する(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; });
これで、「レコードを編集する」などのボタンをクリックした瞬間に一度visibility
がhidden
になり、その後スクリプト処理が完了した時点でvisibility
がvisible
になります。今度こそ、処理が完了するまでレコード全体を非表示にするスクリプトが完成しましたとさ。
どっとはらい。それでも問屋は卸さない
これで完成かと思いきや、実際に動かしてみるとレコード詳細表示画面からレコード編集画面に遷移した際にレコード全体が表示されないことがありました。
これは何故でしょうか。
処理の動きを時系列で考えてみます。
- レコード詳細表示画面で編集ボタンをクリック。
- 追加したクリックイベントが発火し、
visibility
をhidden
にする。- レコード編集画面での画面表示のイベントにより、
visibility
をvisible
にする。本来はこのような順番で処理されるのが理想なのですが、レコード数やフィールド数が少ない時には2.と3.の順番が逆になる場合があることが分かりました。
つまり、
- レコード詳細表示画面で編集ボタンをクリック。
- レコード編集画面での画面表示のイベントにより、
visibility
をvisible
にする。- 追加したクリックイベントが発火し、
visibility
をhidden
にする。という順番で処理される場合があるのです。
こうなってしまうと、最終的にvisibility
がhidden
になってしまうため、レコード全体が表示されなくなってしまいます。非同期で処理を遅延
クリックイベントとkintone上での画面表示イベント、どちらが先に処理されるかはその時々で変わるため分かりません。
分からないものは仕方ないので、visibility
をvisible
にする際に、その時点のvisibility
を取得して判定することにします。取得した
visibility
がvisible
の場合は、ボタンクリック時のイベントがまだ処理されていないと考えられるためvisibility
がhidden
になるまで待機します。
visibility
がhidden
になった、つまりボタンクリック時のイベントが処理されたのを確認した上でvisibility
をvisible
に変更します。ただし、何かの手違いで
visibility
がhidden
にならない場合を考慮して、一定回数試行した後は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は仕様上、複数登録したスクリプトのうちどれか一つでエラーが発生すると他のスクリプトの実行も停止してしまいます。
他にもスクリプトやプラグインを登録している場合は、そちらでスクリプトエラーが発生した時にもレコードが表示されなくなってしまいますので、使用する際は注意してください。
- 投稿日:2020-03-29T20:08:17+09:00
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 xxxxxxAuthor name
- 作者の名前を入力
? Author name xxxChoose programming language
- TypeScriptを使うか選択
? Choose programming language (Use arrow keys) ❯ JavaScript TypeScriptChoose the package manage
- YarnとNpmのどちらかを使うか選択
? Choose the package manager (Use arrow keys) ❯ Yarn NpmChoose 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.jsChoose custom server framework
- サーバーサイドのフレームワークを選択
? Choose custom server framework (Use arrow keys) ❯ None (Recommended) AdonisJs Express Fastify Feathers hapi Koa MicroChoose the runtime for TypeScript
- Runtimeを使うかの選択
- Runtimeについてはドキュメント参照
? Choose the runtime for TypeScript (Use arrow keys) ❯ Default @nuxt/typescript-runtimeChoose 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 ◯ DotEnvChoose linting tools
- 静的解析の選択
? Choose linting tools (Press <space> to select, <a> to toggle all, <i> to inver t selection) ❯◯ ESLint ◯ Prettier ◯ Lint staged files ◯ StyleLintChoose test framework
- ユニットテストの選択
? Choose test framework (Use arrow keys) ❯ None Jest AVAChoose rendering mod
- SSRかSPAの選択
? Choose rendering mode (Use arrow keys) ❯ Universal (SSR) Single Page AppChoose development tool
jsconfig.json
とSemantic Pull Request
を入れるか選択jsconfig.json
はVS 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インストール完了
起動する
yarnの場合
yarn run devnpmの場合
npm run dev
- http://localhost:3000/ にアクセス
ビルド
dist
以下にファイルが出力されるyarn run buildgenerate
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' ] }
- 投稿日:2020-03-29T20:01:29+09:00
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.csscanvas{ width: 300px; height: 400px; border: solid 1px #000; }JavaScriptで実装
実装する機能
・input type="file"のファイルの取得
・ファイルのURLの取得
・canvasのsrcにファイルのURLを設定index.jsvar 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に画像が表示されるようになる
参考
・画像ファイルの読み込み(https://javascript.programmer-reference.com/js-canvas-draw/)
・画像をcanvasに表示する(https://www.pazru.net/html5/File/040.html)
- 投稿日:2020-03-29T19:54:22+09:00
[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.jsctx.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)
メゾットで目盛線を描画しています。目盛の見た目を設定
②では、時刻を表す目盛を求めて目盛の太さを設定し、③の関数
_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も身につく!
- 投稿日:2020-03-29T19:49:52+09:00
Concurrent Mode時代のReact設計論 (3) SuspenseやuseTransitionが何を解決するか
この記事は「Concurrent Mode時代のReact設計論」シリーズの3番目の記事です。
シリーズ一覧
- Concurrent Mode時代のReact設計論 (1) Concurrent Modeにおける非同期処理
- Concurrent Mode時代のReact設計論 (2) useTransitionを活用する
- Concurrent Mode時代のReact設計論 (3) SuspenseやuseTransitionが何を解決するか
- Concurrent Mode時代のReact設計論 (4) render-as-you-fetchのためのコンポーネント設計(仮)
- Concurrent Mode時代のReact設計論 (5) トランジションを軸に設計する(仮)
- Concurrent Mode時代のReact設計論 (6) ステート管理ライブラリの展望(仮)
- Concurrent Mode時代のReact設計論 (7) まとめ(仮)
Suspense
やuseTransition
が何を解決するか前回までは、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のデータをロード中の段階」は、
PageA
のisLoading
ステートがtrue
になり、Root
がfetchUsers()
の結果を待っている段階として現れます。そして、「ロードが終わって画面Bをレンダリングする段階」はRoot
のsetState
でステートを変更して画面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> ); };
isLoading
をuseState
で宣言するのをやめてuseTransition
を使うようになりました。画面Bへの遷移(goToPageB()
)をstartTransition
で囲むことで、遷移時にサスペンドが発生したらボタンにLoadinng...
が表示されるという制御がされています。目ざとい方は、この設計は微妙だと思ったかもしれません。というのも、
startTransition
は中でステートを更新することで意味を発揮する関数なのに、goToPageB
という関数は「画面Bに遷移する」という抽象化された意味を持たされており、中でステートの更新が行われることが明らかではありません。今回はgoToPageB
の実態がsetState({ ... })
なので偶々うまくいっていますが、startTransition
とsetStage
という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の恩恵をより受けるためにどのような設計がベストかについて考えていきます。
次の記事: 鋭意執筆中です。
- 投稿日:2020-03-29T19:49:45+09:00
Concurrent Mode時代のReact設計論 (2) useTransitionを活用する
この記事は「Concurrent Mode時代のReact設計論」シリーズの2番目の記事です。
シリーズ一覧
- Concurrent Mode時代のReact設計論 (1) Concurrent Modeにおける非同期処理
- Concurrent Mode時代のReact設計論 (2) useTransitionを活用する
- Concurrent Mode時代のReact設計論 (3) SuspenseやuseTransitionが何を解決するか
- Concurrent Mode時代のReact設計論 (4) render-as-you-fetchのためのコンポーネント設計(仮)
- Concurrent Mode時代のReact設計論 (5) トランジションを軸に設計する(仮)
- Concurrent Mode時代のReact設計論 (6) ステート管理ライブラリの展望(仮)
- Concurrent Mode時代のReact設計論 (7) まとめ(仮)
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
に渡されたコールバック関数は即座に呼び出されます。この実装では、ボタンを押すと以下のスクリーンショットのような挙動となります。
これを理解するために。
useTransition
の挙動を簡単に説明します。startTransition
の内部で行われたステートの更新がサスペンドを発生させた場合、変更後ではなく変更前のステートがレンダリングされます。ただし、このとき変更前のステートではuseTransition
が返すisLoading
がtrue
になっています。投げられたPromiseが解決された場合は変更後のステートで再レンダリングされます。
useTransition
を使わない場合との違いはサスペンド中に現れます。useTransition
を使わない場合はSuspense
によるフォールバックが表示されますが、useTransition
を使う場合はフォールバックは表示されず、代わりにステート更新前の状態が(isLoading
がtrue
で)レンダリングされるのです。
useTransition
にオプションとして渡したtimeoutMs
は、この「isLoading
がtrue
の状態」の最大持続時間を表します。この時間が過ぎてもPromiseが解決されなかった場合、諦めて変更後のステートがレンダリングされます。ただし、まだPromiseが解決されていないのでSuspense
によりフォールバックが表示されます。ボタンがクリックされてからの流れは次のようになります。
- 初期状態では、
usersFetcher=undefined, isLoading=false
である。(上のスクリーンショットの左の状態)startTransition
内でsetUsersFetcher
が呼ばれ、usersFetcher
ステートが更新される。(このときnew Fetcher
で作られたオブジェクトをF
とする)useTransition
の効果ににより、まずusersFetcher=undefined, isLoading=true
の状態でContainer
がレンダリングされ、DOMに反映される。(上のスクリーンショットの真ん中の状態)- 次に、新しいステート(
usersFetcher=F, isLoading=false
)でContainer
がレンダリングされる。これはUserList
のレンダリングに繋がり、UserList
のレンダリングはサスペンドする。useTransition
の効果により、この状態はDOMに反映されない。F
が持つPromiseが解決されると、新しいステート(usersFetcher=F, isLoading=false
)でContainer
が再レンダリングされる。今回はサスペンドが発生せずにレンダリングが完了し、この状態がDOMに反映される。(スクリーンショットの右の状態)ポイントは、
useTransition
内でステートの更新を行なった場合、新しいステートよりも「元のステート+isLoading=true
」のレンダリングが優先されるということです。これは、isLoading=true
の状態でユーザーへのフィードバックを表すことを意図しているためです。ユーザーへのフィードバックは最優先で画面に反映されるべきであるため、これが最初に処理されます。ちなみに、
startTransition
の中と外の両方でステートの更新を行うことができます。この場合、startTransition
の外で行なった更新は3の段階で反映されています(もちろん5の段階にも反映されます)。また、
timeoutMs
で設定した時間を超えない限り、Suspense
のfallback
で指定した内容は表示されなくなります。useTransition
をきちんと使っている限りは、Suspense
のfallback
はいわば最終防衛ラインのような扱いになり、高頻度でユーザーが目にするものではなくなります。
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が何を解決するか
- 投稿日:2020-03-29T19:49:45+09:00
Concurrent Mode時代のReact設計論 (2) `useTransition`を活用する
この記事は「Concurrent Mode時代のReact設計論」シリーズの2番目の記事です。
シリーズ一覧
- Concurrent Mode時代のReact設計論 (1) Concurrent Modeにおける非同期処理
- Concurrent Mode時代のReact設計論 (2) useTransitionを活用する
- Concurrent Mode時代のReact設計論 (3) SuspenseやuseTransitionが何を解決するか
- Concurrent Mode時代のReact設計論 (4) render-as-you-fetchのためのコンポーネント設計(仮)
- Concurrent Mode時代のReact設計論 (5) トランジションを軸に設計する(仮)
- Concurrent Mode時代のReact設計論 (6) ステート管理ライブラリの展望(仮)
- Concurrent Mode時代のReact設計論 (7) まとめ(仮)
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
に渡されたコールバック関数は即座に呼び出されます。この実装では、ボタンを押すと以下のスクリーンショットのような挙動となります。
これを理解するために。
useTransition
の挙動を簡単に説明します。startTransition
の内部で行われたステートの更新がサスペンドを発生させた場合、変更後ではなく変更前のステートがレンダリングされます。ただし、このとき変更前のステートではuseTransition
が返すisLoading
がtrue
になっています。投げられたPromiseが解決された場合は変更後のステートで再レンダリングされます。
useTransition
を使わない場合との違いはサスペンド中に現れます。useTransition
を使わない場合はSuspense
によるフォールバックが表示されますが、useTransition
を使う場合はフォールバックは表示されず、代わりにステート更新前の状態が(isLoading
がtrue
で)レンダリングされるのです。
useTransition
にオプションとして渡したtimeoutMs
は、この「isLoading
がtrue
の状態」の最大持続時間を表します。この時間が過ぎてもPromiseが解決されなかった場合、諦めて変更後のステートがレンダリングされます。ただし、まだPromiseが解決されていないのでSuspense
によりフォールバックが表示されます。ボタンがクリックされてからの流れは次のようになります。
- 初期状態では、
usersFetcher=undefined, isLoading=false
である。(上のスクリーンショットの左の状態)startTransition
内でsetUsersFetcher
が呼ばれ、usersFetcher
ステートが更新される。(このときnew Fetcher
で作られたオブジェクトをF
とする)useTransition
の効果ににより、まずusersFetcher=undefined, isLoading=true
の状態でContainer
がレンダリングされ、DOMに反映される。(上のスクリーンショットの真ん中の状態)- 次に、新しいステート(
usersFetcher=F, isLoading=false
)でContainer
がレンダリングされる。これはUserList
のレンダリングに繋がり、UserList
のレンダリングはサスペンドする。useTransition
の効果により、この状態はDOMに反映されない。F
が持つPromiseが解決されると、新しいステート(usersFetcher=F, isLoading=false
)でContainer
が再レンダリングされる。今回はサスペンドが発生せずにレンダリングが完了し、この状態がDOMに反映される。(スクリーンショットの右の状態)ポイントは、
useTransition
内でステートの更新を行なった場合、新しいステートよりも「元のステート+isLoading=true
」のレンダリングが優先されるということです。これは、isLoading=true
の状態でユーザーへのフィードバックを表すことを意図しているためです。ユーザーへのフィードバックは最優先で画面に反映されるべきであるため、これが最初に処理されます。ちなみに、
startTransition
の中と外の両方でステートの更新を行うことができます。この場合、startTransition
の外で行なった更新は3の段階で反映されています(もちろん5の段階にも反映されます)。また、
timeoutMs
で設定した時間を超えない限り、Suspense
のfallback
で指定した内容は表示されなくなります。useTransition
をきちんと使っている限りは、Suspense
のfallback
はいわば最終防衛ラインのような扱いになり、高頻度でユーザーが目にするものではなくなります。
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が何を解決するか
- 投稿日:2020-03-29T19:49:37+09:00
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を理解していなくても大丈夫です。(宣伝)
シリーズ一覧
- Concurrent Mode時代のReact設計論 (1) Concurrent Modeにおける非同期処理
- Concurrent Mode時代のReact設計論 (2) useTransitionを活用する
- Concurrent Mode時代のReact設計論 (3) SuspenseやuseTransitionが何を解決するか
- Concurrent Mode時代のReact設計論 (4) render-as-you-fetchのためのコンポーネント設計(仮)
- Concurrent Mode時代のReact設計論 (5) トランジションを軸に設計する(仮)
- Concurrent Mode時代のReact設計論 (6) ステート管理ライブラリの展望(仮)
- Concurrent Mode時代のReact設計論 (7) まとめ(仮)
現在は(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が解決されたら再度レンダリングが試みられます。まだ、サスペンドが発生したときに代替のビューを提供する機能が提供されます(
Suspense
やuseTransition
)。これらの機能を使うことで、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」ボタンを押すとusersFetcher
にFetcher<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
の中身がレンダリングされます。より具体的な流れとしては以下のことが発生しています。
Container
内でsetUsersFetcher
が呼び出されることでusersFetcher
ステートにFetcher
が入る。Container
が再レンダリングされてUserList
がレンダリングされる。UserList
がレンダリングされる(関数UserList
が呼び出される)最中に、get()
でPromiseがthrow
される(UserList
がサスペンドする)。- サスペンドが発生したので、
Suspense
の中身として<p>Loading...</p>
がレンダリングされる。- しばらくして
usersFetcher
が返したPromiseが解決される。- ReactがPromiseの解決を検知し、以前サスペンドした
UserList
が再レンダリングされる。- 今回は
get()
がPromiseを投げない(解決済のため)のでUserList
はサスペンドされずに描画される。一応画面の動きを示しておくと、このようになります。
従来の方式との比較
一応、従来の方式(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を活用する
実際に上述のアプリで使われているバージョンではさらに
getOrUndefined
というメソッド(解決前だったらthrow
するのではなくundefined
を返す)があるのですが、これが本質的に必要なのかは悩んでいます。設計力の不足により必要になってしまっただけかもしれません。 ↩
- 投稿日:2020-03-29T19:49:00+09:00
常に動く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」をクリック・・・・う。。まあよくある。
(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ではどうやってみたらいいかわからずわざわざこんなことしました。。。 )
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に返す。
分岐部分.phpif(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 ]; } }サンプル
できたー レンタルサーバ上で動いたー!
— yuta kawashima (@y_kawashima_) March 29, 2020
前回のはngrokでやってたから普段使えなかったけど、これでいつでも使える!
#protoout pic.twitter.com/HznZSciExJ全ソースコード
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を使ったお引っ越しもやってみようと思います。
- 投稿日:2020-03-29T19:48:37+09:00
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成功しました!番外編
他にも対処法はいくつかあり、therubyracerというGemのインストールでも解決できるようです。(参考リンクご参照ください)
現在、チーム開発中だったので、自分以外の複数端末への影響を考え今回はGem以外の方法を選択しました。
参考
にさせていただきました。ありがとうございます。
https://qiita.com/azusanakano/items/771dc9919f347de061d7
- 投稿日:2020-03-29T19:34:38+09:00
Dart における class とObject の使い方。
Dartにおける class と object の使い方
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を書いてみましょう。
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を使って)。
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を作成。
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())に、更なる機能を付け加える。
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)
- 投稿日:2020-03-29T18:52:51+09:00
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}
- 投稿日:2020-03-29T17:38:47+09:00
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>(今回取得した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 %> <!-- xmlのitemをループ処理する --> <% 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> <% } %>表示結果
変更後のコード
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/58Dateオブジェクトの使い方入門
https://www.sejuku.net/blog/30171Dateオブジェクトのプロパティ(日付の表示形式を参考にしました。)
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
- 投稿日:2020-03-29T17:38:47+09:00
【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>(今回取得した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 %> <!-- xmlのitemをループ処理する --> <% 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> <% } %>表示結果
変更後のコード
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/58Dateオブジェクトの使い方入門
https://www.sejuku.net/blog/30171Dateオブジェクトのプロパティ(日付の表示形式を参考にしました。)
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
- 投稿日:2020-03-29T17:35:02+09:00
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
- 投稿日:2020-03-29T17:20:21+09:00
Vue.jsでTo-Do Listを作ってみた。
Vue.jsの学習を本日から始めました!
めちゃくちゃ面白かったので、記事にします。やったこと
「今日のTo-Do List」を作成しました。
普段はUdemyや本を参考に学習することが多いのですが、今回は久しぶりにドットインストールを使ってみました。
ドットインストールさん、分かりやすかったです。ありがとうございます。回し者ではありません。ドットインストールを参考に作ったので、見た目はほぼドットインストールのままですが、ToDoをする予定時刻を自分で入れてみました。
これからもっと手のこんだアプリケーションをVue.jsで作っていきたいです。学んだこと
導入が楽
scriptタグをhtmlファイルに記載するだけで使えました。
開発環境の構築は大変な時が多々ありますが、Vue.jsは非常に楽でした。双方向データバインディング
テキストボックスに入力した途端に出力される文字が変わる、といったことがこの双方向データバインディングに例になると思います。
これを最初学んだときは驚きました。しかも、それが数行のコードですぐ簡単に実装できてしまうのです。学びやすい
学習コストが少し低いように感じました。まだ最初だからだと思いますが、最初にしてはとっつきやすかったです。
これからVue Routerを使ってSPAを作ろうと思っておりますが、おそらくその過程で苦戦することでしょう。。。これから
初めてJavaScriptのフレームワークを本格的に学習してみましたが、非常に興味深かったです。
上でも書いたようにこれからはVue.jsでシングルページアプリケーションを作ってみます。
その際にはまた記事にします!参考
- 投稿日:2020-03-29T17:15:48+09:00
今更ながら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.jsximport 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.jsximport 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.jsximport 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.jsximport 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.jsximport 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.jsxconst [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.jsxconst 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を踏まえて処理したい場合に有効
- 投稿日:2020-03-29T16:58:06+09:00
【曖昧さ回避】ブラウザレンダリングにおける「ファイルの読み込み」が意味するものとは
「ファイルの読み込み」とは
ブラウザレンダリングの仕組みを解説するサイトや書籍には、「ファイルを読み込んで〜」のような説明が多くあります。
自分がレンダリング工程を勉強しているときに、この「読み込み」という言葉がファイルのDownload(転送)を指すのか、ファイルのParse(解析)を指すのか、はたまたレンダリング全体のことを言っているのか、説明する場面によって意味が変わる曖昧な言葉だなーと感じていました。ここではブラウザレンダリングの仕組みについて、1.HTMLのみ、2.HTMLとCSS、3.HTMLとJavaScript、4.HTMLとCSSとJavaScriptの4パターンに分けて、レンダリングフローに定義された言葉に当てはめながら説明していきたいと思います。
(検証環境:Google Chrome バージョン: 80.0.3987.87)
ブラウザレンダリングの仕組みの大枠
ブラウザレンダリングのフローは大きく4つの工程に分けられ、それぞれの工程は更にいくつかの細かい工程に分けられます。
(参考:Webフロントエンド ハイパフォーマンス チューニング -久保田 光則 (著) )
- Loading(データのダウンロード・解析)
- Download
- Parse
- Scripting(JSの実行)
- Rendering(スタイルの計算、当て込み)
- Calculate Style
- Layout
- Painting(描画)
- Paint
- Rasterize
- Composite Layers
図を見ていると全ての工程がシリアル(直列)に進んでいくように誤解しやすいのですが、実際はそうではありません。
レンダリングエンジンがページ表示を最適化する中で、部分的にでも準備ができた段階で、都度次の工程に進むこともあります。本記事では主にLoading(Download、Parse)とScriptingの工程に関して、ファイルごとにどのように影響を及ぼし合い、レンダリングの処理順が決まっているかについて説明します。
RenderingやPaintingの工程を含むブラウザレンダリング全体の仕組みについては以下記事が詳しいです。
フロントエンジニアなら知っておきたいブラウザレンダリングの仕組みをわかりやすく解説! | LeapIn1.HTMLのみ
はじめに外部ファイル「読み込み」記述が一切ない純粋なHTMLファイルについて、
ブラウザ検索バーにURLを入力し、HTTPプロトコルで通信してページを表示する場合を考えます。
(参考:ネットワークやTCP/IPやHTTPの基本(初学者向け) - Qiita)レンダリングの工程としては、まずHTMLのDownloadが始まりますが、
ここでのポイントは、サーバからHTMLファイルなどのリソースが転送される手法は0か1の転送ではなく、
セグメントに分割しながら転送されるということです。
(どのくらいまとめて送るのかについてはサーバサイドで制御するようです)前提として、ブラウザはUX向上のため画面に何も表示されていない時間を短くするように動きます。
よって全てのHTMLDownloadが完了していなくても、転送されたHTMLセグメントを元にParse(DOMツリー構築)や後続の処理が進み、準備ができたDOMから画面描画が始まります。
上記はChrome DevToolsのNetworkパネルであり、一つのHTMLファイルをダウンロード完了するまでの解析図です。(テスト用にサーバサイド(PHP)でファイルの転送や解析速度を調整しています)Waiting(TTFB:Time To First Byte)とはファイル転送リクエストを送ってからクライアント側で最初のデータを受け取るまでにかかる時間(主にサーバサイドの処理時間)であり、Content Downloadとは最初のデータを受け取ってから全てのデータを受け取りきるまでにかかる時間です。
解析グラフによるとContent Downloadに合計2sかかっていますが、その間も転送されてきているデータを元に別の処理(Parse、Rendering、Painting)が都度進んで描画が始まっており、それは同Performanceパネルで解析することができます。↓
データを受け取る(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
この場合も、まずHTMLのDownload、Parseが始まり、解析途中で
link要素
を見つけた段階でCSSのDownloadが始まります。↓
CSSのDownloadはHTMLのParseをブロックしないので、CSSDownload中もHTMLParseが並行して進みます。
そしてその先に再び外部CSS「読み込み」記述ががあれば、同時に複数のCSSDownloadが始まります。ただし、モダンブラウザでは(同じドメインの)TCP接続は同時に6本までという制限があるため、7本目以降の接続は前の接続の終了を待ってからとなります。
見ての通りこれではダウンロードしたいファイルが多いほどページ表示速度が遅くなってしまいます。そのため、対応策としてファイルを可能な限りまとめてリクエスト必要数を抑えたり、CDNなどを利用してあえて別ドメインから接続することでスループットを上げたり、一つのTCP接続で同時に複数のリクエスト/レスポンスを処理できるhttp/2プロトコルで通信するなどの手法が存在します。
参考:そろそろ知っておきたいHTTP/2の話 - Qiita
CSSのParse
CSSもHTMLと同様にDownloadの次の工程として、Parse(CSSOMの構築)の工程があります。
考慮すべき注意点は以下です。
- CSSParseは見かけ上はHTMLParseと並行して行われる。
- HTMLは描画工程に進もうとするDOMの、直前までに記載されているCSSのLoading(Download、Parse)が完了しない限り、Renderingフローに進まない。(描画処理が行われない)
HTMLParseとCSSParseはどちらもレンダリングエンジンのmainスレッドで行われますが、mainスレッドでは同時に一つの処理しか行えないため、それぞれの処理が同時に走ることはありません。
ですが、HTMLParseのアイドル時間などにCSSParseが進むため、見かけ上は2つが並行して行われているように見えます。
(そもそもCSSParseにかかる時間はブラウザレンダリング全体の時間からすると極めて短く、議論に上がりにくい部分のようです。)また、CSSのLoadingが進行中の場合は、たとえHTMLParseが先に完了していてもRenderingなどの次の工程に進まず、結果として画面描画が行われません。
これはブラウザがFOUC(Flash of Unstyled Contentの略。スタイルがついていないコンテンツが一瞬表示されること)を防ぐために、CSSParseの完了を待ってスタイルが適応された画面描画を行おうとするためです。
上記Performanceパネル解析図を見ても、Finish Loading(CSSParseの完了)まで、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の最後に記述するべきと言われる理由の一つです。
上記図より、Send RequestでJSDownloadが始まると、Evaluate Script工程が完了するまでHTMLParseが行われていないことが分かります。async属性とdefer属性
script要素
によるJSの「読み込み」記述はそれ以降のHTMLParseをブロックしますが、script要素
にasync
やdefer
の属性をつけることによってJSのDownloadを非同期に行い、HTMLParseと同時に処理することができます。HTML<script src="main.js" async ></script> <!-- もしくは --> <script src="main.js" defer ></script>以下は先程と同じ記述で、
defer属性
を使用したときのPerformanceパネルの解析結果です。
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>CSSDownloadはHTMLParseをブロックしないため、HTMLParseは
script要素
の記述に到達しJSのDownloadが始まります。
先程「HTMLは直前までのCSSLoading(Download、Parse)が完了していない限り、Renderingフローに進まない」と説明しましたが、実は同様にJSも直前までのCSSLoading(Download、Parse)が完了していない限り、Scriptingの工程に進まない性質があります。つまりこの場合、CSSよりもJSのほうが速くDownloadが完了したとしても、CSSParseが完了するまでScriptingが待機状態になるということです。
↑JSのほうがCSSよりも1s速くDownloadが完了していますが、
↑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が同時に行われていることが分かります。
実はChromeなどのモダンブラウザには、HTMLParseが進んでいない部分についてもDownloadが必要な記述がないか確認し、もしあれば事前にそのファイルのDownloadを開始する機能があります。(Preload Scanner)よってこの場合も、ブラウザはJSのDownload中にその先にあるCSSの「読み込み」記述を読み取り、CSSDownloadも同時に進めることでレンダリングを高速化しているのです。
※Preload Scanner機能で事前処理できるのはDownloadの工程だけです。ParseやScriptingの工程は本来のレンダリングフローに沿って行われます。
参考:rel="preload"を極めるために必要な2種類のプリロード機能 | Raccoon Tech Blog
まとめ
- HTMLはセグメントごとにDownloadが行われ、都度Parseなどの先の工程に進む
- CSSのDownloadはHTMLのParseをブロックしない
- CSSのParseは見かけ上はHTMLParseと並行して行われる
- HTMLは直前までのCSSLoading(Download、Parse)が完了していない限り、Renderingの工程に進まない
- JSのDownloadとScripting(実行)はHTMLのParseをブロックする
- JSも直前までのCSSLoading(Download、Parse)が完了しない限り、Scriptingの工程に進まない
誤った解釈等ございましたら、ご教授お願いいたします。。
参考
- Webフロントエンド ハイパフォーマンス チューニング -久保田 光則 (著)
- フロントエンジニアなら知っておきたいブラウザレンダリングの仕組みをわかりやすく解説! | LeapIn
- ネットワークやTCP/IPやHTTPの基本(初学者向け) - Qiita
- Ace the Lighthouse Audit: Best Practices for Consistent Interactivity | Lumavate
- そろそろ知っておきたいHTTP/2の話 - Qiita
- scriptタグに async / defer を付けた場合のタイミング - Qiita
- DOMContentLoaded周りの処理を詳しく調べてみました - Qiita
- rel="preload"を極めるために必要な2種類のプリロード機能 | Raccoon Tech Blog
- フロントエンドのパフォーマンスを徹底解説!ブラウザの気持ちで理解するHTML/Javascript/CSSの話 | Raccoon Tech Blog
- 投稿日:2020-03-29T16:22:09+09:00
【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 span
にposition: 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は一層おもしろみが増す気がします。
- 投稿日:2020-03-29T16:22:09+09:00
【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 span
にposition: 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は一層おもしろみが増す気がしました。
- 投稿日:2020-03-29T16:03:07+09:00
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.tsimport 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.tsexport default interface Iterator { hasNext(): boolean; next(): any; }
hasNext
メソッドは、集合の各要素を順番に処理する中で次の要素があるのかどうかを返却します。
next
メソッドは、集合の各要素を順番に処理する中で次の要素それ自体を返却します。
具体的な処理内容は本インターフェースの実装で確認しましょう。modules/TaskListIterator.ts
Iterator
インターフェースの実装です。TaskListIterator.tsimport 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.tsimport Iterator from './Iterator'; export default interface Aggregate { iterator(): Iterator; }modules/TaskList.ts
Aggregate
インターフェースの実装です。TaskList.tsimport 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パターンの全体像を整理するのにお役立てください。
- 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ファイル自動生成も試したい以上です。
最後までご覧いただきありがとうございました!
- 投稿日:2020-03-29T15:17:32+09:00
練習記録【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
ーーーーーーーーーーーーーーーーーーーーーーー
って表示されるよ。
- 投稿日:2020-03-29T15:17:32+09:00
練習記録【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
ーーーーーーーーーーーーーーーーーーーーーーー
って表示されるよ。
- 投稿日:2020-03-29T15:17:32+09:00
練習記録【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
ーーーーーーーーーーーーーーーーーーーーーーー
って表示されるよ。
- 投稿日:2020-03-29T14:21:39+09:00
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 を理解するためのツボ
さて、前置きが長くなりましたが本題です。
たった 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の根本的な理解ができる、革命的な良書です。分厚いので手強そうに見えますが、実際はとても親切で分かりやすい作りです。本書も一人一冊は欲しいところです。