LoginSignup
11
5

More than 3 years have passed since last update.

【設定1行】Nuxt.jsとNext.jsの内部リンクを型で正確に取得できるTS製の最強ライブラリ「pathpida」

Posted at

2020年10月に以下の記事でちょっと話題になった frourio の開発者です。
憧れのTypeScriptフルスタック環境がコマンド1発で作れる超軽量フレームワーク「frourio」

1年前くらいに自分のプロジェクトで使うために開発した「pathpida」がリニューアルでメチャクチャ便利になったのでみなさんにも紹介します。
pathpidaはNuxt.jsとNext.jsそれぞれのルーティング規約に最適化しているのでたった1行の設定で型安全に使うことが出来ます。

logo.png

どんな問題を解決するのか

以下のように /post/1 に遷移する nuxt-link があるとします。

<template>
  <nuxt-link :to="link" />
</template>

<script lang="ts">
import Vue from 'vue'

export default Vue.extend({
  data() {
    return {
      postId: 1
    }
  },
  computed: {
    link() {
      return `/post/${this.postId}`
    }
  }
})
</script>

この時、/post/{this.postId} が内部リンクとして適切かどうかを静的に型検査することはできません。
もし pages/post/_postId.vue というファイルが存在しなければページ遷移に失敗します。

TypeScript4.1で追加されたTemplate Literal Typesを使おうとしてもpagesにある大量のファイルパスを手書きするのは困難です。
開発途中でURLが変更になりpages内のファイル名を変えた際に、膨大なソースコード内から目視で古いパスを探し出して書き換える作業もみな経験してきたことでしょう。

そんな問題を解決するのがpathpidaです。
名前の通り、pagesディレクトリを見て内部リンク取得用クライアントを自動生成してくれる非常に賢いライブラリ。

例えば、pagesディレクトリが以下の状態のとき

pages/post/_postId.vue
pages/post/index.vue
pages/index.vue

以下のTypeScriptファイルが自動生成されます。

plugins/$path.ts
export const pagesPath = {
  post: {
    _postId: (postId: number | string) => ({
      $url: () => ({ path: `/post/${postId}` })
    }),
    $url: () => ({ path: '/post' })
  },
  $url: () => ({ path: '/' })
}

見ての通りpagesPathのプロパティがpagesのファイルと対応。
$urlメソッドが返すオブジェクトはそのまま nuxt-link$router に渡すことが出来ます。

コンポーネントで使うイメージはこんな感じ

components/Sample.vue
<template>
  <nuxt-link :to="link" />
</template>

<script lang="ts">
import Vue from 'vue'

export default Vue.extend({
  data() {
    return {
      postId: 1
    }
  },
  computed: {
    link() {
      return this.$pagesPath.post._postId(this.postId).$url()
    }
  }
})
</script>

もちろん nuxt-link に直接セットすることもできます。

components/Sample.vue
<template>
  <nuxt-link :to="$pagesPath.post._postId(postId).$url()" />
</template>

<script lang="ts">
import Vue from 'vue'

export default Vue.extend({
  data() {
    return {
      postId: 1
    }
  }
})
</script>

とっても手軽かつ型安全に内部リンクを扱うことが出来るのです!

Nuxt.jsで使ってみよう

Nuxt.js + TypeScriptの環境がすでに出来ている前提で説明を進めます。
Next.jsユーザーは以下の記事を参考にしてください。
Zenn - 設定0行でNext.jsとNuxt.jsの内部リンクを型安全に取得できる最強ライブラリ「pathpida」

pathpidaの他にnpm-run-allもインストールします。
依存関係ではないのですが、開発中はnpm-run-allがあると便利です。

$ yarn add pathpida npm-run-all --dev
package.json
{
  "scripts": {
    "dev": "run-p dev:*",
    "dev:nuxt": "nuxt-ts",
    "dev:path": "pathpida --watch",
    "build": "pathpida && nuxt-ts build"
  }
}

後で plugins/$path.ts が自動生成されるので nuxt.config.js(ts) に設定を1行追加します。

nuxt.config.js
{
  plugins: ['~/plugins/$path']
}

devコマンドでNuxt.jsと一緒にpathpidaも監視モードで起動します。

$ yarn dev

plugins/$path.ts ファイルが自動生成されます。

pages/
  index.vue
plugins/
  $path.ts
plugins/$path.ts
import { Plugin } from '@nuxt/types'

export const pagesPath = {
  $url: () => ({ pathname: '/' })
}

const pathPlugin: Plugin = (_, inject) => {
  inject('pagesPath', pagesPath)
}

export default pathPlugin

あとはpagesディレクトリにファイルを増やすたびに plugins/$path.ts ファイルが書き変わります。
※Nuxt.jsの場合、pathpidaはハイフンから始まるファイルを無視する

VueコンポーネントやVuexで this.$pagesPath を呼び出して nuxt-link$router と今まで通りに組み合わせて使えます。

pages/
  articles/
    _id.vue
  users/
    _userId.vue
  index.vue
plugins/
  $path.ts
components/ActiveLink.vue
<template>
  <div>
    <nuxt-link :to="$pagesPath.articles._id(1).$url()" />
    <div @click="onclick" />
  </div>
</template>

<script lang="ts">
import Vue from 'vue'

export default Vue.extend({
  methods: {
    onclick() {
      this.$router.push(this.$pagesPath.users._userId(1).$url())
    }
  }
})
</script>

必須クエリを指定

pages配下のコンポーネントでQuery型をexportするだけでクエリを指定できます。
例えば、 /users?userId={number} というページを作るには

pages/users.vue
<template>
  <div />
</template>

<script lang="ts">
import Vue from 'vue'

+ export type Query = {
+  userId: number
+ }

export default Vue.extend({
})
</script>

と書くだけです。他のコンポーネントからは

<template>
  <div @click="onclick" />
</template>

<script lang="ts">
import Vue from 'vue'

export default Vue.extend({
  methods: {
    onclick() {
      this.$router.push(this.$pagesPath.users.$url({ query: { userId: 1 }))
    }
  }
})
</script>

このようにクエリをセットできます。
注意点は、Query型に他の型の参照を含んではいけないということ。
VueファイルからQueryをexportしているものの、 plugins/$path.ts ではimport出来ないのです。
そこでpathpidaはQuery型を正規表現で plugins/$path.ts にコピペする処理をしています。

plugins/$path.ts
import { Plugin } from '@nuxt/types'

+ export type Query = {
+  userId: number
+ }

export const pagesPath = {
  users: {
    $url: (url: { query: Query }) => ({ path: '/users', query: url.query })
  }
}

const pathPlugin: Plugin = (_, inject) => {
  inject('pagesPath', pagesPath)
}

export default pathPlugin

なので以下のように型参照を含んでいると型でエラーになります。

pages/users.vue
<template>
  <div />
</template>

<script lang="ts">
import Vue from 'vue'

+ type UserId = number

+ export type Query = {
+  userId: UserId
+ }

export default Vue.extend({
})
</script>
plugins/$path.ts
import { Plugin } from '@nuxt/types'

+ export type Query = {
+  userId: UserId // UserIdが見つからなくてエラー
+ }

export const pagesPath = {
  users: {
    $url: (url: { query: Query }) => ({ path: '/users', query: url.query })
  }
}

const pathPlugin: Plugin = (_, inject) => {
  inject('pagesPath', pagesPath)
}

export default pathPlugin

TypeScript Compiler APIで何とかすることも出来そうですが、クエリは単純な型が一般的で参照を使わずとも運用回避できると考えシンプルに正規表現でコピペする方法を採用しました。

オプションのクエリを指定

上記の方法だと、クエリのプロパティ全てをオプショナルにしても$urlに空のオブジェクトを渡す必要があります。
クエリを渡すことそのものをオプショナルにするにはOptionalQuery型をexportします。

pages/users.vue
<template>
  <div />
</template>

<script lang="ts">
import Vue from 'vue'

+ export type OptionalQuery = {
+  userId: number
+ }

export default Vue.extend({
})
</script>

これで$urlを空で呼ぶことが可能になります。

<template>
  <div @click="onclick" />
</template>

<script lang="ts">
import Vue from 'vue'

export default Vue.extend({
  methods: {
    onclick() {
      this.$router.push(this.$pagesPath.users.$url({ query: { userId: 1 }))
      this.$router.push(this.$pagesPath.users.$url())
    }
  }
})
</script>

ハッシュを指定

$urlメソッドの引数に好きな文字列のhashプロパティを渡すだけです。
# は自動で付与されます。

<template>
  <div @click="onclick" />
</template>

<script lang="ts">
import Vue from 'vue'

export default Vue.extend({
  methods: {
    onclick() {
      this.$router.push(this.$pagesPath.users.$url({ query: { userId: 1 }, hash: 'hoge' ))
      this.$router.push(this.$pagesPath.users.$url({ hash: 'fuga' }))
    }
  }
})
</script>

staticディレクトリの静的ファイルのパスも型安全に取得

pathpidaコマンドにenableStaticオプションを渡すと、staticディレクトリの内容からstaticPathクライアントを生成します。

package.json
{
  "scripts": {
    "dev": "run-p dev:*",
    "dev:nuxt": "nuxt-ts",
    "dev:path": "pathpida --enableStatic --watch",
    "build": "pathpida --enableStatic && nuxt-ts build"
  }
}

publicにJSONとPNG画像がある想定

plugins/$path.ts

static/aa.json
static/bb/cc.png

staticPathクライアントは$urlメソッドが無くて直接パス文字列を取得できます。

<template>
  <img :src="$staticPath.bb.cc_png" />
</template>

<script lang="ts">
import Vue from 'vue'

export default Vue.extend({
  created() {
    console.log(this.$staticPath.aa_json) // /aa.json
  }
})
</script>

今後の展開

年末年始の休暇中にSapper(Svelte)のパス生成にも対応する予定です。
そのあとサンプルコードが揃ったらcreate-frourio-appに組み込みます。

そして2021年は・・・限界まで資金を投入してfrourioエコシステムの海外展開を行います!
Railsを倒せるかどうかに関心はありませんが、TypeScriptのフルスタックフレームワークと言えばfrourio一強という未来を実現したいですね。

最後まで読んでいただきありがとうございました。
GitHubにスターを押して行ってもらえるとオープンソース開発の活力になります!
https://github.com/aspida/pathpida

サポート

年末年始もNetflixを観ながらダラダラとコードを書いて過ごすだけなのでpathpidaやfrourioの質問・要望をお気軽にください。(魔女の旅々はいいぞ)

11
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
5