mirror of
https://github.com/terribleplan/next.js.git
synced 2024-01-19 02:48:18 +00:00
Introduce "asPath" into router and getInitialProps (#1857)
* Add asPath to next/router and getInitialProps context. * Add test cases. * Update docs. * Build as-path pages before they use.
This commit is contained in:
parent
6e89d49f99
commit
24f3f143a6
|
@ -29,6 +29,8 @@ const {
|
||||||
location
|
location
|
||||||
} = window
|
} = window
|
||||||
|
|
||||||
|
const asPath = getURL()
|
||||||
|
|
||||||
const pageLoader = new PageLoader(buildId, assetPrefix)
|
const pageLoader = new PageLoader(buildId, assetPrefix)
|
||||||
window.__NEXT_LOADED_PAGES__.forEach(({ route, fn }) => {
|
window.__NEXT_LOADED_PAGES__.forEach(({ route, fn }) => {
|
||||||
pageLoader.registerPage(route, fn)
|
pageLoader.registerPage(route, fn)
|
||||||
|
@ -56,7 +58,7 @@ export default async () => {
|
||||||
Component = ErrorComponent
|
Component = ErrorComponent
|
||||||
}
|
}
|
||||||
|
|
||||||
router = createRouter(pathname, query, getURL(), {
|
router = createRouter(pathname, query, asPath, {
|
||||||
pageLoader,
|
pageLoader,
|
||||||
Component,
|
Component,
|
||||||
ErrorComponent,
|
ErrorComponent,
|
||||||
|
@ -107,7 +109,7 @@ export async function renderError (error) {
|
||||||
console.error(errorMessage)
|
console.error(errorMessage)
|
||||||
|
|
||||||
if (prod) {
|
if (prod) {
|
||||||
const initProps = { err: error, pathname, query }
|
const initProps = { err: error, pathname, query, asPath }
|
||||||
const props = await loadGetInitialProps(ErrorComponent, initProps)
|
const props = await loadGetInitialProps(ErrorComponent, initProps)
|
||||||
ReactDOM.render(createElement(ErrorComponent, props), errorContainer)
|
ReactDOM.render(createElement(ErrorComponent, props), errorContainer)
|
||||||
} else {
|
} else {
|
||||||
|
@ -120,8 +122,8 @@ async function doRender ({ Component, props, hash, err, emitter }) {
|
||||||
Component !== ErrorComponent &&
|
Component !== ErrorComponent &&
|
||||||
lastAppProps.Component === ErrorComponent) {
|
lastAppProps.Component === ErrorComponent) {
|
||||||
// fetch props if ErrorComponent was replaced with a page component by HMR
|
// fetch props if ErrorComponent was replaced with a page component by HMR
|
||||||
const { pathname, query } = router
|
const { pathname, query, asPath } = router
|
||||||
props = await loadGetInitialProps(Component, { err, pathname, query })
|
props = await loadGetInitialProps(Component, { err, pathname, query, asPath })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (emitter) {
|
if (emitter) {
|
||||||
|
|
|
@ -13,7 +13,7 @@ const SingletonRouter = {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create public properties and methods of the router in the SingletonRouter
|
// Create public properties and methods of the router in the SingletonRouter
|
||||||
const propertyFields = ['components', 'pathname', 'route', 'query']
|
const propertyFields = ['components', 'pathname', 'route', 'query', 'asPath']
|
||||||
const coreMethodFields = ['push', 'replace', 'reload', 'back', 'prefetch']
|
const coreMethodFields = ['push', 'replace', 'reload', 'back', 'prefetch']
|
||||||
const routerEvents = ['routeChangeStart', 'beforeHistoryChange', 'routeChangeComplete', 'routeChangeError']
|
const routerEvents = ['routeChangeStart', 'beforeHistoryChange', 'routeChangeComplete', 'routeChangeError']
|
||||||
|
|
||||||
|
|
|
@ -27,7 +27,7 @@ export default class Router {
|
||||||
this.ErrorComponent = ErrorComponent
|
this.ErrorComponent = ErrorComponent
|
||||||
this.pathname = pathname
|
this.pathname = pathname
|
||||||
this.query = query
|
this.query = query
|
||||||
this.as = as
|
this.asPath = as
|
||||||
this.subscriptions = new Set()
|
this.subscriptions = new Set()
|
||||||
this.componentLoadCancel = null
|
this.componentLoadCancel = null
|
||||||
this.onPopState = this.onPopState.bind(this)
|
this.onPopState = this.onPopState.bind(this)
|
||||||
|
@ -190,7 +190,7 @@ export default class Router {
|
||||||
}
|
}
|
||||||
|
|
||||||
const { Component } = routeInfo
|
const { Component } = routeInfo
|
||||||
const ctx = { pathname, query }
|
const ctx = { pathname, query, asPath: as }
|
||||||
routeInfo.props = await this.getInitialProps(Component, ctx)
|
routeInfo.props = await this.getInitialProps(Component, ctx)
|
||||||
|
|
||||||
this.components[route] = routeInfo
|
this.components[route] = routeInfo
|
||||||
|
@ -229,13 +229,13 @@ export default class Router {
|
||||||
this.route = route
|
this.route = route
|
||||||
this.pathname = pathname
|
this.pathname = pathname
|
||||||
this.query = query
|
this.query = query
|
||||||
this.as = as
|
this.asPath = as
|
||||||
this.notify(data)
|
this.notify(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
onlyAHashChange (as) {
|
onlyAHashChange (as) {
|
||||||
if (!this.as) return false
|
if (!this.asPath) return false
|
||||||
const [ oldUrlNoHash ] = this.as.split('#')
|
const [ oldUrlNoHash ] = this.asPath.split('#')
|
||||||
const [ newUrlNoHash, newHash ] = as.split('#')
|
const [ newUrlNoHash, newHash ] = as.split('#')
|
||||||
|
|
||||||
// If the urls are change, there's more than a hash change
|
// If the urls are change, there's more than a hash change
|
||||||
|
|
|
@ -238,6 +238,7 @@ export default Page
|
||||||
|
|
||||||
- `pathname` - path section of URL
|
- `pathname` - path section of URL
|
||||||
- `query` - query string section of URL parsed as an object
|
- `query` - query string section of URL parsed as an object
|
||||||
|
- `asPath` - the actual url path
|
||||||
- `req` - HTTP request object (server only)
|
- `req` - HTTP request object (server only)
|
||||||
- `res` - HTTP response object (server only)
|
- `res` - HTTP response object (server only)
|
||||||
- `jsonPageRes` - [Fetch Response](https://developer.mozilla.org/en-US/docs/Web/API/Response) object (client only)
|
- `jsonPageRes` - [Fetch Response](https://developer.mozilla.org/en-US/docs/Web/API/Response) object (client only)
|
||||||
|
@ -283,6 +284,7 @@ Each top-level component receives a `url` property with the following API:
|
||||||
|
|
||||||
- `pathname` - `String` of the current path excluding the query string
|
- `pathname` - `String` of the current path excluding the query string
|
||||||
- `query` - `Object` with the parsed query string. Defaults to `{}`
|
- `query` - `Object` with the parsed query string. Defaults to `{}`
|
||||||
|
- `asPath` - `String` of the actual path (including the query) shows in the browser
|
||||||
- `push(url, as=url)` - performs a `pushState` call with the given url
|
- `push(url, as=url)` - performs a `pushState` call with the given url
|
||||||
- `replace(url, as=url)` - performs a `replaceState` call with the given url
|
- `replace(url, as=url)` - performs a `replaceState` call with the given url
|
||||||
|
|
||||||
|
|
|
@ -54,7 +54,8 @@ async function doRender (req, res, pathname, query, {
|
||||||
])
|
])
|
||||||
Component = Component.default || Component
|
Component = Component.default || Component
|
||||||
Document = Document.default || Document
|
Document = Document.default || Document
|
||||||
const ctx = { err, req, res, pathname, query }
|
const asPath = req.url
|
||||||
|
const ctx = { err, req, res, pathname, query, asPath }
|
||||||
const props = await loadGetInitialProps(Component, ctx)
|
const props = await loadGetInitialProps(Component, ctx)
|
||||||
|
|
||||||
// the response might be finshed on the getinitialprops call
|
// the response might be finshed on the getinitialprops call
|
||||||
|
|
22
test/integration/basic/pages/nav/as-path-using-router.js
Normal file
22
test/integration/basic/pages/nav/as-path-using-router.js
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import React from 'react'
|
||||||
|
import Router from 'next/router'
|
||||||
|
|
||||||
|
export default class extends React.Component {
|
||||||
|
constructor (...args) {
|
||||||
|
super(...args)
|
||||||
|
this.state = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
const asPath = Router.asPath
|
||||||
|
this.setState({ asPath })
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
return (
|
||||||
|
<div className='as-path-content'>
|
||||||
|
{this.state.asPath}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
15
test/integration/basic/pages/nav/as-path.js
Normal file
15
test/integration/basic/pages/nav/as-path.js
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
export default class extends React.Component {
|
||||||
|
static getInitialProps ({ asPath, req }) {
|
||||||
|
return { asPath }
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
return (
|
||||||
|
<div className='as-path-content'>
|
||||||
|
{this.props.asPath}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -35,6 +35,9 @@ export default class extends Component {
|
||||||
<a id='query-string-link' style={linkStyle}>QueryString</a>
|
<a id='query-string-link' style={linkStyle}>QueryString</a>
|
||||||
</Link>
|
</Link>
|
||||||
<Link href='/nav/about' replace><a id='about-replace-link' style={linkStyle}>Replace state</a></Link>
|
<Link href='/nav/about' replace><a id='about-replace-link' style={linkStyle}>Replace state</a></Link>
|
||||||
|
<Link href='/nav/as-path' as='/as/path'><a id='as-path-link' style={linkStyle}>As Path</a></Link>
|
||||||
|
<Link href='/nav/as-path'><a id='as-path-link-no-as' style={linkStyle}>As Path (No as)</a></Link>
|
||||||
|
<Link href='/nav/as-path-using-router'><a id='as-path-using-router-link' style={linkStyle}>As Path (Using Router)</a></Link>
|
||||||
<button
|
<button
|
||||||
onClick={() => this.visitQueryStringPage()}
|
onClick={() => this.visitQueryStringPage()}
|
||||||
style={linkStyle}
|
style={linkStyle}
|
||||||
|
|
|
@ -347,5 +347,44 @@ export default (context, render) => {
|
||||||
browser.close()
|
browser.close()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('with asPath', () => {
|
||||||
|
describe('inside getInitialProps', () => {
|
||||||
|
it('should show the correct asPath with a Link with as prop', async () => {
|
||||||
|
const browser = await webdriver(context.appPort, '/nav/')
|
||||||
|
const asPath = await browser
|
||||||
|
.elementByCss('#as-path-link').click()
|
||||||
|
.waitForElementByCss('.as-path-content')
|
||||||
|
.elementByCss('.as-path-content').text()
|
||||||
|
|
||||||
|
expect(asPath).toBe('/as/path')
|
||||||
|
browser.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should show the correct asPath with a Link without the as prop', async () => {
|
||||||
|
const browser = await webdriver(context.appPort, '/nav/')
|
||||||
|
const asPath = await browser
|
||||||
|
.elementByCss('#as-path-link-no-as').click()
|
||||||
|
.waitForElementByCss('.as-path-content')
|
||||||
|
.elementByCss('.as-path-content').text()
|
||||||
|
|
||||||
|
expect(asPath).toBe('/nav/as-path')
|
||||||
|
browser.close()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('with next/router', () => {
|
||||||
|
it('should show the correct asPath', async () => {
|
||||||
|
const browser = await webdriver(context.appPort, '/nav/')
|
||||||
|
const asPath = await browser
|
||||||
|
.elementByCss('#as-path-using-router-link').click()
|
||||||
|
.waitForElementByCss('.as-path-content')
|
||||||
|
.elementByCss('.as-path-content').text()
|
||||||
|
|
||||||
|
expect(asPath).toBe('/nav/as-path-using-router')
|
||||||
|
browser.close()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,6 +51,8 @@ describe('Basic Features', () => {
|
||||||
renderViaHTTP(context.appPort, '/nav/hash-changes'),
|
renderViaHTTP(context.appPort, '/nav/hash-changes'),
|
||||||
renderViaHTTP(context.appPort, '/nav/shallow-routing'),
|
renderViaHTTP(context.appPort, '/nav/shallow-routing'),
|
||||||
renderViaHTTP(context.appPort, '/nav/redirect'),
|
renderViaHTTP(context.appPort, '/nav/redirect'),
|
||||||
|
renderViaHTTP(context.appPort, '/nav/as-path'),
|
||||||
|
renderViaHTTP(context.appPort, '/nav/as-path-using-router'),
|
||||||
|
|
||||||
renderViaHTTP(context.appPort, '/nested-cdm/index')
|
renderViaHTTP(context.appPort, '/nested-cdm/index')
|
||||||
])
|
])
|
||||||
|
|
|
@ -3,8 +3,8 @@
|
||||||
import cheerio from 'cheerio'
|
import cheerio from 'cheerio'
|
||||||
|
|
||||||
export default function ({ app }, suiteName, render) {
|
export default function ({ app }, suiteName, render) {
|
||||||
async function get$ (path) {
|
async function get$ (path, query) {
|
||||||
const html = await render(path)
|
const html = await render(path, query)
|
||||||
return cheerio.load(html)
|
return cheerio.load(html)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,6 +69,11 @@ export default function ({ app }, suiteName, render) {
|
||||||
expect($('pre').text()).toMatch(/This is an expected error/)
|
expect($('pre').text()).toMatch(/This is an expected error/)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('asPath', async () => {
|
||||||
|
const $ = await get$('/nav/as-path', { aa: 10 })
|
||||||
|
expect($('.as-path-content').text()).toBe('/nav/as-path?aa=10')
|
||||||
|
})
|
||||||
|
|
||||||
test('error 404', async () => {
|
test('error 404', async () => {
|
||||||
const $ = await get$('/non-existent')
|
const $ = await get$('/non-existent')
|
||||||
expect($('h1').text()).toBe('404')
|
expect($('h1').text()).toBe('404')
|
||||||
|
|
|
@ -11,7 +11,8 @@ export const nextBuild = build
|
||||||
export const pkg = _pkg
|
export const pkg = _pkg
|
||||||
|
|
||||||
export function renderViaAPI (app, pathname, query = {}) {
|
export function renderViaAPI (app, pathname, query = {}) {
|
||||||
return app.renderToHTML({}, {}, pathname, query)
|
const url = `${pathname}?${qs.stringify(query)}`
|
||||||
|
return app.renderToHTML({ url }, {}, pathname, query)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function renderViaHTTP (appPort, pathname, query = {}) {
|
export function renderViaHTTP (appPort, pathname, query = {}) {
|
||||||
|
|
Loading…
Reference in a new issue