Merge branch 'main' of https://github.com/microsoft/Web-Dev-For-Beginners into main
commit
4d0a784607
@ -0,0 +1,304 @@
|
|||||||
|
# バンキングアプリを作ろう その 1: Web アプリの HTML テンプレートとルート
|
||||||
|
|
||||||
|
## レッスン前の小テスト
|
||||||
|
|
||||||
|
[レッスン前の小テスト](https://nice-beach-0fe9e9d0f.azurestaticapps.net/quiz/41)
|
||||||
|
|
||||||
|
### イントロダクション
|
||||||
|
|
||||||
|
ブラウザに JavaScript が登場して以来、Web サイトはこれまで以上にインタラクティブで複雑になっています。Web 技術は現在では、ブラウザに直接実行される完全に機能的なアプリケーションを作成するために一般的に使用されており、[Web アプリケーション](https://ja.wikipedia.org/wiki/%E3%82%A6%E3%82%A7%E3%83%96%E3%82%A2%E3%83%97%E3%83%AA%E3%82%B1%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3)と呼ばれています。Web アプリケーションは高度にインタラクティブであるため、ユーザーはアクションが実行されるたびに全ページのリロードを待ちたくありません。そのため、JavaScript は DOM を使用して HTML を直接更新し、よりスムーズなユーザーエクスペリエンスを提供するために使用されます。
|
||||||
|
|
||||||
|
このレッスンでは、HTML テンプレートを使用して、HTML ページ全体をリロードすることなく表示・更新できる複数の画面を作成し、銀行の Web アプリを作成するための基礎を構築していきます。
|
||||||
|
|
||||||
|
### 前提条件
|
||||||
|
|
||||||
|
このレッスンで構築する Web アプリをテストするためには、ローカルの Web サーバーが必要です。もし持っていない場合は、[Node.js](https://nodejs.org/ja) をインストールして、プロジェクトフォルダから `npx lite-server` コマンドを使用してください。これでローカルの Web サーバーが作成され、ブラウザでアプリを開くことができます。
|
||||||
|
|
||||||
|
### 準備
|
||||||
|
|
||||||
|
コンピュータ上に `bank` という名前のフォルダを作成し、その中に `index.html` というファイルを作成します。この HTML [ボイラープレート](https://en.wikipedia.org/wiki/Boilerplate_code) から始めます。
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Bank App</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- ここで作業することになります。 -->
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## HTML テンプレート
|
||||||
|
|
||||||
|
Web ページに複数の画面を作成したい場合、表示したい画面ごとに1つの HTML ファイルを作成するのが1つの解決策です。しかし、この方法にはいくつかの不都合があります。
|
||||||
|
|
||||||
|
- 画面切り替えの際に HTML 全体を再読み込みしなければならず、時間がかかることがあります
|
||||||
|
- 画面間でデータを共有するのは難しいです
|
||||||
|
|
||||||
|
もう一つのアプローチは、HTML ファイルを一つだけ持ち、`<template>` 要素を使って複数の [HTML テンプレート](https://developer.mozilla.org/ja/docs/Web/HTML/Element/template)を定義することです。テンプレートはブラウザに表示されない再利用可能な HTML ブロックであり、JavaScript を使って実行時にインスタンス化する必要があります。
|
||||||
|
|
||||||
|
### タスク
|
||||||
|
|
||||||
|
ログインページとダッシュボードの 2 つの画面を持つ銀行アプリを作成します。まず、アプリの異なる画面をインスタンス化するために使用するプレースホルダ要素を HTML の body に追加します。
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div id="app">Loading...</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
JavaScript での検索が容易になるように、`id` を付与しています。
|
||||||
|
|
||||||
|
> ヒント: この要素の内容が置き換えられるので、アプリの読み込み中に表示される読み込みメッセージやインジケータを入れることができます。
|
||||||
|
|
||||||
|
次に、ログインページの HTML テンプレートを下に追加します。今のところ、私たちはそこにタイトルとナビゲーションを実行するために使用するリンクを含むセクションを置くだけです。
|
||||||
|
|
||||||
|
```html
|
||||||
|
<template id="login">
|
||||||
|
<h1>Bank App</h1>
|
||||||
|
<section>
|
||||||
|
<a href="/dashboard">Login</a>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
次に、ダッシュボードページ用に別の HTML テンプレートを追加します。このページには異なるセクションが含まれます。
|
||||||
|
|
||||||
|
- タイトルとログアウトリンクのあるヘッダー
|
||||||
|
- 銀行口座の当座預金残高
|
||||||
|
- 表に表示されるトランザクションのリスト
|
||||||
|
|
||||||
|
```html
|
||||||
|
<template id="dashboard">
|
||||||
|
<header>
|
||||||
|
<h1>Bank App</h1>
|
||||||
|
<a href="/login">Logout</a>
|
||||||
|
</header>
|
||||||
|
<section>
|
||||||
|
Balance: 100$
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<h2>Transactions</h2>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Date</th>
|
||||||
|
<th>Object</th>
|
||||||
|
<th>Amount</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody></tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
> ヒント: HTML テンプレートを作成する際に、どのように見えるかを確認したい場合は、`<template>` と `</template>` の行を `<!-- -->` で囲んでコメントアウトすることができます。
|
||||||
|
|
||||||
|
✅ なぜテンプレートに `id` 属性を使うと思いますか? クラスのような他のものを使うことはできないのでしょうか?
|
||||||
|
|
||||||
|
## JavaScript でテンプレートを表示する
|
||||||
|
|
||||||
|
現在の HTML ファイルをブラウザで試してみると、`Loading...` と表示されて動かなくなるのがわかるでしょう。これは、HTML テンプレートをインスタンス化して表示するために JavaScript コードを追加する必要があるためです。
|
||||||
|
|
||||||
|
テンプレートのインスタンス化は通常3つのステップで行われます。
|
||||||
|
|
||||||
|
1. 例えば、[`document.getElementById`](https://developer.mozilla.org/ja/docs/Web/API/Document/getElementById) を使用して、DOM 内のテンプレート要素を取得します
|
||||||
|
2. [`cloneNode`](https://developer.mozilla.org/ja/docs/Web/API/Node/cloneNode) を使用して、テンプレート要素のクローンを作成します
|
||||||
|
3. 例えば [`appendChild`](https://developer.mozilla.org/ja/docs/Web/API/Node/appendChild) を使用して、可視要素の下の DOM にアタッチします
|
||||||
|
|
||||||
|
✅ なぜ DOM にアタッチする前にテンプレートをクローンする必要があるのでしょうか? このステップをスキップしたらどうなると思いますか?
|
||||||
|
|
||||||
|
### タスク
|
||||||
|
|
||||||
|
プロジェクトフォルダに `app.js` という名前の新しいファイルを作成し、そのファイルを HTML の `<head>` セクションにインポートします。
|
||||||
|
|
||||||
|
```html
|
||||||
|
<script src="app.js" defer></script>
|
||||||
|
```
|
||||||
|
|
||||||
|
では、`app.js` で新しい関数 `updateRoute` を作成します。
|
||||||
|
|
||||||
|
```js
|
||||||
|
function updateRoute(templateId) {
|
||||||
|
const template = document.getElementById(templateId);
|
||||||
|
const view = template.content.cloneNode(true);
|
||||||
|
const app = document.getElementById('app');
|
||||||
|
app.innerHTML = '';
|
||||||
|
app.appendChild(view);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
ここで行うことは、上記の3つのステップとまったく同じです。テンプレートを `templateId` という名前でインスタンス化し、そのクローンされたコンテンツをアプリのプレースホルダ内に配置します。テンプレートのサブツリー全体をコピーするには、`cloneNode(true)` を使用する必要があることに注意してください。
|
||||||
|
|
||||||
|
テンプレートのサブツリー全体をコピーするには、`cloneNode(true)` を使用する必要があることに注意してください。
|
||||||
|
|
||||||
|
```js
|
||||||
|
updateRoute('login');
|
||||||
|
```
|
||||||
|
|
||||||
|
✅ このコード `app.innerHTML = '';` の目的は何ですか?これがないとどうなるのでしょうか?
|
||||||
|
|
||||||
|
## ルートの作成
|
||||||
|
|
||||||
|
Web アプリの話をするときに、**URL** を表示すべき特定の画面にマッピングする意図を *ルーティング* と呼んでいます。複数の HTML ファイルを持つ Web サイトでは、ファイルパスが URL に反映されるため、これは自動的に行われます。たとえば、プロジェクトフォルダにこれらのファイルがあるとします。
|
||||||
|
|
||||||
|
```
|
||||||
|
mywebsite/index.html
|
||||||
|
mywebsite/login.html
|
||||||
|
mywebsite/admin/index.html
|
||||||
|
```
|
||||||
|
|
||||||
|
ルートに `mywebsite` を指定して Web サーバを作成した場合、URL のマッピングは以下のようになる。
|
||||||
|
|
||||||
|
```
|
||||||
|
https://site.com --> mywebsite/index.html
|
||||||
|
https://site.com/login.html --> mywebsite/login.html
|
||||||
|
https://site.com/admin/ --> mywebsite/admin/index.html
|
||||||
|
```
|
||||||
|
|
||||||
|
しかし、私たちの Web アプリでは、すべての画面を含む単一の HTML ファイルを使用しているので、このデフォルトの動作は役に立ちません。この map を手動で作成し、JavaScript を使用して表示されるテンプレートの更新を実行する必要があります。
|
||||||
|
|
||||||
|
### タスク
|
||||||
|
|
||||||
|
URL パスとテンプレート間の [map](https://en.wikipedia.org/wiki/Associative_array) を実装するために、シンプルなオブジェクトを使用します。このオブジェクトを `app.js` ファイルの先頭に追加します。
|
||||||
|
|
||||||
|
```js
|
||||||
|
const routes = {
|
||||||
|
'/login': { templateId: 'login' },
|
||||||
|
'/dashboard': { templateId: 'dashboard' },
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
では、`updateRoute` 関数を少し修正してみましょう。引数に `templateId` を直接渡すのではなく、まず現在の URL を見て、map を使って対応するテンプレート ID の値を取得したいと思います。URL からパス部分だけを取得するには、[`window.location.pathname`](https://developer.mozilla.org/en-US/docs/Web/API/Location/pathname)を使うことができます。
|
||||||
|
|
||||||
|
```js
|
||||||
|
function updateRoute() {
|
||||||
|
const path = window.location.pathname;
|
||||||
|
const route = routes[path];
|
||||||
|
|
||||||
|
const template = document.getElementById(route.templateId);
|
||||||
|
const view = template.content.cloneNode(true);
|
||||||
|
const app = document.getElementById('app');
|
||||||
|
app.innerHTML = '';
|
||||||
|
app.appendChild(view);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
ここでは、宣言したルートを対応するテンプレートにマッピングしてみました。ブラウザの URL を手動で変更することで正常に動作するか試してみてください。
|
||||||
|
|
||||||
|
✅ URL に未知のパスを入力するとどうなるでしょうか? どうすれば解決できるのでしょうか?
|
||||||
|
|
||||||
|
## ナビゲーションの追加
|
||||||
|
|
||||||
|
私たちのアプリの次のステップは、URL を手動で変更することなく、ページ間を移動する可能性を追加することです。これは2つのことを意味します。
|
||||||
|
|
||||||
|
1. 現在の URL を更新する
|
||||||
|
2. 新しい URL に基づいて表示されるテンプレートを更新する
|
||||||
|
|
||||||
|
2番目の部分はすでに `updateRoute` 関数で処理したので、現在の URL を更新する方法を見つけなければなりません。
|
||||||
|
|
||||||
|
JavaScript、特に [history.pushState`](https://developer.mozilla.org/ja/docs/Web/API/History/pushState) を使う必要があります。これは HTML をリロードせずに URL を更新して閲覧履歴に新しいエントリを作成することができます。
|
||||||
|
|
||||||
|
> 注: HTML アンカー要素[`<a href>`](https://developer.mozilla.org/ja/docs/Web/HTML/Element/a)は単独で使用して異なる URL へのハイパーリンクを作成することができますが、ブラウザはデフォルトで HTML をリロードさせることになります。カスタム javascript でルーティングを扱う際には、クリックイベントの preventDefault() 関数を使用して、この動作を防ぐ必要があります。
|
||||||
|
|
||||||
|
### タスク
|
||||||
|
|
||||||
|
アプリ内でナビゲートするために使用できる新しい関数を作成してみましょう。
|
||||||
|
|
||||||
|
```js
|
||||||
|
function navigate(path) {
|
||||||
|
window.history.pushState({}, path, window.location.origin + path);
|
||||||
|
updateRoute();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
このメソッドは最初に与えられたパスに基づいて現在の URL を更新し、その後テンプレートを更新します。プロパティ `window.location.origin` は URL のルートを返すので、与えられたパスから完全な URL を再構築することができます。
|
||||||
|
|
||||||
|
これでこの関数ができたので、パスが定義されたルートにマッチしない場合の問題を解決することができます。一致するルートが見つからなかった場合は、既存のルートにフォールバックを追加して `updateRoute` 関数を修正する。
|
||||||
|
|
||||||
|
```js
|
||||||
|
function updateRoute() {
|
||||||
|
const path = window.location.pathname;
|
||||||
|
const route = routes[path];
|
||||||
|
|
||||||
|
if (!route) {
|
||||||
|
return navigate('/login');
|
||||||
|
}
|
||||||
|
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
ルートが見つからない場合は、`login` ページにリダイレクトします。
|
||||||
|
|
||||||
|
リンクがクリックされたときに URL を取得し、ブラウザのデフォルトのリンク動作を防ぐための関数を作ってみましょう。
|
||||||
|
|
||||||
|
```js
|
||||||
|
function onLinkClick(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
navigate(event.target.href);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
HTML の *Login* と *Logout* リンクにバインディングを追加してナビゲーションシステムを完成させましょう。
|
||||||
|
|
||||||
|
```html
|
||||||
|
<a href="/dashboard" onclick="onLinkClick()">Login</a>
|
||||||
|
...
|
||||||
|
<a href="/login" onclick="onLinkClick()">Logout</a>
|
||||||
|
```
|
||||||
|
|
||||||
|
[`onclick`](https://developer.mozilla.org/ja/docs/Web/API/GlobalEventHandlers/onclick) 属性を使用して、`click` イベントを JavaScript コードにバインドし、ここでは `navigate()` 関数の呼び出しを行います。
|
||||||
|
|
||||||
|
これらのリンクをクリックしてみると、アプリの異なる画面間を移動できるようになるはずです。
|
||||||
|
|
||||||
|
✅ `history.pushState` メソッドは HTML5 標準の一部であり、[すべての最新ブラウザ](https://caniuse.com/?search=pushState)で実装されています。古いブラウザ用の Web アプリを構築している場合、この API の代わりに使用できるトリックがあります。パスの前に[ハッシュ (`#`)](https://en.wikipedia.org/wiki/URI_fragment) を使用すると、通常のアンカーナビゲーションで動作し、ページを再読み込みしないルーティングを実装することができます。その目的は、ページ内に内部リンクを作成することです。
|
||||||
|
|
||||||
|
## ブラウザの戻るボタンと進むボタンの扱い
|
||||||
|
|
||||||
|
`history.pushState` を使うと、ブラウザのナビゲーション履歴に新しいエントリが作成されます。ブラウザの *戻るボタン* を押すと、以下のように表示されることを確認することができます。
|
||||||
|
|
||||||
|
![ナビゲーション履歴のスクリーンショット](../history.png)
|
||||||
|
|
||||||
|
何度か戻るボタンをクリックしてみると、現在の URL が変わって履歴が更新されていますが、同じテンプレートが表示され続けています。
|
||||||
|
|
||||||
|
これは、履歴が変わるたびに `updateRoute()` を呼び出す必要があることを知らないからです。[`history.pushState` のドキュメント](https://developer.mozilla.org/ja/docs/Web/API/History/pushState)を見てみると、状態が変化した場合、つまり別の URL に移動した場合には、[`popstate`](https://developer.mozilla.org/en-US/docs/Web/API/Window/popstate_event)イベントが発生することがわかります。これを使ってこの問題を解決しましょう。
|
||||||
|
|
||||||
|
### タスク
|
||||||
|
|
||||||
|
ブラウザの履歴が変更されたときに表示されるテンプレートが更新されるようにするために、`updateRoute()` を呼び出す新しい関数をアタッチします。これは `app.js` ファイルの下部で行います。
|
||||||
|
|
||||||
|
```js
|
||||||
|
window.onpopstate = () => updateRoute();
|
||||||
|
updateRoute();
|
||||||
|
```
|
||||||
|
|
||||||
|
> 注: ここでは簡潔さのために `popstate` イベントハンドラを宣言するために [アロー関数](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Functions/Arrow_functions) を使用していますが、通常の関数でも同じように動作します。
|
||||||
|
|
||||||
|
これは、アロー関数についてのリフレッシュビデオです。
|
||||||
|
|
||||||
|
[![Arrow Functions](https://img.youtube.com/vi/OP6eEbOj2sc/0.jpg)](https://youtube.com/watch?v=OP6eEbOj2sc "Arrow Functions")
|
||||||
|
|
||||||
|
今度はブラウザの戻るボタンと進むボタンを使ってみて、今回表示されたルートが正しく更新されているかどうかを確認してみてください。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 チャレンジ
|
||||||
|
|
||||||
|
このアプリのクレジットを表示する3ページ目のテンプレートとルートを追加します。
|
||||||
|
|
||||||
|
## レッスン後の小テスト
|
||||||
|
|
||||||
|
[レッスン後の小テスト](https://nice-beach-0fe9e9d0f.azurestaticapps.net/quiz/42)
|
||||||
|
|
||||||
|
## 復習と自己学習
|
||||||
|
|
||||||
|
ルーティングは Web 開発の驚くほどトリッキーな部分の1つで、特に Web がページ更新の動作からシングルページアプリケーションのページ更新へと移行するにつれ、そのような部分が増えてきています。[Azure Static Web Apps プレビューでのルート](https://docs.microsoft.com/ja-jp/azure/static-web-apps/routes)がルーティングを扱うことについて少し読んでみてください。そのドキュメントに記載されているいくつかの決定が必要な理由を説明できますか?
|
||||||
|
|
||||||
|
## 課題
|
||||||
|
|
||||||
|
[ルーティングの改善](assignment.ja.md)
|
@ -0,0 +1,14 @@
|
|||||||
|
# ルーティングの改善
|
||||||
|
|
||||||
|
## 説明書
|
||||||
|
|
||||||
|
The routes declaration contains currently only the template ID to use. But when displaying a new page, a bit more is needed sometimes. Let's improve our routing implementation with two additional features:
|
||||||
|
|
||||||
|
- Give titles to each template and update the window title with this new title when the template changes.
|
||||||
|
- Add an option to run some code after the template changes. We want to print `'Dashboard is shown'` in the developer console every time the dashboard page is displayed.
|
||||||
|
|
||||||
|
## ルーブリック
|
||||||
|
|
||||||
|
| Criteria | Exemplary | Adequate | Needs Improvement |
|
||||||
|
| -------- | ---------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- |
|
||||||
|
| | The two features are implemented and working. Title and code addition also work for a new route added in the `routes` declaration. | The two features work, but the behavior is hardcoded and not configurable via the `routes` declaration. Adding a third route with title and code addition does not work or works partially. | One of the features is missing or not working properly. |
|
@ -0,0 +1,292 @@
|
|||||||
|
# バンキングアプリを作ろう その 2: ログインと登録フォームの構築
|
||||||
|
|
||||||
|
## レッスン前の小テスト
|
||||||
|
|
||||||
|
[レッスン前の小テスト](https://nice-beach-0fe9e9d0f.azurestaticapps.net/quiz/43)
|
||||||
|
|
||||||
|
### イントロダクション
|
||||||
|
|
||||||
|
最近のほとんどの Web アプリでは、アカウントを作成して自分だけのプライベート空間を持つことができます。複数のユーザーが同時に Web アプリにアクセスすることができるため、各ユーザーの個人情報を個別に保存し、どの情報を表示するかを選択する仕組みが必要になります。ここでは、[ユーザー ID を安全に管理する方法](https://en.wikipedia.org/wiki/Authentication)については、それ自体が広範なトピックなので取り上げませんが、各ユーザーがアプリ上で1つ(または複数)の銀行口座を作成できるようにしておきます。
|
||||||
|
|
||||||
|
このパートでは、HTML フォームを使用して、Web アプリにログインと登録を追加します。プログラムでサーバー API にデータを送信する方法、最終的にユーザー入力の基本的な検証ルールを定義する方法を見ていきます。
|
||||||
|
|
||||||
|
### 前提条件
|
||||||
|
|
||||||
|
このレッスンでは、Web アプリの [HTML テンプレートとルーティング](../../1-template-route/README.md)が完了している必要があります。また、アカウントを作成するためのデータを送信できるように、ローカルに [Node.js](https://nodejs.org/ja) と[サーバー API を実行する](../../api/translations/README.ja.md)をインストールする必要があります。
|
||||||
|
|
||||||
|
ターミナルでこのコマンドを実行することで、サーバーが正常に動作していることを確認することができます。
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl http://localhost:5000/api
|
||||||
|
# -> 結果として "Bank API v1.0.0" を返す必要があります。
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## フォームとコントロール
|
||||||
|
|
||||||
|
`<form>` 要素は HTML ドキュメントのセクションをカプセル化し、ユーザがインタラクティブなコントロールを使ってデータを入力したり送信したりすることができます。フォームの中で利用できるユーザーインターフェイス (UI) コントロールには様々な種類がありますが、最も一般的なものは `<input>` と `<button>` 要素です。
|
||||||
|
|
||||||
|
例えば、ユーザがユーザ名を入力できるフィールドを作成するには、`<input>` 要素を使用することができます。
|
||||||
|
|
||||||
|
```html
|
||||||
|
<input id="username" name="username" type="text">
|
||||||
|
```
|
||||||
|
|
||||||
|
`name` 属性はフォームデータを送信する際のプロパティ名として使われます。`id` 属性は `<label>` をフォームコントロールに関連付けるために使われます。
|
||||||
|
|
||||||
|
> UI を構築する際に使用できるすべてのネイティブ UI 要素のアイデアを得るために、[`<input>` タイプ](https://developer.mozilla.org/ja/docs/Web/HTML/Element/input) と [その他のフォームコントロール](https://developer.mozilla.org/ja/docs/Learn/Forms/Other_form_controls) のリスト全体を見てみましょう。
|
||||||
|
|
||||||
|
✅ `<input>` は [空の要素](https://developer.mozilla.org/ja/docs/Glossary/Empty_element) であることに注意してください。それは一致するクロージングタグを追加すべき*ではありません*。タグ自身で閉じる `<input/>` 記法を使うことはできますが、必須ではありません。
|
||||||
|
|
||||||
|
フォーム内の `<button>` 要素は少し特殊です。`type` 属性を指定しないと、ボタンが押されたときに自動的にフォームデータをサーバに送信します。以下に可能な `type` の値を示します。
|
||||||
|
|
||||||
|
- `submit`: `<form>`内のデフォルトの値で、ボタンはフォームの送信アクションをトリガーします
|
||||||
|
- `reset`: ボタンはすべてのフォームコントロールを初期値にリセットします
|
||||||
|
- `button`: ボタンが押されたときのデフォルトの動作を割り当てないでください。その後、JavaScript を使ってカスタムアクションを割り当てることができます
|
||||||
|
|
||||||
|
### タスク
|
||||||
|
|
||||||
|
まずは `login` テンプレートにフォームを追加してみましょう。*ユーザ名*フィールドと*ログイン*ボタンが必要です。
|
||||||
|
|
||||||
|
```html
|
||||||
|
<template id="login">
|
||||||
|
<h1>Bank App</h1>
|
||||||
|
<section>
|
||||||
|
<h2>Login</h2>
|
||||||
|
<form id="loginForm">
|
||||||
|
<label for="username">Username</label>
|
||||||
|
<input id="username" name="user" type="text">
|
||||||
|
<button>Login</button>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
よく見ると、ここに `<label>` 要素が追加されていることがわかります。`<label>` 要素はユーザー名フィールドなどの UI コントロールに名前を追加するために使われます。ラベルはフォームを読みやすくするために重要ですが、それだけではありません。
|
||||||
|
|
||||||
|
- ラベルをフォームコントロールに関連付けることで、(スクリーンリーダーのような) 支援技術を使用しているユーザーが、どのようなデータを提供することが求められているのかを理解するのに役立ちます
|
||||||
|
- ラベルをクリックすると、関連する入力に直接フォーカスを当てることができるので、タッチスクリーンベースのデバイスでも手が届きやすくなります
|
||||||
|
|
||||||
|
> ウェブ上の [アクセシビリティ](https://developer.mozilla.org/ja/docs/Learn/Accessibility/What_is_accessibility) は、見落とされがちな非常に重要なトピックです。[セマンティックな HTML 要素](https://developer.mozilla.org/ja/docs/Learn/Accessibility/HTML) のおかげで、適切に使用すれば、アクセシブルなコンテンツを作成することは難しくありません。[アクセシビリティについての詳細を読む](https://developer.mozilla.org/ja/docs/Web/Accessibility)ことで、よくある間違いを回避し、責任ある開発者になることができます。
|
||||||
|
|
||||||
|
あとは、前のもののすぐ下に登録用の第二形態を追加します。
|
||||||
|
|
||||||
|
```html
|
||||||
|
<hr/>
|
||||||
|
<h2>Register</h2>
|
||||||
|
<form id="registerForm">
|
||||||
|
<label for="user">Username</label>
|
||||||
|
<input id="user" name="user" type="text">
|
||||||
|
<label for="currency">Currency</label>
|
||||||
|
<input id="currency" name="currency" type="text" value="$">
|
||||||
|
<label for="description">Description</label>
|
||||||
|
<input id="description" name="description" type="text">
|
||||||
|
<label for="balance">Current balance</label>
|
||||||
|
<input id="balance" name="balance" type="number" value="0">
|
||||||
|
<button>Register</button>
|
||||||
|
</form>
|
||||||
|
```
|
||||||
|
|
||||||
|
`value` 属性を用いて、与えられた入力に対してデフォルト値を定義することができます。
|
||||||
|
また、`balance` の入力が `number` 型であることにも注目してください。他の入力とは違うように見えますか? これを使ってみてください。
|
||||||
|
|
||||||
|
✅ キーボードだけでフォームをナビゲートして対話することができますか? あなたならどうしますか?
|
||||||
|
|
||||||
|
## サーバーへのデータ送信
|
||||||
|
|
||||||
|
機能的な UI ができたので、次のステップはデータをサーバーに送信することです。現在のコードを使って簡単なテストをしてみましょう。*ログイン*または*登録*ボタンをクリックするとどうなりますか?
|
||||||
|
|
||||||
|
ブラウザの URL セクションの変化に気付きましたか?
|
||||||
|
|
||||||
|
![登録ボタンをクリックした後のブラウザのURL変更のスクリーンショット](../images/click-register.png)
|
||||||
|
|
||||||
|
`<form>` のデフォルトのアクションは、フォームを現在のサーバの URL に [GET メソッド](https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.3) を使って送信し、フォームのデータを URL に直接追加することです。この方法にはいくつかの欠点があります。
|
||||||
|
|
||||||
|
- 送信されるデータのサイズが非常に限られています (2000 文字程度)
|
||||||
|
- データは URL で直接見ることができます (パスワードには向いていません)
|
||||||
|
- ファイルのアップロードでは動作しません
|
||||||
|
|
||||||
|
そのため、これまでの制限を受けずに、HTTP リクエストの本文でフォームデータをサーバに送信する [POST メソッド](https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.5)を利用するように変更することができます。
|
||||||
|
|
||||||
|
> POST はデータを送信するために最も一般的に使用される方法ですが、[いくつかの特定のシナリオ](https://www.w3.org/2001/tag/doc/whenToUseGet.html)では、例えば検索フィールドを実装する場合には、GET メソッドを使用することが望ましいです。
|
||||||
|
|
||||||
|
### タスク
|
||||||
|
|
||||||
|
登録フォームに `action` と `method` プロパティを追加します。
|
||||||
|
|
||||||
|
```html
|
||||||
|
<form id="registerForm" action="//localhost:5000/api/accounts" method="POST">
|
||||||
|
```
|
||||||
|
|
||||||
|
ここで、あなたの名前で新しいアカウントを登録してみてください。*登録*ボタンをクリックすると、このような画面が表示されるはずです。
|
||||||
|
|
||||||
|
![アドレス localhost:5000/api/accounts のブラウザウィンドウで、ユーザーデータを含む JSON 文字列を表示しています。](./images/form-post.png)
|
||||||
|
|
||||||
|
すべてがうまくいけば、サーバーはあなたのリクエストに、作成されたアカウントデータを含む [JSON](https://www.json.org/json-ja.html) レスポンスを返すはずです。
|
||||||
|
|
||||||
|
✅ 同じ名前で再度登録してみてください。どうなるでしょうか?
|
||||||
|
|
||||||
|
## ページを再読み込みせずにデータを送信する
|
||||||
|
|
||||||
|
お気づきのように、先ほど使用したアプローチに若干の問題があります。フォームを送信するときです。これが発生するとアプリを終了し、ブラウザはサーバーの URL にリダイレクトします。[シングルページアプリケーション(SPA)](https://en.wikipedia.org/wiki/Single-page_application) を作成しているので、ウェブアプリですべてのページのリロードを回避しようとしています。
|
||||||
|
|
||||||
|
ページのリロードを強制せずにフォームデータをサーバに送信するには、JavaScript のコードを使用しなければなりません。`<form>` 要素の `action` プロパティに URL を記述する代わりに、カスタムアクションを実行するために `javascript:` 文字列の前に任意の JavaScript コードを使用することができます。これを使うと、これまでブラウザが自動的に行っていたタスクを実装しなければならないことになります。
|
||||||
|
|
||||||
|
- フォームデータを取得する
|
||||||
|
- フォームデータを適切なフォーマットに変換してエンコードする
|
||||||
|
- HTTPリクエストを作成してサーバーに送信する
|
||||||
|
|
||||||
|
### タスク
|
||||||
|
|
||||||
|
登録フォームの `action` は、次のように置き換えてください。
|
||||||
|
|
||||||
|
```html
|
||||||
|
<form id="registerForm" action="javascript:register()">
|
||||||
|
```
|
||||||
|
|
||||||
|
`app.js`を開いて `register` という名前の関数を追加します。
|
||||||
|
|
||||||
|
```js
|
||||||
|
function register() {
|
||||||
|
const registerForm = document.getElementById('registerForm');
|
||||||
|
const formData = new FormData(registerForm);
|
||||||
|
const data = Object.fromEntries(formData);
|
||||||
|
const jsonData = JSON.stringify(data);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
ここでは、`getElementById()`を使ってフォーム要素を取得し、[`FormData`](https://developer.mozilla.org/ja/docs/Web/API/FormData) ヘルパーを使ってフォームコントロールからキーと値のペアのセットとして値を抽出します。次に、[`Object.fromEntries()`](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Object/fromEntries) を使用してデータを通常のオブジェクトに変換し、最後に Web 上でデータを交換するために一般的に使用されるフォーマットである [JSON](https://www.json.org/json-ja.html) にデータをシリアライズします。
|
||||||
|
|
||||||
|
これで、データをサーバに送信する準備ができました。`createAccount` という名前の関数を新規に作成しましょう。
|
||||||
|
|
||||||
|
```js
|
||||||
|
async function createAccount(account) {
|
||||||
|
try {
|
||||||
|
const response = await fetch('//localhost:5000/api/accounts', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: account
|
||||||
|
});
|
||||||
|
return await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
return { error: error.message || 'Unknown error' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
この関数は何をしているのでしょうか?まず、ここでの `async` キーワードに注目してください。これは、この関数が [**非同期的に**](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/async_function) を実行するコードを含んでいることを意味します。await` キーワードと一緒に使用すると、非同期コードが実行されるのを待つことができます。
|
||||||
|
|
||||||
|
以下に、`async/await` の使用法についての簡単なビデオを示します。
|
||||||
|
|
||||||
|
[![Async and Await for managing promises](https://img.youtube.com/vi/YwmlRkrxvkk/0.jpg)](https://youtube.com/watch?v=YwmlRkrxvkk "Async and Await for managing promises")
|
||||||
|
|
||||||
|
JSON データをサーバに送信するには、`fetch()` API を使用します。このメソッドは2つのパラメータを受け取ります。
|
||||||
|
|
||||||
|
- サーバの URL なので、ここに `/localhost:5000/api/accounts` を返します
|
||||||
|
- リクエストの設定。ここでメソッドを `POST` に設定し、リクエストのための `body` を提供します。JSON データをサーバに送るので、`Content-Type` ヘッダを `application/json` に設定する必要があります
|
||||||
|
|
||||||
|
サーバはリクエストに対して JSON で応答するので、`await response.json()` を使って JSON の内容を解析し、結果のオブジェクトを返すことができます。このメソッドは非同期なので、返す前に `await` キーワードを使って、解析中のエラーが発生した場合にはそれもキャッチするようにしています。
|
||||||
|
|
||||||
|
次に、`register` 関数にコードを追加して `createAccount()` を呼び出すようにします。
|
||||||
|
|
||||||
|
```js
|
||||||
|
const result = await createAccount(jsonData);
|
||||||
|
```
|
||||||
|
|
||||||
|
ここでは `await` キーワードを使用しているので、レジスタ関数の前に `async` キーワードを追加する必要があります。
|
||||||
|
|
||||||
|
```js
|
||||||
|
async function register() {
|
||||||
|
```
|
||||||
|
|
||||||
|
最後に、結果を確認するためのログを追加してみましょう。最終的な関数は以下のようになるはずです。
|
||||||
|
|
||||||
|
```js
|
||||||
|
async function register() {
|
||||||
|
const registerForm = document.getElementById('registerForm');
|
||||||
|
const formData = new FormData(registerForm);
|
||||||
|
const jsonData = JSON.stringify(Object.fromEntries(formData));
|
||||||
|
const result = await createAccount(jsonData);
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
return console.log('An error occured:', result.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Account created!', result);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
ちょっと長かったですが、無事にたどり着きました! [ブラウザの開発者ツール](https://developer.mozilla.org/ja/docs/Learn/Common_questions/What_are_browser_developer_tools)を開いて、新しいアカウントを登録してみると、Web ページには何も変化がないはずなのですが、コンソールにはすべてが正常に動作することを確認するメッセージが表示されます。
|
||||||
|
|
||||||
|
![ブラウザコンソールにログメッセージを表示するスクリーンショット](../images/browser-console.png)
|
||||||
|
|
||||||
|
✅ データは安全にサーバーに送られていると思いますか? もし何者かにリクエストを傍受されたらどうしますか? 安全なデータ通信については、[HTTPS](https://ja.wikipedia.org/wiki/HTTPS) を読むとより詳しく知ることができます。
|
||||||
|
|
||||||
|
## データの検証
|
||||||
|
|
||||||
|
最初にユーザー名を設定せずに新規アカウントを登録しようとすると、サーバーがステータスコード [400 (Bad Request)](https://developer.mozilla.org/ja/docs/Web/HTTP/Status/400) でエラーを返していることがわかります。
|
||||||
|
|
||||||
|
サーバーにデータを送信する前に、可能な限り事前に [フォームデータの検証](https://developer.mozilla.org/ja/docs/Learn/Forms/Form_validation) を行い、有効なリクエストを送信していることを確認するのが良い方法です。HTML5 フォームコントロールは、様々な属性を使った組み込みのバリデーションを提供しています。
|
||||||
|
|
||||||
|
- `required`: フィールドには入力する必要があります。そうでない場合は、フォームを送信することができません
|
||||||
|
- `minlength` と `maxlength`: テキストフィールドの最小文字数と最大文字数を定義します
|
||||||
|
- `min` と `max`: 数値フィールドの最小値と最大値を定義します
|
||||||
|
- `type`: `number`, `email`, `file` や [その他の組み込み型](https://developer.mozilla.org/ja/docs/Web/HTML/Element/input) のような、期待されるデータの種類を定義します。この属性はフォームコントロールの視覚的なレンダリングを変更することもできます
|
||||||
|
- `pattern`: これを使用すると、入力されたデータが有効かどうかをテストするための [正規表現](https://developer.mozilla.org/ja/docs/Web/JavaScript/Guide/Regular_Expressions) パターンを定義することができます
|
||||||
|
|
||||||
|
> ヒント: CSS 疑似クラス `:valid` と `:invalid` を利用して、フォームコントロールの見た目を有効か無効かによってカスタマイズすることができます。
|
||||||
|
|
||||||
|
### タスク
|
||||||
|
|
||||||
|
有効な新規アカウントを作成するためには、ユーザー名と通貨の2つの必須フィールドがあり、その他のフィールドは任意です。フォームの HTML を更新し、`required` 属性とフィールドのラベルのテキストの両方を使用してください。
|
||||||
|
|
||||||
|
```html
|
||||||
|
<label for="user">Username (required)</label>
|
||||||
|
<input id="user" name="user" type="text" required>
|
||||||
|
...
|
||||||
|
<label for="currency">Currency (required)</label>
|
||||||
|
<input id="currency" name="currency" type="text" value="$" required>
|
||||||
|
```
|
||||||
|
|
||||||
|
このサーバの実装ではフィールドの最大長に特定の制限はありませんが、ユーザのテキスト入力に対して合理的な制限を定義することは常に良い習慣です。
|
||||||
|
|
||||||
|
テキストフィールドに `maxlength` 属性を追加します。
|
||||||
|
|
||||||
|
```html
|
||||||
|
<input id="user" name="user" type="text" maxlength="20" required>
|
||||||
|
...
|
||||||
|
<input id="currency" name="currency" type="text" value="$" maxlength="5" required>
|
||||||
|
...
|
||||||
|
<input id="description" name="description" type="text" maxlength="100">
|
||||||
|
```
|
||||||
|
|
||||||
|
これで、*Register* ボタンを押したときに、フィールドが定義したバリデーションルールに準拠していない場合は、次のように表示されるはずです。
|
||||||
|
|
||||||
|
![フォームを送信しようとしたときの検証エラーを示すスクリーンショット](../images/validation-error.png)
|
||||||
|
|
||||||
|
このように、サーバにデータを送信する前に実行されるバリデーションのことを **クライアントサイド** のバリデーションと呼びます。しかし、データを送信せずにすべてのチェックを実行できるとは限らないことに注意してください。例えば、サーバーにリクエストを送らずに、同じユーザー名のアカウントが既に存在するかどうかを確認することはできません。サーバー上で実行される追加のバリデーションは、**サーバーサイド**のバリデーションと呼ばれます。
|
||||||
|
|
||||||
|
通常は両方を実装する必要があり、クライアントサイドのバリデーションを使用すると、ユーザーへのフィードバックを即座に提供することでユーザーエクスペリエンスが向上しますが、サーバーサイドのバリデーションは、操作するユーザーデータが健全で安全であることを確認するために非常に重要です。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 チャレンジ
|
||||||
|
|
||||||
|
ユーザーが既に存在する場合には、エラーメッセージを HTML に表示します。
|
||||||
|
|
||||||
|
ここでは、少しのスタイリングの後に最終的なログインページがどのように見えるかの例を示します。
|
||||||
|
|
||||||
|
![CSSスタイルを追加した後のログインページのスクリーンショット](../images/result.png)
|
||||||
|
|
||||||
|
## レッスン後の小テスト
|
||||||
|
|
||||||
|
[レッスン後の小テスト](https://nice-beach-0fe9e9d0f.azurestaticapps.net/quiz/44)
|
||||||
|
|
||||||
|
## 復習と自己学習
|
||||||
|
|
||||||
|
開発者は、フォーム構築の取り組み、特に検証戦略に関して、非常にクリエイティブになっています。[CodePen](https://codepen.com) を見て、さまざまなフォームの流れについて学びましょう。
|
||||||
|
|
||||||
|
## 課題
|
||||||
|
|
||||||
|
[銀行アプリのスタイル設定](assignment.ja.md)
|
@ -0,0 +1,13 @@
|
|||||||
|
# 銀行アプリのスタイル設定
|
||||||
|
|
||||||
|
## 説明書
|
||||||
|
|
||||||
|
新しい `styles.css` ファイルを作成し、現在の `index.html` ファイルにリンクを追加します。作成した CSS ファイルに、*Login* と *Dashboard* ページがすっきりと整然と見えるように、いくつかのスタイルを追加します。アプリに独自のブランディングを与えるために、カラーテーマを作成してみてください。
|
||||||
|
|
||||||
|
> ヒント: 必要に応じて HTML を修正し、新しい要素やクラスを追加することができます。
|
||||||
|
|
||||||
|
## ルーブリック
|
||||||
|
|
||||||
|
| 基準 | 模範的な例 | 適切な | 改善が必要 |
|
||||||
|
| -------- | ----------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------- |
|
||||||
|
| | すべてのページは、一貫した色のテーマと異なるセクションが適切に立っていると、きれいで読みやすいように見えます。 | ページはスタイリングされていますが、テーマがなかったり、セクションが明確に区切られていなかったりします。 | ページにスタイルがなく、セクションが乱雑に見え、情報が読みにくい。 |
|
@ -0,0 +1,332 @@
|
|||||||
|
# バンキングアプリを作ろう その 3: データの取得と利用方法
|
||||||
|
|
||||||
|
## レッスン前の小テスト
|
||||||
|
|
||||||
|
[レッスン前の小テスト](https://nice-beach-0fe9e9d0f.azurestaticapps.net/quiz/45)
|
||||||
|
|
||||||
|
### イントロダクション
|
||||||
|
|
||||||
|
すべての Web アプリケーションの中核には、*データ*があります。データには様々な形がありますが、その主な目的は常にユーザーに情報を表示することです。Web アプリケーションがますますインタラクティブで複雑になってきているため、ユーザーがどのように情報にアクセスして対話するかは、現在では Web 開発の重要な部分となっています。
|
||||||
|
|
||||||
|
このレッスンでは、サーバーから非同期にデータを取得し、このデータを使用して HTML をリロードせずに Web ページに情報を表示する方法を見ていきます。
|
||||||
|
|
||||||
|
### 前提条件
|
||||||
|
|
||||||
|
このレッスンでは、Web アプリの[ログインと登録フォーム](../../2-forms/translations/README.ja.md)の部分をビルドしておく必要があります。また、アカウントデータを取得するためには、ローカルに [Node.js](https://nodejs.org) と [サーバー API を実行する](../../api/translations/README.ja.md)をインストールする必要があります。
|
||||||
|
|
||||||
|
ターミナルで以下のコマンドを実行することで、サーバーが正常に動作していることを確認できます。
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl http://localhost:5000/api
|
||||||
|
# -> 結果として "Bank API v1.0.0" を返す必要があります。
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## AJAX とデータ取得
|
||||||
|
|
||||||
|
従来の Web サイトでは、ユーザーがリンクをクリックしたり、フォームを使用してデータを送信したりした際に、HTML ページ全体をリロードすることで表示されるコンテンツを更新しています。新しいデータの読み込みが必要になるたびに、Web サーバはブラウザで処理する必要のある新しい HTML ページを返し、現在のユーザのアクションを中断し、リロード中のインタラクションを制限します。このワークフローは、*マルチページアプリケーション* または *MPA* とも呼ばれます。
|
||||||
|
|
||||||
|
![複数ページのアプリケーションでワークフローを更新する](../images/mpa.png)
|
||||||
|
|
||||||
|
Web アプリケーションがより複雑でインタラクティブになり始めた頃、[AJAX (Asynchronous JavaScript and XML)](https://en.wikipedia.org/wiki/Ajax_(programming))と呼ばれる新しい技術が登場しました。この技術は、Web アプリケーションが HTML ページをリロードすることなく、JavaScript を使ってサーバーから非同期にデータを送受信することを可能にし、結果として、より高速な更新とよりスムーズなユーザーのインタラクションを実現します。サーバーから新しいデータを受信すると、[DOM](https://developer.mozilla.org/ja/docs/Web/API/Document_Object_Model) API を使用して現在の HTML ページを JavaScript で更新することもできます。時間の経過とともに、このアプローチは現在では [*シングルページアプリケーション* または *SPA*](https://en.wikipedia.org/wiki/Single-page_application) と呼ばれているものへと発展してきました。
|
||||||
|
|
||||||
|
![シングルページアプリケーションでワークフローを更新](../images/spa.png)
|
||||||
|
|
||||||
|
AJAX が最初に導入されたとき、データを非同期で取得できる唯一の API は [`XMLHttpRequest`](https://developer.mozilla.org/ja/docs/Web/API/XMLHttpRequest/Using_XMLHttpRequest) でした。しかし、現在のブラウザはより便利で強力な [`Fetch` API](https://developer.mozilla.org/ja/docs/Web/API/Fetch_API) を実装しており、Promise を使用し、JSON データを操作するのに適しています。
|
||||||
|
|
||||||
|
> 最新のブラウザはすべて `Fetch API` をサポートしていますが、もしあなたの Web アプリケーションをレガシーブラウザや古いブラウザで動作させたいのであれば、最初に [caniuse.com の互換性テーブル](https://caniuse.com/fetch) をチェックするのが良いでしょう。
|
||||||
|
|
||||||
|
### タスク
|
||||||
|
|
||||||
|
[前回のレッスン](../../2-forms/translations/README.ja.md)では、アカウントを作成するための登録フォームを暗示しました。ここでは、既存のアカウントを使ってログインし、そのデータを取得するコードを追加します。`app.js` ファイルを開き、新しい `login` 関数を追加します。
|
||||||
|
|
||||||
|
```js
|
||||||
|
async function login() {
|
||||||
|
const loginForm = document.getElementById('loginForm')
|
||||||
|
const user = loginForm.user.value;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
ここではまずフォーム要素を `getElementById()` で取得し、入力からユーザ名を `loginForm.user.value` で取得します。すべてのフォームコントロールは、フォームのプロパティとしてその名前 (HTML では `name` 属性を使って設定されています) によってアクセスすることができます。
|
||||||
|
|
||||||
|
登録の際に行ったことと同様の方法で、フォームコントロールにアクセスすることができます。サーバーリクエストを実行するために別の関数を作成しますが、今回はアカウントデータを取得します。
|
||||||
|
|
||||||
|
```js
|
||||||
|
async function getAccount(user) {
|
||||||
|
try {
|
||||||
|
const response = await fetch('//localhost:5000/api/accounts/' + encodeURIComponent(user));
|
||||||
|
return await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
return { error: error.message || 'Unknown error' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
ここでは `fetch` API を使ってサーバから非同期にデータを要求しますが、今回はデータを問い合わせるだけなので、呼び出す URL 以外の余分なパラメータは必要ありません。デフォルトでは、`fetch` は [`GET`](https://developer.mozilla.org/ja/docs/Web/HTTP/Methods/GET) HTTP リクエストを作成します。
|
||||||
|
|
||||||
|
✅ `encodeURIComponent()` は URL の特殊文字をエスケープする関数です。この関数を呼び出さずに URL の `user` の値を直接使うと、どのような問題が発生する可能性があるのでしょうか?
|
||||||
|
|
||||||
|
それでは、`login` 関数を `getAccount` を使うように更新してみましょう。
|
||||||
|
|
||||||
|
```js
|
||||||
|
async function login() {
|
||||||
|
const loginForm = document.getElementById('loginForm')
|
||||||
|
const user = loginForm.user.value;
|
||||||
|
const data = await getAccount(user);
|
||||||
|
|
||||||
|
if (data.error) {
|
||||||
|
return console.log('loginError', data.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
account = data;
|
||||||
|
navigate('/dashboard');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
まず、`getAccount` は非同期関数なので、これを `await` キーワードにマッチさせてサーバの結果を待つ必要があります。他のサーバからのリクエストと同様に、エラーが発生した場合にも対処しなければなりません。今のところ、エラーを表示するためのログメッセージを追加して、それをレイヤーに戻すだけです。
|
||||||
|
|
||||||
|
その後、データをどこかに保存しておく必要があるので、後でダッシュボードの情報を表示するためにデータを使用することができます。変数 `account` はまだ存在しないので、ファイルの先頭にグローバル変数を作成します。
|
||||||
|
|
||||||
|
```js
|
||||||
|
let account = null;
|
||||||
|
```
|
||||||
|
|
||||||
|
ユーザデータが変数に保存された後、既に持っている `navigate()` 関数を使って *ログイン* ページから *ダッシュボード* に移動することができます。
|
||||||
|
|
||||||
|
最後に、HTML を修正してログインフォームが送信されたときに `login` 関数を呼び出す必要があります。
|
||||||
|
|
||||||
|
```html
|
||||||
|
<form id="loginForm" action="javascript:login()">
|
||||||
|
```
|
||||||
|
|
||||||
|
すべてが正しく動作していることをテストします。新しいアカウントを登録して、同じアカウントを使ってログインしようとすることで、`register` 関数を完成させることができます。
|
||||||
|
|
||||||
|
次の部分に移る前に、関数の下部にこれを追加することで `register` 関数を完成させることもできます。
|
||||||
|
|
||||||
|
```js
|
||||||
|
account = result;
|
||||||
|
navigate('/dashboard');
|
||||||
|
```
|
||||||
|
|
||||||
|
✅ デフォルトでは、閲覧している Web ページよりも*同じドメインとポート*からしかサーバー API を呼び出すことができないことをご存知でしょうか? これはブラウザによって強制されたセキュリティメカニズムです。しかし待ってください、私たちの Web アプリは `localhost:3000` で動作していますが、サーバー API は `localhost:5000` で動作しています。[Cross-Origin Resource Sharing (CORS)](https://developer.mozilla.org/ja/docs/Web/HTTP/CORS)と呼ばれる技術を使用することで、サーバーがレスポンスに特殊なヘッダを追加し、特定のドメインの例外を許可することで、クロスオリジンの HTTP リクエストを実行することが可能になります。
|
||||||
|
|
||||||
|
## データを表示するために HTML を更新する
|
||||||
|
|
||||||
|
ユーザーデータが手に入ったので、それを表示するために既存の HTML を更新しなければなりません。DOM から要素を取得する方法は既に知っていますが、例として `document.getElementById()` を使用します。ベースとなる要素ができたら、それを修正したり、子要素を追加したりするために使えるAPIをいくつか紹介します。
|
||||||
|
|
||||||
|
- 要素のテキストを変更するには、[`textContent`](https://developer.mozilla.org/ja/docs/Web/API/Node/textContent)プロパティを使用します。この値を変更すると、(あれば) すべての要素の子要素が削除され、提供されたテキストに置き換えられることに注意してください。このように、与えられた要素に空の文字列 `''` を代入することで、与えられた要素のすべての子を削除する効率的な方法でもあります。
|
||||||
|
|
||||||
|
- [`document.createElement()`](https://developer.mozilla.org/ja/docs/Web/API/Document/createElement) と [`append()`](https://developer.mozilla.org/ja/docs/Web/API/ParentNode/append) メソッドを使用すると、1つ以上の新しい子要素を作成して添付することができます
|
||||||
|
|
||||||
|
✅ 要素の [`innerHTML`](https://developer.mozilla.org/ja/docs/Web/API/Element/innerHTML) プロパティを使用して HTML の内容を変更することも可能ですが、これは [クロスサイトスクリプティング (XSS)](https://developer.mozilla.org/ja/docs/Glossary/Cross-site_scripting) 攻撃に対して脆弱なので避けるべきです。
|
||||||
|
|
||||||
|
### タスク
|
||||||
|
|
||||||
|
ダッシュボード画面に移る前に、*ログイン* ページでもう一つやるべきことがあります。現在、存在しないユーザ名でログインしようとすると、コンソールにメッセージが表示されますが、通常のユーザの場合は何も変わらず、何が起こっているのかわかりません。
|
||||||
|
|
||||||
|
必要に応じてエラーメッセージを表示できるように、ログインフォームにプレースホルダ要素を追加してみましょう。良い場所はログインの直前の `<button>` です。
|
||||||
|
|
||||||
|
```html
|
||||||
|
...
|
||||||
|
<div id="loginError"></div>
|
||||||
|
<button>Login</button>
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
この `<div>` 要素は空で、コンテンツを追加するまで何も表示されません。また、JavaScript で簡単に取得できるように `id` を与えています。
|
||||||
|
|
||||||
|
ファイル `app.js` に戻り、新しいヘルパー関数 `updateElement` を作成します。
|
||||||
|
|
||||||
|
```js
|
||||||
|
function updateElement(id, text) {
|
||||||
|
const element = document.getElementById(id);
|
||||||
|
element.textContent = text;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
これは非常に簡単で、要素 *id* と *text* が与えられると、一致する `id` で DOM 要素のテキスト内容を更新します。このメソッドを `login` 関数の先ほどのエラーメッセージの代わりに使ってみましょう。
|
||||||
|
|
||||||
|
```js
|
||||||
|
if (data.error) {
|
||||||
|
return updateElement('loginError', data.error);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
無効なアカウントでログインしようとすると、このように表示されるはずです。
|
||||||
|
|
||||||
|
![ログイン中に表示されるエラーメッセージを示すスクリーンショット](../images/login-error.png)
|
||||||
|
|
||||||
|
これで、視覚的にはエラーテキストが表示されるようになりましたが、スクリーンリーダーで試してみると、何もアナウンスされないことに気づくでしょう。ページに動的に追加されたテキストをスクリーンリーダーでアナウンスするには、[ライブリージョン](https://developer.mozilla.org/ja/docs/Web/Accessibility/ARIA/ARIA_Live_Regions) と呼ばれるものを使用する必要があります。ここでは、アラートと呼ばれる特定のタイプのライブリージョンを使用するつもりです。
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div id="loginError" role="alert"></div>
|
||||||
|
```
|
||||||
|
|
||||||
|
`register` 関数のエラーに対しても同様の動作を実装する (HTML の更新を忘れずに)。
|
||||||
|
|
||||||
|
## ダッシュボードに情報を表示する
|
||||||
|
|
||||||
|
先ほど見たのと同じテクニックを使って、ダッシュボード・ページにアカウント情報を表示してみましょう。
|
||||||
|
|
||||||
|
サーバから受け取ったアカウントオブジェクトはこんな感じです。
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"user": "test",
|
||||||
|
"currency": "$",
|
||||||
|
"description": "Test account",
|
||||||
|
"balance": 75,
|
||||||
|
"transactions": [
|
||||||
|
{ "id": "1", "date": "2020-10-01", "object": "Pocket money", "amount": 50 },
|
||||||
|
{ "id": "2", "date": "2020-10-03", "object": "Book", "amount": -10 },
|
||||||
|
{ "id": "3", "date": "2020-10-04", "object": "Sandwich", "amount": -5 }
|
||||||
|
],
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> 注: 生活を楽にするためには、すでにデータが登録されている `test` アカウントを使用することができます。
|
||||||
|
|
||||||
|
### タスク
|
||||||
|
|
||||||
|
まずは HTML の「バランス」の部分を置き換えてプレースホルダ要素を追加してみましょう。
|
||||||
|
|
||||||
|
```html
|
||||||
|
<section>
|
||||||
|
Balance: <span id="balance"></span><span id="currency"></span>
|
||||||
|
</section>
|
||||||
|
```
|
||||||
|
|
||||||
|
また、アカウントの説明を表示するためのセクションをすぐ下に追加します。
|
||||||
|
|
||||||
|
```html
|
||||||
|
<h2 id="description"></h2>
|
||||||
|
```
|
||||||
|
|
||||||
|
✅ アカウントの説明は、その下にあるコンテンツのタイトルとして機能するため、意味的には見出しとしてマークアップされます。アクセシビリティにとって[見出し構造](https://www.nomensa.com/blog/2017/how-structure-headings-web-accessibility)がどのように重要であるかについて詳しく知り、他に何が見出しになり得るかを判断するためにページを批判的に見てみましょう。
|
||||||
|
|
||||||
|
次に、プレースホルダを埋めるための新しい関数を `app.js` に作成します。
|
||||||
|
|
||||||
|
```js
|
||||||
|
function updateDashboard() {
|
||||||
|
if (!account) {
|
||||||
|
return navigate('/login');
|
||||||
|
}
|
||||||
|
|
||||||
|
updateElement('description', account.description);
|
||||||
|
updateElement('balance', account.balance.toFixed(2));
|
||||||
|
updateElement('currency', account.currency);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
まず、先に進む前に必要なアカウントデータがあることを確認します。次に、先ほど作成した `updateElement()` 関数を使って HTML を更新します。
|
||||||
|
|
||||||
|
> 残高表示をよりきれいにするために、メソッド [`toFixed(2)`](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Number/toFixed) を使用して、小数点以下2桁の値を強制的に表示します。
|
||||||
|
|
||||||
|
これで、ダッシュボードがロードされるたびに `updateDashboard()` 関数を呼び出す必要があります。既に [レッスン1の課題] (../../1-template-route/translations/README.ja.md) を終了している場合は簡単ですが、そうでない場合は以下の実装を使用することができます。
|
||||||
|
|
||||||
|
このコードを `updateRoute()` 関数の最後に追加します。
|
||||||
|
|
||||||
|
```js
|
||||||
|
if (typeof route.init === 'function') {
|
||||||
|
route.init();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
ルート定義を更新します。
|
||||||
|
|
||||||
|
```js
|
||||||
|
const routes = {
|
||||||
|
'/login': { templateId: 'login' },
|
||||||
|
'/dashboard': { templateId: 'dashboard', init: updateDashboard }
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
この変更により、ダッシュボードページが表示されるたびに関数 `updateDashboard()` が呼び出されるようになりました。ログインすると、アカウントの残高、通貨、説明が表示されるようになります。
|
||||||
|
|
||||||
|
## HTML テンプレートを使用してテーブルの行を動的に作成
|
||||||
|
|
||||||
|
[最初のレッスン](../../1-template-route/translations/README.ja.md)では、アプリのナビゲーションを実装するために HTML テンプレートを [`appendChild()`](https://developer.mozilla.org/ja/docs/Web/API/Node/appendChild) メソッドと一緒に使用しました。テンプレートは小さくして、ページの繰り返し部分を動的に埋め込むために使用することもできます。
|
||||||
|
|
||||||
|
同様のアプローチを使用して、HTML テーブルにトランザクションのリストを表示します。
|
||||||
|
|
||||||
|
### タスク
|
||||||
|
|
||||||
|
HTML `<body>` に新しいテンプレートを追加します。
|
||||||
|
|
||||||
|
```html
|
||||||
|
<template id="transaction">
|
||||||
|
<tr>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
このテンプレートは1つのテーブル行を表し、3つのカラムを入力します。*日付*、*オブジェクト*、トランザクションの*金額*です。
|
||||||
|
|
||||||
|
次に、この `id` プロパティをダッシュボードテンプレート内のテーブルの `<tbody>` 要素に追加すると、JavaScript を使って見つけやすくなります。
|
||||||
|
|
||||||
|
```html
|
||||||
|
<tbody id="transactions"></tbody>
|
||||||
|
```
|
||||||
|
|
||||||
|
HTML の準備ができたので、JavaScript のコードに切り替えて新しい関数 `createTransactionRow` を作成しましょう。
|
||||||
|
|
||||||
|
```js
|
||||||
|
function createTransactionRow(transaction) {
|
||||||
|
const template = document.getElementById('transaction');
|
||||||
|
const transactionRow = template.content.cloneNode(true);
|
||||||
|
const tr = transactionRow.querySelector('tr');
|
||||||
|
tr.children[0].textContent = transaction.date;
|
||||||
|
tr.children[1].textContent = transaction.object;
|
||||||
|
tr.children[2].textContent = transaction.amount.toFixed(2);
|
||||||
|
return transactionRow;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
この関数はその名前が示す通りのことを行います。先ほど作成したテンプレートを使って新しいテーブルの行を作成し、トランザクションデータを使ってその内容を埋めます。先ほど作成したテンプレートを使って新しいテーブル行を作成し、トランザクションデータを使って内容を埋めます。
|
||||||
|
|
||||||
|
```js
|
||||||
|
const transactionsRows = document.createDocumentFragment();
|
||||||
|
for (const transaction of account.transactions) {
|
||||||
|
const transactionRow = createTransactionRow(transaction);
|
||||||
|
transactionsRows.appendChild(transactionRow);
|
||||||
|
}
|
||||||
|
updateElement('transactions', transactionsRows);
|
||||||
|
```
|
||||||
|
|
||||||
|
ここでは [`document.createDocumentFragment()`](https://developer.mozilla.org/ja/docs/Web/API/Document/createDocumentFragment) というメソッドを使用しています。これは新しい DOM フラグメントを作成し、その上で作業を行い、最終的にそれを HTML テーブルにアタッチする前のものです。
|
||||||
|
|
||||||
|
このコードが動作する前に、もう一つやらなければならないことがあります。というのは、現在のところ `updateElement()` 関数はテキストコンテンツのみをサポートしているからです。コードを少し変更してみましょう。
|
||||||
|
|
||||||
|
```js
|
||||||
|
function updateElement(id, textOrNode) {
|
||||||
|
const element = document.getElementById(id);
|
||||||
|
element.textContent = ''; // Removes all children
|
||||||
|
element.append(textOrNode);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
私たちは [`append()`](https://developer.mozilla.org/en-US/docs/Web/API/ParentNode/append) メソッドを使用しています。これにより、テキストや [DOM Nodes](https://developer.mozilla.org/ja/docs/Web/API/Node) を親要素にアタッチすることができ、すべてのユースケースに最適です。
|
||||||
|
|
||||||
|
`test` アカウントを使ってログインしてみると、ダッシュボード上にトランザクションリストが表示されるはずです 🎉。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 チャレンジ
|
||||||
|
|
||||||
|
ダッシュボードページを実際の銀行アプリのように見せるために一緒に作業しましょう。すでにアプリのスタイルを設定している場合は、[メディアクエリ](https://developer.mozilla.org/ja/docs/Web/CSS/Media_queries)を使用して、デスクトップとモバイルデバイスの両方でうまく機能する[レスポンシブデザイン](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Responsive/responsive_design_building_blocks)を作成してみてください。
|
||||||
|
|
||||||
|
ダッシュボードページのスタイリング例です。
|
||||||
|
|
||||||
|
![スタイリング後のダッシュボードの結果例のスクリーンショット](../images/screen2.png)
|
||||||
|
|
||||||
|
## レッスン後の小テスト
|
||||||
|
|
||||||
|
[レッスン後の小テスト](https://nice-beach-0fe9e9d0f.azurestaticapps.net/quiz/46)
|
||||||
|
|
||||||
|
## 課題
|
||||||
|
|
||||||
|
[コードのリファクタとコメント](assignment.ja.md)
|
@ -0,0 +1,15 @@
|
|||||||
|
# コードのリファクタとコメント
|
||||||
|
|
||||||
|
## 説明書
|
||||||
|
|
||||||
|
コードベースが大きくなってくると、読みやすく保守性の高いコードを維持するために、頻繁にリファクタリングを行うことが重要になってきます。コメントを追加して `app.js` をリファクタリングし、コードの品質を向上させましょう。
|
||||||
|
|
||||||
|
- サーバー API のベース URL のような定数を抽出します
|
||||||
|
- 類似したコードの因数分解: 例えば、`sendRequest()` 関数を作成して `createAccount()` と `getAccount()` の両方で使用するコードを再グループ化することができます
|
||||||
|
- 読みやすいようにコードを再編成し、コメントを追加します
|
||||||
|
|
||||||
|
## ルーブリック
|
||||||
|
|
||||||
|
| 基準 | 模範的な例 | 適切な | 改善が必要 |
|
||||||
|
| -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
|
||||||
|
| | コードはコメントされており、さまざまなセクションでよく整理されており、読みやすくなっています。定数が抽出され、因数分解された `sendRequest()` 関数が作成されています。 | コードはクリーンですが、より多くのコメント、定数抽出、因数分解などで改善することができます。 | コードは乱雑で、コメントされておらず、定数は抽出されておらず、コードは因数分解されていません。 |
|
@ -0,0 +1,281 @@
|
|||||||
|
# バンキングアプリを作ろう その 4: 状態管理の概念
|
||||||
|
|
||||||
|
## レッスン前の小テスト
|
||||||
|
|
||||||
|
[レッスン前の小テスト](https://nice-beach-0fe9e9d0f.azurestaticapps.net/quiz/47)
|
||||||
|
|
||||||
|
### イントロダクション
|
||||||
|
|
||||||
|
Web アプリケーションが成長するにつれて、すべてのデータの流れを追跡することが難しくなります。どのコードがデータを取得し、どのページがデータを消費し、どこでいつ更新する必要があるのか...メンテナンスが難しい厄介なコードになってしまいがちです。これは、ユーザーデータなど、アプリの異なるページ間でデータを共有する必要がある場合に特に当てはまります。*状態管理*の概念は、あらゆる種類のプログラムに常に存在していますが、Web アプリの複雑さが増すにつれ、開発中に考えるべき重要なポイントになってきています。
|
||||||
|
|
||||||
|
この最後のパートでは、状態の管理方法を再考するために構築したアプリを見ていきます。任意の時点でのブラウザの更新をサポートし、ユーザーセッション間でのデータの永続化を可能にします。
|
||||||
|
|
||||||
|
### 前提条件
|
||||||
|
|
||||||
|
このレッスンでは、Web アプリの[データ取得](./././3-data/translations/README.ja.md)の部分が完了している必要があります。また、アカウントデータを管理するためには、ローカルに [Node.js](https://nodejs.org/ja) をインストールし、[サーバー API を実行する](.../../api/translations/README.ja.md)をインストールする必要があります。
|
||||||
|
|
||||||
|
ターミナルでこのコマンドを実行することで、サーバーが正常に動作しているかどうかをテストすることができます。
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl http://localhost:5000/api
|
||||||
|
# -> 結果として "Bank API v1.0.0" を返す必要があります。
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 状態管理を再考する
|
||||||
|
|
||||||
|
[前回のレッスン](../../3-data/translations/README.ja.md)では、現在ログインしているユーザーの銀行データを含むグローバル変数 `account` を使って、アプリの基本的な状態の概念を紹介しました。しかし、現在の実装にはいくつかの欠陥があります。ダッシュボード上でページをリフレッシュしてみてください。何が起こるのでしょうか?
|
||||||
|
|
||||||
|
現在のコードには3つの問題があります。
|
||||||
|
|
||||||
|
- ブラウザをリフレッシュするとログインページに戻るため、状態は保持されません
|
||||||
|
- 状態を変更する関数が複数あります。アプリが大きくなると、変更を追跡するのが難しくなり、1つの更新を忘れがちになります
|
||||||
|
- 状態が片付かず、*Logout* をクリックしてもログインページになってもアカウントデータが残っています
|
||||||
|
|
||||||
|
これらの問題に一つずつ対処するためにコードを更新することもできますが、コードの重複が多くなり、アプリがより複雑になり、メンテナンスが難しくなります。あるいは、数分間小休止して、戦略を再考することもできます。
|
||||||
|
|
||||||
|
> ここで本当に解決しようとしている問題は何か?
|
||||||
|
|
||||||
|
[状態管理](https://en.wikipedia.org/wiki/State_management)は、この2つの特定の問題を解決するための良いアプローチを見つけることがすべてです。
|
||||||
|
|
||||||
|
- アプリ内のデータフローをわかりやすく保つには?
|
||||||
|
- アプリ内のデータフローを理解しやすい状態に保つには?
|
||||||
|
|
||||||
|
これらの問題を解決したら、他の問題はすでに解決されているか、簡単に解決できるようになっているかもしれません。これらの問題を解決するための多くの可能なアプローチがありますが、ここでは、**データとそれを変更する方法**を集中化することで構成される一般的な解決策を採用します。データの流れは次のようになります。
|
||||||
|
|
||||||
|
![HTML、ユーザーアクション、状態間のデータフローを示すスキーマ](../images/data-flow.png)
|
||||||
|
|
||||||
|
> ここでは、データが自動的にビューの更新のトリガーとなる部分は、[Reactive Programming](https://en.wikipedia.org/wiki/Reactive_programming)のより高度な概念に関連しているので、ここでは取り上げません。深く掘り下げたい方には良いフォローアップテーマになるでしょう。
|
||||||
|
|
||||||
|
✅ 状態管理へのさまざまなアプローチを持つライブラリはたくさんありますが、[Redux](https://redux.js.org) は人気のあるオプションです。大規模な Web アプリケーションで直面する可能性のある問題や、それをどのように解決できるかを学ぶための良い方法として、使用されている概念やパターンを見てみましょう。
|
||||||
|
|
||||||
|
### タスク
|
||||||
|
|
||||||
|
まずは少しリファクタリングをしてみましょう。`account` 宣言を置換します。
|
||||||
|
|
||||||
|
```js
|
||||||
|
let account = null;
|
||||||
|
```
|
||||||
|
|
||||||
|
このようにします。
|
||||||
|
|
||||||
|
```js
|
||||||
|
let state = {
|
||||||
|
account: null
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
このアイデアは、単一のステートオブジェクトにすべてのアプリデータを*中央集権化することです。今のところは `account` があるだけなので、あまり変化はありませんが、進化のためのパスを作成します。
|
||||||
|
|
||||||
|
また、これを使って関数を更新しなければなりません。関数 `register()` と `login()` において、`account = ...` を `state.account = ...` に置き換えてください。
|
||||||
|
|
||||||
|
関数 `updateDashboard()` の先頭に以下の行を追加します。
|
||||||
|
|
||||||
|
```js
|
||||||
|
const account = state.account;
|
||||||
|
```
|
||||||
|
|
||||||
|
今回のリファクタリングだけではあまり改善は見られませんでしたが、次の変更のための基礎を固めようと考えたのです。
|
||||||
|
|
||||||
|
## データ変更の追跡
|
||||||
|
|
||||||
|
データを保存するために `state` オブジェクトを配置したので、次のステップは更新を一元化することです。目的は、いつ変更があったのか、いつ変更が発生したのかを簡単に把握できるようにすることです。
|
||||||
|
|
||||||
|
`state` オブジェクトに変更が加えられないようにするためには、`state` オブジェクトを [*immutable*](https://en.wikipedia.org/wiki/Immutable_object) と考えるのも良い方法です。これはまた、何かを変更したい場合には新しいステートオブジェクトを作成しなければならないことを意味します。このようにすることで、潜在的に望ましくない[副作用](https://en.wikipedia.org/wiki/Side_effect_(computer_science)についての保護を構築し、アンドゥ/リドゥの実装のようなアプリの新機能の可能性を開くと同時に、デバッグを容易にします。例えば、ステートに加えられたすべての変更をログに記録し、バグの原因を理解するために変更の履歴を保持することができます。
|
||||||
|
|
||||||
|
JavaScript では、[`Object.freeze()`](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze) を使って、オブジェクトの不変バージョンを作成することができます。不変オブジェクトに変更を加えようとすると例外が発生します。
|
||||||
|
|
||||||
|
✅ *浅い*不変オブジェクトと*深い*不変オブジェクトの違いを知っていますか? それについては [こちら](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze#What_is_shallow_freeze) を参照してください。
|
||||||
|
|
||||||
|
### タスク
|
||||||
|
|
||||||
|
新しい `updateState()` 関数を作成してみましょう。
|
||||||
|
|
||||||
|
```js
|
||||||
|
function updateState(property, newData) {
|
||||||
|
state = Object.freeze({
|
||||||
|
...state,
|
||||||
|
[property]: newData
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
この関数では、新しいステートオブジェクトを作成し、[*spread (`...`) operator*](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/Spread_syntax#spread_in_object_literals) を使用して前のステートからデータをコピーしています。次に、[ブラケット表記](https://developer.mozilla.org/ja/docs/Web/JavaScript/Guide/Working_with_Objects#objects_and_properties) `[property]` を使用して、ステートオブジェクトの特定のプロパティを新しいデータでオーバーライドします。最後に、`Object.freeze()`を使ってオブジェクトをロックし、変更を防ぎます。今のところ、`account` プロパティだけがステートに保存されていますが、この方法では必要なだけのプロパティをステートに追加することができます。
|
||||||
|
|
||||||
|
また、`state` の初期化を更新して初期状態も凍結されるようにします。
|
||||||
|
|
||||||
|
```js
|
||||||
|
let state = Object.freeze({
|
||||||
|
account: null
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
その後、`state.account = result;` の代入を `state.account = result;` に置き換えて `register` 関数を更新します。
|
||||||
|
|
||||||
|
```js
|
||||||
|
updateState('account', result);
|
||||||
|
```
|
||||||
|
|
||||||
|
同じことを `login` 関数で行い、`state.account = data;` に置き換えます。
|
||||||
|
|
||||||
|
```js
|
||||||
|
updateState('account', data);
|
||||||
|
```
|
||||||
|
|
||||||
|
ここで、ユーザーが *Logout* をクリックしたときにアカウントデータがクリアされない問題を修正します。
|
||||||
|
|
||||||
|
新しい関数 `logout()` を作成します。
|
||||||
|
|
||||||
|
```js
|
||||||
|
function logout() {
|
||||||
|
updateState('account', null);
|
||||||
|
navigate('/login');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`updateDashboard()` で、リダイレクト `return navigate('/login');` を `return logout()` に置き換えてください。
|
||||||
|
|
||||||
|
新しいアカウントを登録して、ログアウトとログインを繰り返してみて、すべてが正常に動作することを確認してください。
|
||||||
|
|
||||||
|
> ヒント: `updateState()` の下部に `console.log(state)` を追加し、ブラウザの開発ツールでコンソールを開くことで、すべての状態の変化を見ることができます。
|
||||||
|
|
||||||
|
## 状態を維持する
|
||||||
|
|
||||||
|
ほとんどの Web アプリは、データを保持しておかないと正常に動作しません。すべての重要なデータは通常、データベースに保存され、サーバー API を介してアクセスされます。しかし、時には、より良いユーザーエクスペリエンスや読み込みパフォーマンスを向上させるために、ブラウザ上で実行されているクライアントアプリのデータを永続化することも興味深いことです。
|
||||||
|
|
||||||
|
ブラウザにデータを永続化する場合、いくつかの重要な質問があります。
|
||||||
|
|
||||||
|
- *データは機密性の高いものでしょうか?* ユーザーパスワードなどの機密性の高いデータをクライアントに保存することは避けるべきです
|
||||||
|
- *このデータをどのくらいの期間保存する必要がありますか?* このデータにアクセスするのは現在のセッションのためだけですか、それとも永遠に保存したいですか?
|
||||||
|
|
||||||
|
Web アプリ内の情報を保存する方法は、目的に応じて複数あります。例えば、URL を使用して検索クエリを保存し、ユーザー間で共有できるようにすることができます。また、[認証](https://en.wikipedia.org/wiki/Authentication)情報のように、データをサーバーと共有する必要がある場合は、[HTTP クッキー](https://developer.mozilla.org/ja/docs/Web/HTTP/Cookies)を使用することもできます。
|
||||||
|
|
||||||
|
もう一つの選択肢は、データを保存するための多くのブラウザ API のうちの一つを使用することです。その中でも特に興味深いものが2つあります。
|
||||||
|
|
||||||
|
- [`localStorage`](https://developer.mozilla.org/ja/docs/Web/API/Window/localStorage): [Key/Value ストア](https://en.wikipedia.org/wiki/Key%E2%80%93value_database)は、異なるセッションにまたがって現在の Web サイトに固有のデータを永続化することができます。保存されたデータは期限切れになることはありません
|
||||||
|
- [`sessionStorage`](https://developer.mozilla.org/ja/docs/Web/API/Window/sessionStorage): これは `localStorage` と同じように動作しますが、保存されたデータはセッションの終了時(ブラウザが閉じられた時)に消去されます
|
||||||
|
|
||||||
|
これらの API はどちらも[文字列](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/String)しか保存できないことに注意してください。複雑なオブジェクトを格納したい場合は、[`JSON.stringify()`](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify) を使って [JSON](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/JSON) 形式にシリアライズする必要があります。
|
||||||
|
|
||||||
|
✅ サーバーで動作しない Web アプリを作成したい場合は、[`IndexedDB` API](https://developer.mozilla.org/ja/docs/Web/API/IndexedDB_API) を使用してクライアント上にデータベースを作成することも可能です。これは高度なユースケースや、かなりの量のデータを保存する必要がある場合には、使用するのがより複雑になるため、予約されています。
|
||||||
|
|
||||||
|
### タスク
|
||||||
|
|
||||||
|
ユーザーが明示的に *Logout* ボタンをクリックするまでログインしたままにしたいので、`localStorage` を使ってアカウントデータを保存します。まず、データを保存するためのキーを定義しましょう。
|
||||||
|
|
||||||
|
```js
|
||||||
|
const storageKey = 'savedAccount';
|
||||||
|
```
|
||||||
|
|
||||||
|
そして、この行を `updateState()` 関数の最後に追加します。
|
||||||
|
|
||||||
|
```js
|
||||||
|
localStorage.setItem(storageKey, JSON.stringify(state.account));
|
||||||
|
```
|
||||||
|
|
||||||
|
これで、以前はすべての状態の更新を一元化していたので、ユーザーアカウントのデータは永続化され、常に最新の状態になります。ここからが、以前のすべてのリファクタリングの恩恵を受け始めるところです 🙂。
|
||||||
|
|
||||||
|
データが保存されているので、アプリが読み込まれたときに復元することにも気を配らなければなりません。初期化コードが増えてくるので、`app.js` の下部に以前のコードも含めた `init` 関数を新たに作成しておくといいかもしれません。
|
||||||
|
|
||||||
|
```js
|
||||||
|
function init() {
|
||||||
|
const savedAccount = localStorage.getItem(storageKey);
|
||||||
|
if (savedAccount) {
|
||||||
|
updateState('account', JSON.parse(savedAccount));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 前回の初期化コード
|
||||||
|
window.onpopstate = () => updateRoute();
|
||||||
|
updateRoute();
|
||||||
|
}
|
||||||
|
|
||||||
|
init();
|
||||||
|
```
|
||||||
|
|
||||||
|
ここでは保存されたデータを取得し、もしあればそれに応じて状態を更新します。ページの更新中に状態に依存するコードがあるかもしれないので、ルートを更新する前にこれを行うことが重要です。
|
||||||
|
|
||||||
|
アカウントデータを保持しているので、*ダッシュボード* ページをアプリケーションのデフォルトページにすることもできます。データが見つからない場合は、ダッシュボードが *ログイン* ページにリダイレクトします。`updateRoute()` で、フォールバックの `return navigate('/login');` を `return navigate('dashboard');` に置き換えます。
|
||||||
|
|
||||||
|
アプリでログインしてページを更新してみてください。このアップデートで初期の問題はすべて解決しました。
|
||||||
|
|
||||||
|
## データの更新
|
||||||
|
|
||||||
|
...しかし、我々はまた、新しいものを作ったかもしれません。おっと!
|
||||||
|
|
||||||
|
`test` アカウントを使ってダッシュボードに行き、ターミナルで以下のコマンドを実行して新しいトランザクションを作成します。
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl --request POST \
|
||||||
|
--header "Content-Type: application/json" \
|
||||||
|
--data "{ \"date\": \"2020-07-24\", \"object\": \"Bought book\", \"amount\": -20 }" \
|
||||||
|
http://localhost:5000/api/accounts/test/transactions
|
||||||
|
```
|
||||||
|
|
||||||
|
ダッシュボードのページをブラウザで更新してみてください。どうなりますか?新しいトランザクションが表示されましたか?
|
||||||
|
|
||||||
|
この状態は `localStorage` のおかげで無期限に保持されますが、アプリからログアウトして再度ログインするまで更新されません。
|
||||||
|
|
||||||
|
これを修正するために考えられる戦略の1つは、ダッシュボードがロードされるたびにアカウントデータをリロードして、データのストールを回避することです。
|
||||||
|
|
||||||
|
### タスク
|
||||||
|
|
||||||
|
新しい関数 `updateAccountData` を作成します。
|
||||||
|
|
||||||
|
```js
|
||||||
|
async function updateAccountData() {
|
||||||
|
const account = state.account;
|
||||||
|
if (!account) {
|
||||||
|
return logout();
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await getAccount(account.user);
|
||||||
|
if (data.error) {
|
||||||
|
return logout();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateState('account', data);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
このメソッドは現在ログインしているかどうかをチェックし、サーバからアカウントデータをリロードします。
|
||||||
|
|
||||||
|
`refresh` という名前の別の関数を作成します。
|
||||||
|
|
||||||
|
```js
|
||||||
|
async function refresh() {
|
||||||
|
await updateAccountData();
|
||||||
|
updateDashboard();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
これはアカウントデータを更新し、ダッシュボードページの HTML を更新する処理を行います。ダッシュボードルートがロードされたときに呼び出す必要があるものです。これでルート定義を更新します。
|
||||||
|
|
||||||
|
```js
|
||||||
|
const routes = {
|
||||||
|
'/login': { templateId: 'login' },
|
||||||
|
'/dashboard': { templateId: 'dashboard', init: refresh }
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
ダッシュボードをリロードしてみると、更新されたアカウントデータが表示されるはずです。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 チャレンジ
|
||||||
|
|
||||||
|
ダッシュボードがロードされるたびにアカウントデータをリロードするようになりましたが、すべてのアカウントデータを保持する必要があると思いますか?
|
||||||
|
|
||||||
|
アプリが動作するために絶対に必要なものだけを含むように、`localStorage` から保存およびロードされるものを変更するために、一緒に作業してみてください。
|
||||||
|
|
||||||
|
## レッスン後の小テスト
|
||||||
|
|
||||||
|
[レッスン後の小テスト](https://nice-beach-0fe9e9d0f.azurestaticapps.net/quiz/48)
|
||||||
|
|
||||||
|
## 課題
|
||||||
|
|
||||||
|
[「トランザクションの追加」ダイアログの実装](assignment.ja.md)
|
||||||
|
|
||||||
|
課題を終えた後の結果の一例です。
|
||||||
|
|
||||||
|
![「トランザクションの追加」ダイアログの例を示すスクリーンショット](../images/dialog.png)
|
@ -0,0 +1,25 @@
|
|||||||
|
# 「トランザクションの追加」ダイアログの実装
|
||||||
|
|
||||||
|
## 説明書
|
||||||
|
|
||||||
|
私たちの銀行アプリには、1つの重要な機能がまだありません。
|
||||||
|
前の4つのレッスンで学んだことをすべて使って、「トランザクションの追加」ダイアログを実装します。
|
||||||
|
|
||||||
|
- ダッシュボードページに「トランザクションの追加」ボタンを追加します
|
||||||
|
- HTML テンプレートで新しいページを作成するか、JavaScript を使用してダッシュボード・ページを離れることなくダイアログの HTML を表示/非表示にするかのいずれかを選択します (そのためには [`hidden`](https://developer.mozilla.org/ja/docs/Web/HTML/Global_attributes/hidden) プロパティを使用するか、CSS クラスを使用することができます)
|
||||||
|
- ダイアログの [キーボードとスクリーンリーダーのアクセシビリティ] (https://developer.paciellogroup.com/blog/2018/06/the-current-state-of-modal-dialog-accessibility/) が処理されていることを確認します
|
||||||
|
- 入力データを受け取るための HTML フォームを実装する
|
||||||
|
- フォームデータから JSON データを作成して API に送る
|
||||||
|
- ダッシュボードページを新しいデータで更新する
|
||||||
|
|
||||||
|
[サーバー API の仕様](./.../.../api/translations/README.ja.md)を見て、どの API を呼び出す必要があるか、期待される JSON 形式は何かを確認してください。
|
||||||
|
|
||||||
|
以下は、課題を完了した後の結果の例です。
|
||||||
|
|
||||||
|
![「トランジションの追加」ダイアログの例を示すスクリーンショット](../../images/dialog.png)
|
||||||
|
|
||||||
|
## ルーブリック
|
||||||
|
|
||||||
|
| 基準 | 模範的な例 | 適切な | 改善が必要 |
|
||||||
|
| -------- | ------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------- | --------------------------------------------|
|
||||||
|
| | トランザクションの追加は、レッスンで見たすべてのベストプラクティスに従って完全に実装されています。 | トランザクションを追加することは実装されていますが、レッスンで見たベストプラクティスに従っていなかったり、部分的にしか動作しなかったりします。 | トランザクションを追加しても全く機能しません。 |
|
@ -0,0 +1,33 @@
|
|||||||
|
# 銀行 API
|
||||||
|
|
||||||
|
> 銀行 API は [Node.js](https://nodejs.org/ja/) + [Express](https://expressjs.com/ja/) で構築されています。
|
||||||
|
|
||||||
|
API はすでにあなたのために構築されており、演習の一部ではありません。
|
||||||
|
|
||||||
|
しかし、このような API の構築方法に興味があるのであれば、このシリーズのビデオを見ることができます: https://aka.ms/NodeBeginner (ビデオ 17 から 21 では、この API を正確にカバーしています)。
|
||||||
|
|
||||||
|
また、こちらのインタラクティブなチュートリアルもご覧ください: https://aka.ms/learn/express-api
|
||||||
|
|
||||||
|
## サーバーの実行
|
||||||
|
|
||||||
|
[Node.js](https://nodejs.org/ja/) がインストールされていることを確認してください。
|
||||||
|
|
||||||
|
1. このリポジトリを Git でクローンします
|
||||||
|
2. `api` フォルダでターミナルを開き、`npm install` を実行します
|
||||||
|
3. `npm start` を実行します
|
||||||
|
|
||||||
|
サーバーは `5000` ポートで待ち受けを開始するはずです。
|
||||||
|
|
||||||
|
> 注意: すべてのエントリはメモリに保存され、永続化されないので、サーバを停止するとすべてのデータが失われます。
|
||||||
|
|
||||||
|
## API の詳細
|
||||||
|
|
||||||
|
ルート | 説明
|
||||||
|
---------------------------------------------|------------------------------------
|
||||||
|
GET /api/ | サーバー情報を取得します
|
||||||
|
POST /api/accounts/ | アカウントを作成します。例: `{ user: 'Yohan', description: 'My budget', currency: 'EUR', balance: 100 }`
|
||||||
|
GET /api/accounts/:user | 指定したアカウントのすべてのデータを取得します
|
||||||
|
DELETE /api/accounts/:user | 指定したアカウントを削除します
|
||||||
|
POST /api/accounts/:user/transactions | トランザクションを追加します。例: `{ date: '2020-07-23T18:25:43.511Z', object: 'Bought a book', amount: -20 }`
|
||||||
|
DELETE /api/accounts/:user/transactions/:id | 指定されたトランザクションを削除します
|
||||||
|
|
@ -0,0 +1,13 @@
|
|||||||
|
# 銀行アプリ
|
||||||
|
|
||||||
|
> バニラ HTML5、CSS、JavaScript で構築された銀行アプリプロジェクトのソリューション例 (フレームワークやライブラリは使用していません)。
|
||||||
|
|
||||||
|
## アプリの実行
|
||||||
|
|
||||||
|
まず、[API サーバー](../../api/translations/README.ja.md)が起動していることを確認します。
|
||||||
|
|
||||||
|
アプリの実行にはどの Web サーバーを使用しても構いませんが、API を実行するためには [Node.js](https://nodejs.org/ja) がインストールされている必要があるので、以下のようにします。
|
||||||
|
|
||||||
|
1. このレポを Git でクローンします
|
||||||
|
2. ターミナルを開き、`npx lite-server solution` を実行します。これで、`3000` ポートで開発用ウェブサーバが起動します
|
||||||
|
3. ブラウザで `http://localhost:3000` を開いてアプリを実行します
|
Loading…
Reference in new issue