mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-22 13:45:18 +00:00
refactor(nuxt3): cleanup data fetching and improved useAsyncData
(#699)
This commit is contained in:
parent
e614328406
commit
2bf645bd73
@ -24,7 +24,7 @@ Under the hood, `defer: false` uses `<Suspense>` to block the loading of the rou
|
|||||||
|
|
||||||
```vue
|
```vue
|
||||||
<script setup>
|
<script setup>
|
||||||
const { data } = await asyncData('time', () => $fetch('/api/count'))
|
const { data } = await useAsyncData('time', () => $fetch('/api/count'))
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
Page visits: {{ data.count }}
|
{{ data }}
|
||||||
|
<button :disabled="pending" @click="refresh">
|
||||||
|
Refrash Data
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
const { data } = await asyncData('time', () => $fetch('/api/count'))
|
const { data, refresh, pending } = await useAsyncData('/api/hello', () => $fetch('/api/hello'))
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,3 +0,0 @@
|
|||||||
let ctr = 0
|
|
||||||
|
|
||||||
export default () => ({ count: ++ctr })
|
|
1
examples/async-data/server/api/hello.js
Normal file
1
examples/async-data/server/api/hello.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
export default () => `Hello world! (Generated at ${new Date().toGMTString()})`
|
@ -35,7 +35,7 @@ import { ContentLoader } from 'vue-content-loader'
|
|||||||
export default defineNuxtComponent({
|
export default defineNuxtComponent({
|
||||||
components: { ContentLoader },
|
components: { ContentLoader },
|
||||||
setup () {
|
setup () {
|
||||||
const { data, pending } = asyncData(
|
const { data, pending } = useAsyncData(
|
||||||
'time',
|
'time',
|
||||||
() =>
|
() =>
|
||||||
new Promise(resolve =>
|
new Promise(resolve =>
|
||||||
|
@ -8,10 +8,6 @@ export * from '@vue/composition-api'
|
|||||||
const mock = () => () => { throw new Error('not implemented') }
|
const mock = () => () => { throw new Error('not implemented') }
|
||||||
|
|
||||||
export const useAsyncData = mock()
|
export const useAsyncData = mock()
|
||||||
export const asyncData = mock()
|
|
||||||
export const useSSRRef = mock()
|
|
||||||
export const useData = mock()
|
|
||||||
export const useGlobalData = mock()
|
|
||||||
export const useHydration = mock()
|
export const useHydration = mock()
|
||||||
|
|
||||||
// Runtime config helper
|
// Runtime config helper
|
||||||
|
@ -317,7 +317,7 @@ export default {
|
|||||||
/** Set to false to disable the Nuxt `validate()` hook */
|
/** Set to false to disable the Nuxt `validate()` hook */
|
||||||
validate: true,
|
validate: true,
|
||||||
/** Set to false to disable the Nuxt `asyncData()` hook */
|
/** Set to false to disable the Nuxt `asyncData()` hook */
|
||||||
asyncData: true,
|
useAsyncData: true,
|
||||||
/** Set to false to disable the Nuxt `fetch()` hook */
|
/** Set to false to disable the Nuxt `fetch()` hook */
|
||||||
fetch: true,
|
fetch: true,
|
||||||
/** Set to false to disable `$nuxt.isOnline` */
|
/** Set to false to disable `$nuxt.isOnline` */
|
||||||
|
@ -60,7 +60,7 @@ export default {
|
|||||||
* @example
|
* @example
|
||||||
* ```js
|
* ```js
|
||||||
* export default {
|
* export default {
|
||||||
* async asyncData ({ params, error, payload }) {
|
* async useAsyncData ({ params, error, payload }) {
|
||||||
* if (payload) return { user: payload }
|
* if (payload) return { user: payload }
|
||||||
* else return { user: await backend.fetchUser(params.id) }
|
* else return { user: await backend.fetchUser(params.id) }
|
||||||
* }
|
* }
|
||||||
|
@ -1,119 +1,115 @@
|
|||||||
import { onBeforeMount, onUnmounted, ref, unref } from 'vue'
|
import { onBeforeMount, onUnmounted, ref } from 'vue'
|
||||||
import type { UnwrapRef, Ref } from 'vue'
|
import type { Ref } from 'vue'
|
||||||
|
|
||||||
import { ensureReactive, useGlobalData } from './data'
|
|
||||||
import { NuxtApp, useNuxtApp } from '#app'
|
import { NuxtApp, useNuxtApp } from '#app'
|
||||||
|
|
||||||
export type AsyncDataFn<T> = (ctx?: NuxtApp) => Promise<T>
|
export interface AsyncDataOptions<T> {
|
||||||
|
|
||||||
export interface AsyncDataOptions {
|
|
||||||
server?: boolean
|
server?: boolean
|
||||||
defer?: boolean
|
defer?: boolean
|
||||||
|
default?: () => T
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AsyncDataState<T> {
|
export interface _AsyncData<T> {
|
||||||
data: UnwrapRef<T>
|
data: Ref<T>
|
||||||
pending: Ref<boolean>
|
pending: Ref<boolean>
|
||||||
fetch: (force?: boolean) => Promise<UnwrapRef<T>>
|
refresh: (force?: boolean) => Promise<void>
|
||||||
error?: any
|
error?: any
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AsyncDataResult<T> = AsyncDataState<T> & Promise<AsyncDataState<T>>
|
export type AsyncData<T> = _AsyncData<T> & Promise<_AsyncData<T>>
|
||||||
|
|
||||||
export function useAsyncData (defaults?: AsyncDataOptions) {
|
const getDefault = () => null
|
||||||
const nuxt = useNuxtApp()
|
|
||||||
const onBeforeMountCbs: Array<() => void> = []
|
|
||||||
|
|
||||||
if (process.client) {
|
export function useAsyncData<T extends Record<string, any>> (key: string, handler: (ctx?: NuxtApp) => Promise<T>, options: AsyncDataOptions<T> = {}): AsyncData<T> {
|
||||||
onBeforeMount(() => {
|
// Validate arguments
|
||||||
onBeforeMountCbs.forEach((cb) => { cb() })
|
if (typeof key !== 'string') {
|
||||||
onBeforeMountCbs.splice(0, onBeforeMountCbs.length)
|
throw new TypeError('asyncData key must be a string')
|
||||||
})
|
}
|
||||||
|
if (typeof handler !== 'function') {
|
||||||
onUnmounted(() => onBeforeMountCbs.splice(0, onBeforeMountCbs.length))
|
throw new TypeError('asyncData handler must be a function')
|
||||||
}
|
}
|
||||||
|
|
||||||
nuxt._asyncDataPromises = nuxt._asyncDataPromises || {}
|
// Apply defaults
|
||||||
|
options = { server: true, defer: false, default: getDefault, ...options }
|
||||||
|
|
||||||
return function asyncData<T extends Record<string, any>> (
|
// Setup nuxt instance payload
|
||||||
key: string,
|
const nuxt = useNuxtApp()
|
||||||
handler: AsyncDataFn<T>,
|
|
||||||
options: AsyncDataOptions = {}
|
|
||||||
): AsyncDataResult<T> {
|
|
||||||
if (typeof handler !== 'function') {
|
|
||||||
throw new TypeError('asyncData handler must be a function')
|
|
||||||
}
|
|
||||||
options = {
|
|
||||||
server: true,
|
|
||||||
defer: false,
|
|
||||||
...defaults,
|
|
||||||
...options
|
|
||||||
}
|
|
||||||
|
|
||||||
const globalData = useGlobalData(nuxt)
|
// Setup hook callbacks once per instance
|
||||||
|
const instance = getCurrentInstance()
|
||||||
const state = {
|
if (!instance._nuxtOnBeforeMountCbs) {
|
||||||
data: ensureReactive(globalData, key) as UnwrapRef<T>,
|
const cbs = instance._nuxtOnBeforeMountCbs = []
|
||||||
pending: ref(true)
|
if (instance && process.client) {
|
||||||
} as AsyncDataState<T>
|
onBeforeMount(() => {
|
||||||
|
cbs.forEach((cb) => { cb() })
|
||||||
const fetch = (force?: boolean): Promise<UnwrapRef<T>> => {
|
cbs.splice(0, cbs.length)
|
||||||
if (nuxt._asyncDataPromises[key] && !force) {
|
|
||||||
return nuxt._asyncDataPromises[key]
|
|
||||||
}
|
|
||||||
state.pending.value = true
|
|
||||||
nuxt._asyncDataPromises[key] = Promise.resolve(handler(nuxt)).then((result) => {
|
|
||||||
for (const _key in result) {
|
|
||||||
// @ts-expect-error
|
|
||||||
state.data[_key] = unref(result[_key])
|
|
||||||
}
|
|
||||||
return state.data
|
|
||||||
}).finally(() => {
|
|
||||||
state.pending.value = false
|
|
||||||
nuxt._asyncDataPromises[key] = null
|
|
||||||
})
|
})
|
||||||
|
onUnmounted(() => cbs.splice(0, cbs.length))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const asyncData = {
|
||||||
|
data: ref(nuxt.payload.data[key] ?? options.default()),
|
||||||
|
pending: ref(true),
|
||||||
|
error: ref(null)
|
||||||
|
} as AsyncData<T>
|
||||||
|
|
||||||
|
asyncData.refresh = (force?: boolean) => {
|
||||||
|
// Avoid fetching same key more than once at a time
|
||||||
|
if (nuxt._asyncDataPromises[key] && !force) {
|
||||||
return nuxt._asyncDataPromises[key]
|
return nuxt._asyncDataPromises[key]
|
||||||
}
|
}
|
||||||
|
asyncData.pending.value = true
|
||||||
const fetchOnServer = options.server !== false
|
// TODO: Cancel previus promise
|
||||||
const clientOnly = options.server === false
|
// TODO: Handle immediate errors
|
||||||
|
nuxt._asyncDataPromises[key] = Promise.resolve(handler(nuxt))
|
||||||
// Server side
|
.then((result) => {
|
||||||
if (process.server && fetchOnServer) {
|
asyncData.data.value = result
|
||||||
fetch()
|
asyncData.error.value = null
|
||||||
}
|
})
|
||||||
|
.catch((error: any) => {
|
||||||
// Client side
|
asyncData.error.value = error
|
||||||
if (process.client) {
|
asyncData.data.value = options.default()
|
||||||
// 1. Hydration (server: true): no fetch
|
})
|
||||||
if (nuxt.isHydrating && fetchOnServer) {
|
.finally(() => {
|
||||||
state.pending.value = false
|
asyncData.pending.value = false
|
||||||
}
|
nuxt.payload.data[key] = asyncData.data.value
|
||||||
// 2. Initial load (server: false): fetch on mounted
|
delete nuxt._asyncDataPromises[key]
|
||||||
if (nuxt.isHydrating && clientOnly) {
|
})
|
||||||
// Fetch on mounted (initial load or deferred fetch)
|
return nuxt._asyncDataPromises[key]
|
||||||
onBeforeMountCbs.push(fetch)
|
|
||||||
} else if (!nuxt.isHydrating) { // Navigation
|
|
||||||
if (options.defer) {
|
|
||||||
// 3. Navigation (defer: true): fetch on mounted
|
|
||||||
onBeforeMountCbs.push(fetch)
|
|
||||||
} else {
|
|
||||||
// 4. Navigation (defer: false): await fetch
|
|
||||||
fetch()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = Promise.resolve(nuxt._asyncDataPromises[key]).then(() => state) as AsyncDataResult<T>
|
|
||||||
res.data = state.data
|
|
||||||
res.pending = state.pending
|
|
||||||
res.fetch = fetch
|
|
||||||
return res
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
export function asyncData<T extends Record<string, any>> (
|
const fetchOnServer = options.server !== false
|
||||||
key: string, handler: AsyncDataFn<T>, options?: AsyncDataOptions
|
const clientOnly = options.server === false
|
||||||
): AsyncDataResult<T> {
|
|
||||||
return useAsyncData()(key, handler, options)
|
// Server side
|
||||||
|
if (process.server && fetchOnServer) {
|
||||||
|
asyncData.refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client side
|
||||||
|
if (process.client) {
|
||||||
|
// 1. Hydration (server: true): no fetch
|
||||||
|
if (nuxt.isHydrating && fetchOnServer) {
|
||||||
|
asyncData.pending.value = false
|
||||||
|
}
|
||||||
|
// 2. Initial load (server: false): fetch on mounted
|
||||||
|
if (nuxt.isHydrating && clientOnly) {
|
||||||
|
// Fetch on mounted (initial load or deferred fetch)
|
||||||
|
instance._nuxtOnBeforeMountCbs.push(asyncData.refresh)
|
||||||
|
} else if (!nuxt.isHydrating) { // Navigation
|
||||||
|
if (options.defer) {
|
||||||
|
// 3. Navigation (defer: true): fetch on mounted
|
||||||
|
instance._nuxtOnBeforeMountCbs.push(asyncData.refresh)
|
||||||
|
} else {
|
||||||
|
// 4. Navigation (defer: false): await fetch
|
||||||
|
asyncData.refresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow directly awaiting on asyncData
|
||||||
|
const asyncDataPromise = Promise.resolve(nuxt._asyncDataPromises[key]).then(() => asyncData) as AsyncData<T>
|
||||||
|
Object.assign(asyncDataPromise, asyncData)
|
||||||
|
|
||||||
|
return asyncDataPromise as AsyncData<T>
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@ import type { DefineComponent } from 'vue'
|
|||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import type { LegacyContext } from '../legacy'
|
import type { LegacyContext } from '../legacy'
|
||||||
import { useNuxtApp } from '../nuxt'
|
import { useNuxtApp } from '../nuxt'
|
||||||
import { asyncData } from './asyncData'
|
import { useAsyncData } from './asyncData'
|
||||||
|
|
||||||
export const NuxtComponentIndicator = '__nuxt_component'
|
export const NuxtComponentIndicator = '__nuxt_component'
|
||||||
|
|
||||||
@ -14,7 +14,7 @@ async function runLegacyAsyncData (res: Record<string, any> | Promise<Record<str
|
|||||||
const vm = getCurrentInstance()
|
const vm = getCurrentInstance()
|
||||||
const { fetchKey } = vm.proxy.$options
|
const { fetchKey } = vm.proxy.$options
|
||||||
const key = typeof fetchKey === 'function' ? fetchKey(() => '') : fetchKey || route.fullPath
|
const key = typeof fetchKey === 'function' ? fetchKey(() => '') : fetchKey || route.fullPath
|
||||||
const { data } = await asyncData(`options:asyncdata:${key}`, () => fn(nuxt._legacyContext))
|
const { data } = await useAsyncData(`options:asyncdata:${key}`, () => fn(nuxt._legacyContext))
|
||||||
Object.assign(await res, toRefs(data))
|
Object.assign(await res, toRefs(data))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,65 +0,0 @@
|
|||||||
import { getCurrentInstance, isReactive, reactive } from 'vue'
|
|
||||||
import type { UnwrapRef } from 'vue'
|
|
||||||
import { useNuxtApp } from '#app'
|
|
||||||
|
|
||||||
export function ensureReactive<
|
|
||||||
T extends Record<string, any>,
|
|
||||||
K extends keyof T
|
|
||||||
> (data: T, key: K): UnwrapRef<T[K]> {
|
|
||||||
if (!isReactive(data[key])) {
|
|
||||||
data[key] = reactive(data[key] || ({} as T[K]))
|
|
||||||
}
|
|
||||||
return data[key]
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a unique string suitable for syncing data between server and client.
|
|
||||||
*
|
|
||||||
* @param nuxt (optional) A Nuxt instance
|
|
||||||
* @param vm (optional) A Vue component - by default it will use the current instance
|
|
||||||
*/
|
|
||||||
export function useSSRRef (nuxt = useNuxtApp(), vm = getCurrentInstance()): string {
|
|
||||||
if (!vm) {
|
|
||||||
throw new Error('This must be called within a setup function.')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Server
|
|
||||||
if (process.server) {
|
|
||||||
if (!vm.attrs['data-ssr-ref']) {
|
|
||||||
nuxt._refCtr = nuxt._refCtr || 1
|
|
||||||
vm.attrs['data-ssr-ref'] = String(nuxt._refCtr++)
|
|
||||||
}
|
|
||||||
return vm.attrs['data-ssr-ref'] as string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Client
|
|
||||||
/* TODO: unique value for multiple calls */
|
|
||||||
return vm.vnode.el?.dataset?.ssrRef || String(Math.random())
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Allows accessing reactive data that can be synced between server and client.
|
|
||||||
*
|
|
||||||
* @param nuxt (optional) A Nuxt instance
|
|
||||||
* @param vm (optional) A Vue component - by default it will use the current instance
|
|
||||||
*/
|
|
||||||
export function useData<T = Record<string, any>> (
|
|
||||||
nuxt = useNuxtApp(),
|
|
||||||
vm = getCurrentInstance()
|
|
||||||
): UnwrapRef<T> {
|
|
||||||
const ssrRef = useSSRRef(nuxt, vm)
|
|
||||||
|
|
||||||
nuxt.payload.data = nuxt.payload.data || {}
|
|
||||||
|
|
||||||
return ensureReactive(nuxt.payload.data, ssrRef)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Allows accessing reactive global data that can be synced between server and client.
|
|
||||||
*
|
|
||||||
* @param nuxt - (optional) A Nuxt instance
|
|
||||||
*/
|
|
||||||
export function useGlobalData (nuxt = useNuxtApp()): Record<string, any> {
|
|
||||||
nuxt.payload.data = nuxt.payload.data || {}
|
|
||||||
return nuxt.payload.data
|
|
||||||
}
|
|
@ -1,4 +1,3 @@
|
|||||||
export { defineNuxtComponent } from './component'
|
export { defineNuxtComponent } from './component'
|
||||||
export { useAsyncData, asyncData } from './asyncData'
|
export { useAsyncData } from './asyncData'
|
||||||
export { useData } from './data'
|
|
||||||
export { useHydration } from './hydrate'
|
export { useHydration } from './hydrate'
|
||||||
|
@ -70,8 +70,9 @@ export function createNuxtApp (options: CreateOptions) {
|
|||||||
const nuxt: NuxtApp = {
|
const nuxt: NuxtApp = {
|
||||||
provide: undefined,
|
provide: undefined,
|
||||||
globalName: 'nuxt',
|
globalName: 'nuxt',
|
||||||
payload: {},
|
payload: reactive(process.server ? { serverRendered: true, data: {} } : (window.__NUXT__ || { data: {} })),
|
||||||
isHydrating: process.client,
|
isHydrating: process.client,
|
||||||
|
_asyncDataPromises: {},
|
||||||
...options
|
...options
|
||||||
} as any as NuxtApp
|
} as any as NuxtApp
|
||||||
|
|
||||||
@ -95,20 +96,11 @@ export function createNuxtApp (options: CreateOptions) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (process.server) {
|
if (process.server) {
|
||||||
nuxt.payload = {
|
|
||||||
serverRendered: true
|
|
||||||
}
|
|
||||||
|
|
||||||
nuxt.ssrContext = nuxt.ssrContext || {}
|
|
||||||
|
|
||||||
// Expose to server renderer to create window.__NUXT__
|
// Expose to server renderer to create window.__NUXT__
|
||||||
|
nuxt.ssrContext = nuxt.ssrContext || {}
|
||||||
nuxt.ssrContext.payload = nuxt.payload
|
nuxt.ssrContext.payload = nuxt.payload
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.client) {
|
|
||||||
nuxt.payload = window.__NUXT__ || {}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Expose runtime config
|
// Expose runtime config
|
||||||
if (process.server) {
|
if (process.server) {
|
||||||
nuxt.provide('config', options.ssrContext.runtimeConfig.private)
|
nuxt.provide('config', options.ssrContext.runtimeConfig.private)
|
||||||
|
3
packages/nuxt3/src/app/types/shims.d.ts
vendored
3
packages/nuxt3/src/app/types/shims.d.ts
vendored
@ -33,4 +33,7 @@ declare module '@vue/runtime-core' {
|
|||||||
interface App<HostElement> {
|
interface App<HostElement> {
|
||||||
$nuxt: NuxtApp
|
$nuxt: NuxtApp
|
||||||
}
|
}
|
||||||
|
interface ComponentInternalInstance {
|
||||||
|
_nuxtOnBeforeMountCbs: Function[]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
const identifiers = {
|
const identifiers = {
|
||||||
'#app': [
|
'#app': [
|
||||||
'useAsyncData',
|
'useAsyncData',
|
||||||
'asyncData',
|
|
||||||
'defineNuxtComponent',
|
'defineNuxtComponent',
|
||||||
'useNuxtApp',
|
'useNuxtApp',
|
||||||
'defineNuxtPlugin',
|
'defineNuxtPlugin',
|
||||||
|
Loading…
Reference in New Issue
Block a user