フロントエンドの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
...
}
理由としてはいくつかあります。
- 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()
...
}
ここで少し面倒な点がいくつかあります
new User(...)
時に、APIからのレスポンスを1つ1つ入れるのが面倒
constructor
の定義が面倒くさい
簡単にモデルを書けるようにする
実験的な実装なので信用しないでください
以下のようにモデルの基底クラスを定義しておきます。
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から定義したり、バリデーションを追加できるようにすると良さそうです。
でもそこまでいくとライブラリーがありそうなので探すと良さそうです。良いのがあったら教えてください。
他のオススメ記事
blog.hirokiky.org
blog.hirokiky.org