diff --git a/examples/nuxt-prefetch/README.md b/examples/nuxt-prefetch/README.md new file mode 100644 index 0000000000..9907d2f545 --- /dev/null +++ b/examples/nuxt-prefetch/README.md @@ -0,0 +1,3 @@ +# Example of Nuxt.js prefetching + +Learn more at https://github.com/nuxt/nuxt.js/pull/4574 diff --git a/examples/nuxt-prefetch/assets/check.svg b/examples/nuxt-prefetch/assets/check.svg new file mode 100644 index 0000000000..bb093ca332 --- /dev/null +++ b/examples/nuxt-prefetch/assets/check.svg @@ -0,0 +1 @@ + diff --git a/examples/nuxt-prefetch/layouts/default.vue b/examples/nuxt-prefetch/layouts/default.vue new file mode 100644 index 0000000000..a8c577b30c --- /dev/null +++ b/examples/nuxt-prefetch/layouts/default.vue @@ -0,0 +1,57 @@ + + + diff --git a/examples/nuxt-prefetch/nuxt.config.js b/examples/nuxt-prefetch/nuxt.config.js new file mode 100644 index 0000000000..928401301f --- /dev/null +++ b/examples/nuxt-prefetch/nuxt.config.js @@ -0,0 +1,12 @@ +export default { + head: { + titleTemplate: '%s - NuxtJS Prefetching' + }, + router: { + // To disable prefetching, uncomment the line + // prefetchLinks: false + + // Activate prefetched class (default: false) + linkPrefetchedClass: 'nuxt-link-prefetched' + } +} diff --git a/examples/nuxt-prefetch/package.json b/examples/nuxt-prefetch/package.json new file mode 100644 index 0000000000..0cc6a5c7c8 --- /dev/null +++ b/examples/nuxt-prefetch/package.json @@ -0,0 +1,11 @@ +{ + "name": "example-hello-world", + "dependencies": { + "nuxt": "latest" + }, + "scripts": { + "dev": "nuxt", + "build": "nuxt build", + "start": "nuxt start" + } +} diff --git a/examples/nuxt-prefetch/pages/accelerated.vue b/examples/nuxt-prefetch/pages/accelerated.vue new file mode 100755 index 0000000000..de9c09d6d0 --- /dev/null +++ b/examples/nuxt-prefetch/pages/accelerated.vue @@ -0,0 +1,17 @@ + + + diff --git a/examples/nuxt-prefetch/pages/active.vue b/examples/nuxt-prefetch/pages/active.vue new file mode 100755 index 0000000000..de9c09d6d0 --- /dev/null +++ b/examples/nuxt-prefetch/pages/active.vue @@ -0,0 +1,17 @@ + + + diff --git a/examples/nuxt-prefetch/pages/agile.vue b/examples/nuxt-prefetch/pages/agile.vue new file mode 100755 index 0000000000..de9c09d6d0 --- /dev/null +++ b/examples/nuxt-prefetch/pages/agile.vue @@ -0,0 +1,17 @@ + + + diff --git a/examples/nuxt-prefetch/pages/brisk.vue b/examples/nuxt-prefetch/pages/brisk.vue new file mode 100755 index 0000000000..de9c09d6d0 --- /dev/null +++ b/examples/nuxt-prefetch/pages/brisk.vue @@ -0,0 +1,17 @@ + + + diff --git a/examples/nuxt-prefetch/pages/dashing.vue b/examples/nuxt-prefetch/pages/dashing.vue new file mode 100755 index 0000000000..de9c09d6d0 --- /dev/null +++ b/examples/nuxt-prefetch/pages/dashing.vue @@ -0,0 +1,17 @@ + + + diff --git a/examples/nuxt-prefetch/pages/electric.vue b/examples/nuxt-prefetch/pages/electric.vue new file mode 100755 index 0000000000..de9c09d6d0 --- /dev/null +++ b/examples/nuxt-prefetch/pages/electric.vue @@ -0,0 +1,17 @@ + + + diff --git a/examples/nuxt-prefetch/pages/flashing.vue b/examples/nuxt-prefetch/pages/flashing.vue new file mode 100755 index 0000000000..de9c09d6d0 --- /dev/null +++ b/examples/nuxt-prefetch/pages/flashing.vue @@ -0,0 +1,17 @@ + + + diff --git a/examples/nuxt-prefetch/pages/fleet.vue b/examples/nuxt-prefetch/pages/fleet.vue new file mode 100755 index 0000000000..de9c09d6d0 --- /dev/null +++ b/examples/nuxt-prefetch/pages/fleet.vue @@ -0,0 +1,17 @@ + + + diff --git a/examples/nuxt-prefetch/pages/fleeting.vue b/examples/nuxt-prefetch/pages/fleeting.vue new file mode 100755 index 0000000000..de9c09d6d0 --- /dev/null +++ b/examples/nuxt-prefetch/pages/fleeting.vue @@ -0,0 +1,17 @@ + + + diff --git a/examples/nuxt-prefetch/pages/flying.vue b/examples/nuxt-prefetch/pages/flying.vue new file mode 100755 index 0000000000..de9c09d6d0 --- /dev/null +++ b/examples/nuxt-prefetch/pages/flying.vue @@ -0,0 +1,17 @@ + + + diff --git a/examples/nuxt-prefetch/pages/hot.vue b/examples/nuxt-prefetch/pages/hot.vue new file mode 100755 index 0000000000..de9c09d6d0 --- /dev/null +++ b/examples/nuxt-prefetch/pages/hot.vue @@ -0,0 +1,17 @@ + + + diff --git a/examples/nuxt-prefetch/pages/hurried.vue b/examples/nuxt-prefetch/pages/hurried.vue new file mode 100755 index 0000000000..de9c09d6d0 --- /dev/null +++ b/examples/nuxt-prefetch/pages/hurried.vue @@ -0,0 +1,17 @@ + + + diff --git a/examples/nuxt-prefetch/pages/index.vue b/examples/nuxt-prefetch/pages/index.vue new file mode 100755 index 0000000000..de9c09d6d0 --- /dev/null +++ b/examples/nuxt-prefetch/pages/index.vue @@ -0,0 +1,17 @@ + + + diff --git a/examples/nuxt-prefetch/pages/nimble.vue b/examples/nuxt-prefetch/pages/nimble.vue new file mode 100755 index 0000000000..de9c09d6d0 --- /dev/null +++ b/examples/nuxt-prefetch/pages/nimble.vue @@ -0,0 +1,17 @@ + + + diff --git a/examples/nuxt-prefetch/pages/quick.vue b/examples/nuxt-prefetch/pages/quick.vue new file mode 100755 index 0000000000..de9c09d6d0 --- /dev/null +++ b/examples/nuxt-prefetch/pages/quick.vue @@ -0,0 +1,17 @@ + + + diff --git a/examples/nuxt-prefetch/pages/racing.vue b/examples/nuxt-prefetch/pages/racing.vue new file mode 100755 index 0000000000..de9c09d6d0 --- /dev/null +++ b/examples/nuxt-prefetch/pages/racing.vue @@ -0,0 +1,17 @@ + + + diff --git a/examples/nuxt-prefetch/pages/rapid.vue b/examples/nuxt-prefetch/pages/rapid.vue new file mode 100755 index 0000000000..de9c09d6d0 --- /dev/null +++ b/examples/nuxt-prefetch/pages/rapid.vue @@ -0,0 +1,17 @@ + + + diff --git a/examples/nuxt-prefetch/pages/ready.vue b/examples/nuxt-prefetch/pages/ready.vue new file mode 100755 index 0000000000..de9c09d6d0 --- /dev/null +++ b/examples/nuxt-prefetch/pages/ready.vue @@ -0,0 +1,17 @@ + + + diff --git a/examples/nuxt-prefetch/pages/snap.vue b/examples/nuxt-prefetch/pages/snap.vue new file mode 100755 index 0000000000..de9c09d6d0 --- /dev/null +++ b/examples/nuxt-prefetch/pages/snap.vue @@ -0,0 +1,17 @@ + + + diff --git a/examples/nuxt-prefetch/pages/swift.vue b/examples/nuxt-prefetch/pages/swift.vue new file mode 100755 index 0000000000..de9c09d6d0 --- /dev/null +++ b/examples/nuxt-prefetch/pages/swift.vue @@ -0,0 +1,17 @@ + + + diff --git a/examples/nuxt-prefetch/pages/winged.vue b/examples/nuxt-prefetch/pages/winged.vue new file mode 100755 index 0000000000..de9c09d6d0 --- /dev/null +++ b/examples/nuxt-prefetch/pages/winged.vue @@ -0,0 +1,17 @@ + + + diff --git a/examples/nuxt-prefetch/store/index.js b/examples/nuxt-prefetch/store/index.js new file mode 100644 index 0000000000..0295063e7e --- /dev/null +++ b/examples/nuxt-prefetch/store/index.js @@ -0,0 +1,25 @@ +export const state = () => ({ + links: [ + 'index', + 'agile', + 'brisk', + 'hot', + 'nimble', + 'quick', + 'rapid', + 'swift', + 'accelerated', + 'active', + 'dashing', + 'electric', + 'flashing', + 'fleet', + 'fleeting', + 'flying', + 'hurried', + 'racing', + 'ready', + 'snap', + 'winged' + ] +}) diff --git a/packages/config/src/config/router.js b/packages/config/src/config/router.js index 3b0a316a6d..200c22bfc8 100644 --- a/packages/config/src/config/router.js +++ b/packages/config/src/config/router.js @@ -6,9 +6,11 @@ export default () => ({ middleware: [], linkActiveClass: 'nuxt-link-active', linkExactActiveClass: 'nuxt-link-exact-active', + linkPrefetchedClass: false, extendRoutes: null, scrollBehavior: null, parseQuery: false, stringifyQuery: false, - fallback: false + fallback: false, + prefetchLinks: true }) diff --git a/packages/vue-app/src/index.js b/packages/vue-app/src/index.js index 9ce5163e8c..2cf859e2b1 100644 --- a/packages/vue-app/src/index.js +++ b/packages/vue-app/src/index.js @@ -17,7 +17,8 @@ export const templatesFiles = [ 'components/nuxt-error.vue', 'components/nuxt-loading.vue', 'components/nuxt-child.js', - 'components/nuxt-link.js', + 'components/nuxt-link.server.js', + 'components/nuxt-link.client.js', 'components/nuxt.js', 'components/no-ssr.js', 'views/app.template.html', diff --git a/packages/vue-app/template/App.js b/packages/vue-app/template/App.js index c21dfb29c9..633e0b72f5 100644 --- a/packages/vue-app/template/App.js +++ b/packages/vue-app/template/App.js @@ -55,6 +55,7 @@ export default { ]) }, data: () => ({ + isOnline: true, layout: null, layoutName: '' }), @@ -65,8 +66,12 @@ export default { // Add this.$nuxt in child instances Vue.prototype.<%= globals.nuxt %> = this // add to window so we can listen when ready - if (typeof window !== 'undefined') { + if (process.client) { window.<%= globals.nuxt %> = <%= (globals.nuxt !== '$nuxt' ? 'window.$nuxt = ' : '') %>this + this.refreshOnlineStatus() + // Setup the listeners + window.addEventListener('online', this.refreshOnlineStatus) + window.addEventListener('offline', this.refreshOnlineStatus) } // Add $nuxt.error() this.error = this.nuxt.error @@ -79,7 +84,24 @@ export default { 'nuxt.err': 'errorChanged' }, <% } %> + computed: { + isOffline() { + return !this.isOnline + } + }, methods: { + refreshOnlineStatus() { + if (process.client) { + if (typeof window.navigator.onLine === 'undefined') { + // If the browser doesn't support connection status reports + // assume that we are online because most apps' only react + // when they now that the connection has been interrupted + this.isOnline = true + } else { + this.isOnline = window.navigator.onLine + } + } + }, <% if (loading) { %> errorChanged() { if (this.nuxt.err && this.$loading) { diff --git a/packages/vue-app/template/client.js b/packages/vue-app/template/client.js index 5b535d69f4..58681d4c78 100644 --- a/packages/vue-app/template/client.js +++ b/packages/vue-app/template/client.js @@ -1,5 +1,5 @@ import Vue from 'vue' -import middleware from './middleware' +import middleware from './middleware.js' import { applyAsyncData, sanitizeComponent, @@ -14,8 +14,13 @@ import { compile, getQueryDiff, globalHandleError -} from './utils' -import { createApp, NuxtError } from './index' +} from './utils.js' +import { createApp, NuxtError } from './index.js' +import NuxtLink from './components/nuxt-link.<%= router.prefetchLinks ? "client" : "server" %>.js' // should be included after ./index.js + +// Component: +Vue.component(NuxtLink.name, NuxtLink) +Vue.component('NLink', NuxtLink) const noopData = () => { return {} } const noopFetch = () => {} diff --git a/packages/vue-app/template/components/nuxt-link.client.js b/packages/vue-app/template/components/nuxt-link.client.js new file mode 100644 index 0000000000..919dfb9c80 --- /dev/null +++ b/packages/vue-app/template/components/nuxt-link.client.js @@ -0,0 +1,101 @@ +<%= isTest ? '// @vue/component' : '' %> +import Vue from 'vue' + +const requestIdleCallback = window.requestIdleCallback || + function (cb) { + const start = Date.now() + return setTimeout(function () { + cb({ + didTimeout: false, + timeRemaining: function () { + return Math.max(0, 50 - (Date.now() - start)) + }, + }) + }, 1) + } +const observer = window.IntersectionObserver && new window.IntersectionObserver(entries => { + entries.forEach(({ intersectionRatio, target: link }) => { + if (intersectionRatio <= 0) { + return + } + link.__prefetch() + }) +}) + +export default { + extends: Vue.component('RouterLink'), + name: 'NuxtLink', + props: { + noPrefetch: { + type: Boolean, + default: false + }<% if (router.linkPrefetchedClass) { %>, + prefetchedClass: { + type: String, + default: '<%= router.linkPrefetchedClass %>' + }<% } %> + }, + mounted() { + if (!this.noPrefetch) { + requestIdleCallback(this.observe, { timeout: 2e3 }) + } + }, + beforeDestroy() { + if (this.__observed) { + observer.unobserve(this.$el) + delete this.$el.__prefetch + } + }, + methods: { + observe() { + // If no IntersectionObserver, avoid prefetching + if (!observer) { + return + } + // Add to observer + if (this.shouldPrefetch()) { + this.$el.__prefetch = this.prefetch.bind(this) + observer.observe(this.$el) + this.__observed = true + }<% if (router.linkPrefetchedClass) { %> else { + this.addPrefetchedClass() + }<% } %> + }, + shouldPrefetch() { + return this.getPrefetchComponents().length > 0 + }, + canPrefetch() { + const conn = navigator.connection + const hasBadConnection = this.$nuxt.isOffline || (conn && ((conn.effectiveType || '').includes('2g') || conn.saveData)) + + return !hasBadConnection + }, + getPrefetchComponents() { + const ref = this.$router.resolve(this.to, this.$route, this.append) + const Components = ref.resolved.matched.map((r) => r.components.default) + + return Components.filter((Component) => typeof Component === 'function' && !Component.options && !Component.__prefetched) + }, + prefetch() { + if (!this.canPrefetch()) { + return + } + // Stop obersing this link (in case of internet connection changes) + observer.unobserve(this.$el) + const Components = this.getPrefetchComponents() + + for (const Component of Components) { + try { + Component() + Component.__prefetched = true + } catch (e) {} + }<% if (router.linkPrefetchedClass) { %> + this.addPrefetchedClass()<% } %> + }<% if (router.linkPrefetchedClass) { %>, + addPrefetchedClass() { + if (this.prefetchedClass !== 'false') { + this.$el.className += (this.$el.className + ' ' + this.prefetchedClass).trim() + } + }<% } %> + } +} diff --git a/packages/vue-app/template/components/nuxt-link.js b/packages/vue-app/template/components/nuxt-link.js deleted file mode 100644 index 8077605310..0000000000 --- a/packages/vue-app/template/components/nuxt-link.js +++ /dev/null @@ -1,8 +0,0 @@ -<%= isTest ? '// @vue/component' : '' %> -export default { - name: 'NuxtLink', - functional: true, - render(h, { data, children }) { - return h('router-link', data, children) - } -} diff --git a/packages/vue-app/template/components/nuxt-link.server.js b/packages/vue-app/template/components/nuxt-link.server.js new file mode 100644 index 0000000000..ea3efd6fbd --- /dev/null +++ b/packages/vue-app/template/components/nuxt-link.server.js @@ -0,0 +1,13 @@ +<%= isTest ? '// @vue/component' : '' %> +import Vue from 'vue' + +export default { + extends: Vue.component('RouterLink'), + name: 'NuxtLink', + props: { + noPrefetch: { + type: Boolean, + default: false + } + } +} diff --git a/packages/vue-app/template/index.js b/packages/vue-app/template/index.js index a8469e3f42..2d3bb9a31e 100644 --- a/packages/vue-app/template/index.js +++ b/packages/vue-app/template/index.js @@ -3,7 +3,6 @@ import Meta from 'vue-meta' import { createRouter } from './router.js' import NoSsr from './components/no-ssr.js' import NuxtChild from './components/nuxt-child.js' -import NuxtLink from './components/nuxt-link.js' import NuxtError from '<%= components.ErrorPage ? components.ErrorPage : "./components/nuxt-error.vue" %>' import Nuxt from './components/nuxt.js' import App from '<%= appPath %>' @@ -23,9 +22,7 @@ Vue.component(NoSsr.name, NoSsr) Vue.component(NuxtChild.name, NuxtChild) Vue.component('NChild', NuxtChild) -// Component: ` Vue.component(Nuxt.name, Nuxt) diff --git a/packages/vue-app/template/server.js b/packages/vue-app/template/server.js index 6b68834ac5..cda9be6553 100644 --- a/packages/vue-app/template/server.js +++ b/packages/vue-app/template/server.js @@ -1,9 +1,14 @@ import { stringify } from 'querystring' import Vue from 'vue' import omit from 'lodash/omit' -import middleware from './middleware' -import { applyAsyncData, sanitizeComponent, getMatchedComponents, getContext, middlewareSeries, promisify, urlJoin } from './utils' -import { createApp, NuxtError } from './index' +import middleware from './middleware.js' +import { applyAsyncData, sanitizeComponent, getMatchedComponents, getContext, middlewareSeries, promisify, urlJoin } from './utils.js' +import { createApp, NuxtError } from './index.js' +import NuxtLink from './components/nuxt-link.server.js' + +// Component: +Vue.component(NuxtLink.name, NuxtLink) +Vue.component('NLink', NuxtLink) const debug = require('debug')('nuxt:render') debug.color = 4 // force blue color diff --git a/packages/vue-app/template/utils.js b/packages/vue-app/template/utils.js index 808643886b..1b7f7bfee1 100644 --- a/packages/vue-app/template/utils.js +++ b/packages/vue-app/template/utils.js @@ -566,4 +566,3 @@ function formatQuery(query) { return key + '=' + val }).filter(Boolean).join('&') } - diff --git a/packages/vue-app/types/index.d.ts b/packages/vue-app/types/index.d.ts index 183a6cb35d..c9582f3a82 100644 --- a/packages/vue-app/types/index.d.ts +++ b/packages/vue-app/types/index.d.ts @@ -59,7 +59,11 @@ export interface ErrorParams { message?: string; } -export interface LoadingObject { - start(): void; - finish(): void; +export interface NuxtApp extends Vue { + isOffline: boolean; + isOnline: boolean; + $loading: { + start(): void; + finish(): void; + }; } diff --git a/packages/vue-app/types/vue.d.ts b/packages/vue-app/types/vue.d.ts index ee24895b10..ac827c2a47 100644 --- a/packages/vue-app/types/vue.d.ts +++ b/packages/vue-app/types/vue.d.ts @@ -5,7 +5,7 @@ import Vue, { ComponentOptions } from "vue"; import { Route } from "vue-router"; import { MetaInfo } from "vue-meta"; -import { Context, Middleware, Transition, LoadingObject } from "./index"; +import { Context, Middleware, Transition, NuxtApp } from "./index"; declare module "vue/types/options" { interface ComponentOptions { @@ -24,8 +24,6 @@ declare module "vue/types/options" { declare module "vue/types/vue" { interface Vue { - $nuxt: { - $loading: LoadingObject; - }; + $nuxt: NuxtApp; } } diff --git a/packages/vue-app/vetur/nuxt-tags.json b/packages/vue-app/vetur/nuxt-tags.json index 410e1f650e..a2d05df3f0 100644 --- a/packages/vue-app/vetur/nuxt-tags.json +++ b/packages/vue-app/vetur/nuxt-tags.json @@ -3,7 +3,7 @@ "attributes": [ "nuxtChildKey" ], - "description": "The nuxt component." + "description": "Component to render the current nuxt page." }, "n-child": { "description": "Component for displaying the children components in a nested route." @@ -23,7 +23,7 @@ "exact-active-class", "no-prefetch" ], - "description": "Component for routing. Same as now." + "description": "Component for navigating between Nuxt pages." }, "nuxt-link": { "attributes": [ @@ -37,7 +37,7 @@ "exact-active-class", "no-prefetch" ], - "description": "Component for routing. Same as now." + "description": "Component for navigating between Nuxt pages." }, "no-ssr": { "description": "Component for excluding a part of your app from server-side rendering." diff --git a/test/unit/async-config.size-limit.test.js b/test/unit/async-config.size-limit.test.js index f098431e91..2ff88a739c 100644 --- a/test/unit/async-config.size-limit.test.js +++ b/test/unit/async-config.size-limit.test.js @@ -34,6 +34,6 @@ describe('size-limit test', () => { const responseSizeBytes = responseSizes.reduce((bytes, responseLength) => bytes + responseLength, 0) const responseSizeKilobytes = Math.ceil(responseSizeBytes / 1024) // Without gzip! - expect(responseSizeKilobytes).toBeLessThanOrEqual(171) + expect(responseSizeKilobytes).toBeLessThanOrEqual(180) }) })