Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Speculate Navigations for Client-Side JS #10560

Merged
merged 3 commits into from
Feb 17, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 16 additions & 11 deletions packages/next/client/link.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
declare const __NEXT_DATA__: any

import { resolve, parse, UrlObject } from 'url'
import React, { Component, Children } from 'react'
import Router from './router'
import React, { Children, Component } from 'react'
import { parse, resolve, UrlObject } from 'url'
import { PrefetchOptions } from '../next-server/lib/router/router'
import {
execOnce,
formatWithValidation,
getLocationOrigin,
} from '../next-server/lib/utils'
import Router from './router'

function isLocal(href: string) {
const url = parse(href, false, true)
Expand Down Expand Up @@ -127,14 +128,18 @@ class Link extends Component<LinkProps> {
this.cleanUpListeners()
}

getHref() {
getPaths() {
const { pathname } = window.location
const { href: parsedHref } = this.formatUrls(this.props.href, this.props.as)
return resolve(pathname, parsedHref)
const { href: parsedHref, as: parsedAs } = this.formatUrls(
this.props.href,
this.props.as
)
const resolvedHref = resolve(pathname, parsedHref)
return [resolvedHref, parsedAs ? resolve(pathname, parsedAs) : resolvedHref]
}

handleRef(ref: Element) {
const isPrefetched = prefetched[this.getHref()]
const isPrefetched = prefetched[this.getPaths()[0]]
if (this.p && IntersectionObserver && ref && ref.tagName) {
this.cleanUpListeners()

Expand Down Expand Up @@ -201,11 +206,11 @@ class Link extends Component<LinkProps> {
})
}

prefetch() {
prefetch(options?: PrefetchOptions) {
if (!this.p || typeof window === 'undefined') return
// Prefetch the JSON page if asked (only in the client)
const href = this.getHref()
Router.prefetch(href)
const [href, asPath] = this.getPaths()
Router.prefetch(href, asPath, options)
prefetched[href] = true
}

Expand Down Expand Up @@ -239,7 +244,7 @@ class Link extends Component<LinkProps> {
if (child.props && typeof child.props.onMouseEnter === 'function') {
child.props.onMouseEnter(e)
}
this.prefetch()
this.prefetch({ priority: true })
},
onClick: (e: React.MouseEvent) => {
if (child.props && typeof child.props.onClick === 'function') {
Expand Down
21 changes: 15 additions & 6 deletions packages/next/next-server/lib/router/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import { ParsedUrlQuery } from 'querystring'
import { ComponentType } from 'react'
import { parse, UrlObject } from 'url'

import mitt, { MittEmitter } from '../mitt'
import {
AppContextType,
Expand Down Expand Up @@ -52,6 +51,10 @@ export type NextRouter = BaseRouter &
| 'isFallback'
>

export type PrefetchOptions = {
priority?: boolean
}

type RouteInfo = {
Component: ComponentType
props?: any
Expand Down Expand Up @@ -652,11 +655,16 @@ export default class Router implements BaseRouter {
}

/**
* Prefetch `page` code, you may wait for the data during `page` rendering.
* Prefetch page code, you may wait for the data during page rendering.
* This feature only works in production!
* @param url of prefetched `page`
* @param url the href of prefetched page
* @param asPath the as path of the prefetched page
*/
prefetch(url: string): Promise<void> {
prefetch(
url: string,
asPath: string = url,
options: PrefetchOptions = {}
): Promise<void> {
return new Promise((resolve, reject) => {
const { pathname, protocol } = parse(url)

Expand All @@ -674,8 +682,9 @@ export default class Router implements BaseRouter {
return
}

const route = toRoute(pathname)
this.pageLoader.prefetch(route).then(resolve, reject)
this.pageLoader[options.priority ? 'loadPage' : 'prefetch'](
ijjk marked this conversation as resolved.
Show resolved Hide resolved
toRoute(pathname)
).then(() => resolve(), reject)
})
}

Expand Down
2 changes: 1 addition & 1 deletion test/integration/preload-viewport/pages/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export default () => {
/>
<p id="scroll-to-me">Hi 👋</p>
<Link href="/another">
<a>to /another</a>
<a id="link-another">to /another</a>
</Link>
</div>
)
Expand Down
58 changes: 50 additions & 8 deletions test/integration/preload-viewport/test/index.test.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
/* eslint-env jest */
/* global jasmine */
import webdriver from 'next-webdriver'
import { join } from 'path'
import {
nextServer,
runNextCommand,
startApp,
stopApp,
waitFor,
} from 'next-test-utils'
import webdriver from 'next-webdriver'
import { join } from 'path'

jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 60 * 5

Expand Down Expand Up @@ -98,24 +98,66 @@ describe('Prefetching Links in viewport', () => {
}
})

it('should fallback to prefetching onMouseEnter with invalid ref', async () => {
it('should prefetch with link in viewport and inject script on hover', async () => {
let browser
try {
browser = await webdriver(appPort, '/invalid-ref')
await browser.elementByCss('#btn-link').moveTo()
browser = await webdriver(appPort, '/')
await browser.elementByCss('#scroll-to-another').click()
await waitFor(2 * 1000)

const links = await browser.elementsByCss('link[rel=prefetch]')
let found = false
let foundLink = false

for (const link of links) {
const href = await link.getAttribute('href')
if (href.includes('another')) {
found = true
foundLink = true
break
}
}
expect(found).toBe(true)
expect(foundLink).toBe(true)

await browser.elementByCss('#link-another').moveTo()
await waitFor(2 * 1000)

const scripts = await browser.elementsByCss(
// Mouse hover is a high-priority fetch
'script:not([async])'
)
let scriptFound = false
for (const aScript of scripts) {
const href = await aScript.getAttribute('src')
if (href.includes('another')) {
scriptFound = true
break
}
}
expect(scriptFound).toBe(true)
} finally {
if (browser) await browser.close()
}
})

it('should inject a <script> tag when onMouseEnter (even with invalid ref)', async () => {
let browser
try {
browser = await webdriver(appPort, '/invalid-ref')
await browser.elementByCss('#btn-link').moveTo()
await waitFor(2 * 1000)

const scripts = await browser.elementsByCss(
// Mouse hover is a high-priority fetch
'script:not([async])'
)
let scriptFound = false
for (const aScript of scripts) {
const href = await aScript.getAttribute('src')
if (href.includes('another')) {
scriptFound = true
break
}
}
expect(scriptFound).toBe(true)
} finally {
if (browser) await browser.close()
}
Expand Down