Make組ブログ

Python、Webサービスや製品開発、ライブラリー開発についてhirokikyが書きます

highlight.jsで行ごとに区切りつつハイライトする

highlight.js というコードハイライトをするJavaScriptのライブラリーがあります。

highlightjs.org

このライブラリーで、「行ごと」にHTML要素を分割しつつハイライトする方法を説明します。

highilght.jsの基本的な使い方をおさらい

highlight.jsは基本的に関数を呼ぶだけでコードハイライトができるので便利です。 以下の例では "# Heading 1" という文字列をMarkdownとしてハイライトしています。

import hljs from 'highlight.js'

let result = hljs.highlight("markdown", "# Heading1")
console.log(result.value)  // '<span class="hljs-section"># Heading 1</span>'

highlight.js は '<span class="hljs-section"># Heading 1</span>' のようなハイライト用のHTMLタグが埋められた結果を返します。 .hljs-section クラスにCSSを当てることでハイライトができるというものです。

失敗例: 行を分けてからハイライトするとうまくいかない

画面の仕様により、表示するハイライト済みのコードを行ごとに区切りたいことがあります。 例えば以下のように、行番号を表示しつつコードを表示したいときなどがあります。

<table>
  <tr>
    <td>1</td>
    <td><span class="hljs-section"># Heading1</span></td>
  </tr>
  <tr>
    <td>2</td>
    <td>This is body</td>
  </tr>
</table>

もちろん行番号を表示するコードの実装方法も様々ですが、画面や動作の都合上 <table> で実装するとします。 このとき、肝心のコードの内容は # Heading 1This is body に行ごとに区切る必要があります。

以下のように行ごとにハイライトするのは良くありません。

import hljs from 'highlight.js'

let body = `# Heading 1
This is body

\`\`\`python
import this
\`\`\`
`

for (let row of body.split("\n")) {
  let result = hljs.highlight("markdown", row)
  console.log(result.value)
}

理由は、複数行にまたがるシンタックスMarkdownならコードブロックなど)に対応できないからです。 上記の例だと```pythonimport this 、 ``` が別々に解釈されるので、サブブロック(import this)のハイライトが効かなくなります。

回答: 状態を共有しながらハイライトする

highlight.jsの .highilght(...) 関数は第4引数に状態を渡せます。 ハイライトした結果の状態を引き継いで渡すことで、複数行にまたがるシンタックスでもうまく解釈されます。

function lineByLineHighilght (body) {
  let state = null
  return body.split("\n").map(function (row) {
    let result = hljs.highlight('markdown', row, true, state);
    state = result.top  // result.topの状態を次に受け渡す
    return result.value + "<br/>"
  })
}

こうすることで行ごとに区切りつつ、行ごとにハイライトが効くようになります (行ごとに毎度 <span class="...">...</span> は正しく閉じられます)。

8月23日のAWSの大規模障害でMultiAZでもALB(ELB)が特定条件で500エラーを返すことがあったという話

このブログ記事で 「MultiAZ」にしていたら何事も全て大丈夫という認識を変えられると嬉しいです (当該の時点で障害起こした人はちゃんとMultiAZにしてなかったんでしょ?という人の認識も変えられると嬉しいです)。

MultiAZにしておくことは基本 です。 その上でも、 安心しきらずに監視は必要 という話をしています。

  • MultiAZ構成にしておきましょう
  • そのうえで監視、検知、トレーサビリティを大切にしましょう

MultiAZ要らないという見当外れの解釈はしないでください (一部、間違えた解釈をしてるコメントも見受けられましたが、大いに違います)。

前提

2019-08-23、AWSで大規模な障害が起こりました。 障害の一般的な内容は以下のとおりです。

私は、AWSの障害については仕方のないものだと思っています。 このブログ記事はAWSの障害や対応について論じたり、補償などを求めるものではありません。誤解なきようお願いします。

8月29日追記: AWSからこの記事での報告と一致する説明が追記されました

www.publickey1.jp

tech.nikkeibp.co.jp

以前のレポートではALBについての説明などありませんでしたが、8月29日時点で追記が確認されました。 やはり、他のマネージドサービスでも影響があり、 ALBでも一部の構成の場合エラーが発生していたと説明されています 。 その構成の場合はMultiAZ構成でも障害の影響を受けていたということです。

ALB(ELB)がたまに500を返すようになる

23日午後6時30分ごろから、ロードバランサーが500を返すようになりました。 障害が主に発生していたのは午後1時か、2時ごろだったので、後になってからALBが500を返すようになったという状態です。

  • ALB自体が500を返す(バックエンドからの500ではない)
  • バックエンドのサーバーは異常なし
  • ブラウザーでアクセスしても問題ないが、 特定の別サーバーからアクセスするとたまに500になる

昼過ぎのころもインスタンスが勝手に死んだりしていましたが、分散した構成でしたので大規模な問題にはなりませんでした。 「特定のAvailabilityZoneで障害なんだな、MultiAZにしていて良かった」と僕はそのときは思っていました。

が、そのあとロードバランサーが突然500を返すようになります。 最初はアプリの問題かと色々調べたのですが、結局アプリサーバーは正常に稼働しており、ロードバランサー自体が500を返していると特定できました。

後になっての知識ですが、同時刻に冷却システムの復旧とインスタンスの復旧、一部のインスタンスで(ハードウェアの限界による)リタイアがあったようなので、その当たりが関係しているのかもしれません。

ALBのデプロイをするAZで、特定のAZを使わなくすると解決

ALBを利用させているAZから、特定のAZを消すと問題なく動作するようになりました。 ALBの設定で配置する対象のサブネットを変更しました(VPCのサブネットはAvailabilityZoneに紐付いています)。

もちろん「特定のAZで障害があった」とは私も認識していましたが、ALBの設定(ALBをデプロイするAZの設定)まで変えようとは思っていませんでした。

f:id:hirokiky:20190823200044p:plain
事態の収束を告げるグラフ

これはALB自体のモニタリングのグラフです。ロードバランサー自体が返している5xxエラーの数です。ロードバランサーは通常、正常に稼働しているインスタンスが1つもない場合などに5xx系のエラーを返しますが、このときはALB以下には正常なインスタンスが紐付いていました。自動で治る雰囲気が無かったのでALBのAZの設定を変えると、グラフ右端のように問題は解決しました。

f:id:hirokiky:20190826134101p:plain
全体像(時刻はUTC

今回はすぐに検知して、調査、対応できましたが、 大きな問題でなく「静かに少し死ぬ」、そして影響範囲は大きいというのは大変でした。検知やトレーサビリティーの大切さを思い知りました

今回の問題について

今回の障害について、「MultiAZ構成だったから大丈夫だった」というような声をSNSなどでも見聞きしましたが、今回発生したすべての問題をそれだけで解決できるとは思いません(障害を起こしたサービスに対して安易な批判をするのは控えたほうが良いでしょう)。 MultiAZは当たり前として、監視や復旧のための準備が必要 です(このALBの問題もそうですし、RDSの応答がなくなる問題や、コンソール画面を操作できなくなる問題、AMIがビルドできない問題もあったと話に聞いています)。 AWSのマネージドサービス全体に関わる問題ですし、システム構成、アプリケーションの作り、時の運に影響して問題は発生し得る状況でした。

障害がどう影響したかなどは詳しくは分かりません。もちろんアプリサーバー側が古いコネクションを持っており問題を抱えたALBにアクセスしていたのか?なども考えられますが、ALBの設定を変更すればすぐに治ったので決定的にそれが原因だとも言えない状態です。

ともかく安易に「コレをしていたから大丈夫」、「コレをしないのが悪いんだ」と思わずに、今一度問題が起こっていないか、今後何をできるかを考えたほうが良いでしょう。 特定のAZでインスタンスが落ちちゃうくらいならMultiAZで対応できますが、ロードバランサーで問題が発生する & 自動でキレイに元通りにならないと対応は簡単ではなくなります。

検知の重要性

インフラを信じる、お金を使うということは大事ですが、今回のようにクラウドサービス自体が予期しない動作になることもあると勉強になりました。

「MultiAZだから大丈夫」というのも、そもそもクラウドサービス自体がすべて正常に動作しているから言えることです。予期しない状況ですべてのシステム、アプリがうまく動作するとも限りません。足がかりにしている地盤そのものに問題が発生する場合どう検知して対応したら良いでしょうか?

インフラ環境を信じすぎず、自分のアプリも信じすぎず、問題が発生する前提でちゃんと監視、検知、トレースすることは重要だと勉強になりました。

関連する他の記事

似た事象にあったBoardさんのとても誠実でまとまった報告

the-board.jp

今回の私が取った対応の具体的な操作方法

dev.classmethod.jp

他にオススメの記事

blog.hirokiky.org

Djangoでdjango-hijack-adminをカスタムユーザーモデルと使うときのハマりポイント

django-hijack-adminDjangoのカスタムユーザーを併せて使うとき、Adminサイトへの登録でハマるので書いておきます。

django-hijackとは、Admin画面から別ユーザーのセッションを乗っ取れるライブラリーです。 個別のユーザーで問題が発生しているときに状態を確認したり、ローカルで動作確認するときに複数アカウントを切り替えやすくなったりで便利になります。

github.com

django-hijack-adminはdjango-hijackの機能をAdmin画面から使いやすくしてくれるものです。 かなり小さい実装なので、ライブラリー自体は参考実装にして自分で書いてしまっても十分なものです。

カスタムユーザーをAdminに登録するときにハマる

カスタムユーザーモデルを作って拡張したModelAdminを登録しようとするときに、django-hijack-adminをINSTALLED_APPに入れているとハマります。 具体的にはカスタムユーザーモデル用のModelAdminの内容がAdmin画面に反映されなくなります。

from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as AuthAdmin

from . import models


@admin.register(models.User)
class UserAdmin(AuthAdmin):
    # デフォルトのUserAdminに独自の拡張をする
    fieldsets = AuthAdmin.fieldsets + (("Additional", {"fields": ("icon",)}),)

ここでは fieldsetsicon というフィールドを追加していますが、Admin画面に反映されなくなります。

django-hijack-adminは独自のUserAdminを登録する

原因は django-hijack-admin も独自のUserAdminを登録しようとすることです。 django-hijack-adminは「Hijack」するためのボタンを追加するために、 auth.admin.UserAdmin を拡張したクラスをAdminに登録します。 Userモデルに対応するModelAdminをunregisterして登録するので、カスタムユーザー用に自身のUserAdminを登録しても無駄になります。

if hijack_admin_settings.HIJACK_REGISTER_ADMIN:
    UserModel = get_user_model()
    admin.site.unregister(UserModel)
    admin.site.register(UserModel, HijackUserAdmin)

github.com

解決策

以下の用に3つ設定します

  1. settingsに HIJACK_REGISTER_ADMIN = False を設定する
  2. UserAdminでHijackUserAdminMixin をMixinする
  3. list_displayに 'hijack_field' を足す

このようになります。

from hijack_admin.admin import HijackUserAdminMixin


@admin.register(models.User)
class UserAdmin(AuthAdmin):
    ...
    list_display = AuthAdmin.list_display + ('hijack_field',)

カスタムユーザーを使うときの設定方法は READMEに書かれています 。 ですがdjango-hijack-adminがデフォルトで、元の動作を音もなく書き換えるので少し気づきにくいと思います。

もしdjango-hijack-adminを使っていて「おかしいな」というときは疑ってみてください。

他のおすすめ記事

blog.hirokiky.org

Vue.js+VueRouterでページの離脱、再読込、別ルートへの移動時に警告を表示する

ページの離脱時に警告を表示するには、 beforeunload イベントを使えば簡単にできます。 ですが、Vue.jsでVueRouterを使っている場合、ページの移動で beforeunload イベントは発生しません。 理由はブラウザーの画面自体が切り替わっていないからです(ページの遷移はVueRouter, つまりJavaScriptが同一の画面内で制御している)。

WordPressやDropboxPaperも、ページ離脱時には confirm() やモーダル表示を使ってページ遷移時に警告を表示しています (もちろんbeforeunload時の警告も併用しています)。

Vue.js + VueRouterでページ離脱、再読込時に警告を表示する

以下のように beforeunload を使えば良いです(クロスブラウザー対応どうこうはうまく書き換えてください)。

  methods: {
    handler (event) {
      event.returnValue = "Data you've inputted won't be synced"
    }
  },
  created () {
    window.addEventListener("beforeunload", this.handler)
  },
  destroyed () {
    window.removeEventListener("beforeunload", this.handler)
  }

vue-prevent-unload というライブラリーもありますが、小さすぎる実装なので参考にするだけで良いでしょう。 必要なコンポーネントに処理を書くか、独自のvue-prevent-unloadのようなコンポーネントをプロジェクト以下に置いておけば十分です。 例えば以下のようなVueコンポーネントStopUnload.js としておくなどです(あくまで参考実装です)。

export default {
  name: 'StopUnload',
  props: ["stop"],
  render: () => null,
  methods: {
    handler (event) {
      if (this.stop) {
        event.returnValue = "Data you've inputted won't be synced"
      }
    }
  },
  created () {
    window.addEventListener("beforeunload", this.handler)
  },
  destroyed () {
    window.removeEventListener("beforeunload", this.handler)
  }
}

ページ遷移時にも警告をする

ただこれだけではVueRouterによるページの遷移時に警告が表示されません。 以下のようにVueコンポーネント内に beforeRouteLeave を書けばVueRouterでのページ移動を検知できます。

beforeRouteLeave (to, from, next) {
  let answer = window.confirm("Data you've inputted won't be synced, OK?")
  if (answer) {
    next()
  } else {
    next(false)
  }
}

router.vuejs.org

ただしこの場合、VueRouterの routes に登録されているVueコンポーネントに上記の処理を書いてください(Viewとしてのコンポーネントに書く)。 VueRouterに直接関係しないコンポーネントでは beforeRouteLeave は呼び出されません。なので上記で例示したStopUnoadコンポーネントbeforeRouteLeave を書いても機能しません。

windowのbeforeunloadと、beforeRouteLeaveを使うことで、Vue.js+VueRouterでSPAを作っているときもページの離脱時に警告を表示できます。

他のオススメ記事

blog.hirokiky.org

Vue.jsでAPIにないフィールドはモデルにも作らないプラクティス - モデルとViewModelの区別の仕方

前回の記事はこちらです。先に読まれておくことをオススメします。

blog.hirokiky.org

モデルに置く値、ViewModelに置く値の区別をつけよう

Vue.jsで開発するときに、モデルに置くべき値とViewModelに置くべき値を区別できればかなりキレイに設計できます。 区別の判断は少し難しいですが、「バックエンドのAPIで保存・読み込みする値のみモデルで管理する」と考えてみると勘所が分かりやすいです。 バックエンドのAPIなどが無い場合は、例えばlocalStorageに保存する値をモデルとすると良いでしょう。

前提: 記事の読み込み、保存する画面で説明します

説明のためのプロジェクトの説明をします。

記事(Post)の編集をする画面をVue.jsで作り、バックエンドのAPIからデータを取得、保存するとしましょう。 ここではAPIから以下のレスポンスがあると想定します。

{
    "id": 1,
    "title": "タイトル",
    "body": "本文"
}

この場合、APIのレスポンスはモデルで管理すべきです(参考: JavaScript (ES6) でAPIから受け取ったデータをモデルに入れるプラクティス - Make組ブログ

export class Post {
  constructor (id, title, body="") {
    this.id = id
    this.title = title
    this.body = body
  }
}

読み込み、保存する処理は以下のようにします

import axios from 'axios'

import { Post } from './api'


export async function getPost (id) {
  let res = axios.get(`/posts/${id}/`)
  return new Post(res.data.id, res.data.title, res.data.body)
}


export async function patchPost (post) {
  let res = axios.patch(`/posts/${post.id}/`, {body: post.body})
  post.body = res.data.body
  return post}

単純にVueコンポーネントを作る

さて、ここでどのようにVueコンポーネントを書くべきでしょうか? 一番単純に作ると以下のようになります。

<template>
  <div v-if="post">
    <textarea v-model="post.body"></textarea>
    <button @click="save">Save</button>
  </div>
</template>

<script>
import * as api from './api'


export default {
  name: 'Post',
  data () { return {
    post: null
  } },
  methods: {
    async save () {
      await api.patchPost(this.post)
    }
  },
  async mounted () {
    this.post = await api.getPost(1)
  }
}
</script>

モデルに書く?

ここで「値が変更されていないときは Save ボタンを disabled にしたい」としましょう。 この場合、値が変更されたことを検知するのはどこが良いでしょうか?

せっかくJavaScriptclass が使えるので、以下のように getter, setter で書くとカッコイイ気がします。 が、結論から言うとあまりオススメしません。

export class Post {
  constructor (id, title, body="") {
    this.id = id
    this.title = title
    this._body = body
    this.changed = false
  }

  get body  () {
    return this._body
  }

  set body (value) {
    this._body = value
    this.changed = true 
  }
}

この場合、Vueコンポーネントは以下のようになるでしょう。 changed = false にする処理や、 :disabled="!post.changed" が足されています。

<template>
  <div v-if="post">
    <textarea v-model="post.body"></textarea>
    <button @click="save" :disabled="!post.changed">Save</button>
  </div>
</template>

<script>
import * as api from './api'


export default {
  name: 'Post',
  data () { return {
    post: null
  } },
  methods: {
    async save () {
      await api.patchPost(this.post)
      post.changed = false
    }
  },
  async mounted () {
    this.post = await api.getPost(1)
  }
}
</script>

この程度ではあまり問題になりませんが、 1つのコンポーネントでしか使わないような処理や状態をモデルに入れるとモデルが肥大化していきます 。 変更の検知をする部分はVueコンポーネントに書くほうが良いです。

changedAPI(データの読み込み・保存)には関係しない。モデルに書くべきでは無いかも?」と考えてみてください。

Vueコンポーネントに書こう

モデルの定義は元に戻して(getter, setterを消して)、Vueコンポーネントを以下のように書いてみましょう。 「変更されたかどうか」という状態をVueコンポーネントに持つようになっています。

<template>
  <div v-if="post">
    <textarea v-model="body"></textarea>
    <button @click="save" :disabled="!changed">Save</button>
  </div>
</template>

<script>
import * as api from './api'


export default {
  name: 'Post',
  data () { return {
    post: null,
    body: ""
  } },
  methods: {
    async save () {
      this.post.body = body
      await api.patchPost(this.post)
    }
  },
  computed: {
    changed () {
      return this.body != this.post.body
    }
  },
  async mounted () {
    this.post = await api.getPost(1)
    this.body = this.post.body
  }
}
</script>

textarea ではVueコンポーネントbody を編集するようにして、 post.body は触れないようにしています。 「変更されたかどうか」は changed というcomputedで計算しています。

changeddata で管理するフラグにしても良いです。

  methods: {
    update (value) {
      this.body = value
      this.changed = true
    }
  }

こう変更することで、モデルとしたPostクラスでは changed という実装が無くなりました。 「記事」として管理すべき値はモデルに、「編集する」という責任範囲で管理すべき値はコンポーネントに置きましょう。 そうすることで、モデルの実装を減らして、細かく分けられたVueコンポーネントに実装を分散できます。

もし複数コンポーネントで状態を共有するのであれば、Vuexを使えば済みます。 もちろん今回の説明も「こっちが正解」というわけではありません。ですが、「全てモデルに書く」、「全てViewコンポーネントに書く」のでなく、適宜コンポーネントを分離して値を管理する場所を分けることで、より良い設計が自然とできるようになるでしょう。

他のオススメ記事

blog.hirokiky.org

ここ1年以上同じ服を着たので服装まとめ

f:id:hirokiky:20190819140615j:plain
クローゼットの中

この1年半ほど、自分の服装を固定化しています。 平たく言うと、毎日同じものを着るということです。 ミニマリストしぶ という人に影響を受けてやり始めたんですが、、意外とかなり快適で長いこと続いています。 極端に全く同じというわけではなくて、基本的なプリセットを用意しておこうという程度の話です。

同じ服を着るにあたって「自分にとって最高のもの」をまず見つけるのに苦労したので、今ようやく落ち着いてきたのでまとめておきます。

色々書いてますが、他にもオススメあれば教えてください!ぜひ!

Tシャツ

ヘインズのプレミアムジャパンフィットが一番コスパ最強だった

検討した無地Tシャツ

無地Tシャツは他にもヘインズBEEFY、ヘインズジャパンフィット、ZOZOのTシャツ、 THREE DOTSのジョシュ を試したが、プレミアムジャパンフィットが最強。 品質でいうならTHREE DOTSのジョシュをオススメしたいけど、1枚で1万円もするので2枚しかもっていない。

ズボン

ジーパンをはくならEDWINが一番良い(と昔から思っている)。 買うときはジーンズメイトにでも行って、裾上げしてもらうことを強くオススメします。

手頃な値段でシッカリしていて、かつ日本人的にはきやすいのでとても良い。 僕は細身なのでスリムなモデルを選んでいるが、それでもダボつくくらいなのでこれがちょうど良い。 EDWINの402を3本買って回しています。

ZOZOジーパン、ユニクロジーパンを試したけど、生地がチープであまり好きになれなかった。

ワイシャツ

とはいえTシャツ以外のときもあります。

最近試しに買ってみたのだけど、 ワイシャツにおいて鎌倉シャツは本当に一番コスパが良かった 。 ワイシャツ着るときはVネックの白いシャツを着るか、素肌にそのまま着ています。

www.shirt.co.jp

リネンとかオックスフォード地だとジーパンにもあわせられるので良い。 鎌倉シャツ、買いな!(鷹宮リオン風)

上着

上着は基本的に好きなものを着ている。 three dots か、 nano universe か、無印か、ユニクロのジャケットかパーカーを着ている。

f:id:hirokiky:20190819140706j:plain
ジャケット

無印のパーカーは着心地も良いのに安いので一番オススメしたい。

f:id:hirokiky:20190819140645j:plain
無印のパーカー最強

靴下

大したこだわりはないけど、安い中ではこれが一番良かった。

少し薄手なので、シッカリした靴下をはきたい人には向いていない。 僕は、薄いくらいのほうがゴワゴワしなくて好きなのでこれにしている。

オススメあれば教えてください

下着

試した中では一番良かった。

無印で売っている綿のボクサーも良い。 でもAmazonでポチれるのはやはり便利なのでコレが良い。

オススメあれば教えてください

履きやすさではNewBalanceのMRL996が一番良かった。

とはいえMRL996は店頭のみのモデルを買った(茶色のスウェード地のモデルは店頭にしかなかった)。

あとは NIKEのタンジュンを最近買ったのだけど、コスパ面ではタンジュンが単純に最高すぎるのでオススメしたい 。 作りはかなりチープだけど、5000円とは思えない履き心地。

サンダルの場合はビルケンシュトックが一番良い。 ビルケンシュトックは元BeProud CTOのshinにオススメしてもらって買った。

もともと合成皮のモデルを買っていたが、履きつぶしたときに本皮のモデルにした。

時計

僕は時計が好きなので色々な種類を持ってつけていたい。 機械式でコスパ最強で使いやすいのはこのSEIKO SARB033だった。

[セイコー]SEIKO 腕時計 MECHANICAL メカニカル SARB033 メンズ

[セイコー]SEIKO 腕時計 MECHANICAL メカニカル SARB033 メンズ

クォーツならこれがオススメ

個人的に一番好きなのはCA-53W

[カシオスタンダード] 腕時計 CA-53W-1Z 逆輸入品 ブラック

[カシオスタンダード] 腕時計 CA-53W-1Z 逆輸入品 ブラック

理想を言えば欲しい時計はゴマンとある。ムーンウォッチかモナコポルトギーゼかカラトラバがほしいです。

完成イメージ図

この写真の一番右の人。

blog.hirokiky.org

無印のパーカーを愛する人。 自分の全身が映っている写真ってなかなかなかったので、ギターの練習用に自分の姿を撮ったものです。

f:id:hirokiky:20190819144005p:plain
誰かさん

頑張って練習してますが、少し覗き込みすぎで姿勢が良くないです。

まとめ

  • 無地Tシャツはヘインズのプレミアムジャパンフィットが良い
  • EDWINは良い
  • 無印のパーカーは良い
  • 鎌倉シャツは良い
  • NIKEタンジュンは良い

靴下とか下着とか、まだ完璧に満足してるわけではないので、似たことやってる人は情報ください

他のオススメ記事

blog.hirokiky.org

JavaScript (ES6) でAPIから受け取ったデータをモデルに入れるプラクティス

フロントエンドの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 を使いまわす
}

ここで少し面倒な点がいくつかあります

  • new User(...) 時に、APIからのレスポンスを1つ1つ入れるのが面倒
  • constructor の定義が面倒くさい
    • Pythonのdataclassのように書きたい

簡単にモデルを書けるようにする

実験的な実装なので信用しないでください

以下のようにモデルの基底クラスを定義しておきます。

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