Vue.js入門 ―最速で作るシンプルなWebアプリケーション

第5回シングルページアプリケーションを拡充する

はじめに

本稿は前回のシングルページアプリケーションの基礎を作成するの続編です。Vue Routerを使用して、データを取得する方法やフック関数について解説します。また、Vue Routerの機能を組み合わせた簡単なシングルページアプリケーション(以下SPA)の実装例を紹介します。

データの取得

SPAではページ遷移をした際に、APIを通じて取得したデータをUIに表示する場面が頻繁にあるでしょう。Vue Routerを使ったアプリでは、Vueのコンポーネントのcreatedwatchを使って実装するのが一般的です。

前回の記事のサンプルコードでは、トップページとユーザー一覧ページがあるシンプルなSPAを作成しました。このSPAを拡張して、ユーザー一覧ページに切り替えたタイミングでユーザーの情報を取得して表示してみましょう。

ナビゲーションのHTMLのテンプレートは前回の記事のものと変わりません。

<div id="app">
  <router-link to="/top">トップページ</router-link>
  <router-link to="/users">ユーザー一覧ページ</router-link>
  <router-view></router-view>
</div>

/usersにアクセスした時のユーザー一覧用のコンポーネントが、前回までのものと比べて少し複雑になりそうです。したがって、今回は文字列をtemplateプロパティにセットするのではなく、HTML上に定義したテンプレートから読み込むようにしてみましょう。

<script type="x-template" id="user-list">
  <div>
    <div class="loading" v-if="loading">ロード中...</div>
    <div v-if="error" class="error">
      {{ error }}
    </div>
    <!-- usersがロードされたら各ユーザーの名前を表示する -->
    <div v-for="user in users" :key="user.id">
      <h2>{{ user.name }}</h2>
    </div>
  </div>
</script>

上記のテンプレートHTMLは、rootのVueをmountする対象が記述されているHTMLと同じファイル内に記述します。

次は、このテンプレートを使用して、ユーザー一覧データを表示するコンポーネントのJavaScriptの実装です。

// 擬似的にAPI経由で情報を取得したようにする
var getUsers = function (callback) {
  setTimeout(function () {
    callback(null, [
      {
        id: '001',
        name: 'Takuya Tejima'
      },
      {
        id: '002',
        name: 'Yohei Noda'
      }
    ])
  }, 1000)
}

var UserList = {
  // HTML上のscriptタグのidを指定する
  template: '#user-list',
  data: function () {
    return {
      loading: false,
      // 初期値の空配列を定義
      users: function () { return [] },
      error: null
    }
  },

  // 初期化時にデータを取得する
  created: function () {
    this.fetchData()
  },

  // ルーティングが変更された時に再度データを取得するために$routeの変更をwatchする
  watch: {
    '$route': 'fetchData'
  },

  methods: {
    fetchData: function () {
      this.loading = true
      // 取得したデータの結果をusersに格納する
      getUsers((function (err, users) {
        this.loading = false
        if (err) {
          this.error = err.toString()
        } else {
          this.users = users
        }
      }).bind(this))
    }
  }
}

これでコンポーネントの実装は完了です。次に、Vue Router初期化時に/usersへのアクセスとUserListコンポーネントをマッピングします。この部分は前回の内容と変わりません。

  var router = new VueRouter({
    routes: [
      {
        path: '/top',
        component: {
          template: '<div>トップページです。</div>'
        }
      },
      {
        path: '/users',
        component: UserList
      }
    ]
  })

  var app = new Vue({
    router: router
  }).$mount('#app')

実行すると、"ロード中"と表示された後に、以下のような結果が表示されるでしょう。

図1 データ取得後のUI
図1 データ取得後のUI

コードサンプルはこちらで確認できます。

フック関数

Vue Routerでは、ページ遷移が実行される前後に処理を追加できるフック関数の仕組みが提供されています。以下、グローバルのフック関数ルート単位のフック関数コンポーネント内のフック関数それぞれ3つのパターンを紹介します。

グローバルのフック関数

全てのページ遷移に対して設定できるフック関数です。router.beforeEachに関数をセットすると、ページ遷移が起こる直前にその関数が実行されます。

引数のtoとfromには、現在遷移しようとしているルーティングの遷移先ルートと遷移元ルートの情報が入っています。このtoとfromに格納されるルートは、マッチしたルートのパスやコンポーネントの情報を持っています。ルートオブジェクトの詳細については公式ドキュメントをご参照ください。

router.beforeEach((to, from, next) => {
  // ユーザー一覧ページへアクセスした時に/topへリダイレクトする例
  if (to.path === '/users') {
    next('/top')
  } else {
    // 引数なしでnextを呼び出すと通常通りの遷移が行われる
    next()
  }
})

上記の例では、ユーザー一覧ページへアクセスした時に/topへリダイレクトする方法を紹介するためにnext('/top')と記述しています。問題なく通常のルーティングとして遷移したい場合は、next()と引数なしで呼び出してください。このフック関数内でnextを呼び出さないと、延々と遷移が終わらなくなる点に注意してください。

ルート単位のフック関数

もし全ての遷移に対してではなく特定のルート単位でフックを追加したい場合は、Vue Router初期化時のマッピング定義時に指定できます。beforeEnterを記述することで、ルーティング前のフックを追加することができます。

var router = new VueRouter({
  routes: [
    {
      path: '/users',
      component: UserList,
      beforeEnter: (to, from, next) => {
        // /users?redirect=true でアクセスされた時だけtopにリダイレクトするフック関数を追加
        if (to.query.redirect === 'true') {
          next('/top')
        } else {
          next()
        }
      }
    }
  ]
})

コンポーネント内のフック関数

コンポーネント内でフック関数を足したいときには、コンポーネント内で定義するフック関数が利用できます。beforeRouteEnterを使ってデータを取得する例を紹介します。本稿のデータの取得で解説した方法とは異なる実装例として、コンポーネントが表示される前にデータの取得を行い、完了したタイミングでページ遷移が行われます。

var UserList = {
  template: '#user-list',

  data: function () {
    return {
      users: function () { return [] },
      error: null
    }
  },

  // ページ遷移が行われ、このコンポーネントが初期化される前に呼び出される
  beforeRouteEnter: function (to, from, next) {
    getUsers((function (err, users) {
      if (err) {
        this.error = err.toString()
      } else {
        // nextに渡すcallbackでコンポーネントにアクセスすることができます
        next(function (vm) {
          vm.users = users
        })
      }
    }).bind(this))
  }
}

上記のコードの動作サンプルはこちらです。この例ではコンポーネントが表示されるタイミングのフック関数であるbeforeRouteEnterを利用しました。他にも、次の遷移の発生によりコンポーネントが去っていく際のフック関数beforeRouteLeaveも利用可能です。beforeRouteLeaveを使うと、たとえば保存していない変更がある時にページを去る際に、confirmを表示するなどの実装も可能になります。

簡易認証付きSPAの実装

ルート単位のフック関数を使用して、ダミーデータを利用した簡易認証付きSPAを実装してみましょう。

本稿で作成したUserListのコンポーネントにユーザー詳細ページへのリンクを追加します。そのユーザー詳細ページへのアクセスにはログインが必要になるような実装を紹介します。

まずはダミーデータ(emailアドレス:vue@example.comパスワード:vueを使って、認証するLoginモジュールを作成します。

var Auth = {
  login: function (email, pass, cb) {
    // ダミーデータを使った擬似ログイン
    setTimeout(function () {
      if (email === 'vue@example.com' && pass === 'vue') {
        // ログイン成功時はローカルストレージにtokenを保存する
        localStorage.token = Math.random().toString(36).substring(7)
        if (cb) { cb(true) }
      } else {
        if (cb) { cb(false) }
      }
    }, 0)
  },

  logout: function () {
    delete localStorage.token
  },

  loggedIn: function () {
    // ローカルストレージにtokenがあればログイン状態とみなす
    return !!localStorage.token
  }
}

次に、ユーザーの詳細ページへ遷移しようとした時に、認証ページを表示するようにルート単位のフック関数を定義します。

  var router = new VueRouter({
    routes: [
      {
        path: '/top',
        component: {
          template: '<div>トップページです。</div>'
        }
      },
      {
        path: '/users',
        component: UserList
      },
      {
        path: '/users/:id',
        component: UserDetail,
        beforeEnter: function (to, from, next) {
          // 認証されていない状態でアクセスした時はloginページに遷移する
          if (!Auth.loggedIn()) {
            next({
              path: '/login',
              query: { redirect: to.fullPath }
            })
          } else {
            // 認証済みであればそのままユーザー詳細ページへ進む
            next()
          }
        }
      },
      {
        path: '/login',
        component: Login
      },
      {
        path: '/logout',
        beforeEnter: function (to, from, next) {
          Auth.logout()
          next('/')
        }
      }
    ]
  })

次にログインコンポーネントを作成しましょう。認証が失敗した場合は、エラーメッセージを表示するようにします。

<script type="x-template" id="login">
  <div>
    <h2>Login</h2>
    <p v-if="$route.query.redirect">
      ログインしてください
    </p>
    <form @submit.prevent="login">
      <label><input v-model="email" placeholder="email"></label>
      <label><input v-model="pass" placeholder="password" type="password"></label><br>
      <button type="submit">ログイン</button>
      <p v-if="error" class="error">ログインに失敗しました</p>
    </form>
  </div>
</script>
var Login = {
  template: '#login',
  data: function () {
    return {
      email: 'vue@example.com',
      pass: '',
      error: false
    }
  },
  methods: {
    login: function () {
      Auth.login(this.email, this.pass, (function (loggedIn) {
        if (!loggedIn) {
          this.error = true
        } else {
          // redirectパラメーターが付いている場合はそのパスに遷移
          this.$router.replace(this.$route.query.redirect || '/')
        }
      }).bind(this))
    }
  }
}

上記の実装で詳細ページにアクセスしようとした時に以下のような認証ページが表示されます。

その他、詳細ページのコンポーネント実装やログアウトの機能なども含めた今回のSPA動作サンプルは、こちらで確認できます。

実際にユーザー一覧ページからユーザーの名前をクリックすると、ログイン画面が表示されます。

図2 認証画面
図2 認証画面

メールアドレスにvue@example.comパスワードにvueを入力すると認証が成功し、ユーザーの詳細ページへ遷移します。

図3 ユーザー詳細ページ
図3 ユーザー詳細ページ

まとめ

いかがでしたでしょうか、前回と今回でVue Routerの基本から少し高度な機能を使ったSPA実装について紹介しました。

Vue.jsとVue Routerを使うと一見複雑そうに見えるWebアプリケーションが思ったより簡単に実装できることを実感できたのではないでしょうか。次回最終回はVue.jsをマスターするための高度な機能の解説をする予定です。乞うご期待。

おすすめ記事

記事・ニュース一覧