navigateTo()、router.push()、window.location.hrefの違いを知ろう
🎠 はじめに
Nuxtで開発をしていく上で navigateTo() にも router.push() にも window.location.href にも出会った。
全員やっていること一緒じゃない...??と思ったので自分なりに整理してみた備忘録。
🎢 【急いでいる人向け】 まとめてみるとこんな感じ
| navigateTo() | router.push() | window.location.href | |
|---|---|---|---|
| 同一ホスト内の画面遷移 | ✅ | ✅ | ✅ |
| 画面遷移時の表示方法 | クライアントサイドルーティング | クライアントサイドルーティング | フルページリロード |
| 外部URLへの画面遷移 | ✅ | ❌ | ✅ |
| サーバー側での実行 | ✅ | ❌ | ❌ |
Nuxtで開発しているのであれば navigateTo() を使うのが無難そう。
🎪 登場人物の紹介
navigateTo()
Nuxtに設定されているルーティング機能。
// 通常遷移 /hoge
navigateTo('/hoge');
// クエリ付き /hoge?test=123
navigateTo({ path: '/hoge', query: { test: '123' } });
// 外部遷移 https://example.com
navigateTo('https://example.com', { external: true });
また、以下のようにmiddlewareでリダイレクトをかけることができる
export default defineNuxtRouteMiddleware(async (to) => {
if (to.path !== '/fuga') {
return navigateTo('/fuga', { redirectCode: 301 });
}
});
router.push()
Vueの公式プラグインであるVue Routerの機能。
const router = useRouter();
// 通常遷移 /hoge
router.push('/hoge');
// クエリ付き /hoge?test=123
router.push({ path: '/hoge', query: { test: '123' } });
外部URLの遷移はできない。
window.location.href
遷移先のURLを送るので文字列で送る感じ。外部URLも検知して遷移してくれる。
// 通常遷移 /hoge
window.location.href = '/hoge';
// クエリ付き /hoge?test=123
window.location.href = '/hoge?test=123';
// 外部遷移 https://example.com
window.location.href = 'https://example.com';
🎡 違いを考える上での整理めも
そもそもnavigateTo()って
ベースの挙動は router.push() なのでできることや表示方法が似ている部分が多い。
参考: この記事の最後に書きました
同一ホスト内の画面遷移
これは言わずもがなみんなできる
画面遷移時の表示方法
クライアントサイドルーティングとフルページリロードに分かれる。
Vue Routerの説明に以下が記載されている。
Vue Router is the official client-side routing solution for Vue.
Client-side routing is used by single-page applications (SPAs) to tie the browser URL to the content seen by the user. As users navigate around the application, the URL updates accordingly, but the page doesn't need to be reloaded from the server.
訳してみると
Vue Router は Vue の公式クライアントサイドルーティングソリューションです。
クライアントサイドルーティングは、シングルページアプリケーション(SPA)においてブラウザの URL をユーザーに表示されるコンテンツと結びつけるために使用されます。ユーザーがアプリケーション内を移動すると、URL がそれに応じて更新されますが、ページをサーバーから再読み込みする必要はありません。
とのこと。
つまり画面遷移をするときはSPAのようにコンテンツの差分を取得して埋め込む形で対応していく。
この形にすることで真っ白ページの時間がなくなり、ユーザーから見たらスムーズに画面遷移している形に見える。
対してフルページリロードは 遷移前のページを破棄→遷移先のページを1から構成 という流れになるのでページを読み込む時間が発生する。
外部URLへの画面遷移
router.push() は外部URLへの遷移ができない。
下記のように指定すると 'hoge' を指定した時のように相対パスで遷移する。
// [現在のパス名]/https://example.com に遷移してしまう
router.push('https://example.com');
サーバー側での実行
navigateTo() だけ実行できる。
手元の環境でやった感じの結果は以下。
// サーバー側でのみ実行
if (import.meta.server) {
// 問題なく実行できる
await navigateTo('/hoge');
// エラーにならないが遷移もできない
router.push('/hoge');
// windowが見つからないエラーがサーバー側で発生してしまう
window.location.href = '/hoge';
}
♨️ 所感
ざっとみて見た感じ navigateTo() であれば一番担保できるので navigateTo() を使うのが良さそう。
「はじめにアクセスしたページだけでなくて全てSSRで遷移したい!」って場合はnavigateTo('/hoge', { external: true }) にしたら [同一ホスト名]/hoge にSSRで遷移できそうだったからそういう需要がある場合はぜひ。
【参考】 navigateTo() のコード
navigateTo()のコードをかいつまんで理解していく。
navigateTo()のコードを丸々持ってくるとこんな感じ。
export const navigateTo = (to: RouteLocationRaw | undefined | null, options?: NavigateToOptions): Promise<void | NavigationFailure | false> | false | void | RouteLocationRaw => {
to ||= '/'
const toPath = typeof to === 'string' ? to : 'path' in to ? resolveRouteObject(to) : useRouter().resolve(to).href
// Early open handler
if (import.meta.client && options?.open) {
const { target = '_blank', windowFeatures = {} } = options.open
const features: string[] = []
for (const [feature, value] of Object.entries(windowFeatures)) {
if (value !== undefined) {
features.push(`${feature.toLowerCase()}=${value}`)
}
}
open(toPath, target, features.join(', '))
return Promise.resolve()
}
const isExternalHost = hasProtocol(toPath, { acceptRelative: true })
const isExternal = options?.external || isExternalHost
if (isExternal) {
if (!options?.external) {
throw new Error('Navigating to an external URL is not allowed by default. Use `navigateTo(url, { external: true })`.')
}
const { protocol } = new URL(toPath, import.meta.client ? window.location.href : 'http://localhost')
if (protocol && isScriptProtocol(protocol)) {
throw new Error(`Cannot navigate to a URL with '${protocol}' protocol.`)
}
}
const inMiddleware = isProcessingMiddleware()
// Early redirect on client-side
if (import.meta.client && !isExternal && inMiddleware) {
if (options?.replace) {
if (typeof to === 'string') {
const { pathname, search, hash } = parseURL(to)
return {
path: pathname,
...(search && { query: parseQuery(search) }),
...(hash && { hash }),
replace: true,
}
}
return { ...to, replace: true }
}
return to
}
const router = useRouter()
const nuxtApp = useNuxtApp()
if (import.meta.server) {
if (nuxtApp.ssrContext) {
const fullPath = typeof to === 'string' || isExternal ? toPath : router.resolve(to).fullPath || '/'
const location = isExternal ? toPath : joinURL(useRuntimeConfig().app.baseURL, fullPath)
const redirect = async function (response: any) {
// TODO: consider deprecating in favour of `app:rendered` and removing
await nuxtApp.callHook('app:redirected')
const encodedLoc = location.replace(URL_QUOTE_RE, '%22')
const encodedHeader = encodeURL(location, isExternalHost)
nuxtApp.ssrContext!['~renderResponse'] = {
statusCode: sanitizeStatusCode(options?.redirectCode || 302, 302),
body: `<!DOCTYPE html><html><head><meta http-equiv="refresh" content="0; url=${encodedLoc}"></head></html>`,
headers: { location: encodedHeader },
}
return response
}
// We wait to perform the redirect last in case any other middleware will intercept the redirect
// and redirect somewhere else instead.
if (!isExternal && inMiddleware) {
router.afterEach(final => final.fullPath === fullPath ? redirect(false) : undefined)
return to
}
return redirect(!inMiddleware ? undefined : /* abort route navigation */ false)
}
}
// Client-side redirection using vue-router
if (isExternal) {
// Run any cleanup steps for the current scope, like ending BroadcastChannel
nuxtApp._scope.stop()
if (options?.replace) {
location.replace(toPath)
} else {
location.href = toPath
}
// Within in a Nuxt route middleware handler
if (inMiddleware) {
// Abort navigation when app is hydrated
if (!nuxtApp.isHydrating) {
return false
}
// When app is hydrating (i.e. on page load), we don't want to abort navigation as
// it would lead to a 404 error / page that's blinking before location changes.
return new Promise(() => { })
}
return Promise.resolve()
}
return options?.replace ? router.replace(to) : router.push(to)
}
サーバー側の処理は以下の部分なのでちょっと省略したりコメントを書いたりしてみる。
// サーバー側での処理対応
if (import.meta.server) {
// サーバーサイドでだけ利用可能な情報がある場合のみ実施
if (nuxtApp.ssrContext) {
const fullPath = typeof to === 'string' || isExternal ? toPath : router.resolve(to).fullPath || '/'
const location = isExternal ? toPath : joinURL(useRuntimeConfig().app.baseURL, fullPath)
// リダイレクト処理をする関数を定義
const redirect = async function (response: any) {
// 今回は中身省略
}
// 外部URLでは無い かつ middlewareに記載されている場合、他のmiddlewareとかも確認して最終的に指定されたURLに遷移したいのであればリダイレクトさせる処理を登録。
if (!isExternal && inMiddleware) {
router.afterEach(final => final.fullPath === fullPath ? redirect(false) : undefined)
return to
}
// それ以外は即座にリダイレクトさせる(middlewareの場合は本来の画面への遷移は中断)
return redirect(!inMiddleware ? undefined : /* abort route navigation */ false)
}
}
残りの部分も同じように省略したりコメントを書いたりしてみる。
// 外部URLの場合はreplaceがあればlocation.replace()と、なければlocal.href と同じ挙動で遷移
if (isExternal) {
nuxtApp._scope.stop()
if (options?.replace) {
location.replace(toPath)
} else {
location.href = toPath
}
if (inMiddleware) {
// サーバーの静的HTMLにJSを組みんでいる最中であれば遷移を中断
if (!nuxtApp.isHydrating) {
return false
}
return new Promise(() => {})
}
return Promise.resolve()
}
// それ以外の場合はoption.replaceに合わせてrouter.replace()かrouter.push()を実行
return options?.replace ? router.replace(to) : router.push(to)
つまり、navigateTo() は router.push() の動作をベースにサーバー側ではリダイレクトしたり外部URL遷移の設定を追加したりしているということ。
Discussion