diff --git a/packages/types/app/index.d.ts b/packages/types/app/index.d.ts index ffc3b762f8..f07d7bfbc7 100644 --- a/packages/types/app/index.d.ts +++ b/packages/types/app/index.d.ts @@ -61,6 +61,7 @@ export interface Context { redirected: boolean next: NextFunction beforeRenderFns: Array<() => any> + fetchCounters: Record nuxt: { layout: string data: Array> diff --git a/packages/types/app/vue.d.ts b/packages/types/app/vue.d.ts index f91bcc25a8..41b4497df4 100644 --- a/packages/types/app/vue.d.ts +++ b/packages/types/app/vue.d.ts @@ -12,6 +12,7 @@ declare module 'vue/types/options' { interface ComponentOptions { asyncData?(ctx: Context): Promise | object | void fetch?(ctx: Context): Promise | void + fetchKey?: string | ((getKey: (id: string) => number) => string) fetchDelay?: number fetchOnServer?: boolean | (() => boolean) head?: MetaInfo | (() => MetaInfo) diff --git a/packages/vue-app/template/App.js b/packages/vue-app/template/App.js index c8f7c95b02..81ea927710 100644 --- a/packages/vue-app/template/App.js +++ b/packages/vue-app/template/App.js @@ -317,7 +317,7 @@ export default { <% } %> setPagePayload(payload) { this._pagePayload = payload - this._payloadFetchIndex = 0 + this._fetchCounters = {} }, async fetchPayload(route) { <% if (nuxtOptions.generate.manifest) { %> diff --git a/packages/vue-app/template/mixins/fetch.client.js b/packages/vue-app/template/mixins/fetch.client.js index 16f7f0f13e..ea8ed615b8 100644 --- a/packages/vue-app/template/mixins/fetch.client.js +++ b/packages/vue-app/template/mixins/fetch.client.js @@ -1,5 +1,5 @@ import Vue from 'vue' -import { hasFetch, normalizeError, addLifecycleHook } from '../utils' +import { hasFetch, normalizeError, addLifecycleHook, createGetCounter } from '../utils' const isSsrHydration = (vm) => vm.$vnode && vm.$vnode.elm && vm.$vnode.elm.dataset && vm.$vnode.elm.dataset.fetchKey const nuxtState = window.<%= globals.context %> @@ -38,7 +38,7 @@ function created() { // Hydrate component this._hydrated = true - this._fetchKey = +this.$vnode.elm.dataset.fetchKey + this._fetchKey = this.$vnode.elm.dataset.fetchKey const data = nuxtState.fetch[this._fetchKey] // If fetch error @@ -64,7 +64,17 @@ function createdFullStatic() { return } this._hydrated = true - this._fetchKey = this.<%= globals.nuxt %>._payloadFetchIndex++ + + const defaultKey = this.$options._scopeId || this.$options.name || '' + const getCounter = createGetCounter(this.<%= globals.nuxt %>._fetchCounters, defaultKey) + + if (typeof this.$options.fetchKey === 'function') { + this._fetchKey = this.$options.fetchKey.call(this, getCounter) + } else { + const key = 'string' === typeof this.$options.fetchKey ? this.$options.fetchKey : defaultKey + this._fetchKey = key + ':' + getCounter(key) + } + const data = this.<%= globals.nuxt %>._pagePayload.fetch[this._fetchKey] // If fetch error diff --git a/packages/vue-app/template/mixins/fetch.server.js b/packages/vue-app/template/mixins/fetch.server.js index 9257031520..68929625d7 100644 --- a/packages/vue-app/template/mixins/fetch.server.js +++ b/packages/vue-app/template/mixins/fetch.server.js @@ -1,5 +1,5 @@ import Vue from 'vue' -import { hasFetch, normalizeError, addLifecycleHook, purifyData } from '../utils' +import { hasFetch, normalizeError, addLifecycleHook, purifyData, createGetCounter } from '../utils' async function serverPrefetch() { if (!this._fetchOnServer) { @@ -19,14 +19,20 @@ async function serverPrefetch() { // Define an ssrKey for hydration - this._fetchKey = this.$ssrContext.nuxt.fetch.length + this._fetchKey = this._fetchKey || this.$ssrContext.fetchCounters['']++ // Add data-fetch-key on parent element of Component const attrs = this.$vnode.data.attrs = this.$vnode.data.attrs || {} attrs['data-fetch-key'] = this._fetchKey // Add to ssrContext for window.__NUXT__.fetch - this.$ssrContext.nuxt.fetch.push(this.$fetchState.error ? { _error: this.$fetchState.error } : purifyData(this._data)) + <% if (debug) { %> + if (this.$ssrContext.nuxt.fetch[this._fetchKey] !== undefined) { + console.warn(`Duplicate fetch key detected (${this._fetchKey}). This may lead to unexpected results.`) + } + <% } %> + this.$ssrContext.nuxt.fetch[this._fetchKey] = + this.$fetchState.error ? { _error: this.$fetchState.error } : purifyData(this._data) } export default { @@ -41,6 +47,16 @@ export default { this._fetchOnServer = this.$options.fetchOnServer !== false } + const defaultKey = this.$options._scopeId || this.$options.name || '' + const getCounter = createGetCounter(this.$ssrContext.fetchCounters, defaultKey) + + if (typeof this.$options.fetchKey === 'function') { + this._fetchKey = this.$options.fetchKey.call(this, getCounter) + } else { + const key = 'string' === typeof this.$options.fetchKey ? this.$options.fetchKey : defaultKey + this._fetchKey = key + getCounter(key) + } + // Added for remove vue undefined warning while ssr this.$fetch = () => {} // issue #8043 Vue.util.defineReactive(this, '$fetchState', { diff --git a/packages/vue-app/template/server.js b/packages/vue-app/template/server.js index 0b4fb62b29..2456301cf9 100644 --- a/packages/vue-app/template/server.js +++ b/packages/vue-app/template/server.js @@ -73,7 +73,11 @@ export default async (ssrContext) => { // Used for beforeNuxtRender({ Components, nuxtState }) ssrContext.beforeRenderFns = [] // Nuxt object (window.{{globals.context}}, defaults to window.__NUXT__) - ssrContext.nuxt = { <% if (features.layouts) { %>layout: 'default', <% } %>data: [], <% if (features.fetch) { %>fetch: [], <% } %>error: null<%= (store ? ', state: null' : '') %>, serverRendered: true, routePath: '' } + ssrContext.nuxt = { <% if (features.layouts) { %>layout: 'default', <% } %>data: [], <% if (features.fetch) { %>fetch: {}, <% } %>error: null<%= (store ? ', state: null' : '') %>, serverRendered: true, routePath: '' } + <% if (features.fetch) { %> + ssrContext.fetchCounters = {} + <% } %> + // Remove query from url is static target if (process.static && ssrContext.url) { ssrContext.url = ssrContext.url.split('?')[0] diff --git a/packages/vue-app/template/utils.js b/packages/vue-app/template/utils.js index 6435b7297f..b4cda63c57 100644 --- a/packages/vue-app/template/utils.js +++ b/packages/vue-app/template/utils.js @@ -10,6 +10,15 @@ if (process.client) { } } +export function createGetCounter (counterObject, defaultKey = '') { + return function getCounter (id = defaultKey) { + if (counterObject[id] === undefined) { + counterObject[id] = 0 + } + return counterObject[id]++ + } +} + export function empty () {} export function globalHandleError (error) { diff --git a/test/dev/basic.generate.test.js b/test/dev/basic.generate.test.js index 8440789b62..a7086c72c5 100644 --- a/test/dev/basic.generate.test.js +++ b/test/dev/basic.generate.test.js @@ -24,6 +24,9 @@ describe('basic generate', () => { generate: { static: false, dir: '.nuxt-generate' + }, + publicRuntimeConfig: { + generated: true } }) const nuxt = new Nuxt(config) @@ -142,6 +145,13 @@ describe('basic generate', () => { expect(html).toContain('

Nuxt

') }) + test('/fetch', async () => { + const window = await generator.nuxt.server.renderAndGetWindow(url('/fetch')) + const html = window.document.body.innerHTML + expect(html).toContain('true') + expect(window.__NUXT__.fetch.custom100.fetched).toBe(true) + }) + test('/тест雨 (test non ascii route)', async () => { const window = await generator.nuxt.server.renderAndGetWindow(url('/тест雨')) const html = window.document.body.innerHTML diff --git a/test/e2e/fetch.browser.test.js b/test/e2e/fetch.browser.test.js index a3e10af286..dbd4830fe5 100644 --- a/test/e2e/fetch.browser.test.js +++ b/test/e2e/fetch.browser.test.js @@ -76,6 +76,59 @@ describe('basic browser', () => { expect(await page.$text('pre')).toContain('kevinmarrec') }) + test('/nested', async () => { + await page.nuxt.navigate('/nested') + const fetchKeys = await page.evaluate(() => Object.keys(window.__NUXT__.fetch)) + expect(fetchKeys).toEqual([ + '0', + 'DefaultLayout0' + ]) + expect(await page.$text('div')).toContain('foo-bar-baz') + expect(await page.$text('div')).toContain('fizz-buzz') + expect(await page.$text('button')).toContain('fetch') + }) + + test('/nested/child', async () => { + await page.nuxt.navigate('/nested/child') + await page.waitForSelector('pre') + expect(await page.$text('pre')).toContain('Atinux') + const fetchKeys = await page.evaluate(() => Object.keys(window.__NUXT__.fetch)) + expect(fetchKeys).toEqual([ + '0', + 'DefaultLayout0' + ]) + expect(await page.$text('div')).toContain('foo-bar-baz') + expect(await page.$text('div')).toContain('fizz-buzz') + }) + + test('ssr: /nested', async () => { + page = await browser.page(url('/nested')) + expect(await page.$text('div')).toContain('foo-bar-baz') + expect(await page.$text('div')).toContain('fizz-buzz') + const fetchKeys = await page.evaluate(() => Object.keys(window.__NUXT__.fetch)) + expect(fetchKeys).toEqual([ + '0', + 'DefaultLayout0', + 'ie0' + ]) + expect(await page.$text('button')).toContain('has fetch') + }) + + test('ssr: /nested/child', async () => { + page = await browser.page(url('/nested/child')) + expect(await page.$text('pre')).toContain('Atinux') + const fetchKeys = await page.evaluate(() => Object.keys(window.__NUXT__.fetch)) + expect(fetchKeys).toEqual([ + '0', + 'DefaultLayout0', + 'team0' + ]) + const team = await page.evaluate(() => window.__NUXT__.fetch.team0.team) + expect(team.includes('Atinux')) + expect(await page.$text('div')).toContain('foo-bar-baz') + expect(await page.$text('div')).toContain('fizz-buzz') + }) + test('ssr: /fetch-root', async () => { const page = await browser.page(url('/fetch-root')) expect(await page.$text('button')).toContain('has fetch') @@ -132,7 +185,7 @@ describe('basic browser', () => { // Fragments const { data, fetch } = await page.evaluate(() => window.__NUXT__) expect(data.length).toBe(1) - expect(fetch.length).toBe(1) + expect(Object.keys(fetch).length).toBe(2) // asyncData mutations expect(data[0]).toMatchObject({ async: 'data', async2: 'data2' }) diff --git a/test/fixtures/basic/pages/fetch.vue b/test/fixtures/basic/pages/fetch.vue index 8b35719db4..8a07957716 100644 --- a/test/fixtures/basic/pages/fetch.vue +++ b/test/fixtures/basic/pages/fetch.vue @@ -7,6 +7,7 @@ + {{ fetched }} @@ -18,10 +19,25 @@ const getData = () => fetch(`${baseURL}/test`) .then(r => r + ` (From ${name})`) export default { - async asyncData () { + async asyncData ({ $config }) { + if ($config.generated) { return } + const data = await getData() return { data } }, + data: () => ({ + num: 10, + fetched: false + }), + async fetch () { + await new Promise((resolve) => { + this.fetched = true + resolve() + }) + }, + fetchKey (getCounter) { + return 'custom' + this.num + getCounter('custom') + }, methods: { async update () { this.data = await getData() diff --git a/test/fixtures/fetch/components/Team.vue b/test/fixtures/fetch/components/Team.vue index 4a6f155b04..1c8257f2b1 100644 --- a/test/fixtures/fetch/components/Team.vue +++ b/test/fixtures/fetch/components/Team.vue @@ -12,6 +12,7 @@ diff --git a/test/fixtures/fetch/pages/nested.vue b/test/fixtures/fetch/pages/nested.vue new file mode 100644 index 0000000000..0582372601 --- /dev/null +++ b/test/fixtures/fetch/pages/nested.vue @@ -0,0 +1,22 @@ + + + diff --git a/test/fixtures/fetch/pages/nested/_slug.vue b/test/fixtures/fetch/pages/nested/_slug.vue new file mode 100644 index 0000000000..9a3b4484dd --- /dev/null +++ b/test/fixtures/fetch/pages/nested/_slug.vue @@ -0,0 +1,13 @@ + + + diff --git a/test/fixtures/fetch/pages/nested/index.vue b/test/fixtures/fetch/pages/nested/index.vue new file mode 100644 index 0000000000..172f7f5db6 --- /dev/null +++ b/test/fixtures/fetch/pages/nested/index.vue @@ -0,0 +1,26 @@ + + +