Closed18

unplugin-vue-router を使ってみる

shingo.sasakishingo.sasaki

Automatic file based Routing in Vue with TS support

の通り、VueRouter におけるファイルベースのルーティングと型サポートをビルドタイムで行ってくれるツールらしい。 Nuxt におけるルーティングとの違いはよくわからない。

shingo.sasakishingo.sasaki

一旦 Vite で単純な Vue + VueRouter アプリを用意しよう。

$ yarn create vite --template vue-ts
yarn create v1.22.19
[1/4] 🔍  Resolving packages...
[2/4] 🚚  Fetching packages...
[3/4] 🔗  Linking dependencies...
[4/4] 🔨  Building fresh packages...

success Installed "create-vite@3.0.2" with binaries:
      - create-vite
      - cva
✔ Project name: … unplugin-vue-router-sample
$ yarn add vue-router
$ yarn install

あとは適当にスキャフォルドされたコードを削除していって準備OK

shingo.sasakishingo.sasaki

インストール

$ yarn add -D unplugin-vue-router

devDependencies でいいってことは、やっぱランタイムプラグインじゃなくてビルドタイムでだけ使うのか。

shingo.sasakishingo.sasaki

Vite の設定も指示通りに

vite.config.ts
// vite.config.ts
import VueRouter from 'unplugin-vue-router/vite'

export default defineConfig({
  plugins: [
    VueRouter({
      /* options */
    }),
    // ⚠️ Vue must be placed after VueRouter()
    Vue(),
  ],
})
shingo.sasakishingo.sasaki

お、サーバー起動しただけで勝手に型定義が生成されたぞ。

typed-router.d.ts
// Generated by unplugin-vue-router. ‼️ DO NOT MODIFY THIS FILE ‼️
// It's recommended to commit this file.
// Make sure to add this file to your tsconfig.json file as an "includes" or "files" entry.

/// <reference types="unplugin-vue-router/client" />

import type {
  // type safe route locations
  RouteLocationTypedList,
  RouteLocationResolvedTypedList,
  RouteLocationNormalizedTypedList,
  RouteLocationNormalizedLoadedTypedList,

  // helper types
  // route definitions
  RouteRecordInfo,
  ParamValue,
  ParamValueOneOrMore,
  ParamValueZeroOrMore,
  ParamValueZeroOrOne,

  // vue-router extensions
  _RouterTyped,
  RouterLinkTyped,
  NavigationGuard,
  UseLinkFnTyped,

  // data fetching
  _DataLoader,
  _DefineLoaderOptions,
} from 'unplugin-vue-router'

declare module 'vue-router/auto/routes' {
  export interface RouteNamedMap {
  }
}

declare module 'vue-router/auto' {
  import type { RouteNamedMap } from 'vue-router/auto/routes'

  export type RouterTyped = _RouterTyped<RouteNamedMap>

  /**
   * Type safe version of `RouteLocationNormalized` (the type of `to` and `from` in navigation guards).
   * Allows passing the name of the route to be passed as a generic.
   */
  export type RouteLocationNormalized<Name extends keyof RouteNamedMap = keyof RouteNamedMap> = RouteLocationNormalizedTypedList<RouteNamedMap>[Name]

  /**
   * Type safe version of `RouteLocationNormalizedLoaded` (the return type of `useRoute()`).
   * Allows passing the name of the route to be passed as a generic.
   */
  export type RouteLocationNormalizedLoaded<Name extends keyof RouteNamedMap = keyof RouteNamedMap> = RouteLocationNormalizedLoadedTypedList<RouteNamedMap>[Name]

  /**
   * Type safe version of `RouteLocationResolved` (the returned route of `router.resolve()`).
   * Allows passing the name of the route to be passed as a generic.
   */
  export type RouteLocationResolved<Name extends keyof RouteNamedMap = keyof RouteNamedMap> = RouteLocationResolvedTypedList<RouteNamedMap>[Name]

  /**
   * Type safe version of `RouteLocation` . Allows passing the name of the route to be passed as a generic.
   */
  export type RouteLocation<Name extends keyof RouteNamedMap = keyof RouteNamedMap> = RouteLocationTypedList<RouteNamedMap>[Name]

  /**
   * Generate a type safe params for a route location. Requires the name of the route to be passed as a generic.
   */
  export type RouteParams<Name extends keyof RouteNamedMap> = RouteNamedMap[Name]['params']
  /**
   * Generate a type safe raw params for a route location. Requires the name of the route to be passed as a generic.
   */
  export type RouteParamsRaw<Name extends keyof RouteNamedMap> = RouteNamedMap[Name]['paramsRaw']

  export function useRouter(): RouterTyped
  export function useRoute<Name extends keyof RouteNamedMap = keyof RouteNamedMap>(name?: Name): RouteLocationNormalizedLoadedTypedList<RouteNamedMap>[Name]

  export const useLink: UseLinkFnTyped<RouteNamedMap>

  export function onBeforeRouteLeave(guard: NavigationGuard<RouteNamedMap>): void
  export function onBeforeRouteUpdate(guard: NavigationGuard<RouteNamedMap>): void

  // Experimental Data Fetching

  export function defineLoader<
    P extends Promise<any>,
    Name extends keyof RouteNamedMap = keyof RouteNamedMap,
    isLazy extends boolean = false,
  >(
    name: Name,
    loader: (route: RouteLocationNormalizedLoaded<Name>) => P,
    options?: _DefineLoaderOptions<isLazy>,
  ): _DataLoader<Awaited<P>, isLazy>
  export function defineLoader<
    P extends Promise<any>,
    isLazy extends boolean = false,
  >(
    loader: (route: RouteLocationNormalizedLoaded) => P,
    options?: _DefineLoaderOptions<isLazy>,
  ): _DataLoader<Awaited<P>, isLazy>

  export {
    _definePage as definePage,
    _HasDataLoaderMeta as HasDataLoaderMeta,
    _setupDataFetchingGuard as setupDataFetchingGuard,
    _stopDataFetchingScope as stopDataFetchingScope,
  } from 'unplugin-vue-router/runtime'
}

declare module 'vue-router' {
  import type { RouteNamedMap } from 'vue-router/auto/routes'

  export interface TypesConfig {
    beforeRouteUpdate: NavigationGuard<RouteNamedMap>
    beforeRouteLeave: NavigationGuard<RouteNamedMap>

    $route: RouteLocationNormalizedLoadedTypedList<RouteNamedMap>[keyof RouteNamedMap]
    $router: _RouterTyped<RouteNamedMap>

    RouterLink: RouterLinkTyped<RouteNamedMap>
  }
}
shingo.sasakishingo.sasaki

まだ VueRouter を一切使ってないからここが空なんだな

declare module 'vue-router/auto/routes' {
  export interface RouteNamedMap {
  }
}
shingo.sasakishingo.sasaki

型ファイルが生成されてたので TypeScript でそれを参照するように設定する必要がある

tsconfig.json
{
  // ...
  "include": [/* ... */ "typed-router.d.ts"]
  // ...
}
shingo.sasakishingo.sasaki

ルーターはこう定義すれば、ルートを自動挿入してくれるから手動定義する必要がないらしい

router.ts
import { createRouter, createWebHistory } from "vue-router/auto";

const router = createRouter({
  history: createWebHistory(),
});

export default router;
main.ts
import { createRouter, createWebHistory } from "vue-router/auto";

const router = createRouter({
  history: createWebHistory(),
});

export default router;
shingo.sasakishingo.sasaki

By default, this plugins checks the folder at src/pages for any .vue files and generates the corresponding routing structure basing itself in the file name. This way, you no longer need to maintain a routes array when adding routes to your application, instead just add the new .vue component to the routes folder and let this plugin do the rest!

とのことなので、src/pages にページコンポーネントを作っていく。

shingo.sasakishingo.sasaki

適当にこんなファイルをおいてみた

  • src/pages/index.vue
  • src/pages/users/[id].vue

ビルドすると

$ yarn dev

型ファイルにルートが追加されてる!

declare module 'vue-router/auto/routes' {
  export interface RouteNamedMap {
    '/': RouteRecordInfo<'/', '/', Record<never, never>, Record<never, never>>,
    '/users/[id]': RouteRecordInfo<'/users/[id]', '/users/:id', { id: ParamValue<true> }, { id: ParamValue<false> }>,
  }
}
shingo.sasakishingo.sasaki

ルート生成のルールは Nuxt でお馴染みな感じがするので特に違和感もないかな。
ルートパラメータの型付けをどこまでできるのか。

shingo.sasakishingo.sasaki
export default defineComponent({
  setup() {
    const route = useRoute();
    if (route.name === "/users/[id]") {
      route.params.id
    }
  },
});

こうすると route が確定して route.params.id に型安全アクセスできるんだけど、これも自動でやってくれるものな気がするな。

shingo.sasakishingo.sasaki

あー、これでいいのか。

export default defineComponent({
  setup() {
    const route = useRoute('/users/[id]');
    return { route }
  },
});

流石にこれは手入力いるのかな。補完は効くから手軽ではあるけど。

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