PCがあれば何でもできる!

へっぽこアラサープログラマーが、覚えたての知識を得意げにお届けします

【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なクローンでもない、シンプルな処理なので、重くはないはず。