フロントエンドのJavaScriptで、バックエンドのAPIからの結果をモデルに入れるプラクティスについてまとめます (より良い方法があったら教えてください)。
まず、以下のようなレスポンスがAPIからあると想定します。
{ "id": 1, "username": "hirokiky", "last_name": "Kiyohara", "first_name": "Hiroki" }
データをそのまま使いまわすのはイマイチ
このデータを受け取ったままの状態でJavaScript上で使うのはイマイチです。
import axios from 'axios' async function initialize () { let res = await axios.get("/user") let user = res.data ... // 以降、userを使いまわす }
理由としてはいくつかあります。
- APIが snake_case で返すと、JavaScript内でも
user.last_name
とかく必要があるuser.lastName
のように camelCase にしたい
- APIの実装が変わるとアプリケーションの深い部分で影響を受けることがある
- データを受け取った段階で検知したい
user
などのデータに振る舞いを持たせられないuser.fullName
のような処理を持たせたい
モデルを定義する
モデルを定義しておくと良いでしょう。
一旦、このように定義しました(models.js
に書く想定です)。
export class User { constructor (id, username, lastName="", firstName="") { this.id = id this.username = username this.lastName = lastName this.firstName = firstName } get fullName () { return this.lastName + this.firstName } }
これをAPIからデータを受け取ったときに使います。
ついでに、APIアクセスをまとめたJavaScriptファイルにまとめておいたほうが良いです
(api.js
に書く想定です。バックエンドのAPIの名前が明確ならその名前でも良いでしょう)。
import axios from 'axios' import { User } from '@/models' export async function getAuth () { let res = await axios.get("/user") return new User( id=res.data.id, username=res.data.username, lastName=res.data.last_name, firstName=res.data.first_name ) }
元の処理では上記の getAuth
を使うと良いです。
import { getAuth } from '@/api' async function initialize () { let user = await getAuth() ... // 以下 user を使いまわす }
ここで少し面倒な点がいくつかあります
簡単にモデルを書けるようにする
実験的な実装なので信用しないでください
以下のようにモデルの基底クラスを定義しておきます。
class Model { static fields () { return {} } constructor(options={}) { var v for (let [key, value] of Object.entries(this.constructor.fields())) { if (options.hasOwnProperty(key)) { v = options[key] } else { v = value } this[key] = v } } }
各モデルのクラスでは以下のように使います。
fields
メソッドには各フィールドの定義を書きます。
export class User { static fields () { return { id: null, username: "", lastName: "", firstName: "" } } }
fields
からは {フィールド名: デフォルト値}
というオブジェクトを返します。
fields
がstaticメソッドなのは、ES6にはクラス変数が無いから、仕方なくこう定義しています。
User
は以下のように作ります。
new User({id: 1, username: "hirokiky"})
APIのデータからモデルを作る
ここで、 Model
にメソッドを追加して、APIから受け取ったデータを使ってインスタンスを簡単に作れるようにしましょう。
function makeSnake (s) { return s.replace(/(?:^|\.?)([A-Z])/g, function (x,y){return "_" + y.toLowerCase()}).replace(/^_/, "") } function copyFields (fromObj, toObj, fields, fromSnake=false, toSnake=false) { for (var field of fields) { var value, fromFieldName if (fromSnake) { fromFieldName = makeSnake(field) } else { fromFieldName = field } if (fromObj.hasOwnProperty(fromFieldName)) { value = fromObj[fromFieldName] } else { value = null } if (toSnake) { toObj[makeSnake(field)] = value } else { toObj[field] = value } } return toObj } class Model { // ... static fromAPIData (options, initial={}) { return new this(copyFields(options, initial, Object.keys(this.fields()), true, false)) } }
こうしておくと、以下のように snake_case でUserのインスタンスを作成できます。
User.fromAPIData({id: 1, username: "hirokiky", 'last_name': "Kiyohara"})
API呼び出し時にも簡単にインスタンスを作れるようになりました。
import axios from 'axios' import { User } from '@/models' export async function getAuth () { let res = await axios.get("/user") return User.fromAPIData(res.data) }
まぁ、 getAuth
関数内で snake_case から camelCase に変換して new User(toCamelCase(res.data))
としたほうが良いかもしれませんが。
まとめ
- フロントエンドのJavaScriptを書くときもモデルを定義したほうが良いよ
- モデルの定義を毎度書くのは面倒だから、楽できると良いよね
今後、各ModelクラスをJsonSchemaから定義したり、バリデーションを追加できるようにすると良さそうです。 でもそこまでいくとライブラリーがありそうなので探すと良さそうです。良いのがあったら教えてください。