1
0
Fork 0
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:
Tim Neutkens 2018-11-25 00:47:39 +01:00 committed by GitHub
parent 401594ed36
commit cad19c808c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 98 additions and 98 deletions

View file

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

View file

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

View file

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

View file

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

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

View file

@ -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.

View file

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