設定0行でNext.jsとNuxt.jsの内部リンクを型安全に取得できる最強ライブラリ「pathpida」
Qiita TypeScript Advent Calendar 2020 最終日の記事です。
TypeScript製の内部リンク取得ライブラリ「pathpida」
最近ちょっと話題になった frourio と aspida を開発したSolufaです。
Zenn初投稿を記念して、新作ライブラリ「pathpida」を紹介します。
と言っても完全な新作ではなく、初回リリースからもうすぐ1年が経ち月間DL数は1,000を超えています。自分が関わる案件だけで静かに検証を続け、ようやっと今週全面リニューアルして一般告知が出来るようになりました!
pathpidaはNext.jsとNuxt.jsそれぞれのルーティング規約に最適化しているので設定不要で型安全に使うことが出来ます。
どんな問題を解決するのか
以下のように/post/1
に遷移するLinkがあるとします。
import Link from 'next/link'
export default () => {
const url = `/post/${1}`
return <Link href={url} />
}
この時、/post/{pid}
が内部リンクとして適切かどうかを静的に型検査することはできません。
もしpages/post/[pid].tsx
というファイルが存在しなければページ遷移に失敗します。
TypeScript4.1で追加されたTemplate Literal Typesを使おうとしてもpagesにある大量のファイルパスを手書きするのは困難です。
開発途中でURLが変更になりpages内のファイル名を変えた際に、膨大なソースコード内から目視で古いパスを探し出して書き換える作業もみな経験してきたことでしょう。
そんな問題を解決するのがpathpidaです。
名前の通り、pagesディレクトリを見てaspidaライクな内部リンク取得用クライアントを自動生成してくれる非常に賢いライブラリ。
例えば、pagesディレクトリが以下の状態のとき
pages/[pid].tsx
pages/blog/[...slug].tsx
pages/index.tsx
以下のTypeScriptファイルが自動生成されます。
export const pagesPath = {
_pid: (pid: number | string) => ({
$url: () => ({ pathname: '/[pid]', query: { pid }})
}),
blog: {
_slug: (slug: string[]) => ({
$url: () => ({ pathname: '/blog/[...slug]', query: { slug }})
})
},
$url: () => ({ pathname: '/' })
}
見ての通りpagesPathのプロパティがpagesのファイルと対応。
$urlメソッドが返すオブジェクトはそのまま next/link
や next/router
に渡すことが出来ます。
コンポーネントで使うイメージはこんな感じ
import Link from 'next/link'
import { pagesPath } from '../lib/$path'
export default () => {
return <Link href={pagesPath.blog._slug(['a', 'b', 'c']).$url()} />
}
とっても手軽かつ型安全に内部リンクを扱うことが出来るのです!
Next.jsで使ってみよう
Next.js + TypeScriptの環境がすでに出来ている前提で説明を進めます。
pathpidaの他にnpm-run-allもインストールします。
依存関係ではないのですが、開発中はnpm-run-allがあると便利です。
$ yarn add pathpida npm-run-all --dev
{
"scripts": {
"dev": "run-p dev:*",
"dev:next": "next dev",
"dev:path": "pathpida --watch",
"build": "pathpida && next build"
}
}
devコマンドでNext.jsと一緒にpathpidaも監視モードで起動します。
$ yarn dev
utils
または lib
ディレクトリどちらかに $path.ts
ファイルが自動生成されます。
※両方存在しない場合はlibディレクトリを自動作成
※v0.10.0で src/pages
src/lib
にも対応
lib/
$path.ts
pages/
_app.tsx
index.tsx
export const pagesPath = {
$url: () => ({ pathname: '/' })
}
あとはpagesディレクトリにファイルを増やすたびに lib/$path.ts
ファイルが書き変わります。
※Next.jsの場合、pathpidaはpages/apiディレクトリとアンダースコアから始まるファイルを無視する
pagesPathをインポートしてNext.jsのlinkやrouterを今まで通りに使えます。
lib/
$path.ts
pages/
articles/
[id].tsx
users/
[...userInfo].tsx
_app.tsx
index.tsx
import Link from 'next/link'
import { useRouter } from 'next/router'
import { pagesPath } from '../lib/$path'
function ActiveLink() {
const router = useRouter()
const handleClick = () => {
router.push(pagesPath.users._userInfo(['mario', 'hello', 'world!']).$url())
}
return <>
<div onClick={handleClick}>Hello</div>
<Link href={pagesPath.articles._id(1).$url()}>
World!
</Link>
</>
}
export default ActiveLink
必須クエリを指定
pages配下のコンポーネントでQuery型をexportするだけでクエリを指定できます。
例えば、 /user?userId={number}
というページを作るには
export type Query = {
userId: number
}
export default () => <div />
と書くだけです。
import { pagesPath } from '../lib/$path'
pagesPath.user.$url({ query: { userId: 1 }})
このようにクエリをセットできます。
オプションでクエリを指定
上記の方法だと、プロパティ全てをオプショナルにしても$urlに空のオブジェクトを渡す必要があります。
クエリを渡すことそのものをオプショナルにするにはOptionalQuery型をexportします。
export type OptionalQuery = {
userId: number
}
export default () => <div />
これで$urlを空で呼ぶことが可能になります。
import { pagesPath } from '../lib/$path'
pagesPath.user.$url({ query: { userId: 1 }})
pagesPath.user.$url()
ハッシュを指定
$urlメソッドの引数に好きな文字列のhashプロパティを渡すだけです。
#
は自動で付与されます。
import { pagesPath } from '../lib/$path'
pagesPath.user.$url({ query: { userId: 1 }, hash: 'hoge' })
pagesPath.user.$url({ hash: 'fuga' })
publicディレクトリの静的ファイルのパスも型安全に取得
pathpidaコマンドにenableStaticオプションを渡すと、publicディレクトリの内容からstaticPathクライアントを生成します。
{
"scripts": {
"dev": "run-p dev:*",
"dev:next": "next dev",
"dev:path": "pathpida --enableStatic --watch",
"build": "pathpida --enableStatic && next build"
}
}
publicにJSONとPNG画像がある想定
public/aa.json
public/bb/cc.png
lib/$path.ts or utils/$path.ts
staticPathクライアントは$urlメソッドが無くて直接パス文字列を取得できます。
import Link from 'next/link'
import { staticPath } from '../lib/$path'
console.log(staticPath.aa_json) // /aa.json
export default () => <img src={staticPath.bb.cc_png} />
Nuxt.jsならVue/Vuexインスタンスに対応
Nuxt.js + TypeScriptで使う場合は1行だけ設定追加が必要です。(タイトル詐欺…)
{
plugins: ['~/plugins/$path']
}
これだけでVue/Vuexインスタンスから $pagesPath
でアクセスできます。
※enableStaticオプションを使えばstaticのファイルパスも $staticPath
で取得可能
<template>
<div>
<nuxt-link :to="$pagesPath.post._pid(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.post._pid(1).$url())
}
}
})
</script>
pathpidaはプロジェクトルートに nuxt.config.js(ts)
があればNuxt.js、なければNext.jsだと判定します。
Next.jsとNuxt.jsでは$path.tsの内容が異なるので注意してください。
※Nuxt.jsの場合、pathpidaはハイフンから始まるファイルを無視する
※VueファイルのQuery型をTSファイルでimport出来ない都合上、Nuxt.jsでは型参照を持つQueryを指定することが出来ない(詳細はNuxt.js環境で$path.tsのQuery定義箇所を参照)
今後の展開
年末年始の休暇中にSapper(Svelte)のパス生成にも対応する予定です。
そのあとサンプルコードが揃ったらcreate-frourio-appに組み込みます。
そして2021年は・・・限界まで資金を投入してfrourioエコシステムの海外展開を行います!
Railsを倒せるかどうかに関心はありませんが、TypeScriptのフルスタックフレームワークと言えばfrourio一強という未来を実現したいですね。
最後まで読んでいただきありがとうございました。
GitHubにスターを押して行ってもらえるとオープンソース開発の活力になります!
サポート
年末年始もNetflixを観ながらダラダラとコードを書いて過ごすだけなのでpathpidaやfrourioの質問・要望をお気軽にください。(魔女の旅々はいいぞ)
Discussion