From 41e24a516ae33913c63aea45d44c062955087d92 Mon Sep 17 00:00:00 2001 From: Joe Haddad Date: Mon, 17 Feb 2020 15:46:15 -0500 Subject: [PATCH 1/2] Speculate Navigations for Client-Side JS (#10560) * Speculate Navigations for Client-Side JS * add tests * avoid desctructure --- packages/next/client/link.tsx | 27 +++++---- .../next/next-server/lib/router/router.ts | 21 +++++-- .../preload-viewport/pages/index.js | 2 +- .../preload-viewport/test/index.test.js | 58 ++++++++++++++++--- 4 files changed, 82 insertions(+), 26 deletions(-) diff --git a/packages/next/client/link.tsx b/packages/next/client/link.tsx index df83a94c54733..0f24f02c5d3b6 100644 --- a/packages/next/client/link.tsx +++ b/packages/next/client/link.tsx @@ -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) @@ -127,14 +128,18 @@ class Link extends Component { 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() @@ -201,11 +206,11 @@ class Link extends Component { }) } - 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 } @@ -239,7 +244,7 @@ class Link extends Component { 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') { diff --git a/packages/next/next-server/lib/router/router.ts b/packages/next/next-server/lib/router/router.ts index 6bd26cddd8c1c..76b5626d53c2a 100644 --- a/packages/next/next-server/lib/router/router.ts +++ b/packages/next/next-server/lib/router/router.ts @@ -3,7 +3,6 @@ import { ParsedUrlQuery } from 'querystring' import { ComponentType } from 'react' import { parse, UrlObject } from 'url' - import mitt, { MittEmitter } from '../mitt' import { AppContextType, @@ -52,6 +51,10 @@ export type NextRouter = BaseRouter & | 'isFallback' > +export type PrefetchOptions = { + priority?: boolean +} + type RouteInfo = { Component: ComponentType props?: any @@ -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 { + prefetch( + url: string, + asPath: string = url, + options: PrefetchOptions = {} + ): Promise { return new Promise((resolve, reject) => { const { pathname, protocol } = parse(url) @@ -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']( + toRoute(pathname) + ).then(() => resolve(), reject) }) } diff --git a/test/integration/preload-viewport/pages/index.js b/test/integration/preload-viewport/pages/index.js index 9143a65cc1db7..a0cb7a920dfc7 100644 --- a/test/integration/preload-viewport/pages/index.js +++ b/test/integration/preload-viewport/pages/index.js @@ -22,7 +22,7 @@ export default () => { />

Hi 👋

- to /another + to /another ) diff --git a/test/integration/preload-viewport/test/index.test.js b/test/integration/preload-viewport/test/index.test.js index 96e246fe0f871..81b035e2dbe85 100644 --- a/test/integration/preload-viewport/test/index.test.js +++ b/test/integration/preload-viewport/test/index.test.js @@ -1,7 +1,5 @@ /* eslint-env jest */ /* global jasmine */ -import webdriver from 'next-webdriver' -import { join } from 'path' import { nextServer, runNextCommand, @@ -9,6 +7,8 @@ import { stopApp, waitFor, } from 'next-test-utils' +import webdriver from 'next-webdriver' +import { join } from 'path' jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 60 * 5 @@ -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