#はじめに
連休を機に考える、怠惰な私の自習戦略にて立てた計画に沿った自習の記録です。
前回:年末年始Webアプリ開発自習の記録4: Typescript開発環境準備
#書き直したコード
大したことしていないコードなのに、すごい量が増えました。参考にしたページは最後にまとめます。
ハマったポイントもかなりあって忘れそうなので、ファイル毎に記録していきます。
##Config
idにリクエスト送信情報を紐付けた設定ファイルですが、https
モジュールからrequest
モジュールに鞍替えしたために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#だとこんな感じでイベントを登録して発動できるのですが、あいにくこういう機能はありませんでした。
void RecieverMethod()
{
var EventSender = new EventSender();
EventSender.SomeEvent += SomeEventHandle; //イベントハンドラ追加
EventSender.SomeEvent.Add(SomeEventHandle); //イベントハンドラ追加(旧式)
}
void SomeEventHandle(object sender, SomeEventArgs e)
{
// イベント処理
}
class Sender
{
event EventHandler<SomeEventArgs> SomeEvent;
SenderMethod()
{
SomeEvent?.Invoke();
}
}
ないのでそれっぽいものを作ってみたものがEventDelegaterです。
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では配列や連想配列を駆使するため、下記のように付属のメソッドを組み合わせざるを得ませんでした。
public Remove(method: (sender: object, args: TEventArgs) => void): void {
const target: number = this.subscribers.indexOf(method);
if (target !== -1) {
this.subscribers.splice(target, 1);
}
}
##拡張メソッド
ここまで書いてふと思ったのですが、拡張メソッドを実装できれば上記のように面倒なコードを書かずに一発で削除できるのではないかと思って調べたところ、案の定ありましたので書いてみました。
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のクラスをいちいち作っていくのは面倒でした。仕方ないですけど、もっと良い方法はないですかね。
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) {
}
}
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の可能性があることのほうがおかしい気がしてきました。
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
のサーバーを立てて、リクエストが来たらイベントを発動するクラスです。
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
はコードの存在する場所ではなく、誰がそれを実行したかに依存するみたいです。
constructor(private port: number) {
const onRequestComingHandler = this.OnRequestComing.bind(this);
this.server = http.createServer(onRequestComingHandler);
}
上記のようにbind
メソッドでthis
を明示的に指定することができました。
###response処理のさせかた
response: http.ServerResponse
をイベントに乗せて送っていますが、イベントの受け手側がhttp
モジュールに依存することになるため、本当はやりたくありませんでした。しかし良い感じにラップする方法も思いつかなかったのでこのようになりました。
##Main
これが最後のファイルで、エントリポイントとなるMainです。
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
として認識されました。
import data from "./config.json";
そしてFindOptionsById
メソッドで取り出した要素が普通にキャストされました。Json側でrequest.CoreOptions
のインターフェースにない要素を持っている場合はエラーとなります。動的に生成されるJsonの場合はどうなるんでしょうね。
###タプルみたいな配列
これを本当にタプルと言っていいのか疑問ですが、FindOptionsById
メソッドのように配列に型を書くとタプルが定義できます。
実際はobject
の配列で、トランスコンパイラが型をチェックしているのではないかと思います。
#おわりに
思っていたより時間がかかりましたが、初めてにしてはよく書けたような充実感があります。制約が厳しいので大規模開発には向いていると思いますが、コード量の少ないプロトタイピングにおいてはJavascriptで書いた方が良さそうですね。
これで最初に作りたかったものについては一度切り上げます。次は自習戦略の2番目に作りたかったものに挑戦します。
次回:年末年始Webアプリ開発自習の記録6: Node.js+Electron+Vue.js開発環境準備
#参考にしたページ
- TypeScriptの型入門
- TypeScript プログラミング
- Node.js v11.6.0 Documentation
- How do I remove an array item in TypeScript?
- Array - MDN web docs
- 【Node.js入門】requestモジュールでGET / POST通信する方法!
- HTTPとPOSTとGET
- Handbook - js Studio
- null と undefined の違い
- TypeScript Handbook を読む (13. Modules)
- TypeScript Json Mapper
- TypeScriptのリフレクションでJSONの型変換を自動化する
- How to Import json into TypeScript
- Cannot read property 'fuga' of undefined とは
- TypeScriptのclassをオブジェクトで初期化する
- 【TypeScript】thisの使い方にハマった!thisを保持する3つの方法
- Why is “forEach not a function” for this object?
- 【TypeScript】拡張メソッドの実装(基本型)
- Extending Array in TypeScript
- How to remove an element from an array
- JavaScriptの配列の使い方まとめ。要素の追加,結合,取得,削除。
- Declaration Merging - Typescript
- 【Javascript】演算子をオーバーロードしたい話
- Does typescript have auto-properties yet?
- TypeScriptのDecoratorについて – 公式ドキュメント日本語訳
- Creating a property getter and setter with TypeScript Decorator