Vue.js入門 #07 4章 Vue Routerによるシングルページアプリケーション(SPA)

前 [Vue.js入門 #06 3章 コンポーネントの基礎（コンポーネント間の通信／スロットコンテンツ／ログインフォーム）](https://qiita.com/tseno/items/ddb5142f74bae3aa111a)
次 [Vue.js入門 #08 4章 Vue Routerの高度な機能](https://qiita.com/tseno/items/4b299464c4fc5605e515)

# Vue Routerによるシングルページアプリケーション

## シングルページアプリケーションとルーティング

- シングルページアプリケーション(SPA)とは、はじめにHTMLをロードして、以後はAjaxで情報を取得し動的にページを更新するWebアプリケーションのこと。
- SPAではページ遷移をクライアントサイドで行う。その際にAjaxを使用して、必要なデータを取得してViewの表示を行う。
- SPAを実装するには、以下のことを考慮する必要がある。
    - クライアントサイドでの履歴管理なども含めたページ遷移
    - 非同期によるデータ取得
    - Viewのレンダリング
    - モジュール化されたコードの管理

## Vue Routerとは

- Vue Routerは、Vue.jsの公式のプラグインとして提供されているSPA構築のためのルーティングライブラリ。
- Vue Routerは、基本的なページ遷移の他にも以下のような機能がある。
    - ネストしたルーティング
    - リダイレクトとエイリアス
    - HTML5 History APIとURL Hashによる履歴管理（IE9での自動的なフォールバック）
    - 自動的にCSSクラスがアクティブになるリンクの仕組み
    - Vue.jsのトランジションの仕組みを使ったページ遷移のトランジション
    - スクロールの振る舞いのカスタマイズ

# ルーティングの基礎

## ルーターのインストール

```html
<script src="https://unpkg.com/vue@2.5.17"></script>
<script src="https://unpkg.com/vue-router@3.0.1"></script>
```

## ルーティング設計



- Vue Routerにおけるルートとは、Vue.jsのコンポーネントを特定のURLにマッピングしたオブジェクト。これをルーターコンストラクタを用いたルーター初期化時のroutesオプションに設定する。

```js
// ルート定義
{
  path: '/someurl', // URLを指定 ファイル名#/someurlでアクセスできる
  component: {
    template: '...' // コンポーネントの構文、またはコンストラクタベースのコンポーネント
  }
}

// ルーターコンストラクタ、これを new Vue()に渡す
new VueRouter({
  routes: [ ] // ルート定義を配列で渡す。
})
```

```html
<!-- Vue.jsとVue Routerを読み込んでおく -->
<script>
// ルートオプションを渡してルーターインスタンスを生成
var router = new VueRouter({
  // コンポーネントをマッピングしたルート定義を配列で渡す
  routes: [
    {
      path: '/top',
      component: {
        template: '<div>トップページです。</div>'
      }
    },
    {
      path: '/users',
      template: '<div>ユーザー一覧ページです。</div>'
    }
  ]
})
var app = new Vue({
  router: router
}).$mount('#app')
</script>

```


### HTML側の指定とページ遷移の実行

#### HTML側

- `<router-link to="/top">`タグでリンク先を指定する。デフォルトでは`<a>`タグとしてレンダリングされる。
- ルート内でマッピングしたコンポーネントは`<router-view></router-view>`にレンダリングされる。

```html
<script src="https://unpkg.com/vue@2.5.17"></script>
<script src="https://unpkg.com/vue-router@3.0.1"></script>
<div id="app">
  <router-link to="/top">トップページ</router-link>
  <router-link to="/users">ユーザー一覧ページ</router-link>
  <router-view></router-view>
</div>
```

- VueRouterインスタンス引数にpathとcomponentがセットの配列を渡す。
- Vueインスタンスの引数に、routerインスタンスを渡す。
- #appをVueインスタンスにマウントする。


```js
// ルートオプションを渡してルーターインスタンスを生成
var router = new VueRouter({
  routes: [
    {
      path: '/top',
      component: {
        template: '<div>トップページです</div>'
      }
    },
    {
      path: '/users',
      component: {
        template: '<div>ユーザー一覧ページです。</div>'
      }
    }
  ]
})

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

- それぞれのリンクをクリックすると表示が切り替わることを確認。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/88686/734bd7a0-672f-d8da-48f7-75829a546fc5.png)
![image.png](https://qiita-image-store.s3.amazonaws.com/0/88686/528e698d-9e7a-6565-92d1-f42ae905293c.png)


# 実践的なルーティングのための機能

## URLパラメーターの扱いとパターンマッチング

- path上のURLにコロン付きのパターンで指定することで、URLからパラメータを取得できる。
- `user/123`のとき、`$route.params.userid`で123を取得できる。

```js
var router = new VueRouter({
  routes: [
    {
      path: '/user/:userid',
      component: {
        template: '<div>ユーザーIDは {{ $route.params.userid }}</div>'
      }
    }
  ]
})
```


## 名前付きルート

- 名前付きルートを呼び出すには、（コロン付きの）`:to`に、パラメータで指定できる。

```html
<script src="https://unpkg.com/vue@2.5.17"></script>
<script src="https://unpkg.com/vue-router@3.0.1"></script>
<div id="app">
  <router-link :to="{ name: 'user', params: { userId: 123 }}">ユーザー詳細ページ</router-link>
  <router-view></router-view>
</div>
```

- `name: 'user'`のように名前を指定する。

```js
var router = new VueRouter({
  routes: [
    {
      path: '/user/:userid',
      name: 'user',
      component: {
        template: '<div>ユーザーIDは {{ $route.params.userId }}です。</div>'
      }
    }
  ]
})

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

![image.png](https://qiita-image-store.s3.amazonaws.com/0/88686/72158979-d6fb-5ef4-cea4-fa4dc9b0320e.png)


##  router.pushを使った遷移

`<router-link>`は宣言的な書き方。`router.push`を使うとプログラム上で動的な遷移が可能になる。

```js
router.push({ name: 'user', params: { userId: 123 }})
```

## フック関数

### グローバルのフック関数

- 全てのページ遷移に対して設定できるフック関数。
- router.beforeEachに関数をセットすると、ページ遷移が起こる直前にその関数が実行される。
- 引数のfrom、toには遷移しようとしているルーティングの遷移元・遷移先ルートの情報が入っている。
- 以下は、`/users`に遷移しようとすると`/top`に遷移され、それ以外は通常通りの遷移が行われる。

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

```

### ルート単位のフック関数

- 全ての遷移ではなく、ルート単位でフックを追加するには、Vue Route初期化時のルート定義の時に個別に指定する。
- ルート定義にbeforeEnterを記述することで、ルーティング前のフックを追加する。

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

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

- ルート定義ではなく、コンポーネント側でもフック関数は定義できる。
- コンポーネントのオプションとして、`beforeRouteEnter`を使ってデータを取得する。

```js
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))
  }
}
```

- このほかに、フック関数`beforeRouteLeave`は遷移の発生によりコンポーネントが去っていく時に呼ばれ、例えば未保存のページから遷移しようとするタイミングでconfirmを出力することが可能になる。

# サンプルアプリケーションの実装

- /top（トップ）
- /users（ユーザー一覧） → /users/:id（ユーザー詳細）
- /login（新規ログインなし） ログイン後↓
- /users/new（新規ログイン済み） 

#

- 今回のサンプルアプリケーションのベースとなるHTML


```html
<script src="https://unpkg.com/vue@2.5.17"></script>
<script src="https://unpkg.com/vue-router@3.0.1"></script>

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

## リストページの実装

- ユーザー一覧のテンプレート

```html
<script type="text/x-template" id="user-list">
  <div>ユーザー一覧ページ</div>
</script>
```

- ユーザー一覧のコンポーネントで、上記テンプレートを指定する。

```js
// ユーザーリストコンポーネント
var UserList = {
  template: '#user-list',
}
```

- ルーターに「`/top`と`<div>トップページです。</div>`」、「/usersとユーザー一覧テンプレート」をマッピングする。

```js
var router = new VueRouter({
  routes: [
    {
      path: '/top',
      component: {
        template: '<div>トップページです。</div>'
      }
    },
    {
      path: '/users',
      component: UserList
    }
  ]
})
// appとルーターをマウントする。
var app = new Vue({
  router: router
}).$mount('#app')
</script>
```

## APIによるデータ通信

- SPAでは、ページ遷移をした際に、APIを通じて取得したデータをUIに表示することが頻繁にある。
- Vue Routerでは、非同期通信によるデータ取得を行う場合は、createdとwatchを使って実装するのが一般的。
- watchは、算出プロパティを汎用的にしたVueコンポーネントのオプション。
- ユーザー一覧ページに切り替えたタイミングでユーザーの情報を取得して表示する。

```html
<script type="text/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>
```

```js
// JSONを返す関数
// 擬似的にAPI経由で情報を取得したようにする
var getUsers = function (callback) {
  setTimeout(function () {
    callback(null, [
    	{
      	id: 1,
        name: 'Takuya Tajima'
      },
    	{
      	id: 2,
        name: 'Yohei Noda'
      }
    ])
  }, 1000)
}
// ユーザーリストコンポーネント
var UserList = {
  template: '#user-list',
  data: function () {
    return {
      loading: false,
      users: function () {
        return []
      },
      error: null
    }
  },

  created: function () {
    this.fetchData()
  },

  watch: {
    '$route': 'fetchData'
  },

  methods: {
    fetchData: function () {
      this.loading = true
      getUsers((function (err, users) {
        this.loading = false
        if (err) {
          this.error = err.toString()
        } else {
          this.users = users
        }
      }).bind(this))
    }
  }
}

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

- ユーザー一覧の画面が出力される。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/88686/c5140dbc-9256-78c1-2bcb-c8600f74e27e.png)


## 詳細ページの実装

- ユーザー詳細のテンプレートを追加する。

```html
<!-- ユーザー詳細ページのテンプレート -->
<script type="text/x-template" id="user-detail">
  <div>
    <div class="loading" v-if="loading">読み込み中...</div>
    <div v-if="error" class="error">
      {{ error }}
    </div>
    <div v-if="user">
      <h2>{{ user.name }}</h2>
      <p>{{ user.description }}</p>
    </div>
  </div>
</script>
```

- `userData`のデータを追加する。
- `getUser`関数を追加する。
- `UserDetail`コンポーネントを追加する。

```js

// ダミーデータの定義。本来はデータベースの情報をAPI経由で取得する
var userData = [
  {
    id: 1,
    name: 'Takuya Tejima',
    description: '東南アジアで働くエンジニアです。'
  },
  {
    id: 2,
    name: 'Yohei Noda',
    description: 'アウトドア・フットサルが趣味のエンジニアです。'
  }
]

var getUser = function (userId, callback) {
  setTimeout(function () {
    var filteredUsers = userData.filter(function (user) {
      return user.id === parseInt(userId, 10)
    })
    callback(null, filteredUsers && filteredUsers[0])
  }, 1000)
}

// ユーザー詳細コンポーネント
var UserDetail = {
  template: '#user-detail',
  data: function () {
    return {
      loading: false,
      user: null,
      error: null
    }
  },

  created: function () {
    this.fetchData()
  },

  watch: {
    '$route': 'fetchData'
  },

  methods: {
    fetchData: function () {
      this.loading = true
      // this.$route.params.userId に現在のURL上のパラメーターに対応したuserIdが格納される
      getUser(this.$route.params.userId, (function (err, user) {
        this.loading = false
        if (err) {
          this.error = err.toString()
        } else {
          this.user = user
        }
      }).bind(this))
    }
  }
}

var router = new VueRouter({
  routes: [
    {
      path: '/top',
      component: {
        template: '<div>トップページです。</div>'
      }
    },
    {
      path: '/users',
      component: UserList
    },
    {
      // /users/newの前にこのルートを定義するとパターンマッチにより/users/newが動作しなくなるので注意
      path: '/users/:userId',
      component: UserDetail
    }
  ]
})
var app = new Vue({
  router: router
}).$mount('#app')
```

## ユーザー登録ページの実装

- ルーター部分に`/users/new`を追加する。

```js
var router = new VueRouter({
  routes: [
    {
      path: '/top',
      component: {
        template: '<div>トップページです。</div>'
      }
    },
    {
      path: '/users',
      component: UserList
    },
    {
      path: '/users/new',
      component: UserCreate
    },
    {
      // /users/newの前にこのルートを定義するとパターンマッチにより/users/newが動作しなくなるので注意
      path: '/users/:userId',
      component: UserDetail
    }
  ]
})
```

- ユーザー作成ページのテンプレートを追加する。

```html
<script type="x-template" id="user-create">
  <div>
    <div class="sending" v-if="sending">Sending...</div>
    <div>
      <h2>新規ユーザー作成</h2>
      <div>
        <label>名前: </label>
        <input type="text" v-model="user.name">
      </div>
      <div>
        <label>説明文: </label>
        <textarea v-model="user.description"></textarea>
      </div>
      <div v-if="error" class="error">
        {{ error }}
      </div>
      <div>
        <input type="button" @click="createUser" value="送信">
      </div>
    </div>
  </div>
</script>
```

- postUser関数を追加する。擬似的にサーバーに投げるような関数。
- UserCreate(新規ユーザー)コンポーネント

```js
// 擬似的にAPI経由で情報を更新したようにする
// 実際のWebアプリケーションではServerへPOSTリクエストを行う
var postUser = function (params, callback) {
  setTimeout(function () {
    // idは追加されるごとに自動的にincrementされていく
    params.id = userData.length + 1
    userData.push(params)
    callback(null, params)
  }, 1000)
}

// 新規ユーザー作成コンポーネント
var UserCreate = {
  template: '#user-create',
  data: function () {
    return {
      sending: false,
      user: this.defaultUser(),
      error: null
    }
  },

  created: function () {
  },

  methods: {
    defaultUser: function () {
      return {
        name: '',
        description: ''
      }
    },

    createUser: function () {
      // 入力パラメーターのバリデーション
      if (this.user.name.trim() === '') {
        this.error = 'Nameは必須です'
        return
      }
      if (this.user.description.trim() === '') {
        this.error = 'Descriptionは必須です'
        return
      }
      postUser(this.user, (function (err, user) {
        this.sending = false
        if (err) {
          this.error = err.toString()
        } else {
          this.error = null
          // デフォルトでフォームをリセット
          this.user = this.defaultUser()
          alert('新規ユーザーが登録されました')
          // ユーザー一覧ページに戻る
          this.$router.push('/users')
        }
      }).bind(this))
    }
  }
}　
```

## ログイン・ログアウトの実装

- Authメソッドの追加。
    - `email === 'vue@example.com' && pass === 'vue'`のとき、ログインが成功する。

```js
// サンプルアプリケーション用のダミー認証モジュール
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
  }
}
```

- ルーターの修正。
    - `/users/new`に、beforeEnter(ルーティング前)のフック関数を追加する。
    - /login、/logoutを追加する。

```js
var router = new VueRouter({
  // 各ルートにコンポーネントをマッピング
  // コンポーネントはVue.extend() によって作られたコンポーネントコンストラクタでも
  // コンポーネントオプションのオブジェクトでも渡せる
  routes: [
    {
      path: '/top',
      component: {
        template: '<div>トップページです。</div>'
      }
    },
    {
      path: '/users',
      component: UserList
    },
    {
      path: '/users/new',
      component: UserCreate,
      beforeEnter: function (to, from, next) {
        // 認証されていない状態でアクセスした時はloginページに遷移する
        if (!Auth.loggedIn()) {
          next({
            path: '/login',
            query: { redirect: to.fullPath }
          })
        } else {
          // 認証済みであればそのまま新規ユーザー作成ページへ進む
          next()
        }
      }
    },
    {
      // /users/newの前にこのルートを定義するとパターンマッチにより/users/newが動作しなくなるので注意
      path: '/users/:userId',
      component: UserDetail
    },
    {
      path: '/login',
      component: Login
    },
    {
      path: '/logout',
      beforeEnter: function (to, from, next) {
        Auth.logout()
        next('/')
      }
    }
  ]
})
var app = new Vue({
  data: { // dataにAuthを追加する
    Auth: Auth
  },
  router: router
}).$mount('#app')
```

## ログインコンポーネントの作成

- ログインテンプレートの作成

```html
<!-- ログインページのテンプレート -->
<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>
```

- 上記テンプレートを、以下の`Login`コンポーネントで指定する。

```js
// ログインコンポーネント
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))
    }
  }
}
```

- グローバルメニューに、作成した全てのリンクを追加する。`v-show`を使って、ログイン状態で表示／非表示を切り替える。

```html
<div id="app">
  <nav v-cloak>
    <router-link to="/top">トップページ</router-link>
    <router-link to="/users">ユーザー一覧ページ</router-link>
    <router-link to="/users/new?redirect=true">新規ユーザー登録</router-link>
    <router-link to="/login" v-show="!Auth.loggedIn()">ログイン</router-link>
    <router-link to="/logout" v-show="Auth.loggedIn()">ログアウト</router-link>
  </nav>
  <router-view></router-view>
</div>
```

- ログイン画面

![image.png](https://qiita-image-store.s3.amazonaws.com/0/88686/f3ed5a6f-b1aa-e3f3-d144-9f338967075e.png)

- 新規ユーザー作成画面

![image.png](https://qiita-image-store.s3.amazonaws.com/0/88686/1c74ae76-c45a-7ed3-1548-43122ee115d2.png)

- 登録されたことを確認

![image.png](https://qiita-image-store.s3.amazonaws.com/0/88686/e180a73c-5e61-21eb-1edd-ef334f1f85ee.png)

## サンプルアプリケーションの全体像

### HTML

```html
<div id="app">
  <nav v-cloak>
    <router-link to="/top">トップページ</router-link>
    <router-link to="/users">ユーザー一覧ページ</router-link>
    <router-link to="/users/new?redirect=true">新規ユーザー登録</router-link>
    <router-link to="/login" v-show="!Auth.loggedIn()">ログイン</router-link>
    <router-link to="/logout" v-show="Auth.loggedIn()">ログアウト</router-link>
  </nav>
  <router-view></router-view>
</div>


<!-- ユーザー一覧ページのテンプレート -->
<script type="x-template" id="user-list">
  <div>
    <div class="loading" v-if="loading">読み込み中...</div>
    <div v-if="error" class="error">
      {{ error }}
    </div>
    <div v-for="user in users" :key="user.id">
      <router-link :to="{ path: '/users/' + user.id }">{{ user.name }}</router-link>
    </div>
  </div>
</script>

<!-- ユーザー詳細ページのテンプレート -->
<script type="x-template" id="user-detail">
  <div>
    <div class="loading" v-if="loading">読み込み中...</div>
    <div v-if="error" class="error">
      {{ error }}
    </div>
    <div v-if="user">
      <h2>{{ user.name }}</h2>
      <p>{{ user.description }}</p>
    </div>
  </div>
</script>

<!-- ユーザー作成ページのテンプレート -->
<script type="x-template" id="user-create">
  <div>
    <div class="sending" v-if="sending">Sending...</div>
    <div>
      <h2>新規ユーザー作成</h2>
      <div>
        <label>名前: </label>
        <input type="text" v-model="user.name">
      </div>
      <div>
        <label>説明文: </label>
        <textarea v-model="user.description"></textarea>
      </div>
      <div v-if="error" class="error">
        {{ error }}
      </div>
      <div>
        <input type="button" @click="createUser" value="送信">
      </div>
    </div>
  </div>
</script>

<!-- ログインページのテンプレート -->
<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>
```

### JavaScript

```js
// サンプルアプリケーション用のダミー認証モジュール
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
  }
}

// ダミーデータの定義。本来はデータベースの情報をAPI経由で取得する
var userData = [
  {
    id: 1,
    name: 'Takuya Tejima',
    description: '東南アジアで働くエンジニアです。'
  },
  {
    id: 2,
    name: 'Yohei Noda',
    description: 'アウトドア・フットサルが趣味のエンジニアです。'
  }
]

// 擬似的にAPI経由で情報を取得したようにする
var getUsers = function (callback) {
  setTimeout(function () {
    callback(null, userData)
  }, 1000)
}

var getUser = function (userId, callback) {
  setTimeout(function () {
    var filteredUsers = userData.filter(function (user) {
      return user.id === parseInt(userId, 10)
    })
    callback(null, filteredUsers && filteredUsers[0])
  }, 1000)
}

// 擬似的にAPI経由で情報を更新したようにする
// 実際のWebアプリケーションではServerへPOSTリクエストを行う
var postUser = function (params, callback) {
  setTimeout(function () {
    // idは追加されるごとに自動的にincrementされていく
    params.id = userData.length + 1
    userData.push(params)
    callback(null, params)
  }, 1000)
}

// ログインコンポーネント
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))
    }
  }
}

// ユーザーリストコンポーネント
var UserList = {
  template: '#user-list',
  data: function () {
    return {
      loading: false,
      users: function () {
        return []
      },
      error: null
    }
  },

  created: function () {
    this.fetchData()
  },

  watch: {
    '$route': 'fetchData'
  },

  methods: {
    fetchData: function () {
      this.loading = true
      getUsers((function (err, users) {
        this.loading = false
        if (err) {
          this.error = err.toString()
        } else {
          this.users = users
        }
      }).bind(this))
    }
  }
}

// ユーザー詳細コンポーネント
var UserDetail = {
  template: '#user-detail',
  data: function () {
    return {
      loading: false,
      user: null,
      error: null
    }
  },

  created: function () {
    this.fetchData()
  },

  watch: {
    '$route': 'fetchData'
  },

  methods: {
    fetchData: function () {
      this.loading = true
      // this.$route.params.userId に現在のURL上のパラメーターに対応したuserIdが格納される
      getUser(this.$route.params.userId, (function (err, user) {
        this.loading = false
        if (err) {
          this.error = err.toString()
        } else {
          this.user = user
        }
      }).bind(this))
    }
  }
}

// 新規ユーザー作成コンポーネント
var UserCreate = {
  template: '#user-create',
  data: function () {
    return {
      sending: false,
      user: this.defaultUser(),
      error: null
    }
  },

  created: function () {
  },

  methods: {
    defaultUser: function () {
      return {
        name: '',
        description: ''
      }
    },

    createUser: function () {
      // 入力パラメーターのバリデーション
      if (this.user.name.trim() === '') {
        this.error = 'Nameは必須です'
        return
      }
      if (this.user.description.trim() === '') {
        this.error = 'Descriptionは必須です'
        return
      }
      postUser(this.user, (function (err, user) {
        this.sending = false
        if (err) {
          this.error = err.toString()
        } else {
          this.error = null
          // デフォルトでフォームをリセット
          this.user = this.defaultUser()
          alert('新規ユーザーが登録されました')
          // ユーザー一覧ページに戻る
          this.$router.push('/users')
        }
      }).bind(this))
    }
  }
}

// ルートオプションを渡してルーターインスタンスを生成
var router = new VueRouter({
  // 各ルートにコンポーネントをマッピング
  // コンポーネントはVue.extend() によって作られたコンポーネントコンストラクタでも
  // コンポーネントオプションのオブジェクトでも渡せる
  routes: [
    {
      path: '/top',
      component: {
        template: '<div>トップページです。</div>'
      }
    },
    {
      path: '/users',
      component: UserList
    },
    {
      path: '/users/new',
      component: UserCreate,
      beforeEnter: function (to, from, next) {
        // 認証されていない状態でアクセスした時はloginページに遷移する
        if (!Auth.loggedIn()) {
          next({
            path: '/login',
            query: { redirect: to.fullPath }
          })
        } else {
          // 認証済みであればそのまま新規ユーザー作成ページへ進む
          next()
        }
      }
    },
    {
      // /users/newの前にこのルートを定義するとパターンマッチにより/users/newが動作しなくなるので注意
      path: '/users/:userId',
      component: UserDetail
    },
    {
      path: '/login',
      component: Login
    },
    {
      path: '/logout',
      beforeEnter: function (to, from, next) {
        Auth.logout()
        next('/top')
      }
    },
    {
      // 定義されていないパスへの対応。トップページへリダイレクトする。
      path: '*',
      redirect: '/top'
    }
  ]
})

// ルーターのインスタンスをrootとなるVueインスタンスに渡す
var app = new Vue({
  data: {
    Auth: Auth
  },
  router: router
}).$mount('#app')
```

### CSS

```css
[v-cloak] {
  display: none
}
```

#### まとめ

- なかなか難易度が高いですが、少しづつ改造をしていく中で理解したいと思います💧

参考
[Vue.js入門 基礎から実践アプリケーション開発まで](https://www.amazon.co.jp/dp/B07J6FP6NQ/ref=dp-kindle-redirect?_encoding=UTF8&btkr=1)