【TypeScript】express4.xとangular1.5間で、オブジェクトをシリアライズ化して送受信
node + express4.xでREST APIを作り、angular1.5から利用する際、 お互いにjavascriptなのに、送受信の度にjsonで表現できないオブジェクトを、元の状態に戻すのがめんどくさい。
例えばmoment.jsを利用して日付を送る場合、
const reqData = { date: moment([2016,1,1]), };
↑のようなオブジェクトを送ろうと思っても、jsonでは
{ "date": "2016-01-01T00:00:000" }
としか表現できない。なのでjsonを受け取った側でmomentのオブジェクトに戻したければ、
resData.date = moment(resData.date);
こんな感じにしないといけない。
これが、
- ブラウザのリクエストをサーバーが受け取った際
- サーバーのレスポンスをブラウザが受け取った際
の2箇所で発生するため、サーバー側とブラウザ側に同じような処理を書いているうちに、だんだん馬鹿らしくなってくる。
そこで、うまく通信をシリアライズ化して、面倒を減らせないかなという目論見のもと、↓のようなコードを書いてみた。
// serializer.ts 'use strict'; import * as _ from 'lodash'; import * as moment from 'moment'; /** シリアライズ済オブジェクトインターフェース */ interface SerializedObject { $serialize: { name: string; value: any; } } /** 変換定義インターフェース */ interface Definition { /** シリアライズ化対象のオブジェクトか判定 */ target: (v: any) => boolean; /** シリアライズ化処理 */ serialize: (v) => string; /** デシリアライズ化処理 */ deserialize: (v: string) => any; } /** 変換定義:ここに定義した型のみ変換 */ const definitions: {[name: string]: Definition} = { moment: { target: (v: moment.Moment) => { return moment.isMoment(v); }, serialize: (v: moment.Moment) => { return v.toISOString(); }, deserialize: (v: string) => { return moment(v); }, }, Date: { target: (v: Date) => { return v instanceof Date; }, serialize: (v: Date) => { return v.toISOString(); }, deserialize: (v: string) => { return new Date(v); }, } }; /** JSON.stringify用のreplacer */ export function replacer(key: string, obj: any) { if (typeof obj === 'undefined' || obj === null) { return obj; } if (Array.isArray(obj)) { return (<any[]>obj).map((v) => { return replaceValue(v); }); } if (typeof obj === 'object') { return _.fromPairs(Object.keys(obj).map((k) => { return [k, replaceValue(obj[k])]; })); } return obj; } function replaceValue(v: any) { for (let name of Object.keys(definitions)) { const d = definitions[name]; if (d.target(v)) { const sobj: SerializedObject = <any>{}; sobj.$serialize = { name, value: d.serialize(v) }; return sobj; } } return v; } /** JSON.parse用のreviver */ export function reviver(key: string, obj: any) { if (typeof obj === 'undefined' || obj === null) { return obj; } const sobj = <SerializedObject>obj; if (sobj.$serialize) { return definitions[sobj.$serialize.name].deserialize(sobj.$serialize.value); } return obj; } /** オブジェクトをシリアライズ化 */ export function serialize(obj: any) { return JSON.stringify(obj, replacer); }; /** jsonをデシリアライズ化 */ export function deserialize(json: string) { return JSON.parse(json, reviver); }
やっていることとしては、definitionsに定義された、オブジェクトが見つかると、特殊な記法に変換する。
例えば
import * as serializer from './serializer'; const reqData = { date: moment([2016,1,1]), number: 1 }; console.log(serializer.serialize(reqData));
だと、こんな感じの出力になる。
{ "date": { "$serialize": { "name": "moment", "value": "2016-01-01T00:00:000Z" } }, "number": 1 }
後は、$serializeというキーを探して、定義をもとに変換しなおしているという流れ。
ここまでできたら、expressとangularそれぞれに、このシリアライズ処理をやってもらうだけ。
angular側はこんな感じ。 transformRequestとtransformResponseを使って、シリアライズ処理を入れる。 毎回指定するのが面倒なら、defaultのtransformに設定してもいいけど、他サーバーとの通信も考えると、ラップするような形がいいかも。
$http.post<any>(url, data, { transformRequest: (d: any) => { return serializer.serialize(d); }, transformResponse: (d: any) => { return serializer.deserialize(d); }, responseType: 'text', }).then();
express側はこんな感じ。 json変換時のreplacerとreviverを指定できる。
const app = express(); // レスポンスのjson化のreplacerを指定 app.set('json replacer', serializer.replacer); // リクエストをjson復元のreviverを指定 app.use(bodyParser.urlencoded({ extended: true })); app.use(bodyParser.json({ reviver: serializer.reviver }));
これで後は、definitionsに書いているオブジェクトに関しては、何も考えないで受け渡しができる。
deepなクローンでもない、シンプルな処理なので、重くはないはず。