LoginSignup
5
8

More than 5 years have passed since last update.

Isomorphic JavaScript でコードを仕様書として利用する

Posted at

はじめに

Virtual DOM の普及や webpack の環境整備などに伴い,最近では Isomorphic な JS 実装をよく見かけるようになりました.

Isomorphic JavaScript では,サーバ・クライアント間のロジックを同期できるところに強みがあります.バリデーションロジックなどがいい例ですが,クライアントサイドでの入力チェックと,サーバサイドでのリクエストチェックで同一のコードを参照することができ,ロバストな設計を実現することができます.

今回は,これらの共通化されたコードの一部そのものを,さらに 仕様書 としても利用するアプローチを考えてみます.これにより,サーバ・クライアント間の同期だけでなく,実装・仕様間の同期もおこなうことができます.

実装例

URL

1 つの例として,複数の URL を 1 ファイルに切り出し,仕様書として利用することを考えてみます.

共通化するコード

以下が共通化するコードです.大きくサイトのエンドポイントと API のエンドポイントの 2 つに分け,エンドポイントごとにドキュメントを記述しています.

urls.js
export const userPageUrl = {
  site: {
    /**
     * ユーザ一覧ページ
     */
    users: '/users',
    /**
     * 特定のユーザのページ
     * @param {number} id - ユーザの id
     */
    user: '/user/{id}'
  },
  api: {
    /**
     * 特定のユーザの情報を取得する API
     * @param {number} id - ユーザの id
     */
    user: '/api/user/{id}'
  }
}

サーバサイド

以下が Node サーバ (Koa) の実装例です.以下で利用している koa-router では,/user/:id のような形で URL を登録することでパラメータを取得できるため,{id} の部分に :id を指定しています.また,変数展開には string-format というライブラリを利用していますが,ES6 のテンプレートリテラルなどでも同様のことを実現できます.

import format from 'string-format'
import Router from 'koa-router'
import { userPageUrl } from 'urls'

const router = new Router()

router.get(
  userPageUrl.site.users,
  (ctx, next) => { /* ... */ }
)

router.get(
  format(userPageUrl.site.user, { id: ':id' }),  // '/user/:id'
  (ctx, next) => {
    // this.params.id で id を取得
  }
) 

クライアントサイド

以下がクライアントサイドの実装例です.Virtual DOM のレンダリングも API リクエストも,展開の仕方は Node と同様です.

import format from 'string-format'
import axios from 'axios'
import { userPageUrl } from 'urls'

/**
 * Virtual DOM のレンダリング
 */
export const UserLink = ({ user }) => (
  <a href={format(userPageUrl.site.user, { id: user.id })}>
    {user.name}
  </a>
)

/**
 * API リクエスト
 */
export const fetchUserById = id => {
  return dispatch => {
    axios.get(format(userPageUrl.api.user, { id }))
    .then(response => /* ... */)
    .catch(error => /* ... */)
  }
}

このようにすることで,例えば /api/user/{id}/api/users/{id} に変更したいということがあったとしても,仕様書となる urls.js を 1 行書き換えるだけで,サーバ・クライアントのコードは一切変更することなしに,エンドポイント名の変更を実現できます.

urls.js は単なるプロパティの定義にすぎないので,このままでも十分ドキュメントの機能を果たしますが,必要であれば html や markdown の簡単なドキュメントを生成してもいいかもしれません.

ステータスコード

もう 1 つの例として,ステータスコードの共通化を挙げてみます.URL と同様に 1 つのファイルに共通化しておくことで,サーバ・クライアントそれぞれでステータスコードを直接記述する必要がなくなると同時に,ステータスコードの仕様としての役割も果たします.

やっていることは URL と同様なので,細かい説明は省略しています.

共通化するコード

status-code.js
export const errorCode = {
  /**
   * ユーザの認証に失敗した場合
   */
  unauthorized: 401,
  /**
   * ユーザにアクセス権がない場合
   */
  forbidden: 403,
  /**
   * リソースが存在しない場合
   */
  notFound: 404
}

サーバサイド

import format from 'string-format'
import Router from 'koa-router'
import { userPageUrl } from 'urls'
import { errorCode } from 'status-code'

const router = new Router()

router.get(
  format(userPageUrl.api.user, { id: ':id' }),
  async (ctx, next) => {
    await userController.fetchUserById(this.params.id)
    .then(response => this.body = response)
    .catch(error => {
      if (error instanceof UserNotFound) {
        this.status = errorCode.notFound  // 404
      } else {
         // ...
      }
    })
  }
) 

クライアントサイド

import format from 'string-format'
import axios from 'axios'
import { userPageUrl } from 'urls'
import { errorCode } from 'status-code'

export const fetchUserById = id => {
  return dispatch => {
    axios.get(format(userPageUrl.api.user, { id }))
    .then(response => /* ... */)
    .catch(error => {
      if (error.status = errorCode.notFound /* 404 */) {
        // ...
      } else {
        // ...
      }
    })
  }
}

おわりに

Isomorphic な環境において,サーバ・クライアント・仕様の 3 者間で足並みをそろえてアップデートできる仕組みを実現しました.

実装と仕様書の乖離をなくしたいといったモチベーション自体は, JSON Schema などにも見受けられますが,今回例に挙げた URL 設計などのように,やり方次第で JSON API フォーマットに限らず様々な JS のコードが仕様書を兼ねられるのではないかと思います.

5
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
8