mirror of
https://github.com/terribleplan/next.js.git
synced 2024-01-19 02:48:18 +00:00
Implement the Singleton Router API (#429)
* Immplement the initial singleton Router. * Use the new SingletonRouter for HMR error handling. * Use SingletonRouter inside the Link. * Create an example app using the Router. * Make the url parameter optional in Router.push and Router.replace * Add a section about next/router in the README.
This commit is contained in:
parent
955f6817c4
commit
22776c2eee
47
README.md
47
README.md
|
@ -142,9 +142,9 @@ For the initial page load, `getInitialProps` will execute on the server only. `g
|
|||
- `xhr` - XMLHttpRequest object (client only)
|
||||
- `err` - Error object if any error is encountered during the rendering
|
||||
|
||||
### Routing
|
||||
### Routing with <Link>
|
||||
|
||||
Client-side transitions between routes are enabled via a `<Link>` component
|
||||
Client-side transitions between routes can be enabled via a `<Link>` component
|
||||
|
||||
#### pages/index.js
|
||||
|
||||
|
@ -178,6 +178,49 @@ Each top-level component receives a `url` property with the following API:
|
|||
- `pushTo(url)` - performs a `pushState` call that renders the new `url`. This is equivalent to following a `<Link>`
|
||||
- `replaceTo(url)` - performs a `replaceState` call that renders the new `url`
|
||||
|
||||
### Routing with next/router
|
||||
|
||||
You can also do client-side page transitions using the `next/router`. This is the same API used inside the above `<Link />` component.
|
||||
|
||||
```jsx
|
||||
import Router from 'next/router'
|
||||
|
||||
const routeTo(href) {
|
||||
return (e) => {
|
||||
e.preventDefault()
|
||||
Router.push(href)
|
||||
}
|
||||
}
|
||||
|
||||
export default () => (
|
||||
<div>Click <a href='#' onClick={routeTo('/about')}>here</a> to read more</div>
|
||||
)
|
||||
```
|
||||
|
||||
#### pages/about.js
|
||||
|
||||
```jsx
|
||||
export default () => (
|
||||
<p>Welcome to About!</p>
|
||||
)
|
||||
```
|
||||
|
||||
Above `Router` object comes with the following API:
|
||||
|
||||
- `route` - `String` of the current route
|
||||
- `pathname` - `String` of the current path excluding the query string
|
||||
- `query` - `Object` with the parsed query string. Defaults to `{}`
|
||||
- `push(url, pathname=url)` - performs a `pushState` call associated with the current component
|
||||
- `replace(url, pathname=url)` - performs a `replaceState` call associated with the current component
|
||||
|
||||
> Usually, route is the same as pathname.
|
||||
> But when used with programmatic API, route and pathname can be different.
|
||||
> "route" is your actual page's path while "pathname" is the path of the url mapped to it.
|
||||
>
|
||||
> Likewise, url and path is the same usually.
|
||||
> But when used with programmatic API, "url" is the route with the query string.
|
||||
> "pathname" is the path of the url mapped to it.
|
||||
|
||||
### Prefetching Pages
|
||||
|
||||
Next.js exposes a module that configures a `ServiceWorker` automatically to prefetch pages: `next/prefetch`.
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import { createElement } from 'react'
|
||||
import { render } from 'react-dom'
|
||||
import HeadManager from './head-manager'
|
||||
import domready from 'domready'
|
||||
import { rehydrate } from '../lib/css'
|
||||
import Router from '../lib/router'
|
||||
import { createRouter } from '../lib/router'
|
||||
import App from '../lib/app'
|
||||
import evalScript from '../lib/eval-script'
|
||||
|
||||
|
@ -19,25 +18,18 @@ const {
|
|||
}
|
||||
} = window
|
||||
|
||||
domready(() => {
|
||||
const Component = evalScript(component).default
|
||||
const ErrorComponent = evalScript(errorComponent).default
|
||||
|
||||
const router = new Router(pathname, query, {
|
||||
export const router = createRouter(pathname, query, {
|
||||
Component,
|
||||
ErrorComponent,
|
||||
ctx: { err }
|
||||
})
|
||||
|
||||
// This it to support error handling in the dev time with hot code reload.
|
||||
if (window.next) {
|
||||
window.next.router = router
|
||||
}
|
||||
|
||||
const headManager = new HeadManager()
|
||||
const container = document.getElementById('__next')
|
||||
const appProps = { Component, props, router, headManager }
|
||||
|
||||
if (ids) rehydrate(ids)
|
||||
render(createElement(App, appProps), container)
|
||||
})
|
||||
|
|
|
@ -1,27 +1,27 @@
|
|||
/* global next */
|
||||
import webpackHotMiddlewareClient from 'webpack-hot-middleware/client?overlay=false&reload=true'
|
||||
import Router from '../lib/router'
|
||||
|
||||
const handlers = {
|
||||
reload (route) {
|
||||
if (route === '/_error') {
|
||||
for (const r of Object.keys(next.router.components)) {
|
||||
const { Component } = next.router.components[r]
|
||||
for (const r of Object.keys(Router.components)) {
|
||||
const { Component } = Router.components[r]
|
||||
if (Component.__route === '/_error-debug') {
|
||||
// reload all '/_error-debug'
|
||||
// which are expected to be errors of '/_error' routes
|
||||
next.router.reload(r)
|
||||
Router.reload(r)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
next.router.reload(route)
|
||||
Router.reload(route)
|
||||
},
|
||||
change (route) {
|
||||
const { Component } = next.router.components[route] || {}
|
||||
const { Component } = Router.components[route] || {}
|
||||
if (Component && Component.__route === '/_error-debug') {
|
||||
// reload to recover from runtime errors
|
||||
next.router.reload(route)
|
||||
Router.reload(route)
|
||||
}
|
||||
},
|
||||
hardReload () {
|
||||
|
|
13
examples/using-router/README.md
Normal file
13
examples/using-router/README.md
Normal file
|
@ -0,0 +1,13 @@
|
|||
# Example app utilizing next/router for routing
|
||||
|
||||
This example features:
|
||||
|
||||
* An app linking pages using `next/router` instead of `<Link>` component.
|
||||
* Access the pathname using `next/router` and render it in a component
|
||||
|
||||
## How to run it
|
||||
|
||||
```sh
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
31
examples/using-router/components/Header.js
Normal file
31
examples/using-router/components/Header.js
Normal file
|
@ -0,0 +1,31 @@
|
|||
import React from 'react'
|
||||
import Router from 'next/router'
|
||||
|
||||
const styles = {
|
||||
a: {
|
||||
marginRight: 10
|
||||
}
|
||||
}
|
||||
|
||||
const Link = ({ children, href }) => (
|
||||
<a
|
||||
href='#'
|
||||
style={styles.a}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
Router.push(href)
|
||||
}}
|
||||
>
|
||||
{ children }
|
||||
</a>
|
||||
)
|
||||
|
||||
export default () => (
|
||||
<div>
|
||||
<Link href='/'>Home</Link>
|
||||
<Link href='/about'>About</Link>
|
||||
<div>
|
||||
<small>Now you are in the route: {Router.route} </small>
|
||||
</div>
|
||||
</div>
|
||||
)
|
16
examples/using-router/package.json
Normal file
16
examples/using-router/package.json
Normal file
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"name": "shared-modules",
|
||||
"version": "1.0.0",
|
||||
"description": "This example features:",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"dev": "next",
|
||||
"build": "next build",
|
||||
"start": "next start"
|
||||
},
|
||||
"dependencies": {
|
||||
"next": "*"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC"
|
||||
}
|
9
examples/using-router/pages/about.js
Normal file
9
examples/using-router/pages/about.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
import React from 'react'
|
||||
import Header from '../components/Header'
|
||||
|
||||
export default () => (
|
||||
<div>
|
||||
<Header />
|
||||
<p>This is the about page.</p>
|
||||
</div>
|
||||
)
|
9
examples/using-router/pages/index.js
Normal file
9
examples/using-router/pages/index.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
import React from 'react'
|
||||
import Header from '../components/Header'
|
||||
|
||||
export default () => (
|
||||
<div>
|
||||
<Header />
|
||||
<p>HOME PAGE is here!</p>
|
||||
</div>
|
||||
)
|
|
@ -1,10 +1,7 @@
|
|||
import React, { Component, PropTypes, Children } from 'react'
|
||||
import React, { Component, Children } from 'react'
|
||||
import Router from './router'
|
||||
|
||||
export default class Link extends Component {
|
||||
static contextTypes = {
|
||||
router: PropTypes.object
|
||||
}
|
||||
|
||||
constructor (props) {
|
||||
super(props)
|
||||
this.linkClicked = this.linkClicked.bind(this)
|
||||
|
@ -30,7 +27,7 @@ export default class Link extends Component {
|
|||
const url = as || href
|
||||
|
||||
// straight up redirect
|
||||
this.context.router.push(route, url)
|
||||
Router.push(route, url)
|
||||
.then((success) => {
|
||||
if (!success) return
|
||||
if (scroll !== false) window.scrollTo(0, 0)
|
||||
|
|
51
lib/router/index.js
Normal file
51
lib/router/index.js
Normal file
|
@ -0,0 +1,51 @@
|
|||
import _Router from './router'
|
||||
|
||||
// holds the actual router instance
|
||||
let router = null
|
||||
|
||||
const SingletonRouter = {}
|
||||
|
||||
// Create public properties and methods of the router in the SingletonRouter
|
||||
const propertyFields = ['route', 'components', 'pathname', 'query']
|
||||
const methodFields = ['push', 'replace', 'reload', 'back']
|
||||
|
||||
propertyFields.forEach((field) => {
|
||||
// Here we need to use Object.defineProperty because, we need to return
|
||||
// the property assigned to the actual router
|
||||
// The value might get changed as we change routes and this is the
|
||||
// proper way to access it
|
||||
Object.defineProperty(SingletonRouter, field, {
|
||||
get () {
|
||||
return router[field]
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
methodFields.forEach((field) => {
|
||||
SingletonRouter[field] = (...args) => {
|
||||
return router[field](...args)
|
||||
}
|
||||
})
|
||||
|
||||
// This is an internal method and it should not be called directly.
|
||||
//
|
||||
// ## Client Side Usage
|
||||
// We create the router in the client side only for a single time when we are
|
||||
// booting the app. It happens before rendering any components.
|
||||
// At the time of the component rendering, there'll be a router instance
|
||||
//
|
||||
// ## Server Side Usage
|
||||
// We create router for every SSR page render.
|
||||
// Since rendering happens in the same eventloop this works properly.
|
||||
export const createRouter = function (...args) {
|
||||
router = new _Router(...args)
|
||||
return router
|
||||
}
|
||||
|
||||
// Export the actual Router class, which is also use internally
|
||||
// You'll ever need to access this directly
|
||||
export const Router = _Router
|
||||
|
||||
// Export the SingletonRouter and this is the public API.
|
||||
// This is an client side API and doesn't available on the server
|
||||
export default SingletonRouter
|
|
@ -1,6 +1,6 @@
|
|||
import { parse } from 'url'
|
||||
import evalScript from './eval-script'
|
||||
import shallowEquals from './shallow-equals'
|
||||
import evalScript from '../eval-script'
|
||||
import shallowEquals from '../shallow-equals'
|
||||
|
||||
export default class Router {
|
||||
constructor (pathname, query, { Component, ErrorComponent, ctx } = {}) {
|
||||
|
@ -97,11 +97,11 @@ export default class Router {
|
|||
window.history.back()
|
||||
}
|
||||
|
||||
push (route, url) {
|
||||
push (route, url = route) {
|
||||
return this.change('pushState', route, url)
|
||||
}
|
||||
|
||||
replace (route, url) {
|
||||
replace (route, url = route) {
|
||||
return this.change('replaceState', route, url)
|
||||
}
|
||||
|
|
@ -48,7 +48,6 @@
|
|||
"chokidar": "1.6.1",
|
||||
"cross-spawn": "5.0.1",
|
||||
"del": "2.2.2",
|
||||
"domready": "1.0.8",
|
||||
"friendly-errors-webpack-plugin": "1.1.2",
|
||||
"glamor": "2.20.12",
|
||||
"glob-promise": "3.1.0",
|
||||
|
|
|
@ -3,7 +3,7 @@ import { createElement } from 'react'
|
|||
import { renderToString, renderToStaticMarkup } from 'react-dom/server'
|
||||
import requireModule from './require'
|
||||
import read from './read'
|
||||
import Router from '../lib/router'
|
||||
import { createRouter } from '../lib/router'
|
||||
import Head, { defaultHead } from '../lib/head'
|
||||
import App from '../lib/app'
|
||||
|
||||
|
@ -56,10 +56,11 @@ async function doRender (req, res, pathname, query, {
|
|||
if (res.finished) return
|
||||
|
||||
const renderPage = () => {
|
||||
const router = createRouter(pathname, query)
|
||||
const app = createElement(App, {
|
||||
Component,
|
||||
props,
|
||||
router: new Router(pathname, query)
|
||||
router
|
||||
})
|
||||
const html = (staticMarkup ? renderToStaticMarkup : renderToString)(app)
|
||||
const head = Head.rewind() || defaultHead()
|
||||
|
|
Loading…
Reference in a new issue