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

feat(nuxt-link): Smart prefetching and $nuxt.isOffline #4574

Merged
merged 32 commits into from
Dec 28, 2018
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
c019997
feat(nuxt-link): Improve <n-link> and add automatic prefetch
Atinux Dec 17, 2018
355ba63
Update packages/vue-app/template/components/nuxt-link.js
manniL Dec 20, 2018
9d2f5ab
add missing space
manniL Dec 20, 2018
c3a8da4
Merge branch 'dev' into feat-nlink-prefetch
Dec 20, 2018
d9d691f
feat(nuxt-link): Split in two components for smaller bundle
Atinux Dec 21, 2018
27fc3df
fix(vue-app): Use requestIdleCallback
Atinux Dec 21, 2018
8ecc729
chore(vue-app): Improve nuxt prefetch strategy for nuxt links
Atinux Dec 24, 2018
f2f328c
chore(vue-app): Add .isOnline and handle it for prefetch
Atinux Dec 26, 2018
1499ed0
Merge branch 'dev' into feat-nlink-prefetch
Dec 26, 2018
d9e5812
chore(vue-app): Add .isOffline and use it
Atinux Dec 27, 2018
406d210
chore(vue-app): Add .isOffline
Atinux Dec 27, 2018
7f260bf
chore(server): Check is options.modern is given in dev mode
Atinux Dec 27, 2018
42d8191
chore(vue-app): Add intersection-observer polyfill if router.prefetch…
Atinux Dec 27, 2018
9f4f481
chore(vue-app): Remove polyfill
Atinux Dec 27, 2018
485e6e8
chore(vue-app): Use only process.client
Atinux Dec 27, 2018
1bfc8bc
chore(vue-app): Add TS typings for .isOnline and isOffline
Atinux Dec 27, 2018
aecb458
Merge branch 'dev' into feat-nlink-prefetch
Dec 27, 2018
844da5a
chore(vue-app): Update typings by @kevinmarrec
Atinux Dec 27, 2018
316899d
Merge branch 'feat-nlink-prefetch' of github.com:nuxt/nuxt.js into fe…
Atinux Dec 27, 2018
78580a6
chore(vue-app): Reorder names
Atinux Dec 27, 2018
7274c99
examples(nuxt-prefetch): Add Nuxt prefetching example
Atinux Dec 27, 2018
8bdadc9
chore(vue-app): Add router.linkPrefetchedClass
Atinux Dec 27, 2018
85105b8
lint(vue-app): Fix lint
Atinux Dec 28, 2018
28d32ac
chore(vue-app): Use intersectionRatio, recommend by @maoberlehner
Atinux Dec 28, 2018
94360d0
fix(lint): Fix linting issues
Atinux Dec 28, 2018
a2d8673
lint(vue-app): Fix again (lol)
Atinux Dec 28, 2018
0fc946a
types(vue-app): Update TS typings
Atinux Dec 28, 2018
8f70dc9
chore(vue-app): Update Vetur tags description
Atinux Dec 28, 2018
d81fbb0
fix(vue-app): Use prefetchClass
Atinux Dec 28, 2018
bd505fc
chore(vue-app): Disable linkPrefetchedClass by default
Atinux Dec 28, 2018
e02001c
Merge branch 'dev' into feat-nlink-prefetch
Dec 28, 2018
23d53fa
Merge branch 'dev' into feat-nlink-prefetch
Dec 28, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/config/src/config/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ export default () => ({
scrollBehavior: null,
parseQuery: false,
stringifyQuery: false,
fallback: false
fallback: false,
prefetchLinks: true
})
2 changes: 1 addition & 1 deletion packages/server/src/middleware/modern.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ const setModernMode = (req, options) => {
req.modernMode = isModernBrowser
}
if (options.dev) {
req.devModernMode = isModernBrowser
req.devModernMode = options.modern && isModernBrowser
}
}

Expand Down
3 changes: 2 additions & 1 deletion packages/vue-app/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
24 changes: 23 additions & 1 deletion packages/vue-app/template/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export default {
])
},
data: () => ({
isOnline: true,
layout: null,
layoutName: ''
}),
Expand All @@ -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
Expand All @@ -79,7 +84,24 @@ export default {
'nuxt.err': 'errorChanged'
},
<% } %>
computed: {
isOffline() {
return !this.isOnline
}
Atinux marked this conversation as resolved.
Show resolved Hide resolved
},
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) {
Expand Down
11 changes: 8 additions & 3 deletions packages/vue-app/template/client.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import Vue from 'vue'
import middleware from './middleware'
import middleware from './middleware.js'
pi0 marked this conversation as resolved.
Show resolved Hide resolved
import {
applyAsyncData,
sanitizeComponent,
Expand All @@ -14,8 +14,13 @@ import {
compile,
getQueryDiff,
globalHandleError
} from './utils'
import { createApp, NuxtError } from './index'
} from './utils.js'
import { createApp, NuxtError } from './index.js'
manniL marked this conversation as resolved.
Show resolved Hide resolved
import NuxtLink from './components/nuxt-link.<%= router.prefetchLinks ? "client" : "server" %>.js' // should be included after ./index.js

// Component: <NuxtLink>
Vue.component(NuxtLink.name, NuxtLink)
Vue.component('NLink', NuxtLink)

const noopData = () => { return {} }
const noopFetch = () => {}
Expand Down
89 changes: 89 additions & 0 deletions packages/vue-app/template/components/nuxt-link.client.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<%= isTest ? '// @vue/component' : '' %>
import Vue from 'vue'

const requestIdleCallback = window.requestIdleCallback ||
Atinux marked this conversation as resolved.
Show resolved Hide resolved
function (cb) {
Atinux marked this conversation as resolved.
Show resolved Hide resolved
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(({ isIntersecting, target: link }) => {
if (!isIntersecting) {
Atinux marked this conversation as resolved.
Show resolved Hide resolved
return
}
link.__prefetch()
})
})

export default {
extends: Vue.component('RouterLink'),
name: 'NuxtLink',
props: {
noPrefetch: {
type: Boolean,
default: false
}
},
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
}
},
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) {}
}
}
}
}
8 changes: 0 additions & 8 deletions packages/vue-app/template/components/nuxt-link.js

This file was deleted.

13 changes: 13 additions & 0 deletions packages/vue-app/template/components/nuxt-link.server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<%= isTest ? '// @vue/component' : '' %>
import Vue from 'vue'

export default {
extends: Vue.component('RouterLink'),
Atinux marked this conversation as resolved.
Show resolved Hide resolved
name: 'NuxtLink',
props: {
noPrefetch: {
type: Boolean,
default: false
}
}
}
5 changes: 1 addition & 4 deletions packages/vue-app/template/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 %>'
Expand All @@ -23,9 +22,7 @@ Vue.component(NoSsr.name, NoSsr)
Vue.component(NuxtChild.name, NuxtChild)
Vue.component('NChild', NuxtChild)

// Component: <NuxtLink
Vue.component(NuxtLink.name, NuxtLink)
Vue.component('NLink', NuxtLink)
// Component NuxtLink is imported in server.js or client.js
pi0 marked this conversation as resolved.
Show resolved Hide resolved

// Component: <Nuxt>`
Vue.component(Nuxt.name, Nuxt)
Expand Down
11 changes: 8 additions & 3 deletions packages/vue-app/template/server.js
Original file line number Diff line number Diff line change
@@ -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: <NuxtLink>
Vue.component(NuxtLink.name, NuxtLink)
Vue.component('NLink', NuxtLink)

const debug = require('debug')('nuxt:render')
debug.color = 4 // force blue color
Expand Down
1 change: 0 additions & 1 deletion packages/vue-app/template/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -566,4 +566,3 @@ function formatQuery(query) {
return key + '=' + val
}).filter(Boolean).join('&')
}

9 changes: 9 additions & 0 deletions packages/vue-app/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,12 @@ export interface LoadingObject {
start(): void;
Atinux marked this conversation as resolved.
Show resolved Hide resolved
finish(): void;
}

export interface NuxtApp extends Vue {
isOffline: boolean;
isOnline: boolean;
$loading: {
start(): void;
finish(): void;
};
}
6 changes: 2 additions & 4 deletions packages/vue-app/types/vue.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, LoadingObject, NuxtApp } from "./index";
Atinux marked this conversation as resolved.
Show resolved Hide resolved

declare module "vue/types/options" {
interface ComponentOptions<V extends Vue> {
Expand All @@ -24,8 +24,6 @@ declare module "vue/types/options" {

declare module "vue/types/vue" {
interface Vue {
$nuxt: {
$loading: LoadingObject;
};
$nuxt: NuxtApp
Atinux marked this conversation as resolved.
Show resolved Hide resolved
}
}
2 changes: 1 addition & 1 deletion test/unit/async-config.size-limit.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
})