年末年始Webアプリ開発自習の記録5: Node.js+TypescriptでHabiticaにPOSTを送るWebアプリケーション作成

#はじめに
**[連休を機に考える、怠惰な私の自習戦略][Plan]**にて立てた計画に沿った自習の記録です。
前回：**[年末年始Webアプリ開発自習の記録4: Typescript開発環境準備][Prev]**

#書き直したコード
大したことしていないコードなのに、すごい量が増えました。参考にしたページは最後にまとめます。
ハマったポイントもかなりあって忘れそうなので、ファイル毎に記録していきます。

##Config
idにリクエスト送信情報を紐付けた設定ファイルですが、```https```モジュールから```request```モジュールに鞍替えしたためにjsonの構造を変える必要がありました。

```json:config.json
[
    {
        "options":{
            "method": "POST",
            "headers":
            {
                "cookie": [""],
                "content-length": "0",
                "x-api-key": "調べていれとく",
                "x-api-user": "調べていれとく",
                "content-type": "application/json"
            }
        },
        "id": "なんかいれとく",
        "url": "https://habitica.com/api/v3/tasks/05084be1-4f85-42a8-90bb-6314361ac128/score/up"
    }
]
```
```request()```関数を使う時にIntelliSenseが引数を教えてくれたので、```uri: string```と```options?: request.CoreOptions```を持つように変更しました。
```request.CoreOptions```型はF12押したらわかりました。ありがたいです。

##EventDelegater
C#だとこんな感じでイベントを登録して発動できるのですが、あいにくこういう機能はありませんでした。

```c#:登録側
void RecieverMethod()
{
    var EventSender = new EventSender();
    EventSender.SomeEvent += SomeEventHandle; //イベントハンドラ追加
    EventSender.SomeEvent.Add(SomeEventHandle); //イベントハンドラ追加（旧式）
}
void SomeEventHandle(object sender, SomeEventArgs e)
{
    // イベント処理
}
```

```c#:発動側
class Sender
{
    event EventHandler<SomeEventArgs> SomeEvent;
    SenderMethod()
    {
        SomeEvent?.Invoke();
    }
}
```
ないのでそれっぽいものを作ってみたものがEventDelegaterです。

```typescript:EventDelegater.ts
export default class EventDelegater<TEventArgs> {

    private subscribers: Array<(sender: object, args: TEventArgs) => void>;

    constructor() {
        this.subscribers = new Array();
    }

    public Add(method: (sender: object, args: TEventArgs) => void): void {
        this.subscribers.push(method);
    }

    public Remove(method: (sender: object, args: TEventArgs) => void): void {
        const target: number = this.subscribers.indexOf(method);
        if (target !== -1) {
            this.subscribers.splice(target, 1);
        }
    }

    public Invoke(sender: object, args: TEventArgs): void {
        if (this.subscribers.length !== 0) {
            this.subscribers.forEach((element) => {
                element(sender, args);
            });
        }
    }
}
```
本家はうまいこと発動側しかInvokeできないようになっているのですが、その仕組みを細部までわかっていない上に、それを調べるのは本筋ではないので、あくまでTypescriptに慣れる範囲で簡易的な実装に留めています。

###関数型
関数型は```(sender: object, args: TEventArgs) => void```のように書けることがわかりました。

###配列の操作
配列の中身を消すのに手こずりました。C#ではコレクションの型が豊富で、適切なコレクションを選択すればリッチな機能で簡単に操作可能でしたが、TypescriptないしJavascriptでは配列や連想配列を駆使するため、下記のように付属のメソッドを組み合わせざるを得ませんでした。

```typescript:この部分
public Remove(method: (sender: object, args: TEventArgs) => void): void {
    const target: number = this.subscribers.indexOf(method);
    if (target !== -1) {
        this.subscribers.splice(target, 1);
    }
}
```

##拡張メソッド
ここまで書いてふと思ったのですが、拡張メソッドを実装できれば上記のように面倒なコードを書かずに一発で削除できるのではないかと思って調べたところ、案の定ありましたので書いてみました。

```typescript:Array.Extended.ts
export { };

declare global {
    // tslint:disable-next-line:interface-name
    interface Array<T> {
        Remove(condition: ((item: T) => boolean)): T[];
        RemoveFirst(item: T): T[];
        RemoveAll(item: T): T[];
    }
}

Array.prototype.Remove = function(condition) {
    const removeList: number[] = new Array();
    for (const i of this) {
        if (condition(i)) {
            removeList.unshift(i);
        }
    }
    removeList.forEach((target) => {
        this.splice(target, 1);
    });
    return this;
};

Array.prototype.RemoveFirst = function(item) {
    const target: number = this.indexOf(item);
    if (target !== -1) {
        this.splice(target, 1);
    }
    return this;
};

Array.prototype.RemoveAll = function(item) {
    let removed: boolean = false;
    do {
        const target: number = this.indexOf(item);
        if (target !== -1) {
            this.splice(target, 1);
            removed = true;
        } else {
            removed = false;
        }
    } while (removed);
    return this;
};
```

```EventDelegater.ts```の冒頭に```import "./Array.Extended";```を追加して使えるようになりました。

最初の```export { };```の意味はよくわかりません、そのうち調べる羽目になるでしょう。
ややハマりかけたのは拡張の実装が```Array<T>.prototype.メソッド名```ではなく```Array.prototype.メソッド名```となることです。つまり中でTが使えません。

###オーバーロード
はじめは```Remove(condition: ((item: T) => boolean)): T[];```と```Remove(item: T): T[];```を作ろうとしていましたが、TSLintに```Remove(condition: ((item: T) => boolean))|T: T[];```とするよう怒られました。
全然意味が違うのに何でだろうと思っていながら渋々従ったところ、実装の```Array.prototype.Remove = function(condition) {...}```にある```condition```が```any```になってしまったことで気がつきました。
Typescriptは静的型付け言語ですが、生成されるJavascriptはそうではありません。引数の型が異なることを知っているのはTypescriptのトランスコンパイラだけであり、Javascriptになってしまったら区別ができないということですね。

##EventArgs
いくらC#の流儀に寄せたとはいえ、EventArgsのクラスをいちいち作っていくのは面倒でした。仕方ないですけど、もっと良い方法はないですかね。

```typescript:GetRequestEventArgs.ts
import http = require("http");
import url = require("url");

export default class GetRequestEventArgs {
    public get Query(): url.UrlWithParsedQuery|null {
        return this.query;
    }
    public get Response(): http.ServerResponse {
        return this.response;
    }
    constructor(private query: url.UrlWithParsedQuery|null, private response: http.ServerResponse) {

    }
}
```

```typescript:PostRequestEventArgs.ts
import http = require("http");

export default class PostRequestEventArgs {
    public get Body(): Buffer {
        return this.body;
    }
    public get Response(): http.ServerResponse {
        return this.response;
    }
    constructor(private body: Buffer, private response: http.ServerResponse) {

    }
}
```

###Nullableな型
C#ではリテラルな型以外はnullの可能性があったので、てっきりTypescriptもそうかなと思ってしまったのですが違いました。
よくよく考えてみると、そもそもそのようにリテラルな型以外はnullの可能性があることのほうがおかしい気がしてきました。

```Typescript:null許容型の例
public get Query(): url.UrlWithParsedQuery|null {
    return this.query;
}
```
上記のように```url.UrlWithParsedQuery|null```と書くことで実現できます。この記法は```null```だけに限らず```undefined```やその他の型も追加できます。
先の拡張メソッドの部分で既に登場してしまいましたが、私がこれを知ったのは本来はここでした。

###型の読み込み
他のクラスの型を参照する方法がわからずにハマりました。```EventDelegater```を最初に書いたので気がつかなかったのですが、ファイルの先頭がいきなりクラスだった場合は同じ場所にあれば参照可能でした。理由はわかりません。
その後上記のクラスらを書いたところ、この後説明する```Receiver```クラスで参照できずに困りました。調べると```namespace```やらなんやら出てきて試してみるも、ことごとくTSLint先生が古いだの非推奨だのとお怒りになられ、ついにわかったのは```export default```という修飾子をつけて```import```することでした。```default```とすると```import```した変数そのままで型が参照可能になることはわかりましたが、```export```の正確な仕様はまだ理解していません。

##Receiver
ポートを指定して```http```のサーバーを立てて、リクエストが来たらイベントを発動するクラスです。

```typescript:Receiver.ts
import http = require("http");
import url = require("url");
import EventDelegater from "./EventDelegater";
import GetRequestEventArgs from "./GetRequestEventArgs";
import PostRequestEventArgs from "./PostRequestEventArgs";

export default class Receiver {

    public get Port(): number {
        return this.port;
    }
    public get GetRequestEvent(): EventDelegater<GetRequestEventArgs> {
        return this.getRequestEvent;
    }
    public get PostRequestEvent(): EventDelegater<PostRequestEventArgs> {
        return this.postRequestEvent;
    }
    public get IsRunning(): boolean {
        return this.isRunning;
    }

    private server: http.Server;
    private getRequestEvent: EventDelegater<GetRequestEventArgs> = new EventDelegater();
    private postRequestEvent: EventDelegater<PostRequestEventArgs> = new EventDelegater();
    private isRunning: boolean = false;

    constructor(private port: number) {
        const onRequestComingHandler = this.OnRequestComing.bind(this);
        this.server = http.createServer(onRequestComingHandler);
    }

    public StartServer(): void {
        if (!this.isRunning) {
            this.isRunning = true;
            this.server.listen(this.port);
        }
    }

    public StopServer(): void {
        if (this.isRunning) {
            this.isRunning = false;
            this.server.close();
        }
    }

    private OnRequestComing(request: http.IncomingMessage, response: http.ServerResponse): void {
        if (request.method === "GET") {
            let urlQueries = null;
            if (request.url !== undefined) {
                urlQueries = url.parse(request.url, true);
            }
            this.GetRequestEvent.Invoke(this, new GetRequestEventArgs(urlQueries, response));
        }

        if (request.method === "POST") {
            const chunks: Uint8Array[] = new Array();
            request.on("data", (chunk) => {
                chunks.push(chunk);
            });

            request.on("end", () => {
                const body = Buffer.concat(chunks);
                this.PostRequestEvent.Invoke(this, new PostRequestEventArgs(body, response));
            });
        }
    }
}
```

###thisのスコープ
リクエストが来た時に実行される```OnRequestComing```メソッドですが、この中の```this.GetRequestEvent.Invoke(this, new GetRequestEventArgs(urlQueries, response));```を実行したときに```Cannot read property 'Invoke' of undefined```と出て「は？」となりました。
このメソッドは非同期なのでもしやと思い、Debugしてわかったのは```OnRequestComing```メソッド内でのthisはどうやらインスタンス自身とは別物であるということでした。Javascriptにおいて```this```はコードの存在する場所ではなく、誰がそれを実行したかに依存するみたいです。

```typescript:bindする
constructor(private port: number) {
    const onRequestComingHandler = this.OnRequestComing.bind(this);
    this.server = http.createServer(onRequestComingHandler);
}
```
上記のように```bind```メソッドで```this```を明示的に指定することができました。

###response処理のさせかた
```response: http.ServerResponse```をイベントに乗せて送っていますが、イベントの受け手側が```http```モジュールに依存することになるため、本当はやりたくありませんでした。しかし良い感じにラップする方法も思いつかなかったのでこのようになりました。

##Main
これが最後のファイルで、エントリポイントとなるMainです。

```typescript:main.ts
import http = require("http");
import request = require("request");
import data from "./config.json";
import GetRequestEventArgs from "./GetRequestEventArgs";
import Receiver from "./Receiver";

const port = 3000;
const receiver: Receiver = new Receiver(port);
receiver.GetRequestEvent.Add(GetRequestEventHandler);
receiver.StartServer();

// tslint:disable-next-line:variable-name
function GetRequestEventHandler(_sender: object, eventArgs: GetRequestEventArgs): void {
    if (eventArgs.Query !== null) {
        const queries = eventArgs.Query;
        const result: [request.CoreOptions, string]|null = FindOptionsById(queries.query.id as string);

        if (result !== null) {
            const options: request.CoreOptions = result[0];
            const requestUrl: string = result[1];
            request(requestUrl, options, (error: any, response: request.Response, body: any) => {
                if (error === undefined) {
                    console.log(error);
                } else {
                    if (response.headers["set-cookie"] !== undefined) {
                        UpdateCookieById(queries.query.id as string, response.headers["set-cookie"]);
                    }
                    eventArgs.Response.writeHead(response.statusCode, response.headers);
                    eventArgs.Response.write(body);
                }
                eventArgs.Response.end();
            });
        } else {
            SendResponseForError(eventArgs.Response);
        }

    } else {
        SendResponseForError(eventArgs.Response);
    }
}

function SendResponseForError(response: http.ServerResponse): void {
    response.writeHead(400, {"Content-Type": "application/json"});
    response.write(JSON.stringify({
        error: "Bad Request",
        message: "Wrong request.",
        success: false,
    }));
    response.end();
}

function FindOptionsById(id: string): [request.CoreOptions, string]|null {
    let options: request.CoreOptions|null = null;
    let resultUrl: string|null = null;
    data.forEach((element) => {
        if (id === element.id) {
            options = element.options;
            resultUrl = element.url;
        }
    });
    if (options !== null && resultUrl !== null) {
        return [options, resultUrl];
    }
    return null;
}

function UpdateCookieById(id: string, cookie: string[]): void {
    data.forEach((element) => {
        if (id === element.id) {
            element.options.headers.cookie = cookie;
        }
    });
}
```

###Jsonのロード
どうしたら```config.json```の、それも中にある```options```を都合良く```request.CoreOptions```とできるかにかなり悩みました。デシリアライズだのキャスタブルだの調べましたが結果はシンプルでした。```resolveJsonModule```オプションを```tsonfig.json```に追加することで、下記の1行で```data```として認識されました。

```typescript
import data from "./config.json";
```

そして```FindOptionsById```メソッドで取り出した要素が普通にキャストされました。Json側で```request.CoreOptions```のインターフェースにない要素を持っている場合はエラーとなります。動的に生成されるJsonの場合はどうなるんでしょうね。

###タプルみたいな配列
これを本当にタプルと言っていいのか疑問ですが、```FindOptionsById```メソッドのように配列に型を書くとタプルが定義できます。
実際は```object```の配列で、トランスコンパイラが型をチェックしているのではないかと思います。

#おわりに
思っていたより時間がかかりましたが、初めてにしてはよく書けたような充実感があります。制約が厳しいので大規模開発には向いていると思いますが、コード量の少ないプロトタイピングにおいてはJavascriptで書いた方が良さそうですね。
これで最初に作りたかったものについては一度切り上げます。次は自習戦略の2番目に作りたかったものに挑戦します。
次回：**[年末年始Webアプリ開発自習の記録6: Node.js+Electron+Vue.js開発環境準備][Next]**

#参考にしたページ
- **[TypeScriptの型入門](https://qiita.com/uhyo/items/e2fdef2d3236b9bfe74a)**
- **[TypeScript プログラミング](https://docs.solab.jp/typescript/)**
- **[Node.js v11.6.0 Documentation](https://nodejs.org/api/events.html)**
- **[How do I remove an array item in TypeScript?](https://stackoverflow.com/questions/15292278/how-do-i-remove-an-array-item-in-typescript)**
- **[Array - MDN web docs](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array)**
- **[【Node.js入門】requestモジュールでGET / POST通信する方法！](https://www.sejuku.net/blog/73698)**
- **[HTTPとPOSTとGET](https://qiita.com/Sekky0905/items/dff3d0da059d6f5bfabf)**
- **[Handbook - js Studio](http://js.studio-kingdom.com/typescript/handbook/)**
- **[null と undefined の違い](https://qiita.com/shuhei/items/199281e07925d24059d5)**
- **[TypeScript Handbook を読む (13. Modules)](https://qiita.com/murank/items/0fc4e059be8ea5e1c274)**
- **[TypeScript Json Mapper](http://cloudmark.github.io/Json-Mapping/)**
- **[TypeScriptのリフレクションでJSONの型変換を自動化する](https://qiita.com/bitrinjani/items/d60bdac10e5ced126d1a)**
- **[How to Import json into TypeScript](https://hackernoon.com/import-json-into-typescript-8d465beded79)**
- **[Cannot read property 'fuga' of undefined　とは](https://qiita.com/nakamaru/items/3e11bb5b0d586a6d5545)**
- **[TypeScriptのclassをオブジェクトで初期化する](https://qiita.com/Tsuyoshi84/items/e74109e2ccc0f4e625aa)**
- **[【TypeScript】thisの使い方にハマった！thisを保持する3つの方法](https://kuroeveryday.blogspot.com/2015/04/this.html)**
- **[Why is “forEach not a function” for this object?](https://stackoverflow.com/questions/31096596/why-is-foreach-not-a-function-for-this-object/31096661)**
- **[【TypeScript】拡張メソッドの実装(基本型)](https://kakkoyakakko2.hatenablog.com/entry/2018/06/12/003000)**
- **[Extending Array in TypeScript](https://stackoverflow.com/questions/12802383/extending-array-in-typescript)**
- **[How to remove an element from an array](https://www.simonewebdesign.it/how-to-remove-element-from-array/)**
- **[JavaScriptの配列の使い方まとめ。要素の追加,結合,取得,削除。](https://qiita.com/takeharu/items/d75f96f81ff83680013f)**
- **[Declaration Merging - Typescript](https://www.typescriptlang.org/docs/handbook/declaration-merging.html)**
- **[【Javascript】演算子をオーバーロードしたい話](https://qiita.com/qr_gho_staff/items/5ef26b0a6a353bfd4b47)**
- **[Does typescript have auto-properties yet?](https://stackoverflow.com/questions/47731392/does-typescript-have-auto-properties-yet)**
- **[TypeScriptのDecoratorについて – 公式ドキュメント日本語訳](https://mae.chab.in/archives/59845)**
- **[Creating a property getter and setter with TypeScript Decorator](https://stackoverflow.com/questions/49877756/creating-a-property-getter-and-setter-with-typescript-decorator)**

[Plan]:https://qiita.com/dmorita/items/7a85ee9c2de506c27502
[Prev]:https://qiita.com/dmorita/items/36bafecd1763328a6d59
[Next]:https://qiita.com/dmorita/items/