1
0
Fork 0
mirror of https://github.com/terribleplan/next.js.git synced 2024-01-19 02:48:18 +00:00
next.js/lib/router/router.js
Arunoda Susiripala 311e4ca0ee Make sure the router is aware of the nextExport
Based on the we can change the routing to do SSR always.
Also make sure pageLoader don't download the page via
client side twice.
2017-05-08 18:20:50 -07:00

357 lines
9.8 KiB
JavaScript

/* global __NEXT_DATA__ */
import { parse, format } from 'url'
import mitt from 'mitt'
import shallowEquals from '../shallow-equals'
import PQueue from '../p-queue'
import { loadGetInitialProps, getURL } from '../utils'
import { _notifyBuildIdMismatch } from './'
export default class Router {
constructor (pathname, query, as, { pageLoader, Component, ErrorComponent, err } = {}) {
// represents the current component key
this.route = toRoute(pathname)
// set up the component cache (by route keys)
this.components = {}
// We should not keep the cache, if there's an error
// Otherwise, this cause issues when when going back and
// come again to the errored page.
if (Component !== ErrorComponent) {
this.components[this.route] = { Component, err }
}
// Handling Router Events
this.events = mitt()
this.pageLoader = pageLoader
this.prefetchQueue = new PQueue({ concurrency: 2 })
this.ErrorComponent = ErrorComponent
this.pathname = pathname
this.query = query
this.asPath = as
this.subscriptions = new Set()
this.componentLoadCancel = null
this.onPopState = this.onPopState.bind(this)
if (typeof window !== 'undefined') {
// in order for `e.state` to work on the `onpopstate` event
// we have to register the initial route upon initialization
this.changeState('replaceState', format({ pathname, query }), getURL())
window.addEventListener('popstate', this.onPopState)
}
}
async onPopState (e) {
if (!e.state) {
// We get state as undefined for two reasons.
// 1. With older safari (< 8) and older chrome (< 34)
// 2. When the URL changed with #
//
// In the both cases, we don't need to proceed and change the route.
// (as it's already changed)
// But we can simply replace the state with the new changes.
// Actually, for (1) we don't need to nothing. But it's hard to detect that event.
// So, doing the following for (1) does no harm.
const { pathname, query } = this
this.changeState('replaceState', format({ pathname, query }), getURL())
return
}
const { url, as, options } = e.state
this.replace(url, as, options)
}
update (route, Component) {
const data = this.components[route]
if (!data) {
throw new Error(`Cannot update unavailable route: ${route}`)
}
const newData = { ...data, Component }
this.components[route] = newData
if (route === this.route) {
this.notify(newData)
}
}
async reload (route) {
delete this.components[route]
this.pageLoader.clearCache(route)
if (route !== this.route) return
const { pathname, query } = this
const url = window.location.href
this.events.emit('routeChangeStart', url)
const routeInfo = await this.getRouteInfo(route, pathname, query, url)
const { error } = routeInfo
if (error && error.cancelled) {
return
}
this.notify(routeInfo)
if (error) {
this.events.emit('routeChangeError', error, url)
throw error
}
this.events.emit('routeChangeComplete', url)
}
back () {
window.history.back()
}
push (url, as = url, options = {}) {
return this.change('pushState', url, as, options)
}
replace (url, as = url, options = {}) {
return this.change('replaceState', url, as, options)
}
async change (method, _url, _as, options) {
// If url and as provided as an object representation,
// we'll format them into the string version here.
const url = typeof _url === 'object' ? format(_url) : _url
const as = typeof _as === 'object' ? format(_as) : _as
// We need to load the page via SSR everytime if we
// are in the nextExport mode.
if (__NEXT_DATA__.nextExport) {
// Add the ending slash to the paths. So, we can serve the
// "<page>/index.html" directly.
const endsWithSlash = /\/$/.test(as)
window.location.href = endsWithSlash ? as : `${as}/`
return
}
this.abortComponentLoad(as)
const { pathname, query } = parse(url, true)
// If the url change is only related to a hash change
// We should not proceed. We should only replace the state.
if (this.onlyAHashChange(as)) {
this.changeState('replaceState', url, as)
return
}
// If asked to change the current URL we should reload the current page
// (not location.reload() but reload getInitalProps and other Next.js stuffs)
// We also need to set the method = replaceState always
// as this should not go into the history (That's how browsers work)
if (!this.urlIsNew(pathname, query)) {
method = 'replaceState'
}
const route = toRoute(pathname)
const { shallow = false } = options
let routeInfo = null
this.events.emit('routeChangeStart', as)
// If shallow === false and other conditions met, we reuse the
// existing routeInfo for this route.
// Because of this, getInitialProps would not run.
if (shallow && this.isShallowRoutingPossible(route)) {
routeInfo = this.components[route]
} else {
routeInfo = await this.getRouteInfo(route, pathname, query, as)
}
const { error } = routeInfo
if (error && error.cancelled) {
return false
}
this.events.emit('beforeHistoryChange', as)
this.changeState(method, url, as, options)
const hash = window.location.hash.substring(1)
this.set(route, pathname, query, as, { ...routeInfo, hash })
if (error) {
this.events.emit('routeChangeError', error, as)
throw error
}
this.events.emit('routeChangeComplete', as)
return true
}
changeState (method, url, as, options = {}) {
if (method !== 'pushState' || getURL() !== as) {
window.history[method]({ url, as, options }, null, as)
}
}
async getRouteInfo (route, pathname, query, as) {
let routeInfo = null
try {
routeInfo = this.components[route]
if (!routeInfo) {
routeInfo = { Component: await this.fetchComponent(route, as) }
}
const { Component } = routeInfo
const ctx = { pathname, query, asPath: as }
routeInfo.props = await this.getInitialProps(Component, ctx)
this.components[route] = routeInfo
} catch (err) {
if (err.cancelled) {
return { error: err }
}
if (err.buildIdMismatched) {
// Now we need to reload the page or do the action asked by the user
_notifyBuildIdMismatch(as)
// We also need to cancel this current route change.
// We do it like this.
err.cancelled = true
return { error: err }
}
if (err.statusCode === 404) {
// Indicate main error display logic to
// ignore rendering this error as a runtime error.
err.ignore = true
}
const Component = this.ErrorComponent
routeInfo = { Component, err }
const ctx = { err, pathname, query }
routeInfo.props = await this.getInitialProps(Component, ctx)
routeInfo.error = err
}
return routeInfo
}
set (route, pathname, query, as, data) {
this.route = route
this.pathname = pathname
this.query = query
this.asPath = as
this.notify(data)
}
onlyAHashChange (as) {
if (!this.asPath) return false
const [ oldUrlNoHash ] = this.asPath.split('#')
const [ newUrlNoHash, newHash ] = as.split('#')
// If the urls are change, there's more than a hash change
if (oldUrlNoHash !== newUrlNoHash) {
return false
}
// If there's no hash in the new url, we can't consider it as a hash change
if (!newHash) {
return false
}
// Now there's a hash in the new URL.
// We don't need to worry about the old hash.
return true
}
urlIsNew (pathname, query) {
return this.pathname !== pathname || !shallowEquals(query, this.query)
}
isShallowRoutingPossible (route) {
return (
// If there's cached routeInfo for the route.
Boolean(this.components[route]) &&
// If the route is already rendered on the screen.
this.route === route
)
}
async prefetch (url) {
if (__NEXT_DATA__.nextExport) return
// We don't add support for prefetch in the development mode.
// If we do that, our on-demand-entries optimization won't performs better
if (process.env.NODE_ENV === 'development') return
const { pathname } = parse(url)
const route = toRoute(pathname)
return this.prefetchQueue.add(() => this.fetchRoute(route))
}
async fetchComponent (route, as) {
let cancelled = false
const cancel = this.componentLoadCancel = function () {
cancelled = true
}
const Component = await this.fetchRoute(route)
if (cancelled) {
const error = new Error(`Abort fetching component for route: "${route}"`)
error.cancelled = true
throw error
}
if (cancel === this.componentLoadCancel) {
this.componentLoadCancel = null
}
return Component
}
async getInitialProps (Component, ctx) {
let cancelled = false
const cancel = () => { cancelled = true }
this.componentLoadCancel = cancel
const props = await loadGetInitialProps(Component, ctx)
if (cancel === this.componentLoadCancel) {
this.componentLoadCancel = null
}
if (cancelled) {
const err = new Error('Loading initial props cancelled')
err.cancelled = true
throw err
}
return props
}
async fetchRoute (route) {
return await this.pageLoader.loadPage(route)
}
abortComponentLoad (as) {
if (this.componentLoadCancel) {
this.events.emit('routeChangeError', new Error('Route Cancelled'), as)
this.componentLoadCancel()
this.componentLoadCancel = null
}
}
notify (data) {
this.subscriptions.forEach((fn) => fn(data))
}
subscribe (fn) {
this.subscriptions.add(fn)
return () => this.subscriptions.delete(fn)
}
}
function toRoute (path) {
return path.replace(/\/$/, '') || '/'
}