mirror of
https://github.com/terribleplan/next.js.git
synced 2024-01-19 02:48:18 +00:00
Use <link rel=“prefetch”> for prefetching (#5737)
* Use <link rel=“prefetch”> for prefetching Fixes #5734 * Fix unit tests for router * Add test for prefetch * Rename test * Check all logs for message
This commit is contained in:
parent
401594ed36
commit
cad19c808c
|
@ -1,83 +0,0 @@
|
|||
// based on https://github.com/sindresorhus/p-queue (MIT)
|
||||
// modified for browser support
|
||||
|
||||
class Queue {
|
||||
constructor () {
|
||||
this._queue = []
|
||||
}
|
||||
enqueue (run) {
|
||||
this._queue.push(run)
|
||||
}
|
||||
dequeue () {
|
||||
return this._queue.shift()
|
||||
}
|
||||
get size () {
|
||||
return this._queue.length
|
||||
}
|
||||
}
|
||||
|
||||
export default class PQueue {
|
||||
constructor (opts) {
|
||||
opts = Object.assign({
|
||||
concurrency: Infinity,
|
||||
queueClass: Queue
|
||||
}, opts)
|
||||
|
||||
if (opts.concurrency < 1) {
|
||||
throw new TypeError('Expected `concurrency` to be a number from 1 and up')
|
||||
}
|
||||
|
||||
this.queue = new opts.queueClass() // eslint-disable-line new-cap
|
||||
this._pendingCount = 0
|
||||
this._concurrency = opts.concurrency
|
||||
this._resolveEmpty = () => {}
|
||||
}
|
||||
_next () {
|
||||
this._pendingCount--
|
||||
|
||||
if (this.queue.size > 0) {
|
||||
this.queue.dequeue()()
|
||||
} else {
|
||||
this._resolveEmpty()
|
||||
}
|
||||
}
|
||||
add (fn, opts) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const run = () => {
|
||||
this._pendingCount++
|
||||
|
||||
fn().then(
|
||||
val => {
|
||||
resolve(val)
|
||||
this._next()
|
||||
},
|
||||
err => {
|
||||
reject(err)
|
||||
this._next()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (this._pendingCount < this._concurrency) {
|
||||
run()
|
||||
} else {
|
||||
this.queue.enqueue(run, opts)
|
||||
}
|
||||
})
|
||||
}
|
||||
onEmpty () {
|
||||
return new Promise(resolve => {
|
||||
const existingResolve = this._resolveEmpty
|
||||
this._resolveEmpty = () => {
|
||||
existingResolve()
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
}
|
||||
get size () {
|
||||
return this.queue.size
|
||||
}
|
||||
get pending () {
|
||||
return this._pendingCount
|
||||
}
|
||||
}
|
|
@ -3,7 +3,6 @@
|
|||
import { parse, format } from 'url'
|
||||
import EventEmitter from '../EventEmitter'
|
||||
import shallowEquals from '../shallow-equals'
|
||||
import PQueue from '../p-queue'
|
||||
import { loadGetInitialProps, getURL } from '../utils'
|
||||
import { _rewriteUrlForNextExport } from './'
|
||||
|
||||
|
@ -30,7 +29,6 @@ export default class Router {
|
|||
this.events = Router.events
|
||||
|
||||
this.pageLoader = pageLoader
|
||||
this.prefetchQueue = new PQueue({ concurrency: 2 })
|
||||
this.ErrorComponent = ErrorComponent
|
||||
this.pathname = pathname
|
||||
this.query = query
|
||||
|
@ -359,7 +357,7 @@ export default class Router {
|
|||
|
||||
const { pathname } = parse(url)
|
||||
const route = toRoute(pathname)
|
||||
return this.prefetchQueue.add(() => this.fetchRoute(route))
|
||||
return this.pageLoader.prefetch(route)
|
||||
}
|
||||
|
||||
async fetchComponent (route, as) {
|
||||
|
|
|
@ -1,6 +1,19 @@
|
|||
/* global window, document */
|
||||
/* global document */
|
||||
import EventEmitter from 'next-server/dist/lib/EventEmitter'
|
||||
|
||||
// smaller version of https://gist.github.com/igrigorik/a02f2359f3bc50ca7a9c
|
||||
function listSupports (list, token) {
|
||||
if (!list || !list.supports) {
|
||||
return false
|
||||
}
|
||||
try {
|
||||
return list.supports(token)
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const supportsPrefetch = listSupports(document.createElement('link').relList, 'prefetch')
|
||||
const webpackModule = module
|
||||
|
||||
export default class PageLoader {
|
||||
|
@ -9,7 +22,7 @@ export default class PageLoader {
|
|||
this.assetPrefix = assetPrefix
|
||||
|
||||
this.pageCache = {}
|
||||
this.pageLoadedHandlers = {}
|
||||
this.prefetchCache = new Set()
|
||||
this.pageRegisterEvents = new EventEmitter()
|
||||
this.loadingRoutes = {}
|
||||
}
|
||||
|
@ -110,10 +123,30 @@ export default class PageLoader {
|
|||
}
|
||||
}
|
||||
|
||||
async prefetch (route) {
|
||||
route = this.normalizeRoute(route)
|
||||
const scriptRoute = route === '/' ? '/index.js' : `${route}.js`
|
||||
if (this.prefetchCache.has(scriptRoute)) {
|
||||
return
|
||||
}
|
||||
this.prefetchCache.add(scriptRoute)
|
||||
|
||||
const link = document.createElement('link')
|
||||
// Feature detection is used to see if prefetch is supported, else fall back to preload
|
||||
// Mainly this is for Safari
|
||||
// https://caniuse.com/#feat=link-rel-prefetch
|
||||
// https://caniuse.com/#feat=link-rel-preload
|
||||
link.rel = supportsPrefetch ? 'prefetch' : 'preload'
|
||||
link.href = `${this.assetPrefix}/_next/static/${encodeURIComponent(this.buildId)}/pages${scriptRoute}`
|
||||
link.as = 'script'
|
||||
document.head.appendChild(link)
|
||||
}
|
||||
|
||||
clearCache (route) {
|
||||
route = this.normalizeRoute(route)
|
||||
delete this.pageCache[route]
|
||||
delete this.loadingRoutes[route]
|
||||
delete this.loadingRoutes[route]
|
||||
|
||||
const script = document.getElementById(`__NEXT_PAGE__${route}`)
|
||||
if (script) {
|
||||
|
|
|
@ -50,7 +50,7 @@
|
|||
"autodll-webpack-plugin": "0.4.2",
|
||||
"babel-core": "7.0.0-bridge.0",
|
||||
"babel-loader": "8.0.2",
|
||||
"babel-plugin-react-require": "3.0.1",
|
||||
"babel-plugin-react-require": "3.0.0",
|
||||
"babel-plugin-transform-react-remove-prop-types": "0.4.15",
|
||||
"case-sensitive-paths-webpack-plugin": "2.1.2",
|
||||
"cross-spawn": "5.1.0",
|
||||
|
|
28
test/integration/production/pages/prefetch.js
Normal file
28
test/integration/production/pages/prefetch.js
Normal file
|
@ -0,0 +1,28 @@
|
|||
import Link from 'next/link'
|
||||
|
||||
export default () => {
|
||||
return <div>
|
||||
<ul>
|
||||
<li>
|
||||
<Link href='/' prefetch>
|
||||
<a>index</a>
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href='/process-env' prefetch>
|
||||
<a>process env</a>
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href='/counter' prefetch>
|
||||
<a>counter</a>
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href='/about' prefetch>
|
||||
<a>about</a>
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
}
|
|
@ -210,6 +210,21 @@ describe('Production Usage', () => {
|
|||
browser.close()
|
||||
})
|
||||
|
||||
it('should add prefetch tags when Link prefetch prop is used', async () => {
|
||||
const browser = await webdriver(appPort, '/prefetch')
|
||||
const elements = await browser.elementsByCss('link[rel=prefetch]')
|
||||
expect(elements.length).toBe(4)
|
||||
await Promise.all(
|
||||
elements.map(async (element) => {
|
||||
const rel = await element.getAttribute('rel')
|
||||
const as = await element.getAttribute('as')
|
||||
expect(rel).toBe('prefetch')
|
||||
expect(as).toBe('script')
|
||||
})
|
||||
)
|
||||
browser.close()
|
||||
})
|
||||
|
||||
it('should reload the page on page script error with prefetch', async () => {
|
||||
const browser = await webdriver(appPort, '/counter')
|
||||
const counter = await browser
|
||||
|
@ -220,7 +235,14 @@ describe('Production Usage', () => {
|
|||
// Let the browser to prefetch the page and error it on the console.
|
||||
await waitFor(3000)
|
||||
const browserLogs = await browser.log('browser')
|
||||
expect(browserLogs[0].message).toMatch(/\/no-such-page.js - Failed to load resource/)
|
||||
let foundLog = false
|
||||
browserLogs.forEach((log) => {
|
||||
if (log.message.match(/\/no-such-page\.js - Failed to load resource/)) {
|
||||
foundLog = true
|
||||
}
|
||||
})
|
||||
|
||||
expect(foundLog).toBe(true)
|
||||
|
||||
// When we go to the 404 page, it'll do a hard reload.
|
||||
// So, it's possible for the front proxy to load a page from another zone.
|
||||
|
|
|
@ -5,6 +5,7 @@ class PageLoader {
|
|||
constructor (options = {}) {
|
||||
this.options = options
|
||||
this.loaded = {}
|
||||
this.prefetched = {}
|
||||
}
|
||||
|
||||
loadPage (route) {
|
||||
|
@ -14,6 +15,10 @@ class PageLoader {
|
|||
return new Promise((resolve) => setTimeout(resolve, this.options.delay))
|
||||
}
|
||||
}
|
||||
|
||||
prefetch (route) {
|
||||
this.prefetched[route] = true
|
||||
}
|
||||
}
|
||||
|
||||
describe('Router', () => {
|
||||
|
@ -26,10 +31,10 @@ describe('Router', () => {
|
|||
const route = '/routex'
|
||||
await router.prefetch(route)
|
||||
|
||||
expect(pageLoader.loaded['/routex']).toBeTruthy()
|
||||
expect(pageLoader.prefetched['/routex']).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should only run two jobs at a time', async () => {
|
||||
it('should call prefetch correctly', async () => {
|
||||
global.__NEXT_DATA__ = {}
|
||||
// delay loading pages for an hour
|
||||
const pageLoader = new PageLoader({ delay: 1000 * 3600 })
|
||||
|
@ -40,11 +45,8 @@ describe('Router', () => {
|
|||
router.prefetch('route3')
|
||||
router.prefetch('route4')
|
||||
|
||||
// Wait for a bit
|
||||
await new Promise((resolve) => setTimeout(resolve, 50))
|
||||
|
||||
expect(Object.keys(pageLoader.loaded).length).toBe(2)
|
||||
expect(Object.keys(pageLoader.loaded)).toEqual(['route1', 'route2'])
|
||||
expect(Object.keys(pageLoader.prefetched).length).toBe(4)
|
||||
expect(Object.keys(pageLoader.prefetched)).toEqual(['route1', 'route2', 'route3', 'route4'])
|
||||
})
|
||||
|
||||
it('should run all the jobs', async () => {
|
||||
|
@ -60,7 +62,7 @@ describe('Router', () => {
|
|||
await router.prefetch(routes[2])
|
||||
await router.prefetch(routes[3])
|
||||
|
||||
expect(Object.keys(pageLoader.loaded)).toEqual(routes)
|
||||
expect(Object.keys(pageLoader.prefetched)).toEqual(routes)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
Loading…
Reference in a new issue