mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-25 07:05:11 +00:00
feat(nuxt): server-only pages (#24954)
This commit is contained in:
parent
c05239901a
commit
196223c0fb
@ -357,13 +357,21 @@ function navigate(){
|
|||||||
</script>
|
</script>
|
||||||
```
|
```
|
||||||
|
|
||||||
## Custom routing
|
## Server-Only Pages
|
||||||
|
|
||||||
|
You can define a page as [server only](/docs/guide/directory-structure/components#server-components) by giving it a `.server.vue` suffix. While you will be able to navigate to the page using client-side navigation, controlled by `vue-router`, it will be rendered with a server component automatically, meaning the code required to render the page will not be in your client-side bundle.
|
||||||
|
|
||||||
|
::note
|
||||||
|
You will also need to enable `experimental.componentIslands` in order to make this possible.
|
||||||
|
::
|
||||||
|
|
||||||
|
## Custom Routing
|
||||||
|
|
||||||
As your app gets bigger and more complex, your routing might require more flexibility. For this reason, Nuxt directly exposes the router, routes and router options for customization in different ways.
|
As your app gets bigger and more complex, your routing might require more flexibility. For this reason, Nuxt directly exposes the router, routes and router options for customization in different ways.
|
||||||
|
|
||||||
:read-more{to="/docs/guide/going-further/custom-routing"}
|
:read-more{to="/docs/guide/going-further/custom-routing"}
|
||||||
|
|
||||||
## Multiple pages directories
|
## Multiple Pages Directories
|
||||||
|
|
||||||
By default, all your pages should be in one `pages` directory at the root of your project.
|
By default, all your pages should be in one `pages` directory at the root of your project.
|
||||||
|
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import { defineComponent, h, ref } from 'vue'
|
import { defineComponent, h, ref } from 'vue'
|
||||||
import NuxtIsland from '#app/components/nuxt-island'
|
import NuxtIsland from '#app/components/nuxt-island'
|
||||||
|
import { useRoute } from '#app/composables/router'
|
||||||
|
import { isPrerendered } from '#app/composables/payload'
|
||||||
|
|
||||||
/*@__NO_SIDE_EFFECTS__*/
|
/*@__NO_SIDE_EFFECTS__*/
|
||||||
export const createServerComponent = (name: string) => {
|
export const createServerComponent = (name: string) => {
|
||||||
@ -25,3 +27,33 @@ export const createServerComponent = (name: string) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*@__NO_SIDE_EFFECTS__*/
|
||||||
|
export const createIslandPage = (name: string) => {
|
||||||
|
return defineComponent({
|
||||||
|
name,
|
||||||
|
inheritAttrs: false,
|
||||||
|
props: { lazy: Boolean },
|
||||||
|
async setup (props, { slots, expose }) {
|
||||||
|
const islandRef = ref<null | typeof NuxtIsland>(null)
|
||||||
|
|
||||||
|
expose({
|
||||||
|
refresh: () => islandRef.value?.refresh()
|
||||||
|
})
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const path = await isPrerendered(route.path) ? route.path : route.fullPath.replace(/#.*$/, '')
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
return h('div', [
|
||||||
|
h(NuxtIsland, {
|
||||||
|
name: `page:${name}`,
|
||||||
|
lazy: props.lazy,
|
||||||
|
ref: islandRef,
|
||||||
|
context: { url: path }
|
||||||
|
}, slots)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@ -72,12 +72,17 @@ export const componentsIslandsTemplate: NuxtTemplate = {
|
|||||||
// components.islands.mjs'
|
// components.islands.mjs'
|
||||||
getContents ({ app }) {
|
getContents ({ app }) {
|
||||||
const components = app.components
|
const components = app.components
|
||||||
|
const pages = app.pages
|
||||||
const islands = components.filter(component =>
|
const islands = components.filter(component =>
|
||||||
component.island ||
|
component.island ||
|
||||||
// .server components without a corresponding .client component will need to be rendered as an island
|
// .server components without a corresponding .client component will need to be rendered as an island
|
||||||
(component.mode === 'server' && !components.some(c => c.pascalName === component.pascalName && c.mode === 'client'))
|
(component.mode === 'server' && !components.some(c => c.pascalName === component.pascalName && c.mode === 'client'))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const pageExports = pages?.filter(p => (p.mode === 'server' && p.file && p.name)).map((p) => {
|
||||||
|
return `"page:${p.name}": defineAsyncComponent(${genDynamicImport(p.file!)}.then(c => c.default || c))`
|
||||||
|
}) || []
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'import { defineAsyncComponent } from \'vue\'',
|
'import { defineAsyncComponent } from \'vue\'',
|
||||||
'export const islandComponents = import.meta.client ? {} : {',
|
'export const islandComponents = import.meta.client ? {} : {',
|
||||||
@ -87,7 +92,7 @@ export const componentsIslandsTemplate: NuxtTemplate = {
|
|||||||
const comment = createImportMagicComments(c)
|
const comment = createImportMagicComments(c)
|
||||||
return ` "${c.pascalName}": defineAsyncComponent(${genDynamicImport(c.filePath, { comment })}.then(c => ${exp}))`
|
return ` "${c.pascalName}": defineAsyncComponent(${genDynamicImport(c.filePath, { comment })}.then(c => ${exp}))`
|
||||||
}
|
}
|
||||||
).join(',\n'),
|
).concat(pageExports).join(',\n'),
|
||||||
'}'
|
'}'
|
||||||
].join('\n')
|
].join('\n')
|
||||||
}
|
}
|
||||||
|
@ -400,6 +400,10 @@ export default defineNuxtModule({
|
|||||||
const sourceFiles = nuxt.apps.default?.pages?.length ? getSources(nuxt.apps.default.pages) : []
|
const sourceFiles = nuxt.apps.default?.pages?.length ? getSources(nuxt.apps.default.pages) : []
|
||||||
|
|
||||||
for (const key in manifest) {
|
for (const key in manifest) {
|
||||||
|
if (manifest[key].src && Object.values(nuxt.apps).some(app => app.pages?.some(page => page.mode === 'server' && page.file === join(nuxt.options.srcDir, manifest[key].src!) ))) {
|
||||||
|
delete manifest[key]
|
||||||
|
continue
|
||||||
|
}
|
||||||
if (manifest[key].isEntry) {
|
if (manifest[key].isEntry) {
|
||||||
manifest[key].dynamicImports =
|
manifest[key].dynamicImports =
|
||||||
manifest[key].dynamicImports?.filter(i => !sourceFiles.includes(i))
|
manifest[key].dynamicImports?.filter(i => !sourceFiles.includes(i))
|
||||||
|
@ -14,6 +14,7 @@ import type { NuxtPage } from 'nuxt/schema'
|
|||||||
|
|
||||||
import { uniqueBy } from '../core/utils'
|
import { uniqueBy } from '../core/utils'
|
||||||
import { toArray } from '../utils'
|
import { toArray } from '../utils'
|
||||||
|
import { distDir } from '../dirs'
|
||||||
|
|
||||||
enum SegmentParserState {
|
enum SegmentParserState {
|
||||||
initial,
|
initial,
|
||||||
@ -58,6 +59,7 @@ export async function resolvePagesRoutes (): Promise<NuxtPage[]> {
|
|||||||
|
|
||||||
const allRoutes = await generateRoutesFromFiles(uniqueBy(scannedFiles, 'relativePath'), {
|
const allRoutes = await generateRoutesFromFiles(uniqueBy(scannedFiles, 'relativePath'), {
|
||||||
shouldExtractBuildMeta: nuxt.options.experimental.scanPageMeta || nuxt.options.experimental.typedPages,
|
shouldExtractBuildMeta: nuxt.options.experimental.scanPageMeta || nuxt.options.experimental.typedPages,
|
||||||
|
shouldUseServerComponents: !!nuxt.options.experimental.componentIslands,
|
||||||
vfs: nuxt.vfs
|
vfs: nuxt.vfs
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -66,6 +68,7 @@ export async function resolvePagesRoutes (): Promise<NuxtPage[]> {
|
|||||||
|
|
||||||
type GenerateRoutesFromFilesOptions = {
|
type GenerateRoutesFromFilesOptions = {
|
||||||
shouldExtractBuildMeta?: boolean
|
shouldExtractBuildMeta?: boolean
|
||||||
|
shouldUseServerComponents?: boolean
|
||||||
vfs?: Record<string, string>
|
vfs?: Record<string, string>
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -87,6 +90,13 @@ export async function generateRoutesFromFiles (files: ScannedFile[], options: Ge
|
|||||||
// Array where routes should be added, useful when adding child routes
|
// Array where routes should be added, useful when adding child routes
|
||||||
let parent = routes
|
let parent = routes
|
||||||
|
|
||||||
|
if (segments[segments.length - 1].endsWith('.server')) {
|
||||||
|
segments[segments.length - 1] = segments[segments.length - 1].replace('.server', '')
|
||||||
|
if (options.shouldUseServerComponents) {
|
||||||
|
route.mode = 'server'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (let i = 0; i < segments.length; i++) {
|
for (let i = 0; i < segments.length; i++) {
|
||||||
const segment = segments[i]
|
const segment = segments[i]
|
||||||
|
|
||||||
@ -439,7 +449,18 @@ export function normalizeRoutes (routes: NuxtPage[], metaImports: Set<string> =
|
|||||||
meta: `${metaImportName} || {}`,
|
meta: `${metaImportName} || {}`,
|
||||||
alias: `${metaImportName}?.alias || []`,
|
alias: `${metaImportName}?.alias || []`,
|
||||||
redirect: `${metaImportName}?.redirect`,
|
redirect: `${metaImportName}?.redirect`,
|
||||||
component: genDynamicImport(file, { interopDefault: true })
|
component: page.mode === 'server'
|
||||||
|
? `() => createIslandPage(${route.name})`
|
||||||
|
: genDynamicImport(file, { interopDefault: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (page.mode === 'server') {
|
||||||
|
metaImports.add(`
|
||||||
|
let _createIslandPage
|
||||||
|
async function createIslandPage (name) {
|
||||||
|
_createIslandPage ||= await import(${JSON.stringify(resolve(distDir, 'components/runtime/server-component'))}).then(r => r.createIslandPage)
|
||||||
|
return _createIslandPage(name)
|
||||||
|
};`)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (route.children != null) {
|
if (route.children != null) {
|
||||||
|
@ -17,7 +17,7 @@ describe('pages:generateRoutesFromFiles', () => {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const tests: Array<{
|
const tests: Array<{
|
||||||
description: string
|
description: string
|
||||||
files?: Array<{ path: string; template?: string; }>
|
files?: Array<{ path: string; template?: string; }>
|
||||||
@ -570,6 +570,7 @@ describe('pages:generateRoutesFromFiles', () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
result = await generateRoutesFromFiles(test.files.map(file => ({
|
result = await generateRoutesFromFiles(test.files.map(file => ({
|
||||||
|
shouldUseServerComponents: true,
|
||||||
absolutePath: file.path,
|
absolutePath: file.path,
|
||||||
relativePath: file.path.replace(/^(pages|layer\/pages)\//, '')
|
relativePath: file.path.replace(/^(pages|layer\/pages)\//, '')
|
||||||
})), { shouldExtractBuildMeta: true, vfs })
|
})), { shouldExtractBuildMeta: true, vfs })
|
||||||
|
@ -28,6 +28,15 @@ export type NuxtPage = {
|
|||||||
alias?: string[] | string
|
alias?: string[] | string
|
||||||
redirect?: RouteLocationRaw
|
redirect?: RouteLocationRaw
|
||||||
children?: NuxtPage[]
|
children?: NuxtPage[]
|
||||||
|
/**
|
||||||
|
* Set the render mode.
|
||||||
|
*
|
||||||
|
* `all` means the page will be rendered isomorphically - with JavaScript both on client and server.
|
||||||
|
*
|
||||||
|
* `server` means pages are automatically rendered with server components, so there will be no JavaScript to render the page in your client bundle.
|
||||||
|
* @default 'all'
|
||||||
|
*/
|
||||||
|
mode?: 'server' | 'all'
|
||||||
}
|
}
|
||||||
|
|
||||||
export type NuxtMiddleware = {
|
export type NuxtMiddleware = {
|
||||||
|
@ -2090,6 +2090,17 @@ describe('component islands', () => {
|
|||||||
|
|
||||||
await startServer()
|
await startServer()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('render island page', async () => {
|
||||||
|
const { page } = await renderPage('/')
|
||||||
|
|
||||||
|
const islandPageRequest = page.waitForRequest((req) => {
|
||||||
|
return req.url().includes('/__nuxt_island/page:server-page')
|
||||||
|
})
|
||||||
|
await page.getByText('to server page').click()
|
||||||
|
await islandPageRequest
|
||||||
|
await page.locator('#server-page').waitFor()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe.runIf(isDev() && !isWebpack)('vite plugins', () => {
|
describe.runIf(isDev() && !isWebpack)('vite plugins', () => {
|
||||||
|
3
test/fixtures/basic/pages/index.vue
vendored
3
test/fixtures/basic/pages/index.vue
vendored
@ -86,6 +86,9 @@
|
|||||||
<NuxtLink to="/big-page-1">
|
<NuxtLink to="/big-page-1">
|
||||||
to big 1
|
to big 1
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
|
<NuxtLink to="/server-page">
|
||||||
|
to server page
|
||||||
|
</NuxtLink>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
10
test/fixtures/basic/pages/server-page.server.vue
vendored
Normal file
10
test/fixtures/basic/pages/server-page.server.vue
vendored
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<template>
|
||||||
|
<div id="server-page">
|
||||||
|
Hello this is a server page
|
||||||
|
<NuxtLink
|
||||||
|
to="/"
|
||||||
|
>
|
||||||
|
to home
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
|
</template>
|
Loading…
Reference in New Issue
Block a user