Next.js のソースコードリーディングメモ (next/router, next/link編)
vercel.json
のgithub
オプションについては、以下を参照
まずは /packages/next/client/router.ts
から読んでみる
/packages/next/shared/lib/router.ts
からいろいろ import されていて、その中を見ると、 NextRouter
の型定義がされてる
export type NextRouter = BaseRouter &
Pick<
Router,
| 'push'
| 'replace'
| 'reload'
| 'back'
| 'prefetch'
| 'beforePopState'
| 'events'
| 'isFallback'
| 'isReady'
| 'isPreview'
>
Pickは初めて見が、Tから任意のプロパティだけを抽出した型を作るためのtypeか
client/index.tsx
の中で AppContainer
コンポーネントにて
<RouterContext.Provider value={makePublicRouterInstance(router)}>
<HeadManagerContext.Provider value={headManager}>
<ImageConfigContext.Provider
value={process.env.__NEXT_IMAGE_OPTS as any as ImageConfigComplete}
>
{children}
</ImageConfigContext.Provider>
</HeadManagerContext.Provider>
</RouterContext.Provider>
普段、useRouterが使えるのも、RouterContextとして、ルーターのインスタンスがprovideされているからということがわかった。
ルーターのインスタンスは、makePublicRouterInstance(router)
で生成されている。
routerは index.tsx のL.181でletで定義されている。
値が代入されるのは initNext
関数の中で、createRouter
関数が呼び出されて、その結果が代入されている。
createRouter
関数の中ではsingletonRoute.routerの初期化が行われてそれが返されている。
一方routerを引数にとっている、makePublicRouterInstance
はNextRouter型のインスタンスを生成していそう。
NextRouterの型は
export type NextRouter = BaseRouter &
Pick<
Router,
| 'push'
| 'replace'
| 'reload'
| 'back'
| 'prefetch'
| 'beforePopState'
| 'events'
| 'isFallback'
| 'isReady'
| 'isPreview'
>
なので、内部で使っていたRouter型のインスタンスに対して、APIで公開するメソッドを限定するのが目的っぽい。
makePublicRouterInstance
は Router → NextRouterへの変換を担っているわけか
makePublicRouterInstance
のコードをもうちょっと見てみると
// Events is a static property on the router, the router doesn't have to be initialized to use it
instance.events = Router.events
イベントとして、Router.eventsを定義していそう
で、Router.eventsを見てみると
static events: MittEmitter<RouterEvent> = mitt()
mittとはなんぞや?
イベント管理の仕組みっぽい。subscribeはonメソッド、publishはemitメソッドを呼び出すことで動く模様
ちなみに、RouterEventは
const routerEvents = [
'routeChangeStart',
'beforeHistoryChange',
'routeChangeComplete',
'routeChangeError',
'hashChangeStart',
'hashChangeComplete',
] as const
export type RouterEvent = typeof routerEvents[number]
と、定義されている。
enumを定義するときは、個人的にはオブジェクトでやることが多かった。配列でやるパターンは初めて見た。
makePublicRouterInstance
関数の上には // This function is used to create the
withRouter router instance
というコメントが、、
useRouterは使うが、withRouterってなんやっけ?
ということで調べてみる
公式のサンプルには
If useRouter is not the best fit for you, withRouter can also add the same router object to any component.
import { withRouter } from 'next/router'
function Page({ router }) {
return <p>{router.pathname}</p>
}
export default withRouter(Page)
と書かれている。 If useRouter is not the best fit for you
というのがどういう状況かわからないが、クラスコンポーネントの中では useRouter
が使えないので、クラスコンポーネントを使っている場合は、withRouterを使うという選択肢がありそう。
一方、サンプルは関数コンポーネントで書かれているので、うーんとなる。
話を戻して、withRouterの実装を確認してみる。next/client/with-router.tsx
の中で定義されてた。
export default function withRouter<
P extends WithRouterProps,
C = NextPageContext
>(
ComposedComponent: NextComponentType<C, any, P>
): React.ComponentType<ExcludeRouterProps<P>> {
function WithRouterWrapper(props: any): JSX.Element {
return <ComposedComponent router={useRouter()} {...props} />
}
WithRouterWrapper.getInitialProps = ComposedComponent.getInitialProps
// This is needed to allow checking for custom getInitialProps in _app
;(WithRouterWrapper as any).origGetInitialProps = (
ComposedComponent as any
).origGetInitialProps
if (process.env.NODE_ENV !== 'production') {
const name =
ComposedComponent.displayName || ComposedComponent.name || 'Unknown'
WithRouterWrapper.displayName = `withRouter(${name})`
}
return WithRouterWrapper
}
useRouterを使って取り出したrouterインスタンスを直下のコンポーネントにpropsとして渡しているだけじゃん!
フックではなくHOCで実装していた頃の名残で残っていると考えても良さそう
client側のrouterの挙動はちょっとわかったので、次はLinkコンポーネントを見てみる
next/client/link.tsx
を見ていく
Linkコンポーネントを見てみる。
ぱっと見、前半はvalidationチェックとかっぽい。
return されているのは React.cloneElement(child, childProps)
それぞれ見ていく
child
は child = React.Children.only(children)
と定義されてる。
React.Children自体初めて見た
React.Children はデータ構造が非公開の this.props.children を扱うためのユーティリティを提供します。
React.Children.only
はというと
children が 1 つの子要素しか持たないことを確認し、結果を返します。そうでない場合、このメソッドはエラーを投げます。
リンクコンポーネントの下にaタグが複数あったりすると、処理できないからこういうassertion的な処理を入れているのか
次は childProps
を見ていく。次のように定義されている。
const childProps: {
onMouseEnter?: React.MouseEventHandler
onClick: React.MouseEventHandler
href?: string
ref?: any
} = {
ref: setRef,
onClick: (e: React.MouseEvent) => {
if (process.env.NODE_ENV !== 'production') {
if (!e) {
throw new Error(
`Component rendered inside next/link has to pass click event to "onClick" prop.`
)
}
}
if (child.props && typeof child.props.onClick === 'function') {
child.props.onClick(e)
}
if (!e.defaultPrevented) {
linkClicked(e, router, href, as, replace, shallow, scroll, locale)
}
},
}
childProps.onMouseEnter = (e: React.MouseEvent) => {
if (child.props && typeof child.props.onMouseEnter === 'function') {
child.props.onMouseEnter(e)
}
if (isLocalURL(href)) {
prefetch(router, href, as, { priority: true })
}
}
onMouseEnter
もオブジェクトの中で定義しても良い気がするが、まあいいか
onClick
から見ていく
if (child.props && typeof child.props.onClick === 'function') {
child.props.onClick(e)
}
if (!e.defaultPrevented) {
linkClicked(e, router, href, as, replace, shallow, scroll, locale)
}
子コンポーネントのonClick属性が実行される。
その処理の中でpreventDefaultされていなければ、linkClicked
が実行される模様。
linkClicked
を確認してみる
function linkClicked(
e: React.MouseEvent,
router: NextRouter,
href: string,
as: string,
replace?: boolean,
shallow?: boolean,
scroll?: boolean,
locale?: string | false
): void {
const { nodeName } = e.currentTarget
// anchors inside an svg have a lowercase nodeName
const isAnchorNodeName = nodeName.toUpperCase() === 'A'
if (isAnchorNodeName && (isModifiedEvent(e) || !isLocalURL(href))) {
// ignore click for browser’s default behavior
return
}
e.preventDefault()
// replace state instead of push if prop is present
router[replace ? 'replace' : 'push'](href, as, {
shallow,
locale,
scroll,
})
}
aタグの場合に、何かキーを押しながらリンクをクリックしたり、新しいタブを開いたり、外部のドメインのURLがリンク先の場合はearly returnされて、そうでない場合はページが遷移される。
ここまでの流れ的に、aタグでonClick属性が与えられていればその挙動があればそれが優先されるし、もしそれがなければデフォルトの挙動としてページ遷移が発生すると考えれば良さそう
では次に、childProps.onMouseEnter
を見ていく
気になるのは prefetch
なので、それを見ていく
function prefetch(
router: NextRouter,
href: string,
as: string,
options?: PrefetchOptions
): void {
if (typeof window === 'undefined' || !router) return
if (!isLocalURL(href)) return
// Prefetch the JSON page if asked (only in the client)
// We need to handle a prefetch error here since we may be
// loading with priority which can reject but we don't
// want to force navigation since this is only a prefetch
router.prefetch(href, as, options).catch((err) => {
if (process.env.NODE_ENV !== 'production') {
// rethrow to show invalid URL errors
throw err
}
})
const curLocale =
options && typeof options.locale !== 'undefined'
? options.locale
: router && router.locale
// Join on an invalid URI character
prefetched[href + '%' + as + (curLocale ? '%' + curLocale : '')] = true
}
router.prefetchを呼び出して、完了したら、prefechedという名前のオブジェクトに値をtrueとして格納していく感じっぽい
もうちょっと詳しく見たいので、router.prefetch
を見てみる
処理の中身はここらへん
await Promise.all([
this.pageLoader._isSsg(route).then((isSsg: boolean) => {
return isSsg
? fetchNextData(
this.pageLoader.getDataHref({
href: url,
asPath: resolvedAs,
ssg: true,
locale:
typeof options.locale !== 'undefined'
? options.locale
: this.locale,
}),
false,
false, // text
this.sdc,
true
)
: false
}),
this.pageLoader[options.priority ? 'loadPage' : 'prefetch'](route),
])
Promise.allの対象の配列の1つ目の要素では、ssgのページか判定して(_isSsg
)、ssgページであればfetchするという処理を行っている
2つ目の要素は、priorityによってloadPageかprefetchかに切り替えている
自分だったら
options.priority ? this.pageLoader.loadPage(route) : this.pageLoader.prefetch(route)
感じで書いちゃうが、こういう書き方もできたんだ、、
_isSsg
の挙動が気になってちょっと調べてみる。
処理の中身はこんな感じ
this.promisedSsgManifest.then((manifest) => manifest.has(route))
promisedSsgManifest に routeが登録されているか確認している
は next/client/page-loader.ts
で定義されてた。
this.promisedSsgManifest = new Promise((resolve) => {
if (window.__SSG_MANIFEST) {
resolve(window.__SSG_MANIFEST)
} else {
window.__SSG_MANIFEST_CB = () => {
resolve(window.__SSG_MANIFEST!)
}
}
})
windowオブジェクトの__SSG_MANIFESTがssgManifestか。
この __SSG_MANIFEST
に値が設定がされている場所がよくわからなかったが、
を見てみると、ビルド時に_ssgManifest.js
ファイルが生成されている模様(自分のnext.jsのアプリでもそうなっているようだった)
この中にSSGされたページのURLが記載されていて、アプリケーション起動時にこのファイルを読み込んでいる?(未検証)
next/linkも一旦気が済んだので、おしまい
next/linkを読んだことで、
の記事もすんなりと理解できるようになった