mirror of
https://github.com/terribleplan/next.js.git
synced 2024-01-19 02:48:18 +00:00
Introduce a simple prefetching solution (#957)
* Implement a very simple prefetching solution. * Remove next-prefetcher. * Require 'whatwg-fetch' only in the client. * Use xhr in the code. * Use a simple fetching solution. * Fix 404 and xhr status issue. * Move the prefetching implementation to next/router. * Add deprecated warnning for next/prefetch * Run only 2 parellel prefetching request at a time. * Change xhr to jsonPageRes. * Improve the prefetching logic. * Add unit tests covering the Router.prefetch() * Update examples to use the new syntax. * Update docs. * Use execOnce() to manage warn printing. * Remove prefetcher building from the flyfile.js Because, we no longer use it.
This commit is contained in:
parent
d382e4db9a
commit
14c86bef1d
27
README.md
27
README.md
|
@ -344,40 +344,37 @@ Router.onRouteChangeError = (err, url) => {
|
|||
<ul><li><a href="./examples/with-prefetching">Prefetching</a></li></ul>
|
||||
</details></p>
|
||||
|
||||
Next.js exposes a module that configures a `ServiceWorker` automatically to prefetch pages: `next/prefetch`.
|
||||
Next.js has an API which allows you to prefetch pages.
|
||||
|
||||
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).
|
||||
|
||||
> With prefetching Next.js only download JS code. When the page is getting rendered, you may need to wait for the data.
|
||||
|
||||
#### With `<Link>`
|
||||
|
||||
You can substitute your usage of `<Link>` with the default export of `next/prefetch`. For example:
|
||||
You can add `prefetch` prop to any `<Link>` and Next.js will prefetch those pages in the background.
|
||||
|
||||
```jsx
|
||||
import Link from 'next/prefetch'
|
||||
import Link from 'next/link'
|
||||
|
||||
// example header component
|
||||
export default () => (
|
||||
<nav>
|
||||
<ul>
|
||||
<li><Link href='/'><a>Home</a></Link></li>
|
||||
<li><Link href='/about'><a>About</a></Link></li>
|
||||
<li><Link href='/contact'><a>Contact</a></Link></li>
|
||||
<li><Link prefetch ref='/'><a>Home</a></Link></li>
|
||||
<li><Link prefetch href='/about'><a>About</a></Link></li>
|
||||
<li><Link prefetch href='/contact'><a>Contact</a></Link></li>
|
||||
</ul>
|
||||
</nav>
|
||||
)
|
||||
```
|
||||
|
||||
When this higher-level `<Link>` component is first used, the `ServiceWorker` gets installed. To turn off prefetching on a per-`<Link>` basis, you can use the `prefetch` attribute:
|
||||
|
||||
```jsx
|
||||
<Link href='/contact' prefetch={false}><a>Home</a></Link>
|
||||
```
|
||||
|
||||
#### Imperatively
|
||||
|
||||
Most needs are addressed by `<Link />`, but we also expose an imperative API for advanced usage:
|
||||
Most prefetching needs are addressed by `<Link />`, but we also expose an imperative API for advanced usage:
|
||||
|
||||
```jsx
|
||||
import { prefetch } from 'next/prefetch'
|
||||
import Router from 'next/router'
|
||||
export default ({ url }) => (
|
||||
<div>
|
||||
<a onClick={ () => setTimeout(() => url.pushTo('/dynamic'), 100) }>
|
||||
|
@ -385,7 +382,7 @@ export default ({ url }) => (
|
|||
</a>
|
||||
{
|
||||
// but we can prefetch it!
|
||||
prefetch('/dynamic')
|
||||
Router.prefetch('/dynamic')
|
||||
}
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -1,107 +0,0 @@
|
|||
/* global self */
|
||||
|
||||
const CACHE_NAME = 'next-prefetcher-v1'
|
||||
const log = () => {}
|
||||
|
||||
self.addEventListener('install', () => {
|
||||
log('Installing Next Prefetcher')
|
||||
})
|
||||
|
||||
self.addEventListener('activate', (e) => {
|
||||
log('Activated Next Prefetcher')
|
||||
e.waitUntil(Promise.all([
|
||||
resetCache(),
|
||||
notifyClients()
|
||||
]))
|
||||
})
|
||||
|
||||
self.addEventListener('fetch', (e) => {
|
||||
// bypass all requests except JSON pages.
|
||||
if (!(/\/_next\/[^/]+\/pages\//.test(e.request.url))) return
|
||||
|
||||
e.respondWith(getResponse(e.request))
|
||||
})
|
||||
|
||||
self.addEventListener('message', (e) => {
|
||||
switch (e.data.action) {
|
||||
case 'ADD_URL': {
|
||||
log('CACHING ', e.data.url)
|
||||
sendReply(e, cacheUrl(e.data.url))
|
||||
break
|
||||
}
|
||||
case 'RESET': {
|
||||
log('RESET')
|
||||
sendReply(e, resetCache())
|
||||
break
|
||||
}
|
||||
default:
|
||||
console.error('Unknown action: ' + e.data.action)
|
||||
}
|
||||
})
|
||||
|
||||
function sendReply (e, result) {
|
||||
const payload = { action: 'REPLY', actionType: e.data.action, replyFor: e.data.id }
|
||||
result
|
||||
.then((result) => {
|
||||
payload.result = result
|
||||
e.source.postMessage(payload)
|
||||
})
|
||||
.catch((error) => {
|
||||
payload.error = error.message
|
||||
e.source.postMessage(payload)
|
||||
})
|
||||
}
|
||||
|
||||
function cacheUrl (url) {
|
||||
const req = new self.Request(url, {
|
||||
mode: 'no-cors',
|
||||
headers: {
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
return self.caches.open(CACHE_NAME)
|
||||
.then((cache) => {
|
||||
return self.fetch(req)
|
||||
.then((res) => cache.put(req, res))
|
||||
})
|
||||
}
|
||||
|
||||
function getResponse (req) {
|
||||
return self.caches.open(CACHE_NAME)
|
||||
.then((cache) => cache.match(req))
|
||||
.then((res) => {
|
||||
if (res) {
|
||||
log('CACHE HIT: ' + req.url)
|
||||
return res
|
||||
} else {
|
||||
log('CACHE MISS: ' + req.url)
|
||||
return self.fetch(req)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function resetCache () {
|
||||
let cache
|
||||
|
||||
return self.caches.open(CACHE_NAME)
|
||||
.then((c) => {
|
||||
cache = c
|
||||
return cache.keys()
|
||||
})
|
||||
.then(function (items) {
|
||||
const deleteAll = items.map((item) => cache.delete(item))
|
||||
return Promise.all(deleteAll)
|
||||
})
|
||||
}
|
||||
|
||||
function notifyClients () {
|
||||
return self.clients.claim()
|
||||
.then(() => self.clients.matchAll())
|
||||
.then((clients) => {
|
||||
const notifyAll = clients.map((client) => {
|
||||
return client.postMessage({ action: 'NEXT_PREFETCHER_ACTIVATED' })
|
||||
})
|
||||
return Promise.all(notifyAll)
|
||||
})
|
||||
}
|
|
@ -1,23 +1,23 @@
|
|||
import Link, { prefetch } from 'next/prefetch'
|
||||
import RegularLink from 'next/link'
|
||||
import Router from 'next/router'
|
||||
import Link from 'next/link'
|
||||
|
||||
export default () => (
|
||||
<div>
|
||||
{ /* Prefetch using the declarative API */ }
|
||||
<Link href='/'>
|
||||
<Link prefetch href='/'>
|
||||
<a>Home</a>
|
||||
</Link>
|
||||
|
||||
<Link href='/features'>
|
||||
<Link prefetch href='/features'>
|
||||
<a>Features</a>
|
||||
</Link>
|
||||
|
||||
{ /* we imperatively prefetch on hover */ }
|
||||
<RegularLink href='/about'>
|
||||
<a onMouseEnter={() => { prefetch('/about'); console.log('prefetching /about!') }}>About</a>
|
||||
</RegularLink>
|
||||
<Link href='/about'>
|
||||
<a onMouseEnter={() => { Router.prefetch('/about'); console.log('prefetching /about!') }}>About</a>
|
||||
</Link>
|
||||
|
||||
<Link href='/contact' prefetch={false}>
|
||||
<Link href='/contact'>
|
||||
<a>Contact (<small>NO-PREFETCHING</small>)</a>
|
||||
</Link>
|
||||
|
||||
|
|
15
flyfile.js
15
flyfile.js
|
@ -1,7 +1,6 @@
|
|||
const webpack = require('webpack')
|
||||
const notifier = require('node-notifier')
|
||||
const childProcess = require('child_process')
|
||||
const webpackConfig = require('./webpack.config')
|
||||
const isWindows = /^win/.test(process.platform)
|
||||
|
||||
export async function compile(fly) {
|
||||
|
@ -42,15 +41,7 @@ export async function copy(fly) {
|
|||
}
|
||||
|
||||
export async function build(fly) {
|
||||
await fly.serial(['copy', 'compile', 'prefetcher'])
|
||||
}
|
||||
|
||||
const compiler = webpack(webpackConfig)
|
||||
export async function prefetcher(fly) {
|
||||
compiler.run((err, stats) => {
|
||||
if (err) throw err
|
||||
notify('Built release prefetcher')
|
||||
})
|
||||
await fly.serial(['copy', 'compile'])
|
||||
}
|
||||
|
||||
export async function bench(fly) {
|
||||
|
@ -70,8 +61,8 @@ export default async function (fly) {
|
|||
await fly.watch('bin/*', 'bin')
|
||||
await fly.watch('pages/**/*.js', 'copy')
|
||||
await fly.watch('server/**/*.js', 'server')
|
||||
await fly.watch('client/**/*.js', ['client', 'prefetcher'])
|
||||
await fly.watch('lib/**/*.js', ['lib', 'prefetcher'])
|
||||
await fly.watch('client/**/*.js', ['client'])
|
||||
await fly.watch('lib/**/*.js', ['lib'])
|
||||
}
|
||||
|
||||
export async function release(fly) {
|
||||
|
|
11
lib/link.js
11
lib/link.js
|
@ -62,7 +62,7 @@ export default class Link extends Component {
|
|||
}
|
||||
|
||||
render () {
|
||||
let { children } = this.props
|
||||
let { children, prefetch } = this.props
|
||||
// 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>
|
||||
|
@ -79,11 +79,18 @@ export default class Link extends Component {
|
|||
props.href = this.props.as || this.props.href
|
||||
}
|
||||
|
||||
// Prefetch the JSON page if asked (only in the client)
|
||||
if (prefetch) {
|
||||
if (typeof window !== 'undefined') {
|
||||
Router.prefetch(props.href)
|
||||
}
|
||||
}
|
||||
|
||||
return React.cloneElement(child, props)
|
||||
}
|
||||
}
|
||||
|
||||
export function isLocal (href) {
|
||||
function isLocal (href) {
|
||||
const origin = getLocationOrigin()
|
||||
return !/^(https?:)?\/\//.test(href) ||
|
||||
origin === href.substr(0, origin.length)
|
||||
|
|
159
lib/prefetch.js
159
lib/prefetch.js
|
@ -1,150 +1,33 @@
|
|||
/* global __NEXT_DATA__ */
|
||||
|
||||
import React from 'react'
|
||||
import Link, { isLocal } from './link'
|
||||
import { parse as urlParse } from 'url'
|
||||
import Link from './link'
|
||||
import Router from './router'
|
||||
import { warn, execOnce } from './utils'
|
||||
|
||||
class Messenger {
|
||||
constructor () {
|
||||
this.id = 0
|
||||
this.callacks = {}
|
||||
this.serviceWorkerReadyCallbacks = []
|
||||
this.serviceWorkerState = null
|
||||
const warnImperativePrefetch = execOnce(() => {
|
||||
const message = '> You are using deprecated "next/prefetch". It will be removed with Next.js 2.0.\n' +
|
||||
'> Use "Router.prefetch(href)" instead.'
|
||||
warn(message)
|
||||
})
|
||||
|
||||
navigator.serviceWorker.addEventListener('message', ({ data }) => {
|
||||
if (data.action !== 'REPLY') return
|
||||
if (this.callacks[data.replyFor]) {
|
||||
this.callacks[data.replyFor](data)
|
||||
}
|
||||
})
|
||||
const wantLinkPrefetch = execOnce(() => {
|
||||
const message = '> You are using deprecated "next/prefetch". It will be removed with Next.js 2.0.\n' +
|
||||
'> Use "<Link prefetch />" instead.'
|
||||
warn(message)
|
||||
})
|
||||
|
||||
// Reset the cache always.
|
||||
// Sometimes, there's an already running service worker with cached requests.
|
||||
// If the app doesn't use any prefetch calls, `ensureInitialized` won't get
|
||||
// called and cleanup resources.
|
||||
// So, that's why we do this.
|
||||
this._resetCache()
|
||||
}
|
||||
|
||||
send (payload) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (this.serviceWorkerState === 'REGISTERED') {
|
||||
this._send(payload, handleCallback)
|
||||
} else {
|
||||
this.serviceWorkerReadyCallbacks.push(() => {
|
||||
this._send(payload, handleCallback)
|
||||
})
|
||||
}
|
||||
|
||||
function handleCallback (err) {
|
||||
if (err) return reject(err)
|
||||
return resolve()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
_send (payload, cb = () => {}) {
|
||||
const id = this.id ++
|
||||
const newPayload = { ...payload, id }
|
||||
|
||||
this.callacks[id] = (data) => {
|
||||
if (data.error) {
|
||||
cb(data.error)
|
||||
} else {
|
||||
cb(null, data.result)
|
||||
}
|
||||
|
||||
delete this.callacks[id]
|
||||
}
|
||||
|
||||
navigator.serviceWorker.controller.postMessage(newPayload)
|
||||
}
|
||||
|
||||
_resetCache (cb) {
|
||||
const reset = () => {
|
||||
this._send({ action: 'RESET' }, cb)
|
||||
}
|
||||
|
||||
if (navigator.serviceWorker.controller) {
|
||||
reset()
|
||||
} else {
|
||||
navigator.serviceWorker.oncontrollerchange = reset
|
||||
}
|
||||
}
|
||||
|
||||
ensureInitialized () {
|
||||
if (this.serviceWorkerState) {
|
||||
return
|
||||
}
|
||||
|
||||
this.serviceWorkerState = 'REGISTERING'
|
||||
navigator.serviceWorker.register('/_next-prefetcher.js')
|
||||
|
||||
// Reset the cache after registered
|
||||
// We don't need to have any old caches since service workers lives beyond
|
||||
// life time of the webpage.
|
||||
// With this prefetching won't work 100% if multiple pages of the same app
|
||||
// loads in the same browser in same time.
|
||||
// Basically, cache will only have prefetched resourses for the last loaded
|
||||
// page of a given app.
|
||||
// We could mitigate this, when we add a hash to a every file we fetch.
|
||||
this._resetCache((err) => {
|
||||
if (err) throw err
|
||||
this.serviceWorkerState = 'REGISTERED'
|
||||
this.serviceWorkerReadyCallbacks.forEach(cb => cb())
|
||||
this.serviceWorkerReadyCallbacks = []
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function hasServiceWorkerSupport () {
|
||||
return (typeof navigator !== 'undefined' && navigator.serviceWorker)
|
||||
}
|
||||
|
||||
const PREFETCHED_URLS = {}
|
||||
let messenger
|
||||
|
||||
if (hasServiceWorkerSupport()) {
|
||||
messenger = new Messenger()
|
||||
}
|
||||
|
||||
function getPrefetchUrl (href) {
|
||||
let { pathname } = urlParse(href)
|
||||
const url = `/_next/${__NEXT_DATA__.buildId}/pages${pathname}`
|
||||
|
||||
return url
|
||||
}
|
||||
|
||||
export async function prefetch (href) {
|
||||
if (!hasServiceWorkerSupport()) return
|
||||
if (!isLocal(href)) return
|
||||
|
||||
// Register the service worker if it's not.
|
||||
messenger.ensureInitialized()
|
||||
|
||||
const url = getPrefetchUrl(href)
|
||||
if (!PREFETCHED_URLS[url]) {
|
||||
PREFETCHED_URLS[url] = messenger.send({ action: 'ADD_URL', url: url })
|
||||
}
|
||||
|
||||
return PREFETCHED_URLS[url]
|
||||
}
|
||||
|
||||
export async function reloadIfPrefetched (href) {
|
||||
const url = getPrefetchUrl(href)
|
||||
if (!PREFETCHED_URLS[url]) return
|
||||
|
||||
delete PREFETCHED_URLS[url]
|
||||
await prefetch(href)
|
||||
export function prefetch (href) {
|
||||
warnImperativePrefetch()
|
||||
return Router.prefetch(href)
|
||||
}
|
||||
|
||||
export default class LinkPrefetch extends React.Component {
|
||||
render () {
|
||||
const { href } = this.props
|
||||
if (this.props.prefetch !== false) {
|
||||
prefetch(href)
|
||||
wantLinkPrefetch()
|
||||
const props = {
|
||||
...this.props,
|
||||
prefetch: this.props.prefetch === false ? this.props.prefetch : true
|
||||
}
|
||||
|
||||
return (<Link {...this.props} />)
|
||||
return (<Link {...props} />)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ const SingletonRouter = {
|
|||
|
||||
// Create public properties and methods of the router in the SingletonRouter
|
||||
const propertyFields = ['components', 'pathname', 'route', 'query']
|
||||
const coreMethodFields = ['push', 'replace', 'reload', 'back']
|
||||
const coreMethodFields = ['push', 'replace', 'reload', 'back', 'prefetch']
|
||||
const routerEvents = ['routeChangeStart', 'routeChangeComplete', 'routeChangeError']
|
||||
|
||||
propertyFields.forEach((field) => {
|
||||
|
|
|
@ -1,11 +1,15 @@
|
|||
/* global __NEXT_DATA__ */
|
||||
/* global __NEXT_DATA__, fetch */
|
||||
|
||||
import { parse, format } from 'url'
|
||||
import evalScript from '../eval-script'
|
||||
import shallowEquals from '../shallow-equals'
|
||||
import { EventEmitter } from 'events'
|
||||
import { reloadIfPrefetched } from '../prefetch'
|
||||
import { loadGetInitialProps, getLocationOrigin } from '../utils'
|
||||
import { loadGetInitialProps, LockManager, getLocationOrigin } from '../utils'
|
||||
|
||||
// Add "fetch" polyfill for older browsers
|
||||
if (typeof window !== 'undefined') {
|
||||
require('whatwg-fetch')
|
||||
}
|
||||
|
||||
export default class Router extends EventEmitter {
|
||||
constructor (pathname, query, { Component, ErrorComponent, err } = {}) {
|
||||
|
@ -15,6 +19,10 @@ export default class Router extends EventEmitter {
|
|||
|
||||
// set up the component cache (by route keys)
|
||||
this.components = { [this.route]: { Component, err } }
|
||||
// contain a map of response of prefetched routes
|
||||
this.prefetchedRoutes = {}
|
||||
this.prefetchingLockManager = new LockManager(2)
|
||||
this.prefetchingRoutes = {}
|
||||
|
||||
this.ErrorComponent = ErrorComponent
|
||||
this.pathname = pathname
|
||||
|
@ -96,7 +104,8 @@ export default class Router extends EventEmitter {
|
|||
|
||||
async reload (route) {
|
||||
delete this.components[route]
|
||||
await reloadIfPrefetched(route)
|
||||
delete this.prefetchedRoutes[route]
|
||||
this.prefetchingRoutes[route] = 'IGNORE'
|
||||
|
||||
if (route !== this.route) return
|
||||
|
||||
|
@ -184,8 +193,8 @@ export default class Router extends EventEmitter {
|
|||
const routeInfo = {}
|
||||
|
||||
try {
|
||||
const { Component, err, xhr } = routeInfo.data = await this.fetchComponent(route)
|
||||
const ctx = { err, xhr, pathname, query }
|
||||
const { Component, err, jsonPageRes } = routeInfo.data = await this.fetchComponent(route)
|
||||
const ctx = { err, pathname, query, jsonPageRes }
|
||||
routeInfo.props = await this.getInitialProps(Component, ctx)
|
||||
} catch (err) {
|
||||
if (err.cancelled) {
|
||||
|
@ -214,35 +223,74 @@ export default class Router extends EventEmitter {
|
|||
return this.pathname !== pathname || !shallowEquals(query, this.query)
|
||||
}
|
||||
|
||||
async prefetch (url) {
|
||||
const { pathname } = parse(url)
|
||||
const route = toRoute(pathname)
|
||||
|
||||
const done = await this.prefetchingLockManager.get()
|
||||
// It's possible for some other "prefetch" process
|
||||
// to start prefetching the same route
|
||||
// So, we should not fetch it again
|
||||
if (this.prefetchingRoutes[route]) return done()
|
||||
|
||||
// It's possible that, this is already prefetched.
|
||||
// So, we should not fetch it again
|
||||
if (this.prefetchedRoutes[route]) return done()
|
||||
|
||||
// Mark as we are prefetching the route
|
||||
this.prefetchingRoutes[route] = true
|
||||
|
||||
const complete = () => {
|
||||
delete this.prefetchingRoutes[route]
|
||||
done()
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await this.fetchUrl(route)
|
||||
// Router.relaod() process may ask us to ignore the current prefetching route
|
||||
// In that case, we need to discard it
|
||||
if (this.prefetchingRoutes[route] !== 'IGNORE') {
|
||||
this.prefetchedRoutes[route] = res
|
||||
}
|
||||
complete()
|
||||
} catch (ex) {
|
||||
complete()
|
||||
throw ex
|
||||
}
|
||||
}
|
||||
|
||||
async fetchComponent (route) {
|
||||
let data = this.components[route]
|
||||
if (!data) {
|
||||
let cancel
|
||||
if (data) return data
|
||||
|
||||
data = await new Promise((resolve, reject) => {
|
||||
this.componentLoadCancel = cancel = () => {
|
||||
if (xhr.abort) {
|
||||
xhr.abort()
|
||||
const error = new Error('Fetching componenet cancelled')
|
||||
error.cancelled = true
|
||||
reject(error)
|
||||
}
|
||||
}
|
||||
|
||||
const url = `/_next/${__NEXT_DATA__.buildId}/pages${route}`
|
||||
const xhr = loadComponent(url, (err, data) => {
|
||||
if (err) return reject(err)
|
||||
resolve({ ...data, xhr })
|
||||
})
|
||||
})
|
||||
|
||||
if (cancel === this.componentLoadCancel) {
|
||||
this.componentLoadCancel = null
|
||||
}
|
||||
|
||||
this.components[route] = data
|
||||
let cancelled = false
|
||||
const cancel = this.componentLoadCancel = function () {
|
||||
cancelled = true
|
||||
}
|
||||
return data
|
||||
|
||||
let jsonPageRes = this.prefetchedRoutes[route]
|
||||
if (!jsonPageRes) {
|
||||
jsonPageRes = await this.fetchUrl(route)
|
||||
}
|
||||
|
||||
const jsonData = await jsonPageRes.json()
|
||||
const newData = {
|
||||
...loadComponent(jsonData),
|
||||
jsonPageRes
|
||||
}
|
||||
|
||||
if (cancelled) {
|
||||
const error = new Error(`Abort fetching component for route: "${route}"`)
|
||||
error.cancelled = true
|
||||
throw error
|
||||
}
|
||||
|
||||
if (cancel === this.componentLoadCancel) {
|
||||
this.componentLoadCancel = null
|
||||
}
|
||||
|
||||
this.components[route] = newData
|
||||
return newData
|
||||
}
|
||||
|
||||
async getInitialProps (Component, ctx) {
|
||||
|
@ -265,6 +313,16 @@ export default class Router extends EventEmitter {
|
|||
return props
|
||||
}
|
||||
|
||||
async fetchUrl (route) {
|
||||
const url = `/_next/${__NEXT_DATA__.buildId}/pages${route}`
|
||||
const res = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: { 'Accept': 'application/json' }
|
||||
})
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
abortComponentLoad () {
|
||||
if (this.componentLoadCancel) {
|
||||
this.componentLoadCancel()
|
||||
|
@ -292,47 +350,9 @@ function toRoute (path) {
|
|||
return path.replace(/\/$/, '') || '/'
|
||||
}
|
||||
|
||||
function loadComponent (url, fn) {
|
||||
return loadJSON(url, (err, data) => {
|
||||
if (err) return fn(err)
|
||||
function loadComponent (jsonData) {
|
||||
const module = evalScript(jsonData.component)
|
||||
const Component = module.default || module
|
||||
|
||||
let module
|
||||
try {
|
||||
module = evalScript(data.component)
|
||||
} catch (err) {
|
||||
return fn(err)
|
||||
}
|
||||
|
||||
const Component = module.default || module
|
||||
fn(null, { Component, err: data.err })
|
||||
})
|
||||
}
|
||||
|
||||
function loadJSON (url, fn) {
|
||||
const xhr = new window.XMLHttpRequest()
|
||||
xhr.onload = () => {
|
||||
let data
|
||||
|
||||
try {
|
||||
data = JSON.parse(xhr.responseText)
|
||||
} catch (err) {
|
||||
fn(new Error('Failed to load JSON for ' + url))
|
||||
return
|
||||
}
|
||||
|
||||
fn(null, data)
|
||||
}
|
||||
xhr.onerror = () => {
|
||||
fn(new Error('XHR failed. Status: ' + xhr.status))
|
||||
}
|
||||
xhr.onabort = () => {
|
||||
const err = new Error('XHR aborted')
|
||||
err.cancelled = true
|
||||
fn(err)
|
||||
}
|
||||
xhr.open('GET', url)
|
||||
xhr.setRequestHeader('Accept', 'application/json')
|
||||
xhr.send()
|
||||
|
||||
return xhr
|
||||
return { Component, err: jsonData.err }
|
||||
}
|
||||
|
|
28
lib/utils.js
28
lib/utils.js
|
@ -54,6 +54,34 @@ export async function loadGetInitialProps (Component, ctx) {
|
|||
return props
|
||||
}
|
||||
|
||||
export class LockManager {
|
||||
constructor (maxLocks) {
|
||||
this.maxLocks = maxLocks
|
||||
this.clients = []
|
||||
this.runningLocks = 0
|
||||
}
|
||||
|
||||
get () {
|
||||
return new Promise((resolve) => {
|
||||
this.clients.push(resolve)
|
||||
this._giveLocksIfPossible()
|
||||
})
|
||||
}
|
||||
|
||||
_giveLocksIfPossible () {
|
||||
if (this.runningLocks < this.maxLocks) {
|
||||
const client = this.clients.shift()
|
||||
if (!client) return
|
||||
|
||||
this.runningLocks ++
|
||||
client(() => {
|
||||
this.runningLocks --
|
||||
this._giveLocksIfPossible()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getLocationOrigin () {
|
||||
const { protocol, hostname, port } = window.location
|
||||
return `${protocol}//${hostname}${port ? ':' + port : ''}`
|
||||
|
|
|
@ -85,6 +85,7 @@
|
|||
"webpack": "2.2.1",
|
||||
"webpack-dev-middleware": "1.10.0",
|
||||
"webpack-hot-middleware": "2.16.1",
|
||||
"whatwg-fetch": "^2.0.2",
|
||||
"write-file-webpack-plugin": "3.4.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
87
test/unit/router.test.js
Normal file
87
test/unit/router.test.js
Normal file
|
@ -0,0 +1,87 @@
|
|||
/* global describe, it, expect */
|
||||
import Router from '../../dist/lib/router/router'
|
||||
|
||||
describe('Router', () => {
|
||||
describe('.prefetch()', () => {
|
||||
it('should prefetch a given page', async () => {
|
||||
const router = new Router('/', {})
|
||||
const res = { aa: 'res' }
|
||||
const route = 'routex'
|
||||
router.fetchUrl = (r) => {
|
||||
expect(r).toBe(route)
|
||||
return Promise.resolve(res)
|
||||
}
|
||||
await router.prefetch(route)
|
||||
|
||||
expect(router.prefetchedRoutes[route]).toBe(res)
|
||||
})
|
||||
|
||||
it('should stop if it\'s prefetched already', async () => {
|
||||
const router = new Router('/', {})
|
||||
const route = 'routex'
|
||||
router.prefetchedRoutes[route] = { aa: 'res' }
|
||||
router.fetchUrl = () => { throw new Error('Should not happen') }
|
||||
await router.prefetch(route)
|
||||
})
|
||||
|
||||
it('should stop if it\'s currently prefetching', async () => {
|
||||
const router = new Router('/', {})
|
||||
const route = 'routex'
|
||||
router.prefetchingRoutes[route] = true
|
||||
router.fetchUrl = () => { throw new Error('Should not happen') }
|
||||
await router.prefetch(route)
|
||||
})
|
||||
|
||||
it('should ignore the response if it asked to do', async () => {
|
||||
const router = new Router('/', {})
|
||||
const res = { aa: 'res' }
|
||||
const route = 'routex'
|
||||
|
||||
let called = false
|
||||
router.fetchUrl = () => {
|
||||
called = true
|
||||
router.prefetchingRoutes[route] = 'IGNORE'
|
||||
return Promise.resolve(res)
|
||||
}
|
||||
await router.prefetch(route)
|
||||
|
||||
expect(router.prefetchedRoutes[route]).toBeUndefined()
|
||||
expect(called).toBe(true)
|
||||
})
|
||||
|
||||
it('should only run two jobs at a time', async () => {
|
||||
const router = new Router('/', {})
|
||||
let count = 0
|
||||
|
||||
router.fetchUrl = () => {
|
||||
count++
|
||||
return new Promise((resolve) => {})
|
||||
}
|
||||
|
||||
router.prefetch('route1')
|
||||
router.prefetch('route2')
|
||||
router.prefetch('route3')
|
||||
router.prefetch('route4')
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 50))
|
||||
|
||||
expect(count).toBe(2)
|
||||
expect(Object.keys(router.prefetchingRoutes)).toEqual(['route1', 'route2'])
|
||||
})
|
||||
|
||||
it('should run all the jobs', async () => {
|
||||
const router = new Router('/', {})
|
||||
const routes = ['route1', 'route2', 'route3', 'route4']
|
||||
|
||||
router.fetchUrl = () => Promise.resolve({ aa: 'res' })
|
||||
|
||||
await router.prefetch(routes[0])
|
||||
await router.prefetch(routes[1])
|
||||
await router.prefetch(routes[2])
|
||||
await router.prefetch(routes[3])
|
||||
|
||||
expect(Object.keys(router.prefetchedRoutes)).toEqual(routes)
|
||||
expect(Object.keys(router.prefetchingRoutes)).toEqual([])
|
||||
})
|
||||
})
|
||||
})
|
|
@ -1,35 +0,0 @@
|
|||
const resolve = require('path').resolve
|
||||
const webpack = require('webpack')
|
||||
|
||||
module.exports = {
|
||||
entry: './client/next-prefetcher.js',
|
||||
output: {
|
||||
filename: 'next-prefetcher-bundle.js',
|
||||
path: resolve(__dirname, 'dist/client')
|
||||
},
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
'process.env': {
|
||||
NODE_ENV: JSON.stringify('production')
|
||||
}
|
||||
})
|
||||
],
|
||||
module: {
|
||||
rules: [{
|
||||
test: /\.js$/,
|
||||
exclude: /node_modules/,
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
babelrc: false,
|
||||
presets: [
|
||||
['env', {
|
||||
targets: {
|
||||
// All browsers which supports service workers
|
||||
browsers: ['chrome 49', 'firefox 49', 'opera 41']
|
||||
}
|
||||
}]
|
||||
]
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
|
@ -4835,7 +4835,7 @@ whatwg-encoding@^1.0.1:
|
|||
dependencies:
|
||||
iconv-lite "0.4.13"
|
||||
|
||||
whatwg-fetch@>=0.10.0:
|
||||
whatwg-fetch@>=0.10.0, whatwg-fetch@^2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.2.tgz#fe294d1d89e36c5be8b3195057f2e4bc74fc980e"
|
||||
|
||||
|
|
Loading…
Reference in a new issue