1
0
Fork 0
mirror of https://github.com/terribleplan/next.js.git synced 2024-01-19 02:48:18 +00:00

fix client/router

This commit is contained in:
nkzawa 2016-10-08 14:12:51 +09:00
parent 89f96cc160
commit e2ab55ad23
5 changed files with 164 additions and 174 deletions

View file

@ -12,7 +12,7 @@ const {
const App = app ? evalScript(app).default : DefaultApp
const Component = evalScript(component).default
const router = new Router({ Component, props })
const router = new Router({ Component })
const headManager = new HeadManager()
const container = document.getElementById('__next')
const appProps = { Component, props, router, headManager }

View file

@ -4,119 +4,105 @@ import shallowEquals from './shallow-equals'
export default class Router {
constructor (initialData) {
this.subscriptions = []
const id = createUid()
const route = toRoute(location.pathname)
this.currentRoute = route
this.currentComponentData = { ...initialData, id }
// represents the current component key
this.route = toRoute(location.pathname)
// set up the component cache (by route keys)
this.components = { [route]: initialData }
// in order for `e.state` to work on the `onpopstate` event
// we have to register the initial route upon initialization
this.replace(id, getURL())
this.components = { [this.route]: initialData }
this.subscriptions = new Set()
this.componentLoadCancel = null
this.onPopState = this.onPopState.bind(this)
window.addEventListener('unload', () => {})
window.addEventListener('popstate', this.onPopState)
}
onPopState (e) {
this.abortComponentLoad()
const cur = this.currentComponentData.id
const url = getURL()
const { fromComponent, route } = e.state || {}
if (fromComponent && cur && fromComponent === cur) {
// if the component has not changed due
// to the url change, it means we only
// need to notify the subscriber about
// the URL change
this.set(url)
} else {
this.fetchComponent(route || url, (err, data) => {
if (err) {
// the only way we can appropriately handle
// this failure is deferring to the browser
// since the URL has already changed
location.reload()
} else {
this.currentRoute = route || toRoute(location.pathname)
this.currentComponentData = data
this.set(url)
}
})
}
const route = (e.state || {}).route || toRoute(location.pathname)
Promise.resolve()
.then(async () => {
const data = await this.fetchComponent(route)
let props
if (route !== this.route) {
props = await this.getInitialProps(data.Component)
}
this.route = route
this.set(getURL(), { ...data, props })
})
.catch((err) => {
if (err.cancelled) return
// the only way we can appropriately handle
// this failure is deferring to the browser
// since the URL has already changed
location.reload()
})
}
update (route, data) {
async update (route, data) {
data.Component = evalScript(data.component).default
delete data.component
this.components[route] = data
if (route === this.currentRoute) {
let cancelled = false
const cancel = () => { cancelled = true }
this.componentLoadCancel = cancel
getInitialProps(data, (err, dataWithProps) => {
if (cancel === this.componentLoadCancel) {
this.componentLoadCancel = false
}
if (cancelled) return
if (err) throw err
this.currentComponentData = dataWithProps
this.notify()
})
}
}
goTo (url, fn) {
this.change('pushState', null, url, fn)
if (route === this.route) {
let props
try {
props = await this.getInitialProps(data.Component)
} catch (err) {
if (err.cancelled) return false
throw err
}
this.notify({ ...data, props })
}
return true
}
back () {
history.back()
}
push (fromComponent, url, fn) {
this.change('pushState', fromComponent, url, fn)
push (route, url) {
return this.change('pushState', route, url)
}
replace (id, url, fn) {
this.change('replaceState', id, url, fn)
replace (route, url) {
return this.change('replaceState', route, url)
}
change (method, id, url, fn) {
async change (method, route, url) {
if (!route) route = toRoute(parse(url).pathname)
this.abortComponentLoad()
const set = (id) => {
const state = id ? { fromComponent: id, route: this.currentRoute } : {}
history[method](state, null, url)
this.set(url)
if (fn) fn(null)
let data
let props
try {
data = await this.fetchComponent(route)
if (route !== this.route) {
props = await this.getInitialProps(data.Component)
}
} catch (err) {
if (err.cancelled) return false
throw err
}
if (this.currentComponentData && id !== this.currentComponentData.id) {
this.fetchComponent(url, (err, data) => {
if (!err) {
this.currentRoute = toRoute(url)
this.currentComponentData = data
set(data.id)
}
if (fn) fn(err, data)
})
} else {
set(id)
}
history[method]({ route }, null, url)
this.route = route
this.set(url, { ...data, props })
return true
}
set (url) {
set (url, data) {
const parsed = parse(url, true)
if (this.urlIsNew(parsed)) {
this.pathname = parsed.pathname
this.query = parsed.query
this.notify()
this.notify(data)
}
}
@ -124,52 +110,66 @@ export default class Router {
return this.pathname !== pathname || !shallowEquals(query, this.query)
}
fetchComponent (url, fn) {
const { pathname } = parse(url)
const route = toRoute(pathname)
async fetchComponent (url) {
const route = toRoute(parse(url).pathname)
let data = this.components[route]
if (data) return data
let cancel
let cancelled = false
let componentXHR = null
const cancel = () => {
cancelled = true
if (componentXHR && componentXHR.abort) {
componentXHR.abort()
}
}
if (this.components[route]) {
const data = this.components[route]
getInitialProps(data, (err, dataWithProps) => {
if (cancel === this.componentLoadCancel) {
this.componentLoadCancel = false
}
if (cancelled) return
fn(err, dataWithProps)
})
this.componentLoadCancel = cancel
return
}
const componentUrl = toJSONUrl(route)
data = await new Promise((resolve, reject) => {
this.componentLoadCancel = cancel = () => {
cancelled = true
if (componentXHR.abort) componentXHR.abort()
componentXHR = loadComponent(componentUrl, (err, data) => {
if (cancel === this.componentLoadCancel) {
this.componentLoadCancel = false
}
if (err) {
if (!cancelled) fn(err)
} else {
const d = { ...data, id: createUid() }
// we update the cache even if cancelled
if (!this.components[route]) {
this.components[route] = d
}
if (!cancelled) fn(null, d)
const err = new Error('Cancelled')
err.cancelled = true
reject(err)
}
const componentXHR = loadComponent(componentUrl, (err, data) => {
if (err) return reject(err)
resolve(data)
})
})
if (cancel === this.componentLoadCancel) {
this.componentLoadCancel = null
}
// we update the cache even if cancelled
if (data) this.components[route] = data
if (cancelled) {
const err = new Error('Cancelled')
err.cancelled = true
throw err
}
return data
}
async getInitialProps (Component) {
let cancelled = false
const cancel = () => { cancelled = true }
this.componentLoadCancel = cancel
const props = await (Component.getInitialProps ? Component.getInitialProps({}) : {})
if (cancel === this.componentLoadCancel) {
this.componentLoadCancel = null
}
if (cancelled) {
const err = new Error('Cancelled')
err.cancelled = true
throw err
}
return props
}
abortComponentLoad () {
@ -179,44 +179,44 @@ export default class Router {
}
}
notify () {
this.subscriptions.forEach(fn => fn())
notify (data) {
this.subscriptions.forEach((fn) => fn(data))
}
subscribe (fn) {
this.subscriptions.push(fn)
return () => {
const i = this.subscriptions.indexOf(fn)
if (~i) this.subscriptions.splice(i, 1)
}
this.subscriptions.add(fn)
return () => this.subscriptions.delete(fn)
}
}
// every route finishing in `/test/` becomes `/test`
export function toRoute (path) {
return path.replace(/\/$/, '') || '/'
}
export function toJSONUrl (route) {
return ('/' === route ? '/index' : route) + '.json'
}
export function loadComponent (url, fn) {
return loadJSON(url, (err, data) => {
if (err && fn) fn(err)
const { component, props } = data
const Component = evalScript(component).default
getInitialProps({ Component, props }, fn)
})
}
function getURL () {
return location.pathname + (location.search || '') + (location.hash || '')
}
function createUid () {
return Math.floor(Math.random() * 1e16)
function toRoute (path) {
return path.replace(/\/$/, '') || '/'
}
function toJSONUrl (route) {
return ('/' === route ? '/index' : route) + '.json'
}
function loadComponent (url, fn) {
return loadJSON(url, (err, data) => {
if (err) return fn(err)
const { component } = data
let module
try {
module = evalScript(component)
} catch (err) {
return fn(err)
}
const Component = module.default || module
fn(null, { Component })
})
}
function loadJSON (url, fn) {
@ -234,21 +234,10 @@ function loadJSON (url, fn) {
fn(null, data)
}
xhr.onerror = () => {
if (fn) fn(new Error('XHR failed. Status: ' + xhr.status))
fn(new Error('XHR failed. Status: ' + xhr.status))
}
xhr.open('GET', url)
xhr.send()
return xhr
}
function getInitialProps (data, fn) {
const { Component: { getInitialProps } } = data
if (getInitialProps) {
Promise.resolve(getInitialProps({}))
.then((props) => fn(null, { ...data, props }))
.catch(fn)
} else {
fn(null, data)
}
}

View file

@ -24,9 +24,10 @@ export default class App extends Component {
componentDidMount () {
const { router } = this.props
this.close = router.subscribe(() => {
this.close = router.subscribe((data) => {
const props = data.props || this.state.props
const state = propsToState({
...router.currentComponentData,
...data,
router
})
@ -55,13 +56,15 @@ export default class App extends Component {
function propsToState (props) {
const { Component, router } = props
const { route } = router
const url = {
query: router.query,
pathname: router.pathname,
back: () => router.back(),
goTo: (url, fn) => router.goTo(url, fn),
push: (url, fn) => router.push(Component, url, fn),
replace: (url, fn) => router.replace(Component, url, fn)
push: (url) => router.push(route, url),
pushTo: (url) => router.push(null, url),
replace: (url) => router.replace(route, url),
replaceTo: (url) => router.replace(null, url)
}
return {

View file

@ -27,15 +27,13 @@ export default class Link extends Component {
e.preventDefault()
// straight up redirect
this.context.router.goTo(href, (err) => {
if (err) {
if (this.props.onError) this.props.onError(err)
return
}
if (false !== scroll) {
window.scrollTo(0, 0)
}
this.context.router.push(null, href)
.then((success) => {
if (!success) return
if (false !== scroll) window.scrollTo(0, 0)
})
.catch((err) => {
if (this.props.onError) this.props.onError(err)
})
}
@ -45,15 +43,15 @@ export default class Link extends Component {
onClick: this.linkClicked
}
const isChildAnchor = child && 'a' === child.type
const isAnchor = child && 'a' === child.type
// if child does not specify a href, specify it
// so that repetition is not needed by the user
if (!isChildAnchor || !('href' in child.props)) {
if (!isAnchor || !('href' in child.props)) {
props.href = this.props.href
}
if (isChildAnchor) {
if (isAnchor) {
return React.cloneElement(child, props)
} else {
return <a {...props}>{child}</a>

View file

@ -35,7 +35,7 @@ export async function render (path, req, res, { dir = process.cwd(), dev = false
html,
head,
css,
data: { component },
data: { component, props },
hotReload: false,
dev
})