1
0
Fork 0
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:
Arunoda Susiripala 2017-02-15 14:22:22 +05:30 committed by GitHub
parent d382e4db9a
commit 14c86bef1d
13 changed files with 264 additions and 392 deletions

View file

@ -344,40 +344,37 @@ Router.onRouteChangeError = (err, url) => {
<ul><li><a href="./examples/with-prefetching">Prefetching</a></li></ul> <ul><li><a href="./examples/with-prefetching">Prefetching</a></li></ul>
</details></p> </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). 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>` #### 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 ```jsx
import Link from 'next/prefetch' import Link from 'next/link'
// example header component // example header component
export default () => ( export default () => (
<nav> <nav>
<ul> <ul>
<li><Link href='/'><a>Home</a></Link></li> <li><Link prefetch ref='/'><a>Home</a></Link></li>
<li><Link href='/about'><a>About</a></Link></li> <li><Link prefetch href='/about'><a>About</a></Link></li>
<li><Link href='/contact'><a>Contact</a></Link></li> <li><Link prefetch href='/contact'><a>Contact</a></Link></li>
</ul> </ul>
</nav> </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 #### 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 ```jsx
import { prefetch } from 'next/prefetch' import Router from 'next/router'
export default ({ url }) => ( export default ({ url }) => (
<div> <div>
<a onClick={ () => setTimeout(() => url.pushTo('/dynamic'), 100) }> <a onClick={ () => setTimeout(() => url.pushTo('/dynamic'), 100) }>
@ -385,7 +382,7 @@ export default ({ url }) => (
</a> </a>
{ {
// but we can prefetch it! // but we can prefetch it!
prefetch('/dynamic') Router.prefetch('/dynamic')
} }
</div> </div>
) )

View file

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

View file

@ -1,23 +1,23 @@
import Link, { prefetch } from 'next/prefetch' import Router from 'next/router'
import RegularLink from 'next/link' import Link from 'next/link'
export default () => ( export default () => (
<div> <div>
{ /* Prefetch using the declarative API */ } { /* Prefetch using the declarative API */ }
<Link href='/'> <Link prefetch href='/'>
<a>Home</a> <a>Home</a>
</Link> </Link>
<Link href='/features'> <Link prefetch href='/features'>
<a>Features</a> <a>Features</a>
</Link> </Link>
{ /* we imperatively prefetch on hover */ } { /* we imperatively prefetch on hover */ }
<RegularLink href='/about'> <Link href='/about'>
<a onMouseEnter={() => { prefetch('/about'); console.log('prefetching /about!') }}>About</a> <a onMouseEnter={() => { Router.prefetch('/about'); console.log('prefetching /about!') }}>About</a>
</RegularLink> </Link>
<Link href='/contact' prefetch={false}> <Link href='/contact'>
<a>Contact (<small>NO-PREFETCHING</small>)</a> <a>Contact (<small>NO-PREFETCHING</small>)</a>
</Link> </Link>

View file

@ -1,7 +1,6 @@
const webpack = require('webpack') const webpack = require('webpack')
const notifier = require('node-notifier') const notifier = require('node-notifier')
const childProcess = require('child_process') const childProcess = require('child_process')
const webpackConfig = require('./webpack.config')
const isWindows = /^win/.test(process.platform) const isWindows = /^win/.test(process.platform)
export async function compile(fly) { export async function compile(fly) {
@ -42,15 +41,7 @@ export async function copy(fly) {
} }
export async function build(fly) { export async function build(fly) {
await fly.serial(['copy', 'compile', 'prefetcher']) await fly.serial(['copy', 'compile'])
}
const compiler = webpack(webpackConfig)
export async function prefetcher(fly) {
compiler.run((err, stats) => {
if (err) throw err
notify('Built release prefetcher')
})
} }
export async function bench(fly) { export async function bench(fly) {
@ -70,8 +61,8 @@ export default async function (fly) {
await fly.watch('bin/*', 'bin') await fly.watch('bin/*', 'bin')
await fly.watch('pages/**/*.js', 'copy') await fly.watch('pages/**/*.js', 'copy')
await fly.watch('server/**/*.js', 'server') await fly.watch('server/**/*.js', 'server')
await fly.watch('client/**/*.js', ['client', 'prefetcher']) await fly.watch('client/**/*.js', ['client'])
await fly.watch('lib/**/*.js', ['lib', 'prefetcher']) await fly.watch('lib/**/*.js', ['lib'])
} }
export async function release(fly) { export async function release(fly) {

View file

@ -62,7 +62,7 @@ export default class Link extends Component {
} }
render () { 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 // 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') { if (typeof children === 'string') {
children = <a>{children}</a> children = <a>{children}</a>
@ -79,11 +79,18 @@ export default class Link extends Component {
props.href = this.props.as || this.props.href 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) return React.cloneElement(child, props)
} }
} }
export function isLocal (href) { function isLocal (href) {
const origin = getLocationOrigin() const origin = getLocationOrigin()
return !/^(https?:)?\/\//.test(href) || return !/^(https?:)?\/\//.test(href) ||
origin === href.substr(0, origin.length) origin === href.substr(0, origin.length)

View file

@ -1,150 +1,33 @@
/* global __NEXT_DATA__ */
import React from 'react' import React from 'react'
import Link, { isLocal } from './link' import Link from './link'
import { parse as urlParse } from 'url' import Router from './router'
import { warn, execOnce } from './utils'
class Messenger { const warnImperativePrefetch = execOnce(() => {
constructor () { const message = '> You are using deprecated "next/prefetch". It will be removed with Next.js 2.0.\n' +
this.id = 0 '> Use "Router.prefetch(href)" instead.'
this.callacks = {} warn(message)
this.serviceWorkerReadyCallbacks = []
this.serviceWorkerState = null
navigator.serviceWorker.addEventListener('message', ({ data }) => {
if (data.action !== 'REPLY') return
if (this.callacks[data.replyFor]) {
this.callacks[data.replyFor](data)
}
}) })
// Reset the cache always. const wantLinkPrefetch = execOnce(() => {
// Sometimes, there's an already running service worker with cached requests. const message = '> You are using deprecated "next/prefetch". It will be removed with Next.js 2.0.\n' +
// If the app doesn't use any prefetch calls, `ensureInitialized` won't get '> Use "<Link prefetch />" instead.'
// called and cleanup resources. warn(message)
// 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) { export function prefetch (href) {
if (err) return reject(err) warnImperativePrefetch()
return resolve() return Router.prefetch(href)
}
})
}
_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 default class LinkPrefetch extends React.Component { export default class LinkPrefetch extends React.Component {
render () { render () {
const { href } = this.props wantLinkPrefetch()
if (this.props.prefetch !== false) { const props = {
prefetch(href) ...this.props,
prefetch: this.props.prefetch === false ? this.props.prefetch : true
} }
return (<Link {...this.props} />) return (<Link {...props} />)
} }
} }

View file

@ -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']
const coreMethodFields = ['push', 'replace', 'reload', 'back'] const coreMethodFields = ['push', 'replace', 'reload', 'back', 'prefetch']
const routerEvents = ['routeChangeStart', 'routeChangeComplete', 'routeChangeError'] const routerEvents = ['routeChangeStart', 'routeChangeComplete', 'routeChangeError']
propertyFields.forEach((field) => { propertyFields.forEach((field) => {

View file

@ -1,11 +1,15 @@
/* global __NEXT_DATA__ */ /* global __NEXT_DATA__, fetch */
import { parse, format } from 'url' import { parse, format } from 'url'
import evalScript from '../eval-script' import evalScript from '../eval-script'
import shallowEquals from '../shallow-equals' import shallowEquals from '../shallow-equals'
import { EventEmitter } from 'events' import { EventEmitter } from 'events'
import { reloadIfPrefetched } from '../prefetch' import { loadGetInitialProps, LockManager, getLocationOrigin } from '../utils'
import { loadGetInitialProps, getLocationOrigin } from '../utils'
// Add "fetch" polyfill for older browsers
if (typeof window !== 'undefined') {
require('whatwg-fetch')
}
export default class Router extends EventEmitter { export default class Router extends EventEmitter {
constructor (pathname, query, { Component, ErrorComponent, err } = {}) { constructor (pathname, query, { Component, ErrorComponent, err } = {}) {
@ -15,6 +19,10 @@ export default class Router extends EventEmitter {
// set up the component cache (by route keys) // set up the component cache (by route keys)
this.components = { [this.route]: { Component, err } } 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.ErrorComponent = ErrorComponent
this.pathname = pathname this.pathname = pathname
@ -96,7 +104,8 @@ export default class Router extends EventEmitter {
async reload (route) { async reload (route) {
delete this.components[route] delete this.components[route]
await reloadIfPrefetched(route) delete this.prefetchedRoutes[route]
this.prefetchingRoutes[route] = 'IGNORE'
if (route !== this.route) return if (route !== this.route) return
@ -184,8 +193,8 @@ export default class Router extends EventEmitter {
const routeInfo = {} const routeInfo = {}
try { try {
const { Component, err, xhr } = routeInfo.data = await this.fetchComponent(route) const { Component, err, jsonPageRes } = routeInfo.data = await this.fetchComponent(route)
const ctx = { err, xhr, pathname, query } const ctx = { err, pathname, query, jsonPageRes }
routeInfo.props = await this.getInitialProps(Component, ctx) routeInfo.props = await this.getInitialProps(Component, ctx)
} catch (err) { } catch (err) {
if (err.cancelled) { if (err.cancelled) {
@ -214,35 +223,74 @@ export default class Router extends EventEmitter {
return this.pathname !== pathname || !shallowEquals(query, this.query) 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) { async fetchComponent (route) {
let data = this.components[route] let data = this.components[route]
if (!data) { if (data) return data
let cancel
data = await new Promise((resolve, reject) => { let cancelled = false
this.componentLoadCancel = cancel = () => { const cancel = this.componentLoadCancel = function () {
if (xhr.abort) { cancelled = true
xhr.abort() }
const error = new Error('Fetching componenet cancelled')
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 error.cancelled = true
reject(error) throw 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) { if (cancel === this.componentLoadCancel) {
this.componentLoadCancel = null this.componentLoadCancel = null
} }
this.components[route] = data this.components[route] = newData
} return newData
return data
} }
async getInitialProps (Component, ctx) { async getInitialProps (Component, ctx) {
@ -265,6 +313,16 @@ export default class Router extends EventEmitter {
return props 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 () { abortComponentLoad () {
if (this.componentLoadCancel) { if (this.componentLoadCancel) {
this.componentLoadCancel() this.componentLoadCancel()
@ -292,47 +350,9 @@ function toRoute (path) {
return path.replace(/\/$/, '') || '/' return path.replace(/\/$/, '') || '/'
} }
function loadComponent (url, fn) { function loadComponent (jsonData) {
return loadJSON(url, (err, data) => { const module = evalScript(jsonData.component)
if (err) return fn(err)
let module
try {
module = evalScript(data.component)
} catch (err) {
return fn(err)
}
const Component = module.default || module const Component = module.default || module
fn(null, { Component, err: data.err })
})
}
function loadJSON (url, fn) { return { Component, err: jsonData.err }
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
} }

View file

@ -54,6 +54,34 @@ export async function loadGetInitialProps (Component, ctx) {
return props 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 () { export function getLocationOrigin () {
const { protocol, hostname, port } = window.location const { protocol, hostname, port } = window.location
return `${protocol}//${hostname}${port ? ':' + port : ''}` return `${protocol}//${hostname}${port ? ':' + port : ''}`

View file

@ -85,6 +85,7 @@
"webpack": "2.2.1", "webpack": "2.2.1",
"webpack-dev-middleware": "1.10.0", "webpack-dev-middleware": "1.10.0",
"webpack-hot-middleware": "2.16.1", "webpack-hot-middleware": "2.16.1",
"whatwg-fetch": "^2.0.2",
"write-file-webpack-plugin": "3.4.2" "write-file-webpack-plugin": "3.4.2"
}, },
"devDependencies": { "devDependencies": {

87
test/unit/router.test.js Normal file
View 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([])
})
})
})

View file

@ -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']
}
}]
]
}
}]
}
}

View file

@ -4835,7 +4835,7 @@ whatwg-encoding@^1.0.1:
dependencies: dependencies:
iconv-lite "0.4.13" iconv-lite "0.4.13"
whatwg-fetch@>=0.10.0: whatwg-fetch@>=0.10.0, whatwg-fetch@^2.0.2:
version "2.0.2" version "2.0.2"
resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.2.tgz#fe294d1d89e36c5be8b3195057f2e4bc74fc980e" resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.2.tgz#fe294d1d89e36c5be8b3195057f2e4bc74fc980e"