アクトインディ開発者ブログ

子供とお出かけ情報「いこーよ」を運営する、アクトインディ株式会社の開発者ブログです

Nuxt.jsのネストした動的ルーティングで困ったので調べてみた

morishitaです。

SPAモードで動作するNuxtアプリケーションでネストした動的ルーティングの動作が思ったのと違ったので調べてみました。

やりたかったこと

Railsのノリで次のようなパス階層をNuxt.jsでやりたいと思いました。

パス 説明
/ サイトトップ。
/groups/:group :groupに対応したグループの詳細。
実際には所属メンバーの一覧
/groups/:group/members/:member グループ:groupに所属する
メンバー:memberの詳細。

:group:memberはそれぞれのID(自然数)です。

複数のグループが存在し、そのそれぞれに所属するメンバーがいるというよくあるアプリケーションです。

期待したこと

公式ドキュメントの動的でネストされたルートを参考に次のようなファイル群を作りました。

pages/
├── index.vue
└── groups
     ├── _group.vue
     └── _group
          └── members
              └── _member.vue

すると、それぞれ次の様に表示されるのだと思っていました。

パス 表示されるモジュール
/ pages/index.vue
/groups/:group pages/groups/_group.vue
/groups/:group/members/:member pages/groups/_group/members/_member.vue

やってみた結果

ブラウザで次のパスにアクセスした場合には期待通りの表示でした。
それぞれ上記に示したVueモジュールの内容が表示されます。

  • /
  • /groups/:group

しかし、/groups/:group/members/:member は期待通りではありませんでした。/groups/:group の内容が表示されるのです。

うーん、どうしてだ? こういうときはドキュメントに立ち返ってみます。
ネストされたルートに次の様の警告が書かれていました。

警告: <nuxt-child/> を親コンポーネント内(.vue ファイル内)に書くことを忘れないでください。

なるほど。実装が足りていなかったのね。
でも、親要素に追加? そうなの? と思いつつやってみました。

/groups/:group/members/:member にアクセスするとpages/groups/_group/members/_member.vueの内容が表示される様にはなりました。
しかし、pages/groups/_group.vueの内容も表示されます。

あれ? これは思ってたのと違うぞ...。

実験

ということでもう少し詳しく調べるために次のような pages/ 以下のファイル構成でNuxtの動作を確認してみました。

pages/
├── index.vue
└── groups/
     ├── index.vue
     ├── _group.vue
     └── _group/
          └── members/
              ├── index.vue
              └── _member.vue

pages/index.vue の内容は次の通り。
他のページコンポーネントの中身も基本的に同じで、FILE_NAMEの値と<style>の色がそれぞれ違う程度です。

<template lang="pug">
div
  section
    h1 FILE: {{fileName}}
    ul
      li ROUTE NAME: {{JSON.stringify($route.name)}}
      li PATH: {{JSON.stringify($route.path)}}
      li PARAMS: {{JSON.stringify($route.params)}}
    nuxt-child
</template>

<script lang="ts">
import { Component, Prop, Vue } from 'nuxt-property-decorator'

const FILE_NAME = 'pages/index.vue' // ファイルごとに異なる

@Component
class GroupIndex extends Vue {
  @Prop({ default: FILE_NAME }) fileName!: string

  asyncData() {
    console.log(`asyncData IN ${FILE_NAME}`)
  }

  async fetch() {
    console.log(`fetch IN ${FILE_NAME}`)
  }

  validate(arg) {
    console.log(`validate IN ${FILE_NAME}`)
    return true
  }

  beforeCreate() {
    console.log(`beforeCreate IN ${FILE_NAME}`)
  }

  created() {
    console.log(`created IN ${this.fileName}`)
  }

  beforeMount() {
    console.log(`beforeMount IN ${this.fileName}`)
  }

  mounted() {
    console.log(`mounted IN ${this.fileName}`)
  }
}
export default GroupIndex
</script>

<style lang="sass" scoped>
section
  border: solid 1px red
  background-color: #FF82B2
  padding: 3px
</style>

ブラウザでアクセスすると次の項目を表示します。

  • 実行されているファイル名
  • Nuxt(Vue-Router)が判定しているルーティング名 ROUTE NAME(=$route.name)
  • アクセスしているパス PATH(=$route.path)
  • パスパラメータ PARAMS(=$route.params)

ライフサイクルの実行順を確認するために、各フックメソッドでconsole.logを実行するようにしています。

Nuxt-ts-sampleにすべてのコードを置いています。

/ の表示

プラウザで / にアクセスにすると次の様に表示されます。

f:id:HeRo:20190703221717p:plain
/ へのアクセス結果

pages/index.vue が表示されているのがわかります。

/groups の表示

/groups にアクセスにすると次の様に表示されます。

f:id:HeRo:20190703221811p:plain
/groups へのアクセス結果

pages/groups/index.vue が表示されています。

/groups/1 の表示

/groups/1 の表示は次の通り。

f:id:HeRo:20190703221958p:plain
/groups/1 へのアクセス結果

表示されるのは pages/groups/_group.vue

ここまでは予想通り、期待通りです。

/groups/1/members の表示

さて、ここからです。
/groups/1members にアクセスにすると次の様に表示されます。

f:id:HeRo:20190703222140p:plain
/groups/1/members へのアクセス結果

なんと、親コンポーネントであるpages/groups/_group.vueの内容と 子のpages/groups/_group/members/index.vueの内容の両方が表示されます。

ライフサイクルメソッドの実行順を Dev Tool の Console の出力で見てみます。
まず、親、子の順でvalidateメソッドが呼ばれます。
その後は親コンポーネントがマウント直前まで処理されます。
そして子コンポーネントがマウント直前まで処理され、準備ができたところで子、親の順でマウントされます。

Vue.js devtoolsで、ルーティングを確認してみます。

f:id:HeRo:20190703222402p:plain
Vue.js devtools

すると to.matched に次の2つのパスが表示されています。

  • "/groups/:group" (pages/groups/_group.vueに対応)
  • "/groups/:group/members" (pages/groups/_group/members/index.vueに対応)

結局、matched と判定されたパスに対応するモジュールが実行されるってことなんですね。なるほど。

/groups/1/members/2 の表示

最後に /groups/1/members/2 を見てみます。

f:id:HeRo:20190703222602p:plain
/groups/1/members/2 へのアクセス結果

子コンポーネントが pages/groups/group/members/member.vue にかわってはいますが、親と子、両方のコンポーネントが表示されています。 /groups/1/membersと同様ですね。

f:id:HeRo:20190703222704p:plain
Vue.js devtools

結局Nuxtのルーティングはどうなっているのか?

Nuxtが /pages 配下のファイルの配置から自動的に構成するルーティング情報の全体を見てみると次の様になっています。

f:id:HeRo:20190703222800p:plain
Vue.js devtools の Routing

/members 以下は /groups/:group の子供として扱われているのがわかります。

そして子パスにアクセスすると親ページコンポーネントも表示されるという...1

解決案

さて、Nuxtのルーティングの仕組みはわかりました。
パスに対応して配置したコンポーネントがちゃんと表示されていることもわかりました。
でも、最初に掲げた「やりたかったこと」の要件的には親コンポーネントの内容は表示したくはありません2

ルーティングの設定を自動に頼らず自前で設定すると思うようにできるのかもですが、面倒なのでしたくないです。

うーん、どうしたものか…。

ちょっと泥臭い感じですが、次の様にすることにしました。

<template lang="pug">
div
  section(v-if="matchName")
    h1 FILE: {{fileName}}
    ul
      li ROUTE NAME: {{JSON.stringify($route.name)}}
      li PATH: {{JSON.stringify($route.path)}}
      li PARAMS: {{JSON.stringify($route.params)}}
  nuxt-child
</template>

<script lang="ts">
import { Component, Prop, Vue } from 'nuxt-property-decorator'

const FILE_NAME = 'pages/groups/_group.vue'

@Component
class Group extends Vue {
  @Prop({ default: FILE_NAME }) fileName!: string

  /**
   * アクセスされているルーティング名がこのコンポーネントのものに一致するかを判定する。
   */
  get matchName(): boolean {
    return this.$route.name === 'groups-group'
  }

  asyncData({ route }) {
    console.log(`asyncData IN ${FILE_NAME}`)
    // 親・子どちらへのアクセスでも実行したい処理

    if (route.name !== 'groups-group') return

    // 親コンポーネントのパスへのアクセス時のみ実行したい処理
  }

  // 以下略
}
export default Group
</script>

親コンポーネントとなる pages/groups/_group.vue に アクセサメソッドmatchNameを追加します3
その中でアクセスされているルーティング名が、コンポーネントのものと一致するかどうかを判定します。
そして親コンポーネントのルート要素にv-if="isMatchName"追加します。これにより自身のパスにアクセスされた場合には親コンポーネントを表示し、子コンポーネントのパスへのアクセス時には親コンポーネントは表示されないようにします。

また、asyncDatafetch メソッド内で子コンポーネントへのアクセス時に実行したくない処理がある場合にはルーティング名で実行の要否を分岐させたい場合もあるでしょう。 ただ、asyncDatafetch メソッド内ではまだコンポーネントのインスタンスが生成されていないのでインスタンスメソッドであるmatchNameが使えません。
上記のコード例の様にそれぞれのメソッドの引数として渡されるrouteからnameを取り出してルーティング名との一致をチェックする必要があります。

もうちょっと他にやりようはないのかなと思いはするのですが、 これでやりたかったことはなんとか実現できそうです4

まとめ

  • Nuxtでは pages 以下のディレクトリ、ページの配置で自動的にルーティングが決められる
  • ネストしたルーティングを使う場合、親コンポーネントに<nuxt-child>を追加する必要あり
  • そのままだと、子コンポーネントのパスへのアクセス時も親コンポーネントが表示されるので一工夫必要

最後に

アクトインディではエンジニアを募集しています。 actindi.net


  1. どうもこれがVue Routerの仕様っぽい。
    ネストされたルート | Vue Router

  2. /groups/:groupとしての共通ヘッダ等があるなら表示されるのは便利な仕様なのだと思います。

  3. nuxt-property-decoratorを使っているのでVue的には算出プロパティとなります。

  4. validateメソッドで falseを返せばそのモジュールは実行されないかと思いきや、to.matchedのモジュールのいずれかでfalseを返した時点で エラーページ行となってしまいます。to.matchedを細工するスマートな方法ないのかな…。Vue Routerをもっと知る必要がありそう。