vite を使って Next.js みたいにルーティングしたい

どうも、uzimaru です。

今回は vite を使ってるときの React のルーティングを Next.js みたいにする方法を書こうと思います。

vite とは?

もうだいぶ有名になっているので説明はいらないと思いますが、簡単に。

公式ページから引用すると 次世代フロントエンドツール らしいです。かっこいいですね。

簡単に言うと webpack とか parcel みたいなフロントエンドのビルドツールで、dev-server のときにはネイティブ ESM を使うことでバンドルを最小限に抑えるためめちゃくちゃ早く起動して、めちゃくちゃ早くファイルを更新できるというやつです。爆速開発体験を得ることが出来ます。最高

Next.js みたいなルーティングをする

結論から言うと hannoeru/vite-plugin-pages を使います。

このプラグインは、 Next.js のように pages に置いたファイルを見てルーティングを生成してくれるものです。ダイナミックルーティングにもサポートしており、 PathParams を指定するパターンは next , nuxt , remix から選べます。( React はドキュメントだと experimental と書かれているんですが、いまのところ不具合は無いです)

これを導入して終了!

なのですが、それだと面白くないのでもうちょい書きます。

ルーティングされる Path を列挙する

例えば以下のようなディレクトリ構成だとします

bashpages
├── todo
│  └── [id]
│       ├── index.tsx
│       └── edit.tsx
└── index.tsx

/ にアクセスしたら TodoList が見れて Item をクリックしたら /todo/:id に飛ぶ。 /todo/:id/edit で諸々の編集が出来る。みたいなアプリケーション。

このアプリケーションが取りうる Path は、 / , /todo/:id , /todo/:id/edit です。

TodoList の一覧で /todo/:id に飛ばすために Path を組み立てますが、そのときはこんな感じに書くと思います

tsxconst TodoItem = ({ id, title }) => {
    return (
        
  • {title}
  • ) }

    これでも良いんですが、このアプリケーションに変更が入って、/todo/:id/todo/:userId/:id になったとします。

    そうなると↑のコードでは Invalid な Path になってしまいます。

    ここで考えられるのが取りうる Path をすべて列挙して、それを使って Path を組み立てるという方法。自前でルーティングを定義してる場合はそこから列挙することも可能ですが、今回は自動化してます。自分で書いて定義してもいいが多重管理になってしまうので避けたい。

    そこで vite-plugin-pages に生えてる onRoutesGenerated を使います。

    シグネチャはこんな感じ onRoutesGenerated: (routes: any[]) => Awaitable<any[] | void>

    ドキュメントの routesany[] になってますが、ここには RouteObject[] が入ってきます( RouteObject は react-router のもの)

    こいつを使って取りうる Path を自動的に列挙します。

    コードはこんな感じ

    tsx{
        ...
        onRoutesGenerated(routes: RouteObject[]) {
          const ast = generatePathType(getPathString(routes))
          let source = ts.createSourceFile('d.ts', '', ts.ScriptTarget.Latest)
          source = factory.updateSourceFile(source, [ast])
          const printer = ts.createPrinter()
          const r = printer.printFile(source)
        
          fs.writeFileSync(
            'src/path.d.ts',
            r
          )
        }
        ...
    }
    
    const getPathString = (routes: RouteObject[], base = ''): string[] =>
      routes.flatMap(route => {
        if (route.index) {
          return [base]
        } else if (route.children === undefined) {
          return [`${base}/${route.path}`]
        } else {
          return getPathString(route.children, `${base}/${route.path}`)
        }
      })
    
    const generatePathType = (pathList: string[]) => {
      return factory.createTypeAliasDeclaration(
        undefined,
        [factory.createModifier(ts.SyntaxKind.ExportKeyword)],
        factory.createIdentifier('Path'),
        undefined,
        factory.createUnionTypeNode(
          pathList.map(path => factory.createLiteralTypeNode(factory.createStringLiteral(path)))
        )
      )
    }

    getPathString では RouteObject[] を再帰的に巡って取りうる Path の配列を計算します。

    その値を使って generatePathType で TypeScript の AST を作ります(普通に routes.join('|') でも良いんですが….)

    最後にその AST を使ってファイルに書き込むという流れです。上のコード例では src/path.d.ts に出力されます。

    出力されるファイルはこんな感じ

    tsxexport type Path = '/' | '/todo' | '/todo/:id' | '/todo/:id/edit'

    使うときは

    tsximport { Path } from 'src/path.d'

    で取れるのであとは煮るなり焼くなり好きに使います。

    まとめ

    • hannoeru/vite-plugin-pages を使うと pages に置いたファイルを見てルーティングをしてくれる
    • onRoutesGenerated を使うと RouteObject が生成されたタイミングで RouteObject[] を使ったコードを実行出来る
    • RouteObject[] から Path を列挙することで型安全に Path を組み立てれるようになる