Typescriptを利用したPromiseベースのHttpClient

動機

クライアントからXMLHttpRequest を使いたいと思ったら axios があれば事足りるのだが、運用を考えるなるべく余分なライブラリを使わないほうが良いと思い自作してみた。

また機能自体はミニマムなので、axios にはほど遠いのだが、promise や XMLHttpRequest への理解が曖昧だったので、メモとして理解した内容を残しておく。

動かして簡単に動かしてみたい方は以下の確認環境を作ったのでよかったらどうぞ。

サンプルと確認環境

https://github.com/nokogiring/Promise-based-RESTClient

参考

環境

  • macOS 10.12.6
  • Typescript 2.7.2

解説

  • 今回作るのはREST APIの基本部分
  • /users にアクセスすると ユーザー情報の取得、更新、作成、削除を行える REST API

REST API

Method URL staus header
GET /users 200
POST /users 201 'Content-type': 'application/json'
PUT /users/${id} 200 'Content-type': 'application/json'
DELETE /users/${id} 200
  • Method と status の組み合わせはサーバーサイドの実行環境にて異なるので注意必要

GETで取得できるデータ

users.json
[
  {
    "id": 1,
    "name": "nanami",
    "age": 25
  },
  {
    "id": 2,
    "name": "sayuri",
    "age": "25"
  },
  {
    "id": 3,
    "name": "mai",
    "age": 25
  }
]

REST-Clientの実装

基本はXMLHttpクラスのpublicな関数(get,post,put,delete)を呼び出すことで、成功と失敗時のコールバック関数を実行するための Promise オブジェクトを返却する。このあたりの実装は、参考に記載に promise-book の実装通り。

GET,POST,PUT,DELETE の共通部分は request として切り出して、メソッド毎の差分を引数で与える。

class XMLHttp {

    static get = (url: string) => {
        return XMLHttp.request(url, {}, 'GET', 200, '');
    }

    static post = (url: string, data: {}) => {
        return XMLHttp.request(url, { 'Content-type': 'application/json' }, 'POST', 201, JSON.stringify(data));
    }

    static put = (url: string, data: {}) => {
        return XMLHttp.request(url, { 'Content-type': 'application/json' }, 'PUT', 200, JSON.stringify(data));
    }

    static delete = (url: string) => {
        return XMLHttp.request(url, {}, 'DELETE', 200, '');
    }

    private static request = (url: string, headers: {}, method: string, status: number, stringdata: string) => {
        return new Promise(((resolve, reject) => {
            const req = new XMLHttpRequest();
            req.open(method, url, true);
            for (let key in headers) {
                if (headers.hasOwnProperty(key)) {
                    req.setRequestHeader(key, headers[key]);
                }
            }
            req.onreadystatechange = () => {
                if (req.readyState === XMLHttpRequest.DONE) {
                    if (req.status === status) {
                        resolve(req.responseText);
                    } else {
                        reject(new Error(req.statusText));
                    }
                }
            };
            req.onerror = () => {
                reject(new Error(req.statusText));
            };
            req.send(stringdata === '' ? null : stringdata);
        }));
    }
}

ググって他のサンプルを見るとreadyStateの変更イベントハンドラーを GET だと onload とか POST だと onreadystatechange が呼び出されていた。

MDN web docs によると

onreadystatechange は XMLHttpRequest のインスタンスとしてすべてのブラウザーが対応しています。
それ以来、数多くの追加のイベントハンドラーが様々なブラウザーに実装されてきています(onload, onerror, onprogress, など)。 XMLHttpRequest の使用を参照してください。

だそうで、それぞれ使い方によって用途が異なる。

今回の用途だと open() 後に readystate が変化するたびにイベントハンドリングを行い、DONE になったタイミングで resolve , reject処理を行えば事足りるので、どのメソッドでも onreadystatechange を利用している。

【参考】 XMLHttpRequest.readyState

状態 説明
0 UNSENT クライアントは作成済み。open() はまだ呼ばれていない。
1 OPENED open() が呼び出し済み。
2 HEADERS_RECEIVED send() が呼び出し済みで、ヘッダーとステータスが利用可能。
3 LOADING ダウンロード中。responseText には部分データが入っている。
4 DONE 操作が完了した。

薄いラッパーの実装

User以外のデータの取得の拡張性を考慮し、通信が発生する クラスと実行クラスからの呼び出しに1枚薄いラッパークラスを作っておく。
この辺は好みの問題でもあると思うので、直でXMLHttp を呼び出すように作ってもいいと思う。

interface User {
    id: number,
    name: string,
    age: number
}

class Request4User {

    private static get HOST() {
        return ' http://localhost:3000/users';
    }

    static getUsers = () => {
        return XMLHttp.get(Request4User.HOST);
    }

    static findUser = (id: number) => {
        return XMLHttp.get(`${Request4User.HOST}/${id}`);
    }

    static createUser = (user: User) => {
        const jsonData = {
            name: user.name,
            age: user.age
        };
        return XMLHttp.post(`${Request4User.HOST}`, jsonData);
    }

    static updateUser = (user: User) => {
        const jsonData = {
            name: user.name,
            age: user.age
        };
        return XMLHttp.put(`${Request4User.HOST}/${user.id}`, jsonData);
    }

    static deleteUser = (id: number) => {
        return XMLHttp.delete(`${Request4User.HOST}/${id}`);
    }

}

実行クラスの実装

最後は呼び出し元。GET,POST,PUT,DELETEが全て実行されていることを確認する

Request4User.getUsers()
    .then((response) => {
        console.log("\n\n*** getUsers ***\n");
        console.log(response)
    })
    .catch(console.error);

const misa: User = {
    "id": 4,
    "name": "misa",
    "age": 24
}


Request4User.createUser(misa)
    .then((response) => {
        console.log("\n\n*** createUser ***\n");
        console.log(response)
    })
    .catch(console.error);

Request4User.findUser(4)
    .then((response) => {
        console.log("\n\n*** findUser ***\n");
        console.log(response)
    })
    .catch(console.error);

const MableAntie: User = {
    "id": 4,
    "name": "marble auntie",
    "age": 24
}

Request4User.updateUser(MableAntie)
    .then((response) => {
        console.log("\n\n*** updateUser ***\n");
        console.log(response)
    })
    .catch(console.error);

Request4User.deleteUser(1)
    .then((response) => {
        console.log("\n\n*** deleteUser ***\n");
    })
    .catch(console.error)

実行結果(ブラウザのコンソールで確認)

*** getUsers ***

app.js:71 [
  {
    "id": 1,
    "name": "nanami",
    "age": 24
  },
  {
    "id": 2,
    "name": "sayuri",
    "age": "25"
  },
  {
    "id": 3,
    "name": "mai",
    "age": 25
  }
]
app.js:81 

*** createUser ***

app.js:82 {
  "name": "misa",
  "age": 24,
  "id": 4
}
app.js:104 

*** deleteUser ***

app.js:87 

*** findUser ***

app.js:88 {
  "name": "misa",
  "age": 24,
  "id": 4
}
app.js:98 

*** updateUser ***

app.js:99 {
  "name": "marble auntie",
  "age": 24,
  "id": 4
}

以上が、Typescriptを利用したPromiseベースのHttpClientの実装サンプルでした。
もしもっと良いやり方があるとか、考慮が足りていないとかあれば、ご指摘お願いします。

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.