mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-21 21:25: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>
|
||||
```
|
||||
|
||||
## 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.
|
||||
|
||||
: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.
|
||||
|
||||
|
@ -1,5 +1,7 @@
|
||||
import { defineComponent, h, ref } from 'vue'
|
||||
import NuxtIsland from '#app/components/nuxt-island'
|
||||
import { useRoute } from '#app/composables/router'
|
||||
import { isPrerendered } from '#app/composables/payload'
|
||||
|
||||
/*@__NO_SIDE_EFFECTS__*/
|
||||
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'
|
||||
getContents ({ app }) {
|
||||
const components = app.components
|
||||
const pages = app.pages
|
||||
const islands = components.filter(component =>
|
||||
component.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'))
|
||||
)
|
||||
|
||||
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 [
|
||||
'import { defineAsyncComponent } from \'vue\'',
|
||||
'export const islandComponents = import.meta.client ? {} : {',
|
||||
@ -87,7 +92,7 @@ export const componentsIslandsTemplate: NuxtTemplate = {
|
||||
const comment = createImportMagicComments(c)
|
||||
return ` "${c.pascalName}": defineAsyncComponent(${genDynamicImport(c.filePath, { comment })}.then(c => ${exp}))`
|
||||
}
|
||||
).join(',\n'),
|
||||
).concat(pageExports).join(',\n'),
|
||||
'}'
|
||||
].join('\n')
|
||||
}
|
||||
|
@ -400,6 +400,10 @@ export default defineNuxtModule({
|
||||
const sourceFiles = nuxt.apps.default?.pages?.length ? getSources(nuxt.apps.default.pages) : []
|
||||
|
||||
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) {
|
||||
manifest[key].dynamicImports =
|
||||
manifest[key].dynamicImports?.filter(i => !sourceFiles.includes(i))
|
||||
|
@ -14,6 +14,7 @@ import type { NuxtPage } from 'nuxt/schema'
|
||||
|
||||
import { uniqueBy } from '../core/utils'
|
||||
import { toArray } from '../utils'
|
||||
import { distDir } from '../dirs'
|
||||
|
||||
enum SegmentParserState {
|
||||
initial,
|
||||
@ -58,6 +59,7 @@ export async function resolvePagesRoutes (): Promise<NuxtPage[]> {
|
||||
|
||||
const allRoutes = await generateRoutesFromFiles(uniqueBy(scannedFiles, 'relativePath'), {
|
||||
shouldExtractBuildMeta: nuxt.options.experimental.scanPageMeta || nuxt.options.experimental.typedPages,
|
||||
shouldUseServerComponents: !!nuxt.options.experimental.componentIslands,
|
||||
vfs: nuxt.vfs
|
||||
})
|
||||
|
||||
@ -66,6 +68,7 @@ export async function resolvePagesRoutes (): Promise<NuxtPage[]> {
|
||||
|
||||
type GenerateRoutesFromFilesOptions = {
|
||||
shouldExtractBuildMeta?: boolean
|
||||
shouldUseServerComponents?: boolean
|
||||
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
|
||||
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++) {
|
||||
const segment = segments[i]
|
||||
|
||||
@ -439,7 +449,18 @@ export function normalizeRoutes (routes: NuxtPage[], metaImports: Set<string> =
|
||||
meta: `${metaImportName} || {}`,
|
||||
alias: `${metaImportName}?.alias || []`,
|
||||
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) {
|
||||
|
@ -17,7 +17,7 @@ describe('pages:generateRoutesFromFiles', () => {
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
const tests: Array<{
|
||||
description: string
|
||||
files?: Array<{ path: string; template?: string; }>
|
||||
@ -570,6 +570,7 @@ describe('pages:generateRoutesFromFiles', () => {
|
||||
|
||||
try {
|
||||
result = await generateRoutesFromFiles(test.files.map(file => ({
|
||||
shouldUseServerComponents: true,
|
||||
absolutePath: file.path,
|
||||
relativePath: file.path.replace(/^(pages|layer\/pages)\//, '')
|
||||
})), { shouldExtractBuildMeta: true, vfs })
|
||||
|
@ -28,6 +28,15 @@ export type NuxtPage = {
|
||||
alias?: string[] | string
|
||||
redirect?: RouteLocationRaw
|
||||
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 = {
|
||||
|
@ -2090,6 +2090,17 @@ describe('component islands', () => {
|
||||
|
||||
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', () => {
|
||||
|
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">
|
||||
to big 1
|
||||
</NuxtLink>
|
||||
<NuxtLink to="/server-page">
|
||||
to server page
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</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