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
next: NextFunction
beforeRenderFns: Array<() => any>
fetchCounters: Record<string, number>
nuxt: {
layout: string
data: Array<Record<string, any>>

View File

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

View File

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

View File

@ -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

View File

@ -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', {

View File

@ -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]

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 globalHandleError (error) {

View File

@ -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('<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 () => {
const window = await generator.nuxt.server.renderAndGetWindow(url('/тест雨'))
const html = window.document.body.innerHTML

View File

@ -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' })

View File

@ -7,6 +7,7 @@
<button @click="reload">
Reload
</button>
<code>{{ fetched }}</code>
</div>
</template>
@ -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()

View File

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

View File

@ -51,7 +51,30 @@
Deprecated fetch
</n-link>
</li>
<li>
<n-link to="/nested/item">
Nested fetch
</n-link>
</li>
</ul>
foo-bar-{{ foo }}
<nuxt />
</div>
</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>