Vue.jsやReactといったJSフレームワークにはルーティングという機能が実装されています。ルーティングとはHTTPのリクエストに応じて、ページを遷移させる機能のことで、これを利用すれば、自由にページ内情報(ユーザーインターフェース)を切り替えることができます。
ただし、これらのコンポーネントは親子関係を持っていないので、階層のあるコンポーネント処理と同じようにしてデータの受け渡しができません。そこで、ルーティングを使っても、自在にデータを受け渡しして、かつデータの同期が取れないかを調べてみました。
その結果、わかったことは数通りあります。一般的な方法は2のようですが。
- 方法1:ルーティングを制御するrouter.jsのプロパティを用いて、データを受け渡す
- 方法2:親コンポーネントからprovideを使って、データを受け渡す
今回のテストに用いる制御プログラム
ルーティング機能を用いた簡易買い物かご機能になります(Reactで記述して公開していたソースを改造したものを、更にそのままVue3に書き換えたものです)。
main.jsからアクセスしている主コンポーネントはGlobalState、ルーティング元はその子コンポーネントに当たるMainNavigationで、そのルーティング先にProducts.vue、Cart.vue、Articles.vueがそれぞれ置かれています。また、ナビ部分はルーティング先ではないですが、そこにデータを受け渡す方法も説明します。
■src
- ■css(デザイン制御用、表示は割愛)
- ■pages
- MainNavigation.vue //子コンポーネント(ナビ部分。ここにルーティング用のリンクを記述)
- Products.vue //商品一覧(ここに商品を入れるボタンがある)
- Cart.vue //買い物かご(ここに商品差し戻し、購入のボタンがある)
- Articles.vue //お買い上げリスト
- ShopContext.vue //オブジェクト情報の格納
- GlobalState.vue //親コンポーネント
- Reducers.vue //共通の処理制御用
- router.js //ルーティング制御用
※■はディレクトリ
方法1 router.jsのプロパティを用いて、データを受け渡す
この方法でルーティングでリンクされているコンポーネントを、データ同期処理できます。ただ、そこで送られたデータはあくまで値渡し用であり、これを同期できるようにする方法がどこにも載っておらず悪戦苦闘しました。それで公式ガイドや開発系記事などを参考にして一旦は解決しましたので、備忘録として記しておきます。
A:ルーティング先にデータを受け渡す
ルーティング設定
まずはVue3でルーティングを実施するためにはいくつかの初期設定が必要です。
npmかyarnでvue-routerをインストール(例はnpm)
#npm install vue-router@4
※Vue3でvue-routerを使用する場合、Vue-router4以上が必要で、それにはvue3.2以上が必要になります。適宜バージョンをアップさせてインストールしましょう。
main.jsにvue-router使用の明記
インストールができたらmain.jsにvue-router使用を付記しておきます。
import App from './GlobalState.vue' //親となるコンポーネント
import router from './router' //vue-routerの呼び出し
/*中略*/
const app = createApp(App)
app.use(router) //これを追記して、vue-routerを使用できるようにする
これが親コンポーネントです。ここに紐づけた子コンポーネントMainNavigationにルーティング先を制御しています。
<template>
<MainNavigation /><!-- これがルーティングを制御した子コンポーネント -->
</template>
<script>
import MainNavigation from './pages/MainNavigation.vue'
export default {
components:{
MainNavigation
}
}
</script>
ルーティングの仕組み
ここでVueにおけるルーティングの簡単なおさらいですが、vue-routerにおけるルーティングの仕組みの特色として、Reactとの違いは表示場所を記述する必要がある点にあります。router-linkタグが遷移用リンクになり、toで遷移先を指定できます。そして、router-viewタグに遷移先が表示されます。
<template>
<main class="main-navigation">
<ul>
<!-- `router.js` で定義したルーティングルールとの紐付けを行っている -->
<li><router-link to="/">Products</router-link></li>
<li><router-link to="/cart">Cart()</router-link></li>
<li><router-link to="/articles">Articles()</router-link></li>
</ul>
</main>
<router-view></router-view><!--ここに表示される-->
</template>
そして、ルーティング制御用のスクリプトは以下のようになっています。vue2までとはかなり記述ルールが異なっているので、注意が必要です。pathが遷移先、nameが外部ファイル名、componentが呼び出し用のコンポーネント名になります。そして、リンクさせるコンポーネントに対しては、必ずimportコマンドでファイルを呼び出しておく必要があります。
import {onMounted} from "vue"
import {createRouter,createWebHistory} from "vue-router"
import Products from "./pages/Products.vue" //ルーティングに用いたいファイル
import Cart from "./pages/Cart.vue" //ルーティングに用いたいファイル
import Articles from "./pages/Articles.vue" //ルーティングに用いたいファイル
const routes = [
{
path: "/",
name: "products",
component: Products,
},
{
path:"/cart",
name: "cart",
component: Cart,
},
{
path:"/articles",
name:"articles",
component: Articles,
},
];
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes: routes,
})
export default router
データをルーティング先に受け渡す
ここからが本番です。前述したようにルーティング元とルーティング先は親子関係を持っていないため、v-bind、v-onディレクティブを用いたコンポーネント間における値の受け渡しはできません。ページ番号など単一のプリミティブ変数は、v-bind:toディレクティブ(略称:to)で対応できるのですが、今回受け渡したいのは一定のデータ量を持ったオブジェクトなので、そこに渡すわけにはいきません。そこで、変数を効率よくルーティング先に受け渡す方法がないかを調べてみたところ、公式サイトにヒントとなる記述がありました。
Passing Props to Route Components
どうやらルーティングの制御にpropsを設定できるようです。そして、試しに一つだけ商品を代入してみると、トップページに表示することができました。
{
path: "/",
name: "products",
component: Products,
props: { products:[{ id: "p1", title: "花王 バブ ゆず", price: 60, stock: 10 }]}
},
ルーティング先はこうなっており、propsのproductsの中に、router.jsからのオブジェクトが受け渡されています。
<template>
<main class="products">
<ul>
<li v-for="(product,i) in products" :key="i">
<div >
<strong>{{ product.title }}</strong> - {{ product.price}}円
<template v-if="product.stock > 0 "> 【残り{{product.stock}}個】 </template>
</div>
<div>
<button @click="rdc.reducer('add',product,storage)">かごに入れる</button>
</div>
</li>
</ul>
</main>
</template>
<script>
export default {
props:{
products,
}
</script>
ですが、propsに逐一記述しておくのは面倒なので、どこかに変数を格納しておいて、それを呼び出せないかを試してみました。それでexport default内やら色々と変数の置き場を探してみたのですが、なかなか見つからず、そんなときふと思いついたようにrouter.jsはスクリプトなので、スクリプト内に書き込んでみたところ、一旦は成功しました。
//直接変数を書き込む
const products =[
{ id: "p1", title: "花王 バブ ゆず", price: 60, stock: 10 },
{ id: "p2", title: "バスクリン きき湯", price: 798 , stock: 3 },
{ id: "p3", title: "アース 温素 琥珀の湯", price: 980, stock: 2 },
{ id: "p4", title: "白元アース いい湯旅立ちボトル", price: 398, stock: 6 },
{ id: "p5", title: "クラシエ 旅の宿", price: 598, stock: 7 }
]
const routes =
{
path: "/",
name: "products",
component: Products,
props: { storage: products}
}
変数を格納した外部ファイルから呼び出したデータをルーティング元から受け渡す
ですが、書き込んだ先はあくまでルーティング制御用のスクリプトであり、極力他のデータを載せてソースを汚したくないので、使用する変数全部を外部から取得できないかを調べてみた
ところ、その方法が載っていたので、それを活用してみました(jsファイルにしても行けるみたいです)。
この外部ファイル化は後でも活用します。
【Vue】メソッドを外部のjsファイルに移動する方法。外部ファイルの処理結果をvueに戻す。
<script>
const Storage = ()=>{
return{
products:[
{ id: "p1", title: "花王 バブ ゆず", price: 60, stock: 10 },
{ id: "p2", title: "バスクリン きき湯", price: 798 , stock: 3 },
{ id: "p3", title: "アース 温素 琥珀の湯", price: 980, stock: 2 },
{ id: "p4", title: "白元アース いい湯旅立ちボトル", price: 398, stock: 6 },
{ id: "p5", title: "クラシエ 旅の宿", price: 598, stock: 7 }
],
cart: [],
articles: [],
money: 10000,
total: 0, //残額
}
}
export default Storage
</script>
returnされた内容が定数Storageに格納されます。このStorageを外部ファイルとして呼び出しメソッドとして実行、変数storageに格納した後はpropsプロパティを使って受け渡すだけです。
import Storage from "./pages/shopContext.vue" //外部から呼び出したメソッド
const storage = Storage() //メソッドを実行してオブジェクトを格納
※ただし、これはあくまで暫定的な処置(そのままだとオブジェクトの同期をとることができません)なので、ルーティング先同士でデータの同期を取るにはまだ工夫が必要になります。
ルーティング先同士でデータの同期を取る
ここからが問題です。ルーティング先同士は何度も説明したように親子関係を持っていないので、propsを用いたデータのやり取りができません。なので、どうやってオブジェクトの同期を取ればいいのか調べてみたところ、オブジェクトを参照渡しすればいけるようです。
同期データを参照渡しで受け渡す方法
Vue3で子コンポーネントに同期データを受け渡す方法ですが、ひとまずreactiveメソッドで値渡しにしておく必要があり、それを子コンポーネントからtoRefsメソッドで受け取ることで実現できます。実はルーティング先にデータを受け渡す場合も同じ
方法が使用できます。
※これもテストした結果ですが、データ格納ファイルshopContext.vueにreactive制御を実行してもproxy制御がかかってうまくいきません。
import Storage from "./pages/shopContext.vue"
import {reactive } from "vue" //リアクティブメソッドを呼び出し
const storage = Storage() //外部ファイルに格納したデータを格納
const state = reactive({ //格納したオブジェクトstorageをリアクティブメソッドで値渡しにする
storage:storage
})
const routes = [
{
path: "/",
name: "products",
component: Products,
props: { storage: state.storage} //値渡しされたオブジェクト
},
]
<script>
import { toRefs } from 'vue' //toRefsメソッドの呼び出し
export default {
props:{
storage: Object,
},
setup(props){
const { storage } = toRefs(props) //toRefsメソッドを用いて参照渡しで受け取る
}
}
</script>
こうすることで、ルーティング先には同期データが格納されるようになりますが、ここでまた解決しておいた方が望ましい問題が発生します。
メソッドの一元化
このシステムには買い物かごに入れる機能と、買い物かごから商品を戻す機能及び購入して買い物かごを空にして、所持品に格納する機能がそれぞれ、products.vueとcart.vueと別々の場所にあります。これを別々のファイルで制御したりするとデータのやり取りが複雑になってしまいます。
そこで、ReactのuseReducerのようにメソッドを一つの外部ファイルで一元化できないか調べてみたところ、先程の外部メソッドを呼び出す方法が使えました。そこでreducers.vueという外部ファイルを作成し、メソッド処理を一元化してみました。
<template>
<main class="products">
<ul>
<li v-for="(product,i) in storage.products" :key="i">
<div >
<strong>{{ product.title }}</strong> - {{ product.price}}円
<template v-if="product.stock > 0 "> 【残り{{product.stock}}個】 </template>
</div>
<div>
<button @click="rdc.reducer('add',product,storage)">かごに入れる</button>
</div>
</li>
</ul>
</main>
</template>
<script>
import { toRefs } from 'vue'
import Reducers from "./reducers.vue" //共通の処理用メソッド呼び出し
export default {
props:{
storage: Object,
},
setup(props){
const { storage } = toRefs(props)
const rdc = Reducers() //共通の処理用メソッド管理
return{
rdc
}
}
}
</script>
<style scoped>
@import '../css/products.css';
</style>
外部ファイルのreducerメソッドをルーティング先で呼び出すにはrdcオブジェクト(reducers.vueを呼び出したオブジェクト)から呼び出す必要があります。
<button @click="rdc.reducer('add',product,storage)">かごに入れる</button>
ここで呼び出しているreducers.vueファイルですが、ここではひとまず処理の分岐を行うために、reducerというメソッドの引数に(機能,選択されたオブジェクト,受け渡されたstorage)を渡して処理を分岐しています。そしてcompositionAPIは逐一戻り値をreturnしなくてもいいので、処理用のメソッドを記入しておくだけで十分です。
また、compositionAPIは分割代入を用いてデータを処理するので、各メソッドでReact用に記述したものを使い回すだけで使用できました。更にVue3のcompositionAPIにおける優れた点は参照渡ししておくと、後はどこで処理を行っても自動で同期を取ってくれる点です。
また、このシステムのメソッドは別記事用に作ったReact用の使いまわしなんですが、compositionAPIになって変数が参照渡しで制御するようになったため、そのままメソッドを使いまわしたりできます。
※実は、Angularでも使い回すことができます。
※各種制御用のメソッドは割愛
<script>
import { ref } from 'vue'
const Reducers= ()=>{
const storage = ref(null)
setup:{
/*各種処理用メソッドは割愛*/
const reducer = (mode,selected,storage)=>{
switch(mode){
case "add": addProductToCart(selected,storage) //買い物かごに追加
break
case "remove": removeProductFromCart(selected,storage) //買い物かごから除去
break
case "buy": buyIt(selected,storage) //購入
break
}
}
return{
reducer
}
}
}
export default Reducers
</script>
※実は、やってることはコンポーザブルと全く同じことがわかりました。ただuseReducerという名称にするのはちょっと紛らわしい(Reactのフックと同じなので)ので、あえてルールに従わない名称にしています。
router.jsからpropプロパティではなく、metaプロパティから値を受け渡す方法
ルーティングによるデータの受け渡し方法を調べていたときに、propsからではなく、metaプロパティから値を受け渡す方法もあるみたいです。その場合は、useRouteメソッド(useRouterメソッドではない)を用いれば、受け取れるようです。
useRouteメソッドは現在のルーティング情報(プロパティ)を取得するためのメソッドなので、そのmetaプロパティに値を格納することで、ルーティング先から受け取ることができます。
参考になった記事
const storage = Storage()
const state = reactive({
storage:storage
})
const routes = [
{
path: "/",
name: "products",
component: Products,
meta: { storage: state.storage}
},
useRouteメソッドでmetaデータを受け取る
import {useRoute} from 'vue-router'
import Reducers from "./reducers.vue"
export default {
setup(){
const current = useRoute() //useRouteでルーティングからメタデータを受け取る
const storage = current.meta.storage //受け渡されたstorage
const rdc = Reducers()
return{
rdc,storage
}
}
}
※useRouterメソッドについては方法2で後述しますが、働きは全く違います。useRouterメソッドはルーティング制御スクリプトではなく、親コンポーネントからルーティング先にデータを受け渡したい場合に用います。
B:イベントバスを自作して任意のファイルにデータを受け渡す
あとはナビゲーションバーに対し買い物かごの個数を制御する変数だけですが、ここはルーティング先ではないナビゲーションバーに存在
するので今までの渡し方だと同期データを反映できません。かといって親子コンポーネント化してしまうと、ルーティング制御が邪魔をして、二重にコンポーネントが作られたりしてしまいます。
そこで、親子間でも兄弟間でもないコンポーネント同士でデータの受け渡しができないか調べてみたところ、イベントバスを利用すると行けるようです。ところが、それらはVue2のoptionsAPIでの記事ばかりでVue3のcompositionAPIを用いた方法がなかなか見つかりません。詳しく調べたみたところ、公式にはVue3で廃止とあり、補足的にmittライブラリなどを使用してくれと記述されているに過ぎませんでした。ですが、Stack Overflowにそれを用いた有効な記事が見つかったので、それに倣って作成してみました。
<template>
<main class="main-navigation">
<ul>
<!-- `router.js` で定義したルーティングルールとの紐付けを行っている -->
<li><router-link to="/">Products</router-link></li>
<li><router-link to="/cart">Cart({{"ここに個数を表示したい"}})</router-link></li>
<li><router-link to="/articles">Articles({{"ここに個数を表示したい"}})</router-link></li>
</ul>
</main>
<router-view></router-view>
</template>
イベントバスの準備
上記のようにVue3でイベントバスを利用するにはmittというライブラリを外部からインストールする必要があります。これは割とすんなりインストールしてくれました。
#npm install --save mitt
次はmain.jsにmittを使用するという追記が必要です。
import { createApp } from 'vue'
import App from './GlobalState.vue'
import router from './router'
import mitt from 'mitt' //これを追記。mittライブラリの呼び出し
const emitter = mitt() //これを追記。mittライブラリをメソッド化
const app = createApp(App)
app.use(router)
app.config.globalProperties.emitter = emitter; //これを追記。appに紐づけ
app.mount('#app')
イベントバス用のコンポーザブルを作成する
これが、この議題の一番の要点で、前述したように通常ではcompositionAPIでイベントバスは使用できません。それを利用するために、処理用のイベントバススクリプトを自作、つまり独自のコンポーザブルファイルを作成していたようです(回答者の文章を読むと「定義ファイルを作成」とあります)。そこで、参照先通りに全く同じ定義ファイルを作成しました。
import { ref } from "vue";
const bus = ref(new Map());
export default function useEventsBus(){
function emit(event, ...args) {
bus.value.set(event, args);
}
return {
emit, //これが受け渡し用のメソッドとなる
bus //これが受け取り用のメソッドとなる
}
}
データの転送元の設定
これを転送元と転送先で読み込ませ、追記していきます。当初はルーティング先に設定していたのですが、共通の処理用ファイルreducers.vue内に記述すれば、あらゆるタイミングで同期を取ってくれることがわかったので、かなり効率的に記述することができました。
定義ファイルはuseEventBusというコンポーザブルで呼び出し、それをemitというメソッドで転送を実行します(書き方は子コンポーネント転送時に用いるemitと同じになっており、任意のキーfrom-route
を用いて、データの転送を行います)。
※もし、子コンポーネントからも転送する場合は予約語emitの名称が重複してしまうので、上記定義ファイルの戻り値をemit2などに変更しておく必要があります。
<script>
import useEventBus from '../eventBus.js' //先程作成したイベントバス制御用コンポーザブル
const Reducers= ()=>{
setup:{
//データ転送の実行
const {emit} = useEventBus() //これを使って転送用メソッド呼び出す
const setEmit = (storage)=>{
const cnt_cart = storage.cart.length
const cnt_articles = storage.articles.length
emit('from-route',[cnt_cart,cnt_articles]) //子コンポーネント転送のメソッドと同じ書き方
}
/*中略*/
const reducer = (mode,selected,storage)=>{
switch(mode){
case "add": addProductToCart(selected,storage)
break
case "remove": removeProductFromCart(selected,storage)
break
case "buy": buyIt(selected,storage)
break
}
setEmit(storage) //転送処理の実行
}
return{
reducer,setEmit
}
}
}
export default Reducers
</script>
データの受取先の設定
では受取先の記述方法ですが、これも上の記事を参考に作成しました。
<template>
<main class="main-navigation">
<ul>
<!-- `router.js` で定義したルーティングルールとの紐付けを行っている -->
<li><router-link to="/">Products</router-link></li>
<li><router-link to="/cart">Cart({{state.cnt_cart}})</router-link></li>
<li><router-link to="/articles">Articles({{state.cnt_articles}})</router-link></li>
</ul>
</main>
<router-view></router-view>
</template>
<script>
import {reactive,watch } from 'vue'
import useEventBus from '../eventBus.js'
export default {
setup(){
let state = reactive({
cnt_cart: null,
cnt_articles: null,
})
const { bus } = useEventBus()
watch(()=>bus.value.get('from-route'),(val)=>{
const vals = val ?? []
let values = vals[0]
state.cnt_cart = values[0]
state.cnt_articles = values[1]
})
return{
state
}
}
}
</script>
大事な処理は以下の部分です。共通のキーfrom-route
を用いてデータを受け取り、それを変数に代入することで、テンプレート上の変数に代入が可能になります。
<script>
import {reactive,watch } from 'vue' //監視プロパティを追記
export default {
setup(){
let state = reactive({
cnt_cart: null,
cnt_articles: null,
})
const { bus } = useEventBus() //今度は受取処理なのでメソッドをbusとする
//監視プロパティを用いて同じキーでデータをキャッチ
watch(()=>bus.value.get('from-route'),(val)=>{
const vals = val ?? []
let values = vals[0] //転送オブジェクトが格納されているインデックスは0
state.cnt_cart = values[0] //カートの個数
state.cnt_articles = values[1] //購入商品の個数
})
return{
state
}
}
}
</script>
※初期読込用にproducts.vueのライフサイクルフックにも同じ転送用メソッドを設定しておくと、ロード時に0が反映されます。
<script>
export default {
props:{
storage: Object,
},
setup(props){
const { storage } = toRefs(props)
const rdc = Reducers()
onMounted(()=>{
rdc.setEmit(props.storage) //初期動作用
})
return{
rdc
}
}
}
</script>
しっかりナビ部分にもデータ同期が行き渡り、個数(商品数)が反映されました(本来なら商品個数を乗せるべきですが)。
方法2:親コンポーネントからprovideを使って、データを受け渡す
この方法は、以下のページを参考にしたもので、親コンポーネントからルーティング先という兄弟コンポーネントに対しても、provideとinjectという便利なデータ入出力メソッドを使えば、データの受け渡しができるようです。また、この方法だと前述のイベントバスも不要なので、よりスリムな記述形式で実現できました。
イベントバスが廃止になったのも、この非常に利便性の高い2つの入出力メソッドが実装されたからです(更に言えば、この2つのお陰でVuexを実装しなくても、このようなデータの状態管理ができるようになります)。
親コンポーネントの記述
親コンポーネントGlobalState.vueに対し、以下の記述をしていきます。また、この方法だとルーターを経由しないので、どの兄弟コンポーネントからでもデータをtoRefsメソッドなしで受け取ることができます。
provideメソッドは名の通り、データを共通キーによって分配できるという便利なデータ受け渡し用メソッドで、以下のようになっています。また、データは必ずreactiveメソッドを用いて値渡しにしておく必要があります。
ただし、provideメソッドはsetup関数直下でしか使用できません。またreactiveで値渡ししたものしかinjectで受け取ることができず、同期も取れないので注意してください。
provide(任意のキー,受け渡すデータ)
<template>
<MainNavigation />
</template>
<script>
import MainNavigation from './pages/MainNavigation.vue'
import Storage from "./pages/shopContext.vue"
import {provide,reactive} from "vue"
export default {
components:{
MainNavigation
},
setup(){
const state = reactive({storage:Storage()})
provide('key',state) //データを兄弟コンポーネントに受け渡す。keyは任意の共通キー
}
}
</script>
ただ、このままではルーティング先で同期データを受け取ることができません(promiseの規約違反)。なので、ここから二通りのいずれかの方法が必要になります。
- useRouterメソッドを使う
- Suspenseコンポーネントを使う
useRouterを用いて、ルーティング先にデータを受け渡す
まずは、一般的な方法です。それが方法1で軽く触れたuseRouterメソッドを用いる方法で、**ルーティング先にデータを読み込ませることができます。useRouterはReactのフックっぽい名前ですが、vue-router内のメソッドで、ルーティングを経由してデータを受け渡しするデータ保持のためのメソッドです。
※あくまで中継用のプロトタイプを作成するだけなので、パラメータやメタ情報を取得したり、ルーティング情報を取得したりするuseRouteメソッドとは働きが異なります。
具体的には
import {useRouter } from 'vue-router'
export default{
setup(){
const router = useRouter() //ルーター中継用のプロトタイプ作成
const hogeFunc ()=>{
router.push('/') //ルーティング先に転送
}
}
}
これでデータはルーティング内でやりとりできます。したがって、方法1のようにreducers.vueにメソッドを仕掛けておけば、兄弟コンポーネント間で、自在にデータを同期できます。
また、データ同期はrouter.push('/')で、データ更新をルーターに通知することで実現できます。
<script>
import { reactive } from 'vue'
import {useRouter } from 'vue-router'
const Reducers= ()=>{
setup:{
const router = useRouter() //中継用のプロトタイプ作成
/*中略*/
const reducer = (mode,selected,storage)=>{
switch(mode){
case "add": addProductToCart(selected,storage)
break
case "remove": removeProductFromCart(selected,storage)
break
case "buy": buyIt(selected,storage)
break
}
router.push('/') //データ更新をルーターに通知
}
return{
reducer
}
}
</script>
※router.push(ルーティング先,パラメータ)は第二引数にパラメータを代入することで、新規データを受け渡すこともできたりします。また、第一引数も任意のパスを記述することで、送りたい場所を調節することもできます。
※次はreactiveを経由しない方法で、Suspenseというコンポーネントが試験的に導入されています。
Suspenseコンポーネントを用いて、ルーティング先にデータを受け渡す
方法はルーティング先をSuspenseコンポーネントで囲むだけです。これでSuspenseコンポーネントを経由させることで、ルーティング先の兄弟コンポーネントからも値を受け取ることができるわけです。
※ただ、これは公式マニュアルにもあるように、suspenseというコンポーネントがまだ実験的な機能であるため、本番で使用するのは差し控えるようにという注意があるので、参考ということで記載しておきます。
<template>
<Suspense>
<MainNavigation />
</Suspense>
</template>
<script>
import MainNavigation from './pages/MainNavigation.vue'
import Storage from "./pages/shopContext.vue"
import {provide,reactive} from "vue"
export default {
components:{
MainNavigation
},
setup(){
const state = {storage:Storage()}
provide('key',state)
}
}
</script>
データの受け取り
データの受け取りはinjectメソッドを用いて受け取ります。injectは引数に先程provideメソッドの第1引数で受け渡しに用いた共通のキーを代入することで、第2引数のデータをそのまま受け取ることができます。これをsetupメソッドの中で展開して、returnで返すだけです。これで、方法1の記事ではイベントバスを用いる必要があったナビ部分も、同じ方法で受け渡すことが可能です。この部分はuseRouterメソッドでもSuspenseコンポーネントでも同じです。
const data = inject(provideで設定したキー)
<template>
<main class="products">
<ul>
<li v-for="(product,i) in storage.products" :key="i">
<div >
<strong>{{ product.title }}</strong> - {{ product.price}}円
<template v-if="product.stock > 0 "> 【残り{{product.stock}}個】 </template>
</div>
<div>
<button @click="rdc.reducer('add',product,storage)">かごに入れる</button>
</div>
</li>
</ul>
</main>
</template>
<script>
import { inject} from 'vue'
import Reducers from "./reducers.vue"
export default {
setup(){
const {storage} = inject('key')
const rdc = Reducers()
return{
rdc,storage
}
}
}
</script>
詳細ページを作成(パラメータのやりとり)
パラメータをやりとりする際にはuseRouterとuseRouteがそれぞれ必要になります。おさらいですが、
- useRouter ネットワークにパラメータを受け渡す側
- useRoute ネットワークからパラメータを受け取る側
このように覚えればいいでしょう(Angularのrouterとrouteの関係と同じようなものです)。
パラメータを転送する
パラメータを転送する場合、URLに変数をそのまま埋め込もうとしてもうまくいきません。なので、テンプレートにクリックイベントを記述し、そのイベントからuseRouterオブジェクトが持つpush
メソッドでパラメータを埋め込むようにすればうまくいきます。
<template>
<main class="products">
<ul>
<li v-for="(product,i) in storage.products" :key="i">
<div >
<router-link to="/" @click="getId(product.id)"><strong>{{ product.title }}</strong></router-link>
- {{ product.price}}円
<template v-if="product.stock > 0 "> 【残り{{product.stock}}個】 </template>
</div>
<div>
<button @click="rdc.reducer('add',product,storage)">かごに入れる</button>
</div>
</li>
</ul>
</main>
</template>
<script>
import { inject,onMounted} from 'vue'
import {useRouter } from 'vue-router' //使用するのはuseRouter
import Reducers from "./reducers2.vue"
export default {
setup(){
const router = useRouter() //プロトタイプ作成
const {storage} = inject('key')
const rdc = Reducers()
//クリックイベント
const getId = (id)=>{
id = id.replace("p","")
router.push(`/detail/${id}`) //pushメソッドでパラメータごと埋め込む
}
return{
rdc,storage,viewDetail
}
}
}
</script>
受信設定
router.jsには以下のように追記しておきましょう。
import Detail from "./pages/Detail.vue" //コンポーネントを追記
const routes = [
{
path: "/",
name: "products2",
component: Products,
},
//ルーティング情報を追記
{
path: "/detail/:id", //パラメータを以下のように記述しておけば、:idで受け取ることができる
name: "detail",
component: Detail,
},
]
パラメータを受け取る
では、詳細情報を記述したコンポーネントでパラメータの受け取ります。今度は受け取り側なのでuseRouteを使用し、パラメータを取得します(route.params.idが:idと同値)。それをonMountedフック内で作業し、変数をstateで返せば、変数を展開することができます。
<template>
<ul>
<li>{{ state.item.title }}</li>
</ul>
</template>
<script>
import { inject,reactive,onMounted} from 'vue'
import { useRoute } from 'vue-router'
export default{
setup(){
const {storage} = inject('key')
const state = reactive({item:[]})
const route = useRoute() //ルーティング情報の取得
onMounted(()=>{
const id = route.params.id //パラメータ情報を取得
const selid = `p${id}` //検索idと一致させる
const item = storage.products.find((item)=>item.id === selid)
state.item = item
})
return{
state
}
}
}
</script>
クエリパラメータから受け渡しする
今度はクエリパラメータからgetで取得するように書き換えてみます。書き換えが必要な部分だけをピックアップしました。Vueはrouter.pushに自在にパラメータを埋め込める上に、取得もroute.queryから自在にクエリパラメータを取得できるので、比較的書き換えが楽です。
const getId = (id)=>{
router.push(`/detail?id=${id}`) //送信するパスを書き換える
}
{
path: "/detail", //getの場合はパラメータ表記不要
name: "detail2",
component: Detail,
},
onMounted(()=>{
const selid = route.query.id //今度はqueryプロパティに属している
const item = storage.products.find((item)=>item.id === selid)
state.item = item
})