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 @@
+
+
+
+ You are offline
+
+
+
{{ $route.name }}
+
+
+
+
+
+
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 @@
+
+
+
+ /{{ link }}
+
+
+
+
+
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 @@
+
+
+
+ /{{ link }}
+
+
+
+
+
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 @@
+
+
+
+ /{{ link }}
+
+
+
+
+
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 @@
+
+
+
+ /{{ link }}
+
+
+
+
+
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 @@
+
+
+
+ /{{ link }}
+
+
+
+
+
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 @@
+
+
+
+ /{{ link }}
+
+
+
+
+
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 @@
+
+
+
+ /{{ link }}
+
+
+
+
+
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 @@
+
+
+
+ /{{ link }}
+
+
+
+
+
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 @@
+
+
+
+ /{{ link }}
+
+
+
+
+
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 @@
+
+
+
+ /{{ link }}
+
+
+
+
+
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 @@
+
+
+
+ /{{ link }}
+
+
+
+
+
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 @@
+
+
+
+ /{{ link }}
+
+
+
+
+
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 @@
+
+
+
+ /{{ link }}
+
+
+
+
+
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 @@
+
+
+
+ /{{ link }}
+
+
+
+
+
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 @@
+
+
+
+ /{{ link }}
+
+
+
+
+
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 @@
+
+
+
+ /{{ link }}
+
+
+
+
+
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 @@
+
+
+
+ /{{ link }}
+
+
+
+
+
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 @@
+
+
+
+ /{{ link }}
+
+
+
+
+
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 @@
+
+
+
+ /{{ link }}
+
+
+
+
+
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 @@
+
+
+
+ /{{ link }}
+
+
+
+
+
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 @@
+
+
+
+ /{{ link }}
+
+
+
+
+
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)
})
})