Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
67
Help us understand the problem. What is going on with this article?
@tseno

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

More than 1 year has passed since last update.

Vue.js入門 #06 3章 コンポーネントの基礎(コンポーネント間の通信/スロットコンテンツ/ログインフォーム)
Vue.js入門 #08 4章 Vue Routerの高度な機能

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のトランジションの仕組みを使ったページ遷移のトランジション
    • スクロールの振る舞いのカスタマイズ

ルーティングの基礎

ルーターのインストール

<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オプションに設定する。
// ルート定義
{
  path: '/someurl', // URLを指定 ファイル名#/someurlでアクセスできる
  component: {
    template: '...' // コンポーネントの構文、またはコンストラクタベースのコンポーネント
  }
}

// ルーターコンストラクタ、これを new Vue()に渡す
new VueRouter({
  routes: [ ] // ルート定義を配列で渡す。
})
<!-- 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>にレンダリングされる。
<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インスタンスにマウントする。
// ルートオプションを渡してルーターインスタンスを生成
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
image.png

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

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

  • path上のURLにコロン付きのパターンで指定することで、URLからパラメータを取得できる。
  • user/123のとき、$route.params.useridで123を取得できる。
var router = new VueRouter({
  routes: [
    {
      path: '/user/:userid',
      component: {
        template: '<div>ユーザーIDは {{ $route.params.userid }}</div>'
      }
    }
  ]
})

名前付きルート

  • 名前付きルートを呼び出すには、(コロン付きの):toに、パラメータで指定できる。
<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'のように名前を指定する。
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

router.pushを使った遷移

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

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

フック関数

グローバルのフック関数

  • 全てのページ遷移に対して設定できるフック関数。
  • router.beforeEachに関数をセットすると、ページ遷移が起こる直前にその関数が実行される。
  • 引数のfrom、toには遷移しようとしているルーティングの遷移元・遷移先ルートの情報が入っている。
  • 以下は、/usersに遷移しようとすると/topに遷移され、それ以外は通常通りの遷移が行われる。
router.beforeEach(function (to, from, next)) {
  // ユーザー一覧ページへアクセスしたときに/topへリダイレクトされる例
  if (to.path === '/users') {
    next('/top')
  } else {
    // 引数無しでnextを呼び出すと通常通りの遷移が行われる
    next()
  }
}

ルート単位のフック関数

  • 全ての遷移ではなく、ルート単位でフックを追加するには、Vue Route初期化時のルート定義の時に個別に指定する。
  • ルート定義にbeforeEnterを記述することで、ルーティング前のフックを追加する。
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を使ってデータを取得する。
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
<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>

リストページの実装

  • ユーザー一覧のテンプレート
<script type="text/x-template" id="user-list">
  <div>ユーザー一覧ページ</div>
</script>
  • ユーザー一覧のコンポーネントで、上記テンプレートを指定する。
// ユーザーリストコンポーネント
var UserList = {
  template: '#user-list',
}
  • ルーターに「/top<div>トップページです。</div>」、「/usersとユーザー一覧テンプレート」をマッピングする。
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コンポーネントのオプション。
  • ユーザー一覧ページに切り替えたタイミングでユーザーの情報を取得して表示する。
<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>
// 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

詳細ページの実装

  • ユーザー詳細のテンプレートを追加する。
<!-- ユーザー詳細ページのテンプレート -->
<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コンポーネントを追加する。

// ダミーデータの定義。本来はデータベースの情報を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を追加する。
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
    }
  ]
})
  • ユーザー作成ページのテンプレートを追加する。
<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(新規ユーザー)コンポーネント
// 擬似的に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'のとき、ログインが成功する。
// サンプルアプリケーション用のダミー認証モジュール
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を追加する。
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')

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

  • ログインテンプレートの作成
<!-- ログインページのテンプレート -->
<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コンポーネントで指定する。
// ログインコンポーネント
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を使って、ログイン状態で表示/非表示を切り替える。
<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

  • 新規ユーザー作成画面

image.png

  • 登録されたことを確認

image.png

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

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

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

[v-cloak] {
  display: none
}

まとめ

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

参考
Vue.js入門 基礎から実践アプリケーション開発まで

67
Help us understand the problem. What is going on with this article?
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
tseno
Java、Kotlinのフリーランスエンジニア

Comments

No comments
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account Login
67
Help us understand the problem. What is going on with this article?