1
0
Fork 0
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:
Arunoda Susiripala 2016-12-19 20:10:26 +05:30 committed by Guillermo Rauch
parent 955f6817c4
commit 22776c2eee
14 changed files with 218 additions and 56 deletions

View file

@ -22,7 +22,7 @@ and add a script to your package.json like this:
{
"scripts": {
"dev": "next"
}
}
}
```
@ -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,11 +178,54 @@ 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`.
Next.js exposes a module that configures a `ServiceWorker` automatically to prefetch pages: `next/prefetch`.
Since Next.js server-renders your pages, this allows all the future interaction paths of your app to be instant. Effectively Next.js gives you the great initial download performance of a _website_, with the ahead-of-time download capabilities of an _app_. [Read more](https://zeit.co/blog/next#anticipation-is-the-key-to-performance).
Since Next.js server-renders your pages, this allows all the future interaction paths of your app to be instant. Effectively Next.js gives you the great initial download performance of a _website_, with the ahead-of-time download capabilities of an _app_. [Read more](https://zeit.co/blog/next#anticipation-is-the-key-to-performance).
#### Link prefetching
@ -251,7 +294,7 @@ export default class Error extends React.Component {
### Custom configuration
For custom advanced behavior of Next.js, you can create a `next.config.js` in the root of your project directory (next to `pages/` and `package.json`).
For custom advanced behavior of Next.js, you can create a `next.config.js` in the root of your project directory (next to `pages/` and `package.json`).
Note: `next.config.js` is a regular Node.js module, not a JSON file. It gets used by the Next server and build phases, and not included in the browser build.
@ -264,7 +307,7 @@ module.exports = {
### Customizing webpack config
In order to extend our usage of `webpack`, you can define a function that extends its config.
In order to extend our usage of `webpack`, you can define a function that extends its config.
The following example shows how you can use [`react-svg-loader`](https://github.com/boopathi/react-svg-loader) to easily import any `.svg` file as a React component, without modification.

View file

@ -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 Component = evalScript(component).default
const ErrorComponent = evalScript(errorComponent).default
const router = new Router(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)
export const router = createRouter(pathname, query, {
Component,
ErrorComponent,
ctx: { err }
})
const headManager = new HeadManager()
const container = document.getElementById('__next')
const appProps = { Component, props, router, headManager }
if (ids) rehydrate(ids)
render(createElement(App, appProps), container)

View file

@ -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 () {

View 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
```

View 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>
)

View 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"
}

View 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>
)

View 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>
)

View file

@ -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,14 +27,14 @@ export default class Link extends Component {
const url = as || href
// straight up redirect
this.context.router.push(route, url)
.then((success) => {
if (!success) return
if (scroll !== false) window.scrollTo(0, 0)
})
.catch((err) => {
if (this.props.onError) this.props.onError(err)
})
Router.push(route, url)
.then((success) => {
if (!success) return
if (scroll !== false) window.scrollTo(0, 0)
})
.catch((err) => {
if (this.props.onError) this.props.onError(err)
})
}
render () {

51
lib/router/index.js Normal file
View 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

View file

@ -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)
}

View file

@ -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",

1
router.js Normal file
View file

@ -0,0 +1 @@
module.exports = require('./dist/lib/router')

View file

@ -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()