Closed13

Vue Router Data Loaders の rfc を読む

shingo.sasakishingo.sasaki

背景

データフェッチングにおける銀の弾丸はない。

データフェッチングの戦略や手法には各アプリケーションの UX やアーキテクチャに依存するため、唯一の正解はないので。

ただし、VueRouter における標準的なデータフェッチング改善の仕組みが用意できれば、データフェッチにおける複雑性の低減と柔軟性の向上を実現するプラクティスになるのではと考え提案している。

個人の感想

わかりみが深い。ページコンポーネントにおいてデータフェッチをどのタイミングでどう行うかがにデファクトスタンダードがないし、割と自由奔放に実装されちゃうよね。

shingo.sasakishingo.sasaki

特徴

  • データフェッチをナビゲーションサイクルに自動で結合できるようになる
  • URLパラメータ、ハッシュが書き換わると自動で再フェッチする
  • レスポンスのクライアントキャッシュに時間ベースの有効期限を設け、自動で再フェッチする
  • フェッチに関わる loading/error の状態管理ができる
  • 直列・並列両リクエストに対応

個人の感想

ここだけよむと SWR みたいな仕組みをルートに結合できるって感じかな。わかりやすそう。

shingo.sasakishingo.sasaki

備考

このプロポーザルは Vue Router 4 に関するものだけど、一部の DX 改善系の仕組みは unplugin-vue-router を使うことで、特に型ルーティングに大幅な改善が期待できる

個人の感想

知らないパッケージ出てきた。さきにこれをチェックしておこうか。

shingo.sasakishingo.sasaki

unplugin-vue-router

https://github.com/posva/unplugin-vue-router

Automatically typed file based routing for vue router

んー、 Nuxt みたいなファイルベースルーティングが出来るって話?

https://github.com/posva/unplugin-vue-router/blob/10c2d9e8f6ee4b4f0f3266054d376cd10651cb5b/package.json#L105-L107

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 に近いものってぐらいの認識でいよう。

shingo.sasakishingo.sasaki

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> でコンポーネントの定義を行うって分け方を意識するとスッキリするのかも?

shingo.sasakishingo.sasaki

モチベーション1

現状の VueRouter のナビゲーションガードを使ってデータフェッチをハンドリングするには以下の問題がある

  • meta 属性のセットアップが面倒
  • onBeforeRouteUpdate を使った場合、初回ページ描画時の対応が漏れる
  • beforeRouteEnter を使った場合、next() に型が付かないのと、フェッチしたデータを受け渡すために pinia や vuex のようなデータストアが必要
  • route.params を watch するやりかたは、コンポーネントがデータを持ってない状態でレンダリングされてしまうのと、SSRが不可能

本 rfc のゴールは、これらの観点をよりシンプル、より理解しやすく出来るようにすること。Nuxt のようなフレームワークサポートや、SSRまで視野に入ってる。

個人の感想

ナビゲーションガードを使ったデータフェッチの制御をあんまりやった記憶がないから、この辺のツラミはそんなにわからなかった。

あと弊社プロダクトだと、フェッチしたデータはすべてストア(vuex) で持つようにしてるから、意外と苦労しなかったのかもしれない。

route.params をウォッチするのがしんどいのはわかる。

shingo.sasakishingo.sasaki

モチベーション2

以下はRFCではスコープ対象外だけど、拡張可能なAPIの提供によってユーザーランド上で実装可能になる想定

  • vue-query みたいなキャッシュ機構
  • ページネーションの実装
  • ナビゲーション外での自動リフレッシュ

個人の感想

vue-query は Vue における ReactQuery や SRW みたいなもの。
https://github.com/DamianOsipiuk/vue-query

この RFC の参照実装の時点でそれに近いことを実現してそうだけど、VueQuery のほうはもっと高度な使い方出来るのかな。

shingo.sasakishingo.sasaki

モチベーション3

データフェッチはナビゲーションをブロックすることなく、ナビゲーションサイクルに統合することも目的としており、以下の観点で有用になる

  • コンポーネントマウント前にデータが存在することを保証する
  • ローディング中であることを UI で通知する
  • ページ間を移動する際のスクロールをすぐに出来るようにする
  • フェッチが1回しか起こらないことを保証する
  • 他のデータフェッチの仕組み(e.g. vue-query) と比べて圧倒的に軽量

個人の感想

ここは正直何を言ってるのかよくわからなかった。非同期でデータフェッチするのに、まるで同期でフェッチしてるかのように見せる仕組みがある…?一度アクセスしたページならデータをキャッシュしてるとかそういう話?わかってないけどとりあえず続けて読んでく。

shingo.sasakishingo.sasaki

Detailed design

理想的には unplugin-vue-router と組み合わせて使うことで最高の DX を得ることが出来るけど、それは必須ではない。今のところの実装はこのプラグインにしかないが、以下が実装されている。

  • ページコンポーネントの名前付きエクスポートをチェックして、生成されたルーティングにメタプロパティを設定する
  • ローダーを解決するナビゲーションガードを自動で追加
  • defineLoader 関数

defineLoader 関数は、フェッチしたデータを含んだ Promse を戻す async function を渡す必要がある。

defineLoader の戻り値はコンポーザブルで、すべてのコンポーネントから利用可能だが、ページコンポーネントからエクスポートすることで、 VueRouter のナビゲーションサイクルに統合することができる。

ローダー関数は route を引数で受け取ることができる。

個人の感想

少しずつ見えてきたけど、もしかすると unplugin-vue-router を一度使ってみないとイメージがわかない点が多いのかもしれない。

shingo.sasakishingo.sasaki

ルートテーブルでのページコンポーネントとローダー関数の紐付け。

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 なしでの利用が現実的じゃなく感じてくる。

shingo.sasakishingo.sasaki

ざっと読んだ。
冗長な定義が必要なのはあるけど、一貫したルーティングベースのデータフェッチができる仕組みとしてはあると嬉しいという印象。

このスクラップは2022/09/04にクローズされました