提交 e2ab55ad 编写于 作者: N nkzawa

fix client/router

上级 89f96cc1
...@@ -12,7 +12,7 @@ const { ...@@ -12,7 +12,7 @@ const {
const App = app ? evalScript(app).default : DefaultApp const App = app ? evalScript(app).default : DefaultApp
const Component = evalScript(component).default const Component = evalScript(component).default
const router = new Router({ Component, props }) const router = new Router({ Component })
const headManager = new HeadManager() const headManager = new HeadManager()
const container = document.getElementById('__next') const container = document.getElementById('__next')
const appProps = { Component, props, router, headManager } const appProps = { Component, props, router, headManager }
......
...@@ -4,119 +4,105 @@ import shallowEquals from './shallow-equals' ...@@ -4,119 +4,105 @@ import shallowEquals from './shallow-equals'
export default class Router { export default class Router {
constructor (initialData) { constructor (initialData) {
this.subscriptions = [] // represents the current component key
this.route = toRoute(location.pathname)
const id = createUid()
const route = toRoute(location.pathname)
this.currentRoute = route
this.currentComponentData = { ...initialData, id }
// set up the component cache (by route keys) // set up the component cache (by route keys)
this.components = { [route]: initialData } this.components = { [this.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.subscriptions = new Set()
this.componentLoadCancel = null
this.onPopState = this.onPopState.bind(this) this.onPopState = this.onPopState.bind(this)
window.addEventListener('unload', () => {})
window.addEventListener('popstate', this.onPopState) window.addEventListener('popstate', this.onPopState)
} }
onPopState (e) { onPopState (e) {
this.abortComponentLoad() this.abortComponentLoad()
const cur = this.currentComponentData.id
const url = getURL() const route = (e.state || {}).route || toRoute(location.pathname)
const { fromComponent, route } = e.state || {}
if (fromComponent && cur && fromComponent === cur) { Promise.resolve()
// if the component has not changed due .then(async () => {
// to the url change, it means we only const data = await this.fetchComponent(route)
// need to notify the subscriber about let props
// the URL change if (route !== this.route) {
this.set(url) props = await this.getInitialProps(data.Component)
} else { }
this.fetchComponent(route || url, (err, data) => {
if (err) { this.route = route
this.set(getURL(), { ...data, props })
})
.catch((err) => {
if (err.cancelled) return
// the only way we can appropriately handle // the only way we can appropriately handle
// this failure is deferring to the browser // this failure is deferring to the browser
// since the URL has already changed // since the URL has already changed
location.reload() location.reload()
} else {
this.currentRoute = route || toRoute(location.pathname)
this.currentComponentData = data
this.set(url)
}
}) })
} }
}
update (route, data) { async update (route, data) {
data.Component = evalScript(data.component).default data.Component = evalScript(data.component).default
delete data.component delete data.component
this.components[route] = data this.components[route] = data
if (route === this.currentRoute) {
let cancelled = false if (route === this.route) {
const cancel = () => { cancelled = true } let props
this.componentLoadCancel = cancel try {
getInitialProps(data, (err, dataWithProps) => { props = await this.getInitialProps(data.Component)
if (cancel === this.componentLoadCancel) { } catch (err) {
this.componentLoadCancel = false if (err.cancelled) return false
} throw err
if (cancelled) return
if (err) throw err
this.currentComponentData = dataWithProps
this.notify()
})
} }
this.notify({ ...data, props })
} }
return true
goTo (url, fn) {
this.change('pushState', null, url, fn)
} }
back () { back () {
history.back() history.back()
} }
push (fromComponent, url, fn) { push (route, url) {
this.change('pushState', fromComponent, url, fn) return this.change('pushState', route, url)
} }
replace (id, url, fn) { replace (route, url) {
this.change('replaceState', id, url, fn) return this.change('replaceState', route, url)
} }
change (method, id, url, fn) { async change (method, route, url) {
this.abortComponentLoad() if (!route) route = toRoute(parse(url).pathname)
const set = (id) => { this.abortComponentLoad()
const state = id ? { fromComponent: id, route: this.currentRoute } : {}
history[method](state, null, url)
this.set(url)
if (fn) fn(null)
}
if (this.currentComponentData && id !== this.currentComponentData.id) { let data
this.fetchComponent(url, (err, data) => { let props
if (!err) { try {
this.currentRoute = toRoute(url) data = await this.fetchComponent(route)
this.currentComponentData = data if (route !== this.route) {
set(data.id) props = await this.getInitialProps(data.Component)
} }
if (fn) fn(err, data) } catch (err) {
}) if (err.cancelled) return false
} else { throw err
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) const parsed = parse(url, true)
if (this.urlIsNew(parsed)) { if (this.urlIsNew(parsed)) {
this.pathname = parsed.pathname this.pathname = parsed.pathname
this.query = parsed.query this.query = parsed.query
this.notify() this.notify(data)
} }
} }
...@@ -124,52 +110,66 @@ export default class Router { ...@@ -124,52 +110,66 @@ export default class Router {
return this.pathname !== pathname || !shallowEquals(query, this.query) return this.pathname !== pathname || !shallowEquals(query, this.query)
} }
fetchComponent (url, fn) { async fetchComponent (url) {
const { pathname } = parse(url) const route = toRoute(parse(url).pathname)
const route = toRoute(pathname)
let data = this.components[route]
if (data) return data
let cancel
let cancelled = false let cancelled = false
let componentXHR = null
const cancel = () => { const componentUrl = toJSONUrl(route)
data = await new Promise((resolve, reject) => {
this.componentLoadCancel = cancel = () => {
cancelled = true cancelled = true
if (componentXHR.abort) componentXHR.abort()
if (componentXHR && componentXHR.abort) { const err = new Error('Cancelled')
componentXHR.abort() err.cancelled = true
} reject(err)
} }
if (this.components[route]) { const componentXHR = loadComponent(componentUrl, (err, data) => {
const data = this.components[route] if (err) return reject(err)
getInitialProps(data, (err, dataWithProps) => { resolve(data)
if (cancel === this.componentLoadCancel) { })
this.componentLoadCancel = false
}
if (cancelled) return
fn(err, dataWithProps)
}) })
this.componentLoadCancel = cancel
return
}
const componentUrl = toJSONUrl(route)
componentXHR = loadComponent(componentUrl, (err, data) => {
if (cancel === this.componentLoadCancel) { if (cancel === this.componentLoadCancel) {
this.componentLoadCancel = false this.componentLoadCancel = null
} }
if (err) {
if (!cancelled) fn(err)
} else {
const d = { ...data, id: createUid() }
// we update the cache even if cancelled // we update the cache even if cancelled
if (!this.components[route]) { if (data) this.components[route] = data
this.components[route] = d
if (cancelled) {
const err = new Error('Cancelled')
err.cancelled = true
throw err
} }
if (!cancelled) fn(null, d)
return data
} }
})
async getInitialProps (Component) {
let cancelled = false
const cancel = () => { cancelled = true }
this.componentLoadCancel = cancel 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 () { abortComponentLoad () {
...@@ -179,44 +179,44 @@ export default class Router { ...@@ -179,44 +179,44 @@ export default class Router {
} }
} }
notify () { notify (data) {
this.subscriptions.forEach(fn => fn()) this.subscriptions.forEach((fn) => fn(data))
} }
subscribe (fn) { subscribe (fn) {
this.subscriptions.push(fn) this.subscriptions.add(fn)
return () => { return () => this.subscriptions.delete(fn)
const i = this.subscriptions.indexOf(fn)
if (~i) this.subscriptions.splice(i, 1)
}
} }
} }
// every route finishing in `/test/` becomes `/test` function getURL () {
return location.pathname + (location.search || '') + (location.hash || '')
}
export function toRoute (path) { function toRoute (path) {
return path.replace(/\/$/, '') || '/' return path.replace(/\/$/, '') || '/'
} }
export function toJSONUrl (route) { function toJSONUrl (route) {
return ('/' === route ? '/index' : route) + '.json' return ('/' === route ? '/index' : route) + '.json'
} }
export function loadComponent (url, fn) { function loadComponent (url, fn) {
return loadJSON(url, (err, data) => { return loadJSON(url, (err, data) => {
if (err && fn) fn(err) if (err) return fn(err)
const { component, props } = data
const Component = evalScript(component).default
getInitialProps({ Component, props }, fn)
})
}
function getURL () { const { component } = data
return location.pathname + (location.search || '') + (location.hash || '')
}
function createUid () { let module
return Math.floor(Math.random() * 1e16) try {
module = evalScript(component)
} catch (err) {
return fn(err)
}
const Component = module.default || module
fn(null, { Component })
})
} }
function loadJSON (url, fn) { function loadJSON (url, fn) {
...@@ -234,21 +234,10 @@ function loadJSON (url, fn) { ...@@ -234,21 +234,10 @@ function loadJSON (url, fn) {
fn(null, data) fn(null, data)
} }
xhr.onerror = () => { 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.open('GET', url)
xhr.send() xhr.send()
return xhr 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)
}
}
...@@ -24,9 +24,10 @@ export default class App extends Component { ...@@ -24,9 +24,10 @@ export default class App extends Component {
componentDidMount () { componentDidMount () {
const { router } = this.props const { router } = this.props
this.close = router.subscribe(() => { this.close = router.subscribe((data) => {
const props = data.props || this.state.props
const state = propsToState({ const state = propsToState({
...router.currentComponentData, ...data,
router router
}) })
...@@ -55,13 +56,15 @@ export default class App extends Component { ...@@ -55,13 +56,15 @@ export default class App extends Component {
function propsToState (props) { function propsToState (props) {
const { Component, router } = props const { Component, router } = props
const { route } = router
const url = { const url = {
query: router.query, query: router.query,
pathname: router.pathname, pathname: router.pathname,
back: () => router.back(), back: () => router.back(),
goTo: (url, fn) => router.goTo(url, fn), push: (url) => router.push(route, url),
push: (url, fn) => router.push(Component, url, fn), pushTo: (url) => router.push(null, url),
replace: (url, fn) => router.replace(Component, url, fn) replace: (url) => router.replace(route, url),
replaceTo: (url) => router.replace(null, url)
} }
return { return {
......
...@@ -27,15 +27,13 @@ export default class Link extends Component { ...@@ -27,15 +27,13 @@ export default class Link extends Component {
e.preventDefault() e.preventDefault()
// straight up redirect // straight up redirect
this.context.router.goTo(href, (err) => { this.context.router.push(null, href)
if (err) { .then((success) => {
if (!success) return
if (false !== scroll) window.scrollTo(0, 0)
})
.catch((err) => {
if (this.props.onError) this.props.onError(err) if (this.props.onError) this.props.onError(err)
return
}
if (false !== scroll) {
window.scrollTo(0, 0)
}
}) })
} }
...@@ -45,15 +43,15 @@ export default class Link extends Component { ...@@ -45,15 +43,15 @@ export default class Link extends Component {
onClick: this.linkClicked onClick: this.linkClicked
} }
const isChildAnchor = child && 'a' === child.type const isAnchor = child && 'a' === child.type
// if child does not specify a href, specify it // if child does not specify a href, specify it
// so that repetition is not needed by the user // 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 props.href = this.props.href
} }
if (isChildAnchor) { if (isAnchor) {
return React.cloneElement(child, props) return React.cloneElement(child, props)
} else { } else {
return <a {...props}>{child}</a> return <a {...props}>{child}</a>
......
...@@ -35,7 +35,7 @@ export async function render (path, req, res, { dir = process.cwd(), dev = false ...@@ -35,7 +35,7 @@ export async function render (path, req, res, { dir = process.cwd(), dev = false
html, html,
head, head,
css, css,
data: { component }, data: { component, props },
hotReload: false, hotReload: false,
dev dev
}) })
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册