mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-11 08:33:53 +00:00
feat(nuxt): add dedicated #teleports
element for ssr teleports (#25043)
This commit is contained in:
parent
27f9c55fc2
commit
536998727a
@ -29,7 +29,7 @@ import unheadPlugins from '#internal/unhead-plugins.mjs'
|
|||||||
// eslint-disable-next-line import/no-restricted-paths
|
// eslint-disable-next-line import/no-restricted-paths
|
||||||
import type { NuxtPayload, NuxtSSRContext } from '#app'
|
import type { NuxtPayload, NuxtSSRContext } from '#app'
|
||||||
// @ts-expect-error virtual file
|
// @ts-expect-error virtual file
|
||||||
import { appHead, appRootId, appRootTag } from '#internal/nuxt.config.mjs'
|
import { appHead, appRootId, appRootTag, appTeleportId, appTeleportTag } from '#internal/nuxt.config.mjs'
|
||||||
// @ts-expect-error virtual file
|
// @ts-expect-error virtual file
|
||||||
import { buildAssetsURL, publicAssetsURL } from '#paths'
|
import { buildAssetsURL, publicAssetsURL } from '#paths'
|
||||||
|
|
||||||
@ -137,7 +137,7 @@ const getSSRRenderer = lazyCachedFunction(async () => {
|
|||||||
if (import.meta.dev && process.env.NUXT_VITE_NODE_OPTIONS) {
|
if (import.meta.dev && process.env.NUXT_VITE_NODE_OPTIONS) {
|
||||||
renderer.rendererContext.updateManifest(await getClientManifest())
|
renderer.rendererContext.updateManifest(await getClientManifest())
|
||||||
}
|
}
|
||||||
return `<${appRootTag}${appRootId ? ` id="${appRootId}"` : ''}>${html}</${appRootTag}>`
|
return APP_ROOT_OPEN_TAG + html + APP_ROOT_CLOSE_TAG
|
||||||
}
|
}
|
||||||
|
|
||||||
return renderer
|
return renderer
|
||||||
@ -149,10 +149,11 @@ const getSPARenderer = lazyCachedFunction(async () => {
|
|||||||
|
|
||||||
// @ts-expect-error virtual file
|
// @ts-expect-error virtual file
|
||||||
const spaTemplate = await import('#spa-template').then(r => r.template).catch(() => '')
|
const spaTemplate = await import('#spa-template').then(r => r.template).catch(() => '')
|
||||||
|
.then(r => APP_ROOT_OPEN_TAG + r + APP_ROOT_CLOSE_TAG)
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
manifest,
|
manifest,
|
||||||
renderToString: () => `<${appRootTag}${appRootId ? ` id="${appRootId}"` : ''}>${spaTemplate}</${appRootTag}>`,
|
renderToString: () => spaTemplate,
|
||||||
buildAssetsURL
|
buildAssetsURL
|
||||||
}
|
}
|
||||||
// Create SPA renderer and cache the result for all requests
|
// Create SPA renderer and cache the result for all requests
|
||||||
@ -230,8 +231,15 @@ async function getIslandContext (event: H3Event): Promise<NuxtIslandContext> {
|
|||||||
return ctx
|
return ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const HAS_APP_TELEPORTS = !!(appTeleportTag && appTeleportId)
|
||||||
|
const APP_TELEPORT_OPEN_TAG = HAS_APP_TELEPORTS ? `<${appTeleportTag} id="${appTeleportId}">` : ''
|
||||||
|
const APP_TELEPORT_CLOSE_TAG = HAS_APP_TELEPORTS ? `</${appTeleportTag}>` : ''
|
||||||
|
|
||||||
|
const APP_ROOT_OPEN_TAG = `<${appRootTag}${appRootId ? ` id="${appRootId}"` : ''}>`
|
||||||
|
const APP_ROOT_CLOSE_TAG = `</${appRootTag}>`
|
||||||
|
|
||||||
const PAYLOAD_URL_RE = process.env.NUXT_JSON_PAYLOADS ? /\/_payload.json(\?.*)?$/ : /\/_payload.js(\?.*)?$/
|
const PAYLOAD_URL_RE = process.env.NUXT_JSON_PAYLOADS ? /\/_payload.json(\?.*)?$/ : /\/_payload.js(\?.*)?$/
|
||||||
const ROOT_NODE_REGEX = new RegExp(`^<${appRootTag}${appRootId ? ` id="${appRootId}"` : ''}>([\\s\\S]*)</${appRootTag}>$`)
|
const ROOT_NODE_REGEX = new RegExp(`^${APP_ROOT_OPEN_TAG}([\\s\\S]*)${APP_ROOT_CLOSE_TAG}$`)
|
||||||
|
|
||||||
const PRERENDER_NO_SSR_ROUTES = new Set(['/index.html', '/200.html', '/404.html'])
|
const PRERENDER_NO_SSR_ROUTES = new Set(['/index.html', '/200.html', '/404.html'])
|
||||||
|
|
||||||
@ -459,7 +467,10 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
|
|||||||
head: normalizeChunks([headTags, ssrContext.styles]),
|
head: normalizeChunks([headTags, ssrContext.styles]),
|
||||||
bodyAttrs: bodyAttrs ? [bodyAttrs] : [],
|
bodyAttrs: bodyAttrs ? [bodyAttrs] : [],
|
||||||
bodyPrepend: normalizeChunks([bodyTagsOpen, ssrContext.teleports?.body]),
|
bodyPrepend: normalizeChunks([bodyTagsOpen, ssrContext.teleports?.body]),
|
||||||
body: [process.env.NUXT_COMPONENT_ISLANDS ? replaceIslandTeleports(ssrContext, _rendered.html) : _rendered.html],
|
body: [
|
||||||
|
process.env.NUXT_COMPONENT_ISLANDS ? replaceIslandTeleports(ssrContext, _rendered.html) : _rendered.html,
|
||||||
|
APP_TELEPORT_OPEN_TAG + (HAS_APP_TELEPORTS ? joinTags([ssrContext.teleports?.[`#${appTeleportId}`]]) : '') + APP_TELEPORT_CLOSE_TAG
|
||||||
|
],
|
||||||
bodyAppend: [bodyTags]
|
bodyAppend: [bodyTags]
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -534,7 +545,7 @@ function normalizeChunks (chunks: (string | undefined)[]) {
|
|||||||
return chunks.filter(Boolean).map(i => i!.trim())
|
return chunks.filter(Boolean).map(i => i!.trim())
|
||||||
}
|
}
|
||||||
|
|
||||||
function joinTags (tags: string[]) {
|
function joinTags (tags: Array<string | undefined>) {
|
||||||
return tags.join('')
|
return tags.join('')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -184,6 +184,21 @@ export default defineUntypedSchema({
|
|||||||
*/
|
*/
|
||||||
rootTag: {
|
rootTag: {
|
||||||
$resolve: val => val || 'div'
|
$resolve: val => val || 'div'
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Customize Nuxt root element tag.
|
||||||
|
*/
|
||||||
|
teleportTag: {
|
||||||
|
$resolve: val => val || 'div'
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Customize Nuxt Teleport element id.
|
||||||
|
* @type {string | false}
|
||||||
|
*/
|
||||||
|
teleportId: {
|
||||||
|
$resolve: val => val === false ? false : (val || 'teleports')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -2470,6 +2470,23 @@ describe('keepalive', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('teleports', () => {
|
||||||
|
it('should append teleports to body', async () => {
|
||||||
|
const html = await $fetch('/teleport')
|
||||||
|
|
||||||
|
// Teleport is prepended to body, before the __nuxt div
|
||||||
|
expect(html).toContain('<div>Teleport</div><!--teleport anchor--><div id="__nuxt">')
|
||||||
|
// Teleport start and end tag are rendered as expected
|
||||||
|
expect(html).toContain('<div><!--teleport start--><!--teleport end--><h1>Normal content</h1></div>')
|
||||||
|
})
|
||||||
|
it('should render teleports to app teleports element', async () => {
|
||||||
|
const html = await $fetch('/nuxt-teleport')
|
||||||
|
|
||||||
|
// Teleport is appended to body, after the __nuxt div
|
||||||
|
expect(html).toContain('<div><!--teleport start--><!--teleport end--><h1>Normal content</h1></div></div></div><span id="nuxt-teleport"><div>Nuxt Teleport</div><!--teleport anchor--></span><script')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('Node.js compatibility for client-side', () => {
|
describe('Node.js compatibility for client-side', () => {
|
||||||
it('should work', async () => {
|
it('should work', async () => {
|
||||||
const { page } = await renderPage('/node-compat')
|
const { page } = await renderPage('/node-compat')
|
||||||
|
@ -32,7 +32,7 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
|
|||||||
const serverDir = join(rootDir, '.output/server')
|
const serverDir = join(rootDir, '.output/server')
|
||||||
|
|
||||||
const serverStats = await analyzeSizes(['**/*.mjs', '!node_modules'], serverDir)
|
const serverStats = await analyzeSizes(['**/*.mjs', '!node_modules'], serverDir)
|
||||||
expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot('"204k"')
|
expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot('"205k"')
|
||||||
|
|
||||||
const modules = await analyzeSizes('node_modules/**/*', serverDir)
|
const modules = await analyzeSizes('node_modules/**/*', serverDir)
|
||||||
expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot('"1335k"')
|
expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot('"1335k"')
|
||||||
@ -72,7 +72,7 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
|
|||||||
const serverDir = join(rootDir, '.output-inline/server')
|
const serverDir = join(rootDir, '.output-inline/server')
|
||||||
|
|
||||||
const serverStats = await analyzeSizes(['**/*.mjs', '!node_modules'], serverDir)
|
const serverStats = await analyzeSizes(['**/*.mjs', '!node_modules'], serverDir)
|
||||||
expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot('"523k"')
|
expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot('"524k"')
|
||||||
|
|
||||||
const modules = await analyzeSizes('node_modules/**/*', serverDir)
|
const modules = await analyzeSizes('node_modules/**/*', serverDir)
|
||||||
expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot('"78.0k"')
|
expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot('"78.0k"')
|
||||||
|
2
test/fixtures/basic/nuxt.config.ts
vendored
2
test/fixtures/basic/nuxt.config.ts
vendored
@ -14,6 +14,8 @@ export default defineNuxtConfig({
|
|||||||
app: {
|
app: {
|
||||||
pageTransition: true,
|
pageTransition: true,
|
||||||
layoutTransition: true,
|
layoutTransition: true,
|
||||||
|
teleportId: 'nuxt-teleport',
|
||||||
|
teleportTag: 'span',
|
||||||
head: {
|
head: {
|
||||||
charset: 'utf-8',
|
charset: 'utf-8',
|
||||||
link: [undefined],
|
link: [undefined],
|
||||||
|
8
test/fixtures/basic/pages/nuxt-teleport.vue
vendored
Normal file
8
test/fixtures/basic/pages/nuxt-teleport.vue
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<teleport to="#nuxt-teleport">
|
||||||
|
<div>Nuxt Teleport</div>
|
||||||
|
</teleport>
|
||||||
|
<h1>Normal content</h1>
|
||||||
|
</div>
|
||||||
|
</template>
|
8
test/fixtures/basic/pages/teleport.vue
vendored
Normal file
8
test/fixtures/basic/pages/teleport.vue
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<teleport to="body">
|
||||||
|
<div>Teleport</div>
|
||||||
|
</teleport>
|
||||||
|
<h1>Normal content</h1>
|
||||||
|
</div>
|
||||||
|
</template>
|
Loading…
Reference in New Issue
Block a user