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にすべてのコードを置いています。
/ の表示
プラウザで / にアクセスにすると次の様に表示されます。
pages/index.vue が表示されているのがわかります。
/groups の表示
/groups にアクセスにすると次の様に表示されます。
pages/groups/index.vue が表示されています。
/groups/1 の表示
/groups/1 の表示は次の通り。
表示されるのは pages/groups/_group.vue。
ここまでは予想通り、期待通りです。
/groups/1/members の表示
さて、ここからです。
/groups/1members にアクセスにすると次の様に表示されます。
なんと、親コンポーネントであるpages/groups/_group.vueの内容と 子のpages/groups/_group/members/index.vueの内容の両方が表示されます。
ライフサイクルメソッドの実行順を
Dev Tool の Console の出力で見てみます。
まず、親、子の順でvalidate
メソッドが呼ばれます。
その後は親コンポーネントがマウント直前まで処理されます。
そして子コンポーネントがマウント直前まで処理され、準備ができたところで子、親の順でマウントされます。
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 を見てみます。
子コンポーネントが pages/groups/group/members/member.vue にかわってはいますが、親と子、両方のコンポーネントが表示されています。 /groups/1/membersと同様ですね。
結局Nuxtのルーティングはどうなっているのか?
Nuxtが /pages 配下のファイルの配置から自動的に構成するルーティング情報の全体を見てみると次の様になっています。
/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"
追加します。これにより自身のパスにアクセスされた場合には親コンポーネントを表示し、子コンポーネントのパスへのアクセス時には親コンポーネントは表示されないようにします。
また、asyncData
や fetch
メソッド内で子コンポーネントへのアクセス時に実行したくない処理がある場合にはルーティング名で実行の要否を分岐させたい場合もあるでしょう。
ただ、asyncData
や fetch
メソッド内ではまだコンポーネントのインスタンスが生成されていないのでインスタンスメソッドであるmatchName
が使えません。
上記のコード例の様にそれぞれのメソッドの引数として渡されるroute
からname
を取り出してルーティング名との一致をチェックする必要があります。
もうちょっと他にやりようはないのかなと思いはするのですが、 これでやりたかったことはなんとか実現できそうです4。
まとめ
- Nuxtでは pages 以下のディレクトリ、ページの配置で自動的にルーティングが決められる
- ネストしたルーティングを使う場合、親コンポーネントに
<nuxt-child>
を追加する必要あり - そのままだと、子コンポーネントのパスへのアクセス時も親コンポーネントが表示されるので一工夫必要
最後に
アクトインディではエンジニアを募集しています。 actindi.net
-
どうもこれがVue Routerの仕様っぽい。
ネストされたルート | Vue Router↩ -
/groups/:groupとしての共通ヘッダ等があるなら表示されるのは便利な仕様なのだと思います。↩
-
nuxt-property-decorator
を使っているのでVue的には算出プロパティとなります。↩ -
validateメソッドで
false
を返せばそのモジュールは実行されないかと思いきや、to.matched
のモジュールのいずれかでfalseを返した時点で エラーページ行となってしまいます。to.matched
を細工するスマートな方法ないのかな…。Vue Routerをもっと知る必要がありそう。↩