feat(vue-app): support custom fetchKey for full static generation (#8466)

[release]
This commit is contained in:
Daniel Roe 2020-12-17 11:49:59 +00:00 committed by GitHub
parent 5a5161aa52
commit 92018e586b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 215 additions and 10 deletions

View File

@ -61,6 +61,7 @@ export interface Context {
redirected: boolean redirected: boolean
next: NextFunction next: NextFunction
beforeRenderFns: Array<() => any> beforeRenderFns: Array<() => any>
fetchCounters: Record<string, number>
nuxt: { nuxt: {
layout: string layout: string
data: Array<Record<string, any>> data: Array<Record<string, any>>

View File

@ -12,6 +12,7 @@ declare module 'vue/types/options' {
interface ComponentOptions<V extends Vue> { interface ComponentOptions<V extends Vue> {
asyncData?(ctx: Context): Promise<object | void> | object | void asyncData?(ctx: Context): Promise<object | void> | object | void
fetch?(ctx: Context): Promise<void> | void fetch?(ctx: Context): Promise<void> | void
fetchKey?: string | ((getKey: (id: string) => number) => string)
fetchDelay?: number fetchDelay?: number
fetchOnServer?: boolean | (() => boolean) fetchOnServer?: boolean | (() => boolean)
head?: MetaInfo | (() => MetaInfo) head?: MetaInfo | (() => MetaInfo)

View File

@ -317,7 +317,7 @@ export default {
<% } %> <% } %>
setPagePayload(payload) { setPagePayload(payload) {
this._pagePayload = payload this._pagePayload = payload
this._payloadFetchIndex = 0 this._fetchCounters = {}
}, },
async fetchPayload(route) { async fetchPayload(route) {
<% if (nuxtOptions.generate.manifest) { %> <% if (nuxtOptions.generate.manifest) { %>

View File

@ -1,5 +1,5 @@
import Vue from 'vue' 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 isSsrHydration = (vm) => vm.$vnode && vm.$vnode.elm && vm.$vnode.elm.dataset && vm.$vnode.elm.dataset.fetchKey
const nuxtState = window.<%= globals.context %> const nuxtState = window.<%= globals.context %>
@ -38,7 +38,7 @@ function created() {
// Hydrate component // Hydrate component
this._hydrated = true this._hydrated = true
this._fetchKey = +this.$vnode.elm.dataset.fetchKey this._fetchKey = this.$vnode.elm.dataset.fetchKey
const data = nuxtState.fetch[this._fetchKey] const data = nuxtState.fetch[this._fetchKey]
// If fetch error // If fetch error
@ -64,7 +64,17 @@ function createdFullStatic() {
return return
} }
this._hydrated = true 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] const data = this.<%= globals.nuxt %>._pagePayload.fetch[this._fetchKey]
// If fetch error // If fetch error

View File

@ -1,5 +1,5 @@
import Vue from 'vue' import Vue from 'vue'
import { hasFetch, normalizeError, addLifecycleHook, purifyData } from '../utils' import { hasFetch, normalizeError, addLifecycleHook, purifyData, createGetCounter } from '../utils'
async function serverPrefetch() { async function serverPrefetch() {
if (!this._fetchOnServer) { if (!this._fetchOnServer) {
@ -19,14 +19,20 @@ async function serverPrefetch() {
// Define an ssrKey for hydration // 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 // Add data-fetch-key on parent element of Component
const attrs = this.$vnode.data.attrs = this.$vnode.data.attrs || {} const attrs = this.$vnode.data.attrs = this.$vnode.data.attrs || {}
attrs['data-fetch-key'] = this._fetchKey attrs['data-fetch-key'] = this._fetchKey
// Add to ssrContext for window.__NUXT__.fetch // 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 { export default {
@ -41,6 +47,16 @@ export default {
this._fetchOnServer = this.$options.fetchOnServer !== false 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 // Added for remove vue undefined warning while ssr
this.$fetch = () => {} // issue #8043 this.$fetch = () => {} // issue #8043
Vue.util.defineReactive(this, '$fetchState', { Vue.util.defineReactive(this, '$fetchState', {

View File

@ -73,7 +73,11 @@ export default async (ssrContext) => {
// Used for beforeNuxtRender({ Components, nuxtState }) // Used for beforeNuxtRender({ Components, nuxtState })
ssrContext.beforeRenderFns = [] ssrContext.beforeRenderFns = []
// Nuxt object (window.{{globals.context}}, defaults to window.__NUXT__) // 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 // Remove query from url is static target
if (process.static && ssrContext.url) { if (process.static && ssrContext.url) {
ssrContext.url = ssrContext.url.split('?')[0] ssrContext.url = ssrContext.url.split('?')[0]

View File

@ -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 empty () {}
export function globalHandleError (error) { export function globalHandleError (error) {

View File

@ -24,6 +24,9 @@ describe('basic generate', () => {
generate: { generate: {
static: false, static: false,
dir: '.nuxt-generate' dir: '.nuxt-generate'
},
publicRuntimeConfig: {
generated: true
} }
}) })
const nuxt = new Nuxt(config) const nuxt = new Nuxt(config)
@ -142,6 +145,13 @@ describe('basic generate', () => {
expect(html).toContain('<p>Nuxt</p>') expect(html).toContain('<p>Nuxt</p>')
}) })
test('/fetch', async () => {
const window = await generator.nuxt.server.renderAndGetWindow(url('/fetch'))
const html = window.document.body.innerHTML
expect(html).toContain('<code>true</code>')
expect(window.__NUXT__.fetch.custom100.fetched).toBe(true)
})
test('/тест雨 (test non ascii route)', async () => { test('/тест雨 (test non ascii route)', async () => {
const window = await generator.nuxt.server.renderAndGetWindow(url('/тест雨')) const window = await generator.nuxt.server.renderAndGetWindow(url('/тест雨'))
const html = window.document.body.innerHTML const html = window.document.body.innerHTML

View File

@ -76,6 +76,59 @@ describe('basic browser', () => {
expect(await page.$text('pre')).toContain('kevinmarrec') 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 () => { test('ssr: /fetch-root', async () => {
const page = await browser.page(url('/fetch-root')) const page = await browser.page(url('/fetch-root'))
expect(await page.$text('button')).toContain('has fetch') expect(await page.$text('button')).toContain('has fetch')
@ -132,7 +185,7 @@ describe('basic browser', () => {
// Fragments // Fragments
const { data, fetch } = await page.evaluate(() => window.__NUXT__) const { data, fetch } = await page.evaluate(() => window.__NUXT__)
expect(data.length).toBe(1) expect(data.length).toBe(1)
expect(fetch.length).toBe(1) expect(Object.keys(fetch).length).toBe(2)
// asyncData mutations // asyncData mutations
expect(data[0]).toMatchObject({ async: 'data', async2: 'data2' }) expect(data[0]).toMatchObject({ async: 'data', async2: 'data2' })

View File

@ -7,6 +7,7 @@
<button @click="reload"> <button @click="reload">
Reload Reload
</button> </button>
<code>{{ fetched }}</code>
</div> </div>
</template> </template>
@ -18,10 +19,25 @@ const getData = () => fetch(`${baseURL}/test`)
.then(r => r + ` (From ${name})`) .then(r => r + ` (From ${name})`)
export default { export default {
async asyncData () { async asyncData ({ $config }) {
if ($config.generated) { return }
const data = await getData() const data = await getData()
return { data } 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: { methods: {
async update () { async update () {
this.data = await getData() this.data = await getData()

View File

@ -12,6 +12,7 @@
<script> <script>
export default { export default {
fetchKey: 'team',
data () { data () {
return { return {
team: [] team: []

View File

@ -51,7 +51,30 @@
Deprecated fetch Deprecated fetch
</n-link> </n-link>
</li> </li>
<li>
<n-link to="/nested/item">
Nested fetch
</n-link>
</li>
</ul> </ul>
foo-bar-{{ foo }}
<nuxt /> <nuxt />
</div> </div>
</template> </template>
<script>
export default {
name: 'DefaultLayout',
data () {
return {
foo: 'bar'
}
},
async fetch () {
await new Promise((resolve) => {
this.foo = 'baz'
resolve()
})
}
}
</script>

22
test/fixtures/fetch/pages/nested.vue vendored Normal file
View File

@ -0,0 +1,22 @@
<template>
<div>
fizz-{{ foo }}
<NuxtChild />
</div>
</template>
<script>
export default {
data () {
return {
foo: 'bar'
}
},
async fetch () {
await new Promise((resolve) => {
this.foo = 'buzz'
resolve()
})
}
}
</script>

View File

@ -0,0 +1,13 @@
<template>
<team />
</template>
<script>
import Team from '@/components/Team.vue'
export default {
components: {
Team
}
}
</script>

View File

@ -0,0 +1,26 @@
<template>
<div>
<button @click="$fetch">
fetch {{ foo }}
</button>
<NuxtChild />
</div>
</template>
<script>
export default {
fetchKey (getCounter) {
return 'ie' + getCounter('ie')
},
data () {
return {
foo: null
}
},
async fetch () {
await new Promise(resolve => setTimeout(resolve, 100))
this.foo = this.$fetch ? 'has fetch' : 'hasn\'t fetch'
}
}
</script>