提交 38822717 编写于 作者: S Sergio Daniel Xalambrí 提交者: Arunoda Susiripala

Add support for URL objects in Link and Router (#1345)

* Add support for URL objects in Link and Router

* Fix typo in comment

* Fix possible bug if the `href` prop is `null`

* Document the usage of URL objects in Link and Router

* Update readme.md

* Parse URL to get the host & hostname in `isLocal`

This should check if the current location and the checked URL have the same `host` or `hostname`.

* Format `as` parameter from object to string if required

* Format `href` and `as` inside the construct and componentWillReceiveProps

* Use `JSON.stringify` to compare objects

* Add usage example

* chore(package): update chromedriver to version 2.28.0 (#1386)

https://greenkeeper.io/

* Refactor the codebase a bit.

* Change the example name.

* Add a few test cases.

* Add the example to the README.
上级 1ae3c2e6
# URL object routing
## How to use
Download the example [or clone the repo](https://github.com/zeit/next.js):
```bash
curl https://codeload.github.com/zeit/next.js/tar.gz/master | tar -xz --strip=2 next.js-master/examples/with-url-object-routing
cd with-url-object-routing
```
Install it and run:
```bash
npm install
npm run dev
```
Deploy it to the cloud with [now](https://zeit.co/now) ([download](https://zeit.co/download))
```bash
now
```
## The idea behind the example
Next.js allows using [Node.js URL objects](https://nodejs.org/api/url.html#url_url_strings_and_url_objects) as `href` and `as` values for `<Link>` component and parameters of `Router#push` and `Router#replace`.
This simplify the usage of parameterized URLs when you have many query values.
{
"scripts": {
"dev": "node server.js",
"build": "next build",
"start": "NODE_ENV=production node server.js"
},
"dependencies": {
"next": "beta",
"path-match": "1.2.4",
"react": "^15.4.2",
"react-dom": "^15.4.2"
}
}
import React from 'react'
import Link from 'next/link'
import Router from 'next/router'
const href = {
pathname: '/about',
query: { name: 'zeit' }
}
const as = {
pathname: '/about/zeit',
hash: 'title-1'
}
const handleClick = () => Router.push(href, as)
export default (props) => (
<div>
<h1>About {props.url.query.name}</h1>
{props.url.query.name === 'zeit' ? (
<Link href='/'>
<a>Go to home page</a>
</Link>
) : (
<button onClick={handleClick}>Go to /about/zeit</button>
)}
</div>
)
import React from 'react'
import Link from 'next/link'
const href = {
pathname: '/about',
query: { name: 'next' }
}
const as = {
pathname: '/about/next',
hash: 'title-1'
}
export default () => (
<div>
<h1>Home page</h1>
<Link href={href} as={as}>
<a>Go to /about/next</a>
</Link>
</div>
)
const { createServer } = require('http')
const { parse } = require('url')
const next = require('next')
const pathMatch = require('path-match')
const dev = process.env.NODE_ENV !== 'production'
const app = next({ dev })
const handle = app.getRequestHandler()
const route = pathMatch()
const match = route('/about/:name')
app.prepare()
.then(() => {
createServer((req, res) => {
const { pathname } = parse(req.url)
const params = match(pathname)
if (params === false) {
handle(req, res)
return
}
app.render(req, res, '/about', params)
})
.listen(3000, (err) => {
if (err) throw err
console.log('> Ready on http://localhost:3000')
})
})
import { resolve } from 'url'
import { resolve, format, parse } from 'url'
import React, { Component, Children, PropTypes } from 'react'
import Router from './router'
import { warn, execOnce, getLocationOrigin } from './utils'
export default class Link extends Component {
constructor (props) {
super(props)
constructor (props, ...rest) {
super(props, ...rest)
this.linkClicked = this.linkClicked.bind(this)
this.formatUrls(props)
}
static propTypes = {
......@@ -25,6 +26,10 @@ export default class Link extends Component {
]).isRequired
}
componentWillReceiveProps (nextProps) {
this.formatUrls(nextProps)
}
linkClicked (e) {
if (e.currentTarget.nodeName === 'A' &&
(e.metaKey || e.ctrlKey || e.shiftKey || (e.nativeEvent && e.nativeEvent.which === 2))) {
......@@ -32,7 +37,7 @@ export default class Link extends Component {
return
}
let { href, as } = this.props
let { href, as } = this
if (!isLocal(href)) {
// ignore click if it's outside our scope
......@@ -68,7 +73,7 @@ export default class Link extends Component {
// Prefetch the JSON page if asked (only in the client)
const { pathname } = window.location
const href = resolve(pathname, this.props.href)
const href = resolve(pathname, this.href)
Router.prefetch(href)
}
......@@ -77,13 +82,25 @@ export default class Link extends Component {
}
componentDidUpdate (prevProps) {
if (this.props.href !== prevProps.href) {
if (JSON.stringify(this.props.href) !== JSON.stringify(prevProps.href)) {
this.prefetch()
}
}
// We accept both 'href' and 'as' as objects which we can pass to `url.format`.
// We'll handle it here.
formatUrls (props) {
this.href = props.href && typeof props.href === 'object'
? format(props.href)
: props.href
this.as = props.as && typeof props.as === 'object'
? format(props.as)
: props.as
}
render () {
let { children } = this.props
let { href, as } = this
// Deprecated. Warning shown by propType check. If the childen provided is a string (<Link>example</Link>) we wrap it in an <a> tag
if (typeof children === 'string') {
children = <a>{children}</a>
......@@ -97,7 +114,7 @@ export default class Link extends Component {
// If child is an <a> tag and doesn't have a href attribute we specify it so that repetition is not needed by the user
if (child.type === 'a' && !('href' in child.props)) {
props.href = this.props.as || this.props.href
props.href = as || href
}
return React.cloneElement(child, props)
......@@ -105,9 +122,10 @@ export default class Link extends Component {
}
function isLocal (href) {
const origin = getLocationOrigin()
return !/^(https?:)?\/\//.test(href) ||
origin === href.substr(0, origin.length)
const url = parse(href, false, true)
const origin = parse(getLocationOrigin(), false, true)
return (!url.host || !url.hostname) ||
(origin.host === url.host || origin.hostname === url.hostname)
}
const warnLink = execOnce(warn)
......@@ -128,7 +128,12 @@ export default class Router extends EventEmitter {
return this.change('replaceState', url, as, options)
}
async change (method, 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
this.abortComponentLoad(as)
const { pathname, query } = parse(url, true)
......
......@@ -271,6 +271,27 @@ Each top-level component receives a `url` property with the following API:
The second `as` parameter for `push` and `replace` is an optional _decoration_ of the URL. Useful if you configured custom routes on the server.
##### With URL object
<p><details>
<summary><b>Examples</b></summary>
<ul>
<li><a href="./examples/with-url-object-routing">With URL Object Routing</a></li>
</ul>
</details></p>
The component `<Link>` can also receive an URL object and it will automatically format it to create the URL string.
```jsx
// pages/index.js
import Link from 'next/link'
export default () => (
<div>Click <Link href={{ pathname: 'about', query: { name: 'Zeit' }}}<a>here</a></Link> to read more</div>
)
```
That will generate the URL string `/about?name=Zeit`, you can use every property as defined in the [Node.js URL module documentation](https://nodejs.org/api/url.html#url_url_strings_and_url_objects).
#### Imperatively
<p><details>
......@@ -303,6 +324,24 @@ The second `as` parameter for `push` and `replace` is an optional _decoration_ o
_Note: in order to programmatically change the route without triggering navigation and component-fetching, use `props.url.push` and `props.url.replace` within a component_
##### With URL object
You can use an URL object the same way you use it in a `<Link>` component to `push` and `replace` an url.
```jsx
import Router from 'next/router'
const handler = () => Router.push({
pathname: 'about',
query: { name: 'Zeit' }
})
export default () => (
<div>Click <span onClick={handler}>here</span> to read more</div>
)
```
This uses of the same exact parameters as in the `<Link>` component.
##### Router Events
You can also listen to different events happening inside the Router.
......
import Link from 'next/link'
import { Component } from 'react'
import Router from 'next/router'
let counter = 0
......@@ -13,6 +14,12 @@ export default class extends Component {
this.forceUpdate()
}
visitQueryStringPage () {
const href = { pathname: '/nav/querystring', query: { id: 10 } }
const as = { pathname: '/nav/querystring/10', hash: '10' }
Router.push(href, as)
}
render () {
return (
<div className='nav-home'>
......@@ -20,6 +27,20 @@ export default class extends Component {
<Link href='/empty-get-initial-props'><a id='empty-props' style={linkStyle}>Empty Props</a></Link>
<Link href='/nav/self-reload'><a id='self-reload-link' style={linkStyle}>Self Reload</a></Link>
<Link href='/nav/shallow-routing'><a id='shallow-routing-link' style={linkStyle}>Shallow Routing</a></Link>
<Link
href={{ pathname: '/nav/querystring', query: { id: 10 } }}
as={{ pathname: '/nav/querystring/10', hash: '10' }}
>
<a id='query-string-link' style={linkStyle}>QueryString</a>
</Link>
<button
onClick={() => this.visitQueryStringPage()}
style={linkStyle}
id='query-string-button'
>
Visit QueryString Page
</button>
<p>This is the home.</p>
<div id='counter'>
Counter: {counter}
......
......@@ -8,7 +8,7 @@ export default class AsyncProps extends React.Component {
render () {
return (
<div>
<div className='nav-querystring'>
<Link href={`/nav/querystring?id=${parseInt(this.props.id) + 1}`}>
<a id='next-id-link'>Click here</a>
</Link>
......
......@@ -236,5 +236,33 @@ export default (context, render) => {
browser.close()
})
})
describe('with URL objects', () => {
it('should work with <Link/>', async () => {
const browser = await webdriver(context.appPort, '/nav')
const text = await browser
.elementByCss('#query-string-link').click()
.waitForElementByCss('.nav-querystring')
.elementByCss('p').text()
expect(text).toBe('10')
expect(await browser.url())
.toBe(`http://localhost:${context.appPort}/nav/querystring/10#10`)
browser.close()
})
it('should work with "Router.push"', async () => {
const browser = await webdriver(context.appPort, '/nav')
const text = await browser
.elementByCss('#query-string-button').click()
.waitForElementByCss('.nav-querystring')
.elementByCss('p').text()
expect(text).toBe('10')
expect(await browser.url())
.toBe(`http://localhost:${context.appPort}/nav/querystring/10#10`)
browser.close()
})
})
})
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册