Vue Router Data Loaders の rfc を読む
概要
VueRouter
にデータフェッチの仕組みを取り入れる rfc が上がっていたので読んでみる。
背景
データフェッチングにおける銀の弾丸はない。
データフェッチングの戦略や手法には各アプリケーションの UX やアーキテクチャに依存するため、唯一の正解はないので。
ただし、VueRouter における標準的なデータフェッチング改善の仕組みが用意できれば、データフェッチにおける複雑性の低減と柔軟性の向上を実現するプラクティスになるのではと考え提案している。
個人の感想
わかりみが深い。ページコンポーネントにおいてデータフェッチをどのタイミングでどう行うかがにデファクトスタンダードがないし、割と自由奔放に実装されちゃうよね。
特徴
- データフェッチをナビゲーションサイクルに自動で結合できるようになる
- URLパラメータ、ハッシュが書き換わると自動で再フェッチする
- レスポンスのクライアントキャッシュに時間ベースの有効期限を設け、自動で再フェッチする
- フェッチに関わる loading/error の状態管理ができる
- 直列・並列両リクエストに対応
個人の感想
ここだけよむと SWR みたいな仕組みをルートに結合できるって感じかな。わかりやすそう。
備考
このプロポーザルは Vue Router 4 に関するものだけど、一部の DX 改善系の仕組みは unplugin-vue-router
を使うことで、特に型ルーティングに大幅な改善が期待できる
個人の感想
知らないパッケージ出てきた。さきにこれをチェックしておこうか。
unplugin-vue-router
Automatically typed file based routing for vue router
んー、 Nuxt みたいなファイルベースルーティングが出来るって話?
peerDependencies
に VueRouter が入ってるから、VueRouter を拡張するもので良さそう。
This build-time plugin simplifies your routing setup and makes it safer and easier to use thanks to TypeScript. Requires Vue Router at least 4.1.0.
ビルドタイムで型生成するって感じなのかな。
とりあえず Nuxt に近いものってぐらいの認識でいよう。
Basic example
- データフェッチを行う関数は
defineLoader
で定義する (以下ローダー) - ローダーはページコンポーネントから export することで、ルートと紐付けることができる
- ページコンポーネントは
<script setup>
では書けないので<script>
を別途書く必要がある
<script lang="ts">
import { getUserById } from '../api'
import { defineLoader } from 'vue-router'
// 任意の関数名でローダーを export する
export const useUserData = defineLoader(async (route) => {
// 任意のデータフェッチ処理
const user = await getUserById(route.params.id)
// 利用したいレスポンスデータを返す
return user
})
// 通常のコンポーネント定義は default export を使う
export default defineComponent({
name: 'custom-name',
inheritAttrs: false
})
</script>
<script lang="ts" setup>
// ローダー関数を呼び出すことで、データフェッチの状態とレスポンスを利用できる
const { data: user, pending, error, refresh } = useUserData()
// data は常に存在し、URLが変更されると再フェッチされる
</script>
-
user
pending
error
は当然 reactive に -
refresh
は明示的に呼び出すことで、再フェッチを強制可能 -
useUserData()
はこのコンポーネントでなくても使用可能 (再利用可能) -
useUserData()
はどこで宣言してもよいが、必ずページコンポーネントで export する - URLクエリ、URLハッシュ変更時に自動で再フェッチ
個人の感想
ページコンポーネントから export するって制約があるのがちょっと引っかかるけど、基本的に使い勝手は良さそうに見える。
<script>
でデータフェッチを定義して、 <script setup>
でコンポーネントの定義を行うって分け方を意識するとスッキリするのかも?
モチベーション1
現状の VueRouter のナビゲーションガードを使ってデータフェッチをハンドリングするには以下の問題がある
-
meta
属性のセットアップが面倒 -
onBeforeRouteUpdate
を使った場合、初回ページ描画時の対応が漏れる -
beforeRouteEnter
を使った場合、next()
に型が付かないのと、フェッチしたデータを受け渡すために pinia や vuex のようなデータストアが必要 -
route.params
を watch するやりかたは、コンポーネントがデータを持ってない状態でレンダリングされてしまうのと、SSRが不可能
本 rfc のゴールは、これらの観点をよりシンプル、より理解しやすく出来るようにすること。Nuxt のようなフレームワークサポートや、SSRまで視野に入ってる。
個人の感想
ナビゲーションガードを使ったデータフェッチの制御をあんまりやった記憶がないから、この辺のツラミはそんなにわからなかった。
あと弊社プロダクトだと、フェッチしたデータはすべてストア(vuex) で持つようにしてるから、意外と苦労しなかったのかもしれない。
route.params
をウォッチするのがしんどいのはわかる。
モチベーション2
以下はRFCではスコープ対象外だけど、拡張可能なAPIの提供によってユーザーランド上で実装可能になる想定
-
vue-query
みたいなキャッシュ機構 - ページネーションの実装
- ナビゲーション外での自動リフレッシュ
個人の感想
vue-query
は Vue における ReactQuery や SRW みたいなもの。
この RFC の参照実装の時点でそれに近いことを実現してそうだけど、VueQuery のほうはもっと高度な使い方出来るのかな。
モチベーション3
データフェッチはナビゲーションをブロックすることなく、ナビゲーションサイクルに統合することも目的としており、以下の観点で有用になる
- コンポーネントマウント前にデータが存在することを保証する
- ローディング中であることを UI で通知する
- ページ間を移動する際のスクロールをすぐに出来るようにする
- フェッチが1回しか起こらないことを保証する
- 他のデータフェッチの仕組み(e.g. vue-query) と比べて圧倒的に軽量
個人の感想
ここは正直何を言ってるのかよくわからなかった。非同期でデータフェッチするのに、まるで同期でフェッチしてるかのように見せる仕組みがある…?一度アクセスしたページならデータをキャッシュしてるとかそういう話?わかってないけどとりあえず続けて読んでく。
Detailed design
理想的には unplugin-vue-router
と組み合わせて使うことで最高の DX を得ることが出来るけど、それは必須ではない。今のところの実装はこのプラグインにしかないが、以下が実装されている。
- ページコンポーネントの名前付きエクスポートをチェックして、生成されたルーティングにメタプロパティを設定する
- ローダーを解決するナビゲーションガードを自動で追加
-
defineLoader
関数
defineLoader
関数は、フェッチしたデータを含んだ Promse を戻す async function を渡す必要がある。
defineLoader
の戻り値はコンポーザブルで、すべてのコンポーネントから利用可能だが、ページコンポーネントからエクスポートすることで、 VueRouter のナビゲーションサイクルに統合することができる。
ローダー関数は route
を引数で受け取ることができる。
個人の感想
少しずつ見えてきたけど、もしかすると unplugin-vue-router
を一度使ってみないとイメージがわかない点が多いのかもしれない。
ということで先に、本rfc の内容は抜きにして unplugin-vue-router
を単体で使って遊んでみることに。
ルートテーブルでのページコンポーネントとローダー関数の紐付け。
import { LoaderSymbol } from 'vue-router'
const routes = [
{
path: '/users/:id',
component: () => import('@/pages/UserDetails.vue'),
meta: {
// 👇 v array of all the necessary loaders
[LoaderSymbol]: [() => import('@/pages/UserDetails.vue')]
},
},
// Named views must include all page component lazy imports
{
path: '/users/:id',
components: {
default () => import('@/pages/UserDetails.vue'),
aux () => import('@/pages/UserDetailsAux.vue'),
},
meta: {
[LoaderSymbol]: [() => import('@/pages/UserDetails.vue'), () => import('@/pages/UserDetailsAux.vue')],
},
},
// Nested routes follow the same pattern, declare an array of lazy imports relevant to each routing level
{
path: '/users/:id',
component: () => import('@/pages/UserDetails.vue'),
meta: {
[LoaderSymbol]: [() => import('@/pages/UserDetails.vue')],
},
children: [
{
path: 'edit',
component: () => import('@/pages/UserEdit.vue'),
meta: {
[LoaderSymbol]: [() => import('@/pages/UserEdit.vue')],
},
}
]
},
]
This is pretty verbose and that's why it is recommended to use unplugin-vue-router to make this completely automatic: the plugin generates the routes with the symbols and loaders.
なるほどなぁ。 unplugin-vue-router
ならディレクトリツリーベースで自動でルートテーブルを生成するから、これも一切不要になるってことか。
これを考えると unplugin-vue-router
なしでの利用が現実的じゃなく感じてくる。
ざっと読んだ。
冗長な定義が必要なのはあるけど、一貫したルーティングベースのデータフェッチができる仕組みとしてはあると嬉しいという印象。