2020-05-20

Ajax通信をする小さなアプリの作成

JavaScript Primer の第二部をやったメモ.

Ajax通信 · JavaScript Primer #jsprimer を実際にやって見た際のメモ.

作るもの

  • APIを呼び出して,GitHubからユーザー情報を取得する
  • 取得した情報をページに表示する

環境

  • macOS catalina
  • Node.js v13.13.0

真っ白のページを作る

まず,サーバーを立てて,HTMLにアクセスできる状態にします.

プロジェクトディレクトリの作成

今回はAjax-sampleというディレクトリを作成し,そのディレクトリ以下で作業をします.

HTMLファイルの作成

エントリーポイントとなるhtmlファイルを作成します.index.jsを読み込むだけの真っ白なページです.

index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Ajax Sample</title>
</head>
<body>
<script src="index.js"></script>
</body>
</html>

JavaScriptファイルの作成

読み込めているかを確認するだけなので,適当な文字列を表示するコードを書きます.

index.js
console.log("OK");

サーバーを立てる

Node.jsを使ってサーバーを立てます.Express.jsを使ってもいいですが,今回はhttp.createServerを使って素朴に実装します.

一部のIDEは特別な設定をしなくてもhtmlをローカルサーバー上で見られるので,この部分は省略できます.

実装

Server.js
server.js
const http = require('http');
const fs = require('fs');
const contentType = new Map([
    ["html", "text/html"],
    ["js", "text/javascript"],
    ["css", "text/css"],
    ["png", "image/png"],
    ["ico", "image/vnd.microsoft.icon"],
]);


http.createServer(function (req, res) {
    const ext = req.url.split('.').pop();

    try {
        const data = fs.readFileSync('.' + req.url);
        res.writeHead(200, {'Content-Type': contentType.get(ext)});
        res.write(data);
    } catch (e) {
        res.writeHead(404, {'Content-Type': 'text/plain'});
        res.write(e.message);
    }
    res.end();
}).listen(8080);

http.createServer

req.urlで指定されたファイルを返します.指定されたファイルが存在しない時,エラーメッセージを返します.

server.js
http.createServer(function (req, res) {
    const ext = req.url.split('.').pop();

    try {
        const data = fs.readFileSync('.' + req.url);
        res.writeHead(200, {'Content-Type': contentType.get(ext)});
        res.write(data);
    } catch (e) {
        res.writeHead(404, {'Content-Type': 'text/plain'});
        res.write(e.message);
    }
    res.end();
}).listen(8080);

参考:Node.js http.createServer() Method

contentType

[拡張子, contentType]Map.リクエストされたファイルの拡張子によって,適切なContentTypeを返します.

server.js
const contentType = new Map([
    ["html", "text/html"],
    ["js", "text/javascript"],
    ["css", "text/css"],
    ["png", "image/png"],
    ["ico", "image/vnd.microsoft.icon"],
]);

参考:よくある MIME タイプ - HTTP | MDN

HTMLの表示

 Ajax-sample
 ├── index.html
 ├── index.js
 └── server.js

Ajax-sample下でnode server.jsを実行し,http://localhost:8080/index.htmlにアクセスすると,以下のようなページが表示されます.コンソールには"OK"が表示されます.ファビコンは用意していないので404が返ります.

undefined

今回の実装ではhttp://localhost:8080以下の文字列をファイル名として処理するので,http://localhost:8080http://localhost:8080/indexではファイルが読み込めません.

HTTP通信

次に,API(https://developer.github.com/v3/users/)を呼び出して,レスポンスをコンソールに表示します.

HTTPリクエストを送る関数の作成

fetchメソッドでHTTPリクエストを作成,送信ができます.このメソッドはPromiseを返すため,thenで成功時と失敗時に呼ばれる関数を登録します.また,レスポンスのデータをjsonに変換するメソッドもPromiseを返すので,jsonをコンソールに表示する関数を渡しておきます.

特定の文字がURIに含まれていると正しく動作しないため,encodeURIComponent()でエスケープしておきます.

index.js
function fetchUserInfo(userId) {
  fetch(`https://api.github.com/users/${encodeURIComponent(userId)}`)
    .then(
      (response) => {
         if (response.ok) {
           return response.json().then(userInfo =>
             console.log(userInfo));
         } else {
           console.log("Error :", response);
         }
      }, 
      (error) => {
        console.log(error);
      }
    );
}

参考:encodeURIComponent() - JavaScript | MDN

HTTPリクエストを送るためのボタンを作成

HTMLにボタンを作成し,そのボタンを押すとfetchUserInfoを呼び出すようにします.ここでは,userIdを埋め込んでいます.

index.html
...
<body>
<button onclick="fetchUserInfo('noy72');">Get user info</button>
<script src="index.js"></script>
</body>
...

実際に送ってみる

http://localhost:8080/index.htmlにアクセスし,表示されているボタンを押すと,HTTPリクエストが送られます.結果は以下のようにコンソールに表示されます.

undefined

データをページに表示する

得られたデータをページに表示するようにします.

今回は,

  • アバター
  • ユーザー名
  • ユーザーID
  • フォロー数
  • フォロワー数
  • レポジトリ数

を表示表示します.

HTMLの組み立て

HTMLは以下のような構造にします.

.
├── ユーザー名,ユーザーID
├── アバター
└── .
    ├── フォロー数
    ├── フォロワー数
    └──レポジトリ数

Element#innerHTMLプロパティに作成したHTML文字列をセットする方法がありますが,HTMLのエスケープ処理をしたくないので,今回はElementオブジェクトを生成してツリーを構築します.

結果を表示する部分の作成

button要素とscript要素の間に空のdiv要素を入れておきます.

index.html
...
<button onclick="fetchUserInfo('noy72');">Get user info</button>
<div></div> <!-- ここにユーザー情報を表示する -->
<script src="index.js"></script>
...

HTMLの組み立てとDOMへの要素の追加

HTML要素はdocument.createElementで,単なる文字列はdocument.createTextNodeで作成します.Element#appendChildで子要素を追加してHTMLを組み立てます.

組み立てたHTMLは,用意しておいたdiv要素に挿入します.今回はquerySelectordiv要素を受け取り,そこに組み立てたHTMLを挿入します.

index.js
function buildHTML(info) {
    const user_name = document.createElement("h4");
    user_name.appendChild(
        document.createTextNode(`${info.name} (@${info.login})`)
    );

    const avatar = document.createElement("img");
    avatar.src = info.avatar_url;
    avatar.alt = info.login;
    avatar.height = 100;
    
    const list = document.createElement("ul");
    const following = document.createElement("li");
    following.appendChild(
        document.createTextNode(`Following: ${info.following}`)
    );
    const followers = document.createElement("li");
    followers.appendChild(
        document.createTextNode(`Followers: ${info.followers}`)
    );
    const repos = document.createElement("li");
    repos.appendChild(
        document.createTextNode(`Repos: ${info.public_repos}`)
    );
    list.appendChild(following);
    list.appendChild(followers);
    list.appendChild(repos);
    
    const result = document.querySelector('body > div');
    result.appendChild(user_name);
    result.appendChild(avatar);
    result.appendChild(list);
}

上記のコードは以下のようなHTMLを生成します(${}の部分は展開されます).

<div>
  <h4>${info.name} (@${info.login})</h4>
  <img src="${info.avater_url}" alt="${info.login}" height="100">
  <ul>
    <li>Following: ${info.following}</li>
    <li>Followers: ${info.followers}</li>
    <li>Repos: ${info.public_repos}</li>
  </ul>
</div>

buildHTMLを呼ぶ

fetchして得られたデータをbuildHTMLの引数にし,関数を呼びます.これで,ボタンを押すとデータが表示できるようになります.

index.js
if (response.ok) {
  response.json().then(userInfo => {
    buildHTML(userInfo);
  });
} else {
...
undefined

Asyncを使う

現在はfetchメソッドのコールバックでbuildHTMLメソッドを呼び,DOMに変更を加えています.

これを,Asyncを使って同期処理のように書き換えてみます.

main関数の追加

直接fetchUserInfoを呼ばずに,main関数を通して呼ぶようにします.

index.js
function main(){
    fetchUserInfo("noy72")
}
index.html
...
<body>
<button onclick="main();">Get user info</button>
<div></div>
...

Promiseオブジェクトを返すようにする

fetchUserInfoでDOMを書き換えるのではなく,Promiseオブジェクトを返すようにします.成功した場合はResponse#jsonの戻り値をそのまま返し,失敗した場合はPromise.rejectでエラーを返します.

index.js
function fetchUserInfo(userId) {
    return fetch(`https://api.github.com/users/${encodeURIComponent(userId)}`).then(
        (response) => {
            if (response.ok) {
                return response.json();
            } else {
                return Promise.reject(
                    new Error(`${response.status} ${response.statusText}`)
                );
            }
        }
    );
}

mainでPromiseを処理

fetchUserInfoPromiseを返すようになったので,mainで処理します.成功した場合は(userInfo) => buildHTML(userInfo)が実行され,DOMを書き換えます.エラーが発生した場合は,(error) => console.log(error)が実行されます.

index.js
function main() {
    fetchUserInfo("noy72")
        .then((userInfo) => buildHTML(userInfo))
        .catch((error) => console.log(error))
}

これでPromiseを使った処理ができるようになりました.コードは変わりましたが意味的にはPromiseを使っていないのと同じなので,前回実行した時と同様の動作をします.

Asyncを使う

Promiseを返すfetchUserInfoawaitすることができます.

index.js
async function main() {
    try {
        const userInfo = await fetchUserInfo("noy72");
        buildHTML(userInfo);
    } catch (error) {
        console.log(error);
    }
}

fetchのコールバックで処理を行う実装から,手続的に処理を行う実装になりました.

作成したアプリのリポジトリ

[https://github.com/noy72/JavaScript-Sample-Apps/tree/master/Ajax-sample:title]

まとめ

GithubのAPIを呼び出し,取得したデータを表示するアプリを作成しました.

  • サーバーを立てて,HTMLの表示とJavaScriptの実行をした
  • fetchを使ってHTTPリクエストを送った
  • DOMを書き換えて,APIから取得したデータを表示した
  • Async functionに置き換えた.

現在の実装ではユーザー名を埋め込んでいますが,Promiseを活用する · JavaScript Primer #jsprimer ではユーザー名を変更できるように実装しています.